b1d5fbefcba02fdc6c19fef85cec1c4e82fc8dea
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ef2bd65c27 |
feat(api): #286 — mount the whole /api surface at /api/v1
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 3m4s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Version every /api route to /api/v1 (251 controller routes + ~24 Location
headers + the scanner callback URL + the Startup request-log literal),
uniform across the machine API, auth, scanner and scripted-build surfaces.
Add ApiVersionRewriteMiddleware: a legacy unversioned /api/* request is
rewritten (NOT redirected) to /api/v1/* in-pipeline — method, body, auth
headers and query survive — carrying RFC 8594 Deprecation/Sunset headers,
so curl / the future MCP server / bookmarks keep working. An already-
versioned path passes through; a future /api/v2 is never forced to v1.
Standardize the route convention (leading-slash absolute route per method,
no class-[Route] — except the two Scanner/Scripted controllers whose ~all
actions share a parametrized {id} prefix), enforced by ApiRouteVersioningTests
(^/api/v\d+/ over the whole Controllers.Api surface; browser-nav
/auth/oidc/login is out of scope).
Regenerate v1.json (160 paths, all /api/v1)/endpoint-index/v1.d.ts; sweep 945
SPA request literals + the test mocks (regex + positional URL parsers). /api/v1
is additive-only after freeze; the legacy-rewrite shim sunsets in ~2 releases
(owner decision) with removal tracked as a Phase-3 follow-up.
Docs: decisions.md 2026-07-13, api-conventions §1/§9, rest-api/spa-conventions/
blazor-route-parity/e2e-local/domain-model.
fixes #286
refs #197
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
8090e10408 |
fix(api): #265 — If-Match evaluates per RFC 7232 (valid-but-non-matching → 412, not 400)
The shared optimistic-concurrency parser (ConcurrencyHeaders.ParseIfMatch) classified any non-canonical/weak/list If-Match value as Malformed → 400. Per RFC 7232 §3.1 a syntactically -valid entity-tag that simply doesn't strong-match must be 412; 400 is only for a genuine grammar violation. - Rewrite ParseIfMatch as a real RFC 7232 entity-tag/list parser: walks the comma-separated 1#entity-tag list, validates each [W/]DQUOTE *etagc DQUOTE member, and collects the strong members whose opaque text is our canonical decimal. Weak / empty / non-canonical / out-of-range tags are valid but contribute no version (→ empty set → 412); genuine grammar violations (unquoted, SP-in-tag, unterminated, garbage) → 400. - Reshape IfMatchCondition.ExpectedVersion : Option<int> → ExpectedVersions : Option<Seq<int>> and VersionedAggregateExtensions.CheckVersion → set membership (any strong match proceeds; empty set always 412). Threads through 10 replace/update commands + handlers + request mappers + 9 controllers. - No wire-contract change (400 + 412 already declared on every PUT; the field is header-derived and internal — no DTO/route/response-type/OpenAPI change). - Tests: ConcurrencyHeadersTests rewritten for the new classification (lists, weak, empty, non-canonical → Version/empty-set; grammar violations → Malformed) + new VersionedAggregateExtensionsTests for CheckVersion membership/empty-set/force-write. - Docs: api-conventions.md §7a rewritten; decisions.md entry appended. Refs #253 #197 fixes #265 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1a24298105 |
fix(#253 PR2): close review findings (ETag/items consistency + SPA load ordering)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m2s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Independent Codex review of #268 found two Blockers the fork missed + two Mediums: - Blocker: replace PUTs returned the handler's item snapshot but re-queried the root for the ETag separately, so a racing writer could pair stale items with a newer ETag (silent overwrite). All four controllers now reload root-then-items (version-first, fail-safe) and 404 when the root is gone between commit and reload — matching the Block reference. Fixes the Blocker + the Medium '200 without ETag' case together. - Blocker: PlaylistsScreen loaded items+root via Promise.all (concurrent), pairing a stale name with the current ETag; now sequential (items-with-meta first, then root). - Medium: SchedulesScreen loadItems now marks not-loaded/loading up front so canEdit is false through the 412 conflict reload (no stale-draft edits lost). Controller unit-test mocks updated to stub the new reload query. Full suite green (ErsatzTV.Tests 1334, web 667, check:api no drift). |
||
|
|
611924c0ee |
feat(#253 PR2): optimistic-concurrency contract for Template and DecoTemplate
Wire the frozen ETag/If-Match/412 recipe (Block reference implementation) onto the Template and DecoTemplate aggregates: - ReplaceTemplateItems / ReplaceDecoTemplateItems commands gain Option<int> ExpectedVersion; ToCommand() on the request DTOs threads it through from If-Match. - Handlers introduce the version check as a standalone Either after validation (never via Apply), bump Version unconditionally before saving, and persist through SaveChangesWithConcurrencyGuard so a losing writer maps to 412 instead of 500. DecoTemplate's post-commit playout Reset enqueue now only runs after a successful save. - TemplateViewModel / DecoTemplateViewModel carry Version (header-only, not echoed in the response body), populated in Mapper. - TemplateController / DecoTemplateController: GET items emits a strong ETag of the root's version; PUT parses If-Match (400 on malformed), threads the expected version into the command, and returns the new ETag from the refreshed root on success. Both PUT actions now use the handler's returned item list directly instead of re-querying items. - SPA: templates.ts / decoTemplates.ts gain getXItemsWithMeta and an If-Match-aware replaceX; TemplateEditor / DecoTemplateEditor hold the ETag in a ref, read items-with-meta first on load, and open a "changed elsewhere" ConfirmDialog on a 412 instead of navigating away. Tests: new ReplaceTemplateItemsHandlerConcurrencyTests / ReplaceDecoTemplateItemsHandlerConcurrencyTests mirror the Block concurrency contract tests (stale/matching/absent If-Match, no-op bump, racing-save 412, non-vacuous backstop). TemplateControllerTests / DecoTemplateControllerTests gain ETag/If-Match/412 coverage. TemplatesScreen.test.tsx / DecoTemplatesScreen.test.tsx gain a 412 conflict-dialog test mirroring BlocksScreen's. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cda5c12e34 |
feat(api): REST endpoints for deco templates and deco template groups (#144 S4 (#162))
Adds DecoTemplateController mirroring TemplateController: CRUD for deco template groups, deco templates (flat list + by-id), item listing, and a full-replace PUT for a deco template's item list. Hardening (deviation from a literal port of the existing handlers, matching the #144 S2 fix for ReplaceTemplateItemsHandler): - CreateDecoTemplateHandler now validates DecoTemplateGroupId exists before insert (previously a bad id hit the FK constraint at SaveChanges and surfaced as a 500; now a 422). - ReplaceDecoTemplateItemsHandler now rejects invalid items (unknown DecoId, StartTime >= EndTime unless EndTime is the end-of-day sentinel 00:00:00, or overlapping ranges) with a 422 instead of silently dropping/persisting them - the same silent-drop/silent-overlap bug class already fixed for templates. Response DTOs serialize the raw item TimeSpans (via .TimeOfDay), so an end-of-day item still round-trips as StartTime=22:00:00/EndTime=00:00:00 regardless of the ViewModel's day-wrapping DateTime representation. |