const twilio = require('twilio'); const { AlertRule, AlertLog, User, Device, DroneDetection } = require('../models'); const { Op } = require('sequelize'); const { getDroneTypeInfo } = require('../utils/droneTypes'); class AlertService { constructor() { this.twilioClient = null; this.twilioPhone = null; this.twilioEnabled = false; this.activeAlerts = new Map(); // Track active alerts for clear notifications this.initializeTwilio(); } // RSSI-based threat assessment for security installations assessThreatLevel(rssi, droneType) { // RSSI typically ranges from -30 (very close) to -100 (very far) // For 15km range detection, we need to establish threat zones for sensitive facilities let threatLevel = 'low'; let estimatedDistance = 0; let description = ''; let actionRequired = false; // Convert RSSI to estimated distance (rough calculation) // Formula: Distance (m) = 10^((RSSI_at_1m - RSSI) / (10 * n)) // Where n = path loss exponent (typically 2-4 for outdoor environments) const rssiAt1m = -30; // Typical RSSI at 1 meter const pathLossExponent = 3; // Outdoor environment with obstacles estimatedDistance = Math.pow(10, (rssiAt1m - rssi) / (10 * pathLossExponent)); // Threat level assessment based on distance zones for sensitive facilities if (rssi >= -40) { // Very close: 0-50 meters - CRITICAL THREAT threatLevel = 'critical'; description = 'IMMEDIATE THREAT: Drone within security perimeter (0-50m)'; actionRequired = true; } else if (rssi >= -55) { // Close: 50-200 meters - HIGH THREAT threatLevel = 'high'; description = 'HIGH THREAT: Drone approaching facility (50-200m)'; actionRequired = true; } else if (rssi >= -70) { // Medium: 200-1000 meters - MEDIUM THREAT threatLevel = 'medium'; description = 'MEDIUM THREAT: Drone in facility vicinity (200m-1km)'; actionRequired = false; } else if (rssi >= -85) { // Far: 1-5 kilometers - LOW THREAT threatLevel = 'low'; description = 'LOW THREAT: Drone detected at distance (1-5km)'; actionRequired = false; } else { // Very far: 5-15 kilometers - MONITORING ONLY threatLevel = 'monitoring'; description = 'MONITORING: Drone detected at long range (5-15km)'; actionRequired = false; } // Get drone type information using our comprehensive mapping const droneTypeInfo = getDroneTypeInfo(droneType); // Store original distance-based assessment const distanceBasedThreat = threatLevel; // Adjust threat level based on drone type and category if (droneTypeInfo.category.includes('Military') && droneTypeInfo.name !== 'Unknown') { // Special handling for known military drones - only escalate if not at extreme distance if (rssi >= -85 && (droneTypeInfo.name === 'Orlan' || droneTypeInfo.name === 'Zala' || droneTypeInfo.name === 'Eleron')) { // Military drones within reasonable detection range get escalated if (distanceBasedThreat === 'monitoring') threatLevel = 'low'; else if (distanceBasedThreat === 'low') threatLevel = 'medium'; else if (distanceBasedThreat === 'medium') threatLevel = 'high'; else if (distanceBasedThreat === 'high') threatLevel = 'critical'; description = `MILITARY THREAT: ${droneTypeInfo.name.toUpperCase()} DETECTED - ENHANCED RESPONSE REQUIRED`; actionRequired = (threatLevel !== 'low' && threatLevel !== 'monitoring'); console.log(`🚨 MILITARY DRONE DETECTED: ${droneTypeInfo.name} - Escalated to ${threatLevel} (RSSI: ${rssi})`); } else { // For very distant military drones (RSSI < -85), preserve distance-based assessment description += ` - ${droneTypeInfo.name.toUpperCase()} MILITARY DRONE DETECTED (DISTANT)`; console.log(`🚨 MILITARY DRONE DETECTED: ${droneTypeInfo.name} - Using distance-based threat level: ${threatLevel} (RSSI: ${rssi})`); } } else if (droneTypeInfo.threat_level === 'high' || droneTypeInfo.category.includes('Professional')) { // Professional/Commercial drone - escalate threat one level from distance-based if (distanceBasedThreat === 'monitoring') threatLevel = 'low'; else if (distanceBasedThreat === 'low') threatLevel = 'medium'; else if (distanceBasedThreat === 'medium') threatLevel = 'high'; description += ` - ${droneTypeInfo.name.toUpperCase()} DETECTED`; actionRequired = (threatLevel !== 'low' && threatLevel !== 'monitoring'); } else if (droneTypeInfo.category.includes('Racing')) { // Racing/Fast drone - escalate if close if (rssi >= -55) { if (distanceBasedThreat === 'monitoring') threatLevel = 'low'; else if (distanceBasedThreat === 'low') threatLevel = 'medium'; else if (distanceBasedThreat === 'medium') threatLevel = 'high'; description += ` - HIGH-SPEED ${droneTypeInfo.name.toUpperCase()} DETECTED`; actionRequired = true; } } return { level: threatLevel, estimatedDistance: Math.round(estimatedDistance), rssi, droneType: droneTypeInfo.name, droneCategory: droneTypeInfo.category, threatLevel: droneTypeInfo.threat_level, description, requiresImmediateAction: actionRequired, priority: threatLevel === 'critical' ? 1 : threatLevel === 'high' ? 2 : threatLevel === 'medium' ? 3 : 4 }; } initializeTwilio() { // Check if Twilio credentials are provided const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const phoneNumber = process.env.TWILIO_PHONE_NUMBER; // If any Twilio credential is missing or empty, disable SMS functionality if (!accountSid || !authToken || !phoneNumber || accountSid.trim() === '' || authToken.trim() === '' || phoneNumber.trim() === '') { console.log('šŸ“± Twilio credentials not configured - SMS alerts disabled (Development Mode)'); console.log('ā„¹ļø To enable SMS alerts, set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER'); this.twilioEnabled = false; this.twilioClient = null; return; } // Validate Twilio Account SID format (only if provided) if (accountSid && !accountSid.startsWith('AC')) { console.log('āš ļø Invalid Twilio Account SID format - SMS alerts disabled (Development Mode)'); console.log('ā„¹ļø Account SID must start with "AC"'); this.twilioEnabled = false; this.twilioClient = null; return; } try { this.twilioClient = twilio(accountSid, authToken); this.twilioPhone = phoneNumber; this.twilioEnabled = true; console.log('šŸ“± Twilio SMS service initialized successfully'); } catch (error) { console.error('āŒ Failed to initialize Twilio:', error.message); console.log('šŸ“± SMS alerts disabled due to Twilio initialization error'); this.twilioEnabled = false; this.twilioClient = null; } } async processAlert(detection) { try { // Skip alert processing for drone type 0 (None) - no actual detection if (detection.drone_type === 0) { console.log(`šŸ” Skipping alert processing for drone type 0 (None) - detection ${detection.id}`); return; } console.log(`šŸ” Processing alert for detection ${detection.id}`); // Assess threat level based on RSSI and drone type const threatAssessment = this.assessThreatLevel(detection.rssi, detection.drone_type); console.log('āš ļø Threat assessment:', threatAssessment); // Update detection with threat assessment await detection.update({ processed: true, threat_level: threatAssessment.level, estimated_distance: threatAssessment.estimatedDistance }); // Get all active alert rules const alertRules = await AlertRule.findAll({ where: { is_active: true }, include: [{ model: User, as: 'user', where: { is_active: true } }], order: [ // Order by priority: critical, high, medium, low ['priority', 'DESC'] ] }); console.log(`šŸ“‹ Found ${alertRules.length} active alert rules`); // ENHANCED: Filter rules that match this detection and find the highest priority one const matchingRules = []; for (const rule of alertRules) { if (await this.shouldTriggerAlert(rule, detection, threatAssessment)) { matchingRules.push(rule); console.log(`āœ… Rule "${rule.name}" matches (priority: ${rule.priority})`); } else { console.log(`āŒ Rule "${rule.name}" does not match conditions`); } } if (matchingRules.length === 0) { console.log('šŸ“­ No alert rules match this detection'); return; } // PRIORITY-BASED DEDUPLICATION: Only trigger the highest priority rule const highestPriorityRule = matchingRules[0]; // Already sorted by priority DESC console.log(`šŸŽÆ ALERT DEDUPLICATION: ${matchingRules.length} rules matched, triggering only highest priority:`); console.log(` Selected Rule: "${highestPriorityRule.name}" (priority: ${highestPriorityRule.priority})`); if (matchingRules.length > 1) { console.log(` Skipped Rules:`); matchingRules.slice(1).forEach(rule => { console.log(` - "${rule.name}" (priority: ${rule.priority})`); }); } // Trigger only the highest priority alert await this.triggerAlert(highestPriorityRule, detection, threatAssessment); // Track active alert for potential clear notification const alertKey = `${highestPriorityRule.id}-${detection.device_id}`; this.activeAlerts.set(alertKey, { rule: highestPriorityRule, detection, threatAssessment, alertTime: new Date() }); } catch (error) { console.error('Error processing alert:', error); throw error; } } async shouldTriggerAlert(rule, detection, threatAssessment) { try { console.log(`šŸ” Evaluating rule "${rule.name}" for detection from device ${detection.device_id}`); // PRIORITY 1: Device-specific filter (most important) if (rule.device_ids && rule.device_ids.length > 0) { if (!rule.device_ids.includes(detection.device_id)) { console.log(`āŒ Rule "${rule.name}": Device ${detection.device_id} not in allowed devices [${rule.device_ids.join(', ')}]`); return false; } else { console.log(`āœ… Rule "${rule.name}": Device ${detection.device_id} is in allowed devices`); } } else { console.log(`āœ… Rule "${rule.name}": No device filter (applies to all devices)`); } // PRIORITY 2: Critical threats and Orlan drones (security overrides) const isOrlanDrone = detection.drone_type === 1; const isCriticalThreat = threatAssessment.level === 'critical'; if (isOrlanDrone) { console.log(`🚨 ORLAN DRONE DETECTED - Rule "${rule.name}" will trigger (security override)`); return true; } if (isCriticalThreat) { console.log(`🚨 CRITICAL THREAT DETECTED - Rule "${rule.name}" will trigger (security override)`); return true; } // PRIORITY 3: Threat level requirements if (rule.min_threat_level) { const threatLevels = { 'monitoring': 0, 'low': 1, 'medium': 2, 'high': 3, 'critical': 4 }; const requiredLevel = threatLevels[rule.min_threat_level] || 0; const currentLevel = threatLevels[threatAssessment.level] || 0; if (currentLevel < requiredLevel) { console.log(`āŒ Rule "${rule.name}": Threat level ${threatAssessment.level} below minimum ${rule.min_threat_level}`); return false; } else { console.log(`āœ… Rule "${rule.name}": Threat level ${threatAssessment.level} meets minimum ${rule.min_threat_level}`); } } // PRIORITY 4: Drone type filter if (rule.drone_types && rule.drone_types.length > 0 && !rule.drone_types.includes(detection.drone_type)) { console.log(`āŒ Rule "${rule.name}": Drone type ${detection.drone_type} not in allowed types [${rule.drone_types.join(', ')}]`); return false; } // PRIORITY 5: RSSI thresholds (distance-based) if (rule.min_rssi && detection.rssi < rule.min_rssi) { console.log(`āŒ Rule "${rule.name}": RSSI ${detection.rssi} below minimum ${rule.min_rssi}`); return false; } if (rule.max_rssi && detection.rssi > rule.max_rssi) { console.log(`āŒ Rule "${rule.name}": RSSI ${detection.rssi} above maximum ${rule.max_rssi}`); return false; } // PRIORITY 6: Distance requirements if (rule.max_distance && threatAssessment.estimatedDistance > rule.max_distance) { console.log(`āŒ Rule "${rule.name}": Distance ${threatAssessment.estimatedDistance}m exceeds maximum ${rule.max_distance}m`); return false; } // PRIORITY 7: Frequency ranges if (rule.frequency_ranges && rule.frequency_ranges.length > 0) { const inRange = rule.frequency_ranges.some(range => detection.freq >= range.min && detection.freq <= range.max ); if (!inRange) { console.log(`āŒ Rule "${rule.name}": Frequency ${detection.freq}MHz not in allowed ranges`); return false; } } // PRIORITY 8: Time window and minimum detections if (rule.min_detections > 1) { const timeWindowStart = new Date(Date.now() - rule.time_window * 1000); const recentDetections = await DroneDetection.count({ where: { device_id: detection.device_id, drone_id: detection.drone_id, server_timestamp: { [Op.gte]: timeWindowStart } } }); if (recentDetections < rule.min_detections) { console.log(`āŒ Rule "${rule.name}": Only ${recentDetections} detections, need ${rule.min_detections}`); return false; } } // PRIORITY 9: Cooldown period check if (rule.cooldown_period > 0) { const cooldownStart = new Date(Date.now() - rule.cooldown_period * 1000); const recentAlert = await AlertLog.findOne({ where: { alert_rule_id: rule.id, status: 'sent', sent_at: { [Op.gte]: cooldownStart } }, include: [{ model: DroneDetection, as: 'detection', where: { device_id: detection.device_id, drone_id: detection.drone_id } }] }); if (recentAlert) { console.log(`āŒ Rule "${rule.name}": Still in cooldown period`); return false; } } // PRIORITY 10: Active hours and days if (rule.active_hours || rule.active_days) { const now = new Date(); const currentHour = now.getHours(); const currentMinute = now.getMinutes(); const currentTime = currentHour * 60 + currentMinute; const currentDay = now.getDay() || 7; // Convert Sunday from 0 to 7 if (rule.active_days && !rule.active_days.includes(currentDay)) { console.log(`āŒ Rule "${rule.name}": Not active on day ${currentDay}`); return false; } if (rule.active_hours) { const startTime = this.parseTime(rule.active_hours.start); const endTime = this.parseTime(rule.active_hours.end); if (startTime !== null && endTime !== null) { if (startTime <= endTime) { // Same day range if (currentTime < startTime || currentTime > endTime) { console.log(`āŒ Rule "${rule.name}": Outside active hours`); return false; } } else { // Overnight range if (currentTime < startTime && currentTime > endTime) { console.log(`āŒ Rule "${rule.name}": Outside active hours (overnight)`); return false; } } } } } console.log(`āœ… Rule "${rule.name}": All conditions met - WILL TRIGGER`); return true; } catch (error) { console.error(`Error evaluating alert rule ${rule.name}:`, error); return false; } } async triggerAlert(rule, detection, threatAssessment) { try { const user = rule.user; const device = await Device.findByPk(detection.device_id); // Generate enhanced alert message with threat assessment const message = this.generateSecurityAlertMessage(detection, device, rule, threatAssessment); // SECURITY ENHANCEMENT: For critical threats, send to all available channels const channels = threatAssessment.level === 'critical' ? ['sms', 'email', 'webhook'] // Force all channels for critical threats : rule.alert_channels || ['sms']; // Send alerts through configured channels for (const channel of channels) { let alertLog = null; try { switch (channel) { case 'sms': if (rule.alert_channels.includes('sms') && rule.sms_phone_number) { alertLog = await this.sendSMSAlert(rule.sms_phone_number, message, rule, detection, threatAssessment); } break; case 'email': if (rule.alert_channels.includes('email') && (rule.email || user.email)) { alertLog = await this.sendEmailAlert(rule.email || user.email, message, rule, detection, threatAssessment); } break; case 'webhook': if (rule.alert_channels.includes('webhook') && rule.webhook_url) { alertLog = await this.sendWebhookAlert(rule.webhook_url, detection, device, rule, threatAssessment); } break; default: console.warn(`Unknown alert channel: ${channel}`); } if (alertLog) { console.log(`🚨 ${threatAssessment.level.toUpperCase()} THREAT: Alert sent via ${channel} for detection ${detection.id}`); } } catch (channelError) { console.error(`Error sending ${channel} alert:`, channelError); // Log failed alert await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: channel, recipient: channel === 'sms' ? (rule.sms_phone_number || 'SMS_CONFIGURED') : channel === 'email' ? (rule.email || user.email || 'EMAIL_CONFIGURED') : (rule.webhook_url || 'WEBHOOK_CONFIGURED'), message: message, status: 'failed', error_message: channelError.message, priority: rule.priority }); } } } catch (error) { console.error('Error triggering alert:', error); throw error; } } async sendSMSAlert(phoneNumber, message, rule, detection) { // Check if Twilio is enabled if (!this.twilioEnabled || !this.twilioClient) { console.log('šŸ“± SMS alert skipped - Twilio not configured'); console.log(`šŸ“± Would have sent to ${phoneNumber}:`); console.log(`šŸ“± Message: ${message}`); return await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: 'sms', recipient: phoneNumber, message: message, status: 'failed', sent_at: new Date(), external_id: null, priority: rule.priority, error_message: 'SMS service not configured' }); } try { console.log(`šŸ“± Sending SMS alert to ${phoneNumber} for rule: ${rule.name}`); console.log(`šŸ“± Message: ${message}`); const twilioMessage = await this.twilioClient.messages.create({ body: message, from: this.twilioPhone, to: phoneNumber }); console.log(`āœ… SMS sent successfully to ${phoneNumber}: ${twilioMessage.sid}`); return await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: 'sms', recipient: phoneNumber, message: message, status: 'sent', sent_at: new Date(), external_id: twilioMessage.sid, priority: rule.priority }); } catch (error) { console.error(`āŒ Failed to send SMS to ${phoneNumber}:`, error.message); return await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: 'sms', recipient: phoneNumber, message: message, status: 'failed', sent_at: new Date(), external_id: null, priority: rule.priority, error_message: error.message }); } } async sendEmailAlert(email, message, rule, detection) { // Email implementation would go here // For now, just log the alert console.log(`Email alert would be sent to ${email}: ${message}`); return await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: 'email', recipient: email, message: message, status: 'sent', sent_at: new Date(), priority: rule.priority }); } async sendWebhookAlert(webhookUrl, detection, device, rule) { const payload = { event: 'drone_detection', timestamp: new Date().toISOString(), detection: { id: detection.id, device_id: detection.device_id, drone_id: detection.drone_id, drone_type: detection.drone_type, rssi: detection.rssi, freq: detection.freq, geo_lat: detection.geo_lat, geo_lon: detection.geo_lon, server_timestamp: detection.server_timestamp }, device: { id: device.id, name: device.name, geo_lat: device.geo_lat, geo_lon: device.geo_lon, location_description: device.location_description }, alert_rule: { id: rule.id, name: rule.name, priority: rule.priority } }; const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'DroneDetectionSystem/1.0' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`Webhook failed with status ${response.status}`); } return await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: 'webhook', recipient: webhookUrl, message: JSON.stringify(payload), status: 'sent', sent_at: new Date(), priority: rule.priority }); } generateAlertMessage(detection, device, rule) { const deviceName = device.name || `Device ${device.id}`; const location = device.location_description || `${device.geo_lat}, ${device.geo_lon}` || 'Unknown location'; return `🚨 DRONE ALERT: Drone detected by ${deviceName} at ${location}. ` + `Drone ID: ${detection.drone_id}, Frequency: ${detection.freq}MHz, ` + `RSSI: ${detection.rssi}dBm. Time: ${new Date().toLocaleString()}`; } // SECURITY ENHANCEMENT: Enhanced message generation with threat assessment generateSecurityAlertMessage(detection, device, rule, threatAssessment) { const timestamp = new Date().toLocaleString('sv-SE', { timeZone: 'Europe/Stockholm' }); const deviceName = device ? device.name : `Device ${detection.device_id}`; const location = device ? device.location : 'Unknown location'; // Create security-focused alert message let message = `🚨 SECURITY ALERT 🚨\n`; message += `THREAT LEVEL: ${threatAssessment.level.toUpperCase()}\n`; message += `${threatAssessment.description}\n\n`; message += `šŸ“ LOCATION: ${location}\n`; message += `šŸ”§ DEVICE: ${deviceName}\n`; message += `šŸ“ DISTANCE: ~${threatAssessment.estimatedDistance}m\n`; message += `šŸ“¶ SIGNAL: ${detection.rssi} dBm\n`; message += `🚁 DRONE TYPE: ${threatAssessment.droneType}\n`; message += `ā° TIME: ${timestamp}\n`; if (threatAssessment.requiresImmediateAction) { message += `\nāš ļø IMMEDIATE ACTION REQUIRED\n`; message += `Security personnel should respond immediately.`; } return message; } parseTime(timeString) { if (!timeString) return null; const match = timeString.match(/^(\d{1,2}):(\d{2})$/); if (!match) return null; const hours = parseInt(match[1]); const minutes = parseInt(match[2]); if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { return null; } return hours * 60 + minutes; } // Check for cleared alerts (call this periodically) async checkClearedAlerts() { try { const now = new Date(); const clearThreshold = 5 * 60 * 1000; // 5 minutes without detection = cleared for (const [alertKey, alertData] of this.activeAlerts.entries()) { const timeSinceAlert = now - alertData.alertTime; if (timeSinceAlert > clearThreshold) { // Check if there are any recent detections from this device const recentDetections = await DroneDetection.count({ where: { device_id: alertData.detection.device_id, device_timestamp: { [Op.gte]: new Date(now - clearThreshold) } } }); if (recentDetections === 0) { await this.sendClearAlert(alertData); this.activeAlerts.delete(alertKey); } } } } catch (error) { console.error('Error checking cleared alerts:', error); } } async sendClearAlert(alertData) { const { rule, detection } = alertData; if (rule.alert_channels.includes('sms') && rule.sms_phone_number) { const device = await Device.findByPk(detection.device_id); const clearMessage = this.generateClearMessage(device, rule); console.log(`🟢 ALERT CLEARED - Sending clear notification for ${device.name} to ${rule.sms_phone_number}`); await this.sendSMSAlert(rule.sms_phone_number, clearMessage, rule, detection, null); } } generateClearMessage(device, rule) { return `🟢 ALL CLEAR 🟢\n\n` + `šŸ“ LOCATION: ${device.location_description || device.name}\n` + `šŸ”§ DEVICE: ${device.name}\n` + `ā° TIME: ${new Date().toLocaleString('sv-SE')}\n\n` + `āœ… No drone activity detected for 5+ minutes.\n` + `šŸ›”ļø Area is secure.`; } /** * Check alert rules for a detection and trigger alerts if needed * @param {Object} detection - The drone detection object * @returns {Promise} - Array of triggered alerts */ async checkAlertRules(detection) { try { // Get the device to determine tenant context const device = await Device.findByPk(detection.device_id); if (!device) { console.log(`Device ${detection.device_id} not found for detection`); return []; } const rules = await AlertRule.findAll({ where: { tenant_id: device.tenant_id, is_active: true } }); const triggeredAlerts = []; for (const rule of rules) { let shouldTrigger = true; // Check drone type filter if (rule.drone_type !== null && rule.drone_type !== detection.drone_type) { shouldTrigger = false; } // Check minimum RSSI if (rule.min_rssi !== null && detection.rssi < rule.min_rssi) { shouldTrigger = false; } // Check maximum distance (if coordinates are available) if (rule.max_distance !== null && detection.geo_lat && detection.geo_lon) { // Calculate distance logic would go here // For now, assume within range } if (shouldTrigger) { triggeredAlerts.push({ rule_id: rule.id, rule_name: rule.name, rule: rule, detection: detection, triggered_at: new Date() }); } } return triggeredAlerts; } catch (error) { console.error('Error checking alert rules:', error); return []; } } /** * Log an alert to the database * @param {Object} rule - The alert rule that was triggered * @param {Object} detection - The detection that triggered the alert * @param {string} alertType - Type of alert (sms, email, etc.) * @param {string} recipient - Alert recipient * @returns {Promise} - The created alert log entry */ async logAlert(rule, detection, alertType = 'sms', recipient = 'test@example.com') { try { const alertLog = await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: alertType, recipient: recipient, message: `Alert triggered for ${rule.name}`, status: 'sent', sent_at: new Date() }); return alertLog; } catch (error) { console.error('Error logging alert:', error); throw error; } } /** * Process a detection alert workflow * @param {Object} detection - The drone detection * @returns {Promise} - Array of processed alerts */ async processDetectionAlert(detection) { try { const triggeredRules = await this.checkAlertRules(detection); const processedAlerts = []; for (const rule of triggeredRules) { // Assess threat level const threat = this.assessThreatLevel(detection.rssi, detection.drone_type); // Log the alert const alertLog = await this.logAlert(rule, detection); // Send notification if threshold is met if (threat.threatLevel === 'critical' || threat.threatLevel === 'high') { await this.sendSMSAlert(detection, threat, rule); } processedAlerts.push({ rule, threat, alertLog }); } return processedAlerts; } catch (error) { console.error('Error processing detection alert:', error); return []; } } /** * Clear expired alerts from active tracking * @returns {Promise} - Number of alerts cleared */ async clearExpiredAlerts() { try { const expiredCount = this.activeAlerts.size; this.activeAlerts.clear(); return expiredCount; } catch (error) { console.error('Error clearing expired alerts:', error); return 0; } } /** * Cleanup old alerts from the database * @param {number} maxAge - Maximum age in milliseconds (default: 30 days) * @returns {Promise} - Number of alerts cleaned up */ async cleanupOldAlerts(maxAge = 30 * 24 * 60 * 60 * 1000) { try { const cutoffDate = new Date(Date.now() - maxAge); const deletedCount = await AlertLog.destroy({ where: { sent_at: { [Op.lt]: cutoffDate } } }); return deletedCount; } catch (error) { console.error('Error cleaning up old alerts:', error); return 0; } } } // Export the class directly (not a singleton instance) module.exports = AlertService;