phase: 91_admin_theme_tab
Build and Push Containers / build-and-push-app (push) Successful in 5m43s
Build and Push Containers / build-and-push-db (push) Successful in 12s

All verification is complete — this pass needed no code changes. Final report:

**Phase 91 — Admin Theme tab: final verification pass (all 6 tasks already in `complete/`)**

- Verified pre-paint theming end-to-end: `ui_settings` store + resolver, admin `GET/PUT /api/ui-settings`, `CachingMiddleware` inline-`<style id="bor-theme">` injection before `</head>` (incl. `/shared/<token>` prefix branch, unit-pinned), CSP sha256 exemption for the inline tag, Theme tab shell + `theme.js` editor, CSS-file theming fully retired.
- No defects found; zero changes made — working tree left exactly as the task executors left it.
- Tests: `uv run pytest --cov=app` → 1841 passed, 0 failed (TOTAL coverage **99%**; theming/ui_settings/caching all 100%); `uv run pytest tests/e2e/test_admin_theme_tab.py -v --no-cov` → **5 passed** in isolation.
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings.
- Criteria: (1) unset deployment byte-identical, no `#bor-theme` anywhere — ✓ (unit no-op test + E2E reset byte-compare); `rg "BOR_THEME|themes/"` → single hit is the permitted doc-history comment in `frontend/index.html`. (2) admin-only gate + 403s for anonymous and token users — ✓ (E2E test 3). (3) saved theme inline before `</head>` on every page incl. `/shared/<token>`, computed `--brand` on first paint for admin + anonymous — ✓ (E2E test 2 + unit). (4) reset → byte-identical; 5 contrast pairs warn <4.5:1, non-blocking — ✓ (E2E tests 4–5). (5) suite green, >90% coverage, lint clean — ✓. (6) commit deferred to harness per rules.
- Notable: `.agents/PLAN.md` is absent from the repo — the phase overview's Design section was used as the binding spec; no deviation resulted.
- Next pending phase: **none** — 91 is the last phase in `todo/`.
This commit is contained in:
2026-09-09 17:22:24 -04:00
parent 3095c4c577
commit d22d260b8b
74 changed files with 4448 additions and 675 deletions
+54 -18
View File
@@ -9,6 +9,14 @@ Phase 80 note: the suggestions pins are the exception — the chips are
the last 3 questions asked once any are saved, so the env-override
pin (the override is the SEED) needs an empty ``saved_chats``;
the full state matrix lives in ``test_suggestions_api.py``.
Phase 91 (task 01) note: the ``/api/config`` pins are now DB-backed —
the three UI strings are the EFFECTIVE values (the ``ui_settings`` row
over the env values, B1), resolved through a short-lived session, so
the pins take the ``db`` fixture (skip when the stack is down) and
start from an empty ``ui_settings`` table (the env-only-deployment
state; the DB-over-env behaviour itself is pinned in
test_ui_settings_api.py).
"""
from __future__ import annotations
@@ -22,6 +30,14 @@ from app.config import get_settings
from tests.conftest import ADMIN_PASSWORD
def _clear_ui_settings(db: Session) -> None:
"""The env-only-deployment state for the /api/config pins: no
ui_settings row, so the effective strings are the env values
(phase 91, task 01)."""
db.execute(text("DELETE FROM ui_settings"))
db.commit()
def test_health_reports_ok(client) -> None:
r = client.get("/api/health")
assert r.status_code == 200
@@ -31,34 +47,41 @@ def test_health_reports_ok(client) -> None:
assert body["version"]
def test_config_returns_default_app_metadata(client) -> None:
"""GET /api/config is public (anonymous) and returns exactly six
def test_config_returns_default_app_metadata(client, db: Session) -> None:
"""GET /api/config is public (anonymous) and returns exactly five
keys — the phase-39 app metadata, the phase-59 docs flag (inert
false while BOR_DOCS_REPO is empty — the "Save as doc" gating),
and the phase-62 UI customization strings (composer placeholder,
footer line, theme file name)."""
footer line). Phase 91: with an empty ui_settings table the
effective strings are the env defaults (B1 — DB-over-env, the row
absent here); the retired CSS-file theming's ``theme`` key is gone
(task 03 — the five keys are the entire contract)."""
_clear_ui_settings(db)
r = client.get("/api/config")
assert r.status_code == 200
body = r.json()
assert set(body) == {
"app_name", "version", "docs_repo_configured",
"input_placeholder", "footer_text", "theme",
"input_placeholder", "footer_text",
}
assert body["app_name"] == "Brain of Reese"
assert body["version"] == get_settings().app_version
assert body["docs_repo_configured"] is False
# Phase 62: UNSET => the phase-61 neutral copy stands (the
# byte-identical contract); an empty theme = the built-in palette.
# byte-identical contract).
assert body["input_placeholder"] == "Ask me anything…"
assert body["footer_text"] == "Powered by self-hosted models"
assert body["theme"] == ""
def test_config_follows_overridden_app_name(client) -> None:
"""GET /api/config reflects a Settings override (e.g. BOR_APP_NAME)."""
def test_config_follows_overridden_app_name(client, db: Session) -> None:
"""GET /api/config reflects a Settings override (e.g. BOR_APP_NAME).
Phase 91: the override is the ENV side of the DB-over-env resolver —
with an empty ui_settings row the effective app_name is the
overridden env value."""
from app.config import Settings
from app.main import app as fastapi_app
_clear_ui_settings(db)
fastapi_app.dependency_overrides[get_settings] = lambda: Settings(
app_name="Brain of Testy"
)
@@ -68,7 +91,7 @@ def test_config_follows_overridden_app_name(client) -> None:
body = r.json()
assert set(body) == {
"app_name", "version", "docs_repo_configured",
"input_placeholder", "footer_text", "theme",
"input_placeholder", "footer_text",
}
assert body["app_name"] == "Brain of Testy"
assert body["version"] == "0.1.0"
@@ -77,18 +100,21 @@ def test_config_follows_overridden_app_name(client) -> None:
fastapi_app.dependency_overrides.clear()
def test_config_serves_ui_customization_overrides(client) -> None:
"""Phase 62: the three UI customization keys mirror Settings
overrides (``BOR_INPUT_PLACEHOLDER`` / ``BOR_FOOTER_TEXT`` /
``BOR_THEME``) verbatim — the values the frontend brand layer
applies at boot, so this dict is the whole contract."""
def test_config_serves_ui_customization_overrides(client, db: Session) -> None:
"""Phase 62: the UI customization string keys mirror Settings
overrides (``BOR_INPUT_PLACEHOLDER`` / ``BOR_FOOTER_TEXT``) — the
values the frontend brand layer applies at boot, so this dict is
the whole contract. Phase 91: placeholder + footer are the
EFFECTIVE strings — the env overrides win with an empty
ui_settings row (B1); the retired theming's ``theme`` key is gone
(task 03)."""
from app.config import Settings
from app.main import app as fastapi_app
_clear_ui_settings(db)
fastapi_app.dependency_overrides[get_settings] = lambda: Settings(
input_placeholder="Ask the vault…",
footer_text="Powered by my own models",
theme="indigo.css",
)
try:
r = client.get("/api/config")
@@ -96,16 +122,15 @@ def test_config_serves_ui_customization_overrides(client) -> None:
body = r.json()
assert set(body) == {
"app_name", "version", "docs_repo_configured",
"input_placeholder", "footer_text", "theme",
"input_placeholder", "footer_text",
}
assert body["input_placeholder"] == "Ask the vault…"
assert body["footer_text"] == "Powered by my own models"
assert body["theme"] == "indigo.css"
finally:
fastapi_app.dependency_overrides.clear()
def test_config_docs_flag_tracks_settings(client) -> None:
def test_config_docs_flag_tracks_settings(client, db: Session) -> None:
"""Phase 59 (task 05): ``docs_repo_configured`` mirrors
``settings.docs_configured`` — a real bool (never a truthy string)
that flips true the moment BOR_DOCS_REPO is non-empty: that flag is
@@ -113,6 +138,7 @@ def test_config_docs_flag_tracks_settings(client) -> None:
from app.config import Settings
from app.main import app as fastapi_app
_clear_ui_settings(db)
fastapi_app.dependency_overrides[get_settings] = lambda: Settings(
app_name="Brain of Testy",
docs_repo="/srv/docs-repo",
@@ -206,6 +232,10 @@ def test_suggestions_honors_bor_suggestions_env_override(
# shell-body marker (the Tokens view section is inside the
# shell; the per-view title is client-side now).
("/tokens.html", 'id="view-tokens"'), # phase 79: shell route
# Phase 91 (task 04): /theme.html is a SHELL route too — the
# shell-body marker (the Theme view section is inside the
# shell; the per-view title is client-side now).
("/theme.html", 'id="view-theme"'), # phase 91: shell route
("/shared.html", "Shared conversation"), # phase 51: anonymous shared page
],
)
@@ -246,6 +276,7 @@ def test_index_page_no_cache_with_versioned_asset_refs(client) -> None:
["/sources.html", "/document.html", "/login.html", "/tuning.html",
"/git-sources.html", "/history.html", # phase 50: + History (shell route, task 03)
"/tokens.html", # phase 79 task 06: + Tokens (shell route)
"/theme.html", # phase 91 task 04: + Theme (shell route)
"/shared.html"], # phase 51: + the anonymous shared page
)
def test_html_pages_no_cache_with_versioned_refs(client, path: str) -> None:
@@ -271,6 +302,11 @@ def test_html_pages_no_cache_with_versioned_refs(client, path: str) -> None:
# client-side title: the pin asserts the shell never carries
# the per-view title statically (the router writes it).
("/tokens.html", 'id="view-tokens"', "Access tokens · Brain of Reese"), # phase 79 task 06
# phase 91 task 04: the seventh view — there was never a
# standalone theme.html, so "old_title" is the router's
# client-side title: the pin asserts the shell never carries
# the per-view title statically (the router writes it).
("/theme.html", 'id="view-theme"', "Theme · Brain of Reese"), # phase 91 task 04
],
)
def test_shell_routes_serve_the_shell_no_cache_versioned(