2026-05-16 — Sidecar HTTP inspector mockups (Variant A, critique-hardened)¶
Motivation¶
The Control Plane sidecar already receives every operator HTTP request, but the Sidecars card on the dashboard only renders aggregate health. Operators have no way to see which request was slow, who called it, or what headers/body were sent — they have to dig through container logs.
We need a per-request HTTP inspector on the sidecar card before wiring it to
the real ring buffer (api/services/request_metrics.py). Following the prior
AKS-bento pattern, we first scaffolded three design variants in a static
mockup, picked one, and critique-hardened it through three rounds before
committing to backend work.
User-facing change¶
- New mockup route
/mockups/sidecar-inspectorregistered above the frontend catch-all in web/src/App.tsx. - Three variants rendered on the same fake fixture
(
generateFixture(NOW)withseededRandom(20260516), 80 requests) so the interaction patterns can be compared on identical input: - Variant A — Timeline scatter + right drawer. Classic APM. Dots = one request each (x = time, y = log-scaled latency). Click a dot or a row to open the detail drawer.
- Variant B — Sparkline grid + inline expand. One row per request, in-row expand shows headers/body.
- Variant C — Lane swimlane + bottom sheet. Status-class lanes (2xx/3xx/4xx/5xx) with bottom sheet detail.
- Variant A is the recommendation and was hardened through three rounds of self-critique. The other two variants stay in the mockup for design comparison only — they are not wired to anything.
The mockup is not wired to any real endpoint. The page header explicitly
calls this out: "None of this is wired to a real endpoint — once a variant is
chosen, backend capture (with header / body redaction + 4 KiB caps) will be
added to api/services/request_metrics.py."
Variant A — what landed after 3 critique rounds¶
| Surface | Feature |
|---|---|
| Header | ● LIVE indicator (green pulse when stream active, yellow PAUSED when paused), status-code count chips (N ok · N 3xx · N 4xx · N 5xx) computed from the visible window, window selector 1m / 5m / 15m, ⚠ Errors toggle (4xx+5xx only), compact icon-only Pause/Resume button — all on a single header row |
| Chart | Inline SVG 880×220 scatter on a brighter rgba(255,255,255,0.07) plot panel; axis lines + tick marks + rotated axis titles; log-scaled y axis with 10/50/200/1000/2000 ms gridlines; dashed SLA 2000 ms line with a <title> ("requests above this line breach the 2 s p95 budget"); 5xx dots get a contrasting halo ring so they don't disappear into 2xx green; hover crosshair (vertical + horizontal dashed lines through the hovered dot); HTML tooltip clamped to the viewport (TIP_W=260 / TIP_H=124 with IIFE flipLeft/flipUp); selected dot keeps a ring even after the mouse leaves; empty-window state ("No requests in selected window") |
| Tooltip | Method · status pill · latency (color-graded by latencyTone(ms): success <200ms, primary <500ms, warning <2000ms, danger ≥2000ms) · path · caller · timestamp · "Click point for full request / response" hint |
| Table | Sticky header (z-index 1); rows are tabIndex=0 with Enter/Space keyboard activation; Duration column color-graded by latencyTone(); ChevronRight row affordance; Filter by path, caller, request_id, status code… search box with X of Y counter and clear-X; "Show N more · X hidden" pagination button; rows respect the window selector + errors-only + search filter cascade |
| Drawer | role="dialog" with aria-label; Esc closes; Copy as curl button generates curl -X METHOD 'https://elb.example.com{path}' -H 'k: v' --data 'body' from the redacted headers/body; per-section Copy buttons; sections: Request ID / Time / Caller / Client IP / Status·Duration·Size / Request headers (Authorization redacted) / Response headers / Body (4 KiB cap visualised) |
Critique rounds (what the rubric caught)¶
- Round 1 — functional bugs. Tooltip Y-clipping at top of chart, empty
fixture crash (
Math.min(...[])= Infinity), broken rotatedChevronUpon table rows, missing PAUSED visual when Pause button hit, missing status-class count chips, missing Errors-only filter, drawer not Esc-closable, drawer missing ARIA role. - Round 2 — hardening features. Window selector (1m/5m/15m), search box
with filter cascade, 5xx halo for color-blind readability, selected-dot
ring strengthened,
latencyTone()applied to both tooltip and table Duration column, table "Show more" pagination, keyboard nav on rows, chart x-axis anchored towindowStart/windowEndinstead of data min/max so the time scale is honest when the window is sparse. - Round 3 — polish.
● LIVEheartbeat indicator withlivePulsekeyframe, hover crosshair on chart,<title>annotation on the SLA reference line, Copy-as-curl button in drawer header, header collapsed into a single row by replacing "Errors only / All" with⚠ Errorsand the Pause/Resume label with a 26×22 icon-only button.
API / IaC diff¶
None in this change. The mockup is purely a web/ page that consumes a
deterministic in-memory fixture. The backend wire-up (extending
api/services/request_metrics.py _Sample to capture headers/body/caller/
client IP/request_id, adding /api/monitor/sidecar-requests, replacing the
Variant A fixture with the typed client) is the explicit follow-up.
Files touched¶
web/src/pages/mockups/SidecarInspectorMockups.tsx— new (~1600 lines). Three variants + shared fixture + Variant A round-3 hardening.web/src/App.tsx— registers/mockups/sidecar-inspectorabove the catch-all redirect.
Validation evidence¶
All screenshots captured against http://127.0.0.1:18080/mockups/sidecar-inspector
served by the local elb-control-local compose project after
cd web && npm run build (clean — no TS errors) and a frontend sidecar
restart.
| Round | Evidence |
|---|---|
| Round 1 — baseline + Errors+Paused filter (80→1 row) | sidecar-r1.png · sidecar-r1-errors-paused.png |
| Round 2 — windowed (1m + search filter cascade 80→13→4) | sidecar-r2.png · sidecar-r2-1m-search.png |
| Round 3 — compact header with LIVE indicator | sidecar-r3.png (initial 2-row layout caught by self-review) · sidecar-r3b.png (single-row after compaction) |
| Round 3 — hover crosshair + colored tooltip | sidecar-r3-hover-tooltip.png |
| Round 3 — drawer + Copy-as-curl button | sidecar-r3-drawer.png |
Build artifacts:
cd web && npm run build→ clean (tsc -b && vite build, ~7 s).get_errorsonSidecarInspectorMockups.tsxandApp.tsx→ no diagnostics.
Manual interaction checklist verified via playwright snapshot:
- Hover on a 2xx dot shows tooltip with
GET 200 …ms /api/monitor/aksplus crosshair lines through the dot. - Clicking a table row opens the drawer (role="dialog") with all expected
sections; the
curlandClose request detail (Esc)buttons render. - Status count chips reflect the visible window (
72 ok · 7 3xx · 0 4xx · 1 5xx). - Window switch
5m → 1mreduces visible samples (80 samples→13 requests); typing in the search box further narrows to 4. ⚠ Errorstoggle plus Pause demonstrates the empty/paused state without crashing.
Scope notes (what is NOT in this change)¶
- No backend changes.
api/services/request_metrics.pystill records onlyts / path / status / duration_ms. Adding header / body / caller / client IP / request_id capture (withAuthorization/Cookie/X-Api-Key/X-Auth-Tokenredaction at capture time and a 4 KiB body cap forapplication/jsonandtext/*only) is the next change and will have its own/api/monitor/sidecar-requestsroute inapi/routes/monitor.py. - No real Sidecars card wire-up.
web/src/components/cards/SidecarsCard/is untouched. Once the backend route lands, the chosen Variant A components will be lifted out of the mockup file and replace the existing card surface, with the fake fixture swapped for a typed client inweb/src/api/endpoints.ts. - Variants B and C stay in the mockup as design comparison only — no hardening pass, no test coverage, will be deleted from the mockup once Variant A is integrated.
- Storage
publicNetworkAccessinvariant unchanged — this is a read-only inspector for in-process request metrics, not a data-plane surface.