/* 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 "Sources" nav link (#nav-sources) — phase 19 UX revision * (owner permission 2026-08-23): hidden for anonymous on every page * that has a nav (chat, sources, login), revealed for admin. The * link SHIPS hidden in the HTML (anonymous-safe default — the * phase-16 "absent, not hidden" spirit), so no anonymous user ever * sees it for a frame; * • 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): 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; 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(); }); }