Using a metrics object as a useEffect dep causes the loop:
onMetrics → setMetricsById → Dashboard re-renders → ServerWorkspaceRows
re-renders → useQueries runs → TanStack Query cache subscriber fires
(Sidebar's setNow) → query result objects are new references → metrics
useMemo returns new object → effect dep changed → onMetrics again → ∞
Fix: depend on the five primitive values (workspaceCount, conclusionCount,
queueActive, queuePending, health) directly. React compares primitives by
value, so the effect only fires when actual data changes, not on reference
churn from re-renders.
Also adds server-workspace-rows.test.tsx with three focused unit tests:
correct health:ok report, stability after load (no re-fire), and health
transition coverage.