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