From 26239f05a34af07ebba764d669343c32e40e63bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20Garc=C3=ADa?= Date: Tue, 7 Apr 2026 17:18:11 -0500 Subject: [PATCH] fix: deactivate meeting button and better deactivation heuristics (#950) --- server/reflector/worker/process.py | 29 ++++++----- www/app/(app)/rooms/_components/RoomTable.tsx | 52 ++++++++++++++++--- 2 files changed, 59 insertions(+), 22 deletions(-) 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 + + + + )} + ); }