#197 Bundle C: OpenAPI contract honesty (#287) + wrap raw VMs / nullable DTOs (#288) + channel re-key #296

Merged
timothy merged 8 commits from bundle-c-197-contract into main 2026-07-12 12:25:21 +02:00
Owner

Closes #287, closes #288. Part of the #197 contract-freeze arc (Phase-2). #286 (/api/v1 versioning) stays parked until this lands (both regenerate the OpenAPI artifacts).

What's in it

#287 — OpenAPI contract honesty by construction (4 transformers on the "v1" doc, Startup.cs):

  • ApiKey security scheme + per-operation security/401 injected via the same shared predicate the filter enforces (ApiKeyAuthorizationFilter.EndpointRequiresKey) — spec can't drift from enforcement.
  • Synthesized stable, order-independent operationIds (verb-suffixed on collision) for the ~90 ops that lacked a Name=.
  • 400 ValidationProblemDetails documented on body/param-binding ops; ProblemDetails self-provisioned.
  • DayOfWeek → string enum (added to UseStringEnumSchemas); the SPA WithDayNames override removed.

#288 — wrap the last raw ViewModels + nullable-honest DTOs + search paging:

  • No /api/* action returns an Application VM anymore: Collection/Schedule/SmartCollection/Resolution wrapped in ResponseModels (Version now header-only, SPA-verified), and the channel detail GET/writes return a new ChannelDetailResponseModel (the full editable field set the channel editor needs — distinct from the lean list ChannelResponseModel).
  • #nullable enable across all ErsatzTV.Core/Api/ files; members ? only where the mapper can emit null (honest MediaSources fields).
  • pageNum added to GET /api/search.

Channel playout/reset re-keyed to {id:int} (owner-directed, Fable design verdict): the Channel admin API keys on the immutable Id, never the user-mutable Number. POST /api/channels/{id:int}/playout/reset (new GetPlayoutIdByChannelId; by-number kept for HlsSessionWorker broadcast). See docs/decisions.md 2026-07-12.

Verification

  • dotnet build ErsatzTV.sln clean; 1472 ErsatzTV.Tests pass; SPA typecheck/lint/test (691) + check:api clean.
  • Independent reviews: Codex = MERGEABLE, cold adversarial = MERGEABLE-WITH-NITS (both LOWs fixed in c40e78d8), fix-commit re-review = MERGEABLE.
  • Live headless E2E 5/5: API-key gate, channel-editor round-trip (pre-fill + save + reload persist — the highest-risk change), reset re-key (POST /api/channels/1/playout/reset → 202), DayOfWeek string round-trip, search.

Docs

docs/api-conventions.md (§2/§3a/§5/§7a/§9), 3 docs/decisions.md entries, docs/rest-api.md reset route, regenerated v1.json + endpoint-index.md + v1.d.ts.

Not in scope (follow-ups)

  • #295 — browser SPA auth = OIDC + local login (demote key to external/MCP-only). #287's ApiKey scheme documents auth for the spec's machine audience, which stays correct regardless.
  • #286 — /api/v1 versioning (next, after this lands).

🤖 Generated with Claude Code

Closes #287, closes #288. Part of the #197 contract-freeze arc (Phase-2). #286 (`/api/v1` versioning) stays parked until this lands (both regenerate the OpenAPI artifacts). ## What's in it **#287 — OpenAPI contract honesty by construction** (4 transformers on the "v1" doc, `Startup.cs`): - `ApiKey` security scheme + per-operation `security`/`401` injected via the **same** shared predicate the filter enforces (`ApiKeyAuthorizationFilter.EndpointRequiresKey`) — spec can't drift from enforcement. - Synthesized **stable, order-independent** `operationId`s (verb-suffixed on collision) for the ~90 ops that lacked a `Name=`. - `400 ValidationProblemDetails` documented on body/param-binding ops; `ProblemDetails` self-provisioned. - `DayOfWeek` → string enum (added to `UseStringEnumSchemas`); the SPA `WithDayNames` override removed. **#288 — wrap the last raw ViewModels + nullable-honest DTOs + search paging:** - No `/api/*` action returns an Application VM anymore: Collection/Schedule/SmartCollection/Resolution wrapped in ResponseModels (`Version` now header-only, SPA-verified), and the channel detail GET/writes return a new **`ChannelDetailResponseModel`** (the full editable field set the channel editor needs — distinct from the lean list `ChannelResponseModel`). - `#nullable enable` across all `ErsatzTV.Core/Api/` files; members `?` only where the mapper can emit null (honest MediaSources fields). - `pageNum` added to `GET /api/search`. **Channel `playout/reset` re-keyed to `{id:int}`** (owner-directed, Fable design verdict): the Channel admin API keys on the immutable `Id`, never the user-mutable `Number`. `POST /api/channels/{id:int}/playout/reset` (new `GetPlayoutIdByChannelId`; by-number kept for `HlsSessionWorker` broadcast). See `docs/decisions.md` 2026-07-12. ## Verification - `dotnet build ErsatzTV.sln` clean; **1472** `ErsatzTV.Tests` pass; SPA `typecheck`/`lint`/`test` (691) + `check:api` clean. - Independent reviews: **Codex = MERGEABLE**, cold adversarial = MERGEABLE-WITH-NITS (both LOWs fixed in `c40e78d8`), fix-commit re-review = **MERGEABLE**. - **Live headless E2E 5/5**: API-key gate, channel-editor round-trip (pre-fill + save + reload persist — the highest-risk change), reset re-key (`POST /api/channels/1/playout/reset` → 202), DayOfWeek string round-trip, search. ## Docs `docs/api-conventions.md` (§2/§3a/§5/§7a/§9), 3 `docs/decisions.md` entries, `docs/rest-api.md` reset route, regenerated `v1.json` + `endpoint-index.md` + `v1.d.ts`. ## Not in scope (follow-ups) - **#295** — browser SPA auth = OIDC + local login (demote key to external/MCP-only). #287's `ApiKey` scheme documents auth for the spec's machine audience, which stays correct regardless. - **#286** — `/api/v1` versioning (next, after this lands). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added the apireview labels 2026-07-12 12:14:19 +02:00
timothy added 8 commits 2026-07-12 12:14:20 +02:00
24 #nullable enable flips across ErsatzTV.Core/Api; Collection/Schedule/
SmartCollection/Resolution VMs wrapped in ResponseModels (Version now
header-only, SPA-verified); pageNum threaded into GET /api/search.

Refs #288 #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ApiKey security scheme + per-op security/401 via shared EndpointRequiresKey
predicate (no drift from enforcement); synthesized stable operationIds;
400 ValidationProblemDetails on binding ops; DayOfWeek as string enum.

Refs #287 #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GetById/Create/Update return ChannelResponseModel via new GetChannelByIdForApi
read-side query; POST /api/channels/{id:int}/playout/reset (new
GetPlayoutIdByChannelId; by-number kept for HlsSessionWorker broadcast).

Refs #288 #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mint ChannelDetailResponseModel (faithful detail DTO exposing the raw editable
field set the channel editor reads: raw FFmpegProfileId/WatermarkId/FallbackFillerId
ids, the mode enums, logo, playoutCount, id) and route GetById/Create/Update through
it, replacing the lean list ChannelResponseModel that resolved the profile to a name
and dropped the editable ids (a functional regression for draftFromChannel). The lean
ChannelResponseModel stays unchanged for GET /api/channels. webEncodedName dropped
(SPA never reads it). Logo is mirrored as a Core ChannelLogoResponseModel since the
Application ArtworkContentTypeModel can't be referenced from Core.

Repoint the hand-written SPA client aliases now that the VMs are gone from the schema:
Channel -> ChannelDetailResponseModel, MediaCollection/SmartCollection -> *ResponseModel,
ProgramSchedule -> ProgramScheduleResponseModel. Fix #288 honest-nullability test fallout
in search.test.ts (null -> [] for now-non-null id arrays). Include the already-on-disk
playouts.ts WithDayNames removal and regenerate v1.json + v1.d.ts + endpoint-index.md
(authoritative final regen; the reset endpoint's {channelNumber}->{id} re-key surfaces
in the generated docs and the OpenApi error-contract test).

Refs #288 #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
§2 raw-VM wrapping + universal #nullable enable; §3a/§5/§7a/§9 updated for
reset re-key, DayOfWeek string, header-only Version, security-by-construction;
3 decisions.md entries (#287/#288/channel-key); rest-api.md reset route.

Refs #287 #288 #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(api): #197 Bundle C review nits — order-independent operationIds + nullable MediaSources fields
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m55s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m18s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m36s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m21s
c40e78d840
Refs #287 #288 #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
timothy merged commit c40e78d840 into main 2026-07-12 12:25:21 +02:00
timothy deleted branch bundle-c-197-contract 2026-07-12 12:25:22 +02:00
Sign in to join this conversation.