diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatsAggregationTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatsAggregationTests.cs index 8245a756..bc6c95ac 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatsAggregationTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatsAggregationTests.cs @@ -43,6 +43,10 @@ public class SessionStatsAggregationTests [Fact] public void Active_count_includes_InProgress_Pending_Stalled_but_not_terminal() { + // "Currently enrolling" covers everything still in flight: InProgress + // (mid-enrollment), Pending (WhiteGlove waiting for user), Stalled + // (>60min stale — non-terminal, can heal back to InProgress). Terminal + // states (Succeeded / Failed) and Unknown are NOT in flight. var now = DateTime.UtcNow; var sessions = new[] { diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Sessions.cs b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Sessions.cs index 9332555e..3869a5ac 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Sessions.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Sessions.cs @@ -841,8 +841,8 @@ namespace AutopilotMonitor.Functions.Services switch (s.Status) { case SessionStatus.InProgress: - case SessionStatus.Pending: - case SessionStatus.Stalled: + case SessionStatus.Pending: // WhiteGlove pre-prov complete, waiting for user — still in flight + case SessionStatus.Stalled: // >60min no progress, non-terminal — can heal back to InProgress active++; break; case SessionStatus.Succeeded: diff --git a/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardStats.ts b/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardStats.ts index 4dedcf4d..d39ac850 100644 --- a/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardStats.ts +++ b/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardStats.ts @@ -175,9 +175,13 @@ export function useDashboardStats({ }, [fetchStats]); // Refetch on scope change (tenant switch, GA toggle, filter Submit, days change). + // Reset stats synchronously so any previous-scope numbers (e.g. cross-tenant + // sums when toggling GA off) don't linger on the cards if the refetch errors + // out or returns nothing — the user sees "..." instead of stale data. useEffect(() => { if (disabled) return; if (!globalAdminMode && !tenantId) return; + setStats(null); fetchStats(); // fetchStats is stable; intentionally not in deps to avoid re-running on identity flips. // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/Web/autopilot-monitor-web/app/dashboard/page.tsx b/src/Web/autopilot-monitor-web/app/dashboard/page.tsx index 96c2dab1..4e550889 100644 --- a/src/Web/autopilot-monitor-web/app/dashboard/page.tsx +++ b/src/Web/autopilot-monitor-web/app/dashboard/page.tsx @@ -134,7 +134,7 @@ function HomeContent() { // the client has paginated into view. Refreshes on SignalR newSession/newevents // (debounced) and on SignalR reconnect to recover from any missed messages. const isRegularUser = !!user && !user.isTenantAdmin && !user.isGlobalAdmin && user.role !== "Operator"; - const { stats: dashboardStats, loading: statsLoading } = useDashboardStats({ + const { stats: dashboardStats } = useDashboardStats({ tenantId, globalAdminMode, submittedTenantIdFilter, @@ -348,35 +348,38 @@ function HomeContent() { )} - {/* Stats cards — server-aggregated (see useDashboardStats) */} + {/* Stats cards — server-aggregated (see useDashboardStats). + `dashboardStats === null` covers both initial load and post-scope-change + reset; show a non-zero placeholder so a fetch error doesn't masquerade + as legitimate zeros. */}