phase: 85_mobile_menu_gate_overlap

All verification complete. Final report:

**Phase 85 final verification pass — all green** (tasks 01–03 already complete; independently re-verified from scratch)
- Verified fix in tree: `styles.css` `.auth-gate` z-index 500→15 + rewritten stacking comment (no `z-index: 500` left); `index.html`/`document.html` comment-only; `token-gate.js` docstring-only (logic byte-identical); `test_mobile_hamburger_nav.py` real-click conversion + new `test_anonymous_toggle_tappable_with_gate_up`; new `tests/unit/test_gate_header_stacking.py` (3 pins); `test_api_tokens.py` untouched
- `uv run pytest` → 1717 passed, 1 warning (exit 0)
- `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90%)
- E2E in isolation: `test_mobile_hamburger_nav.py` **8 passed**; `test_api_tokens.py` **9 passed** (unchanged); `test_smoke.py` **3 passed**; `test_document_viewer.py` **7 passed**
- `uv run ruff check . && uv run pyright` → clean / 0 errors
- Live probe (375×812, anonymous, fresh server): on `/` and `/history.html` `elementFromPoint` at toggle → SVG `path`, never `#auth-gate`; real click opens menu (`aria-expanded=true`); exactly one visible nav link (Chat) + sign-in copy; Chat link topmost over gate; `#main` inert — criteria 1, 2, 3 confirmed directly
- Criteria: (1) real-click menu on / + /history ✅ (2) dropdown above gate ✅ (3) anonymous contents + #main inert ✅ (4) admin byte-identical (phase-46 tests green) ✅ (5) doc-viewer gate under bar (CSS pin + doc-viewer suite) ✅ (6) full gate green ✅ (7) diff scoped to the 5 code files + new unit test + phase files, nothing in `app/` ✅ (8) commit + todo→complete move left to harness per executor rules (task files already in `complete/`)
- Note: `.agents/remediation_plan.md` (untracked) is a pre-existing artifact of the earlier security audit — left untouched; a missing "N passed" line seen mid-pass was my own `-q`+addopts `-q` = `-qq` quirk, not a defect
- Next pending phase: `86_history_page_width`
This commit is contained in:
2026-09-08 01:56:37 -04:00
parent f4150421bb
commit 412a560348
24 changed files with 692 additions and 57 deletions
@@ -0,0 +1,12 @@
All verification complete. Final report:
**Phase 85 final verification pass — all green** (tasks 01–03 already complete; independently re-verified from scratch)
- Verified fix in tree: `styles.css` `.auth-gate` z-index 500→15 + rewritten stacking comment (no `z-index: 500` left); `index.html`/`document.html` comment-only; `token-gate.js` docstring-only (logic byte-identical); `test_mobile_hamburger_nav.py` real-click conversion + new `test_anonymous_toggle_tappable_with_gate_up`; new `tests/unit/test_gate_header_stacking.py` (3 pins); `test_api_tokens.py` untouched
- `uv run pytest` → 1717 passed, 1 warning (exit 0)
- `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90%)
- E2E in isolation: `test_mobile_hamburger_nav.py` **8 passed**; `test_api_tokens.py` **9 passed** (unchanged); `test_smoke.py` **3 passed**; `test_document_viewer.py` **7 passed**
- `uv run ruff check . && uv run pyright` → clean / 0 errors
- Live probe (375×812, anonymous, fresh server): on `/` and `/history.html` `elementFromPoint` at toggle → SVG `path`, never `#auth-gate`; real click opens menu (`aria-expanded=true`); exactly one visible nav link (Chat) + sign-in copy; Chat link topmost over gate; `#main` inert — criteria 1, 2, 3 confirmed directly
- Criteria: (1) real-click menu on / + /history ✅ (2) dropdown above gate ✅ (3) anonymous contents + #main inert ✅ (4) admin byte-identical (phase-46 tests green) ✅ (5) doc-viewer gate under bar (CSS pin + doc-viewer suite) ✅ (6) full gate green ✅ (7) diff scoped to the 5 code files + new unit test + phase files, nothing in `app/` ✅ (8) commit + todo→complete move left to harness per executor rules (task files already in `complete/`)
- Note: `.agents/remediation_plan.md` (untracked) is a pre-existing artifact of the earlier security audit — left untouched; a missing "N passed" line seen mid-pass was my own `-q`+addopts `-q` = `-qq` quirk, not a defect
- Next pending phase: `86_history_page_width`
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
............................................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,17 @@
Task 01 complete — all green.
**Implemented**
- `frontend/assets/styles.css`: `.auth-gate` `z-index: 500 → 15` (the one-value fix, A1) + stacking comment rewritten to the phase-85 contract (15 gate < 20 header < 21 dropdown; skip-link 100 / 1000-family overlays unaffected; `#main` lock is JS, not z-order)
- `frontend/index.html` / `frontend/document.html`: gate-section comments reworded (header stays reachable; `#main` stays inert) — markup byte-identical, comment-only
- `frontend/assets/token-gate.js`: docstring sweep only — "only interactive surface" → "only CONTENT-level interactive surface" + phase-85 note; logic byte-identical
- New `tests/unit/test_gate_header_stacking.py`: 3 source-level z-order pins (gate=15, header=20 with gate<header, mobile `.app-nav` keeps 21>15) + the gate still covers content (fixed/inset:0, `[hidden]` display:none)
**Test / lint results**
- `uv run pytest tests/unit/test_gate_header_stacking.py -v` → 3 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1717 passed, `app/` TOTAL **99%** (>90% ✓)
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings
- `git diff --stat` limited to the 4 frontend files (CSS value + comments only) + the new test; `grep "z-index: 500"` no longer matches anything
**Decisions**: the dropdown's `z-index: 21` lives on the base `.app-nav` rule (shared with `.is-open`), so the pin targets `.app-nav` in the ≤640px block — same as the existing phase-46 pin; value 15 kept as owner-decided.
**Next pending task**: `02_e2e_pin_updates.md` (same phase — real-click conversion + TODO-regression E2E pin).
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
............................................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,19 @@
All green. Task 02 is complete.
**Implemented**
- `tests/e2e/test_mobile_hamburger_nav.py`: retired `_js_open_menu` (JS-dispatch drive) — anonymous tests now use the real-click helper `_open_menu` (`page.click("#nav-toggle")`), docstring rewritten to the phase-85 contract (gate z 15 below header z 20)
- New regression pin `test_anonymous_toggle_tappable_with_gate_up`: with the gate up (`#main` inert), `elementFromPoint` at the toggle center resolves to the toggle/SVG child, never `#auth-gate`; real click opens the menu above the gate (Chat link visible + topmost at its center, gate still up); Esc settles closed
- Module docstring: phase-85 stacking paragraph + numbered mapping entry; all phase-79 "intercepted by the gate" test docstrings/comments swept (assertions kept exactly: one visible "Chat" link, Esc refocus, outside-click at (10,780) doesn't close, 180ms pins)
- `test_api_tokens.py`: verified no hamburger pins (grep clean); run unchanged, green
**Results**
- `uv run pytest tests/e2e/test_mobile_hamburger_nav.py -v --no-cov` → 8 passed (isolation)
- `uv run pytest tests/e2e/test_api_tokens.py -v --no-cov` → 9 passed, file unchanged (isolation)
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → 3 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1717 passed, coverage 99% (>90%)
- `uv run ruff check . && uv run pyright` → clean
- Criterion grep `querySelector('#nav-toggle').click()` → no matches; my diff touches only the one E2E file (frontend changes in tree are task-01's, committed atomically by the harness at phase end)
**Deviations** — none; the JS helper was folded into `_open_menu` rather than duplicated (identical real-click body + same 3 assertions).
**Next pending task:** `.agents/phases/todo/85_mobile_menu_gate_overlap/03_verify_and_commit.md`
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
............................................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,11 @@
Screenshot confirms the fix visually: the anonymous menu (Chat + Sign in) renders **above** the gate card. Phase 85 is fully gated.
**Report — Phase 85, task 03 (verify & commit)**
- Full gate green: `uv run pytest` → 1717 passed; `--cov=app` → TOTAL **99%** (>90% floor, no `app/` change)
- E2E in isolation: `test_mobile_hamburger_nav.py` → 8 passed (real-click contract + new `elementFromPoint` pin); `test_api_tokens.py` → 9 passed, **file unchanged** (phase-79 lock contract intact); `test_smoke.py` → 3 passed; `test_document_viewer.py` → 7 passed
- `uv run ruff check . && uv run pyright` → clean (0 errors/warnings)
- Live check (dev server :8000, 375×812, not signed in): 12/12 checks passed — real tap opens the menu above the gate (Chat + sign-in visible, token card behind/below), `elementFromPoint` at toggle resolves to the SVG (never `#auth-gate`), wrong-token error line + menu independence (Esc closes, gate stays), admin menu unchanged; log + 2 screenshots in `.agents/reports/85_mobile_menu_gate_overlap/`
- Working tree verified: only `styles.css` (z 500→15 + comment), `index.html`/`document.html` (comments only), `token-gate.js` (docstring only), `test_mobile_hamburger_nav.py`, new `tests/unit/test_gate_header_stacking.py` + phase files — nothing in `app/`, `pyproject.toml`, `uv.lock`
- Deviation (per executor rules, overrides task steps 3–4): **no `git commit` made and phase dir not moved** — all changes left in the working tree for the harness's single atomic phase commit and the `todo/ → complete/` move
- Next pending task: none in this phase (task 03 was the last); following phases: `86_history_page_width`, `87_big_read_progress`
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
............................................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+16 -11
View File
@@ -1631,23 +1631,28 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* Phase 79 (task 05): the in-app token gate — the gate surface of the
two token-only pages (the shell + the document viewer, one shared
.auth-gate markup each). A body-level FIXED overlay (the
body-level doc-modal precedent): while visible it is the ONLY
interactive surface — assets/token-gate.js locks the root (#main,
inert) so focus and keyboard traversal stay inside the gate
(WCAG, the inert-pair contract).
Stacking: z-index 500 — above the app content (the sticky header is
20, the skip-link 100) but BELOW the doc-modal (1000), which can
only be opened from the unlocked app anyway. Solid --bg: the canvas
+ grid texture live on <html> (the body stays transparent), so the
overlay reads as the app's own surface — no blur (the phase-08
perf anchor).
body-level doc-modal precedent): while visible it is the only
CONTENT-level interactive surface — assets/token-gate.js locks the
root (#main, inert) so focus and keyboard traversal stay inside
the gate (WCAG, the inert-pair contract).
Stacking (phase 85, TODO.md L3): z-index 15 — ABOVE all app
content (static) but BELOW the sticky header (20) and its mobile
dropdown (21, inside the header's stacking context): the
unauthenticated visitor keeps a reachable hamburger + menu while
the gate still covers and locks the app content (#main stays
inert — the lock is JS, not z-order — phase 79). The skip-link
(100) and the 1000-family overlays (doc-modal, sync backdrop) are
unaffected — all above 15; the doc-modal can only be opened from
the unlocked app anyway. Solid --bg: the canvas + grid texture
live on <html> (the body stays transparent), so the overlay reads
as the app's own surface — no blur (the phase-08 perf anchor).
The centered card reuses the #sources-gate visual language
(surface card + hairline + glyph + heading + sub + action) in a
tighter column. */
.auth-gate {
position: fixed;
inset: 0;
z-index: 500;
z-index: 15;
display: flex; /* the card is the only in-flow child — margin: auto centers it */
overflow-y: auto; /* short viewports: the card scrolls fully into view */
background: var(--bg);
+7 -3
View File
@@ -193,9 +193,13 @@ export async function mountGate(lockRoot, onAuthed) {
/* Anonymous: reveal the gate (drop hidden AND inert — the inert-pair
contract), lock the app, focus the token input. The gate is the
only interactive surface while visible: the lock root is inert,
so Tab never reaches the locked app (the composer on the shell,
the content on the viewer). */
only CONTENT-level interactive surface while visible: the lock
root is inert, so Tab never reaches the locked app (the composer
on the shell, the content on the viewer). The header was never
the lock root (and is never inert) — it was only visually
covered pre-phase-85: the gate's z-index (15) now sits below the
header (20), so the mobile hamburger + menu stay reachable while
the app content stays inert-locked (TODO.md L3). */
if (gate) {
gate.hidden = false;
gate.inert = false;
+4 -1
View File
@@ -162,7 +162,10 @@
root; the existing boot sequence (whoami → load content) runs
as onAuthed — an admin (or a validly cached token user) gets
it immediately, the gate never shows. SHIPS hidden + inert
(the phase-16 ship-hidden pattern). -->
(the phase-16 ship-hidden pattern). Phase 85 (TODO.md L3):
same stacking as the shell gate — the overlay (z-index 15)
sits below the viewer's sticky bar (20), so the hamburger +
menu stay reachable while #main stays inert-locked. -->
<section class="auth-gate" id="doc-auth-gate" hidden inert aria-labelledby="doc-auth-gate-title">
<div class="auth-gate-card">
<div class="auth-gate-glyph" aria-hidden="true">
+7 -3
View File
@@ -896,9 +896,13 @@
<!-- Phase 79 (task 05): the in-app token gate — a body-level
position:fixed; inset:0 overlay (the body-level doc-modal
precedent) that is the ONLY interactive surface while visible:
assets/token-gate.js locks #main (inert) so focus and keyboard
traversal stay in the gate (WCAG, the inert-pair contract). It
precedent) that is the only CONTENT-level interactive surface
while visible: assets/token-gate.js locks #main (inert) so
focus and keyboard traversal stay in the gate (WCAG, the
inert-pair contract). Phase 85 (TODO.md L3): the overlay sits
BELOW the sticky header (z-index 15 < 20) — it covers #main's
content region visually, but the header (hamburger + menu)
stays reachable for the unauthenticated visitor. It
SHIPS hidden + inert (the phase-16 ship-hidden pattern) — an
authenticated boot (admin, or a valid cached token re-authed
silently at mount) never shows it for a frame. The gate is the
+133 -39
View File
@@ -15,6 +15,14 @@ visibility contract intact INSIDE the menu; at >640px the bar is
byte-identical to pre-phase-46 (hamburger absent, inline pills). No
document is ever needed — the suite exercises the shared header only.
Phase 85 (TODO.md L3): the anonymous visitor meets the token gate —
but the gate (z-index 15) now sits BELOW the sticky header (z 20), so
a REAL tap on ``#nav-toggle`` reaches the toggle for anonymous
visitors exactly as for admin: every test in this suite drives the
menu with real clicks (the phase-79 JS-dispatched workaround for the
gate-overlay interception is retired — the gate covers only ``#main``,
which stays ``inert``; it no longer covers the header).
The conftest ``page`` fixture is 1280×800, so the mobile tests create
fresh 375×812 pages via the session ``browser`` fixture (one page per
test; the reduced-motion test gets its own context).
@@ -29,20 +37,24 @@ Test → story mapping (Playwright Mapping Rule):
2. ``test_anonymous_menu_contents`` — anonymous at 375px: the menu
shows EXACTLY one visible link ("Chat"); the three admin-only links
stay ``hidden`` inside the menu; the open flips ``aria-expanded``.
3. ``test_admin_menu_contents`` — admin at 375px: the menu shows all
3. ``test_anonymous_toggle_tappable_with_gate_up`` — the TODO.md L3
regression pin: anonymous toggle tappable with the gate up —
``elementFromPoint`` at the toggle center never ``#auth-gate``,
real click opens the menu above the gate.
4. ``test_admin_menu_contents`` — admin at 375px: the menu shows all
four links (the whoami reveal works inside the menu).
4. ``test_link_click_navigates_and_closes`` — admin at 375px: clicking
5. ``test_link_click_navigates_and_closes`` — admin at 375px: clicking
"RAG" navigates to /sources.html and the menu on the arrival
page ships closed.
5. ``test_esc_and_outside_close`` — Esc closes AND returns focus to the
6. ``test_esc_and_outside_close`` — Esc closes AND returns focus to the
toggle; an outside click does NOT close (accepted — see the test
docstring for why).
6. ``test_animation_and_reduced_motion`` — motion allowed: the
7. ``test_animation_and_reduced_motion`` — motion allowed: the
180ms opacity/transform transition pair is live and the open flips
class + aria; ``reducedMotion: "reduce"``: no transition in EITHER
state (the .is-open state included — the specificity trap) and
open/close still works.
7. ``test_desktop_unchanged`` — 1280×800 regression: the hamburger is
8. ``test_desktop_unchanged`` — 1280×800 regression: the hamburger is
``display: none`` and the inline nav renders in the bar exactly as
before (admin: all four links, all inside the header band).
"""
@@ -105,6 +117,13 @@ def _visible_nav_links(page: Page) -> list[str]:
def _open_menu(page: Page) -> None:
"""Open the menu with a REAL click on the toggle.
Phase 85 (TODO.md L3): the gate (z-index 15) sits BELOW the sticky
header (z 20), so a real tap reaches the toggle for anonymous
visitors too — the phase-79 JS-dispatched workaround (the gate then
covered the header at z 500 and intercepted the click) is retired,
and every test in this suite uses this one real-click helper."""
page.click("#nav-toggle")
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
# to_have_class(string) is an EXACT match on the class attribute —
@@ -113,20 +132,6 @@ def _open_menu(page: Page) -> None:
expect(page.locator("#app-nav")).to_have_css("opacity", "1")
def _js_open_menu(page: Page) -> None:
"""Phase 79 (task 05): the in-app token gate is a full-viewport
overlay for ANONYMOUS visitors — it physically covers the header,
so a real click on #nav-toggle is intercepted by the gate (the gate
is the only interactive surface; the header is locked out with the
rest of the page). The binding is identical, so the menu contract
is driven programmatically: a JS-dispatched click runs the exact
same listener a real click would."""
page.evaluate("() => document.querySelector('#nav-toggle').click()")
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
expect(page.locator("#app-nav")).to_have_class(re.compile(r"\bis-open\b"))
expect(page.locator("#app-nav")).to_have_css("opacity", "1")
def _assert_menu_closed(page: Page) -> None:
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "false")
assert "is-open" not in (page.locator("#app-nav").get_attribute("class") or ""), (
@@ -205,17 +210,17 @@ def test_anonymous_menu_contents(
preserved by reusing the same <nav> element); opening flips
aria-expanded true.
Phase 79 (task 05): the anonymous visitor meets the token gate — a
full-viewport overlay that covers the header — so the toggle is
driven programmatically (the binding is identical; see
_js_open_menu)."""
Phase 85 (TODO.md L3): the anonymous visitor meets the token gate
— but the gate (z 15) now sits BELOW the header (z 20), so the
toggle is a real, tappable target and the menu is driven with a
real click exactly as for admin."""
page = _mobile_page(browser)
try:
page.goto(app_url)
_wait_settled_anonymous(page)
_assert_menu_closed(page)
_js_open_menu(page)
_open_menu(page)
assert _visible_nav_links(page) == ["Chat"], (
"anonymous: the menu must show exactly one visible link (Chat)"
)
@@ -223,7 +228,96 @@ def test_anonymous_menu_contents(
expect(page.locator(sel)).to_be_hidden()
# A second click closes it again — aria-expanded round-trips.
page.evaluate("() => document.querySelector('#nav-toggle').click()")
page.click("#nav-toggle")
_assert_menu_closed(page)
finally:
page.close()
def test_anonymous_toggle_tappable_with_gate_up(
browser: Browser, app_url: str, db_ready: None
) -> None:
"""TODO.md L3 regression pin (phase 85): the anonymous visitor meets
the token gate, but the gate (z-index 15) sits BELOW the sticky
header (z 20) — so the toggle is a REAL, tappable target:
``document.elementFromPoint`` at the toggle center resolves to the
toggle itself (or its SVG child), NEVER ``#auth-gate`` (the exact
probe that returned the gate in the bug reproduction), and a real
click opens the menu ABOVE the gate — the "Chat" link visible with
the gate still up (the dropdown's z context, 21 inside the
header's 20, paints over the gate's 15). The lock is untouched:
``#main`` stays ``inert``. Leaves the page clean — Esc settles the
menu closed."""
page = _mobile_page(browser)
try:
page.goto(app_url)
_wait_settled_anonymous(page)
# The gate is up and the app is locked (the phase-79 contract)…
expect(page.locator("#auth-gate")).to_be_visible(timeout=30_000)
assert page.evaluate("() => document.getElementById('main').inert === true"), (
"the gate must keep #main inert while it is up"
)
# …but the probe at the toggle center no longer resolves to the
# gate: it hits the toggle itself or a descendant of it (the
# SVG path) — the exact probe that returned #auth-gate in the
# bug reproduction (TODO.md L3).
probe = page.evaluate(
"""() => {
const t = document.querySelector('#nav-toggle');
const r = t.getBoundingClientRect();
const el = document.elementFromPoint(
r.left + r.width / 2, r.top + r.height / 2);
return {
id: el ? el.id : null,
tag: el ? el.tagName.toLowerCase() : null,
toggle_or_descendant: el !== null && t.contains(el),
};
}"""
)
assert probe["id"] != "auth-gate", (
f"the toggle center must not resolve to the gate, "
f"got <{probe['tag']} id={probe['id']!r}> (the phase-79 bug)"
)
assert probe["toggle_or_descendant"], (
f"the toggle center must resolve to the toggle or its SVG "
f"child, got <{probe['tag']} id={probe['id']!r}>"
)
# A REAL click opens the menu (the owner's exact scenario —
# "clicking it … does not expand the menu" is fixed), and the
# menu is ABOVE the gate: with the gate still up the
# dropdown's "Chat" link is visible, and the topmost element at
# its center is the link (or a descendant), never #auth-gate.
_open_menu(page)
expect(page.locator("#auth-gate")).to_be_visible()
chat = page.locator("#app-nav a[href='/']")
expect(chat).to_be_visible()
link_probe = page.evaluate(
"""() => {
const a = document.querySelector("#app-nav a[href='/']");
const r = a.getBoundingClientRect();
const el = document.elementFromPoint(
r.left + r.width / 2, r.top + r.height / 2);
return {
id: el ? el.id : null,
tag: el ? el.tagName.toLowerCase() : null,
link_or_descendant: el !== null && a.contains(el),
};
}"""
)
assert link_probe["id"] != "auth-gate", (
f"the open dropdown must paint ABOVE the gate, the Chat "
f"link center resolved to the gate ({link_probe!r})"
)
assert link_probe["link_or_descendant"], (
f"the Chat link must be the topmost target at its center, "
f"got <{link_probe['tag']} id={link_probe['id']!r}>"
)
# Leave the page clean: Esc settles it closed.
page.keyboard.press("Escape")
_assert_menu_closed(page)
finally:
page.close()
@@ -303,11 +397,10 @@ def test_esc_and_outside_close(
page.goto(app_url)
_wait_settled_anonymous(page)
# Phase 79 (task 05): the anonymous visitor's toggle click is
# intercepted by the gate overlay — drive the identical binding
# programmatically (see _js_open_menu).
# Phase 85 (TODO.md L3): the gate (z 15) sits below the header
# (z 20) — the real click reaches the anonymous toggle too.
# Esc closes + refocuses the opener.
_js_open_menu(page)
_open_menu(page)
page.keyboard.press("Escape")
_assert_menu_closed(page)
assert page.evaluate("() => document.activeElement.id") == "nav-toggle", (
@@ -316,10 +409,11 @@ def test_esc_and_outside_close(
# Outside click: the menu STAYS open (accepted behavior — the
# locked close set is Esc + link + resize, not backdrop click).
# Phase 79 (task 05): for the anonymous visitor the "outside"
# point is the gate overlay itself — a REAL mouse click below
# the centered card (outside the nav, intercepted by the gate).
_js_open_menu(page)
# Phase 85: for the anonymous visitor the "outside" point is
# the gate overlay itself — a REAL mouse click below the
# centered card, outside the nav (the gate covers the content
# but no longer the header; it carries no close listener).
_open_menu(page)
page.mouse.click(10, 780) # the gate overlay — a neutral, non-nav point
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
assert "is-open" in (page.locator("#app-nav").get_attribute("class") or ""), (
@@ -364,9 +458,9 @@ def test_animation_and_reduced_motion(
f"got {report['property']!r}"
)
# Opening flips class + aria together (the animated state).
# Phase 79 (task 05): the anonymous toggle click is intercepted
# by the gate overlay — programmatic drive, same binding.
_js_open_menu(page)
# Phase 85: the gate sits below the header — the real click
# reaches the anonymous toggle.
_open_menu(page)
page.keyboard.press("Escape")
_assert_menu_closed(page)
finally:
@@ -389,9 +483,9 @@ def test_animation_and_reduced_motion(
assert _stilled("#app-nav") == "0s", (
f"reduced motion: closed state must not transition, got {_stilled('#app-nav')!r}"
)
# Phase 79 (task 05): programmatic drive (the gate overlay
# intercepts the anonymous real click — same binding).
_js_open_menu(rpage)
# Phase 85: real drive (the gate no longer intercepts the
# anonymous click — it sits below the header).
_open_menu(rpage)
expect(rpage.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
expect(rpage.locator("#app-nav")).to_have_class(re.compile(r"\bis-open\b"))
assert _stilled("#app-nav") == "0s", (
+126
View File
@@ -0,0 +1,126 @@
"""Unit: the gate-below-header z-order contract (phase 85, task 01).
TODO.md L3 (owner bug report, 2026-09-07): "Only on the chat page, and
only when navigating there directly, does the hamburger menu on mobile
not work. … This makes the menu inaccessible on mobile."
Bug basis (confirmed by live reproduction): the phase-79 token gate
ships as a full-viewport overlay at ``z-index: 500`` — ABOVE the sticky
``.app-header`` (20) — so for an unauthenticated visitor
``document.elementFromPoint`` at the ``#nav-toggle`` center returned
``#auth-gate`` in every shell view: a real tap on the hamburger was
intercepted by the overlay and the menu could never open. (The chat
page was just the entry point; the gate broke the menu on every view.
``login.html`` has no gate, which is why the owner saw it work "when
logging in".)
The fix is ONE CSS value (owner decision A1): ``.auth-gate`` moves to
``z-index: 15`` — above all app content (static) but BELOW the sticky
header (20) and its mobile dropdown (21, inside the header's stacking
context). The gate still covers and locks the app content (``#main``
stays ``inert`` — the lock is JS, not z-order — phase 79); only the
header's VISUAL lock is lifted. This module pins the three z-order
values at source level (the house pattern of
``tests/unit/test_hamburger_nav.py``: read ``styles.css`` as text, no
browser) so the regression that put the gate at 500 cannot return.
"""
from __future__ import annotations
import re
from pathlib import Path
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
STYLES_CSS = FRONTEND / "assets" / "styles.css"
def _text(path: Path) -> str:
assert path.is_file(), f"missing frontend file: {path}"
return path.read_text(encoding="utf-8")
def _css() -> str:
"""styles.css with comments stripped (a comment may legally carry
braces — the brace-matching helpers below must never see them)."""
return re.sub(r"/\*.*?\*/", "", _text(STYLES_CSS), flags=re.S)
def _media_block(css: str, query: str) -> str:
"""The full text of the FIRST ``@media <query>`` block (brace-matched,
nested rules included verbatim)."""
m = re.search(re.escape(query) + r"[^{]*\{", css)
assert m, f"missing {query!r} media query in styles.css"
depth = 0
for i in range(m.end() - 1, len(css)):
if css[i] == "{":
depth += 1
elif css[i] == "}":
depth -= 1
if depth == 0:
return css[m.start() : i + 1]
raise AssertionError(f"unbalanced braces in {query!r} media block")
def _rule_block(css: str, selector: str) -> str:
"""The first rule body for ``selector`` (e.g. ``.auth-gate``). The
``\\s*\\{`` requires the opening brace right after the selector, so a
decorated variant (``.auth-gate[hidden]``) is never matched."""
m = re.search(r"(?<![\w-])" + re.escape(selector) + r"\s*\{([^}]*)\}", css)
assert m, f"missing rule for {selector!r}"
return m.group(1)
def _z_index(block: str) -> int:
m = re.search(r"z-index:\s*(\d+)", block)
assert m, "the rule must carry an explicit z-index"
return int(m.group(1))
def test_auth_gate_sits_below_the_sticky_header() -> None:
"""THE regression pin: the .auth-gate overlay carries z-index 15 —
below the sticky .app-header (20), so taps on the bar (and the
hamburger) reach the bar for the unauthenticated visitor instead of
the overlay (TODO.md L3). The old 500 (above the header) must not
return."""
css = _css()
gate = _z_index(_rule_block(css, ".auth-gate"))
header = _z_index(_rule_block(css, ".app-header"))
assert gate == 15, f".auth-gate must be pinned at z-index 15 (found {gate})"
assert header == 20, f".app-header must keep z-index 20 (found {header})"
assert gate < header, (
"the gate must sit BELOW the header — otherwise the overlay "
"intercepts the #nav-toggle again (the phase-79 bug)"
)
def test_auth_gate_still_covers_the_app_content() -> None:
"""The lock is JS, not z-order (phase 79): the rest of the overlay
contract is untouched — body-level fixed full-viewport cover
(position:fixed + inset:0), and the [hidden] state stays
display:none (the ship-hidden skeleton contract)."""
css = _css()
block = _rule_block(css, ".auth-gate")
assert "position: fixed" in block, ".auth-gate must stay position:fixed"
assert "inset: 0" in block, ".auth-gate must still cover the full viewport"
hidden = _rule_block(css, ".auth-gate[hidden]")
assert "display: none" in hidden, (
".auth-gate[hidden] must stay display:none (ship-hidden)"
)
def test_mobile_dropdown_stays_above_the_gate() -> None:
"""The mobile dropdown (.app-nav, ≤640px — the z-index lives on the
base rule, shared by the closed and the .is-open states) keeps
z-index 21 = header + 1 (the phase-46 relationship pinned in
test_hamburger_nav): inside the header's stacking context the open
menu paints above the bar content AND above the gate (15)."""
css = _css()
mobile = _media_block(css, "@media (max-width: 640px)")
assert _z_index(_rule_block(mobile, ".app-nav")) == 21, (
"the mobile .app-nav must keep z-index 21 (header 20 + 1)"
)
# The header (20) itself is global (outside media queries) — the
# ≤640px block restyles it but never changes its stacking level,
# so the dropdown's 21 stays the header's own +1 on mobile too.
assert _z_index(_rule_block(mobile, ".app-nav")) > 15, (
"the open menu must paint above the gate (21 > 15)"
)