fix(multiuser): fix cross-user queue badge, progress bar, and gallery hijacking#9314
fix(multiuser): fix cross-user queue badge, progress bar, and gallery hijacking#9314lstein wants to merge 5 commits into
Conversation
In multiuser mode, queue item status changes and batch-enqueued events are
emitted only to the owning user's socket room (for privacy). The frontend badge
and progress bar only refetch the global queue status in response to those
events, so a non-admin only learns about queue changes from their *own* jobs.
Once their jobs finish they get no further signal: the global total (the "/Y"
in "X/Y") and the in_progress count freeze, leaving the badge wrong and the
progress bar animating indefinitely.
Broadcast a content-free `queue_counts_changed` event to the whole queue room
whenever counts change (a status transition or an enqueue). It carries only the
queue_id — no user_id, batch_id, session_id, or counts — so every subscriber can
refetch GET /queue/{id}/status, which already redacts per-user data. Nothing
private is leaked, and it never fires on high-frequency invocation progress
events.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In multiuser mode, admins are subscribed to the "admin" socket room and so receive invocation_complete events for every user. addImagesToGallery ran for all of them, so any image generated by another user would insert into the admin's gallery and auto-switch the admin's selected board to that user's board, displaying their image. This did not happen between two unprivileged users because they never receive each other's events. Gate addImagesToGallery on ownership: when an authenticated user is present and the event's user_id differs from theirs, skip the gallery insertion and auto-switch entirely. In single-user mode there is no authenticated user, so the original behavior is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The badge-livening change emits a redacted queue_counts_changed nudge to the whole queue room on status changes and batch enqueues. Update the two routing tests to check per-event: the private event still must not reach the queue_id room, but the data-free counts nudge legitimately does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JPPhoto
left a comment
There was a problem hiding this comment.
I found one thing so far.
-
invokeai/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx:262The new cross-user guard only wraps
addImagesToGallery()at lines 48-61, but admin clients still process another user'sinvocation_completebefore that guard. The backend sends every invocation event to theadminroom ininvokeai/invokeai/app/api/sockets.py:304-316, andInvocationCompleteEventcarries the owneruser_id. When an admin receives another user's completion event, the handler still reads and upserts$nodeExecutionStatesat lines 265-280, clears canvas workflow integration processing at lines 283-284, and clears$lastProgressEventat line 289. This can mark the admin's local workflow nodes complete, append another user's outputs to matching node IDs, or stop the admin's canvas workflow integration spinner even though the event was not theirs. Common node IDs make this realistic, andgetUpdatedNodeExecutionStateOnInvocationComplete()creates state even when one did not exist.To expose this issue, add a frontend
vitestforbuildOnInvocationCompletethat setsauth.user.user_idto an admin user, seeds$nodeExecutionStatesor canvas integration processing state, invokes the handler withdata.user_idfor a different user, and asserts that node execution state, canvas processing state, gallery state, and$lastProgressEventare not mutated.
This PR fixes three related multiuser-mode issues that all stem from how socket events are (correctly) isolated per user, while frontend state was assuming it sees every relevant event.
1. Queue badge shows the wrong global total
Reproduction: User A queues 2 jobs; User B immediately queues 2 jobs. User A's badge shows
2/2(wrong — should be2/4); User B's shows2/4(correct).Root cause:
queue_item_status_changedandbatch_enqueuedevents are emitted only to the owning user's socket room (privacy — they carry unsanitizeduser_id/batch_id/session_id). The frontend badge only refetches the global queue status in response to those events, so a non-admin only learns about queue changes from their own jobs. Whoever queued first captured the global total before the other user's jobs existed, then stopped refetching.2. Progress bar animates indefinitely
Same root cause: once a user's own jobs finish they get no further signal, so the global
in_progresscount freezes at a non-zero value.ProgressBar.tsxreads that frozenin_progress > 0(withlastProgressEventnull) and staysisIndeterminateforever.Fix for 1 & 2
Broadcast a content-free
queue_counts_changedevent to the whole queue room whenever counts change (a status transition or an enqueue). It carries onlyqueue_id— nouser_id,batch_id,session_id, or counts — so every subscriber simply refetchesGET /queue/{id}/status, which already redacts per-user data. Nothing private is leaked. It never fires on high-frequency invocation-progress events.invokeai/app/api/sockets.py—_broadcast_queue_counts_changed()helper, called from the status-changed and batch-enqueued branches of_handle_queue_event.invokeai/frontend/web/src/services/events/types.ts— type the new event.invokeai/frontend/web/src/services/events/setEventListeners.tsx— handler invalidating onlySessionQueueStatus.3. Another user's image hijacks an admin's gallery
Reproduction: Admin (User A) and unprivileged User B logged in on separate browsers. Any image B renders switches A's selected board to B's board and displays B's image. Does not happen between two unprivileged users.
Root cause: Admins are in the
adminsocket room and receive invocation_complete events for every user.addImagesToGalleryran for all of them, inserting other users' images into the admin's gallery and auto-switching the admin's selected board.Fix for 3
Gate
addImagesToGalleryon ownership: when an authenticated user is present and the event'suser_iddiffers from theirs, skip the gallery insertion and auto-switch. In single-user mode there is no authenticated user, so the original behavior is preserved.invokeai/frontend/web/src/services/events/onInvocationComplete.tsxTesting
2/4, count down together, and clear with no lingering spinner.tsc --noEmit,eslint --max-warnings=0,prettier --checkall pass.ruff checkandruff format --checkpass.🤖 Generated with Claude Code