AddItemsDialog: a failed add is silently swallowed when the dialog is closed mid-request #830
Open
opened 2026-08-25 23:24:21 +02:00 by timothy
·
1 comment
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
fix/830-add-items-error-surface
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
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.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#830
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.
Surfaced by the independent review of #740 (PR for that issue). Pre-existing and NOT fixed by #740 — that issue added the is-mounted guard, which correctly stops a setState on an unmounted tree; it deliberately does not change the UX, and should not have.
The defect
AddItemsDialog.submit(web/src/screens/CollectionsScreen.tsx) POSTs to/api/v1/collections/{id}/itemsand reports failure by rendering an error banner inside the dialog. The dialog is closable mid-request: the footer Cancel button isdisabled={adding}, butDialogcloses on Escape, on a backdrop click and via the header close button, and none of those consultadding(web/src/components/overlay.tsx). Closing unmounts the instance — the caller renders it underkey={`add-${pickerOpen}`}.So: select 12 items → Add → press Escape → the request fails. There is no longer a dialog to render the error into, nothing else surfaces it, and the collection list reloads unchanged. The user believes 12 items were added and nothing was.
The write itself is not lost or partial — it simply never happened, and the UI does not say so.
Why it was left out of #740
#740's scope is the two async guards §3b mandates. Guarding
setErrorthere makes the drop deliberate rather than accidental, which is correct — the fix for the missing feedback is a different mechanism (one that outlives the dialog), not a guard. Bundling a notification surface into that PR would have widened it well past its issue.What to decide
The error has to survive the dialog. Options, in rough order of size:
adding, the way the Cancel button already is. Smallest diff, but it traps the user behind a request with no cancel path, which is whyDialogdoesn't do this by default.CollectionsScreenso the banner renders where the user actually is. Consistent with the collection screen already owningmessageFromCollectionErroroutput.Option 2 looks right, but the pattern should be checked against how other screens report a write failure whose originating surface can close, since this is unlikely to be the only instance — sweep for it rather than fixing this one site (
fix-the-boundary-not-the-site).Done-when
Resolution — option 2, and option 1 was tested and rejected. Gating dismissal on
addingtraps the user behind an in-flight request with no cancel path, and would not cancel the write anyway. There is no toast host in this SPA (option 3), so none was added for one screen. The mechanism isuseDismissSafeError(web/src/hooks.ts): inline while mounted, diverted to a caller-supplied reporter once dismissed. PR #878, recordspa.dismissible-write-failure-reporting,docs/spa-conventions.md§3c.Box 2 — the sweep ran and its answer was "reuse it at ONE site, record the rest". All 68
Dialog/ConfirmDialog/SlideOvercall sites were triaged into three shapes: (A1) the surface owns the error state and really unmounts; (A2) the state survives but its render site is gated by the same condition dismissal clears — the majority, mostly delete-confirm flows; (B) an already-correct screen-level banner. Only A1 atAddItemsDialogis fixed here. The fourmedia/addTo/dialogs are the same shape and were fixed, then withdrawn after review measured the extension unsafe twice — see #877, which carries every measurement. A2 is #877 as well.Box 3 — witnessed, and the split is pinned in both directions. Disarming
reportRef.current(message)reddens the integration test onUnable to find an element with the text: Request failed with status 500. Separately: deleting theif (mountedRef.current)aroundonClose()reddens only the reopen test; deleting theonClose()call entirely reddens only the ordinary-close assertion. The two are orthogonal — neither masks the other.Box 4 — seven rounds, and the review changed the shipped result four times. It measured my central prose claim FALSE, caught a regression my own fix introduced, found three sentences that outlived the code they described, one coverage claim this record's own
mechanics:field contradicted, and a convention that contradicted its own named exemplar. Recorded because the last two are the same shape: after a retraction, the retracted WORDING has to be swept, not just the code. Caveat stated rather than implied: the cross-family (Codex) pass exhausted its quota mid-read and produced no findings, so this was a same-family cold, worktree-isolated reviewer.Related
#740 (the guards, which made this drop deliberate), #685 (the AddItemsDialog work that preceded it), #877 (the withdrawn A1 half + all of A2),
docs/spa-conventions.md§3b.Claiming. Claude Code / Opus 5, orchestrator tier. Worktree
~/ersatztv-wt/830-add-items-error-surface, branchfix/830-add-items-error-surfaceoff a freshorigin/main(8aeacd534).Pre-claim checks (
process.parallel-session-claim), all four clear as of 2026-08-29 19:00 CEST: no open PR body references #830 (the five open PRs are all Renovate);git ls-remote --heads originhas no branch naming 830 oradditems; the issue had zero comments, so no claim predates the label;git fetch origin mainis current.Context for whoever reads this next: three other sessions are live on this repo right now — #823+#824 (worktree
823-824-701-deferrals), #820 (main-3), #812 (main-2). Those four are the entirein-progressset, so #830 does not overlap any of them.Scope note on done-when box 2 (the sweep): the issue asks that the chosen mechanism be checked against other write paths whose originating surface can close mid-request, per
fix-the-boundary-not-the-site. I am treating that sweep as in-scope for this PR rather than a follow-up, and will derive the population of such surfaces from source rather than listing them by hand — an issue's own file list is not the population.