Files
ersatztv/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
T
timothyandClaude Opus 4.8 5c9f04fdec feat(#253 PR2): optimistic-concurrency on schedule-items aggregate
Wire the frozen #253 ETag/If-Match/412 recipe onto ProgramSchedule /
schedule-items, keeping the PR#258 positional in-place reconcile intact.

Backend:
- ReplaceProgramScheduleItems command gains Option<int> ExpectedVersion;
  ReplaceScheduleItemsRequest.ToCommand threads it.
- Handler: standalone CheckVersion Either AFTER validation (so 412 isn't
  flattened to 422), unconditional Version++ before save, guarded save via
  SaveChangesWithConcurrencyGuard, and 412 propagated without running the
  post-save reload/enqueue.
- ProgramScheduleViewModel + Mapper carry Version.
- ScheduleController: GET /items emits ETag; PUT /items parses If-Match
  (malformed -> 400), threads ExpectedVersion, re-queries for the new ETag,
  and advertises 400/412.
- Sibling config-writers (Add/Delete item, Update schedule) bump Version.

Frontend:
- schedules.ts: getScheduleItemsWithMeta + replaceScheduleItems(ifMatch)
  returning ResponseWithMeta.
- SchedulesScreen: etagRef threaded through the #242 dirty-guard (set from
  load + every successful save); 412 opens a conflict ConfirmDialog whose
  Reload discards the draft and re-runs loadItems.

Tests: handler concurrency suite (stale->412 no mutation + fill-group state
untouched, match/absent success+bump, no-op still bumps, racing save->412);
controller ETag/If-Match/412 cases; SchedulesScreen 412-conflict-dialog test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:36:25 +02:00

52 lines
1.5 KiB
C#

using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
public record ReplaceProgramScheduleItem(
int Index,
StartType StartType,
TimeSpan? StartTime,
FixedStartTimeBehavior? FixedStartTimeBehavior,
PlayoutMode PlayoutMode,
CollectionType CollectionType,
int? CollectionId,
int? MultiCollectionId,
int? SmartCollectionId,
int? RerunCollectionId,
int? MediaItemId,
int? PlaylistId,
string SearchTitle,
string SearchQuery,
PlaybackOrder PlaybackOrder,
MarathonGroupBy MarathonGroupBy,
bool MarathonShuffleGroups,
bool MarathonShuffleItems,
int? MarathonBatchSize,
FillWithGroupMode FillWithGroupMode,
MultipleMode MultipleMode,
string MultipleCount,
TimeSpan? PlayoutDuration,
TailMode TailMode,
int? DiscardToFillAttempts,
string CustomTitle,
GuideMode GuideMode,
int? PreRollFillerId,
int? MidRollFillerId,
int? PostRollFillerId,
int? TailFillerId,
int? FallbackFillerId,
List<int> WatermarkIds,
List<int> GraphicsElementIds,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode? SubtitleMode) : IProgramScheduleItemRequest;
public record ReplaceProgramScheduleItems(
int ProgramScheduleId,
List<ReplaceProgramScheduleItem> Items,
Option<int> ExpectedVersion = default) : IRequest<
Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>>;