phase: 91_admin_theme_tab
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:
@@ -57,6 +57,12 @@ Data model — see ``.agents/PLAN.md`` §Data Model:
|
||||
on ``POST /api/token-auth`` (task 03 — the only
|
||||
request that presents the token; the in-app gate
|
||||
re-sends the cached token on every page load).
|
||||
* ``ui_settings`` — single-row UI settings (phase 91): the admin
|
||||
Theme tab's app name, input placeholder, footer
|
||||
text and the 8 identity colors, one row
|
||||
(``id = 1``); every column NULL = "use the
|
||||
default" (env value for the strings, the built-in
|
||||
palette for the colors — task 01).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -384,3 +390,40 @@ class ApiToken(Base):
|
||||
#: (enforced immediately on the holder's next request); NULL while
|
||||
#: active.
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
class UiSettings(Base):
|
||||
"""Single-row UI settings (phase 91, task 01).
|
||||
|
||||
The admin Theme tab (``/theme.html``, tasks 04/05) persists everything
|
||||
the ``BOR_`` env vars and the retired custom-CSS theming supported in
|
||||
ONE row (``id = 1`` — the single row is always id 1; ``GET`` creates
|
||||
nothing, ``PUT`` upserts). The NULL = default rule (B1, owner-locked
|
||||
2026-09-09): every column is nullable, and a NULL (or empty) column
|
||||
means "use the default" — the env value for the three strings
|
||||
(``settings.app_name`` etc.), the built-in palette
|
||||
(:data:`app.core.theming.BUILTIN_COLORS`) for the eight identity
|
||||
colors (B1: no env fallback for colors). :func:`app.core.theming.
|
||||
effective_settings` resolves the effective 11 values both the
|
||||
``GET /api/ui-settings`` and ``GET /api/config`` endpoints serve.
|
||||
"""
|
||||
|
||||
__tablename__ = "ui_settings"
|
||||
|
||||
#: The single row is always id 1 (the ``kb_overview`` / ``sources_meta``
|
||||
#: id=1 precedent — Python-side default; the migration carries no
|
||||
#: server default because the row is created only by the PUT upsert).
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
|
||||
# --- Strings (NULL/empty = "use the env default" — B1) ---
|
||||
app_name: Mapped[str | None] = mapped_column(String(300), nullable=True)
|
||||
input_placeholder: Mapped[str | None] = mapped_column(String(300), nullable=True)
|
||||
footer_text: Mapped[str | None] = mapped_column(String(300), nullable=True)
|
||||
# --- The 8 identity colors (NULL = the built-in — B1), #rrggbb ---
|
||||
bg: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
surface: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
ink: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
ink_soft: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
line: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
brand: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
brand_soft: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
brand_ink: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
|
||||
Reference in New Issue
Block a user