diff --git a/server/reflector/worker/process.py b/server/reflector/worker/process.py
index a9d31c74..432a701c 100644
--- a/server/reflector/worker/process.py
+++ b/server/reflector/worker/process.py
@@ -869,29 +869,30 @@ async def process_meetings():
elif has_had_sessions:
should_deactivate = True
logger_.info("Meeting ended - all participants left")
- elif current_time > end_date:
- should_deactivate = True
- logger_.info(
- "Meeting deactivated - scheduled time ended with no participants",
- )
- elif meeting.platform == "livekit" and not has_had_sessions:
- # LiveKit rooms are destroyed after empty_timeout. Once gone,
- # list_participants returns [] — indistinguishable from "never used".
- # Check if meeting was created >10 min ago; if so, assume room is gone.
+ elif not has_had_sessions:
+ # No sessions recorded — either no one joined, or webhooks
+ # didn't arrive (e.g. local dev without tunnel).
meeting_start = meeting.start_date
if meeting_start.tzinfo is None:
meeting_start = meeting_start.replace(tzinfo=timezone.utc)
age_minutes = (current_time - meeting_start).total_seconds() / 60
- if age_minutes > 10:
+ is_scheduled = bool(meeting.calendar_event_id)
+
+ if is_scheduled and current_time > end_date:
+ # Scheduled meeting past its end time with no participants
should_deactivate = True
logger_.info(
- "LiveKit meeting deactivated - room likely destroyed (no sessions after 10 min)",
+ "Meeting deactivated - scheduled time ended with no participants",
+ )
+ elif not is_scheduled and age_minutes > 30:
+ # On-the-fly meeting with no sessions after 30 min
+ should_deactivate = True
+ logger_.info(
+ "Meeting deactivated - no sessions after 30 min",
age_minutes=round(age_minutes, 1),
)
else:
- logger_.debug("LiveKit meeting still young, keep it")
- else:
- logger_.debug("Meeting not yet started, keep it")
+ logger_.debug("Meeting not yet started, keep it")
if should_deactivate:
await meetings_controller.update_meeting(
diff --git a/www/app/(app)/rooms/_components/RoomTable.tsx b/www/app/(app)/rooms/_components/RoomTable.tsx
index 4b31d6b3..e14281b8 100644
--- a/www/app/(app)/rooms/_components/RoomTable.tsx
+++ b/www/app/(app)/rooms/_components/RoomTable.tsx
@@ -10,14 +10,17 @@ import {
Badge,
VStack,
Icon,
+ Tooltip,
} from "@chakra-ui/react";
import { LuLink, LuRefreshCw } from "react-icons/lu";
+import { FaStop } from "react-icons/fa";
import { FaCalendarAlt } from "react-icons/fa";
import type { components } from "../../../reflector-api";
import {
useRoomActiveMeetings,
useRoomUpcomingMeetings,
useRoomIcsSync,
+ useMeetingDeactivate,
} from "../../../lib/apiHooks";
type Room = components["schemas"]["Room"];
@@ -107,6 +110,7 @@ const getZulipDisplay = (
function MeetingStatus({ roomName }: { roomName: string }) {
const activeMeetingsQuery = useRoomActiveMeetings(roomName);
const upcomingMeetingsQuery = useRoomUpcomingMeetings(roomName);
+ const deactivateMutation = useMeetingDeactivate();
const activeMeetings = activeMeetingsQuery.data || [];
const upcomingMeetings = upcomingMeetingsQuery.data || [];
@@ -121,14 +125,46 @@ function MeetingStatus({ roomName }: { roomName: string }) {
meeting.calendar_metadata?.["title"] || "Active Meeting",
);
return (
-
-
- {title}
-
-
- {meeting.num_clients} participants
-
-
+
+
+
+ {title}
+
+
+ {meeting.num_clients} participants
+
+
+ {activeMeetings.length === 1 && (meeting.num_clients ?? 0) < 2 && (
+
+
+
+ deactivateMutation.mutate({
+ params: { path: { meeting_id: meeting.id } },
+ })
+ }
+ disabled={deactivateMutation.isPending}
+ >
+ {deactivateMutation.isPending ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ End this meeting and stop any active recordings
+
+
+
+ )}
+
);
}