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