missions finition

This commit is contained in:
alma 2026-01-21 15:18:40 +01:00
parent bd112deec5
commit 40dd01c593
5 changed files with 415 additions and 63 deletions

View File

@ -42,7 +42,7 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou
}, [pathname]); }, [pathname]);
// Listen for incoming RocketChat calls via WebSocket // Listen for incoming RocketChat calls via WebSocket
const { incomingCall, setIncomingCall } = useRocketChatCalls(); const { incomingCall, setIncomingCall, clearIncomingCall } = useRocketChatCalls();
// Listen for email notifications // Listen for email notifications
const { emailNotification, setEmailNotification } = useEmailNotifications(); const { emailNotification, setEmailNotification } = useEmailNotifications();
@ -268,15 +268,15 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou
call={incomingCall} call={incomingCall}
onDismiss={() => { onDismiss={() => {
console.log('[LayoutWrapper] Call dismissed'); console.log('[LayoutWrapper] Call dismissed');
setIncomingCall(null); clearIncomingCall();
}} }}
onAccept={(roomId) => { onAccept={(roomId) => {
console.log('[LayoutWrapper] Call accepted, navigating to room:', roomId); console.log('[LayoutWrapper] Call accepted, navigating to room:', roomId);
setIncomingCall(null); clearIncomingCall();
}} }}
onReject={() => { onReject={() => {
console.log('[LayoutWrapper] Call rejected'); console.log('[LayoutWrapper] Call rejected');
setIncomingCall(null); clearIncomingCall();
// TODO: Send reject signal to RocketChat if needed // TODO: Send reject signal to RocketChat if needed
}} }}
/> />

View File

@ -15,6 +15,7 @@ export function useRocketChatCalls() {
const unsubscribeRef = useRef<(() => void) | null>(null); const unsubscribeRef = useRef<(() => void) | null>(null);
const initializedRef = useRef(false); const initializedRef = useRef(false);
const [incomingCall, setIncomingCall] = useState<IncomingCall | null>(null); const [incomingCall, setIncomingCall] = useState<IncomingCall | null>(null);
const currentCallRoomIdRef = useRef<string | null>(null);
/** /**
* Get RocketChat credentials for the current user * Get RocketChat credentials for the current user
@ -106,6 +107,39 @@ export function useRocketChatCalls() {
// Subscribe to call events // Subscribe to call events
const unsubscribe = listener.onCall((callEvent: CallEvent) => { const unsubscribe = listener.onCall((callEvent: CallEvent) => {
// 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] Call ended', {
roomId: callEvent.roomId,
currentCallRoomId: currentCallRoomIdRef.current,
from: callEvent.from.username,
});
// 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;
}
// Handle incoming call events
if (callEvent.type === 'call-incoming') {
console.log('[useRocketChatCalls] 🎉 INCOMING CALL DETECTED!', { console.log('[useRocketChatCalls] 🎉 INCOMING CALL DETECTED!', {
from: callEvent.from.username, from: callEvent.from.username,
roomId: callEvent.roomId, roomId: callEvent.roomId,
@ -129,6 +163,9 @@ export function useRocketChatCalls() {
timestamp: callEvent.timestamp, 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', { console.log('[useRocketChatCalls] 📞 Incoming call notification UI set', {
from: callEvent.from.username, from: callEvent.from.username,
roomId: callEvent.roomId, roomId: callEvent.roomId,
@ -159,6 +196,7 @@ export function useRocketChatCalls() {
}).catch((error) => { }).catch((error) => {
console.error('[useRocketChatCalls] ❌ Error triggering notification', error); console.error('[useRocketChatCalls] ❌ Error triggering notification', error);
}); });
}
}); });
unsubscribeRef.current = unsubscribe; unsubscribeRef.current = unsubscribe;
@ -196,8 +234,18 @@ export function useRocketChatCalls() {
}; };
}, [session?.user?.id, initializeListener]); }, [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 { return {
incomingCall, incomingCall,
setIncomingCall, setIncomingCall,
clearIncomingCall,
}; };
} }

View File

@ -390,7 +390,34 @@ export class RocketChatCallListener {
const messageType = payload?.message?.t; const messageType = payload?.message?.t;
const isCallNotification = messageType === 'videoconf' || messageType === 'audio' || messageType === 'video'; 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!', { logger.info('[ROCKETCHAT_CALL_LISTENER] ✅ VIDEO/AUDIO CALL DETECTED in notification!', {
type: messageType, type: messageType,
sender: payload.sender, sender: payload.sender,
@ -416,22 +443,57 @@ 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')) { if (eventName?.includes('/webrtc')) {
logger.debug('[ROCKETCHAT_CALL_LISTENER] ✅ This is a webrtc event!'); logger.debug('[ROCKETCHAT_CALL_LISTENER] ✅ This is a webrtc event!');
if (args.length > 0) { if (args.length > 0) {
const webrtcData = args[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') { if (webrtcData.action === 'ringing' || webrtcData.type === 'call' || webrtcData.event === 'incoming') {
this.handleCallEvent(webrtcData); 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) // Also check for other possible call-related events (but be more specific)
// Only process if the event name explicitly indicates a call // Process both incoming calls and call endings
if (eventName?.includes('/call') && (eventName?.includes('/incoming') || eventName?.includes('/ringing'))) { if (eventName?.includes('/call')) {
logger.debug('[ROCKETCHAT_CALL_LISTENER] ✅ This might be a call event!'); 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) { if (args.length > 0) {
this.handleCallEvent(args[0]); this.handleCallEvent(args[0]);
} else { } else {
@ -439,6 +501,7 @@ export class RocketChatCallListener {
} }
} }
} }
}
// Log all stream-notify-user messages for debugging // Log all stream-notify-user messages for debugging
if (message.collection === 'stream-notify-user') { if (message.collection === 'stream-notify-user') {
@ -510,6 +573,25 @@ export class RocketChatCallListener {
allKeys: Object.keys(eventData), 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 // Check if this is an incoming call event
// RocketChat sends calls via notifications with message.t === 'videoconf' or 'audio' // RocketChat sends calls via notifications with message.t === 'videoconf' or 'audio'
const isIncomingCall = const isIncomingCall =
@ -526,7 +608,42 @@ export class RocketChatCallListener {
eventData.message?.t === 'video' || eventData.message?.t === 'video' ||
(eventData.type === 'call' && eventData.status === 'ringing'); (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 = { const callEvent: CallEvent = {
type: 'call-incoming', type: 'call-incoming',
from: { from: {

View File

@ -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 <<EOF
SELECT
c.id,
c.name,
c."missionId",
u.email as user_email,
COUNT(e.id) as event_count
FROM "Calendar" c
LEFT JOIN "User" u ON c."userId" = u.id
LEFT JOIN "Event" e ON e."calendarId" = c.id
WHERE c.name LIKE 'Mission: %'
AND c."missionId" IS NULL
GROUP BY c.id, c.name, c."missionId", c."userId", u.email
ORDER BY event_count DESC, c.name;
EOF
echo ""
echo -e "${YELLOW}To actually delete these calendars, run:${NC}"
echo -e "${GREEN} $0 --execute${NC}"
elif [ "$EXECUTE" = true ]; then
echo -e "${RED}Mode: EXECUTE (calendars will be deleted!)${NC}"
echo ""
read -p "Are you sure you want to delete orphaned mission calendars? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
echo "Cancelled."
exit 0
fi
echo ""
echo "Deleting orphaned calendars..."
docker-compose -f docker-compose.prod.yml --env-file .env.production exec -T db psql -U neah_user -d calendar_db <<EOF
-- Delete events first (they will be cascade deleted anyway, but this is explicit)
DELETE FROM "Event"
WHERE "calendarId" IN (
SELECT id FROM "Calendar"
WHERE name LIKE 'Mission: %'
AND "missionId" IS NULL
);
-- Delete orphaned calendars
DELETE FROM "Calendar"
WHERE name LIKE 'Mission: %'
AND "missionId" IS NULL;
-- Show remaining orphaned calendars (should be 0)
SELECT
COUNT(*) as remaining_orphaned_calendars
FROM "Calendar"
WHERE name LIKE 'Mission: %'
AND "missionId" IS NULL;
EOF
echo ""
echo -e "${GREEN}Cleanup completed!${NC}"
fi

View File

@ -0,0 +1,60 @@
-- ============================================
-- Cleanup Orphaned Mission Calendars
-- ============================================
-- This script removes calendars that are linked to deleted missions
-- (calendars with name starting with "Mission: " but missionId is NULL)
--
-- WARNING: This will delete calendars and their events!
-- Review the calendars first before running the DELETE statements.
--
-- Usage:
-- docker-compose -f docker-compose.prod.yml --env-file .env.production exec db psql -U neah_user -d calendar_db -f /path/to/cleanup-orphaned-mission-calendars.sql
-- OR
-- psql -U neah_user -d calendar_db < cleanup-orphaned-mission-calendars.sql
-- Step 1: Review what will be deleted (run this first!)
SELECT
c.id,
c.name,
c."missionId",
c."userId",
u.email as user_email,
COUNT(e.id) as event_count
FROM "Calendar" c
LEFT JOIN "User" u ON c."userId" = u.id
LEFT JOIN "Event" e ON e."calendarId" = c.id
WHERE c.name LIKE 'Mission: %'
AND c."missionId" IS NULL
GROUP BY c.id, c.name, c."missionId", c."userId", u.email
ORDER BY event_count DESC, c.name;
-- Step 2: Check total counts
SELECT
COUNT(*) as orphaned_calendars,
SUM((SELECT COUNT(*) FROM "Event" WHERE "calendarId" = c.id)) as total_events
FROM "Calendar" c
WHERE c.name LIKE 'Mission: %'
AND c."missionId" IS NULL;
-- Step 3: Delete events from orphaned calendars (events will be cascade deleted, but this is explicit)
-- Events are automatically deleted when calendars are deleted due to CASCADE,
-- but we can delete them first if you want to see the progress
DELETE FROM "Event"
WHERE "calendarId" IN (
SELECT id FROM "Calendar"
WHERE name LIKE 'Mission: %'
AND "missionId" IS NULL
);
-- Step 4: Delete orphaned mission calendars
-- UNCOMMENT THE LINE BELOW AFTER REVIEWING THE RESULTS FROM STEP 1
-- DELETE FROM "Calendar"
-- WHERE name LIKE 'Mission: %'
-- AND "missionId" IS NULL;
-- Step 5: Verify cleanup (run after deletion)
SELECT
COUNT(*) as remaining_orphaned_calendars
FROM "Calendar"
WHERE name LIKE 'Mission: %'
AND "missionId" IS NULL;