chore(agent): track .agent/ planning tree in git
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:
@@ -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"
|
||||
```
|
||||
Reference in New Issue
Block a user