mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
feat(web): PR5 — surface Incomplete/AwaitingUser in the UI
- sessionStatus.ts: isTerminalStatus includes Incomplete. - SessionStatusBadge: Incomplete = neutral slate (clearly not a red failure), AwaitingUser = sky/blue 'still going'; clock affordance extended to Incomplete. - Fleet Health: FleetHealthStats.Incomplete (model + BuildFleetHealthPayload) + web interface + a dedicated slate Incomplete stat card (FleetStatCard gained a 'slate' color), grid widened to 5. - Main dashboard KPI row unchanged (its Success Rate is already terminal-only/honest). Verified: web tsc --noEmit clean; backend Functions.Tests 2709 pass.
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ public static class MetricsMath
|
||||
/// </summary>
|
||||
public static FleetHealthMetrics BuildFleetHealthPayload(IReadOnlyList<SessionSummary> 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)
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace AutopilotMonitor.Shared.Models
|
||||
public int Succeeded { get; set; }
|
||||
public int Failed { get; set; }
|
||||
public int InProgress { get; set; }
|
||||
/// <summary>Terminal, non-failure sessions (timeout reclassification). Surfaced as its own count; not a failure.</summary>
|
||||
public int Incomplete { get; set; }
|
||||
/// <summary>Succeeded / total * 100 (one decimal). Note: over all sessions, not just terminal.</summary>
|
||||
public double SuccessRate { get; set; }
|
||||
/// <summary>Average duration in minutes over non-in-progress sessions that carry a duration.</summary>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
<main className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
{/* Top Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 mb-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-5 mb-8">
|
||||
<FleetStatCard
|
||||
title="Success Rate"
|
||||
value={`${stats.successRate.toFixed(1)}%`}
|
||||
@@ -248,6 +249,12 @@ export default function FleetHealthPage() {
|
||||
subtitle="Needs attention"
|
||||
color="red"
|
||||
/>
|
||||
<FleetStatCard
|
||||
title="Incomplete"
|
||||
value={stats.incomplete.toString()}
|
||||
subtitle="No completion signal — not a failure"
|
||||
color="slate"
|
||||
/>
|
||||
<FleetStatCard
|
||||
title="Active Now"
|
||||
value={stats.inProgress.toString()}
|
||||
|
||||
@@ -10,8 +10,12 @@ const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
|
||||
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 = (
|
||||
<span
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// Session status helpers. Mirror of the backend SessionStatus enum
|
||||
// (src/Shared/AutopilotMonitor.Shared/Models/SessionApiModels.cs).
|
||||
// Non-terminal: InProgress, Pending, Stalled, Unknown.
|
||||
// Terminal: Succeeded, Failed.
|
||||
// Non-terminal: InProgress, Pending, Stalled, AwaitingUser, Unknown.
|
||||
// Terminal: Succeeded, Failed, Incomplete.
|
||||
//
|
||||
// AwaitingUser (Device Setup done, user/Account-Setup phase pending) and Incomplete
|
||||
// (terminal, non-failure — no completion or explicit failure was observed) come from the
|
||||
// timeout reclassification, see docs/design/enrollment-status-reclassification.md.
|
||||
|
||||
export function isTerminalStatus(status: string | null | undefined): boolean {
|
||||
return status === "Succeeded" || status === "Failed";
|
||||
return status === "Succeeded" || status === "Failed" || status === "Incomplete";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user