# Handoff — ersatztv#2: REST API for channel / collection / schedule / playout CRUD

**How to use:** start a **fresh, parallel** session and say something like
*"Read `docs/handoffs/rest-api.md` and execute ersatztv#2 — do Phase 1 (investigate + determine the API shape) first and stop for sign-off before writing code."*
Read **ersatztv#2** in full (Gitea, `timothy/ersatztv`) before acting. This runs **in parallel** with smaller work happening on `main` in another session — coordinate via branches/PRs, don't assume exclusive ownership of the tree.

This is a **large greenfield feature**, deliberately split into two phases. Do **not** start coding in Phase 1.

---

## Why (from the issue)

ErsatzTV has no REST API for create/update/delete of channels, collections, schedules, or playouts — management is via the Blazor UI or fragile direct SQLite writes. Direct DB writes are dangerous because:
- EF Core uses **TPT inheritance** (e.g. `ProgramScheduleItem` → `ProgramScheduleOneItem`/…): must insert into the right subtype.
- Many NOT NULL / enum constraints (ScheduleKind, PlaybackOrder, CollectionType, …); wrong values produce broken playouts ("Cannot build playout type None").
- WAL mode → container must be stopped for safe external writes.
- No validation → easy to create unbuildable state.

The API closes this by routing all mutations through EF + existing domain validation, returning proper HTTP codes.

---

## Phase 1 — Investigate & determine the shape (DESIGN ONLY, get sign-off)

Goal: produce a short **design doc** (`docs/rest-api.md`) + an **increment split into sub-issues**, then stop for the user to approve before any implementation. Mirror the rigor we used on #1: ground every claim in the actual code, don't assume.

### 1a. Map what already exists (the issue says `GET /api/channels` already exists — find it)
- Locate the **existing API controller(s)** under `ErsatzTV/Controllers/` (there is already at least one read endpoint — find the route prefix, base class, content negotiation, and any auth/`access_token` handling). The new CRUD endpoints must match this style, not invent a new one.
- The **MediatR CQRS pattern**: queries/commands live in `ErsatzTV.Application/<Area>/{Queries,Commands}/`. Many handlers already return `Either<BaseError, T>`; there is a `.ToActionResult()` extension (used in `IptvController`) that maps `Either` → HTTP. Confirm the exact error/success mapping and reuse it (don't hand-roll status codes).
- The **Blazor pages** that already create/update/delete each resource (under `ErsatzTV/Pages/`): they call MediatR commands and carry the real validation. The API should call the **same** commands/handlers where they exist, and only add new ones where a Blazor flow doesn't map cleanly. Enumerate, per resource, which commands already exist vs. which are net-new.
- The **domain model + EF mapping**: `ErsatzTV.Core/Domain/` entities + `ErsatzTV.Infrastructure/Data/TvContext.cs`. Note the TPT hierarchies (ProgramScheduleItem subtypes, Collection kinds, StreamingMode/ScheduleKind enums). Determine whether any CRUD operation needs a **schema change** (most should not — pure CRUD over existing tables). **If a model change IS needed → dual-provider migration via `scripts/add-migration.sh` (Sqlite + MySql), enforced by the `migrations` CI job.** Flag this explicitly; CRUD-only likely needs none.
- **Auth**: how are existing endpoints secured (the IPTV ones use an `access_token` query param)? Decide the API's auth story and state it. Don't ship unauthenticated mutation endpoints without calling that out.

### 1b. Decide conventions (write them down)
- Route layout (`/api/channels`, `/api/collections/{id}/items`, …) — keep consistent with the existing controller.
- Status codes: 201 Created (+ Location), 200/204, 404, 422 validation. Map from `Either<BaseError,_>`.
- Request/response **DTOs** vs. reusing existing `*ViewModel`s — pick one and be consistent.
- Validation: reuse domain/handler validation; where Blazor validated in the page code-behind, that logic must move into (or already live in) the handler so the API gets it too.
- OpenAPI/Swagger? Pagination for list endpoints? Idempotency of deletes? — decide or explicitly defer.
- **URL emission caveat (from #1):** if any API response includes absolute URLs (logos, stream URLs), they inherit the request-`Host` fragility documented in `docs/m3u-xmltv.md`. Note how the API will handle it (probably: same request-derived host; don't bake).

### 1c. Increment plan (the sub-issues)
Split #2 into vertical slices, **Channels first** as the pattern-setter (it's the simplest full CRUD and the others copy its shape):
- **#2a Channels** — POST/PUT/DELETE (+ confirm existing GETs).
- **#2b Collections** — CRUD + item add/remove.
- **#2c Schedules** — CRUD + schedule-item add/remove (this is the TPT-heavy one; budget for it).
- **#2d Playouts** — create (link channel↔schedule) / delete (+ existing reset).

Each slice = one branch = one PR. Open the sub-issues, link them to #2 (which becomes the tracker), and **stop for sign-off**.

---

## Phase 2 — Implement (workflows / ultracode, one slice at a time)

Only after Phase 1 sign-off. Per slice (start with #2a Channels):

**Shape of the work per resource** (good fit for a `Workflow` pipeline or `ultracode`):
1. For each endpoint: Command + Handler in `ErsatzTV.Application/<Resource>/Commands/` (reuse existing where found), returning `Either<BaseError, T>`.
2. Controller action in `ErsatzTV/Controllers/` mapping via `.ToActionResult()`.
3. Validation reused from the corresponding Blazor flow / domain.
4. **NUnit** tests (+ Shouldly + NSubstitute) — handler unit tests for success + each failure (404/validation), and a controller/integration test for status-code mapping. (No xUnit.)
5. Run the suite locally with `TZ=UTC` (matches CI; see the known TZ-sensitive filler tests, #24).

**Suggested workflow pattern:** pipeline over the endpoints of the slice — stage 1 author handler+test, stage 2 adversarially review each (does it actually go through EF? does it handle TPT/enum constraints? does delete cascade correctly?). Adversarial verify is worth it here because broken writes corrupt playouts. Use `ultracode` if the user opts in, otherwise a single `Workflow` per slice.

**Regression nets already in place — lean on them:**
- **Architecture tests (#12)** enforce layering: controllers in `ErsatzTV`, business logic in `ErsatzTV.Application`, no concrete-provider leakage. New code must pass `ErsatzTV.Architecture.Tests` (will fail the PR if you put logic in the wrong layer).
- **M3U goldens (#11)** guard `ToM3U` output if a channel change touches it.
- Add focused tests for new behavior; consider an integration test that creates→reads→deletes through the real EF stack (SQLite) to prove TPT/cascade correctness.

---

## Process (non-negotiable — CLAUDE.md + `docs/contributing.md`)
- Follow established patterns; diverge only with a stated reason (contributors guide #10).
- Dependencies via **Central Package Management** (`Directory.Packages.props`); never re-add `Version=` to a `<PackageReference>`.
- **One branch = one PR.** PR runs `test` + `migrations` (both **required** to merge). Merge to `main` runs `test`+`migrations`+`build`+smoke/E2E. Verify green before closing each sub-issue.
- Migrations only if the model changes — `scripts/add-migration.sh <Name>` does **both** providers.
- Adversarial self-review of the diff before closing (see memory: adversarial-self-review-at-milestones). Then Task Completion Protocol / `/done <sub-issue>`.
- CI poll: `curl -u timothy:ded89Lm4 …/api/v1/repos/timothy/ersatztv/actions/tasks` (jobs by name), or the runs API.

## Repo state at handoff
- `main` is green; #1 closed (config/topology, not code — see `docs/m3u-xmltv.md`). #5 triaged as Jellyfin/infra (→ server-management).
- `main` requires `Build & test (.NET)` **and** `EF migration integrity (SQLite + MySql)` to merge a PR.
- Test framework is **NUnit** (+ Shouldly + NSubstitute), not xUnit.
- Versioning is CalVer `vYY.N.P`; only tag when the user asks. The API is the first real app-feature work → first such release would be `v26.4.0`.
- Infra: Docker host **bumblebee** `192.168.1.99`; container `ersatztv` (port 8409); registry `192.168.1.95:3000/timothy/ersatztv`. Plain `ssh timothy@192.168.1.99` works; ssh-mcp/docker-mcp need `--key=~/.ssh/id_rsa` + client restart if they 401.
- This session runs **in parallel** with smaller tasks (#28 XMLTV/logo goldens, #24 TZ tests) landing on `main` — rebase before opening PRs.
