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.
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
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>
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.
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).
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
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
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.
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:00MinValue for a never-scanned library. This makes the API itself honest so API/MCP have parity with the UI.Two layers:
GetAllMediaSourcesForApiHandler.NormalizeLastScanmaps any< 2000-01-01value to null. This is permanent, not a stopgap —MediaSourceRepositorystill writes the MinValue sentinel today on Plex/Jellyfin/Emby disable-sync flows (:480/611/976), so a migration-only fix would regress.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;Downis a documented no-op.hasScannedyear-cutoff heuristic; plain null check now that the API is honest.DTO field was already
DateTime?→ OpenAPI unchanged.decisions.mdentry 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: nullvia the real API while a sibling with a real timestamp is preserved.fixes #409
Review-verdict: MERGEABLE @
07705e6eb2Independent 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.
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 @
165851e0f2Cold 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).
07705e6eb2to165851e0f2Re-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 @
d457920116165851e0f2tod457920116Rebased again onto origin/main (decisions.md treadmill; code byte-identical). Setting auto-merge to land the instant CI passes.
Review-verdict: MERGEABLE @
2977f86c25d457920116to2977f86c25