feat(favicon): per-app SVG letter-mark fallback in every samohost-generated vhost - #185
Conversation
…ntation Tests cover: - faviconSvg(appName): letter-mark SVG, deterministic color hash, no SAMO branding (#4263eb/#0c7f4d), no $ or backtick (heredoc-safe), single-quoted attrs - faviconVhostLinesStatic/Node: Caddy snippet shape, file-first / fallback, image/svg+xml - staticMainVhostLines: new optional appName param, favicon block before closing brace - renderVhost: favicon block before catch-all, correct letter, content-type, back-compat - No SAMO platform indigo on any of the 6 client apps Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…erated vhost Implements git-first favicon provisioning so every client app served by samohost gets a favicon at deploy time with no per-repo chore. Apps that ship their own favicon are not overridden. Root cause fixed: samohost-generated vhosts had no favicon route, causing all 6 client apps (samograph, field-record, samorev, game-changers, friends-of-twin-peaks, gregg-brandalise) to show the browser's default "no favicon" icon. Changes: - src/caddy/favicon.ts (new): faviconSvg(appName) generates a 32x32 SVG letter-mark (first letter, hue from djb2 hash, no SAMO platform colors #4263eb/#0c7f4d). faviconVhostLinesStatic/Node() return Caddy snippet lines for each generator. - src/app/heal-script.ts: staticMainVhostLines() gains optional appName param; when present inserts favicon fallback handle before closing brace. All 3 callers (heal, deploy/script.ts, bootstrap.ts) now pass app.name. - src/caddy/render.ts: VhostPlan gains optional appName field; renderVhost() inserts favicon handle block BEFORE the catch-all when appName is set. planFromApp() sets appName=app.name so all node app vhosts get the block automatically. - test/favicon.test.ts: 54 tests covering SVG generation, Caddy snippet shape, staticMainVhostLines integration, renderVhost integration, no SAMO branding. - test/fixtures/samograph-main-vhost.caddy: updated to include favicon block (caddy-a1 golden test pins new expected shape). Caddy validate: both static and node vhost shapes pass `caddy validate`. No extra CI infra needed; auto-heal deploys to all 6 apps within 2 timer cycles (~6 min) after merge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The rebase of the GREEN commit onto the RED commit left a conflict marker in the favicon block position test. Resolved to use the multi-route plan form (which correctly creates a catch-all handle to test ordering against). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Caddy's caddyfile adapter reorders directives by built-in precedence —
try_files (which becomes a `rewrite` handler) is always placed BEFORE
`handle` blocks regardless of source order. The `try_files =404` sentinel
causes Caddy to short-circuit with 404 before any later handle can fire,
making the favicon handle block silently dead code.
Fix: wrap the entire static vhost body in a `route {}` directive, which
opts out of the adapter's reordering and preserves exact source order.
Inside the route block, favicon handles appear BEFORE try_files.
Also restructures the static favicon logic to separate /favicon.svg
(always serves generated SVG) from /favicon.ico (serves app's own file
if present, otherwise falls back to generated SVG). This correctly
handles the case where an app ships its own favicon.ico without
accidentally returning 404 for /favicon.svg.
Local proof (caddy validate + curl):
/favicon.svg (no file) → 200 image/svg+xml letter-mark SVG
/favicon.ico (no file) → 200 image/svg+xml letter-mark SVG
/favicon.ico (file exists) → 200 app's own file (NOT overridden)
/favicon.svg (file exists) → 200 image/svg+xml letter-mark SVG
/index.html (exists) → 200 correct content
/nonexistent (absent) → 404 (normal behavior preserved)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
VERDICT: REQUEST CHANGES Gate status
Findings1. BLOCKING — Cache headers silently dropped for all static apps —
|
Two categories of tests that FAIL on the current implementation: 1. REGRESSION — cache headers lost on favicon-enabled static vhosts: faviconVhostBodyLines() omits staticCacheHeaderLines(), so every static app using the favicon code path (game-changers, samorev, friends-of-twin-peaks, gregg-brandalise) loses Cache-Control: public/immutable on fingerprinted assets and Cache-Control: no-cache on documents. Tests assert both headers ARE present in staticMainVhostLines output (WITH appName) AND in the full buildConfigHealScript output. 2. NAME GUARD — hostile appName not validated: app.name flows unvalidated into Caddy respond strings and SVG text. Tests assert faviconVhostBodyLines and faviconVhostLinesNode THROW on names containing '"', '<', '&' (Caddy/SVG-breaking chars), and DO NOT throw on the real production names. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ame guard
BLOCKING FIX — cache headers restored on favicon-enabled static vhosts:
faviconVhostBodyLines() was omitting staticCacheHeaderLines(), so all
static apps using the favicon code path (game-changers, samorev,
friends-of-twin-peaks, gregg-brandalise) lost:
- Cache-Control: public, max-age=31536000, immutable on fingerprinted assets
- Cache-Control: no-cache on documents (/, *.html, config.js, version.json)
Fix: emit staticCacheHeaderLines("\t\t") INSIDE the route{} block,
before try_files. The @samohost_immutable regexp matches only fingerprinted
filenames (hash suffix) and @samohost_documents path matches only HTML/config
documents — neither matches /favicon.ico or /favicon.svg, so the favicon
handles are fully compatible with the cache matchers in the same route{}.
Also corrects the wrong justifying comment in staticMainVhostLines().
NITS FIX — defensive name guard:
App names are operator-controlled and currently all [a-z0-9-], so this
is not a live risk. Added assertSafeAppName() guard to faviconVhostBodyLines
and faviconVhostLinesNode that throws on names containing '"', '<', '&' or
any other char outside [a-zA-Z0-9_.-]. Makes the safe-charset assumption
explicit and machine-checked.
Also fixes the tlsLine indent (was \t\t, now \t — site-block level).
Caddy validation: both static and node vhost outputs pass `caddy validate`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
src/caddy/favicon.tswith a deterministic per-app SVG letter-mark generator (first letter of app name, hue from djb2 hash, no SAMO platform branding colors)staticMainVhostLines) and node-app vhosts (renderVhost) so every samohost-deployed app gets a favicon at first deploy/favicon.icoare not overridden: static apps use a Caddyfilematcher; node apps usehandle_response @fav404to only serve the letter-mark on upstream 404staticMainVhostLines(heal-script, script.ts, bootstrap.ts) updated to passapp.name— maintains the three-way byte-identity contractRoot cause
samohost-generated Caddy vhosts had no
/favicon.icoroute, so all 6 client apps (samograph, field-record, samorev, game-changers, friends-of-twin-peaks, gregg-brandalise) showed the browser's default "no favicon" icon.Fix layer
No per-repo edits. Fix is at the routing/deploy layer in the two Caddy generators. The existing 3-min auto-heal timer will converge all 5 app VMs within ~6 min after merge.
Constraints met
#4263eb, no#0c7f4d)appNameparam get no favicon block (optional param)validatepasses for both static and node vhost shapesTest plan
test/favicon.test.ts— SVG generation, Caddy snippet shape, staticMainVhostLines integration, renderVhost integration, no SAMO brandingcaddy validatepasses locally for both static and node vhost shapescaddy-a1golden test updated to include favicon block in fixtureheal-static-vhost-byte-identity.test.ts) still passPost-merge apply plan
After merge, the
samohost-trigger.timer(every 3 min) will detectgeneratorShadrift for all 6 apps and auto-heal. No manual steps required. Verify after ~6 min:🤖 Generated with Claude Code