Never-scanned local libraries render "Last scan 12:00 AM" instead of "Never scanned" (LastScan seeded as DateTime.MinValue, not NULL) #409
Closed
opened 2026-07-17 14:31:29 +02:00 by timothy
·
4 comments
No Branch/Tag Specified
main
renovate/lucene.net
renovate/dotnet-monorepo
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
renovate/meziantou.analyzer-3.x
renovate/cliwrap-3.x
913-scripted-playout-401
release/v26.15.0-notes
issue-806-guard-populations
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.16.0
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#409
Reference in New Issue
Block a user
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.
Severity: Low (display). Pre-existing. Found via live E2E while verifying #264 (PR #408) — it is the mirror image of #264 and was masked by it.
What's wrong
The SPA decides the badge with a truthiness check:
…but a never-scanned local library's
LastScanisDateTime.MinValue, notNULL. The API serialises that as the string"0001-01-01T00:00:00", which is truthy in JS, so the "Never scanned" branch is never taken.formatDateTimethen parses it successfully (it's a valid date) and returnstoLocaleTimeString(...)→ the row renders:i.e. the UI asserts a scan happened at midnight for a library that has never been scanned.
sourceLastScanLabel(LibrariesScreen.tsx:465-471) has the same defect — it filters withBoolean(scan), which MinValue also passes.Evidence (live, fresh config DB, 2026-07-17)
A fresh instance seeds six of the seven built-in local libraries with MinValue and exactly one with
NULL:GET /api/v1/media-sourceson an unscanned library returnslastScan='0001-01-01T00:00:00'. So "Never scanned" only renders correctly for Remote Streams (the loneNULL) — the other six render the bogus midnight time. The inconsistency is a seeding accident, not intent.Note this partially contradicts #264's premise ("every local library permanently shows Never scanned"). #264's underlying defect was real and is fixed by PR #408 (the local scanner never wrote library-level
LastScan), but the symptom description only holds where the value isNULL. Since #408 now writes a real timestamp on every successful scan, this residual bug is only visible before a library's first successful scan — which is exactly when the badge matters most.Fix directions (pick one; not obvious which is right — needs a call)
MinValue→nullinGetAllMediaSourcesForApiHandlerso the DTO'slastScanis honestly nullable. Fixes every consumer at once; the DTO field is alreadyDateTime?. Preferred if we treat MinValue as "no value".NULLinstead of MinValue and migrate existing MinValue rows toNULL(dual-provider migration). Cleanest data model, but touches both providers and existing installs.LibrariesScreen. Narrowest, but leaves the lying value on the wire for other consumers (and would need repeating).Direction 1 or 2 preferred — a sentinel that means "never" should not reach the client as a real-looking timestamp. Worth also grepping other screens for the same truthiness-on-a-nullable-date pattern.
Done-when
LibrariesScreen.test.tsx)🤖 Claiming as part of a batched small-issue sweep this session (parallel worktrees, one PR per item). Will post a close comment per the Task Completion Protocol when its PR merges.
✅ Merged via PR #451 (squash, bundled with #447). Root cause: the API serializes
DateTime.MinValuefor a never-scanned library as a truthy ISO string (0001-01-01T00:00:00) whichformatDateTimeparses into a fabricated '12:00 AM'. Done: added a client-sidehasScanned()guard (null/unparseable/year<1900 → 'Never scanned'). Files:web/src/screens/LibrariesScreen.tsx(+ test). Deviation flagged: the issue mildly preferred normalizing at the API read boundary; I chose the pure-SPA fix to avoid a DB migration for a priority:low bug — noted in the PR body for a maintainer call. Pure-SPA leaf; review skipped per rubric (stated).Reopening to do this properly per maintainer preference: the client-side sentinel guard (PR #451) only fixes the SPA, but an API/MCP consumer of
LastScanstill gets the misleading0001-01-01T00:00:00. We want API/MCP parity with the UI. Approach: (1) dual-provider data migration nulling the historicalMinValuesentinel rows (introduced by the oldReset_*migrations) on Library + LibraryPath — fixes the data at rest for ALL consumers; (2) a defensive read-boundary coercion in GetAllMediaSourcesForApiHandler so the API is honest regardless of DB history/provider; (3) simplify the SPA back to a plain null check now that the API is honest. Claiming.✅ Proper fix merged via PR #459 (squash). Root cause: never-scanned libraries carry a
0001-01-01MinValueLastScan— from historicalReset_*migrations AND, ongoing, fromMediaSourceRepositorydisable-sync flows — whichGetAllMediaSourcesForApiHandlerprojected straight to/api/v1/media-sourcesas a fake timestamp. The first pass (#451) only masked it client-side in the SPA, so API/MCP clients still saw it. Done (API/MCP parity per maintainer request): (1) read-boundary coercionNormalizeLastScanin the API handler (the permanent, load-bearing guard); (2) dual-provider data migrationNullOutNeverScannedLastScancleaning historical sentinel rows; (3) removed the SPAhasScannedheuristic. Files: GetAllMediaSourcesForApiHandler.cs, 2 migrations, LibrariesScreen.tsx(+test), GetAllMediaSourcesForApiHandlerTests.cs, docs/decisions.md. Review: cold review MERGEABLE (dup decisions.md entry fixed). Live-E2E PASSED: sentinel library → lastScan null via real API; real timestamp preserved. Follow-up: #460 (optional — normalize MediaSourceRepository writes to null for clean-at-rest data). Docs: decisions.md entry added.