Channel lineup health at-a-glance (status states in the channels list) #72

Closed
opened 2026-07-01 23:44:18 +02:00 by timothy · 3 comments
Owner

UX concept borrowed from PseudoTV (its channel-manager color states). Folds into the redesigned Channels list (#59).

Goal

See the health of a large channel lineup at a glance, especially channels that will fail to play.

Scope

  • Per-channel status indicators in the Channels list, e.g.:
    • User-defined vs auto-generated (from #-auto-tuning) origin,
    • Failed / no content (collection empty, no playout, broken source) — the important one,
    • Disabled / hidden (already exists as (D)/(H) markers — unify visually).
  • Make "this channel won't play and here's why" obvious.

Notes

  • Primarily a UI treatment on the Channels hero screen + a small status query in the API.

Related: #59.

Done-when

  • GET /api/v1/channels returns a per-channel playoutCount (Mirror-aware — a mirror channel counts its source's playouts)
  • The channels list shows a visible "No playout" fault state (not tooltip-only) + a matching No playout filter to collapse a large lineup to just the faults (renamed from "Problems" per review — a broader label would read as a false all-clear while #415's fault classes are uncomputed)
  • The detail endpoint's always-zero playoutCount is fixed, with a regression test proven non-vacuous (removing the include turns it red)
  • Backend + SPA test suites green
  • Docs updated in the same PR: decisions.md (API shape + what is deliberately not computed) and domain-model.md; design-system prototype mirrored (#388)
  • Deferred scope filed as follow-up issues, not silently dropped: #414 (origin marker, blocked-by #383) and #415 (empty-schedule + broken-source attribution)
  • adversarial review passed

Scope note (2026-07-17): scope item (a) origin and most of (b) — empty-schedule, broken-source — are deliberately out of this PR; only "no playout" ships as the fault signal. See decisions.md 2026-07-17 for why each is unsafe to approximate today (short version: EmptyScheduleHealthCheck only understands Classic playouts, media faults have no channel attribution, and no honest origin signal exists without a new column + migration on the auto-tune arc's surface).

UX concept borrowed from **PseudoTV** (its channel-manager color states). Folds into the redesigned Channels list (#59). ## Goal See the health of a large channel lineup at a glance, especially channels that will fail to play. ## Scope - Per-channel status indicators in the Channels list, e.g.: - **User-defined** vs **auto-generated** (from #-auto-tuning) origin, - **Failed / no content** (collection empty, no playout, broken source) — the important one, - **Disabled / hidden** (already exists as (D)/(H) markers — unify visually). - Make "this channel won't play and here's why" obvious. ## Notes - Primarily a UI treatment on the Channels hero screen + a small status query in the API. Related: #59. ## Done-when - [x] `GET /api/v1/channels` returns a per-channel `playoutCount` (Mirror-aware — a mirror channel counts its source's playouts) - [x] The channels list shows a visible "No playout" fault state (not tooltip-only) + a matching **No playout** filter to collapse a large lineup to just the faults (renamed from "Problems" per review — a broader label would read as a false all-clear while #415's fault classes are uncomputed) - [x] The detail endpoint's always-zero `playoutCount` is fixed, with a regression test **proven non-vacuous** (removing the include turns it red) - [x] Backend + SPA test suites green - [x] Docs updated in the same PR: `decisions.md` (API shape + what is deliberately not computed) and `domain-model.md`; design-system prototype mirrored (#388) - [x] Deferred scope filed as follow-up issues, not silently dropped: **#414** (origin marker, blocked-by #383) and **#415** (empty-schedule + broken-source attribution) - [x] adversarial review passed **Scope note (2026-07-17):** scope item (a) *origin* and most of (b) — empty-schedule, broken-source — are deliberately **out** of this PR; only "no playout" ships as the fault signal. See `decisions.md` 2026-07-17 for why each is unsafe to approximate today (short version: `EmptyScheduleHealthCheck` only understands Classic playouts, media faults have no channel attribution, and no honest origin signal exists without a new column + migration on the auto-tune arc's surface).
timothy added the enhancementpriority: low labels 2026-07-01 23:44:18 +02:00
timothy added the frontend label 2026-07-11 19:03:42 +02:00
timothy added priority: medium and removed priority: low labels 2026-07-17 00:25:10 +02:00
Author
Owner

🔗 Session bundle — channel health / status UX: #72, #164. Both surface channel status in the SPA; work together. (Backlog-grooming 2026-07-17.)

🔗 **Session bundle — channel health / status UX**: #72, #164. Both surface channel status in the SPA; work together. (Backlog-grooming 2026-07-17.)
timothy added the in-progress label 2026-07-17 09:34:15 +02:00
Author
Owner

🔵 Claiming (Claude Code / Opus 4.8 orchestrator session, 2026-07-17) — working this together with #164 as the groomed "🔗 Session bundle — channel health / status UX".

Live claimability verified from raw Gitea state (not selector prose): OPEN, unclaimed, priority: medium, GET /issues/72/dependencies → [] (no open blocked-by).

Why this over the strict tier-2 winner (#176) — stated deviation, per the queue protocol:

  • Tier 2's only unclaimed/unblocked candidate was #176, but it is priority: low and design-first, and its central premise ("Autotune | Nothing — Biggest gap") is stale: #69 is CLOSED and the #383 DetailPanel arc is in flight right now. Its design inputs are actively shifting under #70 (distribution — frontier design verdict posted 09:14) and #383/#384, so a design verdict written today would be built on sand.
  • The rest of tier 2 is excluded: #383 is the umbrella of the bundle #384's session is actively sweeping; #385 blocked by open #70; #386 blocked by #385+#384; #395 blocked by open #381; #59 is an epic container.
  • Tier 3 is empty (no review labels; reviewer audits #20–#24 all have posted deliverables). Tier 4 (priority: high) is empty.
  • #72/#164 are priority: medium, dependency-clear, and genuinely disjoint from the three live sessions (#70 scheduling backend, #384 auto-tune backend, #390 CI) — parallel-safe by file surface. #72 is also a concrete shippable child of the very vision #176 describes.

Selector note (recurring lore): the cheap selector ranked #383 first — it missed that #384 is in-progress and that #383 is that bundle's umbrella. Independent re-derivation from raw issue state caught it. Consistent with the #73 lesson: verify the selector's mechanical outputs (deps + priority + claims) yourself.

Design/scope pass next; will report before implementing.

🔵 **Claiming** (Claude Code / Opus 4.8 orchestrator session, 2026-07-17) — working this together with **#164** as the groomed *"🔗 Session bundle — channel health / status UX"*. **Live claimability verified from raw Gitea state** (not selector prose): OPEN, unclaimed, `priority: medium`, `GET /issues/72/dependencies` → `[]` (no open `blocked-by`). **Why this over the strict tier-2 winner (#176)** — stated deviation, per the queue protocol: - Tier 2's only unclaimed/unblocked candidate was **#176**, but it is `priority: low` and design-first, and its central premise (*"Autotune | Nothing — Biggest gap"*) is **stale**: #69 is CLOSED and the #383 DetailPanel arc is in flight right now. Its design inputs are actively shifting under **#70** (distribution — frontier design verdict posted 09:14) and **#383/#384**, so a design verdict written today would be built on sand. - The rest of tier 2 is excluded: **#383** is the umbrella of the bundle #384's session is actively sweeping; **#385** blocked by open #70; **#386** blocked by #385+#384; **#395** blocked by open #381; **#59** is an epic container. - Tier 3 is empty (no `review` labels; reviewer audits #20–#24 all have posted deliverables). Tier 4 (`priority: high`) is **empty**. - #72/#164 are `priority: medium`, dependency-clear, and genuinely **disjoint** from the three live sessions (#70 scheduling backend, #384 auto-tune backend, #390 CI) — parallel-safe by file surface. #72 is also a concrete shippable child of the very vision #176 describes. *Selector note (recurring lore):* the cheap selector ranked **#383 first** — it missed that #384 is `in-progress` and that #383 is that bundle's umbrella. Independent re-derivation from raw issue state caught it. Consistent with the #73 lesson: verify the selector's mechanical outputs (deps + priority + claims) yourself. Design/scope pass next; will report before implementing.
Author
Owner

Done — merged to main (PR #405)

What shipped

The channels list now flags a channel that will never play — the fault #72 called "the important one" — as a visible "No playout" badge plus a matching No playout filter to collapse a large lineup to just the faults. Backed by a new PlayoutCount on the channels API.

Root cause (a live bug found under the feature)

The feature needed the API to answer "will this channel play?", and it turned out the API was lying about it for every channel: ChannelRepository.GetChannel never .Included Playouts, and the read is AsNoTracking with no lazy proxies, so GetChannelByIdForApiHandler's channel.Playouts?.Count ?? 0 could only ever evaluate to 0. GET /api/v1/channels/{id} reported playoutCount: 0 for every channel on the system, which silently disabled the channel editor's playout-source guard (ChannelEditScreen, gated on playoutCount > 0). The server still enforced the invariant, so nothing corrupted — but a user could flip a channel to Mirror, save, and watch the change silently revert. Both call sites now share Mapper.GetPlayoutsCount (Mirror-aware — a mirror channel counts its source's playouts), pinned by a repository-level test proven non-vacuous (removing the include turns it red).

Design decisions (frontier pass; docs/decisions.md 2026-07-17)

  • Lean list DTO, not a new endpoint or /channels/state — health is config-derived, not per-poll runtime state; free because GetAll already loaded the data.
  • A raw PlayoutCount fact, not a status enum — the auto-tune arc (#383/#384) is about to churn the status taxonomy; an enum would force a breaking rev of frozen-additive /api/v1.

Deliberately NOT computed (each would be wrong today, not merely absent) → filed, not dropped

  • #414 — auto-tuned-vs-user origin marker. Needs a Channel column + dual-provider migration; provenance belongs to the auto-tune arc, so blocked-by #383 (dependency set + verified). The playlist-group-join shortcut is recorded as rejected.
  • #415 — per-channel empty-schedule + broken-source attribution. The trap that deferred empty-schedule: EmptyScheduleHealthCheck only understands Classic playouts, so a badge driven off it would be silently wrong for four of the five schedule kinds (#71 class).

The SPA filter stays "No playout" (not "Problems") until #415 gives the broader label a real taxonomy — willNeverPlay's call-site comment says so.

Files changed

ChannelRepository.cs (+2 includes), Mapper.cs (shared GetPlayoutsCount), GetAllChannelsForApiHandler / GetChannelByIdForApiHandler / GetAllChannelsHandler, ChannelResponseModel (+PlayoutCount), OpenApiSerializerContractTests, new ChannelRepositoryPlayoutIncludeTests, ChannelsScreen.tsx + test, regenerated OpenAPI artifacts, design-system/Channels.jsx.

Docs updated (same PR)

docs/decisions.md (API shape + what's deliberately not computed), docs/domain-model.md (Channel health glossary row), design-system prototype mirrored.

Review

Two independent cold passes (full diff + fix-commit re-review), both MERGEABLE. Findings fixed: a Medium (the "Problems" filter over-promised → renamed "No playout") and a Low (a stale "Problems" reference in domain-model.md).

Follow-ups / related

  • #401 — UpdateChannel silently coerces Mirror→Generated when a playout exists (should be 422). Found here, filed separately as an API-semantics change.
  • #414, #415 — the deferred scope above.

Deferred / not addressed

The design-sync pull was not done — the change was mirrored into design-system/Channels.jsx without first pulling the current prototype from Claude Design. If that screen's design has moved since, the mirror is on a stale base; worth a /design-sync reconcile.

## Done — merged to main (PR #405) ### What shipped The channels list now flags a channel that **will never play** — the fault #72 called "the important one" — as a visible **"No playout"** badge plus a matching **No playout** filter to collapse a large lineup to just the faults. Backed by a new `PlayoutCount` on the channels API. ### Root cause (a live bug found under the feature) The feature needed the API to answer "will this channel play?", and it turned out the API was **lying about it for every channel**: `ChannelRepository.GetChannel` never `.Include`d `Playouts`, and the read is `AsNoTracking` with no lazy proxies, so `GetChannelByIdForApiHandler`'s `channel.Playouts?.Count ?? 0` could only ever evaluate to **0**. `GET /api/v1/channels/{id}` reported `playoutCount: 0` for every channel on the system, which **silently disabled the channel editor's playout-source guard** (`ChannelEditScreen`, gated on `playoutCount > 0`). The server still enforced the invariant, so nothing corrupted — but a user could flip a channel to Mirror, save, and watch the change silently revert. Both call sites now share `Mapper.GetPlayoutsCount` (Mirror-aware — a mirror channel counts its source's playouts), pinned by a repository-level test proven non-vacuous (removing the include turns it red). ### Design decisions (frontier pass; `docs/decisions.md` 2026-07-17) - **Lean list DTO, not a new endpoint or `/channels/state`** — health is config-derived, not per-poll runtime state; free because `GetAll` already loaded the data. - **A raw `PlayoutCount` fact, not a status enum** — the auto-tune arc (#383/#384) is about to churn the status taxonomy; an enum would force a breaking rev of frozen-additive `/api/v1`. ### Deliberately NOT computed (each would be *wrong* today, not merely absent) → filed, not dropped - **#414** — auto-tuned-vs-user **origin** marker. Needs a `Channel` column + dual-provider migration; provenance belongs to the auto-tune arc, so **blocked-by #383** (dependency set + verified). The playlist-group-join shortcut is recorded as rejected. - **#415** — per-channel **empty-schedule** + **broken-source** attribution. The trap that deferred empty-schedule: `EmptyScheduleHealthCheck` only understands **Classic** playouts, so a badge driven off it would be silently wrong for four of the five schedule kinds (#71 class). The SPA filter stays "No playout" (not "Problems") until #415 gives the broader label a real taxonomy — `willNeverPlay`'s call-site comment says so. ### Files changed `ChannelRepository.cs` (+2 includes), `Mapper.cs` (shared `GetPlayoutsCount`), `GetAllChannelsForApiHandler` / `GetChannelByIdForApiHandler` / `GetAllChannelsHandler`, `ChannelResponseModel` (+`PlayoutCount`), `OpenApiSerializerContractTests`, new `ChannelRepositoryPlayoutIncludeTests`, `ChannelsScreen.tsx` + test, regenerated OpenAPI artifacts, `design-system/Channels.jsx`. ### Docs updated (same PR) `docs/decisions.md` (API shape + what's deliberately not computed), `docs/domain-model.md` (Channel health glossary row), design-system prototype mirrored. ### Review Two independent cold passes (full diff + fix-commit re-review), both MERGEABLE. Findings fixed: a Medium (the "Problems" filter over-promised → renamed "No playout") and a Low (a stale "Problems" reference in domain-model.md). ### Follow-ups / related - **#401** — `UpdateChannel` silently coerces Mirror→Generated when a playout exists (should be 422). Found here, filed separately as an API-semantics change. - **#414**, **#415** — the deferred scope above. ### Deferred / not addressed The **design-sync pull** was not done — the change was mirrored *into* `design-system/Channels.jsx` without first pulling the current prototype from Claude Design. If that screen's design has moved since, the mirror is on a stale base; worth a `/design-sync` reconcile.
timothy removed the in-progress label 2026-07-17 18:15:16 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: timothy/ersatztv#72