feat(ui): shared header — Sign in/Sign out and New Chat on every page; hide the Sources nav link from anonymous users

This commit is contained in:
2026-08-24 12:32:45 -04:00
parent fd7f02ce68
commit 2afc77ee56
14 changed files with 904 additions and 59 deletions
+25 -7
View File
@@ -4,8 +4,16 @@
* full-width document table, or the designed empty state when nothing is
* indexed yet. Cells are built with DOM APIs (textContent) — never
* innerHTML with document-derived data (XSS-safe by construction).
*
* Phase 19: the page joins the shared header (assets/header.js) — the
* whoami gate below runs on the module's cached promise (one request per
* page, shared with the header toggling), and the header gains the New
* chat button: on a non-chat page "new chat" means going to the chat,
* fresh (clear the phase-14 conversation key, then navigate to "/").
*/
import { clearChatStorage, fetchIsAdmin, initSharedHeader } from "/assets/header.js";
const tbody = document.querySelector("#docs-tbody");
const emptyEl = document.querySelector("#sources-empty");
const tableWrap = document.querySelector(".table-wrap");
@@ -18,13 +26,22 @@ const statLast = document.querySelector("#stat-last");
/* Phase 16: whoami BEFORE the docs fetch. Anonymous visitors get the
* sign-in gate (stat cards + table hidden) and NO /api/docs call — the
* catalog is admin-only. The document viewer itself stays public (the
* soft rule), so the gate copy points at what keeps working. */
async function isAdmin() {
try {
const r = await fetch("/api/whoami");
if (r.ok) return (await r.json()).authenticated === true;
} catch { /* API unreachable: anonymous-safe gate */ }
return false;
* soft rule), so the gate copy points at what keeps working.
* Phase 19: the whoami request is the shared header module's cached
* promise — the same single request initSharedHeader() awaited. */
function isAdmin() {
return fetchIsAdmin();
}
/* Phase 19: New chat on a non-chat page means "go to the chat, fresh":
* clear the phase-14 conversation key, then land on the chat page — its
* empty state, since the conversation is gone from storage. */
const newChatBtn = document.querySelector("#new-chat-btn");
if (newChatBtn) {
newChatBtn.addEventListener("click", () => {
clearChatStorage();
window.location.href = "/";
});
}
function fmtDate(iso) {
@@ -112,6 +129,7 @@ function showEmpty() {
}
(async () => {
await initSharedHeader(); // phase 19: Sign in/out + Sources link in the shared bar
if (!(await isAdmin())) {
// Anonymous: gate in, catalog out, and no /api/docs request at all.
if (statCards) statCards.hidden = true;