Commit Graph
2560 Commits
Author SHA1 Message Date
timothyandClaude Fable 5 caeabf2a2b fix(spa): block-history page-size persistence + History gating, blocks/templates list filter (#213)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Blazor parity conveniences: BlockPlayoutTroubleshootingScreen now persists the block-history
page-size selector to localStorage (ctv-block-history-page-size, same ctv- namespace as
ctv-theme) and restores it on mount, and gates the per-block History action on block.id >= 0
(mirrors BlockPlayoutTroubleshooting.razor, which hides it for synthesized/virtual blocks).
BlocksScreen and TemplatesScreen list screens gain a client-side name/group search filter box,
matching the filter already present on the troubleshooting blocks list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:04:15 +02:00
timothy 8b77d5e739 docs: advance ChicoryTV queue past #208/#209 (PR #216); next = #207+#212 schedules/channel-editor depth
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m49s
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 9m46s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 11m19s
2026-07-10 08:21:58 +02:00
timothy c63cadb834 Merge pull request 'feat: shared Add-to layer — search + media mutation surface (#208, #209)' (#216) from feat/208-209-add-to into main
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Docs update reminder (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
2026-07-10 06:19:04 +00:00
timothy 9e6ea3567c fix(spa): debounce commit must not wipe selection/page when query is unchanged (CI flake + real UX race) (#208 #209)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m55s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-10 08:07:24 +02:00
timothy 279052ccd4 fix(e2e): stale-asset gotcha — rm build wwwroot before copy in e2e-local.sh (+doc)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 2m45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Failing after 2s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-10 07:56:34 +02:00
timothyandClaude Fable 5 50911eb456 fix(spa): review + E2E fixes — schedule-kind gating, portal body font, system-playlist filter, seasons browse, feedback/UX nits (#208 #209)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 3m9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 07:51:49 +02:00
timothy a2ed834ef6 docs: parity verdicts + shared add-to decision entry + spa-conventions §5c (#208 #209)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 3m15s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m29s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-10 00:19:02 +02:00
timothy 9ab851bc6d Merge slice D: media browse/detail mutation surface (#209) 2026-07-10 00:14:37 +02:00
timothyandClaude Fable 5 acc3188a93 feat(spa): media browse/detail mutation surface — add-to, per-show scan, episode info/troubleshoot (#209)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 00:14:06 +02:00
timothy 04cb214ec1 Merge slice C: search-screen mutation surface (#208) 2026-07-10 00:11:54 +02:00
timothyandClaude Fable 5 6426c01efa feat(spa): search-screen mutation surface — add-to, multi-select, add-all, smart collection (#208)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 00:11:24 +02:00
timothy 4c6b10a751 refactor(web): switch add-items/all-items clients to generated OpenAPI types (#208) 2026-07-10 00:03:18 +02:00
timothy 584ad095d9 Merge slice B: shared Add-to dialogs + MediaPosterCard actions slot 2026-07-10 00:00:57 +02:00
timothyandClaude Fable 5 1e72faa60f feat(spa): shared Add-to dialogs + MediaPosterCard actions slot (#208 #209)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 00:00:05 +02:00
timothy 5493065867 Merge slice A: playlist add-items + search all-items endpoints 2026-07-09 23:57:19 +02:00
timothy 819e751cab feat(api): playlist add-items + search all-items endpoints (#208 #209)
- POST /api/playlists/{id:int}/items wraps the existing AddItemsToPlaylist
  command (mirrors CollectionController.AddItems); controller pre-checks
  playlist existence for a real 404, and the handler now rejects adds to
  system (generated) playlists, matching the guard already applied to
  rename/delete/replace-items so the Blazor path gets the same protection.
- GET /api/search/all-items wraps the existing QuerySearchIndexAllItems
  query, returning a new SearchResultAllItemsResponseModel (never expose
  the VM directly) so the SPA's shared "add all to collection/playlist"
  component can materialize ids before calling the add endpoints, same
  two-step flow Blazor's Search.razor already uses.
- Show-detail DTO check: ShowDetailResponseModel already exposes
  libraryId, title, and mediaSourceKind (serialized as a string enum via
  the global StringEnumConverter) - no changes needed.

Adds controller tests (route table + per-action) for both endpoints and
regenerates the OpenAPI document, endpoint index, and SPA client types.
2026-07-09 23:56:50 +02:00
timothyandClaude Fable 5 cfabd6f33f docs: advance ChicoryTV queue past #210/#211 (PR #214); next = #208+#209 shared add-to component
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m14s
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m9s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 7m58s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:40:43 +02:00
timothy 4f8079986f Merge pull request 'feat: playouts delete/reset/erase + collections custom-order/all-kind adds + parity nits (#210, #211, #213)' (#214) from feat/210-211-playouts-collections into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m10s
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m21s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 11m4s
2026-07-09 21:17:29 +00:00
timothyandClaude Fable 5 3528f689d6 test(web): deflake builder collections fan-out test; fix(spa): watermark mode derives from pathname state (review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m8s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
- App.test.tsx: await the initial library fan-out being issued before
  mockClear — passive effects flush asynchronously, so on a slow machine the
  mount fan-out leaked past the clear and polluted the post-click assertion
  (CI-only failure on run 383).
- WatermarksScreen: modeFromPath parses the pathname state string (incl.
  search) instead of reading mutable window.location during render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:06:31 +02:00
timothyandClaude Fable 5 f3d8ac559f docs: parity verdict rows for #210/#211/#213 + decision entries (schedule-reset drop, reorder buttons)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 3m10s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m15s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:42:09 +02:00
timothy 8ba261ded4 Merge branch 'feat/213-nits' into feat/210-211-playouts-collections 2026-07-09 22:40:42 +02:00
timothy 3bccb568ad feat(spa): small parity nits — block copy, watermark copy, trash select-all, trakt copy fix (#213)
- BlocksScreen: add copy-to-group dialog (mirrors TemplatesScreen), wiring the
  already-existing copyBlock() API client
- WatermarksScreen: add copy via /add?from={id} prefill (mirrors
  FFmpegProfilesScreen), no new endpoint needed
- TrashScreen: add Select all / Clear selection to the action bar; bump
  PAGE_SIZE to the search API's max (100) — true paging needs a page param
  the API doesn't have yet
- TraktListsScreen: fix stale callout text — "View matched items" now opens
  the SPA's own search screen, not the Classic UI

docs/decisions.md: record the YAML-validator paste-textarea deviation, the
channel-number prompt-renumber deviation, extend the "table not calendar"
convention to the deco-templates editor, and document the trash 100-item cap.
2026-07-09 22:40:08 +02:00
timothy 606d50bb8d feat(spa): collection custom-order reorder UI + all-kind add picker (#211)
Adds PUT /api/collections/{id}/custom-order support (updateCollectionCustomOrder)
and a reorder mode in ManualItemsView: loads every page of a manual collection
(so the wholesale-replace PUT never drops items), lets the user move items with
up/down icon buttons, and saves/cancels. Reorder is offered for any manual
collection with useCustomPlaybackOrder on, not just movies-only (server/enumerator
already support any kind).

Widens the add-items picker (ADDABLE_TYPE_LIST, MEDIA_KIND_FILTERS,
toAddItemsRequest) from 4 to all 10 addable media kinds; the default "All" search
fan-out stays Movie/Show/Artist (seasons excluded per #180), with the new kinds
reachable via their specific filter, mirroring Blazor's per-kind list pages.
2026-07-09 22:35:43 +02:00
timothyandClaude Fable 5 91e23bcd57 feat(spa): wire playout delete/reset/erase + scheduling context + templates preview calendar (#210)
Adds the SPA slice for the newly-merged playout endpoints:
- api/playouts: deletePlayout, resetChannelPlayout, erasePlayoutItems,
  erasePlayoutItemsAndHistory, getPlayoutItemSchedulingContext (+ types).
- PlayoutsScreen: enable per-playout Reset (channel playout reset), Delete
  (danger, confirm), and erase-by-kind buttons (Block = items + items&history;
  Classic/Sequential/Scripted = items&history; none for ExternalJson); replaces
  the stale "no per-playout reset endpoint" block. Disable Alternate schedules
  for OnDemand Classic playouts (Blazor parity).
- Per-item scheduling-context dialog (info button when hasSchedulingContext &&
  id != null), rendering the decoded JSON in a scrollable <pre>.
- Playout-templates editor: "Preview calendar" toggle with a Monday-first month
  grid; winning template per day computed client-side via a colocated
  appliesToDate/firstMatchingIndex helper that ports
  AlternateScheduleSelector.GetScheduleForDate / PlayoutTemplateEditViewModel
  .AppliesToDate exactly (inclusive range, year-wrap reverse, start-day rollover,
  end-day clamp, explicit-year override).
- Button gains a title prop (parity with IconButton) for the disabled-button hint.

Tests: api client URL/method assertions, PlayoutsScreen action flows
(delete/reset/erase per kind, OnDemand gating, scheduling-context dialog +
error), calendar-helper unit tests, and a preview-calendar render test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:30:28 +02:00
timothyandClaude Fable 5 e66b926206 feat(api): expose PlayoutItem row id on the playout-items list DTO (#210)
The SPA needs the item's row id to call
GET /api/playouts/items/{id}/scheduling-context. Plumbed through
PlayoutItemViewModel -> PlayoutItemResponseModel as a nullable Id
(null for synthesized UNSCHEDULED gap rows, which are PlayoutGaps,
not PlayoutItems). Additive for existing consumers (Playouts.razor
reads the VM by property). Regenerated v1.json + v1.d.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:10:45 +02:00
timothyandClaude Fable 5 f1b3879d51 feat(api): playout erase/scheduling-context + collection custom-order (#210, #211)
Backend slice for the ChicoryTV playouts and collections screens.

PlayoutController:
- POST /api/playouts/{id}/erase-items (204; 404 pre-check; 422 unless
  Block/Sequential/Scripted) -> ErasePlayoutItems
- POST /api/playouts/{id}/erase-items-and-history (204; 404; 422 unless
  Classic/Block/Sequential/Scripted) -> ErasePlayoutHistory
- GET /api/playouts/items/{id}/scheduling-context (200/404) decodes a
  playout item's stored context by row id via a new
  GetPlayoutItemSchedulingContext query that reuses ProcessSchedulingContext
- PlayoutItemResponseModel gains HasSchedulingContext (no raw JSON in list)
- PlayoutListItemResponseModel gains PlayoutMode (ChannelNumber already present)

CollectionController:
- PUT /api/collections/{id}/custom-order (204; 404 pre-check; 422) with
  UpdateCollectionCustomOrderRequest deriving CustomIndex from array order
- GetCollectionItemsHandler orders by CustomIndex (nulls last) then title/id
  when the collection's UseCustomPlaybackOrder is set

Tests: controller route + behavior tests, OpenAPI ProblemDetails TestCases,
GetCollectionItems custom-order handler test. Regenerated v1.json, v1.d.ts,
endpoint-index.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:05:51 +02:00
timothyandClaude Fable 5 eb6e712c38 docs: #91 phase (b) cold-review gate BLOCKED — mutation-depth sweep results, gates #202/#207-#213 (#203 resolved)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m5s
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 9m51s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 7m47s
Cold adversarial review (external, zero shared context) returned GATE: BLOCKED.
blazor-route-parity.md now embeds per-cluster mutation-depth verdicts; schedules +
media-sources rows moved to Section 3 (disproven SPA-READY). Handoff queue reordered:
#210/#211 -> #208/#209 -> #207/#212 -> #202 -> removal PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:43:45 +02:00
timothyandClaude Fable 5 688cf10fee docs: advance ChicoryTV queue past #198/#193 (PRs #200/#201); next = #91 phase b cold-review gate
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m35s
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 9m58s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 11m20s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:22:28 +02:00
timothy 2b91837174 Merge pull request 'fix(openapi): spec property naming now mirrors the runtime Newtonsoft serializer (#198)' (#201) from fix/198-openapi-casing into main
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Docs update reminder (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
2026-07-09 18:19:20 +00:00
timothy 8a263e7c93 Merge pull request 'fix(api): existence-check rerun-collection selectedId per selection type (#193)' (#200) from fix/193-rerun-existence into main
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Docs update reminder (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
2026-07-09 18:19:08 +00:00
timothyandClaude Fable 5 e0d30c9b7b test(api): cover [JsonProperty] path + fail loudly on rename collision (#198, review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m59s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m31s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adversarial-review follow-ups:

- OpenApiSerializerContractTests: add a fifth case, a fully-populated
  ChannelResponseModel — the only DTO with a [JsonProperty("ffmpegProfile")]
  override, i.e. the attribute path of NewtonsoftSchemaNamingTransformer that
  the existing four cases never exercised.
- NewtonsoftSchemaNamingTransformer: a rename colliding with an existing schema
  key now throws InvalidOperationException (naming the type and keys) instead of
  silently overwriting/dropping a property — the generator must never emit a
  lossy spec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:06:26 +02:00
timothyandClaude Fable 5 d9faa8be7b docs: spec-follows-runtime naming decision + casing wart (#198)
- api-conventions.md §5a: runtime Newtonsoft casing vs generated spec, the
  schema transformer that mirrors it, and the contract test guarding it.
- decisions.md: append the "wire format is source of truth; spec follows via the
  real contract resolver" decision.
- spa-conventions.md §4: trust the generated key casing; note the removed
  troubleshooting escape hatch and runtime-cased test mocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:00:48 +02:00
timothyandClaude Fable 5 7ffde6225f fix(web): read runtime-cased ffmpegProfileId/ffmpegProfile keys (#198)
Regenerate v1.d.ts from the aligned spec (fFmpegProfileId->ffmpegProfileId,
fFmpegProfile->ffmpegProfile) and update every SPA reader/writer and test mock
to the runtime casing:

- ChannelEditScreen: read channel.ffmpegProfileId; draft/set/select use
  ffmpegProfileId.
- ChannelBuilder: template reads/writes + ADVANCED_KEYS use ffmpegProfileId.
- App.tsx channel list: read channel.ffmpegProfile (was fFmpegProfile, a latent
  bug that always rendered "Unassigned" since the runtime never sent that key).
- PlaybackTroubleshootingScreen: drop the #198 escape hatch (rawChannel cast +
  dual-read) and read data.channel.ffmpegProfileId directly.
- Test mocks now use runtime casing; pinning comments updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:00:38 +02:00
timothyandClaude Fable 5 f26790aa35 fix(api): mirror runtime Newtonsoft JSON casing in OpenAPI spec (#198)
The OpenAPI document is generated from System.Text.Json metadata, whose
camelCase drifted from the runtime MVC serializer (Newtonsoft via
CustomContractResolver/CustomNamingStrategy): the spec said "fFmpegProfileId"
and "fFmpegProfile" while the wire emits "ffmpegProfileId" (naming-strategy
special case) and "ffmpegProfile" (ChannelResponseModel's [JsonProperty]
override). That fed the SPA the wrong keys.

Add NewtonsoftSchemaNamingTransformer, an OpenAPI schema transformer registered
on all three documents that renames each object schema's Properties (and
Required) keys through the SAME Newtonsoft contract resolver the runtime uses,
so the spec matches the wire format by construction. Regenerate v1.json.

Guard with OpenApiSerializerContractTests: serializes fully-populated DTOs
through the runtime Newtonsoft settings and pins the v1.json schema property
sets to the emitted keys, failing if generation drifts again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:00:23 +02:00
timothyandClaude Fable 5 6dac2d724c fix(#193): review follow-ups — drop Playlist option, non-tracking existence check
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m43s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m52s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adversarial review follow-ups on the #193 rerun selection validation:
- RerunCollectionEditor.razor: remove the CollectionType.Playlist
  dropdown option — RerunCollection has no PlaylistId column, so the
  option previously persisted a broken row and now fails validation.
- RerunCollectionSelectionValidation: replace FindAsync with a
  non-tracking AnyAsync(EF.Property<int>("Id") == id) existence check
  so nothing is materialized/tracked into the save-path context.
- Tests: pin the deliberate 422-not-404 choice
  (ShouldNotBeOfType<NotFoundError>) and cover the unsupported-type
  default arm (Playlist -> "Unsupported collection type"), which was
  reachable via the Blazor editor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:53:29 +02:00
timothyandClaude Fable 5 ad7e151565 fix(#193): validate rerun collection selection existence
RerunCollectionRequestMapping.ResolveSelection builds an id-only VM
with no existence check, so CreateRerunCollectionHandler and
UpdateRerunCollectionHandler blindly copied CollectionId /
MultiCollectionId / SmartCollectionId / MediaItemId from the request
onto the RerunCollection FK columns. A bad id persisted a dangling FK
or 500'd at SaveChanges.

Add RerunCollectionSelectionValidation, a shared per-CollectionType
existence check (Collection/MultiCollection/SmartCollection sets, and
subtype-accurate media-item sets: Shows/Seasons/Artists/Movies/
Episodes/MusicVideos/OtherVideos/Songs/Images/RemoteStreams), composed
into both handlers' Validate via the existing .Apply(...) pattern so a
missing/mismatched id now fails validation (422) with a clear message
instead of persisting or 500ing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:44:31 +02:00
timothyandClaude Fable 5 415da214ec docs: advance ChicoryTV queue past #145 (PR #199); next = #198 casing drift + #193; #91 phase b gated on cold review
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m36s
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m7s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 11m28s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:24:05 +02:00
timothy af17dafcab Merge pull request 'feat: SPA playback troubleshooting screen + status/subtitles/stream-selectors endpoints (#145)' (#199) from feat/145-troubleshooting-spa into main
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Docs update reminder (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
2026-07-09 06:21:03 +00:00
timothyandClaude Fable 5 63983244ab fix(web): channel-mode Play sent ffmpegProfile=0 (runtime/spec key drift, #198)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m16s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m16s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
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>
2026-07-09 08:04:24 +02:00
timothyandClaude Fable 5 1ae8facaab docs(spa-conventions): document HlsPlayer playToken prop
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 07:53:03 +02:00
timothyandClaude Fable 5 284b65a936 docs: #145 review nits — media-sample gating decision, entry-point note
- 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>
2026-07-09 07:52:35 +02:00
timothyandClaude Fable 5 58a3bb1a2b fix(web): repeat Play + startup race in playback troubleshooting
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>
2026-07-09 07:52:27 +02:00
timothyandClaude Fable 5 6e2e0e1858 feat(api): optional refresh param on GET /api/graphics-elements
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>
2026-07-09 07:52:14 +02:00
timothyandClaude Fable 5 add5d4c11d docs: record #145 SPA playback troubleshooting parity, decisions, HLS convention
- 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>
2026-07-09 00:03:46 +02:00
timothyandClaude Fable 5 6202886b8f feat(web): playback troubleshooting screen (#145)
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>
2026-07-09 00:03:39 +02:00
timothyandClaude Fable 5 40299f5ee6 feat(api): SPA wrappers for troubleshoot playback stream-selectors/subtitles/status
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>
2026-07-09 00:03:26 +02:00
timothyandClaude Fable 5 6d9619a3bc feat(web): add hls.js and reusable HlsPlayer component
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>
2026-07-09 00:03:21 +02:00
timothyandClaude Fable 5 8ada776324 test(troubleshoot): cover new troubleshooting playback endpoints
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>
2026-07-08 23:46:41 +02:00
timothyandClaude Fable 5 5815e4b437 feat(api): add troubleshooting stream-selectors, subtitles, and status endpoints
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>
2026-07-08 23:46:35 +02:00
timothyandClaude Fable 5 cf33af4572 feat(troubleshoot): add troubleshooting playback status store
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>
2026-07-08 23:46:25 +02:00