docs(web): document the card/executions/template-delete changes

CLAUDE.md's web/app.py summary, WEB-UI.md's dashboard-layout and Executions
sections, and a new design-history spec covering the tile removal, the
compacted Host card, the dropped level-word strip, and the four cards'
shared <dl> grammar.
This commit is contained in:
2026-07-25 19:35:44 +02:00
parent 3ffcf4314b
commit 612e37844f
3 changed files with 217 additions and 16 deletions
@@ -0,0 +1,128 @@
# Dashboard card consolidation — design (2026-07-25)
Collapse the dashboard's redundant first two rows: six status-header tiles plus
four compact cards that restated them. **The cards win; the tile row is
deleted** and each card becomes the single owner of its subject.
## Problem
The rows said the same thing twice:
| Datum | Tile | Card | Also |
|---|---|---|---|
| Runner up/down | `Runners 2/2` | `partials/runners.html` unit table | Services card, *with* start/stop/restart |
| Cache size + entries | `Cache` | `partials/cache.html` | Maintenance card (purge) |
| CPU cores | sub-line `/ N cores` | `partials/host.html` "CPU cores" | — |
| RAM total | sub-line `/ N GB` | `partials/host.html` "Memory" | — |
Runner state was in **three** places at once. Only three readouts were unique
to the tiles — CPU%, RAM used, and the disk usage whose own card Stage E had
already folded away — and only they needed a new home.
## Decision
One card per subject, no header:
| Card | Route | Poll | Owns |
|---|---|---|---|
| Host | `/host` | 5s | hostname, kernel, full CPU model, cores, **CPU% / RAM / disk + gauges**, uptime, load avg |
| VMware modules | `/vmware` | 15s | `vmmon`, `vmnet`, `vmware-host.service` |
| Runners | `/runners` | 5s | each `act-runner*` unit + sub-state |
| Cargo cache | `/cache` | 5s | root, size, entries, oldest, retention |
Retired: `partials/status_header.html` and its `HealthSummary` /
`_health_summary`. The VM count and orphan count went with them — running VMs
are rows in the Executions card and the orphan sweep is a Maintenance action,
so neither needed a numeric readout of its own.
Rejected alternative: keep the tiles as the state layer and merge the four
cards into one "Platform" detail card. It splits every subject across two
places (is the runner state a tile or a card?), and forces the header to start
polling to avoid going stale. One owner per subject is simpler to keep honest.
## What survives from the tiles
`_resource_levels(resources, disk) -> ResourceLevels` keeps the threshold logic
and the `tile-*` class names, now driving the Host card's three gauges:
- CPU / RAM: `> 75%` amber, `> 90%` red.
- Disk: `> 70%` amber, `> 85%` red.
- Any unreadable source: `unknown` (neutral), never a false green.
**Health is never color-only** — the rule the tiles were built around. Every
gauge is followed by `.resource-level`: the level word plus a check/alert icon,
both keyed off the same class the gauge fill uses.
## New data
`HostResources` gains `cpu_model` / `kernel` / `hostname`, read through the
existing `_read_proc` seam but via `_read_proc_optional` — a missing
`/proc/cpuinfo` must cost the CPU *name*, never the CPU *percentage* that
shares the live-metric `try`. Each degrades to `""`, never `None`, so templates
print them unguarded. `cpu_model` is `""` on architectures that don't emit
`model name` (arm64), which is a normal outcome.
## Stage G — height parity across the four cards
Folding the tiles in left the Host card at eleven `<dl>` rows against its
neighbours' two-to-five, and `.dashboard-grid` uses `align-items: stretch`:
VMware, Runners and Cargo cache were stretched to Host's height and padded with
~200px of empty card. Same data, packed to five rows:
| Was | Now |
|---|---|
| `Hostname`, `Kernel` (2 rows) | `System` — `ci-host · 7.0.0-28-generic` |
| `CPU` (full model), `Cores` (2 rows) | `CPU` — `Intel Core i9-13900K · 16 cores` |
| `Uptime` (1 row) | `.head-meta` at the right end of the card header |
| `Load avg` (1 row) | dim sub-line under the CPU gauge |
| level strip (word + check icon under each gauge) | the word moved into the gauge's `aria-label` |
Nothing is dropped and no new grammar is introduced — the card is still a
`<dl>`, so it still reads like Cargo cache next to it.
Three details the shape depends on:
- **`.head-meta` must reset `text-transform`.** `.card h2` uppercases; an
unreset header meta renders `26H 03M`. `.head-select` (the Executions
row-count selector) already had to do this — same fix, same header slot.
- **Uptime is spelled out** (`uptime 26h 03m`). Outside the `<dl>` it has no
`<dt>`, and a bare duration in a header names nothing.
- **Dropping the level strip does not make health color-only**, which is why
it could go. The rule the tiles were built around was written when a tile
showed a coloured bar and little else; the card states the number in text
above every bar, so `42%` versus `92%` is already readable without seeing a
colour. The word is kept for screen readers via `aria-label`, and the disk
row prints `levels.disk_label` as its value when the probe fails and there
is no gauge to attach it to.
- **The CPU model is trimmed** through `cpu_short` — the raw ~40-char name
wraps to three lines in a quarter-width track, which is the height being
reclaimed. Full name on the row's `title`. This is the filter's first
template caller: it was written for a status tile that was retired before it
ever shipped.
## Stage H — one grammar for all four cards
Host and Cargo cache were always a `<dl>` at the page's plain 15px body text.
VMware and Runners were a bordered `<table>` at `0.9rem` (13.5px) with
uppercase `0.75rem` (11.25px) column headers — two font scales and two visual
grammars in the same row of four cards. Converted VMware and Runners to the
same `<dl>`: component/unit name as `<dt>`, the status badge plus its
sub-state sharing one `<dd>` (the same "value + dim sub-text" shape Host's
Disk row and Runners' own unit rows already used). No table, no header row,
no per-row color tint (`tr.row-alert` dropped along with the `<tr>` it
targeted — the badge already carries the color, so the row no longer
double-encodes it, matching how Host and Cargo cache never tinted a whole
row either).
## Tests
`tests/python/test_web_cards.py`: the `_resource_levels` threshold matrix, each
card's contents (including the three Stage F additions and the three readouts
inherited from the tiles), the "every gauge has a text level" rule, and the
absence of any status header. Stage G pins the packed shape: the paired
`System` / `CPU` rows (and the absence of the four `<dt>`s they replaced),
`count("<dt>") == 5`, uptime in `.head-meta` and not in a `<dt>`, the load
average with no `<dt>` of its own, the trimmed-model-plus-`title` pair, and the
level word present in each gauge's `aria-label` while `resource-level` is
absent from the markup — `id="status-header"` and `stat-tile` must not
appear in the dashboard, and `/status-header` / `/platform` must 404.