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
+56 -15
View File
@@ -108,7 +108,10 @@ uv run uvicorn app.main:app --reload
- **Chat** (`/`) — ask questions; answers stream in with **source chips**
that cite the exact documents used. Clicking a chip opens that document
in an **almost-fullscreen modal** on the same page (no new tab).
in an **almost-fullscreen modal** on the same page (no new tab). The
onboarding suggestion chips follow the last 3 questions asked — on a
fresh deployment (no saved questions yet) they seed from
`BOR_SUGGESTIONS`.
- **Document viewer** — the modal above *is* the viewer; the full text of
any indexed document is served from the database (no filesystem access):
markdown is rendered, every other format (`yaml`, `json`, `py`, `txt`, …)
@@ -119,7 +122,8 @@ uv run uvicorn app.main:app --reload
- **Sources** (`/sources.html`) — the indexed document list; the *Path*
column opens each document in the same **almost-fullscreen modal** (no
new tab). **Admin-only** — anonymous visitors see a sign-in gate instead
(the catalog is what the login locks; the document viewer itself stays
(the catalog is what the admin login locks; the document viewer asks
for an access token — see [API tokens](#api-tokens). Shared chats stay
open to everyone).
- **Git sources** (`/git-sources.html`) — the admin-managed source
registry: the git repositories the **Sync sources** button clones and
@@ -214,10 +218,12 @@ unchanged.
## Admin & sign-in
Brain of Reese has exactly **one account: the admin (you)**. Signing in
unlocks the **full Sources catalog** and the **answer-tuning** controls;
everyone else stays anonymous and keeps **chat** and the **document
viewer** (any document an answer cites can be opened by its direct URL —
the catalog is gated, not the viewer).
unlocks the **full Sources catalog**, the **answer-tuning** controls, and
**token management**. **Shared chats** are the only content that stays
open to anonymous visitors; everyone in between gets an **API token** —
the admin generates one in the **Tokens** view and hands it out, and the
holder enters it at the in-app gate to use chat and open the documents
answers cite (see [API tokens](#api-tokens) below).
### Setup (one-time)
@@ -247,8 +253,14 @@ Set the missing variable(s): BOR_ADMIN_PASSWORD, BOR_SESSION_SECRET …
message — no user enumeration, there is only one user).
- `POST /api/logout` → `204` (session cleared and cookie expired;
idempotent for anonymous callers).
- `GET /api/whoami` → `{"authenticated": bool, "role": "admin"|"anonymous"}`
— the single source of truth for every UI gating decision.
- `GET /api/whoami` → `{"authenticated": bool, "role": "admin"|"user"|"anonymous"}`
— the single source of truth for every UI gating decision
(`authenticated` is true for **both** admin and token users; the UI
gates on `role === "admin"`).
- `POST /api/token-auth {"token": "bor_…"}` → `204` + the **same signed
cookie** (role `user`); malformed / unknown / revoked all get one
generic `401` `{"detail": "invalid token"}` — no enumeration. The two
roles coexist in one session; **Sign out** clears both at once.
- Cookie flags: `same_site="lax"`, `https_only` off — **no HTTPS
enforcement on purpose** (homelab HTTP; the cookie is single-admin
convenience, not a cloud boundary). Max age `BOR_SESSION_MAX_AGE`
@@ -258,12 +270,41 @@ Set the missing variable(s): BOR_ADMIN_PASSWORD, BOR_SESSION_SECRET …
### Who can do what
| Capability | Anonymous | Admin (signed in) |
|---|---|---|
| Chat (`/`) + suggestion chips | yes | yes |
| Document viewer (`/document.html?source=…&path=…`) | yes — any indexed doc by direct URL | yes |
| Sources catalog (`/sources.html`, `GET /api/docs`) | sign-in gate | full catalog |
| Tuning (Tune button, Tuning panel, `/api/steering`) | UI hidden | full |
| Capability | Anonymous | Token user | Admin (signed in) |
|---|---|---|---|
| Shared chats (`/shared/<token>`) | yes | yes | yes |
| Chat (`/`) + suggestion chips | token gate | yes | yes |
| Document viewer (`/document.html?source=…&path=…`) | token gate | yes | yes |
| Sources catalog (`/sources.html`, `GET /api/docs`) | sign-in gate | 403 | full catalog |
| Tuning (Tune button, Tuning panel, `/api/steering`) | UI hidden | 403 | full |
| Saved-chat history (`/history.html`) | sign-in gate | no History view | full |
### API tokens
The admin can hand out access without sharing the admin password.
- **Generate:** sign in as admin → **Tokens** in the navbar
(`/tokens.html`). Type a label (e.g. `alice`) and hit **Generate** — a
`bor_` + 32-hex token appears in the *shown once* block. Copy it now:
only its SHA-256 hash is stored, so the plaintext can never be
retrieved again.
- **Use:** the holder opens the app (or a direct document URL) and gets
the **token gate** instead of content — they enter the token, and the
browser caches it in `localStorage["bor.token"]`, silently re-sending
it on every page load (no re-entry on reloads or new tabs). **Sign
out** clears the cache; a failed silent re-auth (e.g. a revoked token)
drops the cached copy and shows the gate again. Private-mode browsers
still work — the gate just can't cache.
- **Scope:** a token user can chat (with suggestion chips) and open the
documents answers cite — nothing else. The Sources catalog, git
sources, tuning, document drafts, and the saved-chat history stay
admin-only (`403` on their APIs; no History view). **Shared chats
stay open to everyone** — the only anonymous content.
- **Revoke:** **Revoke** on the token's row (inline two-step confirm).
Revocation is **immediate**: the token's next request — including a
fresh login attempt — is refused with the same generic `401` as a bad
token. Revoked tokens stay in the list, marked **Revoked**, with their
last used time.
The public API endpoints stay stateless — the signed cookie is the only
session state in the system.
@@ -779,7 +820,7 @@ served locally (no CDN), `BOR_ENVIRONMENT=production`.
| `BOR_SUMMARY_MAX_CHARS` | `12000` | cap on document content sent to the `lite` summary model at import (see *Document summaries*) |
| `BOR_KB_OVERVIEW_MAX_CHARS` | `4000` | char budget for the `<knowledge_base>` (KB overview) prompt section |
| `BOR_OVERVIEW_INPUT_MAX_CHARS` | `40000` | cap on the document list sent to the `lite` model when generating the KB overview |
| `BOR_SUGGESTIONS` | built-in list | JSON list of onboarding chips |
| `BOR_SUGGESTIONS` | built-in list | JSON seed for the onboarding chips — shown only before the first saved question; afterwards the chips are the last 3 questions asked (phase 80) |
| `BOR_ADMIN_PASSWORD` | *(required)* | the single admin's password (plaintext, `.env`); app refuses to start when empty |
| `BOR_SESSION_SECRET` | *(required)* | signing key for the `bor_session` cookie; `python -c 'import secrets;print(secrets.token_hex(32))'` |
| `BOR_SESSION_MAX_AGE` | `43200` | session-cookie lifetime in seconds (12 h, sliding) |