Live-E2E of the write path (scripts/e2e-local.sh) confirmed the feature end to
end — good URL -> cached /iptv/logos/<hash>, M3U emits the cached URL, cached logo
serves back as PNG, rejected save leaves the prior logo intact — and caught that a
bad/non-image/oversized logo rejects as 422 (BaseError -> ValidationProblemDetails
via ToErrorResult), not 400. 400 remains model-binding-only. Corrected channels.md,
api-conventions.md, and the graphics.channel-logo-caching decision Rule + catalog.
- Medium-1: wrap ExternalLogoMigratorService.ExecuteAsync in try/catch — a DB
exception (e.g. a channel deleted mid-migration -> DbUpdateConcurrencyException)
no longer trips BackgroundServiceExceptionBehavior.StopHost and kills the app;
it logs and self-heals on the next boot. Caller-cancel path handled separately.
- Low-2: CreateChannelHandler/UpdateChannelHandler validation failure now returns
errors.Join() (all accumulated errors) not errors.Head (first only), restoring
the repo-wide convention; regression test added.
- Low-4: corrected the Startup registration comment (migrator self-awaits
WaitForDatabase; order is not load-bearing).
Final whole-branch review: MERGEABLE @ 6d5f6b24 (fable). Carried Minors adjudicated
acceptable-defer.
New lifecycle record graphics.channel-logo-caching (supersedes: none — narrows
for YAML image elements). Regenerated the active catalog. channels.md + api-
conventions.md updated for the new save-time download + 400s.
Design for reversing #511's render-time fetch: an external logo URL is
downloaded once at save time into the artwork cache (content-hash name,
same as an upload), validated against #511's decode budgets, and stored
so nothing downstream knows it came from a URL. Bad URL fails in the
editor, not at 3am. No refresh button — re-add the URL. One-time startup
migration converts existing URL rows; failures leave the row and warn.
refs #511#502