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.
6.4 KiB
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-metamust resettext-transform..card h2uppercases; an unreset header meta renders26H 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%versus92%is already readable without seeing a colour. The word is kept for screen readers viaaria-label, and the disk row printslevels.disk_labelas 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'stitle. 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.