From 6444a158f21e4b7b530bcb1e395e25b030f62b57 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Tue, 20 Oct 2026 22:49:37 -0400 Subject: [PATCH] phase: 20_masthead_nav_polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Phase 20 final verification pass — all four tasks already complete; verified in full.** - Verified: three-row masthead CSS (`.masthead` flex-wrap + two `flex-basis: 100%`, `min-height`, no `margin-inline-start`), un-sticky `position: relative` + `z-index: 40`, 180 ms `visibility/opacity/transform` slide (closed `nav.tabs` / open `.is-open`, both states in the file's single reduced-motion block), both admin links on all 8 signed-in pages, `/login` neither. - `uv run pytest` → **1464 passed, 15 skipped** · `uv run pytest --cov --cov-fail-under=94` → **98.94%** (floor 94, unchanged) · `ruff check src tests agent` clean · `pyright` 0 errors · **`bash .agents/validate.sh` EXIT=0** (incl. `node --check` + repo hygiene) · `uv run pytest -m live --live` → **11 passed**. - Browser (live dev server, invariant 10): `20-task01-rows.cjs` 121 PASS/0 FAIL · `20-task02-unsticky.cjs` 81/0 · `20-task03-slide.cjs` 32/0 · `20-task04-admin-links.cjs` 87/0 · phase-18 `nav-final-pass.cjs` 45/45, `nav-task03-verify` 77, `nav-task04-verify` 24/24 · phase-19 scripts 11/22/24/29 all pass. Screenshots refreshed in `data/logs/` (`masthead-rows*`, `masthead-unsticky*`, `slide-*`, `admin-links-*`). Dev server restarted; `/api/health` all nine sources `ok`. - Completion criteria: 3-row bar @1280/360 ✓ · scroll leaves bar behind + dropdown under whole bar ✓ · slide out/in 180 ms, reduced-motion snap ✓ · `/certs`→`⚷ Certs`, `/settings`→`⚙ Account` before theme toggle, outside `.tabs` ✓ · pytest/coverage/ruff/pyright ✓ · validate ✓ · no regression in tab structure, token reveal, safe-area padding, four close paths, status dots, theme toggle ✓. - Docs: **AGENTS.md** gap 12 still said phase 20 was "in `todo/`… admin-link half has landed" and attributed the wrap to the old fixed 58 px row — rewritten to the completed state (three rows/un-sticky/slide/both links; 24 still todo; 21's `/certs` `.scroll` noted as not reaching `/`'s table). **README.md** `web/` bullet enumerated the controls row without `⏸ Pause` and omitted `/login` from the two-row list — both fixed. Everything else (setup, commands, env table vs `config.py`, routes vs `api.py`, agent/installer flags, testing policy) verified consistent. - Deviations: no dependency bump — `uv pip list --outdated` still reports the pre-existing advisory `fastapi 0.153.0 → 0.156.4` (deps untouched this phase, validate treats it as advisory); `NOTES.md` is empty in the working tree from an operator edit outside these tasks (both items live in phases 20/24) — left as-is; no live-infra changes made or needed. - Next pending phase: `21_page_cleanups` (pause button removal, `/system` GPU values, `/certs` table scroll). --- .../20_masthead_nav_polish/00_phase.md | 0 .../01_two_row_masthead.md | 0 .../02_unsticky_masthead.md | 0 .../03_hamburger_slide.md | 0 .../04_admin_links_visible.md | 0 .../20_masthead_nav_polish__00_phase.a1.err | 0 .../20_masthead_nav_polish__00_phase.a1.md | 9 + ...masthead_nav_polish__00_phase.a1.md.status | 1 + ..._masthead_nav_polish__00_phase.a1.validate | 65 ++++ ...ead_nav_polish__01_two_row_masthead.a1.err | 0 ...head_nav_polish__01_two_row_masthead.a1.md | 21 ++ ...v_polish__01_two_row_masthead.a1.md.status | 1 + ...av_polish__01_two_row_masthead.a1.validate | 65 ++++ ...ad_nav_polish__02_unsticky_masthead.a1.err | 0 ...ead_nav_polish__02_unsticky_masthead.a1.md | 14 + ..._polish__02_unsticky_masthead.a1.md.status | 1 + ...v_polish__02_unsticky_masthead.a1.validate | 65 ++++ ...head_nav_polish__03_hamburger_slide.a1.err | 0 ...thead_nav_polish__03_hamburger_slide.a1.md | 15 + ...av_polish__03_hamburger_slide.a1.md.status | 1 + ...nav_polish__03_hamburger_slide.a1.validate | 65 ++++ ..._nav_polish__04_admin_links_visible.a1.err | 0 ...d_nav_polish__04_admin_links_visible.a1.md | 15 + ...olish__04_admin_links_visible.a1.md.status | 1 + ...polish__04_admin_links_visible.a1.validate | 65 ++++ AGENTS.md | 111 ++++-- README.md | 25 +- src/dashboards/web/pages/certs.html | 10 +- src/dashboards/web/pages/settings.html | 5 +- src/dashboards/web/static/app.js | 3 +- src/dashboards/web/static/dash.css | 184 +++++++--- tests/test_api.py | 324 ++++++++++++++++-- 32 files changed, 960 insertions(+), 106 deletions(-) rename .agents/phases/{todo => complete}/20_masthead_nav_polish/00_phase.md (100%) rename .agents/phases/{todo => complete}/20_masthead_nav_polish/01_two_row_masthead.md (100%) rename .agents/phases/{todo => complete}/20_masthead_nav_polish/02_unsticky_masthead.md (100%) rename .agents/phases/{todo => complete}/20_masthead_nav_polish/03_hamburger_slide.md (100%) rename .agents/phases/{todo => complete}/20_masthead_nav_polish/04_admin_links_visible.md (100%) create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.err create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.md create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.md.status create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.validate create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.err create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.md create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.md.status create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.validate create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.err create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.md create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.md.status create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.validate create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.err create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.md create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.md.status create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.validate create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.err create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.md create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.md.status create mode 100644 .agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.validate diff --git a/.agents/phases/todo/20_masthead_nav_polish/00_phase.md b/.agents/phases/complete/20_masthead_nav_polish/00_phase.md similarity index 100% rename from .agents/phases/todo/20_masthead_nav_polish/00_phase.md rename to .agents/phases/complete/20_masthead_nav_polish/00_phase.md diff --git a/.agents/phases/todo/20_masthead_nav_polish/01_two_row_masthead.md b/.agents/phases/complete/20_masthead_nav_polish/01_two_row_masthead.md similarity index 100% rename from .agents/phases/todo/20_masthead_nav_polish/01_two_row_masthead.md rename to .agents/phases/complete/20_masthead_nav_polish/01_two_row_masthead.md diff --git a/.agents/phases/todo/20_masthead_nav_polish/02_unsticky_masthead.md b/.agents/phases/complete/20_masthead_nav_polish/02_unsticky_masthead.md similarity index 100% rename from .agents/phases/todo/20_masthead_nav_polish/02_unsticky_masthead.md rename to .agents/phases/complete/20_masthead_nav_polish/02_unsticky_masthead.md diff --git a/.agents/phases/todo/20_masthead_nav_polish/03_hamburger_slide.md b/.agents/phases/complete/20_masthead_nav_polish/03_hamburger_slide.md similarity index 100% rename from .agents/phases/todo/20_masthead_nav_polish/03_hamburger_slide.md rename to .agents/phases/complete/20_masthead_nav_polish/03_hamburger_slide.md diff --git a/.agents/phases/todo/20_masthead_nav_polish/04_admin_links_visible.md b/.agents/phases/complete/20_masthead_nav_polish/04_admin_links_visible.md similarity index 100% rename from .agents/phases/todo/20_masthead_nav_polish/04_admin_links_visible.md rename to .agents/phases/complete/20_masthead_nav_polish/04_admin_links_visible.md diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.err b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.md b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.md new file mode 100644 index 0000000..f6ab0b5 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.md @@ -0,0 +1,9 @@ +**Phase 20 final verification pass — all four tasks already complete; verified in full.** + +- Verified: three-row masthead CSS (`.masthead` flex-wrap + two `flex-basis: 100%`, `min-height`, no `margin-inline-start`), un-sticky `position: relative` + `z-index: 40`, 180 ms `visibility/opacity/transform` slide (closed `nav.tabs` / open `.is-open`, both states in the file's single reduced-motion block), both admin links on all 8 signed-in pages, `/login` neither. +- `uv run pytest` → **1464 passed, 15 skipped** · `uv run pytest --cov --cov-fail-under=94` → **98.94%** (floor 94, unchanged) · `ruff check src tests agent` clean · `pyright` 0 errors · **`bash .agents/validate.sh` EXIT=0** (incl. `node --check` + repo hygiene) · `uv run pytest -m live --live` → **11 passed**. +- Browser (live dev server, invariant 10): `20-task01-rows.cjs` 121 PASS/0 FAIL · `20-task02-unsticky.cjs` 81/0 · `20-task03-slide.cjs` 32/0 · `20-task04-admin-links.cjs` 87/0 · phase-18 `nav-final-pass.cjs` 45/45, `nav-task03-verify` 77, `nav-task04-verify` 24/24 · phase-19 scripts 11/22/24/29 all pass. Screenshots refreshed in `data/logs/` (`masthead-rows*`, `masthead-unsticky*`, `slide-*`, `admin-links-*`). Dev server restarted; `/api/health` all nine sources `ok`. +- Completion criteria: 3-row bar @1280/360 ✓ · scroll leaves bar behind + dropdown under whole bar ✓ · slide out/in 180 ms, reduced-motion snap ✓ · `/certs`→`⚷ Certs`, `/settings`→`⚙ Account` before theme toggle, outside `.tabs` ✓ · pytest/coverage/ruff/pyright ✓ · validate ✓ · no regression in tab structure, token reveal, safe-area padding, four close paths, status dots, theme toggle ✓. +- Docs: **AGENTS.md** gap 12 still said phase 20 was "in `todo/`… admin-link half has landed" and attributed the wrap to the old fixed 58 px row — rewritten to the completed state (three rows/un-sticky/slide/both links; 24 still todo; 21's `/certs` `.scroll` noted as not reaching `/`'s table). **README.md** `web/` bullet enumerated the controls row without `⏸ Pause` and omitted `/login` from the two-row list — both fixed. Everything else (setup, commands, env table vs `config.py`, routes vs `api.py`, agent/installer flags, testing policy) verified consistent. +- Deviations: no dependency bump — `uv pip list --outdated` still reports the pre-existing advisory `fastapi 0.153.0 → 0.156.4` (deps untouched this phase, validate treats it as advisory); `NOTES.md` is empty in the working tree from an operator edit outside these tasks (both items live in phases 20/24) — left as-is; no live-infra changes made or needed. +- Next pending phase: `21_page_cleanups` (pause button removal, `/system` GPU values, `/certs` table scroll). diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.md.status b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.validate b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.validate new file mode 100644 index 0000000..e6a266b --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__00_phase.a1.validate @@ -0,0 +1,65 @@ +== uv run pytest --cov (hermetic: no network, live tests skip) == +........................................................................ [ 4%] +........................................................................ [ 9%] +........................................................................ [ 14%] +........................................................................ [ 19%] +........................................................................ [ 24%] +........................................s............................... [ 29%] +........................................................................ [ 34%] +........................................................................ [ 38%] +........................................................................ [ 43%] +........................................................................ [ 48%] +........................................................................ [ 53%] +........................................................................ [ 58%] +........................................................................ [ 63%] +........................................................................ [ 68%] +.....sssssssssss........................................................ [ 73%] +........................................................................ [ 77%] +........................................................................ [ 82%] +........................................................................ [ 87%] +........................................................................ [ 92%] +.....................................................sss................ [ 97%] +....................................... [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 436 0 100% +src/dashboards/app.py 185 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 3490 37 99% +Required test coverage of 94% reached. Total coverage: 98.94% +1464 passed, 15 skipped, 1 warning in 156.47s (0:02:36) +== 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.156.4 wheel diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.err b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.md b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.md new file mode 100644 index 0000000..570c542 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.md @@ -0,0 +1,21 @@ +All green. Final state: + +**Implemented** +- `src/dashboards/web/static/dash.css` — the masthead is now **three left-justified rows at every width** (row 1 brand + `#nav-toggle` + tabs, row 2 `#nav-status` dots, row 3 the `.masthead-right` controls), CSS-only, no markup change: `.masthead` gained `flex-wrap: wrap` + `gap: .35rem 1.1rem` and `height: var(--header-h)` → `min-height`; `#nav-status` and `.masthead-right` each got `flex-basis: 100%` + `justify-content: flex-start`; `.masthead-right` lost `margin-inline-start: auto`. (The task's `flex-direction: column` first option cannot ship — it splits brand and tabs onto separate rows — so the file's own named `flex-wrap` fallback shipped, measured in a browser per the phase-18 rule.) +- ≤640px block re-cut: dropped its `height: auto` / `min-height` / `flex-wrap` machinery (one declaration now, no drift), kept the token reveal, `#nav-status { flex-wrap: nowrap }`, `.masthead-right > * { flex: none }`, the `env(safe-area-inset-top)` line and the dropdown; comments rewritten to the three-row reality. `position: sticky`/`z-index: 40` untouched (task 02's job). +- `tests/test_api.py` — `test_the_masthead_is_three_left_justified_rows_at_every_width` (+ `_rule_body`/`_declarations` helpers) and `test_the_mobile_masthead_block_declares_no_second_bar_height`, both on **served** bytes; mutation-tested (re-adding `height: var(--header-h)`, dropping `#nav-status`'s `flex-basis`, or re-declaring `min-height` in the mobile block each fails the matching test). +- Browser evidence: `data/logs/20-task01-rows.cjs` (7 pages × 1280 px/360 px: row order, left justification, no clipping, no overflow, dropdown under the **whole** bar) — ALL CHECKS PASS; screenshots `data/logs/masthead-rows{1280,360}-*.png` (+ dropdown, login, 768 px). No horizontal overflow at 360/480/641/768/900/1024/1440. + +**Tests / lint / coverage** +- `uv run pytest` → **1460 passed, 15 skipped**; `uv run pytest --cov` → **98.94%** (floor 94%, unchanged); `uv run ruff check src tests agent` → clean; `uv run pyright` → 0 errors; `bash .agents/validate.sh` → **EXIT=0** (incl. `node --check`); `uv run pytest -m live --live` → **11 passed**; `data/logs/nav-final-pass.cjs` → **45/45**. +- Pre-existing advisory from validate: `fastapi 0.153.0 → 0.156.4`. Not touched — this task moved no dependencies (AGENTS rule 8 checks apply after dependency changes). + +**Docs** +- `AGENTS.md`: new "The masthead is three left-justified rows at every width" paragraph (mechanism, why not `column`, the single `min-height`), and the phase-18 hamburger paragraph rewritten (block now tightens gaps instead of wrapping the bar; dropdown sits under the whole bar; nav-final-pass desktop leg noted). +- `README.md`: the `web/` bullet's "above it the masthead stays the one row it has always been" replaced with the three-row description. + +**Decisions/deviations** +- Shipped the task's `flex-wrap` fallback (measured) rather than `flex-direction: column`; `/login` (theme toggle only) now renders a 2-row bar — unavoidable without markup changes, verified clean at both widths. +- Updated the gitignored `data/logs/nav-final-pass.cjs` desktop bound (`<80` → `80–140 px`) since the one-row bar it asserted no longer exists; nav behaviour assertions unchanged and passing. + +**Next pending task:** `.agents/phases/todo/20_masthead_nav_polish/02_unsticky_masthead.md` diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.md.status b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.validate b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.validate new file mode 100644 index 0000000..5b407e9 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__01_two_row_masthead.a1.validate @@ -0,0 +1,65 @@ +== uv run pytest --cov (hermetic: no network, live tests skip) == +........................................................................ [ 4%] +........................................................................ [ 9%] +........................................................................ [ 14%] +........................................................................ [ 19%] +........................................................................ [ 24%] +....................................s................................... [ 29%] +........................................................................ [ 34%] +........................................................................ [ 39%] +........................................................................ [ 43%] +........................................................................ [ 48%] +........................................................................ [ 53%] +........................................................................ [ 58%] +........................................................................ [ 63%] +........................................................................ [ 68%] +.sssssssssss............................................................ [ 73%] +........................................................................ [ 78%] +........................................................................ [ 82%] +........................................................................ [ 87%] +........................................................................ [ 92%] +.................................................sss.................... [ 97%] +................................... [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 436 0 100% +src/dashboards/app.py 185 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 3490 37 99% +Required test coverage of 94% reached. Total coverage: 98.94% +1460 passed, 15 skipped, 1 warning in 153.92s (0:02:33) +== 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.156.4 wheel diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.err b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.md b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.md new file mode 100644 index 0000000..9ad8d87 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.md @@ -0,0 +1,14 @@ +**Task 02 complete — masthead is no longer sticky.** + +- `dash.css` `.masthead`: `position: sticky; top: 0` → `position: relative` (kept `z-index: 40`, so the dropdown's `41` still means "one above the bar"); rewrote the two comments that called the bar sticky (header block + the `nav.tabs.is-open` anchor note). Table-head `sticky; top: 0` (now line 848) untouched. +- `tests/test_api.py`: new `test_the_masthead_scrolls_away_and_stays_the_dropdown_anchor` in the phase-20 layout-pin section — served CSS asserts `position: relative`, no `position: sticky` value, **no `top`**, `z-index: 40`, and that the ≤640px dropdown is still `absolute; top: 100%` stacking above the bar. No pre-existing pin named the declaration, so this is the added one. +- Browser proof (invariant 10): new `data/logs/20-task02-unsticky.cjs` → **80 checks PASS / 0 FAIL** — 7 pages × 1280 px & 360 px, bar scrolls off the top (plus a squeezed-height leg for pages that fit a tall viewport), nothing pinned to the viewport but a `thead`, 360 px dropdown still opens flush under the whole three-row bar, is anchored to `.masthead`, and still hangs off it after a scroll. Screenshots `data/logs/masthead-unsticky*.png` + log. +- Fixed task 01's stale verification script (`data/logs/20-task01-rows.cjs` still demanded `position === "sticky"`) and a dangling "At" task 01 left in README's `web/` bullet. + +**Results:** `uv run pytest` → **1461 passed, 15 skipped**; `bash .agents/validate.sh` → **EXIT=0** (coverage **98.94%** vs 94% floor, `ruff check src tests agent` clean, `pyright` 0 errors, `node --check` of `web/static/*.js` clean); `uv run pytest -m live --live` → **11 passed**. + +**Docs:** AGENTS.md masthead paragraph now records the un-stick (why `relative` not `static`, the menu scrolls with the bar, the only remaining sticky is a table head, the "Chrome computes `top: 0px` either way" note explaining why the offset is a CSS pin), and cites three phase-20 tests + the new script. README's `web/` bullet says the bar is not sticky and why the dropdown stays anchored. + +**Decisions/deviations:** `uv pip list --outdated` still lists `fastapi 0.153.0 → 0.156.4` — pre-existing, deps untouched by this task, so validate treats it as advisory; deliberately not bumped mid-phase. + +**Next pending task:** `20_masthead_nav_polish/03_hamburger_slide.md` (180 ms dropdown slide, stilled under `prefers-reduced-motion`). diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.md.status b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.validate b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.validate new file mode 100644 index 0000000..9f50979 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__02_unsticky_masthead.a1.validate @@ -0,0 +1,65 @@ +== uv run pytest --cov (hermetic: no network, live tests skip) == +........................................................................ [ 4%] +........................................................................ [ 9%] +........................................................................ [ 14%] +........................................................................ [ 19%] +........................................................................ [ 24%] +.....................................s.................................. [ 29%] +........................................................................ [ 34%] +........................................................................ [ 39%] +........................................................................ [ 43%] +........................................................................ [ 48%] +........................................................................ [ 53%] +........................................................................ [ 58%] +........................................................................ [ 63%] +........................................................................ [ 68%] +..sssssssssss........................................................... [ 73%] +........................................................................ [ 78%] +........................................................................ [ 82%] +........................................................................ [ 87%] +........................................................................ [ 92%] +..................................................sss................... [ 97%] +.................................... [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 436 0 100% +src/dashboards/app.py 185 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 3490 37 99% +Required test coverage of 94% reached. Total coverage: 98.94% +1461 passed, 15 skipped, 1 warning in 155.60s (0:02:35) +== 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.156.4 wheel diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.err b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.md b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.md new file mode 100644 index 0000000..85f2877 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.md @@ -0,0 +1,15 @@ +**Task 03 complete — the ≤640px menu slides out and back in.** + +- `dash.css`: `nav.tabs` is now the **closed** dropdown (geometry + `visibility: hidden; opacity: 0; transform: translateY(-8px); pointer-events: none` + `transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 180ms`); `.is-open` flips only those four to their visible counterparts with the `visibility` delay dropped — the `brain_of_reese` `.app-nav` pair. `display: none` had to go (an unrendered box has no intermediate state). +- Merged `nav.tabs, nav.tabs.is-open { transition: none; }` into the file's **only** `prefers-reduced-motion` block (both states named, phase-46 lesson); `app.js` unchanged except one stale comment. +- Tests: 3 new served-CSS pins (closed start / visible end / both-states reduced motion + "no `display: none` may come back") and a `_cascade` helper; two stale pins (phase-18 `display:none`, task-02 dropdown geometry) updated to where the declarations now live. +- Browser (invariant 10): new `data/logs/20-task03-slide.cjs` → **all checks pass** (computed closed state, mid-transition frame at ≈60 ms in *both* directions, 4 close paths + Esc focus, reduced-motion snaps `prop=none`, desktop tabs untouched, hidden menu adds **no** scroll area); screenshots `data/logs/slide-*.png`. +- Also repaired four stale verification probes the change invalidated: `nav-final-pass.cjs` 45/45, `nav-task03-verify.cjs` 77, `nav-task04-verify.cjs` 24, `20-task01-rows.cjs` all pass. + +**Results:** `uv run pytest` → **1464 passed, 15 skipped**; `bash .agents/validate.sh` → **EXIT=0** (coverage **98.94%** vs 94% floor, `ruff check src tests agent` clean, `pyright` 0 errors, `node --check web/static/*.js` clean); `uv run pytest -m live --live` → **11 passed**. + +**Docs:** AGENTS.md nav paragraph rewritten (one box / two states, why `visibility` not `display`, the delayed-then-undelayed `visibility`, both-states reduced motion, the new pins + script; test count 1458→1464); README's `web/` bullet now says the menu slides 180 ms and stills under `prefers-reduced-motion`. + +**Decisions/deviations:** followed the task's prescribed visibility pattern rather than `@starting-style`/`allow-discrete` (wider support, it is the referenced implementation); the explicit reduced-motion pair is redundant with the universal `* …!important` rule and kept deliberately — the task mandates naming both states. `uv pip list --outdated` still lists `fastapi 0.153.0 → 0.156.4`: pre-existing, deps untouched, not bumped mid-phase. + +**Next pending task:** `20_masthead_nav_polish/04_admin_links_visible.md`. diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.md.status b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.validate b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.validate new file mode 100644 index 0000000..42bafdd --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__03_hamburger_slide.a1.validate @@ -0,0 +1,65 @@ +== uv run pytest --cov (hermetic: no network, live tests skip) == +........................................................................ [ 4%] +........................................................................ [ 9%] +........................................................................ [ 14%] +........................................................................ [ 19%] +........................................................................ [ 24%] +........................................s............................... [ 29%] +........................................................................ [ 34%] +........................................................................ [ 38%] +........................................................................ [ 43%] +........................................................................ [ 48%] +........................................................................ [ 53%] +........................................................................ [ 58%] +........................................................................ [ 63%] +........................................................................ [ 68%] +.....sssssssssss........................................................ [ 73%] +........................................................................ [ 77%] +........................................................................ [ 82%] +........................................................................ [ 87%] +........................................................................ [ 92%] +.....................................................sss................ [ 97%] +....................................... [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 436 0 100% +src/dashboards/app.py 185 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 3490 37 99% +Required test coverage of 94% reached. Total coverage: 98.94% +1464 passed, 15 skipped, 1 warning in 156.99s (0:02:36) +== 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.156.4 wheel diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.err b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.md b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.md new file mode 100644 index 0000000..6eff925 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.md @@ -0,0 +1,15 @@ +**Task 04 complete — both admin links stay visible on the admin pages.** + +- `pages/certs.html`: added `⚷ Certs` (order is clock → `⚷ Certs` → `⚙ Account` → theme) and rewrote the comment that justified its absence; also gave its theme toggle the `aria-pressed="false"` the other seven pages carry (pre-existing markup drift, now pinned). +- `pages/settings.html`: added `⚙ Account` between Certs and the theme toggle; comment now says "this one included". +- `tests/test_api.py::test_every_page_links_to_both_admin_pages` rewritten: **one** loop over all eight signed-in pages asserting both full tags, both before `id="theme-toggle"`, Certs-before-Account, neither inside `.tabs`, and an identical theme-toggle tag; the two "links to itself" assertions deleted; added the `/login`-renders-neither assertion via an unsigned client (mutation-checked: removing the link fails with "no route to the certificate authority"). +- Browser (invariant 10): new `data/logs/20-task04-admin-links.cjs` → **86/86 PASS** (cluster rendered + in order at 1280 px and 360 px, self-link click = exactly one `200` document, no redirect, page/cluster unchanged, cross-links still navigate, `/login` neither link); screenshots `data/logs/admin-links-{1280,360}-{certs,settings}.png` (the pair is byte-identical — the clusters now are) + `admin-links-login.png`. +- Fixed a stale probe from task 02 (`20-task02-unsticky.cjs`): its fixed 700 ms sleep raced first paint and reported "max scroll 0"; it now settles on document height and asserts the bar rides the scroll by exactly the scrolled distance — 3 consecutive green runs. `20-task01/03`, `nav-final-pass` (45/45), `nav-task04-verify` (24/24) also re-run green. + +**Results:** `uv run pytest` → **1464 passed, 15 skipped**; coverage **98.94 %** (floor 94 %); `ruff check src tests agent` clean; `pyright` 0 errors; `bash .agents/validate.sh` → **EXIT=0** (incl. `node --check`); `uv run pytest -m live --live` → **11 passed**; dev server restarted, `/api/health` all nine sources `ok`. + +**Docs:** AGENTS.md — both admin-page paragraphs rewritten to the new rule (self-link is a no-op; one loop over eight; `/login` the exception), gap 11's `/certs` line and gap 12's "which that phase is the decision on" resolved. README — intro, *The account page*, and the `web/` masthead bullet now say both links are on all eight signed-in pages. + +**Deviations:** touched the `/certs` theme toggle's `aria-pressed` (defect fix, in service of "identical controls"; no behaviour change); `uv pip list --outdated` still lists `fastapi 0.13 → 0.156` — pre-existing advisory, deps untouched, not bumped mid-phase. + +**Next pending task:** `21_page_cleanups/01_remove_pause_button.md` (phase 20 is now complete). diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.md.status b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.validate b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.validate new file mode 100644 index 0000000..5066ee2 --- /dev/null +++ b/.agents/reports/20_masthead_nav_polish/20_masthead_nav_polish__04_admin_links_visible.a1.validate @@ -0,0 +1,65 @@ +== uv run pytest --cov (hermetic: no network, live tests skip) == +........................................................................ [ 4%] +........................................................................ [ 9%] +........................................................................ [ 14%] +........................................................................ [ 19%] +........................................................................ [ 24%] +........................................s............................... [ 29%] +........................................................................ [ 34%] +........................................................................ [ 38%] +........................................................................ [ 43%] +........................................................................ [ 48%] +........................................................................ [ 53%] +........................................................................ [ 58%] +........................................................................ [ 63%] +........................................................................ [ 68%] +.....sssssssssss........................................................ [ 73%] +........................................................................ [ 77%] +........................................................................ [ 82%] +........................................................................ [ 87%] +........................................................................ [ 92%] +.....................................................sss................ [ 97%] +....................................... [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 436 0 100% +src/dashboards/app.py 185 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 3490 37 99% +Required test coverage of 94% reached. Total coverage: 98.94% +1464 passed, 15 skipped, 1 warning in 155.86s (0:02:35) +== 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.156.4 wheel diff --git a/AGENTS.md b/AGENTS.md index 5590ff2..b0e0c11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -689,8 +689,8 @@ Responsibilities, one line each: name. `/settings` (`pages/settings.html` + `static/page-settings.js`) is a **page and not a sixth dashboard**: same `` and masthead, the five tabs with **no** `aria-current` (it is not one of them), no status dots, and it is reached from the `⚙ Account` link in - `.masthead-right` of every dashboard — outside `.tabs`, so the pinned tab structure is untouched - (`tests/test_api.py::test_every_page_links_to_both_admin_pages`). It prints + `.masthead-right` of every dashboard **including itself** — outside `.tabs`, so the pinned tab + structure is untouched (`tests/test_api.py::test_every_page_links_to_both_admin_pages`). It prints `/api/auth/status` (name, expiry as a local timestamp *and* as time left), signs out through `POST /api/auth/logout`, and changes the password through `POST /api/auth/password` after checking locally what it can judge itself (non-empty current, ≥ 8 new characters — @@ -705,8 +705,15 @@ Responsibilities, one line each: any `401` bounce. `/certs` (`pages/certs.html` + `static/page-certs.js`) is the second of those admin pages, and it is the same shape for the same reasons: five tabs with **no** `aria-current`, no `#nav-status`, - and the `⚷ Certs` masthead link beside `⚙ Account` (neither admin page links to itself, which is - why the pinned two-link audit loops over two different tuples). Everything on it is + and both masthead links — `⚷ Certs` beside `⚙ Account` — exactly as the five dashboards render + them. **Both links are on both admin pages** (phase 20 task 04, `TODO.md:4`), which retires phase + 15's "no self-link" precedent: chrome that changes shape depending on where you already are reads + as a broken button, while a link to the page you are on is a no-op that answers with the page you + were looking at (`data/logs/20-task04-admin-links.cjs` measures exactly that — one document + response, no redirect, the same cluster afterwards). The two links therefore sit on all eight + signed-in pages in one order (clock → `⚷ Certs` → `⚙ Account` → theme toggle), which is what lets + the pinned audit be one loop over all eight instead of two over different tuples; `/login` stays + the one page with neither. Everything on it is `GET /api/certs`: the Root CA block, the display-only Dashboard client line (that certificate is minted with the root and has no route of its own), and one row per host with its status chip, serial, expiry (local timestamp *and* `Dash.duration` left), issued-vs-deployed `bundle_rev`, @@ -739,6 +746,41 @@ Responsibilities, one line each: `severity` verdict the API sent (`tests/test_api.py` fails if a page script names it again). A value the API should rate gets a row in `severity.py`, never a bound in JS. + **The masthead is three left-justified rows at every width** (phase 20 task 01, `TODO.md:5-6`): + row 1 the brand, `#nav-toggle` and the five tabs; row 2 the `#nav-status` per-source dots; row 3 + the `.masthead-right` controls (clock, `⏸ Pause`, `⚷ Certs`, `⚙ Account`, theme). That is CSS and + no markup change, which decides the mechanism: `#nav-status` is a **child** of + `.masthead-right`, so there is no wrapper to promote, and the rows are line breaks — `.masthead` + is `flex-wrap: wrap`, and both `#nav-status` and `.masthead-right` carry `flex-basis: 100%` with + `justify-content: flex-start` (line breaking uses a flex item's **base** size, before shrink, so + a 100 % item is alone on its line: the div's break starts row 2 of the bar and the dots' starts + row 2 inside it, leaving the controls to wrap to row 3). `.masthead-right` lost the + `margin-inline-start: auto` that pinned the cluster to the right edge of the one-row bar. + `flex-direction: column` was the other option the task allowed and is the wrong one: it puts the + brand and the tabs on separate rows. `height: var(--header-h)` became **`min-height`** (three rows + do not fit in 58 px and a fixed height clips the controls) and is declared **once** — the ≤640px + block no longer restates `height`/`min-height`/`flex-wrap`, and that pair drifting is exactly what + would clip the bar on a phone only. A page with no dots (`/certs`, `/settings`) renders two rows, + `/login` two (brand, then the theme toggle). Pinned on the served bytes by the three phase-20 + tests in `tests/test_api.py` (the three rules' declarations, the mobile block's silence about + the bar's height, and the bar's `position`), and measured in a browser at 1280 px and 360 px by + `data/logs/20-task01-rows.cjs` (seven pages × two widths plus the dropdown; screenshots + `data/logs/masthead-rows*.png`). + **The bar is not sticky** (phase 20 task 02, `TODO.md:10` — "It shouldn't be sticky at all. We + want to maximize the amount of screen space for data viewing"): `position: sticky; top: 0` became + **`position: relative`** with no offset, so the whole masthead scrolls away with the page and the + phone's "sticky only part way down" quirk — the fixed-height sticky bar fighting the wrapped + rows — went with it. `relative` rather than the default `static` because `.masthead` is still the + positioned ancestor the ≤640px dropdown's `top: 100%` resolves against (a `static` bar moves the + menu to the top of the document), and `z-index: 40` stays so the menu's `41` still means one + above the bar; the consequence is that the open menu scrolls with the bar, which is what an + unpinned bar means. The only `position: sticky` left in the sheet is a table's own `thead`, inside + its own scroll container, and this task is not it. Proven by `data/logs/20-task02-unsticky.cjs` + (seven pages scrolled at 1280 px and 360 px — with a squeezed-height leg for the pages that fit + a tall viewport — plus the 360 px dropdown still landing directly under the bar and still hanging + off it after a scroll; screenshots `data/logs/masthead-unsticky*.png`). Note that a browser cannot + tell `relative` from `relative; top: 0` — Chrome computes `top: 0px` for either — so the absence + of the offset is a served-CSS pin, not a measurement. **The tab nav collapses into a hamburger at ≤640px** (phase 18): the seven tabbed pages each carry `#nav-toggle` (`class="ghost nav-toggle"`, `type="button"`, `aria-controls="nav-tabs"`, `aria-label="Toggle navigation"`, `☰`) between `.brand` and `