Files
dashboards/.agents/phases/complete/18_nav_hamburger/01_hamburger_markup.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

3.0 KiB

Task 01 — The hamburger button markup

Phase: 18_nav_hamburger · Source: TODO.md:3 — "The nav bar needs to collapse into a hamburger like brain of reese does" Story: n/a

Objective

The seven pages that carry the tab nav each gain the hamburger button and the nav id, so the (later task's) CSS and binding have stable anchors to work on. The markup is identical on every page, and /login — which deliberately renders only the brand link and the theme toggle — stays untouched.

Work

  1. src/dashboards/web/pages/index.html · system.html · disks.html · llm.html · ups.html · settings.html · certs.html — in each masthead, immediately before nav class="tabs", add the toggle:
    <button class="ghost nav-toggle" id="nav-toggle" type="button"
            aria-expanded="false" aria-controls="nav-tabs" aria-label="Toggle navigation">☰</button>
    
    and give the nav its id: <nav class="tabs" id="nav-tabs" aria-label="Dashboards">. The button sits between .brand and nav.tabs, inside the same <header class="masthead"> — one element per page, no script changes in this task.
  2. src/dashboards/web/static/dash.css — add the one base rule beside the other button.ghost, a.ghost family (the masthead section): .nav-toggle { display: none; } — the button is invisible on every width until task 02's ≤640px block shows it, so the project stays exactly as it looks today after this task.
  3. src/dashboards/web/pages/login.html — no change. Verify by grep -c 'id="nav-toggle"' returning 0 (the pin lands in task 04).
  4. ASSUMPTION: the TODO named brain of reese's behaviour, not its breakpoint or glyph. This task takes the 640px breakpoint (brain's (max-width: 640px) and dash.css's existing 640px block) and the ☰ glyph with aria-label="Toggle navigation"; the CSS task owns the visual.
  5. ASSUMPTION: only the tab nav collapses on phones. The masthead-right cluster (#nav-status dots, pause, ⚷ Certs, ⚙ Account, theme toggle) stays visible; the masthead wraps to two rows at ≤640px (task 02).

Testing & Quality

  • Integration: the served pages still pass the untouched masthead pins (test_the_disks_tab_is_in_the_masthead_of_every_page, test_every_page_links_to_both_admin_pages) and the orphan-id audit — #nav-toggle is referenced by the (task 02) stylesheet rule and aria-controls, so the audit must not flag it
  • Coverage: >90% on modified code (HTML only — the gate's node --check and pytest suite are the checks)

Completion Criteria

  • grep -l 'id="nav-toggle"' src/dashboards/web/pages/*.html lists exactly the seven tabbed pages, never login.html
  • every one of the seven carries the identical button block and the nav id (diff of the blocks across pages is empty)
  • uv run pytest -q green; bash .agents/validate.sh green
  • no behavior change: at every width the pages render exactly as before after this task (the button is display:none from the base rule shipped with it)