phase: 93_theme_semantic_completion
All verification is complete. Final report: **Phase 93 — Theme semantic completion: FINAL VERIFICATION PASS — ALL GREEN** - Verified full implementation in tree: migration `0016` (8 nullable semantic columns, applied at head), 17-var `BUILTIN_COLORS`/`COLOR_FIELDS`/`effective_settings`, API validation, `#view-theme` State-colors fieldset (17 pickers), `theme.js` FIELDS/PAIRS (5→8), `.page-head` surface panel (6 shell views + doc-edit + shared.html; login card / document sticky header audited as already-surfaced), mock_llm `content: None` fix - Fixed 2 pre-existing defects (both fail identically on baseline `d4f38ad`, proven via worktree A/B): `test_nav_rename_sources` — expected nav tail missing the phase-91 "Theme" link; `test_stale_ui_copy` — now truncates `saved_chats` before/after (house `test_suggestion_chips` pattern) so the seed-chip contract is deterministic on the shared dev DB (owner's 22 saved chats triggered phase-80 last-3-questions) - Tests: `uv run pytest --cov=app --cov-report=term-missing` → **1868 passed, app/ 99%** (>90% ✓); `uv run ruff check .` → clean; `uv run pyright` → **0 errors** - E2E: dedicated `uv run pytest tests/e2e/test_theme_semantic_completion.py -v --no-cov` → **8/8 in isolation** (all-gray 17-color theme: zero residual color on saved-result/Stale/Revoked/Local/tool-call elements, text labels intact, gray heads non-transparent, pre-paint tag, Reset → byte-identical no-tag); 15 theme/header/nav/responsive suites green in isolation; full 85-file combined run: only the 2 fixed pre-existing failures + 1 combined-run artifact (`test_sync_upload_progress`, green in isolation) - Completion criteria: (1) monochrome E2E ✓ (2) default byte-identical, no `#bor-theme` tag ✓ (3) all page heads on solid surface ✓ (4) suite/coverage/lint/E2E green ✓ (5) phases 01–92 no behavior change ✓ (6) commit left to harness per protocol - Notable: cleaned stray uvicorn leftovers from prior implementation pass (owner's `--reload` dev server untouched); no deviations from the phase design - Next pending phase: `94_ls_tree_drilldown`
This commit is contained in:
+11
-8
@@ -2,8 +2,9 @@
|
||||
|
||||
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 9
|
||||
identity colors. The whole router sits behind
|
||||
admin sets — the app name, input placeholder, footer text, and the 17
|
||||
palette colors (the 9 identity colors + the 8 semantic state colors,
|
||||
phase 93 — B3 revised). 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
|
||||
403 on every route (only the admin themes the deployment).
|
||||
@@ -73,11 +74,13 @@ def _validate_strings(payload: UiSettingsIn) -> dict[str, str | None]:
|
||||
|
||||
|
||||
def _validate_colors(payload: UiSettingsIn) -> dict[str, str | None]:
|
||||
"""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
|
||||
row stays empty, the no-op injection contract)."""
|
||||
"""Validate + normalize the 17 palette colors (the 9 identity +
|
||||
the 8 semantic state — all ``COLOR_FIELDS``-driven, phase 93):
|
||||
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 row stays empty, the no-op injection
|
||||
contract)."""
|
||||
values: dict[str, str | None] = {}
|
||||
for field in theming.COLOR_FIELDS:
|
||||
raw = getattr(payload, field)
|
||||
@@ -113,7 +116,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 12 values (validated and
|
||||
"""Replace the single row with the body's 20 values (validated and
|
||||
normalized — see the module docstring), then report the new
|
||||
effective values.
|
||||
|
||||
|
||||
+80
-30
@@ -33,21 +33,54 @@ Variable Built-in Role
|
||||
``brand_ink`` ``#fca5a5`` brand-tinted text (9.0:1 on ``surface``)
|
||||
=================== ========== =================================================
|
||||
|
||||
The **semantic families are deliberately NOT identity** (B3,
|
||||
owner-locked 2026-09-09): ``--accent-*`` (deflection amber), ``--ok-*``
|
||||
(success green), ``--err-*`` (error red) encode *states*, are already AA
|
||||
in the built-in theme, and are not configurable from the tab — a theme
|
||||
that keeps them stays honest.
|
||||
**The semantic families** — STORABLE since phase 93 (B3 REVISED, owner
|
||||
permission 2026-09-10, TODO.md L3 — the 2026-09-09 lock that they are
|
||||
"NOT identity, not configurable from the tab" is lifted; PLAN.md is
|
||||
being redone by the owner, the decision is recorded in the phase 93
|
||||
overview): the three state families ``--ok-*`` (success green),
|
||||
``--err-*`` (error red), ``--accent-*`` (deflection / tool-line amber)
|
||||
join the storable palette — 17 variables total, all ``NULL = built-in``
|
||||
(B1, no env fallback for colors). They still encode *states* (a
|
||||
monochrome theme conveys state by its TEXT label — "text + color, never
|
||||
color alone", B5 — so grayscale stays honest), and the built-in values
|
||||
keep every existing AA ratio. Column names mirror the CSS variables
|
||||
(``frontend/assets/styles.css`` ``:root``):
|
||||
|
||||
**The five contrast pairs** that must meet WCAG 2.1 AA (>= 4.5:1,
|
||||
AGENTS.md rule 5) — the pairs the layout actually pairs: ``ink`` on
|
||||
``bg``, ``ink`` on ``surface``, ``ink_soft`` on ``surface``, ``bg`` on
|
||||
``brand`` (the text on brand buttons is the DARK background ink —
|
||||
that is the pattern; never white on brand: white on the built-in
|
||||
``#f43f5e`` is 3.7:1, it fails), and ``brand_ink`` on ``surface``. The
|
||||
tab's client-side warnings (task 05) compute exactly these five ratios
|
||||
against the values being saved; the built-in palette itself passes, so
|
||||
the default deployment stays AA without any warning.
|
||||
=================== ========== =================================================
|
||||
Variable Built-in Role
|
||||
=================== ========== =================================================
|
||||
``ok_bg`` ``#10241b`` success pill/text background ("Theme saved.")
|
||||
``ok_ink`` ``#6ee7a8`` success text (10.6:1 on ``ok_bg``)
|
||||
``err_bg`` ``#2d0a0a`` Stale/Revoked pill background
|
||||
``err_ink`` ``#fca5a5`` Stale/Revoked pill text (9.3:1 on ``err_bg``)
|
||||
``err_line`` ``#ef4444`` pill border — decorative (no contrast duty,
|
||||
like ``line`` / ``grid_line``)
|
||||
``accent_bg`` ``#2b2110`` deflection banner / tool-line background
|
||||
``accent_ink`` ``#fbbf24`` tool-call line text (9.5:1 on ``accent_bg``)
|
||||
``accent_line`` ``#f59e0b`` tool-line / deflection border — decorative
|
||||
(no contrast duty, like ``err_line``)
|
||||
=================== ========== =================================================
|
||||
|
||||
**The eight contrast pairs** that must meet WCAG 2.1 AA (>= 4.5:1,
|
||||
AGENTS.md rule 5) — the pairs the layout actually pairs: the five
|
||||
identity pairs (``ink`` on ``bg``, ``ink`` on ``surface``, ``ink_soft``
|
||||
on ``surface``, ``bg`` on ``brand`` (the text on brand buttons is the
|
||||
DARK background ink — that is the pattern; never white on brand: white
|
||||
on the built-in ``#f43f5e`` is 3.7:1, it fails), and ``brand_ink`` on
|
||||
``surface``) plus the three semantic ink-on-bg pairs (``ok_ink`` on
|
||||
``ok_bg``, ``err_ink`` on ``err_bg``, ``accent_ink`` on ``accent_bg``).
|
||||
The two ``*_line`` state variables stay EXCLUDED — decorative borders,
|
||||
no contrast duty (the same rule as ``--line`` / ``--grid-line``). The
|
||||
tab's client-side warnings (phase 91 task 05, extended to the eight
|
||||
pairs in phase 93 task 02) compute exactly these eight ratios against
|
||||
the values being saved; the built-in palette itself passes, so the
|
||||
default deployment stays AA without any warning.
|
||||
|
||||
**Mirror relationship:** ``frontend/assets/theme.js``'s ``PAIRS``
|
||||
array is the client-side mirror of this table — the two must NEVER
|
||||
diverge (``tests/unit/test_theming.py`` pins the mirror: every
|
||||
``PAIRS`` entry appears here as ``fg`` on ``bg``, exactly eight
|
||||
pairs).
|
||||
|
||||
Effective-value resolution (:func:`effective_settings`) — the DB-over-
|
||||
env / DB-over-built-in merge (B1, owner-locked 2026-09-09): the single
|
||||
@@ -68,10 +101,12 @@ from sqlalchemy.orm import Session
|
||||
from app.config import Settings, get_settings
|
||||
from app.models import UiSettings
|
||||
|
||||
#: 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.
|
||||
#: The 17 built-in palette colors, keyed by BARE variable name (no
|
||||
#: ``--``): the 9 identity colors in the themes-README order, then the
|
||||
#: 8 semantic state colors (phase 93 — B3 revised). 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.
|
||||
BUILTIN_COLORS: dict[str, str] = {
|
||||
"bg": "#0f0a0a",
|
||||
"surface": "#1a0f0f",
|
||||
@@ -82,11 +117,23 @@ BUILTIN_COLORS: dict[str, str] = {
|
||||
"brand": "#f43f5e",
|
||||
"brand_soft": "#2d0a0a",
|
||||
"brand_ink": "#fca5a5",
|
||||
# The 8 semantic state colors (phase 93 — the ok / err / accent
|
||||
# families; the *_line entries are decorative borders, no contrast
|
||||
# duty, like line / grid_line).
|
||||
"ok_bg": "#10241b",
|
||||
"ok_ink": "#6ee7a8",
|
||||
"err_bg": "#2d0a0a",
|
||||
"err_ink": "#fca5a5",
|
||||
"err_line": "#ef4444",
|
||||
"accent_bg": "#2b2110",
|
||||
"accent_ink": "#fbbf24",
|
||||
"accent_line": "#f59e0b",
|
||||
}
|
||||
|
||||
#: 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).
|
||||
#: The 17 color field names (dicts preserve insertion order) — the 9
|
||||
#: identity variables in the README's order, then the 8 semantic state
|
||||
#: variables (identity, brand, then state). Used by the resolver, the
|
||||
#: API, and the ``theme_style_tag`` renderer (task 02).
|
||||
COLOR_FIELDS: tuple[str, ...] = tuple(BUILTIN_COLORS)
|
||||
|
||||
#: The 3 display strings the ``ui_settings`` row carries — env fallback
|
||||
@@ -105,15 +152,17 @@ 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 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).
|
||||
* **colors** (the 17 :data:`COLOR_FIELDS` — 9 identity + 8
|
||||
semantic state) — the DB value when not ``None``, else
|
||||
:data:`BUILTIN_COLORS` (B1: no env fallback for colors — the
|
||||
built-in palette IS the default).
|
||||
|
||||
A missing row (``GET`` creates nothing) means "defaults" — the env
|
||||
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 12 keys.
|
||||
the cached :func:`app.config.get_settings`. Returns all 20 values
|
||||
(3 strings + 17 colors).
|
||||
"""
|
||||
if settings is None:
|
||||
settings = get_settings()
|
||||
@@ -134,12 +183,13 @@ 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 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)::
|
||||
Otherwise one ``<style id="bor-theme">`` tag with ALL 17 variables in
|
||||
:data:`COLOR_FIELDS` order (the 9 identity + the 8 semantic state
|
||||
variables — the non-overridden ones repeat their built-in value, so
|
||||
the tag is a complete ``:root`` override and the page never mixes
|
||||
partial palettes)::
|
||||
|
||||
<style id="bor-theme">:root{--bg:#0f0a0a;…;--brand-ink:#fca5a5}</style>
|
||||
<style id="bor-theme">:root{--bg:#0f0a0a;…;--accent-line:#f59e0b}</style>
|
||||
|
||||
Pure function of its input — :func:`inject_theme` places it before
|
||||
the first ``</head>`` of every served HTML page (the phase-91
|
||||
|
||||
+22
-4
@@ -402,10 +402,15 @@ 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 nine identity
|
||||
colors (B1: no env fallback for colors). :func:`app.core.theming.
|
||||
effective_settings` resolves the effective 12 values both the
|
||||
``GET /api/ui-settings`` and ``GET /api/config`` endpoints serve.
|
||||
(:data:`app.core.theming.BUILTIN_COLORS`) for the 17 palette colors
|
||||
(B1: no env fallback for colors) — the 9 identity colors AND the
|
||||
8 semantic state colors (phase 93, task 01; B3 REVISED, owner
|
||||
permission 2026-09-10, TODO.md L3 — the ``--ok-*`` / ``--err-*`` /
|
||||
``--accent-*`` families join the storable palette; PLAN.md is being
|
||||
redone by the owner, the decision is recorded in the phase 93
|
||||
overview). :func:`app.core.theming.effective_settings` resolves the
|
||||
effective 20 values both the ``GET /api/ui-settings`` and
|
||||
``GET /api/config`` endpoints serve.
|
||||
"""
|
||||
|
||||
__tablename__ = "ui_settings"
|
||||
@@ -428,3 +433,16 @@ class UiSettings(Base):
|
||||
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)
|
||||
# --- The 8 semantic state colors (phase 93, B3 revised; NULL = the
|
||||
# built-in — B1), #rrggbb. Column names mirror the CSS variable
|
||||
# names (``--ok-bg`` etc. in ``frontend/assets/styles.css``
|
||||
# :root — the drift test in tests/unit/test_theming.py keeps
|
||||
# the two in lockstep). ---
|
||||
ok_bg: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
ok_ink: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
err_bg: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
err_ink: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
err_line: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
accent_bg: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
accent_ink: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
accent_line: Mapped[str | None] = mapped_column(String(7), nullable=True)
|
||||
|
||||
+24
-3
@@ -843,16 +843,29 @@ class UiSettingsIn(BaseModel):
|
||||
brand: str | None = None
|
||||
brand_soft: str | None = None
|
||||
brand_ink: str | None = None
|
||||
# The 8 semantic state colors (phase 93 — B3 revised): the same
|
||||
# contract as the 9 identity colors (``null`` = back to the
|
||||
# built-in; ``#rrggbb`` is validated + lowercased by the API).
|
||||
ok_bg: str | None = None
|
||||
ok_ink: str | None = None
|
||||
err_bg: str | None = None
|
||||
err_ink: str | None = None
|
||||
err_line: str | None = None
|
||||
accent_bg: str | None = None
|
||||
accent_ink: str | None = None
|
||||
accent_line: str | None = None
|
||||
|
||||
|
||||
class UiSettingsOut(BaseModel):
|
||||
"""Effective UI settings (``GET``/``PUT /api/ui-settings`` response,
|
||||
phase 91, task 01).
|
||||
phase 91, task 01; 17 colors since phase 93).
|
||||
|
||||
All 12 values, all non-null strings: the resolver's
|
||||
All 20 values (3 strings + 17 palette colors — 9 identity + 8
|
||||
semantic state), 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.
|
||||
strings and the built-in palette (the semantic colors report their
|
||||
built-ins — the effective values, the NULL = built-in rule).
|
||||
"""
|
||||
|
||||
app_name: str
|
||||
@@ -867,3 +880,11 @@ class UiSettingsOut(BaseModel):
|
||||
brand: str
|
||||
brand_soft: str
|
||||
brand_ink: str
|
||||
ok_bg: str
|
||||
ok_ink: str
|
||||
err_bg: str
|
||||
err_ink: str
|
||||
err_line: str
|
||||
accent_bg: str
|
||||
accent_ink: str
|
||||
accent_line: str
|
||||
|
||||
Reference in New Issue
Block a user