Files
brain-of-reese/.agent/phases/complete/19_shared_header/00_phase.md
T
ducoterra 4971e2859d
Build and Push Containers / build-and-push-app (push) Successful in 12s
Build and Push Containers / build-and-push-db (push) Successful in 10s
chore(agent): track .agent/ planning tree in git
Remove the blanket .agent/ gitignore so the phase roadmap, user
stories, reports, and PLAN.md are versioned with the code. Only
runtime artifacts (.agent/phase-sessions/, .agent/pipeline.log)
remain ignored. Update AGENTS.md git protocol rule to match.
2026-09-01 10:18:22 -04:00

10 KiB

Phase 19 — Shared Header: auth + New Chat on every page, Sources link admin-only

Story: .agent/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<boolean> — 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; .agent/user_stories/shared-header.md exists.
  • One --no-gpg-sign commit (below); .agent/phases/todo/19_shared_header/ moved to .agent/phases/complete/.

Commit

git add -A .agent/ 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"