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"
```