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
This commit is contained in:
@@ -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/`.
|
||||
Reference in New Issue
Block a user