**Phase 03 final verification pass — all 4 tasks already in `complete/`; verified criteria, fixed 3 pre-existing defects.**
- **Verified live** (headless Chromium + curl against `:8090`, all 6 sources `ok`): 3 panels × six heroes in order `cpu,mem,temp,disk,gpus,net`; charts per panel `util,net[,gpu]` (gateway: no GPU chart/hole); legends name metrics only — no legend names a foreign host; every `<svg>` `role="img"` + host-scoped `aria-label`; 2 sections, 0 `<select>`; `grep -rn "renderCores|core_usage" src/dashboards/web/` → nothing; console error-free; new shot `data/logs/final-system-panels.png`.
- **Defect fixed (credential hygiene, invariant 6)**: `collectors.py` put the *dialed* URL into every `UpstreamError`, and that string lands in the health record (`/api/health`) and the `event` table (`/api/events` → page); `/api/config` also echoed `GUFO_BASE_URL`/`VLLM_BASE_URL` verbatim. Added `config.strip_userinfo()` (now what `Target.display_url` uses) and redacted all message/config URLs. Tests: `test_a_failing_host_never_echoes_its_token_in_the_message` (×3 paths), `test_a_failing_engine_never_echoes_credentials_in_its_url`, `test_a_dead_host_never_writes_its_token_into_the_feed`, `test_engine_urls_are_redacted_the_same_way_hosts_are`, `test_strip_userinfo_is_the_one_redaction_every_url_leaves_by`. (`parse_targets` already moves `HOST_TARGETS` userinfo into a header, so this is a backstop for hosts and a real fix for engine URLs.)
- **Defect fixed (invariant 2)**: `/system` footnote printed `Δ{HOST_INTERVAL}` as the measurement; it now prints each host's measured `window_seconds` (range when they disagree) — `rates over Δ10.0s · poll 10s`. Pinned by `test_system_footnote_prints_the_window_it_actually_measured`. Also fixed the page's stale "window in each header" note and the empty-state's `--token` (argv) advice → `--token-file`.
- `bash .agents/validate.sh` → **EXIT=0**: **472 passed, 7 skipped**, coverage **96.87%** (floor 94), `ruff check src tests agent` clean, `pyright` 0 errors, `node --check` clean · `uv run pytest -m live --live` → **7 passed** · `uv pip list --outdated` empty.
- Criteria: no multi-host legend ✅ · no per-core visual ✅ · six heroes/panel incl. no-GPU `n/a` uncoloured ✅ · per-core history still queryable ✅ (354 pts) · ≥3 shape screenshots ✅ (`system-driveripper/deskwork/gateway.png` + dark/light/stale) · pytest+coverage+lint ✅ · phases 01/02 unchanged (their pin tests + `/api/summary` shape tests pass; live UPS gap in AGENTS.md re-confirmed read-only against real upsd) ✅.
- **Docs**: AGENTS.md — invariant 6 rewritten to cover message/event redaction, `collectors.py` line, new "Δ is a measurement" rule, "hosts answering total" → "nine-figure fleet-totals panel" (the subtitle still counts hosts), test count 452→472, coverage 96.8→96.9, rewrapped an over-long line. README.md — same fleet-totals wording, `RATE_WINDOW_SECONDS` now covers every measured rate (not just tokens), agent route list completed (`/`, `/schema`, `?top=N`), `LIST CMD` in the diagram, new paragraph on URL redaction.
- **Deviation**: the criterion's literal `curl …/api/system/history?host=…&metric=cpu.usage_pct@core7` uses singular params that endpoint has never accepted (it takes `hosts=`/`metrics=` and ignores unknown ones). Verified 354 points via `…system/history?hosts=driveripper&metrics=cpu.usage_pct@core7&range=1h` and `/api/history?source=host&host=…&metric=…`; I did **not** add singular param aliases (unauthorized API surface). No live infra changes made — reads only.
- **Next pending phase**: `04_llm_chart_readability`.
4.5 KiB
Verification notes — /system on every host shape
Date: 2026-10-17 (dev loop: tunnels 9121/9122/9123 + upsd 3494 all OK; dev-server
/api/health all six sources ok before and after verification)
What was checked, on live data
Verified against http://127.0.0.1:8090/system with all three real agents answering, in a
real browser (headless Chromium/Playwright), DOM assertions plus screenshots.
| Host (shape) | Result | Screenshot |
|---|---|---|
driveripper (discrete Radeon AI PRO R9700) |
GPU hero: 3 % ok, .sub = "Radeon AI PRO R9700 · VRAM 30.5 GiB / 31.9 GiB · pool 33 %"; util/net/GPU charts all present inside the panel |
system-driveripper.png (dark), system-driveripper-light.png |
deskwork (Strix Halo APU, two memory pools) |
GPU hero busy went 82 % ok → 85 % warn → 99 % fail across the session, so threshold colour visibly moves; .sub = "Radeon 8060S Graphics · VRAM 172.7 MiB / 512.0 MiB · pool 78 %" — small dedicated VRAM and high shared pool read as two facts, not a contradiction; MEM 91 % and TEMP ~83 °C amber |
system-deskwork.png |
gateway (no GPU, no CPU temp sensor) |
CPU TEMP n/a with no severity class; GPU slot = n/a + "No GPU reported by this agent", uncoloured; no GPU chart and no hole where one would be |
system-gateway.png (dark), system-gateway-light.png |
Full-page shapes: system-shapes-dark.png, system-shapes-light.png.
Failure path: stopped only the gateway tunnel (same commands dev-tunnels.sh stop runs,
scoped to the 9123 entry; remote agent stopped too). After 15 poll failures the panel kept
its last-good heroes (MEM 32 %, DISK 61 %, NET rates — nothing zeroed, nothing n/a), its
dot turned red, the header read stale 2.1m, and the card dimmed via .stale. The other
two panels stayed green ("one dead source must not blink another"). Store recorded exactly
one unreachable and, after ./scripts/dev-tunnels.sh start, one reachable event.
Screenshot: system-stale-gateway.png (+ system-stale-overview.png full page). Restored;
dev-tunnels.sh status and /api/health back to all-OK.
Accessibility pass:
- hero values are real selectable text (
div.value > span.num/span.unit, no SVG paths) — asserted in-page for every hero on all three panels; - colour is never the only signal: each hero prints the number, the NET slot is
deliberately colourless, staleness reads as text (
updated Ns/stale 2.1m); - charts — defect found: the SVGs were anonymous (
aria-labelabsent on container and svg). Fixed:LineChartgained anariaLabeloption that setsrole="img"+aria-labelon the<svg>, and every/systemchart now names its host and metrics (e.g. "driveripper — utilisation over the selected window: CPU, memory and worst disk in per cent"). Pinned bytests/test_api.py::test_every_system_chart_names_itself_for_assistive_tech. The/llmand/upscharts can adopt the same option in phases 04/05.
Both themes checked via the header toggle (severity colours come from theme tokens; the
light-theme deskwork fail-red and warn-amber read clearly — see
system-shapes-light.png, system-gateway-light.png). Note for the next shooter: set
prefers-color-scheme or seed localStorage['dash-theme']; Playwright defaults to light,
which silently mislabels a "dark" screenshot.
curl "localhost:8090/api/system/history?hosts=driveripper&metrics=cpu.usage_pct@core7&range=1h"
→ 354 points: dropping the per-core plot did not drop the data.
Defects found and fixed during verification
- Unlabelled charts (above) — fixed in
charts.js+page-system.js, test-pinned, re-screenshotted. /systemEvents panel was permanently empty — the poller stores transition events under the dashboard name (system) while the page queried/api/events?source=host, so the feed said "No host transitions recorded yet" even with the deliberate gateway outage in the store. Fixed tosource=system(with a comment on the naming trap);tests/test_api.py::test_system_event_feed_queries_the_source_transitions_are_stored_underpins it; the re-shot full-page screenshots show the feed populated with the outage and recovery. Pre-existing since the first commit, not introduced by phase 03.
After the fixes
bash .agents/validate.sh → exit 0: 460 passed + 7 skipped, coverage 96.86 % (≥ 94),
ruff + pyright clean, node --check clean. uv run pytest -m live --live → 7 passed.
Console stayed error-free throughout.