Commit Graph
6 Commits
Author SHA1 Message Date
timothy 8b9a7ed541 feat(414): stamp immutable Channel.Origin (auto-tuned vs user-created) and surface it (#575)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m23s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 14m21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 19m9s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 12m1s
Co-authored-by: Timothy <timothy.look@gmail.com>
Co-committed-by: Timothy <timothy.look@gmail.com>
2026-07-23 18:12:25 +00:00
timothyandClaude Opus 4.8 928784ba48 fix(135): from-lineup advanced overrides can express "clear to none"
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 17s
PR Gates / Docs update reminder (pull_request) Successful in 19s
PR Gates / decisions lifecycle (pull_request) Successful in 22s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m44s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 17m8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 20m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m41s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
CreateChannelFromLineupHandler resolved every advanced override with
advanced.X ?? template.X, so null always meant INHERIT and a channel could
not drop a template-set watermark / filler / preferred language. Add an
optional typed `clear` enum list to CreateChannelFromLineupAdvancedOptions:
omitted/null still inherits (byte-stable for existing clients), a field named
in `clear` is forced to none. Set+clear of the same field is a 422.

The enum (CreateChannelFromLineupClearField) lives in ErsatzTV.Core so the
OpenAPI string-enum scan renders it as a string enum, matching every sibling
advanced-options enum. Handler resolves clearable fields once via
ResolveClearable and validates set/clear conflicts via ValidateClear;
reference validation skips existence checks for cleared (null) refs.

SPA: the shared advancedOptions model re-adds a real "None" option to the five
id selects (watermark + fillers) in both the Channel Builder and the Auto-Tune
DetailPanel, routed through a CLEAR overrides sentinel that applyOverridesToRequest
folds into advanced.clear (never leaking onto the wire as a field value). The
backend enum also covers the preferred audio/subtitle language strings for
machine clients; the SPA text inputs keep "empty = inherit" (tri-state deferred).

Docs: api-conventions.md §2, spa-conventions.md §11, decisions.md record
api.from-lineup-clear-to-none; v1.json + generated TS regenerated.

fixes #135

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:25:58 +02:00
timothyandClaude Opus 4.8 93188eeb5e feat(525): download external-url logo on channel create + create-from-lineup
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:13:21 +02:00
timothyandClaude Opus 4.8 c0da414a4c fix(70): close the review blockers — third playlist writer, weight bounds, overflow
Adversarial review of PR #402 returned BLOCKED. It could not break the WRR math or
the stateless-restore claim (it probed restore across wraps at indices 12/13/20/37
— all held, and the clamp preserves a 1000:1 ratio exactly). What it broke was the
perimeter.

B1 — the validation gate had a hole, so the silent-drop bug shipped.
CreateChannelFromLineup is a THIRD writer of PlaylistItem.PlaybackOrder; its own
guard only covered MultiCollection entries, so a 2+ entry lineup of plain
collections persisted WeightedShuffle straight through to PlaylistEnumerator's
null-drop. My decisions.md claim that "the silent sites never see it" was false as
written — corrected in place, with the lesson recorded: grep every writer of the
field, the non-obvious composite handler is the one that gets missed. The
Add*ToPlaylist handlers are safe only because they hardcode their order.

B2 — Weight had no validation at all, and create/update disagreed on the same
input. EF's HasDefaultValue(1) substitutes 1 for a 0 on INSERT (0 reads as "not
set") but an UPDATE writes the 0 through — and a 0-weight source was filtered out
of the rotation, deleting it from the channel silently. Exactly the failure this
order is careful to avoid everywhere else. Now bounded 1..1000 by a shared
MultiCollectionItemWeight used by both paths so they cannot drift, and clamped
again in the enumerator for rows that predate the gate.

B3 — Sum(weights) is checked arithmetic, so two int.MaxValue weights threw
OverflowException from inside a playout build. Reachable through the API precisely
because of B2. The ceiling fixes both; the sum also widens to long.

M1 the lineup mirror now allows WeightedShuffle for multi collections, matching the
PlayoutModeMustBeValid change it claims to mirror. M3 ScheduleAsGroup is documented
as deliberately unread by this order. L1 MinimumDuration is computed over every
source instead of the current rotation — under the clamp a rotation is a strict
subset and is rebuilt each wrap, so caching over it went stale. L2 the retry guard
keys off the rotation, not the raw collection count.

N1 the tautological default test is gone: it built entities in C#, so it asserted
the property initializer, not the migration — it could not have failed. Replaced
with clamp, overflow, and cross-wrap restore cases (the property the review proved
but found unpinned).

H1 the two follow-ups the PR body claimed were "filed" did not exist. Now filed:
#403 (silent dispatch-fallback hardening) and #404 (SPA weight UI, blocked-by #388).

Core.Tests 565 passed, ErsatzTV.Tests 1643 passed, 0 failed.

Refs #70

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:00:46 +00:00
timothyandClaude Fable 5 cf36c30997 fix(api): rework from-lineup to generated playlist design + review fixes (#63)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m23s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m54s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adversarial review found the previous multi-flood design non-viable:
PlayoutModeSchedulerFlood never yields to a following Dynamic-start
schedule item, so only the first item ever played, and grouping
media items into a Collection silently dropped the requested order.

Redesign:
- Single-item lineup: one ProgramScheduleItemFlood referencing the
  target directly (media item / collection / smart / multi / rerun /
  playlist); no generated collection or playlist. Response PlaylistId
  is null.
- Multi-item lineup (>= 2): one generated IsSystem Playlist in a
  get-or-created IsSystem PlaylistGroup ("Channel Lineups"), one
  PlaylistItem per entry in lineup order with PlayAll=true, referenced
  by a single Flood schedule item. Rerun collections and playlists are
  rejected (422) in multi lineups (PlaylistItem/CollectionKey lack the
  fields to enumerate them).

Review fixes:
- Normalize + strict-validate MediaType<->CollectionType pairs once up
  front (422 on mismatch / wrong id / not exactly one id).
- Reject MultiCollection with non-Shuffle order (mirrors
  PlayoutModeMustBeValid), Mirror playout source, all via 422.
- OnDemand parity: queue TimeShiftOnDemandPlayout post-commit.
- De-collide generated ProgramSchedule and Playlist names against their
  unique indexes instead of leaking a UNIQUE-constraint DbUpdateException.
- Generic 422 on save failure + ILogger; AnyAsync existence checks;
  Either/Validation unwrap via Match; XML doc on request DTO + endpoint.
- Response model: ChannelId, PlaylistId (nullable), ProgramScheduleId,
  PlayoutId (CollectionId removed). Regenerated OpenAPI v1.json + v1.d.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:09:00 +02:00
timothy 6857a0d191 feat(api): composite create-channel endpoint (#63)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m58s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-06 20:27:02 +02:00