Fix jwt-token

This commit is contained in:
2025-08-18 06:44:16 +02:00
parent c03385dc2b
commit f002130c1e
7 changed files with 505 additions and 33 deletions

View File

@@ -19,28 +19,40 @@ function App() {
<Router basename={process.env.NODE_ENV === 'production' ? '/drones' : ''}>
<div className="App">
<Toaster
position="top-right"
position="top-center"
toastOptions={{
duration: 4000,
duration: 3000, // Shorter duration for mobile
style: {
background: '#363636',
color: '#fff',
maxWidth: '90vw', // Limit width on mobile
fontSize: '14px', // Smaller text on mobile
},
success: {
duration: 3000,
duration: 2000, // Even shorter for success
iconTheme: {
primary: '#4ade80',
secondary: '#fff',
},
},
error: {
duration: 5000,
duration: 4000, // Moderate duration for errors
iconTheme: {
primary: '#ef4444',
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>

View File

@@ -14,6 +14,8 @@ export const EditAlertModal = ({ rule, onClose, onSuccess }) => {
cooldown_period: 600,
alert_channels: ['sms'],
min_threat_level: '',
drone_types: [],
device_ids: [],
sms_phone_number: '',
webhook_url: ''
});
@@ -30,6 +32,8 @@ export const EditAlertModal = ({ rule, onClose, onSuccess }) => {
cooldown_period: rule.cooldown_period || 600,
alert_channels: rule.alert_channels || ['sms'],
min_threat_level: rule.min_threat_level || '',
drone_types: rule.drone_types || [],
device_ids: rule.device_ids || [],
sms_phone_number: rule.sms_phone_number || '',
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) => {
e.preventDefault();
setSaving(true);
@@ -148,6 +161,37 @@ export const EditAlertModal = ({ rule, onClose, onSuccess }) => {
</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>
<label className="block text-sm font-medium text-gray-700 mb-1">

View File

@@ -12,8 +12,112 @@ export const SocketProvider = ({ children }) => {
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
@@ -83,26 +187,8 @@ export const SocketProvider = ({ children }) => {
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 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);
}
// Mobile-friendly notification management
handleMobileNotification(alertData);
});
// Listen for device heartbeats
@@ -164,6 +250,18 @@ export const SocketProvider = ({ children }) => {
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,
@@ -171,6 +269,8 @@ export const SocketProvider = ({ children }) => {
deviceStatus,
movementAlerts,
droneTracking,
notificationsEnabled,
toggleNotifications,
joinDeviceRoom,
leaveDeviceRoom,
clearRecentDetections,

View File

@@ -101,3 +101,47 @@ body {
.table td {
@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;
}
}

View File

@@ -257,6 +257,38 @@ const Alerts = () => {
{rule.cooldown_period && (
<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>
</td>
<td>
@@ -427,8 +459,8 @@ const CreateAlertRuleModal = ({ onClose, onSave }) => {
min_detections: 1,
time_window: 300,
cooldown_period: 600,
device_ids: null,
drone_types: null,
device_ids: [],
drone_types: [],
min_rssi: '',
max_rssi: '',
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 (
<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">
@@ -600,6 +641,37 @@ const CreateAlertRuleModal = ({ onClose, onSave }) => {
))}
</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>

View File

@@ -32,7 +32,7 @@ const Dashboard = () => {
const [recentActivity, setRecentActivity] = useState([]);
const [loading, setLoading] = useState(true);
const [showMovementAlerts, setShowMovementAlerts] = useState(true);
const { recentDetections, deviceStatus, connected, movementAlerts } = useSocket();
const { recentDetections, deviceStatus, connected, movementAlerts, notificationsEnabled, toggleNotifications } = useSocket();
useEffect(() => {
fetchDashboardData();
@@ -116,12 +116,28 @@ const Dashboard = () => {
return (
<div className="space-y-6">
{/* Stats */}
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">
System Overview
</h3>
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((item) => (
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">
System Overview
</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">
{stats.map((item) => (
<div
key={item.id}
className="relative bg-white pt-5 px-4 pb-12 sm:pt-6 sm:px-6 shadow rounded-lg overflow-hidden"