* feat(dashboard): surface metrics-paused indicator after sustained poll failure
useDashboardData previously failed silently when /stats or /system/stats
returned an error: stale data kept rendering and the last sync timestamp
quietly drifted. The operator could not tell whether the dashboard was just
slow or whether the Docker socket / metrics path had genuinely gone down.
Track consecutive failures per live-metrics endpoint. After three in a row
on either /stats or /system/stats (≈15 s at the 5 s poll cadence), expose a
metricsStale boolean on the hook result. HealthStatusBar renders a small
amber "metrics paused" chip beside the meta line when set. The indicator
clears on the first successful response when both endpoints are within the
threshold.
A unit test for the threshold logic is intentionally deferred to the Phase
4 E2E dashboard spec, which exercises the same path end-to-end by stopping
the Docker daemon and asserting the user-visible indicator.
* fix(dashboard): rename stale-metrics chip and cover the threshold with tests
Address two follow-up findings from independent review of the earlier
commit on this branch.
1. Rename the masthead chip from "metrics paused" to "metrics stale". The
hook keeps polling on every cycle; the chip describes the freshness of
the displayed numbers, not the polling cadence. The new wording matches
the underlying `metricsStale` state variable.
2. Add a Vitest spec for the threshold logic. Captures the visibilityInterval
callback at registration time and drives each polling cycle on demand,
covering: three consecutive /stats failures trip the indicator and the
next successful poll clears it; three consecutive /system/stats failures
trip the indicator on the other endpoint; clearing requires both
endpoints under threshold (a single endpoint recovering while the other
is still failing keeps the indicator set).
The clarifying comment in useDashboardData notes that polling is unaffected
and only the data freshness is in scope, so future readers do not interpret
"stale" as "paused".
useFleetHeartbeat keyed its effect on activeNode.id and reset its state on
every node switch, even though /fleet/overview returns a fleet-wide payload
that does not change when the user pivots their active local node. The
result was a needless flicker back to the skeleton card and an extra HTTP
request on every node pivot.
Drop the nodeId dependency and the stale-node guard ref. The 30 s
visibility-interval poll remains, so transient remote-node offline state
still surfaces within one polling cycle.