fix(409): report never-scanned LastScan as null for API/MCP parity (migration + read coercion) #459

Merged
timothy merged 1 commits from fix/409b-lastscan-api-parity into main 2026-07-18 21:39:26 +02:00
Owner

Proper fix for #409: the earlier client-side SPA guard (PR #451) only fixed the UI — an API/MCP client hitting /api/v1/media-sources still got the misleading 0001-01-01T00:00:00 MinValue for a never-scanned library. This makes the API itself honest so API/MCP have parity with the UI.

Two layers:

  • Read-boundary coercion (load-bearing, ongoing): GetAllMediaSourcesForApiHandler.NormalizeLastScan maps any < 2000-01-01 value to null. This is permanent, not a stopgap — MediaSourceRepository still writes the MinValue sentinel today on Plex/Jellyfin/Emby disable-sync flows (:480/611/976), so a migration-only fix would regress.
  • Data migration (NullOutNeverScannedLastScan, dual-provider): one-time cleanup of the historical sentinel rows (UPDATE ... SET LastScan = NULL WHERE LastScan < '2000-01-01'). Data-only — model snapshots byte-unchanged; Down is a documented no-op.
  • SPA simplified: dropped the hasScanned year-cutoff heuristic; plain null check now that the API is honest.

DTO field was already DateTime? → OpenAPI unchanged. decisions.md entry added.

Review: DB migration → independent cold review = MERGEABLE-WITH-NITS (the one High — a duplicated decisions.md entry from concurrent editing — is fixed in this commit; predicate verified safe on both providers, no model drift). Live-E2E PASSED: a sentinel-LastScan library reports lastScan: null via the real API while a sibling with a real timestamp is preserved.

fixes #409

Proper fix for #409: the earlier client-side SPA guard (PR #451) only fixed the UI — an API/MCP client hitting /api/v1/media-sources still got the misleading `0001-01-01T00:00:00` MinValue for a never-scanned library. This makes the **API itself honest** so API/MCP have parity with the UI. **Two layers:** - **Read-boundary coercion** (load-bearing, ongoing): `GetAllMediaSourcesForApiHandler.NormalizeLastScan` maps any `< 2000-01-01` value to null. This is permanent, not a stopgap — `MediaSourceRepository` still writes the MinValue sentinel today on Plex/Jellyfin/Emby disable-sync flows (:480/611/976), so a migration-only fix would regress. - **Data migration** (`NullOutNeverScannedLastScan`, dual-provider): one-time cleanup of the historical sentinel rows (`UPDATE ... SET LastScan = NULL WHERE LastScan < '2000-01-01'`). Data-only — model snapshots byte-unchanged; `Down` is a documented no-op. - **SPA simplified**: dropped the `hasScanned` year-cutoff heuristic; plain null check now that the API is honest. DTO field was already `DateTime?` → OpenAPI unchanged. `decisions.md` entry added. **Review:** DB migration → independent cold review = MERGEABLE-WITH-NITS (the one High — a duplicated decisions.md entry from concurrent editing — is fixed in this commit; predicate verified safe on both providers, no model drift). **Live-E2E PASSED**: a sentinel-LastScan library reports `lastScan: null` via the real API while a sibling with a real timestamp is preserved. fixes #409
timothy added 1 commit 2026-07-18 18:56:59 +02:00
fix(409): report never-scanned LastScan as null for API/MCP parity (data migration + read coercion + SPA simplify)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 16s
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 47s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m2s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5m49s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m53s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m7s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
07705e6eb2
Data migration nulls the historical 0001-01-01 sentinel rows on Library/LibraryPath (both providers); GetAllMediaSourcesForApiHandler coerces any residual sentinel to null; SPA drops the client-side heuristic now that the API is honest.

fixes #409

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

Review-verdict: MERGEABLE @ 07705e6eb2
Independent cold review = MERGEABLE-WITH-NITS; the one High (duplicated decisions.md entry from concurrent editing) is fixed in this head. Migration predicate verified safe on both providers (SQLite ISO-text compare; MySQL had no Reset_*LastScan migration → safe no-op), no model-snapshot drift, read-coercion threshold matches the migration. Live-E2E PASSED: sentinel library → lastScan null via real API, real timestamp preserved. BOM-clean.

Review-verdict: MERGEABLE @ 07705e6eb2 Independent cold review = MERGEABLE-WITH-NITS; the one High (duplicated decisions.md entry from concurrent editing) is fixed in this head. Migration predicate verified safe on both providers (SQLite ISO-text compare; MySQL had no Reset_*LastScan migration → safe no-op), no model-snapshot drift, read-coercion threshold matches the migration. Live-E2E PASSED: sentinel library → lastScan null via real API, real timestamp preserved. BOM-clean.
Author
Owner

Rebased on origin/main to resolve the decisions.md append conflict (#403 landed). Code is byte-identical to the reviewed head — only the decisions.md entry was re-appended after #403 s entry (append-only).
Review-verdict: MERGEABLE @ 165851e0f2
Cold review MERGEABLE (dup entry fixed); migration predicate safe on both providers; no model-snapshot drift; live-E2E PASSED (sentinel to null via real API, real timestamp preserved).

Rebased on origin/main to resolve the decisions.md append conflict (#403 landed). Code is byte-identical to the reviewed head — only the decisions.md entry was re-appended after #403 s entry (append-only). Review-verdict: MERGEABLE @ 165851e0f2 Cold review MERGEABLE (dup entry fixed); migration predicate safe on both providers; no model-snapshot drift; live-E2E PASSED (sentinel to null via real API, real timestamp preserved).
timothy force-pushed fix/409b-lastscan-api-parity from 07705e6eb2 to 165851e0f2 2026-07-18 19:33:03 +02:00 Compare
Author
Owner

Re-triggered CI (fresh sha, no content change) — the previous head failed ONLY the decisions.md append-only job, which is a flake: the guard passes locally on that exact head (exit 0, numstat 37 add / 0 del), and even a synthetic PR-merge commit passes; every other job (Build & test, migration integrity, E2E, formatting, API-docs) was green.
Review-verdict: MERGEABLE @ d457920116

Re-triggered CI (fresh sha, no content change) — the previous head failed ONLY the decisions.md append-only job, which is a flake: the guard passes locally on that exact head (exit 0, numstat 37 add / 0 del), and even a synthetic PR-merge commit passes; every other job (Build & test, migration integrity, E2E, formatting, API-docs) was green. Review-verdict: MERGEABLE @ d457920116
timothy force-pushed fix/409b-lastscan-api-parity from 165851e0f2 to d457920116 2026-07-18 20:53:32 +02:00 Compare
Author
Owner

Rebased again onto origin/main (decisions.md treadmill; code byte-identical). Setting auto-merge to land the instant CI passes.
Review-verdict: MERGEABLE @ 2977f86c25

Rebased again onto origin/main (decisions.md treadmill; code byte-identical). Setting auto-merge to land the instant CI passes. Review-verdict: MERGEABLE @ 2977f86c25
timothy force-pushed fix/409b-lastscan-api-parity from d457920116 to 2977f86c25 2026-07-18 21:20:42 +02:00 Compare
timothy scheduled this pull request to auto merge when all checks succeed 2026-07-18 21:20:45 +02:00
timothy merged commit df8c5202d6 into main 2026-07-18 21:39:26 +02:00
Sign in to join this conversation.