6a4d81f862159d4e3dccdc4a7c607be97a8a1bdf
94
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6a4d81f862 |
docs(71): reshuffle action + seed surfacing (parity, domain-model, decisions)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
331a427040 |
Merge pull request 'feat(69): auto-tuning backend — enumerate library metadata, preview + bulk-create channels (PR1)' (#379) from feat/69-auto-tuning into main
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Failing after 13s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 5m7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 8m44s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been skipped
|
||
|
|
0320735f47 |
docs(69): auto-tune OpenAPI regen + api/domain/decisions docs
Refs #69 |
||
|
|
a2c056dd7a |
docs(release): prepare v26.9.0 promotion [decisions-edit]
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Has been cancelled
Consolidate docs/decisions.md (1923 -> 1028) by extracting four cohesive topic clusters into docs/decisions/ (optimistic-concurrency, api-auth-security, release-ci-governance, spa-modularization) — content relocated verbatim (lossless; all rationale + reversals preserved), main Index rebuilt to reference the topic files plus the remaining in-file entries, docs/README.md points back at the decisions Index. Add the v26.9.0 row to the ci-cd.md version table. refs #340 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3bc8192d3b |
feat(iptv): add configurable advertised base URL for M3U/XMLTV (fixes #340)
ErsatzTV built every absolute M3U/XMLTV URL from the incoming request's
Scheme/Host/PathBase, so a client fetching via a host that downstream
consumers can't resolve (e.g. Dispatcharr over Docker DNS → Kodi) baked
that internal host into programme-image/stream URLs.
Add an optional advertised IPTV base URL, backed by the existing
ConfigElement key/value store (key `iptv.base_url`, no EF migration):
- Central pure Core helper `AdvertisedBaseUrl` (TryParse/Resolve):
validates absolute http(s), no credentials/query/fragment, preserves
port + path prefix, normalizes trailing slash. Blank/invalid falls
back to the request-derived values, so unset output is byte-identical.
- Resolved inside `GetChannelPlaylistHandler` (M3U guide/logo/stream) and
`GetChannelGuideHandler` (both XMLTV {RequestBase} sites) — controllers
stay thin, golden tests untouched.
- New `iptv` settings group: GET/PUT /api/v1/settings/iptv (blank clears,
malformed → 422) + a new IPTV section on the SPA Settings screen.
- Scoped to M3U + XMLTV; HDHomeRun deliberately out of scope. Distinct
from ETV_BASE_URL (which only sets ASP.NET PathBase).
Tests: AdvertisedBaseUrl unit tests (override/fallback/port/path/invalid),
handler override tests for both generators, settings controller + handler
tests, SPA client + screen tests. Docs: m3u-xmltv, decisions, domain-model,
regenerated OpenAPI v1.json + v1.d.ts + endpoint-index.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0d7803079c |
ci: add advisory functional-E2E curl harness (fixes #299)
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m54s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 17s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 18s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 4m17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m42s
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 / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m22s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m38s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 6m2s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m41s
Codify the manual live-E2E curl flows sessions have been re-running by hand into a CI regression net: a new `functional-e2e` job boots the app from source (via scripts/e2e-local.sh, parameterized for Release) and drives scripts/ e2e-functional.sh against it. First-cut contracts (all curl-only, deterministic, no seeded media/ffmpeg/browser): - legacy->SPA redirect sweep + the /api,/artwork never-redirect exemption - auth/CSRF/security-stamp flow (setup-claim, read-gate, CSRF, login, logout+revoke) - library-scan status contract (404/202/scan-status) - optimistic-concurrency If-Match/412 round-trip Advisory by design (separate job, not a `build` dependency, not a required check) so a functional-E2E flake can't block the unit-test gate; promote once proven, mirroring the migrations-job rollout. SQLite default -> no DB service. Deferred to #299 follow-ups (need scanner+seeded media or a browser to be deterministic): the racy 409 re-trigger, playout-build lock 409, Playwright UI flows. Assertions verified 30/30 green against a real Release-built instance; caught /artwork/* returning 400 (not the 404 a static read suggested). Docs updated same PR: docs/ci-cd.md (new job), docs/e2e-local.md (harness), docs/decisions.md (append-only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
805ca026c4 |
docs(web): define shell and action ownership
Refs #247 Co-Authored-By: OpenAI Codex <codex@openai.com> |
||
|
|
533abe7cdb |
refactor(spa): extract playouts screen
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 11s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m49s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m21s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Move the Playouts UI, route wrapper, helpers, and detailed behavior tests into a colocated screen module while preserving App-level navigation coverage. Refs #245 Co-Authored-By: OpenAI Codex <codex@openai.com> |
||
|
|
2609b4ce59 |
fix: add cross-origin resource policy header
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 19s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m15s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m26s
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 / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Failing after 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 3m44s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been skipped
Fixes #330 Co-Authored-By: Codex <noreply@openai.com> |
||
|
|
985eed072c |
docs(release): prepare v26.8.0 promotion [decisions-edit]
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m45s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m49s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m51s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m47s
refs #335 Co-Authored-By: Codex <noreply@openai.com> |
||
|
|
216130b4d7 |
fix(#172): API hardening — null-name 500s, duplicate template items, unreachable 404
Clears the still-live findings from #172 (verified against main; #2/#4/#7 and the auth/search/Trakt tail were already deliberate-documented or fixed since 2026-07-07). - Null/empty Name → 500 (10 create/replace handlers). Block/Template/DecoTemplate/Deco Create+Replace/Update + UpdateFFmpegProfile did `request.Name.Length > 50` on a client-nullable string → unhandled NullReferenceException → HTTP 500 (no global exception filter). Now `string.IsNullOrWhiteSpace(request.Name) || .Length > 50` → 422; also rejects empty/whitespace names, matching the group-create handlers' NotEmpty behavior. CreatePlaylist coalesces null→"" at the DTO so it was an empty-name persist, not a 500; guarded the same way. - ReplaceTemplateItems overlap validation iterated with an `item == otherItem` record value-equality skip, so two exact-duplicate items were value-equal and bypassed the intersection check (both persisted). Now index-based (i != j) so duplicates register as a self-intersection and are rejected 422. - Trimmed the unreachable 404 ProducesResponseType from POST /api/blocks/groups and POST /api/templates/groups (a create has no parent lookup that can 404); v1.json regenerated. - Regression tests: all 10 name-guard paths + the duplicate-items path (19 cases). - Docs: decisions.md entry + api-conventions.md §3b null-safe-validation bullet. fixes #172 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
51b67dea06 |
fix(#238): review — trakt sub-route no-op + data-driven wiring test
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 11s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m59s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m35s
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 / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (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 5m25s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m26s
Cold-review follow-ups (both non-blocking):
- Low: the TopBar "Add Trakt List" button was a silent no-op on the
/app/trakt-lists/{id} detail sub-route (setAddOpen state isn't rendered by
the editor branch, and the screen is keyed by pathname so the state
wouldn't survive a navigate). Guard on editingId: route back to the list
from the detail view, open the dialog from the list.
- Nit: the invariant test only spot-checked 2 screens. Replaced with a
data-driven it.each over the 4 URL-navigating create screens (channels,
filler, ffmpeg, watermarks) asserting each banner actually navigates — a
typo'd route id now fails red. Docs wording corrected to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
612b11589c |
fix(#238): wire TopBar primary-action on create screens, drop dead buttons
The shell TopBar rendered a primary-action button (Plus icon) for every screen, but only SchedulesScreen subscribed to its ctv:primary-action event — so every other screen's button was dead (a labelled no-op, or a bare "+" for the ~10 routes whose primaryAction was ''). Resolution (issue #238): the Plus-icon button is a "create new item" affordance. Keep + wire it only on the 8 list screens with a single create flow (channels, schedules, multi/rerun collections, trakt lists, filler presets, ffmpeg profiles, watermarks) via a shared usePrimaryAction hook (web/src/primaryAction.ts); drop it (primaryAction: '') everywhere else — where the action isn't a create (Save/Refresh/Play/Validate/Reset/Scan, all of which have correct in-body controls), is ambiguous (collections tabs), a silent no-op (builder, playlists), or misplaced (dashboard, libraries). The TopBar now renders the button only when primaryAction is non-empty. Also relabels the apiKey route's stale post-#295 "Save key"/description. Docs: spa-conventions.md §10 + decisions.md 2026-07-12. 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> |
||
|
|
6537697fe5 |
test(#319): hash-drift guard prefers the built index.html when present
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m33s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m19s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m14s
Addresses the cold-review LOW: the guard hashed only the committed source web/index.html, so a hypothetical future Vite transform of the inline script would leave the test green while the deployed CSP silently broke the SPA. It now hashes the built wwwroot/app/index.html when it exists (the exact bytes the browser hashes — full coverage on any local build), falling back to the source only in a fresh CI checkout where the built artifact is gitignored/absent. Vite copies the inline script verbatim today, so the two agree. Doc comments + decisions.md synced. Test-and-docs only; no production code change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
873b3e54a5 |
security(#319): enforcing CSP + Permissions-Policy on the host
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 11s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m48s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m49s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m16s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Completes the CSP the #279 baseline-headers middleware deferred. Surfaced by the #314 ZAP baseline (missing CSP/Permissions-Policy WARNs); a #197 exit item. SecurityHeadersMiddleware now also sets Permissions-Policy (deny-all for camera/mic/geolocation/payment/usb) and an enforcing Content-Security-Policy on every response EXCEPT /docs (Scalar) and /openapi — those rely on inline bootstrap scripts/styles a strict policy would break (baseline headers still apply; hardening that admin surface is a #197 follow-up). CSP: default-src 'self'; script-src 'self' + the hash of the SPA's inline theme-bootstrap script (no 'unsafe-inline'/'unsafe-eval'); style-src adds 'unsafe-inline' (React inline styles) + fonts.googleapis.com; font-src adds fonts.gstatic.com; img-src adds data:/blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'. The Google Fonts allowance (the SPA CSS @imports the Geist web font) was caught by live-E2E, which the static recon missed. A guard test hashes the committed web/index.html inline script and fails if it drifts from the middleware constant (the built wwwroot/app is gitignored/absent in CI, and Vite copies the inline script verbatim). Verified: full test pass, live-E2E (SPA renders clean, zero CSP violations), curl (CSP present on /app + /api, absent on /docs + /openapi). Docs: docs/decisions.md entry. fixes #319 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
461c763dc6 |
docs: #295 PR2 + #301 — decisions entry, api-conventions §9, e2e-local browser flow
- decisions.md: new entry (SPA cookie-only cutover, boot-gate-not-route, #301 POST-ification rationale, machine-key-read + OIDC-logout residual) + TOC line. - api-conventions §9: #301 resolved (POST-ify) + 'never add a side-effecting GET' standing rule; machine-key endpoint added to the auth surface list; PR2-shipped note. - e2e-local: fix stale 'no key required' claim (fail-closed since #197) + browser setup/login boot-gate flow. (spa-conventions §5e rewrite landed with the SPA-consumers slice.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4da2b67ab2 |
feat(ci): #315 migration-on-prod-copy smoke for the release path
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 14s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 20s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 20s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m25s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
CI's migrations job only proves a migration is well-formed against a fresh, empty DB. It never exercises the migration — or ErsatzTV's startup data steps (DatabaseMigratorService -> DbInitializer + PopulatePathHashes over the real MediaFile table) — against the accumulated prod SQLite, so a migration green on a fresh DB can still fail/corrupt on prod, found only mid-deploy. scripts/migration-smoke.sh rehearses it on a THROWAWAY copy of the latest prod backup: boots the new image against the copy, gates PASS on the "Done applying database migrations" log line (the migrator is a BackgroundService running concurrently with Kestrel, so HTTP readiness alone doesn't prove migrations finished), FAILs on early container exit / migration exception / timeout / not serving afterwards. Always operates on a copy, never the live DB; tears down its container + temp dir (incl. the container's root-owned config files) on exit. Validated live 2026-07-12: :latest vs a copy of the 283MB prod backup -> migrations applied cleanly, app booted+served, temp dir removed. Home split: this repo owns the script + docs; wiring it into the Komodo pre-deploy step is server-management#589 (cross-repo). Docs: docs/ci-cd.md (Migration integrity), docs/decisions.md (new entry, pure insertion). fixes #315 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
90c8348efe |
fix(process): #317 docs-only exemption stays passthrough, not silent auto-grant [decisions-edit]
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m22s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m30s
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 / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m33s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m36s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m33s
Adversarial-review nit on the first commit: auto-granting the docs-only exemption silently self-merges process-control PRs (.claude/.gitea/.husky — including the gate hook itself) with no prompt and no review, bypassing human-in-the-loop for exactly the files that control the gate. Restrict auto-grant to the genuinely- satisfied (a+b+c) merge path; the docs/process exemption reverts to bare exit-0 passthrough (one normal prompt). Corrects this PR's own decisions.md entry accordingly ([decisions-edit]: a not-yet-merged draft entry, not history). 8 pipe tests green (adds docs-only -> passthrough). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8565f731cd |
fix(process): #317 merge-consent gate auto-grants on satisfied path (no double-prompt)
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m58s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m39s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The H6/H10 merge-consent hook's satisfied path did a bare `exit 0`, which does not auto-approve a PreToolUse tool call — it only declines to block, so control fell through to the normal permission system and the raw MCP prompt still fired. A ready-to-merge PR was therefore confirmed twice (conversationally + a redundant mechanical prompt). Emit permissionDecision "allow" (new `grant` decision) on the satisfied and docs-exempt paths so the derived state IS the consent; deny/ask unchanged (fail-closed); non-merge methods keep the exit-0 passthrough. Docs: CLAUDE.md, kickoff HARD CONSTRAINTS, docs/decisions.md (append-only, pure insert). Verified: 7 pipe tests (satisfied->allow, unticked->deny, stale->deny, red-CI->deny, no-verdict->ask, no-creds->ask, non-merge->passthrough). fixes #317 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
19dbfe9a8b |
docs(process): #311 — formatting-as-you-touch (§7) + rebase-not-merge lore + decisions entry
- contributing.md §7: document fix-formatting-as-you-touch (normalize a legacy file you edit, incl. BOM strip; no big-bang), enforced by the pre-commit hook + the new format CI job. - kickoff lore: flip "merge main into PR branch" -> "rebase on origin/main" (H11 enforces it); keep the regenerate-generated-artifacts-on-conflict nugget. - decisions.md: new entry (+ TOC) for both decisions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b20ee50b1f |
docs(process): #303 follow-ups — Codex-skip rubric + write-path live-E2E requirement
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Two of the four non-hook #303 process follow-ups (the two docs items; the security scan and migration-on-prod-copy smoke are deferred to their own sessions): 1. Codex-skip rubric (kickoff workflow lore): an independent review pass is MANDATORY for diffs touching locks/concurrency, auth/security, API write-path handlers, or migrations, or >~150 changed C# lines; skippable only for a pure-SPA/docs leaf, and a skip must be stated + justified. Makes self-exemption an auditable claim (the correlated-blindspot net). 2. Live-E2E is now a STATED REQUIREMENT for API write-path handler changes: new "When live-E2E is required" section in docs/e2e-local.md + a decisions.md entry, formalizing the #229 lore bullet. The seeding recipe was already in e2e-local.md (added for #220), so the stale "recipe not yet in docs" lore bullet is pruned to a pointer. Docs-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7a9b30de71 |
fix(api): #269 review fixes — rebase force-write delta so rotation survives a race (Codex F1/F3)
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m20s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m50s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 3m43s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 6m51s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m26s
Independent Codex review (reconciled by Fable against a MERGEABLE fork verdict) found SaveChangesForcingVersion silently DROPPED a pending Version++ under a concurrent versioned-write race: on DbUpdateConcurrencyException it adopted the DB's current Version verbatim (original = current = dbVersion), so a bumping sibling committed at dbVersion instead of dbVersion+1. Net: an editor holding the concurrent writer's ETag was never invalidated by the sibling's change — the exact lost-update the #253/#269 contract exists to close, lost under the very condition the helper handles. F1 fix (shared helper, corrects all 25 bumpers incl. the pre-existing Add*ToPlaylist / schedule-item writers): rebase the pending delta on top of the stored token — pendingDelta = current - original; original = dbVersion; current = dbVersion + pendingDelta Bumpers (delta 1) advance to dbVersion+1; non-bumpers/deletes (delta 0, e.g. ErasePlayoutHistory) still adopt the stored token unchanged, so RootWriterForceVersionTests is unaffected. Idempotent across the bounded retry loop. F3: the force-race tests now assert Version==3 (rebase), not just membership survival; added the missing Playout force-race+rotate test. Negative-controlled: with the helper fix reverted, both strengthened tests go red. F2 (Medium, deferred → #308): two concurrent same-item Add*ToCollection can both pass the membership check and the loser 500s on the composite-PK violation (DbUpdateException, which the helper doesn't catch). Pre-existing and narrow (no corruption); doc claims softened to name it. Filed #308. Docs: api-conventions §7a + decisions.md prose corrected from "adopt the stored token" to the rebase semantics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
83f753b211 |
fix(api): #269 rotate aggregate ETag on Collection/Playout config siblings
Complete the #253 optimistic-concurrency contract's cross-editor ETag rotation tail. The non-If-Match config siblings mutated editor-visible state without bumping Version, so a concurrent editor of the same root never invalidated. Now the Collection Add*/Remove handlers bump Collection.Version, and UpdateCollection / UpdatePlayout / the three ScheduleFile writers (which already force-wrote past a concurrent bump) now bump too — all via SaveChangesForcingVersion (no If-Match → force write, never 412/500). No-op idempotence (Fable-caught trap): these gate reindex/BuildPlayout fan-out on SaveChanges()>0, so an unconditional bump would fire spurious rebuilds on an idempotent re-add / same-value re-submit. Each now short-circuits a genuine no-op before the bump — Add handlers by an explicit membership check (also fixing a latent duplicate-CollectionItem insert), scalar writers by ChangeTracker.HasChanges(). Corrects #269's framing: the Add*ToCollection family is not repository-mediated (IMediaCollectionRepository is read-only); each handler writes via its own dbContext, so the scanner's separate membership path is unaffected (a background scan does not rotate the editor ETag). Tests: CollectionEtagRotationTests + PlayoutScheduleFileEtagRotationTests (rotation, no-op-without-bump-or-rebuild, force-write-past-concurrent-bump), no-op guard proven non-vacuous by inverting the membership check. Docs: api-conventions §7a + decisions.md. No new status codes / no OpenAPI change (these endpoints take no If-Match, never 412). The #265 RFC-7232 If-Match parser refinement is a separate PR. fixes #269 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ff3df39c43 |
fix(process): #303 H10 — anchor verdict marker to line-start (close self-reference false-open) [decisions-edit]
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m59s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m13s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Second adversarial re-review (of the fix commit itself — dogfooding H10) found
a remaining false-open: the `review-verdict:` marker was matched anywhere on a
line, so a comment merely QUOTING the positive template — an instruction
("please post: Review-verdict: MERGEABLE @ <head>"), a blocking comment
explaining how to clear itself, or the gate's OWN suggestion text echoed into a
comment — was classified as a real head verdict and self-approved the merge.
Fix: anchor the marker to line-start (`^[[:space:]]*review-verdict:`). A real
verdict line starts with the marker; quoted/instructional uses have text before
it. Also drops the dead `nosha` var (SC2034).
Finding 2 (a BLOCKED mis-anchored to an OLDER sha doesn't retract a
MERGEABLE@head) is deliberately NOT "fixed": staleness is symmetric — a
pre-fix BLOCKED@oldsha must not block forever after the fix changes the sha and
earns a fresh MERGEABLE@head. To retract, re-review head and post BLOCKED@head.
Documented in decisions.md.
Pipe-tested 21 cases (5 new: instructional-quote, self-reference reason-text,
line-start-in-multiline, leading-whitespace, blocking-quotes-template), all
deny/ask/allow correct. bash -n + shellcheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f87a2092c4 |
fix(process): #303 H10 — anchor sha match to the @<sha> field; retraction-wins [decisions-edit]
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m13s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adversarial review found false-opens in the first cut:
- `grep -F "$short"` was an unanchored substring test: a MERGEABLE verdict for
a DIFFERENT/older commit was accepted whenever the head 7-prefix appeared
anywhere on the line (inside a longer sha, or an unrelated commit URL). Now
each verdict line's `@ <sha>` token is extracted and matched to head by git
short-sha prefix semantics (head begins with token, token >=7 chars).
- No retraction semantics: a later `BLOCKED @ head` didn't override an earlier
`MERGEABLE @ head`. Now a negative verdict on head wins -> deny.
- A 7-digit build number falsely tripped the "references an older commit" deny;
the `@`-anchored parse fixes it -> a marker with no @sha now asks, not denies.
Also documents the issue-comment scope (gate reads issues/{pr}/comments, not
Gitea formal-review bodies). Pipe-tested: 16 cases incl. 4 adversarial
false-open reproductions, all now deny/ask. bash -n + shellcheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9fd8f40541 |
chore(process): #303 H10 — review-verdict merge-gate (latest commit must be reviewed)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m4s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m43s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Folds condition (c) into pretooluse-merge-consent.sh (H6): a PR merge is allowed only when a `Review-verdict:` comment references the PR's CURRENT head sha — proving the latest commit was reviewed, not a stale earlier diff (mechanizes the ersatztv#242 "re-review the fix commit" lesson). Graceful adoption mirrors H6's Done-when tiering: - positive verdict @ head -> allow - verdict @ older sha (stale) -> deny (#242 failure mode) - head verdict negative -> deny - marker with no sha / none yet -> ask - comments unfetchable -> ask Reuses H6's PR fetch, docs-only exemption, and Gitea-auth-from-env (one hook, no detection drift — per the #303 methodology review). Pipe-tested 12 cases. Docs: decisions.md (new H10 entry + TOC), CLAUDE.md Task Completion Protocol. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6e92a951eb |
feat(process): #303 decisions.md consolidation size-floor (read-cost, non-blocking) [decisions-edit]
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Failing after 9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 15s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (pull_request) Has been cancelled
Timothy's refinement: the between-releases consolidation floor triggers on the file's READ COST — its line count, i.e. the context an agent burns reading the log — not entry count. The decisions-guard CI job now emits a non-blocking ::warning:: once decisions.md exceeds 1800 lines (the point past which it no longer fits one default 2000-line agent Read). Documented in the decisions.md header, the H9 entry, and ci-cd.md (job description + release-checklist note). Touches committed H9 lines, hence [decisions-edit]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
59ecafecce |
docs(process): #303 fold review nits — H3 case-insensitive, granularity wording, newline note [decisions-edit]
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 11s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adversarial review (PR #305, MERGEABLE) nits: - H3: `grep -iE` so a root `Screenshot.PNG` is caught too (was lowercase-only). - decisions-guard.sh: comment the trailing-newline assumption (dropping the final newline would make git render the next append as a last-line modify -> false-block; self-correcting via [decisions-edit], .editorconfig enforces the newline). - docs: clarify CI is PR-wide (`range`) vs Husky per-commit (`staged`) — shared detection logic, deliberately different granularity; local hook is the stricter gate. Replaces the slightly-overstated "can't drift" wording. Touches the committed H9 decisions.md entry, hence the [decisions-edit] token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
997f96c1f5 |
docs(process): #303 append-only supersession + consolidation convention [decisions-edit]
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
decisions.md header now documents the enforced append-only rule, the `[decisions-edit]` escape, mark-and-keep supersession (banner on the reversed entry + `(superseded)` TOC tag), and consolidation at each release. ci-cd.md documents H3/H9 hooks + the decisions-guard job and adds a "consolidate decisions.md" step to the release checklist. New decisions.md entry records the H9/H3 mechanization. The header edit modifies existing lines, so this commit carries the [decisions-edit] token — the guard working as designed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a81f024840 |
Merge remote-tracking branch 'origin/main' into ci/303-api-docs-blocking
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m21s
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 / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m19s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m30s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m30s
|
||
|
|
08d633c687 |
Merge pull request 'fix(api): #269 force-write non-If-Match root writers past a concurrent Version bump' (#302) from fix/269-force-version-on-root-writers into main
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 3m53s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m57s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
|
||
|
|
c34d2bdbf2 |
Merge remote-tracking branch 'origin/main' into ci/303-api-docs-blocking
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 19s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m7s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts: # docs/decisions.md |
||
|
|
0badff811d |
feat(process): #303 H6 merge-consent derived from ## Done-when checklist
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m26s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m32s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m48s
Wave 2 hook H6: derive merge-consent from state instead of memory. An issue's ## Done-when checklist (issue body) becomes the machine-readable source of truth for whether its PR may merge — the structural fix for the queue-drift #303 tracks (status was living in append-only prose). - pretooluse-merge-consent.sh (Claude PreToolUse on mcp__gitea__ pull_request_write): a merge is ALLOWED only when the PR's CI is green AND every ## Done-when box on the linked issue (fixes #N) is ticked; DENY on an unticked box / red CI; ASK (human prompt) when state isn't derivable (no linked issue, no section, no creds, Gitea down). Docs/ process-only PRs exempt. - .husky/pre-push -> prepush-donewhen.sh: fail-open backstop for a direct `git push origin main`; blocks only on a positively-proven unticked box. Gitea auth from env only (ETV_GITEA_BASICAUTH / ETV_GITEA_TOKEN, ETV_GITEA_URL) — nothing committed; without creds the gate degrades to today's manual confirmation, never a silent pass. Non-breaking rollout: until issues adopt ## Done-when the merge hook simply asks. Pipe-tested: non-merge->allow, no-creds->ask, docs-only->allow, checklist parser (unit), linked-issue extraction, and a live end-to-end block path (temp Done-when on #303 -> exit 1 -> restored). Docs: CLAUDE.md Task Completion Protocol + decisions.md entry. Refs #303. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
aa2e13fa51 |
ci: #303 H4/H5 blocking api-docs gate — fail on stale OpenAPI artifacts
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m15s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adds a blocking `api-docs` CI job: when a PR diff touches the API surface (ErsatzTV/Controllers/Api/** or ErsatzTV.Core/Api/**) it rebuilds the generated artifacts from source — v1.json, v1.d.ts, endpoint-index.md — and fails if any is stale in the diff. Mechanizes the "docs-update in the same PR" rule for the API contract (docs-reminder stays a non-blocking route-parity nudge). Path-gated INSIDE the job (per-step `if:` on a detect output), not via a top-level `if:`, so the check always reports a status on every PR and is safe as a required check: API-free PRs skip the dotnet/node setup + regen and pass trivially. Verified the gate reproduces the committed baseline: a fresh build regenerates v1.json byte-identical to HEAD (incl. all 244 auth security/401 blocks). The only footgun is local — update-openapi.sh runs dotnet-getdocument against the already-built assembly, so a stale bin/ emits a stale spec; api-conventions.md §5 now flags "build first". CI is immune (fresh checkout has no bin/). Docs: api-conventions.md §5 (two-place CI enforcement + stale-assembly note), decisions.md (new entry). Refs #303. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5a12aae66e |
Merge remote-tracking branch 'origin/main' into fix/269-force-version-on-root-writers
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m22s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts: # docs/decisions.md |
||
|
|
171d30c709 |
Merge remote-tracking branch 'origin/main' into fix/269-force-version-on-root-writers
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m58s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts: # .gitignore |
||
|
|
21b49e6a42 |
chore(#269): remove accidental web/node_modules symlink from PR
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m27s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m37s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Fix-commit re-review (cold fork) caught that
|
||
|
|
48d256f83b |
Merge origin/main into feat/295-auth-pr1
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m27s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m36s
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 / Build & test (.NET) (push) Successful in 6m7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m11s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m36s
Brings in the #303 process/rigor hooks + docs. Only conflict-free overlap was docs/decisions.md (main added the ## Index TOC); reconciled by adding the #295 auth entry to the index. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
35e41fdaad |
chore(process): #303 H7 worktree-owner guard + complete Wave 1 wiring
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m1s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m13s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Wave 2 hook H7: never commit/merge inside a sibling worktree another session created (burned us on #289 path-leak + the plumbing-merge workaround). Ownership = a per-session .claude-worktree-owner marker: - posttooluse-worktree-marker.sh stamps a worktree with session_id on `git worktree add` (parses the <path> arg past -b/-B/--reason flags). - pretooluse-worktree-guard.sh denies `git commit`/`git merge` whose effective dir (resolves `git -C <p>` and leading `cd <p> &&`) is a worktree whose marker names a DIFFERENT session. Fail-open: no marker, unparsable, or own session -> allow. Main tree + pre-convention worktrees are never marked, so unaffected. Also completes Wave 1's rollout, which committed pretooluse-bash-guard.sh but left .claude/settings.json and the agent-ram/nav-guard hooks untracked (so nothing was actually wired). Adds the settings.json that registers all five hooks (PreToolUse Bash x2, nav, Agent; PostToolUse Bash) + the .gitignore worktree-marker line, screenshot-scratch rules, and the decisions.md TOC left uncommitted last session. All hooks pipe-tested (7 guard cases + 6 marker cases). Refs #303. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e383c253cc |
fix(api): #269 review — force-write ErasePlayoutHistory + document boundaries
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m54s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m45s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Independent adversarial review (cold fork + Codex) of the first cut converged
on one real miss and two boundaries to document.
- **ErasePlayoutHistoryHandler** (HIGH, both reviewers): modifies Playout ROOT
scalars (Seed/Anchor/OnDemandCheckpoint) *without* bumping Version, inside an
explicit transaction with no try/catch, so it 500s on a concurrent bump —
reachable via POST /api/playouts/{id}/erase-items-and-history. My first sweep
filtered on "Version-bumpers + deletes"; the true exposure surface is "any
handler leaving a versioned root Modified/Deleted", so this slipped through.
Now routes through SaveChangesForcingVersion (+ a non-vacuous through-handler
test that exercises the explicit-transaction path). Re-swept with the correct
filter: ErasePlayoutItems (AsNoTracking + ExecuteDelete children only) and
ResetAllPlayouts (read-only + enqueue) are NOT exposed.
- **Background build/time-shift Playout-scalar writers** (BuildPlayout via
PlayoutBuilder, PlayoutTimeShifter): token-guarded too, but intentionally left
on plain save — they already catch (build-failure, not 500), and force-writing
would persist output built from stale config (the concurrent config bump already
enqueues a rebuild). Documented as a deliberate boundary, not a gap.
- **Item-add index collision** under force-write: documented as an accepted
Phase-1 effect (non-corrupting, self-correcting; reload-recompute refinement
is a #197 candidate).
Also corrects the docs' "every Version bumper" framing to the true filter and the
test docstring's over-broad non-vacuity claim. Full ErsatzTV.Tests green (1483).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e8c3481ea5 |
fix(api): #295 PR1 — fold in fix-commit re-review (2nd Codex round)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 11m9s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Fix-commit re-review confirmed the 1st-round fixes resolved and caught a 2nd round: - HIGH — env-seed vs. setup race: an attacker could claim admin in the startup window before LocalAdminSeedService runs, and the seed's insert would then be swallowed (attacker credential persists, defeating env recovery). Fixed structurally: the setup-claim endpoint is CLOSED (409) whenever Auth:LocalAdmin:Password is configured — the env seed owns the credential, so there's no claim to race (also strengthens the setup-claim TOFU posture). Config.setupRequired reflects it. - LOW — a concurrent setup race-loser now returns 409 (not 422); ClaimLocalAdmin's DbUpdateException catch re-checks existence and rethrows genuine/transient DB errors instead of masking them as "already configured". - MEDIUM (accepted, documented) — two simultaneous authenticated password changes are a non-serializable lost-update; accepted for a single-admin system (self-healing via re-login, implausible timing). +3 AuthController tests (env-seed closes setup / setupRequired gating). Full ErsatzTV.Tests green (1506); no generated drift. Docs updated. Refs #295 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9a9aaf0740 |
fix(api): #295 PR1 — logout ends the session server-side (E2E-caught)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m20s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Live E2E found that replaying a pre-logout cookie still authenticated (200, not 401): SignOutAsync only clears the CLIENT cookie, but the stateless encrypted cookie ticket stays valid server-side because its security stamp is unchanged — a captured cookie was replayable after logout until ticket expiry. Fix: logout now rotates the local-admin security stamp (RotateLocalAdminSecurityStamp), so every outstanding local session (old stamp) fails OnValidatePrincipal on its next request. For the single admin this is "log out everywhere". Gated on an authenticated local session so an unauthenticated caller can't force-revoke the admin. OIDC sessions (no stamp) are unaffected; SignOutAsync still clears the client cookie for UX. +2 handler tests (rotate-when-configured / no-op-when-unconfigured). Auth suite green (21). Docs: decisions.md note updated. Refs #295 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6ac5150fd0 |
fix(api): #295 PR1 — fold in cold-fork + Codex review findings
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m22s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Independent review (cold fork = MERGEABLE-WITH-NITS; Codex = BLOCKED, caught concurrency defects the fork missed). All actionable findings folded in: - HIGH (Codex) atomic first-claim-wins: ClaimLocalAdmin now writes the three credential rows in ONE transaction guarded by the unique ConfigElement.Key index (lost race -> DbUpdateException -> 409), so concurrent claims can't produce a mixed-state credential. - HIGH (Codex) consistent login snapshot: VerifyLocalAdminLogin reads hash+stamp in one query and drops rehash-on-verify, so a login racing a password change can't capture a stamp newer than the hash it verified (concurrent change -> old password fails, or the issued cookie carries the pre-change stamp -> revoked next request). - MEDIUM (Codex) env-seed migration race: LocalAdminSeedService is now a RunOnce BackgroundService that awaits SystemStartup.WaitForDatabase (the migrator is a BackgroundService; registration order didn't guarantee the schema) + try/catch. - MEDIUM (fork M1) ForwardedHeaders: reverted the strict-opt-in flip — it would regress /iptv M3U/XMLTV/HLS absolute-URL generation (Request.Scheme) behind a proxy without KnownProxies. Kept #285 behavior; KnownProxies still recommended. - LOW (Codex/fork) require X-CSRF on /api/auth/logout + /password (the [SkipApiAuthorization] surface isn't covered by the filter's CSRF check; closes forced-logout CSRF). - ChangeLocalAdminPassword also writes hash+stamp atomically. Input length caps on username/password. Deferred with a tracked gate: MEDIUM (Codex) side-effecting [RequiresAuthentication] GETs (troubleshoot playback/archive) aren't CSRF-covered -> #301, gates PR2 (latent in PR1: the SPA still uses the machine key). Verify: full ErsatzTV.Tests green (1501); no OpenAPI/generated drift. Docs updated (api-conventions §9, decisions.md). Refs #295 #301 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d80bf886b2 |
fix(api): #269 force-write non-If-Match root writers past a concurrent Version bump
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m49s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Activating #253's `Version` as an `IsConcurrencyToken` made EF guard every UPDATE *and DELETE* of a versioned root with `WHERE Version=@orig`, so any writer outside the If-Match contract that saves via plain `SaveChangesAsync` throws an unhandled `DbUpdateConcurrencyException`->500 when a replace-all editor bumps the row in its narrow load->save window (ordinary two-tab UI). A completeness sweep (grep every `Version` bumper + every root delete, not just the handlers PR3's close note named) found 17 exposed writers, all now routed through `ConcurrencyExtensions.SaveChangesForcingVersion` (Phase-1 force-write: adopt the stored token and retry; rethrow only on genuine row-deletion): - 9 versioned-root delete handlers (a delete has no ETag to rotate -> force only, no bump) - UpdateProgramScheduleHandler (bumps then saved plainly - the ProgramSchedule case PR3 only suspected; its post-commit query/enqueue also moved to CancellationToken.None per section 7b) - 7 item add/remove bumpers PR2 left on plain save: Add/DeleteProgramScheduleItem + Add{Items,Movie,Show,Season,Episode}ToPlaylist Force-write (not 412) is correct: these endpoints take no If-Match, so an unconditional delete/edit should win. No API contract change (no new response codes) -> no OpenAPI regen. Still deferred to #197 (cross-editor ETag rotation only, not a 500): the non-bumping config siblings + the scanner-shared Add*ToCollection family. Tests: RootWriterForceVersionTests races a bump *through the handler* via a pre-tracked context (non-vacuous - reverting a handler to plain save fails the test, verified) for the Option-delete / Either-delete / bump+update shapes, plus the genuine-conflict rethrow branch and an explicit negative control proving the plain-save path throws. Full ErsatzTV.Tests green (1482). Also strips a pre-existing UTF-8 BOM from the touched handlers to satisfy the .editorconfig `charset=utf-8` rule the pre-commit format hook enforces. Docs: api-conventions section 7a (fan-out completeness) + decisions.md entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0b23d4b6b1 |
feat(api): #295 PR1 — browser SPA session auth (session-OR-key gate, server-only)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m28s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Implements the ratified #295 design (PR1, server-only, backward compatible). The /api surface now accepts a valid X-Api-Key (machine) OR an authenticated session (browser cookie, local login or OIDC), gated by the evolved ApiAuthorizationFilter (renamed from ApiKeyAuthorizationFilter; same fail-closed EndpointRequiresKey predicate). Machine/key behavior is byte-identical and the SPA keeps working via its stored key — the SPA login flow lands in PR2. - ApiAuthorizationFilter: key-first (CSRF-immune) then session; session-authed mutations require the X-CSRF header (403 otherwise). Attributes renamed [RequiresApiKey]->[RequiresAuthentication], [SkipApiKeyAuthorization]->[SkipApiAuthorization]. - Cookie scheme ctv-session always registered (Lax/SameAsRequest/14d sliding, 401 not redirect for /api); OIDC handler revived when configured (profile scope, userinfo, auth-method claim); UseAuthentication/UseAuthorization/UseRateLimiter revived in the legacy MapWhen branch. - Local admin = single credential in ConfigElement rows (username / PBKDF2 hash via Microsoft.Extensions.Identity.Core / rotating security stamp) — NO DB migration. Password change rotates the stamp; CookieSecurityStampValidator revokes stale local sessions. Env-seed recovery (Auth:LocalAdmin:*) via LocalAdminSeedService. - AuthController /api/auth/{config,session,setup,login,logout,password} + browser-nav GET /auth/oidc/login; excluded from OpenAPI (machine-audience spec). Per-IP rate limit on login/setup/password; dummy-hash verify (no user enumeration). - ForwardedHeaders now strict opt-in: X-Forwarded-* ignored unless KnownProxies/Networks configured (rate-limiter IP + cookie-Secure integrity). Deployment: operators behind a proxy must set ForwardedHeaders:KnownProxies. - Tests: session/CSRF filter cases + 17 Application/Auth handler tests; full ErsatzTV.Tests green (1499). No OpenAPI/generated-artifact drift. - Docs: api-conventions section 9 rewritten; decisions.md entry (supersedes #206 inert-OIDC note). Refs #295 #197 #206 #58 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6d31758cca |
feat(api): #271 collections scan-status REST surface + authoritative SPA reconcile
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m51s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Add GET /api/media-sources/collections-scan-status (MediaSourcesController →
GetCollectionsScanStatus handler) reporting which media-source families
(plex/jellyfin/emby) currently hold their external-collections scan lock,
reading IEntityLocker.Are{X}CollectionsLocked(). The lock is family-global
(no source id) and boolean (no percent), so the DTO carries just {family} and
returns only active families — the counterpart to GET /api/libraries/scan-status.
SPA: useCollectionsScan now polls this endpoint and reconciles optimistic
pending against the active-family set (seeding on mount so an in-progress scan
disables buttons immediately), using the same grace-tick helper as library
scans (now generic over the pending key type). Drops COLLECTIONS_PENDING_TIMEOUT_MS
— a long deep scan no longer re-enables the button early, and a fast scan no
longer wedges it disabled for the full timeout. A row shows Scanning when its
family is active or it has an in-grace optimistic pending key.
Tests: handler (3), controller route+delegation (2), SPA api fn + hook reconcile
(mount-seed / 202-promote / 409-keeps-disabled / 404-error). OpenAPI + TS types
regenerated. Docs: api-conventions §3b, blazor-route-parity §5, decisions.md.
Unblocks #91b (arc item 4): Libraries.razor's collections-scan affordance now
has full authoritative parity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
a918ccd60c |
docs: #197 Bundle C — api-conventions/decisions/rest-api sync
§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> |
||
|
|
f8fd9084d1 |
Merge main (CI migration-job retry #294 + #197 tests) into fix/283
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m31s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m40s
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 4m34s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m34s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m54s
# Conflicts: # docs/decisions.md |