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:
@@ -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 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user