phase: 03_system_per_host_panels

**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`.
This commit is contained in:
2026-10-17 04:42:09 -04:00
parent 8b048599f3
commit 9757e3c947
43 changed files with 1686 additions and 464 deletions
@@ -0,0 +1,66 @@
# 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-label` absent on container and
svg). Fixed: `LineChart` gained an `ariaLabel` option that sets `role="img"` +
`aria-label` on the `<svg>`, and every `/system` chart now names its host and metrics
(e.g. "driveripper — utilisation over the selected window: CPU, memory and worst disk in
per cent"). Pinned by `tests/test_api.py::test_every_system_chart_names_itself_for_assistive_tech`.
The `/llm` and `/ups` charts 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
1. **Unlabelled charts** (above) — fixed in `charts.js` + `page-system.js`, test-pinned,
re-screenshotted.
2. **`/system` Events 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 to `source=system` (with a comment on the naming trap);
`tests/test_api.py::test_system_event_feed_queries_the_source_transitions_are_stored_under`
pins 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.
@@ -0,0 +1,10 @@
**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`.
@@ -0,0 +1,46 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 15%]
........................................................................ [ 30%]
........................................................................ [ 45%]
...........................sssssss...................................... [ 60%]
........................................................................ [ 75%]
........................................................................ [ 90%]
............................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 28
src/dashboards/alerts.py 175 0 100%
src/dashboards/api.py 193 0 100%
src/dashboards/app.py 109 6 94% 50-51, 56, 58, 60, 131
src/dashboards/collectors.py 142 5 96% 97-98, 102-103, 120
src/dashboards/config.py 113 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 139 1 99% 226
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 226
src/dashboards/sources/nut.py 367 22 94% 118, 132, 138, 150, 156, 168, 176, 181, 252-253, 255, 414, 458, 470, 505, 538, 542, 595, 599-600, 605, 617
src/dashboards/sources/nut_sim.py 122 21 83% 141, 167-185, 226
src/dashboards/store.py 178 2 99% 243-244
------------------------------------------------------------------
TOTAL 2109 66 97%
Required test coverage of 94% reached. Total coverage: 96.87%
472 passed, 7 skipped, 1 warning in 43.59s
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
@@ -0,0 +1,21 @@
**Implemented**
- `tests/card_contract.py` (new): the single `/system` hero-card payload table — `CARD_METRICS`, `GPU_METRICS`, `SEVERITY_FREE_SECTIONS`, `HOST_SHAPES`, `dig()`, `assert_card_contract()`. Documents the two recorded choices: card temp comes from `cpu.temp_c` (identical to `temperatures.cpu_c`, and what `/api/summary` flattens), disk verdict is `disks.severity.worst_used_pct`; `network` deliberately has no severity.
- `tests/test_system_contract.py` (new, 13 tests): drives the real captures through `parse_host_snapshot()` for all three host shapes (discrete GPU / Strix Halo APU / no GPU), checks the paths over HTTP via `/api/system/snapshot`, pins `gpus == []` (not a missing key), `severity: null` for unknowns, absence of `network.severity`, and that dropped visuals (`cpu.usage_pct@coreN`, per-mount, per-interface, sensors, processes) are **still sampled** and `cpu.usage_pct@core7` still queryable from `/api/system/history`.
- `tests/test_live_fleet.py`: added `test_every_host_serves_the_hero_card_contract` — same table against the real agents.
- `src/dashboards/sources/host.py`: **comments only** (card-temp field choice; why `network` has no severity). No field renamed/removed; no page code touched.
- Verified mutation-sensitivity: renaming `worst_mount` or adding `network.severity` fails 10 contract tests.
**Tests / lint / coverage**
- `uv run pytest tests/test_system_contract.py` → 13 passed
- `bash .agents/validate.sh` (pytest `--cov --cov-fail-under=94`, ruff, pyright, node --check, hygiene) → exit 0: **452 passed, 7 skipped**, coverage **96.86%** (`sources/host.py` 99%), ruff clean, pyright 0 errors
- `uv run pytest -m live --live` (fleet + engines + upsd reachable) → **7 passed** (was 6)
- `uv pip list --outdated` → empty; dev server restarted, `/api/health` ok; live `/api/system/snapshot` passes the contract for deskwork/driveripper/gateway
**Docs**: AGENTS.md (test count 439→452 + new testing-policy bullet on the card-contract table) and README.md (Testing section notes `tests/card_contract.py`).
**Decisions / deviations**
- Contract table lives in `tests/card_contract.py` so hermetic and live tests share one list instead of duplicating paths.
- Task text's `disks.worst.severity` is realised as phase 01's sibling form `disks.severity.worst_used_pct` (no nested `disks.worst` object exists); recorded in the test docstring, no payload change.
- Phase-file curl example for per-core history uses `?host=&metric=`, but the API takes `hosts=`/`metrics=`; the test uses the real names (phase file left untouched).
**Next pending task**: `03_system_per_host_panels/02_hero_cards_without_per_core.md`.
@@ -0,0 +1,46 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 47%]
.......sssssss.......................................................... [ 62%]
........................................................................ [ 78%]
........................................................................ [ 94%]
........................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 28
src/dashboards/alerts.py 175 0 100%
src/dashboards/api.py 193 0 100%
src/dashboards/app.py 109 6 94% 50-51, 56, 58, 60, 131
src/dashboards/collectors.py 140 5 96% 92-93, 97-98, 115
src/dashboards/config.py 110 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 139 1 99% 226
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 226
src/dashboards/sources/nut.py 367 22 94% 118, 132, 138, 150, 156, 168, 176, 181, 252-253, 255, 414, 458, 470, 505, 538, 542, 595, 599-600, 605, 617
src/dashboards/sources/nut_sim.py 122 21 83% 141, 167-185, 226
src/dashboards/store.py 178 2 99% 243-244
------------------------------------------------------------------
TOTAL 2104 66 97%
Required test coverage of 94% reached. Total coverage: 96.86%
452 passed, 7 skipped, 1 warning in 41.93s
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
@@ -0,0 +1,21 @@
All green. Task complete.
**Report**
**Implemented**
- `page-system.js`: `hostCard()` rewritten — one `.heroes` row of six slots in fixed order (CPU, MEM, TEMP, DISK, GPU, NET), each with a `data-sev` hook naming its payload verdict; per-core block (`data-k="cores"`, `renderCores`) deleted; Load/Clock/Disk-read/write/busy/Up moved into the collapsed "More detail" `<details>` (with GPU power/temp table, filesystems, processes).
- `renderHostCard()` now drives colour via `Dash.hero()` from `cpu.severity.usage_pct/temp_c`, `memory.severity.used_pct`, `disks.severity.worst_used_pct`, `gpus[].severity.busy_pct`; zero `severityFor(` calls left. MEM shows `%` big + `113.5 / 125 GiB` sub; DISK sub is `worst_mount`; GPU = one hero per card (model, VRAM used/total, APU `pool %` in `.sub`) or uncoloured "No GPU reported by this agent"; NET is one slot, two lines via `Dash.rate()`, `n/a` (never `0 B/s`) when null, colourless by design.
- `dash.css`: added `.heroes` (auto-fit → 3×2/2×3 wrap), `.hero-slot`, `.hero .hero-lines` — no new colour vocabulary, tokens only.
- `app.js`: fallback docstring updated (phases 04–05 remain).
- Tests: 2 new byte-pinned tests in `tests/test_api.py` (six heroes in order, no per-core, no JS thresholds, NET has no verdict, detail keys survive); `test_severity_for_is_kept_as_the_documented_fallback` narrowed to `/llm` + `/ups` (same pattern phase 02 applied to `/`).
**Verification**
- `bash .agents/validate.sh` → exit 0: `454 passed, 7 skipped`, coverage 96.86% (floor 94), ruff clean, pyright 0 errors, `node --check` clean.
- `uv run pytest -m live --live` → 7 passed (real fleet, incl. hero-card contract on all three agents).
- Live browser (dev server restarted, port 8090): `data/logs/system-heroes-live.png`, `-warn-fail-deskwork.png` (91% amber, 95% red GPU), `-nogpu-gateway.png` (TEMP/GPU `n/a` uncoloured), `-driveripper-card.png`, `-more-detail.png`. Colours tracked live threshold crossings (deskwork GPU 95→fail, 94→warn between polls). `cpu.usage_pct@core7` still queryable (355 pts/1h).
**Docs**: AGENTS.md (`web/` fallback status, new UI-conventions paragraph for the six-hero card) and README.md (`web/` bullet) updated.
**Decisions**: `.cores` CSS + `renderCores` in `charts.js` kept (phase sanctions charts.js-only; task 03 confirms); fleet sub-line "cores" kept (core count, not per-core visual); hero-row test scopes the per-core grep to the card functions for the same reason.
**Next pending task**: `03_system_per_host_panels/03_per_host_charts_replace_shared.md`.
@@ -0,0 +1,46 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 15%]
........................................................................ [ 31%]
........................................................................ [ 46%]
.........sssssss........................................................ [ 62%]
........................................................................ [ 78%]
........................................................................ [ 93%]
............................. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 28
src/dashboards/alerts.py 175 0 100%
src/dashboards/api.py 193 0 100%
src/dashboards/app.py 109 6 94% 50-51, 56, 58, 60, 131
src/dashboards/collectors.py 140 5 96% 92-93, 97-98, 115
src/dashboards/config.py 110 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 139 1 99% 226
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 226
src/dashboards/sources/nut.py 367 22 94% 118, 132, 138, 150, 156, 168, 176, 181, 252-253, 255, 414, 458, 470, 505, 538, 542, 595, 599-600, 605, 617
src/dashboards/sources/nut_sim.py 122 21 83% 141, 167-185, 226
src/dashboards/store.py 178 2 99% 243-244
------------------------------------------------------------------
TOTAL 2104 66 97%
Required test coverage of 94% reached. Total coverage: 96.86%
454 passed, 7 skipped, 1 warning in 42.22s
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
@@ -0,0 +1,17 @@
**Task complete** — `03_per_host_charts_replace_shared.md` (phase 03). Working tree only, no git operations.
**Deleted from `/system`**: the fleet totals card, the six fleet-wide charts (`#chart-cpu|mem|disk|net|gpu|temp` + legends), the Series-detail picker (`#detail-host|metric|chart|note`), `<select>`s, `renderCores()` in `charts.js`, and the `select`/`label.field` CSS they alone used. `main` now holds exactly two sections: host panels + events.
**Each host panel** stacks its own 120px charts under the hero row — util (CPU/mem/worst-disk, one 0-100 axis), net (↓in/↑out), GPU busy per card — GPU slot created and destroyed with `host.gpus`, legends naming metrics (or the GPU model) never hosts, torn down by `pruneHosts` when a host stops reporting.
**One request per host per refresh**: `/api/system/history?hosts=<host>&metrics=<that panel's series>&range=…`; measured live at 5 XHRs per poll for 3 hosts (1 snapshot + 3 history + events) regardless of chart count. `/api/metrics` discovery no longer used by the page.
**Tests** (`tests/test_api.py`, +3): `test_system_page_keeps_no_fleet_chart_or_detail_picker` (deleted ids/labels/`<select>` gone, 2 sections), `test_page_system_script_no_longer_references_the_deleted_ui`, `test_every_system_chart_is_drawn_inside_its_own_host_panel` (all 3 `LineChart` constructions take a card/slot `[data-chart]`, scoped host query, GPU metric built from `gpus[].index`, dynamic GPU add+remove, `pruneHosts` drops card and chart state), `test_one_request_per_host_answers_every_per_host_chart` (integration: the exact per-host query returns all 6 series, unknown `gpu.busy_pct@1` returns empty not 404), `test_render_cores_is_gone_and_the_chart_primitives_remain`.
**Live browser verification** (`http://127.0.0.1:8090`, 3 real hosts): no console/page/request errors, no NaN/undefined, 8 charts all inside panels, legends never list two hosts, each range chip re-fires one request per host (verified 5m→30d), and a host dropped from `/api/system/snapshot` lost its panel *and* its charts. Screenshots: `data/logs/per-host-charts-system-dark.png`, `-light.png`.
**Two defects fixed in passing**: `.chart .grid text` had no `fill`, so every y-axis label rendered black (invisible on the dark theme) — now `var(--text-faint)`, confirmed in both themes; and `drawHostChart()` now shades a chart only when one series has points, because three 0-100 area fills stacked into one block and hid the CPU line behind memory.
**Docs**: README `/system` bullet and AGENTS.md "Charts belong to the host they chart" rewritten, including correcting the stale claim that `renderCores` survives as unused library vocabulary.
**Gate**: `bash .agents/validate.sh` exit 0 — 458 passed, 7 skipped, coverage 96.86% (floor 94), ruff clean, pyright 0 errors, `node --check` clean. Phase/task files untouched.
@@ -0,0 +1,46 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 15%]
........................................................................ [ 30%]
........................................................................ [ 46%]
.............sssssss.................................................... [ 61%]
........................................................................ [ 77%]
........................................................................ [ 92%]
................................. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 28
src/dashboards/alerts.py 175 0 100%
src/dashboards/api.py 193 0 100%
src/dashboards/app.py 109 6 94% 50-51, 56, 58, 60, 131
src/dashboards/collectors.py 140 5 96% 92-93, 97-98, 115
src/dashboards/config.py 110 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 139 1 99% 226
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 226
src/dashboards/sources/nut.py 367 22 94% 118, 132, 138, 150, 156, 168, 176, 181, 252-253, 255, 414, 458, 470, 505, 538, 542, 595, 599-600, 605, 617
src/dashboards/sources/nut_sim.py 122 21 83% 141, 167-185, 226
src/dashboards/store.py 178 2 99% 243-244
------------------------------------------------------------------
TOTAL 2104 66 97%
Required test coverage of 94% reached. Total coverage: 96.86%
458 passed, 7 skipped, 1 warning in 43.12s
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
@@ -0,0 +1,15 @@
**Implemented (task 04 — verify every host shape on live `/system`)**
- Browser-verified all three host shapes live: driveripper (R9700, VRAM 30.5/31.9 GiB + pool line), deskwork (APU — VRAM vs `pool 78%` both legible; GPU visibly crossed ok→warn→fail), gateway (GPU/temp `n/a` uncoloured, no GPU chart).
- Failure path proven: stopped only the gateway tunnel → panel kept last-good heroes (nothing zeroed), dot red, `stale 2.1m` text, dimmed; other hosts unaffected; exactly one unreachable + one reachable event; tunnel restored, fleet all-ok.
- **Fixed 2 defects found**: (1) anonymous chart SVGs — `LineChart` gained an `ariaLabel` option (`role="img"` + `aria-label`), all three `/system` slots name host+metrics; (2) `/system` Events feed queried `source=host` while the poller stores transitions under `system` — feed was empty since the first commit; now populated.
- New pins: `test_every_system_chart_names_itself_for_assistive_tech`, `test_system_event_feed_queries_the_source_transitions_are_stored_under` (tests/test_api.py).
- `NOTES-verify.md` written in the phase directory; both themes checked via header toggle; per-core history still queryable (354 points for `cpu.usage_pct@core7`).
- Screenshots opened/read: `system-driveripper.png`, `system-deskwork.png`, `system-gateway.png`, `system-stale-gateway.png`, `system-shapes-dark/light.png`, `system-driveripper-light.png`, `system-gateway-light.png` (all in `data/logs/`).
**Tests / lint**: `bash .agents/validate.sh` → exit 0 — **460 passed, 7 skipped**, coverage **96.86%** (≥94), ruff clean, pyright 0 errors, `node --check` clean · `uv run pytest -m live --live` (with `.env.dev`) → **7 passed** · `uv pip list --outdated` empty.
**Docs**: AGENTS.md (poller event-source trap; chart `ariaLabel` convention), README.md (chart aria naming in the `/system` paragraph).
**Notable**: Playwright defaults to light `prefers-color-scheme` — screenshots now seed `localStorage['dash-theme']`; noted in NOTES-verify.md for later phases. `/llm`/`/ups` charts can adopt the new `ariaLabel` option in phases 04/05 (not touched here).
**Next pending task**: `04_llm_chart_readability` (phase 03 fully complete after this task).
@@ -0,0 +1,46 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 15%]
........................................................................ [ 30%]
........................................................................ [ 46%]
...............sssssss.................................................. [ 61%]
........................................................................ [ 77%]
........................................................................ [ 92%]
................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 28
src/dashboards/alerts.py 175 0 100%
src/dashboards/api.py 193 0 100%
src/dashboards/app.py 109 6 94% 50-51, 56, 58, 60, 131
src/dashboards/collectors.py 140 5 96% 92-93, 97-98, 115
src/dashboards/config.py 110 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 139 1 99% 226
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 226
src/dashboards/sources/nut.py 367 22 94% 118, 132, 138, 150, 156, 168, 176, 181, 252-253, 255, 414, 458, 470, 505, 538, 542, 595, 599-600, 605, 617
src/dashboards/sources/nut_sim.py 122 21 83% 141, 167-185, 226
src/dashboards/store.py 178 2 99% 243-244
------------------------------------------------------------------
TOTAL 2104 66 97%
Required test coverage of 94% reached. Total coverage: 96.86%
460 passed, 7 skipped, 1 warning in 43.42s
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==