Protocol B append: navbar refresh + History refresh button (77, TODO L3), static background — glow layers removed (78, TODO L4), admin-issued API tokens with the in-app gate + browser caching, only shared chats stay anonymous (79, TODO L5), onboarding chips as the last 3 questions asked with the env seed only before the first (80, TODO L6). TODO.md cleared — its items now live in .agents/phases/todo/. Owner-confirmed assumptions recorded in each phase overview (A1–A7, chat 2026-09-06).
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/.