import React, { useState, useEffect } from 'react'; import api from '../services/api'; import { format } from 'date-fns'; import { t } from '../utils/tempTranslations'; import { PlusIcon, BellIcon, CheckCircleIcon, XCircleIcon, ExclamationTriangleIcon, ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; import { EditAlertModal, DetectionDetailsModal } from '../components/AlertModals'; const Alerts = () => { const [alertRules, setAlertRules] = useState([]); const [alertLogs, setAlertLogs] = useState([]); const [alertStats, setAlertStats] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState('rules'); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [editingRule, setEditingRule] = useState(null); const [showDetectionModal, setShowDetectionModal] = useState(false); const [selectedDetection, setSelectedDetection] = useState(null); const [expandedGroups, setExpandedGroups] = useState(new Set()); useEffect(() => { fetchAlertData(); }, []); const fetchAlertData = async () => { try { const [rulesRes, logsRes, statsRes] = await Promise.all([ api.get('/alerts/rules'), api.get('/alerts/logs?limit=50'), api.get('/alerts/stats?hours=24') ]); setAlertRules(rulesRes.data?.data || []); setAlertLogs(logsRes.data?.data || []); setAlertStats(statsRes.data?.data || null); } catch (error) { console.error('Error fetching alert data:', error); // Set default values on error setAlertRules([]); setAlertLogs([]); setAlertStats(null); } finally { setLoading(false); } }; // Group alerts by alert_event_id to show related alerts together const groupAlertsByEvent = (logs) => { const grouped = {}; const ungrouped = []; logs.forEach(log => { if (log.alert_event_id) { if (!grouped[log.alert_event_id]) { grouped[log.alert_event_id] = []; } grouped[log.alert_event_id].push(log); } else { ungrouped.push(log); } }); // Convert grouped object to array of arrays, sorted by most recent alert in each group const groupedArrays = Object.values(grouped).map(group => group.sort((a, b) => new Date(b.sent_at) - new Date(a.sent_at)) ).sort((a, b) => new Date(b[0].sent_at) - new Date(a[0].sent_at)); // Add ungrouped alerts as individual groups ungrouped.forEach(log => groupedArrays.push([log])); return groupedArrays; }; const toggleGroupExpansion = (groupIndex) => { const newExpanded = new Set(expandedGroups); if (newExpanded.has(groupIndex)) { newExpanded.delete(groupIndex); } else { newExpanded.add(groupIndex); } setExpandedGroups(newExpanded); }; // Get drone type information with visual styling const getDroneTypeInfo = (detection) => { if (!detection || detection.drone_type === undefined) { return { name: 'Unknown', color: 'gray', bgColor: 'bg-gray-100', textColor: 'text-gray-600', icon: '🔍' }; } // Map drone types based on the backend droneTypes utility const droneTypeMap = { 0: { name: 'Unknown', color: 'gray', bgColor: 'bg-gray-100', textColor: 'text-gray-600', icon: '🔍' }, 1: { name: 'Generic', color: 'blue', bgColor: 'bg-blue-100', textColor: 'text-blue-800', icon: '🚁' }, 2: { name: 'Orlan', color: 'red', bgColor: 'bg-red-100', textColor: 'text-red-800', icon: '⚠️' }, 3: { name: 'Zala', color: 'orange', bgColor: 'bg-orange-100', textColor: 'text-orange-800', icon: '🔶' }, 4: { name: 'Forpost', color: 'purple', bgColor: 'bg-purple-100', textColor: 'text-purple-800', icon: '🟣' }, 5: { name: 'Inokhodets', color: 'indigo', bgColor: 'bg-indigo-100', textColor: 'text-indigo-800', icon: '🔷' }, 6: { name: 'Lancet', color: 'red', bgColor: 'bg-red-200', textColor: 'text-red-900', icon: '💥' }, 7: { name: 'Shahed', color: 'yellow', bgColor: 'bg-yellow-100', textColor: 'text-yellow-800', icon: '⚡' }, 8: { name: 'Geran', color: 'amber', bgColor: 'bg-amber-100', textColor: 'text-amber-800', icon: '🟨' }, 9: { name: 'Kub', color: 'green', bgColor: 'bg-green-100', textColor: 'text-green-800', icon: '🟢' }, 10: { name: 'X-UAV', color: 'teal', bgColor: 'bg-teal-100', textColor: 'text-teal-800', icon: '🔷' }, 11: { name: 'SuperCam', color: 'cyan', bgColor: 'bg-cyan-100', textColor: 'text-cyan-800', icon: '📷' }, 12: { name: 'Eleron', color: 'lime', bgColor: 'bg-lime-100', textColor: 'text-lime-800', icon: '🟩' }, 13: { name: 'DJI', color: 'blue', bgColor: 'bg-blue-200', textColor: 'text-blue-900', icon: '📱' }, 14: { name: 'Autel', color: 'violet', bgColor: 'bg-violet-100', textColor: 'text-violet-800', icon: '🟪' }, 15: { name: 'Parrot', color: 'emerald', bgColor: 'bg-emerald-100', textColor: 'text-emerald-800', icon: '🦜' }, 16: { name: 'Skydio', color: 'sky', bgColor: 'bg-sky-100', textColor: 'text-sky-800', icon: '☁️' }, 17: { name: 'CryptoOrlan', color: 'red', bgColor: 'bg-red-300', textColor: 'text-red-900', icon: '🔴' } }; return droneTypeMap[detection.drone_type] || droneTypeMap[0]; }; const handleDeleteRule = async (ruleId) => { if (window.confirm('Are you sure you want to delete this alert rule?')) { try { await api.delete(`/alerts/rules/${ruleId}`); fetchAlertData(); } catch (error) { console.error('Error deleting alert rule:', error); } } }; const handleEditRule = (rule) => { setEditingRule(rule); setShowEditModal(true); }; const handleViewDetection = async (detectionId) => { try { const response = await api.get(`/detections/${detectionId}`); setSelectedDetection(response.data.data); setShowDetectionModal(true); } catch (error) { console.error('Error fetching detection details:', error); } }; const getStatusIcon = (status) => { switch (status) { case 'sent': return ; case 'failed': return ; case 'pending': return ; default: return ; } }; const getPriorityColor = (priority) => { switch (priority) { case 'critical': return 'bg-red-100 text-red-800'; case 'high': return 'bg-orange-100 text-orange-800'; case 'medium': return 'bg-yellow-100 text-yellow-800'; case 'low': return 'bg-green-100 text-green-800'; default: return 'bg-gray-100 text-gray-800'; } }; if (loading) { return (
{t('alerts.loading')}
); } return (

{t('alerts.title')}

{t('alerts.description')}

{/* Alert Stats */} {alertStats && (
{alertStats.total_alerts}
{t('alerts.totalAlerts24h')}
{alertStats.sent_alerts}
{t('alerts.sentSuccessfully')}
{alertStats.failed_alerts}
{t('alerts.failed')}
{alertStats.pending_alerts}
{t('alerts.pending')}
)} {/* Tabs */}
{/* Alert Rules Tab */} {activeTab === 'rules' && (
{(alertRules?.length || 0) === 0 ? (

{t('alerts.noAlertRules')}

{t('alerts.noAlertRulesDescription')}

) : (
{(alertRules || []).map((rule) => ( ))}
{t('alerts.name')} {t('alerts.priority')} {t('alerts.channels')} {t('alerts.conditions')} {t('alerts.status')} {t('alerts.created')} {t('alerts.actions')}
{rule.name}
{rule.description && (
{rule.description}
)}
{t(`alerts.${rule.priority}`)}
{(rule.alert_channels || []).map((channel, index) => ( {channel} ))}
{rule.min_detections > 1 && (
{t('alerts.minDetections')}: {rule.min_detections}
)} {rule.time_window && (
{t('alerts.timeWindow')}: {rule.time_window}s
)} {rule.cooldown_period && (
{t('alerts.cooldown')}: {rule.cooldown_period}s
)} {rule.min_threat_level && (
{t('alerts.minThreat')}: {rule.min_threat_level}
)} {rule.drone_types && rule.drone_types.length > 0 && (
{t('alerts.droneTypes')}:
{rule.drone_types.map((typeId, index) => { const droneTypeKeys = { 0: 'consumer', 1: 'orlan', 2: 'professional', 3: 'racing', 4: 'unknown' }; return ( {t(`alerts.${droneTypeKeys[typeId] || 'unknown'}`)} {typeId === 1 && '⚠️'} ); })}
)}
{rule.is_active ? t('alerts.active') : t('alerts.inactive')}
{format(new Date(rule.created_at), 'MMM dd, yyyy')}
)}
)} {/* Alert Logs Tab */} {activeTab === 'logs' && (
{(alertLogs?.length || 0) === 0 ? (

{t('alerts.noAlertLogs')}

{t('alerts.noAlertLogsDescription')}

) : (

💡 Alert Grouping: Related alerts (SMS, email, webhook) for the same detection event are grouped together. Click the expand button (▶) or "+X more" badge to see all alerts in a group.

{groupAlertsByEvent(alertLogs || []).map((alertGroup, groupIndex) => { // Display the primary alert (first in group) const primaryAlert = alertGroup[0]; const relatedAlerts = alertGroup.slice(1); return ( {/* Show related alerts as sub-rows if expanded */} {expandedGroups.has(groupIndex) && relatedAlerts.map((alert, alertIndex) => ( ))} ); })}
{t('alerts.status')} {t('alerts.type')} {t('alerts.recipient')} {t('alerts.rule')} Drone Type {t('alerts.droneId')} {t('alerts.detection')} {t('alerts.message')} {t('alerts.sentAt')} Event ID
{relatedAlerts.length > 0 && ( )} {getStatusIcon(primaryAlert.status)} {primaryAlert.status} {relatedAlerts.length > 0 && ( )}
{primaryAlert.alert_type} {!expandedGroups.has(groupIndex) && relatedAlerts.map((alert, idx) => ( {alert.alert_type} ))}
{primaryAlert.recipient} {!expandedGroups.has(groupIndex) && relatedAlerts.length > 0 && relatedAlerts.some(a => a.recipient !== primaryAlert.recipient) && (
+{relatedAlerts.filter(a => a.recipient !== primaryAlert.recipient).length} others
)}
{primaryAlert.rule?.name || t('alerts.unknownRule')}
{(() => { const droneTypeInfo = getDroneTypeInfo(primaryAlert.detection); return (
{droneTypeInfo.icon} {droneTypeInfo.name} {droneTypeInfo.name === 'Orlan' && ( ⚠️ MILITARY )} {droneTypeInfo.name === 'Lancet' && ( 💥 KAMIKAZE )} {droneTypeInfo.name === 'CryptoOrlan' && ( 🔴 ENCRYPTED )} {droneTypeInfo.name === 'DJI' && ( 📱 COMMERCIAL )}
); })()}
{primaryAlert.detection?.drone_id ? ( {primaryAlert.detection.drone_id} ) : ( {t('alerts.na')} )}
{primaryAlert.detection_id ? ( ) : ( {t('alerts.na')} )}
{primaryAlert.message}
{format(new Date(primaryAlert.sent_at), 'MMM dd, HH:mm')}
{primaryAlert.alert_event_id ? ( {primaryAlert.alert_event_id.substring(0, 8)}... ) : ( - )}
{getStatusIcon(alert.status)} {alert.status}
{alert.alert_type}
{alert.recipient}
{alert.rule?.name || t('alerts.unknownRule')}
{(() => { const droneTypeInfo = getDroneTypeInfo(alert.detection); return (
{droneTypeInfo.icon} {droneTypeInfo.name}
); })()}
{alert.detection?.drone_id ? ( {alert.detection.drone_id} ) : ( {t('alerts.na')} )}
{alert.detection_id ? ( ) : ( {t('alerts.na')} )}
{alert.message}
{format(new Date(alert.sent_at), 'MMM dd, HH:mm')}
{alert.alert_event_id ? ( {alert.alert_event_id.substring(0, 8)}... ) : ( - )}
)}
)} {/* Create Alert Rule Modal */} {showCreateModal && ( setShowCreateModal(false)} onSave={() => { setShowCreateModal(false); fetchAlertData(); }} /> )} {showEditModal && editingRule && ( { setShowEditModal(false); setEditingRule(null); }} onSuccess={fetchAlertData} /> )} {showDetectionModal && selectedDetection && ( { setShowDetectionModal(false); setSelectedDetection(null); }} /> )}
); }; const CreateAlertRuleModal = ({ onClose, onSave }) => { const [formData, setFormData] = useState({ name: '', description: '', priority: 'medium', alert_channels: ['sms'], min_detections: 1, time_window: 300, cooldown_period: 600, device_ids: [], drone_types: [], min_rssi: '', max_rssi: '', sms_phone_number: '', webhook_url: '' }); const [saving, setSaving] = useState(false); const [devices, setDevices] = useState([]); const [droneTypes, setDroneTypes] = useState([]); const [loadingData, setLoadingData] = useState(true); useEffect(() => { fetchDevicesAndDroneTypes(); }, []); const fetchDevicesAndDroneTypes = async () => { try { const [devicesResponse, droneTypesResponse] = await Promise.all([ api.get('/devices'), api.get('/drone-types') ]); setDevices(devicesResponse.data.data || []); setDroneTypes(droneTypesResponse.data.data || []); } catch (error) { console.error('Error fetching devices and drone types:', error); } finally { setLoadingData(false); } }; const handleSubmit = async (e) => { e.preventDefault(); setSaving(true); try { const payload = { ...formData }; // Clean up empty values if (!payload.description || payload.description.trim() === '') delete payload.description; if (!payload.min_rssi) delete payload.min_rssi; if (!payload.max_rssi) delete payload.max_rssi; if (!payload.device_ids || payload.device_ids.length === 0) delete payload.device_ids; if (!payload.drone_types || payload.drone_types.length === 0) delete payload.drone_types; // Only include webhook_url if webhook channel is selected if (!payload.alert_channels || !payload.alert_channels.includes('webhook')) { delete payload.webhook_url; } await api.post('/alerts/rules', payload); onSave(); } catch (error) { console.error('Error creating alert rule:', error); } finally { setSaving(false); } }; const handleChange = (e) => { const { name, value, type } = e.target; setFormData(prev => ({ ...prev, [name]: type === 'number' ? parseInt(value) || 0 : value })); }; const handleChannelChange = (channel, checked) => { setFormData(prev => ({ ...prev, alert_channels: checked ? [...prev.alert_channels, channel] : prev.alert_channels.filter(c => c !== channel) })); }; const handleDroneTypeChange = (droneType, checked) => { setFormData(prev => ({ ...prev, drone_types: checked ? [...prev.drone_types, droneType] : prev.drone_types.filter(type => type !== droneType) })); }; const handleDeviceChange = (deviceId, checked) => { setFormData(prev => ({ ...prev, device_ids: checked ? [...prev.device_ids, deviceId] : prev.device_ids.filter(id => id !== deviceId) })); }; return (

Create Alert Rule