Commit Graph
2443 Commits
Author SHA1 Message Date
timothyandClaude Fable 5 57bb320af8 feat(api): REST endpoints for decos, deco groups, playout default deco (#144 S3 (#162))
Adds DecoController (groups + decos CRUD + full-state PUT), playlist and
artist/multi-collection search picker wrappers, a PlaylistController for the
break-content playlist cascade, and PUT /api/playouts/{id}/deco. Fixes the
CreateDecoHandler missing deco-group existence check (FK 500 -> 422), matching
the CreateBlock/CreateTemplate precedent. ReplaceDecoRequest.ToCommand rejects
Merge on the non-mergeable sections (422).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:22:39 +02:00
timothy d48e48f1be fix(templates-screen): exclude placeholder blocks, warn on midnight crossing
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m28s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
GET /api/blocks emits a synthesized negative-id "(none)" row per empty
block group; the block picker was including it as a selectable option.
Filter to b.id > 0, mirroring the backend's real block ids.

Also mirror ReplaceTemplateItemsHandler's new end-of-day validation on
the client: flag an item whose start time + block duration crosses
midnight and disable Save, same as the existing overlap-warning path.

Adds coverage: block picker excludes negative-id rows; validate() flags
a midnight-crossing item.
2026-07-07 18:54:34 +02:00
timothy 0299293e14 fix(templates): validate blockId existence and end-of-day in ReplaceTemplateItems
PUT /api/templates/{id} with an unknown blockId threw KeyNotFoundException
(HTTP 500) because the dictionary indexer assumed every referenced block
had been loaded. Validate all blockIds exist before building the overlap
list and return a 422 with "[BlockId] {id} does not exist." on failure,
matching ReplaceBlockItemsHandler's message convention.

Also reject items whose start time + block duration crosses midnight
(> 24:00). Previously these were silently accepted by PUT, then dropped
by GetTemplateItemsHandler's end-of-day filter on every subsequent GET,
so a saved item would vanish and be deleted on the next save.
2026-07-07 18:54:30 +02:00
timothy 2071858523 Merge remote-tracking branch 'origin/main' into feat/144-s2-templates 2026-07-07 18:42:11 +02:00
timothyandClaude Fable 5 a0521cd6dc feat(web): Templates screen (list + editor) on the REST API
Adds a ChicoryTV Templates screen under /app/templates: grouped template
list with group/template create, copy, and delete, and a
/app/templates/{id} editor (name, add-content row that picks a block
group -> block -> five-minute start time, a time-sorted item table with
remove, and a client-side overlap warning that mirrors
ReplaceTemplateItemsHandler's server-side check before allowing save).

Deviates from the Blazor TemplateEditor.razor's drag-to-place day
calendar: uses a plain sorted table + picker row instead, per the S2
spec (parity, not a pixel port).

Adds web/src/api/templates.ts client + copyBlock to blocks.ts (S1 left
the block-copy endpoint unwired; CopyBlock's CQRS shape is identical to
CopyTemplate so it rides along here), plus regenerated v1.d.ts types.

#144 S2 (#162)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:37:59 +02:00
timothyandClaude Fable 5 0203b5b154 chore(api): regenerate OpenAPI spec for templates + block copy endpoints
#144 S2 (#162)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:31:46 +02:00
timothyandClaude Fable 5 ae10eba4d7 feat(api): REST endpoints for templates, template groups, items + block copy
Adds TemplateController mirroring BlockController: groups CRUD, template
CRUD, item replace (PUT full-replace via ReplaceTemplateItems), and a
copy endpoint. Also wires the missing POST /api/blocks/{id}/copy since
CopyBlock's CQRS shape is directly analogous to CopyTemplate.

Fixes CreateTemplateHandler to validate the target template group exists
before insert (previously an unhandled FK violation -> 500), matching the
CreateBlockHandler fix (1e34e00e) for the same class of bug.

New DTOs in ErsatzTV.Core/Api/Scheduling; TimeSpan start times serialize
the same way as PlayoutResponseModel.DailyRebuildTime.

#144 S2 (#162)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:31:17 +02:00
timothy 3b7d5fd5a1 Merge pull request 'feat(web+api): blocks CRUD, items editor & preview (#144 S1, #162)' (#171) from feat/144-s1-blocks into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m27s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m46s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m16s
2026-07-07 16:25:25 +00:00
timothy e3f2cd631a Merge remote-tracking branch 'origin/main' into feat/144-s1-blocks
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m31s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-07 18:15:29 +02:00
timothy a20fef6096 fix(web): round-trip legacy Artist/MultiCollection block items; fix stale AsyncPicker state
BlocksScreen couldn't load/save blocks containing Artist or
MultiCollection items (creatable upstream; ReplaceBlockItemsHandler
already supports both) - itemFromResponse dropped multiCollectionId
and left selectionName empty for those kinds, and toRequestItem
hardcoded multiCollectionId to null, so re-saving such a block
produced a 422 and made it uneditable. Carry the ids/names through
DraftItem and render them read-only (parity with Blazor, which
doesn't offer these kinds in the creation picker either).

Also key the item AsyncPicker by selection identity + collection
type so switching rows or collection type remounts it with clean
state, instead of carrying over a stale query/options list that
could store a wrong-kind id.
2026-07-07 18:15:21 +02:00
timothy 1e34e00e31 fix(api): validate block group exists on block creation
POST /api/blocks with a nonexistent blockGroupId returned an
unhandled DbUpdateException (HTTP 500) because CreateBlockHandler
never checked the referenced block group before inserting, letting
the SQLite FK constraint raise. Add a BaseError validation (422),
matching the "does not exist" precedent already used elsewhere.

Found via live E2E testing.
2026-07-07 18:15:12 +02:00
timothy a228927b4f Merge pull request 'feat(web+api): playout create for all kinds + detail updates (#144 S5, #162)' (#170) from feat/144-s5-playout-create into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m30s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m21s
2026-07-07 16:09:56 +00:00
timothyandClaude Fable 5 ecd9d526d0 feat(web): Blocks screen (list + editor) on the REST API
Adds a ChicoryTV Blocks screen under /app/blocks: grouped block list with
group/block create + delete, and a /app/blocks/{id} editor (name, duration
hours + 5-min increments, stop-scheduling, item table with add/remove/move/
duplicate, per-item collection-type pickers via debounced search endpoints,
conditional playback-order, EPG/watermark toggles, watermark & graphics
multi-selects, and a non-persisting playout preview dialog).

Adds web/src/api/blocks.ts client + regenerated v1.d.ts types.

#144 S1 (#162)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:51:15 +02:00
timothyandClaude Fable 5 6b5bc8e29a feat(api): REST endpoints for blocks, block groups, items, preview + scheduling search pickers
Adds BlockController (block groups + blocks CRUD, items GET, full replace,
non-persisting preview) mirroring ScheduleController, plus four scheduling
search picker endpoints on SearchController (collections, television shows,
television seasons, smart collections). Response DTOs in ErsatzTV.Core/Api/
Scheduling; request DTOs with ToCommand index auto-assignment.

#144 S1 (#162)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:42:40 +02:00
timothyandClaude Fable 5 f06bc064a0 fix(api): apply schedule-file update before daily rebuild time in PUT /api/playouts/{id} (#170 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
A rejected scripted schedule file no longer leaves DailyRebuildTime
half-applied; the file update is the only fallible step post-pre-check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:30:38 +02:00
timothyandClaude Fable 5 15f2beb7f9 fix(web): derive schedules-loading state instead of sync setState in effect (lint)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m43s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:28:13 +02:00
timothy 605b325370 feat(web): wire up Add Playout dialog and playout-details editing (#144 S5 (#162))
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 42s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
- Enable the previously-disabled "Add Playout" button on the Playouts screen:
  a new dialog picks kind (Classic/Block/Sequential/Scripted/ExternalJson),
  channel (annotating channels that already appear in the loaded playouts list
  as "already has a playout" -- the channel list DTO has no playout-count
  field, so this is a best-effort annotation; a real conflict still surfaces
  via the server's 422 in the dialog's error banner), a schedule picker for
  Classic, and a schedule-file input with the same helper text as the Blazor
  PlayoutEditor for the file-backed kinds. On success the new playout is
  selected and the list refreshes.
- Add an "Edit details" affordance on the Playout detail card: a dialog to set
  the daily reset time (half-hour options mirroring SchedulePlayoutReset.razor,
  plus "Do not automatically reset") and, for Sequential/Scripted/ExternalJson
  playouts, the schedule file.
- Add App.test.tsx coverage: classic-playout creation (asserts the POST body
  and dialog close), inline 422 error surfacing, daily-rebuild-time edit
  (asserts the PUT body), and schedule-file-field visibility gated by kind.
  Extends mockDashboardApi with method-aware POST /api/playouts and
  PUT /api/playouts/{id} handling.
2026-07-07 17:09:02 +02:00
timothy 9b7718fd7c feat(web): playout create/update API client for all schedule kinds (#144 S5 (#162))
Regenerate v1.d.ts from the updated OpenAPI spec and add createPlayout()/
updatePlayoutDetails() to the playouts API client, with URL/body-assert tests
following the ffmpegProfiles.test.ts pattern.
2026-07-07 17:08:49 +02:00
timothy 5f202a2175 feat(api): REST endpoints to create playouts of all kinds + update details (#144 S5 (#162))
- CreatePlayoutRequest now carries a PlayoutScheduleKind discriminator, a
  nullable ProgramScheduleId, and a ScheduleFile so POST /api/playouts can
  create Classic, Block, Sequential, Scripted, or ExternalJson playouts (not
  just Classic). ToCommand() validates per-kind requirements and returns
  Either<BaseError, CreatePlayout>, surfacing 422 on mismatched fields via the
  existing ToErrorResult() mapping.
- Add PUT /api/playouts/{id} (UpdatePlayoutDetailsRequest): DailyRebuildTime is
  always applied (null clears the daily reset, matching the Blazor
  SchedulePlayoutReset "Do not automatically reset" semantics); ScheduleFile is
  only valid for Sequential/Scripted/ExternalJson playouts (422 otherwise) and
  dispatches the matching Update*Playout command.
- Playout existence is checked via GetPlayoutById (real 404) before dispatching
  UpdatePlayout, since the command's own "Playout does not exist." validation
  produces a plain BaseError (422), not NotFoundError -- an existing quirk in
  UpdatePlayoutHandler left as-is (out of scope for this slice).
- Extend ApiErrorResponseMetadataTests + PlayoutControllerTests for the new
  Update action and the widened Create action (block/sequential/file-kind
  validation paths).
- Regenerate ErsatzTV/wwwroot/openapi/v1.json via update-openapi.sh.
2026-07-07 17:08:43 +02:00
timothyandClaude Fable 5 e00b7e4d11 docs: advance ChicoryTV queue past parity pass 2 (#142/#143 closed, #141/#145 partial, #157 fixed; PRs #165-#169); next = #144 scheduling slices
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m49s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m49s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 16:24:18 +02:00
timothy 3c37ec5f38 Merge pull request 'feat(web+api): media browse, search & trash for SPA parity (#141)' (#168) from feat/141-media-browse into main
Build ErsatzTV Image / Build & push image (amd64) (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-07 14:20:23 +00:00
timothy f1fae6397e Merge remote-tracking branch 'origin/main' into feat/141-media-browse
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m16s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2026-07-07 16:07:18 +02:00
timothy 72b6d0a76a fix(web): scope library/add-item pickers to their 4 media kinds (#168 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
GET /api/library/browse without mediaType now spans all 10 media kinds
(was 4: Movie/TelevisionShow/TelevisionSeason/Artist), which flooded two
pickers with episodes/songs/etc. mixed in with the intended top-level
items. Fan both consumers out per-kind and merge, restoring pre-#168
scoping without touching the API:

- ChannelBuilder's library browse tab now fans out Movie/TelevisionShow/
  TelevisionSeason/Artist per page (mirrors the existing loadCollections
  pattern) and sums per-kind totalCount so paging/load-more still works.
- CollectionsScreen's add-items search now fans out over the same 4
  addable kinds instead of filtering one unscoped page client-side.

Extended App.test.tsx and CollectionsScreen.test.tsx to assert the
per-kind requests and updated a few fixtures that relied on the old
unscoped browse call returning collection-kind items under the library
tab.
2026-07-07 16:06:50 +02:00
timothy 117afa310e Merge pull request 'test(web): make Libraries scan-polling tests deterministic (fixes #157)' (#169) from fix/157-flaky-scan-polling into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m36s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m14s
2026-07-07 14:02:31 +00:00
timothyandClaude Fable 5 7c4cf55715 test(web): make Libraries scan-polling tests deterministic (fixes #157)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m28s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m43s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The five Libraries polling tests mock window.setInterval to capture the
poll handler, but that handler is registered in a passive useEffect that
only runs after an out-of-act state commit (the async fetch that flips
hasActiveScans). findBy*/waitFor resolve on the DOM mutation via
MutationObserver, which can win the race against React's passive-effect
flush under CI load - leaving intervalHandlers empty when the test
invokes it. The forEach was then a silent no-op: no poll fetch, so
'route delta' timed out at scan-status count > 1, and 'stops polling'
never cleared the 75% label.

Fix: new runPollTick helper flushes pending effects with an empty
act(), asserts the poll handler is actually registered (turning any
future regression into a clear failure instead of a timeout), and
invokes the handlers inside act() so fetch microtasks and state updates
are fully applied before assertions run. All post-tick waitFor blocks
become plain synchronous expects - nothing depends on wall-clock
scheduling anymore. Also aligned the 'stops polling' setInterval mock
with its siblings' >=10000ms cadence filter so it captures exactly the
poll handler, not waitFor's internal 50ms interval callback.

Verified: 20x full App.test.tsx loop under CPU contention, 0 failures;
full vitest suite 202 passed; tsc + eslint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:51:57 +02:00
timothy 7083301ce6 Merge remote-tracking branch 'origin/main' into feat/141-media-browse
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m5s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	ErsatzTV.Tests/Controllers/OpenApiErrorResponseContractTests.cs
#	web/src/App.tsx
#	web/src/screens/SettingsScreen.tsx
2026-07-07 15:50:03 +02:00
timothyandClaude Fable 5 14cafca740 feat(web): media browse, global search & trash screens (#141)
- MediaBrowseScreen (/app/media): one parameterized screen for all 9 top-level
  kinds with an in-screen kind switcher, per-kind search box, 100/page paging
- SearchScreen (/app/search): grouped per-kind results with counts + 'See all';
  TopBar search input now navigates here
- TrashScreen (/app/trash): state:FileNotFound results, multi-select + Empty Trash
  via DELETE /api/media-items and /api/maintenance/empty_trash (confirm dialogs)
- Shared media/mediaKinds (icon/label maps, hueOf, duration helpers) + MediaPosterCard;
  ChannelBuilder and CollectionsScreen now reuse the single-source maps (their
  exhaustive Record<LibraryBrowseMediaType> had to cover the 6 new kinds anyway)
- api: search.ts, mediaItems.ts, maintenance.ts (+ URL-assert tests)
- One consolidated 'Browse' nav entry with an in-screen kind switcher instead of
  9 per-kind nav rows (deviation from Blazor's per-kind Media links)
- Nav: Media group gains Browse/Search/Trash; Settings Classic-UI help trimmed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:39:54 +02:00
timothyandClaude Fable 5 cf21f889bf ci: retrigger main build (flaky Libraries scan-polling test, see #157)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m16s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m49s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:33:54 +02:00
timothy 063dae731f Merge pull request 'feat(web+api): Trakt lists screen + REST API for SPA parity (#142)' (#167) from feat/142-trakt-lists into main
Build ErsatzTV Image / Build & test (.NET) (push) Failing after 1m18s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
2026-07-07 13:31:05 +00:00
timothyandClaude Fable 5 9c03184707 fix: address #167 review — release Trakt lock on failed enqueue; mark busy after generatePlaylist save
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:26:01 +02:00
timothyandClaude Fable 5 6c68c291a0 feat(api): browse all media kinds, grouped search, delete media-items (#141, #161)
- Extend /api/library/browse to episodes, music videos, songs, other videos,
  images and remote streams (new LibraryBrowseMediaType values + hydrators);
  add optional Subtitle to LibraryBrowseItemResponseModel for leaf-item context
- Add GET /api/search: grouped per-kind results reusing the browse query/shape;
  empty query -> 422
- Add DELETE /api/media-items: body { ids }, empty -> 422, success -> 204
- Tests: SearchController, MediaItemsController, security + OpenAPI contract entries
- Regenerate openapi v1.json + web v1.d.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:23:07 +02:00
timothy 1b9d85896c Merge remote-tracking branch 'origin/main' into feat/142-trakt-lists
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m28s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m40s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	ErsatzTV.Tests/Controllers/ApiControllerSecurityTests.cs
#	web/src/App.tsx
#	web/src/screens/SettingsScreen.tsx
2026-07-07 15:13:22 +02:00
timothyandClaude Fable 5 e2663758e1 ci: retrigger main build (flaky Libraries scan-polling test, see #157)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m16s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m41s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m57s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:10:31 +02:00
timothy 5c932fdc54 feat(web): Trakt Lists screen for SPA parity (#142)
Adds web/src/api/trakt.ts wrapping the new /api/trakt/* endpoints
(#160) plus URL-assert tests, and web/src/screens/TraktListsScreen.tsx:
a table (slug, name, match count) with add-by-URL, match/refresh,
delete, and a sub-path editor at /app/trakt-lists/{id} (slug read-only,
autoRefresh/generatePlaylist toggles, save via PUT) - same allowSubPaths
pattern as ChannelEditScreen/SettingsScreen. Since add/match/delete are
async background jobs, the screen polls GET /api/trakt/status while
busy (disabling actions) and refreshes the list on the busy -> idle
transition.

"View matched items" links out to the classic Blazor search page
(/search?query=trakt_list:{traktId}) since the SPA has no search
screen yet (#161) - same interim deep-link pattern used elsewhere.
Registered in App.tsx's Media nav group; SettingsScreen's Classic-UI
help text no longer lists trakt now that it's SPA-native.
2026-07-07 15:09:30 +02:00
timothy 4306b39ef8 feat(api): Trakt lists REST endpoints (#160)
Adds TraktController (GET/POST /api/trakt/lists, GET/PUT/DELETE by id,
POST .../match, GET /api/trakt/status) covering the API gap for SPA
parity issue #142. Add/match/delete dispatch to the same
ChannelWriter<IBackgroundServiceRequest> the classic Blazor "Trakt
Lists" page uses, gated by the existing IEntityLocker (LockTrakt/
IsTraktLocked) singleton; GET /api/trakt/status wraps IsTraktLocked as
the HTTP-observable substitute for the Blazor page's OnTraktChanged
event. URL validation mirrors AddTraktListHandler.ValidateUrl's regexes
(replicated, since that method is private to the handler and returns a
handler-private record) so an obviously-invalid URL gets a synchronous
422 before dispatch.

Adds TraktListResponseModel/PagedTraktListsResponseModel/
TraktStatusResponseModel DTOs, AddTraktListRequest/UpdateTraktListRequest,
controller tests, ApiControllerSecurityTests + OpenApiErrorResponseContractTests
coverage, and regenerates the checked-in OpenAPI document.
2026-07-07 15:09:12 +02:00
timothy e0f36a12fb Merge pull request 'feat(web+api): FFmpeg profile, filler preset & watermark editors for SPA parity (#143)' (#166) from feat/143-transcoding-editors into main
Build ErsatzTV Image / Build & test (.NET) (push) Failing after 1m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 3m28s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been skipped
2026-07-07 13:06:38 +00:00
timothyandClaude Fable 5 66abb24402 test: cover FillerPresetController + WatermarkController in API security test (#166 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m15s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:01:10 +02:00
timothy 7454411dee Merge remote-tracking branch 'origin/main' into feat/143-transcoding-editors
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	web/src/App.tsx
#	web/src/screens/SettingsScreen.tsx
2026-07-07 14:55:55 +02:00
timothyandClaude Fable 5 bef33c2950 fix(api): 404 for missing filler preset / watermark on PUT and DELETE (#159)
Live E2E found PUT/DELETE on a missing filler preset or watermark returned 422
instead of 404. Root cause: the must-exist checks fed a NotFoundError through
LanguageExt Validation, whose aggregation to Either flattens the BaseError
subtype, so ApiResults.ToErrorResult never saw a NotFoundError. The precedent
handlers (DeleteProgramSchedule, DeleteFFmpegProfile, UpdateProgramSchedule)
avoid this by resolving must-exist as an Option and returning the NotFoundError
directly as an Either Left via Option.Match — restructured the four
filler/watermark Update/Delete handlers to that pattern (remaining name
validation still 422s). No Blazor behavior change (it only reads error.Value).

Verified live: PUT/DELETE missing → 404, GET missing → 404, empty-name POST →
422; happy-path CRUD unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:48:38 +02:00
timothyandClaude Fable 5 e95eed11dd feat(web): FFmpeg profile, filler preset, watermark editors (#143)
Three new SPA list+editor surfaces, closing the Blazor-only gap found in the
#91 phase-(b) recon:

- FFmpeg Profiles (System group): list with resolution/video/audio summary +
  add/copy/delete; full General/Video/Audio editor. Per-hwaccel video-format
  and profile rules ported from FFmpegProfileEditViewModelValidator; video
  preset list ported from FFmpegLibraryHelper.PresetsForFFmpegProfile /
  AvailablePresets.ForAccelAndFormat (cited in code). Resolutions from
  /api/settings/resolutions, hwaccel kinds from the new lookup endpoint.
  Copy = client-side clone into the create form (no server copy endpoint).
- Filler Presets (Media group): list + editor with the Blazor VM interlocks
  (Fallback/Tail force mode None; non-MidRoll clears expression; Fallback
  disables chapters; Playlist collection type forces Count mode; duration/
  count/pad fields gated by mode). Collection pickers use the existing
  /api/library/browse client for all seven collection types incl. Playlist.
- Watermarks (System group): list + editor with mode-driven field enabling,
  ChannelLogo vs Custom image source, upload via /api/artwork/uploads
  (target=watermark) and preview via /artwork/watermarks/{path}.
- App.tsx: ScreenIds/routes (sub-paths /add + /{id}) and nav registration;
  api modules ffmpegProfiles/fillerPresets/watermarks with URL-assert tests;
  SettingsScreen Classic-UI help text updated (these are no longer
  Blazor-only) and the FFmpeg-profiles card now links to the new screen.
- Regenerated web/src/api/generated/v1.d.ts from the #159 OpenAPI changes.

web verification: typecheck clean; vitest 188/188 (14 files); vite build OK;
eslint clean on touched files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:33:01 +02:00
timothyandClaude Fable 5 46c8c2e2dc feat(api): filler preset + watermark editor CRUD; ffmpeg profile round-trip (#159)
Backend plumbing for the SPA transcoding editors (#143 blocker):

- FFmpeg profile response DTO round-trip: add NormalizeAudio, NormalizeVideo,
  PadMode, TargetLoudness, NormalizeColors, ResolutionId (keep Resolution name
  for compat); DeinterlaceVideo now bool (was bool?). #nullable enable on the DTO.
- New GET /api/ffmpeg/hardware-acceleration-kinds wrapping
  GetSupportedHardwareAccelerationKinds (returns enum names as strings).
- Filler preset CRUD: GET-by-id/POST/PUT/DELETE on FillerPresetController with
  full request/response DTOs; new FillerPresetFullResponseModel +
  GetFillerPresetByIdForApi. CreateFillerPreset now returns the new id
  (CreateFillerPresetResult) to match sibling Create commands and enable a
  201+Location; updated the one Blazor call site.
- Watermark CRUD: GET-by-id/POST/PUT/DELETE on WatermarkController with full
  DTOs (path+contentType, all 15 fields); new WatermarkFullResponseModel +
  GetWatermarkByIdForApi.
- Startup: register WatermarkLocation/WatermarkSize as OpenAPI string enums
  (they live in ErsatzTV.FFmpeg.State and were documented as ints, mismatching
  the Newtonsoft StringEnumConverter runtime serialization).
- Tests: full CRUD controller tests for filler + watermark; contract-test
  entries (404/401/422) for the new mutating endpoints; regenerated v1.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:06:26 +02:00
timothy e0a860e577 Merge pull request 'feat(web+api): logs & troubleshooting screens for SPA parity (#145)' (#165) from feat/145-logs-troubleshooting into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 3m51s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m16s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m56s
2026-07-07 12:00:28 +00:00
timothyandClaude Fable 5 24a26cbe34 fix: address #165 review findings (stale-response guard, security-test coverage, route/help-text nits)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m37s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:55:05 +02:00
timothy 5530db7089 feat(web): logs and troubleshooting screens for SPA parity (#145)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m26s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m39s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Add typed API wrappers (api/logs.ts, api/troubleshoot.ts) and two new
screens: LogsScreen (paged, level-badged, server-side filtered log table)
and TroubleshootingScreen (General JSON viewer with copy, plus per-platform
NVIDIA/QSV/VAAPI/VideoToolbox capability tabs when populated). Both are
registered under a new "System" nav group in App.tsx alongside Settings.
Settings' Classic UI help text and About card now point at the new screens
instead of the Blazor logs/troubleshooting pages.
2026-07-07 13:42:01 +02:00
timothy bb34014a07 chore(api): regenerate OpenAPI spec + SPA types for logs/troubleshoot-info (#145) 2026-07-07 13:34:53 +02:00
timothy dfa561a97e feat(api): logs and troubleshooting-info endpoints for SPA parity (#145, #158)
Add GET /api/logs (thin wrapper over GetRecentLogEntries; paged, filtered,
clamped pageSize like LibraryBrowseController) and GET /api/troubleshoot/info
(wraps GetTroubleshootingInfo; General section serialized to the same JSON
shape the legacy Blazor Troubleshooting page renders, plus per-platform
capability dumps). Also adds [EndpointGroupName("general")] to the existing
troubleshoot playback endpoints so they surface in the OpenAPI spec (#158
item 3).
2026-07-07 13:34:47 +02:00
timothyandClaude Fable 5 6e091c98ae ci: retrigger main build (flaky Libraries scan-polling test, see #157)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m15s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m43s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m38s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:05:56 +02:00
timothyandClaude Fable 5 22fa3ecb08 docs: advance ChicoryTV queue past parity #140/#146/#147 (PRs #150/#154/#156); gaps #151-#153/#155 filed; next prompt = parity #141-#145
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m55s
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 43s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 12:57:51 +02:00
timothy d58deddc48 Merge pull request 'feat(web): collections management screen — manual + smart collections (#140)' (#156) from feat/140-collections-screen into main
Build ErsatzTV Image / Build & push image (amd64) (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-07 10:54:19 +00:00
timothyandClaude Fable 5 5995707c84 feat(web): collections management screen for manual + smart collections (#140)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Replaces the PlaceholderScreen at /app/collections with a real screen that
covers manual and smart collections, matching the SPA's extracted-screen
pattern (screens/CollectionsScreen.tsx; App.tsx gets only the route branch +
import).

Manual collections: list, create, rename, delete, and a per-row toggle for
UseCustomPlaybackOrder (PUT). Item management: an add-items picker that searches
the library (library/browse) and buckets results into the typed
AddItemsToCollectionRequest, plus per-item remove.

Smart collections: list, create, edit (name + query), delete, with a live
result preview that runs the query through library/browse.

API layer: new api/collections.ts owns the typed CRUD (getCollections/
getSmartCollections moved here from schedules.ts; schedules imports them). Unit
tests cover the client, the add-items bucket mapping, and the screen.

Honest gap: no API endpoint lists a manual collection's items by id. The items
view uses a best-effort collection:"name" Lucene search (movies/shows/seasons/
artists only) behind a prominent note. Multi/rerun/playlist collections are out
of scope (no API) and pointed to the Classic UI. Follow-ups #151/#152/#153.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 12:44:51 +02:00