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:
+65
-1
@@ -18,7 +18,7 @@ from pathlib import Path
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import FileResponse
|
||||
from starlette.responses import FileResponse, Response
|
||||
|
||||
from app.api.admin_settings import router as admin_settings_router
|
||||
from app.api.auth import router as auth_router
|
||||
@@ -45,6 +45,7 @@ from app.api.sync import router as sync_router
|
||||
from app.api.tokens import router as tokens_router
|
||||
from app.api.ui_settings import router as ui_settings_router
|
||||
from app.config import get_settings, invalidate_settings
|
||||
from app.core import theming
|
||||
from app.core.auth import ensure_admin_configured
|
||||
from app.core.caching import configure_caching
|
||||
from app.core.compression import SelectiveGZipMiddleware
|
||||
@@ -55,6 +56,7 @@ from app.core.security_headers import SecurityHeadersMiddleware
|
||||
from app.core.settings_migrate import migrate_legacy_docs_creds
|
||||
from app.core.settings_overlay import enabled as settings_db_enabled
|
||||
from app.core.settings_overlay import load_overrides, register_invalidator
|
||||
from app.db import SessionLocal
|
||||
|
||||
configure_logging()
|
||||
configure_debugging()
|
||||
@@ -96,6 +98,55 @@ def _shell_routes(app: FastAPI, static_dir: Path, paths: tuple[str, ...]) -> Non
|
||||
)(_shell_view)
|
||||
|
||||
|
||||
def _favicon() -> Response:
|
||||
"""Phase 177 (task 02, D2 — A2/A3 owner-confirmed): the themed
|
||||
favicon over the wire.
|
||||
|
||||
Renders :func:`app.core.theming.favicon_svg` from the EFFECTIVE
|
||||
palette on EVERY request — one short-lived ``SessionLocal()``
|
||||
session, the ``caching.py`` middleware's house pattern: NO process
|
||||
cache, so a runtime theme change (the admin Theme tab's Save) is
|
||||
visible on the next request without a restart (a single-row SELECT
|
||||
is negligible at homelab traffic). ANY exception (a DB blip, a
|
||||
pre-migration boot) falls back to the BUILT-IN palette with 200 —
|
||||
never 500 (the theme-tag's never-break contract, ``caching.py``'s
|
||||
DB-blip fallback): the tab icon degrades to the built-in art, the
|
||||
app never errors on it.
|
||||
|
||||
Deliberately OUTSIDE ``/api/*``: no per-response byte-budget line
|
||||
(AGENTS rule 10 is ``/api/*``-only) and NO auth dependency — the
|
||||
favicon is chrome, public like the login page.
|
||||
``CachingMiddleware`` passes the response through untouched
|
||||
(``/favicon.svg`` is neither in ``HTML_PAGES`` nor under
|
||||
``/assets/``) → the route's own ``Cache-Control: no-cache`` is the
|
||||
freshness contract (the palette changes at runtime; the pages are
|
||||
no-cache too, so the browser revalidates the favicon together with
|
||||
them — freshness is revalidation, not a ``?v=`` bust). The route is
|
||||
not under ``/assets/``, so its ``<link>`` ref can never be touched
|
||||
by the ``?v=`` rewrite. ``SecurityHeadersMiddleware`` still adds the
|
||||
CSP — no change needed (the phase-123 ``img-src 'self' data:``
|
||||
carve-out covers task 04's ``data:`` preview; CSP3 governs
|
||||
``link[rel=icon]`` by ``img-src``).
|
||||
"""
|
||||
try:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
effective = theming.effective_settings(db)
|
||||
finally:
|
||||
db.close()
|
||||
svg = theming.favicon_svg(effective)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"favicon: theme read failed — serving the built-in favicon"
|
||||
)
|
||||
svg = theming.favicon_svg(theming.BUILTIN_COLORS)
|
||||
return Response(
|
||||
svg,
|
||||
media_type="image/svg+xml",
|
||||
headers={"Cache-Control": "no-cache"},
|
||||
)
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
# Fail loud BEFORE serving anything (phase 16): missing
|
||||
# BOR_ADMIN_PASSWORD / BOR_SESSION_SECRET raises at boot, naming the
|
||||
@@ -189,6 +240,19 @@ def create_app() -> FastAPI:
|
||||
app.include_router(chats_public_router, prefix="/api")
|
||||
app.include_router(chats_shared_page_router) # no prefix — /shared/<token>
|
||||
|
||||
# Phase 177 (task 02, D2 — A2/A3 owner-confirmed): the themed
|
||||
# favicon route — registered AFTER the router includes and BEFORE
|
||||
# the static catch-all mount (routes-first, the phase-51/76
|
||||
# pattern): a file named favicon.svg in the static dir could never
|
||||
# shadow it. HEAD joins GET for static-file parity (the
|
||||
# phase-76 ``_shell_routes`` parity idiom — the pre-phase static
|
||||
# file answered both methods, the dynamic route keeps that).
|
||||
# See the handler docstring for the never-break DB-blip fallback
|
||||
# and the /api/*-out rationale.
|
||||
app.api_route(
|
||||
"/favicon.svg", methods=["GET", "HEAD"], include_in_schema=False
|
||||
)(_favicon)
|
||||
|
||||
# Cache busting (phase 33): the five HTML pages revalidate (no-cache)
|
||||
# with ?v=<token> asset refs; /assets/* becomes immutable for a year.
|
||||
# Added after the session middleware, so it wraps the whole app
|
||||
|
||||
Reference in New Issue
Block a user