const { Device, AlertRule, AlertLog, Heartbeat, sequelize } = require('../models'); const { Op } = require('sequelize'); class DeviceHealthService { constructor() { this.isRunning = false; this.checkInterval = 5 * 60 * 1000; // Check every 5 minutes this.offlineThreshold = 30 * 60 * 1000; // 30 minutes without heartbeat = offline this.offlineDevices = new Map(); // Track devices that are already reported offline this.intervalId = null; } start() { if (this.isRunning) { console.log('⚡ Device Health Service is already running'); return; } console.log('⚡ Starting Device Health Monitoring Service'); console.log(` Check Interval: ${this.checkInterval / 60000} minutes`); console.log(` Offline Threshold: ${this.offlineThreshold / 60000} minutes`); this.isRunning = true; // Run initial check this.checkDeviceHealth(); // Schedule periodic checks this.intervalId = setInterval(() => { this.checkDeviceHealth(); }, this.checkInterval); } stop() { if (!this.isRunning) { console.log('⚡ Device Health Service is not running'); return; } console.log('⚡ Stopping Device Health Monitoring Service'); this.isRunning = false; if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } } async checkDeviceHealth() { try { console.log('🔍 Checking device health status...'); const now = new Date(); const activeDevices = await Device.findAll({ where: { is_active: true, is_approved: true }, include: [{ model: Heartbeat, as: 'heartbeats', limit: 1, order: [['received_at', 'DESC']] }] }); let onlineCount = 0; let offlineCount = 0; let newlyOfflineDevices = []; let recoveredDevices = []; for (const device of activeDevices) { const timeSinceLastHeartbeat = device.last_heartbeat ? (now - new Date(device.last_heartbeat)) / 1000 : null; const expectedInterval = device.heartbeat_interval || 300; // 5 minutes default const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < (expectedInterval * 2); const isOfflineForAlert = timeSinceLastHeartbeat && timeSinceLastHeartbeat > (this.offlineThreshold / 1000); if (isOnline) { onlineCount++; // Check if device was previously offline and is now recovered if (this.offlineDevices.has(device.id)) { recoveredDevices.push(device); this.offlineDevices.delete(device.id); } } else if (isOfflineForAlert) { offlineCount++; // Check if this is a newly offline device if (!this.offlineDevices.has(device.id)) { newlyOfflineDevices.push(device); this.offlineDevices.set(device.id, { device, offlineSince: new Date(device.last_heartbeat), alertSent: false }); } } } console.log(`📊 Device Health Summary: ${onlineCount} online, ${offlineCount} offline`); if (newlyOfflineDevices.length > 0) { console.log(`🚨 Found ${newlyOfflineDevices.length} newly offline devices`); await this.handleOfflineDevices(newlyOfflineDevices); } if (recoveredDevices.length > 0) { console.log(`✅ Found ${recoveredDevices.length} recovered devices`); await this.handleRecoveredDevices(recoveredDevices); } // Clean up old offline devices that are no longer active/approved this.cleanupOfflineDevices(activeDevices); } catch (error) { console.error('❌ Error checking device health:', error); } } async handleOfflineDevices(offlineDevices) { for (const device of offlineDevices) { try { // Find all active alert rules that could apply to device offline events // For now, we'll look for rules that monitor specific devices or all devices const deviceOfflineRules = await AlertRule.findAll({ where: { is_active: true, [Op.or]: [ // Rules that monitor all devices (device_ids is null) { device_ids: null }, // Rules that specifically include this device // Use a simple approach - convert device ID to string and check if it's in the JSON array sequelize.literal(`device_ids::text LIKE '%${device.id}%'`) ] } }); if (deviceOfflineRules.length === 0) { console.log(`⚠️ No alert rules configured for device ${device.id} (${device.name})`); continue; } // For device offline events, we'll create a special alert // since the current AlertRule model doesn't have specific device offline fields for (const rule of deviceOfflineRules) { await this.triggerDeviceOfflineAlert(rule, device); } // Mark as alert sent if (this.offlineDevices.has(device.id)) { this.offlineDevices.get(device.id).alertSent = true; } } catch (error) { console.error(`❌ Error handling offline device ${device.id}:`, error); } } } async handleRecoveredDevices(recoveredDevices) { for (const device of recoveredDevices) { try { // Find all active alert rules for this device const deviceOfflineRules = await AlertRule.findAll({ where: { is_active: true, [Op.or]: [ // Rules that monitor all devices (device_ids is null) { device_ids: null }, // Rules that specifically include this device // Use a simple approach - convert device ID to string and check if it's in the JSON array sequelize.literal(`device_ids::text LIKE '%${device.id}%'`) ] } }); // Send recovery notifications for (const rule of deviceOfflineRules) { await this.triggerDeviceRecoveryAlert(rule, device); } } catch (error) { console.error(`❌ Error handling recovered device ${device.id}:`, error); } } } async triggerDeviceOfflineAlert(rule, device) { try { const AlertService = require('./alertService'); const alertService = new AlertService(); const timeSinceLastHeartbeat = device.last_heartbeat ? Math.floor((new Date() - new Date(device.last_heartbeat)) / 1000 / 60) : 'unknown'; // Generate offline alert message const message = this.generateOfflineMessage(device, timeSinceLastHeartbeat); // Send alerts through configured channels const channels = rule.alert_channels || ['sms']; for (const channel of channels) { try { switch (channel) { case 'sms': if (rule.sms_phone_number) { await alertService.sendSMSAlert(rule.sms_phone_number, message, rule, null); console.log(`📱 Device offline SMS alert sent for ${device.name} to ${rule.sms_phone_number}`); } break; case 'email': if (rule.email || rule.user?.email) { await alertService.sendEmailAlert(rule.email || rule.user.email, message, rule, null); console.log(`📧 Device offline email alert sent for ${device.name}`); } break; case 'webhook': if (rule.webhook_url) { const webhookData = { type: 'device_offline', device: { id: device.id, name: device.name, location: device.location_description, last_heartbeat: device.last_heartbeat, time_offline_minutes: timeSinceLastHeartbeat }, rule: { id: rule.id, name: rule.name }, timestamp: new Date().toISOString() }; await alertService.sendWebhookAlert(rule.webhook_url, null, device, rule, webhookData); console.log(`🔗 Device offline webhook alert sent for ${device.name}`); } break; } } catch (channelError) { console.error(`❌ Failed to send ${channel} alert for offline device ${device.id}:`, channelError); } } } catch (error) { console.error(`❌ Error triggering offline alert for device ${device.id}:`, error); } } async triggerDeviceRecoveryAlert(rule, device) { try { const AlertService = require('./alertService'); const alertService = new AlertService(); // Generate recovery alert message const message = this.generateRecoveryMessage(device); // Send recovery alerts through configured channels const channels = rule.alert_channels || ['sms']; for (const channel of channels) { try { switch (channel) { case 'sms': if (rule.sms_phone_number) { await alertService.sendSMSAlert(rule.sms_phone_number, message, rule, null); console.log(`📱 Device recovery SMS alert sent for ${device.name} to ${rule.sms_phone_number}`); } break; case 'email': if (rule.email || rule.user?.email) { await alertService.sendEmailAlert(rule.email || rule.user.email, message, rule, null); console.log(`📧 Device recovery email alert sent for ${device.name}`); } break; case 'webhook': if (rule.webhook_url) { const webhookData = { type: 'device_recovered', device: { id: device.id, name: device.name, location: device.location_description, last_heartbeat: device.last_heartbeat }, rule: { id: rule.id, name: rule.name }, timestamp: new Date().toISOString() }; await alertService.sendWebhookAlert(rule.webhook_url, null, device, rule, webhookData); console.log(`🔗 Device recovery webhook alert sent for ${device.name}`); } break; } } catch (channelError) { console.error(`❌ Failed to send ${channel} recovery alert for device ${device.id}:`, channelError); } } } catch (error) { console.error(`❌ Error triggering recovery alert for device ${device.id}:`, error); } } generateOfflineMessage(device, timeSinceLastHeartbeat) { const deviceName = device.name || `Device ${device.id}`; const location = device.location_description || 'Unknown location'; const lastSeen = device.last_heartbeat ? new Date(device.last_heartbeat).toLocaleString('sv-SE') : 'Never'; return `🚨 DEVICE OFFLINE ALERT 🚨\n\n` + `📍 LOCATION: ${location}\n` + `🔧 DEVICE: ${deviceName}\n` + `⏰ OFFLINE FOR: ${timeSinceLastHeartbeat} minutes\n` + `📅 LAST SEEN: ${lastSeen}\n\n` + `❌ Device has stopped sending heartbeats.\n` + `🔧 Check device power, network connection, or physical access.\n\n` + `⚠️ Security monitoring may be compromised in this area.`; } generateRecoveryMessage(device) { const deviceName = device.name || `Device ${device.id}`; const location = device.location_description || 'Unknown location'; const recoveredAt = new Date().toLocaleString('sv-SE'); return `✅ DEVICE RECOVERED ✅\n\n` + `📍 LOCATION: ${location}\n` + `🔧 DEVICE: ${deviceName}\n` + `⏰ RECOVERED AT: ${recoveredAt}\n\n` + `✅ Device is now sending heartbeats again.\n` + `🛡️ Security monitoring restored for this area.`; } cleanupOfflineDevices(activeDevices) { const activeDeviceIds = new Set(activeDevices.map(d => d.id)); for (const [deviceId] of this.offlineDevices) { if (!activeDeviceIds.has(deviceId)) { this.offlineDevices.delete(deviceId); } } } getStatus() { return { isRunning: this.isRunning, checkInterval: this.checkInterval, offlineThreshold: this.offlineThreshold, offlineDevicesCount: this.offlineDevices.size, offlineDevices: Array.from(this.offlineDevices.entries()).map(([id, data]) => ({ deviceId: id, deviceName: data.device.name, offlineSince: data.offlineSince, alertSent: data.alertSent })) }; } } module.exports = DeviceHealthService;