phase: 25_masthead_control_placement

**Phase 25 final verification pass** (all 3 tasks were already in `complete/`, so this was the phase-level verify + docs pass)

**Verified**
- `#nav-status` first child of `.masthead-right` on all 7 signed-in templates (incl. `/certs`, `/settings`); `app.js`/`charts.js` untouched
- `#nav-status` + `.masthead-right` both `justify-content: flex-end`, `margin-inline-start` still absent, row-break (`flex-wrap` + two `flex-basis: 100%`), non-sticky bar, dropdown slide + 4 close paths, `setInterval(refreshStatus, 15000)` all intact (dash.css diff = comments + the two alignment values)
- Clock is the last control on all 7 templates; no `order:` anywhere in the bar

**Fixed (defects found in this pass)**
- `tests/test_api.py::test_every_page_links_to_both_admin_pages` — duplicated `tabs = html[...]` statement (task 03 edit artifact)
- `certs.html` / `settings.html` comments said "all **eight** signed-in pages"; the signed-in set is seven (`/login` is the eighth, anonymous, and has no links/clock)
- `AGENTS.md` — suite tally `1542 → 1543 passed`; "the three phase-20 tests" re-worded, since task 02 renamed one to `test_the_masthead_is_three_rows_right_justified_at_every_width`

**Results**
- `uv run pytest` → **1543 passed, 15 skipped**, 0 failed
- `bash .agents/validate.sh` → **exit 0**: coverage **98.94 %** (floor 94 %), `ruff` All checks passed, `pyright` 0 errors, `node --check` clean, hygiene clean
- `uv run pytest -m live --live` (`.env.dev` sourced) → **11 passed**; `./scripts/dev-server.sh status` → 9/9 sources `ok`
- Browser: `node data/logs/25-task03-verify.cjs` → **235 checks, 0 FAIL**, exit 0 (`data/logs/25-task03-verify.out`, 16 `masthead-25-*.png`)

**Completion criteria**: badges pinned+measured (green `system/disks/llm/ups` on both admin pages, 1280 px and 360 px) ✅ · both rows flush right, three rows + dropdown unchanged (pinned + measured) ✅ · clock farthest-right (pinned markup; measured right edge 1260.0 at 1280 px, ≤900 px hidden by the long-standing readout rule with source order asserted) ✅ · pytest/coverage/ruff/pyright ✅ · validate.sh ✅ · no behaviour change in completed work ✅

**Docs**: README reviewed end-to-end against the code (served-route audit vs documented routes, env table vs `config.py`, commands vs `scripts/`, public-path table, `web/` masthead bullet) — consistent, no changes needed beyond the phase's own edits. AGENTS.md — two fixes above; spot-checked every doc-named test, function, script, threshold, `install-agent.sh` sha256 (`ffbcd91f…` matches) and agent version (1.1.0).

**Deviations**: left the pre-existing `fastapi 0.153.0 → 0.159.0` advisory (validate prints it as advisory; no dependency work in scope, AGENTS rule 8 triggers on touching deps). No live-infra changes. Overview's "all eight signed-in pages" for the clock is verified against seven templates + `/login` (which has no clock, pinned).

**Next pending phase**: `.agents/phases/todo/26_ups_event_split/` (3 tasks).
This commit is contained in:
2026-10-21 16:42:14 -04:00
parent 558a83d0be
commit 2bf236fd92
31 changed files with 559 additions and 75 deletions
@@ -0,0 +1,25 @@
**Phase 25 final verification pass** (all 3 tasks were already in `complete/`, so this was the phase-level verify + docs pass)
**Verified**
- `#nav-status` first child of `.masthead-right` on all 7 signed-in templates (incl. `/certs`, `/settings`); `app.js`/`charts.js` untouched
- `#nav-status` + `.masthead-right` both `justify-content: flex-end`, `margin-inline-start` still absent, row-break (`flex-wrap` + two `flex-basis: 100%`), non-sticky bar, dropdown slide + 4 close paths, `setInterval(refreshStatus, 15000)` all intact (dash.css diff = comments + the two alignment values)
- Clock is the last control on all 7 templates; no `order:` anywhere in the bar
**Fixed (defects found in this pass)**
- `tests/test_api.py::test_every_page_links_to_both_admin_pages` — duplicated `tabs = html[...]` statement (task 03 edit artifact)
- `certs.html` / `settings.html` comments said "all **eight** signed-in pages"; the signed-in set is seven (`/login` is the eighth, anonymous, and has no links/clock)
- `AGENTS.md` — suite tally `1542 → 1543 passed`; "the three phase-20 tests" re-worded, since task 02 renamed one to `test_the_masthead_is_three_rows_right_justified_at_every_width`
**Results**
- `uv run pytest` → **1543 passed, 15 skipped**, 0 failed
- `bash .agents/validate.sh` → **exit 0**: coverage **98.94 %** (floor 94 %), `ruff` All checks passed, `pyright` 0 errors, `node --check` clean, hygiene clean
- `uv run pytest -m live --live` (`.env.dev` sourced) → **11 passed**; `./scripts/dev-server.sh status` → 9/9 sources `ok`
- Browser: `node data/logs/25-task03-verify.cjs` → **235 checks, 0 FAIL**, exit 0 (`data/logs/25-task03-verify.out`, 16 `masthead-25-*.png`)
**Completion criteria**: badges pinned+measured (green `system/disks/llm/ups` on both admin pages, 1280 px and 360 px) ✅ · both rows flush right, three rows + dropdown unchanged (pinned + measured) ✅ · clock farthest-right (pinned markup; measured right edge 1260.0 at 1280 px, ≤900 px hidden by the long-standing readout rule with source order asserted) ✅ · pytest/coverage/ruff/pyright ✅ · validate.sh ✅ · no behaviour change in completed work ✅
**Docs**: README reviewed end-to-end against the code (served-route audit vs documented routes, env table vs `config.py`, commands vs `scripts/`, public-path table, `web/` masthead bullet) — consistent, no changes needed beyond the phase's own edits. AGENTS.md — two fixes above; spot-checked every doc-named test, function, script, threshold, `install-agent.sh` sha256 (`ffbcd91f…` matches) and agent version (1.1.0).
**Deviations**: left the pre-existing `fastapi 0.153.0 → 0.159.0` advisory (validate prints it as advisory; no dependency work in scope, AGENTS rule 8 triggers on touching deps). No live-infra changes. Overview's "all eight signed-in pages" for the clock is verified against seven templates + `/login` (which has no clock, pinned).
**Next pending phase**: `.agents/phases/todo/26_ups_event_split/` (3 tasks).
@@ -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%]
........................................................................ [ 60%]
........................................................................ [ 64%]
........................................................................ [ 69%]
.sssssssssss............................................................ [ 73%]
........................................................................ [ 78%]
........................................................................ [ 83%]
........................................................................ [ 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 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%
1543 passed, 15 skipped, 1 warning in 163.67s (0:02:43)
== 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.0 wheel
@@ -0,0 +1,25 @@
All green. Report:
**Implemented (phase 25 task 01)**
- `src/dashboards/web/pages/certs.html` + `settings.html`: added `<div id="nav-status" class="gap" aria-live="polite"></div>` as the first child of `.masthead-right` (same position as the five dashboards); deleted settings.html's "No `#nav-status`…" comment whose premise the operator overruled.
- `app.js`: **unchanged** — `initChrome()`'s `if (nav)` block builds the four badges from `/api/health` whenever the container exists.
- `tests/test_api.py`: flipped the two no-dots pins (`/settings`, `/certs`) to dots-present + re-worded docstrings/messages; added `test_every_signed_in_page_carries_the_fleet_badges` (one `MARKUP_PAGES` loop pinning the exact element as first child of `.masthead-right`, plus the `app.js` container guard, the four badge labels and the untouched 15 s refresh).
- Browser proof: `data/logs/25-task01-badges.cjs` (headless, `20-task01-rows.cjs` convention) → **ALL PASS (90 checks)**, transcript `data/logs/25-task01-badges.out`, screenshots `data/logs/25-task01-{overview,settings,certs}-badges.png` — badges populate `system=ok disks=ok llm=ok ups=ok` on `/certs` and `/settings`, identical to `/`, at 1280 px and 360 px; `/login` (unsigned) still has none.
- Dev server restarted; `./scripts/dev-server.sh status` = 9 sources `ok`.
**Tests / lint / coverage**
- `uv run pytest` → `1543 passed, 15 skipped` (was 1542+15; +1 new test), exit 0
- `uv run pytest --cov` → `TOTAL 3495 37 99%`, "Required test coverage of 94% reached. Total coverage: 98.94%" (no Python source lines added)
- `uv run ruff check src tests agent` → All checks passed · `uv run pyright` → 0 errors
- `bash .agents/validate.sh` → exit 0 (pytest+cov, ruff, pyright, `node --check`); only advisory: `fastapi 0.153.0 → 0.158.1` outdated — dependencies untouched by this task, so not bumped
**Docs**
- `AGENTS.md`: `/settings` "no status dots" → the four badges (with the TODO/`initChrome` reason); `/certs` "no `#nav-status`" → badges like every masthead; the three-rows paragraph now says every masthead page renders three rows and `/login` remains the only two-row page.
- `README.md`: status dots are on **every** signed-in page; the "a page with no indicators — `/login`, `/settings`, `/certs`" clause narrowed to `/login` only. Left-alignment wording deliberately left for task 02.
**Notes / deviations**
- Kept task 01 strictly placement-of-container: no CSS, no `app.js`, no order change (tasks 02/03 own right-justification and clock-rightmost). New pin asserts "first child", which survives task 03's clock move.
- Pre-existing working-tree change I did not make and left alone: `NOTES.md` is emptied (the two items consumed by phases 20/24).
- The interactive-browser hook fired but no visible session was needed — this phase's mandate is the repo's own headless `data/logs/*.cjs` verification, which ran.
**Next pending task:** `25_masthead_control_placement/02_right_justify_masthead_rows.md`
@@ -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%]
........................................................................ [ 60%]
........................................................................ [ 64%]
........................................................................ [ 69%]
.sssssssssss............................................................ [ 73%]
........................................................................ [ 78%]
........................................................................ [ 83%]
........................................................................ [ 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 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%
1543 passed, 15 skipped, 1 warning in 161.44s (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.158.1 wheel
@@ -0,0 +1,15 @@
**Implemented (phase 25 task 02)**
- `src/dashboards/web/static/dash.css`: `#nav-status` and `.masthead-right` `justify-content: flex-start` → `flex-end`; `margin-inline-start` still absent from the cluster; row-break (`flex-wrap` + both `flex-basis: 100%`), `min-height`, gaps and the whole ≤640px block (incl. `#nav-toggle { margin-inline-start: auto }`) untouched.
- Re-worded every comment that stated the old alignment: the header block, `Row 3:`, `Row 2:`, the mobile block intro, and the mobile "row alignment is the base rule's …" note.
- `tests/test_api.py`: renamed the pin to `test_the_masthead_is_three_rows_right_justified_at_every_width`, flipped only the two alignment assertions to `flex-end`, re-worded docstring/messages; all row-break/height/gap/no-`margin-inline-start` guards kept. The mobile-height and unsticky pins are unchanged and green.
- Measured half: `data/logs/25-task02-justify.cjs` (7 signed-in pages × 1280/360 px + unsigned `/login`) — both rows compute `flex-end` and sit flush with the bar's right padding edge (0.00 px gap), 4 badges per row, still three rows, dropdown still under the whole bar, no sideways scroll: **ALL CHECKS PASSED**; screenshots `data/logs/25-task02-{index,certs,settings}-{1280,360}.png`, `25-task02-login-1280.png`, transcript `25-task02-justify.out`.
**Tests / lint / coverage**
- `bash .agents/validate.sh` → EXIT=0: `1543 passed, 15 skipped`, **coverage 98.94 %** (floor 94 %), `ruff check src tests agent` All checks passed, `pyright` 0 errors, `node --check web/static/*.js` clean.
- `uv run pytest -m live --live` (with `.env.dev`) → **11 passed**; `./scripts/dev-server.sh status` → 9/9 sources `ok`, served `/static/dash.css` carries the two `flex-end` declarations.
**Docs**: `AGENTS.md` masthead paragraph now reads "three rows … the two control rows flush right", gained a phase-25-task-02 paragraph naming the renamed pin + the browser script, and gap 12's "three left-justified rows" wording fixed; `README.md`'s masthead sentence likewise. Clock-order sentences left for task 03.
**Notes/deviations**: no scope change (clock move not started). Pre-existing, untouched: `validate.sh` advisory `fastapi 0.153.0 → 0.158.1` (no dependency work in this task) and `NOTES.md` already emptied by earlier tasks in the working tree.
**Next pending task**: `.agents/phases/todo/25_masthead_control_placement/03_clock_rightmost.md`.
@@ -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%]
........................................................................ [ 60%]
........................................................................ [ 64%]
........................................................................ [ 69%]
.sssssssssss............................................................ [ 73%]
........................................................................ [ 78%]
........................................................................ [ 83%]
........................................................................ [ 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 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%
1543 passed, 15 skipped, 1 warning in 162.55s (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.158.1 wheel
@@ -0,0 +1,16 @@
**Implemented (phase 25 task 03)**
- All 7 templates (`index/system/disks/llm/ups/settings/certs.html`): `<span class="clock" id="clock">` moved from first control to **last** child of `.masthead-right`, so the row reads `⚷ Certs` → `⚙ Account` → `☾ Dark` → clock; the seven control-row blocks are byte-identical (md5-verified), `#nav-status` stays first child.
- `dash.css`: header block + `Row 3:` comment re-worded to the new order and to say the clock's place is markup, not a CSS `order:`. No declaration changed.
- `tests/test_api.py::test_every_page_links_to_both_admin_pages`: docstring order sentence updated, both `before_theme` assertions kept, added `html.index('id="clock"') > html.index('id="theme-toggle"')` per page, plus a guard that `.masthead`/`.masthead-right`/`#nav-status`/`.clock` carry **no** `order:` declaration (the task's ASSUMPTION, made enforceable). `/login` half untouched.
- `data/logs/25-task03-verify.cjs` + `.out` + 16 screenshots `data/logs/masthead-25-*.png`: 7 pages × 1280/360 px + `/login` unsigned at both — three rows, both right-hand rows flush with the bar's right padding edge (measured on the chips' content, not the full-basis row box, and explicitly *not* flush left), clock the rightmost painted thing on the bar, cluster source order, four populated badges on `/certs`/`/settings`, 360 px dropdown still under the whole bar, no sideways scroll.
**Tests / lint / coverage**
- `bash .agents/validate.sh` → **1543 passed, 15 skipped**, coverage **98.94 %** (floor 94 %), `ruff check src tests agent` All checks passed, `pyright` 0 errors, `node --check web/static/*.js` clean, hygiene clean (only the pre-existing advisory `fastapi 0.153.0 → 0.158.2`; no dependency work in this task).
- `uv run pytest -m live --live` (`.env.dev` sourced) → **11 passed** (first attempt had one transient `deskwork … /snapshot timed out`, clean on re-run); `./scripts/dev-server.sh status` → 9/9 sources `ok`.
- Browser pass → **ALL CHECKS PASSED** (235 checks, 0 failures).
**Docs**: `AGENTS.md` — control-order sentence in the `/certs` paragraph, the row-3 description, a new phase-25-task-03 paragraph (markup-vs-`order:` rationale, the pin, the ≤900 px clock note, the script/screenshots); `README.md` — the `web/` masthead bullet now reads `⚷ Certs` / `⚙ Account` / `☾ Dark` / clock with "the time is the farthest-right thing on the bar".
**Decisions/deviations**: the long-standing `@media (max-width: 900px){.clock{display:none}}` stays (task said no declaration changes; it's a readout, not a control), so the clock's pixel claim is measured at 1280 px and the 360 px leg asserts the clock is hidden *by that rule* and the source order still ends with it — if the operator wants the time on the phone too, that hide is the one line to drop. No live-infra changes; `TODO.md`/`NOTES.md` working-tree edits are pre-existing, not mine.
**Next pending task**: `.agents/phases/todo/26_ups_event_split/01_page_side_event_split.md` (this task file is the last of phase 25).
@@ -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%]
........................................................................ [ 60%]
........................................................................ [ 64%]
........................................................................ [ 69%]
.sssssssssss............................................................ [ 73%]
........................................................................ [ 78%]
........................................................................ [ 83%]
........................................................................ [ 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 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%
1543 passed, 15 skipped, 1 warning in 163.74s (0:02:43)
== 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.0 wheel
+63 -19
View File
@@ -750,7 +750,10 @@ Responsibilities, one line each:
unsigned because a signed-in `GET /login` is a `303` and would audit `/`'s markup under the wrong
name. `/settings` (`pages/settings.html` + `static/page-settings.js`) is a **page and not a
sixth dashboard**: same `<head>` 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
not one of them), the same four `#nav-status` badges as every dashboard (phase 25 task 01 —
`TODO.md:3`'s "hosts disks engines UPS disappear when in the certs/account page" was the
missing container, and `Dash.initChrome()` fills it from `/api/health` with no JS change), and
it is reached from the `⚙ Account` link in
`.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
@@ -766,15 +769,17 @@ Responsibilities, one line each:
`csrf` is now invalid — every later write, the UPS buttons included, 403s until a reload) and on
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 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
is the same shape for the same reasons: five tabs with **no** `aria-current`, the four fleet
badges in `#nav-status` like every other masthead (phase 25 task 01, same reasoning as
`/settings` above), 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 seven
signed-in pages (the five dashboards plus `/settings` and `/certs`) in one order
(clock → `⚷ Certs` → `⚙ Account` → theme toggle), which is what lets
(`⚷ Certs` → `⚙ Account` → theme toggle → clock — phase 25 task 03 moved the clock to the end so
it is the farthest-right item on a `flex-end` row), which is what lets
the pinned audit be one loop over `MARKUP_PAGES` instead of two over different tuples; `/login`
is the eighth page and the one with neither. Everything on it is
`GET /api/certs`: the Root CA block, the display-only Dashboard client line (that certificate is
@@ -814,17 +819,21 @@ 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, `⚷ Certs`, `⚙ Account`, theme — phase 21 task 01 deleted
the `⏸ Pause` that used to sit between the clock and the two links, button, `Space` shortcut and
**The masthead is three rows at every width, the two control rows flush right** (phase 20 task
01 made the rows, `TODO.md:5-6`; phase 25 task 02 re-justified them, `TODO.md:4` — "I made a
mistake earlier, I did want hosts disks engines UPS badges and certs/account/dark right
justified"): row 1 the brand, `#nav-toggle` and the five tabs; row 2 the `#nav-status`
per-source dots; row 3
the `.masthead-right` controls (`⚷ Certs`, `⚙ Account`, theme, then the clock — phase 25 task 03
moved the clock last for `TODO.md:5`'s "the time should show up as the farthest right item";
phase 21 task 01 deleted the `⏸ Pause` that used to sit beside the links, button, `Space` shortcut and
`setPaused`/`_paused` plumbing alike: it froze the numbers on screen without growing a `stale`
label, and a backgrounded tab already idles its polls, which is the only stop this app needs).
That is CSS and
That was 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
`justify-content: flex-end` (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.
@@ -832,12 +841,46 @@ Responsibilities, one line each:
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
would clip the bar on a phone only. Every page that renders the bar now renders three rows —
`/certs` and `/settings` grew the dots row in phase 25 task 01, where `#nav-status` is pinned as
the **first child** of `.masthead-right` on all seven signed-in pages (`app.js` is untouched:
the container's existence is what makes `initChrome` build the four badges, which is why the fix
was markup and not JS) — and `/login` remains the one page with no dots, rendering two rows
(brand, then the theme toggle). Pinned on the served bytes by phase 20's three tests in
`tests/test_api.py` — the row declarations (task 02 renamed that one to
`test_the_masthead_is_three_rows_right_justified_at_every_width`), 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`).
`data/logs/masthead-rows*.png`). **Phase 25 task 02 moved the alignment of those two rows to
the other edge** (`TODO.md:4` — "I made a mistake earlier, I did want hosts disks engines UPS
badges and certs/account/dark right justified"): the two `justify-content` declarations are
`flex-end`, `margin-inline-start` is still absent from the cluster (a full-basis row aligned by
`flex-end` is the mechanism, not the old single-row right-pinning), and the ≤640px block still
restates neither the alignment nor the bar's height, so the phone's wrapped control row follows
the base rule. The pin is `tests/test_api.py::test_the_masthead_is_three_rows_right_justified_at_every_width`
(the row-break assertions are the same ones phase 20 wrote, deliberately: the re-justify must
not cost the rows), and the measured half is `data/logs/25-task02-justify.cjs` — seven pages ×
two widths, each demanding both rows compute `flex-end` and sit flush with the bar's right
padding edge, the four badges present, three rows intact, the dropdown still under the whole
bar and no sideways scroll, plus `/login`'s two-row bar with no badges
(`data/logs/25-task02-justify.out`, screenshots `data/logs/25-task02-*.png`). **Phase 25 task 03
then put the clock at the end of the control row** (`TODO.md:5` — "The time should show up as the
farthest right item on the navbar"): the `<span class="clock" id="clock">` moved from the first
control to the last one on all seven templates, so the row reads `⚷ Certs` → `⚙ Account` → theme
toggle → clock. It is a **markup** move and deliberately not a CSS `order:` declaration, because
on a `flex-end` row the last child *is* the farthest-right item and an `order:` override would be
something the served-byte pin cannot see — `test_every_page_links_to_both_admin_pages` therefore
asserts `html.index('id="clock"') > html.index('id="theme-toggle"')` on every page (the two
Certs/Account-before-theme assertions stay, and the `/login` half of the test is untouched).
The clock keeps its long-standing `@media (max-width: 900px) { .clock { display: none } }` hide:
it is a readout and not a control, so this is not the "no control is hidden to make the width
work" rule the masthead keeps. Measured by `data/logs/25-task03-verify.cjs` — seven pages × two
widths, each demanding three rows, both right-hand rows flush with the bar's right padding edge
and the chips' content (not the full-basis row box) measured to prove it is not flush left, the
clock the rightmost painted thing on the bar at 1280 px, the cluster's source order unchanged at
360 px where the clock is hidden, and the four badges populated on `/certs` and `/settings` as on
the dashboards; plus the 360 px dropdown and `/login` unsigned at both widths
(`data/logs/25-task03-verify.out`, screenshots `data/logs/masthead-25-*.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
@@ -1186,7 +1229,7 @@ uv run python scripts/dev-certs-export.py --url http://127.0.0.1:8090 \
# GET /api/certs/<name>/bundle and writes
# data/certs/<name>.bundle at 0600 — mints nothing
uv run pytest # hermetic: 1542 passed + 15 skipped (11 live,
uv run pytest # hermetic: 1543 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
@@ -2362,7 +2405,8 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
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
beside it, `20_masthead_nav_polish` has landed in full (now complete): the bar is three
left-justified rows at every width, it is no longer sticky, the phone menu slides, and both
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
`⚙ Account` and `⚷ Certs` render on all seven signed-in pages including the two they name
(task 04, reversing phase 15's "no self-link" rule at the operator's request). The other,
`24_dev_mtls_rollout` (never accepting a non-mTLS agent) is closed: its fleet half landed on
+11 -6
View File
@@ -751,13 +751,18 @@ certs.py ─ root CA + client cert, minted once at startup (unreadable row → n
that row count, so `/` and `/disks` can never describe two different fleets.
* `web/` — five static dashboard pages (`/`, `/system`, `/disks`, `/llm`, `/ups`) plus the three
admin pages (`/login`, `/settings`, `/certs`), one stylesheet, hand-rolled
SVG charts (`charts.js`), and a masthead status dot per dashboard job (`disks` has its own:
SVG charts (`charts.js`), and a masthead status dot per dashboard job on **every** signed-in
page — the two admin pages included (`disks` has its own:
the same agent can answer the 10-second utilisation poll and have a wedged SMART pass).
The masthead is **three left-justified rows at every width**: the brand and the five dashboard
tabs, then the per-source indicators on their own row, then the clock / `⚷ Certs` / `⚙ Account`
/ `☾ Dark` controls on a row under those (a page with no
indicators — `/login`, `/settings`, `/certs` — renders two, and a control is never hidden to make a
narrower width work). There is no control that stops the polling (phase 21): the bar used to carry
The masthead is **three rows at every width, the two control rows flush right**: the brand and the five dashboard
tabs, then the per-source indicators on their own row, then the `⚷ Certs` / `⚙ Account` /
`☾ Dark` / clock controls on a row under those, in that order, so the **time is the farthest-right
thing on the bar** (phase 25: the badges and the cluster were re-justified right, then the clock
moved to the end of the markup — `flex-end` makes the last child the rightmost item, and it stays
a markup fact rather than a CSS `order:` so the pin can read it; the clock is hidden below 900 px
as it has always been, being a readout and not a control). Only `/login` renders no
indicators, and so two rows, and a control is never hidden to make a
narrower width work. There is no control that stops the polling (phase 21): the bar used to carry
a `⏸ Pause` button with a `Space` shortcut, and it left the numbers on screen looking current with
no `stale` label on them. Both admin links are on all seven
signed-in pages — the five dashboards plus `/settings` and `/certs`, the two they name included —
+3 -2
View File
@@ -14,7 +14,7 @@
that marks a tab this page is not would be a lie about the navigation. The masthead
carries both admin links — `⚷ Certs` and `⚙ Account` — outside `.tabs`, so the tab
structure stays five links deep, and it carries them **here too** (`TODO.md:4`): the
controls are the same on all eight signed-in pages, and a link to the page you are on is
controls are the same on all seven signed-in pages, and a link to the page you are on is
a no-op the operator can press without being surprised, which beats chrome that
disappears when it is most being used. -->
<button class="ghost nav-toggle" id="nav-toggle" type="button"
@@ -27,10 +27,11 @@
<a href="/ups">UPS</a>
</nav>
<div class="masthead-right">
<span class="clock" id="clock">--:--:--</span>
<div id="nav-status" class="gap" aria-live="polite"></div>
<a class="ghost account" href="/certs" title="Agent certificates">⚷ Certs</a>
<a class="ghost account" href="/settings" title="Session and password">⚙ Account</a>
<button class="ghost" id="theme-toggle" type="button" aria-pressed="false">☾ Dark</button>
<span class="clock" id="clock">--:--:--</span>
</div>
</header>
+1 -1
View File
@@ -21,10 +21,10 @@
</nav>
<div class="masthead-right">
<div id="nav-status" class="gap" aria-live="polite"></div>
<span class="clock" id="clock">--:--:--</span>
<a class="ghost account" href="/certs" title="Agent certificates">⚷ Certs</a>
<a class="ghost account" href="/settings" title="Session and password">⚙ Account</a>
<button class="ghost" id="theme-toggle" type="button" aria-pressed="false">☾ Dark</button>
<span class="clock" id="clock">--:--:--</span>
</div>
</header>
+1 -1
View File
@@ -21,10 +21,10 @@
</nav>
<div class="masthead-right">
<div id="nav-status" class="gap" aria-live="polite"></div>
<span class="clock" id="clock">--:--:--</span>
<a class="ghost account" href="/certs" title="Agent certificates">⚷ Certs</a>
<a class="ghost account" href="/settings" title="Session and password">⚙ Account</a>
<button class="ghost" id="theme-toggle" type="button" aria-pressed="false">☾ Dark</button>
<span class="clock" id="clock">--:--:--</span>
</div>
</header>
+1 -1
View File
@@ -21,10 +21,10 @@
</nav>
<div class="masthead-right">
<div id="nav-status" class="gap" aria-live="polite"></div>
<span class="clock" id="clock">--:--:--</span>
<a class="ghost account" href="/certs" title="Agent certificates">⚷ Certs</a>
<a class="ghost account" href="/settings" title="Session and password">⚙ Account</a>
<button class="ghost" id="theme-toggle" type="button" aria-pressed="false">☾ Dark</button>
<span class="clock" id="clock">--:--:--</span>
</div>
</header>
+3 -4
View File
@@ -13,7 +13,7 @@
<!-- The five dashboards, and no `aria-current`: this page is not one of them, and a tab
marked current that is not in the list would claim a location the page does not have.
It is reached from the `⚙ Account` link in the masthead of every dashboard — this one
included (`TODO.md:4`): the two admin links are the same on all eight signed-in pages,
included (`TODO.md:4`): the two admin links are the same on all seven signed-in pages,
and chrome that vanishes on the page it names is the thing the operator filed. -->
<button class="ghost nav-toggle" id="nav-toggle" type="button"
aria-expanded="false" aria-controls="nav-tabs" aria-label="Toggle navigation">☰</button>
@@ -25,12 +25,11 @@
<a href="/ups">UPS</a>
</nav>
<div class="masthead-right">
<!-- No `#nav-status`: a session page has no fleet reachability to report, and
`Dash.initChrome()` builds the dots only when the container exists. -->
<span class="clock" id="clock">--:--:--</span>
<div id="nav-status" class="gap" aria-live="polite"></div>
<a class="ghost account" href="/certs" title="Agent certificates">⚷ Certs</a>
<a class="ghost account" href="/settings" title="Session and password">⚙ Account</a>
<button class="ghost" id="theme-toggle" type="button" aria-pressed="false">☾ Dark</button>
<span class="clock" id="clock">--:--:--</span>
</div>
</header>
+1 -1
View File
@@ -21,10 +21,10 @@
</nav>
<div class="masthead-right">
<div id="nav-status" class="gap" aria-live="polite"></div>
<span class="clock" id="clock">--:--:--</span>
<a class="ghost account" href="/certs" title="Agent certificates">⚷ Certs</a>
<a class="ghost account" href="/settings" title="Session and password">⚙ Account</a>
<button class="ghost" id="theme-toggle" type="button" aria-pressed="false">☾ Dark</button>
<span class="clock" id="clock">--:--:--</span>
</div>
</header>
+1 -1
View File
@@ -21,10 +21,10 @@
</nav>
<div class="masthead-right">
<div id="nav-status" class="gap" aria-live="polite"></div>
<span class="clock" id="clock">--:--:--</span>
<a class="ghost account" href="/certs" title="Agent certificates">⚷ Certs</a>
<a class="ghost account" href="/settings" title="Session and password">⚙ Account</a>
<button class="ghost" id="theme-toggle" type="button" aria-pressed="false">☾ Dark</button>
<span class="clock" id="clock">--:--:--</span>
</div>
</header>
+31 -22
View File
@@ -96,11 +96,16 @@ a:hover { text-decoration: underline; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* ---- header ------------------------------------------------------------- */
/* The masthead is **three left-justified rows at every width** (phase 20 task 01, `TODO.md:5-6`
* — "the hosts disks engines and UPS indicators … left justified in their own row", "the
* ⚷ Certs ⚙ Account ☾ Dark buttons … in their own row under" them): row 1 the brand,
/* The masthead is **three rows at every width, the two control rows flush right** (phase 20
* task 01 made the rows, `TODO.md:5-6` — "the hosts disks engines and UPS indicators … in
* their own row", "the ⚷ Certs ⚙ Account ☾ Dark buttons … in their own row under" them; phase
* 25 task 02 re-justified them right, `TODO.md:4` — "I made a mistake earlier, I did want
* hosts disks engines UPS badges and certs/account/dark right justified"): row 1 the brand,
* `#nav-toggle` and the five tabs; row 2 the `#nav-status` reachability dots; row 3 the
* `.masthead-right` controls (clock, `⚷ Certs`, `⚙ Account`, theme).
* `.masthead-right` controls — `⚷ Certs`, `⚙ Account`, the theme toggle, then the clock, in
* that markup order because on a `flex-end` row the **last** child is the farthest-right item
* (phase 25 task 03, `TODO.md:5` — "The time should show up as the farthest right item on the
* navbar"; nothing is reordered in CSS, so the pin can read the markup).
*
* No markup changed to get them, and that decides the mechanism. `#nav-status` is the first
* child of `.masthead-right`, so there is no wrapper to promote: in a `flex-wrap: wrap`
@@ -169,31 +174,34 @@ nav.tabs a {
nav.tabs a:hover { background: var(--bg-sunken); color: var(--text); text-decoration: none; }
nav.tabs a[aria-current="page"] { background: var(--accent-soft); color: var(--accent); }
/* Row 3: the controls, left-justified under the indicators. `margin-inline-start: auto` is
* what pinned this cluster to the right edge of the old single-row bar; in a row of its own it
* pushes against nothing, and `justify-content: flex-start` is the TODO's word for where the
* buttons go. `flex-basis: 100%` is the row break (see `.masthead` above) and `flex-wrap` is
* what a phone gets when clock + two links + theme will not share a line — phase 18's
* rule, and no control is ever hidden to make the arithmetic work. */
/* Row 3: the controls, flush right under the indicators. `margin-inline-start: auto` is what
* pinned this cluster to the right edge of the old single-row bar and stays **absent**: on a
* row of its own it pushes against nothing, so the alignment is `justify-content: flex-end`
* (phase 25 task 02 — the operator overruled phase 20's `flex-start`; the badges and the
* cluster are right-justified). `flex-basis: 100%` is the row break (see `.masthead` above)
* and `flex-wrap` is what a phone gets when `⚷ Certs` + `⚙ Account` + theme will not share a line
* (the clock is the last child and hides below 900px on its own — a readout, not a control, so
* that is not the "no control is hidden to fit the width" rule) — phase 18's rule, and nothing in
* this block hides a button to make the arithmetic work. */
.masthead-right {
flex-basis: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
justify-content: flex-end;
gap: .35rem .55rem;
}
/* Row 2: the per-source indicators, left-justified on a line of their own, so a readout is
* never mistaken for one more button beside it. `.gap` already makes this a flex row; the
* `flex-basis` is the break inside `.masthead-right`, `justify-content` the alignment, and the
* gap is restated at .55rem so the dots read as a row of their own rather than as the tail of
* the cluster above. */
/* Row 2: the per-source indicators, flush right on a line of their own, so a readout lines up
* with the controls under it and is never mistaken for one more button beside them. `.gap`
* already makes this a flex row; the `flex-basis` is the break inside `.masthead-right`,
* `justify-content` the alignment, and the gap is restated at .55rem so the dots read as a row
* of their own rather than as the tail of the cluster below. */
#nav-status {
flex-basis: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
justify-content: flex-end;
gap: .55rem;
}
@@ -247,9 +255,10 @@ button.ghost:disabled { opacity: .45; cursor: not-allowed; }
* and since task 03, it owns the slide too: `nav.tabs` is the closed menu and `.is-open` its
* visible counterpart, with a 180 ms `opacity`/`transform` transition on both.
*
* Above 640px nothing here applies, which is the point: the same three left-justified rows —
* brand + toggle, indicators, controls — with the tabs inline on the first one and the button
* hidden. What a phone changes is only that the tabs become a menu and the gaps tighten.
* Above 640px nothing here applies, which is the point: the same three rows — brand + toggle,
* indicators, controls, the last two flush right — with the tabs inline on the first one and
* the button hidden. What a phone changes is only that the tabs become a menu and the gaps
* tighten.
*/
@media (max-width: 640px) {
/* The bar's vertical rhythm, and nothing structural. The three rows come from the base
@@ -284,8 +293,8 @@ button.ghost:disabled { opacity: .45; cursor: not-allowed; }
* still wraps *within itself*: `⚷ Certs`, `⚙ Account` and the theme button do not fit in
* 320px once over on the narrowest phones, and nothing here hides a control to make
* the arithmetic work — a phone gets the same words every other width does. The row
* alignment is the base rule's `flex-start`, restated nowhere: phase 20 replaced the
* right-justified cluster, so this block only tightens the gap. */
* alignment is the base rule's `flex-end` (phase 25 task 02), restated nowhere: this block
* only tightens the gap, so the two rows cannot disagree about which edge they sit on. */
.masthead-right { flex-wrap: wrap; gap: .4rem; }
.masthead-right > * { flex: none; white-space: nowrap; }
#nav-status { flex-wrap: nowrap; }
+94 -17
View File
@@ -1046,7 +1046,10 @@ def test_the_settings_page_is_not_one_of_the_dashboards(client) -> None:
assert 'aria-current="page"' not in html
for path in PAGES:
assert f'href="{path}"' in html, f"/settings cannot navigate to {path}"
assert 'id="nav-status"' not in html, "/settings grew fleet dots it has nothing to fill"
# The fleet badges belong here too (`TODO.md:3`): the container is what made them appear on
# the five dashboards, and a page that renders the masthead without it is the page where they
# "disappear". `Dash.initChrome()` fills it from `/api/health` exactly as it fills the others.
assert 'id="nav-status"' in html, "/settings lost the fleet badges the operator asked for"
def test_the_settings_page_refuses_a_caller_with_no_session(tmp_path: Path) -> None:
@@ -1075,10 +1078,9 @@ def test_the_settings_page_refuses_a_caller_with_no_session(tmp_path: Path) -> N
def test_every_page_links_to_both_admin_pages(client, tmp_path: Path) -> None:
"""`⚙ Account` and `⚷ Certs` are the only way to the two admin pages, so they are everywhere.
One loop over all seven signed-in pages (`MARKUP_PAGES`), because the controls are now the *same*
on all of
them: clock → `⚷ Certs` → `⚙ Account` → theme toggle, in `.masthead-right` and **outside**
`.tabs` (the tab strip's structure — five dashboards, one of them current — is pinned by
One loop over all seven signed-in pages (`MARKUP_PAGES`), because the control row is the *same*
on all of them: `⚷ Certs` → `⚙ Account` → theme toggle → clock, in `.masthead-right` and
**outside** `.tabs` (the tab strip's structure — five dashboards, one of them current — is pinned by
`test_the_disks_tab_is_in_the_masthead_of_every_page`, and a sixth entry with no
`aria-current` would break what that marker means).
@@ -1087,6 +1089,15 @@ def test_every_page_links_to_both_admin_pages(client, tmp_path: Path) -> None:
button to disappear … These should remain visible"): masthead chrome that changes shape
depending on where you already are reads as a broken control, while a link to the page you
are on is a no-op that answers with the page you were looking at.
The clock last is phase 25 task 03 (`TODO.md:5` — "The time should show up as the farthest
right item on the navbar"). It is pinned as **markup**, not as a CSS `order` property, because
that is what makes the claim true: task 02 left the control row `justify-content: flex-end`, so
the last child of `.masthead-right` *is* the farthest-right item, and an `order:` override the
pin could not see would leave this test green over a bar that says something else. (Below 900 px
the clock is hidden by the long-standing `.clock` media rule — a readout, not a control, so
hiding it is not the "no control is hidden to fit the width" rule this app keeps — and the
markup order this test reads is width-independent.)
"""
account_open = '<a class="ghost account" href="/settings"'
certs_open = '<a class="ghost account" href="/certs"'
@@ -1096,6 +1107,14 @@ def test_every_page_links_to_both_admin_pages(client, tmp_path: Path) -> None:
account = f'{account_open} title="Session and password">⚙ Account</a>'
certs_link = f'{certs_open} title="Agent certificates">⚷ Certs</a>'
theme = '<button class="ghost" id="theme-toggle" type="button" aria-pressed="false">'
# The other half of the clock claim, from the stylesheet side: no `order:` on anything in the
# bar, because a re-ordering property would leave this markup audit describing a bar that
# paints in some other order (`data/logs/25-task03-verify.cjs` measures the painted order at
# both widths; this is the served-byte guard under that measurement).
css = _css(client)
for selector in (".masthead", ".masthead-right", "#nav-status", ".clock"):
assert "order" not in _declarations(css, selector), \
f"{selector} grew an `order:` declaration — the clock's place in the row is markup"
for path in MARKUP_PAGES:
html = client.get(path).text
assert certs_link in html, f"{path} has no route to the certificate authority"
@@ -1105,6 +1124,10 @@ def test_every_page_links_to_both_admin_pages(client, tmp_path: Path) -> None:
assert account in before_theme, f"{path} put the Account link after the theme toggle"
assert before_theme.index(certs_link) < before_theme.index(account), \
f"{path} orders the two admin controls differently from the dashboards"
# …and the clock after the toggle, i.e. last in `.masthead-right`: on the `flex-end`
# control row that is the farthest-right item on the bar (TODO.md:5).
assert html.index('id="clock"') > html.index('id="theme-toggle"'), \
f"{path} does not end its control row with the clock"
tabs = html[html.index('<nav class="tabs"'):html.index("</nav>")]
assert "/certs" not in tabs, f"{path} made Certs a dashboard tab"
assert "/settings" not in tabs, f"{path} made Account a dashboard tab"
@@ -1134,8 +1157,9 @@ def test_every_page_links_to_both_admin_pages(client, tmp_path: Path) -> None:
def test_the_certs_page_is_a_page_and_not_a_sixth_dashboard(client) -> None:
"""`/certs` borrows the dashboard chrome and none of the dashboard meaning (task 15.05).
Five tabs with nothing current, no `#nav-status` (a CA page has no fleet reachability to
report), and the same head and stylesheet as every other page — this is the
Five tabs with nothing current, the four fleet badges like every other masthead (phase 25:
the badges report the fleet, and that report is the point of the site on this page too), and
the same head and stylesheet as every other page — this is the
`test_the_settings_page_is_not_one_of_the_dashboards` assertion for the second admin page,
and it is what a sixth tab in the strip would fail on.
"""
@@ -1143,7 +1167,7 @@ def test_the_certs_page_is_a_page_and_not_a_sixth_dashboard(client) -> None:
assert html.count('aria-current="page"') == 0
tabs = re.findall(r'<a href="([^"]+)"[^>]*aria-current="page"', html)
assert tabs == [], f"/certs marked a tab current: {tabs}"
assert 'id="nav-status"' not in html, "/certs grew fleet dots it has nothing to fill"
assert 'id="nav-status"' in html, "/certs lost the fleet badges the operator asked for"
for path in PAGES:
assert f'href="{path}"' in html, f"/certs cannot navigate to {path}"
assert '/static/dash.css' in html
@@ -4619,6 +4643,57 @@ def test_the_login_page_carries_no_nav_and_no_toggle(tmp_path: Path) -> None:
"/login stopped loading the shared chrome whose guard is its no-toggle contract")
# ---- the fleet badges on every masthead (phase 25 task 01) ------------------ #
#
# `TODO.md:3-4` — "hosts disks engines UPS disappear when in the certs/account page" and the
# operator's correction: "I did want hosts disks engines UPS badges". `Dash.initChrome()`
# builds the four badges whenever `#nav-status` exists, so the **container** is the whole
# difference between a page that has them and a page that does not — the two admin pages shipped
# without one, which is why they read as the badges vanishing rather than as two odd pages. The
# fix is the container and nothing else, so the pin is the container: on every page that renders
# the masthead, in the position the five dashboards already used (first child of
# `.masthead-right`, which is what puts it on its own row — task 02 re-justifies that row and this
# pin deliberately says nothing about which way the badges face). `tests/test_auth_api.py` keeps
# its own assertion that `/login` has none: a stranger has no fleet to be told about.
#: The container exactly as all seven signed-in pages write it (`index.html` is the reference).
NAV_STATUS_ELEMENT = '<div id="nav-status" class="gap" aria-live="polite"></div>'
#: The four badges `initChrome()` renders, in its own order.
NAV_STATUS_BADGES = ("hosts", "disks", "engines", "UPS")
def test_every_signed_in_page_carries_the_fleet_badges(client) -> None:
"""`#nav-status` is the first child of `.masthead-right` on all seven signed-in pages.
One loop over `MARKUP_PAGES`, because the control is the same on all of them: the five
dashboards had it, `/certs` and `/settings` grew it. The `app.js` half of the assertion is
what makes the markup mean something — the badges are built from the container's existence,
so a script that stopped reading `#nav-status` would turn seven `<div>`s into dead markup and
the orphan-`id` audit alone would not say so (it also matches `#nav-status` inside the CSS).
"""
js = _code(client.get("/static/app.js").text)
assert "const nav = $('#nav-status');" in js, \
"initChrome() no longer builds the badges from #nav-status, so the container is inert"
assert "if (nav) {" in js, \
"the container guard is gone — a page without #nav-status (only /login) would throw"
for label in NAV_STATUS_BADGES:
assert f"label: '{label}'" in js, f"the badge table lost {label}"
assert "setInterval(refreshStatus, 15000)" in js, \
"the badges' refresh cadence moved — this phase changes placement, not polling"
for path in MARKUP_PAGES:
header = _masthead(client.get(path).text)
assert NAV_STATUS_ELEMENT in header, \
f"{path} has no fleet badges in its masthead (TODO.md:3)"
after_cluster = header[header.index('<div class="masthead-right">')
+ len('<div class="masthead-right">'):]
assert after_cluster.lstrip().startswith(NAV_STATUS_ELEMENT), \
f"{path} does not render #nav-status as the first child of .masthead-right "\
"(the five dashboards' position is what puts the badges on their own row)"
assert header.count('id="nav-status"') == 1, f"{path} renders the badge container twice"
def test_the_nav_toggle_css_and_binding_carry_the_same_breakpoint(client) -> None:
"""Hidden by default, revealed at one breakpoint, in two files that must agree on its value.
@@ -4682,7 +4757,8 @@ def test_the_nav_toggle_css_and_binding_carry_the_same_breakpoint(client) -> Non
# ---- the three-row masthead (phase 20 task 01) ------------------------------ #
#
# `TODO.md:5-6` asked for the per-source indicators and the `⚷ Certs ⚙ Account ☾ Dark` cluster
# each on their own left-justified line. The markup could not change — `#nav-status` is a child
# each on their own line (phase 25 task 02 moved both of those lines flush right; the *rows* and
# the mechanism below are phase 20's). The markup could not change — `#nav-status` is a child
# of `.masthead-right`, and the tab structure is pinned by
# `test_the_disks_tab_is_in_the_masthead_of_every_page` — so the rows are made of line breaks:
# in a `flex-wrap: wrap` container an item whose *base* size fills the line is alone on it (the
@@ -4729,13 +4805,14 @@ def _cascade(css: str, *selectors: str) -> dict[str, str]:
return merged
def test_the_masthead_is_three_left_justified_rows_at_every_width(client) -> None:
"""Brand + tabs, then the indicators, then the controls — three rows, two of them flush left.
def test_the_masthead_is_three_rows_right_justified_at_every_width(client) -> None:
"""Brand + tabs, then the indicators, then the controls — three rows, two of them flush right.
Every assertion here is one declaration the row layout depends on, read out of the
**served** stylesheet: `flex-wrap` plus the two `flex-basis: 100%` rules are the rows, the
two `justify-content` values are the left justification, and the absence of
`margin-inline-start` on the cluster is the end of the right-pinned single-row bar.
two `justify-content` values are the right justification (phase 25 task 02, after the
operator overruled phase 20's `flex-start`), and the absence of `margin-inline-start` on the
cluster keeps the *mechanism* the alignment rather than the old single-row right-pinning.
"""
css = _css(client)
bar = _declarations(css, ".masthead")
@@ -4753,16 +4830,16 @@ def test_the_masthead_is_three_left_justified_rows_at_every_width(client) -> Non
cluster = _declarations(css, ".masthead-right")
assert cluster.get("flex-basis") == "100%", \
"the control cluster no longer starts its own row under the indicators"
assert cluster.get("justify-content") == "flex-start", \
"the controls are no longer left-justified in their own row"
assert cluster.get("justify-content") == "flex-end", \
"the controls are no longer right-justified in their own row"
assert "margin-inline-start" not in cluster, \
"margin-inline-start: auto pushes the cluster back to the right edge of its own row"
dots = _declarations(css, "#nav-status")
assert dots.get("flex-basis") == "100%", \
"the status indicators no longer get a row to themselves"
assert dots.get("justify-content") == "flex-start", \
"the indicators are no longer left-justified"
assert dots.get("justify-content") == "flex-end", \
"the indicators are no longer right-justified"
def test_the_mobile_masthead_block_declares_no_second_bar_height(client) -> None: