Fix jwt-token
This commit is contained in:
@@ -134,10 +134,12 @@ class DeviceProbeSimulator:
|
|||||||
'key': str(device_id) # Use device_id as the key
|
'key': str(device_id) # Use device_id as the key
|
||||||
}
|
}
|
||||||
|
|
||||||
print(f"📡 Sending heartbeat: {payload}")
|
print(f"📡 Sending heartbeat to {self.api_base_url}/detectors")
|
||||||
|
print(f" Payload: {payload}")
|
||||||
|
print(f" Headers: {dict(self.session.headers)}")
|
||||||
|
|
||||||
response = self.session.post(
|
response = self.session.post(
|
||||||
f"{self.api_base_url}/detectors", # Updated to use detectors endpoint
|
f"{self.api_base_url}/detectors", # Sending to /detectors endpoint
|
||||||
json=payload,
|
json=payload,
|
||||||
verify=False,
|
verify=False,
|
||||||
timeout=10
|
timeout=10
|
||||||
@@ -146,11 +148,12 @@ class DeviceProbeSimulator:
|
|||||||
if response.status_code in [200, 201]:
|
if response.status_code in [200, 201]:
|
||||||
device_name = device.get('name', f'Device {device_id}')
|
device_name = device.get('name', f'Device {device_id}')
|
||||||
location = device.get('location_description', 'Unknown location')
|
location = device.get('location_description', 'Unknown location')
|
||||||
print(f"💓 Device {device_id}: Heartbeat sent successfully ({device_name} - {location})")
|
print(f"✅ Device {device_id}: Heartbeat sent successfully ({device_name} - {location})")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"❌ Failed to send heartbeat for Device {device_id}: HTTP {response.status_code}")
|
print(f"❌ Failed to send heartbeat for Device {device_id}: HTTP {response.status_code}")
|
||||||
print(f" Response: {response.text}")
|
print(f" Response headers: {dict(response.headers)}")
|
||||||
|
print(f" Response body: {response.text}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const { sequelize } = require('./models');
|
|||||||
const routes = require('./routes');
|
const routes = require('./routes');
|
||||||
const { initializeSocketHandlers } = require('./services/socketService');
|
const { initializeSocketHandlers } = require('./services/socketService');
|
||||||
const AlertService = require('./services/alertService');
|
const AlertService = require('./services/alertService');
|
||||||
|
const DeviceHealthService = require('./services/deviceHealthService');
|
||||||
|
const { initializeHealthService } = require('./routes/deviceHealth');
|
||||||
const seedDatabase = require('./seedDatabase');
|
const seedDatabase = require('./seedDatabase');
|
||||||
const errorHandler = require('./middleware/errorHandler');
|
const errorHandler = require('./middleware/errorHandler');
|
||||||
const { apiDebugMiddleware } = require('./utils/apiDebugLogger');
|
const { apiDebugMiddleware } = require('./utils/apiDebugLogger');
|
||||||
@@ -152,6 +154,25 @@ async function startServer() {
|
|||||||
|
|
||||||
console.log('🔄 Clear alert monitoring: ✅ Started');
|
console.log('🔄 Clear alert monitoring: ✅ Started');
|
||||||
|
|
||||||
|
// Initialize Device Health Monitoring Service
|
||||||
|
const deviceHealthService = new DeviceHealthService();
|
||||||
|
initializeHealthService(deviceHealthService); // Make it available to API routes
|
||||||
|
deviceHealthService.start();
|
||||||
|
console.log('🏥 Device health monitoring: ✅ Started');
|
||||||
|
|
||||||
|
// Graceful shutdown for device health service
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('SIGTERM received, shutting down gracefully');
|
||||||
|
deviceHealthService.stop();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('SIGINT received, shutting down gracefully');
|
||||||
|
deviceHealthService.stop();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`📊 Health check: http://localhost:${PORT}/health`);
|
console.log(`📊 Health check: http://localhost:${PORT}/health`);
|
||||||
console.log(`🌐 API endpoint: http://localhost:${PORT}/api`);
|
console.log(`🌐 API endpoint: http://localhost:${PORT}/api`);
|
||||||
console.log('================================================\n');
|
console.log('================================================\n');
|
||||||
|
|||||||
121
server/routes/deviceHealth.js
Normal file
121
server/routes/deviceHealth.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const DeviceHealthService = require('../services/deviceHealthService');
|
||||||
|
|
||||||
|
// Global instance (will be initialized on server start)
|
||||||
|
let deviceHealthService = null;
|
||||||
|
|
||||||
|
// Initialize the service reference
|
||||||
|
const initializeHealthService = (service) => {
|
||||||
|
deviceHealthService = service;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get device health service status
|
||||||
|
router.get('/status', (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!deviceHealthService) {
|
||||||
|
return res.status(503).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Device health service not initialized'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = deviceHealthService.getStatus();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
...status,
|
||||||
|
checkIntervalMinutes: status.checkInterval / 60000,
|
||||||
|
offlineThresholdMinutes: status.offlineThreshold / 60000
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting device health status:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to get device health status',
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start device health monitoring
|
||||||
|
router.post('/start', (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!deviceHealthService) {
|
||||||
|
return res.status(503).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Device health service not initialized'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceHealthService.start();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Device health monitoring started'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting device health service:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to start device health service',
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop device health monitoring
|
||||||
|
router.post('/stop', (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!deviceHealthService) {
|
||||||
|
return res.status(503).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Device health service not initialized'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceHealthService.stop();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Device health monitoring stopped'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping device health service:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to stop device health service',
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger manual health check
|
||||||
|
router.post('/check', async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!deviceHealthService) {
|
||||||
|
return res.status(503).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Device health service not initialized'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await deviceHealthService.checkDeviceHealth();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Manual health check completed'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error running manual health check:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to run health check',
|
||||||
|
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = { router, initializeHealthService };
|
||||||
@@ -7,6 +7,7 @@ const userRoutes = require('./user');
|
|||||||
const alertRoutes = require('./alert');
|
const alertRoutes = require('./alert');
|
||||||
const dashboardRoutes = require('./dashboard');
|
const dashboardRoutes = require('./dashboard');
|
||||||
const healthRoutes = require('./health');
|
const healthRoutes = require('./health');
|
||||||
|
const { router: deviceHealthRoutes } = require('./deviceHealth');
|
||||||
const debugRoutes = require('./debug');
|
const debugRoutes = require('./debug');
|
||||||
const detectorsRoutes = require('./detectors');
|
const detectorsRoutes = require('./detectors');
|
||||||
const detectionsRoutes = require('./detections');
|
const detectionsRoutes = require('./detections');
|
||||||
@@ -18,6 +19,7 @@ router.use('/v1/users', userRoutes);
|
|||||||
router.use('/v1/alerts', alertRoutes);
|
router.use('/v1/alerts', alertRoutes);
|
||||||
router.use('/v1/dashboard', dashboardRoutes);
|
router.use('/v1/dashboard', dashboardRoutes);
|
||||||
router.use('/v1/health', healthRoutes);
|
router.use('/v1/health', healthRoutes);
|
||||||
|
router.use('/v1/device-health', deviceHealthRoutes);
|
||||||
router.use('/v1/detectors', detectorsRoutes);
|
router.use('/v1/detectors', detectorsRoutes);
|
||||||
router.use('/v1/detections', detectionsRoutes);
|
router.use('/v1/detections', detectionsRoutes);
|
||||||
router.use('/v1/drone-types', droneTypesRoutes);
|
router.use('/v1/drone-types', droneTypesRoutes);
|
||||||
@@ -28,6 +30,7 @@ router.use('/users', userRoutes);
|
|||||||
router.use('/alerts', alertRoutes);
|
router.use('/alerts', alertRoutes);
|
||||||
router.use('/dashboard', dashboardRoutes);
|
router.use('/dashboard', dashboardRoutes);
|
||||||
router.use('/health', healthRoutes);
|
router.use('/health', healthRoutes);
|
||||||
|
router.use('/device-health', deviceHealthRoutes);
|
||||||
router.use('/debug', debugRoutes);
|
router.use('/debug', debugRoutes);
|
||||||
router.use('/detectors', detectorsRoutes);
|
router.use('/detectors', detectorsRoutes);
|
||||||
router.use('/detections', detectionsRoutes);
|
router.use('/detections', detectionsRoutes);
|
||||||
@@ -46,6 +49,7 @@ router.get('/', (req, res) => {
|
|||||||
alerts: '/api/alerts',
|
alerts: '/api/alerts',
|
||||||
dashboard: '/api/dashboard',
|
dashboard: '/api/dashboard',
|
||||||
health: '/api/health',
|
health: '/api/health',
|
||||||
|
'device-health': '/api/device-health',
|
||||||
'drone-types': '/api/drone-types'
|
'drone-types': '/api/drone-types'
|
||||||
},
|
},
|
||||||
documentation: '/api/docs'
|
documentation: '/api/docs'
|
||||||
|
|||||||
377
server/services/deviceHealthService.js
Normal file
377
server/services/deviceHealthService.js
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
const { Device, AlertRule, AlertLog, Heartbeat } = 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 monitor device offline events
|
||||||
|
const deviceOfflineRules = await AlertRule.findAll({
|
||||||
|
where: {
|
||||||
|
is_active: true,
|
||||||
|
[Op.or]: [
|
||||||
|
// Rules specifically for device offline monitoring
|
||||||
|
{ conditions: { device_offline: true } },
|
||||||
|
// Rules that include this specific device
|
||||||
|
{
|
||||||
|
[Op.and]: [
|
||||||
|
{ conditions: { device_ids: { [Op.contains]: [device.id] } } },
|
||||||
|
{ conditions: { device_offline: true } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deviceOfflineRules.length === 0) {
|
||||||
|
console.log(`⚠️ No offline alert rules configured for device ${device.id} (${device.name})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger alerts for each matching rule
|
||||||
|
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]: [
|
||||||
|
{ conditions: { device_offline: true } },
|
||||||
|
{
|
||||||
|
[Op.and]: [
|
||||||
|
{ conditions: { device_ids: { [Op.contains]: [device.id] } } },
|
||||||
|
{ conditions: { device_offline: true } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
195
test_device_health.py
Normal file
195
test_device_health.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Device Health Monitoring Test Script
|
||||||
|
Tests the device health monitoring system by checking current device status
|
||||||
|
and simulating offline/recovery scenarios.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Disable SSL warnings for self-signed certificates
|
||||||
|
import warnings
|
||||||
|
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
|
||||||
|
|
||||||
|
# Configuration from environment variables
|
||||||
|
API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:3002/api')
|
||||||
|
BASE_PATH = os.getenv('VITE_BASE_PATH', '').rstrip('/')
|
||||||
|
|
||||||
|
# If BASE_PATH is set, construct the full URL
|
||||||
|
if BASE_PATH and not API_BASE_URL.endswith('/api'):
|
||||||
|
domain = API_BASE_URL.replace('/api', '').replace('/drones/api', '').replace('/uggla/api', '')
|
||||||
|
API_BASE_URL = f"{domain}{BASE_PATH}/api"
|
||||||
|
|
||||||
|
def test_device_health_status():
|
||||||
|
"""Test device health monitoring status"""
|
||||||
|
print("🔍 Testing Device Health Monitoring System")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check device health service status
|
||||||
|
print("📊 Checking device health service status...")
|
||||||
|
response = requests.get(f"{API_BASE_URL}/device-health/status", verify=False, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data.get('success'):
|
||||||
|
status = data['data']
|
||||||
|
print(f"✅ Device Health Service Status:")
|
||||||
|
print(f" Running: {status['isRunning']}")
|
||||||
|
print(f" Check Interval: {status['checkIntervalMinutes']} minutes")
|
||||||
|
print(f" Offline Threshold: {status['offlineThresholdMinutes']} minutes")
|
||||||
|
print(f" Currently Offline Devices: {status['offlineDevicesCount']}")
|
||||||
|
|
||||||
|
if status['offlineDevices']:
|
||||||
|
print(f" Offline Devices:")
|
||||||
|
for device in status['offlineDevices']:
|
||||||
|
print(f" - Device {device['deviceId']}: {device['deviceName']}")
|
||||||
|
print(f" Offline since: {device['offlineSince']}")
|
||||||
|
print(f" Alert sent: {device['alertSent']}")
|
||||||
|
else:
|
||||||
|
print(f" ✅ All devices are online")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to get status: {data.get('message', 'Unknown error')}")
|
||||||
|
else:
|
||||||
|
print(f"❌ HTTP Error: {response.status_code}")
|
||||||
|
print(f" Response: {response.text}")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"❌ Network error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Unexpected error: {e}")
|
||||||
|
|
||||||
|
def test_manual_health_check():
|
||||||
|
"""Trigger a manual health check"""
|
||||||
|
print("\n🔧 Triggering Manual Health Check")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{API_BASE_URL}/device-health/check", verify=False, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data.get('success'):
|
||||||
|
print(f"✅ {data['message']}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed: {data.get('message', 'Unknown error')}")
|
||||||
|
else:
|
||||||
|
print(f"❌ HTTP Error: {response.status_code}")
|
||||||
|
print(f" Response: {response.text}")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"❌ Network error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Unexpected error: {e}")
|
||||||
|
|
||||||
|
def test_device_list():
|
||||||
|
"""Check current device list and their status"""
|
||||||
|
print("\n📱 Checking Device List and Status")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_BASE_URL}/devices", verify=False, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data.get('success') and data.get('data'):
|
||||||
|
devices = data['data']
|
||||||
|
print(f"📊 Found {len(devices)} devices:")
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
status = device.get('stats', {}).get('status', 'unknown')
|
||||||
|
last_heartbeat = device.get('last_heartbeat')
|
||||||
|
time_since = device.get('stats', {}).get('time_since_last_heartbeat')
|
||||||
|
|
||||||
|
status_icon = "✅" if status == "online" else "❌" if status == "offline" else "⚠️"
|
||||||
|
|
||||||
|
print(f" {status_icon} Device {device['id']}: {device.get('name', 'Unnamed')}")
|
||||||
|
print(f" Status: {status}")
|
||||||
|
print(f" Location: {device.get('location_description', 'No location')}")
|
||||||
|
print(f" Active: {device.get('is_active', False)}")
|
||||||
|
print(f" Approved: {device.get('is_approved', False)}")
|
||||||
|
|
||||||
|
if last_heartbeat:
|
||||||
|
print(f" Last Heartbeat: {last_heartbeat}")
|
||||||
|
else:
|
||||||
|
print(f" Last Heartbeat: Never")
|
||||||
|
|
||||||
|
if time_since is not None:
|
||||||
|
print(f" Time Since Last Heartbeat: {time_since:.0f} seconds")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
print(f"❌ No devices found or invalid response")
|
||||||
|
else:
|
||||||
|
print(f"❌ HTTP Error: {response.status_code}")
|
||||||
|
print(f" Response: {response.text}")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"❌ Network error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Unexpected error: {e}")
|
||||||
|
|
||||||
|
def test_alert_rules():
|
||||||
|
"""Check if there are any device offline alert rules configured"""
|
||||||
|
print("\n⚠️ Checking Device Offline Alert Rules")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_BASE_URL}/alerts/rules", verify=False, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data.get('success') and data.get('data'):
|
||||||
|
rules = data['data']
|
||||||
|
offline_rules = []
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
conditions = rule.get('conditions', {})
|
||||||
|
if conditions.get('device_offline'):
|
||||||
|
offline_rules.append(rule)
|
||||||
|
|
||||||
|
if offline_rules:
|
||||||
|
print(f"📋 Found {len(offline_rules)} device offline alert rules:")
|
||||||
|
for rule in offline_rules:
|
||||||
|
print(f" 📌 Rule: {rule['name']}")
|
||||||
|
print(f" Description: {rule.get('description', 'No description')}")
|
||||||
|
print(f" Active: {rule.get('is_active', False)}")
|
||||||
|
print(f" Channels: {rule.get('alert_channels', [])}")
|
||||||
|
if rule.get('sms_phone_number'):
|
||||||
|
print(f" SMS: {rule['sms_phone_number']}")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
print("⚠️ No device offline alert rules found!")
|
||||||
|
print(" You may want to create alert rules for device offline monitoring.")
|
||||||
|
else:
|
||||||
|
print(f"❌ No alert rules found")
|
||||||
|
else:
|
||||||
|
print(f"❌ HTTP Error: {response.status_code}")
|
||||||
|
print(f" Response: {response.text}")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"❌ Network error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Unexpected error: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🏥 DEVICE HEALTH MONITORING TEST")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"📡 API URL: {API_BASE_URL}")
|
||||||
|
print(f"⏰ Test Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
test_device_health_status()
|
||||||
|
test_device_list()
|
||||||
|
test_alert_rules()
|
||||||
|
test_manual_health_check()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("🏁 Device Health Monitoring Test Complete")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user