Files
ducoterra 5e267737dc phase: 21_page_cleanups
**Phase 21 final verification pass** (all 3 task files already in `complete/`, working tree carries the implementation)

- **Verified** task 01: `#pause`/`Space`/`setPaused`/`_paused` gone — `grep -ri pause src/dashboards/web/` empty, 0 pause hits in all 8 served pages + 10 static assets; re-ran `data/logs/21-task01-nopause.cjs` → 30/30 PASS (hidden tab: 0 data fetches in 22 s, one poll 2 ms after return).
- **Verified** task 02: re-ran `21-task02-gpu-values.cjs` on the live fleet → all PASS (deskwork/driveripper show Model/VRAM/Pool/Temp at 16.3 px; gateway = 5 heroes, slot `hidden`, no `n/a`; hero order unchanged; GPU history chart still builds per host).
- **Verified** task 03: re-ran `21-task03-scroll.cjs` → 14/14 PASS (`class="data scroll"`, scrollWidth 1021 vs clientWidth 292 at 360 px, Rotate/Revoke/bundle reachable and hit-testable; unchanged at 1280 px). Screenshots refreshed in `data/logs/`.
- **Gate**: `uv run pytest` → **1471 passed, 15 skipped**; `--cov --cov-fail-under=94` → **98.94%**; `ruff check src tests agent` clean; `pyright` **0 errors**; `bash .agents/validate.sh` → **EXIT 0** (incl. `node --check`); `uv run pytest -m live --live` → **11 passed**; `/api/health` all 9 sources `ok`.
- **Docs fixed** (pre-existing + this phase): "all **eight** signed-in pages" was a miscount of `MARKUP_PAGES` (7 = 5 dashboards + `/settings` + `/certs`; `/login` is the 8th, unsigned) → corrected in `README.md`, `AGENTS.md` (×2) and the `tests/test_api.py` docstring/comment; `AGENTS.md` Commands hermetic tally 1470 → **1471**.
- **Docs reviewed, consistent**: Quickstart/commands/flags (`install-agent.sh`, `capture-fixtures.sh`, `dev-server.sh`), the env table vs `config.py`, every documented route vs the served route table, masthead/`/system` GPU/`/certs` scroll descriptions vs code, and all `data/logs/*` + `tests/fixtures/*` references.
- **Deviation**: `validate.sh`'s advisory `fastapi 0.153.0 → 0.156.4` left alone — no dependency was touched this phase, so AGENTS rule 8 ("check after touching dependencies") is not triggered; upgrading here would be an unrelated behaviour change.
- No live-infra changes made (repo-side + browser verification only).
- **Next pending phase**: `22_disks_hide_unconfigured`.
2026-10-21 01:44:52 -04:00

122 lines
6.5 KiB
Python

"""The `/system` hero-card payload contract, shared by the hermetic and the live tests.
Phase 03 makes `/system` one panel per host with six big colour-coded numbers. The paths
below are the payload fields those cards are allowed to read; they were read out of
`parse_host_snapshot()` and are pinned hermetically in `tests/test_system_contract.py`
(driven from `tests/fixtures/agent_*.json`) and against the real fleet in
`tests/test_live_fleet.py::test_every_host_serves_the_hero_card_contract`.
Keeping the list here rather than in one test file means the live assertions cannot drift
away from the fixture assertions: there is one table, and it is the thing under test.
=========================== ==========================================================
Card Path in `/api/system/snapshot` `hosts[]`
=========================== ==========================================================
cpu % ``cpu.usage_pct`` (+ ``cpu.severity.usage_pct``)
mem % ``memory.used_pct`` (+ ``memory.severity.used_pct``)
cpu temp ``cpu.temp_c`` (+ ``cpu.severity.temp_c``)
disk usage % ``disks.worst_used_pct`` + ``disks.worst_mount`` (+ verdict)
graphics card ``gpus[].model/busy_pct/vram_*/mem_used_pct/temp_c`` (no verdict)
network down/up ``network.rx_bytes_per_s`` / ``tx_bytes_per_s`` (no verdict)
=========================== ==========================================================
CPU temp comes from ``cpu.temp_c`` rather than ``temperatures.cpu_c``: both are the agent's
own ``cpu_temperature_celsius``, and ``cpu.temp_c`` is the field ``/api/summary`` flattens
into its fleet row, so the card and the overview cannot drift apart.
Deliberately *not* card metrics (legibility, not missing data — the series stay in SQLite):
per-core usage (``cpu.usage_pct@coreN``), swap, the process table, the raw sensor list, and
per-interface rows. Network carries no ``severity`` on purpose; throughput has no good/bad,
so ``severity.THRESHOLDS`` has no ``net.*`` row. GPU rows carry no ``severity`` either
(phase 06): these are LLM servers, a 95 %-busy card is the job and not a fault, so
``THRESHOLDS`` has no ``gpu.*`` row and the card prints its numbers uncoloured at every
utilisation — the *absence* of the dict is pinned below, the way ``network``'s is.
``gpus[].temp_c`` is on the card because the number worth reading *while* a card is busy is
how hot it is (phase 06, TODO.md L9). It is the agent's **max over that card's hwmon
labels** — the same max the agent already computes and the same value stored as
``gpu.temp_c@N`` — so the card prints a measurement rather than a new field, and the value
may honestly be ``None`` on a box whose driver exposes no temperature input (phase 21 task
02 made it one of the card's four labelled values, and the GPU-less host that has no card at
all prints no values at all — see ``GPU_VALUE_CELLS`` in ``tests/test_api.py``).
"""
from __future__ import annotations
from typing import Any
#: Card metric -> dotted payload paths. Present is the contract; a value may be ``None``
#: on a host that genuinely does not have the number (gateway reports no CPU temperature).
CARD_METRICS: dict[str, tuple[str, ...]] = {
"cpu %": ("cpu.usage_pct", "cpu.severity.usage_pct"),
"mem %": ("memory.used_pct", "memory.severity.used_pct"),
"cpu temp": ("cpu.temp_c", "cpu.severity.temp_c", "temperatures.cpu_c"),
# the mount is what makes the percentage actionable: 88 % used — of which disk?
"disk %": ("disks.worst_used_pct", "disks.worst_mount", "disks.severity.worst_used_pct"),
"network down/up": ("network.rx_bytes_per_s", "network.tx_bytes_per_s"),
}
#: Per-GPU paths, checked on every row of `gpus[]` — and on none of them when the list is
#: empty, which is how a host without a GPU stays honest (no card, not a card at 0 % busy).
GPU_METRICS: tuple[str, ...] = (
"model",
"busy_pct",
"vram_used_bytes",
"vram_total_bytes",
"vram_used_pct",
# the honest APU view: vram + GTT, because a Strix Halo keeps the model in GTT
"mem_used_pct",
# the Temp cell of the card's four labelled values (phase 21 task 02): the agent's max
# over this card's hwmon labels. None is legal — a driver with no temperature input is
# `n/a` in that cell and must never be rendered as 0 °C.
"temp_c",
)
#: `network` must never grow a `severity` dict (see the module docstring).
SEVERITY_FREE_SECTIONS: tuple[str, ...] = ("network",)
#: The host shapes the captures in `tests/fixtures/` cover, and therefore the shapes a card
#: must handle: a discrete GPU, an APU (VRAM *and* GTT), and no GPU at all. A new capture
#: has to be added here — `test_the_contract_covers_every_captured_host` fails otherwise.
HOST_SHAPES: tuple[str, ...] = ("driveripper", "deskwork", "gateway")
def dig(node: Any, path: str) -> Any:
"""Follow a dotted payload path, failing with the path in the message.
A missing key is a contract break; a ``None`` value is not (that renders `n/a`), which is
why this returns the value instead of asserting on it.
"""
walked = ""
for part in path.split("."):
assert isinstance(node, dict), f"{path}: hit a non-dict at {walked or 'payload'}"
assert part in node, (
f"{path}: no key {part!r} under {walked or 'payload'} — a hero card would print "
f"n/a for a number the API used to serve"
)
node = node[part]
walked = f"{walked}.{part}" if walked else part
return node
def assert_card_contract(payload: Any) -> None:
"""Every card path exists on this host payload, GPU rows included."""
for paths in CARD_METRICS.values():
for path in paths:
dig(payload, path)
gpus = payload.get("gpus")
assert isinstance(gpus, list), "gpus must be a list — [] when the agent reports no GPU"
for gpu in gpus:
for path in GPU_METRICS:
dig(gpu, path)
assert "severity" not in gpu, (
"gpus[].severity would grade utilisation on a fleet built to be busy — "
"severity.THRESHOLDS has no gpu.* row and a busy GPU is the job, not a fault"
)
for section in SEVERITY_FREE_SECTIONS:
block = payload.get(section)
assert isinstance(block, dict), f"{section} must be a dict"
assert "severity" not in block, (
f"{section}.severity would be a colour nobody measured — throughput has no "
f"good/bad and severity.THRESHOLDS has no {section}.* row"
)