Task 3 of ersatztv#71 (reshuffle-playout). Appends Seed as the last
positional field on PlayoutNameViewModel, threading it through the
Mapper list projection, GetPlayoutByIdHandler detail projection, and
the four Update*PlayoutHandler VM constructions, then exposing it on
PlayoutListItemResponseModel/PlayoutResponseModel via the controller's
ToResponse/ToListItemResponse mappers.
ChannelControllerTests.MakePlayout was an additional construction site
not listed in the task brief (positional-record break); updated to
append 0 for Seed alongside the brief's ErsatzTV.Tests changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The go-live arc (#237) is closed; add a 'Current phase' section stating the
terminal transition to backlog mode + structural guidance (empty arc/gate/review
tiers are the expected steady state; fall through to the priority-label cascade).
Drift-proof: terminal fact + structure only, no named frontier issue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DeleteSmartCollection rollback in CreateAutoTunedChannelsHandler.CreateOne
was called without exception handling; a transient infra exception during
the best-effort rollback would propagate and abort the whole batch,
contradicting the comment's stated intent. Wrap the send in try/catch so
an orphaned SmartCollection is the accepted degraded outcome instead.
- CreateAutoTunedChannelsRequest.ToCommand(): guard null Channels (was NREing on
a request body that omits "channels", causing HTTP 500).
- CreateAutoTunedChannelsHandler.CreateOne: when CreateChannelFromLineup returns
Left (Skipped/Failed), roll back the just-created SmartCollection via
DeleteSmartCollection so retries don't fail on SmartCollection-name uniqueness.
Best-effort; the delete result does not change the outcome.
- PreviewAutoTuneChannelsHandler: filter out proposals whose generated name
exceeds the 50-char Channel.Name limit before number allocation, so numbers
aren't wasted on proposals that can never be created.
- docs/superpowers/specs/2026-07-16-auto-tuning-design.md: fix field-name drift
in JSON examples (proposedNumber -> number, error -> reason) to match the
actual AutoTuneProposal/AutoTuneChannelOutcome DTOs.
Refs #69
Consolidate docs/decisions.md (1923 -> 1028) by extracting four cohesive
topic clusters into docs/decisions/ (optimistic-concurrency,
api-auth-security, release-ci-governance, spa-modularization) — content
relocated verbatim (lossless; all rationale + reversals preserved), main
Index rebuilt to reference the topic files plus the remaining in-file
entries, docs/README.md points back at the decisions Index. Add the
v26.9.0 row to the ci-cd.md version table.
refs #340
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task-by-task TDD plan for the auto-tuning backend: axis map, number
allocator, preview query + EF enumeration, bulk-create orchestration,
REST endpoints, OpenAPI + docs.
Refs #69
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two missing-base-reset defects in the ChicoryTV SPA shell, both confirmed by
rendering the real app (not source-reading):
- #373: the browser-default `body { margin: 8px }` was never reset, so it framed
every full-viewport layout — the app shell and the shell-less `.ctv-auth-page`
boot pages (login/setup/checking/error), both `min-height:100vh` — with a light
border on all four edges. Fix: `html, body { margin: 0 }` + paint the app surface
on `body` so any residual gap/overscroll stays dark (matches the design-system
reset in forms.card.html / chicorytv-admin templates). Verified body margin 0 and
no edge border on the boot page and the shell, warm theme, 1280px and 900px.
- #377: the SPA ships no global `box-sizing` reset (default content-box), so
`.ctv-nav-item { width:100%; padding:0 10px }` overflowed `.ctv-nav`
(`overflow:auto`) by 20px → a horizontal scrollbar in the sidebar. Fix: scope
`box-sizing: border-box` to `.ctv-nav-item`. Verified nav scrollWidth==clientWidth
(216==216) and no horizontal scroll after the fix; the intended vertical nav scroll
is unaffected.
Docs: spa-conventions.md §1 records the base reset and the deliberate absence of a
global box-sizing reset (set border-box locally when combining width:100% + padding).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guard analyzer package references for the inline-version MCP tool and copy the SDK suggestion baseline into the Docker build context before restore.
Refs #15
Co-Authored-By: Codex <codex@openai.com>
Centralize SDK and threading analyzers, baseline the .NET 10 All rule inventory at suggestion severity, and promote S3981 repo-wide. Fix the always-true worker count predicate and cover the idle/active branches.
Fixes#15
Co-Authored-By: Codex <codex@openai.com>
Completes the outstanding Plex flow in the #202 post-merge integration
checklist against a disposable test Plex Media Server: pin-flow auth,
server/library discovery (the 2026-07-13 gap that returned servers:[]),
sync-preference persistence, and sign-out cleanup all passed. Records
exact API state evidence and the test-admin credential reset.
fixes#333
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Independent review found that AdvertisedBaseUrl.TryParse accepts a path
prefix containing '&' (a legal URL-path char kept out of uri.Query), but
GetChannelGuideHandler substituted {RequestBase} raw into pre-built XML
written unescaped — so a configured base like https://host/a&b emitted a
bare '&', malforming the entire XMLTV guide (clients reject the document).
Escape the substituted base with SecurityElement.Escape before the raw
replace, mirroring how {AccessTokenUri} is already pre-escaped (&).
No-op for normal URLs (goldens unchanged); M3U output is untouched (M3U
isn't XML). Adds a regression test asserting '&' → '&' in the guide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ErsatzTV built every absolute M3U/XMLTV URL from the incoming request's
Scheme/Host/PathBase, so a client fetching via a host that downstream
consumers can't resolve (e.g. Dispatcharr over Docker DNS → Kodi) baked
that internal host into programme-image/stream URLs.
Add an optional advertised IPTV base URL, backed by the existing
ConfigElement key/value store (key `iptv.base_url`, no EF migration):
- Central pure Core helper `AdvertisedBaseUrl` (TryParse/Resolve):
validates absolute http(s), no credentials/query/fragment, preserves
port + path prefix, normalizes trailing slash. Blank/invalid falls
back to the request-derived values, so unset output is byte-identical.
- Resolved inside `GetChannelPlaylistHandler` (M3U guide/logo/stream) and
`GetChannelGuideHandler` (both XMLTV {RequestBase} sites) — controllers
stay thin, golden tests untouched.
- New `iptv` settings group: GET/PUT /api/v1/settings/iptv (blank clears,
malformed → 422) + a new IPTV section on the SPA Settings screen.
- Scoped to M3U + XMLTV; HDHomeRun deliberately out of scope. Distinct
from ETV_BASE_URL (which only sets ASP.NET PathBase).
Tests: AdvertisedBaseUrl unit tests (override/fallback/port/path/invalid),
handler override tests for both generators, settings controller + handler
tests, SPA client + screen tests. Docs: m3u-xmltv, decisions, domain-model,
regenerated OpenAPI v1.json + v1.d.ts + endpoint-index.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared Build & test job OOM-killed (exit 137) with coverage enabled: the
whole-solution Architecture.Tests process instruments both EF migration folders
(~2.59M generated lines vs ~200k authored), ballooning coverlet's in-memory
hit-count arrays past the runner's limit.
Add coverlet.runsettings excluding **/Migrations/*.cs (+ generated/compiler
attributes) and pass it via --settings on the Test step. This cuts the
instrumented surface ~126x (2,510,185 -> 19,963 coverable lines in the
Architecture.Tests process, verified locally) and makes the reported percentage
reflect authored code. Documented in docs/ci-cd.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cold-review should-fix on PR #370: `dotnet tool install` errors under set -e
if the tool is already present (retried step / cached runner image), which
would silently degrade the continue-on-error summary step to a no-op. Use
`dotnet tool update` (install-or-update) pinned to 5.5.10 for reproducible
output. Also order coverlet.collector after CliWrap to keep the ItemGroup
alphabetical (nit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#15 scope item 4 ("collect with coverlet and publish a summary; decide on
a floor later"). coverlet.collector was already referenced by four of the
six test projects but the CI Test step never passed --collect and nothing
summarized the result.
- Add coverlet.collector to ErsatzTV.Core.Tests and ErsatzTV.Tests so
coverage is uniform across all *.Tests projects.
- CI Test step now runs --collect:"XPlat Code Coverage" --results-directory
./coverage, then a best-effort Coverage summary step merges the per-project
Cobertura reports with ReportGenerator (TextSummary to the log,
MarkdownSummaryGithub to the job step summary). No floor is enforced yet;
continue-on-error keeps it purely informational.
- gitignore /coverage/; document the step in docs/ci-cd.md (test job).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge PR #366: fix(web) coherent, recoverable Plex authorized-but-no-servers state (#345)
Separates Plex authorization (stored token) from connection (discovered servers)
so the authorized/unlocked/empty-servers state is coherent and recoverable
(offers Sign out, does not claim a connection). Gates the empty-servers
messaging on a released lock so it does not fire during discovery.
fixes#345