From 40dd01c59338daabf4fbbb8ff8144c246c935ee1 Mon Sep 17 00:00:00 2001 From: alma Date: Wed, 21 Jan 2026 15:18:40 +0100 Subject: [PATCH] missions finition --- components/layout/layout-wrapper.tsx | 8 +- hooks/use-rocketchat-calls.ts | 144 ++++++++++++------ lib/services/rocketchat-call-listener.ts | 139 +++++++++++++++-- scripts/cleanup-orphaned-mission-calendars.sh | 127 +++++++++++++++ .../cleanup-orphaned-mission-calendars.sql | 60 ++++++++ 5 files changed, 415 insertions(+), 63 deletions(-) create mode 100755 scripts/cleanup-orphaned-mission-calendars.sh create mode 100644 scripts/cleanup-orphaned-mission-calendars.sql diff --git a/components/layout/layout-wrapper.tsx b/components/layout/layout-wrapper.tsx index b8213ee..25e83f2 100644 --- a/components/layout/layout-wrapper.tsx +++ b/components/layout/layout-wrapper.tsx @@ -42,7 +42,7 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou }, [pathname]); // Listen for incoming RocketChat calls via WebSocket - const { incomingCall, setIncomingCall } = useRocketChatCalls(); + const { incomingCall, setIncomingCall, clearIncomingCall } = useRocketChatCalls(); // Listen for email notifications const { emailNotification, setEmailNotification } = useEmailNotifications(); @@ -268,15 +268,15 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou call={incomingCall} onDismiss={() => { console.log('[LayoutWrapper] Call dismissed'); - setIncomingCall(null); + clearIncomingCall(); }} onAccept={(roomId) => { console.log('[LayoutWrapper] Call accepted, navigating to room:', roomId); - setIncomingCall(null); + clearIncomingCall(); }} onReject={() => { console.log('[LayoutWrapper] Call rejected'); - setIncomingCall(null); + clearIncomingCall(); // TODO: Send reject signal to RocketChat if needed }} /> diff --git a/hooks/use-rocketchat-calls.ts b/hooks/use-rocketchat-calls.ts index 741fc89..7645bd9 100644 --- a/hooks/use-rocketchat-calls.ts +++ b/hooks/use-rocketchat-calls.ts @@ -15,6 +15,7 @@ export function useRocketChatCalls() { const unsubscribeRef = useRef<(() => void) | null>(null); const initializedRef = useRef(false); const [incomingCall, setIncomingCall] = useState(null); + const currentCallRoomIdRef = useRef(null); /** * Get RocketChat credentials for the current user @@ -106,59 +107,96 @@ export function useRocketChatCalls() { // Subscribe to call events const unsubscribe = listener.onCall((callEvent: CallEvent) => { - console.log('[useRocketChatCalls] πŸŽ‰ INCOMING CALL DETECTED!', { - from: callEvent.from.username, - roomId: callEvent.roomId, - roomName: callEvent.roomName, - }); + // Handle call ended events (when caller hangs up) + if (callEvent.type === 'call-ended') { + console.log('[useRocketChatCalls] πŸ“ž CALL ENDED - Checking if notification should be removed', { + endedRoomId: callEvent.roomId, + currentCallRoomId: currentCallRoomIdRef.current, + from: callEvent.from.username, + }); - logger.info('[useRocketChatCalls] Incoming call detected', { - from: callEvent.from.username, - roomId: callEvent.roomId, - }); + logger.info('[useRocketChatCalls] Call ended', { + roomId: callEvent.roomId, + currentCallRoomId: currentCallRoomIdRef.current, + from: callEvent.from.username, + }); - // Show incoming call notification UI (Outlook-style rectangle) - setIncomingCall({ - from: { - userId: callEvent.from.userId, - username: callEvent.from.username, - name: callEvent.from.name || callEvent.from.username, - }, - roomId: callEvent.roomId, - roomName: callEvent.roomName || callEvent.roomId, - timestamp: callEvent.timestamp, - }); + // Only remove notification if it matches the current call's roomId + // This prevents removing notifications from other calls + if (currentCallRoomIdRef.current === callEvent.roomId || !currentCallRoomIdRef.current) { + console.log('[useRocketChatCalls] βœ… Removing notification for ended call', { + roomId: callEvent.roomId, + }); + setIncomingCall(null); + currentCallRoomIdRef.current = null; + } else { + console.log('[useRocketChatCalls] ⏭️ Ignoring call ended event - different room', { + endedRoomId: callEvent.roomId, + currentCallRoomId: currentCallRoomIdRef.current, + }); + } + return; + } - console.log('[useRocketChatCalls] πŸ“ž Incoming call notification UI set', { - from: callEvent.from.username, - roomId: callEvent.roomId, - }); + // Handle incoming call events + if (callEvent.type === 'call-incoming') { + console.log('[useRocketChatCalls] πŸŽ‰ INCOMING CALL DETECTED!', { + from: callEvent.from.username, + roomId: callEvent.roomId, + roomName: callEvent.roomName, + }); - // Trigger notification badge - // For calls, we want to increment the existing count, not replace it - // So we fetch current count first, then increment - triggerNotification({ - source: 'rocketchat', - count: 1, // This will be added to existing count in the registry - items: [ - { - id: `call-${callEvent.roomId}-${Date.now()}`, - title: `πŸ“ž Appel entrant de ${callEvent.from.name || callEvent.from.username}`, - message: `Appel vidΓ©o/audio dans ${callEvent.roomName || 'chat'}`, - link: `/parole?room=${callEvent.roomId}`, - timestamp: callEvent.timestamp, - metadata: { - type: 'call', - from: callEvent.from, - roomId: callEvent.roomId, - }, + logger.info('[useRocketChatCalls] Incoming call detected', { + from: callEvent.from.username, + roomId: callEvent.roomId, + }); + + // Show incoming call notification UI (Outlook-style rectangle) + setIncomingCall({ + from: { + userId: callEvent.from.userId, + username: callEvent.from.username, + name: callEvent.from.name || callEvent.from.username, }, - ], - }).then(() => { - console.log('[useRocketChatCalls] βœ… Notification triggered successfully'); - }).catch((error) => { - console.error('[useRocketChatCalls] ❌ Error triggering notification', error); - }); + roomId: callEvent.roomId, + roomName: callEvent.roomName || callEvent.roomId, + timestamp: callEvent.timestamp, + }); + + // Track the current call's roomId so we can match it when the call ends + currentCallRoomIdRef.current = callEvent.roomId; + + console.log('[useRocketChatCalls] πŸ“ž Incoming call notification UI set', { + from: callEvent.from.username, + roomId: callEvent.roomId, + }); + + // Trigger notification badge + // For calls, we want to increment the existing count, not replace it + // So we fetch current count first, then increment + triggerNotification({ + source: 'rocketchat', + count: 1, // This will be added to existing count in the registry + items: [ + { + id: `call-${callEvent.roomId}-${Date.now()}`, + title: `πŸ“ž Appel entrant de ${callEvent.from.name || callEvent.from.username}`, + message: `Appel vidΓ©o/audio dans ${callEvent.roomName || 'chat'}`, + link: `/parole?room=${callEvent.roomId}`, + timestamp: callEvent.timestamp, + metadata: { + type: 'call', + from: callEvent.from, + roomId: callEvent.roomId, + }, + }, + ], + }).then(() => { + console.log('[useRocketChatCalls] βœ… Notification triggered successfully'); + }).catch((error) => { + console.error('[useRocketChatCalls] ❌ Error triggering notification', error); + }); + } }); unsubscribeRef.current = unsubscribe; @@ -196,8 +234,18 @@ export function useRocketChatCalls() { }; }, [session?.user?.id, initializeListener]); + /** + * Helper function to clear the incoming call notification + * This ensures we also clear the roomId reference + */ + const clearIncomingCall = useCallback(() => { + setIncomingCall(null); + currentCallRoomIdRef.current = null; + }, []); + return { incomingCall, setIncomingCall, + clearIncomingCall, }; } diff --git a/lib/services/rocketchat-call-listener.ts b/lib/services/rocketchat-call-listener.ts index d82a4a8..5805300 100644 --- a/lib/services/rocketchat-call-listener.ts +++ b/lib/services/rocketchat-call-listener.ts @@ -390,7 +390,34 @@ export class RocketChatCallListener { const messageType = payload?.message?.t; const isCallNotification = messageType === 'videoconf' || messageType === 'audio' || messageType === 'video'; - if (isCallNotification) { + // Check if this is a call ending notification + const notificationText = notification.text || payload?.text || notification.title || payload?.title || ''; + const isCallEndedNotification = + notificationText.toLowerCase().includes('call ended') || + notificationText.toLowerCase().includes('appel terminΓ©') || + notificationText.toLowerCase().includes('call cancelled') || + notificationText.toLowerCase().includes('appel annulΓ©') || + payload?.message?.action === 'end' || + payload?.message?.action === 'hangup' || + payload?.message?.action === 'cancel'; + + if (isCallEndedNotification) { + logger.info('[ROCKETCHAT_CALL_LISTENER] πŸ“ž CALL ENDED DETECTED in notification!', { + notificationText, + roomId: payload?.rid, + sender: payload?.sender, + }); + + // Handle as call ended event + this.handleCallEvent({ + type: 'call-ended', + action: 'end', + from: payload?.sender || {}, + roomId: payload?.rid, + roomName: payload?.name || notification.title, + message: payload?.message, + }); + } else if (isCallNotification) { logger.info('[ROCKETCHAT_CALL_LISTENER] βœ… VIDEO/AUDIO CALL DETECTED in notification!', { type: messageType, sender: payload.sender, @@ -416,26 +443,62 @@ export class RocketChatCallListener { } } - // Check if this is a webrtc event (only process if it looks like an incoming call) + // Check if this is a webrtc event (process both incoming calls and call endings) if (eventName?.includes('/webrtc')) { logger.debug('[ROCKETCHAT_CALL_LISTENER] βœ… This is a webrtc event!'); if (args.length > 0) { const webrtcData = args[0]; - // Only process if it's an incoming call (ringing, offer, etc.) + // Process incoming calls (ringing, offer, etc.) if (webrtcData.action === 'ringing' || webrtcData.type === 'call' || webrtcData.event === 'incoming') { this.handleCallEvent(webrtcData); } + // Process call endings (hangup, end, cancel, reject, etc.) + else if ( + webrtcData.action === 'hangup' || + webrtcData.action === 'end' || + webrtcData.action === 'cancel' || + webrtcData.action === 'reject' || + webrtcData.action === 'decline' || + webrtcData.type === 'call-ended' || + webrtcData.event === 'hangup' || + webrtcData.event === 'end' || + webrtcData.status === 'ended' || + webrtcData.status === 'cancelled' + ) { + logger.info('[ROCKETCHAT_CALL_LISTENER] πŸ“ž Call ended detected in webrtc event', { + action: webrtcData.action, + type: webrtcData.type, + event: webrtcData.event, + status: webrtcData.status, + roomId: webrtcData.roomId || webrtcData.rid + }); + this.handleCallEvent(webrtcData); + } } } // Also check for other possible call-related events (but be more specific) - // Only process if the event name explicitly indicates a call - if (eventName?.includes('/call') && (eventName?.includes('/incoming') || eventName?.includes('/ringing'))) { - logger.debug('[ROCKETCHAT_CALL_LISTENER] βœ… This might be a call event!'); - if (args.length > 0) { - this.handleCallEvent(args[0]); - } else { - this.handleCallEvent(message.fields); + // Process both incoming calls and call endings + if (eventName?.includes('/call')) { + const isIncomingCall = eventName?.includes('/incoming') || eventName?.includes('/ringing'); + const isCallEnded = + eventName?.includes('/end') || + eventName?.includes('/hangup') || + eventName?.includes('/cancel') || + eventName?.includes('/reject') || + eventName?.includes('/decline'); + + if (isIncomingCall || isCallEnded) { + logger.debug('[ROCKETCHAT_CALL_LISTENER] βœ… This might be a call event!', { + eventName, + isIncomingCall, + isCallEnded + }); + if (args.length > 0) { + this.handleCallEvent(args[0]); + } else { + this.handleCallEvent(message.fields); + } } } } @@ -510,6 +573,25 @@ export class RocketChatCallListener { allKeys: Object.keys(eventData), }); + // Check if this is a call ending event (hangup, end, cancel, reject, etc.) + const isCallEnded = + callType === 'call-ended' || + callType === 'hangup' || + callType === 'end' || + callType === 'cancel' || + callType === 'reject' || + callType === 'decline' || + eventData.action === 'hangup' || + eventData.action === 'end' || + eventData.action === 'cancel' || + eventData.action === 'reject' || + eventData.action === 'decline' || + eventData.status === 'ended' || + eventData.status === 'cancelled' || + eventData.status === 'rejected' || + eventData.event === 'hangup' || + eventData.event === 'end'; + // Check if this is an incoming call event // RocketChat sends calls via notifications with message.t === 'videoconf' or 'audio' const isIncomingCall = @@ -526,7 +608,42 @@ export class RocketChatCallListener { eventData.message?.t === 'video' || (eventData.type === 'call' && eventData.status === 'ringing'); - if (isIncomingCall && roomId) { + // Handle call ended events + if (isCallEnded && roomId) { + const callEvent: CallEvent = { + type: 'call-ended', + from: { + userId: from._id || from.userId || from.id || '', + username: from.username || from.name || 'Unknown', + name: from.name || from.username || 'Unknown', + }, + roomId, + roomName: roomName || from.name || 'Chat', + timestamp: new Date(), + }; + + logger.info('[ROCKETCHAT_CALL_LISTENER] πŸ“ž CALL ENDED DETECTED!', { + from: callEvent.from.username, + roomId: callEvent.roomId, + roomName: callEvent.roomName, + callType, + action: eventData.action, + status: eventData.status, + }); + + // Notify all handlers that the call has ended + this.callHandlers.forEach((handler) => { + try { + handler(callEvent); + } catch (error) { + logger.error('[ROCKETCHAT_CALL_LISTENER] Error in call ended handler', { + error: error instanceof Error ? error.message : String(error) + }); + } + }); + } + // Handle incoming call events + else if (isIncomingCall && roomId) { const callEvent: CallEvent = { type: 'call-incoming', from: { diff --git a/scripts/cleanup-orphaned-mission-calendars.sh b/scripts/cleanup-orphaned-mission-calendars.sh new file mode 100755 index 0000000..6f88d21 --- /dev/null +++ b/scripts/cleanup-orphaned-mission-calendars.sh @@ -0,0 +1,127 @@ +#!/bin/bash + +# ============================================ +# Cleanup Orphaned Mission Calendars Script +# ============================================ +# This script helps clean up calendars from deleted missions +# +# Usage: +# ./scripts/cleanup-orphaned-mission-calendars.sh [--dry-run] [--execute] +# +# Options: +# --dry-run : Show what would be deleted (default) +# --execute : Actually delete the orphaned calendars + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +DRY_RUN=true +EXECUTE=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --execute) + EXECUTE=true + DRY_RUN=false + shift + ;; + --dry-run) + DRY_RUN=true + EXECUTE=false + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--dry-run] [--execute]" + exit 1 + ;; + esac +done + +echo -e "${YELLOW}========================================${NC}" +echo -e "${YELLOW}Cleanup Orphaned Mission Calendars${NC}" +echo -e "${YELLOW}========================================${NC}" +echo "" + +# Check if docker-compose is available +if ! command -v docker-compose &> /dev/null; then + echo -e "${RED}Error: docker-compose not found${NC}" + exit 1 +fi + +# Check if .env.production exists +if [ ! -f ".env.production" ]; then + echo -e "${RED}Error: .env.production file not found${NC}" + exit 1 +fi + +if [ "$DRY_RUN" = true ]; then + echo -e "${YELLOW}Mode: DRY RUN (no changes will be made)${NC}" + echo "" + echo "Reviewing orphaned calendars..." + echo "" + + docker-compose -f docker-compose.prod.yml --env-file .env.production exec -T db psql -U neah_user -d calendar_db <