phase: 93_theme_semantic_completion
Build and Push Containers / build-and-push-app (push) Successful in 1m56s
Build and Push Containers / build-and-push-db (push) Successful in 11s

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:
2026-09-10 16:43:08 -04:00
parent d4f38ad3ce
commit 9188be259b
44 changed files with 3019 additions and 196 deletions
@@ -1,62 +0,0 @@
# Phase 93 — Theme completion: semantic state colors become tab-controlled + readable page heads
**Source:** `TODO.md` L3 — "I created a black/white/gray theme for brain of reese and found multiple cases of color still in the UI which tells me the customization is not complete. Screenshots are in the theme_fixes/ folder. Note the green text "Theme saved", the red "Revoked" tag, the red "Stale" tag, The green "Local" tag, The yellow "Listing documents" and "Reading" tool calls. Also the header and description of each page needs a background - the grid makes it hard to read."
**Story:** n/a (owner TODO item — theme-customization completion on `91_admin_theme_tab` / `92_theme_save_and_coverage`).
**Context:** The admin Theme tab (phases 91/92) persists the **9 identity variables** (`bg, surface, ink, ink_soft, line, grid_line, brand, brand_soft, brand_ink`) in the single-row `ui_settings` table (`app/models.py` `UiSettings`), resolves them in `app/core/theming.py` (`BUILTIN_COLORS` / `COLOR_FIELDS` / `effective_settings`), serves them via `GET/PUT /api/ui-settings` (`app/api/ui_settings.py`), injects them pre-paint as `<style id="bor-theme">:root{…}</style>` with a matching CSP hash (`theme_style_tag` / `theme_csp_hash` / `app/core/caching.py`), and edits them live in `frontend/assets/theme.js` (the `FIELDS` array drives pickers, live preview, PUT body, contrast warnings). Phase 92 removed every hardcoded color literal, but the **semantic state families are raw `:root` variables the tab cannot reach** — `--ok-bg/--ok-ink` (the green "Theme saved." result text, the green `LOCAL` badge `.git-source-kind.is-local`), `--err-bg/--err-ink/--err-line` (the rose `Stale` / `Revoked` pills `.stale-pill`), `--accent-bg/--accent-ink/--accent-line` (the amber agent tool-call lines `.tool-call`), built-in values at `frontend/assets/styles.css` L29–36. Separately, every page's `h1` + description (`.page-head`) sits directly on the background grid texture and is hard to read (all 8 screenshots in `theme_fixes/`).
## Objective
Make theme customization **complete**: a monochrome (black/white/gray) theme can be saved with zero residual color, because the three semantic state families become Theme-tab-controlled (17 palette variables total) with `NULL = built-in` (the default theme renders byte-identical to today); and every page's header + description block gets a solid surface background so the grid never fights the heading text. State stays honest: every state element keeps its **text** label (the "text + color, never color alone" house rule), so a grayscale theme conveys state by words, not hues.
## Owner-permitted decision recorded here (PLAN.md is being redone by the owner)
- **B3 revised (owner permission 2026-09-10, TODO.md L3):** the semantic families `--ok-*`, `--err-*`, `--accent-*` become **storable and Theme-tab-controlled** (the 2026-09-09 lock that they "are NOT identity … not configurable from the tab" is lifted). `NULL = built-in`, so the built-in theme's look and all existing AA ratios are unchanged; only the tab's reachable surface grows.
- Header panel design (owner-permitted by the same TODO line — "needs a background"): a solid `var(--surface)` panel behind each page head, not a full-bleed band (ASSUMPTION in task 03).
## Dependencies
- `92_theme_save_and_coverage` (complete) — the whole phase builds on it: the `ui_settings` row + resolver + admin API, the pre-paint injection + CSP hash in `app/core/caching.py`, the `#view-theme` form (`frontend/index.html`) + editor (`frontend/assets/theme.js`), and the E2E suites `tests/e2e/test_admin_theme_tab.py` / `test_theme_save_and_coverage.py` / `test_configurable_brand.py` (all stay green — a no-op theme must remain byte-identical).
## Design (shared by all tasks — the executor reads this, not the chat)
### 17-variable palette (tasks 01, 02)
The 9 identity variables are untouched. **8 semantic variables** join the storable palette, column names mirroring the CSS variables:
| Column / field | CSS var | Built-in (from `styles.css` `:root`) | UI role |
|---|---|---|---|
| `ok_bg` | `--ok-bg` | `#10241b` | success pill/text background ("Theme saved.") |
| `ok_ink` | `--ok-ink` | `#6ee7a8` | success text |
| `err_bg` | `--err-bg` | `#2d0a0a` | Stale/Revoked pill background |
| `err_ink` | `--err-ink` | `#fca5a5` | Stale/Revoked pill text |
| `err_line` | `--err-line` | `#ef4444` | Stale/Revoked pill border (decorative — no contrast duty) |
| `accent_bg` | `--accent-bg` | `#2b2110` | deflection banner / tool-line background |
| `accent_ink` | `--accent-ink` | `#fbbf24` | tool-call line text ("Listing…", "Reading…") |
| `accent_line` | `--accent-line` | `#f59e0b` | tool-line left border / deflection border (decorative) |
Server plumbing mirrors the phase-92 `grid_line` pattern exactly: migration `0016` (after `0015_grid_line.py`), 8 nullable `String(7)` columns, `BUILTIN_COLORS` extended (the `:root`-drift test in `tests/unit/test_theming.py` parses the built-ins from `styles.css` — no new hardcoded palette copy), `COLOR_FIELDS` order = the 9 identity vars **then** the 8 semantic vars (structural first, brand middle, state last). Everything downstream is `COLOR_FIELDS`-driven and picks the 8 up with **zero logic change**: `effective_settings`, `theme_style_tag` (the pre-paint tag gains 8 declarations when any var is non-default; the byte-identical no-op contract holds — the tag is still `""` only when all 17 equal their built-ins), `theme_csp_hash` (recomputed per theme, runtime), the `app/core/caching.py` injection, and the `COLOR_FIELDS`-driven loops in `app/api/ui_settings.py`.
### Contrast pairs 5 → 8 (task 02)
The tab's client-side AA warnings (`theme.js` `PAIRS`, L184) gain three ink-on-bg pairs: `ok_ink` on `ok_bg`, `err_ink` on `err_bg`, `accent_ink` on `accent_bg`. The two `_line` vars stay excluded (decorative borders, no contrast duty — same rule as `--line`/`--grid-line`). The authoritative table lives in `app/core/theming.py`'s docstring — extend it there and in `theme.js`'s pair list together.
### Page-head panel (task 03)
`.page-head` (the shell's standard frame: `h1` + description — `styles.css` L1536 ff.) gets `background: var(--surface)`, `padding`, `border: 1px solid var(--line)` and the house card radius, on EVERY page: the seven shell views (`#view-chat`, `#view-tuning`, `#view-rag`, `#view-git-sources`, `#view-history`, `#view-tokens`, `#view-theme` — one shared rule covers them) plus the standalone pages' header blocks (`frontend/login.html`, `frontend/document.html`, `frontend/shared.html`, `frontend/doc-edit.html` — audit each; apply the same panel treatment to whatever class carries their h1 + lede, including the shared-page head that reuses the `.page-head h1` size per the `styles.css` L3189 note). The panel must not break: the `#view-history .page-head` flex row (title left, stale pill right — `styles.css` L2550), the mobile wrap of `.page-head-row` (L4221), or any existing header/nav/responsive E2E.
### Monochrome E2E (task 04)
One dedicated story suite: as admin, `PUT /api/ui-settings` an all-gray 17-color theme (every channel R=G=B; pairs still ≥ 4.5:1), then assert per element that the **computed** color is grayscale and the **text** state label is still present: "Theme saved." result (`#theme-result`), a `Stale` pill (History — seed an out-of-generation saved chat, the phase-53 pattern in `tests/e2e/test_stale_saved_chats.py`), a `Revoked` pill (Tokens — create + revoke via the UI), the `LOCAL` badge (Git sources — register a local-directory source, the `tests/e2e/test_local_directory_sources.py` pattern), and a `.tool-call` line (Chat — a mock-LLM turn that executes a tool, the `tests/e2e/test_agent_document_tools.py` pattern). Plus: every page head's computed `background-color` is non-transparent, and a fresh `page.goto` of each page paints the gray palette pre-paint (the `#bor-theme` tag carries all 17 when non-default).
## Tasks
1. `01_semantic_columns.md` — migration 0016 + `UiSettings` columns + `BUILTIN_COLORS`/`effective_settings` + schema + API validation
2. `02_theme_tab_state_section.md` — `#view-theme` form fields + `theme.js` `FIELDS`/`PAIRS` (5→8 pairs) + live preview/Reset coverage
3. `03_page_head_background.md` — `.page-head` (and standalone-page) surface panel across every page
4. `04_e2e_monochrome_theme.md` — dedicated Playwright suite: full-gray theme → zero residual color, state text intact, heads readable
## Testing & Quality
- Unit/integration: resolver round-trip for all 17 (NULL=clear, built-in→NULL normalization, 422 on bad hex names the field); pre-paint tag + CSP hash with the 17 vars (non-default subset, no-op byte-identical); drift test covers the 17; `PUT/GET /api/ui-settings` integration for the 8 new fields; existing theming suites (`tests/unit/test_theming.py`, `test_ui_settings.py`, `test_caching.py`, `tests/integration/test_ui_settings_api.py`, `test_security_headers.py`) stay green.
- Coverage: **>90%** on `app/` (`uv run pytest --cov=app --cov-report=term-missing`).
- E2E: `uv run pytest tests/e2e/test_theme_semantic_completion.py -v --no-cov` in isolation; the existing theme suites (`test_admin_theme_tab.py`, `test_theme_save_and_coverage.py`, `test_configurable_brand.py`, `test_dark_tech_theme.py`, `test_header_consistency.py`, `test_nav_*`, responsive) stay green in isolation.
- Lint/types: `uv run ruff check . && uv run pyright`.
## Completion Criteria
- [ ] An all-gray 17-color theme saved from the tab renders with **no colored element anywhere** (verified by the new E2E's computed-style assertions) while Stale/Revoked/Local/Tool state text remains visible
- [ ] Default (no row / all NULL) deployment serves byte-identical HTML (no `#bor-theme` tag) and the built-in theme's 8 semantic look is unchanged
- [ ] Every page head (7 shell views + 4 standalone pages) has a solid surface background over the grid
- [ ] test suite green, coverage >90%, ruff + pyright clean, dedicated E2E green in isolation
- [ ] no behavior change in completed phases (phases 01–92 suites green)
- [ ] one atomic Conventional Commit, `--no-gpg-sign` (e.g. `feat(theme): make semantic state colors tab-controlled and give page heads a surface panel`)
@@ -1,32 +0,0 @@
# Task 01 — Semantic color columns: migration, resolver, schema, API
**Phase:** `93_theme_semantic_completion` · **Source:** `TODO.md:3` — "I created a black/white/gray theme for brain of reese and found multiple cases of color still in the UI which tells me the customization is not complete. … Note the green text "Theme saved", the red "Revoked" tag, the red "Stale" tag, The green "Local" tag, The yellow "Listing documents" and "Reading" tool calls. …" (this task makes the 8 semantic values storable + resolvable — the server half)
**Story:** n/a (owner TODO item).
## Objective
Persist and resolve the 8 semantic palette values (`ok_bg, ok_ink, err_bg, err_ink, err_line, accent_bg, accent_ink, accent_line`) with the exact same plumbing as the 9 identity vars, so `effective_settings` returns all 17 and the pre-paint tag / CSP hash pick them up with zero further change.
## Work
1. `alembic/versions/0016_ui_settings_semantic.py` — new migration (down revision `0015_grid_line`, the current head): add the 8 nullable `String(7)` columns to `ui_settings` (no server defaults — the row is created only by the PUT upsert, house rule); **tested downgrade** drops them (A13).
2. `app/models.py` — `UiSettings`: the 8 `Mapped[str | None] = mapped_column(String(7), nullable=True)` columns after `brand_ink`, with the B1 `NULL = the built-in` docstring extended to the semantic family (citing the B3 revision, owner permission 2026-09-10, `TODO.md` L3 — PLAN.md is being redone by the owner, the decision is recorded in `00_phase.md`).
3. `app/core/theming.py` — extend `BUILTIN_COLORS` with the 8 built-ins **parsed from / equal to** the existing `:root` values in `frontend/assets/styles.css` (`--ok-bg #10241b`, `--ok-ink #6ee7a8`, `--err-bg #2d0a0a`, `--err-ink #fca5a5`, `--err-line #ef4444`, `--accent-bg #2b2110`, `--accent-ink #fbbf24`, `--accent-line #f59e0b` — no new hardcoded copy; the drift test is the guard); `COLOR_FIELDS` = the 9 identity vars then the 8 semantic vars (identity, brand, then state); `effective_settings` resolves 20 values (3 strings + 17 colors) with the same column-by-column DB-over-built-in merge.
- Update the module docstring: the 9-variable identity table stays; add a short "semantic families" table (the 8 vars, roles, and the note that they are now storable — B3 revised) and extend the "five contrast pairs" paragraph to the **eight** pairs (see `02_theme_tab_state_section.md`).
4. `app/schemas.py` — `UiSettingsIn`: the 8 `str | None = None` fields; the output model (`UiSettingsOut` or its current name in the same file): same 8, documented as effective values.
5. `app/api/ui_settings.py` — the color loops are `COLOR_FIELDS`-driven (phase 92): verify `_validate_colors` / `_validate_strings` and the GET builder need no per-field edits; if any explicit field list remains, extend it with the 8. Keep the 422 fixed-detail style (a bad hex names the field) and the built-in→NULL normalization (a semantic color equal to its built-in stores NULL).
6. `frontend/assets/styles.css` — **no change expected**: the 8 vars already exist in `:root` with the built-in values (L29–36). If the drift test (below) surfaces a mismatch between the `:root` literals and step 3's table, fix the table, not the CSS.
- ASSUMPTION: `NULL = built-in` for the semantic family (B1 rule, no env fallback for colors) — the default deployment is byte-identical and the owner's existing monochrome theme (a `ui_settings` row) keeps its saved identity values; the 8 new columns are NULL there until the owner re-saves with grays (the tab's live values after this phase are the built-ins, which the owner then edits).
- ASSUMPTION: `COLOR_FIELDS` order = identity (9) then semantic (8) — the pre-paint tag's byte layout changes for non-default themes (the CSP hash is runtime-computed, so nothing static breaks); the no-op tag stays `""`.
## Testing & Quality
- Unit: `tests/unit/test_theming.py` — the `:root`-drift assertion now covers all 17 (it parses `styles.css`, so it must pass unmodified or with the 8 added to its expected set); `theme_style_tag` with one non-default semantic var → tag contains exactly the 17 declarations in `COLOR_FIELDS` order; all-17-default → `""` (byte-identical contract); `theme_csp_hash` matches the tag. `tests/unit/test_ui_settings.py` — built-in→NULL normalization + 422 naming for the new fields.
- Integration: `tests/integration/test_ui_settings_api.py` — PUT with the 8 fields (hex lowercased, empty→NULL, built-in→NULL) and GET returning effective values; admin-only gate unchanged (token user 403).
- `tests/integration/test_security_headers.py` — the CSP `style-src` hash still matches the injected tag (it computes it at runtime).
- Coverage: **>90%** on this task's new/modified code (full gate: `app/`).
## Completion Criteria
- [ ] `uv run alembic upgrade head` and `uv run alembic downgrade -1` both green (tested downgrade)
- [ ] `effective_settings` returns 20 values; `PUT/GET /api/ui-settings` round-trips the 8 semantic fields
- [ ] no-op theme still serves byte-identical HTML (no `#bor-theme` tag)
- [ ] full test suite green, coverage >90%
- [ ] no behavior change in completed work (phases 01–92 suites green)
@@ -1,28 +0,0 @@
# Task 02 — Theme tab: the "State colors" section (8 pickers, 5→8 contrast pairs)
**Phase:** `93_theme_semantic_completion` · **Source:** `TODO.md:3` — "…multiple cases of color still in the UI which tells me the customization is not complete. Note the green text "Theme saved", the red "Revoked" tag, the red "Stale" tag, The green "Local" tag, The yellow "Listing documents" and "Reading" tool calls. …" (this task exposes the 8 semantic values in the admin Theme tab)
**Story:** n/a (owner TODO item).
## Objective
The Theme tab edits all 17 palette variables: a new "State colors" fieldset with the 8 semantic pickers, live-preview and Save/Reset coverage, and the client-side AA warnings extended from five to eight pairs.
## Work
1. `frontend/index.html` — `#view-theme` (the shell view, ~L972): add a "State colors" fieldset after the palette fieldset with 8 `<input type="color">` fields, E2E-stable ids following the house pattern: `theme-ok-bg`, `theme-ok-ink`, `theme-err-bg`, `theme-err-ink`, `theme-err-line`, `theme-accent-bg`, `theme-accent-ink`, `theme-accent-line` (labels name the role: "Success text (--ok-ink)", "Error pill background (--err-bg)", …). Static defaults = the built-ins (the house contract: the drift/E2E pattern asserts static values against `styles.css` `:root`). Also update the palette fieldset's "five pairs" copy to reflect the eight (the exact legend text is the executor's call — keep the WCAG 2.1 AA (4.5:1) wording).
2. `frontend/assets/theme.js` — `FIELDS` (L128ff): append the 8 entries `{ field, id, kind: "color" }` in the same fieldset order. Everything that iterates `FIELDS` then covers them automatically: live preview (`documentElement.style.setProperty`, L265), `collectBody` (the PUT body), `clearPreview`, and the `applyServedTheme` / served-theme sync from phase 92 (the `#bor-theme` tag content builder at ~L306 maps the fields — verify it uses `FIELDS` and gains the 8 declarations in `COLOR_FIELDS` order).
3. `frontend/assets/theme.js` — `PAIRS` (L184): add the three ink-on-bg pairs `[ok_ink, ok_bg]`, `[err_ink, err_bg]`, `[accent_ink, accent_bg]` (the two `_line` vars stay excluded — decorative borders have no contrast duty, same rule as `--line`/`--grid-line`). Update the "five pairs" comments (L85, L154) to eight.
4. `app/core/theming.py` docstring — the authoritative pair table (done in task 01 if not already): five → eight pairs; `theme.js`'s `PAIRS` and this table must never diverge (note the mirror relationship in both).
- ASSUMPTION: fieldset/label wording ("State colors"; Success / Error / Notice roles for ok / err / accent) — the TODO names the *elements*, not the labels; the executor keeps the house label style ("Role (--var-name)").
- ASSUMPTION: the 3 new warning pairs are ink-on-bg only (matching how the existing 5 pairs are chosen — "the pairs the layout actually pairs"); no new warning for the decorative `_line` vars.
## Testing & Quality
- Unit (house style — frontend files are read as text, e.g. `tests/unit/test_big_read_progress.py`): extend or add a theming-frontend test asserting `theme.js`'s `FIELDS` lists all 12 + 8 ids in order and `PAIRS` has exactly 8 entries; `index.html` `#view-theme` contains all 20 color inputs with the E2E-stable ids and built-in static values.
- Unit: `tests/unit/test_theming.py` — the pair table in the docstring matches `theme.js` `PAIRS` (if the house already pins this mirror, extend it; otherwise add the assertion here).
- E2E coverage lands in task 04 (the monochrome suite drives the new pickers through Save/Reset).
- Coverage: **>90%** on this task's new/modified code (full gate: `app/`).
## Completion Criteria
- [ ] the tab shows 20 color fields; typing a gray in any of the 8 previews live (computed style on `<html>`), persists on Save (PUT body carries all 20), and returns to the built-in on Reset
- [ ] a failing pair among the 8 is listed in `#theme-contrast` (`role=alert`) as `--ok-ink on --ok-bg: x.x:1 — needs 4.5:1` in the house format
- [ ] full test suite green, coverage >90%
- [ ] no behavior change in completed work (phase 91/92 E2E suites green)
@@ -1,36 +0,0 @@
# Task 03 — Page heads get a surface panel (grid no longer fights the headings)
**Phase:** `93_theme_semantic_completion` · **Source:** `TODO.md:3` — "…Also the header and description of each page needs a background - the grid makes it hard to read." (all 8 `theme_fixes/` screenshots show the `h1` + description sitting directly on the grid texture: Knowledge base, Git sources, Global Tuning, Access tokens, Theme, History)
**Story:** n/a (owner TODO item).
## Objective
Every page's header + description block reads cleanly over the background grid: one shared `.page-head` surface panel rule covering all shell views, plus the equivalent treatment for standalone pages whose h1/lede sits directly on the grid.
## Work
1. `frontend/assets/styles.css` — the `.page-head` rule (L1536ff): add the panel — `background: var(--surface)`, `padding: 1rem 1.25rem` (tune to the existing card rhythm), `border: 1px solid var(--line)`, `border-radius` matching the house card radius. This single rule covers every shell view that uses the class — `#view-tuning` (L303), `#view-rag` (L365), `#view-git-sources` (L499), `#view-history` (L739), `#view-tokens` (L842), `#view-theme` (L998) in `frontend/index.html` (and `#view-chat` if it carries a `.page-head` — audit).
2. Layout safety (verified, not assumed):
- `#view-history .page-head` is a **flex row** (title left, stale pill right — L2550ff): the panel must wrap the whole row without breaking the flex alignment (padding on the flex container, not on the children).
- Mobile: `.page-head-row { flex-wrap: wrap; }` (L4221) must keep working inside the panel; no horizontal overflow at 360px (the E2E responsive suites are the gate).
- The panel must not introduce a new color literal — `var(--surface)` / `var(--line)` only (phase-92 invariant: zero hardcoded color literals outside `:root`).
3. Standalone pages — audit each and apply the same panel treatment ONLY where the h1/lede sits directly on the grid:
- `frontend/doc-edit.html` — already `.page-head` (L36): covered by step 1, verify.
- `frontend/login.html` — `h1#login-title` (L121): if the title/lede are already inside a card, leave untouched; otherwise give the header block the same panel.
- `frontend/document.html` — `h1#doc-title` (L118): same rule.
- `frontend/shared.html` — `h1#shared-title` (L143): same rule (its head reuses the `.page-head h1` size per the L3189 note — if it carries the class it is covered by step 1).
4. `tests/unit/` — house-style CSS text test (the `test_background_no_motion.py` pattern of parsing `styles.css` rules): assert the `.page-head` rule declares a non-transparent `background` built from a `var(--…)` (no literal), plus padding/border/radius.
- ASSUMPTION: solid `var(--surface)` panel (not translucent, not full-bleed band, no blur — the phase-08 perf anchor forbids blur) — the TODO only says "needs a background"; surface is the house card color and is itself tab-controlled (a monochrome theme grays it automatically).
- ASSUMPTION: standalone pages whose heading is already inside a card need no change (the TODO targets grid-exposed text; the executor documents any deliberate skip in the commit message).
## Testing & Quality
- Unit: the CSS text test above.
- Integration: none expected (pure CSS) — the API suites stay green.
- E2E (gate, run in isolation): `test_header_consistency.py`, `test_nav_consistency.py`, `test_sticky_navbar.py`, `test_responsive_polish.py`, `test_history_page_width.py`, `test_mobile_hamburger_nav.py` — all must stay green; the new panel's assertions land in task 04's suite.
- Coverage: **>90%** on `app/` (unchanged code — keep the gate green).
## Completion Criteria
- [ ] every shell view's h1 + description sits on a solid surface panel (visual: `theme_fixes/` screenshots 2, 3, 4, 6, 8 conditions gone)
- [ ] the History flex-row head and the 360px wrap are unbroken
- [ ] zero new color literals outside `:root`
- [ ] full test suite green, coverage >90%
- [ ] no behavior change in completed work
@@ -1,35 +0,0 @@
# Task 04 — E2E: a complete monochrome theme (the dedicated story suite)
**Phase:** `93_theme_semantic_completion` · **Source:** `TODO.md:3` — the whole item (this task is the story gate: a saved black/white/gray theme leaves **no** residual color anywhere and every page head reads cleanly)
**Story:** n/a (owner TODO item — one Playwright file per story, run in isolation, A16).
## Objective
`tests/e2e/test_theme_semantic_completion.py` proves the TODO is done: with an all-gray 17-color theme saved from the admin tab, every state element the owner screenshot computes gray, state text stays present, every page head has a non-transparent background, and a fresh load paints the gray palette pre-paint.
## Work
1. `tests/e2e/test_theme_semantic_completion.py` (new, isolated — `tests/e2e/conftest.py` fixtures `app_server` + `mock_llm`; admin login via `tests/e2e/auth_helpers.py`):
- **Save the gray theme:** `PUT /api/ui-settings` with all 17 colors on a grayscale ramp where every channel is R=G=B and all **eight** contrast pairs pass ≥ 4.5:1 (e.g. `bg #111111, surface #1e1e1e, ink #f2f2f2, ink_soft #b3b3b3, line #3a3a3a, grid_line #2b2b2b, brand #9a9a9a, brand_soft #2c2c2c, brand_ink #d4d4d4, ok_bg #161616, ok_ink #e0e0e0, err_bg #191919, err_ink #e6e6e6, err_line #6a6a6a, accent_bg #1c1c1c, accent_ink #dedede, accent_line #787878` — the executor computes/pins exact values that pass; the strings stay default). Drive it THROUGH the tab UI (set the 8 new pickers + save) for at least one round-trip — the form wiring is the point — plus a direct-PUT path for the rest.
- **Grayscale helper:** a computed-style check asserting `r == g == b` (the "still color" detector the TODO names).
- **State elements (computed gray + text intact):**
- `#theme-result` "Theme saved." (Theme page, after save — the green text from screenshot 7)
- `.stale-pill` "Stale" (History — seed an out-of-generation saved chat, the `tests/e2e/test_stale_saved_chats.py` pattern)
- the Revoked pill (Tokens — generate + revoke a token through the UI, the `tests/e2e/test_api_tokens.py` pattern)
- `.git-source-kind.is-local` "Local" (Git sources — register a local-directory source, the `tests/e2e/test_local_directory_sources.py` pattern; screenshot 3)
- a `.tool-call` line (Chat — a `mock_llm` turn that executes one tool call; `ls` needs no KB, the `tests/e2e/test_agent_document_tools.py` pattern; screenshots 1: the yellow "Listing documents" / "Reading")
- **Page heads:** on each of the seven shell views (`/`, `/tuning.html`, `/sources.html`, `/git-sources.html`, `/history.html`, `/tokens.html`, `/theme.html`) + the login page: the h1/lede block's computed `background-color` is non-transparent, and its ink-on-panel ratio ≥ 4.5 (screenshots 2, 3, 4, 6, 8).
- **Pre-paint + persistence:** a fresh `page.goto` on a themed page finds the `#bor-theme` tag whose content carries all 17 variables (the non-default case) and the body's computed background equals the saved `bg` on first paint; after `Reset to defaults` the tag is gone from the live document and a fresh load serves byte-identical default HTML (no tag).
2. Run it in isolation and keep the existing suites green in isolation: `test_admin_theme_tab.py`, `test_theme_save_and_coverage.py`, `test_configurable_brand.py`, `test_dark_tech_theme.py`, `test_header_consistency.py`, `test_security_headers.py` (the CSP hash must cover the 17-var tag).
- ASSUMPTION: the mock-LLM tool turn uses `ls` (zero KB dependency) — the `.tool-call` line is the same DOM element whether the tool is `ls`/`read`/`grep`, so one line suffices for the TODO's "Listing documents" / "Reading" cases.
- ASSUMPTION: the login/document/shared standalone pages are checked for head background only where task 03 gave them a panel (the executor mirrors task 03's audit outcomes here).
## Testing & Quality
- This IS the E2E task: `uv run pytest tests/e2e/test_theme_semantic_completion.py -v --no-cov` green in isolation (db up: `podman compose up -d db`).
- Regression gate: the suites named in Work item 2 stay green in isolation; full unit + integration green; coverage **>90%** on `app/`.
## Completion Criteria
- [ ] the suite is green in isolation and encodes every screenshot in `theme_fixes/` as a computed-style assertion
- [ ] a grayscale theme ⇒ zero colored pixels in the asserted elements; state text ("Stale", "Revoked", "Local", "Theme saved.") still visible
- [ ] all page heads have a non-transparent, AA-readable background
- [ ] no-op/Reset deployment byte-identical (no `#bor-theme` tag)
- [ ] full test suite green, coverage >90%