Per-channel fault detection beyond "no playout" (empty schedule, broken source) #415
Closed
opened 2026-07-17 16:32:47 +02:00 by timothy
·
3 comments
No Branch/Tag Specified
main
renovate/lucene.net
renovate/dotnet-monorepo
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
renovate/meziantou.analyzer-3.x
renovate/cliwrap-3.x
913-scripted-playout-401
release/v26.15.0-notes
issue-806-guard-populations
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.16.0
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#415
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Deferred from #72 (channel health / status UX) — the rest of scope item (b). #72 shipped only the no-playout fault; the channels list badge and filter are deliberately named "No playout" rather than "Problems" precisely because these classes are uncomputed (a "Problems 0" reading on a lineup holding a dead channel is a false all-clear — worse than no affordance). See
docs/decisions.md2026-07-17.1. Empty schedule behind an existing playout
EmptyScheduleHealthCheck(ErsatzTV.Infrastructure/Health/Checks/) already runs the "schedule has playouts but zero items" query and says such schedules "WILL cause errors" — but it selectsProgramSchedulenames, never channel ids.The trap that deferred it: it only understands Classic
ProgramScheduleplayouts. Block, Sequential, Scripted and ExternalJson channels have noProgramScheduleat all (docs/domain-model.md→ entity chain), so a badge driven off that query would be silently absent or wrong for four of the five schedule kinds — the #71 "verify a shared primitive covers ALL variants" class of bug.Needs a per-kind notion of "empty" first (what does an empty Block template / empty YAML mean?), then a per-channel projection.
2. Broken / missing source
FileNotFoundHealthCheck/UnavailableHealthCheckcount media items server-wide; nothing maps them back to a channel. Attribution means walking media item → collection → schedule/block → playout → channel. That's a project, not a field — and needs a cost story (#72'sPlayoutCountwas free becauseGetAllalready included the data).Suggested approach
Once a fault taxonomy genuinely exists, revisit naming the SPA filter "Problems" (see
willNeverPlayinChannelsScreen.tsx— its comment explicitly asks the next person not to broaden the label until this lands).Done-when
PlayoutScheduleKindvalues (or explicitly scoped, with the unsupported kinds reporting unknown rather than healthy) — resolved via built-timeline detection (kind-agnostic by construction; on-demand/unbuilt →unknown)PlayoutItem → MediaItem.Stateseam (exact attribution)GROUP BY PlayoutIdaggregate; integration test + live-E2E confirmed single querywillNeverPlay's comment updated — renamed to "Problems";willNeverPlay→hasProblems, comment rewrittendocs/decisions.md+docs/domain-model.mdupdated —api.channel-health-signalsuperseded byapi.channel-health-object; domain-model + api/spa conventions updatedProblemsbug), re-reviewed cleanImplemented in PR #581.
🔗 Session bundle — channel health / status UX (deferred from #72): #414, #415
The two scope items deferred from #72 (channel health/status UX): #414 (distinguish auto-tuned from user-created channels — needs a real Origin signal) and #415 (per-channel fault detection beyond 'no playout'). Both extend the same channels-list health surface; see docs/decisions.md 2026-07-17.
When you claim any member, check this cluster for co-workable siblings and sweep the disjoint set in one session (per docs/handoffs/chicorytv-issue-queue.md → bundles).
Claiming (Claude Code, Opus 4.8 orchestrator). Bundle sibling #414 has merged (#575), so this is the last open member of the channel-health bundle deferred from #72. This is design-first ("a project, not a field") — starting with scope/design on the per-kind "empty schedule" taxonomy and the broken-source-attribution go/no-go before any code.
Closing record
Outcome: Shipped in PR #581 (squash-merged).
ChannelResponseModel/ChannelDetailResponseModelnow carry a server-derivedhealthobject{ status: Healthy|Problems|Unknown, faults[], playoutCount, brokenSourceItemCount }; the SPA channels list gained a single "Problems" rollup filter with per-fault badges (No playout/Never built/Build failed/Empty schedule/Broken source).Root cause: N/A (feature). The pre-existing gap: only
PlayoutCount==0("No playout") was computed per-channel; empty-schedule and broken-source faults were real but unattributed.Decisions/conventions changed:
api.channel-health-signal(#72) → superseded by new activeapi.channel-health-object. Chose built-timeline detection (Approach A) over per-kind config introspection: every fault derives fromPlayout.BuildStatus+ the materializedPlayoutItemtimeline (Finish >= now,MediaItem.State), which is kind-agnostic by construction (all 5PlayoutScheduleKindcovered — the #71 trap can't bite) and gives exact broken-source attribution with no media→collection→channel join. Server owns the rollup so SPA + MCP read one verdict.Reusable knowledge: (1) The assessable gate must key on the owning channel's
PlayoutMode, not the surface channel's — a mirror channel is forcedContinuous, so a mirror of an on-demand source would otherwise false-flagProblems(absence signals) on the source's drained playouts while the source reportsUnknown. Presence signals (BuildFailed/BrokenSource) apply in all modes; absence signals (NeverBuilt/EmptyUpcoming) suppress for on-demand. (2) A supersession's archived record must keep its rationale prose verbatim — the CIdecisions lifecyclejob runs the validator in diff mode (--base/--head) and flags an appended "Why superseded" paragraph as a prose edit needing[decisions-edit]; the bare validator misses this.Verification: 1867/1867
ErsatzTV.Tests(per-kind guard, real-SQLite repo aggregate, mirror-of-on-demand regression tests); 24 SPA Vitest; live-E2E (health serializes on list+detail, no N+1); 6 per-task reviews + frontier whole-branch review (caught+fixed the mirror bug); full CI green on head4f028120.Deferred: A
(PlayoutId, Finish)index onPlayoutItemif the aggregate is ever measured hot on a large lineup (not needed now). No follow-up issue filed.Docs updated:
docs/decisions.md(+docs/decisions/archive/api.md),docs/domain-model.md,docs/api-conventions.md,docs/spa-conventions.md.timothy referenced this issue2026-07-25 11:34:36 +02:00