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-independentoperationIds (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.
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)
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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #287, closes #288. Part of the #197 contract-freeze arc (Phase-2). #286 (
/api/v1versioning) 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):ApiKeysecurity scheme + per-operationsecurity/401injected via the same shared predicate the filter enforces (ApiKeyAuthorizationFilter.EndpointRequiresKey) — spec can't drift from enforcement.operationIds (verb-suffixed on collision) for the ~90 ops that lacked aName=.400 ValidationProblemDetailsdocumented on body/param-binding ops;ProblemDetailsself-provisioned.DayOfWeek→ string enum (added toUseStringEnumSchemas); the SPAWithDayNamesoverride removed.#288 — wrap the last raw ViewModels + nullable-honest DTOs + search paging:
/api/*action returns an Application VM anymore: Collection/Schedule/SmartCollection/Resolution wrapped in ResponseModels (Versionnow header-only, SPA-verified), and the channel detail GET/writes return a newChannelDetailResponseModel(the full editable field set the channel editor needs — distinct from the lean listChannelResponseModel).#nullable enableacross allErsatzTV.Core/Api/files; members?only where the mapper can emit null (honest MediaSources fields).pageNumadded toGET /api/search.Channel
playout/resetre-keyed to{id:int}(owner-directed, Fable design verdict): the Channel admin API keys on the immutableId, never the user-mutableNumber.POST /api/channels/{id:int}/playout/reset(newGetPlayoutIdByChannelId; by-number kept forHlsSessionWorkerbroadcast). Seedocs/decisions.md2026-07-12.Verification
dotnet build ErsatzTV.slnclean; 1472ErsatzTV.Testspass; SPAtypecheck/lint/test(691) +check:apiclean.c40e78d8), fix-commit re-review = MERGEABLE.POST /api/channels/1/playout/reset→ 202), DayOfWeek string round-trip, search.Docs
docs/api-conventions.md(§2/§3a/§5/§7a/§9), 3docs/decisions.mdentries,docs/rest-api.mdreset route, regeneratedv1.json+endpoint-index.md+v1.d.ts.Not in scope (follow-ups)
ApiKeyscheme documents auth for the spec's machine audience, which stays correct regardless./api/v1versioning (next, after this lands).🤖 Generated with Claude Code
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>