Files
local-ci-cd-system/docs/superpowers/specs/2026-07-25-card-consolidation.md
T
Simone 612e37844f 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.
2026-07-25 19:35:44 +02:00

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-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.