# Decisions — append-only log Purpose: why the codebase does what it does, so agents don't "fix" an established convention or relitigate a settled call. Append new entries at the bottom in date order; never edit or delete past entries except to fix a factual error. **Update this doc in the same PR that changes any fact below (or that establishes a new convention worth recording).** ## 2026-06 — REST API wraps existing MediatR handlers 1:1, no service layer The REST API (#2, `docs/rest-api.md`) is thin controllers over the existing MediatR Create/Update/Delete handlers — no new service/business-logic layer was introduced, since nearly every handler already returns `Either`, which maps cleanly to HTTP status codes. Latent handler bugs (missing existence checks, `KeyNotFoundException` risk, etc.) are fixed **at the handler**, converting what would have 500'd into a proper 404/422 — not papered over in the controller. Established across the #2a–#2e gap-issue PRs. Deep FK ids nested inside item-list request bodies (e.g. a schedule item's `CollectionId`) are deliberately **not** existence-checked at that depth, to avoid N+1 validation queries — precedent set by the schedules endpoints (#172); see `docs/api-conventions.md` §3 for the up-to-date statement of this rule. ## 2026-06 — UI rebuild is a React SPA (ChicoryTV) on the REST API, not a Blazor reskin #59 committed to a full SPA rebuild rather than reskinning Blazor Server pages. Blazor removal is split into two phases under #91: **(a)** root-flip (SPA becomes `/`) + legacy-route redirects — DONE, merged via PR #148 (`ErsatzTV/LegacyUiRedirects.cs`, `feat/91-cutover` → main). **(b)** full Blazor removal — gated on every route having an SPA equivalent; tracked route-by-route in `docs/blazor-route-parity.md`. ## 2026-07 — Response DTOs live in `ErsatzTV.Core/Api`, file-scoped `#nullable enable` New REST response DTOs go in `ErsatzTV.Core/Api//*ResponseModel.cs` and mirror the shape of the corresponding Application-layer ViewModel — controllers never expose VM types directly. Because `ErsatzTV.Core.csproj` sets `disable` project-wide, any response-model file with an optional member needs its own `#nullable enable` pragma at the top (most already have one). `ErsatzTV.Application` has no nullable context at all — do not add `?` annotations to types living there; that's a Core/Api-layer-only convention. Full detail: `docs/api-conventions.md` §2. ## 2026-07 — PUT-replace list endpoints derive `Index` from array order; alternate-schedules last row = catch-all default For "replace the whole list" endpoints (PUT over a collection — schedule items, template items, etc.), the item's `Index` is derived from its position in the request array, not from a client-supplied index/order field — established by `ReplaceScheduleItemsRequest.ToCommand` (`Items.Select((item, index) => item.ToReplaceCommand(index))`). Separately, `ProgramScheduleAlternate` and `PlayoutTemplate` rows (both `IAlternateScheduleItem`) are evaluated in `Index` order, first-match-wins; the convention is to place the least-conditional (or unconditional) row **last** so it acts as the catch-all default. Established by the alternate-schedules work (PR #179, `AlternateScheduleSelector.cs`). ## 2026-07 — Templates editor in the SPA is a table, not Blazor's drag-calendar The legacy Blazor `TemplateEditor.razor` used a drag-and-drop day-grid calendar UI. The SPA equivalent (`/app/templates/{id}`, PR #173) renders the same day/block assignment as a table instead. This is an accepted, deliberate parity deviation — don't "fix" it to match Blazor's interaction model without discussing it first. ## 2026-07-07 — API artwork contract: rooted URLs produced server-side API response DTOs return artwork as rooted, directly-usable URLs (`/artwork/posters/...`, `/artwork/thumbnails/...`, `/artwork/fanart/...`), plus passthrough for absolute `http(s)://` URLs and Jellyfin/Emby proxy variants. Established by PR #181 (`ErsatzTV.Application/LibraryBrowse/Queries/GetLibraryBrowseItemsHandler.cs`, private `Artwork(...)` helper — comment: *"Returns a rooted, directly-usable artwork URL for the SPA's ``... the SPA [needs it pre-rooted]"*), then generalized into the reusable `ApiArtwork` helper (`ErsatzTV.Core/Api/ApiArtwork.cs`, PR #183). Root cause: the SPA has no ``, unlike Blazor, so relative artwork paths that worked for Blazor pages 404 in the SPA. Do **not** reuse the Application-layer Mappers used by Blazor (e.g. `MediaCards`/`Television` mappers) for new API DTOs — those still return old Blazor-convention relative paths; map from the domain/VM directly and root the path via `ApiArtwork`. ## 2026-07-07 — Decode-style endpoints take a row id and look up server-side Endpoints that decode/expand opaque stored state accept a database row id and resolve server-side, rather than accepting client-supplied serialized state to decode. Established by `GET /api/playouts/history/{id}` (`PlayoutController.GetHistoryDetails`, PR #182) — the row's raw JSON (`Key`/`Details`) is decoded server-side into `PlayoutHistoryDetailsResponseModel`, the client never round-trips the raw payload itself. ## 2026-07-07 — Season/episode/music-video drill-in via `parentId`, not new child-listing endpoints Rather than adding dedicated child-listing endpoints per media kind (e.g. "list episodes of a season"), the library-browse endpoint takes an optional `parentId` query param and the SPA drills in by re-querying with it. Established across PRs #181/#183 (library-picker season drill-in, then media-detail's season/episode/artist/music-video browsing). Avoids a combinatorial explosion of per-kind child endpoints. ## 2026-07-07 — Convention docs read at session start, updated in-PR `docs/api-conventions.md`, `docs/spa-conventions.md`, `docs/e2e-local.md`, `docs/blazor-route-parity.md`, `docs/domain-model.md`, `docs/decisions.md`, and `docs/README.md` are the standing reference set every ChicoryTV session should read before starting work, and each one carries an explicit "update this doc in the same PR" rule rather than deferring doc updates to a follow-up. These docs **replace per-session recon** — an agent reads the index (`docs/README.md`) and the relevant convention doc instead of re-deriving conventions from the code each time it starts API/SPA/E2E/parity work. A testing map and a generated-endpoint index are tracked as still-to-come under #185. Drafting this doc set also surfaced a drift in `ApiControllerSecurityTests.cs`'s hardcoded controller registry (several controllers under `ErsatzTV/Controllers/Api/` are missing from it — see `docs/api-conventions.md` §6) — tracked as a follow-up under #184 rather than fixed inline, since it's a pre-existing gap, not something this doc-drafting pass caused.