Commit Graph
11 Commits
Author SHA1 Message Date
timothy 1a7f15fb27 fix(510): address independent review — Resource null guard, honest routing claim
Two independent reviews (cross-family Codex + cold Opus) both returned
BLOCKED. Findings, all verified against source before acting:

1. Resource arm could throw ArgumentNullException (Codex, Medium). Making the
   channel/global Resource arm reachable exposed that CreateWatermarkHandler
   and UpdateWatermarkHandler write `Image = null` for EVERY non-Custom
   watermark, so an API-created Resource watermark reached
   Path.Combine(folder, null). Added the blank/null guard the arm never had.
   This was live at the playout-item level too, not just newly-reachable code.

2. "Routing is unaffected" was false (Codex, Low but sharp). The predicate is
   unchanged, but CanUseFFmpegNativeWatermark also tests Count == 1, and
   dropping an unresolvable watermark shortens the list. A deco with one valid
   and one missing permanent watermark now routes ffmpeg-native where it
   previously routed to the graphics engine. Intended, but observable -- so it
   is documented and pinned by a test rather than claimed away.

3. "Exactly one resolver" over-claimed (Opus, High). True of the selector, not
   the application: the song-progress overlay is built as a WatermarkOptions
   directly by the streaming and troubleshooting handlers, unchecked, and can
   still hand ffmpeg a nonexistent -i. Pre-existing; scoped the claim in the
   record and channels.md and filed #653.

4. Undeclared crash->degrade change (Opus, Medium). Channel/global Custom had
   no blank-image guard, so a cleared image hit ImageCache's fileName[..2] and
   threw out of stream startup. Now declared in the record and tested.

5. Contradictory rule text (Opus, Medium) -- the catalog one-liner said
   "always no bug" while the body documents the playout-item fall-through
   exception. Qualified; catalog regenerated.

6. History was wrong in both the record and the XML docs: the three precedence
   levels did NOT all check every source -- channel/global had no Resource arm
   and threw. Corrected.

Tests: 30 in the fixture now (was 18). New coverage for the preserved
blank-Custom fall-through (to channel AND to global), the complement case
(missing-but-named must NOT fall through), null/blank Resource, and the
valid+missing routing case. 17 of 24 failed against the pre-fix resolver
before this round; the fixture stays mutation-sensitive.

Also: hoisted the mock-filesystem Initialize() out of its loop so a
multi-file case cannot silently seed only the last file, and marked the
generated-URL test honestly as redundant-by-construction rather than
claiming independent coverage.

The decision record is now 81 prose lines, over the 60-line ceiling. Declared
as a legitimate decline per docs.corpus-size-signal: the length is the review
findings above, each a distinct fact, not redundancy.

refs #510 #652 #653
2026-07-26 21:12:13 +02:00
timothy 9cbe70e486 fix(510): one watermark resolver for all four attachment points
WatermarkSelector resolved watermarks in two places with two policies. The
three precedence levels (playout item, channel, global) existence-checked
every image source and degraded to None; the deco path had its own copy of
the same switch that returned whatever path it computed, unchecked. So one
channel could disagree with itself about whether an on-screen bug rendered,
based only on how the watermark was attached.

#502 deferred this here but scoped it to ChannelLogo. It was never
ChannelLogo-only: the deco path skipped the existence check for Custom and
Resource too. Extract one ResolveWatermark used by all four sites.

Severity is not cosmetic. A dead LOCAL path is not harmlessly skipped --
CanUseFFmpegNativeWatermark hands a single permanent watermark to ffmpeg as
a bare -i argument and excludes only URLs, so the deco path could hand
ffmpeg a nonexistent input file.

The generated-initials nameplate was real: a live-E2E on a real transcoded
frame confirmed it composited via the deco path (/iptv/logos/gen is on
ArtworkController, which has no auth filter, so the container-internal
self-fetch succeeded). The #502-era comment claiming "it has never rendered
here" was wrong, and the new record says so. It is still removed: serving it
means an HTTP fetch inside stream startup, which graphics.channel-logo-caching
(#525) eliminated for logos, and it depends on #1's hardcoded localhost.
Reviving it by caching the image instead is #652.

Measured blast radius on prod: 0 Deco rows, 0 DecoWatermark rows, all 43
channels have logo artwork -- no rendered output changes.

Preserved deliberately: a playout-item Custom watermark with a blank image
still falls THROUGH to the channel/global watermark; unifying resolution must
not change which watermark wins. Routing is untouched.

Strict improvement: the channel and global arms previously threw
NotSupportedException on a Resource watermark; they now resolve it. The
default arm still throws so a new image source fails loudly.

Tests: 18 new cases including a positive control and 8 deco-vs-channel parity
cases. 12 of the 18 fail against the pre-fix resolver, which is what proves
they are load-bearing rather than vacuous.

fixes #510
2026-07-26 20:54:00 +02:00
timothyandClaude Opus 4.8 b7d58bbf32 docs(74): tighten overlay suppression rule (Merge-during-filler also clears)
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 15s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 17s
PR Gates / Docs update reminder (pull_request) Successful in 20s
PR Gates / decisions lifecycle (pull_request) Successful in 22s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 16m45s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 16m51s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 21m18s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Folds whole-branch review finding #2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 22:11:46 +02:00
timothy c80839b338 docs(74): channel-level graphics attachment + On Now/Next overlay 2026-07-22 22:11:46 +02:00
timothyandClaude Opus 4.8 dfed9a393b fix(68): rebuild on-demand channel guide (and mirrors) on thaw
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 27s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m19s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m26s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m52s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
An on-demand channel (`PlayoutMode.OnDemand`) already is the "resume where I
left off" feature: `Playout.OnDemandCheckpoint` persists the viewer's position
and `PlayoutTimeShifter.TimeShift` slides the materialized timeline forward on
tune-in so the paused item is active again. Because it rewrites `GuideStart`/
`GuideFinish` alongside `Start`/`Finish`, guide and playback freeze together —
structurally avoiding the free-running-wall-clock desync #68 was filed about.

The one gap: `TimeShift` rewrote the stored `PlayoutItem` rows but the XMLTV
guide is served from a cached fragment that only `RefreshChannelData` rebuilds,
and the tune-in path never enqueued it. So an external EPG client polling after
a thaw could see a stale timeline until the next incidental rebuild.

Fix: `IPlayoutTimeShifter.TimeShift` now returns the channel numbers whose cached
guide is stale — the shifted channel plus any channels that mirror it (the same
fan-out `BuildPlayoutHandler` already does) — and `TimeShiftOnDemandPlayoutHandler`
enqueues a `RefreshChannelData` for each on `CancellationToken.None` (post-commit
side effect must not be abandoned if the session token cancels).

Tests: handler enqueues a rebuild per stale channel (+ mirror + no-shift cases);
`PlayoutTimeShifter` reports source+mirrors on a shift, empty on Continuous /
zero-offset / active-unforced, and correctly seeds+shifts a never-watched playout.
Non-vacuity of the enqueue proven by a compiling negative control.

Docs: channels.md (On-demand resume section), domain-model.md, decisions.md
(scheduling.ondemand-guide-refresh-on-thaw). Per-viewer resume is out of scope
(single per-channel checkpoint; #68 says per-channel suffices).

fixes #68

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 19:12:42 +02:00
timothy aa76994825 docs(525): correct logo-download rejection status to 422, not 400
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 8s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / decisions lifecycle (pull_request) Successful in 24s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m21s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m21s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m34s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 13m59s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m49s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
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.
2026-07-21 13:19:07 +02:00
timothy 56a18cd5dc docs(525): record download-on-save decision; update channels + api-conventions
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.
2026-07-21 13:18:48 +02:00
timothy 893d4a6398 test(67): de-vacuify the template-stamping assertion; cover adopt-then-delete
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 11s
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 1m6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m25s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 16m1s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 20m26s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 21m19s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Whole-branch review found the stamping test was structurally vacuous: a bare
foreach over ChannelTemplates.Where(IsSystem) passes with zero iterations, so
the test would have stayed green if template seeding silently bailed out.
Assert the collection is non-empty first. Proven non-vacuous by a negative
control (forcing SeedChannelTemplates to bail makes exactly this test fail).

Also cover the ACTUAL production sequence -- adopt an existing hand-made row,
then delete it -- which the previous no-resurrect test did not exercise (it
covered seed-then-delete). The marker is written on the adopt path too, so
the deleted row must stay deleted.

docs: note that a deleted preset degrades to no default rather than failing,
and that the default applies to newly created channels, not retroactively.

Refs #67
2026-07-20 21:13:47 +02:00
timothy bb7f57d04d docs(67): record the shared-preset decision, watermark seeding, and the additive DTO field 2026-07-20 20:48:14 +02:00
timothyandClaude Opus 4.8 62c15da490 docs(73): document seasonal / date-conditional scheduling (already implemented)
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Has been cancelled
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been cancelled
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been cancelled
fixes #73

#73's premise ("ErsatzTV has no native date-conditional scheduling today")
is false. The predicate (IAlternateScheduleItem) is shipped on both engines,
evaluated by AlternateScheduleSelector, reachable from the SPA, and already
unit-tested — so the only real gap was discoverability, and it was a docs gap:
the mechanism was documented as a mechanism, never as a task.

- channels.md: task-shaped "Recipe: seasonal / holiday programming" for both
  engines, plus the gotchas — chiefly that blank StartYear/EndYear is what
  makes a range repeat every year (AlternateScheduleSelector.cs:32-40), that
  the override branch needs BOTH years, and that explicit years disable
  wrap-around detection.
- domain-model.md: glossary row (mirrors the #77 "already exists" row).
- decisions.md: entry recording the verdict and the rejected asks — notably
  that "prioritize collection X during a date range" is #70's weighting
  problem, not a second primitive built here while #70 is mid-flight.

No production code changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 09:20:05 +02:00
timothyandClaude Opus 4.6 f1e97b94a7 Add architecture docs and fork maintenance strategy (#6)
Build / Calculate version information (push) Successful in 13s
Build / build_and_upload (push) Failing after 0s
Build / build_images (push) Failing after 0s
Close stale issues / stale (push) Successful in 15s
Document channel architecture, M3U/XMLTV integration with Jellyfin,
and fork maintenance strategy for the archived upstream. Also includes
CLAUDE.md updates for implementer workflow and project boundaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:42:07 +01:00