Fix jwt-token
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
|
import { MapContainer, TileLayer, Marker, Popup, Circle } from 'react-leaflet';
|
||||||
import { Icon } from 'leaflet';
|
import { Icon } from 'leaflet';
|
||||||
import { useSocket } from '../contexts/SocketContext';
|
import { useSocket } from '../contexts/SocketContext';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
@@ -45,20 +45,85 @@ const createDeviceIcon = (status, hasDetections) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Drone detection icon
|
||||||
|
const createDroneIcon = (rssi, droneType) => {
|
||||||
|
// Color based on signal strength
|
||||||
|
let color = '#ff6b6b'; // Default red
|
||||||
|
|
||||||
|
if (rssi > -50) color = '#ff4757'; // Very strong - bright red
|
||||||
|
else if (rssi > -60) color = '#ff6b6b'; // Strong - red
|
||||||
|
else if (rssi > -70) color = '#ffa726'; // Medium - orange
|
||||||
|
else if (rssi > -80) color = '#ffca28'; // Weak - yellow
|
||||||
|
else color = '#66bb6a'; // Very weak - green
|
||||||
|
|
||||||
|
return new Icon({
|
||||||
|
iconUrl: `data:image/svg+xml;base64,${btoa(`
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="28" height="28">
|
||||||
|
<circle cx="12" cy="12" r="11" fill="${color}" stroke="#fff" stroke-width="2" opacity="0.8"/>
|
||||||
|
<path d="M12 6l-2 4h4l-2-4zm0 6l-3 3h6l-3-3z" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
`)}`,
|
||||||
|
iconSize: [28, 28],
|
||||||
|
iconAnchor: [14, 14],
|
||||||
|
popupAnchor: [0, -14],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const MapView = () => {
|
const MapView = () => {
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [selectedDevice, setSelectedDevice] = useState(null);
|
const [selectedDevice, setSelectedDevice] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [mapCenter, setMapCenter] = useState([59.3293, 18.0686]); // Stockholm default
|
const [mapCenter, setMapCenter] = useState([59.3293, 18.0686]); // Stockholm default
|
||||||
const [mapZoom, setMapZoom] = useState(10);
|
const [mapZoom, setMapZoom] = useState(10);
|
||||||
|
const [showDroneDetections, setShowDroneDetections] = useState(true);
|
||||||
|
const [droneDetectionHistory, setDroneDetectionHistory] = useState([]);
|
||||||
const { recentDetections, deviceStatus } = useSocket();
|
const { recentDetections, deviceStatus } = useSocket();
|
||||||
|
|
||||||
|
// Drone types mapping
|
||||||
|
const droneTypes = {
|
||||||
|
1: "DJI Mavic",
|
||||||
|
2: "Racing Drone",
|
||||||
|
3: "DJI Phantom",
|
||||||
|
4: "Fixed Wing",
|
||||||
|
5: "Surveillance",
|
||||||
|
0: "Unknown"
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDevices();
|
fetchDevices();
|
||||||
const interval = setInterval(fetchDevices, 30000); // Refresh every 30 seconds
|
const interval = setInterval(fetchDevices, 30000); // Refresh every 30 seconds
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Update drone detection history when new detections arrive
|
||||||
|
useEffect(() => {
|
||||||
|
if (recentDetections.length > 0) {
|
||||||
|
const latestDetection = recentDetections[0];
|
||||||
|
|
||||||
|
// Add to history with timestamp for fade-out
|
||||||
|
setDroneDetectionHistory(prev => [
|
||||||
|
{
|
||||||
|
...latestDetection,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
id: `${latestDetection.device_id}-${latestDetection.drone_id}-${latestDetection.device_timestamp}`
|
||||||
|
},
|
||||||
|
...prev.slice(0, 19) // Keep last 20 detections
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}, [recentDetections]);
|
||||||
|
|
||||||
|
// Clean up old detections (fade them out after 5 minutes)
|
||||||
|
useEffect(() => {
|
||||||
|
const cleanup = setInterval(() => {
|
||||||
|
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
|
||||||
|
setDroneDetectionHistory(prev =>
|
||||||
|
prev.filter(detection => detection.timestamp > fiveMinutesAgo)
|
||||||
|
);
|
||||||
|
}, 30000); // Clean up every 30 seconds
|
||||||
|
|
||||||
|
return () => clearInterval(cleanup);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchDevices = async () => {
|
const fetchDevices = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/devices/map');
|
const response = await api.get('/devices/map');
|
||||||
@@ -90,6 +155,20 @@ const MapView = () => {
|
|||||||
return recentDetections.filter(d => d.device_id === deviceId);
|
return recentDetections.filter(d => d.device_id === deviceId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDetectionAge = (detection) => {
|
||||||
|
const ageMs = Date.now() - detection.timestamp;
|
||||||
|
const ageMinutes = ageMs / (1000 * 60);
|
||||||
|
return ageMinutes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDetectionOpacity = (detection) => {
|
||||||
|
const age = getDetectionAge(detection);
|
||||||
|
if (age < 1) return 1.0; // Full opacity for first minute
|
||||||
|
if (age < 3) return 0.8; // 80% for 1-3 minutes
|
||||||
|
if (age < 5) return 0.5; // 50% for 3-5 minutes
|
||||||
|
return 0.2; // 20% for older detections
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-96">
|
<div className="flex items-center justify-center h-96">
|
||||||
@@ -100,18 +179,38 @@ const MapView = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
<div>
|
||||||
Device Map
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
</h3>
|
Device Map
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
</h3>
|
||||||
Real-time view of all devices and their detection status
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
</p>
|
Real-time view of all devices and drone detections
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showDroneDetections}
|
||||||
|
onChange={(e) => setShowDroneDetections(e.target.checked)}
|
||||||
|
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">Show Drone Detections</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{droneDetectionHistory.length > 0 && (
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{droneDetectionHistory.length} recent detection{droneDetectionHistory.length > 1 ? 's' : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Map */}
|
{/* Map */}
|
||||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
<div className="h-96 lg:h-[600px]">
|
<div className="h-96 lg:h-[600px] relative">
|
||||||
<MapContainer
|
<MapContainer
|
||||||
center={mapCenter}
|
center={mapCenter}
|
||||||
zoom={mapZoom}
|
zoom={mapZoom}
|
||||||
@@ -156,7 +255,82 @@ const MapView = () => {
|
|||||||
</Marker>
|
</Marker>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* Drone Detection Markers */}
|
||||||
|
{showDroneDetections && droneDetectionHistory
|
||||||
|
.filter(detection => detection.geo_lat && detection.geo_lon)
|
||||||
|
.map(detection => {
|
||||||
|
const opacity = getDetectionOpacity(detection);
|
||||||
|
const age = getDetectionAge(detection);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={detection.id}>
|
||||||
|
<Marker
|
||||||
|
position={[detection.geo_lat, detection.geo_lon]}
|
||||||
|
icon={createDroneIcon(detection.rssi, detection.drone_type)}
|
||||||
|
opacity={opacity}
|
||||||
|
>
|
||||||
|
<Popup>
|
||||||
|
<DroneDetectionPopup detection={detection} age={age} droneTypes={droneTypes} />
|
||||||
|
</Popup>
|
||||||
|
</Marker>
|
||||||
|
|
||||||
|
{/* Detection range circle for recent detections */}
|
||||||
|
{age < 2 && (
|
||||||
|
<Circle
|
||||||
|
center={[detection.geo_lat, detection.geo_lon]}
|
||||||
|
radius={200} // 200m detection radius
|
||||||
|
pathOptions={{
|
||||||
|
color: detection.rssi > -60 ? '#ff4757' : '#ffa726',
|
||||||
|
fillColor: detection.rssi > -60 ? '#ff4757' : '#ffa726',
|
||||||
|
fillOpacity: 0.1 * opacity,
|
||||||
|
weight: 2,
|
||||||
|
opacity: opacity * 0.5
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|
||||||
|
{/* Map Legend */}
|
||||||
|
<div className="absolute bottom-4 left-4 bg-white bg-opacity-90 backdrop-blur-sm rounded-lg p-3 shadow-lg text-xs">
|
||||||
|
<div className="font-semibold mb-2">Legend</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||||
|
<span>Device Online</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||||
|
<span>Device Detecting</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-3 h-3 bg-gray-500 rounded-full"></div>
|
||||||
|
<span>Device Offline</span>
|
||||||
|
</div>
|
||||||
|
{showDroneDetections && (
|
||||||
|
<>
|
||||||
|
<div className="border-t border-gray-200 mt-2 pt-1">
|
||||||
|
<div className="font-medium">Drone Detections:</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-3 h-3 bg-red-500 rounded-full opacity-100"></div>
|
||||||
|
<span>Strong Signal (>-60dBm)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-3 h-3 bg-orange-500 rounded-full opacity-80"></div>
|
||||||
|
<span>Medium Signal (-60 to -70dBm)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-3 h-3 bg-green-500 rounded-full opacity-60"></div>
|
||||||
|
<span>Weak Signal (<-70dBm)</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -238,6 +412,73 @@ const DevicePopup = ({ device, status, detections }) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const DroneDetectionPopup = ({ detection, age, droneTypes }) => (
|
||||||
|
<div className="p-2 min-w-[250px]">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h4 className="font-semibold text-red-700 flex items-center space-x-1">
|
||||||
|
<span>🚨</span>
|
||||||
|
<span>Drone Detection</span>
|
||||||
|
</h4>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
|
age < 1 ? 'bg-red-100 text-red-800' :
|
||||||
|
age < 3 ? 'bg-orange-100 text-orange-800' :
|
||||||
|
'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{age < 1 ? 'LIVE' : `${Math.round(age)}m ago`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Drone ID:</span>
|
||||||
|
<div className="text-gray-900">{detection.drone_id}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Type:</span>
|
||||||
|
<div className="text-gray-900">{droneTypes[detection.drone_type] || 'Unknown'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">RSSI:</span>
|
||||||
|
<div className={`font-mono ${
|
||||||
|
detection.rssi > -50 ? 'text-red-600' :
|
||||||
|
detection.rssi > -70 ? 'text-orange-600' :
|
||||||
|
'text-green-600'
|
||||||
|
}`}>
|
||||||
|
{detection.rssi}dBm
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Frequency:</span>
|
||||||
|
<div className="text-gray-900">{detection.freq}MHz</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Confidence:</span>
|
||||||
|
<div className="text-gray-900">{(detection.confidence_level * 100).toFixed(0)}%</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">Duration:</span>
|
||||||
|
<div className="text-gray-900">{(detection.signal_duration / 1000).toFixed(1)}s</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2 border-t border-gray-200">
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
<div>Detected by: Device {detection.device_id}</div>
|
||||||
|
<div>Location: {detection.geo_lat?.toFixed(4)}, {detection.geo_lon?.toFixed(4)}</div>
|
||||||
|
<div>Time: {format(new Date(detection.device_timestamp), 'MMM dd, HH:mm:ss')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const DeviceListItem = ({ device, status, detections, onClick }) => (
|
const DeviceListItem = ({ device, status, detections, onClick }) => (
|
||||||
<div
|
<div
|
||||||
className="px-6 py-4 hover:bg-gray-50 cursor-pointer transition-colors"
|
className="px-6 py-4 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||||
|
|||||||
Reference in New Issue
Block a user