Task 2 report — dashboard hydrates SLA bands + alert thresholds from /api/config
Plan: ahu-ai-observatory/docs/superpowers/plans/2026-07-09-configurable-thresholds.md, Task 2.
Branch: feat/configurable-thresholds (this repo, ahu-observatory-dashboard).
Status
DONE. All three gates pass: npx tsc --noEmit (clean), npx vitest run (45 files / 409 tests,
all green), npm run build (succeeds). No regressions in the pre-existing suite (App.test.tsx,
tones.test.ts, alerts-bar.test.tsx all still pass unmodified).
What changed
src/lib/api.ts— addedSLABand/ThresholdsConfigtypes (mirroring the contract exactly:
sla.{error_rate,latency_ms,queue_ms,gpu_util_pct}each{good_below,warning_below};
alerts.{error_rate_pct,min_calls,queue_saturation_ms,gpu_saturation_pct,window_min,poll_ms})
andgetConfig(): Promise<ThresholdsConfig>onApiClient, hittingGET /api/configthrough
the same tokenlessrequest()path/healthzalready uses (no bearer header logic needed —
request()only setsAuthorizationwhengetToken()returns a token).src/lib/thresholds.ts(new) — the store.DEFAULT_THRESHOLDSis today's hardcoded values,
verbatim, and is the module's initialstate.applyThresholds(partial?: PartialThresholdsConfig | null)merges over the current state field-by-field (every leaf optional, so a caller can
override one band field, one whole band, or nothing) and is a no-op onundefined/null.
Getters:getThresholds(),getSLA(),getAlertThresholds().useThresholds()wraps
useSyncExternalStorefor components that need a hydrate-triggered re-render.resetThresholds()
is exported for test cleanup.src/lib/tones.ts— the four tone fns kept their exact signatures
(errorRateTone(rate: number): Tone, etc.) but now readgetSLA().<band>at call time instead
of a hardcoded literal; a sharedbandTone(value, goodBelow, warningBelow)helper replaces the
four duplicated if/else ladders.src/components/alerts-bar.tsx—evaluateAlerts/evaluateGPUAlertgained an optional
second parameter (thresholds: AlertThresholds = getAlertThresholds()) so existing single-arg
calls (incl. the untouched test file) still work, whileAlertsBaritself passes its
useThresholds()-sourced snapshot explicitly — this made the dependency real for
useMemo/react-hooks/exhaustive-depsinstead of a hidden global read.GPU_SATURATION_PCT
stays exported (= DEFAULT_THRESHOLDS.alerts.gpu_saturation_pct) soalerts-bar.test.tsx's
import keeps working untouched. Window/poll are now read from the store too
(windowMin/pollMsreplaceALERT_WINDOW_MIN/ALERT_POLL_MS).src/App.tsx—Providersfires a bootstrapuseEffect(mount-only) that does
createClient(() => null).getConfig().then(applyThresholds).catch(() => {})— tokenless client
(mirrorsHealthPill's pattern), fire-and-forget, never blocks render, swallows any error
(404/network/CORS) so defaults stand.- 5 views (
SLAView,ExecutiveView,OperatorView,AnalyticsView,SecurityView) — each
gained exactly 2 lines: an import ofuseThresholdsand one bareuseThresholds()call at the
top of the component body. See "Re-render approach" below for why. - Tests: new
src/lib/thresholds.test.ts(defaults match today's values;applyThresholds
partial-overrides only given fields, including a full-payload replace; no-op on
undefined/null;errorRateTone(0.3)flips from'critical'to'warning'after widening
warning_belowto 0.5, thenresetThresholds()restores it).tones.test.tsand
alerts-bar.test.tsxneeded no changes — neither asserted the old module consts directly
(onlyGPU_SATURATION_PCT's value, which is preserved).
Store / re-render approach (and why)
Module-level singleton (state + a Set of listener callbacks), no external state library.
Getters (getSLA, getAlertThresholds, getThresholds) are the "hot path" reads used by
tones.ts and by evaluateAlerts/evaluateGPUAlert's default parameter — plain synchronous
reads, no subscription, so nothing about their call sites had to change. useThresholds() is the
React-facing subscription (useSyncExternalStore), used in two places:
-
AlertsBar— subscribes directly, destructures the livealertssnapshot, and passes it
explicitly intoevaluateAlerts/evaluateGPUAlertand into itsuseMemodependency arrays, so
a hydrate recomputes and re-renders it deterministically. This one was straightforward since
AlertsBarowns the whole computation. -
The 5 views — this was the judgment call. The tone-fn call sites
(errorRateTone(rate),latencyTone(ms), etc.) live inline in each view's render body, and
they read the live global snapshot on every call — so if a view re-renders after hydrate for
any reason, its tones are automatically correct (no memoization hides staleness). The plan
text explicitly allowed relying on the views' existing data-driven re-renders ("if tone fns are
called inside components that already re-render on data... that suffices"), and in practice
that's true here (every view polls its own queries). But I didn't want correctness to depend on
incidental timing — a view that mounts, renders once before its first query resolves, and then
the query keeps returning cached/unchanged data could otherwise show a stale-default tone
indefinitely. So each of the 5 views also calls a bareuseThresholds()(return value
discarded) purely to guarantee at least one re-render whenapplyThresholdsruns. This is a
trivial, deterministic mechanism — confirmed correct becauseuseSyncExternalStoretriggers a
commit on everyemit()from the store, unconditionally, without depending on React Router's
<Outlet>/_renderMatchesmemoization internals (which I did not want to rely on).
I confirmed this experimentally isn't needed for App.test.tsx's existing suite to keep
passing (fetch is globally stubbed there to resolve {status:'ok'} for every request,
including /api/config, which applyThresholds no-ops on via optional chaining since it has no
sla/alerts keys) — the hook call didn't change any existing test's behavior, only added a
render-time guarantee for the real hydrate path.
Confirmed: the tone-fn call sites in all 5 views are untouched (git diff shows only an
import line + one useThresholds() statement added per view, nothing inside the JSX/tone-call
expressions changed) — see git diff --stat below.
Confirmation: 5 views' tone call sites unchanged
src/views/AnalyticsView.tsx | 4 +++
src/views/ExecutiveView.tsx | 4 +++
src/views/OperatorView.tsx | 4 +++
src/views/SLAView.tsx | 4 +++
src/views/SecurityView.tsx | 4 +++
Each diff is exactly: one new import line, one comment line, one useThresholds() call.
errorRateTone(x) / latencyTone(x) / queueTone(x) / gpuUtilTone(x) are byte-identical to
before in every view.
Judgment calls
useThresholds()added to the 5 views (discussed above) — the plan gave latitude here
("do whatever keeps call sites unchanged and reflects a late hydrate"); I chose the
deterministic option over relying on incidental query-driven re-renders.evaluateAlerts/evaluateGPUAlertgained an optional second parameter rather than only
reading the module store internally. The plan didn't constrain their signature (only the tone
fns' signatures were explicitly pinned), andoxlint'sreact-hooks/exhaustive-depsflagged
the memo-with-hidden-global-read pattern as an "unnecessary dependency" — making the dependency
an explicit, defaulted parameter fixed the lint warning and is more testable, while the
default (= getAlertThresholds()) keeps every existing call site (incl. the test file, which
calls both functions with a single argument) working unmodified.PartialThresholdsConfigtype (all leaf fields optional, distinct from a shallow
Partial<ThresholdsConfig>) — needed soapplyThresholds({ alerts: { gpu_saturation_pct: 90 } })type-checks without forcing every sibling field. The fullThresholdsConfig(the real
/api/configresponse) is still structurally assignable to it.- Left
evaluateGPUAlert's empty-list "no alert" honesty rule andevaluateAlerts'completed-
as-success status handling untouched — out of scope for this task.