chore(agent): track .agent/ planning tree in git
Build and Push Containers / build-and-push-app (push) Successful in 12s
Build and Push Containers / build-and-push-db (push) Successful in 10s

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.
This commit is contained in:
2026-09-01 10:18:22 -04:00
parent 5fa620fde5
commit 4971e2859d
818 changed files with 23964 additions and 4 deletions
@@ -0,0 +1,187 @@
# 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
```bash
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"
```
@@ -0,0 +1,138 @@
# Task 01 — header.js shared module + page wiring
**Phase:** `19_shared_header` · **Story:** `.agent/user_stories/shared-header.md`
## Objective
One shared header module drives the auth controls and the Sources nav
link on every page; Sources and the document viewer gain the New Chat /
Sign in / Sign out controls; the Sources nav link is hidden for
anonymous users everywhere.
## Work
1. `frontend/assets/header.js` (new ES module — same style as
`app.js`: header doc comment citing the phase, no dependencies):
- `let adminPromise: Promise<boolean> | null = null;`
`export function fetchIsAdmin(): Promise<boolean>` — first call
stores `fetch("/api/whoami")` → `.then(r => r.ok && (r.json()…
.authenticated === true))` with a catch → `false` (anonymous-safe,
mirrors app.js's current `loadAuthState`); subsequent calls return
the same promise.
- `export async function initSharedHeader(): Promise<boolean>` —
`const admin = await fetchIsAdmin();` then, **only when the
element exists** (`document.querySelector`, null-safe):
- `#sign-in-link` → `hidden = admin`;
- `#sign-out-btn` → `hidden = !admin`;
- `#nav-sources` → `hidden = !admin` (the link ships hidden —
anonymous-safe default, appears for admin).
Returns `admin` (callers may reuse it).
- `export function clearChatStorage(): void` —
`try { localStorage.removeItem("bor.chat.v1"); } catch {}` (same
key + fail-silence contract as app.js's `clearStoredConversation`).
- Sign-out binding (runs at module import, so every page that loads
header.js gets it): if `#sign-out-btn` exists — click →
`disabled = true`, `fetch("/api/logout", {method:"POST"})`
(catch ignored — the reload resets UI), `window.location.reload()`.
2. `frontend/index.html` (chat)
- Sources nav link: add `id="nav-sources"` and the `hidden`
attribute (appears once whoami says admin).
- `<script type="module" src="/assets/header.js"></script>` before
the `app.js` script tag.
3. `frontend/assets/app.js`
- `import { fetchIsAdmin, initSharedHeader } from "/assets/header.js";`
- Boot (the trailing IIFE): replace `await loadAuthState();` with
`isAdmin = await initSharedHeader();` (initSharedHeader returns
admin and already toggled `#sign-in-link` / `#sign-out-btn` /
`#nav-sources`); `loadAuthState`'s body reduces to calling
`fetchIsAdmin()` + `applyAuthState()` — or, simpler, delete
`loadAuthState` and inline: `isAdmin = await fetchIsAdmin();
applyAuthState();` **after** `initSharedHeader()` (the cached
promise means still exactly one whoami per page load).
- **Delete the sign-out listener** (`if (signOutBtn) { …
addEventListener("click", …) }`) — header.js owns it now. Keep the
`signOutBtn` query only if `applyAuthState` still uses it
(it does — for `hidden` toggling — which header.js also does;
`applyAuthState` may keep its toggling, it's idempotent).
- `startNewChat` unchanged (chat-page in-place reset).
4. `frontend/sources.html`
- Sources nav link: add `id="nav-sources"` + `hidden`.
- Append to `.header-inner` (after the nav), copied from
`index.html`: the New Chat button (`#new-chat-btn`, same svg +
`aria-label="New chat"` + `.new-chat-label` span), the
`#sign-in-link` `<a href="/login.html?next=/sources.html" hidden>`
and the `#sign-out-btn` button — **both** start `hidden`, exactly
as in index.html; `initSharedHeader` reveals one after whoami.
- `<script type="module" src="/assets/header.js"></script>` before
the `sources.js` tag.
5. `frontend/assets/sources.js`
- `import { fetchIsAdmin, initSharedHeader, clearChatStorage }
from "/assets/header.js";`
- Boot: call `await initSharedHeader()` (before the docs fetch,
alongside the existing gate check); reimplement the local
`isAdmin()` on top of `fetchIsAdmin()` (drop the private fetch —
one request per page).
- Bind `#new-chat-btn`: click → `clearChatStorage()` →
`window.location.href = "/"`.
6. `frontend/document.html`
- Inside `.doc-header-inner`, after the title block:
`<div class="doc-header-actions">` containing the New Chat button,
`#sign-in-link` (`/login.html?next=/document.html`) and
`#sign-out-btn` — **both** start `hidden` (initSharedHeader
reveals one after whoami) — same markup/aria as index.html.
- `<script type="module" src="/assets/header.js"></script>` before
the `document.js` tag.
7. `frontend/assets/document.js`
- Import the same three header.js exports; at boot (before or after
the doc fetch — independent) `await initSharedHeader()`; bind
`#new-chat-btn` exactly like sources.js.
8. `frontend/login.html`
- Sources nav link: add `id="nav-sources"` + `hidden`; load
`header.js` and call `initSharedHeader()` from `login.js` boot
(login.js already fetches whoami for the redirect — switch it to
the shared `fetchIsAdmin()` so the page makes one request, and
keep its existing "already admin → redirect to `next`" behavior).
9. `frontend/assets/styles.css` — viewer bar only:
- `.doc-header-actions { margin-left: auto; display: flex; gap:
0.5rem; align-items: center; }`
- `.doc-title-block { min-width: 0; }` (title/meta keep their
existing truncation — phase-12 "clip, don't wrap").
- The reused `.new-chat-btn` / `.auth-link` classes already carry
the desktop + ≤640px icon-only rules; if the 360px bar overflows
(the E2E will say), tighten `.doc-header-actions` padding there —
but do NOT change `--header-h` (64/58 are pinned).
10. `tests/unit/test_shared_header.py` (new — source-level, same style
as `test_frontend_feedback.py`):
- `header.js` exists and exports `fetchIsAdmin`,
`initSharedHeader`, `clearChatStorage`; the whoami fetch is
cached (a module-level promise variable — pin the
`adminPromise` marker); `clearChatStorage` references
`"bor.chat.v1"` inside a try/catch.
- `#nav-sources` present and initially `hidden` in index.html,
sources.html, login.html; NOT in document.html.
- sources.html AND document.html contain `#sign-in-link`,
`#sign-out-btn`, `#new-chat-btn`, and load `header.js`.
- `app.js` imports `fetchIsAdmin`/`initSharedHeader` from
`header.js` and contains **no** `signOutBtn.addEventListener`
(the binding moved to the shared module); `login.js` imports
`fetchIsAdmin`.
- `styles.css` defines `.doc-header-actions`.
## Testing & Quality
- No `app/` changes — `uv run pytest --cov=app` stays at today's
number.
- `uv run pytest tests/unit/test_shared_header.py -v --no-cov` green;
full `uv run pytest` green (the phase-16 unit tests for auth config
are untouched).
## Completion Criteria
- [ ] `uv run pytest tests/unit/test_shared_header.py -v --no-cov`
green; `uv run pytest` fully green.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] Manual (dev server): anonymous — chat: Sign in + New Chat shown,
Sources nav link absent; sources page: same + soft gate; viewer:
same + back/title intact. Log in — all three pages show Sign out
+ Sources link (where a nav exists). New Chat from sources →
conversation cleared, lands on chat empty state. Sign out from
the viewer → reload → Sign in back.
- [ ] Chat page still makes exactly one `/api/whoami` request per load
(Network tab) and the steering/tune gating is unchanged.
- [ ] No changes under `app/`, no CDN tags, no new assets.
@@ -0,0 +1,96 @@
# Task 02 — E2E: the shared-header story suite
**Phase:** `19_shared_header` · **Story:** `.agent/user_stories/shared-header.md`
## Objective
Dedicated Playwright gate (A16 — one story, one file, isolated): the
shared bar contract on chat / sources / viewer in both auth states, the
anonymous Sources-link hiding, New Chat from non-chat pages, and sign
out from a non-chat page — plus the header-height and overflow
regressions that this change puts at risk.
## Work
1. `tests/e2e/test_shared_header.py` (new)
- Header comment: story, prereq (`podman compose up -d db`), and the
contract under test (one bar per page: brand + nav [Chat,
Sources-admin-only] + New Chat + Sign in/Sign out on chat &
sources; back + title + New Chat + Sign in/Sign out on the
viewer; heights 64px/58px per phase 12).
- Fixtures mirroring `tests/e2e/test_header_consistency.py`:
seed the DB with the fixture docs (needed for the viewer URL and
the sources catalog); reuse `e2e.auth_helpers.login(page, app_url,
next=…)` for real form logins and the conftest `ADMIN_PASSWORD`.
- Constants: `VIEWER_URL` (a seeded doc, URL-encoded),
`SOURCES_URL = "/sources.html"`.
- Helper `assert_shared_bar(page, admin: bool, page_kind:
"chat"|"sources"|"viewer")` — the heart of the suite, asserting
per kind:
- `#new-chat-btn` visible (all kinds);
- admin → `#sign-out-btn` visible + `#sign-in-link` hidden, else
the inverse (all kinds);
- chat/sources → `#nav-sources` visible iff admin; viewer → no
`#nav-sources` in DOM (`count() == 0`);
- bar height: chat/sources `.app-header` == 64 (viewport 1280) /
58 (≤640), viewer `.doc-header` == same value (bounding boxes,
phase-12 measurement convention).
- **The six scenarios** (also the story's Playwright Mapping Rule):
1. `test_anonymous_bar_on_all_pages` — fresh (anonymous) page:
`assert_shared_bar` for chat, sources, and viewer, admin=False
(desktop viewport).
2. `test_admin_bar_on_all_pages` — `login(page, app_url, next="/")`;
`assert_shared_bar` for all three pages, admin=True. (Also
proves the login → `next` flow still lands right.)
3. `test_sources_nav_hidden_for_anonymous_everywhere` —
anonymous: on chat, sources, and the login page
(`/login.html`), `#nav-sources` is hidden; after login on the
chat page, `#nav-sources` is visible (toggle works, not just
initial state).
4. `test_new_chat_from_sources_clears_and_navigates` — anonymous
is fine: seed a conversation via
`page.add_init_script` setting `localStorage["bor.chat.v1"] =
JSON.stringify({v:1, messages:[{who:"user",
text:"hello brain"},{who:"brain", text:"hey there"}]})` (or
drive it through the chat UI — either, deterministic); go to
sources; click `#new-chat-btn`; expect navigation to `/` with
the empty state visible and `bor.chat.v1` removed
(`page.evaluate` reads localStorage).
5. `test_sign_out_from_viewer_returns_to_anonymous` — login with
`next=/sources.html` (lands on sources, admin); open the
viewer URL directly; `assert_shared_bar(… admin=True,
"viewer")`; click `#sign-out-btn`; after the reload,
`assert_shared_bar(… admin=False, "viewer")`.
6. `test_mobile_bar_fits_and_heights_held` — viewport 375×812,
anonymous: on all three pages the bar height is 58 and
`documentElement.scrollWidth <= clientWidth` (no horizontal
overflow — the pills are icon-only per the existing mobile
rules); repeat the three heights after login (Sign out +
Sources link present) — the bar never grows.
- Determinism note: all assertions are settled-state (no streaming
involved in this story — the chat page is opened at most for its
header; no turn is submitted except where a scenario says so).
2. Regression pass — each **in isolation** (A16), one command each:
`test_header_consistency.py` (64/58px on the three pages — now with
the new pills on sources + viewer, both auth states on chat),
`test_responsive_polish.py` (360px overflow guards),
`test_admin_auth.py` (phase-16 flows: chat header auth, sources
gate, login, sign out — its assertions must still hold with
`#nav-sources` hidden for anonymous),
`test_document_back_navigation.py` (viewer header back-link + title
contract with the new actions wrapper),
`test_chat_persistence.py` (chat New Chat in-place behavior +
restore — untouched code path).
## Testing & Quality
- This suite is the story's gate:
`uv run pytest tests/e2e/test_shared_header.py -v --no-cov` green in
isolation.
- No `app/` or mock changes. If a scenario exposes a real bug, fix it
in the owning frontend file and re-run task 01's unit pins + this
suite.
## Completion Criteria
- [ ] `uv run pytest tests/e2e/test_shared_header.py -v --no-cov` green
in isolation (6/6).
- [ ] All five regression suites above green, one command each.
- [ ] `uv run pytest` (unit + integration) still green;
`uv run ruff check . && uv run pyright` clean.
@@ -0,0 +1,98 @@
# Task 03 — Story file, PLAN revisions, the phase commit
**Phase:** `19_shared_header` · **Story:** `.agent/user_stories/shared-header.md`
## Objective
Record the change: the user story file (AGENTS.md rule 4), the PLAN
revisions with the owner permission noted (this revises a phase-16 UX
choice — say so explicitly), and the single atomic `--no-gpg-sign`
commit with the phase moved to `complete/`.
## Work
1. `.agent/user_stories/shared-header.md` (new — match the sibling
story format):
- Header: `**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.
- 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 (auth page, not an app page).
2. The "Sources" nav link is hidden for anonymous users on every
page that has a nav, and visible for the admin.
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 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.
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).
- UI Visualization & Structure: `header.js` shared module
(`fetchIsAdmin` cached promise, `initSharedHeader` toggles
existing elements only, `clearChatStorage`); element ids
(`#nav-sources` hidden-by-default; `#sign-in-link`
visible-by-default — existing patterns); the viewer's
`.doc-header-actions` wrapper; reused `.new-chat-btn` /
`.auth-link` mobile icon-only rules; login page boundary.
- Playwright Mapping Rule: the six scenarios of
`tests/e2e/test_shared_header.py` verbatim from task 02.
2. `.agent/PLAN.md` revisions — **owner permission 2026-08-23 (this
request)** in each note, phase-16-revision style:
- Header revisions line: append
`; shared header (Phase 19)`.
- **§2 A10 note** (append to the existing 2026-08-22 revision text,
do NOT change the decision itself): *UI revision 2026-08-23
(owner permission): the "Sources" nav link is hidden from
anonymous users on all pages — the soft-gate page and the API
split above are unchanged.*
- **§7.1 layout:** note that the header is a shared contract across
chat / sources / viewer (one bar per page, same controls; the
viewer bar = back + title + actions).
- **§7.5 component inventory:** add `#nav-sources` (Sources nav
link, hidden for anonymous), `#new-chat-btn` + `#sign-in-link` +
`#sign-out-btn` on sources and viewer pages (ids shared with
chat), `.doc-header-actions` (viewer).
- **§12 roadmap:** new row 19 — `19_shared_header` /
`shared-header.md` / `test_shared_header.py`.
- Do not renumber anything or touch other anchors.
3. `README.md` — no change (no operator-facing change).
4. Final validation pass (all gates, AGENTS.md rules 5 + 9):
- `uv run pytest --cov=app --cov-report=term-missing` (≥ today's
number),
- `uv run pytest tests/e2e/test_shared_header.py -v --no-cov` in
isolation, plus the five regression suites from task 02 (one
command each, in isolation),
- `uv run ruff check . && uv run pyright`.
5. Commit + phase move (last step, only when all gates are green):
```bash
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"
mv .agent/phases/todo/19_shared_header .agent/phases/complete/
```
## Testing & Quality
- No new logic — record-keeping + validation pass; the gates above are
the phase's final proof. If validation fails, fix in the owning
task's files, re-run that task's tests, then commit.
## Completion Criteria
- [ ] `.agent/user_stories/shared-header.md` exists with all five
sections (header, narrative, acceptance, UI visualization,
Playwright Mapping Rule).
- [ ] `.agent/PLAN.md` carries the header-revision/§2-A10-note/§7.1/
§7.5/§12 notes with the 2026-08-23 owner-permission wording; the
A10 decision text itself is unaltered.
- [ ] All gates green (coverage, story E2E + 5 regressions in
isolation, ruff + pyright).
- [ ] Exactly one new commit, conventional, `--no-gpg-sign`;
`.agent/phases/todo/19_shared_header/` is now under
`.agent/phases/complete/`.