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
-7
View File
@@ -1,7 +0,0 @@
<!-- Favicon — served statically (AGENTS.md rule 6) instead of a
data: URI: the phase-82 strict CSP (default-src 'self') blocks
data: images, and the audit's "no external hosts" verification
had missed this one. Byte-identical artwork, extracted from the
former <link rel="icon" href="data:image/svg+xml,…"> (2026-09-08,
phase 82 task 03 — the E2E CSP gate caught the violation). -->
<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>

Before

Width:  |  Height:  |  Size: 757 B

+106
View File
@@ -30,6 +30,42 @@
* effect: the 3 strings keep the brand.js runtime application
* (owner-locked B4) — they apply via the /api/config boot fetch on
* the NEXT page load, and the sub-copy says so.
* • favicon live preview (phase 177, A4 — owner-confirmed
* 2026-10-13) — a pick of one of the THREE driving fields
* (``surface`` / ``brand`` / ``brand_ink`` — the artwork's
* color roles: hexagon fill, hexagon stroke + center dot,
* 4 spokes) ALSO repaints the tab icon: the
* ``link[rel="icon"]`` href is set to
* ``themeFaviconDataUri(...)`` — a ``data:image/svg+xml,``
* URI (``encodeURIComponent``-ed) whose SVG is the
* CLIENT-SIDE MIRROR of ``app.core.theming.favicon_svg``
* (the same artwork, the same roles —
* ``tests/unit/test_theme_frontend.py`` keeps the two from
* diverging: the artwork constants are pinned against the
* server source, the ``PAIRS``-mirror house pattern).
* Rebuilt, not patched: a ``surface`` pick repaints with the
* CURRENT ``brand`` / ``brand_ink`` picks (the helper reads
* the three inputs' values). The ``isHex`` guard (belt-
* and-braces — the color inputs are always valid hex, the
* inputs are the ONLY source of the values, so there is no
* XSS path; the house "never build HTML" rule binds the DOM,
* the URI is built from constants + validated hexes) returns
* ``null`` on a non-hex input and the link is left
* untouched. The other 14 picks never touch the link.
* RESTORE TO THE ROUTE — the settled palette always wins: on
* every SETTLED read (Save success, after the
* ``applyServedTheme`` reconcile; Reset success, after the
* reconcile; the Reset-failure ``clearPreview()`` path — the
* picks are stale; the ``bor:view-refresh`` re-show, after
* its ``applyServedTheme``) the href goes back to
* ``"/favicon.svg"`` — the route re-reads the DB per request
* (NO process cache), so the restored URL is always fresh,
* and the page is ``no-cache``, so the browser revalidates
* the favicon with it. CSP (phase 123, per D4): ``data:`` is
* legal for the favicon via the ``img-src 'self' data:``
* carve-out — CSP3 governs ``link[rel=icon]`` by
* ``img-src``, and the carve-out is image-scoped (``data:``
* never becomes a script/style/fetch source).
* • served-theme sync (phase 92, defect 1) — the phase-91 defect:
* Save/Reset removed the preview overrides, and the page then
* fell back to the <style id="bor-theme"> tag baked into THIS
@@ -124,6 +160,10 @@ export async function mount(root) {
const errorEl = root.querySelector("#theme-error");
const resultEl = root.querySelector("#theme-result");
const contrastEl = root.querySelector("#theme-contrast");
/* The tab icon's link (a document-level element, not in the
view — the favicon live preview writes its href; phase 177,
A4). */
const faviconLink = document.querySelector('link[rel="icon"]');
const SAVE_LABEL = "Save theme";
const RESET_LABEL = "Reset to defaults";
@@ -306,6 +346,53 @@ export async function mount(root) {
}
}
/* ---------- favicon live preview (phase 177 — A4) ----------
* The tab icon is the CLIENT-SIDE MIRROR of the server's
* favicon: app/core/theming.py's favicon_svg is the
* authoritative renderer — the SAME artwork (a hexagon +
* center dot + 4 spokes, viewBox 0 0 64 64), the SAME three
* color roles (hexagon fill ← surface, hexagon stroke +
* circle fill ← brand, spokes stroke ← brand_ink). The two
* must never diverge: tests/unit/test_theme_frontend.py
* pins this file's artwork constants (the viewBox, both path
* d-strings, the circle r) against the server's source (the
* PAIRS-mirror house pattern). CSP (phase 123, per D4):
* data: is legal for the favicon via the img-src 'self'
* data: carve-out — CSP3 governs link[rel="icon"] by
* img-src, and the carve-out is image-scoped (data: never
* becomes a script / style / fetch source). */
const FAVICON_DRIVERS = ["surface", "brand", "brand_ink"];
/* The data: URI for the three driving values — the pure half
of the favicon live preview. The isHex guard (belt-and-
braces: the color inputs are always valid hex, the inputs
are the ONLY source of the values, so there is no XSS path
— the house "never build HTML" rule binds the DOM, the URI
is built from constants + validated hexes) returns null on
a non-hex input: the link is then left untouched. */
function themeFaviconDataUri(surface, brand, brandInk) {
if (!isHex(surface) || !isHex(brand) || !isHex(brandInk)) return null;
const svg =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">'
+ '<path d="M32 4 55 18v28L32 60 9 46V18Z" fill="' + surface
+ '" stroke="' + brand + '" stroke-width="4" stroke-linejoin="round"/>'
+ '<circle cx="32" cy="32" r="6.5" fill="' + brand + '"/>'
+ '<path d="M32 25.5V16M32 48v-9.5M25.5 32H16M48 32h-9.5" stroke="'
+ brandInk + '" stroke-width="3" stroke-linecap="round"/>'
+ "</svg>";
return "data:image/svg+xml," + encodeURIComponent(svg);
}
/* The settled palette always wins (A4): after every SETTLED
read (Save success, Reset success, the Reset-failure
clearPreview path, the bor:view-refresh re-show) the href
goes back to the route — which re-reads the DB per request
(NO process cache, so it is always fresh) and whose
no-cache makes the browser revalidate the favicon with it. */
function restoreFavicon() {
if (faviconLink) faviconLink.href = "/favicon.svg";
}
/* ---------- served-theme sync (phase 92, defect 1) ----------
* The phase-91 defect: after a Save/Reset the preview overrides
* were removed and the page fell back to the <style id="bor-theme">
@@ -500,6 +587,7 @@ export async function mount(root) {
const settings = await loadSettings(); // refetch + re-populate
if (settings) {
applyServedTheme(settings); // reconcile the open document
restoreFavicon(); // the settled palette governs the tab icon
}
/* A failed refetch keeps the current <html> overrides on purpose:
the PUT body came from these very inputs, so they ARE the saved
@@ -518,8 +606,10 @@ export async function mount(root) {
const settings = await loadSettings(); // the env / built-in defaults
if (settings) {
applyServedTheme(settings); // tag removed + overrides dropped
restoreFavicon(); // the settled palette governs the tab icon
} else {
clearPreview(); // the reset landed — the picks are stale
restoreFavicon(); // stale picks — back to the route URL
}
}
@@ -562,6 +652,21 @@ export async function mount(root) {
input.addEventListener("input", () => {
previewColor(f.field, input.value);
updateContrast();
/* The favicon live preview (phase 177 — A4): ONLY the
three driving fields repaint the tab icon; the other 14
picks never touch the link. Rebuilt, not patched — the
helper reads the three inputs' CURRENT values, so a
surface pick repaints with the current brand /
brand_ink picks. A null URI (a non-hex value,
defensive) leaves the link untouched. */
if (FAVICON_DRIVERS.includes(f.field)) {
const uri = themeFaviconDataUri(
inputs.surface.value,
inputs.brand.value,
inputs.brand_ink.value,
);
if (uri && faviconLink) faviconLink.href = uri;
}
});
}
if (saveBtn) saveBtn.addEventListener("click", () => void saveTheme());
@@ -577,6 +682,7 @@ export async function mount(root) {
root.addEventListener("bor:view-refresh", () => {
void loadSettings().then((settings) => {
if (settings) applyServedTheme(settings);
restoreFavicon(); // the settled palette governs the tab icon
});
});
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="description" content="Edit a saved chat answer before it is committed to the docs repository (admin-only).">
<title>Edit doc · Brain of Reese</title>
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/assets/styles.css">
</head>
<body>
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="description" content="Read a document indexed in Brain of Reese.">
<title>Document · Brain of Reese</title>
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/assets/styles.css">
</head>
<body>
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="description" content="Ask anything about your indexed documents — every answer cites the exact doc.">
<title>Brain of Reese</title>
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/assets/styles.css">
</head>
<body>
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="description" content="Admin sign-in for Brain of Reese — unlocks the full Sources catalog and answer tuning.">
<title>Sign in · Brain of Reese</title>
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/assets/styles.css">
</head>
<body>
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="description" content="A shared Brain of Reese conversation — read-only.">
<title>Shared conversation · Brain of Reese</title>
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/assets/styles.css">
</head>
<body>