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.overallONLY (the exact ungrouped aggregate);—+ "Requires the observatory percentile API" when absent. A test proves per-group p95s present inrowsare 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 rangesubtitle (window length from the resolved range, never a hardcoded divisor). - GPU utilization now — max of
util_pct, subtitlegpu0 X% · gpu1 Y%, polls/api/gpu/nowevery 60 s. Emptygpuslist (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_msacross engines per bucket. - GPU utilization (%) — one line per
gpu_idfrom/api/gpu/seriesavg_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.ts—SeriesRow+ optionalp95_total_ms/cache_hits;SummaryRow+ optionalcache_hits;SeriesGroupBy+'operation' | 'status';GPUNow/GPUNowResponse/GPUSeriesParams/GPUSeriesRow/GPUSeriesResponse;getGPUNow/getGPUSerieson the existingrequest()pattern. Shapes re-verified againstahu-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 mirrorsExecutiveView.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— additiveunavailableNoteprop (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) —/slaroute, noRequireRolewrapper (executive-level).src/lib/api.test.ts(+ 2 tests) — GPU endpoint paths + query-param building.
Judgment calls
- Empty-GPU tile =
unavailablewith a custom note, not a fake0%or a bare—: extendedStatTilewithunavailableNoteso "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. - 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.
- 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.
unavailableNotedefault keeps existing StatTile behavior byte-identical; no other shared component changed.p95PointsByGroupskips rows withoutp95_total_msand the panel shows a labeled empty state when nothing survives — no interpolation, no fallback toavg_total_ms.- SLAView reuses the
executive__*layout classes (plus anslaroot class) instead of duplicating ~40 lines of SCSS; the classes are pure layout. - 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.
- Table cells pre-format to strings (
formatMs,formatCount) before enteringDataTable— keeps Carbon's row plumbing trivial and the formatting testable.