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
+121
View File
@@ -0,0 +1,121 @@
# 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).