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
6.6 KiB
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-statuslive 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 ownrootinsidemount()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, captureconst wasMounted = mounted[name]BEFORE the mount block. After the view is shown and the head/nav state is written, dispatch the eventif (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 onwasMounted). - Active-view re-click: the click handler's
if (name === current) return;becomes: dispatchbor:view-refreshonviewEls[name]and return — NOpushState(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.jsloadDocsclears viatbody.replaceChildren()(check itsshowEmpty()path also clears the rows — if not, clear at the top ofloadDocs);tuning.jsrenderNotesclears viatuneList.textContent = "";git-sources.jsrenderSourcesreplaces the list (verify the error/empty states reset on a re-call —showLoadErrorhides table AND empty state).history.jsloadChatsAPPENDS — it must remove the data rows (everytrin#history-tbodyEXCEPT 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-gateand 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 (
windowglobal set before the clicks is still readable after — no document load).
Dependencies
— (none; builds on the completed phase-76 shell)
Tasks
01_router_refresh_hook.md— thebor:view-refreshdispatch inrouter.js(re-show + active re-click + popstate; first show exempt) and the History view re-fetching on it.02_refresh_other_views.md— RAG, Sources, and Tuning listen and re-fetch; the Chat view stays untouched (with a comment pinning the exclusion).03_history_refresh_button.md— the History Refresh button + the story E2E suitetest_navbar_refresh.py+ the regression sweep + the atomic commit.
Testing & Quality
- Unit:
tests/unit/test_frontend_router.py— new source-level pins: thebor:view-refreshliteral exists; the dispatch is gated on the pre-mountmountedstate (first show exempt); the re-click branch dispatches instead of a barereturn(nopushState); the three other view modules each carry a listener andapp.jsdoes NOT (negative pin). - E2E: new story suite
tests/e2e/test_navbar_refresh.pyrun 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-covgreen in isolation (DB up).tests/e2e/test_nav_switch_keeps_stream.pystill green UNCHANGED (the stream-survival contract holds with the hook in place).uv run pytestgreen;uv run pytest --cov=app --cov-report=term-missing>90%;uv run ruff check . && uv run pyrightclean.- One atomic
--no-gpg-signConventional-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/.