From 2999ca95a07a3dc5b643f49fbf1eccf20372513a Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:48:44 +0100 Subject: [PATCH] fix: prevent stale voice sidebar after multi-session identity collision When a second browser joins voice, LiveKit disconnects the first browser via identity collision. The first browser's leaveVoice() was optimistically removing the user from voiceUsers, but the user is still in voice from the other session. Add handleForceDisconnect() that clears local connection state without touching voiceUsers, keeping the sidebar accurate. Also fix deploy.sh to clean up stale renamed containers from failed recreates. --- deploy.sh | 3 ++- packages/web/src/hooks/useLiveKit.ts | 2 +- packages/web/src/stores/voiceStore.ts | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/deploy.sh b/deploy.sh index e4179d3b..c35e0954 100755 --- a/deploy.sh +++ b/deploy.sh @@ -65,7 +65,8 @@ deploy() { # Rebuild echo " [3/3] Building and restarting..." - ssh "$PI_USER@$host" "cd $path && docker compose up -d --build" + # Clean up stale renamed containers left by failed recreates (e.g. "d420a6c00439_backspace") + ssh "$PI_USER@$host" "cd $path && docker rm -f \$(docker ps -aq --filter 'name=_backspace' 2>/dev/null) 2>/dev/null; docker compose up -d --build" echo "" echo " Done: $name" diff --git a/packages/web/src/hooks/useLiveKit.ts b/packages/web/src/hooks/useLiveKit.ts index edd0de9f..8a87421c 100644 --- a/packages/web/src/hooks/useLiveKit.ts +++ b/packages/web/src/hooks/useLiveKit.ts @@ -491,7 +491,7 @@ export function useLiveKit() { // → clear voice intent so AppLayout doesn't auto-retry into an infinite loop. // Client-initiated disconnects already clear this via leaveVoice() / VoiceControlBar. if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) { - useVoiceStore.getState().leaveVoice(); + useVoiceStore.getState().handleForceDisconnect(); } }); diff --git a/packages/web/src/stores/voiceStore.ts b/packages/web/src/stores/voiceStore.ts index 468d257f..ac6c2a05 100644 --- a/packages/web/src/stores/voiceStore.ts +++ b/packages/web/src/stores/voiceStore.ts @@ -97,6 +97,7 @@ interface VoiceState { clearAllVoiceUsers: () => void; clearVoiceUsersForOrigin: (origin: string) => void; leaveVoice: () => void; + handleForceDisconnect: () => void; reset: () => void; } @@ -370,6 +371,29 @@ export const useVoiceStore = create()( }); }, + // Force disconnect: clear local connection state but do NOT touch voiceUsers. + // Used for involuntary disconnects (identity collision, server shutdown, etc.) + // where the server is the authority on who's actually in voice. + handleForceDisconnect: () => { + set({ + currentVoiceChannelId: null, + isCameraOn: false, + isScreenSharing: false, + participants: [], + speakingParticipantIds: new Set(), + connectionError: null, + isLiveKitConnected: false, + connectionQuality: 'unknown', + focusedParticipantId: null, + activeDmCall: null, + outgoingCall: null, + deafenedUserIds: new Set(), + streamVolumes: new Map(), + streamMutes: new Map(), + watchingStreams: new Set(), + }); + }, + reset: () => set({ voiceUsers: new Map(), currentVoiceChannelId: null,