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:
@@ -609,8 +609,10 @@ certs.py ─ root CA + client cert, minted once at startup (unreadable row → n
|
||||
marker that says how many it hid.
|
||||
* `api.py` — the JSON every page reads: `/api/config`, `/api/health`, `/api/events`,
|
||||
`/api/history`, `/api/metrics`, `/api/summary`, one `snapshot` + `history` pair per
|
||||
dashboard (`/api/system/*`, `/api/disks/*`, `/api/llm/*`, `/api/ups/*`), and the certificate
|
||||
authority's own surface (`/api/certs`, `/api/certs/<name>/bundle`, `POST …/rotate`, `POST
|
||||
dashboard (`/api/system/*`, `/api/disks/*`, `/api/llm/*`, `/api/ups/*` — including
|
||||
`/api/ups/commands`, which answers what may be sent to each UPS right now and why not when it
|
||||
may not), and the certificate authority's own surface (`/api/certs`,
|
||||
`/api/certs/<name>/bundle`, `POST …/rotate`, `POST
|
||||
…/revoke`, `POST /api/certs/root/rotate`). GETs everywhere except `POST /api/ups/command`,
|
||||
which stays the **only** route that dials anything: the `/api/certs/*` writes are `meta` row
|
||||
writes through `certs.py`, the same class of write `POST /api/auth/password` is, and a
|
||||
@@ -640,9 +642,11 @@ certs.py ─ root CA + client cert, minted once at startup (unreadable row → n
|
||||
* `web/` — five static dashboard pages (`/`, `/system`, `/disks`, `/llm`, `/ups`) plus the three
|
||||
admin pages (`/login`, `/settings`, `/certs`), one stylesheet, hand-rolled
|
||||
SVG charts (`charts.js`), and a masthead status dot per dashboard job (`disks` has its own:
|
||||
the same agent can answer the 10-second utilisation poll and have a wedged SMART pass). No
|
||||
framework, no CDN, no bundler, and no narration: a page never explains its own pipeline, so
|
||||
the words on screen are the ones you act on — a unit, a `Δ10.0s` provenance tag, a
|
||||
the same agent can answer the 10-second utilisation poll and have a wedged SMART pass). At
|
||||
≤640px the five tab links leave the bar and come back as a dropdown under it, opened by the `☰`
|
||||
button (`/login` renders no tabs, so it has no button either), and above it the masthead stays
|
||||
the one row it has always been. No framework, no CDN, no bundler, and no narration: a page never
|
||||
explains its own pipeline, so the words on screen are the ones you act on — a unit, a `Δ10.0s` provenance tag, a
|
||||
`SIMULATED` badge, or the sentence beside an empty panel. The overview (`/`) is exactly two panels — a
|
||||
**Warnings & errors** list built from `/api/summary`'s `alerts` (each row links to the
|
||||
dashboard that explains it) and the **Fleet** table — and nothing else: an empty alert
|
||||
|
||||
Reference in New Issue
Block a user