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.
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)
- 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.
- The "Sources" nav link is hidden for anonymous users on every
page that has a nav (chat, sources, login).
/sources.htmlkeeps its phase-16 soft gate for direct-URL access;GET /api/docsstays 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 astype="module"):export function fetchIsAdmin(): Promise<boolean>— oneGET /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 privateloadAuthStatefetch for this import).export async function initSharedHeader()— awaitsfetchIsAdmin(), 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-linkhidden when admin,#sign-out-btnshown 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 thehiddenattribute (phase-16 "absent, not hidden" spirit) and appears when whoami says admin.
export function clearChatStorage()— removes thebor.chat.v1key in a try/catch (mirrors app.js'sclearStoredConversation).#sign-out-btnbinding lives here (POST/api/logout, disable during the call,location.reload()) —app.jsdeletes its own copy so there is exactly one implementation.#new-chat-btnbinding: the chat page keepsapp.js'sstartNewChat(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— addid="nav-sources"to the Sources nav link (hiddenby default); loadheader.jsbeforeapp.js.sources.html— addid="nav-sources"(hidden) to its Sources nav link; append to.header-innerthe New Chat button +#sign-in-link(/login.html?next=/sources.html) +#sign-out-btn— markup copied fromindex.html(same classes, ids, aria-labels, ≥44px targets); loadheader.jsbeforesources.js; sources.js callsinitSharedHeader()at boot and binds its New Chat button. (sources.js's existingisAdmin()whoami helper keeps working — it can be reimplemented on top offetchIsAdmin()to avoid a second request.)document.html— append a.doc-header-actionswrapper (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-sourcesthere); loadheader.jsbeforedocument.js; document.js callsinitSharedHeader()and binds New Chat.
login.html— addid="nav-sources"(hidden) + loadheader.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-linkclasses, 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 getsmin-width: 0so#doc-title/#doc-metakeep 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_polishpinsscrollWidth <= 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 callsinitSharedHeader()(toggles nav-sources + auth links) beforerestoreConversation(); itsisAdminvalue comes from the sharedfetchIsAdmin()(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-gateare unchanged; nonext-param changes inlogin.js.
Dependencies
16_admin_auth(complete) — the whoami/session model andauth_helpers.login(page, app_url, next=…)E2E helper.14_chat_persistence(complete) — thebor.chat.v1key 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
01_shared_header_module.md—header.jsmodule, HTML wiring on all four pages, viewer-bar CSS, app.js/sources.js/document.js adaptations, source-level unit pins.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).03_docs_plan_commit.md— story file, PLAN revisions (owner permission noted), final validation, the single atomic commit, phase move tocomplete/.
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.jsexportsfetchIsAdmin/initSharedHeader/clearChatStorage; the whoami fetch is cached (single promise);#nav-sourcespresent with initialhiddenin index/sources/login HTML; sources + document HTML carry#sign-in-link,#sign-out-btn,#new-chat-btn;app.jsno longer owns the sign-out binding (nosignOutBtn.addEventListenerin app.js) and importsfetchIsAdmin;styles.csshas.doc-header-actions. - Integration: none (no
app/changes) —uv run pytest --cov=appmust 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 (prereqpodman compose up -d db). - Lint/types:
uv run ruff check . && uv run pyrightclean.
Completion Criteria
- Anonymous, on chat, sources, and the viewer: Sign in + New
Chat visible,
#nav-sourceshidden. Admin, on all three: Sign out + New Chat +#nav-sources(chat/sources) visible. - New Chat from sources/viewer clears
bor.chat.v1and lands on the chat empty state; New Chat on chat behaves exactly as before (in-place reset). uv run pytestgreen;uv run pytest --cov=app --cov-report=term-missing≥ today's number.uv run pytest tests/e2e/test_shared_header.py -v --no-covgreen 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 pyrightclean.- 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.mdexists. - One
--no-gpg-signcommit (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"