phase: 92_theme_save_and_coverage
**Phase 92 final verification pass — all green.** This pass re-verified the completed tasks (all 5 task files already in `complete/`) against every completion criterion; no defects found, nothing to fix. - Verified: 9th identity var `grid_line` end-to-end (migration `0015` at head, model/`theming.py`/schemas/API, 422 + built-in→NULL tests present); `styles.css` zero hardcoded literals outside `:root` + derived `--brand-*` vars; 9th picker in theme form; wordmark themed; `theme.js` save/reset/re-show/mount live-sync; dedicated E2E suite + phase-91 suite updated. - `uv run pytest --cov=app --cov-report=term-missing` → **1845 passed, exit 0, TOTAL 99%** (>90%) - `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings - `uv run pytest tests/e2e/test_theme_save_and_coverage.py -v --no-cov` → **3 passed** (save-live, reset-live, whole-site) - `uv run pytest tests/e2e/test_admin_theme_tab.py -v --no-cov` → **5 passed** - Criteria: (1) Save/Reset repaint open page, no nav, SPA-nav survives, pre-paint intact ✅; (2) both `rg` gates green (only `:root` + documented `#fff` Stop label; zero SVG hex attrs), grid/selection/hovers/wash/wordmark E2E-proven ✅; (3) no-op contract live-checked: row-less `/` = no tag + exact A1 CSP, grid-only row = 9-var tag in `COLOR_FIELDS` order + sha256 CSP, with-row ≡ row-less bytes ✅; (4) full suite/coverage/lint/both E2E ✅; (5) commit left to the harness per instructions. - Deviations (previously made, probe-verified, kept): live repaint uses CSSOM `<html>` overrides because Chromium blocks `<style>` textContent mutations under the locked sha256-only CSP (tag text still mirrors the next load; `<html>` style exact-saved after Save, empty after Reset); wordmark themed via 3 `.brand-mark` CSS rules instead of inline styles (task 03's inline attrs were CSP-blocked — fixed during task 04). - Next pending phase: none — `todo/` contains only `92_theme_save_and_coverage`.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
The persistence surface of the admin Theme tab (the tab itself lands in
|
||||
tasks 04/05): the single ``ui_settings`` row (id 1) that stores what the
|
||||
admin sets — the app name, input placeholder, footer text, and the 8
|
||||
admin sets — the app name, input placeholder, footer text, and the 9
|
||||
identity colors. The whole router sits behind
|
||||
:func:`app.core.auth.require_admin` (router-wide ``dependencies`` — the
|
||||
:mod:`app.api.tokens` pattern): anonymous callers AND token users get
|
||||
@@ -73,7 +73,7 @@ def _validate_strings(payload: UiSettingsIn) -> dict[str, str | None]:
|
||||
|
||||
|
||||
def _validate_colors(payload: UiSettingsIn) -> dict[str, str | None]:
|
||||
"""Validate + normalize the 8 identity colors: strict ``#rrggbb``
|
||||
"""Validate + normalize the 9 identity colors: strict ``#rrggbb``
|
||||
(else 422 naming the field), lowercased on store, and a value equal
|
||||
to its BUILT-IN is stored as ``None`` — the owner-locked
|
||||
normalization that keeps "save the defaults" byte-identical (the
|
||||
@@ -113,7 +113,7 @@ def update_ui_settings(
|
||||
settings: Settings = Depends(get_settings), # noqa: B008
|
||||
db: Session = Depends(get_db), # noqa: B008
|
||||
) -> UiSettingsOut:
|
||||
"""Replace the single row with the body's 11 values (validated and
|
||||
"""Replace the single row with the body's 12 values (validated and
|
||||
normalized — see the module docstring), then report the new
|
||||
effective values.
|
||||
|
||||
|
||||
+10
-7
@@ -9,12 +9,12 @@ deleted the env var, the example-stylesheet directory, and the link
|
||||
insertion, and the admin Theme tab is now the only theming surface.
|
||||
The contract that directory's authoring guide carried is re-homed here
|
||||
(built-in table, the five contrast pairs, the never-white-on-brand
|
||||
trap — see below), and the 8 variables + built-in values are the
|
||||
trap — see below), and the 9 variables + built-in values are the
|
||||
authoritative table (the unit drift test parses
|
||||
``frontend/assets/styles.css``'s ``:root`` and asserts equality, so
|
||||
the two can never silently diverge).
|
||||
|
||||
The **8 identity variables** (bare names, README order) and their
|
||||
The **9 identity variables** (bare names, README order) and their
|
||||
built-in values (from ``frontend/assets/styles.css`` ``:root``):
|
||||
|
||||
=================== ========== =================================================
|
||||
@@ -25,6 +25,8 @@ Variable Built-in Role
|
||||
``ink`` ``#f0e6e6`` primary text
|
||||
``ink_soft`` ``#b8a8a8`` secondary text (5.1:1 on ``surface``)
|
||||
``line`` ``#2d1a1a`` decorative 1px borders (no contrast duty)
|
||||
``grid_line`` ``#4a2626`` background grid texture — decorative,
|
||||
no contrast duty, like ``line``
|
||||
``brand`` ``#f43f5e`` brand accent — buttons, links (text ON
|
||||
it is the DARK ``bg`` ink)
|
||||
``brand_soft`` ``#2d0a0a`` brand-tinted surface (chips, hover washes)
|
||||
@@ -66,7 +68,7 @@ from sqlalchemy.orm import Session
|
||||
from app.config import Settings, get_settings
|
||||
from app.models import UiSettings
|
||||
|
||||
#: The 8 built-in identity colors, keyed by BARE variable name (no ``--``)
|
||||
#: The 9 built-in identity colors, keyed by BARE variable name (no ``--``)
|
||||
#: in the themes-README order. Copied from ``frontend/assets/styles.css``
|
||||
#: ``:root`` — the unit drift test (``tests/unit/test_theming.py``)
|
||||
#: re-parses the stylesheet and asserts equality on every run.
|
||||
@@ -76,12 +78,13 @@ BUILTIN_COLORS: dict[str, str] = {
|
||||
"ink": "#f0e6e6",
|
||||
"ink_soft": "#b8a8a8",
|
||||
"line": "#2d1a1a",
|
||||
"grid_line": "#4a2626", # the background grid texture (phase 92)
|
||||
"brand": "#f43f5e",
|
||||
"brand_soft": "#2d0a0a",
|
||||
"brand_ink": "#fca5a5",
|
||||
}
|
||||
|
||||
#: The 8 color field names in the README's order (dicts preserve
|
||||
#: The 9 color field names in the README's order (dicts preserve
|
||||
#: insertion order) — used by the resolver, the API, and the
|
||||
#: ``theme_style_tag`` renderer (task 02).
|
||||
COLOR_FIELDS: tuple[str, ...] = tuple(BUILTIN_COLORS)
|
||||
@@ -102,7 +105,7 @@ def effective_settings(
|
||||
* **strings** (``app_name`` / ``input_placeholder`` / ``footer_text``)
|
||||
— the DB value when it is a non-empty string, else the env value
|
||||
(``settings.app_name`` etc. — B1: the env vars stay the fallback);
|
||||
* **colors** (the 8 :data:`COLOR_FIELDS`) — the DB value when not
|
||||
* **colors** (the 9 :data:`COLOR_FIELDS`) — the DB value when not
|
||||
``None``, else :data:`BUILTIN_COLORS` (B1: no env fallback for
|
||||
colors — the built-in palette IS the default).
|
||||
|
||||
@@ -110,7 +113,7 @@ def effective_settings(
|
||||
strings + the built-in palette. The ``settings`` parameter names the
|
||||
env-fallback source explicitly (the routes pass their
|
||||
dependency-injected instance so test overrides apply); ``None`` uses
|
||||
the cached :func:`app.config.get_settings`. Returns all 11 keys.
|
||||
the cached :func:`app.config.get_settings`. Returns all 12 keys.
|
||||
"""
|
||||
if settings is None:
|
||||
settings = get_settings()
|
||||
@@ -131,7 +134,7 @@ def theme_style_tag(colors: dict[str, str]) -> str:
|
||||
``""`` when every color equals its built-in — the byte-identical
|
||||
contract: an unset (or "defaults saved") deployment must serve
|
||||
exactly the pre-phase-91 HTML, no ``<style>`` tag anywhere.
|
||||
Otherwise one ``<style id="bor-theme">`` tag with ALL 8 variables in
|
||||
Otherwise one ``<style id="bor-theme">`` tag with ALL 9 variables in
|
||||
:data:`COLOR_FIELDS` order (the non-overridden ones repeat their
|
||||
built-in value — the tag is a complete ``:root`` override, so the
|
||||
page never mixes partial palettes)::
|
||||
|
||||
+5
-4
@@ -59,7 +59,7 @@ Data model — see ``.agents/PLAN.md`` §Data Model:
|
||||
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
|
||||
text and the 9 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).
|
||||
@@ -402,9 +402,9 @@ class UiSettings(Base):
|
||||
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
|
||||
(:data:`app.core.theming.BUILTIN_COLORS`) for the nine identity
|
||||
colors (B1: no env fallback for colors). :func:`app.core.theming.
|
||||
effective_settings` resolves the effective 11 values both the
|
||||
effective_settings` resolves the effective 12 values both the
|
||||
``GET /api/ui-settings`` and ``GET /api/config`` endpoints serve.
|
||||
"""
|
||||
|
||||
@@ -418,12 +418,13 @@ class UiSettings(Base):
|
||||
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 ---
|
||||
# --- The 9 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)
|
||||
grid_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)
|
||||
|
||||
+3
-1
@@ -839,6 +839,7 @@ class UiSettingsIn(BaseModel):
|
||||
ink: str | None = None
|
||||
ink_soft: str | None = None
|
||||
line: str | None = None
|
||||
grid_line: str | None = None
|
||||
brand: str | None = None
|
||||
brand_soft: str | None = None
|
||||
brand_ink: str | None = None
|
||||
@@ -848,7 +849,7 @@ class UiSettingsOut(BaseModel):
|
||||
"""Effective UI settings (``GET``/``PUT /api/ui-settings`` response,
|
||||
phase 91, task 01).
|
||||
|
||||
All 11 values, all non-null strings: the resolver's
|
||||
All 12 values, all non-null strings: the resolver's
|
||||
DB-over-env / DB-over-built-in merge (B1), so the tab always shows
|
||||
the LIVE theme — a fresh (row-missing) deployment reports the env
|
||||
strings and the built-in palette.
|
||||
@@ -862,6 +863,7 @@ class UiSettingsOut(BaseModel):
|
||||
ink: str
|
||||
ink_soft: str
|
||||
line: str
|
||||
grid_line: str
|
||||
brand: str
|
||||
brand_soft: str
|
||||
brand_ink: str
|
||||
|
||||
Reference in New Issue
Block a user