phase: 27_mobile_tables_and_audit

The audit re-ran green (exit 0). Now checking the docs for defects introduced by the phase.
This commit is contained in:
2026-10-21 23:43:25 -04:00
parent 9ff3c7889b
commit 5d37444fb0
24 changed files with 772 additions and 24 deletions
+156 -18
View File
@@ -800,11 +800,15 @@ Responsibilities, one line each:
`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. The agents
table is `table.data.scroll` — the repo's one scroll mechanism (`TODO.md:9`: at phone width the
table is `table.data.scroll` — the class-level 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`).
it (measured at 360 px and 1280 px by `data/logs/21-task03-scroll.cjs`). Since phase 27 task 01
it is no longer the only thing that scrolls — at ≤640px every `table.data` does — and `.scroll`
out-ranks that rule (two classes to one), so this table keeps its both-axis box and its 320 px
height cap on a phone exactly as on a desktop (both halves measured at both widths by
`data/logs/27-task01-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
@@ -1229,7 +1233,7 @@ uv run python scripts/dev-certs-export.py --url http://127.0.0.1:8090 \
# GET /api/certs/<name>/bundle and writes
# data/certs/<name>.bundle at 0600 — mints nothing
uv run pytest # hermetic: 1545 passed + 15 skipped (11 live,
uv run pytest # hermetic: 1549 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
@@ -1582,6 +1586,54 @@ rules a removed panel made dead (`.grid.wide` went with the overview's fleet+eve
component vocabulary the pages are still being converted to (`.hero-bar`, the grid and bar
variants) stays even while only one page uses it.
**A wide table scrolls sideways inside its own card, and only inside its card** (phase 27 task 01,
`TODO.md:7`). At ≤640px `table.data` is `display: block; overflow-x: auto` — no `max-height`,
because `.scroll`'s 320 px cap is that modifier's vertical-use contract and a phone does not need
a height-capped table on top of a horizontal one — and `.body.stack > *` carries `min-width: 0`,
without which the first rule is inert: a card body is a one-column grid, a grid item's automatic
minimum size is its content's min-content width, and every cell is `white-space: nowrap`, so a
wide table grew the column to 520 px inside a 320 px card instead of overflowing its own box
(measured on the live `/system` disclosures, where `scrollWidth === clientWidth` said there was
nothing to scroll). One class rule reaches all seven tables — `#fleet-table`, `#agents-table`, the
four `page-system.js` tables inside "More detail" and the NUT-variables table in `page-ups.js` —
which is why no table carries a hand-placed `.scroll`; `tests/test_api.py` therefore pins the pair
in the served ≤640px block, pins the all-widths `table.data`/`table.data.scroll` rules unchanged,
and pins that every `<table>` on every served page and in every page script still carries `data`
(a table outside the selector would clip again in silence). The scroll container is the table,
never `main`: phase 18's "the document does not scroll sideways at a phone width" still holds, and
`data/logs/27-task01-scroll.cjs` measures both halves at 360 px and 1280 px.
**Nothing is allowed to be painted past a card edge and eaten** (phase 27 task 03, the audit's
findings). `.card { overflow: hidden }` is what makes three separate layout facts fatal, and the
three fixes are one declaration each — all of them *at every width*, because a clipped fact is a
bug at 1280 px too and each is inert where the content already fits:
- **`.card > header { flex-wrap: wrap }`** (+ `min-width: 0; max-width: 100%` on the header's `h3`
and chips, `white-space: normal` on the chip). A header is a flex line of `nowrap` facts, and a
line that cannot fit *shrinks* its one shrinkable child instead of wrapping: measured at 1280 px
`/system`'s `.updated` stamp was squeezed to 67 px and rendered `updated` / `4s` on two torn
lines, while at 360 px the OS/model chip pushed the stamp 18-191 px past the card so the age of
every number disappeared. Wrapping costs lines (74 px instead of 62 px on a host header, 104 px
on the `apc` card at 360 px) and costs no content. `.chip` itself keeps `white-space: nowrap` —
this is an exception for header chips, not a change to the chip vocabulary.
- **`.card > .body.stack > details { overflow-x: auto }`**. A disclosure is a stack item whose
automatic minimum size is its content's min-content width, so one wide table grew the whole item
to 520 px inside a 404 px card at 1280 px — and because the card clips, the GPU table's last
column *and* the `charts-box` item stretched to the same 520 px were painted under the next card
(tail right 554 px, next card at 438 px), reachable by no gesture. The disclosure is the
scroller rather than the table because hoisting task 01's `display: block` would make a
block-level table shrink-to-fit, un-stretching every table that fits its card.
- task 01's `table.data` / `.body.stack > *` pair at ≤640px, unchanged.
The pair is pinned on served bytes by
`tests/test_api.py::test_a_card_header_wraps_its_facts_instead_of_losing_its_tail` and
`::test_a_card_disclosure_scrolls_what_it_reveals_at_every_width` (the second also demands no
`max-height`/`overflow-y`/`overflow` shorthand — a height cap is `.scroll`'s contract — and that
the rule is *not* inside the ≤640px block, since the defect it repairs is at the control width);
`data/logs/27-task03-verify.cjs` measures both fixes and states what each one costs, and
`data/logs/27-task03-control.cjs` re-creates every first-run offender with the declarations
neutralised, so neither rule is decorative and the audit's reachability check is not vacuous.
A big number is `Dash.hero()` on `.hero` / `.hero-bar` (`dash.css`): 2.4rem tabular numerals,
muted unit, mono `.sub` footnote (`Δ2.0s` on `/llm`'s rates since phase 23, "at the current load"
on `/ups`), coloured by the payload's `severity` through the existing `.ok` / `.warn` / `.fail`
@@ -2428,21 +2480,107 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
this gap. What the fleet now *never* does is answer an agent over plain `http` or accept a
bearer token on the certificate path — the TODO's "the client should never allow non-mtls
communication" is the shipped state, and a new host has to be registered the same way.
12. **The nav collapses on a phone; the page content below it does not** (phase 18's verification
pass, 2026-10-20). At 360px `/`'s fleet table measures 450-510 px inside a 320 px
`section.card`, and `.card { overflow: hidden }` clips the tail columns (`WORST DISK`,
`GPU BUSY`, `TEMP`, `UP`) with no scroll container to reach them — `table.data` only scrolls
with its `.scroll` modifier, which that table does not carry. Measured as pre-existing by
serving the `git show HEAD:` bytes of both `dash.css` and `pages/index.html` over a live
session: same clipping (a clipped card never un-clips when the document scrolls sideways).
What phase 18 did change is that the document no longer scrolls sideways at a phone width at
all — 360 px where it was 750 px — because the bar that overflowed its fixed 58 px row now
breaks into rows, so nothing is lost and nothing gained: the masthead is complete at 360 px and
the table tail is still out of reach. The fix is a responsive-table pass (`.scroll` on the wide
tables, or a per-host card layout at ≤640px) and it touches pinned page shape on `/`, so it is
a phase rather than a cleanup — `21_page_cleanups`' `.scroll` on the `/certs` table is the same
shape applied to one table and does not reach this one; `27_mobile_tables_and_audit` is that
pass, still in `todo/`. Of the two operator TODO items that sat
12. **Closed: the nav collapses on a phone, every table scrolls inside its own card, and no card
edge eats a fact** (the clipping is phase 18's verification measurement of 2026-10-20; the
phone half was closed by `27_mobile_tables_and_audit` task 01 and everything the audit found
after it by task 03, both on 2026-10-21).
What was wrong: at 360px `/`'s fleet table measured 450-510 px inside a 320 px `section.card`,
and `.card { overflow: hidden }` clipped the tail columns (`WORST DISK`, `GPU BUSY`, `TEMP`,
`UP`) with no scroll container to reach them — `table.data` scrolled only with its `.scroll`
modifier, which that table does not carry. It was measured as pre-existing by serving the
`git show HEAD:` bytes of both `dash.css` and `pages/index.html` over a live session: same
clipping (a clipped card never un-clips when the document scrolls sideways). Phase 18 had
already stopped the *document* scrolling sideways at a phone width — 360 px where it was
750 px — by breaking the bar into rows, so the masthead was complete and only the table tail
was out of reach.
**The phone half is closed**, and not by the shape gap 12 proposed (`.scroll` on the wide
tables, or a per-host card layout): one ≤640px rule makes every `table.data` a horizontal
scroll container inside the card, with no height cap, plus `min-width: 0` on the items of
`.body.stack` — the second declaration is what the first one needed, because a card body is a
one-column grid whose items may not be narrower than their `nowrap` content by default (before
it, `/system`'s "More detail" tables measured 520 px in a 320 px card with
`scrollWidth === clientWidth`: nothing to scroll, tail clipped). Pinned on the served
stylesheet by `tests/test_api.py::test_every_table_data_scrolls_sideways_at_the_phone_breakpoint`
and on every `<table>` still carrying `data`, and measured by `data/logs/27-task01-scroll.cjs`
(+ `data/logs/27-task01-*.png`): `/`, `/system` and `/ups` reach their tail columns at 360 px,
`/certs` keeps its `.scroll` box, no page scrolls sideways, and `table.data` is a plain table
with no scrollbar at 1280 px.
Task 02 wrote that audit: `data/logs/27-task02-mobile-audit.cjs`
(`NODE_PATH=<playwright node_modules> node …`, `DASH_BASE`/`DASH_PW`, transcript
`27-task02-mobile-audit.out`, screenshots `data/logs/mobile-27-<page>-<width>.png`) sweeps
**every page the app serves** — the seven signed-in ones plus `/login` unsigned, eight in
total, not the nine the phase file counted — at 360 px and 1280 px, and per page demands: no
sideways document scroll, no `table.data` clipped (its `overflow-x` must be `auto`/`scroll`
when its content is wider, and its tail column measured reachable), no element past the
viewport except inside a horizontal scroll container, the three-row masthead with its controls
on screen and the dropdown under the whole bar, plus each expanded disclosure opened at **both**
widths (`/system`'s "More detail", `/ups`'s NUT variables — a closed `<details>` still lays its
content out in Chrome, so a table behind one is measured only once revealed) and phase 26's
`/ups` two-card leg, whose `hidden` state is checked against the page's own
`POWER_EVENT_KINDS`. Its first run (2026-10-21, live fleet; 280 PASS / 6 FAIL / 1 FINDING,
identical on a re-run) is green on **every table claim at both widths** — task 01's rule holds,
disclosures included — and its findings list is the pre-existing header defect: `.card > header`
is `display: flex` with `nowrap` content, so at a 320 px card the header's tail is pushed past
the card's content edge and clipped — the `.updated` age stamp on `/system` (right 388-415 px),
`/llm` (374-393) and `/ups` (392-531) against a 360 px viewport, plus `.chip.model` (376) and
`.chip.health` (463) on the `apc` card. The one FINDING is the control-width half of the table
defect: all four of the opened host's `/system` "More detail" tables are 520 px inside a 404 px
card at 1280 px. The script exits non-zero on any FAIL, and task 03 brought it to **exit 0** at
both widths.
**Task 03 closed both findings**, with two CSS declarations and no markup change, and the
audit's sweep came out stricter rather than weaker.
*The 6 FAILs — a card header's tail was eaten at a phone width.* `.card > header` was a single
`nowrap` flex line, so the `.updated` age stamp stood 18-191 px past its card on `/system`,
`/llm` and `/ups` and the `apc` card's `.chip.model` / `.chip.health` did too, all of it
swallowed by `.card { overflow: hidden }`. The fix is the row, not the content:
`flex-wrap: wrap` plus `min-width: 0; max-width: 100%` on the header's `h3` and chips and
`white-space: normal` on a header chip (`.chip` elsewhere keeps `nowrap`). Wrapping costs
lines, which is stated rather than hidden: a host header is 74 px instead of 62 px, the `apc`
card's 104 px at 360 px, and at 320 px the 46-character model chip renders in full inside its
own pill. What the fix also exposed is that "inert at the control width" was the wrong
expectation: at 1280 px `/system`'s headers *did* move, because a nowrap line that cannot fit
does not overflow, it **shrinks** its one shrinkable child, so `updated 5s` was rendering as
`updated` / `5s` torn across two lines inside a 67 px box. `Range.getClientRects()` counts
those line boxes, which is how `data/logs/27-task03-verify.cjs` separates a header that gained
a line it needed (3 of 4 on `/system`) from one moved for nothing (0 on every other page).
*The 1 FINDING — the control-width half of the table defect, which was worse than clipped.* A
disclosure is a `.body.stack` grid item whose automatic minimum size is its content's
min-content width, so `/system`'s "More detail" grew to 520 px inside a 404 px card at 1280 px
and the GPU table's last column — `97.8 GiB`, tail right 554 px against a card content edge of
424 px — was painted **under the next card**, which starts at 438 px; the `charts-box` item
beside it was stretched to the same 520 px, so a chart's newest edge went with it. One
declaration fixes it: `.card > .body.stack > details { overflow-x: auto }` — the *disclosure*
is the scroll container, because hoisting task 01's `display: block` to every width would make
a block-level table shrink-to-fit and un-stretch every table that fits its card today
(measured: the three tables that fit are 377 px and fill 377 px, the one that does not is 520
px and scrolls, its tail landing at 411 px inside the card). It is deliberately **not** inside
the ≤640px block, because the defect it repairs is at 1280 px.
*The audit changed shape too, and only upward.* Check (2b) had asserted geometry ("no table box
past the card's content edge"), which is not what gap 12 is about and which a scroll container
satisfies only by accident; both widths now assert **reachability** — a table standing past its
card must have a scroll container between it and that card, and scrolling it must land the
tail cell inside the card — and an unreachable spill FAILs at either width. Two new checks
came with it: no card header's facts past the card (both widths; the viewport-only scan cannot
see a 1280 px clip) and every table that fits still fills its container (the no-layout-shift
half, measured rather than assumed). The final run over the live fleet is **332 PASS /
0 FAIL / 0 FINDING** (9 skips, each one a check that width cannot answer — the clock, hidden
below 900 px) on all eight served pages at both widths, exit 0
(`data/logs/27-task02-mobile-audit.out`, screenshots `mobile-27-*.png`; the first run is kept
as `27-task02-first-run.out`), and `data/logs/27-task03-control.cjs` proves the new check is
not vacuous by neutralising both declarations and re-creating the whole first-run findings list
(3/4/2 header offenders at 360 px, 8 stranded disclosure tables at 1280 px). The served-byte
pins are `test_a_card_header_wraps_its_facts_instead_of_losing_its_tail` and
`test_a_card_disclosure_scrolls_what_it_reveals_at_every_width`, beside task 01's
`test_every_table_data_scrolls_sideways_at_the_phone_breakpoint` and
`test_every_table_on_every_page_carries_the_class_the_rule_targets`.
Of the two operator TODO items that sat
beside it, `20_masthead_nav_polish` has landed in full (now complete): the bar is three
rows at every width (flush right on the two control rows since phase 25 task 02), it is no
longer sticky, the phone menu slides, and both