diff --git a/src/Web/autopilot-monitor-web/app/progress/page.tsx b/src/Web/autopilot-monitor-web/app/progress/page.tsx index 32c04723..d1e139eb 100644 --- a/src/Web/autopilot-monitor-web/app/progress/page.tsx +++ b/src/Web/autopilot-monitor-web/app/progress/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useMemo } from "react"; import { API_BASE_URL } from "@/lib/config"; import { useTenant } from "../../contexts/TenantContext"; import { useAuth } from "../../contexts/AuthContext"; @@ -22,6 +22,17 @@ interface Session { failureReason?: string; } +interface EnrollmentEvent { + eventId: string; + sessionId: string; + timestamp: string; + eventType: string; + phase: number; + message: string; + sequence: number; + data?: Record; +} + const phaseSteps = [ { id: 0, label: "Device registered", shortLabel: "Registration" }, { id: 1, label: "Network configured", shortLabel: "Network" }, @@ -40,6 +51,8 @@ export default function ProgressPortalPage() { const [searching, setSearching] = useState(false); const [searched, setSearched] = useState(false); const [notFound, setNotFound] = useState(false); + const [headerCollapsed, setHeaderCollapsed] = useState(false); + const [events, setEvents] = useState([]); const { tenantId } = useTenant(); const { getAccessToken } = useAuth(); @@ -86,6 +99,40 @@ export default function ProgressPortalPage() { }; }, [on, off]); + // Fetch events when a session is found + const lastFetchedSessionId = useRef(null); + useEffect(() => { + if (!session) return; + if (lastFetchedSessionId.current === session.sessionId) return; + lastFetchedSessionId.current = session.sessionId; + const fetchEvents = async () => { + try { + const token = await getAccessToken(); + if (!token) return; + const response = await fetch( + `${API_BASE_URL}/api/sessions/${session.sessionId}/events?tenantId=${tenantId}`, + { headers: { Authorization: `Bearer ${token}` } } + ); + if (response.ok) { + const data = await response.json(); + const fetched: EnrollmentEvent[] = data.events || []; + setEvents((prev) => { + if (prev.length === 0) return fetched; + const existingIds = new Set(prev.map((e) => e.eventId)); + const newEvents = fetched.filter((e) => !existingIds.has(e.eventId)); + if (newEvents.length === 0) return prev; + return [...prev, ...newEvents].sort( + (a, b) => a.sequence - b.sequence + ); + }); + } + } catch (error) { + console.error("Failed to fetch events:", error); + } + }; + fetchEvents(); + }, [session?.sessionId]); + const searchBySerial = async () => { if (!serialInput.trim()) return; @@ -93,6 +140,8 @@ export default function ProgressPortalPage() { setSearched(true); setNotFound(false); setSession(null); + setEvents([]); + lastFetchedSessionId.current = null; try { const token = await getAccessToken(); @@ -128,6 +177,7 @@ export default function ProgressPortalPage() { if (found) { setSession(found); + setHeaderCollapsed(true); } else { setNotFound(true); } @@ -144,6 +194,50 @@ export default function ProgressPortalPage() { if (e.key === "Enter") searchBySerial(); }; + // Derive app installation progress from events + const appProgress = useMemo(() => { + // Check esp_ui_state events for app progress + const espEvents = events.filter((e) => e.eventType === "esp_ui_state"); + if (espEvents.length > 0) { + const latest = espEvents[espEvents.length - 1]; + const d = latest.data; + if (d) { + const completed = parseInt( + d.blocking_apps_completed ?? d.blockingAppsCompleted ?? "0", + 10 + ); + const total = parseInt( + d.blocking_apps_total ?? d.blockingAppsTotal ?? "0", + 10 + ); + const currentItem = + d.current_item ?? d.currentItem ?? d.status_text ?? d.statusText ?? null; + if (total > 0) return { completed, total, currentItem }; + } + } + + // Check download_progress events for current app name + const downloadEvents = events.filter((e) => e.eventType === "download_progress"); + if (downloadEvents.length > 0) { + const latest = downloadEvents[downloadEvents.length - 1]; + const d = latest.data; + if (d) { + const appName = d.app_name ?? d.appName ?? null; + const status = d.status ?? ""; + const isComplete = + status === "completed" || + (parseInt(d.bytes_total ?? d.bytesTotal ?? "0", 10) > 0 && + parseInt(d.bytes_downloaded ?? d.bytesDownloaded ?? "0", 10) >= + parseInt(d.bytes_total ?? d.bytesTotal ?? "0", 10)); + if (appName && !isComplete) { + return { completed: 0, total: 0, currentItem: appName }; + } + } + } + + return null; + }, [events]); + // Derive progress const overallProgress = session ? session.status === "Succeeded" @@ -175,52 +269,118 @@ export default function ProgressPortalPage() { return (
-
- {/* Header */} -
-
- - - -
-

- Device Setup Progress -

-

- Enter your device serial number to check status -

-
- - {/* Search */} -
-
- setSerialInput(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Enter serial number or device name..." - className="w-full px-4 py-3 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg" - /> -
+
+ {/* Collapsible Header + Search */} + {headerCollapsed && session ? ( -
+ ) : ( + <> + {/* Full Header */} +
+ {session && ( + + )} +
+ + + +
+

+ Device Setup Progress +

+

+ Enter your device serial number to check status +

+
+ + {/* Search */} +
+
+ setSerialInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Enter serial number or device name..." + className="w-full px-4 py-3 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg" + /> +
+ +
+ + )} {/* Not Found */} {notFound && ( @@ -331,82 +491,96 @@ export default function ProgressPortalPage() { const isFailed = step.id === effectivePhase && session.status === "Failed"; - const isPending = step.id > effectivePhase; return ( -
- {/* Icon */} -
- {isCompleted ? ( -
- - - -
- ) : isCurrent ? ( -
-
-
- ) : isFailed ? ( -
- - - -
- ) : ( -
-
-
- )} -
+
+
+ {/* Icon */} +
+ {isCompleted ? ( +
+ + + +
+ ) : isCurrent ? ( +
+
+
+ ) : isFailed ? ( +
+ + + +
+ ) : ( +
+
+
+ )} +
- {/* Label */} - - {step.label} - {isCurrent && - step.id === 5 && - ` (${session.currentPhase === 5 ? "in progress" : ""})`} - + {/* Label */} +
+ + {step.label} + {isCurrent && + step.id === 5 && + appProgress && + appProgress.total > 0 && + appProgress.completed > 0 && + ` (${appProgress.completed}/${appProgress.total})`} + + {/* App install detail below the "Installing apps" step */} + {isCurrent && step.id === 5 && ( +
+
+ + {appProgress?.currentItem + ? `${appProgress.currentItem} installing...` + : "Installing applications..."} + +
+ )} +
+
); })}
{/* Estimated Time / Status */} - {session.status === "InProgress" && estimatedRemaining && ( + {session.status === "InProgress" && estimatedRemaining != null && estimatedRemaining > 0 && (
Estimated time remaining:{" "} @@ -417,15 +591,6 @@ export default function ProgressPortalPage() {
)} - {session.status === "InProgress" && ( -
-

- You can leave your device on this screen. Setup will - continue automatically. -

-
- )} - {session.status === "Succeeded" && (