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`.
This commit is contained in:
2026-10-21 01:44:52 -04:00
parent 973b7299fe
commit 5e267737dc
36 changed files with 830 additions and 139 deletions
+54 -27
View File
@@ -710,10 +710,11 @@ Responsibilities, one line each:
15's "no self-link" precedent: chrome that changes shape depending on where you already are reads
as a broken button, while a link to the page you are on is a no-op that answers with the page you
were looking at (`data/logs/20-task04-admin-links.cjs` measures exactly that — one document
response, no redirect, the same cluster afterwards). The two links therefore sit on all eight
signed-in pages in one order (clock → `⚷ Certs` → `⚙ Account` → theme toggle), which is what lets
the pinned audit be one loop over all eight instead of two over different tuples; `/login` stays
the one page with neither. Everything on it is
response, no redirect, the same cluster afterwards). The two links therefore sit on all seven
signed-in pages (the five dashboards plus `/settings` and `/certs`) in one order
(clock → `⚷ Certs` → `⚙ Account` → theme toggle), which is what lets
the pinned audit be one loop over `MARKUP_PAGES` instead of two over different tuples; `/login`
is the eighth page and the one with neither. Everything on it is
`GET /api/certs`: the Root CA block, the display-only Dashboard client line (that certificate is
minted with the root and has no route of its own), and one row per host with its status chip,
serial, expiry (local timestamp *and* `Dash.duration` left), issued-vs-deployed `bundle_rev`,
@@ -731,7 +732,12 @@ Responsibilities, one line each:
names every payload key it indexes, and `tests/test_api.py` resolves that table against a live
`GET /api/certs` row in both directions (the `SLOT_FIELDS` device), so a rename on either side
fails a test instead of printing `n/a` beside a healthy fleet. The two CNs are markup, and pinned
to `certs.ROOT_CN`/`certs.CLIENT_CN` the way `/settings` pins its password floor.
to `certs.ROOT_CN`/`certs.CLIENT_CN` the way `/settings` pins its password floor. The agents
table is `table.data.scroll` — the repo's one scroll mechanism (`TODO.md:9`: at phone width the
serial / bundle / `san` / controls tail was clipped by `.card { overflow: hidden }` with no way
to reach it), and the class on the served table and the served `table.data.scroll` rule are
pinned **together** in `tests/test_api.py`, because the mechanism exists only while both name
it (measured at 360 px and 1280 px by `data/logs/21-task03-scroll.cjs`).
`app.js`'s masthead status dots are one per dashboard job —
including `disks`, because the same agent can answer the 10-second utilisation poll and
have a wedged SMART pass, and one dot for both would be right about neither. `app.js` owns the
@@ -748,7 +754,11 @@ Responsibilities, one line each:
gets a row in `severity.py`, never a bound in JS.
**The masthead is three left-justified rows at every width** (phase 20 task 01, `TODO.md:5-6`):
row 1 the brand, `#nav-toggle` and the five tabs; row 2 the `#nav-status` per-source dots; row 3
the `.masthead-right` controls (clock, `⏸ Pause`, `⚷ Certs`, `⚙ Account`, theme). That is CSS and
the `.masthead-right` controls (clock, `⚷ Certs`, `⚙ Account`, theme — phase 21 task 01 deleted
the `⏸ Pause` that used to sit between the clock and the two links, button, `Space` shortcut and
`setPaused`/`_paused` plumbing alike: it froze the numbers on screen without growing a `stale`
label, and a backgrounded tab already idles its polls, which is the only stop this app needs).
That is CSS and
no markup change, which decides the mechanism: `#nav-status` is a **child** of
`.masthead-right`, so there is no wrapper to promote, and the rows are line breaks — `.masthead`
is `flex-wrap: wrap`, and both `#nav-status` and `.masthead-right` carry `flex-basis: 100%` with
@@ -1064,7 +1074,7 @@ set -a; . ./.env.dev; set +a # dev dials the fleet the prod way, no
./scripts/dev-server.sh status # per-source ok/DOWN — unauthenticated: /api/health
./scripts/dev-server.sh logs
uv run pytest # hermetic: 1464 passed + 15 skipped (11 live,
uv run pytest # hermetic: 1471 passed + 15 skipped (11 live,
# 4 store_perf), no network
uv run pytest --cov # coverage gate (98.94%; floor 94%)
uv run pytest -m live --live # opt-in: real fleet / engines / upsd
@@ -1480,24 +1490,41 @@ body moved inside a disclosure stops being padded until something says so — wh
and `.events-pager` are class-based and needed no new rule to keep the 320px scroll and the
`1–50 of N` strip working inside it.
The `/system` host card is one row of six heroes — CPU, MEM, TEMP, DISK, GPU, NET (the
The `/system` host card is one row of six hero slots — CPU, MEM, TEMP, DISK, GPU, NET (the
`.heroes` grid; `tests/test_api.py` pins the slots and their order on the served script).
Colour comes from the payload's `severity` dicts and nowhere else; a missing number or
verdict — gateway's CPU temp, any host's absent GPU — prints `n/a` **uncoloured**. NET is
one slot with two lines (`.hero-lines`, `↓` in / `↑` out) and is deliberately colourless:
throughput has no good/bad, so `network` carries no severity. The GPU slot holds one hero
per card whose `.sub` carries model, VRAM used/total, the honest APU `pool` view and the
card's temperature (`gpus[].temp_c` — the agent's max over that card's hwmon labels, the
same value stored as `gpu.temp_c@N`, so the hero and the history cannot disagree), and it is
colourless for the same reason (phase 06): a 95 %-busy card on an LLM server is the job, so
the hero carries no `severity` and no utilisation bar — a bar under a verdictless number
would be a verdict by shape. A card whose driver exposes no temperature input prints **no**
temperature in that footnote rather than `n/a` or `0 °C`: "where you can" is the operator's
wording, and `n/a` is reserved for numbers the card is supposed to have. Everything not
glance-worthy — load averages, clock, disk read/write/busy, uptime, GPU power and the
per-label temp breakdown, filesystems, processes — folds into a collapsed "More detail"
`<details>`: a simpler surface, not a less complete page (the table keeps its Temp column;
this was a promotion, not a de-duplication).
Colour comes from the payload's `severity` dicts and nowhere else, and a missing **number or
verdict** prints `n/a` **uncoloured** — gateway's CPU temp, a driver with no temperature
input, a rate with no second sample yet. What a machine does not **have** is a different
fact and prints nothing at all: the GPU slot ships `hidden` and `renderGpuSlot()` un-hides
it only for a host whose payload carries a card, so gateway's card is **five** heroes with
no GPU slot in the row (`TODO.md:12` — "Boxes without a GPU should not report N/A but
instead should omit these values"), and the sheet's `[hidden] { display: none !important }`
is what takes the wrapper out of the grid so the row reflows instead of leaving a wide gap.
The two rules are one sentence each in the page and one test each; collapsing them into "any
missing thing is `n/a`" is what made a router look broken over the GPU it never had
(invariant 3). NET is one slot with two lines (`.hero-lines`, `↓` in / `↑` out) and is
deliberately colourless: throughput has no good/bad, so `network` carries no severity.
The GPU slot is one busy hero per card plus **four labelled values** under it — **Model**
(mono, `gpus[].model`), **VRAM** (`used / total`), **Pool** (`mem_used_pct`, the honest APU
view a Strix Halo needs because its model lives in GTT) and **Temp** (`gpus[].temp_c` — the
agent's max over that card's hwmon labels, the same value stored as `gpu.temp_c@N`, so the
cell and the history cannot disagree; the per-label breakdown stays in "More detail"). They
replaced one ` · `-joined 11px `.sub` — `TODO.md:12`: "`Radeon 8060S Graphics · VRAM 180.2
MiB / 512.0 MiB · pool 79% · 69 °C` is too much info for such small text and should be its
own values" — and they are `.metric` cells in a `.gpu-values` grid, so a 20-character model
string wraps instead of truncating. The four cells come from one `GPU_VALUE_FIELDS` table,
which `tests/test_api.py` resolves against every capture **and** against the served
`/api/system/snapshot` (`SLOT_FIELDS`' device): a rename on either side fails a test instead
of printing `n/a` beside a healthy fleet. A field the agent did not publish prints `n/a` in
its own cell (the card is real, so it is a number the card is supposed to have) and never
`0 °C`. Nothing in this slot is coloured and nothing draws a bar — no `severity`, no
`ok|warn|fail` token in the markup or the grid's rules (phase 06: a 95 %-busy card on an LLM
server is the job, and a bar under a verdictless number would be a verdict by shape).
Everything not glance-worthy — load averages, clock, disk read/write/busy, uptime, GPU power
and the per-label temp breakdown, filesystems, processes — folds into a collapsed "More
detail" `<details>`: a simpler surface, not a less complete page (the table keeps its Temp
column; this was a promotion, not a de-duplication).
A `Δ<window>` label is a measurement, never a config value: `sources/host.py` sends the window
the estimator actually used as the payload's `window_seconds`, and `/system`'s footnote prints
@@ -1601,8 +1628,8 @@ transition events — and nothing else: the fleet-wide chart stack, the nine-fig
panel and the host/metric detail picker are deleted, along with the `#chart-*` / `#detail-*` ids and
the `select` / `label.field` styling only those pickers used. Each panel stacks its own
`.host-charts` — utilisation (CPU, memory, worst disk), network (in/out), then GPU busy — where
the GPU slot is created and destroyed with `host.gpus`, so gateway shows neither a phantom card
nor a hole. **That stack is collapsed behind a per-host `Charts` `<details>`** (phase 06: "only
the GPU **chart** slot is created and destroyed with `host.gpus` (its own thing from the hero
slot above it, which is hidden instead), so gateway shows neither a phantom card nor a hole. **That stack is collapsed behind a per-host `Charts` `<details>`** (phase 06: "only
visible if expanded… there will be lots of hosts"): the default `/system` view is the hero row,
the summary carries the window label the stack will draw (the chips themselves stay in the page
head, one control per page), and `drawHostCharts()` is the single place that decides whether to
@@ -2025,7 +2052,7 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
shape applied to one table and does not reach this one. Of the two operator TODO items that sat
beside it, `20_masthead_nav_polish` has landed in full (now complete): the bar is three
left-justified rows at every width, it is no longer sticky, the phone menu slides, and both
`⚙ Account` and `⚷ Certs` render on all eight signed-in pages including the two they name
`⚙ Account` and `⚷ Certs` render on all seven signed-in pages including the two they name
(task 04, reversing phase 15's "no self-link" rule at the operator's request). The other,
`24_dev_mtls_rollout` (never accepting a non-mTLS agent — fleet gap 11's rollout, half of it),
is still in `todo/`.