missions finition
This commit is contained in:
parent
bd112deec5
commit
40dd01c593
@ -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
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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,59 +107,96 @@ export function useRocketChatCalls() {
|
|||||||
|
|
||||||
// Subscribe to call events
|
// Subscribe to call events
|
||||||
const unsubscribe = listener.onCall((callEvent: CallEvent) => {
|
const unsubscribe = listener.onCall((callEvent: CallEvent) => {
|
||||||
console.log('[useRocketChatCalls] 🎉 INCOMING CALL DETECTED!', {
|
// Handle call ended events (when caller hangs up)
|
||||||
from: callEvent.from.username,
|
if (callEvent.type === 'call-ended') {
|
||||||
roomId: callEvent.roomId,
|
console.log('[useRocketChatCalls] 📞 CALL ENDED - Checking if notification should be removed', {
|
||||||
roomName: callEvent.roomName,
|
endedRoomId: callEvent.roomId,
|
||||||
});
|
currentCallRoomId: currentCallRoomIdRef.current,
|
||||||
|
from: callEvent.from.username,
|
||||||
|
});
|
||||||
|
|
||||||
logger.info('[useRocketChatCalls] Incoming call detected', {
|
logger.info('[useRocketChatCalls] Call ended', {
|
||||||
from: callEvent.from.username,
|
roomId: callEvent.roomId,
|
||||||
roomId: callEvent.roomId,
|
currentCallRoomId: currentCallRoomIdRef.current,
|
||||||
});
|
from: callEvent.from.username,
|
||||||
|
});
|
||||||
|
|
||||||
// Show incoming call notification UI (Outlook-style rectangle)
|
// Only remove notification if it matches the current call's roomId
|
||||||
setIncomingCall({
|
// This prevents removing notifications from other calls
|
||||||
from: {
|
if (currentCallRoomIdRef.current === callEvent.roomId || !currentCallRoomIdRef.current) {
|
||||||
userId: callEvent.from.userId,
|
console.log('[useRocketChatCalls] ✅ Removing notification for ended call', {
|
||||||
username: callEvent.from.username,
|
roomId: callEvent.roomId,
|
||||||
name: callEvent.from.name || callEvent.from.username,
|
});
|
||||||
},
|
setIncomingCall(null);
|
||||||
roomId: callEvent.roomId,
|
currentCallRoomIdRef.current = null;
|
||||||
roomName: callEvent.roomName || callEvent.roomId,
|
} else {
|
||||||
timestamp: callEvent.timestamp,
|
console.log('[useRocketChatCalls] ⏭️ Ignoring call ended event - different room', {
|
||||||
});
|
endedRoomId: callEvent.roomId,
|
||||||
|
currentCallRoomId: currentCallRoomIdRef.current,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[useRocketChatCalls] 📞 Incoming call notification UI set', {
|
// Handle incoming call events
|
||||||
from: callEvent.from.username,
|
if (callEvent.type === 'call-incoming') {
|
||||||
roomId: callEvent.roomId,
|
console.log('[useRocketChatCalls] 🎉 INCOMING CALL DETECTED!', {
|
||||||
});
|
from: callEvent.from.username,
|
||||||
|
roomId: callEvent.roomId,
|
||||||
|
roomName: callEvent.roomName,
|
||||||
|
});
|
||||||
|
|
||||||
// Trigger notification badge
|
logger.info('[useRocketChatCalls] Incoming call detected', {
|
||||||
// For calls, we want to increment the existing count, not replace it
|
from: callEvent.from.username,
|
||||||
// So we fetch current count first, then increment
|
roomId: callEvent.roomId,
|
||||||
triggerNotification({
|
});
|
||||||
source: 'rocketchat',
|
|
||||||
count: 1, // This will be added to existing count in the registry
|
// Show incoming call notification UI (Outlook-style rectangle)
|
||||||
items: [
|
setIncomingCall({
|
||||||
{
|
from: {
|
||||||
id: `call-${callEvent.roomId}-${Date.now()}`,
|
userId: callEvent.from.userId,
|
||||||
title: `📞 Appel entrant de ${callEvent.from.name || callEvent.from.username}`,
|
username: callEvent.from.username,
|
||||||
message: `Appel vidéo/audio dans ${callEvent.roomName || 'chat'}`,
|
name: callEvent.from.name || callEvent.from.username,
|
||||||
link: `/parole?room=${callEvent.roomId}`,
|
|
||||||
timestamp: callEvent.timestamp,
|
|
||||||
metadata: {
|
|
||||||
type: 'call',
|
|
||||||
from: callEvent.from,
|
|
||||||
roomId: callEvent.roomId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
roomId: callEvent.roomId,
|
||||||
}).then(() => {
|
roomName: callEvent.roomName || callEvent.roomId,
|
||||||
console.log('[useRocketChatCalls] ✅ Notification triggered successfully');
|
timestamp: callEvent.timestamp,
|
||||||
}).catch((error) => {
|
});
|
||||||
console.error('[useRocketChatCalls] ❌ Error triggering notification', error);
|
|
||||||
});
|
// 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;
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,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')) {
|
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');
|
||||||
if (args.length > 0) {
|
const isCallEnded =
|
||||||
this.handleCallEvent(args[0]);
|
eventName?.includes('/end') ||
|
||||||
} else {
|
eventName?.includes('/hangup') ||
|
||||||
this.handleCallEvent(message.fields);
|
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),
|
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: {
|
||||||
|
|||||||
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