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:
+25
@@ -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).
|
||||
+1
@@ -0,0 +1 @@
|
||||
ok
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
== uv run pytest --cov (hermetic: no network, live tests skip) ==
|
||||
........................................................................ [ 4%]
|
||||
........................................................................ [ 9%]
|
||||
........................................................................ [ 13%]
|
||||
........................................................................ [ 18%]
|
||||
........................................................................ [ 23%]
|
||||
..........................................................s............. [ 27%]
|
||||
........................................................................ [ 32%]
|
||||
........................................................................ [ 36%]
|
||||
........................................................................ [ 41%]
|
||||
........................................................................ [ 46%]
|
||||
........................................................................ [ 50%]
|
||||
........................................................................ [ 55%]
|
||||
........................................................................ [ 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
|
||||
+25
@@ -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`
|
||||
+1
@@ -0,0 +1 @@
|
||||
ok
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
== uv run pytest --cov (hermetic: no network, live tests skip) ==
|
||||
........................................................................ [ 4%]
|
||||
........................................................................ [ 9%]
|
||||
........................................................................ [ 13%]
|
||||
........................................................................ [ 18%]
|
||||
........................................................................ [ 23%]
|
||||
..........................................................s............. [ 27%]
|
||||
........................................................................ [ 32%]
|
||||
........................................................................ [ 36%]
|
||||
........................................................................ [ 41%]
|
||||
........................................................................ [ 46%]
|
||||
........................................................................ [ 50%]
|
||||
........................................................................ [ 55%]
|
||||
........................................................................ [ 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
|
||||
+15
@@ -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`.
|
||||
+1
@@ -0,0 +1 @@
|
||||
ok
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
== uv run pytest --cov (hermetic: no network, live tests skip) ==
|
||||
........................................................................ [ 4%]
|
||||
........................................................................ [ 9%]
|
||||
........................................................................ [ 13%]
|
||||
........................................................................ [ 18%]
|
||||
........................................................................ [ 23%]
|
||||
..........................................................s............. [ 27%]
|
||||
........................................................................ [ 32%]
|
||||
........................................................................ [ 36%]
|
||||
........................................................................ [ 41%]
|
||||
........................................................................ [ 46%]
|
||||
........................................................................ [ 50%]
|
||||
........................................................................ [ 55%]
|
||||
........................................................................ [ 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
|
||||
+16
@@ -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).
|
||||
+1
@@ -0,0 +1 @@
|
||||
ok
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
== uv run pytest --cov (hermetic: no network, live tests skip) ==
|
||||
........................................................................ [ 4%]
|
||||
........................................................................ [ 9%]
|
||||
........................................................................ [ 13%]
|
||||
........................................................................ [ 18%]
|
||||
........................................................................ [ 23%]
|
||||
..........................................................s............. [ 27%]
|
||||
........................................................................ [ 32%]
|
||||
........................................................................ [ 36%]
|
||||
........................................................................ [ 41%]
|
||||
........................................................................ [ 46%]
|
||||
........................................................................ [ 50%]
|
||||
........................................................................ [ 55%]
|
||||
........................................................................ [ 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
|
||||
@@ -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
|
||||
|
||||
@@ -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 —
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user