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
+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: