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.
6.9 KiB
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
- 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).
- 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).
- Anonymous direct-URL access to
/sources.htmlstill shows the phase-16 soft gate (link hidden, gate intact); the API split is unchanged (/api/docs→ 403 for anonymous). - 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). - Sign out works from any page (logout + reload → anonymous state restored on that page).
- 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).
- Exactly one
/api/whoamirequest 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 astype="module"), loaded before each page script:fetchIsAdmin()— oneGET /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()— awaitsfetchIsAdmin(), 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 thebor.chat.v1key in a try/catch (same key + fail-silence contract as app.js'sclearStoredConversation).- The
#sign-out-btnbinding 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-sourcesships hidden-by-default in the HTML (anonymous-safe — revealed once whoami says admin, never flashed for anonymous);#sign-in-link/#sign-out-btnfollow 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-actionswrapper (New Chat + Sign in / Sign out) appended to the right of the title block —margin-left: autoflex row; the title block keeps itsmin-width: 0truncation (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.jsonly — 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'sstartNewChat(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):
test_anonymous_bar_on_all_pages— fresh (anonymous) page: the shared bar on chat, sources (phase-16#sources-gatestill visible above it), and the viewer, in the anonymous state (desktop viewport).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 →nextflow still lands right.test_sources_nav_hidden_for_anonymous_everywhere— anonymous:#nav-sourceshidden on chat, sources, and the login page; after a real login on the chat page,#nav-sourcesis visible (the toggle works, not just the initial state).test_new_chat_from_sources_clears_and_navigates— a seededbor.chat.v1conversation; on sources,#new-chat-btn→ navigation to/with the empty state visible and the localStorage key removed.test_sign_out_from_viewer_returns_to_anonymous— log in withnext=/sources.html; open the viewer (admin bar);#sign-out-btn→ after the reload the same page shows the anonymous bar.test_mobile_bar_fits_and_heights_held— viewport 375×812, anonymous: 58px bars on all three pages withdocumentElement.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).