Slice S6 — the FINAL slice of the #162 scheduling REST plan (parity issue #144).
API (PlayoutController)
GET/PUT /api/playouts/{id}/alternate-schedules (Classic playouts only — 422 otherwise): index assigned from array order, last row = catch-all default whose scheduleId is written to playout.ProgramScheduleId (semantics verified against AlternateScheduleSelector first-match-wins + the handler's Max(Index) default-write; a PUT of exactly what GET returned round-trips with zero churn incl. the synthesized catch-all). Non-empty 422 (guards the handler's Max() throw the upstream TODO warned about); scheduleId existence 422 without dispatch.
GET/PUT /api/playouts/{id}/templates (Block only): templateId/decoTemplateId existence 422s; empty list allowed (Blazor parity — verified upstream has no non-empty guard).
Date-range hardening (review find): out-of-range month/day with limitToDateRange:true → 422 on both PUTs (previously crashed AlternateScheduleSelector — its own catch re-throws for bad months).
PlayoutResponseModel gained nullable decoId/decoName (the S3-deferred default-deco read-side); all 6 PlayoutNameViewModel construction sites updated.
Known wire quirk documented: DayOfWeek serializes as day-name strings (StringEnumConverter) though the OpenAPI schema says integer — TS client overrides on both request/response paths (E2E-confirmed; #126 precedent).
SPA
Playouts route gains sub-paths: /app/playouts/{id}/alternate-schedules + /app/playouts/{id}/templates — reorderable priority table (last alt-schedule row badged "Default" with recurrence controls disabled + explanatory note, since the server coerces it to match-all) + detail panel (schedule or template+decoTemplate pickers, days-of-week chips with Weekdays/Weekends helpers, days-of-month/months grids, date-range gating with nullable year).
Entry buttons kind-gated on the playout card (Classic → alternate schedules; Block → templates + a default-deco select wired to PUT /{id}/deco).
E2E-found navigation bug fixed: sub-route clicks now actually render (new PlayoutsRouteScreen wrapper owning pathname state + popstate, the established BlocksScreen pattern — routeFromLocation returns the same route object for sub-paths so App-level state never re-rendered). Regression tests assert client-side screen swap without reload.
Live E2E: full API matrix on real Classic + Block playouts (kind-gating both directions, default-schedule flip on reorder confirmed via scheduleName, existence/non-empty 422s, deco read-side, DayOfWeek wire evidence) + browser flows (kind-gated buttons, both editors add/reorder/save/reload, default-deco set+clear, date-range gating, Default badge follows reorder).
Slice S6 — the FINAL slice of the #162 scheduling REST plan (parity issue #144).
**API** (PlayoutController)
- `GET/PUT /api/playouts/{id}/alternate-schedules` (Classic playouts only — 422 otherwise): index assigned from array order, **last row = catch-all default** whose scheduleId is written to `playout.ProgramScheduleId` (semantics verified against `AlternateScheduleSelector` first-match-wins + the handler's Max(Index) default-write; a PUT of exactly what GET returned round-trips with zero churn incl. the synthesized catch-all). Non-empty 422 (guards the handler's `Max()` throw the upstream TODO warned about); scheduleId existence 422 without dispatch.
- `GET/PUT /api/playouts/{id}/templates` (Block only): templateId/decoTemplateId existence 422s; empty list allowed (Blazor parity — verified upstream has no non-empty guard).
- Date-range hardening (review find): out-of-range month/day with `limitToDateRange:true` → 422 on both PUTs (previously crashed `AlternateScheduleSelector` — its own catch re-throws for bad months).
- `PlayoutResponseModel` gained nullable `decoId`/`decoName` (the S3-deferred default-deco read-side); all 6 `PlayoutNameViewModel` construction sites updated.
- Known wire quirk documented: `DayOfWeek` serializes as day-name strings (StringEnumConverter) though the OpenAPI schema says integer — TS client overrides on both request/response paths (E2E-confirmed; #126 precedent).
**SPA**
- Playouts route gains sub-paths: `/app/playouts/{id}/alternate-schedules` + `/app/playouts/{id}/templates` — reorderable priority table (last alt-schedule row badged "Default" with recurrence controls disabled + explanatory note, since the server coerces it to match-all) + detail panel (schedule or template+decoTemplate pickers, days-of-week chips with Weekdays/Weekends helpers, days-of-month/months grids, date-range gating with nullable year).
- Entry buttons kind-gated on the playout card (Classic → alternate schedules; Block → templates + a default-deco select wired to `PUT /{id}/deco`).
- E2E-found navigation bug fixed: sub-route clicks now actually render (new `PlayoutsRouteScreen` wrapper owning pathname state + popstate, the established BlocksScreen pattern — `routeFromLocation` returns the same route object for sub-paths so App-level state never re-rendered). Regression tests assert client-side screen swap without reload.
**Verification**
- ErsatzTV.Tests 786 green; Core.Tests 493 green; web 301 green; lint/tsc/build clean.
- Live E2E: full API matrix on real Classic + Block playouts (kind-gating both directions, default-schedule flip on reorder confirmed via scheduleName, existence/non-empty 422s, deco read-side, DayOfWeek wire evidence) + browser flows (kind-gated buttons, both editors add/reorder/save/reload, default-deco set+clear, date-range gating, Default badge follows reorder).
- Adversarial review: no must-fix (priority/round-trip semantics verified end-to-end); date-range should-fix applied; nits → #172.
Completes the #162 slice plan (S1–S6 all merged/pending-merge). Part of #144 — close-out comment follows after merge. Do not auto-close.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
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>
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.
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.
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.
timothy
merged commit c480fbe8d5 into main2026-07-07 21:03:11 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Slice S6 — the FINAL slice of the #162 scheduling REST plan (parity issue #144).
API (PlayoutController)
GET/PUT /api/playouts/{id}/alternate-schedules(Classic playouts only — 422 otherwise): index assigned from array order, last row = catch-all default whose scheduleId is written toplayout.ProgramScheduleId(semantics verified againstAlternateScheduleSelectorfirst-match-wins + the handler's Max(Index) default-write; a PUT of exactly what GET returned round-trips with zero churn incl. the synthesized catch-all). Non-empty 422 (guards the handler'sMax()throw the upstream TODO warned about); scheduleId existence 422 without dispatch.GET/PUT /api/playouts/{id}/templates(Block only): templateId/decoTemplateId existence 422s; empty list allowed (Blazor parity — verified upstream has no non-empty guard).limitToDateRange:true→ 422 on both PUTs (previously crashedAlternateScheduleSelector— its own catch re-throws for bad months).PlayoutResponseModelgained nullabledecoId/decoName(the S3-deferred default-deco read-side); all 6PlayoutNameViewModelconstruction sites updated.DayOfWeekserializes as day-name strings (StringEnumConverter) though the OpenAPI schema says integer — TS client overrides on both request/response paths (E2E-confirmed; #126 precedent).SPA
/app/playouts/{id}/alternate-schedules+/app/playouts/{id}/templates— reorderable priority table (last alt-schedule row badged "Default" with recurrence controls disabled + explanatory note, since the server coerces it to match-all) + detail panel (schedule or template+decoTemplate pickers, days-of-week chips with Weekdays/Weekends helpers, days-of-month/months grids, date-range gating with nullable year).PUT /{id}/deco).PlayoutsRouteScreenwrapper owning pathname state + popstate, the established BlocksScreen pattern —routeFromLocationreturns the same route object for sub-paths so App-level state never re-rendered). Regression tests assert client-side screen swap without reload.Verification
Completes the #162 slice plan (S1–S6 all merged/pending-merge). Part of #144 — close-out comment follows after merge. Do not auto-close.
🤖 Generated with Claude Code
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>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>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.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.