Commit Graph
3021 Commits
Author SHA1 Message Date
timothy ea6b509016 fix(176): guard Builder toggle for out-of-subset queries; resync query on reseed 2026-07-18 02:41:50 +02:00
timothy 8483af41fc docs(176): plan Task 8 — guard Builder toggle + resync query (matches shipped fix) 2026-07-18 02:41:50 +02:00
timothy 60e8655a00 feat(176): Builder|Advanced toggle in the SmartCollection dialog 2026-07-18 02:41:50 +02:00
timothy 2763f65b35 test(176): cover RuleBuilder field-change operator/value reset 2026-07-18 02:41:50 +02:00
timothy ceb9069bae feat(176): RuleBuilder component 2026-07-18 02:41:50 +02:00
timothy 7d553ef00c refactor(176): expose clean non-null RuleField from useSearchFields 2026-07-18 02:41:50 +02:00
timothy 279c0d6ce5 docs(176): plan — hook exposes clean non-null RuleField (Core DTOs are nullable-disabled) 2026-07-18 02:41:50 +02:00
timothy 30a8f67e8e feat(176): field-catalog api + useSearchFields hook
Task 6: getSearchFields() client fn + SearchField type in api/search.ts,
and the useSearchFields() hook in builder/rules/fieldCatalog.ts that
reshapes the catalog into fieldTypes (name -> FieldType) and byGroup
views for the rule builder (Tasks 7-8).

Generated SearchFieldResponseModel has all-nullable fields (name/label/
type/group/values), unlike the brief's non-null assumption -- adapted by
dropping fields with a null name and defaulting group to 'Other' / type
to 'text' rather than indexing under undefined.
2026-07-18 02:41:50 +02:00
timothy 03646a2ed9 test(176): compile/parse round-trip property test 2026-07-18 02:41:50 +02:00
timothy 5186bed910 docs(176): plan — broaden round-trip generator to special chars; escaping now total 2026-07-18 02:41:50 +02:00
timothy 218e1810cb fix(176): total escaping so wildcard/quoted values round-trip (parse review) 2026-07-18 02:41:50 +02:00
timothy 0b5258533a feat(176): Lucene-subset parser (exact inverse of compiler) 2026-07-18 02:41:50 +02:00
timothy bc524d0b53 fix(176): split fulltext matches/notMatches from text contains 2026-07-18 02:41:50 +02:00
timothy 7f7062729d docs(176): fix plan operator model — split fulltext matches/notMatches from text contains (Task 3 review Critical) 2026-07-18 02:41:50 +02:00
timothy fd786da649 feat(176): rule model + Lucene-subset compiler 2026-07-18 02:41:50 +02:00
timothy ae22107d98 feat(176): GET /api/v1/search/fields endpoint + regenerated api artifacts 2026-07-18 02:41:50 +02:00
timothy 41df582032 feat(176): search field catalog query + curated list 2026-07-18 02:41:50 +02:00
timothy aa4127fcef docs(176): fix plan test-project path + parse.ts self-review nits; add SDD ledger 2026-07-18 02:41:50 +02:00
timothyandClaude Opus 4.8 8c7af0e202 docs(176): implementation plan — SmartCollection visual rule builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 02:41:50 +02:00
timothyandClaude Opus 4.8 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>
2026-07-18 02:41:50 +02:00
timothy 6204e30c7c Merge pull request 'feat(386): Auto-Tune per-channel DetailPanel slide-over (SPA)' (#432) from feat/386-autotune-detailpanel 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 7m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m31s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 6m19s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 14m0s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Failing after 13m7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Failing after 13m15s
2026-07-17 23:54:38 +00:00
timothy 56fffacfd4 fix(386): address cold-review findings on the DetailPanel
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 11s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m16s
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 5s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m16s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 17m48s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m16s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Cold adversarial review (no blockers, 3 real Mediums):
- Query&size "Order" row ignored the Shuffle toggle (dead ?? fallback showing
  the static axis order); now reflects shuffleOn, consistent with the subtitle.
- Clearing Name/Number to '' flagged the row Edited + armed the unsaved-changes
  guard, but the payload reverted to the proposal default — the two "edited"
  derivations disagreed. overrideEdited now ignores an empty (inherited) value.
- runPreview did not clear per-channel overrides, so edits (incl. pinned numbers)
  silently carried into a re-generated batch → collision risk. Fresh preview now
  resets overrides/detailKey/logo cache.
Tests: empty-name-not-Edited + re-preview-clears-overrides.

Live-E2E (local instance, seeded TV library): preview → Configure → toggle
Shuffle → Create verified end-to-end; the created channel's schedule shows
PlaybackOrder=Shuffle (overridden) vs SeasonEpisode (axis default), confirming
the per-channel override flows UI → SPA → backend → playout.

Refs #386
2026-07-18 01:31:40 +02:00
timothy 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
2026-07-18 01:31:40 +02:00
timothy 0a34d1a585 Merge pull request 'feat(164): guided remediation for health checks' (#430) from feat/164-health-checks-ux 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 / 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 / Build & test (.NET) (push) Successful in 7m1s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 13m0s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 17m5s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 6m0s
2026-07-17 23:12:19 +00:00
timothyandClaude Opus 4.8 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>
2026-07-18 00:51:40 +02:00
timothy 1abc5df18c Merge pull request 'feat(385): per-channel overrides in auto-tune bulk-create' (#428) from feat/385-autotune-overrides 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 / 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 5m30s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 17m47s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 17m59s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m51s
2026-07-17 22:03:53 +00:00
timothyandClaude Opus 4.8 b9ab2d79f3 test(385): cover override-driven per-channel failure isolation
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m33s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m4s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 13m57s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 11m58s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m58s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adds the one coverage case the cold review flagged (LOW): a two-channel batch
where the first channel's advanced override is rejected downstream and the
second, override-free channel is still Created — directly exercising the
partial-success / batch-independence guarantee for the new override path.
Test-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 23:33:10 +02:00
timothyandClaude Opus 4.8 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>
2026-07-17 23:33:10 +02:00
timothy ba707f2e23 Merge pull request 'ci: pre-push guard against pushing an uncommitted working-tree change (H13)' (#424) from ci/dirty-worktree-guard 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 / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m19s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 13m58s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 19m15s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m41s
2026-07-17 21:25:30 +00:00
timothy 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
2026-07-17 23:05:30 +02:00
timothy 5e0c53c339 Merge pull request 'docs(lore): run local gate + cold review BEFORE pushing to Gitea CI' (#426) from docs/review-before-ci into main
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 / Build & test (.NET) (push) Successful in 8s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 19s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 20s
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 & push image (amd64) (push) Successful in 7s
2026-07-17 20:59:34 +00:00
timothy 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).
2026-07-17 22:39:42 +02:00
timothy 4818b07668 Merge pull request 'fix(416): docs-only skip never fired — detect against FETCH_HEAD (shallow-checkout safe)' (#429) from fix/416-shallow-detect 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 7m12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m2s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Has started running
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
2026-07-17 20:39:04 +00:00
timothy 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
2026-07-17 22:02:06 +02:00
timothy 74005cc952 fix(416): detect against FETCH_HEAD with a two-dot diff (shallow-checkout safe)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 7s
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 / Build & test (.NET) (pull_request) Successful in 7m39s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 15s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m54s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m27s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The test/migrations jobs check out fetch-depth:1. A shallow clone has NO
origin/<base> tracking ref and no merge-base, so the three-dot
origin/main...HEAD errored -> empty diff -> docs_only=false -> EVERY docs-only PR
silently ran the full matrix (safe but the skip never fired). git fetch always
writes FETCH_HEAD, which resolves in a shallow clone; diff against it with a
two-dot tree diff (no merge-base). Confirmed in a real shallow file:// clone:
origin/main did NOT resolve and three-dot errored, while FETCH_HEAD two-dot
correctly returned the docs file. api-docs/format were unaffected only because
they use fetch-depth:0.

Refs #416
2026-07-17 21:33:53 +02:00
timothy 1b355660c5 Merge pull request 'fix(320): break troubleshoot segment-wait loop on ffmpeg failure' (#423) from fix/320-troubleshoot-segment-wait-isfailed 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 / 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 / Build & test (.NET) (push) Failing after 24m59s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m13s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 14m34s
2026-07-17 18:43:07 +00:00
timothy 9c72aebab0 Merge pull request 'ci(416): skip heavy jobs on docs-only changes without bricking the merge gate' (#422) from ci/416-docs-only-skip 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 / EF migration integrity (SQLite + MySql) (push) Successful in 5m59s
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 5m12s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Reviewed-on: #422
2026-07-17 18:10:24 +00:00
timothyandClaude Opus 4.8 de63603aab test(320): don't blanket-delete foreign *.ts in the shared troubleshooting folder
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 / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 2m30s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m7s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 13m45s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12m32s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m37s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Re-review nit: the regression test deleted every *.ts in the machine-global
TranscodeTroubleshootingFolder, which could nuke a live troubleshooting session's
segments on a dev machine (reaping files it didn't create). Drop the sweep and
keep only Directory.CreateDirectory — the folder-exists guarantee is what closes
the false-pass hole; NUnit runs serially and no test leaves >= 2 stray .ts, so
determinism is unaffected (negative control re-verified: still fails in ~10s).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 20:03:10 +02:00
timothyandClaude Opus 4.8 5f8525eed7 fix(320): address cold-review nits — drop redundant cancel term, harden test
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 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 4m49s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 53s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Has been cancelled
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Has been cancelled
Cold review (PR #423) found two LOW nits:
- The segment-wait break also checked cancellationToken.IsCancellationRequested,
  which could exit into the Ok gate and return a spurious 200 for a cancelled
  request with no segments. The term is redundant (Task.Delay already throws on
  cancel -> caught -> 404), so break on notifier.IsFailed only.
- The regression test's non-vacuousness was filesystem-state dependent (absent
  troubleshooting folder -> Directory.GetFiles throws -> false pass pre-fix).
  Seed an empty folder like the sibling success test so the spin path is
  deterministic regardless of NUnit run order.

Negative control re-verified: removing the IsFailed break fails the test in ~10s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 19:57:55 +02:00
timothyandClaude Opus 4.8 b2059bd2a6 fix(320): break troubleshoot segment-wait loop on ffmpeg failure
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 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m48s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 51s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m28s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m26s
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 12m43s
The second (segment-readiness) poll in POST /api/v1/troubleshoot/playback/start
only checked !hasSegments. If ffmpeg died after writing the HLS playlist but
before the first segments appeared, the loop spun until the client cancelled —
tying up the request thread and holding the troubleshooting lock (worse since
#301 moved the manifest fetch server-side per #320's writeup). Mirror the first
(playlist) wait loop's exit check so a dead ffmpeg or client cancellation breaks
the wait; the existing !IsFailed guard then falls through to the terminal
NotFoundProblem, releasing the thread + lock.

Adds a non-vacuous regression test (verified it fails on the pre-fix spinning
loop via a bounded cancellation deadline).

fixes #320

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 19:50:54 +02:00
timothy f7b97adce8 ci(416): harden docs-only detection with --no-renames (review finding)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 5s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m6s
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 5s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m34s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The changed-set git diff had rename detection on by default, so a code->docs
rename (Foo.cs -> docs/Foo.md) showed only the destination and was misclassified
as docs-only, skipping required tests on a code change. --no-renames surfaces the
source deletion -> full matrix. Empirically verified. This is the cold-review
MEDIUM; it was applied in the working tree but never committed before the first
push (index/worktree mismatch) — committing it now.

Refs #416
2026-07-17 19:44:25 +02:00
timothy 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
2026-07-17 19:44:25 +02:00
timothy 5ba737bec7 ci(416): skip heavy jobs on docs-only changes
Docs-only changes (docs/** or *.md) ran the full docker-build matrix (~9 min).
Each heavy job (test, migrations, functional-e2e, build) now runs
scripts/ci-detect-docs-only.sh as its first post-checkout step and gates every
real step on docs_only!='true'. The jobs still RUN and report success in
seconds, so the two required contexts keep reporting — a docs-only PR stays
mergeable (never an if:-skipped required job; Gitea 1.25.4 reports if-skip as
'skipped', verified with a throwaway probe PR). build skips its image steps on a
docs-only push to main; tag builds force docs_only=false. Detection uses
--no-renames so a code->docs rename can never be misclassified as docs-only.

Refs #416
2026-07-17 19:44:25 +02:00
timothy ffa8e5a000 Merge pull request 'fix(376): XML-escape access_token value in XMLTV guide output' (#419) from fix/376-xmltv-token-escape 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 / 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 5m23s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 18m59s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 19m55s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m19s
2026-07-17 17:34:24 +00:00
timothyandClaude Opus 4.8 bf15677dc6 fix(376): XML-escape access_token value in XMLTV guide output
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 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m2s
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 47s
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
`GetChannelGuideHandler` interpolated `request.AccessToken` (HTTP-request-
derived, from `?access_token=`) raw into the pre-built XMLTV cache fragments.
A token containing `&`, `<`, `>`, or `"` would emit invalid XML and malform
the entire guide. Escape it with `SecurityElement.Escape`, consistent with
how #340 escaped `{RequestBase}`.

The M3U path (`ChannelPlaylist.ToM3U`) also interpolates the token but M3U is
not XML, so escaping there is neither needed nor correct — left unchanged.

Regression test `Guide_xml_escapes_access_token` drives the real handler with
a token containing all four XML-special chars and asserts the output is
escaped (sibling to the #340 `Guide_xml_escapes_advertised_base_url` test).
Verified non-vacuous: it fails with the escape reverted.

fixes #376

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 19:04:42 +02:00
timothy fa7e8d75c4 Merge pull request 'feat(70): weighted / fair-share content distribution (WeightedShuffle playback order)' (#402) from feat/70-weighted-distribution 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 / EF migration integrity (SQLite + MySql) (push) Successful in 5m35s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 4m53s
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 / Build & test (.NET) (push) Successful in 18m26s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 7m40s
2026-07-17 17:00:46 +00:00
9752111240 docs(70): fix the last stale claim — found by reading every weight comment, not by grepping verbs
Round-4 review returned BLOCKED, and its diagnosis is the sharpest of the four:
my "re-derived by grep" claim was false. The reviewer's grep included `dropped`;
mine had `drop(ped)? every` and missed the bare word — so
MultiCollectionConfiguration.cs:38 ("a 0-weight source is dropped by the
enumerator") never matched. That is the same failure as the previous round, one
level up: I replaced a list of SITES with a list of VERBS I guessed and called it
a class. Fourth consecutive incomplete completeness claim, and again the code twin
of a doc line I had already corrected.

The site is now accurate. The DB default of 1 is still right, but for the reason
decisions.md already gives: a 0 backfill means nothing on a share-of-airtime scale.
The enumerator clamps such a row to the floor, so it rotates rather than vanishing
-- the drop it cited cannot happen since EffectiveWeight replaced the Weight > 0
filter, and A_Non_Positive_Weight_Does_Not_Delete_The_Source pins that.

Method changed, not just the text: instead of guessing which verb a stale claim
might use, enumerate EVERY comment/doc line mentioning weight across the diff and
read them. 41 lines, one false. Both automated passes then produced false
positives on different subjects -- a line-based check flagged EffectiveWeight's
"must not drop..." (the framing verb wraps to the previous line), and a
sentence-aware one flagged ReplacePlaylistItemsHandler's "the item is dropped from
the playlist" (true, and about unknown ORDERS, not weights: PlaylistEnumerator
really has 0 default arms and gates on `enumerator is not null` at :210). Both were
read and cleared rather than counted, which is the only reason this message can say
what it says.

Verification: Build 0 errors; Core.Tests 566; ErsatzTV.Tests 1673; 0 failed; no BOM
on any touched non-generated .cs; decisions-guard exit 0.

Refs #70

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:00:46 +00:00
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>
2026-07-17 17:00:46 +00:00
7446ce0293 test(70): classic-weighted golden — weight beats collection size end-to-end
The unit tests pin the enumerator's sequence in isolation; this pins that the real
PlayoutBuilder actually distributes by weight, through the whole chain:
MultiCollection -> MediaCollectionRepository -> CollectionWithItems.Weight ->
ShuffleSourceBuilder -> WeightedShuffleCollectionEnumerator -> PlayoutItems.

The fixture is deliberately lopsided: the HEAVY source (weight 3) is the SMALL
collection (2 items) and the LIGHT source (weight 1) is the LARGE one (4). Over
the pinned 2-day window that yields 81 : 27 = exactly 3.00 : 1 — the smaller
collection taking the larger share, which is the entire point of the feature and
is not reachable by any existing order: Shuffle is size-proportional (~2:4 here)
and ShuffleInOrder plays every item exactly once per cycle. Longest consecutive
same-source run is 3, so it interleaves smoothly rather than draining blocks.

Same determinism contract as Classic_shuffle: pinned Playout.Seed + Continue mode
(Reset randomizes the seed). Channel number/GUID 5 — every golden fixture shares
one in-memory DB, so those must be globally unique.

The net is armed, not decorative: with the golden absent the test fails (missing
golden is a hard failure by design), and regenerating added only classic-weighted
— the three existing goldens are byte-identical, so nothing was silently
re-baselined.

Core.Tests 566 passed, 0 failed.

Refs #70

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:00:46 +00:00
0295a8a9f6 style(70): strip UTF-8 BOM from the .cs files this PR touches
CI's Formatting job failed: 19 touched files carried a BOM, which .editorconfig
forbids (charset=utf-8). Pure encoding change — one byte per file, no semantic
diff (verified: every hunk is `-namespace` -> `+namespace`).

Self-inflicted. The patches that edited these legacy files wrote them back as
utf-8-sig to "preserve the existing style", but the #311 fix-as-you-touch gate
requires a file to be normalized when you touch it — that is the whole point of
scoping the gate to changed files instead of reformatting the ~2500 legacy BOM
files at once. dotnet format leaves the EF-generated Designer/snapshot files
alone as generated code, and its verify skips them the same way, so they stay as
ef emitted them.

Two corrections to what I believed going in:
- `dotnet format --include` does NOT no-op here. It reported `error CHARSET` for
  each file and exit 2, reproducing CI exactly, and fixed them in place. The note
  claiming otherwise is wrong for this invocation.
- My first BOM check reported all files clean. The od pattern was wrong; reading
  the first three bytes directly found 19. A detector that can only say "ok" is
  worse than no detector.

Core.Tests 565, ErsatzTV.Tests 1673, Architecture.Tests 5 — all passed. API
artifacts still in sync.

Refs #70

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:00:46 +00:00