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.
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
# Phase 19 — Shared Header: auth + New Chat on every page, Sources link admin-only
|
||||
|
||||
**Story:** `.agents/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; `.agents/user_stories/shared-header.md` exists.
|
||||
- [ ] One `--no-gpg-sign` commit (below);
|
||||
`.agents/phases/todo/19_shared_header/` moved to
|
||||
`.agents/phases/complete/`.
|
||||
|
||||
## Commit
|
||||
```bash
|
||||
git add -A .agents/ 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:** `.agents/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:** `.agents/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:** `.agents/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. `.agents/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. `.agents/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 .agents/ 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 .agents/phases/todo/19_shared_header .agents/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
|
||||
- [ ] `.agents/user_stories/shared-header.md` exists with all five
|
||||
sections (header, narrative, acceptance, UI visualization,
|
||||
Playwright Mapping Rule).
|
||||
- [ ] `.agents/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`;
|
||||
`.agents/phases/todo/19_shared_header/` is now under
|
||||
`.agents/phases/complete/`.
|
||||
Reference in New Issue
Block a user