phase: 18_nav_hamburger
**Phase 18 — collapse the nav bar into a hamburger: complete, all green.**
Verified (criteria):
- ≤640px: the tab links leave the bar, the ☰ is visible, and it opens a dropdown
under the sticky masthead (`top: 100%`, `z-index: 41`, viewport-wide) with the
five stacked 40px links; above 640px the one-row desktop masthead is unchanged
and the button hidden — `data/logs/nav-task04-verify.cjs` 24/24 and
`data/logs/nav-final-pass.cjs` 45/45; screenshots
`data/logs/nav-hamburger-{closed,open}-360.png`, `data/logs/nav-masthead-desktop-1280.png` ✓
- The four close paths: the button again, a tab click (navigates and closes),
Esc (focus back on the opener), and `matchMedia` resize back above the
breakpoint (drops a stale `.is-open`; `aria-expanded` tracks state on every
path through the single `setNavMenu()` funnel) ✓
- `uv run pytest` → 1454 passed, 15 skipped; `bash .agents/validate.sh` → exit 0
(coverage 98.94% ≥ 94, ruff clean, pyright 0 errors, `node --check` + hygiene
clean); served-page pins: button on all seven tabbed pages and nowhere on
/login, CSS and binding compared on the 640px literal ✓
Real-device finding (mobile Chrome): the emulated 360px pass was blind to the
system top-edge zone — Android Chrome is edge-to-edge by default, so the sticky
bar started under the status bar and the first-row ☰ sat in a region the device
does not deliver taps to (unreachable, not unbound; the same symptom brain of
reese fixed in production). The ≤640px block now pads the bar by
`env(safe-area-inset-top, 0px)` (the block's `padding-block` value restated on
the top side), verified on the operator's phone; `env()` resolves to 0 on every
non-notch surface, so all other widths render byte-identical. The served-byte
pin's rule loop carries the line; CDP's `safeAreaInsets` override does not feed
`env()`, which is the honest limit of the emulated proof.
Docs: AGENTS.md *UI conventions* masthead entry extended with the three pins,
the drift-comparison, the safe-area rule, and the real-device verification
caveat; README's `web/` bullet names the ≤640px collapse with the desktop
masthead unchanged above the breakpoint.
Deviations: pinned the shipped token shape (`#nav-toggle { display:
var(--nav-toggle-display) }` + the `:root` token) instead of the task file's
literal `.nav-toggle { display: none }` — that literal loses to
`button.ghost { display: inline-flex }` (task 01 measured it); the test
docstring says so, so nobody "restores" the broken shape. `uv pip list
--outdated` still advisory (fastapi 0.153.0 → 0.156.2); this phase moved no
dependency. NOTES.md (emptied) / TODO.md (operator items added) are
harness-side changes — left in the working tree, as in phase 17.
Next pending phase: `.agents/phases/todo/19_ups_events_expandable/`.
LLM-Generated: true
This commit is contained in:
@@ -739,6 +739,43 @@ 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 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">`,
|
||||
and `/login` carries neither — it renders the brand link and the theme toggle only. Its base
|
||||
rule is `#nav-toggle { display: var(--nav-toggle-display) }` with `--nav-toggle-display: none`
|
||||
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
|
||||
`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
|
||||
deliver taps to the page: the first-row button in it is *unreachable*, not unbound, and no
|
||||
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
|
||||
`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
|
||||
`matchMedia` turning false on resize back above the breakpoint — which drops a stale `.is-open`
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
Metric naming: dotted (`cpu.usage_pct`, `mem.used_pct`, `disk.used_pct@/srv`,
|
||||
`gpu.busy_pct@0`, `llm.decode_tokens_per_second`, `ups.charge_pct`), `@stream` for per-device
|
||||
@@ -960,7 +997,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: 1451 passed + 15 skipped (11 live,
|
||||
uv run pytest # hermetic: 1454 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
|
||||
@@ -1883,6 +1920,22 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
|
||||
which is the one route that must not be taken until that snapshot is re-staged from this repo:
|
||||
the Oct-18 copy predates phase 17's template *and* its re-run guard, so it would rewrite that
|
||||
unit silently — and no live change was made while pinning phase 16.
|
||||
12. **The nav collapses on a phone; the page content below it does not** (phase 18's verification
|
||||
pass, 2026-10-20). At 360px `/`'s fleet table measures 450-510 px inside a 320 px
|
||||
`section.card`, and `.card { overflow: hidden }` clips the tail columns (`WORST DISK`,
|
||||
`GPU BUSY`, `TEMP`, `UP`) with no scroll container to reach them — `table.data` only scrolls
|
||||
with its `.scroll` modifier, which that table does not carry. Measured as pre-existing by
|
||||
serving the `git show HEAD:` bytes of both `dash.css` and `pages/index.html` over a live
|
||||
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
|
||||
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. Two operator TODO items are unphased alongside it: never
|
||||
accepting a non-mTLS agent (fleet gap 11's rollout, half of it), and keeping both
|
||||
`⚙ Account` / `⚷ Certs` masthead links visible on the admin pages that today do not link to
|
||||
themselves (phase 15's deliberate rule, now questioned).
|
||||
|
||||
## Locked decisions (and why)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user