phase: 177_themed_favicon
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:
@@ -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 |
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user