External-collections scan has no status/progress REST surface — SPA collections buttons are optimistic + timeout-bounded #271
Closed
opened 2026-07-11 19:25:08 +02:00 by timothy
·
3 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#271
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.
Backlog follow-up from #91b (SPA Libraries.razor parity, landed 2026-07-11).
LibrariesScreennow exposes External Collections quick/deep scan buttons wired toPOST /api/media-sources/{plex|jellyfin|emby}/{id}/scan-collections. Unlike library scans — which reconcile their optimistic "pending" flag againstGET /api/libraries/scan-status— collections scans have no equivalent status/progress endpoint. The legacy Blazor page observed collections locks purely via in-processIEntityLocker.Are{X}CollectionsLockedevents, which have no HTTP mirror.Consequence today:
useCollectionsScan(web/src/api/libraries.ts) disables a collections button optimistically on click and re-enables it after a fixedCOLLECTIONS_PENDING_TIMEOUT_MS(a 409 "already scanning" is benign and keeps it disabled until the timeout). So the button's "scanning" state is not authoritative — a long deep-collections scan re-enables the button before it finishes, and a fast scan holds the button disabled for the full timeout.Fix (proposed): add a collections scan-status/lock REST surface (e.g.
GET /api/media-sources/{family}/{id}/collections-scan-status, or fold collections locks into a unified/api/scan-status) so the SPA can reconcile collections pending against a live active set, matching the library-scan UX. Then drop the timeout fallback in favor of authoritative reconcile.Severity: Low — capability parity is met (the scan is genuinely queued/triggered); only the live progress affordance is approximate. Not a #91b gate.
Refs:
docs/decisions.md2026-07-11 "External Collections in the SPA…";docs/blazor-route-parity.md§5.Related nuance (surfaced by the PR #272 adversarial review): the backend collections lock is family-global, not per-source —
LockPlexCollections()/Are{X}CollectionsLocked()take no source id (PlexMediaSourcesController.cs:301,IEntityLocker). Blazor disabled all same-family collection rows while any one scanned. The SPAuseCollectionsScankeys optimistic pending by${family}:${sourceId}, so with two Plex sources, scanning A leaves B's button enabled; clicking B yields a benign 409 (correctly handled — B goes pending until the timeout). No correctness harm, just a visual divergence from Blazor's all-rows-disabled behavior.A proper collections status/lock surface (this issue) should expose the lock at whatever granularity the backend actually enforces (family-global today), letting the SPA disable the matching row set authoritatively instead of per-source-optimistically.
Claiming — session started 2026-07-12 (afternoon). Picking this up to unblock #91b (arc item 4): the external-collections scan needs an HTTP status/lock surface so
Libraries.razorcan be deleted and the SPA collections buttons can reconcile authoritatively instead of relying on the fixed timeout.Plan (following the library-scan-status precedent + api-conventions §3b): add a collections scan-status REST surface at the granularity the backend actually enforces (family-global collections lock, per the PR #272 review note), then drop the
COLLECTIONS_PENDING_TIMEOUT_MSfallback inuseCollectionsScanin favor of authoritative reconcile. Will confirm the exact endpoint shape after recon.Closed — PR #298 (FF-merged to
main4112413c; PR CI green, branch deleted)What was done. Added an authoritative HTTP status surface for external-collections scans and rewired the SPA to reconcile against it, retiring the fixed timeout.
GET /api/media-sources/collections-scan-status(MediaSourcesController→GetCollectionsScanStatushandler) returns oneCollectionsScanStatusResponseModel { family }per media-source family (plex/jellyfin/emby) whose collections lock is currently held, readingIEntityLocker.Are{X}CollectionsLocked(). Returns only active families (empty = nothing running) — the counterpart toGET /api/libraries/scan-status. Two deliberate shape differences dictated by the lock: family-global (no source id — an entry means every source of that family is scanning, matching Blazor's all-rows-disabled) and no percent (collections scans expose only a boolean lock).useCollectionsScannow polls the endpoint and reconciles optimistic pending against the active-family set, seeding on mount so an already-running scan disables the buttons immediately (the old timeout couldn't). Reuses the library hook'spruneGraceExpiredPendinggrace-tick helper (generalized over the key type) to absorb the click→observed-active lag / fast-scan race.COLLECTIONS_PENDING_TIMEOUT_MSremoved. A row shows Scanning when its family is active or has an in-grace pending key; a cold-review nit fix also disables all rows of a family on click (and drops the redundant sibling-source POST), matching Blazor.Root cause (of the gap, not a bug). Collections locks were family-global in-process
IEntityLockerstate with no HTTP mirror — a known limitation logged when the SPALibraries.razorport landed (decisions.md 2026-07-11), filed as this issue so the timeout wasn't mistaken for the end state. This closes that follow-up.Files changed.
ErsatzTV/Controllers/Api/MediaSourcesController.cs,ErsatzTV.Application/MediaSources/Queries/GetCollectionsScanStatus{,Handler}.cs,ErsatzTV.Core/Api/MediaSources/CollectionsScanStatusResponseModel.cs,web/src/api/libraries.ts,web/src/screens/LibrariesScreen.tsx, tests (handler ×3, controller ×2, SPA api-fn + hook reconcile incl. deferred-POST optimistic-window + sibling-guard), regeneratedv1.json/v1.d.ts/endpoint-index.md, docs (api-conventions.md§3b,blazor-route-parity.md§5,decisions.md2026-07-12).Verification. Full local pass: ErsatzTV.Tests 1477, Core.Tests 507, Architecture.Tests 5, web 697. Live smoke: 401 without key, 200 +
[]with key (matches/api/libraries/scan-status). Cold adversarial fork review (MERGEABLE-WITH-NITS → both nits fixed) + fix-commit re-review (MERGEABLE). PR CI green (Build & test, EF migration integrity, Docs reminder).Deferred / follow-up. None. The endpoint reports lock state regardless of who holds it (API scan, scheduled sync, another tab), so it's more correct than the click-scoped timeout it replaces.
Impact. Removes the last F9 API gap that gated deleting
Libraries.razor→ unblocks #91b (arc item 4).