/* Brain of Reese — admin sign-in (phase 16, A10 revised). * * One admin, one password. On submit → POST /api/login: 204 sets the * signed session cookie and we redirect to `?next` (same-origin relative * URLs only — "/…" but never "//host" or an absolute URL; default * /sources.html). A 401 keeps the form and announces through the * role=alert error region. On load, /api/whoami already says admin → * straight to `next`, no form. * * Phase 19 (phase 34 task 03, owner confirmation 2026-08-26): the * whoami check runs on the shared header module's cached promise * (assets/header.js) — one request per page, and the module's * initSharedHeader() settles the login page's FULL shared header — * the SAME bar as every other page (nav incl. the admin-only links, * Tuning toggle, Sync, New chat, the auth pair). * * No CDN, no state in this file: the signed cookie is the whole session. * All DOM ids match frontend/login.html. */ import { fetchIsAdmin, initSharedHeader } from "./header.js"; const form = document.querySelector("#login-form"); const passwordInput = document.querySelector("#login-password"); const submitBtn = document.querySelector("#login-submit"); const errorEl = document.querySelector("#login-error"); const DEFAULT_NEXT = "/sources.html"; /* Same-origin relative URLs only: honor `?next=/…`, reject anything that would leave the origin (protocol-relative "//…" or absolute). */ function safeNext() { const next = new URLSearchParams(window.location.search).get("next") || DEFAULT_NEXT; return next.startsWith("/") && !next.startsWith("//") ? next : DEFAULT_NEXT; } function showError(message) { errorEl.textContent = message; errorEl.hidden = false; submitBtn.disabled = false; passwordInput.focus(); passwordInput.select(); } /* Phase 19: the shared header module IS the whoami call site (cached * promise, anonymous-safe) — same result as the private fetch it * replaces: a network failure stays on the form (submit will explain). */ function alreadySignedIn() { return fetchIsAdmin(); } form.addEventListener("submit", async (e) => { e.preventDefault(); errorEl.hidden = true; errorEl.textContent = ""; submitBtn.disabled = true; try { const r = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password: passwordInput.value }), }); if (r.status === 204) { // Session cookie set — off to the requested page. window.location.replace(safeNext()); return; } // One generic failure (401); anything else is a server-side surprise. const detail = r.status === 401 ? "Invalid password — try again." : `Sign-in failed (HTTP ${r.status}) — try again.`; showError(detail); } catch { showError("Could not reach the server — try again."); } }); /* The bar settles in BOTH branches (phase 34 task 05 — the login page * carries the FULL shared header, so a signed-in admin who lands here * gets the settled admin bar for the frame before the redirect, not * the ship-hidden state): the whoami promise is already settled by * this point, so initSharedHeader adds no request and no delay, and * the phase-16 redirect itself is unchanged. For anonymous visitors it * settles the reduced bar: Sign in visible, the admin-only controls * stay hidden, the steering toggle + panel removed. */ (async () => { const admin = await alreadySignedIn(); await initSharedHeader(); if (admin) { window.location.replace(safeNext()); return; } passwordInput.focus(); })();