feat(385): per-channel overrides in auto-tune bulk-create #428

Merged
timothy merged 2 commits from feat/385-autotune-overrides into main 2026-07-18 00:03:53 +02:00
Owner

Auto-Tune DetailPanel backend (#385) — the additive per-channel overrides half.

What

POST /api/v1/channels/auto-tune's AutoTunedChannelRequest gains three optional, backward-compatible per-channel fields (omit = PR1 behavior exactly):

  • templateId — overrides the batch template per channel.
  • advanced — reuses the manual Channel Builder's CreateChannelFromLineupAdvancedOptionsRequest verbatim (same 24-field override set + ToCommand(), same advanced.X ?? template.X stamp-at-create contract). The axis default fills PlaybackOrder only when the caller left it null.
  • logo — uploaded channel image, Sanitized() at the request boundary (#283 stored-XSS defense), forwarded to CreateChannelFromLineup.Logo.

Resolved per channel in CreateAutoTunedChannelsHandler.CreateOne, so one channel's bad override still yields a per-channel Failed/Skipped without aborting the batch.

Deferred (not in this PR)

  • Per-source rotation weights + query corrections → #425. #70's WeightedShuffle reads weights only off MultiCollection join rows; an auto-tuned channel is one SmartCollection (fair-share only). Honoring per-source weights needs a MultiCollection-of-per-source-SmartCollections redesign that contradicts "one live query per channel" — a [PLAN-MODE] design call.
  • Bug initials + fallback colour generated logo — needs persisted Channel state + FFmpeg-pipeline wiring; the uploaded logo covers the image case.

Tests / docs

  • Handler override-threading tests (per-channel wins, axis default preserved, no-override baseline) + request ToCommand() logo-sanitization test. 1105 controller/channel tests green.
  • OpenAPI trio regenerated. Docs: decisions.md (+ Index), api-conventions.md, domain-model.md.

fixes #385

Auto-Tune DetailPanel backend (#385) — the additive per-channel overrides half. ## What `POST /api/v1/channels/auto-tune`'s `AutoTunedChannelRequest` gains three **optional**, backward-compatible per-channel fields (omit = PR1 behavior exactly): - **`templateId`** — overrides the batch template per channel. - **`advanced`** — reuses the manual Channel Builder's `CreateChannelFromLineupAdvancedOptionsRequest` verbatim (same 24-field override set + `ToCommand()`, same `advanced.X ?? template.X` stamp-at-create contract). The axis default fills `PlaybackOrder` only when the caller left it null. - **`logo`** — uploaded channel image, `Sanitized()` at the request boundary (#283 stored-XSS defense), forwarded to `CreateChannelFromLineup.Logo`. Resolved per channel in `CreateAutoTunedChannelsHandler.CreateOne`, so one channel's bad override still yields a per-channel Failed/Skipped without aborting the batch. ## Deferred (not in this PR) - **Per-source rotation weights + query corrections → #425.** #70's WeightedShuffle reads weights only off MultiCollection join rows; an auto-tuned channel is one SmartCollection (fair-share only). Honoring per-source weights needs a MultiCollection-of-per-source-SmartCollections redesign that contradicts "one live query per channel" — a `[PLAN-MODE]` design call. - **Bug initials + fallback colour generated logo** — needs persisted `Channel` state + FFmpeg-pipeline wiring; the uploaded `logo` covers the image case. ## Tests / docs - Handler override-threading tests (per-channel wins, axis default preserved, no-override baseline) + request `ToCommand()` logo-sanitization test. 1105 controller/channel tests green. - OpenAPI trio regenerated. Docs: `decisions.md` (+ Index), `api-conventions.md`, `domain-model.md`. fixes #385
Author
Owner

Independent cold review (review-only agent, cross-model rotation unavailable this window → cold same-model, per the skip-rubric which permits a cold agent) + live-E2E complete.

Cold review verified all seven risk points on the write-path handler: axis-default merge (Advanced.PlaybackOrder ?? axisOrder — confirmed necessary, since CreateChannelFromLineupHandler defaults a null order to Chronological), logo Sanitized() at the boundary (#283), template fallback, batch independence, nullability/no-schema-drift, no shared-reference with mutation, and non-vacuous tests. No BLOCKER/HIGH/MEDIUM; two LOW nits — one addressed (added a per-channel override-failure isolation test), one accepted (redundant defensive ?? None).

Live-E2E (real instance, seeded TV library, scan): POST /api/v1/channels/auto-tune with per-channel overrides → 200/Created (no 500); read-back proved every override persisted and round-tripped distinct from the batch-default baseline — per-channel templateId won over batch, logo persisted, DB-confirmed Chronological + shuffleScheduleItems=true on the schedule; playout built, /iptv/channels.m3u + /iptv/xmltv.xml → 200 with the override logo. Baseline (no overrides) → Created (backward-compat intact).

Head delta since the review's base (3285a3d8 → e91f72c9) is test-only (the isolation test the review requested); the reviewed logic is unchanged and is the shipped logic.

Review-verdict: MERGEABLE @ e91f72c99c8da0c119e717de9b2cff20a0ff9f09

Independent cold review (review-only agent, cross-model rotation unavailable this window → cold same-model, per the skip-rubric which permits a cold agent) + live-E2E complete. **Cold review** verified all seven risk points on the write-path handler: axis-default merge (`Advanced.PlaybackOrder ?? axisOrder` — confirmed *necessary*, since `CreateChannelFromLineupHandler` defaults a null order to `Chronological`), logo `Sanitized()` at the boundary (#283), template fallback, batch independence, nullability/no-schema-drift, no shared-reference `with` mutation, and non-vacuous tests. No BLOCKER/HIGH/MEDIUM; two LOW nits — one addressed (added a per-channel override-failure isolation test), one accepted (redundant defensive `?? None`). **Live-E2E** (real instance, seeded TV library, scan): `POST /api/v1/channels/auto-tune` with per-channel overrides → 200/Created (no 500); read-back proved every override persisted and round-tripped distinct from the batch-default baseline — per-channel `templateId` won over batch, logo persisted, DB-confirmed `Chronological` + `shuffleScheduleItems=true` on the schedule; playout built, `/iptv/channels.m3u` + `/iptv/xmltv.xml` → 200 with the override logo. Baseline (no overrides) → Created (backward-compat intact). Head delta since the review's base (3285a3d8 → e91f72c9) is **test-only** (the isolation test the review requested); the reviewed logic is unchanged and is the shipped logic. Review-verdict: MERGEABLE @ e91f72c99c8da0c119e717de9b2cff20a0ff9f09
timothy closed this pull request 2026-07-17 22:08:55 +02:00
timothy reopened this pull request 2026-07-17 22:08:58 +02:00
timothy force-pushed feat/385-autotune-overrides from e91f72c99c to f2a60a4660 2026-07-17 23:07:10 +02:00 Compare
Author
Owner

Rebased onto origin/main (main advanced 13 commits; the only conflict was append-vs-append in docs/decisions.md, resolved by keeping both entries — no code change). The reviewed logic is byte-identical to the MERGEABLE review at e91f72c9; the head delta e91f72c9 → f2a60a46 is purely the rebase (decisions.md merge). Post-rebase locally: solution builds, no OpenAPI drift, format + BOM clean, 10 auto-tune tests green.

Review-verdict: MERGEABLE @ f2a60a46605938db5445e0f5dd6d8ec09b6c1564

Rebased onto origin/main (main advanced 13 commits; the only conflict was append-vs-append in `docs/decisions.md`, resolved by keeping both entries — no code change). The reviewed logic is **byte-identical** to the MERGEABLE review at e91f72c9; the head delta e91f72c9 → f2a60a46 is purely the rebase (decisions.md merge). Post-rebase locally: solution builds, no OpenAPI drift, format + BOM clean, 10 auto-tune tests green. Review-verdict: MERGEABLE @ f2a60a46605938db5445e0f5dd6d8ec09b6c1564
timothy force-pushed feat/385-autotune-overrides from f2a60a4660 to b9ab2d79f3 2026-07-17 23:33:47 +02:00 Compare
Author
Owner

Rebased again onto origin/main (main advanced via #424 — same append-vs-append decisions.md conflict, resolved keeping both). Diff vs the previously CI-green head f2a60a46 is only main's inherited #424 files + the decisions.md merge — no code/API change (git diff f2a60a46 b9ab2d79 = .husky/pre-push, the new #424 hook, and docs/decisions.md). The reviewed logic is unchanged.

Review-verdict: MERGEABLE @ b9ab2d79f3

Rebased again onto origin/main (main advanced via #424 — same append-vs-append `decisions.md` conflict, resolved keeping both). Diff vs the previously CI-green head f2a60a46 is **only** main's inherited #424 files + the decisions.md merge — **no code/API change** (`git diff f2a60a46 b9ab2d79` = `.husky/pre-push`, the new #424 hook, and `docs/decisions.md`). The reviewed logic is unchanged. Review-verdict: MERGEABLE @ b9ab2d79f3535bab174467092bc951e61997fda4
timothy scheduled this pull request to auto merge when all checks succeed 2026-07-17 23:34:11 +02:00
timothy merged commit 1abc5df18c into main 2026-07-18 00:03:53 +02:00
timothy deleted branch feat/385-autotune-overrides 2026-07-18 00:03:55 +02:00
Sign in to join this conversation.