feat: phases 77–80 — navbar view refresh, static background, API tokens, history suggestion chips
Single consolidated commit for four completed, validated phases (77, 78, 79, 80). The pipeline run left all work uncommitted because the harness commits only with PHASE_COMMIT=1 while child executors are forbidden from committing; the phases themselves all passed validation and moved to .agents/phases/complete/. Phase 77 — navbar view refresh - router.js dispatches bor:view-refresh on re-show / active re-click / popstate (gated on wasMounted; first show and boot exempt) - History / RAG / Sources / Tuning re-fetch on refresh (admin branch); Chat deliberately excluded (stream survival) - History "Refresh" button (admin-only, in-flight disable + status line) - New story suite tests/e2e/test_navbar_refresh.py (7 tests) Phase 78 — static background - Removed the animated glow layers; static 44px grid over the flat --bg canvas; default and reduced-motion renders byte-identical - Updated background/theme E2E suites; removed bg-glow test pins Phase 79 — API tokens - api_tokens model + migration 0012; hash-only token service - Admin tokens API + Tokens admin view; POST /api/token-auth; live-revoking require_user on chat / suggestions / document content - Frontend token gate with localStorage cache; anonymous E2E suites migrated to token login - New story suite tests/e2e/test_api_tokens.py (9 tests) Phase 80 — history suggestion chips - last_questions() endpoint with SEED fallback; startNewChat() refetch - Seed-semantics docs (config.py, .env.example, README) - Integration state matrix + E2E suite rewritten to the 4 chip states Also included: phase-76 report artifacts and the repo restore-test-db skill (previously untracked), scripts/* ruff fixes from phase 77. Final gate state (phase 80 final pass, covers everything above): - uv run pytest --cov=app → 1637 passed, 0 failed, app/ coverage 99% - uv run ruff check . && uv run pyright → clean, 0 errors - Per-phase story E2E suites green in isolation
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# Phase 77 — Navbar clicks refresh the view's data (fresh list on re-show + a History refresh button)
|
||||
|
||||
**Source:** `TODO.md` L3 — "Clicking navbar icons should refresh the relevant page. For example, clicking 'history' doesn't load new history until I refresh. The history page should also have a refresh button."
|
||||
**Story:** n/a (TODO-derived — descendant of `.agents/user_stories/chat-history.md` (phase 50) and the phase-76 shell)
|
||||
**Context:** `frontend/assets/router.js` (the phase-76 shell router: the `VIEW` map, mount-once / hide-forever, `switchTo(name, { userInitiated })`, the delegated nav click handler with its `name === current` early return, the `popstate` handler), `frontend/assets/history.js` (the History view module — `loadChats()` APPENDS rows and is called once at mount), `frontend/assets/sources.js` (`loadDocs()` ~line 472 — already clears `tbody` before rendering), `frontend/assets/git-sources.js` (`loadSources()` ~line 230 — render + announce), `frontend/assets/tuning.js` (`loadNotes()` ~line 116 — `renderNotes` clears), `frontend/index.html` (`#view-history`'s `.page-head` — h1 "Saved chats" + sub; the `#history-status` live region), `tests/unit/test_frontend_router.py` (the source-level router pins), `tests/e2e/test_nav_switch_keeps_stream.py` (the phase-76 suite — must stay green UNCHANGED).
|
||||
|
||||
## Objective
|
||||
Since the phase-76 shell, a view's data is fetched exactly once, at mount (mount-once, hide-forever) — a History view opened at 10:00 still shows 10:00's data at 10:30. Make every user-initiated re-show of a view re-fetch its list, and give History an explicit Refresh button. The Chat view is deliberately out of scope: its in-flight stream and local conversation must survive (the phase-76 contract).
|
||||
|
||||
## Owner decisions (chat, 2026-09-06 — recorded per AGENTS.md rule 3)
|
||||
- **A1 confirmed:** "the relevant page" = the four data views (History, RAG, Sources, Tuning). **Chat is EXCLUDED from the refresh hook** — the in-flight SSE stream and the local conversation persist (the phase-76 LOCKED refinement).
|
||||
- The refresh fires on: (a) a switch TO the view when it is already mounted, (b) a re-click of the active view's own nav link (today a no-op), (c) back/forward (`popstate`) onto an already-mounted view. The FIRST show (the mount) and the boot never fire it — the mount's own load is the first fetch.
|
||||
- The History refresh button lives in the view's page-head and announces through the existing `#history-status` live region.
|
||||
|
||||
## Design (shared by all tasks — the executor reads this, not the chat)
|
||||
- **Mechanism — a DOM event, zero router-state changes:** on any user-initiated re-show, the router dispatches `new CustomEvent("bor:view-refresh")` on the view's `<section>` root. A view module that wants fresh data listens on its own `root` inside `mount()` and re-runs its existing load function. Modules that do not listen are unaffected — the chat view never listens.
|
||||
- **First-show exemption (the no-double-fetch rule):** in `switchTo`, capture `const wasMounted = mounted[name]` BEFORE the mount block. After the view is shown and the head/nav state is written, dispatch the event `if (wasMounted)` — a re-show. The first show (mount) loads once and dispatches nothing; boot (`userInitiated: false`) can never dispatch (boot always finds an unmounted view or the chat view, and the dispatch site is gated on `wasMounted`).
|
||||
- **Active-view re-click:** the click handler's `if (name === current) return;` becomes: dispatch `bor:view-refresh` on `viewEls[name]` and return — NO `pushState` (the URL already IS that view's path); the mobile menu still closes (the container handler runs regardless).
|
||||
- **Re-entrance of the load functions:** each must be safe to call repeatedly. Verified: `sources.js` `loadDocs` clears via `tbody.replaceChildren()` (check its `showEmpty()` path also clears the rows — if not, clear at the top of `loadDocs`); `tuning.js` `renderNotes` clears via `tuneList.textContent = ""`; `git-sources.js` `renderSources` replaces the list (verify the error/empty states reset on a re-call — `showLoadError` hides table AND empty state). `history.js` `loadChats` **APPENDS** — it must remove the data rows (every `tr` in `#history-tbody` EXCEPT the hidden `#history-empty-row`) before re-fetching.
|
||||
- **Gate guard:** a view only re-fetches after its whoami gate has passed (History: anonymous shows `#history-gate` and NEVER calls `/api/chats` — the phase-50 contract the story E2E pins; the listener must respect the same branch).
|
||||
- **Focus/scroll unchanged:** the router still lands the viewport at the top on user-initiated switches; the refresh is a background re-fetch behind the already-shown view.
|
||||
- **E2E "freshness" proof:** create new backing data via the API AFTER a view has loaded, nav back (or re-click / refresh-button), assert the new row — with the phase-76 canonical same-document sentinel (`window` global set before the clicks is still readable after — no document load).
|
||||
|
||||
## Dependencies
|
||||
— (none; builds on the completed phase-76 shell)
|
||||
|
||||
## Tasks
|
||||
1. `01_router_refresh_hook.md` — the `bor:view-refresh` dispatch in `router.js` (re-show + active re-click + popstate; first show exempt) and the History view re-fetching on it.
|
||||
2. `02_refresh_other_views.md` — RAG, Sources, and Tuning listen and re-fetch; the Chat view stays untouched (with a comment pinning the exclusion).
|
||||
3. `03_history_refresh_button.md` — the History Refresh button + the story E2E suite `test_navbar_refresh.py` + the regression sweep + the atomic commit.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: `tests/unit/test_frontend_router.py` — new source-level pins: the `bor:view-refresh` literal exists; the dispatch is gated on the pre-mount `mounted` state (first show exempt); the re-click branch dispatches instead of a bare `return` (no `pushState`); the three other view modules each carry a listener and `app.js` does NOT (negative pin).
|
||||
- E2E: new story suite `tests/e2e/test_navbar_refresh.py` run in isolation (the scenarios live in task 03).
|
||||
- Coverage: **>90%** on `app/` — this phase is frontend-only; the floor is preserved by not regressing.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_navbar_refresh.py -v --no-cov` green in isolation (DB up).
|
||||
- [ ] `tests/e2e/test_nav_switch_keeps_stream.py` still green UNCHANGED (the stream-survival contract holds with the hook in place).
|
||||
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
|
||||
- [ ] One atomic `--no-gpg-sign` Conventional-Commits commit (e.g. `feat(ui): refresh view data on navbar re-show + History refresh button`) whose body cites TODO.md L3; phase dir moved to `.agents/phases/complete/`.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Task 01 — The `bor:view-refresh` hook; History re-fetches on a user-initiated re-show
|
||||
|
||||
**Phase:** `77_navbar_refresh` · **Source:** `TODO.md:3` — "Clicking navbar icons should refresh the relevant page. For example, clicking 'history' doesn't load new history until I refresh."
|
||||
**Story:** n/a (TODO-derived)
|
||||
|
||||
## Objective
|
||||
A user-initiated re-show of an already-mounted view re-fetches its data: `router.js` dispatches `bor:view-refresh` on the view's section (re-shows and active re-clicks only — never on the first mount or boot), and the History view listens and re-loads its chat list.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/router.js` — in `switchTo`: capture `const wasMounted = mounted[name]` BEFORE the mount block; after the show + head/nav state is written (before the focus/scroll tail is fine — the event order is: visible → refresh dispatched), `if (wasMounted) root.dispatchEvent(new CustomEvent("bor:view-refresh"))`. In the delegated nav click handler, replace the `if (name === current) return;` early return with a dispatch of `bor:view-refresh` on `viewEls[name]` + `return` (no `pushState` — the URL is already this view's path; the mobile menu still closes via the container handler). The `popstate` path flows through `switchTo`, so it inherits the `wasMounted` gating automatically. Update the file-header contract comment with the refresh rule: the event fires exactly when an already-mounted view is shown again — first show and boot never (the mount's own load is the first fetch).
|
||||
2. `frontend/assets/history.js` — inside `mount(root)`:
|
||||
- make `loadChats()` re-entrant: at its top, remove the data rows — every `tr` in `#history-tbody` EXCEPT the hidden `#history-empty-row` — so a re-load replaces the list instead of appending a duplicate set;
|
||||
- add the listener in the ADMIN branch (after the `fetchIsAdmin()` gate passes — anonymous shows `#history-gate` and never fetches, per the phase-50 contract the story E2E pins): `root.addEventListener("bor:view-refresh", () => { if (started) loadChats(); })` where `started` flips to `true` once the first `loadChats()` call is made (the gate branch that `return`s early must not arm a listener that fetches).
|
||||
3. `tests/unit/test_frontend_router.py` — new source pins (house pattern — read the JS source, no browser): the `bor:view-refresh` literal exists in `router.js`; the dispatch site is guarded by a pre-mount `mounted` capture (assert the `wasMounted`-style capture appears before the mount block — e.g. the capture assignment precedes the `mounted[name]` set); the re-click branch dispatches (assert the `name === current` branch contains the dispatch literal, not a bare return); `history.js` contains the listener.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: item 3 (mechanism-level pins; the existing router pins — pushState switches, mount-once, hidden+inert, single-writer head/nav — must stay green UNCHANGED).
|
||||
- E2E: none yet — the story suite lands in task 03.
|
||||
- Coverage: n/a for this task (frontend-only) — the `app/` floor must not regress.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/unit/test_frontend_router.py -v` green (old + new pins).
|
||||
- [ ] Manual spot check (optional): load History as admin, save a new chat via the API, click another view then History again — the new row is there without a document reload.
|
||||
- [ ] `uv run ruff check . && uv run pyright` clean — the gate runs after this task.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Task 02 — RAG, Sources, and Tuning join the refresh (Chat stays out)
|
||||
|
||||
**Phase:** `77_navbar_refresh` · **Source:** `TODO.md:3` — "Clicking navbar icons should refresh the relevant page."
|
||||
**Story:** n/a (TODO-derived)
|
||||
|
||||
## Objective
|
||||
The three remaining data views re-fetch on `bor:view-refresh` with the same gate-guarded, re-entrant pattern as History. The Chat view deliberately does NOT listen — its in-flight stream and local conversation persist (the phase-76 contract).
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/sources.js` — in `mount(root)`, in the admin branch (after the `fetchIsAdmin()` gate passes): `root.addEventListener("bor:view-refresh", () => loadDocs())`. Verify `loadDocs()` re-entrance end to end: the populated path already clears (`tbody.replaceChildren()`); check the `showEmpty()` path — if it does not clear the tbody rows, a refresh from a populated list into an empty result would leave ghost rows: add the row-clear at the top of `loadDocs()` (the hidden empty-row stays in place, exactly the History pattern from task 01).
|
||||
2. `frontend/assets/git-sources.js` — same: `root.addEventListener("bor:view-refresh", () => loadSources())` in the admin branch. Verify a re-call resets all three list states (populated render, the empty state, and the `showLoadError` state — `showLoadError` already hides the table AND the empty state, so an error followed by a successful refresh must clear the error: `loadSources` already calls `hideLoadError()` on success — confirm).
|
||||
3. `frontend/assets/tuning.js` — same: `root.addEventListener("bor:view-refresh", () => loadNotes())` in the admin branch; `renderNotes` already clears (`tuneList.textContent = ""`). Note: `loadNotes` keeps the last rendered list on a failed fetch (its documented contract) — a refresh that fails must behave the same way (no change needed; the listener just calls the function).
|
||||
4. `frontend/assets/app.js` (the chat view) — add NO listener. Add a one-line comment where the chat view's module-scope state begins: the `bor:view-refresh` exclusion is deliberate — the in-flight SSE stream and the local conversation must survive every switch (phase 76), so the chat view never re-fetches on a show.
|
||||
5. `tests/unit/test_frontend_router.py` (or the neighboring source-pin file if the house split the pins — match where the task-01 pins landed) — source pins: `sources.js`, `git-sources.js`, and `tuning.js` each contain the `bor:view-refresh` listener; `app.js` does NOT (negative pin — the exclusion is a contract, not an oversight).
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: item 5.
|
||||
- E2E: covered by the task-03 story suite — one assertion per view (mutate the backing data via the API while the view is hidden, nav back, the row reflects the mutation; where a view's backing data has no cheap API mutation path, assert via the Playwright request log that the re-fetch happened on re-show).
|
||||
- Coverage: n/a (frontend) — the `app/` floor preserved.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] All four data views re-fetch on a re-show; the chat view is untouched (stream survival still holds — `test_nav_switch_keeps_stream.py` green).
|
||||
- [ ] The new unit pins are green; `uv run ruff check . && uv run pyright` clean — the gate runs after this task.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Task 03 — History refresh button + the story E2E suite + the commit
|
||||
|
||||
**Phase:** `77_navbar_refresh` · **Source:** `TODO.md:3` — "…The history page should also have a refresh button."
|
||||
**Story:** n/a (TODO-derived)
|
||||
|
||||
## Objective
|
||||
The explicit half of the item: a visible Refresh control on the History view — plus the phase's Playwright story suite proving every refresh path, the regression sweep, and the atomic commit.
|
||||
|
||||
## Work
|
||||
1. `frontend/index.html` — in `#view-history`'s `.page-head`: add a right-aligned actions slot (`.page-head` becomes a flex row — title block left, actions right; wraps below 640px): `<button type="button" class="history-refresh" id="history-refresh" aria-label="Refresh saved chats">` with the house inline-SVG refresh glyph (`aria-hidden="true"`) + a visible "Refresh" text label (the phase-46 auth-link convention: label visible ≥640px, icon-only below — the `aria-label` keeps the accessible name in both).
|
||||
2. `frontend/assets/history.js` — bind `#history-refresh` (admin branch only — the button lives in the view, which is admin-gated): on click: disable the button (no double-fire while in flight) → `loadChats()` → announce the outcome in `#history-status` (`Saved chats refreshed.` on success; the existing failure lines on a non-2xx / network error — reuse the exact copy `loadChats`'s callers would see) → re-enable the button. The button stays reachable while the empty state is showing (it sits in the page-head, outside the table wrap).
|
||||
3. `frontend/assets/styles.css` — `.history-refresh` (reuse the `.new-chat-btn` visual language: brand background, WCAG 4.5:1, `focus-visible` ring, hover state) + the `.page-head` flex layout (no layout change for views that have no actions slot — the other four views' page-heads are untouched).
|
||||
4. `tests/e2e/test_navbar_refresh.py` (NEW story suite — run in isolation, DB up, the mock-LLM fixture from `tests/e2e/conftest.py`; admin via `auth_helpers.login`):
|
||||
- **re-show refresh:** sign in → create saved chat A via `POST /api/chats` (authed httpx or the UI) → nav to History (row A visible) → create chat B via the API → nav to Chat → nav back to History → row B visible; a `window` sentinel set before the nav clicks is still readable afterwards (the phase-76 canonical no-document-load proof).
|
||||
- **active-view re-click:** with History visible (rows A, B) → create C via the API → click the History nav link AGAIN → C appears; the URL is still `/history.html` (no new history entry — `history.length` unchanged).
|
||||
- **refresh button:** create D via the API → click `#history-refresh` → D appears; `#history-status` announces `Saved chats refreshed.`; the button is `disabled` during the in-flight request (assert via the Playwright request hook or a short-poll on the disabled state).
|
||||
- **popstate:** Chat → History (loads) → create E via the API → `page.go_back()` → History view → E present.
|
||||
- **the four views:** one assertion per data view that a re-show re-fetches (RAG / Sources / Tuning / History — the request-log or row-delta pattern from task 02).
|
||||
- **stream-survival control:** send a question (mock LLM — the ~8 s stream) → mid-stream nav to RAG → back to Chat → the FULL answer completes (the phase-76 contract holds with the hook in place). The unchanged `tests/e2e/test_nav_switch_keeps_stream.py` run in isolation is the additional control.
|
||||
5. Regression sweep + commit: `uv run pytest --cov=app --cov-report=term-missing` (>90%), `uv run ruff check . && uv run pyright`, `tests/e2e/test_nav_switch_keeps_stream.py` green unchanged in isolation, then ONE atomic `--no-gpg-sign` Conventional-Commits commit — e.g. `feat(ui): refresh view data on navbar re-show + History refresh button` — body cites TODO.md L3; move the phase dir to `.agents/phases/complete/`.
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: item 4 is the story gate (AGENTS.md rules 4 + 9 — one file, run in isolation).
|
||||
- Coverage: **>90%** on `app/` (frontend-only phase — the floor is preserved).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_navbar_refresh.py -v --no-cov` green in isolation.
|
||||
- [ ] The Refresh button is visible, keyboard-reachable, and announces (WCAG 2.1 AA basics — AGENTS.md rule 5).
|
||||
- [ ] Full suite + coverage + lint green; the phase-76 suite unchanged-green; one atomic commit; phase dir in `.agents/phases/complete/`.
|
||||
Reference in New Issue
Block a user