diff --git a/docs/design/enrollment-status-reclassification-plan.md b/docs/design/enrollment-status-reclassification-plan.md index 9084afbb..cb07d62f 100644 --- a/docs/design/enrollment-status-reclassification-plan.md +++ b/docs/design/enrollment-status-reclassification-plan.md @@ -89,13 +89,17 @@ timeline AND let the backend terminalize precisely instead of guessing with grac - [ ] **Deferred to PR5:** Fleet Health payload Incomplete count (its SuccessRate is documented as "over all sessions" with equivalence tests — fold the Incomplete surfacing in with the web work). -## PR5 — Web surface +## PR5 — Web surface ✅ done (typecheck clean) -- [ ] Mirror enum + `isTerminalStatus` (`utils/sessionStatus.ts`): `Incomplete` terminal, - `AwaitingUser` non-terminal. -- [ ] `SessionStatusBadge.tsx` colours/labels for both new states. -- [ ] Dashboard / Fleet Health cards render the third bucket; failure-rate copy updated. -- [ ] `lib/__tests__/sessionStatus.test.ts` updated. +- [x] `utils/sessionStatus.ts`: `isTerminalStatus` includes `Incomplete`; docstring notes AwaitingUser non-terminal. +- [x] `SessionStatusBadge.tsx`: `Incomplete` = neutral slate (clearly not red), `AwaitingUser` = sky/blue + ("still going"); ⏱️ affordance extended to Incomplete (the silence family). +- [x] Fleet Health: `FleetHealthStats.Incomplete` (C# model + `MetricsMath.BuildFleetHealthPayload`) + + web interface + a dedicated slate "Incomplete" stat card (`FleetStatCard` gained a `slate` color); + grid widened to 5. +- [x] Main dashboard KPI row left as-is (its Success Rate is already the honest terminal rate); the third + state is surfaced via the badge everywhere, the Fleet Health card, and the metrics summary `incomplete`. +- [x] Verified: `tsc --noEmit` clean; backend suite 2709 pass. ## PR6 — Backfill (optional, one-shot) diff --git a/src/Backend/AutopilotMonitor.Functions/Helpers/MetricsMath.cs b/src/Backend/AutopilotMonitor.Functions/Helpers/MetricsMath.cs index b63dd278..f3fe1833 100644 --- a/src/Backend/AutopilotMonitor.Functions/Helpers/MetricsMath.cs +++ b/src/Backend/AutopilotMonitor.Functions/Helpers/MetricsMath.cs @@ -99,7 +99,7 @@ public static class MetricsMath /// public static FleetHealthMetrics BuildFleetHealthPayload(IReadOnlyList sessions, int days) { - int succeeded = 0, failed = 0, inProgress = 0; + int succeeded = 0, failed = 0, inProgress = 0, incomplete = 0; long completedDurationSeconds = 0; int completedWithDurationCount = 0; @@ -110,6 +110,7 @@ public static class MetricsMath case SessionStatus.Succeeded: succeeded++; break; case SessionStatus.Failed: failed++; break; case SessionStatus.InProgress: inProgress++; break; + case SessionStatus.Incomplete: incomplete++; break; } if (s.Status != SessionStatus.InProgress && s.DurationSeconds is int d && d > 0) @@ -126,6 +127,7 @@ public static class MetricsMath Succeeded = succeeded, Failed = failed, InProgress = inProgress, + Incomplete = incomplete, SuccessRate = total > 0 ? Math.Round((double)succeeded / total * 100, 1) : 0, AvgDurationMinutes = completedWithDurationCount > 0 ? (int)Math.Round((double)completedDurationSeconds / completedWithDurationCount / 60.0, MidpointRounding.AwayFromZero) diff --git a/src/Shared/AutopilotMonitor.Shared/Models/FleetHealthModels.cs b/src/Shared/AutopilotMonitor.Shared/Models/FleetHealthModels.cs index c6f6bfe6..06b08da7 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/FleetHealthModels.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/FleetHealthModels.cs @@ -34,6 +34,8 @@ namespace AutopilotMonitor.Shared.Models public int Succeeded { get; set; } public int Failed { get; set; } public int InProgress { get; set; } + /// Terminal, non-failure sessions (timeout reclassification). Surfaced as its own count; not a failure. + public int Incomplete { get; set; } /// Succeeded / total * 100 (one decimal). Note: over all sessions, not just terminal. public double SuccessRate { get; set; } /// Average duration in minutes over non-in-progress sessions that carry a duration. diff --git a/src/Web/autopilot-monitor-web/app/fleet-health/components/FleetStatCard.tsx b/src/Web/autopilot-monitor-web/app/fleet-health/components/FleetStatCard.tsx index 4a49e025..05f8b47d 100644 --- a/src/Web/autopilot-monitor-web/app/fleet-health/components/FleetStatCard.tsx +++ b/src/Web/autopilot-monitor-web/app/fleet-health/components/FleetStatCard.tsx @@ -9,13 +9,14 @@ export default function FleetStatCard({ title: string; value: string; subtitle: string; - color: "green" | "blue" | "red" | "yellow"; + color: "green" | "blue" | "red" | "yellow" | "slate"; }) { const colorClasses = { green: "border-green-500 bg-green-50", blue: "border-blue-500 bg-blue-50", red: "border-red-500 bg-red-50", yellow: "border-yellow-500 bg-yellow-50", + slate: "border-slate-400 bg-slate-50", }; const valueColors = { @@ -23,6 +24,7 @@ export default function FleetStatCard({ blue: "text-blue-700", red: "text-red-700", yellow: "text-yellow-700", + slate: "text-slate-600", }; return ( diff --git a/src/Web/autopilot-monitor-web/app/fleet-health/hooks/useFleetHealth.ts b/src/Web/autopilot-monitor-web/app/fleet-health/hooks/useFleetHealth.ts index a2f3d5f7..67ef0187 100644 --- a/src/Web/autopilot-monitor-web/app/fleet-health/hooks/useFleetHealth.ts +++ b/src/Web/autopilot-monitor-web/app/fleet-health/hooks/useFleetHealth.ts @@ -14,6 +14,8 @@ export interface FleetHealthStats { succeeded: number; failed: number; inProgress: number; + /** Terminal, non-failure (timeout reclassification). Surfaced separately; not counted as a failure. */ + incomplete: number; successRate: number; avgDurationMinutes: number; } diff --git a/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx b/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx index e47baa4d..f46a18f6 100644 --- a/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx +++ b/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx @@ -124,6 +124,7 @@ export default function FleetHealthPage() { succeeded: s?.succeeded ?? 0, failed: s?.failed ?? 0, inProgress: s?.inProgress ?? 0, + incomplete: s?.incomplete ?? 0, successRate: s?.successRate ?? 0, avgDuration: s?.avgDurationMinutes ?? 0, }; @@ -223,7 +224,7 @@ export default function FleetHealthPage() {
{/* Top Stats */} -
+
+ = { InProgress: { color: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300", text: "In Progress" }, Pending: { color: "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300", text: "Pending" }, Stalled: { color: "bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300", text: "Stalled" }, + // Non-terminal: Device Setup done, waiting on the user / Account-Setup phase. Blue-ish "still going". + AwaitingUser: { color: "bg-sky-100 text-sky-800 dark:bg-sky-900/40 dark:text-sky-300", text: "Awaiting User" }, Succeeded: { color: "bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300", text: "Succeeded" }, Failed: { color: "bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300", text: "Failed" }, + // Terminal but NOT a failure — no completion or explicit failure signal. Neutral slate, clearly not red. + Incomplete: { color: "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300", text: "Incomplete" }, Unknown: { color: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300", text: "Unknown" }, }; @@ -26,7 +30,11 @@ export function SessionStatusBadge({ adminMarkedAction?: string | null; }) { const config = STATUS_CONFIG[status] || STATUS_CONFIG.Unknown; - const isTimeout = status === "Failed" && !!failureReason?.toLowerCase().includes("timed out"); + // The ⏱️ affordance marks the silence/timeout family: a "timed out" Failed, or any Incomplete + // (which is, by definition, a session that went silent without a completion). + const isTimeout = + (status === "Failed" && !!failureReason?.toLowerCase().includes("timed out")) || + status === "Incomplete"; const pill = (