Files
ersatztv/docs/decisions.md
T
timothyandClaude Fable 5 50ae0a7f3b
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m56s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m52s
docs: onboarding/convention docs part 1 + handoff past #180/#182/#183 (parity endgame)
Adds docs/README.md (index), api-conventions.md, spa-conventions.md, e2e-local.md +
scripts/e2e-local.sh, blazor-route-parity.md (#91 phase-b tracker), domain-model.md,
decisions.md. Rule (CLAUDE.md): read these at session start instead of re-recon; update
in the same PR that changes a convention/route/decision. Part 2 = #185.

Handoff: #180/#141/#158/#161 closed (PRs #181/#182/#183); #145 playback-only; #91
readiness plan posted; next prompt = #185 + quick wins + #155/#151/#152/#153.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 00:03:01 +02:00

99 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<BaseError, T>`, 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/<Domain>/*ResponseModel.cs` and mirror the shape of
the corresponding Application-layer ViewModel — controllers never expose VM types directly. Because
`ErsatzTV.Core.csproj` sets `<Nullable>disable</Nullable>` 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 `<img src>`... 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 `<base href>`, 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.