Files
ersatztv/docs/handoffs/rest-api.md
T
timothyandClaude Opus 4.8 7dfd4c37bb
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m30s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m35s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m51s
docs: add ersatztv#2 REST API handoff (Phase 1 design → Phase 2 implement)
Two-phase handoff for a parallel session: Phase 1 investigates the
existing API/CQRS/validation patterns and produces a design + increment
split (Channels-first); Phase 2 implements per vertical slice via
workflows/ultracode. refs #2

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 13:19:51 +02:00

8.4 KiB

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. ProgramScheduleItemProgramScheduleOneItem/…): 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 *ViewModels — 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.