Everything is verified. Final report: ## Phase 177 — Themed favicon: completion pass (all tasks pre-done, verified end-to-end) **Verified (no code fixes needed — implementation matched the design D1–D4 exactly):** - `theming.favicon_svg()` pure renderer + golden byte-identical pin; `GET/HEAD /favicon.svg` route (no-cache, `image/svg+xml`, built-in 200 fallback on DB blip, routes-first before static mount); 5 templates → bare `href="/favicon.svg"`, static file deleted (Containerfile cp removed); Theme-tab `data:` live preview with 4 restore points; JS↔server mirror pins. **Tests / lint / coverage (all green):** - `uv run pytest --cov=app --cov-report=term-missing` → 4161 passed, `app/` 99% (>90% gate) - `uv run ruff check .` → clean; `uv run pyright` → 0 errors - `uv run pytest tests/e2e/test_themed_favicon.py -v --no-cov` (isolated) → 6 passed - `test_theme_save_and_coverage.py` (92) + `test_theme_semantic_completion.py` (93), isolated → 3 + 8 passed **Completion criteria:** all met — default golden bytes/headers; themed roles + `#bor-theme` non-regression; DB-failure 200 fallback; 5 bare links + `/assets/favicon.svg` 404 + browser rendering; live-preview data: repaint + Save/Reset/re-show restore; gates green; E2E suites green in isolation. Commit: harness commits atomically (overrides AGENTS rule 8 — no git commands run). **Documentation:** README.md verified consistent in full (the phase's favicon paragraph is accurate; all commands, env vars, routes, ports, scripts cross-checked) — no changes needed. AGENTS.md verified consistent — no changes needed. **Notable:** `image-1.png` (owner's TODO screenshot, referenced by phase 176's overview) sits untracked at the repo root — pre-existing, explicitly out of scope per the phase-176 report; left untouched. **Next pending phase:** none — 177 was the last; TODO.md is cleared.
9.9 KiB
Phase 177 — The favicon's colors follow the effective theme palette
Source: TODO.md L3 — "We need the favicon to change colors to match the theme."
Story: n/a (TODO-derived, owner roadmap confirmation 2026-10-13)
Context: frontend/assets/favicon.svg (the current static art — a hexagon + center dot + 4 spokes whose three hardcoded colors map 1:1 onto the built-in palette: #1a0f0f = surface, #f43f5e = brand, #fca5a5 = brand_ink), the five templates' L8 <link rel="icon" type="image/svg+xml" href="/assets/favicon.svg"> (frontend/index.html, frontend/document.html, frontend/login.html, frontend/shared.html, frontend/doc-edit.html), app/core/theming.py (BUILTIN_COLORS, COLOR_FIELDS, effective_settings() — the DB-over-env/built-in resolver with NO process cache, theme_style_tag() + inject_theme() — the pre-paint tag pattern, theme_csp_hash()), app/core/caching.py (CachingMiddleware's per-response short-lived-session theme read with the never-break DB-blip fallback; /favicon.svg is neither in HTML_PAGES nor under /assets/ → passes through untouched), app/main.py create_app() (router includes end ~L199; the static catch-all mount at L252 — routes-first), app/core/security_headers.py (CSP default-src 'self'; … img-src 'self' data: — the phase-123 carve-out that makes a data: favicon URI legal; CSP3 governs link[rel=icon] by img-src), frontend/assets/theme.js (the admin Theme tab — previewColor() live preview, the input bindings, saveTheme()/resetTheme() + the bor:view-refresh re-show handler), tests/unit/test_theming.py (the static pin set incl. test_builtin_colors_match_styles_css_root), tests/unit/test_theme_frontend.py (the JS static-pin pattern), tests/e2e/test_theme_save_and_coverage.py + test_theme_semantic_completion.py (the admin theme-tab E2E pattern: e2e/auth_helpers.login(page, app_url, next="/theme.html"), the #theme-* inputs), AGENTS rules 4/6/8/9/10, PLAN §7.4 (the never-stale feedback contract).
Objective
The browser-tab favicon must carry the EFFECTIVE theme palette (built-in or the owner's saved one), painted on first load like everything else — no red hexagon on a graphite theme. The favicon is served by the app from the same resolver that paints the page (effective_settings), and the admin Theme tab's live (unsaved) picks repaint it too (owner-confirmed A4).
Design (shared by all tasks — the executor reads this, not the chat)
- D1 — the pure renderer (task 01, A1).
theming.favicon_svg(colors: dict[str, str]) -> str— the SAME artwork (viewBox0 0 64 64, the hexagon path, the r=6.5 center circle, the 4 spokes, all stroke widths/caps unchanged) with only the three color roles swapped: hexagonfill→surface, hexagonstroke+ circlefill→brand, spokesstroke→brand_ink. The input is the full 20-value effective dict (mirroringtheme_style_tag's self-contained signature; the function reads its three keys). Byte-identical contract: for the built-in palette the output is EXACTLY the current static file's<svg>element (lowercase hex, same attribute order) — pinned by a golden constant intests/unit/test_theming.py; the static file's comment header is file packaging, not art, and is not part of the contract. - D2 — serving (task 02, A2/A3).
GET /favicon.svg(+ HEAD for static-file parity) — a no-prefix route increate_app()after the router includes and BEFORE the static catch-all mount (routes-first, the phase-51 shared-page pattern). Sync handler: one short-livedSessionLocal()session (thecaching.pyhouse pattern — NO process cache, so a runtime theme change is visible on the next request),effective_settings()→favicon_svg(); ANY exception →logger.exception+favicon_svg(BUILTIN_COLORS)(200, never 500 — the theme-tag's never-break contract,caching.py's DB-blip pattern). Response:media_type="image/svg+xml",Cache-Control: no-cache(the pages are no-cache; the palette changes at runtime, so the browser must revalidate — the route is NOT under/assets/, so no version token; freshness is revalidation, not busting). Deliberately OUTSIDE/api/*: no per-response byte-budget line (AGENTS rule 10 is/api/*-only), no auth dependency (the favicon is chrome, like the login page).CachingMiddlewarepasses it through untouched (neither an HTML page nor an asset);SecurityHeadersMiddlewarestill adds the CSP — no change needed (img-src 'self' data:covers the D4 preview). - D3 — the templates + the static file's deletion (task 03, A2). All five L8 links →
href="/favicon.svg";frontend/assets/favicon.svgis deleted — the route is the single source (the built-in bytes stay pinned by D1's golden constant, so nothing is lost). The new href is not under/assets/, soCachingMiddleware's?v=rewrite regex can never touch it (a pin keeps this explicit — the link stays bare). - D4 — the Theme tab live preview (task 04, A4 — owner-confirmed 2026-10-13).
theme.jsgains a purethemeFaviconDataUri(surface, brand, brandInk)— the CLIENT-SIDE MIRROR oftheming.favicon_svg(same artwork, same roles; a unit pin keeps the two from diverging — thePAIRS-mirror house pattern). The existing per-colorinputbinding ALSO updateslink[rel="icon"].hrefto thedata:URI whenever one of the three driving fields (surface/brand/brand_ink) changes (rebuilt from the three inputs' current values; the other 14 picks never touch it). Restore to"/favicon.svg"on: Save success (afterapplyServedTheme), Reset success, the Reset-failureclearPreview()path, and thebor:view-refreshre-show — after every restore the SETTLED palette (re-read per request, no process cache) governs. CSP note (commented in the code):data:is legal for the favicon via the phase-123img-src 'self' data:carve-out (CSP3:link[rel=icon]is governed byimg-src; the carve-out is image-scoped —data:never becomes a script/style/fetch source). - NOT touched:
CachingMiddleware, the#bor-themetag injection + CSP hash, the 8 semantic state colors (they are NOT in the artwork — a favicon has no state meaning),app/outsidetheming.py/main.py,brand.js, the other templates/assets, thetheme.jssave/reset/contrast machinery beyond the additions above.
Dependencies
176_mobile_settings_value_width(todo) — the immediately preceding phase (Protocol B ordering; no functional dependency).92_theme_save_and_coverage(complete) — the served-theme sync + the#theme-*input contract the D4 additions bind to.91_admin_theme_tab(complete) — the Theme tab's live-preview mechanism (previewColor) and thebor:view-refreshre-show hook.
Tasks
01_favicon_svg_renderer.md—theming.favicon_svg(): the pure palette→artwork renderer, byte-identical for the built-in palette (golden pin) — A1.02_favicon_route.md—GET /favicon.svg(+ HEAD) increate_app(): effective palette, built-in fallback on DB failure (200, never 500),image/svg+xml+no-cache— A2/A3.03_template_link_swap.md— the five template links →/favicon.svg;frontend/assets/favicon.svgdeleted (single source).04_theme_tab_live_preview.md— the Theme tab's live picks repaint the favicon (the CSP-legaldata:mirror, A4); Save/Reset/re-show restore the route URL; the dedicated E2E suite + the atomic commit.
Testing & Quality
- Unit:
tests/unit/test_theming.py(task 01): the golden byte-identical pin, the themed-role pin (the three hexes in their exact roles), the geometry-invariance pin (only the 4 color attributes differ);tests/unit/test_theme_frontend.py(task 04): thedata:mechanism pin (the three driving field names, the exact/favicon.svgrestore, the restore wiring). - Integration:
tests/integration/test_favicon_route.py(tasks 02–03): the three route states (default = golden bytes, themed = effective colors, DB failure = built-in fallback 200), the CSP header presence, the five template link pins (bare/favicon.svg, no?v=, no/assets/favicon.svganywhere in the served HTML), the deleted file's 404. - E2E (AGENTS rule 4, task 04):
tests/e2e/test_themed_favicon.py, run in isolation — the full contract in a real browser (default/themed route bytes, the pre-paint tag non-regression, the live-previewdata:repaint, the restore contract, zero external requests — rule 6). - Coverage: >90% on
app/(uv run pytest --cov=app --cov-report=term-missing); lint/types:uv run ruff check . && uv run pyright.
Completion Criteria
GET /favicon.svgon a default (no ui_settings row) deployment is 200image/svg+xmlwithCache-Control: no-cacheand a body byte-identical to the current static art (the golden pin, the integration test).- With a saved non-default palette (distinct
surface/brand/brand_ink), the route's SVG carries those three hexes in their exact roles, and the served pages STILL inject the#bor-themetag (the phase-91 pin stays green — no regression to the tag/CSP contract). - A DB failure at request time serves the built-in art with 200 (never 500).
- All five templates link
href="/favicon.svg"(bare — no?v=),/assets/favicon.svgis 404 (the file is deleted), and the favicon actually renders in the browser (the E2E). - In the admin Theme tab, a live (unsaved) pick of
surface/brand/brand_inkrepaints the favicon via thedata:URI; Save / Reset / re-show restorehref="/favicon.svg"with the settled palette governing (the E2E). uv run pytestgreen,app/coverage >90%, ruff + pyright clean.uv run pytest tests/e2e/test_themed_favicon.py -v --no-covgreen in isolation;test_theme_save_and_coverage.py+test_theme_semantic_completion.py(phases 92/93) green in isolation (no theme-tab regression).- ONE atomic Conventional Commits commit (AGENTS rule 8,
--no-gpg-sign) covering the phase + this directory + the cleared TODO.md.