feat: phases 77–80 — navbar view refresh, static background, API tokens, history suggestion chips
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 13s

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:
2026-09-07 12:39:01 -04:00
parent 495d042a98
commit 7fce6572d0
215 changed files with 10142 additions and 1643 deletions
@@ -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/`.