From 9bb61124f48961e080ca8687214f92e062cf3b37 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Tue, 17 Feb 2026 15:08:12 +0100 Subject: [PATCH] fix enrollment progress time calculation --- .../app/sessions/[sessionId]/page.tsx | 100 +++++++++++++++--- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx index 6108104f..4bd02a09 100644 --- a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx +++ b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx @@ -1089,24 +1089,95 @@ function PhaseTimeline({ currentPhase, completedPhases, events = [], sessionStat return null; }; - // Calculate phase durations from events - const getPhaseDuration = (phaseId: number): string | null => { - const phaseEvents = events.filter(e => e.phase === phaseId); - if (phaseEvents.length < 2) return null; - - const sorted = [...phaseEvents].sort((a, b) => - new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() - ); - const first = new Date(sorted[0].timestamp).getTime(); - const last = new Date(sorted[sorted.length - 1].timestamp).getTime(); - const durationSec = Math.round((last - first) / 1000); + // Helper: get first/last event timestamp for a phase + const getFirstEventTime = (phaseId: number): number | null => { + const ts = events + .filter(e => e.phase === phaseId) + .map(e => new Date(e.timestamp).getTime()); + return ts.length > 0 ? Math.min(...ts) : null; + }; + const getLastEventTime = (phaseId: number): number | null => { + const ts = events + .filter(e => e.phase === phaseId) + .map(e => new Date(e.timestamp).getTime()); + return ts.length > 0 ? Math.max(...ts) : null; + }; + const formatDuration = (ms: number): string | null => { + const durationSec = Math.round(ms / 1000); if (durationSec < 5) return null; if (durationSec < 60) return `${durationSec}s`; if (durationSec < 3600) return `${Math.floor(durationSec / 60)}m ${durationSec % 60}s`; return `${Math.floor(durationSec / 3600)}h ${Math.floor((durationSec % 3600) / 60)}m`; }; + // Calculate phase durations using phase-transition timestamps + // Parent phases (Device Setup, Account Setup) span until the next major phase starts + // Sub-phases (Apps Device, Apps User) show only their own event range + const getPhaseDuration = (phaseId: number): string | null => { + const isV2 = enrollmentType === "v2"; + + // No timing for Start and Device Preparation (agent not active yet) + if (phaseId === 0 || phaseId === 1) return null; + // No timing for Complete + if (phaseId === 7) return null; + + if (isV2) { + // V2: simpler phases — no sub-phase nesting + // Phase 3 (App Installation): first phase 3 event → first phase 6 event (or last phase 3) + // Phase 6 (Finalizing): first phase 6 event → first phase 7 event (or last phase 6) + const start = getFirstEventTime(phaseId); + if (!start) return null; + + const nextPhaseMap: Record = { 3: 6, 6: 7 }; + const nextPhase = nextPhaseMap[phaseId]; + const end = (nextPhase !== undefined ? getFirstEventTime(nextPhase) : null) ?? getLastEventTime(phaseId); + if (!end || end <= start) return null; + + return formatDuration(end - start); + } + + // V1 phase-transition-based durations: + // Device Setup (2): first phase 2 → first phase 4 (includes Apps Device sub-phase) + // Apps Device (3): first phase 3 → last phase 3 (sub-phase only) + // Account Setup (4): first phase 4 → first phase 6 (includes Apps User sub-phase) + // Apps User (5): first phase 5 → last phase 5 (sub-phase only) + // Finalizing (6): first phase 6 → first phase 7 (or last phase 6) + const start = getFirstEventTime(phaseId); + if (!start) return null; + + let end: number | null = null; + switch (phaseId) { + case 2: // Device Setup → ends when Account Setup starts + end = getFirstEventTime(4) ?? getLastEventTime(3) ?? getLastEventTime(2); + break; + case 3: // Apps (Device) — sub-phase, own event range + end = getLastEventTime(3); + break; + case 4: // Account Setup → ends when Finalizing starts + end = getFirstEventTime(6) ?? getLastEventTime(5) ?? getLastEventTime(4); + break; + case 5: // Apps (User) — sub-phase, own event range + end = getLastEventTime(5); + break; + case 6: // Finalizing → ends when Complete starts + end = getFirstEventTime(7) ?? getLastEventTime(6); + break; + default: + end = getLastEventTime(phaseId); + break; + } + + if (!end || end <= start) return null; + return formatDuration(end - start); + }; + + // Check if a phase is a sub-phase (Apps Device / Apps User in V1) + const isSubPhase = (phaseId: number): boolean => { + if (enrollmentType === "v2") return false; + return phaseId === 3 || phaseId === 5; + }; + // Derive the highest real phase (0-7) seen in events, excluding phase 99 (Failed) const maxEventPhase = (() => { const realPhases = events @@ -1221,8 +1292,11 @@ function PhaseTimeline({ currentPhase, completedPhases, events = [], sessionStat {phase.shortName} {(status === 'completed' || status === 'failed') && getPhaseDuration(phase.id) && ( -
- {getPhaseDuration(phase.id)} +
+ {isSubPhase(phase.id) ? `(${getPhaseDuration(phase.id)})` : getPhaseDuration(phase.id)}
)} {status === 'failed' && (