think
16px
820px

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 — added SLABand/ThresholdsConfig types (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})
    and getConfig(): Promise<ThresholdsConfig> on ApiClient, hitting GET /api/config through
    the same tokenless request() path /healthz already uses (no bearer header logic needed —
    request() only sets Authorization when getToken() returns a token).
  • src/lib/thresholds.ts (new) — the store. DEFAULT_THRESHOLDS is today's hardcoded values,
    verbatim, and is the module's initial state. 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 on undefined/null.
    Getters: getThresholds(), getSLA(), getAlertThresholds(). useThresholds() wraps
    useSyncExternalStore for 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 read getSLA().<band> at call time instead
    of a hardcoded literal; a shared bandTone(value, goodBelow, warningBelow) helper replaces the
    four duplicated if/else ladders.
  • src/components/alerts-bar.tsxevaluateAlerts/evaluateGPUAlert gained an optional
    second parameter (thresholds: AlertThresholds = getAlertThresholds()) so existing single-arg
    calls (incl. the untouched test file) still work, while AlertsBar itself passes its
    useThresholds()-sourced snapshot explicitly — this made the dependency real for
    useMemo/react-hooks/exhaustive-deps instead of a hidden global read. GPU_SATURATION_PCT
    stays exported (= DEFAULT_THRESHOLDS.alerts.gpu_saturation_pct) so alerts-bar.test.tsx's
    import keeps working untouched. Window/poll are now read from the store too
    (windowMin/pollMs replace ALERT_WINDOW_MIN/ALERT_POLL_MS).
  • src/App.tsxProviders fires a bootstrap useEffect (mount-only) that does
    createClient(() => null).getConfig().then(applyThresholds).catch(() => {}) — tokenless client
    (mirrors HealthPill'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 of useThresholds and one bare useThresholds() 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_below to 0.5, then resetThresholds() restores it). tones.test.ts and
    alerts-bar.test.tsx needed no changes — neither asserted the old module consts directly
    (only GPU_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:

  1. AlertsBar — subscribes directly, destructures the live alerts snapshot, and passes it
    explicitly into evaluateAlerts/evaluateGPUAlert and into its useMemo dependency arrays, so
    a hydrate recomputes and re-renders it deterministically. This one was straightforward since
    AlertsBar owns the whole computation.

  2. 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 bare useThresholds() (return value
    discarded) purely to guarantee at least one re-render when applyThresholds runs. This is a
    trivial, deterministic mechanism — confirmed correct because useSyncExternalStore triggers a
    commit on every emit() from the store, unconditionally, without depending on React Router's
    <Outlet>/_renderMatches memoization 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

  1. 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.
  2. evaluateAlerts/evaluateGPUAlert gained 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), and oxlint's react-hooks/exhaustive-deps flagged
    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.
  3. PartialThresholdsConfig type (all leaf fields optional, distinct from a shallow
    Partial<ThresholdsConfig>) — needed so applyThresholds({ alerts: { gpu_saturation_pct: 90 } }) type-checks without forcing every sibling field. The full ThresholdsConfig (the real
    /api/config response) is still structurally assignable to it.
  4. Left evaluateGPUAlert's empty-list "no alert" honesty rule and evaluateAlerts' completed-
    as-success status handling untouched — out of scope for this task.