Files
dashboards/.agents/phases/complete/18_nav_hamburger/00_phase.md
T
ducoterra 10f86772ee 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
2026-10-20 16:47:10 -04:00

4.6 KiB
Raw Blame History

Phase 18 — Collapse the nav bar into a hamburger

Source: TODO.md L3 — "The nav bar needs to collapse into a hamburger like brain of reese does" Story: n/a Context: the seven mastheads that carry the tab nav — src/dashboards/web/pages/{index,system,disks,llm,ups,settings,certs}.html (each has nav class="tabs" aria-label="Dashboards" with the five dashboard links; login.html deliberately has no tabs — it renders only the brand link and the theme toggle, and stays untouched), src/dashboards/web/static/app.js (Dash.initChrome(), called from the one DOMContentLoaded at line 786 — the themeButton/clock/nav-status blocks there are the per-element if (element) no-op pattern this phase's binding follows), src/dashboards/web/static/dash.css (.masthead at line 95 — position: sticky; z-index: 40; height: var(--header-h), nav.tabs at line 131, the existing @media (max-width: 640px) block at line 286, the prefers-reduced-motion block at line 938), and the reference implementation the TODO names — brain_of_reese (sibling repo, read-only): frontend/assets/header.js lines 326–366 (the phase-46 binding: #nav-toggle shown ≤640px, setNavMenu(open) funnel, close on link click / Esc with focus return / matchMedia resize-back). Pinned test neighbours: tests/test_api.py::test_the_disks_tab_is_in_the_masthead_of_every_page (line 3837) and the served-page orphan-id audit in tests/test_auth_api.py (line 1654)

Objective

On viewports ≤640px the masthead's five tab links collapse into a dropdown opened by a hamburger button, mirroring brain of reese's phase-46 behaviour: one shared binding in app.js (a no-op on any page without the elements — /login), the inline nav hidden and the dropdown absolutely positioned under the sticky masthead, and the desktop masthead byte-identical in behaviour above the breakpoint. The masthead-right cluster stays visible on phones; only the tab nav collapses.

Dependencies

  • — (none; builds on the shipped masthead, initChrome, and the 640px media block)

Tasks

  1. 01_hamburger_markup.md — the #nav-toggle button and the nav id, identical across the seven tabbed pages, absent from /login
  2. 02_dropdown_css.md — the ≤640px block: toggle visible, inline tabs hidden, .is-open dropdown under the masthead, two-row masthead
  3. 03_toggle_binding.md — the initChrome binding: toggle + aria-expanded, close on link click / Esc / resize-back
  4. 04_tests_and_verification.md — the pins (present on all seven, absent on /login, CSS and binding), the orphan-id audit, and the narrow-viewport browser verification with screenshots

Testing & Quality

  • Unit/integration: tests/test_api.py — the served-page pins (see task 04); the existing masthead/tab pins (test_the_disks_tab_is_in_the_masthead_of_every_page, test_every_page_links_to_both_admin_pages) and the tests/test_auth_api.py orphan-id audit pass untouched — the new ids are referenced by aria-controls, the stylesheet, and app.js, so the audit must still clear
  • Coverage: >90% on new/modified code (the phase is markup + CSS + one small JS binding; the coverage gate applies to the changed Python test files)
  • The gate (bash .agents/validate.sh) includes node --check of web/static — the modified app.js must pass it

Documentation & Consistency

  • The final pass reviews README.md and AGENTS.md in full against the project as it stands after this phase — fixing every inconsistency found, including pre-existing ones not caused by this phase.
  • AGENTS.md's UI conventions section gains the masthead-collapsing convention (the 640px breakpoint, the toggle's identity, the close paths, and that /login carries no toggle) so a future masthead edit knows the contract. The README's page table is unchanged — the nav is the same five links at every width.

Completion Criteria

  • at ≤640px: the tab links are hidden, the hamburger is visible, and clicking it opens a dropdown containing the five links; above 640px the desktop masthead is unchanged
  • the dropdown closes on link click, on Esc (focus returns to the button), and on resize back above 640px; aria-expanded tracks the state
  • uv run pytest green, coverage not down, ruff + pyright clean
  • bash .agents/validate.sh green (including node --check of the modified app.js)
  • narrow-viewport screenshots (closed, open, and one desktop no-regression shot) in data/logs/ against the running dev server (invariant 10)
  • no behavior change in completed work: the desktop masthead and every page render identically at widths >640px