/* Brain of Reese — shared header module (phase 19). * * Owner report 2026-08-23: clicking "Sources" made New Chat and Sign in * vanish — the user expects ONE consistent bar on every page. This module * is the single owner of the shared header controls: * * • the Sign in / Sign out auth pair (phase 16, exactly one visible — * decided by /api/whoami at load); * • the admin-only nav links — "Sources" (#nav-sources) and "Tuning" * (#nav-tuning, on the Global Tuning page, phase 27) — phase 19 UX * revision (owner permission 2026-08-23): hidden for anonymous on * every page that has a nav (chat, sources, tuning, login), * revealed for admin. The links SHIP hidden in the HTML * (anonymous-safe default — the phase-16 "absent, not hidden" * spirit), so no anonymous user ever sees one for a frame; and the * Sources page's "Sync sources" button (#sync-btn, phase 32) — * the same ship-hidden / reveal-for-admin contract on the SAME * cached whoami (one fetch, no extra request); * • the sign-out click binding (POST /api/logout → reload) — moved * here from app.js so there is exactly one implementation; * • clearChatStorage() — the phase-14 conversation key, for the * New Chat buttons on the NON-CHAT pages (sources / document * viewer / tuning): a new chat means going to the chat, fresh. * * Every page loads this module (type="module", before its page script) * and its page script calls initSharedHeader() once at boot. init… * toggles ONLY the controls that exist on the page — a missing element * is a no-op, which is how the login page reuses the module without * gaining chat controls (no #new-chat-btn / #sign-in-link / * #sign-out-btn in its markup → none appear). * * whoami is fetched at most ONCE per page load: the promise is cached in * the module-level `adminPromise`, so app.js's tuning gate, the sources * page's catalog gate, and the header toggling all share one request. * Anonymous-safe: any network failure resolves to false (the anonymous * UI), mirroring the per-page catch the pages used before phase 19. * * A10/A11 untouched: no API change, no CDN, no state beyond the cached * promise; the soft gate page and the A10 API split are unchanged — * this is UI visibility only. */ let adminPromise = null; /* The SINGLE /api/whoami call site for the whole frontend. First call stores the promise in `adminPromise`; every later call — on this page — returns the same promise, i.e. exactly one request per page load. Anonymous-safe: non-2xx or a network failure resolves to false. */ export function fetchIsAdmin() { if (!adminPromise) { adminPromise = fetch("/api/whoami") .then(async (r) => (r.ok ? (await r.json()).authenticated === true : false)) .catch(() => false); } return adminPromise; } /* Toggle the shared header controls, only the ones present on this page (querySelector, null-safe — missing → no-op). Returns the admin flag so callers can reuse it instead of awaiting fetchIsAdmin() again (the cached promise makes both awaits the same single request). */ export async function initSharedHeader() { const admin = await fetchIsAdmin(); const signIn = document.querySelector("#sign-in-link"); if (signIn) signIn.hidden = admin; const signOut = document.querySelector("#sign-out-btn"); if (signOut) signOut.hidden = !admin; const navSources = document.querySelector("#nav-sources"); if (navSources) navSources.hidden = !admin; // Phase 27: the Global Tuning page's own nav link — admin-only, the // same ship-hidden / reveal-for-admin contract as the Sources link. const navTuning = document.querySelector("#nav-tuning"); if (navTuning) navTuning.hidden = !admin; // Phase 32: the Sources page's "Sync sources" button — admin-only, // revealed on this same cached whoami (anonymous users never see it). const syncBtn = document.querySelector("#sync-btn"); if (syncBtn) syncBtn.hidden = !admin; return admin; } /* Remove the phase-14 conversation key — same key + fail-silence contract as app.js's clearStoredConversation: private mode or a storage error is swallowed, the navigation still happens. */ export function clearChatStorage() { try { localStorage.removeItem("bor.chat.v1"); } catch { /* nothing was stored */ } } /* Sign-out binding (phase 16 behavior, now module-owned): runs at module import, so every page that loads header.js gets it exactly once. Disable during the call, POST /api/logout (the result is ignored — the reload resets the UI either way), then reload so the header re-resolves to the anonymous state (Sign in back, Sources gone). */ const signOutBtn = document.querySelector("#sign-out-btn"); if (signOutBtn) { signOutBtn.addEventListener("click", async () => { signOutBtn.disabled = true; try { await fetch("/api/logout", { method: "POST" }); } catch { /* the reload resets the UI either way */ } window.location.reload(); }); }