phase: 177_themed_favicon
Build and Push Containers / build-and-push-app (push) Successful in 1m49s
Build and Push Containers / build-and-push-db (push) Successful in 12s

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.
This commit is contained in:
2026-10-11 15:33:39 -04:00
parent dd119d534a
commit 36eddce672
40 changed files with 2351 additions and 16 deletions
+113
View File
@@ -29,12 +29,21 @@ DB-over-built-in resolver shared by ``/api/ui-settings`` and
ellipsis. The :root 17-variable set itself stays pinned by
``test_builtin_colors_match_styles_css_root`` (UNCHANGED — this
phase adds no palette variable, B3).
* Phase 177 (task 01): ``favicon_svg`` — the pure palette→artwork
renderer, byte-identical for the built-in palette (the golden
constant ``FAVICON_GOLDEN`` holds the pre-phase static file's
``<svg>`` element — the file is deleted in task 03, so the
constant is the durable contract); the three color roles per A1
(hexagon fill → ``surface``, hexagon stroke + center dot →
``brand``, spokes → ``brand_ink``); the 8 semantic state colors
are NOT in the artwork (a favicon has no state meaning).
"""
from __future__ import annotations
import base64
import hashlib
import re
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any
@@ -572,3 +581,107 @@ def test_cell_clip_ellipsis_pin() -> None:
f".cell-clip white-space drifted: {rule.get('white-space')!r} — "
f"the ellipsis needs the nowrap"
)
# ---------------------------------------------------------------------------
# Phase 177 (task 01): favicon_svg — the pure palette→artwork renderer,
# byte-identical for the built-in palette (A1). The GOLDEN constant is
# the durable contract: it holds the pre-phase static file's ``<svg>``
# element (the file's comment header is file packaging, not art, and is
# NOT part of the contract; the file itself is deleted in task 03).
# ---------------------------------------------------------------------------
#: The pre-phase ``frontend/assets/favicon.svg`` ``<svg>`` element —
#: hexagon + center dot + 4 spokes, ``viewBox 0 0 64 64``, lowercase
#: hex, original attribute order. ``favicon_svg(BUILTIN_COLORS)`` must
#: equal this EXACTLY (341 bytes, single line, no comment header).
FAVICON_GOLDEN = (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">'
'<path d="M32 4 55 18v28L32 60 9 46V18Z" fill="#1a0f0f" '
'stroke="#f43f5e" stroke-width="4" stroke-linejoin="round"/>'
'<circle cx="32" cy="32" r="6.5" fill="#f43f5e"/>'
'<path d="M32 25.5V16M32 48v-9.5M25.5 32H16M48 32h-9.5" '
'stroke="#fca5a5" stroke-width="3" stroke-linecap="round"/>'
"</svg>"
)
def _mask_color_attrs(svg_text: str) -> str:
"""The document with its EXACTLY FOUR color attributes (the
``fill="`` / ``stroke="`` values) masked — the rest (geometry,
``d`` paths, circle ``cx/cy/r``, stroke widths, ``linejoin`` /
``linecap``, attribute order) is then comparable byte for byte.
(``stroke-width`` / ``stroke-linejoin`` / ``stroke-linecap`` never
match — they carry ``-`` before the ``="``.)"""
return re.sub(r'(fill|stroke)="#[0-9a-f]{6}"', r'\1="COLOR"', svg_text)
def test_favicon_svg_builtins_is_byte_identical_to_the_static_art() -> None:
"""The byte-identical contract (D1): for the built-in palette the
output is EXACTLY the static file's ``<svg>`` element — lowercase
hex, same attribute order — pinned by the golden constant (the
durable contract; the static file is deleted in task 03, so the
constant, not the file, keeps the bytes alive)."""
out = theming.favicon_svg(dict(theming.BUILTIN_COLORS))
assert out == FAVICON_GOLDEN
# Single-line element, no comment header (the file's comment is
# file packaging, not art), exact extent.
assert "\n" not in out
assert out.startswith('<svg xmlns="http://www.w3.org/2000/svg"')
assert out.endswith("</svg>")
# The three built-in role values land in the golden's roles.
assert 'fill="#1a0f0f"' in out # surface
assert out.count('"#f43f5e"') == 2 # brand (hexagon stroke + circle)
assert 'stroke="#fca5a5"' in out # brand_ink
def test_favicon_svg_themed_carries_the_three_roles() -> None:
"""A palette with distinct ``surface``/``brand``/``brand_ink``
(plus the other 14 built-ins): the three hexes appear in their
exact attribute roles — asserted by PARSING the SVG attributes
(not by substring) — and none of the other 14 built-in hexes
appears anywhere in the output."""
colors = dict(theming.BUILTIN_COLORS)
colors["surface"] = "#102030"
colors["brand"] = "#4f46e5"
colors["brand_ink"] = "#a5b4fc"
out = theming.favicon_svg(colors)
ns = "{http://www.w3.org/2000/svg}"
root = ET.fromstring(out)
assert root.tag == f"{ns}svg"
assert root.get("viewBox") == "0 0 64 64"
hexagon, circle, spokes = list(root)
# Hexagon: fill ← surface, stroke ← brand (geometry unchanged).
assert hexagon.tag == f"{ns}path"
assert hexagon.get("d") == "M32 4 55 18v28L32 60 9 46V18Z"
assert hexagon.get("fill") == "#102030"
assert hexagon.get("stroke") == "#4f46e5"
# Center dot: fill ← brand.
assert circle.tag == f"{ns}circle"
assert circle.get("fill") == "#4f46e5"
# Spokes: stroke ← brand_ink.
assert spokes.tag == f"{ns}path"
assert spokes.get("stroke") == "#a5b4fc"
# None of the other 14 built-in hexes appears in the output.
for key, value in theming.BUILTIN_COLORS.items():
if key not in ("surface", "brand", "brand_ink"):
assert value not in out, (
f"built-in {key} value {value} must not appear in a themed favicon"
)
def test_favicon_svg_geometry_is_invariant() -> None:
"""Geometry invariance: with a themed palette, everything EXCEPT
the four color attributes is byte-equal to the built-in output
(viewBox, both path ``d``s, circle ``cx/cy/r``, stroke widths,
``linejoin``/``linecap``, attribute order — the mask leaves only
the four ``fill="…"`` / ``stroke="…"`` values to differ)."""
colors = dict(theming.BUILTIN_COLORS)
colors["surface"] = "#102030"
colors["brand"] = "#4f46e5"
colors["brand_ink"] = "#a5b4fc"
themed = theming.favicon_svg(colors)
builtin = theming.favicon_svg(dict(theming.BUILTIN_COLORS))
assert _mask_color_attrs(themed) == _mask_color_attrs(builtin)
# Sanity: the mask masked exactly the four color attributes.
assert _mask_color_attrs(builtin).count('"COLOR"') == 4