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
+23 -12
View File
@@ -192,7 +192,9 @@ CA** block (CN, serial, expiry as a local timestamp *and* as time left, and **Ro
the **Dashboard client** line (the one certificate every agent verifies the dial against —
display-only, because it is minted with the root and has no route of its own), and one row per
host with its status chip, serial, expiry, issued-vs-deployed bundle revision, the addresses its
certificate is valid for, and **Rotate** / **Revoke** / **bundle**.
certificate is valid for, and **Rotate** / **Revoke** / **bundle**. The agents table carries the
stylesheet's own `scroll` modifier, so at phone width its right-hand columns — the serial, the
bundle revisions, the addresses and the controls — are reachable by scrolling instead of clipped.
Nothing there is coloured: `severity.py` rates no certificate, so an expiry is a date and a
bundle revision is a count. The one verdict on the page is the API's `status` word (`active` green,
@@ -491,7 +493,7 @@ documentation and is never loaded, and `scripts/dev-server.sh` exports `.env.dev
| `DASH_SESSION_HOURS` | session lifetime (default `720` = 30 days) |
| `DASH_COOKIE_SECURE` | `1` marks the session cookie `Secure`; off because dev is `http://127.0.0.1`, and a request that arrived over `X-Forwarded-Proto: https` is treated as secure either way |
| `DASH_PBKDF2_ITERATIONS` | verifier cost (default `600000`). The knob exists for the test suite; leave it alone in a deployment. Raising it on a real box does take effect: the next successful sign-in re-hashes a cheaper stored verifier and keeps the signing key |
| `POLL_MS_SYSTEM` / `POLL_MS_LLM` / `POLL_MS_UPS` | how often the *browser* refetches each page (10 s / 2 s / 5 s) — shown in the page footer. `/disks` has no `POLL_MS_*` of its own and polls at a fixed 30 s: its payload only moves when the `disks` job re-reads the agents |
| `POLL_MS_SYSTEM` / `POLL_MS_LLM` / `POLL_MS_UPS` | how often the *browser* refetches each page (10 s / 2 s / 5 s) — shown in the page footer. `/disks` has no `POLL_MS_*` of its own and polls at a fixed 30 s: its payload only moves when the `disks` job re-reads the agents. A backgrounded tab idles its polls and fires one on return; nothing an operator presses stops them (phase 21) |
| `DASH_RELOAD` | `1` makes `uv run dashboards` watch the source and restart on a change — development only, and every poll loop restarts with it |
| `DEBUGPY` / `DEBUGPY_PORT` | `DEBUGPY=1` opens a debugpy listener (default 5678) without blocking startup |
| `RETAIN_RAW_HOURS` / `RETAIN_MINUTE_DAYS` / `RETAIN_HOUR_DAYS` | history rollup and pruning |
@@ -644,12 +646,16 @@ certs.py ─ root CA + client cert, minted once at startup (unreadable row → n
SVG charts (`charts.js`), and a masthead status dot per dashboard job (`disks` has its own:
the same agent can answer the 10-second utilisation poll and have a wedged SMART pass).
The masthead is **three left-justified rows at every width**: the brand and the five dashboard
tabs, then the per-source indicators on their own row, then the clock / `⏸ Pause` (on the four
pages that poll) / `⚷ Certs` / `⚙ Account` / `☾ Dark` controls on a row under those (a page with no
tabs, then the per-source indicators on their own row, then the clock / `⚷ Certs` / `⚙ Account`
/ `☾ Dark` controls on a row under those (a page with no
indicators — `/login`, `/settings`, `/certs` — renders two, and a control is never hidden to make a
narrower width work). Both admin links are on all eight
signed-in pages, the two pages they name included: the cluster is the same wherever you are, and a
link to the page you are already on is a harmless no-op rather than chrome that vanishes. The bar is **not sticky**: it scrolls away
narrower width work). There is no control that stops the polling (phase 21): the bar used to carry
a `⏸ Pause` button with a `Space` shortcut, and it left the numbers on screen looking current with
no `stale` label on them. Both admin links are on all seven
signed-in pages — the five dashboards plus `/settings` and `/certs`, the two they name included —
and not on `/login`, the eighth page and the only anonymous one: the cluster is the same wherever
you are, and a link to the page you are already on is a harmless no-op rather than chrome that
vanishes. The bar is **not sticky**: it scrolls away
with the page, so every pixel of height goes to the data (it stays a positioned element, which is
what keeps the mobile dropdown anchored under it). At ≤640px the five tab links leave the bar
and come back as a dropdown under the whole of it, opened by the `☰` button (`/login` renders no
@@ -674,11 +680,16 @@ certs.py ─ root CA + client cert, minted once at startup (unreadable row → n
charging, dashed outline on battery, a dash through the body when there is no reading) and a
`<title>` / `aria-label` that names it ("apc: Charge 100 percent, on line") unless the number
beside it already does. Each `/system` host is one panel led
by exactly six of them — CPU %, MEM %, TEMP, DISK % (mount underneath), GPU busy (VRAM, the
APU pool and the card's temperature in the footnote — no temperature is printed when the
driver exposes no sensor, never `0 °C`; and likewise never coloured and with no bar: a busy
GPU is the job on an LLM server), and NET on two lines (↓ in / ↑ out, never coloured: a byte
rate has no good/bad). Under the numbers, **each panel charts only its own host** — behind a
by six hero slots — CPU %, MEM %, TEMP, DISK % (mount underneath), GPU busy, and NET on two
lines (↓ in / ↑ out, never coloured: a byte rate has no good/bad). A host with **no GPU**
renders **five**: the GPU slot is omitted rather than filled with `n/a`, because a router is
not broken for the card it never had. A GPU is one busy hero plus **four labelled values**
under it — **Model**, **VRAM** (`used / total`), **Pool** (the honest APU view) and **Temp**
— instead of the dense `model · VRAM · pool · °C` footnote that was 11px mono; a field the
agent did not publish prints `n/a` in its own cell, never `0 °C`, and nothing in the slot is
coloured or barred (a busy GPU is the job on an LLM server). Missing *numbers* still print
`n/a` uncoloured — gateway's CPU temp — which is a different fact from a missing *device*.
Under the numbers, **each panel charts only its own host** — behind a
collapsed **Charts** toggle, because the default view is current stats and a fleet of ten
hosts each drawing three plots is noise: the summary reads `▸ Charts` with the window in it
(`1h`, `24h` …), and opening it is what builds and paints the charts — instantly, from the