Ports the last remaining Blazor-only route — /system/troubleshooting/playback — to the ChicoryTV SPA, completing #145 (the final functional gate for #91 phase b).
Backend
GET /api/troubleshoot/playback/stream-selectors, GET /api/troubleshoot/playback/subtitles/{mediaItemId} (404 pre-check), GET /api/troubleshoot/playback/status.
New ITroubleshootingPlaybackStatusStore (Core singleton, TroubleshootingNotifier precedent) fed by a MediatR INotificationHandler<PlaybackTroubleshootingCompletedNotification>; reset at session lock in PrepareTroubleshootingPlaybackHandler — replaces Blazor''s in-process Courier subscription with a pollable status (state/exitCode/speed/logs tail).
GET /api/graphics-elements?refresh=true option (Blazor refresh-before-list parity).
SPA
/app/troubleshooting/playback (?channel= / ?mediaItem= modes) mirroring PlaybackTroubleshooting.razor field-for-field: profile/stream-selector/subtitle/watermark/graphics pickers with Blazor gating rules, seek defaults, RemoteStream handling; hls.js HlsPlayer (new reusable component, playToken re-attach); 2s status polling with starting→running→settled phase machine (30s timeout); logs pane + speed badge; archive/sample downloads; entry points on the channels table + movie detail page.
Deviations (documented in docs/decisions.md): datetime-local instead of Chronic NL parsing; polling instead of push; media-sample download gated while running.
MUST-FIX: repeat Play with identical settings never re-issued the session request (state no-op) → playToken teardown/re-attach + regression test.
SHOULD-FIX: first-poll race vs late server lock → starting phase ignores stale settled results until server-observed running; graphics list staleness → refresh param.
Live-E2E-found: channel mode sent ffmpegProfile=0 — runtime JSON is Newtonsoft-cased (ffmpegProfileId) while generated types say fFmpegProfileId; worked around here, root cause filed as #198 (also affects ChannelEditScreen on main).
Verification
ErsatzTV.Tests 927, Core.Tests 493(+1 skip), Architecture 5, web 385 — all green locally; live E2E on a scratch instance verified empty state, both modes, request shape (ffmpegProfile=2, ISO start), 30s timeout path, repeat-Play re-request (first request ERR_ABORTED by hls destroy), bogus-id 404 handling, downloads, and /app/troubleshooting regression.
Add a singleton ITroubleshootingPlaybackStatusStore (Core, alongside
TroubleshootingNotifier) that records the exit code + speed of the most
recent troubleshooting playback session. A new MediatR notification
handler writes to it on PlaybackTroubleshootingCompletedNotification, and
PrepareTroubleshootingPlaybackHandler resets it when a new session starts
(both the channel and media-item lock paths).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add three GET endpoints to TroubleshootController for the SPA port of the
playback troubleshooting page:
- /api/troubleshoot/playback/stream-selectors (List<string>)
- /api/troubleshoot/playback/subtitles/{mediaItemId} (404 pre-check via
GetMediaItemInfo; maps SubtitleViewModel -> TroubleshootingSubtitleResponseModel)
- /api/troubleshoot/playback/status (TroubleshootingPlaybackStatusResponseModel:
idle/running/completed/failed + exitCode/speed + logs.txt tail)
Regenerate v1.json, endpoint-index.md, and the web API types.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add controller tests for stream-selectors, subtitles (404 + mapping), and
the status endpoint's idle/running/completed/failed transitions (using the
real status store), plus a unit test for the notification handler. Add the
subtitles 404 case to the OpenAPI ProblemDetails contract test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds hls.js as a web dependency and a small HlsPlayer wrapper that attaches
hls.js when MSE is available (config mirrors the legacy _Host.cshtml
previewChannel: liveDurationInfinity + unbounded manifest time-to-first-byte,
since the troubleshooting playback.m3u8 endpoint blocks until segments exist)
and falls back to native HLS on Safari. Tears down the instance on src change
and unmount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds thin client wrappers (+ URL-building tests) for GET
/api/troubleshoot/playback/{stream-selectors,subtitles/{id},status}, used by
the playback troubleshooting screen (#145).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports Blazor Pages/Troubleshooting/PlaybackTroubleshooting.razor to the SPA at
/app/troubleshooting/playback. Driven by ?mediaItem={id} (media mode) or
?channel={id} (channel mode); neither shows a friendly empty state (no redirect).
- Channel mode: FFmpeg profile (channel's preselected), stream selector, and a
datetime-local Date/Time (deviation from Blazor's Chronic natural-language
parse) sent as the m3u8 'start' ISO param.
- Media mode: subtitle (mutually exclusive with stream selector), watermark +
graphics-element multi-select (repeated watermark/graphicsElement id params),
Start From Beginning (disabled for RemoteStream) + Seek Seconds.
- Play builds playback.m3u8, plays via HlsPlayer, and polls
/api/troubleshoot/playback/status every 2s; on running->completed/failed it
stops polling, shows the log tail + a colored speed badge, and a completion
notice. Polls once on mount to reflect a session running elsewhere.
- Entry points: enable the Channels table Troubleshoot action and add a
Troubleshoot Playback button to the movie detail page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- blazor-route-parity.md: move /system/troubleshooting/playback from Section 3
(BLAZOR-ONLY) to Section 2 (SPA-ready); Section 3 now has no blocking rows.
- decisions.md: poll-based completion feedback (no SPA push channel) and the
datetime-local vs Chronic deviation.
- spa-conventions.md: new HlsPlayer reusable-component + test-mocking note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review SHOULD-FIX (#145): the SPA graphics picker could go stale because
Blazor ran RefreshGraphicsElements (disk->DB sync) before listing, while the
API endpoint never refreshed — a newly added .yml would not appear. GET
/api/graphics-elements?refresh=true now sends RefreshGraphicsElements before
the list query; default false leaves existing callers untouched. The playback
troubleshooting screen passes refresh=true. Controller tests cover
refresh-iff-true ordering; regenerated OpenAPI v1.json (endpoint index and
generated TS schemas unchanged - query params are not part of either).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review MUST-FIX + SHOULD-FIX (#145):
- Repeat Play with unchanged settings was fully broken: the manifest GET is
what starts a session server-side, but an identical URL made setPlayerSrc a
state no-op, so HlsPlayer never re-attached and no request was issued —
while the poll saw the previous session's stored result and toasted a
spurious success over a dead player. HlsPlayer now takes a playToken prop
(incremented per Play) that forces teardown/re-attach even for identical src.
- First-poll race: PrepareTroubleshootingPlayback runs DB queries BEFORE
taking the playback lock, so polls right after Play can still see the stale
settled result. Replaced the manually-seeded 'running' assumption with a
play-phase machine (idle -> starting -> running -> idle): settled statuses
are ignored during 'starting' until the server has reported running at
least once, with a 30s timeout that surfaces an error and stops polling.
Play + downloads are gated and a Starting/Transcoding indicator shows for
the whole busy window.
Nits: mediaItem takes precedence over channel when both query params are
present (matching Blazor); on-mount poll comment notes the intentional
post-restart log recovery. Tests: repeat-play re-issues loadSource, stale
settled ignored until running observed, starting timeout, download gating
while busy, both-params precedence (web suite 378 -> 384).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- decisions.md: Download Media Sample is gated while a session is active in
the SPA (Blazor only gated Download Results) — intentional deviation.
- blazor-route-parity.md: playback row notes entry points are the channels
table + movie detail page only for now (other media kinds need a hand-built
?mediaItem= URL).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live E2E caught what unit tests could not: the runtime JSON from GET
/api/channels/{id} is Newtonsoft-cased ('ffmpegProfileId'), while the
generated OpenAPI types say 'fFmpegProfileId' (STJ naming) — so the
channel-mode default-profile read was undefined, the form held null, and
buildPlaybackUrl fell back to ffmpegProfile=0. The unit-test channel mock was
typed from the same wrong generated key, so mocks conformed to the spec, not
the server.
- Read the runtime key first via a typed escape hatch, fall back to the spec
key, then to the first profile — never 0 (see #198 for the root spec-
generation fix; ChannelEditScreen's same latent bug is #198's scope).
- buildPlaybackUrl now refuses to build a URL with a null profile, and Play
is additionally gated on ffmpegProfileId != null.
- streamSelectorMode/streamSelector verified against live runtime JSON: no
leading acronym, keys match the generated types — noted in a comment.
- Channel mock now pins the RUNTIME shape (ffmpegProfileId), and a new
channel-mode Play test asserts ffmpegProfile=2 + channel=7 + ISO start in
the URL (web suite 384 -> 385).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
timothy
merged commit af17dafcab into main2026-07-09 08:21:04 +02:00
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.
Ports the last remaining Blazor-only route —
/system/troubleshooting/playback— to the ChicoryTV SPA, completing #145 (the final functional gate for #91 phase b).Backend
GET /api/troubleshoot/playback/stream-selectors,GET /api/troubleshoot/playback/subtitles/{mediaItemId}(404 pre-check),GET /api/troubleshoot/playback/status.ITroubleshootingPlaybackStatusStore(Core singleton,TroubleshootingNotifierprecedent) fed by a MediatRINotificationHandler<PlaybackTroubleshootingCompletedNotification>; reset at session lock inPrepareTroubleshootingPlaybackHandler— replaces Blazor''s in-process Courier subscription with a pollable status (state/exitCode/speed/logs tail).GET /api/graphics-elements?refresh=trueoption (Blazor refresh-before-list parity).SPA
/app/troubleshooting/playback(?channel=/?mediaItem=modes) mirroringPlaybackTroubleshooting.razorfield-for-field: profile/stream-selector/subtitle/watermark/graphics pickers with Blazor gating rules, seek defaults, RemoteStream handling; hls.jsHlsPlayer(new reusable component,playTokenre-attach); 2s status polling withstarting→running→settledphase machine (30s timeout); logs pane + speed badge; archive/sample downloads; entry points on the channels table + movie detail page.Review + E2E hardening (fork adversarial review + live Playwright pass)
playTokenteardown/re-attach + regression test.startingphase ignores stale settled results until server-observedrunning; graphics list staleness →refreshparam.ffmpegProfile=0— runtime JSON is Newtonsoft-cased (ffmpegProfileId) while generated types sayfFmpegProfileId; worked around here, root cause filed as #198 (also affectsChannelEditScreenon main).Verification
ErsatzTV.Tests 927, Core.Tests 493(+1 skip), Architecture 5, web 385 — all green locally; live E2E on a scratch instance verified empty state, both modes, request shape (
ffmpegProfile=2, ISOstart), 30s timeout path, repeat-Play re-request (first requestERR_ABORTEDby hls destroy), bogus-id 404 handling, downloads, and/app/troubleshootingregression.Docs: blazor-route-parity (playback row → Section 2; Section 3 now empty), decisions.md, spa-conventions (HlsPlayer), endpoint-index + v1.json regenerated.
Closes #145.
Add three GET endpoints to TroubleshootController for the SPA port of the playback troubleshooting page: - /api/troubleshoot/playback/stream-selectors (List<string>) - /api/troubleshoot/playback/subtitles/{mediaItemId} (404 pre-check via GetMediaItemInfo; maps SubtitleViewModel -> TroubleshootingSubtitleResponseModel) - /api/troubleshoot/playback/status (TroubleshootingPlaybackStatusResponseModel: idle/running/completed/failed + exitCode/speed + logs.txt tail) Regenerate v1.json, endpoint-index.md, and the web API types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Adds thin client wrappers (+ URL-building tests) for GET /api/troubleshoot/playback/{stream-selectors,subtitles/{id},status}, used by the playback troubleshooting screen (#145). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Ports Blazor Pages/Troubleshooting/PlaybackTroubleshooting.razor to the SPA at /app/troubleshooting/playback. Driven by ?mediaItem={id} (media mode) or ?channel={id} (channel mode); neither shows a friendly empty state (no redirect). - Channel mode: FFmpeg profile (channel's preselected), stream selector, and a datetime-local Date/Time (deviation from Blazor's Chronic natural-language parse) sent as the m3u8 'start' ISO param. - Media mode: subtitle (mutually exclusive with stream selector), watermark + graphics-element multi-select (repeated watermark/graphicsElement id params), Start From Beginning (disabled for RemoteStream) + Seek Seconds. - Play builds playback.m3u8, plays via HlsPlayer, and polls /api/troubleshoot/playback/status every 2s; on running->completed/failed it stops polling, shows the log tail + a colored speed badge, and a completion notice. Polls once on mount to reflect a session running elsewhere. - Entry points: enable the Channels table Troubleshoot action and add a Troubleshoot Playback button to the movie detail page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Live E2E caught what unit tests could not: the runtime JSON from GET /api/channels/{id} is Newtonsoft-cased ('ffmpegProfileId'), while the generated OpenAPI types say 'fFmpegProfileId' (STJ naming) — so the channel-mode default-profile read was undefined, the form held null, and buildPlaybackUrl fell back to ffmpegProfile=0. The unit-test channel mock was typed from the same wrong generated key, so mocks conformed to the spec, not the server. - Read the runtime key first via a typed escape hatch, fall back to the spec key, then to the first profile — never 0 (see #198 for the root spec- generation fix; ChannelEditScreen's same latent bug is #198's scope). - buildPlaybackUrl now refuses to build a URL with a null profile, and Play is additionally gated on ffmpegProfileId != null. - streamSelectorMode/streamSelector verified against live runtime JSON: no leading acronym, keys match the generated types — noted in a comment. - Channel mock now pins the RUNTIME shape (ffmpegProfileId), and a new channel-mode Play test asserts ffmpegProfile=2 + channel=7 + ISO start in the URL (web suite 384 -> 385). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>