diff --git a/.agent/phases/todo/40_tuning_toggle_flash/01_toggle_ships_hidden.md b/.agent/phases/complete/40_tuning_toggle_flash/01_toggle_ships_hidden.md similarity index 100% rename from .agent/phases/todo/40_tuning_toggle_flash/01_toggle_ships_hidden.md rename to .agent/phases/complete/40_tuning_toggle_flash/01_toggle_ships_hidden.md diff --git a/frontend/assets/header.js b/frontend/assets/header.js index 6201bda..52381bd 100644 --- a/frontend/assets/header.js +++ b/frontend/assets/header.js @@ -26,10 +26,15 @@ * list (newest-first, textContent-rendered, per-note delete, count * badge, the #steering-announcer live region) — so the toggle can * sit in every page's header with zero page-script duplication. - * refreshSteering() / announceSteering() are exported for the chat - * page's per-bubble Tune form (which stays in app.js); anonymous - * visitors get the phase-16 "absent, not hidden" treatment (toggle - * + panel removed from the DOM, /api/steering never fetched); + * The toggle SHIPS hidden in every page (phase 40, 2026-08-27, + * TODO.md L3 — the exact ship-hidden / reveal-for-admin contract + * the admin-only nav links use: anonymous never sees it for a + * single frame) and initSharedHeader unhides it only when whoami + * says admin. refreshSteering() / announceSteering() are exported + * for the chat page's per-bubble Tune form (which stays in app.js); + * anonymous visitors get the phase-16 "absent, not hidden" + * treatment (toggle + panel removed from the DOM, /api/steering + * never fetched); * • the Sync sources state machine (phase 32, moved here from * sources.js in phase 34 task 02) — the §7.4 never-stale lifecycle * for #sync-btn (idle → running → success | failed): admin-only @@ -124,12 +129,15 @@ export async function initSharedHeader() { // same cached whoami (anonymous users never see it). if (syncBtn) syncBtn.hidden = !admin; // Phase 34: the steering controls (phase 15) are module-owned. Admin: - // refresh the list so the count badge is right before the panel is - // ever opened (fire-and-forget, as the chat page did before the move). - // Anonymous: the toggle + panel are REMOVED from the DOM entirely — - // the phase-16 contract says "absent", not just hidden — and - // /api/steering is never fetched. + // unhide the toggle — it SHIPS hidden (phase 40, 2026-08-27, TODO.md + // L3, the same ship-hidden / reveal-for-admin contract as the + // admin-only nav links) — then refresh the list so the count badge is + // right before the panel is ever opened (fire-and-forget, as the chat + // page did before the move). Anonymous: the toggle + panel are REMOVED + // from the DOM entirely — the phase-16 contract says "absent", not + // just hidden — and /api/steering is never fetched. if (admin) { + if (steeringToggle) steeringToggle.hidden = false; if (steeringPanel) refreshSteering(); } else { steeringToggle?.remove(); diff --git a/frontend/document.html b/frontend/document.html index 571ed84..77eed78 100644 --- a/frontend/document.html +++ b/frontend/document.html @@ -52,7 +52,7 @@ Postgres, read into every system prompt). The behavior is owned by the shared header module (assets/header.js); the #steering-panel section ships in every page's
. --> - block, for the icon/label/badge pins.""" + text = _text(html) + start = text.find('id="steering-toggle"') + assert start != -1, f"{html.name}: missing the #steering-toggle button" + return text[start : text.find("", start)] + + +def _init_body(js: str) -> str: + """The source of initSharedHeader in header.js.""" + fn = js.find("function initSharedHeader") + assert fn != -1, "initSharedHeader must be defined" + return js[fn : js.find("\n}", fn)] + + +# ---------- ship-hidden markup: zero flash for anonymous ---------- + + +def test_steering_toggle_ships_hidden_on_all_six_pages() -> None: + """#steering-toggle carries the ``hidden`` attribute in ALL SIX + pages — the exact ship-hidden contract the admin-only nav links + use, so an anonymous user never sees the "Tuning" button for a + single frame, on any page.""" + for html in PAGES: + tag = _toggle_tag(html) + assert re.search(r"\bhidden\b", tag), ( + f"{html.name}: #steering-toggle must ship hidden" + ) + + +def test_steering_toggle_keeps_its_existing_markup() -> None: + """Only the ``hidden`` attribute was added: type, class, the + aria-expanded / aria-controls wiring, the decorative icon, the + "Tuning" label, and the #steering-count badge stay byte-identical + on every page — the admin UX is unchanged.""" + for html in PAGES: + tag = _toggle_tag(html) + assert 'type="button"' in tag + assert 'class="steering-toggle"' in tag + assert 'aria-expanded="false"' in tag + assert 'aria-controls="steering-panel"' in tag + body = _toggle_body(html) + assert '0' in body, ( + f"{html.name}: count badge markup changed" + ) + + +def test_steering_panel_still_ships_hidden_on_all_six_pages() -> None: + """The #steering-panel section already shipped hidden and stays + that way (this phase never touches the panel).""" + for html in PAGES: + tag = re.search(r']*id="steering-panel"[^>]*>', _text(html)) + assert tag, f"{html.name}: missing the #steering-panel section" + assert re.search(r"\bhidden\b", tag.group(0)), "the panel ships hidden" + + +# ---------- header.js: reveal-for-admin, anonymous removal intact ---------- + + +def test_header_js_unhides_the_toggle_for_admin() -> None: + """Inside initSharedHeader, the admin branch unhides the toggle — + and that line sits BEFORE the refreshSteering() call (the count + badge is right before the panel is ever opened).""" + body = _init_body(_text(HEADER_JS)) + assert "if (steeringToggle) steeringToggle.hidden = false;" in body, ( + "the admin unhide is missing from initSharedHeader" + ) + admin = body.find("if (admin)") + unhide = body.find("steeringToggle.hidden = false") + refresh = body.find("if (steeringPanel) refreshSteering();") + assert -1 < admin < unhide, "the unhide must live in the admin branch" + assert unhide < refresh, "the unhide must precede the refreshSteering() call" + + +def test_anonymous_removal_path_is_intact() -> None: + """The phase-16 "absent, not hidden" contract is preserved: the + anonymous branch still REMOVES the toggle + panel from the DOM — + the new hidden attribute only closes the pre-whoami flash window, + the end state (absent) is unchanged.""" + body = _init_body(_text(HEADER_JS)) + assert "steeringToggle?.remove();" in body + assert "steeringPanel?.remove();" in body + + +# ---------- the nav contract this phase relies on ---------- + + +def test_nav_tuning_still_ships_hidden_on_all_six_pages() -> None: + """The admin-only Tuning NAV LINK (#nav-tuning) — the contract the + toggle now mirrors — still ships hidden on every page: one + ship-hidden / reveal-for-admin family, nav link and toggle alike.""" + for html in PAGES: + tag = re.search(r']*id="nav-tuning"[^>]*>', _text(html)) + assert tag, f"{html.name}: missing the #nav-tuning nav link" + assert re.search(r"\bhidden\b", tag.group(0)), ( + f"{html.name}: #nav-tuning must ship hidden" + )