feat(chat): stop an in-flight answer — Send becomes Stop, the partial is kept and persisted, the model stream is torn down
This commit is contained in:
@@ -1,46 +0,0 @@
|
||||
# Phase 34 — One Navbar on Every Page
|
||||
|
||||
**Source:** `TODO.md` L3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."
|
||||
**Story:** `.agent/user_stories/nav-consistency.md`
|
||||
**Context:** The shared header (phase 19, `frontend/assets/header.js`) + the Tuning nav link (phase 29) already standardize nav + auth on chat / sources / tuning — but `document.html` still uses the separate `.doc-header` variant (back + title + actions, **no nav at all**), `login.html` misses the Tuning link, and two functional controls remain page-scoped: the Tuning steering toggle + panel (chat only, logic in `app.js`) and the Sync sources button (Sources only, logic in `sources.js`). Owner confirmation (2026-08-26): the bar must be identical on **all** pages — nav, Tuning toggle, Sync, New chat, and the auth pair all present everywhere; the locked A10 UI revision stays (admin-only controls hidden for anonymous, active for the admin on every tab).
|
||||
|
||||
## Objective
|
||||
Make the header bar **identical on all five pages** (chat, sources, document viewer, tuning, login): one shared markup block, one owner of all functional control behavior (`header.js`), the viewer's back link + title preserved in a second titlebar row, and the phase-12/19 height contract (64px desktop / 58px ≤640px) applied to the standard row on every page.
|
||||
|
||||
## Dependencies
|
||||
- `19_shared_header` (complete) — the `header.js` module, the nav/auth markup + ids, the cached-one-whoami contract, the ship-hidden/reveal-for-admin pattern.
|
||||
- `29_tuning_nav_link` (complete) — the admin-only `#nav-tuning` reveal pattern this phase completes on the remaining pages.
|
||||
- `15_steering_notes` + `27_global_tuning` (complete) — the steering toggle/panel logic being moved into the shared module; chat-page behavior must not change.
|
||||
- `32_admin_sync_button` (complete) — the sync button state machine + `GET/POST /api/sync` being moved into the shared module; Sources-page behavior (result line + error banner) must not change.
|
||||
- `13_document_back_navigation` (complete) — the `#doc-back` target-resolution behavior the viewer titlebar must preserve.
|
||||
- `16_admin_auth` (complete) — the whoami gate, the soft-gate pages, the sign-out binding.
|
||||
|
||||
## Tasks
|
||||
1. `01_steering_moves_to_module.md` — the steering toggle + panel logic moves from `app.js` into `header.js` (exported `refreshSteering()`); the chat per-bubble Tune form keeps working.
|
||||
2. `02_sync_and_chat_moves_to_module.md` — the sync state machine moves from `sources.js` into `header.js` (`bor:sync-status` event); one module-owned New chat binding; the sign-in `?next=` rewrite.
|
||||
3. `03_full_header_all_pages.md` — all five pages ship the identical header block; `#steering-panel` exists on every page; the viewer becomes standard row + titlebar row; login gains the full header.
|
||||
4. `04_viewer_titlebar_styles.md` — the two-row viewer header styles, the sync button's failed state on non-Sources pages, theme/contrast/focus preserved.
|
||||
5. `05_e2e_and_contract_update.md` — the story E2E suite `test_nav_consistency.py`; `test_header_consistency.py` + `test_shared_header.py` updated to the new viewer contract; regression pass; commit.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: frontend-only — no new `app/` logic; the no-CDN integration test (`tests/integration/test_api.py::test_index_html_served_locally`) must still pass (all new markup is same-origin, no new tags).
|
||||
- Coverage: **>90%** on `app/` — unchanged by this phase (no Python change).
|
||||
- E2E (mandatory, A16): `tests/e2e/test_nav_consistency.py` — the story gate, run in isolation; plus the two contract suites updated in task 05 and the regression list below.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] The same visible header controls appear on **all five pages** in the same order — brand, nav [Chat, Sources, Tuning], Tuning toggle, Sync sources (admin), New chat, exactly one of Sign in / Sign out — verified in `test_nav_consistency.py` for both the admin and the anonymous role.
|
||||
- [ ] The document viewer shows the standard bar (row 1) + back link and title (row 2); `#doc-back` target resolution (phase 13) unchanged.
|
||||
- [ ] The login page carries the full header (nav incl. Tuning, Tuning toggle, Sync, New chat, auth pair).
|
||||
- [ ] Chat page: the steering panel + per-bubble Tune + inline form behave exactly as before; Sources page: the sync button state machine + `#sync-result` line + `#sync-error-banner` behave exactly as before.
|
||||
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged (>90%).
|
||||
- [ ] Regressions green in isolation: `test_header_consistency.py`, `test_shared_header.py`, `test_document_back_navigation.py`, `test_document_viewer.py`, `test_steering.py`, `test_global_tuning.py`, `test_sync_button.py`, `test_tuning_nav_link.py`, `test_smoke.py`, `test_chat_rag.py`, `test_admin_auth.py`.
|
||||
- [ ] `uv run ruff check . && uv run pyright` clean (no Python change, but run the gate).
|
||||
- [ ] UI Structure Check (AGENTS.md rule 5): landmarks / labels / contrast ≥4.5:1 / focus-visible preserved; no CDN (rule 6).
|
||||
- [ ] One `--no-gpg-sign` commit staging only this phase's files; `.agent/phases/todo/34_consistent_navbar/` moved to `.agent/phases/complete/`.
|
||||
|
||||
## Locked decisions
|
||||
- **A10 UI revision preserved** — admin-only controls (Sources / Tuning nav links, Sync button) ship hidden and are revealed only for the signed-in admin; anonymous visitors get the reduced bar, identically on every page (owner confirmation 2026-08-26 — "hidden for anon, visible for admin").
|
||||
- **A11 untouched** — vanilla HTML/CSS/JS, no CDN, no new packages.
|
||||
- **Phase 19 module contract extended, not replaced** — `header.js` keeps the cached one-whoami-per-page promise; it gains ownership of the controls' behavior, not a second whoami.
|
||||
- **Viewer bar superseded** — the phase-19 single-row viewer bar (PLAN.md §7.1 "the viewer bar = back + title + the same actions") is replaced by the two-row layout at the owner's request (this TODO). `PLAN.md` is not edited (Protocol B); this phase directory records the revision.
|
||||
- **A16 / A17 honoured** — one new story E2E suite + one atomic `--no-gpg-sign` commit.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Task 01 — Steering toggle + panel move into header.js
|
||||
|
||||
**Phase:** `34_consistent_navbar` · **Source:** `TODO.md:3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."`
|
||||
**Story:** `.agent/user_stories/nav-consistency.md`
|
||||
|
||||
## Objective
|
||||
Make `frontend/assets/header.js` the owner of the steering toggle + panel behavior (today in `frontend/assets/app.js`), so the toggle can sit in every page's header (task 03) with zero page-script duplication. The chat page's behavior — panel open/close, list, count badge, per-note delete, per-bubble Tune form — must be byte-for-byte the same from the user's perspective.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/header.js` — add the steering logic (runs at module import, like the existing sign-out binding):
|
||||
- `loadSteering()` — `GET /api/steering`; non-2xx (the anonymous 403, unreachable API) → empty list (the current chat-page anonymous state); render via `renderSteeringPanel(notes)`.
|
||||
- `renderSteeringPanel(notes)` — newest-first `<li class="steering-note">` rows: the note as `textContent` in a `span.steering-note-text` (XSS contract unchanged — never innerHTML for the note), a per-note Remove `button.steering-delete` with `aria-label="Delete tuning note: …"`; toggle `#steering-empty`'s `hidden` on `notes.length`; set the `#steering-count` badge text.
|
||||
- `deleteSteeringNote(id, btn)` — disable the row button, `DELETE /api/steering/{id}`, re-load the list, announce through `#steering-announcer` (`role="status"`).
|
||||
- The `#steering-toggle` click binding — open/close `#steering-panel`, flip `aria-expanded`, move focus into the panel on open (the chat-page a11y contract; read `app.js`'s current implementation first and mirror it exactly, including any close-on-Esc / outside-click behavior it has).
|
||||
- **Export `refreshSteering()`** (fetch + render) — task 01's `app.js` change wires the per-bubble Tune form's success path to it.
|
||||
- Update the file's header comment (it now owns the steering controls).
|
||||
2. `frontend/assets/app.js` — remove the steering **panel** section (the `#steering-toggle` / `#steering-count` / `#steering-panel` / `#steering-list` / `#steering-empty` / `#steering-announcer` refs, `loadSteering`, `renderSteeringPanel`, `deleteSteeringNote`, `announceSteering`, the toggle binding) — **keep** the per-bubble `appendTuneButton` + `openTuneForm` (a chat-specific feature): the inline form's success path calls `refreshSteering()` imported from `./header.js` instead of the removed `loadSteering()`. Keep `TUNE_ICON` and the form's fetch/error handling untouched.
|
||||
3. Update the comments that describe the panel as chat-page-owned (app.js header comment, index.html steering comments) — the panel now belongs to the shared module; index.html's markup stays for now (task 03 copies it to the other pages).
|
||||
|
||||
Notes:
|
||||
- All elements are looked up null-safe (`querySelector` + guard) — a page that (still) lacks the panel markup is a no-op, mirroring how `initSharedHeader()` already works. This keeps the app functional between tasks.
|
||||
- Do not change the steering API (`app/api/steering.py`), the panel markup in `index.html`, or the `#steering-panel` styles.
|
||||
|
||||
## Testing & Quality
|
||||
- No Python change; the no-CDN integration test is unaffected.
|
||||
- Coverage: `app/` gate unaffected (no Python change).
|
||||
- The moved logic is behavior-verified by the regression suites in task 05 (`test_steering.py`, `test_global_tuning.py`); until then `uv run pytest` (unit + integration) must stay green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `header.js` exports `refreshSteering()` and owns the toggle binding, panel render, per-note delete, count badge, and announcer.
|
||||
- [ ] `app.js` no longer contains the panel logic; the per-bubble Tune button + inline form remain and call `refreshSteering()` on save.
|
||||
- [ ] The chat page (`/`) still loads, opens, lists, and deletes steering notes exactly as before (manual smoke via the dev server or the regression suites in task 05).
|
||||
- [ ] `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Task 02 — Sync state machine + New chat + sign-in next move into header.js
|
||||
|
||||
**Phase:** `34_consistent_navbar` · **Source:** `TODO.md:3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."`
|
||||
**Story:** `.agent/user_stories/nav-consistency.md`
|
||||
|
||||
## Objective
|
||||
Make `header.js` the owner of the Sync button state machine (today in `sources.js`), the single New chat binding (today duplicated across `app.js` / `sources.js` / `tuning.js` / `document.js`), and the sign-in `?next=` derivation — so the same markup on any page (task 03) behaves identically.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/header.js` — add the sync state machine (read `sources.js`'s sync section first and mirror its contract exactly):
|
||||
- **Boot (admin only):** `await fetchIsAdmin()` on the cached whoami — non-admins never poll (the status endpoint is admin-only). One `GET /api/sync/status`: `running` → enter the running state + start polling (the phase-32 reload-mid-sync re-attach); terminal → render the last result.
|
||||
- **Click `#sync-btn`:** `POST /api/sync` → 202 enters running; 409 attaches to the running state (one sync at a time).
|
||||
- **Poll** `GET /api/sync/status` every 2000 ms — one live timer, stopped on a terminal state. **No client-side hard timeout** (phase-32 locked decision — a sync can outlive the page; the state machine simply keeps polling).
|
||||
- **Button states (§7.4 never-stale):** idle → label "Sync"; running → `disabled` + `aria-busy="true"` + spinner class (`.sync-icon.is-spinning`) + label "Syncing…"; success → label "Synced HH:MM"; failed → error state with the sanitized error string in the button's `title` + `aria-label` (on non-Sources pages that is where the failure is visible — the Sources page's own banner is driven by the event below).
|
||||
- **On every state change** dispatch `window.dispatchEvent(new CustomEvent("bor:sync-status", { detail: <status> }))` where `detail` is the `GET /api/sync/status` object — step 2 points the Sources page's banner/result line at it.
|
||||
2. `frontend/assets/header.js` — **one** New chat binding (module scope, null-safe): if `#messages` exists (chat page) → `window.dispatchEvent(new CustomEvent("bor:new-chat"))` and let the page script act; otherwise `clearChatStorage()` + `location.href = "/"` (the existing non-chat behavior — "new chat" means go to the chat, fresh).
|
||||
3. `frontend/assets/header.js` — **sign-in `?next=` rewrite:** in `initSharedHeader()` (or the module-scope boot), set `#sign-in-link`'s `href` to `/login.html?next=<current pathname>` (default `/`) — the admin lands back on the page they signed in from.
|
||||
- ASSUMPTION: on the chat page this changes the static fallback `?next=/sources.html` to `/` at runtime — landing on the page you signed in from ("return to where you were"). The page markup keeps its current href as the no-JS fallback.
|
||||
4. `frontend/assets/sources.js` — remove the sync state machine (the `#sync-btn` click handler, the 2 s poll loop, the button-state helpers, the boot re-attach). **Keep** `#sync-result` + `#sync-error-banner` rendering, now driven by a `window.addEventListener("bor:sync-status", …)` subscription: `running` → clear the result line, hide the banner; `success` → render the last-result counts in `#sync-result` (reuse the existing formatting, "added" always shown); `failed` → show `#sync-error-banner` with the error text; `idle` → hide the banner, clear the result.
|
||||
5. `frontend/assets/app.js` — replace the direct `#new-chat-btn` click binding with `window.addEventListener("bor:new-chat", startNewChat)` (the `startNewChat` function itself is unchanged).
|
||||
6. `frontend/assets/sources.js`, `frontend/assets/tuning.js`, `frontend/assets/document.js` — remove their `#new-chat-btn` click bindings (the module owns them). Update the file-header comments (document.js: the module now owns New chat; sources.js: sync is module-owned, the banner is event-driven).
|
||||
|
||||
Notes:
|
||||
- Null-safe element lookups throughout (a page that doesn't (yet) have `#sync-btn` is a no-op — the app stays functional between tasks).
|
||||
- The module must keep exactly **one** whoami per page load (the cached promise) — the sync boot may await it but must not add a fetch.
|
||||
- Do not touch `app/api/sync.py` — the API contract is unchanged.
|
||||
|
||||
## Testing & Quality
|
||||
- No Python change; the no-CDN integration test is unaffected.
|
||||
- Coverage: `app/` gate unaffected.
|
||||
- Behavior parity is verified by the regression suites in task 05 (`test_sync_button.py`, `test_shared_header.py`, `test_chat_rag.py`); until then `uv run pytest` must stay green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `header.js` owns: the sync state machine (+ `bor:sync-status` event), the single New chat binding (`bor:new-chat` on chat, clear+navigate elsewhere), and the sign-in `next` rewrite.
|
||||
- [ ] `sources.js` no longer contains the sync state machine — `#sync-result` / `#sync-error-banner` render off the event; no `#new-chat-btn` binding remains in any page script.
|
||||
- [ ] On the Sources page the full phase-32 cycle (click → polling → success counts / failure banner, reload re-attach) still works — confirmed in task 05 via `test_sync_button.py`.
|
||||
- [ ] `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.
|
||||
@@ -1,39 +0,0 @@
|
||||
# Task 03 — The identical full header on all five pages
|
||||
|
||||
**Phase:** `34_consistent_navbar` · **Source:** `TODO.md:3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."`
|
||||
**Story:** `.agent/user_stories/nav-consistency.md`
|
||||
|
||||
## Objective
|
||||
Ship the **identical header block** on all five pages — brand, nav [Chat, Sources, Tuning], Tuning toggle, Sync sources, New chat, Sign in / Sign out — and the `#steering-panel` section on every page; the document viewer keeps back + title in a second titlebar row; the login page finally carries the full header.
|
||||
|
||||
## Work
|
||||
The canonical block is `index.html`'s current header **plus** the `#sync-btn` copied verbatim from `sources.html` (hidden by default, `#sync-label` + `.sync-icon` inside). Place the Sync button **after** the Tuning toggle and **before** the New chat button on every page.
|
||||
|
||||
1. `frontend/index.html` (chat) — add the `#sync-btn` block to the header (the only missing control); everything else already ships. `#steering-panel` stays where it is (after `#kb-banner` in `<main>`).
|
||||
2. `frontend/sources.html` — add the Tuning toggle block (copied from `index.html`: `#steering-toggle` + `#steering-count`) after the nav; add the `#steering-panel` section (copied from `index.html`, incl. the `#steering-announcer` paragraph) as the **first child of `<main>`**; keep the existing `#sync-btn` where it is.
|
||||
3. `frontend/tuning.html` — add the Tuning toggle block + the `#sync-btn` block to the header (same order as chat); add the `#steering-panel` section as the first child of `<main>`.
|
||||
4. `frontend/document.html` — restructure the header:
|
||||
- **Row 1** becomes the standard `.app-header` / `.header-inner` bar, byte-for-byte the same block as the other pages: brand, `<nav class="app-nav">` with `Chat` + `#nav-sources` (hidden) + `#nav-tuning` (hidden), Tuning toggle, `#sync-btn` (hidden), New chat, Sign in (`?next=/document.html` static fallback) / Sign out.
|
||||
- **Row 2** — a new `.doc-titlebar` container inside the same `<header>`, carrying the existing `#doc-back` link + `#doc-title` + `#doc-meta` (moved out of the old `.doc-header-inner` title block, markup otherwise unchanged — `renderDocument` addresses them by id, so `document.js` needs no render change).
|
||||
- ASSUMPTION: **no nav link gets `is-active` / `aria-current` on the viewer** — a document is a detail view reachable from chat or Sources (phase 13's `back` param), so no single nav target is "current". The back link carries the navigation affordance.
|
||||
- The old `.doc-header-actions` wrapper is dropped — its buttons now live in row 1's standard `.header-inner`.
|
||||
5. `frontend/login.html` — full header: the nav gains the `#nav-tuning` link (after `#nav-sources`, same hidden-by-default markup as the other pages); add the Tuning toggle + `#sync-btn` + New chat + the Sign in / Sign out pair (sign-in static fallback `?next=/login.html`); add the `#steering-panel` section as the first child of `<main>`.
|
||||
6. `frontend/assets/header.js` — comment updates only: the viewer now has a nav (its "the viewer has no nav" notes are stale); the module's "missing element is a no-op" contract still holds for any page missing an element. No behavior change — the reveal code already handles `#nav-sources` / `#nav-tuning` / `#sync-btn` / the auth pair wherever they exist.
|
||||
7. Update the stale HTML comments in the touched headers (phase-19/29 comments describing the old page-specific layouts) to reference this phase + the owner confirmation (2026-08-26).
|
||||
|
||||
Rules:
|
||||
- Keep every existing id / class / aria attribute exactly as it exists today (the E2E suites key off them); only ADD missing blocks and move the viewer's title elements.
|
||||
- Ship-hidden stays ship-hidden: `#nav-sources`, `#nav-tuning`, `#sync-btn`, and exactly one of the auth pair are `hidden` in the markup on every page — `header.js` reveals at load (one whoami, cached).
|
||||
- Preserve indentation/markup style so the five headers stay diff-identical (that identity is what task 05's E2E asserts).
|
||||
|
||||
## Testing & Quality
|
||||
- No Python change; the no-CDN integration test must still pass (same-origin markup only).
|
||||
- Coverage: `app/` gate unaffected.
|
||||
- Manual smoke before task 05: with the dev server, as admin and as anonymous, each of the five pages shows the full bar (admin) / reduced bar (anonymous) with no console errors.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] All five page headers contain the same control inventory in the same order: brand, nav [Chat, Sources, Tuning], `#steering-toggle`, `#sync-btn`, `#new-chat-btn`, `#sign-in-link` + `#sign-out-btn`.
|
||||
- [ ] `#steering-panel` (+ `#steering-announcer`) exists on all five pages (chat: after `#kb-banner`; others: first child of `<main>`).
|
||||
- [ ] `document.html` = standard row + `.doc-titlebar` row with `#doc-back` / `#doc-title` / `#doc-meta`; no nav link carries `is-active` there.
|
||||
- [ ] `login.html` carries the full header incl. the `#nav-tuning` link and the auth pair.
|
||||
- [ ] `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Task 04 — Two-row viewer header styles + sync failed state
|
||||
|
||||
**Phase:** `34_consistent_navbar` · **Source:** `TODO.md:3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."`
|
||||
**Story:** `.agent/user_stories/nav-consistency.md`
|
||||
|
||||
## Objective
|
||||
Style the document viewer's new two-row header (standard row + titlebar row) so row 1 is visually indistinguishable from the other pages' bars, and give the Sync button a visible failed state on pages that have no error banner (every page except Sources).
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/styles.css` — **viewer header:**
|
||||
- The viewer's `<header>` keeps the sticky app-frame behavior; **row 1** reuses the existing `.app-header` / `.header-inner` rules verbatim (64px desktop, 58px at ≤640px — the phase-12/19 pinned heights apply because row 1 *is* the standard bar).
|
||||
- New `.doc-titlebar` rules for row 2: `.container`-width inner row with `#doc-back` + the title block; its own height (title line + meta line), `border-top` separator in the existing hairline color, same surface color (`#121a2e`/`#0a0e17` family per the phase-08 palette); `#doc-title` truncates with an ellipsis + `title` attribute instead of the old pill-clipping rule.
|
||||
- The old `.doc-header` / `.doc-header-inner` / `.doc-header-actions` / title-clipping rules are removed or reduced to the two-row structure (keep class names that task 05's updated contract suites reference — check which selectors the suites use before deleting: `.doc-header` may remain as the header element's class wrapping both rows).
|
||||
- The `.steering-panel` positioning rules must work from the new placement on the non-chat pages (first child of `<main>`) — the panel is an in-flow section, so this is expected to be a no-op; verify visually and in the E2E.
|
||||
2. `frontend/assets/styles.css` — **sync failed state (non-Sources pages):** the failed `#sync-btn` gets an error treatment from the phase-08 palette (error ink `#fca5a5` on the error surface `#2d1318`, border `#f59e0b`-free — the error chip uses `#fca5a5`/`#2d1318`, ≈9.1:1) so a failed sync is visible on every page, complementing the `title`/`aria-label` error text set by `header.js`. Reuse the existing `.sync-btn` state classes/styles if phase 32 already defines a failed look; otherwise add it.
|
||||
3. `frontend/assets/styles.css` — **login page:** the full header needs no new rules (it reuses `.app-header`), but confirm the login card layout still centers correctly with the full bar (no header-height regression at ≤640px).
|
||||
4. Accessibility checks (AGENTS.md rule 5): `:focus-visible` 3px outline on the new titlebar back link (it already has the existing `.doc-back` styles — preserve); contrast ≥4.5:1 for title/meta text; `prefers-reduced-motion` still stills the sync spinner.
|
||||
|
||||
## Testing & Quality
|
||||
- CSS-only — no Python change; the no-CDN integration test is unaffected.
|
||||
- Coverage: `app/` gate unaffected.
|
||||
- Visual pass (dev server, admin + anonymous, desktop + 640px): all five pages, viewer row 1 identical to chat's bar; viewer row 2 shows back + title + meta; the sync button's failed state is visible and readable.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] The viewer's row 1 renders pixel-consistent with the chat/sources bars (same height 64px / 58px, same paddings, same controls).
|
||||
- [ ] `.doc-titlebar` renders back + title (ellipsis) + meta badges on one or two tidy lines; sticky with the header.
|
||||
- [ ] A failed sync shows an error-colored `#sync-btn` on non-Sources pages with the sanitized error in `title` / `aria-label`.
|
||||
- [ ] No console layout breakage on the login page (card still centered, 58px bar at ≤640px).
|
||||
- [ ] `uv run pytest` green (no-CDN test included); `uv run ruff check . && uv run pyright` clean.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Task 05 — Story E2E + contract-suite updates + regression pass
|
||||
|
||||
**Phase:** `34_consistent_navbar` · **Source:** `TODO.md:3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."`
|
||||
**Story:** `.agent/user_stories/nav-consistency.md`
|
||||
|
||||
## Objective
|
||||
Prove the contract with the story's dedicated Playwright suite — identical visible header control inventory on all five pages for each role — update the two pre-existing contract suites that encoded the old viewer bar, and run the full regression list.
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/test_nav_consistency.py` (NEW — the story gate, run in isolation). Fixtures: the standard E2E app + DB (see `tests/e2e/conftest.py`); a fixture document for the viewer URL (the `source=docs&path=homelab%2Fkubernetes.md` pattern from `test_shared_header.py`); admin session via `tests/e2e/auth_helpers.py`.
|
||||
- **Admin inventory (all five pages):** `/`, `/sources.html`, `/document.html?source=…&path=…`, `/tuning.html`, `/login.html` — each header contains, visible: the three nav links (Chat, `#nav-sources`, `#nav-tuning`), `#steering-toggle`, `#sync-btn`, `#new-chat-btn`, and `#sign-out-btn` visible with `#sign-in-link` hidden. Assert the same **id + class inventory and DOM order** of the header controls on every page (normalize: the current-page `is-active` nav marker and the sign-in `?next=` value legitimately differ per page).
|
||||
- **Anonymous inventory (all five pages):** nav present with Chat visible and `#nav-sources` / `#nav-tuning` hidden (locked A10 UI revision); `#sync-btn` hidden; `#sign-in-link` visible, `#sign-out-btn` hidden; `#steering-toggle` visible.
|
||||
- **Viewer specifics:** row 1 height equals the chat page's header height (64px desktop / 58px ≤640px); the titlebar row is visible with `#doc-back` + `#doc-title` (rendered document title) + `#doc-meta` badges; clicking `#doc-back` honors the `back` param (phase 13 — one positive + one rejection case).
|
||||
- **Steering works off-chat:** as admin, on `/tuning.html` — seed zero notes (truncate `steering_notes` via a `SessionLocal` like the other suites), click `#steering-toggle` → `#steering-panel` visible + `aria-expanded="true"` + empty state shown; add a note through the panel? (the panel has no add form — it lists notes; assert the toggle open/close cycle + the count badge reads 0) — keep this deterministic, no chat needed.
|
||||
- **Sync present, not triggered:** as admin on `/tuning.html` assert `#sync-btn` is visible (do NOT click it — a real sync would clone real repos; the full state machine is `test_sync_button.py`'s job).
|
||||
2. `tests/e2e/test_header_consistency.py` (UPDATE to the new contract): the viewer assertions change — `.doc-header` is now the two-row header; assert **row 1** (the standard bar) is 64px desktop / 58px ≤640px and identical to the chat/sources bars (the existing `_box_height(page, ".doc-header")` measurement must be pointed at the standard row — use the row-1 selector, e.g. `.app-header .header-inner` inside the viewer header), and assert the titlebar row is present (height > 0). The chat/sources assertions are unchanged.
|
||||
3. `tests/e2e/test_shared_header.py` (UPDATE to the new contract): the "the viewer has no nav — no Sources link in the DOM" assertions flip — the viewer now carries the same nav contract (`.app-nav` with Chat + hidden `#nav-sources` + hidden `#nav-tuning`, revealed for admin). The auth-pair + New chat assertions for the viewer stay (they move from `.doc-header-actions` to the standard bar — update the selectors).
|
||||
4. **Regression pass — each in isolation** (`uv run pytest tests/e2e/<file>.py -v --no-cov`): `test_header_consistency.py`, `test_shared_header.py`, `test_document_back_navigation.py`, `test_document_viewer.py`, `test_steering.py`, `test_global_tuning.py`, `test_sync_button.py`, `test_tuning_nav_link.py`, `test_smoke.py`, `test_chat_rag.py`, `test_admin_auth.py`. Fix fallout in the suites above where the old contract is encoded; fix app code where behavior genuinely changed.
|
||||
5. Full gate: `uv run pytest` (unit + integration), `uv run pytest --cov=app --cov-report=term-missing` (TOTAL unchanged, >90%), `uv run ruff check . && uv run pyright`.
|
||||
6. **UI Structure Check** (AGENTS.md rule 5) on the five headers + the new titlebar: landmarks (`<header>`, `<nav aria-label>`, `<main>`), labels, contrast ≥4.5:1, focus-visible, no CDN (rule 6 — the no-CDN integration test covers it).
|
||||
7. **Commit** (A17): stage only this phase's files (`frontend/**`, `tests/e2e/**`), message `feat(ui): one consistent navbar on every page (TODO.md L3)`, always `--no-gpg-sign`. Move `.agent/phases/todo/34_consistent_navbar/` to `.agent/phases/complete/`.
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: `tests/e2e/test_nav_consistency.py` green **in isolation** — the story gate (A16: one story, one file).
|
||||
- Unit/integration: no new `app/` logic — the existing suite (incl. the no-CDN test) stays green.
|
||||
- Coverage: **>90%** on `app/` — unchanged (no Python change).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_nav_consistency.py -v --no-cov` green in isolation.
|
||||
- [ ] `test_header_consistency.py` + `test_shared_header.py` updated and green; every suite in the task 05 regression list green in isolation.
|
||||
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged (>90%).
|
||||
- [ ] `uv run ruff check . && uv run pyright` clean.
|
||||
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agent/phases/complete/`.
|
||||
Reference in New Issue
Block a user