feat: phases 77–80 — navbar view refresh, static background, API tokens, history suggestion chips
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 13s

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:
2026-09-07 12:39:01 -04:00
parent 495d042a98
commit 7fce6572d0
215 changed files with 10142 additions and 1643 deletions
+48 -7
View File
@@ -32,6 +32,17 @@
* upload-progress poller) persist across every switch; that
* persistence IS the phase-76 fix. The chat view needs no module:
* app.js already ran at shell boot.
* • refresh hook (phase 77): a user-initiated re-show of an
* already-mounted view dispatches the `bor:view-refresh` CustomEvent
* on the view's <section> root — exactly when the view is shown
* AGAIN: a switch back onto it, a re-click of its own (active) nav
* link (NO pushState — the URL is already its path), or back/
* forward (popstate) onto it. The FIRST show (the mount) and boot
* NEVER fire it — the mount's own load is the first fetch. A view
* module opts in by listening on its own root inside mount();
* views that do not listen (Chat — app.js) are unaffected: the
* in-flight stream and the local conversation survive (the phase-76
* LOCKED refinement).
* • show = drop hidden + inert, hide = add BOTH (WCAG: a hidden view
* must not receive focus or keyboard traversal — the inert pair
* pins the [hidden] contract in the a11y tree, AGENTS.md rule 5).
@@ -50,9 +61,9 @@
* chat view, runs at shell boot exactly as before) → router.js
* (module — this file). No CDN, no framework, no bundler dependency:
* a plain ES module whose dynamic imports (./tuning.js, task 01;
* ./sources.js + ./git-sources.js, task 02; ./history.js in task 03)
* resolve relatively in dev and are inlined by the Containerfile's
* esbuild stage in the image.
* ./sources.js + ./git-sources.js, task 02; ./history.js in task 03;
* ./tokens.js in phase 79 task 06) resolve relatively in dev and are
* inlined by the Containerfile's esbuild stage in the image.
*/
/* ---------- the view map (pathname → view name) ----------
@@ -67,6 +78,7 @@ const VIEW = {
"/sources.html": "rag", // phase 76 task 02: the RAG view (knowledge base)
"/git-sources.html": "git-sources", // phase 76 task 02: the Sources view
"/history.html": "history", // phase 76 task 03: the History view (saved chats)
"/tokens.html": "tokens", // phase 79 task 06: the Tokens view (access tokens)
};
/* The nav-link href the router stamps active for each view (the
@@ -77,6 +89,7 @@ const VIEW_PATH = {
rag: "/sources.html",
"git-sources": "/git-sources.html",
history: "/history.html",
tokens: "/tokens.html",
};
/* The lazy view modules — ONLY the non-chat views (chat needs no
@@ -89,6 +102,7 @@ const VIEW_MODULES = {
rag: () => import("./sources.js"), // phase 76 task 02
"git-sources": () => import("./git-sources.js"), // phase 76 task 02
history: () => import("./history.js"), // phase 76 task 03
tokens: () => import("./tokens.js"), // phase 79 task 06
};
/* Per-view document.head values, carried over from the old pages'
@@ -101,6 +115,7 @@ const TITLES = {
rag: "Sources · Brain of Reese", // old sources.html <title>
"git-sources": "Git sources · Brain of Reese", // old git-sources.html <title>
history: "Saved chats · Brain of Reese", // old history.html <title>
tokens: "Access tokens · Brain of Reese",
};
const DESCRIPTIONS = {
chat:
@@ -112,6 +127,7 @@ const DESCRIPTIONS = {
"Add and remove the git repositories Brain of Reese syncs and indexes (admin-only).",
history:
"Saved chats — every conversation is saved automatically, one click back.", // old history.html meta
tokens: "Generate and revoke the API tokens that let people use the app.",
};
/* The brand-resolved display name (phase 39 — brand.js is the single
@@ -136,9 +152,10 @@ for (const name of new Set(Object.values(VIEW))) {
}
/* The mount-once guard: a view is imported + mounted at most ONCE per
document life — re-shows are show/hide only (no refetch, no
re-mount; the view's state persists). Chat starts mounted: app.js
owns it and ran at shell boot. */
document life — re-shows are show/hide only (no re-mount; the view's
state persists). A listening view re-fetches on a re-show through the
phase-77 refresh hook (bor:view-refresh), not a re-mount. Chat starts
mounted: app.js owns it and ran at shell boot. */
const mounted = { chat: true };
const nav = document.getElementById("app-nav");
@@ -156,6 +173,12 @@ async function switchTo(name, { userInitiated }) {
const root = viewEls[name];
if (!root) return;
/* Phase 77: capture the mount state BEFORE the mount block — a
re-show of an already-mounted view dispatches bor:view-refresh
(further down); the first show (the mount) and boot never do (the
mount's own load is the first fetch). */
const wasMounted = mounted[name];
/* Mount-once: the lazy module is imported on FIRST show only, then
mounted into the view's section. The guard runs BEFORE the import
(a re-show never re-imports) and is set only after mount resolves
@@ -193,6 +216,16 @@ async function switchTo(name, { userInitiated }) {
current = name;
/* Phase 77: the re-show refresh — the view is already visible and
the head/nav state is written (event order: visible → refresh),
the focus/scroll tail runs after. Gated on the pre-mount capture:
a first show (the mount's own load is the first fetch) and boot
never dispatch. A listening view re-runs its load; the chat view
never listens (phase-76 stream survival). */
if (wasMounted) {
root.dispatchEvent(new CustomEvent("bor:view-refresh"));
}
/* Focus the target view ONLY on user-initiated switches (navbar
click / popstate) — never on initial boot (no focus steal on
load). The top landing mirrors what the old per-view page loads
@@ -218,7 +251,15 @@ if (nav) {
if (!(href in VIEW)) return; // not a same-shell view — real navigation
e.preventDefault();
const name = VIEW[href];
if (name === current) return; // already visible (the menu still closes)
/* Phase 77: a re-click of the ACTIVE view's own link is a
re-fetch, not a no-op — the same refresh event the re-show
dispatches. No pushState (the URL is already this view's path);
the mobile menu still closes (the container handler runs
regardless). */
if (name === current) {
viewEls[name].dispatchEvent(new CustomEvent("bor:view-refresh"));
return;
}
history.pushState({ view: name }, "", href);
switchTo(name, { userInitiated: true });
});