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:
+41
-5
@@ -265,13 +265,15 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
fetchIsAdmin,
|
||||
fetchWhoami,
|
||||
initSharedHeader,
|
||||
refreshSteering,
|
||||
announceSteering,
|
||||
} from "./header.js";
|
||||
import { openDocumentModal } from "./document-modal.js"; // phase 26: chips open the same-page modal
|
||||
import { mountGate } from "./token-gate.js"; // phase 79 (task 05): the in-app token gate
|
||||
|
||||
/* Phase 77 (task 02): the bor:view-refresh exclusion is deliberate — the in-flight SSE stream and the local conversation must survive every switch (phase 76), so the chat view never listens and never re-fetches on a show. */
|
||||
const messagesEl = document.querySelector("#messages");
|
||||
const emptyState = document.querySelector("#empty-state");
|
||||
const suggestionsEl = document.querySelector("#suggestions");
|
||||
@@ -1830,6 +1832,11 @@ function rememberBrainTurn(rawText, meta, replaceIndex = -1) {
|
||||
const signInLink = document.querySelector("#sign-in-link");
|
||||
const signOutBtn = document.querySelector("#sign-out-btn");
|
||||
let isAdmin = false;
|
||||
// Phase 79 (task 05): the authenticated role — admin OR token user.
|
||||
// The auth PAIR keys off it (both get Sign out, neither sees Sign
|
||||
// in); the admin-ONLY surfaces (Tune, Save as doc, ?chat= boot load)
|
||||
// still key off isAdmin alone.
|
||||
let signedIn = false;
|
||||
|
||||
/* Phase 59 (owner-locked 2026-08-31, TODO.md L3): the docs-push gate
|
||||
* — GET /api/config's ``docs_repo_configured`` (settings.docs_configured
|
||||
@@ -1842,8 +1849,12 @@ let isAdmin = false;
|
||||
let docsRepoConfigured = false;
|
||||
|
||||
function applyAuthState() {
|
||||
if (signInLink) signInLink.hidden = isAdmin;
|
||||
if (signOutBtn) signOutBtn.hidden = !isAdmin;
|
||||
// Phase 79 (task 05): the pair keys off the authenticated role — a
|
||||
// token user (isAdmin false, signedIn true) gets Sign out like the
|
||||
// admin and no Sign in link (idempotent with header.js's own
|
||||
// toggling, which does the same from the shared whoami).
|
||||
if (signInLink) signInLink.hidden = signedIn;
|
||||
if (signOutBtn) signOutBtn.hidden = !signedIn;
|
||||
}
|
||||
|
||||
function startNewChat() {
|
||||
@@ -1855,6 +1866,14 @@ function startNewChat() {
|
||||
removeTyping();
|
||||
messagesEl.querySelectorAll(".msg").forEach((el) => el.remove());
|
||||
if (emptyState) emptyState.hidden = false;
|
||||
// Phase 80 (task 03): the empty state is BACK — the onboarding row was
|
||||
// fetched once at boot, and while the user was chatting the last-3
|
||||
// state moved on. Refetch through the existing progressive-enhancement
|
||||
// path (fetches /api/suggestions, re-renders #suggestions in place,
|
||||
// swallows its own failures — a 401 for an anonymous visitor or a
|
||||
// network drop just leaves the row as-is, no error spam). The
|
||||
// in-flight-turn guard above means this only runs for a real new chat.
|
||||
loadSuggestions();
|
||||
clearErrorBanner();
|
||||
setUiState(UI_STATE.idle);
|
||||
input.value = "";
|
||||
@@ -2337,10 +2356,27 @@ window.addEventListener("pagehide", () => {
|
||||
the anonymous removal of the tuning surface both happen inside
|
||||
initSharedHeader() now. Phase 55 (A2): the local restore hydrates
|
||||
currentChatId from the record (restoreConversation), so the row link
|
||||
survives a plain reload — no Save pill to reveal anymore. */
|
||||
survives a plain reload — no Save pill to reveal anymore.
|
||||
Phase 79 (task 05): the token gate (mountGate) settles BEFORE the
|
||||
header boots — a cached token's silent re-auth lands before the
|
||||
first whoami fires, and the header + the chat-page gating read the
|
||||
post-auth role (the auth pair off `authenticated`, the admin-only
|
||||
surfaces off role === "admin"). */
|
||||
(async () => {
|
||||
// Phase 79 (task 05): the token gate settles FIRST — a cached
|
||||
// bor.token is re-sent to /api/token-auth (silently) BEFORE the
|
||||
// first whoami fires, so initSharedHeader below sees the
|
||||
// POST-re-auth role deterministically (no stale "Sign in" for a
|
||||
// returning token user; the gate and the header share the cached
|
||||
// whoami promise — still exactly one /api/whoami per page load).
|
||||
// onAuthed is a no-op in the shell: the lazy views mount on first
|
||||
// show exactly as today (mount-once, hide-forever untouched), and
|
||||
// the already-mounted views keep their state.
|
||||
await mountGate(document.getElementById("main"), () => {});
|
||||
await initSharedHeader(); // header.js: whoami + Sign in/out + steering gate
|
||||
isAdmin = await fetchIsAdmin(); // the same cached promise — one whoami
|
||||
const who = await fetchWhoami(); // the same cached promise — one whoami
|
||||
isAdmin = who.role === "admin"; // phase 79: admin-only surfaces key off role
|
||||
signedIn = who.authenticated; // the auth pair keys off the authenticated role
|
||||
// Phase 59: /api/config is settled BEFORE any bubble renders —
|
||||
// brand.js's single boot fetch (window.BOR_CONFIG_PROMISE, never
|
||||
// rejecting) has set window.BOR_DOCS_REPO_CONFIGURED (false until
|
||||
|
||||
Reference in New Issue
Block a user