import React, { useState, useEffect } from 'react'; import api from '../services/api'; import { format } from 'date-fns'; import { useTranslation } from '../utils/tempTranslations'; import { PlusIcon, BellIcon, CheckCircleIcon, XCircleIcon, ExclamationTriangleIcon, ChevronDownIcon, ChevronRightIcon, XMarkIcon } from '@heroicons/react/24/outline'; import { EditAlertModal, DetectionDetailsModal } from '../components/AlertModals'; import { useDroneTypes } from '../hooks/useDroneTypes'; const Alerts = () => { const { t } = useTranslation(); // Drone types hook for dynamic drone type data const { getDroneTypeInfo: getDroneTypeInfoFromAPI, loading: droneTypesLoading } = useDroneTypes(); 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 [showAlertDetailsModal, setShowAlertDetailsModal] = useState(false); const [selectedAlertForDetails, setSelectedAlertForDetails] = 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); } }; // Show loading if either alerts or drone types are loading const isLoading = loading || droneTypesLoading; // DEBUG: Validate state integrity useEffect(() => { console.log('DEBUG: Alerts state check', { alertRules: alertRules?.length, alertLogs: alertLogs?.length, alertStats: alertStats ? 'present' : 'null', invalidRules: alertRules?.filter(rule => typeof rule.alert_channels === 'object' && !Array.isArray(rule.alert_channels)) }); }, [alertRules, alertLogs, alertStats]); // 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 (now using dynamic API data) const getDroneTypeInfo = (detection) => { if (!detection || detection.drone_type === undefined) { return { name: 'Unknown', color: 'gray', bgColor: 'bg-gray-100', textColor: 'text-gray-600', icon: '🔍' }; } // Use the dynamic API data from the hook return getDroneTypeInfoFromAPI(detection.drone_type); }; const handleDeleteRule = async (ruleId) => { if (window.confirm(t('alerts.deleteRule') + '?')) { 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); } }; // Parse error message JSON to extract meaningful information const parseErrorMessage = (errorMessage) => { if (!errorMessage) return null; try { // Check if it's a success message if (errorMessage.startsWith('Success:')) { const jsonPart = errorMessage.substring(8).trim(); const successData = JSON.parse(jsonPart); return { type: 'success', data: successData }; } // Check if it's a webhook network error if (errorMessage.startsWith('Webhook network error:')) { const jsonPart = errorMessage.substring(22).trim(); const errorData = JSON.parse(jsonPart); return { type: 'webhook_error', data: errorData }; } // Check if it's an SMS error if (errorMessage.startsWith('SMS error:')) { const jsonPart = errorMessage.substring(10).trim(); const errorData = JSON.parse(jsonPart); return { type: 'sms_error', data: errorData }; } // Check if it's an email error if (errorMessage.startsWith('Email error:')) { const jsonPart = errorMessage.substring(12).trim(); const errorData = JSON.parse(jsonPart); return { type: 'email_error', data: errorData }; } // Try to parse as direct JSON const parsed = JSON.parse(errorMessage); return { type: 'parsed_json', data: parsed }; } catch (error) { // If parsing fails, return the raw message return { type: 'raw', data: { message: errorMessage } }; } }; const handleViewAlertDetails = (alert) => { setSelectedAlertForDetails(alert); setShowAlertDetailsModal(true); }; 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 (isLoading) { return (
{droneTypesLoading ? t('common.loading') : 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}
)}
{(() => { // DEBUG: Check if priority is an object if (typeof rule.priority === 'object' && rule.priority !== null) { console.error('DEBUG: Priority is an object:', rule.priority, 'for rule:', rule.id); return 'Invalid Priority'; } return t(`alerts.${rule.priority}`); })()}
{(() => { // Normalize alert_channels - ensure it's always an array let alertChannels = rule.alert_channels || []; // DEBUG: Add debugging to catch object rendering if (typeof alertChannels === 'object' && alertChannels !== null) { const hasKeys = Object.keys(alertChannels).some(key => ['sms', 'webhook', 'email'].includes(key)); if (hasKeys) { console.log('DEBUG: Found problematic alert_channels object:', alertChannels, 'for rule:', rule.id); } } if (typeof alertChannels === 'object' && !Array.isArray(alertChannels)) { // Convert object like {sms: true, webhook: false, email: true} to array alertChannels = Object.keys(alertChannels).filter(key => alertChannels[key]); } if (!Array.isArray(alertChannels)) { alertChannels = []; // fallback to empty array } // DEBUG: Ensure we're only rendering strings return alertChannels.map((channel, index) => { if (typeof channel !== 'string') { console.error('DEBUG: Non-string channel detected:', channel, typeof channel); return null; } return ( {channel} ); }).filter(Boolean); })()}
{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) => { // Use the dynamic API data instead of hardcoded mapping const droneInfo = getDroneTypeInfoFromAPI(typeId); return ( {droneInfo.name} {droneInfo.warning && '⚠️'} ); })}
)}
{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')} Alert Details {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.warning && ( {droneTypeInfo.warning} )}
); })()}
{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} {droneTypeInfo.warning && ( {droneTypeInfo.warning} )}
); })()}
{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); }} /> )} {showAlertDetailsModal && selectedAlertForDetails && ( { setShowAlertDetailsModal(false); setSelectedAlertForDetails(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 => { // Ensure alert_channels is always an array const currentChannels = Array.isArray(prev.alert_channels) ? prev.alert_channels : []; return { ...prev, alert_channels: checked ? [...currentChannels, channel] : currentChannels.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