feat(web): Channels screen (#84) #124

Merged
timothy merged 2 commits from feat/84-channels-screen into main 2026-07-05 11:26:23 +02:00
Owner

Summary

Implements the ChicoryTV Channels screen from design-system/templates/chicorytv-admin/Channels.jsx against the existing channel APIs. The screen replaces the placeholder route with a dense grouped table, live/on-air treatment, disabled/hidden markers, bulk selection/action bar, row delete, and loading/error/empty states using existing SPA primitives.

Closes #84

Endpoint / Widget Mapping

Screen surface API / DTO mapping Scope decision
Channel table rows GET /api/channels -> ChannelResponseModel[] In scope; sorted by numeric sortNumber, grouped by group.
Group headers/counts ChannelResponseModel.group In scope.
Disabled (D) marker/dimming ChannelResponseModel.isEnabled In scope.
Hidden (H) marker ChannelResponseModel.showInEpg In scope.
Live/on-air filter and row rail GET /api/channels/state -> ChannelStateResponseModel.onAir In scope; rendered as backend reports it.
Now-playing lane/progress nullable ChannelStateResponseModel.nowPlaying In scope; null renders Idle / Off air, no fabricated data.
Streaming badge ChannelResponseModel.streamingMode backend display strings from Mapper.cs In scope; branch strings copied from backend mapper.
Bulk renumber POST /api/channels/bulk/renumber In scope; refetches after mutate.
Bulk move group POST /api/channels/bulk/group In scope; refetches after mutate and shows ProblemDetails errors.
Bulk delete POST /api/channels/bulk/delete In scope; confirmation, refetch, ProblemDetails errors.
Row delete DELETE /api/channels/{id} In scope; confirmation + refetch.
Channel-state polling GET /api/channels/state only In scope; 30s interval, active unmount guard, keeps table visible on poll failure.
Preview Legacy preview needs extra support/JWT/profile data not in list DTO Deferred; affordance is present but disabled instead of opening an incomplete raw URL.
Add/Edit full forms Full Channel Builder is #89 Out of scope; Add/Edit route to builder placeholder only.
Troubleshoot Not in this screen's API contract Out of scope; disabled affordance.

Files Changed

  • web/src/api/channels.ts
  • web/src/App.tsx
  • web/src/App.test.tsx
  • web/src/shell.css

Verification

Web:

  • cd web && npm ci
  • npm run lint
  • npm run typecheck
  • npm test -- --run -> 37 passed
  • npm run build

Backend:

  • TZ=UTC dotnet build ErsatzTV.sln -> passed with existing NuGet advisory warnings and unrelated nullable warnings in artwork upload tests
  • TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-build -v normal -> 368/368 passed
  • TZ=UTC dotnet test ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj --no-build -v normal -> 493 passed, 1 skipped

Deferred / Uncertain

  • Preview remains disabled until the SPA has the equivalent of legacy preview gating, JWT access-token support, and stream dialog behavior.
  • Channel Builder create/edit forms remain #89 scope.
  • Troubleshooting remains outside this issue's frontend contract.
  • Local origin/main resolved to 39d76080 after fetch, not the 4ffd777b SHA mentioned in the prompt; this branch was created from the current origin/main as required by the worktree command.
## Summary Implements the ChicoryTV Channels screen from `design-system/templates/chicorytv-admin/Channels.jsx` against the existing channel APIs. The screen replaces the placeholder route with a dense grouped table, live/on-air treatment, disabled/hidden markers, bulk selection/action bar, row delete, and loading/error/empty states using existing SPA primitives. Closes #84 ## Endpoint / Widget Mapping | Screen surface | API / DTO mapping | Scope decision | | --- | --- | --- | | Channel table rows | `GET /api/channels` -> `ChannelResponseModel[]` | In scope; sorted by numeric `sortNumber`, grouped by `group`. | | Group headers/counts | `ChannelResponseModel.group` | In scope. | | Disabled `(D)` marker/dimming | `ChannelResponseModel.isEnabled` | In scope. | | Hidden `(H)` marker | `ChannelResponseModel.showInEpg` | In scope. | | Live/on-air filter and row rail | `GET /api/channels/state` -> `ChannelStateResponseModel.onAir` | In scope; rendered as backend reports it. | | Now-playing lane/progress | nullable `ChannelStateResponseModel.nowPlaying` | In scope; null renders `Idle` / `Off air`, no fabricated data. | | Streaming badge | `ChannelResponseModel.streamingMode` backend display strings from `Mapper.cs` | In scope; branch strings copied from backend mapper. | | Bulk renumber | `POST /api/channels/bulk/renumber` | In scope; refetches after mutate. | | Bulk move group | `POST /api/channels/bulk/group` | In scope; refetches after mutate and shows ProblemDetails errors. | | Bulk delete | `POST /api/channels/bulk/delete` | In scope; confirmation, refetch, ProblemDetails errors. | | Row delete | `DELETE /api/channels/{id}` | In scope; confirmation + refetch. | | Channel-state polling | `GET /api/channels/state` only | In scope; 30s interval, active unmount guard, keeps table visible on poll failure. | | Preview | Legacy preview needs extra support/JWT/profile data not in list DTO | Deferred; affordance is present but disabled instead of opening an incomplete raw URL. | | Add/Edit full forms | Full Channel Builder is #89 | Out of scope; Add/Edit route to builder placeholder only. | | Troubleshoot | Not in this screen's API contract | Out of scope; disabled affordance. | ## Files Changed - `web/src/api/channels.ts` - `web/src/App.tsx` - `web/src/App.test.tsx` - `web/src/shell.css` ## Verification Web: - `cd web && npm ci` - `npm run lint` - `npm run typecheck` - `npm test -- --run` -> 37 passed - `npm run build` Backend: - `TZ=UTC dotnet build ErsatzTV.sln` -> passed with existing NuGet advisory warnings and unrelated nullable warnings in artwork upload tests - `TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --no-build -v normal` -> 368/368 passed - `TZ=UTC dotnet test ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj --no-build -v normal` -> 493 passed, 1 skipped ## Deferred / Uncertain - Preview remains disabled until the SPA has the equivalent of legacy preview gating, JWT access-token support, and stream dialog behavior. - Channel Builder create/edit forms remain #89 scope. - Troubleshooting remains outside this issue's frontend contract. - Local `origin/main` resolved to `39d76080` after fetch, not the `4ffd777b` SHA mentioned in the prompt; this branch was created from the current `origin/main` as required by the worktree command.
timothy added 1 commit 2026-07-05 10:42:01 +02:00
feat(web): add Channels screen
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m26s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m55s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
15c6cdd086
refs #84
Author
Owner

Fable review — PR #124 (head 15c6cdd0), read-only, 3-lens subagent review

Verdict: APPROVE with findings — no blockers. One SUBSTANTIAL correctness finding + one SUBSTANTIAL test gap should be resolved (before merge or as an immediate follow-up, user decides); the rest are nits.

Lenses: (1) deep React correctness, (2) API contract + test quality, (3) design-system adherence + scope.

Contract verification — all clean

  • Every path/method/body in channels.ts matches ChannelController.cs and BulkChannelRequests.cs field-for-field; status codes (204 success, 404/422 ProblemDetails) correctly handled by client.ts → messageFromError.
  • Fixture strings (HLS Direct, MPEG-TS, …) match Mapper.GetStreamingMode exactly; ChannelState fixtures match ChannelStateResponseModel in generated v1.d.ts exactly.
  • Polling discipline holds: the 30s interval calls only GET /api/channels/state; test mock path-matching order has no false positives.
  • Checkbox indeterminate and ProgressBar value={null} are real prop contracts; CSS uses only design-system tokens (no hardcoded colors); prototype fidelity is close; window.prompt/confirm is an acceptable deferral (no Dialog component exists yet); Preview-disabled and /app/new-channel placeholder routing are correct scope boundaries.

SUBSTANTIAL

  1. Bulk ops act on selected-but-filtered-out rows (App.tsx, selectedChannels): select 3 channels under "All", switch filter to "On air" (1 visible), click "Delete selected" — all 3 are deleted while the header checkbox suggests a smaller selection. Fix: clear selection on filter change, or scope bulk ops to selectedIds ∩ visibleIds.
  2. Bulk renumber is completely untested: no test exercises the Renumber button / POST /api/channels/bulk/renumber, success or failure — one of the three bulk mutations shipped with zero coverage of its request-body shape or error surfacing.

NITS

  1. Stale-poll window: a slow state-poll response that started before a mutation+refresh can overwrite channelStates with pre-mutation data for up to 30s (no request-ordering guard).
  2. Per-row Delete IconButton is not disabled={mutating} (actionbar buttons are) — a second in-flight mutation clears the first's error message.
  3. The 30s poll keeps firing while the screen is in error/loading state (responses discarded; wasted traffic, never recovers the errored screen).
  4. renumberSelected uses Number.parseFloat — silently truncates "5.1.2" → 5.1 and accepts negatives; relies on backend validation.
  5. Test gaps beyond renumber: ChannelsErrorState/Retry never rendered under test; background-poll-failure resilience untested; bulk-delete success path (204 + refetch + selection cleared) not directly asserted.
  6. CSS: var(--pad-cell-x, 12px) fallback is wrong (token resolves to 16px); prototype's row-hover highlight and hover-revealed action buttons not implemented; footer pagination from prototype dropped (fine — no server paging exists — but confirm deliberate).

Verified clean on the scary paths: hook ordering, StrictMode double-mount, poll-vs-loading clobber, double-submit on the actionbar, grouped rendering, unknown-channelId lookups, renumber float math.

Not merging — awaiting user consent per protocol, and a decision on whether findings 1–2 go back to Codex or get fixed in-session.

## Fable review — PR #124 (head 15c6cdd0), read-only, 3-lens subagent review **Verdict: APPROVE with findings — no blockers. One SUBSTANTIAL correctness finding + one SUBSTANTIAL test gap should be resolved (before merge or as an immediate follow-up, user decides); the rest are nits.** Lenses: (1) deep React correctness, (2) API contract + test quality, (3) design-system adherence + scope. ### Contract verification — all clean - Every path/method/body in `channels.ts` matches `ChannelController.cs` and `BulkChannelRequests.cs` field-for-field; status codes (204 success, 404/422 ProblemDetails) correctly handled by `client.ts` → `messageFromError`. - Fixture strings (`HLS Direct`, `MPEG-TS`, …) match `Mapper.GetStreamingMode` exactly; `ChannelState` fixtures match `ChannelStateResponseModel` in generated `v1.d.ts` exactly. - Polling discipline holds: the 30s interval calls only `GET /api/channels/state`; test mock path-matching order has no false positives. - `Checkbox indeterminate` and `ProgressBar value={null}` are real prop contracts; CSS uses only design-system tokens (no hardcoded colors); prototype fidelity is close; `window.prompt/confirm` is an acceptable deferral (no Dialog component exists yet); Preview-disabled and `/app/new-channel` placeholder routing are correct scope boundaries. ### SUBSTANTIAL 1. **Bulk ops act on selected-but-filtered-out rows** (`App.tsx`, `selectedChannels`): select 3 channels under "All", switch filter to "On air" (1 visible), click "Delete selected" — all 3 are deleted while the header checkbox suggests a smaller selection. Fix: clear selection on filter change, or scope bulk ops to `selectedIds ∩ visibleIds`. 2. **Bulk renumber is completely untested**: no test exercises the Renumber button / `POST /api/channels/bulk/renumber`, success or failure — one of the three bulk mutations shipped with zero coverage of its request-body shape or error surfacing. ### NITS 3. Stale-poll window: a slow state-poll response that started before a mutation+refresh can overwrite `channelStates` with pre-mutation data for up to 30s (no request-ordering guard). 4. Per-row Delete `IconButton` is not `disabled={mutating}` (actionbar buttons are) — a second in-flight mutation clears the first's error message. 5. The 30s poll keeps firing while the screen is in error/loading state (responses discarded; wasted traffic, never recovers the errored screen). 6. `renumberSelected` uses `Number.parseFloat` — silently truncates `"5.1.2"` → 5.1 and accepts negatives; relies on backend validation. 7. Test gaps beyond renumber: `ChannelsErrorState`/Retry never rendered under test; background-poll-failure resilience untested; bulk-delete success path (204 + refetch + selection cleared) not directly asserted. 8. CSS: `var(--pad-cell-x, 12px)` fallback is wrong (token resolves to 16px); prototype's row-hover highlight and hover-revealed action buttons not implemented; footer pagination from prototype dropped (fine — no server paging exists — but confirm deliberate). Verified clean on the scary paths: hook ordering, StrictMode double-mount, poll-vs-loading clobber, double-submit on the actionbar, grouped rendering, unknown-channelId lookups, renumber float math. **Not merging** — awaiting user consent per protocol, and a decision on whether findings 1–2 go back to Codex or get fixed in-session.
timothy added 1 commit 2026-07-05 11:20:35 +02:00
fix(web): scope channel bulk ops to visible rows, review fixes for #84
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m58s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
ecf6cf7513
- Derive selectedChannels from visibleChannels and clear selection on
  filter change so bulk operations can never act on rows hidden by the
  active filter (reachable via the 30s state poll under the on-air filter)
- Disable the per-row delete button while a mutation is in flight
- Correct the --pad-cell-x fallback (token resolves to 16px)
- Add tests: bulk renumber request body + selection clearing, and
  selection reset across filter changes

Review: PR #124 findings 1, 2, 4, 8

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
timothy merged commit 7ee6e9870c into main 2026-07-05 11:26:23 +02:00
timothy deleted branch feat/84-channels-screen 2026-07-05 11:26:23 +02:00
Sign in to join this conversation.