Files
brain-of-reese/.agents/phases/todo/77_navbar_refresh/00_phase.md
T
ducoterra 495d042a98
Build and Push Containers / build-and-push-app (push) Successful in 1m54s
Build and Push Containers / build-and-push-db (push) Successful in 13s
chore(agent): phase roadmap from TODO.md — 4 phases (77–80)
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).
2026-09-06 23:54:11 -04:00

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-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/.