aa32fd78dd68bf476df6d0ff464e15fa2ce85e5d
424
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
aa32fd78dd |
fix(477): guard media-server library sweeps against successful-but-empty fetches
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m23s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m20s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m18s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
A successful fetch returning zero items made existing.Except([]) flag the ENTIRE library FileNotFound in one scan — feeding EmptyTrashHandler's permanent delete and emptying every affected collection (dead channels). Add a shared MediaServerReconciliationGuard that skips (and logs a Warning) the sweep when incoming==0 while items exist, wired into the three library-level sweeps (Television shows / Movie / OtherVideo). An empty incoming set is indistinguishable at scan time from a mid-restore / emptied-upstream error (both report a zero total), so this deliberately overrides #476's degenerate "last item removed => empty incoming => flag" case. #476's cascade still fires for partial deletions (survivors present); its characterization test moves from an empty incoming to a survivor+removed partial-deletion case. Tests: policy table (MediaServerReconciliationGuardTests) + per-scanner integration proving the wiring (empty incoming + non-empty existing flags/reindexes nothing). Proven non-vacuous by neutralizing the guard. Nested TV season/episode sweeps left unguarded (bounded blast radius); ratio-threshold + projection-failure detection deferred to a follow-up. docs/decisions.md updated. Fixes #477 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1eff0b39b7 |
fix(473): gate the cancellation filter on the caller's token
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 5s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 16s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m17s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m28s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Third review pass: MERGEABLE WITH NITS. Taking the one finding it asked for before merge, plus a doc nit. The cancellation filter added last commit had no token check, and it spans the whole Transcode body -- including every mediator send (ffprobe via CliWrap, media-server API calls, subtitle extraction, song-video generation). TaskCanceledException is also what HttpClient throws on its OWN timeout, so a real timeout in any of those was being downgraded from an ERROR with a stack trace to a routine "Terminating HLS session" Information line. Behaviour was unchanged (both arms return false) but the fault signal was lost, and this repo has been bitten before by "empty log != the event didn't happen". Now filters on cancellationToken.IsCancellationRequested, so only genuine caller cancellation is treated as a graceful teardown. Doc nit: the <exception> block said cancellation "is thrown"; it is only thrown when the token trips while the probe is in flight -- cancelling after it completes returns normally. Now says "may propagate". Declined the reviewer's optional suggestion to drain until a 0-return instead of reading exactly one byte: reading exactly one byte is what makes the guard safe BY CONSTRUCTION, since a server or proxy that answers 206 with a wider range than requested still cannot be drained unboundedly. 206-only was confirmed correct rather than extended to short 200s, since deciding "short" from Content-Length would reopen the unbounded path for a chunked or Content-Length-less response. Also records the operator's standing rule in the handoff lore: a lone `decisions.md append-only` red is a known infra flake -- do not investigate, rebase, amend or push to clear it; the operator reruns that job from the UI. I violated this earlier in this PR with a tidy-but-wrong "my entry is no longer at EOF" theory, and the rebase did not fix it -- the job went red again on a verified pure-append diff, which is the proof the red was never about the diff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bad19f8d26 |
fix(473): review fixes — only a redirected 404 fails closed
Adversarial review of PR #479 found the stated fail-open contract was not what the code measured, plus four smaller gaps. All fixed here as a follow-up commit (no amend/force-push). High — a 404 from ErsatzTV's OWN endpoint was treated as "media gone". /media/{provider}/... is served by InternalController, which returns NotFound when the media source is unconfigured or momentarily missing (a media-source edit that deletes+reinserts connections, a restore, a partially-configured server). Probing for "any 404" therefore failed CLOSED for every item on that source -- exactly the case the fail-open contract exists to prevent. A media-server 404 always arrives after a redirect, so an un-redirected 404 is now treated as available. Medium — the new switch label was untested and its benefit overstated. maybeDuration/finish are computed before the switch, so `default:` already sized the error card to the next playout item; the label only changes the caption. The handler test asserted call counts only, so deleting the label still passed. It now asserts the error message, and removing the label fails the test (verified). Medium — Plex/Emby branches changed but had no coverage. Added an Emby handler test asserting the probe is called with the emby URL. Low — caller cancellation was swallowed and pinned as desired behaviour. A shutdown / client disconnect is a genuine signal, not a probe failure; it now propagates, and only the probe's own 2s timeout fails open. Low — the response stream was disposed unread, aborting the connection instead of returning it to the pool. The one requested byte is drained. Nit — fully-qualified RangeHeaderValue replaced with a using. docs/decisions.md corrected where it overstated: the switch label's role, the "fixes the class for all three media servers" claim (external-JSON channels bypass ValidatePlayoutItemPath entirely -- filed as #480), and the unmeasured latency assertion. Deferred HEAD-instead-of-GET recorded with its reason rather than silently dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
90dc864ce5 |
fix(473): probe media-server remote streams before handing the URL to ffmpeg
Tuning a channel intermittently hard-failed with ffmpeg exit 8 and
`Server returned 404 Not Found` on /media/jellyfin/{itemId}.
Root cause: ValidatePlayoutItemPath checked `File.Exists` on the local
branch, but the three media-server remote-stream branches returned
`http://localhost:{port}/media/{plex,jellyfin,emby}/{id}` unconditionally.
When the media was gone from the media server too, validation "succeeded"
and ffmpeg was launched against a URL that 404s.
That bypassed the good error path the handler already had
(PlayoutItemDoesNotExistOnDisk renders an error card sized to run until
the NEXT playout item, so the dead item is skipped) and instead landed in
HlsSessionWorker's generic ffmpeg-failure path, which sizes its error card
to the failed 44s work-ahead chunk and then re-selects the SAME broken
item -- a repeating error card for the item's whole slot (~22 min).
Restore the method's own invariant: every PlayoutItemWithPath it returns
has been checked for existence. A definitive 404 now returns the new
PlayoutItemNotAvailableFromMediaServer error, handled in the same switch
arm as PlayoutItemDoesNotExistOnDisk.
The probe is deliberately fail-open: only a 404 reports the media gone.
A timeout, 5xx, auth error or transport failure reports available, so a
probe that cannot answer can never break a tune that would have worked.
That contract is pinned by tests so a later refactor cannot invert it.
Rejected alternatives (see docs/decisions.md): resizing the
HlsSessionWorker retry loop (cannot distinguish a dead item from a
transient transcoder failure -- prod has live VAAPI hwupload -22 failures
that must keep retrying), and writing MediaItemState from the streaming
path (breaks scanner ownership, and would not have fixed this: the item
is RemoteOnly, which PlayoutBuilder's skip does not exclude).
Scanner-side follow-ups filed separately: #476 (FileNotFound does not
cascade show -> episodes, the reason dead items keep being scheduled),
fixes #473
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c5369b1d69 |
ci(412): sample true peak-anon in the test job, not cache-inflated memory.peak
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m15s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m10s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m12s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 13m47s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m12s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The test-job memory instrument (#411) reported memory.peak — the high-water mark of memory.current, which charges reclaimable page cache to the cgroup. A build does heavy NuGet/npm/obj/bin/coverage I/O, so cache can dominate the peak, and page cache is reclaimed under a tighter cap rather than OOM-killed. Sizing a per-job cap (server-management#604) off memory.peak therefore inverts the decision. The OOM-forcing quantity is peak anon, which the kernel exposes no counter for and which the end-of-job split misses (a job that peaks mid-dotnet-test then frees reports a low anon). New scripts/ci-peak-anon.sh: a `start` step (before Build/Test/Coverage) launches a detached background sampler tracking the high-water mark of cgroup anon; a `report` step (last) stops it and prints the sampled peak anon as the headline, keeping memory.peak + end-of-job split as a cache-inflated ceiling and reference. Both continue-on-error + fail-open so they never redden a build. Validated on bumblebee: survives step-boundary re-execs, catches a transient 2.5 GiB anon spike the snapshot reports as 0, stops cleanly on kill, degrades gracefully. Compiler-server A/B (swap-off, sampled peak-anon, n=2 interleaved): OFF (CI config) ~5.84 GiB consistent; ON (defaults) 6.3-7.6 GiB, always higher, + a ~3 GiB resident VBCSCompiler. Disabling the servers is worth it, but OFF sits right at 6 GiB for the build phase alone and the test job adds test+coverage, so #406's "budget loosens well under 6 GiB" premise is not supported. Size the cap off the live test-job sampler. Docs: ci-cd.md instrument section rewritten (peak-anon headline + A/B table + premise verdict); decisions.md entry added. No .cs touched. fixes #412 |
||
|
|
33657b4753 |
docs(469): sweep remaining stale format-job cost/memory claims (review)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 8s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 23s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 20s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m49s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m28s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m33s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 5s
Cold adversarial review (MERGEABLE) flagged two spots still describing the pre-#469 heavy `format` job as current, plus a wording imprecision: - ci-cd.md CI-lane table row: annotate `format` runtime 37s → ~0.5s (#469). - ci-cd.md #406 memory narrative: note the 3.95 GiB Roslyn heap is now moot (folder mode loads no workspace); api-docs remains the lane's real consumer. - Precise the coverage mechanism in ci-cd.md + decisions.md: the naming rule passes the full gate because naming violations have no `dotnet format` batch code-fixer (so `--verify-no-changes` sees no change), not merely a severity floor. Docs-only; no workflow/hook logic change from the reviewed commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
eafb2e39e2 |
perf(469): format gate uses dotnet format whitespace --folder (~480s → ~0.5s)
Build ErsatzTV Image / decisions.md append-only (pull_request) Waiting to run
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m15s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m28s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Has been cancelled
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been cancelled
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Has been cancelled
The blocking `format` CI job and the Husky pre-commit hook verified changed .cs files with `dotnet format ErsatzTV.sln --no-restore --verify-no-changes --include <files>`. `--include` only narrows *which* files are checked, never what gets loaded: the full recipe loaded the ~10-project MSBuild workspace and built a Roslyn compilation per project before checking a single line (~480s locally, whole-solution). Switch both to `dotnet format whitespace . --folder --verify-no-changes --include <files>`, which treats the tree as a plain folder of files, skips MSBuild/Roslyn entirely (~0.5s), and needs no `dotnet restore` (NuGet-cache + Restore steps removed). Coverage is unchanged: folder mode reads .editorconfig and enforces exactly the gate's purpose — whitespace + charset (BOM). Proven non-vacuous (error WHITESPACE on a trailing-space line, error CHARSET on a prepended BOM, exit 0 clean). The full gate never enforced the style/analyzer pass either — a warning-severity naming violation passes the full solution format (exit 0) — and the analyzers that must block (NU1904, S3981) are enforced at compile via WarningsAsErrors, not by this job. Docs: ci-cd.md Formatting section + the obsolete #406 memory note; decisions.md. fixes #469 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
038703fe67 |
test(444): deterministic functional-E2E for the playout-build lock 409 + isLocked projection
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 33s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m24s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m4s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m25s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m37s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adds "Flow C" to scripts/e2e-functional.sh, the last deferred lock-contention flow from #363. A playout build is enqueued onto the single-consumer WorkerService channel and the trigger returns before BuildPlayoutHandler acquires the lock, so an accepted trigger does not prove the lock is held. Flow C makes it deterministic: seed a Classic Flood schedule over a few short ffmpeg episodes, crank PlayoutDaysToBuild=5 (~43k items ~= ~1s build), then POLL GET /playouts/{id} until isLocked:true before firing. Asserts PUT /playouts/{id} -> 409, reset -> 409, and the list-projection isLocked:true while locked; then isLocked:false + PUT -> 200 after the build (proving the 409 is lock-specific). Each racing assertion is guarded so a build that finishes mid-flight degrades to an advisory skip, never a false red; the whole flow self-skips without ffmpeg or if the build is never observed locked. Sized by measurement on a fresh instance -- going wider is counter-productive (a 777k-item build saturates the single worker with post-build gap/overlap jobs). Verified green across 6 fresh-instance runs; cold adversarial review MERGEABLE. Docs: docs/e2e-local.md + docs/ci-cd.md updated to describe Flow C and drop it from the "deferred" lists. fixes #444 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
be25df670e |
feat(431): TTL-cache health-check results; ?refresh=true forces a fresh run
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 16s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m16s
Build ErsatzTV Image / decisions.md append-only (pull_request) Failing after 12m23s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Failing after 14m6s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m44s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m19s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m28s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
GET /api/v1/health re-ran all 14 health checks on every request, 4 of which shell out to ffmpeg/ffprobe via CliWrap — so each poll spawned ~4 subprocesses. The existing HealthCheckSummary cache was write-only. Cache the full result list for 30s inside HealthCheckService keyed on a new "healthcheck.results" entry; a non-forced call returns it on a hit, skipping the checks and the (subscriber-less) summary publish. Add a `bool forceRefresh` first parameter to IHealthCheckService.PerformHealthChecks: the API poll path reads the cache, while startup (RunHealthChecksService) and the troubleshooting support bundle force a fresh run. Refresh surface: GET /api/v1/health gains an optional `[FromQuery] bool refresh` (additive, follows the ?deep= exemplar); the SPA "Refresh health" button calls /api/v1/health?refresh=true, the initial/poll load does not. Tests: HealthCheckService cache-hit vs force-bypass (mutually opposing, non-vacuous), handler+controller refresh-flag threading, SPA refresh URL. Docs: decisions.md 2026-07-19 (#431), api-conventions §2; regenerated v1.json. fixes #431 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1aa1f0cf5b |
fix(464): render real channel logos in guide grid + channels list
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 / Build & test (.NET) (pull_request) Successful in 7m33s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m11s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m31s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 36m5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
The guide/EPG grid (/app/guide) and the channels list (/app/channels) always drew the generated initials "bug" because the browse DTOs never carried a logo URL — GuideScreen/ChannelsScreen rendered <ChannelLogo> with no src. The logo data existed (it round-trips through the channel editor) but never reached these views. Add a rooted, directly-usable Logo URL to ChannelGuideChannelResponseModel and ChannelResponseModel, populated by a single Channels.Mapper.GetLogoUrl helper (#181 artwork convention): /iptv/logos/{file} for an uploaded logo, the absolute URL passed through for an external one, null when unset so the SPA keeps its generated-initials fallback. The guide query now includes Channel.Artwork. Regenerated OpenAPI + v1.d.ts; updated api-conventions.md + domain-model.md. fixes #464 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d2d678aae8 |
test/docs(404): add unrelated-edit round-trip test; correct MultiCollection-only rationale
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m20s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m48s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m10s
Review + live-E2E follow-ups (no behavior change): - Add a test proving the canonical silent-reset trap directly: a rename-only save round-trips both weights untouched (the prior test only edited the weight it asserted). Cold review nit. - Correct the rationale in itemRules.ts + decisions.md: WeightedShuffle is MultiCollection-only in the SPA for *meaningfulness* (per-source weights need 2+ sources), NOT because the classic write path rejects it — live-E2E confirmed the classic engine ACCEPTS it on a plain Collection (200) and degrades to fair-share. The rejection is on the separate playlist/block write paths, whose editors keep their own order lists and already omit it. Live-E2E (real API): weighted multi-collection create + read round-trips weights; rename-only PUT preserves them (no silent reset); WeightedShuffle persists on a classic MultiCollection schedule item. Ratio itself is pinned by the existing PlayoutBuildGoldenTests.Classic_weighted (3:1), untouched by this SPA change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d639946b5c |
feat(404): weighted-distribution SPA — per-source weight inputs + WeightedShuffle order
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 11s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m39s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 16m37s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 21m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 24m50s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The UI half of #70 (backend + API shipped in PR #402). Pure SPA + docs — no new endpoint/DTO (`weight` was already on MultiCollectionItemRequest/Response, and `WeightedShuffle` already in the PlaybackOrder enum). - Multi-collection editor (`/app/multi-collections`): per-source weight input (1..1000, mirroring the API validator), a computed % share (3:1 shows 75/25), and a "Reset to fair share" action (fair-share = all weights 1, decisions.md 2026-07-17 — a reset, not a separate mode). Weight round-trips through the draft (read in itemsFromMultiCollection, written in toItemRequest) so the replace-all PUT never silently resets it. - Classic schedule editor: `WeightedShuffle` offered as a Playback Order ONLY for MultiCollection sources (itemRules `MULTI_COLLECTION_ORDERS`) — it needs per-source weights and the write path rejects it elsewhere. Excluded from fillWithGroup like ShuffleInOrder (PlayoutBuilder schedules fill-groups per-group, incompatible with whole-collection weighted share). - `Input` gained min/max/inputMode/onBlur passthroughs for bounded numeric fields (reusable by #425's weight UI); weight held as a string for smooth editing, clamped on blur and at save so an out-of-range value never 400s. - Docs: domain-model + spa-conventions (replace-all round-trip trap, bounded numeric input pattern) + decisions.md entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
708e602197 |
feat(queue): deterministic scripts/select-queue.sh — stop re-deriving the selector by hand
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (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 16s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m23s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m52s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m23s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m48s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The kickoff dispatches a cheap model to rank the backlog, and the lore then made the orchestrator re-derive its mechanical claims (deps, milestone tiering, priority order, in-progress state) because a small model kept getting them wrong. This pays that tax down: the mechanical checks are now CODE — GET /dependencies exclusion, LOCAL .milestone.state/review/priority tiering (never the no-op ?milestones= filter), (tier,priority,issue#) ordering, in-progress/parked/PR exclusion — leaving only two JUDGMENT flags (CLAIM?, UMBRELLA?) for a human/model to resolve. - scripts/select-queue.sh: fail-open on no-creds/unreachable; ~1.3s; shellcheck clean; UMBRELLA? computed in the jq pass (no per-candidate body fetch). - handoff kickoff: run the script FIRST; trust its deps/tiering/ordering, recheck only flags. - handoff lore: the three 'cheap selector unreliable → re-derive by hand' bullets kept as the EVIDENCE for why the script exists; the prescription is redirected to 'run the script'. Operator-requested this session: 'rather than have the lore make us redo the selector's work, improve the selector.' Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
df8c5202d6 |
fix(409): report never-scanned LastScan as null for API/MCP parity (migration + read coercion) (#459)
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Has been skipped
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 / CI image pin matches docker/ci (push) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build CI Toolchain Image / Build & push CI image (push) Failing after 10m41s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 18m20s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 18m31s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m58s
Co-authored-by: Timothy <timothy.look@gmail.com> Co-committed-by: Timothy <timothy.look@gmail.com> |
||
|
|
dd55f00ed4 |
docs(420): sharpen the skip's honest hit-rate (rebase-before-merge) + cite live combined-status check
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m18s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 14m21s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m49s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Review nits from the scoped #420 review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
922b1ef53c | docs(398,420): document the #420 skip + record build-once measured-and-rejected | ||
|
|
8152afead1 |
Merge pull request 'feat(403): make unsupported PlaybackOrder loud at build time + tripwire' (#457) from feat/403-loud-playbackorder into main
Build ErsatzTV Image / CI image pin matches docker/ci (push) Has been skipped
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) Successful in 7m7s
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 / Functional E2E (curl contracts) (push) Successful in 14m26s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 20m27s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m3s
|
||
|
|
0f34c86afa |
feat(403): make unsupported PlaybackOrder loud at build time + tripwire
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m21s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m31s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 37m32s
Adding a new PlaybackOrder was unsafe by construction: three build-time dispatch sites turned an unknown value into an enumerator silently. Classic substituted RandomizedMediaCollectionEnumerator (the // TODO default arm), PlaylistEnumerator had no default arm so the item was dropped, and BlockPlayoutBuilder's allow-list continue skipped it. (#70 already made YAML/Scripted log a warning and MultiCollectionGroup throws.) - each silent site now logs a Warning naming the order + engine + the fallback taken; the fallback itself is preserved so a live channel never goes dark on one misconfigured item and scheduler goldens do not move. - PlaylistEnumerator.Create gained an optional Option<ILogger> (it was static with no logger -- why the drop was unreportable); loggered callers pass it. - BlockPlayoutBuilder gained an explicit Random arm (it previously reached an enumerator only via the coincidental _ => fallback) and a loud defensive fallback. - new PlaybackOrderSupport matrix (per SchedulingEngineKind) + tripwire PlaybackOrderSupportTests: Supported ∪ Unsupported must partition the enum for every engine, so a new order fails the test until classified. BlockPlayoutBuilder consumes the matrix for its allow-list. - write-path rejection left unchanged (#70 closed the persistence hole; the perimeter has been wrong three times per decisions.md); reverse _ => None mappings reviewed and deferred (different axis; making them loud would warn on legit enumerator types). docs/decisions.md updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ab8e5d7a91 |
ci(338): distinguish ZAP warning (exit 2) from failure (exit 1) in security-scan (#452)
Build ErsatzTV Image / CI image pin matches docker/ci (push) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
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 & test (.NET) (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
Co-authored-by: Timothy <timothy.look@gmail.com> Co-committed-by: Timothy <timothy.look@gmail.com> |
||
|
|
eb47aed767 |
feat(396): collapsible sidebar + nav-group accordions
Add two independent, persisted collapse states to the shell sidebar: - a header toggle that collapses it to a 60px icon rail - collapsible accordions per labeled nav group (Media, System); the unlabeled Primary group is always open, default-collapsed groups State + persistence live in web/src/app/sidebarState.ts (useSidebarState); AppShell stamps ctv-app-shell-collapsed on the shell root and the rail look is CSS-driven. Two namespaced localStorage keys (ctv-sidebar-collapsed, ctv-sidebar-groups) per the persisted-UI-state convention. In the rail, accordions are ignored (all items shown icon-only, label kept in the a11y tree + surfaced as a title tooltip, badges as a corner dot); active-route indicator works in both states; grid-column transition respects prefers-reduced-motion. Tests: colocated sidebarState.test.ts + a new describe in App.test.tsx (default-collapsed, accordion toggle+persist, rail, reload persistence). Docs: spa-conventions §13 + decisions.md 2026-07-18. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a1b75c1f2c |
docs(363): sweep the scan-flow drift the re-review caught
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m18s
The fix commit softened the script's scan-409 wording + dropped ?deep=true but left the docs describing the old behavior. Match them: - docs/e2e-local.md: POST .../scan (no ?deep; note local scans always ForceScan) + "409, deterministic bar a tiny residual TOCTOU gap" (was "guaranteed 409") - docs/ci-cd.md: same "guaranteed 409" -> "409 (deterministic bar ...)" Docs-only; harness behavior unchanged (still 38/38 green, deterministic). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4345180a56 |
review(363): robustness + wording fixes from cold review
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 11s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 20s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m47s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m48s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
- check seed_library_path succeeded (print lastrowid) so a silent seeding failure surfaces as a FAIL instead of degrading Flow A to an advisory skip with no diagnostic (Medium) - anchor the scan-status match to '"libraryId":2,' so it can't substring-match "libraryId":20/23 if the suite ever creates more libraries (Low) - drop the no-op ?deep=true (local scans always ForceScan; deep only affects Plex/Jellyfin/Emby) + note why (Low) - soften "guaranteed 409" for the scan flow to note the tiny residual TOCTOU gap the multi-second scan covers; Flow B stays race-free by construction (Low) - correct the "WAL tolerates a second writer" wording to the real reason (the busy-timeout retry serializes the writer) in the script + both docs (Nit) - use TEST-NET-1 192.0.2.1 (RFC 5737) instead of RFC1918 10.255.255.1 for the non-routable Jellyfin address (Nit) Re-verified: fresh-instance harness runs green (38/38), lock section deterministic. Functional E2E CI job already green on the prior head. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8a85f9ddb5 |
test(363): functional-E2E harness — add deterministic scan-lock + collections-lock 409 flows
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 10s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 28s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m24s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 3m45s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m16s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Extends scripts/e2e-functional.sh with the two IEntityLocker 409 contracts the first cut (ersatztv#299) deferred as "racy", made DETERMINISTIC by firing the racing request only once the lock is provably held (no sleep-and-hope): - library-scan "already scanning" 409: seed ~60 tiny ffmpeg clips into the built-in Shows library so the scanner subprocess runs a few seconds, poll GET /libraries/scan-status until the library is active (that window is a strict subset of the scan lock's held window — StartScan after LockLibrary, EndScan before UnlockLibrary), then a second POST .../scan is a guaranteed 409. Self-skips (advisory) when ffmpeg is absent. - external-collections "already scanning" 409: seed a Jellyfin media-source row pointing at a non-routable address so the background sync hangs and the per-family lock stays held; the lock is taken synchronously before the 202, so the 202 proves it held. collections-scan-status corroborates; unknown source 404. Seeding uses python3's stdlib sqlite3 (already a harness dep) to insert rows the API can't create (LibraryPath, media-source); WAL mode tolerates the second writer. No new CI step/dependency — ffmpeg ships in the toolchain image. Verified: 4/4 fresh-instance runs green (38/38), lock section deterministic. Still deferred to #363 follow-ups: the playout-build lock 409 + isLocked projection (#215) and the UI-interactive Playwright flows. Docs updated same PR: docs/e2e-local.md, docs/ci-cd.md, the functional-e2e job comment in .gitea/workflows/docker-build.yml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
551366aa72 |
Merge pull request 'feat(293): paginate GET /api/v1/search/all-items to cap DoS exposure' (#442) from feat/293-search-allitems-cap into main
Build ErsatzTV Image / CI image pin matches docker/ci (push) Has been skipped
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 / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m46s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 13m21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 17m35s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m25s
|
||
|
|
c57fbf9826 |
docs(293): note the pageNum upper clamp (MaxAllItemsPageNum) in the decision + api-conventions
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m16s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m30s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m32s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12m24s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13m20s
Fix delta re-review flagged the decision entry + §5 note still described the pre-fix pageNum = Math.Max(0, pageNum); the shipped code clamps the upper bound too (0..2_000_000) to stop pageNum*pageSize overflowing int to a 500. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ac7965dee4 |
feat(293): paginate GET /api/v1/search/all-items to cap DoS exposure
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m10s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 14m53s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12m39s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14m31s
The all-items endpoint fired ten index searches with limit:0 (every hit), so a broad authenticated query materialized the whole index into one response. Add optional pageNum/pageSize (clamped 1..1000; pageNum 0..2_000_000 so skip can't overflow int) and an additive per-kind Totals on the response; the SPA add-all flow now pages to completeness instead of a single unbounded fetch. - SearchController.SearchAllItems: clamp params (Logs §1 precedent), map Totals - QuerySearchIndexAllItemsHandler: skip=pageNum*pageSize, limit=pageSize, read SearchResult.TotalCount per kind - SearchResultAllItemsResponseModel: additive Totals (frozen-v1-safe) - web/src/api/search.ts: getSearchAllItems paging params + getAllSearchItemIds (pages until each kind hits its total; empty-page safety break) - tests: controller clamp/thread/totals, handler skip/limit/totals, SPA paging - docs: decisions.md 2026-07-18 (#293), api-conventions.md §5; regenerated OpenAPI Design: issue option (a) full pagination, operator-confirmed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2281f2e764 |
fix(308): idempotent concurrent Add*ToCollection instead of a composite-PK 500
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 40s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7m25s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m59s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m3s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Two concurrent adds of the same item both membership-check it absent, both insert the CollectionItem composite key, and the loser's SaveChangesForcingVersion threw an uncaught DbUpdateException (SQLite 19 / MySQL 1062) -> 500. Now the loser is an idempotent no-op. - ConcurrencyExtensions.TrySaveChangesForcingVersion: bool-returning sibling that catches only a classified unique/PK violation and returns false. - 10 single-item Add*ToCollection handlers: return Unit.Default (no-op, skip fan-out) on false — the racing winner already inserted + rotated + rebuilt. - Bulk AddItemsToCollection: retry on a fresh context against recomputed membership so a partial-overlap collision doesn't drop the non-colliding items (bounded loop; common no-collision path runs once). - Provider detection via a TvContext.IsUniqueConstraintViolation static delegate (matches the existing IsSqlite/LastInsertedRowId provider seam), wired from Startup to SqliteErrorClassifier / MySqlErrorClassifier. - Add*ToPlaylist is NOT affected (PlaylistItem has its own identity PK; a playlist may legitimately contain the same item more than once). Tests: a negative-control anchor proves the race genuinely throws a classified exception; end-to-end handler tests reproduce a real cross-connection race via a shared-cache SQLite harness + a SavingChanges interceptor (the single-conn in-memory fixture cannot). Every fix-dependent test verified to fail with the catch disabled. Docs: api-conventions.md §7a (idempotent insert under concurrency) + decisions/optimistic-concurrency.md. fixes #308 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e364b338e6 |
feat(425): per-source rotation weights + query corrections for auto-tune channels
Auto-tune channels can now carry per-content-source rotation weights (weighted
round-robin, e.g. 3x Show A / 1x Show B) and query corrections (exclude /
add-untagged), supplied at bulk-create time via an optional
`sources: [{sourceId, weight, excluded}]` on each AutoTunedChannelRequest.
Design (Option A, reuse #70): when a source is customized the channel is backed
by a system-owned MultiCollection of per-source SmartCollections carrying the
weights, with PlaybackOrder.WeightedShuffle -- the exact path
WeightedShuffleCollectionEnumerator already consumes. All-default weights keep
the #69 single-SmartCollection fair-share shape.
- Discriminators: TV -> live show_title:"X" (episodes carry no parent-show id in
the index); movies -> stable id:{mediaItemId}.
- Materialization is axis-dependent: TV materializes every base show individually
(un-weighted shows keep per-show fair-share) + a live remainder at weight 1;
MovieGenre materializes only touched movies + one count-weighted remainder.
- Remainder = (base) AND NOT (materialized union excluded) -- a partition.
- New nullable OwnedByChannelId on SmartCollection + MultiCollection
(dual-provider migration); owned rows are hidden from the collection lists and
cascade-cleaned on channel delete.
Tests: AutoTuneAxisMap query/partition units; DB-backed weighted-path handler
tests (TV materialize-all, movie count-remainder, exclusion, no-customization
fallback); delete-cleanup. Docs: decisions.md, domain-model.md, api-conventions.md;
OpenAPI trio regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c91c34f113 | docs(176): decisions + spa-conventions for the SmartCollection rule builder | ||
|
|
8483af41fc | docs(176): plan Task 8 — guard Builder toggle + resync query (matches shipped fix) | ||
|
|
279c0d6ce5 | docs(176): plan — hook exposes clean non-null RuleField (Core DTOs are nullable-disabled) | ||
|
|
5186bed910 | docs(176): plan — broaden round-trip generator to special chars; escaping now total | ||
|
|
218e1810cb | fix(176): total escaping so wildcard/quoted values round-trip (parse review) | ||
|
|
7f7062729d | docs(176): fix plan operator model — split fulltext matches/notMatches from text contains (Task 3 review Critical) | ||
|
|
ae22107d98 | feat(176): GET /api/v1/search/fields endpoint + regenerated api artifacts | ||
|
|
aa4127fcef | docs(176): fix plan test-project path + parse.ts self-review nits; add SDD ledger | ||
|
|
8c7af0e202 |
docs(176): implementation plan — SmartCollection visual rule builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
71159c1a8a |
docs(176): design spec — SmartCollection visual rule builder (sub-project #1)
Scopes #176 down after recon (Autotune #69 + composite create-channel #63 already shipped): the remaining leg is the visual WYSIWYG rule builder. Kodi one-level-nested model, compile-only to a closed Lucene subset (no schema change), backed by a new read-only GET /api/v1/search/fields catalog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f44eee85c5 |
feat(386): Auto-Tune per-channel DetailPanel slide-over (SPA)
Adds a right-hand "Configure" slide-over to each Auto-Tune Preview row, making a proposed channel editable before bulk-create — against the shipped #384/#385 backend only, so no control lacks a wire target. - New reusable SlideOver primitive (components/overlay.tsx), sharing a useOverlayBehavior hook (focus/scroll-lock/Escape/scrim) with Dialog. - Extract the Channel Builder's advanced-options model to builder/advancedOptions.tsx (enum catalogs, ADVANCED_KEYS, effectiveValue, INHERIT/omit useAdvancedOverrides hook); ChannelBuilder imports it unchanged (its tests pass byte-for-byte). The DetailPanel writes its own field JSX over the same hook — shared logic, per-screen layout. - Panes: identity (name/number + logo upload), Playback (Shuffle/Always-playing → advanced.playbackOrder/playoutMode), per-channel template picker, Advanced disclosure, lean read-only Query&size, read-only Content-sources via GET /members. - getAutoTuneChannelMembers API client (#384 read endpoint) + tests. - Screen-scoped §8 unsaved-changes guard + "Edited" row badge. - Dropped as backend-less decoration: MiniEpg, bug-initials generator, query text. Deferred to #425 with an in-pane hint: per-source weight steppers + corrections. - Docs: spa-conventions §11 (SlideOver + shared advanced-options), decisions.md. Refs #386 |
||
|
|
ed6c43065f |
feat(164): guided remediation for health checks (server-declared {Kind, Target})
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 54s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m48s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12m49s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m35s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13m23s
Make the ~14 health checks actionable: each check that has a fix now declares
where to go, and the SPA acts on it.
Backend:
- Widen domain HealthCheckLink (string Link) -> (string Target, HealthCheckLinkKind
Kind) with ExternalDoc|AppRoute + factories; only the 4 link-building checks and
the API mapper touched .Link.
- Evolve HealthCheckResponseModel additively (/api/v1 frozen-additive): keep
deprecated string? Link (still populated), add Brief (the BriefMessage the mapper
was silently dropping) and nested Remediation {Kind, Target}. Kind is a mapped
string, not a wire enum.
- Make Mapper.GetStatus total: NotApplicable no longer throws (defensive; handler
still filters it). InternalsVisibleTo(ErsatzTV.Tests) added to unit-test totality.
- Fix 2 stale Blazor route links (media/trash -> /app/trash, search?query ->
/app/search); add AppRoute remediation to actionable checks that had none
(libraries / schedules / ffmpeg-profiles / settings).
SPA:
- DashboardScreen health panel renders remediation: AppRoute -> client-side nav
button, ExternalDoc -> new-tab anchor; detail text truncates with title-hover.
- Remove the dead "Open Classic UI" -> /system/health row from SettingsScreen
(a #91b leftover that just 302'd to /app); update its regression test.
Docs: decisions.md (#164), api-conventions.md (deprecate-in-place DTO evolution),
blazor-route-parity.md (Section 4 correction); v1.json/v1.d.ts/endpoint-index
regenerated.
fixes #164
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
8f61ad6530 |
feat(385): per-channel overrides in auto-tune bulk-create
Auto-Tune DetailPanel backend (#385), additive half. The create request `AutoTunedChannelRequest` gains three optional per-channel fields, all backward-compatible (omit = PR1 behavior): - `templateId` — overrides the batch template per channel - `advanced` — reuses the manual Channel Builder's `CreateChannelFromLineupAdvancedOptionsRequest` verbatim (24-field override set, `advanced.X ?? template.X` stamp contract). Axis default fills `PlaybackOrder` only when the caller leaves it null. - `logo` — uploaded channel image, `Sanitized()` at the request boundary (#283 stored-XSS defense), forwarded to `CreateChannelFromLineup.Logo` Resolved per channel inside `CreateAutoTunedChannelsHandler.CreateOne`, so one channel's bad override still yields a per-channel Failed/Skipped without aborting the batch. Per-source rotation weights + query corrections are split out to #425 (they need a MultiCollection-of-per-source-SmartCollections redesign — #70's WeightedShuffle reads weights only off MultiCollection join rows, and an auto-tuned channel is one SmartCollection). Bug-initials/colour generated logo also deferred (needs persisted Channel state + FFmpeg-pipeline wiring). Tests: handler override-threading (per-channel wins, axis default preserved, no-override baseline) + request `ToCommand()` logo sanitization. OpenAPI trio regenerated. Docs: decisions.md, api-conventions.md, domain-model.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1df08e86bb |
ci: pre-push guard against pushing an uncommitted working-tree change (H13)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 10s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 33s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m51s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m1s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m3s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
A file in the pushed diff that still has uncommitted working-tree/index changes means the pushed commit doesn't match what you built/reviewed — the #416 index/worktree trap, where a --no-renames review fix lived only in the working tree and shipped without being committed (CI, the push, and the reviewer each saw a different tree). New fail-open pre-push hook blocks that precisely (only files in the branch's diff vs origin/main), after the H11 rebase check. Escape: ETV_ALLOW_DIRTY_PUSH=1. Documented in decisions.md. Refs #416 |
||
|
|
eb1aba99c1 |
docs(lore): run local gate + cold review BEFORE pushing to Gitea CI
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 32s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 29s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 16s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 16s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14s
The handoff PR-routine and HARD-CONSTRAINTS placed the cold adversarial review AFTER push/PR-open. New standing rule (Timothy, 2026-07-17): local gate + cold review run BEFORE the push; fold pre-push fixes into the branch; CI only confirms. Runners should see only already-validated work — a Gitea run can't be cancelled, so a run spent on a lint/review-caught issue is pure waste. Also notes docs-only PRs are seconds now (ersatztv#416). |
||
|
|
082124f706 |
docs(416): note shallow-checkout FETCH_HEAD/two-dot fix (ci-cd + decisions)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 15s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m18s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m7s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Refs #416 |
||
|
|
92bb63b6da |
docs(416): document docs-only CI skip
ci-cd.md gains a 'Docs-only skip' section + triggers-table note; decisions.md records the decision. Cross-refs the separate PR-vs-main rerun redundancy (#420) and the within-run triple build (#398). Refs #416 |
||
|
|
caaae4cd00 |
docs(70): re-derive the stale-claim fix by grep instead of working the review's list
Round-3 review returned BLOCKED: must-fix (b) was not closed. It was right, and the root cause it named is the point of this commit — the previous correction "was scoped to the four sites the reviewer listed rather than re-derived by grep". Fixing the list is not fixing the class. That is the same failure as B1, where the gate covered the two writers already in hand and missed CreateChannelFromLineup. Re-grepped the behavior class instead. Three survivors, two of them missed and one freshly introduced by the correction itself: - CreateMultiCollectionHandler.cs — the create twin of a comment whose UPDATE twin I corrected and whose create twin I never opened. Present tense, and contradicted by two tests in this same PR. - decisions.md — corrected one line in that file and left its sibling. - MultiCollectionItemWeight.cs (and its decisions.md mirror) — the ceiling rationale still claimed unbounded weights overflow the sum. They cannot: EffectiveWeight clamps before every sum and CycleLength widens to long. The earlier pass pattern-matched on the word "filtered" and left the identical defect on the ceiling. The ceiling's real job is the floor's argument — a billion is not a share of airtime any more than 0 is — so it now says that, and credits the clamp with the arithmetic safety it actually provides. Also corrected the writer claim to the right predicate: not "two persisting writers" (Add*ToPlaylist and Trakt persist it too, hardcoded) but two writers that persist a CALLER-SUPPLIED order. The full set is now classified persists-caller-value / persists-hardcoded / in-memory, including Engine/PlaylistHelper, which the previous "two Preview handlers" phrasing missed. That bullet has been wrong three times in the same shape; it now says so, since a lesson that keeps being re-learned is worth recording as a pattern rather than a fact. The BOM check caught this commit re-adding a BOM to the one file patched with utf-8-sig — the same trap, an hour after writing it down. Stripped; the mechanical pre-push check is what makes that survivable. Core.Tests 566, ErsatzTV.Tests 1673, 0 failed. Format verify exit 0. decisions.md +90/-0 (append-only guard green). Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ccef0ca88a |
fix(70): test the weight gate; correct rationale my own clamp made false
Re-review of the fix commit returned MERGEABLE-WITH-NITS. It verified the gate is now complete by enumerating the writers itself (no fourth persisting writer) and proved B2's fix works by writing throwaway handler tests — which was also its point: the fix shipped with none. B2 was create and update silently DISAGREEING on the same input, and the fix re-established agreement with nothing pinning it. Both paths are now driven from one shared case list, plus an explicit test that create and update agree on every case — the per-path tests would both have passed while the two diverged, which is how the bug existed in the first place. Non-vacuity proven: inverting only the update path's validation fails 10 of 20 on a clean build (0 errors, so not a stale-dll pass), and the agreement test is among the failures. The rest is my own prose contradicting my own code. The commit that added EffectiveWeight removed the weight filter, then left four statements asserting a 0-weight source "is filtered out" — two of them authored by that same commit, including the stated justification for Minimum=1 in MultiCollectionItemWeight. A future agent could have read that and deleted the clamp or the floor as redundant; they are belt-and-braces and neither is. Corrected to describe what the code now does: the gate refuses input that means nothing on a share-of-airtime scale, the clamp protects rows predating the gate. Also corrected the writer count in the very bullet whose lesson is "grep every writer of the field": ReplaceBlockItems writes BlockItem.PlaybackOrder, not PlaylistItem.PlaybackOrder. There are TWO persisting writers of PlaylistItem's, and the correction itself had miscounted by conflating the two fields — so the lesson now says to grep each field separately. Core.Tests 565 passed, ErsatzTV.Tests 1673 passed, 0 failed. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
953481c177 |
docs(70): record the WeightedShuffle design and correct the ShuffleInOrder misreading
decisions.md entry covers why one enum value rather than two or a separate setting, why the weight lives on both multi-collection join entities with a DB default of 1, why it can only be applied on the ShuffleInOrder-shaped path (source identity is destroyed on the Shuffle path), why it is stateless, and why cross-engine exposure is closed by write-path validation instead of by changing five shipped fallbacks. The load-bearing part is the ShuffleInOrder clarification. Its balanced-shuffle padding reads as equalization, but the spacers emit nothing — one cycle plays every item once, so airtime stays proportional to collection size. It is anti-clumping, not fair-share. That was misread during this issue's own design pass and would have collapsed #70 to "already shipped"; the distinction is the whole justification for the feature, so it is now written down in both decisions.md and the domain-model glossary rather than left to be rediscovered. domain-model.md gains a Playback order row contrasting Shuffle / ShuffleInOrder / WeightedShuffle, and notes on the multi-collection row that the two join entities are mirrors. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fe6e2722f8 |
docs(lore): batching, no "main checkout", trust the queue, killed≠failed
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 4s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m1s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 19s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 16s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m54s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m5s
Operator-requested after PR #405 pushed 5 times, orphaning live runs the operator had to cancel by hand. Batches every standing-lore correction this session produced into one commit (per the batching rule it adds). New HARD CONSTRAINTS: - BATCH PUSHES. Cancellation is impossible from the agent side on Gitea 1.25.4 — REST .../runs/{id}/cancel and MCP cancel_run both 404, and the web-UI route needs a session+CSRF that doesn't script. Only the operator can cancel, so an orphaned run holds a runner slot until it finishes. If you must supersede a live run, SAY SO. - TRUST THE GITEA BUILD QUEUE. Do not gate/throttle a push on host health; the runners were retuned for stability. Batch because you can't cancel what you orphan, not to protect the host. - BOM-CHECK touched .cs before pushing. The #311 gate is fix-as-you-touch, and it bit two sessions the same day (PR #405 ×6; #70/PR #402 ×19 via Python utf-8-sig writing BOMs back). Verify your detector — an od-based grep reported all-clean while 19 files were dirty. Corrects a claim I nearly published: `dotnet format --include` DOES work here. The apparent no-op was the SHELL — CI's mapfile is bash-only, zsh has no mapfile → empty array → zero files → exit 0. Run it under bash -c. THERE IS NO "main checkout" — the biggest correction here. /Users/timothy/ersatztv is a shared mutable working tree whose HEAD is whatever the last session left there. Its name lies, and it bit TWO sessions on 2026-07-17, both doing the obvious thing: one assumed main and committed onto the #604/CI session's branch 24s after that session's own commit; another read git log there and concluded main was "4 behind origin" — a phantom. Framed as a design flaw, not a discipline failure: "check git status first" appears to confirm the false assumption and then goes stale (it WAS on main at 12:46 and wasn't by 14:17). Worktree discipline itself is healthy — 10 feature worktrees. Diagnosing CI reds — three ways to misread one, all hit this session: - A KILLED job reports conclusion=failure, not cancelled. The tell is a log that stops mid-step with NO error and NO failure marker. A runner retune killed run 1006's migration + E2E on a BOM-only diff that couldn't break them. Log timestamps are UTC, host is UTC+2 — convert before correlating. - cancelled ≠ failure: a cancel is NO verdict, and a run marked failure may hold a genuine job failure from BEFORE the cancel. Monitors must count FAILED and CANCELLED separately. - "Unable to pull refs/heads/v4" is act refreshing its action cache and is followed by "Cloned …" — noise, not a cause. Grep the failure marker, not the word "error". An infra-shaped red (setup/cache step, before your code compiles) is not a code failure; don't file a CI bug off one sample. Also: the cheap selector's failure modes are wider than deps+priority — it also misses in-progress claim state and umbrella-vs-child. Docs-only. |