From 891d4b12bd1479fe40b351a8cb907fd734495530 Mon Sep 17 00:00:00 2001 From: Kartikeya <176560021+karti-ai@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:22:07 -0700 Subject: [PATCH] feat(livekit): close the room when a pod is deleted DELETE /api/pods/:id now also calls closeRoom (RoomServiceClient.deleteRoom) to end the live LiveKit room and disconnect anyone connected. PodView listens for RoomEvent.Disconnected and returns the user to the pod list, so a deleted pod cleanly kicks everyone out instead of stranding them in a dead room. Co-Authored-By: Claude Opus 4.8 --- backend/src/livekit/rooms.ts | 13 +++++++++++++ backend/src/server.ts | 3 ++- frontend/src/components/PodView.tsx | 10 ++++++++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/backend/src/livekit/rooms.ts b/backend/src/livekit/rooms.ts index 52fa10e..6ff28fb 100644 --- a/backend/src/livekit/rooms.ts +++ b/backend/src/livekit/rooms.ts @@ -14,6 +14,19 @@ function svc(): RoomServiceClient { return client; } +/** + * Close a pod's LiveKit room, disconnecting everyone. Best-effort: a missing/ + * already-empty room is fine. Called when a pod is deleted. + */ +export async function closeRoom(podId: string): Promise { + if (!isConfigured()) return; + try { + await svc().deleteRoom(podId); + } catch (e) { + console.warn(`[livekit] closeRoom ${podId} failed: ${(e as Error).message}`); + } +} + /** * Who is currently connected in each pod's LiveKit room, as display names, * keyed by pod id (= room name). Empty rooms are omitted. Returns {} if diff --git a/backend/src/server.ts b/backend/src/server.ts index 60d77de..4cfab12 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -17,7 +17,7 @@ import { removeMember, seedDefaultPods, } from './pods/store.js'; -import { getPresence } from './livekit/rooms.js'; +import { getPresence, closeRoom } from './livekit/rooms.js'; import type { InterventionOutcome } from '@podman/shared'; const app = express(); @@ -121,6 +121,7 @@ app.patch('/api/pods/:id', async (req, res) => { app.delete('/api/pods/:id', async (req, res) => { const ok = await deletePod(req.params.id); if (!ok) return res.status(404).json({ error: 'pod not found' }); + await closeRoom(req.params.id); // end the live LiveKit room too (kicks anyone connected) res.json({ ok: true }); }); diff --git a/frontend/src/components/PodView.tsx b/frontend/src/components/PodView.tsx index a96372c..586f074 100644 --- a/frontend/src/components/PodView.tsx +++ b/frontend/src/components/PodView.tsx @@ -50,6 +50,8 @@ export function PodView({ const audioRef = useRef(null); const beatRef = useRef(null); const screenTrackRef = useRef(null); + const onLeaveRef = useRef(onLeave); + onLeaveRef.current = onLeave; // Subscribe to live room state: participants, active speakers, remote audio. useEffect(() => { @@ -63,13 +65,16 @@ export function PodView({ } }; const onAudioGone = (track: RemoteTrack) => track.detach().forEach((el) => el.remove()); + // Room closed out from under us (e.g. the pod was deleted) → back to the list. + const onDisconnected = () => onLeaveRef.current(); room .on(RoomEvent.ParticipantConnected, refresh) .on(RoomEvent.ParticipantDisconnected, refresh) .on(RoomEvent.ActiveSpeakersChanged, refresh) .on(RoomEvent.TrackSubscribed, onAudio) - .on(RoomEvent.TrackUnsubscribed, onAudioGone); + .on(RoomEvent.TrackUnsubscribed, onAudioGone) + .on(RoomEvent.Disconnected, onDisconnected); return () => { room @@ -77,7 +82,8 @@ export function PodView({ .off(RoomEvent.ParticipantDisconnected, refresh) .off(RoomEvent.ActiveSpeakersChanged, refresh) .off(RoomEvent.TrackSubscribed, onAudio) - .off(RoomEvent.TrackUnsubscribed, onAudioGone); + .off(RoomEvent.TrackUnsubscribed, onAudioGone) + .off(RoomEvent.Disconnected, onDisconnected); }; }, [room, me]);