Fix jwt-token
This commit is contained in:
@@ -19,28 +19,40 @@ function App() {
|
|||||||
<Router basename={process.env.NODE_ENV === 'production' ? '/drones' : ''}>
|
<Router basename={process.env.NODE_ENV === 'production' ? '/drones' : ''}>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Toaster
|
<Toaster
|
||||||
position="top-right"
|
position="top-center"
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
duration: 4000,
|
duration: 3000, // Shorter duration for mobile
|
||||||
style: {
|
style: {
|
||||||
background: '#363636',
|
background: '#363636',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
|
maxWidth: '90vw', // Limit width on mobile
|
||||||
|
fontSize: '14px', // Smaller text on mobile
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
duration: 3000,
|
duration: 2000, // Even shorter for success
|
||||||
iconTheme: {
|
iconTheme: {
|
||||||
primary: '#4ade80',
|
primary: '#4ade80',
|
||||||
secondary: '#fff',
|
secondary: '#fff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
duration: 5000,
|
duration: 4000, // Moderate duration for errors
|
||||||
iconTheme: {
|
iconTheme: {
|
||||||
primary: '#ef4444',
|
primary: '#ef4444',
|
||||||
secondary: '#fff',
|
secondary: '#fff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
// Limit number of visible toasts to prevent screen overflow
|
||||||
|
gutter={8}
|
||||||
|
containerStyle={{
|
||||||
|
top: '1rem',
|
||||||
|
left: '1rem',
|
||||||
|
right: '1rem',
|
||||||
|
bottom: 'auto',
|
||||||
|
}}
|
||||||
|
// Custom container class for responsive styling
|
||||||
|
containerClassName="toast-container"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export const EditAlertModal = ({ rule, onClose, onSuccess }) => {
|
|||||||
cooldown_period: 600,
|
cooldown_period: 600,
|
||||||
alert_channels: ['sms'],
|
alert_channels: ['sms'],
|
||||||
min_threat_level: '',
|
min_threat_level: '',
|
||||||
|
drone_types: [],
|
||||||
|
device_ids: [],
|
||||||
sms_phone_number: '',
|
sms_phone_number: '',
|
||||||
webhook_url: ''
|
webhook_url: ''
|
||||||
});
|
});
|
||||||
@@ -30,6 +32,8 @@ export const EditAlertModal = ({ rule, onClose, onSuccess }) => {
|
|||||||
cooldown_period: rule.cooldown_period || 600,
|
cooldown_period: rule.cooldown_period || 600,
|
||||||
alert_channels: rule.alert_channels || ['sms'],
|
alert_channels: rule.alert_channels || ['sms'],
|
||||||
min_threat_level: rule.min_threat_level || '',
|
min_threat_level: rule.min_threat_level || '',
|
||||||
|
drone_types: rule.drone_types || [],
|
||||||
|
device_ids: rule.device_ids || [],
|
||||||
sms_phone_number: rule.sms_phone_number || '',
|
sms_phone_number: rule.sms_phone_number || '',
|
||||||
webhook_url: rule.webhook_url || ''
|
webhook_url: rule.webhook_url || ''
|
||||||
});
|
});
|
||||||
@@ -53,6 +57,15 @@ export const EditAlertModal = ({ rule, onClose, onSuccess }) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDroneTypeChange = (droneType, checked) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
drone_types: checked
|
||||||
|
? [...prev.drone_types, droneType]
|
||||||
|
: prev.drone_types.filter(type => type !== droneType)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -148,6 +161,37 @@ export const EditAlertModal = ({ rule, onClose, onSuccess }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Drone Types Filter
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs text-gray-500 mb-2">
|
||||||
|
Leave empty to monitor all drone types
|
||||||
|
</div>
|
||||||
|
{[
|
||||||
|
{ id: 0, name: 'Consumer/Hobby' },
|
||||||
|
{ id: 1, name: 'Orlan/Military' },
|
||||||
|
{ id: 2, name: 'Professional/Commercial' },
|
||||||
|
{ id: 3, name: 'Racing/High-speed' },
|
||||||
|
{ id: 4, name: 'Unknown/Custom' }
|
||||||
|
].map(droneType => (
|
||||||
|
<label key={droneType.id} className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.drone_types.includes(droneType.id)}
|
||||||
|
onChange={(e) => handleDroneTypeChange(droneType.id, e.target.checked)}
|
||||||
|
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<span className="ml-2 text-sm text-gray-700">
|
||||||
|
{droneType.name}
|
||||||
|
{droneType.id === 1 && <span className="text-red-600 font-semibold"> (⚠️ High Threat)</span>}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
|||||||
@@ -12,8 +12,112 @@ export const SocketProvider = ({ children }) => {
|
|||||||
const [deviceStatus, setDeviceStatus] = useState({});
|
const [deviceStatus, setDeviceStatus] = useState({});
|
||||||
const [movementAlerts, setMovementAlerts] = useState([]);
|
const [movementAlerts, setMovementAlerts] = useState([]);
|
||||||
const [droneTracking, setDroneTracking] = useState(new Map());
|
const [droneTracking, setDroneTracking] = useState(new Map());
|
||||||
|
const [notificationsEnabled, setNotificationsEnabled] = useState(
|
||||||
|
localStorage.getItem('notificationsEnabled') !== 'false' // Default to enabled
|
||||||
|
);
|
||||||
const { isAuthenticated } = useAuth();
|
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(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
// Initialize socket connection
|
// Initialize socket connection
|
||||||
@@ -83,26 +187,8 @@ export const SocketProvider = ({ children }) => {
|
|||||||
|
|
||||||
setMovementAlerts(prev => [alertData, ...prev.slice(0, 19)]); // Keep last 20 alerts
|
setMovementAlerts(prev => [alertData, ...prev.slice(0, 19)]); // Keep last 20 alerts
|
||||||
|
|
||||||
// Show priority-based notifications
|
// Mobile-friendly notification management
|
||||||
const alertIcon = alertData.analysis.alertLevel >= 3 ? '🚨' :
|
handleMobileNotification(alertData);
|
||||||
alertData.analysis.alertLevel >= 2 ? '⚠️' : '📍';
|
|
||||||
|
|
||||||
const toastOptions = {
|
|
||||||
duration: alertData.analysis.alertLevel >= 2 ? 10000 : 6000,
|
|
||||||
icon: alertIcon,
|
|
||||||
style: {
|
|
||||||
background: alertData.analysis.alertLevel >= 3 ? '#fee2e2' :
|
|
||||||
alertData.analysis.alertLevel >= 2 ? '#fef3c7' : '#e0f2fe'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (alertData.analysis.alertLevel >= 3) {
|
|
||||||
toast.error(alertData.analysis.description, toastOptions);
|
|
||||||
} else if (alertData.analysis.alertLevel >= 2) {
|
|
||||||
toast.error(alertData.analysis.description, toastOptions);
|
|
||||||
} else {
|
|
||||||
toast(alertData.analysis.description, toastOptions);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for device heartbeats
|
// Listen for device heartbeats
|
||||||
@@ -164,6 +250,18 @@ export const SocketProvider = ({ children }) => {
|
|||||||
return droneTracking.get(trackingKey);
|
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 = {
|
const value = {
|
||||||
socket,
|
socket,
|
||||||
connected,
|
connected,
|
||||||
@@ -171,6 +269,8 @@ export const SocketProvider = ({ children }) => {
|
|||||||
deviceStatus,
|
deviceStatus,
|
||||||
movementAlerts,
|
movementAlerts,
|
||||||
droneTracking,
|
droneTracking,
|
||||||
|
notificationsEnabled,
|
||||||
|
toggleNotifications,
|
||||||
joinDeviceRoom,
|
joinDeviceRoom,
|
||||||
leaveDeviceRoom,
|
leaveDeviceRoom,
|
||||||
clearRecentDetections,
|
clearRecentDetections,
|
||||||
|
|||||||
@@ -101,3 +101,47 @@ body {
|
|||||||
.table td {
|
.table td {
|
||||||
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-900;
|
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile-responsive toast notifications */
|
||||||
|
.toast-container {
|
||||||
|
max-height: 50vh !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile optimizations for toasts */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.toast-container {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 4rem !important; /* Below mobile header */
|
||||||
|
left: 0.5rem !important;
|
||||||
|
right: 0.5rem !important;
|
||||||
|
max-height: 40vh !important;
|
||||||
|
z-index: 9999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make individual toasts smaller on mobile */
|
||||||
|
.toast-container > div {
|
||||||
|
max-width: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
padding: 0.75rem !important;
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Limit the number of visible toasts on mobile */
|
||||||
|
.toast-container > div:nth-child(n+4) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet optimizations */
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
.toast-container {
|
||||||
|
max-height: 60vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-container > div:nth-child(n+6) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -257,6 +257,38 @@ const Alerts = () => {
|
|||||||
{rule.cooldown_period && (
|
{rule.cooldown_period && (
|
||||||
<div>Cooldown: {rule.cooldown_period}s</div>
|
<div>Cooldown: {rule.cooldown_period}s</div>
|
||||||
)}
|
)}
|
||||||
|
{rule.min_threat_level && (
|
||||||
|
<div>Min threat: {rule.min_threat_level}</div>
|
||||||
|
)}
|
||||||
|
{rule.drone_types && rule.drone_types.length > 0 && (
|
||||||
|
<div className="mt-1">
|
||||||
|
<div className="text-xs text-gray-500">Drone types:</div>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{rule.drone_types.map((typeId, index) => {
|
||||||
|
const droneTypes = {
|
||||||
|
0: 'Consumer',
|
||||||
|
1: 'Orlan',
|
||||||
|
2: 'Professional',
|
||||||
|
3: 'Racing',
|
||||||
|
4: 'Unknown'
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={`px-1.5 py-0.5 rounded text-xs font-medium ${
|
||||||
|
typeId === 1
|
||||||
|
? 'bg-red-100 text-red-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{droneTypes[typeId] || 'Unknown'}
|
||||||
|
{typeId === 1 && '⚠️'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -427,8 +459,8 @@ const CreateAlertRuleModal = ({ onClose, onSave }) => {
|
|||||||
min_detections: 1,
|
min_detections: 1,
|
||||||
time_window: 300,
|
time_window: 300,
|
||||||
cooldown_period: 600,
|
cooldown_period: 600,
|
||||||
device_ids: null,
|
device_ids: [],
|
||||||
drone_types: null,
|
drone_types: [],
|
||||||
min_rssi: '',
|
min_rssi: '',
|
||||||
max_rssi: '',
|
max_rssi: '',
|
||||||
frequency_ranges: []
|
frequency_ranges: []
|
||||||
@@ -475,6 +507,15 @@ const CreateAlertRuleModal = ({ onClose, onSave }) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDroneTypeChange = (droneType, checked) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
drone_types: checked
|
||||||
|
? [...prev.drone_types, droneType]
|
||||||
|
: prev.drone_types.filter(type => type !== droneType)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
@@ -600,6 +641,37 @@ const CreateAlertRuleModal = ({ onClose, onSave }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Drone Types Filter
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs text-gray-500 mb-2">
|
||||||
|
Leave empty to monitor all drone types
|
||||||
|
</div>
|
||||||
|
{[
|
||||||
|
{ id: 0, name: 'Consumer/Hobby' },
|
||||||
|
{ id: 1, name: 'Orlan/Military' },
|
||||||
|
{ id: 2, name: 'Professional/Commercial' },
|
||||||
|
{ id: 3, name: 'Racing/High-speed' },
|
||||||
|
{ id: 4, name: 'Unknown/Custom' }
|
||||||
|
].map(droneType => (
|
||||||
|
<label key={droneType.id} className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.drone_types.includes(droneType.id)}
|
||||||
|
onChange={(e) => handleDroneTypeChange(droneType.id, e.target.checked)}
|
||||||
|
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<span className="ml-2 text-sm text-gray-700">
|
||||||
|
{droneType.name}
|
||||||
|
{droneType.id === 1 && <span className="text-red-600 font-semibold"> (⚠️ High Threat)</span>}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const Dashboard = () => {
|
|||||||
const [recentActivity, setRecentActivity] = useState([]);
|
const [recentActivity, setRecentActivity] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showMovementAlerts, setShowMovementAlerts] = useState(true);
|
const [showMovementAlerts, setShowMovementAlerts] = useState(true);
|
||||||
const { recentDetections, deviceStatus, connected, movementAlerts } = useSocket();
|
const { recentDetections, deviceStatus, connected, movementAlerts, notificationsEnabled, toggleNotifications } = useSocket();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
@@ -116,10 +116,26 @@ const Dashboard = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
System Overview
|
System Overview
|
||||||
</h3>
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={toggleNotifications}
|
||||||
|
className={`inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md transition-colors ${
|
||||||
|
notificationsEnabled
|
||||||
|
? 'text-gray-700 bg-gray-100 hover:bg-gray-200'
|
||||||
|
: 'text-red-700 bg-red-100 hover:bg-red-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<BellIcon className={`h-4 w-4 mr-2 ${notificationsEnabled ? '' : 'line-through'}`} />
|
||||||
|
{notificationsEnabled ? 'Notifications On' : 'Notifications Off'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{stats.map((item) => (
|
{stats.map((item) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
184
test_orlan_scenario.py
Normal file
184
test_orlan_scenario.py
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Orlan Military Drone Test Script
|
||||||
|
Simulates an Orlan military drone approaching from long distance to a target facility.
|
||||||
|
This test demonstrates the critical alert system for high-threat drones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
API_BASE_URL = "http://selfservice.cqers.com/drones/api"
|
||||||
|
|
||||||
|
def haversine_distance(lat1, lon1, lat2, lon2):
|
||||||
|
"""Calculate distance between two points in kilometers"""
|
||||||
|
R = 6371 # Earth's radius in kilometers
|
||||||
|
|
||||||
|
dlat = math.radians(lat2 - lat1)
|
||||||
|
dlon = math.radians(lon2 - lon1)
|
||||||
|
|
||||||
|
a = (math.sin(dlat/2) * math.sin(dlat/2) +
|
||||||
|
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
|
||||||
|
math.sin(dlon/2) * math.sin(dlon/2))
|
||||||
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
||||||
|
|
||||||
|
return R * c
|
||||||
|
|
||||||
|
def rssi_from_distance(distance_km):
|
||||||
|
"""Calculate RSSI based on distance"""
|
||||||
|
# Orlan drones have powerful transmission systems
|
||||||
|
# Base RSSI at 1km = -60 dBm (stronger than consumer drones)
|
||||||
|
base_rssi = -60
|
||||||
|
path_loss_exponent = 2.5 # Lower path loss due to better equipment
|
||||||
|
|
||||||
|
if distance_km < 0.001: # Less than 1 meter
|
||||||
|
return -30
|
||||||
|
|
||||||
|
rssi = base_rssi - (20 * path_loss_exponent * math.log10(distance_km))
|
||||||
|
return max(rssi, -100) # Cap at -100 dBm
|
||||||
|
|
||||||
|
def fetch_devices():
|
||||||
|
"""Fetch devices from API"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_BASE_URL}/devices/map")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
return data.get('data', [])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching devices: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def send_detection(device, drone_lat, drone_lon, distance_km, step, total_steps):
|
||||||
|
"""Send a detection to the API"""
|
||||||
|
rssi = rssi_from_distance(distance_km)
|
||||||
|
|
||||||
|
# Use the EXACT standard payload format - NEVER change this!
|
||||||
|
detection_data = {
|
||||||
|
"device_id": device["id"],
|
||||||
|
"geo_lat": drone_lat,
|
||||||
|
"geo_lon": drone_lon,
|
||||||
|
"device_timestamp": int(time.time() * 1000), # Current timestamp in milliseconds
|
||||||
|
"drone_type": 1, # Orlan/Military type
|
||||||
|
"rssi": int(rssi),
|
||||||
|
"freq": 24, # Orlan operates on 2.4 GHz (24 = 2400 MHz)
|
||||||
|
"drone_id": 1000 + step # Unique drone ID for tracking
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{API_BASE_URL}/detections", json=detection_data)
|
||||||
|
if response.status_code == 201:
|
||||||
|
print(f"🚨 ORLAN DETECTION {step}/{total_steps}: Distance={distance_km:.2f}km, RSSI={rssi:.0f}dBm")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Failed to send detection: {response.status_code}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error sending detection: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_orlan_approach_scenario():
|
||||||
|
"""Run the Orlan approach scenario"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("🚨 ORLAN MILITARY DRONE APPROACH SIMULATION")
|
||||||
|
print("=" * 60)
|
||||||
|
print("This simulation demonstrates:")
|
||||||
|
print("- Long-range Orlan drone detection")
|
||||||
|
print("- Critical alert escalation")
|
||||||
|
print("- Automatic threat assessment")
|
||||||
|
print("- Real-time approach tracking")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Fetch devices
|
||||||
|
devices = fetch_devices()
|
||||||
|
if not devices:
|
||||||
|
print("❌ No devices found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use the first device as target (usually Arlanda)
|
||||||
|
target_device = devices[0]
|
||||||
|
print(f"🎯 Target: {target_device['name']}")
|
||||||
|
print(f"📍 Location: {target_device['geo_lat']:.4f}, {target_device['geo_lon']:.4f}")
|
||||||
|
|
||||||
|
# Starting position: 15km northeast of target (simulating approach from hostile territory)
|
||||||
|
start_distance = 15.0 # km
|
||||||
|
angle = math.radians(45) # 45 degrees (northeast)
|
||||||
|
|
||||||
|
# Calculate starting position
|
||||||
|
start_lat = target_device['geo_lat'] + (start_distance / 111.0) * math.cos(angle)
|
||||||
|
start_lon = target_device['geo_lon'] + (start_distance / (111.0 * math.cos(math.radians(target_device['geo_lat'])))) * math.sin(angle)
|
||||||
|
|
||||||
|
print(f"🛫 Orlan starting position: {start_lat:.4f}, {start_lon:.4f}")
|
||||||
|
print(f"📏 Initial distance: {start_distance:.1f}km")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Simulation parameters
|
||||||
|
total_steps = 30
|
||||||
|
final_distance = 0.05 # 50 meters final approach
|
||||||
|
|
||||||
|
print("Starting approach simulation...")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for step in range(1, total_steps + 1):
|
||||||
|
# Calculate current distance (exponential approach for realistic acceleration)
|
||||||
|
progress = step / total_steps
|
||||||
|
# Use exponential curve for more realistic approach pattern
|
||||||
|
distance_km = start_distance * (1 - progress) ** 2 + final_distance * progress ** 2
|
||||||
|
|
||||||
|
# Calculate current position
|
||||||
|
current_lat = start_lat + (target_device['geo_lat'] - start_lat) * progress
|
||||||
|
current_lon = start_lon + (target_device['geo_lon'] - start_lon) * progress
|
||||||
|
|
||||||
|
# Send detection
|
||||||
|
success = send_detection(target_device, current_lat, current_lon, distance_km, step, total_steps)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
print(f"❌ Failed to send detection at step {step}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Show threat escalation messages
|
||||||
|
if step == 1:
|
||||||
|
print(" 🔍 Initial long-range detection - monitoring")
|
||||||
|
elif distance_km < 10 and step < 10:
|
||||||
|
print(" ⚠️ Entering medium-range surveillance zone")
|
||||||
|
elif distance_km < 5:
|
||||||
|
print(" 🚨 HIGH ALERT: Orlan approaching critical zone")
|
||||||
|
elif distance_km < 1:
|
||||||
|
print(" 🔥 IMMEDIATE THREAT: Orlan within facility perimeter")
|
||||||
|
elif distance_km < 0.2:
|
||||||
|
print(" 💥 CRITICAL: Orlan directly overhead - TAKE COVER")
|
||||||
|
|
||||||
|
# Variable delay based on distance (faster updates as it gets closer)
|
||||||
|
if distance_km > 10:
|
||||||
|
delay = 3.0 # 3 seconds for long-range
|
||||||
|
elif distance_km > 5:
|
||||||
|
delay = 2.0 # 2 seconds for medium-range
|
||||||
|
elif distance_km > 1:
|
||||||
|
delay = 1.5 # 1.5 seconds for close-range
|
||||||
|
else:
|
||||||
|
delay = 1.0 # 1 second for critical proximity
|
||||||
|
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("🚨 ORLAN APPROACH SIMULATION COMPLETED")
|
||||||
|
print("=" * 60)
|
||||||
|
print("Summary:")
|
||||||
|
print(f"- Total detections sent: {total_steps}")
|
||||||
|
print(f"- Distance covered: {start_distance - final_distance:.1f}km")
|
||||||
|
print(f"- Target facility: {target_device['name']}")
|
||||||
|
print("- All alerts should have triggered critical notifications")
|
||||||
|
print("- Check the dashboard for real-time tracking")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
run_orlan_approach_scenario()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\n⚠️ Simulation interrupted by user")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Error during simulation: {e}")
|
||||||
Reference in New Issue
Block a user