Files
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

122 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).