missions finition
This commit is contained in:
parent
bd112deec5
commit
40dd01c593
@ -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
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -15,6 +15,7 @@ export function useRocketChatCalls() {
|
||||
const unsubscribeRef = useRef<(() => void) | null>(null);
|
||||
const initializedRef = useRef(false);
|
||||
const [incomingCall, setIncomingCall] = useState<IncomingCall | null>(null);
|
||||
const currentCallRoomIdRef = useRef<string | null>(null);
|
||||
|
||||
/**
|
||||
* Get RocketChat credentials for the current user
|
||||
@ -106,6 +107,39 @@ export function useRocketChatCalls() {
|
||||
|
||||
// Subscribe to call events
|
||||
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!', {
|
||||
from: callEvent.from.username,
|
||||
roomId: callEvent.roomId,
|
||||
@ -129,6 +163,9 @@ export function useRocketChatCalls() {
|
||||
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,
|
||||
@ -159,6 +196,7 @@ export function useRocketChatCalls() {
|
||||
}).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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,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')) {
|
||||
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!');
|
||||
// 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 {
|
||||
@ -439,6 +501,7 @@ export class RocketChatCallListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log all stream-notify-user messages for debugging
|
||||
if (message.collection === 'stream-notify-user') {
|
||||
@ -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: {
|
||||
|
||||
127
scripts/cleanup-orphaned-mission-calendars.sh
Executable file
127
scripts/cleanup-orphaned-mission-calendars.sh
Executable 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
|
||||
60
scripts/cleanup-orphaned-mission-calendars.sql
Normal file
60
scripts/cleanup-orphaned-mission-calendars.sql
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user