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
+79 -2
View File
@@ -86,10 +86,13 @@ class LoginRequest(BaseModel):
class WhoamiResponse(BaseModel):
"""``GET /api/whoami`` (phase 16) — drives all UI gating."""
"""``GET /api/whoami`` (phase 16; phase 79 added the ``user`` role)
— drives all UI gating. ``authenticated`` is true for admin AND
user; the UI's admin-only surfaces key off ``role === "admin"``.
"""
authenticated: bool
role: str # "admin" | "anonymous"
role: str # "admin" | "user" | "anonymous"
class SourceRef(BaseModel):
@@ -688,3 +691,77 @@ class DocDraftPushed(BaseModel):
status: Literal["pushed"] = "pushed"
branch: str
commit_sha: str
class TokenCreateRequest(BaseModel):
"""``POST /api/tokens`` body (phase 79, task 02): one named token
to generate and hand out.
``label`` is the hand-out name (e.g. "alice") — display-only, NOT
unique (two tokens may share a label). Trimmed *before* the length
constraints run, so a whitespace-only body is a 422 and a label with
surrounding spaces is stored clean (the ``SteeringNoteIn`` house
``ValueError`` pattern — fail loud at the boundary).
"""
label: str = Field(min_length=1, max_length=120)
@field_validator("label", mode="before")
@classmethod
def _trim_label(cls, v: object) -> object:
return v.strip() if isinstance(v, str) else v
class TokenCreated(BaseModel):
"""``POST /api/tokens`` 201 response (phase 79, task 02).
The ONLY schema in the codebase that carries the plaintext ``token``
— the wire moment it exists exactly once (A4). Every other response
shape (the list row, whoami, …) exposes display fields only: the
stored credential is the hash, and the hash itself is never a wire
field either.
"""
id: uuid.UUID
label: str
token: str
created_at: datetime
class TokenListItem(BaseModel):
"""One row of ``GET /api/tokens`` (phase 79, task 02).
Deliberately secret-free: NO ``token`` field and NO ``token_hash``
field exist on this shape — the list never carries a credential in
either form (A4). ``revoked`` is derived server-side from
``revoked_at is not None`` (the UI renders the Active/Revoked state
from the flag, not the timestamp).
"""
id: uuid.UUID
label: str
created_at: datetime
last_used_at: datetime | None
revoked: bool
class TokenList(BaseModel):
"""``GET /api/tokens`` response: all tokens, newest first
(``created_at desc, id desc``)."""
tokens: list[TokenListItem]
class TokenAuthRequest(BaseModel):
"""``POST /api/token-auth`` body (phase 79, task 03): the plaintext
token a handed-out user presents at the in-app gate.
Deliberately NO min-length validator: an empty/whitespace token is a
MALFORMED login attempt — the endpoint 401s ``invalid token`` (the
phase-16 generic-401 pattern, one message for every failure: no
enumeration). A 422 here would hint at input-shape differences on a
credential endpoint, so the shape is just ``str`` and the endpoint
owns the ``token.strip()`` check.
"""
token: str