Files
brain-of-reese/.agents/user_stories/shared-header.md
T
ducoterra dbf2af26c6 refactor(agents): migrate .agent/ planning tree to .agents/
Standardize on the .agents/ directory (shared with project skills):
phases/, user_stories/, reports/, screenshots/, validate.sh, and
phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves
history; runtime artifacts move alongside).

Updates every reference in AGENTS.md, README.md, .gitignore, app
docstrings, and test story headers. Historical KB content in data/
and the runtime pipeline.log transcript are left untouched.
2026-09-05 10:57:07 -04:00

6.9 KiB
Raw Blame History

Story: Shared Header (Auth + New Chat on Every Page, Sources Link Admin-Only)

Phase: 19_shared_header · E2E: tests/e2e/test_shared_header.py

Narrative

As a user, the top bar should feel like one shared component: when I move to Sources or open a document, I should still see Sign in (or Sign out) and New Chat — and the Sources link should not offer me a page I can't use until I sign in.

  • Given I am on any page (chat, sources, or the document viewer)
  • When the bar renders
  • Then I always see the same controls — New Chat plus exactly one of Sign in (anonymous) / Sign out (admin) — and the "Sources" nav link is visible only to the admin.
  • When I am anonymous and I look at the nav
  • Then the "Sources" link is hidden on every page that has a nav — the soft-gated page is still reachable by direct URL (phase-16 gate intact), but nothing in the UI points me at a page I can't use.

Acceptance criteria

  1. Chat, Sources, and the document viewer each show New Chat + Sign in (anonymous) or Sign out (admin) in the header; the login page shows neither (it is the auth page, not an app page).
  2. The "Sources" nav link is hidden for anonymous users on every page that has a nav (chat, sources, login), and visible for the admin (chat, sources — the viewer has no nav).
  3. Anonymous direct-URL access to /sources.html still shows the phase-16 soft gate (link hidden, gate intact); the API split is unchanged (/api/docs → 403 for anonymous).
  4. New Chat on chat: in-place reset (unchanged). New Chat on sources/viewer: clears the local conversation (bor.chat.v1) and navigates to the chat page (the empty state).
  5. Sign out works from any page (logout + reload → anonymous state restored on that page).
  6. The bar stays exactly 64px (desktop) / 58px (≤640px) on all three pages in both auth states, with no horizontal overflow at 360px (phase-12 contract, phase-07 overflow guard).
  7. Exactly one /api/whoami request per page load (shared cached fetch — every gate and toggle on the page shares the same promise).

UI Visualization & Structure

  • Shared module frontend/assets/header.js (new, ES module — all pages already load JS as type="module"), loaded before each page script:
    • fetchIsAdmin() — one GET /api/whoami, cached in a module-level promise (adminPromise; anonymous-safe: non-2xx or network failure → false); the single whoami call site for the whole frontend, so a page makes exactly one request no matter how many gates reuse it.
    • initSharedHeader() — awaits fetchIsAdmin(), then toggles only the elements that exist on the page (missing → no-op, which is how the login page reuses the module without gaining chat controls): #sign-in-link / #sign-out-btn (exactly one visible — phase-16 semantics) and #nav-sources (shown for admin); returns the admin flag so callers reuse it.
    • clearChatStorage() — removes the bor.chat.v1 key in a try/catch (same key + fail-silence contract as app.js's clearStoredConversation).
    • The #sign-out-btn binding lives here (POST /api/logout, disable during the call, location.reload()) — app.js deleted its copy, so there is exactly one implementation.
  • Element ids (existing patterns): #nav-sources ships hidden-by-default in the HTML (anonymous-safe — revealed once whoami says admin, never flashed for anonymous); #sign-in-link / #sign-out-btn follow the phase-16 pattern (exactly one visible, decided by whoami at load — both start hidden so the wrong state is never shown for a frame).
  • Viewer bar: a .doc-header-actions wrapper (New Chat + Sign in / Sign out) appended to the right of the title block — margin-left: auto flex row; the title block keeps its min-width: 0 truncation (phase-12 "clip, don't wrap") while the two pills fit. The bar height and the back-link/title contract (phase 13) are untouched.
  • Mobile: the new controls reuse .new-chat-btn / .auth-link, so the phase-14/16 icon-only rules (labels hidden, 16px icon shown at ≤640px) apply automatically — the pills never grow the 58px bar and never overflow at 360px.
  • Login page boundary: #nav-sources (hidden) + header.js only — no chat controls are added to its markup, so none appear (it is the auth page, not an app page).
  • Non-chat New Chat: on sources.js / document.js, click → clearChatStorage() → location.href = "/" (a new chat means going to the chat, fresh); chat keeps app.js's startNewChat (in-place reset + focus + announce).

Playwright Mapping Rule

Test Scenario → tests/e2e/test_shared_header.py (DB seeded with the fixture docs for the viewer URL and the admin catalog; real form logins via tests/e2e/auth_helpers.py::login; the assert_shared_bar(admin, page_kind) helper waits for the settled whoami state — exactly one of Sign in / Sign out visible — before asserting the bar):

  1. test_anonymous_bar_on_all_pages — fresh (anonymous) page: the shared bar on chat, sources (phase-16 #sources-gate still visible above it), and the viewer, in the anonymous state (desktop viewport).
  2. test_admin_bar_on_all_pages — login(page, app_url, next="/"): the bar on all three pages in the signed-in state (Sign out + Sources link where a nav exists) — the login → next flow still lands right.
  3. test_sources_nav_hidden_for_anonymous_everywhere — anonymous: #nav-sources hidden on chat, sources, and the login page; after a real login on the chat page, #nav-sources is visible (the toggle works, not just the initial state).
  4. test_new_chat_from_sources_clears_and_navigates — a seeded bor.chat.v1 conversation; on sources, #new-chat-btn → navigation to / with the empty state visible and the localStorage key removed.
  5. test_sign_out_from_viewer_returns_to_anonymous — log in with next=/sources.html; open the viewer (admin bar); #sign-out-btn → after the reload the same page shows the anonymous bar.
  6. test_mobile_bar_fits_and_heights_held — viewport 375×812, anonymous: 58px bars on all three pages with documentElement.scrollWidth <= clientWidth (no horizontal overflow; the pills are icon-only); repeat after login (Sign out + Sources link present) — the bar never grows.

Regression suites (adapted / re-run in isolation by this phase): test_header_consistency.py (64/58px with the new pills on sources + viewer), test_responsive_polish.py (no 360px overflow), test_admin_auth.py (phase-16 flows with #nav-sources hidden for anonymous), test_document_back_navigation.py (viewer back-link/title with the actions wrapper), test_chat_persistence.py (chat New Chat in-place; the "New chat is chat-page-only" pin adapted — the shared bar now puts it on sources/viewer too, by owner permission).