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,