fix(dashboard/stats): reset cards on scope change, pin "in flight" semantics

Follow-up to b76a6cca. Two issues surfaced after deploy:

1. Toggling GA mode off left the cross-tenant numbers stuck on the cards.
   Hook now setStats(null) synchronously before refetching on every scope
   change, and the card render falls back to "..." whenever stats is null
   (covers initial load, post-toggle reset, and fetch errors — previously
   an error would silently show 0).

2. "Active Sessions" intent: the user wants in-flight sessions in the
   broader sense, which matches the original code. AggregateSessionStats
   counts InProgress + Pending (WhiteGlove waiting on user) + Stalled
   (>60min no progress, non-terminal, can heal back to InProgress).
   Test renamed + comment pins the rationale so a future "tighten to
   InProgress only" refactor knows what it's changing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-05-11 14:00:02 +02:00
parent b76a6ccaf0
commit 068321baaa
4 changed files with 20 additions and 9 deletions
@@ -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[]
{
@@ -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:
@@ -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
@@ -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() {
</div>
)}
{/* 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. */}
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-5 mb-2">
<StatsCard
title="Active Sessions"
value={statsLoading && !dashboardStats ? "..." : (dashboardStats?.activeCount ?? 0).toString()}
value={dashboardStats ? dashboardStats.activeCount.toString() : "..."}
description="Currently enrolling"
color="blue"
/>
<StatsCard
title="Success Rate"
value={statsLoading && !dashboardStats ? "..." : `${dashboardStats?.successRatePct ?? 0}%`}
value={dashboardStats ? `${dashboardStats.successRatePct}%` : "..."}
description="Last 7 days"
color="green"
/>
<StatsCard
title="Avg. Duration"
value={statsLoading && !dashboardStats ? "..." : `${dashboardStats?.avgDurationMinutes ?? 0} min`}
value={dashboardStats ? `${dashboardStats.avgDurationMinutes} min` : "..."}
description="Last 7 days"
color="purple"
/>
<StatsCard
title="Total Today"
value={statsLoading && !dashboardStats ? "..." : (dashboardStats?.totalToday ?? 0).toString()}
value={dashboardStats ? dashboardStats.totalToday.toString() : "..."}
description="Started today"
color="indigo"
/>
<StatsCard
title="Failed Today"
value={statsLoading && !dashboardStats ? "..." : (dashboardStats?.failedToday ?? 0).toString()}
value={dashboardStats ? dashboardStats.failedToday.toString() : "..."}
description="Needs attention"
color="red"
/>