# Phase 19 — Shared Header: auth + New Chat on every page, Sources link admin-only **Story:** `.agents/user_stories/shared-header.md` (created by task 03) **Context:** the four page headers (`frontend/index.html`, `sources.html`, `document.html`, `login.html` — each hand-rolled, which is exactly why the controls "disappear" between pages), `frontend/assets/ app.js` (chat's whoami gating + New Chat + sign-out handlers), `sources.js` (whoami-before-docs gate), `document.js`, phase 12's exact header-height contract (64px / 58px — `test_header_consistency.py`), phase 16's auth model (A10 revised). ## Objective Make the title bar actually shared. Owner report 2026-08-23: clicking "Sources" makes **New Chat** and **Sign in** vanish — the user expects one consistent bar on every page. This phase puts the same header controls on **Chat, Sources, and the document viewer** (Sign in / Sign out + New Chat, via one shared module), and — per the same owner instruction — **hides the "Sources" nav link from anonymous users** (revises the phase-16 UX choice "show the link, soft-gate the page"; the soft gate itself stays for direct-URL visitors, and the API rules of the A10 revision are untouched). ## Owner-confirmed changes (2026-08-23, this request) 1. **Sign in / Sign out + New Chat are always visible** on chat, sources, and the document viewer (anonymous AND admin — Sign in vs Sign out per whoami). The login page gets **no** chat controls (it is the auth page, not an app page) — noted boundary, owner may overrule. 2. **The "Sources" nav link is hidden for anonymous users** on every page that has a nav (chat, sources, login). `/sources.html` keeps its phase-16 soft gate for direct-URL access; `GET /api/docs` stays 403 for anonymous (A10 revision unchanged — this is UI visibility, not API access). ## Design - **Shared module `frontend/assets/header.js` (new, ES module — all pages already load JS as `type="module"`):** - `export function fetchIsAdmin(): Promise` — one `GET /api/whoami`, cached in a module-level promise (anonymous-safe: network failure → `false`). Every page's whoami goes through this single function, so the chat page makes exactly one request (app.js swaps its private `loadAuthState` fetch for this import). - `export async function initSharedHeader()` — awaits `fetchIsAdmin()`, then toggles **only the elements that exist on the page** (missing → no-op, which is how the login page reuses it without gaining controls): - `#sign-in-link` hidden when admin, `#sign-out-btn` shown when admin (exactly one visible — phase-16 semantics); - `#nav-sources` (new id on the Sources nav link, every page with a nav) **hidden for anonymous, shown for admin** — new anonymous-safe default: the link ships with the `hidden` attribute (phase-16 "absent, not hidden" spirit) and appears when whoami says admin. - `export function clearChatStorage()` — removes the `bor.chat.v1` key in a try/catch (mirrors app.js's `clearStoredConversation`). - `#sign-out-btn` binding lives here (POST `/api/logout`, disable during the call, `location.reload()`) — `app.js` deletes its own copy so there is exactly one implementation. - `#new-chat-btn` binding: the chat page keeps `app.js`'s `startNewChat` (in-place reset + focus + announce). On **non-chat pages** (sources.js / document.js, ~4 lines each): click → `clearChatStorage()` → `location.href = "/"` (a new chat means going to the chat). - **HTML wiring:** - `index.html` — add `id="nav-sources"` to the Sources nav link (`hidden` by default); load `header.js` before `app.js`. - `sources.html` — add `id="nav-sources"` (`hidden`) to its Sources nav link; append to `.header-inner` the New Chat button + `#sign-in-link` (`/login.html?next=/sources.html`) + `#sign-out-btn` — markup copied from `index.html` (same classes, ids, aria-labels, ≥44px targets); load `header.js` before `sources.js`; sources.js calls `initSharedHeader()` at boot and binds its New Chat button. (sources.js's existing `isAdmin()` whoami helper keeps working — it can be reimplemented on top of `fetchIsAdmin()` to avoid a second request.) - `document.html` — append a `.doc-header-actions` wrapper (New Chat + `#sign-in-link` `/login.html?next=/document.html` + `#sign-out-btn`) to the right of `.doc-header-inner` (the viewer has no nav — no `#nav-sources` there); load `header.js` before `document.js`; document.js calls `initSharedHeader()` and binds New Chat. - `login.html` — add `id="nav-sources"` (`hidden`) + load `header.js` (init only — it toggles the nav link; no chat controls are added, so none appear). - **CSS (`styles.css`):** the new controls reuse the existing `.new-chat-btn` / `.auth-link` classes, so the phase-14/16 mobile icon-only rules (labels hidden, 16px icon shown) apply automatically. New work is the **viewer bar only**: `.doc-header-actions { margin-left: auto; display: flex; gap: 0.5rem; align-items: center; }`; the title block gets `min-width: 0` so `#doc-title`/`#doc-meta` keep truncating (phase-12 "clip, don't wrap") while the two pills fit; the bar must still measure exactly `--header-h` (64px desktop, 58px ≤640px) and produce **no horizontal overflow at 360px** (`test_responsive_polish` pins `scrollWidth <= clientWidth`). The sources bar already fits this exact control set (the chat bar does — it even carries the steering toggle), so no sources CSS is expected. - **`app.js` (chat) adaptations:** boot calls `initSharedHeader()` (toggles nav-sources + auth links) before `restoreConversation()`; its `isAdmin` value comes from the shared `fetchIsAdmin()` (cached — still one whoami per load); delete the now-duplicated sign-out listener. Everything else (steering gating, tune buttons) unchanged. - **Non-goals:** no server-side header (still static templates — A11); no API changes (A10 revision untouched); no login-page chat controls; no change to the document viewer's back-link/title contract (phase 13); the "Sources" **page** soft gate and `#sources-gate` are unchanged; no `next`-param changes in `login.js`. ## Dependencies - `16_admin_auth` (complete) — the whoami/session model and `auth_helpers.login(page, app_url, next=…)` E2E helper. - `14_chat_persistence` (complete) — the `bor.chat.v1` key the non-chat New Chat buttons clear. - `12_header_consistency` (complete) — the 64/58px height contract the new controls must fit inside. - `10_story_document_viewer` + `13_document_back_navigation` (complete) — the viewer header being extended. - `17_thinking_display` / `18_follow_bottom_scroll` (todo) — no code overlap (chat-page turn rendering only); independent order. ## Tasks 1. `01_shared_header_module.md` — `header.js` module, HTML wiring on all four pages, viewer-bar CSS, app.js/sources.js/document.js adaptations, source-level unit pins. 2. `02_e2e_story_suite.md` — `tests/e2e/test_shared_header.py` (the story gate, isolated) + the regression suites (header consistency, responsive polish, admin auth, document back navigation, chat persistence). 3. `03_docs_plan_commit.md` — story file, PLAN revisions (owner permission noted), final validation, the single atomic commit, phase move to `complete/`. ## Locked decisions - **Phase-16 UX revision with owner permission (2026-08-23):** the "Sources" nav link is hidden for anonymous (before: shown, page soft-gated). The **soft gate page and the A10 API split are unchanged** — recorded as a PLAN §7 revision note, not an anchor change. - **A11 untouched** — vanilla JS, no CDN, static templates. **A10 untouched** — endpoint access unchanged. **A16 untouched** — one new story E2E suite + adapted regressions. No other anchor changed. ## Testing & Quality - **Unit (source-level, new `tests/unit/test_shared_header.py`):** `header.js` exports `fetchIsAdmin` / `initSharedHeader` / `clearChatStorage`; the whoami fetch is cached (single promise); `#nav-sources` present with initial `hidden` in index/sources/login HTML; sources + document HTML carry `#sign-in-link`, `#sign-out-btn`, `#new-chat-btn`; `app.js` no longer owns the sign-out binding (no `signOutBtn.addEventListener` in app.js) and imports `fetchIsAdmin`; `styles.css` has `.doc-header-actions`. - **Integration:** none (no `app/` changes) — `uv run pytest --cov=app` must stay at today's number. - **Coverage:** frontend-only; the >90% `app/` gate is unaffected, re-run to prove it. - **E2E:** `tests/e2e/test_shared_header.py` — six scenarios (task 02), green **in isolation** (prereq `podman compose up -d db`). - **Lint/types:** `uv run ruff check . && uv run pyright` clean. ## Completion Criteria - [ ] Anonymous, on **chat, sources, and the viewer**: Sign in + New Chat visible, `#nav-sources` hidden. Admin, on all three: Sign out + New Chat + `#nav-sources` (chat/sources) visible. - [ ] New Chat from sources/viewer clears `bor.chat.v1` and lands on the chat empty state; New Chat on chat behaves exactly as before (in-place reset). - [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` ≥ today's number. - [ ] `uv run pytest tests/e2e/test_shared_header.py -v --no-cov` green in isolation (6/6); regressions green in isolation (one command each): `test_header_consistency.py` (64/58px with the new pills on sources + viewer), `test_responsive_polish.py` (no 360px overflow), `test_admin_auth.py`, `test_document_back_navigation.py`, `test_chat_persistence.py`. - [ ] `uv run ruff check . && uv run pyright` clean. - [ ] UI Structure Check (AGENTS.md rule 5): all new controls reuse labeled ≥44px patterns (aria-labels on icon-only mobile), focus-visible, no CDN tags, one header bar per page, heights unchanged. - [ ] PLAN carries the revisions with the 2026-08-23 owner-permission wording; `.agents/user_stories/shared-header.md` exists. - [ ] One `--no-gpg-sign` commit (below); `.agents/phases/todo/19_shared_header/` moved to `.agents/phases/complete/`. ## Commit ```bash git add -A .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(ui): shared header — Sign in/Sign out and New Chat on every page; hide the Sources nav link from anonymous users" ```