phase: 20_masthead_nav_polish
**Phase 20 final verification pass — all four tasks already complete; verified in full.** - Verified: three-row masthead CSS (`.masthead` flex-wrap + two `flex-basis: 100%`, `min-height`, no `margin-inline-start`), un-sticky `position: relative` + `z-index: 40`, 180 ms `visibility/opacity/transform` slide (closed `nav.tabs` / open `.is-open`, both states in the file's single reduced-motion block), both admin links on all 8 signed-in pages, `/login` neither. - `uv run pytest` → **1464 passed, 15 skipped** · `uv run pytest --cov --cov-fail-under=94` → **98.94%** (floor 94, unchanged) · `ruff check src tests agent` clean · `pyright` 0 errors · **`bash .agents/validate.sh` EXIT=0** (incl. `node --check` + repo hygiene) · `uv run pytest -m live --live` → **11 passed**. - Browser (live dev server, invariant 10): `20-task01-rows.cjs` 121 PASS/0 FAIL · `20-task02-unsticky.cjs` 81/0 · `20-task03-slide.cjs` 32/0 · `20-task04-admin-links.cjs` 87/0 · phase-18 `nav-final-pass.cjs` 45/45, `nav-task03-verify` 77, `nav-task04-verify` 24/24 · phase-19 scripts 11/22/24/29 all pass. Screenshots refreshed in `data/logs/` (`masthead-rows*`, `masthead-unsticky*`, `slide-*`, `admin-links-*`). Dev server restarted; `/api/health` all nine sources `ok`. - Completion criteria: 3-row bar @1280/360 ✓ · scroll leaves bar behind + dropdown under whole bar ✓ · slide out/in 180 ms, reduced-motion snap ✓ · `/certs`→`⚷ Certs`, `/settings`→`⚙ Account` before theme toggle, outside `.tabs` ✓ · pytest/coverage/ruff/pyright ✓ · validate ✓ · no regression in tab structure, token reveal, safe-area padding, four close paths, status dots, theme toggle ✓. - Docs: **AGENTS.md** gap 12 still said phase 20 was "in `todo/`… admin-link half has landed" and attributed the wrap to the old fixed 58 px row — rewritten to the completed state (three rows/un-sticky/slide/both links; 24 still todo; 21's `/certs` `.scroll` noted as not reaching `/`'s table). **README.md** `web/` bullet enumerated the controls row without `⏸ Pause` and omitted `/login` from the two-row list — both fixed. Everything else (setup, commands, env table vs `config.py`, routes vs `api.py`, agent/installer flags, testing policy) verified consistent. - Deviations: no dependency bump — `uv pip list --outdated` still reports the pre-existing advisory `fastapi 0.153.0 → 0.156.4` (deps untouched this phase, validate treats it as advisory); `NOTES.md` is empty in the working tree from an operator edit outside these tasks (both items live in phases 20/24) — left as-is; no live-infra changes made or needed. - Next pending phase: `21_page_cleanups` (pause button removal, `/system` GPU values, `/certs` table scroll).
This commit is contained in:
@@ -689,8 +689,8 @@ Responsibilities, one line each:
|
||||
name. `/settings` (`pages/settings.html` + `static/page-settings.js`) is a **page and not a
|
||||
sixth dashboard**: same `<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
|
||||
`.masthead-right` of every dashboard — outside `.tabs`, so the pinned tab structure is untouched
|
||||
(`tests/test_api.py::test_every_page_links_to_both_admin_pages`). It prints
|
||||
`.masthead-right` of every dashboard **including itself** — outside `.tabs`, so the pinned tab
|
||||
structure is untouched (`tests/test_api.py::test_every_page_links_to_both_admin_pages`). It prints
|
||||
`/api/auth/status` (name, expiry as a local timestamp *and* as time left), signs out through
|
||||
`POST /api/auth/logout`, and changes the password through `POST /api/auth/password` after
|
||||
checking locally what it can judge itself (non-empty current, ≥ 8 new characters —
|
||||
@@ -705,8 +705,15 @@ Responsibilities, one line each:
|
||||
any `401` bounce.
|
||||
`/certs` (`pages/certs.html` + `static/page-certs.js`) is the second of those admin pages, and it
|
||||
is the same shape for the same reasons: five tabs with **no** `aria-current`, no `#nav-status`,
|
||||
and the `⚷ Certs` masthead link beside `⚙ Account` (neither admin page links to itself, which is
|
||||
why the pinned two-link audit loops over two different tuples). Everything on it is
|
||||
and both masthead links — `⚷ Certs` beside `⚙ Account` — exactly as the five dashboards render
|
||||
them. **Both links are on both admin pages** (phase 20 task 04, `TODO.md:4`), which retires phase
|
||||
15's "no self-link" precedent: chrome that changes shape depending on where you already are reads
|
||||
as a broken button, while a link to the page you are on is a no-op that answers with the page you
|
||||
were looking at (`data/logs/20-task04-admin-links.cjs` measures exactly that — one document
|
||||
response, no redirect, the same cluster afterwards). The two links therefore sit on all eight
|
||||
signed-in pages in one order (clock → `⚷ Certs` → `⚙ Account` → theme toggle), which is what lets
|
||||
the pinned audit be one loop over all eight instead of two over different tuples; `/login` stays
|
||||
the one page with neither. Everything on it is
|
||||
`GET /api/certs`: the Root CA block, the display-only Dashboard client line (that certificate is
|
||||
minted with the root and has no route of its own), and one row per host with its status chip,
|
||||
serial, expiry (local timestamp *and* `Dash.duration` left), issued-vs-deployed `bundle_rev`,
|
||||
@@ -739,6 +746,41 @@ Responsibilities, one line each:
|
||||
`severity` verdict the API sent
|
||||
(`tests/test_api.py` fails if a page script names it again). A value the API should rate
|
||||
gets a row in `severity.py`, never a bound in JS.
|
||||
**The masthead is three left-justified rows at every width** (phase 20 task 01, `TODO.md:5-6`):
|
||||
row 1 the brand, `#nav-toggle` and the five tabs; row 2 the `#nav-status` per-source dots; row 3
|
||||
the `.masthead-right` controls (clock, `⏸ Pause`, `⚷ Certs`, `⚙ Account`, theme). That is CSS and
|
||||
no markup change, which decides the mechanism: `#nav-status` is a **child** of
|
||||
`.masthead-right`, so there is no wrapper to promote, and the rows are line breaks — `.masthead`
|
||||
is `flex-wrap: wrap`, and both `#nav-status` and `.masthead-right` carry `flex-basis: 100%` with
|
||||
`justify-content: flex-start` (line breaking uses a flex item's **base** size, before shrink, so
|
||||
a 100 % item is alone on its line: the div's break starts row 2 of the bar and the dots' starts
|
||||
row 2 inside it, leaving the controls to wrap to row 3). `.masthead-right` lost the
|
||||
`margin-inline-start: auto` that pinned the cluster to the right edge of the one-row bar.
|
||||
`flex-direction: column` was the other option the task allowed and is the wrong one: it puts the
|
||||
brand and the tabs on separate rows. `height: var(--header-h)` became **`min-height`** (three rows
|
||||
do not fit in 58 px and a fixed height clips the controls) and is declared **once** — the ≤640px
|
||||
block no longer restates `height`/`min-height`/`flex-wrap`, and that pair drifting is exactly what
|
||||
would clip the bar on a phone only. A page with no dots (`/certs`, `/settings`) renders two rows,
|
||||
`/login` two (brand, then the theme toggle). Pinned on the served bytes by the three phase-20
|
||||
tests in `tests/test_api.py` (the three rules' declarations, the mobile block's silence about
|
||||
the bar's height, and the bar's `position`), and measured in a browser at 1280 px and 360 px by
|
||||
`data/logs/20-task01-rows.cjs` (seven pages × two widths plus the dropdown; screenshots
|
||||
`data/logs/masthead-rows*.png`).
|
||||
**The bar is not sticky** (phase 20 task 02, `TODO.md:10` — "It shouldn't be sticky at all. We
|
||||
want to maximize the amount of screen space for data viewing"): `position: sticky; top: 0` became
|
||||
**`position: relative`** with no offset, so the whole masthead scrolls away with the page and the
|
||||
phone's "sticky only part way down" quirk — the fixed-height sticky bar fighting the wrapped
|
||||
rows — went with it. `relative` rather than the default `static` because `.masthead` is still the
|
||||
positioned ancestor the ≤640px dropdown's `top: 100%` resolves against (a `static` bar moves the
|
||||
menu to the top of the document), and `z-index: 40` stays so the menu's `41` still means one
|
||||
above the bar; the consequence is that the open menu scrolls with the bar, which is what an
|
||||
unpinned bar means. The only `position: sticky` left in the sheet is a table's own `thead`, inside
|
||||
its own scroll container, and this task is not it. Proven by `data/logs/20-task02-unsticky.cjs`
|
||||
(seven pages scrolled at 1280 px and 360 px — with a squeezed-height leg for the pages that fit
|
||||
a tall viewport — plus the 360 px dropdown still landing directly under the bar and still hanging
|
||||
off it after a scroll; screenshots `data/logs/masthead-unsticky*.png`). Note that a browser cannot
|
||||
tell `relative` from `relative; top: 0` — Chrome computes `top: 0px` for either — so the absence
|
||||
of the offset is a served-CSS pin, not a measurement.
|
||||
**The tab nav collapses into a hamburger at ≤640px** (phase 18): the seven tabbed pages each
|
||||
carry `#nav-toggle` (`class="ghost nav-toggle"`, `type="button"`, `aria-controls="nav-tabs"`,
|
||||
`aria-label="Toggle navigation"`, `☰`) between `.brand` and `<nav class="tabs" id="nav-tabs">`,
|
||||
@@ -747,8 +789,9 @@ Responsibilities, one line each:
|
||||
in `:root`, and that token is a specificity ladder, not decoration: the button also carries
|
||||
`button.ghost { display: inline-flex }`, so a `.nav-toggle { display: none }` loses and ships a
|
||||
visible button into the desktop masthead. The `@media (max-width: 640px)` block reveals it
|
||||
through the token, hides `nav.tabs`, wraps `.masthead` and `.masthead-right` into rows (hiding a
|
||||
control to make the width work is not the deal), and pads the bar down by
|
||||
through the token, hides `nav.tabs`, and tightens the bar's `row-gap` plus the cluster's gap (the
|
||||
cluster still wraps inside its own row — hiding a control to make the width work is not the deal)
|
||||
and pads the bar down by
|
||||
`env(safe-area-inset-top, 0px)` (the block's `padding-block` value restated on the top side so
|
||||
the two cannot drift), because a real device's top-edge zone — Android Chrome is edge-to-edge
|
||||
by default, and iOS does the same once the page opts into `viewport-fit=cover` — does not
|
||||
@@ -756,9 +799,24 @@ Responsibilities, one line each:
|
||||
headless emulation has such a zone, so a fully green emulated pass still ships a dead button
|
||||
(the same symptom brain of reese fixed in production; `env()` resolves to 0 on every non-notch
|
||||
surface, which render byte-identical, and the dropdown's `top: 100%` follows the taller bar);
|
||||
it renders `nav.tabs.is-open` as the dropdown:
|
||||
the `nav` element itself, `position: absolute; top: 100%; z-index: 41` under the sticky bar — no
|
||||
second menu element, no `body` marker class, no colour outside the tokens. One binding in
|
||||
it renders the dropdown as the `nav` element itself — since phase 20 task 03 that element is
|
||||
**one box in two states**: `nav.tabs` carries the geometry (`position: absolute; top: 100%;
|
||||
z-index: 41`) under the **whole** bar — three rows since task 01, which is the intended look: a
|
||||
menu below the masthead, not wedged between its rows — *and* the closed state
|
||||
(`visibility: hidden; opacity: 0; transform: translateY(-8px); pointer-events: none`), while
|
||||
`nav.tabs.is-open` flips only those four properties to their visible counterparts. That shape is
|
||||
what the slide needs, not a stylistic choice: the block used to close the menu with
|
||||
`display: none`, and a box that is not rendered has no intermediate state, so the 180 ms
|
||||
`opacity`/`transform` transition the operator asked for (`TODO.md:11`, "a slide out and slide
|
||||
back in", after `brain_of_reese`'s `.app-nav`) could never have run. `visibility` is delayed by
|
||||
exactly the slide on the closed rule (so the box fades out before leaving the tree) and
|
||||
undelayed on `.is-open` (or the menu would be invisible for the first 180 ms of its own
|
||||
opening); `visibility: hidden` + `pointer-events: none` keep the closed menu out of the a11y
|
||||
tree, out of the tab order and unhittable, exactly as `display: none` did, and the closed state
|
||||
costs no scroll area (measured). With no second menu element, no `body` marker class, no colour
|
||||
outside the tokens and **no JS change at all**, both states are named in the file's one
|
||||
`@media (prefers-reduced-motion: reduce)` block — naming only the closed one would leave the
|
||||
higher-specificity open transition animating (brain of reese's phase-46 lesson). One binding in
|
||||
`app.js`'s `initChrome()` owns the state through a `setNavMenu(open)` funnel (`.is-open` on the
|
||||
nav and `aria-expanded` on the button, always together) with **four close paths**: the button
|
||||
again, a tab click delegated on the nav, `Esc` with focus returned to the button, and
|
||||
@@ -766,13 +824,22 @@ Responsibilities, one line each:
|
||||
so the next shrink starts closed and `aria-expanded` never claims a menu that is not on screen.
|
||||
The `if (navToggle && appNav)` guard is the whole `/login` story: the shared script loads there
|
||||
and does nothing. The `640px` literal lives in two files — the stylesheet block and that media
|
||||
query — and has to move in both, or the menu opens into a `display:none` nav. The contract is
|
||||
query — and has to move in both, or the menu opens into a hidden nav. The contract is
|
||||
pinned on the **served** bytes by three `tests/test_api.py` pins — the button on all seven pages
|
||||
and nowhere on `/login`, and one test that **compares** the CSS media query with the JS
|
||||
`matchMedia` instead of grepping for `640px` twice, so a drift fails with both numbers in the
|
||||
message — and proven in a browser at 360px by `data/logs/nav-task04-verify.cjs` (the `/` +
|
||||
`/system` interaction, 24 checks) and `data/logs/nav-final-pass.cjs` (the phase-completion
|
||||
sweep: all seven tabbed pages at 360px and at 1280px, `/login` at both, 45 checks). The
|
||||
sweep: all seven tabbed pages at 360px and at 1280px, `/login` at both, 45 checks — its desktop
|
||||
leg now expects the three-row bar's height, not the old fixed 58 px, and still demands inline
|
||||
tabs, no button and a dropped `.is-open`). Phase 20 task 03 adds three served-CSS pins (the
|
||||
closed state's `visibility`/`opacity`/`transform`/`pointer-events` and its delayed-`visibility`
|
||||
transition, the open state's visible counterparts with the delay dropped, and the both-states
|
||||
reduced-motion rule inside the file's only such block) plus the guard that no `display: none`
|
||||
comes back to that block, and the browser half is `data/logs/20-task03-slide.cjs` (the computed
|
||||
closed state, a mid-transition frame at ≈60 ms in both directions, the four close paths with
|
||||
`aria-expanded`, the snap under emulated `prefers-reduced-motion: reduce`, the untouched desktop
|
||||
tabs and the no-phantom-scroll measurement; screenshots `data/logs/slide-*.png`). The
|
||||
safe-area line is pinned in the same test's rule loop; it was verified on the operator's
|
||||
mobile Chrome rather than in an emulator, because CDP's `safeAreaInsets` override does not
|
||||
feed `env(safe-area-inset-top)` — the one half of this contract a headless pass cannot prove.
|
||||
@@ -997,7 +1064,7 @@ set -a; . ./.env.dev; set +a # dev dials the fleet the prod way, no
|
||||
./scripts/dev-server.sh status # per-source ok/DOWN — unauthenticated: /api/health
|
||||
./scripts/dev-server.sh logs
|
||||
|
||||
uv run pytest # hermetic: 1458 passed + 15 skipped (11 live,
|
||||
uv run pytest # hermetic: 1464 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
|
||||
@@ -1894,7 +1961,8 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
|
||||
and the three writes rotate, revoke, and rotate the root with `restart_required: true` said out
|
||||
loud — each leaving one fixed-text `certs`-source event and no PEM outside `meta` (pinned by a
|
||||
grep of the stored rows and of every payload a browser reads). Task 05 put the page on top of it:
|
||||
`/certs` (`⚷ Certs` in every masthead, including `/settings`) renders that document — the root
|
||||
`/certs` (`⚷ Certs`, in every signed-in masthead — its own included, since phase 20 task 04)
|
||||
renders that document — the root
|
||||
CN/serial/expiry, the display-only client line, one row per host with its status chip and
|
||||
issued-vs-deployed bundle rev and its **Rotate** / **Revoke** / **bundle** controls — and keeps
|
||||
two rules the API already keeps: no colour the payload did not send (no `severity` token on the
|
||||
@@ -1950,14 +2018,17 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
|
||||
session: same clipping (a clipped card never un-clips when the document scrolls sideways).
|
||||
What phase 18 did change is that the document no longer scrolls sideways at a phone width at
|
||||
all — 360 px where it was 750 px — because the bar that overflowed its fixed 58 px row now
|
||||
wraps, so nothing is lost and nothing gained: the masthead is complete at 360 px and the
|
||||
table tail is still out of reach. The fix is a responsive-table pass (`.scroll` on the wide
|
||||
breaks into rows, so nothing is lost and nothing gained: the masthead is complete at 360 px and
|
||||
the table tail is still out of reach. The fix is a responsive-table pass (`.scroll` on the wide
|
||||
tables, or a per-host card layout at ≤640px) and it touches pinned page shape on `/`, so it is
|
||||
a phase rather than a cleanup. The two operator TODO items that sat alongside it are phased
|
||||
now, in `todo/`: `24_dev_mtls_rollout` (never accepting a non-mTLS agent — fleet gap 11's
|
||||
rollout, half of it) and `20_masthead_nav_polish` (keeping both `⚙ Account` / `⚷ Certs`
|
||||
masthead links visible on the admin pages that today do not link to themselves — phase 15's
|
||||
deliberate rule, which that phase is the decision on).
|
||||
a phase rather than a cleanup — `21_page_cleanups`' `.scroll` on the `/certs` table is the same
|
||||
shape applied to one table and does not reach this one. 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
|
||||
`⚙ Account` and `⚷ Certs` render on all eight 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 — fleet gap 11's rollout, half of it),
|
||||
is still in `todo/`.
|
||||
|
||||
## Locked decisions (and why)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user