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
+94 -4
View File
@@ -78,6 +78,35 @@
* at boot via ?chat=, and the router never intercepts them (they
* are not navbar links, and their query string keeps them out of
* the VIEW map).
*
* Phase 77 (task 01) — the re-show refresh: the shell router
* dispatches `bor:view-refresh` on the view's section when the user
* RE-SHOWS an already-mounted view (a switch back onto it, a re-click
* of the History nav link, or back/forward) — the first show (mount)
* and boot never (the mount's own load is the first fetch). This
* module listens on root and re-runs `loadChats()`, which is now
* re-entrant: a re-load drops the data rows (the hidden
* #history-empty-row stays in the tbody) before fetching, so the list
* is REPLACED — never duplicated. The listener is armed only in the
* ADMIN branch, after the whoami gate passes: anonymous shows the
* gate and never fetches (the phase-50 contract the story E2E pins).
*
* Phase 77 (task 03) — the explicit refresh control (TODO.md L3:
* "The history page should also have a refresh button."): the
* #history-refresh button in the view's page-head (OUTSIDE the table
* wrap — reachable while the empty state is showing too). Bound only
* in the admin branch; the anonymous branch HIDES it (the gate is
* what anonymous sees — no dead control beside the sign-in gate).
* Lifecycle: click → disable (no double-fire while in flight) →
* `loadChats()` (re-entrant — the list is replaced) → announce the
* outcome in #history-status → re-enable (success AND failure, the
* finally). Success — a 0-row fetch is a success — lands
* `Saved chats refreshed.`; the failure lines now live INSIDE
* loadChats itself (the house copy: `Couldn't load saved chats — is
* the app reachable?` on a network error, `Couldn't load saved chats
* — try again.` on a non-2xx), so EVERY caller of a failed load —
* the mount's first load, a re-show, the button — sees the outcome
* (the §7.4 never-stale contract; a silent empty table is gone).
*/
import { fetchIsAdmin } from "./header.js";
@@ -89,6 +118,9 @@ export async function mount(root) {
const emptyRow = root.querySelector("#history-empty-row");
const gateEl = root.querySelector("#history-gate");
const statusEl = root.querySelector("#history-status");
// Phase 77 (task 03): the explicit refresh control — the page-head
// button (outside the table wrap, so the empty state never hides it).
const refreshBtn = root.querySelector("#history-refresh");
/* Action feedback — the role="status" live region above the table
(the "never stale" contract: every row action lands a line here,
@@ -441,28 +473,60 @@ export async function mount(root) {
}
/* GET /api/chats → render the rows (latest activity first — the
server's order). A 0-row fetch shows the empty-state row. */
server's order). A 0-row fetch shows the empty-state row.
Re-entrant (phase 77 task 01): a re-show re-run must REPLACE the
list, not append a duplicate set — the data rows (every <tr>
EXCEPT the hidden #history-empty-row, which the load itself
re-hides / reveals) are dropped before the fetch.
Phase 77 (task 03): a FAILED load announces its line in the live
region — the house copy (network: "is the app reachable?"; non-2xx:
"try again.") — and the load RETURNS the outcome: true when the
fetch settled (a 0-row fetch is a SUCCESS — the empty state is
the honest view), false on non-2xx / network error, so the
refresh button's handler can land its own success line. */
async function loadChats() {
if (tbody) {
for (const tr of tbody.querySelectorAll("tr")) {
if (tr !== emptyRow) tr.remove();
}
}
if (emptyRow) emptyRow.hidden = true;
let r;
try {
r = await fetch("/api/chats");
} catch {
announce("Couldn't load saved chats — is the app reachable?");
showEmptyState();
return;
return false;
}
if (!r.ok) {
announce("Couldn't load saved chats — try again.");
showEmptyState();
return;
return false;
}
const { chats } = await r.json();
if (!chats.length) {
showEmptyState();
return;
return true;
}
for (const chat of chats) {
tbody.appendChild(makeRow(chat));
}
return true;
}
/* Phase 77 (task 03): the refresh button's in-flight run — the
re-entrant load (the failure line lands inside it) + the success
line + the re-enable (success AND failure — the finally, so a
click can never leave the button stuck disabled). */
async function refreshChats() {
let ok = false;
try {
ok = await loadChats();
} finally {
if (refreshBtn) refreshBtn.disabled = false;
}
if (ok) announce("Saved chats refreshed.");
}
/* ---------- view boot (phase 76 task 03) ----------
@@ -476,8 +540,34 @@ export async function mount(root) {
if (!(await fetchIsAdmin())) {
if (tableWrap) tableWrap.hidden = true;
if (gateEl) gateEl.hidden = false;
if (refreshBtn) refreshBtn.hidden = true; // no dead control beside the gate
return;
}
if (gateEl) gateEl.hidden = true;
if (refreshBtn) refreshBtn.hidden = false; // admin: the control is live
/* Phase 77: a user-initiated re-show of this already-mounted view
makes the router dispatch bor:view-refresh on the section —
re-load then (loadChats is re-entrant, so the list is replaced).
The listener is armed ONLY here, after the whoami gate passed:
anonymous shows the gate and must never fetch (phase 50). `started`
flips true once the first loadChats() is made (the next line), so
the listener can only ever re-run a load the mount already did. */
let started = false;
root.addEventListener("bor:view-refresh", () => {
if (started) loadChats();
});
/* Phase 77 (task 03): the explicit Refresh control (TODO.md L3) —
the button's own lifecycle: click → disable (no double-fire while
the request is in flight) → refreshChats (the re-entrant load +
the outcome line + the re-enable). It is bound HERE, in the admin
branch only: the view is admin-gated, and anonymous never sees
the button (it is hidden above). */
if (refreshBtn) {
refreshBtn.addEventListener("click", () => {
refreshBtn.disabled = true; // no double-fire while in flight
void refreshChats();
});
}
started = true;
loadChats();
}