think
16px
820px

SLA & Throughput view — build report

Commit: 02bbd60 feat(sla): SLA & Throughput view + GPU telemetry + saturation alert (master)

Gates: npx tsc --noEmit clean · npx vitest run 34 files / 254 tests, all green (was 224 — 30 added) · npm run build passes (pre-existing chunk-size warning only).

What was built

New /sla route, nav label SLA, minRole: 'executive' so every role sees it.

Tiles

  • Fleet latency p95/p99 — from summary.overall ONLY (the exact ungrouped aggregate); + "Requires the observatory percentile API" when absent. A test proves per-group p95s present in rows are not used as a substitute.
  • Error rate (window) — from summary rows; errors = every status outside {ok, completed} (matches the alerts-bar vocabulary).
  • Throughput — total calls + ≈ N calls/min over the range subtitle (window length from the resolved range, never a hardcoded divisor).
  • GPU utilization now — max of util_pct, subtitle gpu0 X% · gpu1 Y%, polls /api/gpu/now every 60 s. Empty gpus list (the server's ≤2-min staleness window makes a dead exporter read empty) → the honest Unavailable state with a "No GPU sample in the last 2 min (exporter down?)" note — never a number, never a tone.

Charts (all on the shared WIB-zoned LineTimeSeries/ChartPanel)

  • Latency p95 by engine (ms) — one line per engine from each row's own p95_total_ms; rows lacking the field are skipped (older builds), with an explicit empty-state note instead of a fabricated percentile.
  • Throughput (calls) — per-engine lines, reusing the same engine series fetch.
  • Error rate (%) — single exact series from group_by=status: per bucket, non-{ok,completed} calls / total.
  • Queue wait avg (ms) — calls-weighted average of avg_queue_ms across engines per bucket.
  • GPU utilization (%) — one line per gpu_id from /api/gpu/series avg_util_pct.

Table

Carbon DataTable (plain @carbon/react table, size sm) per (engine, status): calls, p50, p95, p99, max, avg queue — sorted engine→status, ms via formatMs, absent percentiles render (never 0). Wrapped in ChartPanel so loading/error/empty states are the same honest ones the charts get.

AlertsBar

New GPU saturation banner: separate useQuery polling getGPUNow() every 60 s; pure exported evaluateGPUAlert(gpus) → {active, maxUtil} fires at util_pct ≥ GPU_SATURATION_PCT (95, exported + documented — sustained-across-polls semantics come from the server's 2-min freshness window + 60 s poll). Empty snapshot raises nothing (no data ≠ alert; the SLA tile surfaces exporter death).

Files

  • src/lib/api.tsSeriesRow + optional p95_total_ms/cache_hits; SummaryRow + optional cache_hits; SeriesGroupBy + 'operation' | 'status'; GPUNow/GPUNowResponse/GPUSeriesParams/GPUSeriesRow/GPUSeriesResponse; getGPUNow/getGPUSeries on the existing request() pattern. Shapes re-verified against ahu-ai-observatory/internal/api/server.go (not just the brief).
  • src/lib/sla/metrics.ts (+ .test.ts, 16 tests) — errorRateByBucket, weightedQueueByBucket, gpuUtilPoints, p95PointsByGroup, callsPointsByGroup, windowErrorRate, perMinuteRate. TDD: tests written first.
  • src/views/SLAView.tsx (+ .test.tsx, 13 tests) — the view; test file mirrors ExecutiveView.test.tsx (charts stubbed, spy asserts the data each chart receives, tiles/states asserted in DOM).
  • src/components/alerts-bar.tsx (+ 4 tests) — GPU banner + evaluateGPUAlert.
  • src/components/charts/StatTile.tsx — additive unavailableNote prop (default 'Query failed' — zero behavior change for existing callers).
  • src/lib/nav.ts (+ test updates) — {id:'sla', label:'SLA', path:'/sla', minRole:'executive'} between Overview and Calls.
  • src/App.tsx (+ 1 routing test) — /sla route, no RequireRole wrapper (executive-level).
  • src/lib/api.test.ts (+ 2 tests) — GPU endpoint paths + query-param building.

Judgment calls

  1. Empty-GPU tile = unavailable with a custom note, not a fake 0% or a bare : extended StatTile with unavailableNote so "query failed" and "source has no data" stay distinguishable while both remain value-free. This follows the brief's "unavailable state when gpus list empty" literally without lying about a query failure.
  2. Settled-bucket race guard covers ALL bucketed series (engine, status, GPU): the guard trips if any of the three is still placeholder data across a bucket change, so a stale 1m series can never render under 1h tick labels even if the others settle first — a strict superset of the ExecutiveView single-query pattern.
  3. Queue trend derives from the engine series (already fetched for p95/throughput) rather than a third grouped fetch — the calls-weighted fold is group-dimension-agnostic, so this is the same number with one fewer request.
  4. unavailableNote default keeps existing StatTile behavior byte-identical; no other shared component changed.
  5. p95PointsByGroup skips rows without p95_total_ms and the panel shows a labeled empty state when nothing survives — no interpolation, no fallback to avg_total_ms.
  6. SLAView reuses the executive__* layout classes (plus an sla root class) instead of duplicating ~40 lines of SCSS; the classes are pure layout.
  7. GPU-now polling is intentionally duplicated between the view tile and the AlertsBar under different query keys, per the brief ("separate useQuery in alerts-bar") — different lifecycles (route-scoped vs shell-scoped) justify the extra call.
  8. Table cells pre-format to strings (formatMs, formatCount) before entering DataTable — keeps Carbon's row plumbing trivial and the formatting testable.