296 lines
9.5 KiB
JavaScript
296 lines
9.5 KiB
JavaScript
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
import { io } from 'socket.io-client';
|
|
import { useAuth } from './AuthContext';
|
|
import toast from 'react-hot-toast';
|
|
import APP_CONFIG from '../config/app';
|
|
|
|
const SocketContext = createContext();
|
|
|
|
export const SocketProvider = ({ children }) => {
|
|
const [socket, setSocket] = useState(null);
|
|
const [connected, setConnected] = useState(false);
|
|
const [recentDetections, setRecentDetections] = useState([]);
|
|
const [deviceStatus, setDeviceStatus] = useState({});
|
|
const [movementAlerts, setMovementAlerts] = useState([]);
|
|
const [droneTracking, setDroneTracking] = useState(new Map());
|
|
const [notificationsEnabled, setNotificationsEnabled] = useState(
|
|
localStorage.getItem('notificationsEnabled') !== 'false' // Default to enabled
|
|
);
|
|
const { isAuthenticated } = useAuth();
|
|
|
|
// Mobile notification management
|
|
const [notificationCooldown, setNotificationCooldown] = useState(new Map());
|
|
const [pendingNotifications, setPendingNotifications] = useState([]);
|
|
|
|
// Check if device is mobile
|
|
const isMobile = window.innerWidth <= 768;
|
|
|
|
// Mobile-friendly notification handler
|
|
const handleMobileNotification = (alertData) => {
|
|
// Skip if notifications are disabled
|
|
if (!notificationsEnabled) return;
|
|
|
|
const now = Date.now();
|
|
const cooldownKey = `${alertData.droneId}-${alertData.analysis.alertLevel}`;
|
|
const lastNotification = notificationCooldown.get(cooldownKey) || 0;
|
|
|
|
// Rate limiting based on device type and alert level
|
|
const cooldownPeriod = isMobile
|
|
? (alertData.analysis.alertLevel >= 3 ? 5000 : 15000) // 5s for critical, 15s for others on mobile
|
|
: (alertData.analysis.alertLevel >= 3 ? 2000 : 8000); // 2s for critical, 8s for others on desktop
|
|
|
|
if (now - lastNotification < cooldownPeriod) {
|
|
// Store pending notification for potential grouping
|
|
setPendingNotifications(prev => [...prev, alertData]);
|
|
return;
|
|
}
|
|
|
|
// Update cooldown
|
|
setNotificationCooldown(prev => new Map(prev).set(cooldownKey, now));
|
|
|
|
// Show notification with mobile-optimized settings
|
|
const alertIcon = alertData.analysis.alertLevel >= 3 ? '🚨' :
|
|
alertData.analysis.alertLevel >= 2 ? '⚠️' : '📍';
|
|
|
|
const toastOptions = {
|
|
duration: isMobile
|
|
? (alertData.analysis.alertLevel >= 3 ? 4000 : 2500) // Shorter durations on mobile
|
|
: (alertData.analysis.alertLevel >= 2 ? 6000 : 4000),
|
|
icon: alertIcon,
|
|
style: {
|
|
background: alertData.analysis.alertLevel >= 3 ? '#fee2e2' :
|
|
alertData.analysis.alertLevel >= 2 ? '#fef3c7' : '#e0f2fe',
|
|
fontSize: isMobile ? '13px' : '14px',
|
|
padding: isMobile ? '0.75rem' : '1rem',
|
|
maxWidth: isMobile ? '85vw' : '400px',
|
|
}
|
|
};
|
|
|
|
// Create condensed message for mobile
|
|
const message = isMobile
|
|
? `${alertIcon} Drone ${alertData.droneId}: ${getShortDescription(alertData.analysis)}`
|
|
: alertData.analysis.description;
|
|
|
|
if (alertData.analysis.alertLevel >= 3) {
|
|
toast.error(message, toastOptions);
|
|
} else if (alertData.analysis.alertLevel >= 2) {
|
|
toast.error(message, toastOptions);
|
|
} else {
|
|
toast(message, toastOptions);
|
|
}
|
|
};
|
|
|
|
// Helper function to create short descriptions for mobile
|
|
const getShortDescription = (analysis) => {
|
|
if (analysis.alertLevel >= 3) return 'Critical proximity';
|
|
if (analysis.alertLevel >= 2) return 'Approaching';
|
|
return analysis.movement === 'approaching' ? 'Detected' : 'Movement';
|
|
};
|
|
|
|
// Handle grouped notifications for mobile
|
|
useEffect(() => {
|
|
if (pendingNotifications.length > 0 && isMobile) {
|
|
const timer = setTimeout(() => {
|
|
const criticalCount = pendingNotifications.filter(n => n.analysis.alertLevel >= 3).length;
|
|
const warningCount = pendingNotifications.filter(n => n.analysis.alertLevel === 2).length;
|
|
const infoCount = pendingNotifications.filter(n => n.analysis.alertLevel < 2).length;
|
|
|
|
if (criticalCount > 0) {
|
|
toast.error(`🚨 ${criticalCount} critical alert${criticalCount > 1 ? 's' : ''}`, {
|
|
duration: 5000,
|
|
style: { background: '#fee2e2', fontSize: '13px' }
|
|
});
|
|
} else if (warningCount > 0) {
|
|
toast.error(`⚠️ ${warningCount} warning${warningCount > 1 ? 's' : ''}`, {
|
|
duration: 3000,
|
|
style: { background: '#fef3c7', fontSize: '13px' }
|
|
});
|
|
} else if (infoCount > 0) {
|
|
toast(`📍 ${infoCount} detection${infoCount > 1 ? 's' : ''}`, {
|
|
duration: 2000,
|
|
style: { background: '#e0f2fe', fontSize: '13px' }
|
|
});
|
|
}
|
|
|
|
setPendingNotifications([]);
|
|
}, 3000); // Group notifications for 3 seconds
|
|
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [pendingNotifications, isMobile]);
|
|
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
// Initialize socket connection
|
|
const newSocket = io(APP_CONFIG.isProduction
|
|
? window.location.origin
|
|
: 'http://localhost:3001', {
|
|
path: '/ws'
|
|
});
|
|
|
|
newSocket.on('connect', () => {
|
|
console.log('Connected to server');
|
|
setConnected(true);
|
|
|
|
// Join dashboard room for general updates
|
|
newSocket.emit('join_dashboard');
|
|
|
|
toast.success('Connected to real-time updates');
|
|
});
|
|
|
|
newSocket.on('disconnect', () => {
|
|
console.log('Disconnected from server');
|
|
setConnected(false);
|
|
toast.error('Disconnected from server');
|
|
});
|
|
|
|
newSocket.on('connect_error', (error) => {
|
|
console.error('Connection error:', error);
|
|
setConnected(false);
|
|
toast.error('Failed to connect to server');
|
|
});
|
|
|
|
// Listen for drone detections
|
|
newSocket.on('drone_detection', (detection) => {
|
|
console.log('New drone detection:', detection);
|
|
|
|
setRecentDetections(prev => [detection, ...prev.slice(0, 49)]); // Keep last 50
|
|
|
|
// Update drone tracking
|
|
if (detection.movement_analysis) {
|
|
const trackingKey = `${detection.drone_id}_${detection.device_id}`;
|
|
setDroneTracking(prev => {
|
|
const newTracking = new Map(prev);
|
|
newTracking.set(trackingKey, {
|
|
droneId: detection.drone_id,
|
|
deviceId: detection.device_id,
|
|
lastDetection: detection,
|
|
analysis: detection.movement_analysis,
|
|
timestamp: Date.now()
|
|
});
|
|
return newTracking;
|
|
});
|
|
}
|
|
|
|
// Show toast notification
|
|
toast.error(
|
|
`Drone ${detection.drone_id} detected by ${detection.device?.name || `Device ${detection.device_id}`}`,
|
|
{
|
|
duration: 5000,
|
|
icon: '🚨',
|
|
}
|
|
);
|
|
});
|
|
|
|
// Listen for drone movement alerts
|
|
newSocket.on('drone_movement_alert', (alertData) => {
|
|
console.log('Drone movement alert:', alertData);
|
|
|
|
setMovementAlerts(prev => [alertData, ...prev.slice(0, 19)]); // Keep last 20 alerts
|
|
|
|
// Mobile-friendly notification management
|
|
handleMobileNotification(alertData);
|
|
});
|
|
|
|
// Listen for device heartbeats
|
|
newSocket.on('device_heartbeat', (heartbeat) => {
|
|
console.log('Device heartbeat:', heartbeat);
|
|
|
|
setDeviceStatus(prev => ({
|
|
...prev,
|
|
[heartbeat.device_id]: {
|
|
...heartbeat,
|
|
last_seen: new Date()
|
|
}
|
|
}));
|
|
});
|
|
|
|
// Listen for device updates
|
|
newSocket.on('device_updated', (device) => {
|
|
console.log('Device updated:', device);
|
|
toast.success(`Device ${device.name || device.id} updated`);
|
|
});
|
|
|
|
setSocket(newSocket);
|
|
|
|
return () => {
|
|
newSocket.disconnect();
|
|
};
|
|
} else {
|
|
// Disconnect if not authenticated
|
|
if (socket) {
|
|
socket.disconnect();
|
|
setSocket(null);
|
|
setConnected(false);
|
|
}
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
const joinDeviceRoom = (deviceId) => {
|
|
if (socket) {
|
|
socket.emit('join_device_room', deviceId);
|
|
}
|
|
};
|
|
|
|
const leaveDeviceRoom = (deviceId) => {
|
|
if (socket) {
|
|
socket.emit('leave_device_room', deviceId);
|
|
}
|
|
};
|
|
|
|
const clearRecentDetections = () => {
|
|
setRecentDetections([]);
|
|
};
|
|
|
|
const clearMovementAlerts = () => {
|
|
setMovementAlerts([]);
|
|
};
|
|
|
|
const getDroneTracking = (droneId, deviceId) => {
|
|
const trackingKey = `${droneId}_${deviceId}`;
|
|
return droneTracking.get(trackingKey);
|
|
};
|
|
|
|
// Toggle notifications function
|
|
const toggleNotifications = () => {
|
|
const newValue = !notificationsEnabled;
|
|
setNotificationsEnabled(newValue);
|
|
localStorage.setItem('notificationsEnabled', newValue.toString());
|
|
|
|
toast.success(
|
|
newValue ? 'Notifications enabled' : 'Notifications disabled',
|
|
{ duration: 2000 }
|
|
);
|
|
};
|
|
|
|
const value = {
|
|
socket,
|
|
connected,
|
|
recentDetections,
|
|
deviceStatus,
|
|
movementAlerts,
|
|
droneTracking,
|
|
notificationsEnabled,
|
|
toggleNotifications,
|
|
joinDeviceRoom,
|
|
leaveDeviceRoom,
|
|
clearRecentDetections,
|
|
clearMovementAlerts,
|
|
getDroneTracking
|
|
};
|
|
|
|
return (
|
|
<SocketContext.Provider value={value}>
|
|
{children}
|
|
</SocketContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useSocket = () => {
|
|
const context = useContext(SocketContext);
|
|
if (!context) {
|
|
throw new Error('useSocket must be used within a SocketProvider');
|
|
}
|
|
return context;
|
|
};
|