AHU AI Observatory — Dashboard UI (P1 plan 2) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to execute task-by-task. Each frontend task's implementer MUST invoke
datavizbefore writing any chart code andui-ux-pro-max:ui-ux-pro-maxfor layout/component work. Steps use checkbox (- [ ]) syntax.
Goal: A role-aware web dashboard for ahu-ai-observatory, served on ai-ahu, that turns the live audit stream into readable BI (executive), a searchable call explorer (operator), and gated body/chain/access views (auditor) — consuming ONLY the existing :8300 query API. Real data is already flowing (chatbot traffic since 2026-07-04).
Architecture: React + Vite + TypeScript + Tailwind + shadcn/ui SPA (matches the OCR/chatbot frontends). Charts hand-built to the dataviz skill's specs (Recharts is permitted as the rendering primitive, but palette/marks/tooltips/legend follow dataviz, not Recharts defaults). Served by an nginx container that also reverse-proxies /api/* and /healthz to the observatory (ahu-observatory-observatory-1:8300) so the browser is same-origin — no CORS, and no observatory backend change. The bearer token is entered at a login gate, held in sessionStorage, and forwarded by the browser on same-origin API calls (nginx passes Authorization through).
Tech Stack: Vite 6 · React 19 · TypeScript · Tailwind v4 · shadcn/ui · TanStack Query v5 (server state + keyset pagination) · Recharts (chart rendering primitive only) · nginx:alpine (serve + proxy). No state lib beyond TanStack Query + React context for auth.
Global Constraints
- Repo: new
ahu-observatory-dashboard/under/home/efran/remote-development/poc-ahu-ai/(its own git repo, branchfeat/dashboard-p1— Task 1 creates it). NOT a subdir of the Go repo. - Consumes ONLY the observatory
:8300API as built (do not assume endpoints that don't exist). Authoritative surface — verify against../ahu-ai-observatory/internal/api/server.go: GET /api/summary?from&to→ totals by engine+status (executive+)GET /api/series?from&to&bucket&group_by→ time buckets;group_by ∈ {engine,upstream,traffic_class}; fields includecalls, tokens_in, tokens_out, avg_total_ms, max_total_ms, avg_queue_ms(executive+)GET /api/calls?engine&upstream&status&trace_id&from&to&limit&before_ts&before_id→ metadata list, keyset paginated (operator+)GET /api/calls/{event_id}→ one call's metadata (operator+)GET /api/calls/{event_id}/body→{event_id, request, response}decompressed; every call is logged server-side (auditor only)GET /api/verify-chain?from&to→{ok, bad_chain_pos}(auditor only)GET /api/access-log?limit→ body-access audit rows (auditor only)GET /healthz(open); error shape is{"error":{"code","message"}}.- Roles (allow-sets, exactly): executive → summary+series; operator → +calls+call-detail; auditor → +body+verify-chain+access-log. The UI hides/disables what the token's role can't reach AND handles a 403 gracefully (the server is the real gate; the UI is convenience).
- dataviz non-negotiables are binding (each chart task's reviewer checks them): categorical palette in fixed order never cycled (values below); one y-axis per chart (never dual-axis); sequential = one hue light→dark; legend present for ≥2 series, ≤4 also direct-labeled; recessive grid/axes; text in ink tokens never series color; a table-view toggle on every chart (also satisfies the low-contrast relief rule); crosshair+tooltip on time-series, per-mark hover on bars; light+dark both selected and validated.
- Palette (the dataviz reference instance — validated, use verbatim). Categorical light/dark by slot: 1 blue
#2a78d6/#3987e5, 2 aqua#1baf7a/#199e70, 3 yellow#eda100/#c98500, 4 green#008300/#008300, 5 violet#4a3aa7/#9085e9, 6 red#e34948/#e66767, 7 magenta#e87ba4/#d55181, 8 orange#eb6834/#d95926. Status colors reserved (good/warn/serious/critical) — never reused as a series hue. Sequential blue ramp for magnitude. Each chart task runsscripts/validate_palette.js(in the dataviz skill dir) on the exact hues it uses, both modes, before commit. - No secrets in the repo. The dashboard has no tokens of its own; the user pastes one at login.
- Every created/modified
.mduploaded:curl -F "file=@<file>" https://x056.think.val.id/upload. - The Go observatory repo and all engine repos are read-only context — never modified.
- Commit per task; messages end with
Claude-Session: https://claude.ai/code/session_01Cco5bXPQVNVk342NEW1pdT. Tests:pnpm test(Vitest) +pnpm buildmust pass before each commit;pnpm lintclean.
Explicitly OUT of scope (do not build; note as future)
- Live GPU utilization, queue depth, replica health, autoscaler state — these come from the gateway controller (P3, not built). This dashboard is the BI/audit surface. A later plan adds the ops/fleet console once the controller exposes that telemetry; leave a clearly-labeled "Fleet health — available when GPU cluster management (P3) ships" placeholder card on the operator view, nothing more.
- p50/p95/p99 latency — the API serves avg + max only. Use avg/max; put a one-line note that true percentiles need an observatory API addition (deferred backlog). Do NOT compute percentiles client-side over raw rows.
- Alerting, report export/PDF, saved views — later.
File Structure
index.html · vite.config.ts (proxy /api→:8300 for `pnpm dev`) · tailwind + shadcn config
src/main.tsx · src/App.tsx (router + role-gated routes)
src/lib/api.ts typed fetch client (Authorization from auth ctx; parses {error} shape)
src/lib/auth.tsx AuthProvider: token in sessionStorage, decodes role from a /api/whoami probe OR a role picked at login (see Task 2), logout
src/lib/viz/palette.ts the categorical/sequential/status tokens above as CSS vars + TS constants
src/components/viz/ StatTile.tsx, TimeSeriesChart.tsx, BarBreakdown.tsx, ChartFrame.tsx (title+legend+table-toggle+dark-aware)
src/components/app-shell.tsx nav (role-aware), theme toggle, health pill
src/views/ExecutiveView.tsx · OperatorView.tsx · AuditorView.tsx
src/views/LoginGate.tsx
deploy/nginx.conf (serve dist + proxy /api,/healthz → observatory) · deploy/compose.yaml · Dockerfile
Task 1: Scaffold + palette + API client + nginx proxy shell
Deliverable: pnpm dev runs an empty-but-styled shell; pnpm build produces dist/; the Docker image serves it and proxies /api to the observatory; palette tokens exist as CSS vars + TS constants; a typed api.ts client with an injectable token and {error}-shape parsing; one passing test (api client parses an error body).
- [ ] Create repo + branch;
pnpm create vite@latest . --template react-ts; add Tailwind v4, shadcn/ui init, TanStack Query, Recharts, Vitest. - [ ]
src/lib/viz/palette.ts: the 8 categorical light/dark hues (fixed order), sequential blue ramp, reserved status colors, surface + ink tokens for both modes — as documented in Global Constraints. Export both a CSS-var stylesheet (.viz-root,@media (prefers-color-scheme: dark), and:root[data-theme=dark]/[data-theme=light]overrides so the theme toggle wins both ways) and typed TS constants. - [ ] Run the dataviz validator on the 8 categorical hues,
--mode lightand--mode dark(surfaces from the palette). Paste PASS output into the report. Fix nothing (reference palette passes) — this proves the wiring. - [ ]
src/lib/api.ts:createClient(getToken)returning typed methods for every endpoint in Global Constraints; setsAuthorization: Bearer <token>; on non-2xx parses{error:{code,message}}and throws a typedApiError{status,code,message}; 401→ApiErrorwith a flag the auth layer catches. Failing test first: given a stub 404{"error":{"code":"UNKNOWN_MODEL",...}}, the client throwsApiErrorwith.code==="UNKNOWN_MODEL". - [ ]
vite.config.ts: dev proxy/apiand/healthz→http://192.168.83.20:8300(sopnpm devworks against live data with a token). - [ ]
deploy/nginx.conf: serve/usr/share/nginx/html, SPA fallback toindex.html, andlocation /api/ { proxy_pass http://observatory:8300; proxy_set_header Authorization $http_authorization; }(+ same for/healthz).Dockerfile: node build stage → nginx:alpine.deploy/compose.yaml: dashboard service on theahu-platform_defaultexternal network (to resolve the observatory container), published on a free host port (propose 8320; Task 7 verifies it's free on ai-ahu). - [ ]
pnpm build+pnpm testgreen; commitfeat(scaffold): vite+tailwind+shadcn shell, viz palette, typed api client, nginx proxy.
Interfaces produced: ApiClient type (all endpoint methods + return types), palette CSS-var names + TS constants, ApiError.
Task 2: Auth gate + app shell (role-aware nav, theme toggle, health pill)
Deliverable: Visiting the app with no token shows a login gate (paste token + pick role — see note); with a token, the app shell renders role-appropriate nav, a light/dark toggle (persisted), and a live /healthz status pill. A 401 anywhere returns the user to the gate.
- [ ] Role determination: the API has no
/whoami. Simplest honest approach: the login gate has the user paste their token AND select their role (executive/operator/auditor) from a dropdown — the selected role only drives UI affordances; the server enforces the real gate, so a wrong pick just yields 403s the UI surfaces. Store{token, role}insessionStorageviaAuthProvider. (Document this; a future/whoamiendpoint would remove the manual pick.) - [ ]
AuthProvider+useAuth(): token/role,login,logout, and an axios/fetch interceptor-equivalent that onApiError.status===401clears auth and routes to the gate. - [ ] App shell (
ui-ux-pro-maxfor layout): left/top nav with only the sections the role allows (executive: Overview; operator: +Calls; auditor: +Audit); theme toggle stampingdata-themeon<html>(persisted to localStorage), health pill polling/healthzevery 15s (green ok / red unreachable, with thepending/ingestednumbers in a tooltip). - [ ] Tests: role→visible-nav-items mapping; 401 clears session. Commit
feat(shell): token/role auth gate, role-aware nav, theme toggle, health pill.
Task 3: dataviz-compliant chart primitives
Deliverable: Three reusable, dark-aware, accessible chart components wrapped in a common ChartFrame (title, legend, table-view toggle, empty/loading states). Implementer MUST invoke dataviz first and build to its mark/interaction specs, not Recharts defaults.
- [ ]
ChartFrame: title, optional subtitle, legend (present for ≥2 series, positioned once), a table-view toggle that swaps the chart for an accessible<table>of the same data (satisfies the relief rule for low-contrast slots + accessibility), loading skeleton, empty state. - [ ]
StatTile: hero number + label + optional delta + optional sparkline; ink tokens for text; no legend. For headline figures (total calls, error rate, external-call count). - [ ]
TimeSeriesChart: line/area over time, one y-axis, thin 2px lines, ≥8px markers only on hover, crosshair + tooltip showing all series at the hovered bucket, ≤4 series direct-labeled at line-end else legend, recessive grid. Series colored by categorical slot in fixed order keyed to the entity (engine/status), never repainted when the set filters. - [ ]
BarBreakdown: horizontal bars for magnitude-by-category (calls by engine/model), 4px rounded data-end at baseline, 2px surface gap between bars, per-bar hover tooltip, direct value labels. - [ ] Run
validate_palette.json the exact hues these use (both modes); paste PASS. Tests: table-toggle renders equivalent data; series→fixed-slot color mapping is stable across a changing series set (the anti-pattern guard). Commitfeat(viz): ChartFrame + StatTile + TimeSeries + BarBreakdown per dataviz specs.
Interfaces produced: the three chart component props + ChartFrame.
Task 4: Executive view
Deliverable: The default landing view for all roles. A date-range control (filters row, one row above charts) driving: headline stat tiles, throughput/latency time-series, and per-engine/model breakdowns — all from /api/summary + /api/series.
- [ ] Filters row: date-range picker (default last 24h; presets 1h/24h/7d) + bucket auto-derived from range (1m/1h). Drives all panels via TanStack Query keys.
- [ ] Stat tiles: total calls, overall error rate (from summary status split), total tokens (in+out), and the external-calls proof tile — count of calls with
upstream_class="external_dev"(from agroup_by-less summary or a series sum); in prod this should read 0, rendered with a good/critical status treatment. (If summary doesn't expose upstream_class, use/api/series?group_by=upstreamand map known external upstream ids — document which.) - [ ] Time-series (TimeSeriesChart): calls/min by status (ok vs error, fixed slots); tokens/min (in vs out); avg + max latency (
avg_total_ms,max_total_ms) — note in the panel subtitle that these are avg/max; p95/p99 pending an API addition. - [ ] Breakdown (BarBreakdown): calls by engine, calls by model (from
group_by=engine/upstreamseries summed over the range). - [ ] Everything degrades cleanly for executive-only tokens (only summary/series are needed here — no 403s expected). Loading/empty/error states via ChartFrame. Test: date-range change refetches with new keys; external-calls tile shows 0 vs non-0 treatment. Commit
feat(executive): summary tiles, throughput/latency series, engine/model breakdowns.
Task 5: Operator view — call explorer
Deliverable: A filterable, keyset-paginated table of individual AI calls with a detail drawer, from /api/calls + /api/calls/{id}.
- [ ] Filters row: engine, upstream, status, trace_id, date-range →
/api/callsparams. Results table: ts, engine, surface, model, status (status-colored chip + label, never color-alone), traffic_class, queue_ms, total_ms, tokens. Recessive, dense, sortable-by-ts (server order). - [ ] Keyset pagination using
before_ts+before_id(the API's cursor) — "load more" appends; never offset. TanStack QueryuseInfiniteQuery. - [ ] Row click → detail drawer: full metadata for that call (
/api/calls/{id}), including trace_id with a "show all calls in this trace" action (filters the table by trace_id — the fan-out view). A "View bodies" button that is present only for auditor role and links into the auditor view for that event_id (operator can't fetch bodies — don't show a dead button). - [ ] "Fleet health" placeholder card labeled "Available when GPU cluster management (P3) ships" — static, no fake data.
- [ ] Tests: filter change resets pagination; cursor advances without dup/skip across two pages (mock API). Commit
feat(operator): keyset-paginated call explorer with detail drawer and trace fan-out.
Task 6: Auditor view — body viewer, chain verify, access log
Deliverable: The compliance surface. Body inspection (server-logged), hash-chain verification, and the access-log viewer — all auditor-gated, all handling 403 for lesser roles.
- [ ] Body viewer: given an event_id (deep-linked from operator, or pasted), fetch
/api/calls/{id}/body→ show decompressed request + response in readable panels (JSON-pretty when parseable, raw otherwise), with a prominent "this view is recorded" notice (it is — every fetch logs an access row). Truncation note when the metadata flagged it. - [ ] Chain verify: a "Verify integrity" action over an optional range →
/api/verify-chain→ renderokas a good status banner, orbad_chain_poswith a critical banner naming the first broken position. (Field isbad_chain_pos— confirmed against the shipped API, notfirst_bad.) - [ ] Access log:
/api/access-log?limittable (at, actor [hashed], role, event_id, action) — the "who looked at what" trail, itself the Access Audit Trail deliverable. Note actor is a token-hash, not a name. - [ ] 403 handling: if a non-auditor token reaches these (deep link), show a clear "requires auditor role" state, not a crash. Tests: body-view renders pretty+raw; verify-chain ok vs bad banner; 403 state. Commit
feat(auditor): recorded body viewer, chain verification, access-log trail.
Task 7: Packaging + live deploy on ai-ahu (exit gate)
Deliverable: The dashboard running on ai-ahu against live data, reachable in a browser, screenshotted.
- [ ] Finalize
Dockerfile(multi-stage: pnpm build → nginx:alpine withdeploy/nginx.conf),deploy/compose.yaml(externalahu-platform_defaultnet; host port 8320 — verify free viasson ai-ahu first). README ≤80 lines: what it is, roles, how the token/proxy model works, quickstart, the OUT-of-scope note (fleet health = P3). Upload README. - [ ] rsync repo to
efran@192.168.83.20:~/ahu-observatory-dashboard/(exclude .git/.superpowers/node_modules);docker compose up -d --build; confirm the container is on the platform network and resolvesobservatory. - [ ] Live verification with a real token (operator + auditor from the host's
observatory.yaml): loadhttp://192.168.83.20:8320/, log in, confirm executive tiles show the real ingested calls (the 6 ahu-chatbot + others), the call explorer lists them, a body view works AND creates an access-log row (check the access-log tab reflects your own view), verify-chain returns ok. Take screenshots (headless or via the run skill) of the executive view + call explorer and SendUserFile them. - [ ] Disk check before build (ai-ahu ~87%): the node build happens locally, only the slim nginx image ships — confirm image < 100MB. Commit
feat(deploy): nginx serve+proxy image, compose, README;git tag dash-p1; leave running (note stop command). Write + upload the drill report.
Self-review notes (plan-time)
- Scope honesty: every panel maps to an endpoint that EXISTS today (verified against the built API surface). Fleet/GPU/queue and true percentiles are explicitly deferred with visible placeholders, not faked — the #1 way a dashboard lies.
- dataviz compliance is a per-task reviewer gate (palette validated by script, one-axis, fixed-slot categorical, table-view relief, dark mode validated separately) — not left to taste.
- Security: dashboard holds no token; same-origin nginx proxy avoids CORS and keeps the token in the Authorization header only; body views remain server-logged (the UI can't bypass that). Manual role-pick at login is a documented stopgap for the missing
/whoami. - The one cross-repo dependency (does
/api/summaryexposeupstream_classfor the external-calls tile?) is flagged in Task 4 with a fallback viagroup_by=upstream— the implementer verifies against server.go and picks the working path.