Commit Graph
2476 Commits
Author SHA1 Message Date
timothy dbda2d8991 Merge pull request 'feat(web+api): media detail endpoints+pages, image folder browser (#141, #161 items 3+5)' (#183) from feat/141-media-detail into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m21s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m30s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m20s
2026-07-07 21:08:43 +00:00
timothyandClaude Fable 5 685894517d merge main into feat/141-media-detail (post-#182); regenerate OpenAPI artifacts
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m23s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 23:01:17 +02:00
timothyandClaude Fable 5 d5aa838b22 feat(web): media detail pages + image folder browser (#141/#161)
Add the SPA screens over the new detail/info/image endpoints:

- Detail pages at /app/media/{movies|shows|seasons|artists}/{id}. MediaBrowseScreen
  cards for movies/shows/artists (and season/show drill-in cards) navigate to them.
  Shows list seasons -> seasons list episodes -> artists list music videos, each via
  the browse parentId drill-in, paged. Layout: fanart/poster, title/year/plot, chip
  lists, cast row, movie path + FileNotFound/Unavailable warnings. A "Media Info"
  dialog (streams + chapters) backs onto GET /api/media-items/{id}/info. Add-to-
  collection/playlist deferred to #153/#155 (TODO left in code).
- Image folder browser at /app/media/images/browser: lazy expandable tree, per-folder
  image/subfolder counts + duration, edit dialog (PUT set/clear), and a per-folder
  search link (library_folder_id:{id}). Reachable via a "Folder Browser" button on the
  Images browse view.

New /app/media sub-paths are owned by a MediaRouteScreen wrapper that tracks pathname
locally + listens for popstate (App-level routing returns the same 'media' route object
for base and sub-paths). Client modules web/src/api/mediaDetail.ts + imageFolders.ts,
typed via the generated v1.d.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:56:48 +02:00
timothyandClaude Fable 5 cd31c755bb feat(api): media detail + info + image-folder endpoints (#141/#161)
Add REST endpoints backing the SPA media detail pages and image browser:

- GET /api/movies/{id}, /api/shows/{id}, /api/seasons/{id}, /api/artists/{id}
  wrapping the existing detail queries; 404 on None.
- GET /api/media-items/{id}/info wrapping GetMediaItemInfo; UnableToLocateMediaItem
  -> 404, other errors -> 422.
- GET /api/images/folders?parentId= and PUT /api/images/folders/{id}/duration
  (validates null-or-positive -> 400; existence guard via new ImageFolderExists
  query -> 404).
- Extend GetLibraryBrowseItems parentId drill-in to Episode (episodes of a season,
  episode-number order) and MusicVideo (an artist's music videos, album/track order),
  alongside the existing TelevisionSeason branch.

Response DTOs live in ErsatzTV.Core/Api/* and never expose Application VMs. Artwork
values are rooted for the SPA via a shared ErsatzTV.Core/Api/ApiArtwork helper
(mirrors the #180/#181 browse-handler logic; handles jellyfin/emby proxy prefixes,
http passthrough, empty). Regenerated OpenAPI v1.json + web v1.d.ts. New controllers
registered in ApiControllerSecurityTests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:43:47 +02:00
timothy 1c883ac601 Merge pull request 'feat(web+api): block-playout history + sequential-YAML validator (#145, #158 items 4–5)' (#182) from feat/145-block-history into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m44s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m9s
2026-07-07 20:32:31 +00:00
timothyandClaude Fable 5 d2096c0675 merge main into feat/145-block-history (post-#181); regenerate OpenAPI artifacts
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m52s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:25:49 +02:00
timothyandClaude Fable 5 822f3692dc test(web): api client + screen tests for new troubleshooting screens
Client param-mapping tests for playoutHistory + validateSequentialSchedule,
and screen-level flow tests (Block playout filtering + playout->block->
history->decode drilldown; YAML validate valid/invalid/import-toggle).

Refs #145 #158

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:11:04 +02:00
timothyandClaude Fable 5 ec7531d821 feat(web): block-playout troubleshooting + sequential schedule validator screens
Two new System-area screens over the new endpoints:

- /app/troubleshooting/blocks: pick a Block playout, browse its blocks
  (grouped, name filter), drill into paged block history, click a row to
  decode its details.
- /app/troubleshooting/yaml: paste YAML, toggle import schema, validate;
  shows messages + JSON conversion.

Adds api/playoutHistory client + validateSequentialSchedule; regenerated
generated/v1.d.ts. Async loads follow the LogsScreen seq-guard pattern
(no synchronous setState inside effects).

Refs #145 #158

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:10:57 +02:00
timothyandClaude Fable 5 7787b17495 test(api): controller + handler tests for block history and schedule validation
Route assertions, paging clamp, 404/422/400 paths and mediator interaction
for the new PlayoutController/TroubleshootController endpoints; handler tests
for ValidateSequentialSchedule (valid/invalid/throwing) and
GetPlayoutHistoryDetails (found/not-found/malformed JSON via in-memory SQLite).

Refs #145 #158

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:10:46 +02:00
timothyandClaude Fable 5 f0a423c2de feat(api): block-playout history + sequential-schedule validation endpoints
Adds the REST surface for the #145 troubleshooting leftovers (#158 items 4-5):

- GET /api/playouts/{id}/blocks - blocks a block playout schedules
- GET /api/playouts/{id}/blocks/{blockId}/history - paged block history
- GET /api/playouts/history/{id} - decode a history row by id
- POST /api/troubleshoot/validate-schedule - validate sequential YAML

New MediatR queries GetPlayoutHistoryDetails (Either, 404 unknown row /
422 malformed JSON) and ValidateSequentialSchedule (wraps
ISequentialScheduleValidator, never throws). DecodePlayoutHistoryHandler
and the new by-id handler now share PlayoutHistoryDecoder. Regenerated
openapi/v1.json.

Refs #145 #158

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:10:37 +02:00
timothy 3ec3ab5fe0 Merge pull request 'fix(web+api): library picker artwork URLs + season drill-in instead of tile flooding (#180)' (#181) from fix/180-library-picker into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m43s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m43s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m0s
2026-07-07 20:00:38 +00:00
timothyandClaude Fable 5 82d3b586f2 docs(api): describe library-browse parentId param in OpenAPI (#180 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m53s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:50:17 +02:00
timothyandClaude Fable 5 d05e442605 fix(web): stop season-tile flooding in channel builder and collections (#180)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m16s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Part of the #180 library picker fixes (SPA side).

Channel builder: TelevisionSeason is removed from the library grid fan-out, so a
multi-season show renders as one tile instead of one tile per season. Show tiles
gain a "Seasons" drill-in affordance (both grid and compact layouts) that opens a
dialog listing that show's seasons (via the new GET /api/library/browse?parentId=
&mediaType=TelevisionSeason), each with title, artwork and an Add button that
drops the specific season into the lineup.

Collections add-items dialog: the default search fan-out now excludes seasons,
and a media-kind filter row (All / Movies / Shows / Seasons / Artists,
default = All-without-seasons) keeps seasons reachable when explicitly selected.

Client: getLibraryBrowseItems gains an optional parentId param. Tests cover the
param mapping, the builder no longer requesting TelevisionSeason, and the
collections dialog default-excluding vs explicitly-including seasons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:42:18 +02:00
timothyandClaude Fable 5 54b18e73cd fix(api): root library-browse artwork URLs and add season drill-in (#180)
Part of the #180 library picker fixes (API side).

Broken artwork: GetLibraryBrowseItemsHandler.Artwork returned Blazor-convention
values (a bare cache filename, or a RELATIVE jellyfin/emby proxy path). Blazor's
GetPosterUrl prefixes those with "artwork/posters/" against <base href="/">, but
the React SPA renders item.artwork raw as <img src> from under /app/, so every
image 404'd. The handler now returns rooted, directly-usable URLs
(/artwork/posters/... or /artwork/thumbnails/... per ArtworkKind; jellyfin/emby
mapped to their proxy routes with fillHeight/maxHeight; absolute http(s) URLs
passed through; empty stays empty).

Seasons with no poster of their own now fall back to the parent show's poster
(SeasonArtwork + the extra ShowMetadata.Artwork include).

Season drill-in: GET /api/library/browse gains an optional `parentId` query
param that, with mediaType=TelevisionSeason, returns that show's seasons
(season-number order, bypassing Lucene) so the SPA can expand a show into its
seasons. OpenAPI spec regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:42:10 +02:00
timothyandClaude Fable 5 e58da21a56 docs: queue #180 (channel builder season tiles + broken artwork) first for next session
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m58s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m37s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:18:01 +02:00
timothyandClaude Fable 5 a709dccec7 docs: advance ChicoryTV queue past #144/#162 (scheduling parity DONE, PRs #170-#175/#179); next = #145/#141 leftovers
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m42s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m56s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m24s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:05:59 +02:00
timothy c480fbe8d5 Merge pull request 'feat(web+api): playout alternate schedules + templates editors, default-deco read-side (#144 S6, #162)' (#179) from feat/144-s6-playout-editors 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 19:03:10 +00:00
timothy d7617b7456 fix(api): validate date-range month/day bounds on playout PUT actions
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m59s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
AlternateScheduleSelector.GetScheduleForDate constructs
new DateTime(year, StartMonth, StartDay) and only recovers from an
out-of-range *day* (rolling to the 1st of the next month); an out-of-range
*month* (e.g. 0 or 13) throws again from inside that recovery path and is
never caught, crashing playout building.

Validate StartMonth/EndMonth (1..12) and StartDay/EndDay (1..31) on both
PUT /api/playouts/{id}/alternate-schedules and PUT /api/playouts/{id}/templates
whenever an item sets LimitToDateRange, returning 422 before dispatching to
the mediator. Both actions already documented 422 in their ProducesResponseType
metadata.
2026-07-07 20:56:45 +02:00
timothy fb76f34fc6 fix(web): disable recurrence controls on the alternate-schedules default row
The last row in the alternate-schedules editor is the catch-all default:
ReplacePlayoutAlternateScheduleItemsHandler excludes the highest-index item
from the saved rows entirely and only keeps its ProgramScheduleId as the
playout's default schedule, so any recurrence edits made to that row (days of
week/month, months, date range) were silently discarded on save.

Disable the recurrence controls (chips, selects, limit-to-date-range
checkbox) whenever the selected row is the default, and show an inline note
explaining why. The Templates editor is unaffected — its handler saves every
row's recurrence fields as-is, so RecurrenceFields there keeps full editing.
2026-07-07 20:56:38 +02:00
timothy 0bc74f9b1d fix(web): playout alternate-schedule/template links didn't render on click
routeFromLocation() returns the same ScreenRoute object reference for
/app/playouts and any /app/playouts/{id}/* sub-path, so navigateToPath's
pushState + synthetic popstate caused App's setActiveRoute(routeFromLocation())
to bail via Object.is and never re-invoke ScreenContent. Only a hard reload
picked up the new sub-route.

Mirror the existing BlocksScreen/TemplatesScreen/DecosScreen pattern: add a
PlayoutsRouteScreen wrapper that owns its own pathname state and popstate
listener, so client-side navigation into and out of the alternate-schedules
and templates editors re-renders correctly.
2026-07-07 20:56:32 +02:00
timothy 4f472134fb Merge remote-tracking branch 'origin/main' into feat/144-s6-playout-editors 2026-07-07 20:30:37 +02:00
timothyandClaude Fable 5 ca3fe0c87b feat(web): playout alternate-schedule and template editors on the REST API (#144 S6 (#162))
Adds two sub-path editors under the Playouts screen:
- /app/playouts/{id}/alternate-schedules (Classic playouts)
- /app/playouts/{id}/templates (Block playouts)

Each has a reorderable priority table (up/down/delete) plus a selected-row
detail panel: schedule or template+deco-template pickers, a limit-to-date-range
toggle gating month/day/year selects, and day-of-week / day-of-month / month
multi-select chips with Weekdays/Weekends helpers. Entry points on the playout
card are kind-gated. The Block playout card also gets a default-deco select
wired to PUT /api/playouts/{id}/deco, using the new decoName read field.

DayOfWeek is overridden to day-name strings in the API client (the wire format
is Newtonsoft StringEnumConverter, though the OpenAPI schema types it as number).
Deviations from Blazor: chip multi-select instead of MudSelect, flat grouped
template pickers instead of a group->item cascade, a shorter year range, and the
template calendar preview is omitted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:29:40 +02:00
timothyandClaude Fable 5 50b13afac3 feat(api): REST endpoints for playout alternate schedules and templates (#144 S6 (#162))
Adds per-playout REST for classic-playout alternate schedules and
block-playout templates, plus the default-deco read-side deferred from S3:

- GET/PUT /api/playouts/{id}/alternate-schedules (Classic only; 422 otherwise)
- GET/PUT /api/playouts/{id}/templates (Block only; 422 otherwise)
- PlayoutResponseModel gains decoId/decoName (GetPlayoutById includes Deco)

PUT assigns Index from array order (top = highest priority, last = catch-all
default), mirroring the Blazor editors. Alternate-schedule PUT requires a
non-empty list and existing ProgramScheduleIds; template PUT requires existing
TemplateIds and any supplied DecoTemplateId. Regenerates the OpenAPI spec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:29:29 +02:00
timothy 4ea5273770 Merge pull request 'feat(web+api): deco templates (#144 S4, #162)' (#175) from feat/144-s4-deco-templates into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m38s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m4s
2026-07-07 18:06:36 +00:00
timothyandClaude Fable 5 c745d01a2c fix(app): bound deco template item times to a single day (#144 S4 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m19s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m29s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Out-of-range TimeSpans (25:00:00, 1.06:00:00, negatives) previously
persisted raw while the read side projected .TimeOfDay, so display and
playout behavior diverged silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:00:48 +02:00
timothy 0d9504c16b Merge remote-tracking branch 'origin/main' into feat/144-s4-deco-templates 2026-07-07 19:52:01 +02:00
timothy 2b54a2d591 feat(web): Deco Templates screen (list + editor) on the REST API (#144 S4 (#162))
Adds DecoTemplatesScreen mirroring TemplatesScreen: list mode groups deco
templates by deco template group with create/delete dialogs; editor mode
(/app/deco-templates/{id}) has an add-content row (deco group -> deco ->
start time -> duration hours/minutes pickers), a time-sorted items table
displaying end-of-day items as "24:00", and client-side validation mirroring
ReplaceDecoTemplateItemsHandler's hardened rules (start must precede its
effective end time; no overlapping ranges, with EndTime 00:00:00 treated as
end-of-day for both checks) that disables Save on violation.

No copy action: neither the Blazor DecoTemplateEditor nor the CQRS layer has
a deco-template copy command (unlike Templates, which has both Copy* and a
/copy route), so this isn't a parity gap - just matches upstream scope.

Registered at /app/deco-templates in the primary nav, next to Decos.
2026-07-07 19:51:11 +02:00
timothy cda5c12e34 feat(api): REST endpoints for deco templates and deco template groups (#144 S4 (#162))
Adds DecoTemplateController mirroring TemplateController: CRUD for deco
template groups, deco templates (flat list + by-id), item listing, and a
full-replace PUT for a deco template's item list.

Hardening (deviation from a literal port of the existing handlers, matching
the #144 S2 fix for ReplaceTemplateItemsHandler):
- CreateDecoTemplateHandler now validates DecoTemplateGroupId exists before
  insert (previously a bad id hit the FK constraint at SaveChanges and
  surfaced as a 500; now a 422).
- ReplaceDecoTemplateItemsHandler now rejects invalid items (unknown DecoId,
  StartTime >= EndTime unless EndTime is the end-of-day sentinel 00:00:00,
  or overlapping ranges) with a 422 instead of silently dropping/persisting
  them - the same silent-drop/silent-overlap bug class already fixed for
  templates.

Response DTOs serialize the raw item TimeSpans (via .TimeOfDay), so an
end-of-day item still round-trips as StartTime=22:00:00/EndTime=00:00:00
regardless of the ViewModel's day-wrapping DateTime representation.
2026-07-07 19:51:00 +02:00
timothy 0c355e1132 Merge pull request 'feat(web+api): decos + deco groups, playout default deco, playlist/search pickers (#144 S3, #162)' (#174) from feat/144-s3-decos into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m39s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m2s
2026-07-07 17:42:30 +00:00
timothyandClaude Fable 5 d71a609b4f fix(app): persist MediaItemId on deco break-content updates and adds (#144 S3 review)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m26s
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
UpdateDecoHandler copied every break-content field except MediaItemId,
so TelevisionShow/Season/Artist break content validated then persisted
with a null MediaItemId (upstream bug surfaced by the new REST API).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:36:37 +02:00
timothy 4c215f6f73 Merge remote-tracking branch 'origin/main' into feat/144-s3-decos 2026-07-07 19:23:47 +02:00
timothyandClaude Fable 5 a52939f292 feat(web): Decos screen (list + 6-section editor) on the REST API (#144 S3 (#162))
Adds the /app/decos screen (dual-mode list/editor) mirroring the legacy Blazor
deco form: deco/watermark/graphics/break-content/default-filler/dead-air
sections with mode-gated controls, watermark/graphics multi-selects, break
content with a playlist group->playlist cascade and per-placement uniqueness,
and default-filler/dead-air typeahead pickers. Adds decos/playlists api modules,
artist/multi-collection search wrappers, and regenerated types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:22:49 +02:00
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 86f280f9d0 Merge pull request 'feat(web+api): scheduling templates + block/template copy (#144 S2, #162)' (#173) from feat/144-s2-templates into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m42s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m3s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m58s
2026-07-07 17:00:45 +00: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