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:
@@ -0,0 +1 @@
|
|||||||
|
The audit re-ran green (exit 0). Now checking the docs for defects introduced by the phase.
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
== uv run pytest --cov (hermetic: no network, live tests skip) ==
|
||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
................................................................s....... [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 55%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
.......sssssssssss...................................................... [ 73%]
|
||||||
|
........................................................................ [ 78%]
|
||||||
|
........................................................................ [ 82%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
..................................................................sss... [ 96%]
|
||||||
|
.................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover Missing
|
||||||
|
------------------------------------------------------------------
|
||||||
|
src/dashboards/__init__.py 3 0 100%
|
||||||
|
src/dashboards/__main__.py 14 1 93% 32
|
||||||
|
src/dashboards/alerts.py 176 0 100%
|
||||||
|
src/dashboards/api.py 440 0 100%
|
||||||
|
src/dashboards/app.py 186 0 100%
|
||||||
|
src/dashboards/auth.py 265 0 100%
|
||||||
|
src/dashboards/certs.py 292 0 100%
|
||||||
|
src/dashboards/collectors.py 205 5 98% 284-285, 289-290, 307
|
||||||
|
src/dashboards/config.py 120 0 100%
|
||||||
|
src/dashboards/debug.py 14 0 100%
|
||||||
|
src/dashboards/poller.py 165 3 98% 192-194, 279
|
||||||
|
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
|
||||||
|
src/dashboards/rates.py 73 0 100%
|
||||||
|
src/dashboards/severity.py 49 0 100%
|
||||||
|
src/dashboards/sources/__init__.py 0 0 100%
|
||||||
|
src/dashboards/sources/disks.py 134 0 100%
|
||||||
|
src/dashboards/sources/host.py 209 0 100%
|
||||||
|
src/dashboards/sources/llm.py 178 1 99% 285
|
||||||
|
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
|
||||||
|
src/dashboards/sources/nut_sim.py 134 0 100%
|
||||||
|
src/dashboards/store.py 313 2 99% 553-554
|
||||||
|
------------------------------------------------------------------
|
||||||
|
TOTAL 3495 37 99%
|
||||||
|
Required test coverage of 94% reached. Total coverage: 98.94%
|
||||||
|
1549 passed, 15 skipped, 1 warning in 161.25s (0:02:41)
|
||||||
|
== uv run ruff check src tests agent ==
|
||||||
|
All checks passed!
|
||||||
|
== uv run pyright ==
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
== node --check src/dashboards/web/static/*.js ==
|
||||||
|
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
|
||||||
|
fastapi 0.153.0 0.159.1 wheel
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
**Implemented**
|
||||||
|
- `dash.css` ≤640px block: `table.data { display: block; overflow-x: auto }` (horizontal-only, **no** `max-height`) — one class rule reaches all seven tables; all-widths `table.data.scroll` untouched.
|
||||||
|
- **Deviation (measured, not assumed):** that rule alone was inert for the disclosure tables — `/system`'s "More detail" tables came back 520 px in a 320 px card with `scrollWidth === clientWidth`, because `.body.stack` is a one-column grid whose items may not shrink below `nowrap` content. Added the pinned pair `.body.stack > * { min-width: 0 }`; the same clause also stops the per-host chart stack being drawn at 520 px.
|
||||||
|
- `tests/test_api.py`: `test_every_table_data_scrolls_sideways_at_the_phone_breakpoint` (block+overflow-x, no `max-height`/`overflow-y`/shorthand, base `table.data` still a table, `.scroll` byte-identical, `min-width: 0` present) and `test_every_table_on_every_page_carries_the_class_the_rule_targets`; new `_media_body`/`_table_tags` helpers; phase-21 `.scroll` docstring corrected.
|
||||||
|
- Browser proof `data/logs/27-task01-scroll.cjs` (+`.out`, `27-task01-*.png`): tail columns reachable at 360 px on `/`, `/system`, `/ups`, `/certs` keeps its 320 px both-axis box, no sideways document scroll, plain tables at 1280 px.
|
||||||
|
|
||||||
|
**Verification** — `uv run pytest` 1547 passed / 15 skipped · `uv run pytest --cov --cov-fail-under=94` → 98.94% (baseline held) · `uv run ruff check src tests agent` clean · `uv run pyright` 0 errors · `bash .agents/validate.sh` exit 0 · `uv run pytest -m live --live` 11 passed · dev server restarted, `/api/health` `ok`, nine sources `ok`.
|
||||||
|
|
||||||
|
**Docs** — README (mobile table-scroll sentence); AGENTS.md gap 12 rewritten (phone clipping closed by task 01, evidence named), `/certs` "one scroll mechanism" sentence corrected, new UI-conventions paragraph on the rule pair.
|
||||||
|
|
||||||
|
**Findings logged for tasks 02–03** (pre-existing, byte-identical with the new rules neutralised — task 01's contract is that 1280 px is unchanged): 8/11 `/system` More-detail tables are 520/499 px inside a 404 px card at 1280 px; card *headers* (`.updated`, `.chip.model`, `.chip.health`) stand 18–191 px past their card at 360 px.
|
||||||
|
|
||||||
|
**Next task**: `.agents/phases/todo/27_mobile_tables_and_audit/02_mobile_audit_script.md`
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
== uv run pytest --cov (hermetic: no network, live tests skip) ==
|
||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
..............................................................s......... [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 55%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
.....sssssssssss........................................................ [ 73%]
|
||||||
|
........................................................................ [ 78%]
|
||||||
|
........................................................................ [ 82%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
................................................................sss..... [ 96%]
|
||||||
|
.................................................. [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover Missing
|
||||||
|
------------------------------------------------------------------
|
||||||
|
src/dashboards/__init__.py 3 0 100%
|
||||||
|
src/dashboards/__main__.py 14 1 93% 32
|
||||||
|
src/dashboards/alerts.py 176 0 100%
|
||||||
|
src/dashboards/api.py 440 0 100%
|
||||||
|
src/dashboards/app.py 186 0 100%
|
||||||
|
src/dashboards/auth.py 265 0 100%
|
||||||
|
src/dashboards/certs.py 292 0 100%
|
||||||
|
src/dashboards/collectors.py 205 5 98% 284-285, 289-290, 307
|
||||||
|
src/dashboards/config.py 120 0 100%
|
||||||
|
src/dashboards/debug.py 14 0 100%
|
||||||
|
src/dashboards/poller.py 165 3 98% 192-194, 279
|
||||||
|
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
|
||||||
|
src/dashboards/rates.py 73 0 100%
|
||||||
|
src/dashboards/severity.py 49 0 100%
|
||||||
|
src/dashboards/sources/__init__.py 0 0 100%
|
||||||
|
src/dashboards/sources/disks.py 134 0 100%
|
||||||
|
src/dashboards/sources/host.py 209 0 100%
|
||||||
|
src/dashboards/sources/llm.py 178 1 99% 285
|
||||||
|
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
|
||||||
|
src/dashboards/sources/nut_sim.py 134 0 100%
|
||||||
|
src/dashboards/store.py 313 2 99% 553-554
|
||||||
|
------------------------------------------------------------------
|
||||||
|
TOTAL 3495 37 99%
|
||||||
|
Required test coverage of 94% reached. Total coverage: 98.94%
|
||||||
|
1547 passed, 15 skipped, 1 warning in 162.36s (0:02:42)
|
||||||
|
== uv run ruff check src tests agent ==
|
||||||
|
All checks passed!
|
||||||
|
== uv run pyright ==
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
== node --check src/dashboards/web/static/*.js ==
|
||||||
|
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
|
||||||
|
fastapi 0.153.0 0.159.1 wheel
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
**Implemented**
|
||||||
|
- `data/logs/27-task02-mobile-audit.cjs` — the re-runnable full mobile audit (`DASH_BASE`/`DASH_PW`, `check()` accumulator, non-zero exit on any FAIL, transcript `27-task02-mobile-audit.out`, screenshots `data/logs/mobile-27-<page>-<width>.png`).
|
||||||
|
- Sweep: **8 pages** (7 signed-in + `/login` in a separate *unsigned* context; the plan's "nine" is a miscount — `MARKUP_PAGES` is seven) × **360 px** and **1280 px** control, checking document scroll, per-table clipping (`scrollWidth > clientWidth` ⇒ `overflow-x: auto|scroll`, tail column measured reachable) *and* the second clipping shape (table box past its card), a generic element-fits scan, the overlap-grouped 3-row masthead + controls-on-screen + dropdown-under-bar, both disclosures opened at both widths, and phase 26's `/ups` two-card leg (`hidden === errors.length === 0`, judged with the page's own `POWER_EVENT_KINDS`).
|
||||||
|
- Fixed as I went: legs ran at the wrong viewport, `TOL` unavailable inside `page.evaluate`, closed `<details>` tables counted as rendered (Chrome lays them out).
|
||||||
|
- First run preserved as `data/logs/27-task02-first-run.out`; identical pass/fail set on a re-run.
|
||||||
|
|
||||||
|
**Results** (live fleet, dev server restarted, 9 sources `ok`)
|
||||||
|
- `node --check data/logs/27-task02-mobile-audit.cjs` clean; audit = **280 PASS / 6 FAIL / 1 FINDING**, exit 1 — that exit *is* task 03's work order per the task file ("run twice: once expecting pre-task-03 findings").
|
||||||
|
- Every **table** claim passes at both widths (`/` fleet table 506→292 px scrolls to a reachable tail; `/certs` 1004 px `.scroll`; disclosure tables `block/auto`, no height cap at 360, plain tables at 1280; no sideways document scroll anywhere) — task 01's mechanism is proven.
|
||||||
|
- Findings: `.card > header` is `nowrap` flex, so at a 320 px card the header tail is clipped — `.updated` on `/system` (388-415 px), `/llm` (374-393), `/ups` (392-531) plus `.chip.model`/`.chip.health` on `apc`; 1 control-width FINDING (4 `/system` detail tables 520 px in a 404 px card). Both classes are the ones AGENTS.md gap 12 recorded as pre-existing for task 03 — **not fixed here**, that's task 03's remit.
|
||||||
|
- `uv run pytest` → **1547 passed, 15 skipped**; `--cov` **98.94%** (floor 94%); `ruff` clean; `pyright` 0 errors; `bash .agents/validate.sh` **EXIT=0** (only the pre-existing advisory: fastapi 0.153.0 → 0.159.1; no dependency moved in this task).
|
||||||
|
|
||||||
|
**Docs**: AGENTS.md gap 12 now records the audit script, its check set, the corrected page count and its first-run findings; refreshed the stale pytest tally (1545 → 1547). No README change (nothing user-visible changed).
|
||||||
|
|
||||||
|
**Next**: `27_mobile_tables_and_audit` task **03** (`03_fix_mobile_issues_found.md`) — fix the header clipping (and decide the 1280 px table finding), re-run until the audit exits 0 at both widths.
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
== uv run pytest --cov (hermetic: no network, live tests skip) ==
|
||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
..............................................................s......... [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 55%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
.....sssssssssss........................................................ [ 73%]
|
||||||
|
........................................................................ [ 78%]
|
||||||
|
........................................................................ [ 82%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
................................................................sss..... [ 96%]
|
||||||
|
.................................................. [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover Missing
|
||||||
|
------------------------------------------------------------------
|
||||||
|
src/dashboards/__init__.py 3 0 100%
|
||||||
|
src/dashboards/__main__.py 14 1 93% 32
|
||||||
|
src/dashboards/alerts.py 176 0 100%
|
||||||
|
src/dashboards/api.py 440 0 100%
|
||||||
|
src/dashboards/app.py 186 0 100%
|
||||||
|
src/dashboards/auth.py 265 0 100%
|
||||||
|
src/dashboards/certs.py 292 0 100%
|
||||||
|
src/dashboards/collectors.py 205 5 98% 284-285, 289-290, 307
|
||||||
|
src/dashboards/config.py 120 0 100%
|
||||||
|
src/dashboards/debug.py 14 0 100%
|
||||||
|
src/dashboards/poller.py 165 3 98% 192-194, 279
|
||||||
|
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
|
||||||
|
src/dashboards/rates.py 73 0 100%
|
||||||
|
src/dashboards/severity.py 49 0 100%
|
||||||
|
src/dashboards/sources/__init__.py 0 0 100%
|
||||||
|
src/dashboards/sources/disks.py 134 0 100%
|
||||||
|
src/dashboards/sources/host.py 209 0 100%
|
||||||
|
src/dashboards/sources/llm.py 178 1 99% 285
|
||||||
|
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
|
||||||
|
src/dashboards/sources/nut_sim.py 134 0 100%
|
||||||
|
src/dashboards/store.py 313 2 99% 553-554
|
||||||
|
------------------------------------------------------------------
|
||||||
|
TOTAL 3495 37 99%
|
||||||
|
Required test coverage of 94% reached. Total coverage: 98.94%
|
||||||
|
1547 passed, 15 skipped, 1 warning in 161.95s (0:02:41)
|
||||||
|
== uv run ruff check src tests agent ==
|
||||||
|
All checks passed!
|
||||||
|
== uv run pyright ==
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
== node --check src/dashboards/web/static/*.js ==
|
||||||
|
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
|
||||||
|
fastapi 0.153.0 0.159.1 wheel
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
Final state verified. Summary:
|
||||||
|
|
||||||
|
**Fixed (CSS only, both at every width — no markup, no data/API/page-script change):**
|
||||||
|
1. `.card > header { flex-wrap: wrap }` + `min-width: 0; max-width: 100%` on its `h3`/chips and `white-space: normal` on a header chip → closed all 6 FAILs (the `.updated` age stamp 18‑191 px past its card on `/system`/`/llm`/`/ups`, plus `apc`'s `.chip.model`/`.chip.health`). Nothing hidden: the row wraps (host header 62→74 px; at 1280 `/system`'s stamp had been torn as `updated`/`5s` inside a 67 px shrunk box; `/ups`, `/llm`, `/`, `/certs` geometry byte-identical).
|
||||||
|
2. `.card > .body.stack > details { overflow-x: auto }` → closed the 1 FINDING. It was worse than clipped: `/system`'s "More detail" grew to 520 px in a 404 px card and its last column (tail 554 px) — and the chart item stretched beside it — painted **under the next card** (starts 438 px). Now the disclosure scrolls, tail lands at 411 px inside the card; the three tables that fit stay 377 px and still fill it; the disclosure isn't a height cap, so `.scroll` is untouched.
|
||||||
|
|
||||||
|
**Audit strengthened, not weakened:** check (2b) now asserts *reachability* at both widths (unreachable spill FAILs), plus two new checks — no card header's facts past the card, every fitting table still fills its container. Final run: **332 PASS / 0 FAIL / 0 FINDING**, exit 0, all 8 served pages at 360 px and 1280 px (`data/logs/27-task02-mobile-audit.out`, `mobile-27-*.png`); first run kept as `27-task02-first-run.out`.
|
||||||
|
|
||||||
|
**Evidence:** `data/logs/27-task03-control.cjs` (16 PASS) re-creates every first-run offender with the declarations neutralised (3/4/2 header offenders, 8 stranded tables) — so neither rule is decorative and the new check cannot pass vacuously; `27-task03-verify.cjs` (20 PASS) measures both fixes, the cost of wrapping, and 320 px; screenshots `27-task03-{system-360,ups-360,ups-320,disclosure-rest,disclosure-scrolled,disclosure-unfixed}.png`.
|
||||||
|
|
||||||
|
**Pins:** `test_a_card_header_wraps_its_facts_instead_of_losing_its_tail`, `test_a_card_disclosure_scrolls_what_it_reveals_at_every_width` (asserts no `max-height`/`overflow-y`/`overflow`, and that the rule is outside the ≤640px block); task 01's pin untouched.
|
||||||
|
|
||||||
|
**Docs:** AGENTS.md gap 12 now **closed** (both defects, both fixes, the strengthened check, the control proof) plus the UI-conventions rules; README's mobile paragraph extended.
|
||||||
|
|
||||||
|
**Gate:** `bash .agents/validate.sh` **rc=0** — 1549 passed + 15 skipped, coverage 98.94 % (floor 94), ruff clean, pyright 0 errors, `node --check` clean. Dev server restarted, `/api/health` `ok` with all nine sources. Only the pre-existing advisory remains (fastapi 0.153.0 → 0.159.1; no dependency moved in this task).
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
== uv run pytest --cov (hermetic: no network, live tests skip) ==
|
||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
................................................................s....... [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 55%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
.......sssssssssss...................................................... [ 73%]
|
||||||
|
........................................................................ [ 78%]
|
||||||
|
........................................................................ [ 82%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
..................................................................sss... [ 96%]
|
||||||
|
.................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover Missing
|
||||||
|
------------------------------------------------------------------
|
||||||
|
src/dashboards/__init__.py 3 0 100%
|
||||||
|
src/dashboards/__main__.py 14 1 93% 32
|
||||||
|
src/dashboards/alerts.py 176 0 100%
|
||||||
|
src/dashboards/api.py 440 0 100%
|
||||||
|
src/dashboards/app.py 186 0 100%
|
||||||
|
src/dashboards/auth.py 265 0 100%
|
||||||
|
src/dashboards/certs.py 292 0 100%
|
||||||
|
src/dashboards/collectors.py 205 5 98% 284-285, 289-290, 307
|
||||||
|
src/dashboards/config.py 120 0 100%
|
||||||
|
src/dashboards/debug.py 14 0 100%
|
||||||
|
src/dashboards/poller.py 165 3 98% 192-194, 279
|
||||||
|
src/dashboards/prom.py 112 5 96% 126, 163, 177, 180, 183
|
||||||
|
src/dashboards/rates.py 73 0 100%
|
||||||
|
src/dashboards/severity.py 49 0 100%
|
||||||
|
src/dashboards/sources/__init__.py 0 0 100%
|
||||||
|
src/dashboards/sources/disks.py 134 0 100%
|
||||||
|
src/dashboards/sources/host.py 209 0 100%
|
||||||
|
src/dashboards/sources/llm.py 178 1 99% 285
|
||||||
|
src/dashboards/sources/nut.py 413 20 95% 153, 172, 186, 198, 204, 217, 225, 241, 319-320, 322, 579, 591, 626, 657, 661, 718-719, 724, 736
|
||||||
|
src/dashboards/sources/nut_sim.py 134 0 100%
|
||||||
|
src/dashboards/store.py 313 2 99% 553-554
|
||||||
|
------------------------------------------------------------------
|
||||||
|
TOTAL 3495 37 99%
|
||||||
|
Required test coverage of 94% reached. Total coverage: 98.94%
|
||||||
|
1549 passed, 15 skipped, 1 warning in 166.68s (0:02:46)
|
||||||
|
== uv run ruff check src tests agent ==
|
||||||
|
All checks passed!
|
||||||
|
== uv run pyright ==
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
== node --check src/dashboards/web/static/*.js ==
|
||||||
|
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
|
||||||
|
fastapi 0.153.0 0.159.1 wheel
|
||||||
@@ -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
|
`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
|
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
|
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
|
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
|
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
|
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 —
|
`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
|
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
|
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
|
# GET /api/certs/<name>/bundle and writes
|
||||||
# data/certs/<name>.bundle at 0600 — mints nothing
|
# 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
|
# 4 store_perf), no network
|
||||||
uv run pytest --cov # coverage gate (98.94%; floor 94%)
|
uv run pytest --cov # coverage gate (98.94%; floor 94%)
|
||||||
uv run pytest -m live --live # opt-in: real fleet / engines / upsd
|
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
|
component vocabulary the pages are still being converted to (`.hero-bar`, the grid and bar
|
||||||
variants) stays even while only one page uses it.
|
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,
|
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"
|
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`
|
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
|
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
|
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.
|
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
|
12. **Closed: the nav collapses on a phone, every table scrolls inside its own card, and no card
|
||||||
pass, 2026-10-20). At 360px `/`'s fleet table measures 450-510 px inside a 320 px
|
edge eats a fact** (the clipping is phase 18's verification measurement of 2026-10-20; the
|
||||||
`section.card`, and `.card { overflow: hidden }` clips the tail columns (`WORST DISK`,
|
phone half was closed by `27_mobile_tables_and_audit` task 01 and everything the audit found
|
||||||
`GPU BUSY`, `TEMP`, `UP`) with no scroll container to reach them — `table.data` only scrolls
|
after it by task 03, both on 2026-10-21).
|
||||||
with its `.scroll` modifier, which that table does not carry. Measured as pre-existing by
|
What was wrong: at 360px `/`'s fleet table measured 450-510 px inside a 320 px `section.card`,
|
||||||
serving the `git show HEAD:` bytes of both `dash.css` and `pages/index.html` over a live
|
and `.card { overflow: hidden }` clipped the tail columns (`WORST DISK`, `GPU BUSY`, `TEMP`,
|
||||||
session: same clipping (a clipped card never un-clips when the document scrolls sideways).
|
`UP`) with no scroll container to reach them — `table.data` scrolled only with its `.scroll`
|
||||||
What phase 18 did change is that the document no longer scrolls sideways at a phone width at
|
modifier, which that table does not carry. It was measured as pre-existing by serving the
|
||||||
all — 360 px where it was 750 px — because the bar that overflowed its fixed 58 px row now
|
`git show HEAD:` bytes of both `dash.css` and `pages/index.html` over a live session: same
|
||||||
breaks into rows, so nothing is lost and nothing gained: the masthead is complete at 360 px and
|
clipping (a clipped card never un-clips when the document scrolls sideways). Phase 18 had
|
||||||
the table tail is still out of reach. The fix is a responsive-table pass (`.scroll` on the wide
|
already stopped the *document* scrolling sideways at a phone width — 360 px where it was
|
||||||
tables, or a per-host card layout at ≤640px) and it touches pinned page shape on `/`, so it is
|
750 px — by breaking the bar into rows, so the masthead was complete and only the table tail
|
||||||
a phase rather than a cleanup — `21_page_cleanups`' `.scroll` on the `/certs` table is the same
|
was out of reach.
|
||||||
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
|
**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
|
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
|
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
|
longer sticky, the phone menu slides, and both
|
||||||
|
|||||||
@@ -232,8 +232,11 @@ the **Dashboard client** line (the one certificate every agent verifies the dial
|
|||||||
display-only, because it is minted with the root and has no route of its own), and one row per
|
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
|
host with its status chip, serial, expiry, issued-vs-deployed bundle revision, the addresses its
|
||||||
certificate is valid for, and **Rotate** / **Revoke** / **bundle**. The agents table carries the
|
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
|
stylesheet's own `scroll` modifier, so it is a box on **both** axes with a 320 px height cap at
|
||||||
bundle revisions, the addresses and the controls — are reachable by scrolling instead of clipped.
|
every width and its right-hand columns — the serial, the bundle revisions, the addresses and the
|
||||||
|
controls — are reachable by scrolling instead of clipped. Since phase 27 it is no longer the only
|
||||||
|
thing that scrolls (at ≤640px every `table.data` does, sideways only, no height cap); `.scroll`
|
||||||
|
out-ranks that rule, so this table keeps its own box on a phone exactly as on a desktop.
|
||||||
|
|
||||||
Nothing there is coloured: `severity.py` rates no certificate, so an expiry is a date and a
|
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,
|
bundle revision is a count. The one verdict on the page is the API's `status` word (`active` green,
|
||||||
@@ -775,7 +778,18 @@ certs.py ─ root CA + client cert, minted once at startup (unreadable row → n
|
|||||||
tabs, so it has no button either), and that menu **slides**: 180 ms of fade plus an 8 px rise on
|
tabs, so it has no button either), and that menu **slides**: 180 ms of fade plus an 8 px rise on
|
||||||
the way out and the same on the way back in, with nothing hidden from a keyboard or a screen
|
the way out and the same on the way back in, with nothing hidden from a keyboard or a screen
|
||||||
reader while it is shut, and no motion at all when the system asks for none
|
reader while it is shut, and no motion at all when the system asks for none
|
||||||
(`prefers-reduced-motion`). No framework, no CDN, no bundler, and no narration: a page never
|
(`prefers-reduced-motion`). The same ≤640px block is what makes **every** table scroll sideways
|
||||||
|
inside its own card (phase 27): `table.data` becomes a horizontal scroll container, so the
|
||||||
|
columns past a phone's 320 px — `Worst disk`, `GPU busy`, `Temp`, `Up` on the fleet table — are
|
||||||
|
reachable by dragging the table instead of being clipped by the card, while the document itself
|
||||||
|
still never scrolls sideways and a desktop table that fits shows no scrollbar. Two more things
|
||||||
|
were clipped by that same card edge and are fixed at **every** width: a card header wraps onto a
|
||||||
|
second line when its facts do not fit on one (the OS/model chip and the `updated 5s` age stamp
|
||||||
|
used to be painted past the card and eaten), and a collapsed `Charts` / `More detail` disclosure
|
||||||
|
scrolls whatever it reveals sideways, which is what puts `/system`'s GPU `VRAM`/`GTT` columns
|
||||||
|
inside their card instead of under the neighbouring one. Nothing is hidden for the arithmetic:
|
||||||
|
the row wraps, the disclosure scrolls, and every number stays on screen.
|
||||||
|
No framework, no CDN, no bundler, and no narration: a page never
|
||||||
explains its own pipeline, so the words on screen are the ones you act on — a unit, a `Δ<window>` provenance tag, a
|
explains its own pipeline, so the words on screen are the ones you act on — a unit, a `Δ<window>` provenance tag, a
|
||||||
`SIMULATED` badge, or the sentence beside an empty panel. The overview (`/`) is exactly two panels — a
|
`SIMULATED` badge, or the sentence beside an empty panel. The overview (`/`) is exactly two panels — a
|
||||||
**Warnings & errors** list built from `/api/summary`'s `alerts` (each row links to the
|
**Warnings & errors** list built from `/api/summary`'s `alerts` (each row links to the
|
||||||
|
|||||||
@@ -389,6 +389,56 @@ button.ghost:disabled { opacity: .45; cursor: not-allowed; }
|
|||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Every table scrolls sideways inside its own card (phase 27 task 01, `TODO.md:7` — "None of
|
||||||
|
* the tables are horizontally scrollable on mobile"). Measured on 2026-10-20 (AGENTS.md gap 12):
|
||||||
|
* at 360 px `/`'s fleet table is 450-510 px inside a 320 px `section.card`, and
|
||||||
|
* `.card { overflow: hidden }` clipped
|
||||||
|
* `WORST DISK`, `GPU BUSY`, `TEMP` and `UP` with nothing to reach them.
|
||||||
|
*
|
||||||
|
* The mechanism is the one the all-widths `table.data.scroll` rule proves: `display: block`
|
||||||
|
* keeps the table's internal layout in an anonymous box while the element itself becomes a
|
||||||
|
* block the width of the card, and `overflow-x: auto` gives that box a scrollbar only where
|
||||||
|
* the table is wider than it — so a table that fits paints no scrollbar and shifts nothing.
|
||||||
|
* One class-level rule reaches all seven tables (`#fleet-table`, `#agents-table`, the four
|
||||||
|
* `page-system.js` tables and the NUT-variables table in `page-ups.js`) because all seven
|
||||||
|
* already carry `data`; hand-placing `.scroll` on each is the drift this avoids, and the four
|
||||||
|
* JS-built ones cannot be edited in markup anyway. It also reaches the tables inside
|
||||||
|
* disclosures (`/system`'s "More detail", `/ups`'s raw variables): `overflow-x` on a block
|
||||||
|
* inside a revealed `<details>` is the same box model — measured, not assumed, by expanding
|
||||||
|
* both at 360 px in `data/logs/27-task01-scroll.cjs` (and that leg stays in task 02's sweep).
|
||||||
|
*
|
||||||
|
* Two things this rule deliberately does **not** do. It is horizontal-only: `overflow-x`, not
|
||||||
|
* `overflow`, and **no `max-height`**, because `.scroll`'s 320 px cap is that modifier's
|
||||||
|
* vertical-use contract and a phone given a height-capped table on top of a horizontal one is
|
||||||
|
* two new failure modes bought to fix one. And it fixes nothing at the document level —
|
||||||
|
* phase 18's guarantee that the page itself never scrolls sideways at a phone width stands,
|
||||||
|
* which is only true because the scroll container is the table and not `main`.
|
||||||
|
*
|
||||||
|
* The all-widths `table.data.scroll` rule is untouched and still wins where both apply (two
|
||||||
|
* classes to one), so `#agents-table` keeps its own both-axis box and 320 px cap on a phone
|
||||||
|
* exactly as on a desktop. */
|
||||||
|
table.data {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The other half of the rule above, and without it that rule does nothing for half the tables.
|
||||||
|
* Measured at 360 px on the live fleet: `/system`'s "More detail" tables came back 520 px wide
|
||||||
|
* inside a 320 px card with `scrollWidth === clientWidth` — no scrollbar, tail clipped — because
|
||||||
|
* the card body is `.body.stack`, a one-column **grid**, and a grid item's automatic minimum
|
||||||
|
* size is its content's min-content width. The `<details>` therefore grew the column to the
|
||||||
|
* table's width instead of the table overflowing its own box, and `.card { overflow: hidden }`
|
||||||
|
* ate the difference. `min-width: 0` says the item may be narrower than its content, which is
|
||||||
|
* what a scroll container *is*: nothing can scroll what its own container refuses to shrink.
|
||||||
|
*
|
||||||
|
* It is scoped to the card body's own items, not to `*`: the tables, disclosures and chart
|
||||||
|
* stacks inside a card are what a phone runs out of room for, and a page-level `min-width: 0`
|
||||||
|
* would reach layout this sheet does not own. Side effect, and a good one — the per-host chart
|
||||||
|
* stack in the same column was being laid out at that same 520 px and clipped with the tables. */
|
||||||
|
.body.stack > * {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- layout ------------------------------------------------------------- */
|
/* ---- layout ------------------------------------------------------------- */
|
||||||
@@ -428,10 +478,33 @@ main { padding: 1.15rem 1.25rem 3rem; max-width: 1680px; margin: 0 auto; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .55rem;
|
gap: .55rem;
|
||||||
|
/* One line whenever the facts fit, a second line when they do not (phase 27 task 03).
|
||||||
|
* Measured at 360 px by `data/logs/27-task02-mobile-audit.cjs`: `.card > header` was a
|
||||||
|
* single nowrap flex line, so the tail of a long header — the `.updated` age stamp on
|
||||||
|
* `/system` (right 402-415 px), `/llm` (374-393) and `/ups` (392-531), plus the `apc`
|
||||||
|
* card's `.chip.model` (376) and `.chip.health` (463) — stood *past the card* and
|
||||||
|
* `.card { overflow: hidden }` ate it. The age stamp and the model are facts the
|
||||||
|
* operator reads, so the fix is the row, not the content: nothing here is hidden, it
|
||||||
|
* wraps. A wrapped flex line with room on it is the same single line, and
|
||||||
|
* `data/logs/27-task03-verify.cjs` measures that — `/ups`, `/llm`, `/` and `/certs` keep
|
||||||
|
* byte-identical header and card geometry at 1280 px. `/system`'s host headers are the
|
||||||
|
* exception, and they grew (62 px to 74 px) for a reason worth writing down: a nowrap line
|
||||||
|
* that cannot fit does not overflow, it *shrinks* its one shrinkable child, so the age
|
||||||
|
* stamp was painting `updated` / `5s` torn across two lines inside 67 px. A second line
|
||||||
|
* for a fact that needs one is the cheaper of those two. */
|
||||||
|
flex-wrap: wrap;
|
||||||
padding: .6rem .8rem;
|
padding: .6rem .8rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
background: color-mix(in srgb, var(--bg-raised) 70%, var(--bg-sunken));
|
background: color-mix(in srgb, var(--bg-raised) 70%, var(--bg-sunken));
|
||||||
}
|
}
|
||||||
|
/* The two header pieces that carry free text, and the only reason `min-width` is named:
|
||||||
|
* a flex item's automatic minimum size is its content's min-content width, so without
|
||||||
|
* this a long OS or model string could not wrap inside its own box even on a line of its
|
||||||
|
* own (the 290 px `apc` model chip at a 252 px card). `white-space` is relaxed for chips
|
||||||
|
* only — a chip is a label, and a chip that becomes two lines still says the whole label,
|
||||||
|
* which is what the GPU-value card already decided about the model string in phase 06. */
|
||||||
|
.card > header h3 { min-width: 0; max-width: 100%; }
|
||||||
|
.card > header .chip { min-width: 0; max-width: 100%; white-space: normal; }
|
||||||
.card > header h2, .card > header h3 {
|
.card > header h2, .card > header h3 {
|
||||||
margin: 0; font-size: 12.5px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase;
|
margin: 0; font-size: 12.5px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -912,6 +985,25 @@ table.data .mute { color: var(--text-muted); }
|
|||||||
table.data td.warn { color: var(--warn); }
|
table.data td.warn { color: var(--warn); }
|
||||||
table.data td.fail { color: var(--fail); }
|
table.data td.fail { color: var(--fail); }
|
||||||
table.data.scroll { display: block; overflow: auto; max-height: 320px; }
|
table.data.scroll { display: block; overflow: auto; max-height: 320px; }
|
||||||
|
/* The same gap-12 defect at the widths the ≤640px rule does not reach (phase 27 task 03), and
|
||||||
|
* the reason the fix belongs on the disclosure rather than on the table. `data/logs
|
||||||
|
* /27-task02-mobile-audit.cjs` measured `/system`'s "More detail" tables at 520 px inside a
|
||||||
|
* 404 px card at 1280 px, and the tail was not merely clipped — the over-wide disclosure also
|
||||||
|
* stretched the `charts-box` item beside it to the same 520 px, so a chart plot's right-hand
|
||||||
|
* (newest) edge and the table's last column both painted under the next card.
|
||||||
|
*
|
||||||
|
* Why the *disclosure* and not the table: hoisting task 01's `display: block` would make a
|
||||||
|
* block-level table shrink-to-fit, so the tables that fit their card today (327/257/287 px of
|
||||||
|
* content) would stop filling it — a control-width layout change for the many to rescue the
|
||||||
|
* one. A disclosure, on the other hand, is only ever as wide as the card, and a scroll
|
||||||
|
* container's automatic minimum size is zero, so this one declaration both stops the item
|
||||||
|
* growing past the card and gives the over-wide content a scrollbar; no `min-width: 0` is
|
||||||
|
* needed here (task 01 needed it because there the scroller is the table while the *item* is
|
||||||
|
* not one — measured, `min-width` alone leaves `scrollWidth === clientWidth`, i.e. nothing to
|
||||||
|
* scroll and the tail still clipped). Inert wherever the content fits: `/ups`'s variables
|
||||||
|
* disclosure (586 px in a 632 px card) and both `/disks` chart disclosures measure the same
|
||||||
|
* box, `scrollWidth === clientWidth`, no scrollbar, before and after. */
|
||||||
|
.card > .body.stack > details { overflow-x: auto; }
|
||||||
|
|
||||||
/* ---- charts ------------------------------------------------------------- */
|
/* ---- charts ------------------------------------------------------------- */
|
||||||
/* The chart vocabulary every page draws with. `LineChart` puts the y tick labels in the
|
/* The chart vocabulary every page draws with. `LineChart` puts the y tick labels in the
|
||||||
|
|||||||
+195
-3
@@ -1205,9 +1205,13 @@ def test_the_certs_agents_table_scrolls_to_its_last_column(client) -> None:
|
|||||||
|
|
||||||
`TODO.md:9`: at phone width the serial, expiry, `san` and controls columns were clipped by
|
`TODO.md:9`: at phone width the serial, expiry, `san` and controls columns were clipped by
|
||||||
`.card { overflow: hidden }` with no way to reach them. `dash.css` turns `table.data` into
|
`.card { overflow: hidden }` with no way to reach them. `dash.css` turns `table.data` into
|
||||||
a scroll container **only** through the `scroll` class, so the class and the rule are one
|
a scroll container through the `scroll` class, so the class and the rule are one mechanism
|
||||||
mechanism and are pinned together — the phase-18 idiom of comparing the two places a
|
and are pinned together — the phase-18 idiom of comparing the two places a contract lives,
|
||||||
contract lives, because a rename on either side silently un-scrolls the table.
|
because a rename on either side silently un-scrolls the table.
|
||||||
|
|
||||||
|
(Phase 27 task 01 added a second door: at ≤640px *every* `table.data` scrolls horizontally.
|
||||||
|
That does not retire this pin — `.scroll` out-ranks it, two classes to one, so this table's
|
||||||
|
both-axis box and its 320 px height cap are still decided here, on a phone included.)
|
||||||
"""
|
"""
|
||||||
html = client.get(CERTS_PAGE).text
|
html = client.get(CERTS_PAGE).text
|
||||||
assert '<table class="data scroll" id="agents-table">' in html, \
|
assert '<table class="data scroll" id="agents-table">' in html, \
|
||||||
@@ -5507,6 +5511,194 @@ def test_summary_names_the_drive_the_health_verdict_called_failed(
|
|||||||
store.close()
|
store.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---- every table scrolls sideways on a phone (phase 27 task 01) -------------- #
|
||||||
|
#
|
||||||
|
# `TODO.md:7` — "None of the tables are horizontally scrollable on mobile." The measurement is
|
||||||
|
# AGENTS.md gap 12: at 360 px `/`'s fleet table is 450-510 px inside a 320 px `section.card` and
|
||||||
|
# `.card { overflow: hidden }` clips `WORST DISK` / `GPU BUSY` / `TEMP` / `UP` with nothing to
|
||||||
|
# reach them. The fix is one rule inside the repo's existing ≤640px block, taken from the shape
|
||||||
|
# the all-widths `table.data.scroll` modifier already proves — `display: block` plus a horizontal
|
||||||
|
# scrollbar, so the scroll container is the table and never the document (phase 18's standing
|
||||||
|
# guarantee). It is a class rule rather than seven hand-placed `.scroll` classes because four of
|
||||||
|
# the seven tables exist only in JS-built markup and the class is the whole contract.
|
||||||
|
#
|
||||||
|
# Two failure modes the pins below are aimed at, both named in the task's assumptions: a
|
||||||
|
# `max-height` migrating in from `.scroll` (a phone then gets a height-capped table on top of a
|
||||||
|
# horizontal one), and the all-widths `.scroll` rule being edited to "share" the new mobile one.
|
||||||
|
#
|
||||||
|
# The task assumed one rule would reach every table. Measured on the live fleet at 360 px it did
|
||||||
|
# not reach the disclosure tables: `/system`'s GPU / filesystem / process tables came back 520 px
|
||||||
|
# wide inside a 320 px card with `scrollWidth === clientWidth`, because the card body is
|
||||||
|
# `.body.stack` — a one-column grid — and a grid item's automatic minimum size is its content's
|
||||||
|
# min-content width, so the `<details>` grew the column instead of the table overflowing its own
|
||||||
|
# box. `min-width: 0` on the card body's items is therefore pinned as the second half of one
|
||||||
|
# mechanism: without it the scroll rule is inert for exactly the tables the task asked about.
|
||||||
|
# `data/logs/27-task01-scroll.cjs` is the browser half; task 02's audit is the phase's proof.
|
||||||
|
|
||||||
|
def _media_body(css: str, condition: str) -> str:
|
||||||
|
"""The declarations of one `@media (…)` block, brace-matched, comments stripped.
|
||||||
|
|
||||||
|
`_rule_body` stops at the first `}` because no rule nests; a media block does, and the
|
||||||
|
≤640px block's own comments contain example rules with braces in them — hence running the
|
||||||
|
section through `_code` first. The caller picks the block by condition, so a pin can ask about
|
||||||
|
*the mobile block's* `table.data` without mistaking it for the all-widths rule of the same
|
||||||
|
name further down the file.
|
||||||
|
"""
|
||||||
|
body = _code(css)
|
||||||
|
match = re.search(rf"@media\s*\({re.escape(condition)}\)\s*\{{", body)
|
||||||
|
assert match, f"dash.css no longer has an `@media ({condition})` block"
|
||||||
|
depth, index = 1, match.end()
|
||||||
|
while depth:
|
||||||
|
char = body[index]
|
||||||
|
depth += (char == "{") - (char == "}")
|
||||||
|
index += 1
|
||||||
|
return body[match.end():index - 1]
|
||||||
|
|
||||||
|
|
||||||
|
def _table_tags(text: str) -> list[str]:
|
||||||
|
"""Every `<table …>` opening tag in a document or a JS template, comments already gone."""
|
||||||
|
return re.findall(r"<table\b([^>]*)>", text)
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_table_data_scrolls_sideways_at_the_phone_breakpoint(client) -> None:
|
||||||
|
"""One mobile rule, horizontal-only, no height cap — and the desktop `.scroll` untouched.
|
||||||
|
|
||||||
|
Each assertion is one half of the mechanism: `display: block` is what lets the element be the
|
||||||
|
card's width while the table inside it is wider, `overflow-x: auto` is the scrollbar that
|
||||||
|
appears only when it overflows (so a table that fits paints nothing new at 1280 px), and the
|
||||||
|
absence of `max-height` / `overflow-y` / the `overflow` shorthand is the assumption that a
|
||||||
|
phone gets a sideways scroll and *only* a sideways scroll.
|
||||||
|
"""
|
||||||
|
css = _css(client)
|
||||||
|
# Scoped to the section the nav pins read, so "the mobile rule" means the repo's one mobile
|
||||||
|
# boundary block rather than whichever `@media (max-width: 640px)` happens to come first.
|
||||||
|
mobile_block = _media_body(_css_section(css, "mobile nav"), "max-width: 640px")
|
||||||
|
mobile = _declarations(mobile_block, "table.data")
|
||||||
|
assert mobile.get("display") == "block", (
|
||||||
|
"the mobile `table.data` is not a scroll container — its tail columns are clipped by "
|
||||||
|
"`.card { overflow: hidden }` again (AGENTS.md gap 12)")
|
||||||
|
assert mobile.get("overflow-x") == "auto", (
|
||||||
|
"the mobile rule no longer gives a wide table a way to reach its last column")
|
||||||
|
for forbidden in ("max-height", "overflow-y", "overflow"):
|
||||||
|
assert forbidden not in mobile, (
|
||||||
|
f"the mobile rule declares `{forbidden}`: the 320 px height cap is `.scroll`'s "
|
||||||
|
"vertical-use contract, not what a phone needs on top of a horizontal scroll")
|
||||||
|
|
||||||
|
# The second half of the mechanism, in the same block: a grid item may not be narrower than
|
||||||
|
# its content unless something says so, and the card bodies are one-column grids — so without
|
||||||
|
# this clause the rule above cannot overflow anything inside a disclosure and the tail columns
|
||||||
|
# stay clipped by the card. Pinned together because one without the other is the bug.
|
||||||
|
stack = _declarations(mobile_block, ".body.stack > *")
|
||||||
|
assert stack.get("min-width") == "0", (
|
||||||
|
"the mobile block lost the `min-width: 0` that lets a card body be narrower than the "
|
||||||
|
"table inside it — the disclosure tables grow the grid column and clip again")
|
||||||
|
|
||||||
|
# The all-widths rules are the *other* half of the contract: `table.data` still lays out as a
|
||||||
|
# table above the breakpoint (no scrollbar where the table fits, no layout shift at 1280 px),
|
||||||
|
# and `.scroll` still caps and scrolls both axes wherever it is applied. Read out of the
|
||||||
|
# `tables` section because the mobile rule above has the same selector and comes first.
|
||||||
|
tables = _css_section(css, "tables")
|
||||||
|
base = _declarations(tables, "table.data")
|
||||||
|
assert base.get("display", "table") == "table", (
|
||||||
|
"`table.data` is a block at every width now — the mobile rule leaked out of its media "
|
||||||
|
"block and every table grew a scroll container it does not need")
|
||||||
|
assert base.get("width") == "100%", "the base table lost the width that makes it fill its card"
|
||||||
|
scroll = _declarations(tables, "table.data.scroll")
|
||||||
|
assert scroll == {"display": "block", "overflow": "auto", "max-height": "320px"}, (
|
||||||
|
f"`table.data.scroll` changed shape: {scroll} — `#agents-table`'s both-axis box and "
|
||||||
|
"320 px cap are phase 21's contract and this phase may not edit them")
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_card_header_wraps_its_facts_instead_of_losing_its_tail(client) -> None:
|
||||||
|
"""`.card > header` wraps, and the two free-text pieces are allowed to become narrower.
|
||||||
|
|
||||||
|
The 6 FAILs of the first phase-27 audit run were not tables: at 360 px the header was a
|
||||||
|
single `nowrap` flex line, so its tail — the `.updated` age stamp on `/system` (right
|
||||||
|
402-415 px), `/llm` (374-393) and `/ups` (392-531), plus the `apc` card's `.chip.model`
|
||||||
|
(376) and `.chip.health` (463) — stood past the card and `.card { overflow: hidden }` ate it.
|
||||||
|
"How old is this" and "which box is this" are facts, so the row wraps instead (measured:
|
||||||
|
`data/logs/27-task03-verify.cjs`, and `27-task03-control.cjs` shows these same offenders again
|
||||||
|
with the declarations neutralised).
|
||||||
|
|
||||||
|
`min-width: 0` is pinned with `flex-wrap` because one without the other is not the fix: a
|
||||||
|
flex item's automatic minimum size is its content's min-content width, so a long OS or model
|
||||||
|
string still cannot wrap inside its own box (the `apc` chip measured 290 px against a 252 px
|
||||||
|
card). And `white-space: normal` is scoped to a header chip, not to `.chip`: a chip is a
|
||||||
|
label elsewhere in this app and keeps its one-line shape.
|
||||||
|
"""
|
||||||
|
header = _declarations(_css(client), ".card > header")
|
||||||
|
assert header.get("flex-wrap") == "wrap", (
|
||||||
|
"the card header is a single nowrap flex line again — its tail facts are painted past "
|
||||||
|
"the card and clipped (AGENTS.md gap 12's header finding)")
|
||||||
|
shrink = _declarations(_css(client), ".card > header .chip")
|
||||||
|
assert shrink.get("min-width") == "0" and shrink.get("max-width") == "100%", (
|
||||||
|
"a header chip may not become narrower than its own text: with `flex-wrap` alone the "
|
||||||
|
"longest chip still cannot wrap and the row's tail leaves the card")
|
||||||
|
assert shrink.get("white-space") == "normal", (
|
||||||
|
"a header chip that has to share a line now truncates or overflows instead of wrapping; "
|
||||||
|
"the label has to stay readable in full")
|
||||||
|
assert _declarations(_css(client), ".card > header h3").get("min-width") == "0", (
|
||||||
|
"the header title lost the same allowance — a long host name cannot wrap either")
|
||||||
|
# Read out of the chips section, because `_rule_body` anchors on the selector text and the
|
||||||
|
# header exception above *contains* `.chip {` — the base vocabulary lives further down.
|
||||||
|
assert _declarations(_css_section(_css(client), "bars, dots, chips"), ".chip").get(
|
||||||
|
"white-space") == "nowrap", (
|
||||||
|
"`.chip` itself stopped being one line: this fix is an exception for header chips, not a "
|
||||||
|
"change to the chip vocabulary every other page draws with")
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_card_disclosure_scrolls_what_it_reveals_at_every_width(client) -> None:
|
||||||
|
"""The control-width half of gap 12: the *disclosure* scrolls, so the card stops clipping.
|
||||||
|
|
||||||
|
Task 01 logged this as a FINDING at 1280 px and left the decision here. Measured, `/system`
|
||||||
|
three cards across: the "More detail" item grew to its table's min-content (520 px inside a
|
||||||
|
404 px card), and because the card clips, the GPU table's last column — "97.8 GiB", tail
|
||||||
|
right 554 against a card content edge of 424 — was painted *under the next card* (which
|
||||||
|
starts at 438 px), unreachable by any gesture. The `charts-box` item beside it was stretched
|
||||||
|
to the same 520 px, so a chart's newest edge went with it.
|
||||||
|
|
||||||
|
The fix is one declaration on the disclosure, and these assertions are why it is that one:
|
||||||
|
horizontal only (no `max-height`, no `overflow-y`, no `overflow` shorthand — a height cap is
|
||||||
|
`.scroll`'s contract, and a vertical scrollport inside a card body would be a second one),
|
||||||
|
and **outside** the ≤640px block, because the defect it fixes is at the control width. It is
|
||||||
|
not the mobile rule hoisted: `table.data` above stays `display: table`, which is what keeps
|
||||||
|
every table that fits stretched across its card instead of shrink-wrapped — pinned next door
|
||||||
|
and re-measured by `data/logs/27-task03-verify.cjs`.
|
||||||
|
"""
|
||||||
|
css = _css(client)
|
||||||
|
disc = _declarations(_css_section(css, "tables"), ".card > .body.stack > details")
|
||||||
|
assert disc.get("overflow-x") == "auto", (
|
||||||
|
"a disclosure no longer scrolls what it reveals: a table wider than its card grows the "
|
||||||
|
"item past the card again and `.card { overflow: hidden }` clips the tail column")
|
||||||
|
for forbidden in ("max-height", "overflow-y", "overflow"):
|
||||||
|
assert forbidden not in disc, (
|
||||||
|
f"the disclosure rule declares `{forbidden}`: this fix is a sideways scroll only — "
|
||||||
|
"the 320 px cap belongs to `table.data.scroll` and a vertical scrollport here would "
|
||||||
|
"hide rows the operator has not chosen to hide")
|
||||||
|
mobile = _media_body(_css_section(css, "mobile nav"), "max-width: 640px")
|
||||||
|
assert ".card > .body.stack > details" not in mobile, (
|
||||||
|
"the disclosure scroll moved inside the ≤640px block — the finding it repairs was measured "
|
||||||
|
"at 1280 px, where it would then do nothing")
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_table_on_every_page_carries_the_class_the_rule_targets(client) -> None:
|
||||||
|
"""The mobile rule is class-level, so a `<table>` without `data` would silently not scroll.
|
||||||
|
|
||||||
|
Both halves are read from the served bytes: the seven static tables in the page templates and
|
||||||
|
the five built at runtime by `page-system.js` / `page-ups.js` (four inside `/system`'s "More
|
||||||
|
detail", one the NUT variables list). This is the drift the task chose the class rule over
|
||||||
|
seven hand-placed `.scroll` classes for — and it cuts both ways, which is why it is a test.
|
||||||
|
"""
|
||||||
|
tags = [tag for page in MARKUP_PAGES for tag in _table_tags(_code(client.get(page).text))]
|
||||||
|
tags += [tag for script in PAGE_SCRIPTS.values()
|
||||||
|
for tag in _table_tags(_code(client.get(f"/static/{script}").text))]
|
||||||
|
assert len(tags) >= 7, f"only {len(tags)} tables found — the sweep stopped reading the pages"
|
||||||
|
for tag in tags:
|
||||||
|
assert re.search(r'class="[^"]*\bdata\b', tag), (
|
||||||
|
f"a table outside the rule's class selector: <table{tag}> — it will clip its tail "
|
||||||
|
"columns at 360 px instead of scrolling them")
|
||||||
|
|
||||||
|
|
||||||
# ---- events --------------------------------------------------------------- #
|
# ---- events --------------------------------------------------------------- #
|
||||||
|
|
||||||
def test_events_are_returned_in_json(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_events_are_returned_in_json(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user