Files
brain-of-reese/.agents/phases/complete/77_navbar_refresh/00_phase.md
T
ducoterra 7fce6572d0
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 13s
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
2026-09-07 12:39:01 -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/.