Skip to content

fix(multiuser): fix cross-user queue badge, progress bar, and gallery hijacking#9314

Open
lstein wants to merge 9 commits into
mainfrom
fix-multiuser-badge-count
Open

fix(multiuser): fix cross-user queue badge, progress bar, and gallery hijacking#9314
lstein wants to merge 9 commits into
mainfrom
fix-multiuser-badge-count

Conversation

@lstein

@lstein lstein commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

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 be 2/4); User B's shows 2/4 (correct).

Root cause: queue_item_status_changed and batch_enqueued events are emitted only to the owning user's socket room (privacy — they carry unsanitized user_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_progress count freezes at a non-zero value. ProgressBar.tsx reads that frozen in_progress > 0 (with lastProgressEvent null) and stays isIndeterminate forever.

Fix for 1 & 2

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 queue_id — no user_id, batch_id, session_id, or counts — so every subscriber simply refetches GET /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 only SessionQueueStatus.

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 admin socket room and receive invocation_complete events for every user. addImagesToGallery ran 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 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. In single-user mode there is no authenticated user, so the original behavior is preserved.

  • invokeai/frontend/web/src/services/events/onInvocationComplete.tsx

Testing

  • Manually verified with two users (2 + 2 jobs): both badges settle on 2/4, count down together, and clear with no lingering spinner.
  • Manually verified admin no longer has their board switched when another user generates an image.
  • tsc --noEmit, eslint --max-warnings=0, prettier --check all pass.
  • ruff check and ruff format --check pass.

🤖 Generated with Claude Code

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>
@github-actions github-actions Bot added api python PRs that change python files frontend PRs that change frontend files labels Jun 29, 2026
@lstein lstein added the 6.14.x label Jun 29, 2026
@lstein lstein moved this to 6.14.x Theme: USER EXPERIENCE in Invoke - Community Roadmap Jun 29, 2026
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>
@lstein lstein changed the title fix(multiuser): keep queue badge & progress bar live across users fix(multiuser): fix cross-user queue badge, progress bar, and gallery hijacking Jun 29, 2026
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>
@github-actions github-actions Bot added the python-tests PRs that change python tests label Jun 30, 2026

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one thing so far.

  • invokeai/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx:262

    The new cross-user guard only wraps addImagesToGallery() at lines 48-61, but admin clients still process another user's invocation_complete before that guard. The backend sends every invocation event to the admin room in invokeai/invokeai/app/api/sockets.py:304-316, and InvocationCompleteEvent carries the owner user_id. When an admin receives another user's completion event, the handler still reads and upserts $nodeExecutionStates at lines 265-280, clears canvas workflow integration processing at lines 283-284, and clears $lastProgressEvent at 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, and getUpdatedNodeExecutionStateOnInvocationComplete() creates state even when one did not exist.

    To expose this issue, add a frontend vitest for buildOnInvocationComplete that sets auth.user.user_id to an admin user, seeds $nodeExecutionStates or canvas integration processing state, invokes the handler with data.user_id for a different user, and asserts that node execution state, canvas processing state, gallery state, and $lastProgressEvent are not mutated.

lstein and others added 2 commits July 12, 2026 11:17
# Conflicts:
#	invokeai/app/api/sockets.py
#	tests/app/routers/test_multiuser_authorization.py
…hoto review)

Admins receive every user's invocation events via the "admin" socket room.
The cross-user guard only wrapped addImagesToGallery(), so another user's
invocation_complete still upserted this client's $nodeExecutionStates,
cleared the canvas workflow integration spinner, and reset $lastProgressEvent.

Hoist the guard to the top of the returned handler so a foreign event is
skipped before any side effect runs. Single-user mode (no authenticated user)
still processes every event. Add a vitest covering the admin-receives-foreign,
own-event, and single-user paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lstein

lstein commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @JPPhoto — good catch, that was a real hole. The cross-user guard only wrapped addImagesToGallery(), so an admin receiving another user's invocation_complete via the admin room still ran the node-execution-state upsert, clearCanvasWorkflowIntegrationProcessing(), and the $lastProgressEvent.set(null) at the end of the handler.

Fixed in 2f7547a:

  • Hoisted the guard to the top of the returned handler in onInvocationComplete.tsx, so a foreign event returns before any side effect (node execution state, canvas integration processing, gallery, or $lastProgressEvent). Removed the now-redundant inner guard in addImagesToGallery().
  • Single-user mode is unchanged: when there's no authenticated user (selectCurrentUser is null) every event is the local user's own and falls through as before.
  • Added onInvocationComplete.test.ts as you suggested: an admin receiving another user's event asserts no $nodeExecutionStates upsert, no dispatch, and $lastProgressEvent untouched — plus own-event and single-user positive controls.

Also merged latest main, which had landed a superseding version of the backend badge fix (sanitized companion events via skip_sid instead of the queue_counts_changed nudge). I deferred sockets.py and the routing tests entirely to main's implementation, so this PR is now just the frontend cross-user isolation work.

@JPPhoto JPPhoto closed this Jul 12, 2026
@JPPhoto JPPhoto reopened this Jul 12, 2026
@JPPhoto

JPPhoto commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

@lstein I found one issue:

  • invokeai/frontend/web/src/services/events/setEventListeners.tsx:180: Cross-user filtering remains incomplete and occurs too late. Admin sockets receive every user's invocation and queue-status events from invokeai/app/api/sockets.py:320-343. The ownership check at invokeai/frontend/web/src/services/events/onInvocationComplete.tsx:261 runs only after workflowExecutionCoordinator.onInvocationComplete() has recorded the foreign completion at invokeai/frontend/web/src/services/events/workflowExecutionCoordinator.ts:179-195. Foreign invocation_started, invocation_progress, and invocation_error events remain unfiltered at lines 180-210, allowing them to create or overwrite node execution state and replace $lastProgressEvent. Foreign full queue_item_status_changed events also reach admins without the redacted sentinel and can replace activeWorkflowQueueItemId, reset the admin's node states, or reconcile another user's queue item at workflowExecutionCoordinator.ts:198-233. Another user's workflow can therefore still corrupt an admin's local execution and progress state despite the new completion-handler guard. The added test calls buildOnInvocationComplete() directly, bypassing the coordinator and socket listeners where these mutations occur. To expose this issue, add a test that emits another user's started, progress, error, complete, and full queue-status events through setEventListeners for an authenticated admin and asserts that workflow coordinator state, node execution states, canvas processing state, completed-invocation tracking, and $lastProgressEvent remain unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14.x api frontend PRs that change frontend files python PRs that change python files python-tests PRs that change python tests

Projects

Status: 6.14.x Theme: USER EXPERIENCE

Development

Successfully merging this pull request may close these issues.

2 participants