From 5d37444fb05691b7dd4b3b1793e59d70db945c31 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Wed, 21 Oct 2026 23:43:25 -0400 Subject: [PATCH] phase: 27_mobile_tables_and_audit The audit re-ran green (exit 0). Now checking the docs for defects introduced by the phase. --- .../27_mobile_tables_and_audit/00_phase.md | 0 .../01_scrollable_tables_on_mobile.md | 0 .../02_mobile_audit_script.md | 0 .../03_fix_mobile_issues_found.md | 0 ...7_mobile_tables_and_audit__00_phase.a1.err | 0 ...27_mobile_tables_and_audit__00_phase.a1.md | 1 + ...le_tables_and_audit__00_phase.a1.md.status | 1 + ...ile_tables_and_audit__00_phase.a1.validate | 66 ++++++ ...dit__01_scrollable_tables_on_mobile.a1.err | 0 ...udit__01_scrollable_tables_on_mobile.a1.md | 13 ++ ...1_scrollable_tables_on_mobile.a1.md.status | 1 + ...01_scrollable_tables_on_mobile.a1.validate | 66 ++++++ ...s_and_audit__02_mobile_audit_script.a1.err | 0 ...es_and_audit__02_mobile_audit_script.a1.md | 15 ++ ...audit__02_mobile_audit_script.a1.md.status | 1 + ..._audit__02_mobile_audit_script.a1.validate | 66 ++++++ ...d_audit__03_fix_mobile_issues_found.a1.err | 0 ...nd_audit__03_fix_mobile_issues_found.a1.md | 15 ++ ...t__03_fix_mobile_issues_found.a1.md.status | 1 + ...it__03_fix_mobile_issues_found.a1.validate | 66 ++++++ AGENTS.md | 174 +++++++++++++-- README.md | 20 +- src/dashboards/web/static/dash.css | 92 ++++++++ tests/test_api.py | 198 +++++++++++++++++- 24 files changed, 772 insertions(+), 24 deletions(-) rename .agents/phases/{todo => complete}/27_mobile_tables_and_audit/00_phase.md (100%) rename .agents/phases/{todo => complete}/27_mobile_tables_and_audit/01_scrollable_tables_on_mobile.md (100%) rename .agents/phases/{todo => complete}/27_mobile_tables_and_audit/02_mobile_audit_script.md (100%) rename .agents/phases/{todo => complete}/27_mobile_tables_and_audit/03_fix_mobile_issues_found.md (100%) create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.err create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.md create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.md.status create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.validate create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.err create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.md create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.md.status create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.validate create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.err create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.md create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.md.status create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.validate create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.err create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.md create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.md.status create mode 100644 .agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.validate diff --git a/.agents/phases/todo/27_mobile_tables_and_audit/00_phase.md b/.agents/phases/complete/27_mobile_tables_and_audit/00_phase.md similarity index 100% rename from .agents/phases/todo/27_mobile_tables_and_audit/00_phase.md rename to .agents/phases/complete/27_mobile_tables_and_audit/00_phase.md diff --git a/.agents/phases/todo/27_mobile_tables_and_audit/01_scrollable_tables_on_mobile.md b/.agents/phases/complete/27_mobile_tables_and_audit/01_scrollable_tables_on_mobile.md similarity index 100% rename from .agents/phases/todo/27_mobile_tables_and_audit/01_scrollable_tables_on_mobile.md rename to .agents/phases/complete/27_mobile_tables_and_audit/01_scrollable_tables_on_mobile.md diff --git a/.agents/phases/todo/27_mobile_tables_and_audit/02_mobile_audit_script.md b/.agents/phases/complete/27_mobile_tables_and_audit/02_mobile_audit_script.md similarity index 100% rename from .agents/phases/todo/27_mobile_tables_and_audit/02_mobile_audit_script.md rename to .agents/phases/complete/27_mobile_tables_and_audit/02_mobile_audit_script.md diff --git a/.agents/phases/todo/27_mobile_tables_and_audit/03_fix_mobile_issues_found.md b/.agents/phases/complete/27_mobile_tables_and_audit/03_fix_mobile_issues_found.md similarity index 100% rename from .agents/phases/todo/27_mobile_tables_and_audit/03_fix_mobile_issues_found.md rename to .agents/phases/complete/27_mobile_tables_and_audit/03_fix_mobile_issues_found.md diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.err b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.md b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.md new file mode 100644 index 0000000..a5d467b --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.md @@ -0,0 +1 @@ +The audit re-ran green (exit 0). Now checking the docs for defects introduced by the phase. diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.md.status b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.validate b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.validate new file mode 100644 index 0000000..b53d6b9 --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__00_phase.a1.validate @@ -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 diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.err b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.md b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.md new file mode 100644 index 0000000..0177a5e --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.md @@ -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` diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.md.status b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.validate b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.validate new file mode 100644 index 0000000..f1343eb --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__01_scrollable_tables_on_mobile.a1.validate @@ -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 diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.err b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.md b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.md new file mode 100644 index 0000000..edeaadf --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.md @@ -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--.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 `
` 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. diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.md.status b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.validate b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.validate new file mode 100644 index 0000000..59fdba7 --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__02_mobile_audit_script.a1.validate @@ -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 diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.err b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.md b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.md new file mode 100644 index 0000000..d2d175e --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.md @@ -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). diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.md.status b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.validate b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.validate new file mode 100644 index 0000000..2bf5685 --- /dev/null +++ b/.agents/reports/27_mobile_tables_and_audit/27_mobile_tables_and_audit__03_fix_mobile_issues_found.a1.validate @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 8a5a772..7dd3bc9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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//bundle and writes # data/certs/.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 `` 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 `
` 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= node …`, `DASH_BASE`/`DASH_PW`, transcript + `27-task02-mobile-audit.out`, screenshots `data/logs/mobile-27--.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 `
` 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 diff --git a/README.md b/README.md index e94d41f..ded04ee 100644 --- a/README.md +++ b/README.md @@ -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 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 -stylesheet's own `scroll` modifier, so at phone width its right-hand columns — the serial, the -bundle revisions, the addresses and the controls — are reachable by scrolling instead of clipped. +stylesheet's own `scroll` modifier, so it is a box on **both** axes with a 320 px height cap at +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 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 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 - (`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 `Δ` provenance tag, 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 diff --git a/src/dashboards/web/static/dash.css b/src/dashboards/web/static/dash.css index c9c7ca8..69540bb 100644 --- a/src/dashboards/web/static/dash.css +++ b/src/dashboards/web/static/dash.css @@ -389,6 +389,56 @@ button.ghost:disabled { opacity: .45; cursor: not-allowed; } min-height: 40px; 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 `
` 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 `
` 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 ------------------------------------------------------------- */ @@ -428,10 +478,33 @@ main { padding: 1.15rem 1.25rem 3rem; max-width: 1680px; margin: 0 auto; } display: flex; align-items: center; 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; border-bottom: 1px solid var(--border); 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 { margin: 0; font-size: 12.5px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; 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.fail { color: var(--fail); } 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 ------------------------------------------------------------- */ /* The chart vocabulary every page draws with. `LineChart` puts the y tick labels in the diff --git a/tests/test_api.py b/tests/test_api.py index d3b0325..01b6866 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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 `.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 - mechanism and are pinned together — the phase-18 idiom of comparing the two places a - contract lives, because a rename on either side silently un-scrolls the table. + a scroll container through the `scroll` class, so the class and the rule are one mechanism + and are pinned together — the phase-18 idiom of comparing the two places a contract lives, + 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 assert '
' in html, \ @@ -5507,6 +5511,194 @@ def test_summary_names_the_drive_the_health_verdict_called_failed( 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 `
` 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 `
` opening tag in a document or a JS template, comments already gone.""" + return re.findall(r"]*)>", 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 `
` 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: — it will clip its tail " + "columns at 360 px instead of scrolling them") + + # ---- events --------------------------------------------------------------- # def test_events_are_returned_in_json(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: