Fix jwt-token
This commit is contained in:
263
client/src/components/MovementAlertsPanel.jsx
Normal file
263
client/src/components/MovementAlertsPanel.jsx
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { useSocket } from '../contexts/SocketContext';
|
||||||
|
import {
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
InformationCircleIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
|
SignalIcon,
|
||||||
|
ArrowTrendingUpIcon,
|
||||||
|
ArrowTrendingDownIcon,
|
||||||
|
EyeIcon
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
|
const MovementAlertsPanel = () => {
|
||||||
|
const { movementAlerts, clearMovementAlerts } = useSocket();
|
||||||
|
const [expandedAlert, setExpandedAlert] = useState(null);
|
||||||
|
const [filter, setFilter] = useState('all'); // all, critical, high, medium
|
||||||
|
|
||||||
|
const getAlertIcon = (alertLevel) => {
|
||||||
|
if (alertLevel >= 3) return <ExclamationTriangleIcon className="h-5 w-5 text-red-600" />;
|
||||||
|
if (alertLevel >= 2) return <ExclamationTriangleIcon className="h-5 w-5 text-orange-600" />;
|
||||||
|
return <InformationCircleIcon className="h-5 w-5 text-blue-600" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAlertColor = (alertLevel) => {
|
||||||
|
if (alertLevel >= 3) return 'border-red-200 bg-red-50';
|
||||||
|
if (alertLevel >= 2) return 'border-orange-200 bg-orange-50';
|
||||||
|
return 'border-blue-200 bg-blue-50';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProximityColor = (level) => {
|
||||||
|
switch (level) {
|
||||||
|
case 'VERY_CLOSE': return 'text-red-700 bg-red-100';
|
||||||
|
case 'CLOSE': return 'text-orange-700 bg-orange-100';
|
||||||
|
case 'MEDIUM': return 'text-yellow-700 bg-yellow-100';
|
||||||
|
case 'FAR': return 'text-green-700 bg-green-100';
|
||||||
|
case 'VERY_FAR': return 'text-gray-700 bg-gray-100';
|
||||||
|
default: return 'text-gray-700 bg-gray-100';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMovementIcon = (trend) => {
|
||||||
|
if (trend === 'STRENGTHENING') return <ArrowTrendingUpIcon className="h-4 w-4 text-red-600" />;
|
||||||
|
if (trend === 'WEAKENING') return <ArrowTrendingDownIcon className="h-4 w-4 text-green-600" />;
|
||||||
|
return <SignalIcon className="h-4 w-4 text-gray-600" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredAlerts = movementAlerts.filter(alert => {
|
||||||
|
if (filter === 'all') return true;
|
||||||
|
if (filter === 'critical') return alert.analysis.alertLevel >= 3;
|
||||||
|
if (filter === 'high') return alert.analysis.alertLevel === 2;
|
||||||
|
if (filter === 'medium') return alert.analysis.alertLevel === 1;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const droneTypes = {
|
||||||
|
1: "DJI Mavic",
|
||||||
|
2: "Racing Drone",
|
||||||
|
3: "DJI Phantom",
|
||||||
|
4: "Fixed Wing",
|
||||||
|
5: "Surveillance",
|
||||||
|
0: "Unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow-lg">
|
||||||
|
<div className="px-6 py-4 border-b border-gray-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Movement Alerts</h3>
|
||||||
|
{movementAlerts.length > 0 && (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
{movementAlerts.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<select
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
className="text-sm border border-gray-300 rounded px-2 py-1"
|
||||||
|
>
|
||||||
|
<option value="all">All Alerts</option>
|
||||||
|
<option value="critical">Critical</option>
|
||||||
|
<option value="high">High Priority</option>
|
||||||
|
<option value="medium">Medium Priority</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{movementAlerts.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={clearMovementAlerts}
|
||||||
|
className="text-sm text-gray-600 hover:text-gray-800"
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y divide-gray-200 max-h-96 overflow-y-auto">
|
||||||
|
{filteredAlerts.length === 0 ? (
|
||||||
|
<div className="px-6 py-8 text-center text-gray-500">
|
||||||
|
<EyeIcon className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
||||||
|
<p>No movement alerts</p>
|
||||||
|
<p className="text-sm">Drone movement patterns will appear here</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredAlerts.map((alert, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`border-l-4 ${getAlertColor(alert.analysis.alertLevel)}`}
|
||||||
|
>
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start space-x-3 flex-1">
|
||||||
|
{getAlertIcon(alert.analysis.alertLevel)}
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900">
|
||||||
|
Drone {alert.droneId} • Device {alert.deviceId}
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{format(new Date(alert.timestamp), 'HH:mm:ss')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-700 mt-1">
|
||||||
|
{alert.analysis.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{alert.analysis.rssiTrend && (
|
||||||
|
<div className="flex items-center space-x-4 mt-2 text-xs">
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{getMovementIcon(alert.analysis.rssiTrend.trend)}
|
||||||
|
<span className="text-gray-600">
|
||||||
|
{alert.analysis.rssiTrend.trend.toLowerCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span className="text-gray-500">RSSI:</span>
|
||||||
|
<span className="font-mono text-gray-900">
|
||||||
|
{alert.detection.rssi}dBm
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{alert.analysis.proximityLevel && (
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getProximityColor(alert.analysis.proximityLevel)}`}>
|
||||||
|
{alert.analysis.proximityLevel.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setExpandedAlert(expandedAlert === index ? null : index)}
|
||||||
|
className="ml-2 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
{expandedAlert === index ? (
|
||||||
|
<ChevronUpIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<ChevronDownIcon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expandedAlert === index && (
|
||||||
|
<div className="mt-4 pl-8 space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Drone Type:</span>
|
||||||
|
<div className="text-gray-900">
|
||||||
|
{droneTypes[alert.detection.drone_type] || 'Unknown'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Frequency:</span>
|
||||||
|
<div className="text-gray-900">{alert.detection.freq}MHz</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Confidence:</span>
|
||||||
|
<div className="text-gray-900">
|
||||||
|
{(alert.detection.confidence_level * 100).toFixed(0)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Signal Duration:</span>
|
||||||
|
<div className="text-gray-900">
|
||||||
|
{(alert.detection.signal_duration / 1000).toFixed(1)}s
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{alert.analysis.movement && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700 block mb-1">Movement Pattern:</span>
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
<div>Pattern: <span className="font-mono">{alert.analysis.movement.pattern}</span></div>
|
||||||
|
{alert.analysis.movement.speed > 0 && (
|
||||||
|
<div>Speed: <span className="font-mono">{alert.analysis.movement.speed.toFixed(1)} m/s</span></div>
|
||||||
|
)}
|
||||||
|
{alert.analysis.movement.totalDistance > 0 && (
|
||||||
|
<div>Distance: <span className="font-mono">{(alert.analysis.movement.totalDistance * 1000).toFixed(0)}m</span></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{alert.analysis.detectionCount && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Tracking Stats:</span>
|
||||||
|
<div className="text-sm mt-1">
|
||||||
|
<div>{alert.analysis.detectionCount} detections over {(alert.analysis.timeTracked / 60).toFixed(1)} minutes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{alert.history && alert.history.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700 block mb-2">Recent RSSI History:</span>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{alert.history.slice(-5).map((point, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex flex-col items-center text-xs"
|
||||||
|
>
|
||||||
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-mono ${
|
||||||
|
point.rssi > -50 ? 'bg-red-500' :
|
||||||
|
point.rssi > -60 ? 'bg-orange-500' :
|
||||||
|
point.rssi > -70 ? 'bg-yellow-500' :
|
||||||
|
'bg-green-500'
|
||||||
|
}`}>
|
||||||
|
{point.rssi}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-500 mt-1">
|
||||||
|
{format(new Date(point.timestamp), 'HH:mm')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovementAlertsPanel;
|
||||||
@@ -10,6 +10,8 @@ export const SocketProvider = ({ children }) => {
|
|||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
const [recentDetections, setRecentDetections] = useState([]);
|
const [recentDetections, setRecentDetections] = useState([]);
|
||||||
const [deviceStatus, setDeviceStatus] = useState({});
|
const [deviceStatus, setDeviceStatus] = useState({});
|
||||||
|
const [movementAlerts, setMovementAlerts] = useState([]);
|
||||||
|
const [droneTracking, setDroneTracking] = useState(new Map());
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -49,9 +51,25 @@ export const SocketProvider = ({ children }) => {
|
|||||||
|
|
||||||
setRecentDetections(prev => [detection, ...prev.slice(0, 49)]); // Keep last 50
|
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
|
// Show toast notification
|
||||||
toast.error(
|
toast.error(
|
||||||
`Drone detected by ${detection.device.name || `Device ${detection.device_id}`}`,
|
`Drone ${detection.drone_id} detected by ${detection.device?.name || `Device ${detection.device_id}`}`,
|
||||||
{
|
{
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
icon: '🚨',
|
icon: '🚨',
|
||||||
@@ -59,6 +77,32 @@ export const SocketProvider = ({ children }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Show priority-based notifications
|
||||||
|
const alertIcon = alertData.analysis.alertLevel >= 3 ? '🚨' :
|
||||||
|
alertData.analysis.alertLevel >= 2 ? '⚠️' : '📍';
|
||||||
|
|
||||||
|
const alertColor = alertData.analysis.alertLevel >= 3 ? 'error' :
|
||||||
|
alertData.analysis.alertLevel >= 2 ? 'warning' : 'info';
|
||||||
|
|
||||||
|
toast[alertColor === 'error' ? 'error' : alertColor === 'warning' ? 'error' : 'info'](
|
||||||
|
alertData.analysis.description,
|
||||||
|
{
|
||||||
|
duration: alertData.analysis.alertLevel >= 2 ? 10000 : 6000,
|
||||||
|
icon: alertIcon,
|
||||||
|
style: {
|
||||||
|
background: alertData.analysis.alertLevel >= 3 ? '#fee2e2' :
|
||||||
|
alertData.analysis.alertLevel >= 2 ? '#fef3c7' : '#e0f2fe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Listen for device heartbeats
|
// Listen for device heartbeats
|
||||||
newSocket.on('device_heartbeat', (heartbeat) => {
|
newSocket.on('device_heartbeat', (heartbeat) => {
|
||||||
console.log('Device heartbeat:', heartbeat);
|
console.log('Device heartbeat:', heartbeat);
|
||||||
@@ -109,14 +153,27 @@ export const SocketProvider = ({ children }) => {
|
|||||||
setRecentDetections([]);
|
setRecentDetections([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearMovementAlerts = () => {
|
||||||
|
setMovementAlerts([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDroneTracking = (droneId, deviceId) => {
|
||||||
|
const trackingKey = `${droneId}_${deviceId}`;
|
||||||
|
return droneTracking.get(trackingKey);
|
||||||
|
};
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
socket,
|
socket,
|
||||||
connected,
|
connected,
|
||||||
recentDetections,
|
recentDetections,
|
||||||
deviceStatus,
|
deviceStatus,
|
||||||
|
movementAlerts,
|
||||||
|
droneTracking,
|
||||||
joinDeviceRoom,
|
joinDeviceRoom,
|
||||||
leaveDeviceRoom,
|
leaveDeviceRoom,
|
||||||
clearRecentDetections
|
clearRecentDetections,
|
||||||
|
clearMovementAlerts,
|
||||||
|
getDroneTracking
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useSocket } from '../contexts/SocketContext';
|
import { useSocket } from '../contexts/SocketContext';
|
||||||
|
import MovementAlertsPanel from '../components/MovementAlertsPanel';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import {
|
import {
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
@@ -30,7 +31,8 @@ const Dashboard = () => {
|
|||||||
const [deviceActivity, setDeviceActivity] = useState([]);
|
const [deviceActivity, setDeviceActivity] = useState([]);
|
||||||
const [recentActivity, setRecentActivity] = useState([]);
|
const [recentActivity, setRecentActivity] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { recentDetections, deviceStatus, connected } = useSocket();
|
const [showMovementAlerts, setShowMovementAlerts] = useState(true);
|
||||||
|
const { recentDetections, deviceStatus, connected, movementAlerts } = useSocket();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
@@ -318,6 +320,70 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Movement Alerts Panel */}
|
||||||
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||||
|
<div className="xl:col-span-2">
|
||||||
|
<MovementAlertsPanel />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Movement Summary Stats */}
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||||
|
Movement Tracking
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between p-3 bg-red-50 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-red-900">Critical Alerts</div>
|
||||||
|
<div className="text-sm text-red-700">Very close approaches</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-red-600">
|
||||||
|
{movementAlerts.filter(a => a.analysis.alertLevel >= 3).length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-3 bg-orange-50 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-orange-900">High Priority</div>
|
||||||
|
<div className="text-sm text-orange-700">Approaching drones</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-orange-600">
|
||||||
|
{movementAlerts.filter(a => a.analysis.alertLevel === 2).length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-blue-900">Medium Priority</div>
|
||||||
|
<div className="text-sm text-blue-700">Movement changes</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{movementAlerts.filter(a => a.analysis.alertLevel === 1).length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Total Tracked:</span>
|
||||||
|
<span className="font-medium">{movementAlerts.length} events</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mt-1">
|
||||||
|
<span>Last Alert:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{movementAlerts.length > 0
|
||||||
|
? format(new Date(movementAlerts[0].timestamp), 'HH:mm:ss')
|
||||||
|
: 'None'
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -468,6 +468,55 @@ const DroneDetectionPopup = ({ detection, age, droneTypes }) => (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Movement Analysis */}
|
||||||
|
{detection.movement_analysis && (
|
||||||
|
<div className="pt-2 border-t border-gray-200">
|
||||||
|
<span className="font-medium text-gray-700 block mb-1">Movement Analysis:</span>
|
||||||
|
<div className="text-xs space-y-1">
|
||||||
|
<div className={`px-2 py-1 rounded ${
|
||||||
|
detection.movement_analysis.alertLevel >= 3 ? 'bg-red-100 text-red-800' :
|
||||||
|
detection.movement_analysis.alertLevel >= 2 ? 'bg-orange-100 text-orange-800' :
|
||||||
|
detection.movement_analysis.alertLevel >= 1 ? 'bg-blue-100 text-blue-800' :
|
||||||
|
'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{detection.movement_analysis.description}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{detection.movement_analysis.rssiTrend && (
|
||||||
|
<div className="flex items-center space-x-2 mt-1">
|
||||||
|
<span className="text-gray-600">Trend:</span>
|
||||||
|
<span className={`font-medium ${
|
||||||
|
detection.movement_analysis.rssiTrend.trend === 'STRENGTHENING' ? 'text-red-600' :
|
||||||
|
detection.movement_analysis.rssiTrend.trend === 'WEAKENING' ? 'text-green-600' :
|
||||||
|
'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{detection.movement_analysis.rssiTrend.trend}
|
||||||
|
{detection.movement_analysis.rssiTrend.change !== 0 && (
|
||||||
|
<span className="ml-1">
|
||||||
|
({detection.movement_analysis.rssiTrend.change > 0 ? '+' : ''}{detection.movement_analysis.rssiTrend.change.toFixed(1)}dB)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{detection.movement_analysis.proximityLevel && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-gray-600">Proximity:</span>
|
||||||
|
<span className={`px-1 py-0.5 rounded text-xs font-medium ${
|
||||||
|
detection.movement_analysis.proximityLevel === 'VERY_CLOSE' ? 'bg-red-100 text-red-700' :
|
||||||
|
detection.movement_analysis.proximityLevel === 'CLOSE' ? 'bg-orange-100 text-orange-700' :
|
||||||
|
detection.movement_analysis.proximityLevel === 'MEDIUM' ? 'bg-yellow-100 text-yellow-700' :
|
||||||
|
'bg-green-100 text-green-700'
|
||||||
|
}`}>
|
||||||
|
{detection.movement_analysis.proximityLevel.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="pt-2 border-t border-gray-200">
|
<div className="pt-2 border-t border-gray-200">
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
<div>Detected by: Device {detection.device_id}</div>
|
<div>Detected by: Device {detection.device_id}</div>
|
||||||
|
|||||||
@@ -4,10 +4,32 @@ const Joi = require('joi');
|
|||||||
const { DroneDetection, Device } = require('../models');
|
const { DroneDetection, Device } = require('../models');
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
const AlertService = require('../services/alertService');
|
const AlertService = require('../services/alertService');
|
||||||
|
const DroneTrackingService = require('../services/droneTrackingService');
|
||||||
const { validateRequest } = require('../middleware/validation');
|
const { validateRequest } = require('../middleware/validation');
|
||||||
|
|
||||||
// Initialize AlertService instance
|
// Initialize services
|
||||||
const alertService = new AlertService();
|
const alertService = new AlertService();
|
||||||
|
const droneTracker = new DroneTrackingService();
|
||||||
|
|
||||||
|
// Handle movement alerts from the tracking service
|
||||||
|
droneTracker.on('movement_alert', (alertData) => {
|
||||||
|
const { io } = require('../index');
|
||||||
|
if (io) {
|
||||||
|
// Emit to dashboard with detailed movement information
|
||||||
|
io.emitToDashboard('drone_movement_alert', {
|
||||||
|
...alertData,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit to specific device room
|
||||||
|
io.emitToDevice(alertData.deviceId, 'drone_movement_alert', {
|
||||||
|
...alertData,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🚨 Movement Alert: ${alertData.analysis.description} (Drone ${alertData.droneId})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Validation schema for drone detection
|
// Validation schema for drone detection
|
||||||
const droneDetectionSchema = Joi.object({
|
const droneDetectionSchema = Joi.object({
|
||||||
@@ -45,7 +67,13 @@ router.post('/', validateRequest(droneDetectionSchema), async (req, res) => {
|
|||||||
server_timestamp: new Date()
|
server_timestamp: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit real-time update via Socket.IO
|
// Process detection through tracking service for movement analysis
|
||||||
|
const movementAnalysis = droneTracker.processDetection({
|
||||||
|
...detectionData,
|
||||||
|
server_timestamp: detection.server_timestamp
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit real-time update via Socket.IO with movement analysis
|
||||||
req.io.emit('drone_detection', {
|
req.io.emit('drone_detection', {
|
||||||
id: detection.id,
|
id: detection.id,
|
||||||
device_id: detection.device_id,
|
device_id: detection.device_id,
|
||||||
@@ -56,6 +84,9 @@ router.post('/', validateRequest(droneDetectionSchema), async (req, res) => {
|
|||||||
geo_lat: detection.geo_lat,
|
geo_lat: detection.geo_lat,
|
||||||
geo_lon: detection.geo_lon,
|
geo_lon: detection.geo_lon,
|
||||||
server_timestamp: detection.server_timestamp,
|
server_timestamp: detection.server_timestamp,
|
||||||
|
confidence_level: detection.confidence_level,
|
||||||
|
signal_duration: detection.signal_duration,
|
||||||
|
movement_analysis: movementAnalysis,
|
||||||
device: {
|
device: {
|
||||||
id: device.id,
|
id: device.id,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
@@ -229,4 +260,55 @@ router.get('/:id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/detections/tracking/active - Get active drone tracking information
|
||||||
|
router.get('/tracking/active', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const activeTracking = droneTracker.getAllActiveTracking();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
active_drones: activeTracking.length,
|
||||||
|
tracking_data: activeTracking
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching active tracking:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to fetch active tracking data',
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/detections/tracking/:droneId/:deviceId - Get specific drone tracking
|
||||||
|
router.get('/tracking/:droneId/:deviceId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { droneId, deviceId } = req.params;
|
||||||
|
const trackingData = droneTracker.getDroneStatus(droneId, deviceId);
|
||||||
|
|
||||||
|
if (!trackingData) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'No tracking data found for this drone-device combination'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: trackingData
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching drone tracking:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to fetch drone tracking data',
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
285
server/services/droneTrackingService.js
Normal file
285
server/services/droneTrackingService.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
class DroneTrackingService extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.droneHistory = new Map(); // Map of drone_id -> detection history
|
||||||
|
this.droneProximityAlerts = new Map(); // Map of drone_id -> current status
|
||||||
|
this.proximityThresholds = {
|
||||||
|
VERY_CLOSE: -40, // < -40dBm
|
||||||
|
CLOSE: -50, // -40 to -50dBm
|
||||||
|
MEDIUM: -60, // -50 to -60dBm
|
||||||
|
FAR: -70, // -60 to -70dBm
|
||||||
|
VERY_FAR: -80 // -70 to -80dBm
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clean up old history every 30 minutes
|
||||||
|
setInterval(() => this.cleanupOldHistory(), 30 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
processDetection(detection) {
|
||||||
|
const droneId = detection.drone_id;
|
||||||
|
const deviceId = detection.device_id;
|
||||||
|
const droneKey = `${droneId}_${deviceId}`;
|
||||||
|
|
||||||
|
// Get or create history for this drone-device pair
|
||||||
|
if (!this.droneHistory.has(droneKey)) {
|
||||||
|
this.droneHistory.set(droneKey, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = this.droneHistory.get(droneKey);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Add current detection to history
|
||||||
|
const detectionRecord = {
|
||||||
|
timestamp: now,
|
||||||
|
rssi: detection.rssi,
|
||||||
|
geo_lat: detection.geo_lat,
|
||||||
|
geo_lon: detection.geo_lon,
|
||||||
|
confidence: detection.confidence_level,
|
||||||
|
freq: detection.freq,
|
||||||
|
signal_duration: detection.signal_duration
|
||||||
|
};
|
||||||
|
|
||||||
|
history.push(detectionRecord);
|
||||||
|
|
||||||
|
// Keep only last 50 detections per drone-device pair
|
||||||
|
if (history.length > 50) {
|
||||||
|
history.splice(0, history.length - 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze movement and proximity trends
|
||||||
|
const analysis = this.analyzeMovementTrend(history, detection);
|
||||||
|
|
||||||
|
// Emit movement alerts if significant changes detected
|
||||||
|
if (analysis.alertLevel > 0) {
|
||||||
|
this.emit('movement_alert', {
|
||||||
|
droneId,
|
||||||
|
deviceId,
|
||||||
|
detection,
|
||||||
|
analysis,
|
||||||
|
history: history.slice(-5) // Last 5 detections for context
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return analysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
analyzeMovementTrend(history, currentDetection) {
|
||||||
|
if (history.length < 2) {
|
||||||
|
return this.createAnalysis('INITIAL', 0, 'First detection');
|
||||||
|
}
|
||||||
|
|
||||||
|
const recent = history.slice(-5); // Last 5 detections
|
||||||
|
const current = recent[recent.length - 1];
|
||||||
|
const previous = recent[recent.length - 2];
|
||||||
|
|
||||||
|
// Calculate RSSI trend
|
||||||
|
const rssiTrend = this.calculateRSSITrend(recent);
|
||||||
|
const proximityLevel = this.getProximityLevel(current.rssi);
|
||||||
|
const previousProximityLevel = this.getProximityLevel(previous.rssi);
|
||||||
|
|
||||||
|
// Calculate distance trend (if we have coordinates)
|
||||||
|
let distanceTrend = null;
|
||||||
|
if (recent.length >= 2 && this.hasValidCoordinates(recent)) {
|
||||||
|
distanceTrend = this.calculateDistanceTrend(recent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate movement speed and direction
|
||||||
|
const movement = this.calculateMovementMetrics(recent);
|
||||||
|
|
||||||
|
// Determine alert level and type
|
||||||
|
let alertLevel = 0;
|
||||||
|
let alertType = 'MONITORING';
|
||||||
|
let description = 'Drone being tracked';
|
||||||
|
|
||||||
|
// High priority alerts
|
||||||
|
if (proximityLevel === 'VERY_CLOSE' && rssiTrend.trend === 'STRENGTHENING') {
|
||||||
|
alertLevel = 3;
|
||||||
|
alertType = 'CRITICAL_APPROACH';
|
||||||
|
description = `🚨 CRITICAL: Drone very close and approaching (${current.rssi}dBm)`;
|
||||||
|
} else if (proximityLevel === 'CLOSE' && rssiTrend.trend === 'STRENGTHENING') {
|
||||||
|
alertLevel = 2;
|
||||||
|
alertType = 'HIGH_APPROACH';
|
||||||
|
description = `⚠️ HIGH: Drone approaching detector (${current.rssi}dBm, trend: +${rssiTrend.change.toFixed(1)}dB)`;
|
||||||
|
} else if (rssiTrend.change > 10 && rssiTrend.trend === 'STRENGTHENING') {
|
||||||
|
alertLevel = 2;
|
||||||
|
alertType = 'RAPID_APPROACH';
|
||||||
|
description = `📈 RAPID: Drone rapidly approaching (+${rssiTrend.change.toFixed(1)}dB in ${rssiTrend.timeSpan}s)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Medium priority alerts
|
||||||
|
else if (proximityLevel !== previousProximityLevel) {
|
||||||
|
alertLevel = 1;
|
||||||
|
alertType = 'PROXIMITY_CHANGE';
|
||||||
|
description = `📍 Drone moved from ${previousProximityLevel} to ${proximityLevel} range`;
|
||||||
|
} else if (Math.abs(rssiTrend.change) > 5) {
|
||||||
|
alertLevel = 1;
|
||||||
|
alertType = rssiTrend.trend === 'STRENGTHENING' ? 'APPROACHING' : 'DEPARTING';
|
||||||
|
description = `${rssiTrend.trend === 'STRENGTHENING' ? '🔴' : '🟢'} Drone ${rssiTrend.trend.toLowerCase()} (${rssiTrend.change > 0 ? '+' : ''}${rssiTrend.change.toFixed(1)}dB)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createAnalysis(alertType, alertLevel, description, {
|
||||||
|
rssiTrend,
|
||||||
|
proximityLevel,
|
||||||
|
previousProximityLevel,
|
||||||
|
distanceTrend,
|
||||||
|
movement,
|
||||||
|
detectionCount: history.length,
|
||||||
|
timeTracked: (current.timestamp - history[0].timestamp) / 1000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateRSSITrend(detections) {
|
||||||
|
if (detections.length < 2) return { trend: 'STABLE', change: 0, timeSpan: 0 };
|
||||||
|
|
||||||
|
const latest = detections[detections.length - 1];
|
||||||
|
const oldest = detections[0];
|
||||||
|
const change = latest.rssi - oldest.rssi;
|
||||||
|
const timeSpan = (latest.timestamp - oldest.timestamp) / 1000;
|
||||||
|
|
||||||
|
let trend = 'STABLE';
|
||||||
|
if (change > 2) trend = 'STRENGTHENING';
|
||||||
|
else if (change < -2) trend = 'WEAKENING';
|
||||||
|
|
||||||
|
return { trend, change, timeSpan, rate: change / Math.max(timeSpan, 1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateDistanceTrend(detections) {
|
||||||
|
if (detections.length < 2) return null;
|
||||||
|
|
||||||
|
const distances = detections.map((d, i) => {
|
||||||
|
if (i === 0) return 0;
|
||||||
|
return this.calculateDistance(
|
||||||
|
detections[i-1].geo_lat, detections[i-1].geo_lon,
|
||||||
|
d.geo_lat, d.geo_lon
|
||||||
|
);
|
||||||
|
}).filter(d => d > 0);
|
||||||
|
|
||||||
|
if (distances.length === 0) return null;
|
||||||
|
|
||||||
|
const totalDistance = distances.reduce((sum, d) => sum + d, 0);
|
||||||
|
const avgSpeed = totalDistance / ((detections[detections.length - 1].timestamp - detections[0].timestamp) / 1000);
|
||||||
|
|
||||||
|
return { totalDistance, avgSpeed, movementPoints: distances.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateMovementMetrics(detections) {
|
||||||
|
if (detections.length < 3) return { speed: 0, direction: null, pattern: 'INSUFFICIENT_DATA' };
|
||||||
|
|
||||||
|
const movements = [];
|
||||||
|
for (let i = 1; i < detections.length; i++) {
|
||||||
|
const prev = detections[i - 1];
|
||||||
|
const curr = detections[i];
|
||||||
|
const timeDiff = (curr.timestamp - prev.timestamp) / 1000;
|
||||||
|
|
||||||
|
if (timeDiff > 0 && this.hasValidCoordinates([prev, curr])) {
|
||||||
|
const distance = this.calculateDistance(prev.geo_lat, prev.geo_lon, curr.geo_lat, curr.geo_lon);
|
||||||
|
const speed = (distance * 1000) / timeDiff; // m/s
|
||||||
|
movements.push({ distance, speed, timeDiff });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movements.length === 0) return { speed: 0, direction: null, pattern: 'STATIONARY' };
|
||||||
|
|
||||||
|
const avgSpeed = movements.reduce((sum, m) => sum + m.speed, 0) / movements.length;
|
||||||
|
const totalDistance = movements.reduce((sum, m) => sum + m.distance, 0);
|
||||||
|
|
||||||
|
// Determine movement pattern
|
||||||
|
let pattern = 'MOVING';
|
||||||
|
if (avgSpeed < 1) pattern = 'HOVERING';
|
||||||
|
else if (avgSpeed > 10) pattern = 'FAST_MOVING';
|
||||||
|
else if (totalDistance < 0.1) pattern = 'CIRCLING';
|
||||||
|
|
||||||
|
return { speed: avgSpeed, totalDistance, pattern, movements: movements.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
getProximityLevel(rssi) {
|
||||||
|
if (rssi >= this.proximityThresholds.VERY_CLOSE) return 'VERY_CLOSE';
|
||||||
|
if (rssi >= this.proximityThresholds.CLOSE) return 'CLOSE';
|
||||||
|
if (rssi >= this.proximityThresholds.MEDIUM) return 'MEDIUM';
|
||||||
|
if (rssi >= this.proximityThresholds.FAR) return 'FAR';
|
||||||
|
return 'VERY_FAR';
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateDistance(lat1, lon1, lat2, lon2) {
|
||||||
|
const R = 6371; // Earth radius in km
|
||||||
|
const dLat = this.toRad(lat2 - lat1);
|
||||||
|
const dLon = this.toRad(lon2 - lon1);
|
||||||
|
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
||||||
|
Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) *
|
||||||
|
Math.sin(dLon/2) * Math.sin(dLon/2);
|
||||||
|
return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) * R;
|
||||||
|
}
|
||||||
|
|
||||||
|
toRad(deg) {
|
||||||
|
return deg * (Math.PI/180);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasValidCoordinates(detections) {
|
||||||
|
return detections.every(d => d.geo_lat && d.geo_lon &&
|
||||||
|
Math.abs(d.geo_lat) <= 90 &&
|
||||||
|
Math.abs(d.geo_lon) <= 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
createAnalysis(alertType, alertLevel, description, details = {}) {
|
||||||
|
return {
|
||||||
|
alertType,
|
||||||
|
alertLevel,
|
||||||
|
description,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
...details
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupOldHistory() {
|
||||||
|
const cutoffTime = Date.now() - (2 * 60 * 60 * 1000); // 2 hours ago
|
||||||
|
|
||||||
|
for (const [key, history] of this.droneHistory.entries()) {
|
||||||
|
const filtered = history.filter(record => record.timestamp > cutoffTime);
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
this.droneHistory.delete(key);
|
||||||
|
} else {
|
||||||
|
this.droneHistory.set(key, filtered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDroneStatus(droneId, deviceId) {
|
||||||
|
const droneKey = `${droneId}_${deviceId}`;
|
||||||
|
const history = this.droneHistory.get(droneKey) || [];
|
||||||
|
|
||||||
|
if (history.length === 0) return null;
|
||||||
|
|
||||||
|
const latest = history[history.length - 1];
|
||||||
|
const analysis = this.analyzeMovementTrend(history, { rssi: latest.rssi });
|
||||||
|
|
||||||
|
return {
|
||||||
|
droneId,
|
||||||
|
deviceId,
|
||||||
|
lastSeen: latest.timestamp,
|
||||||
|
currentRSSI: latest.rssi,
|
||||||
|
proximityLevel: this.getProximityLevel(latest.rssi),
|
||||||
|
detectionCount: history.length,
|
||||||
|
analysis,
|
||||||
|
recentHistory: history.slice(-10)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllActiveTracking() {
|
||||||
|
const activeTracking = [];
|
||||||
|
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
|
||||||
|
|
||||||
|
for (const [key, history] of this.droneHistory.entries()) {
|
||||||
|
const latest = history[history.length - 1];
|
||||||
|
if (latest.timestamp > fiveMinutesAgo) {
|
||||||
|
const [droneId, deviceId] = key.split('_');
|
||||||
|
activeTracking.push(this.getDroneStatus(droneId, deviceId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeTracking;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DroneTrackingService;
|
||||||
Reference in New Issue
Block a user