diff --git a/server/routes/health.js b/server/routes/health.js index 7479e7e..cf652e7 100644 --- a/server/routes/health.js +++ b/server/routes/health.js @@ -292,9 +292,14 @@ router.get('/devices', async (req, res) => { const deviceDetails = []; for (const device of devices) { - const lastHeartbeat = device.heartbeats && device.heartbeats[0]; - const isOnline = lastHeartbeat && - (Date.now() - new Date(lastHeartbeat.timestamp).getTime()) < 300000; // 5 minutes + // Get latest heartbeat metrics for this device + const latestHeartbeat = await Heartbeat.findOne({ + where: { device_id: device.id }, + order: [['timestamp', 'DESC']] + }); + + const isOnline = latestHeartbeat && + (Date.now() - new Date(latestHeartbeat.timestamp).getTime()) < 300000; // 5 minutes if (isOnline) { onlineDevices++; @@ -302,12 +307,29 @@ router.get('/devices', async (req, res) => { offlineDevices++; } + // Calculate uptime (time since first heartbeat) + const firstHeartbeat = await Heartbeat.findOne({ + where: { device_id: device.id }, + order: [['timestamp', 'ASC']] + }); + + let uptime = null; + if (firstHeartbeat && latestHeartbeat) { + uptime = Math.floor((new Date(latestHeartbeat.timestamp).getTime() - new Date(firstHeartbeat.timestamp).getTime()) / 1000); + } + deviceDetails.push({ id: device.id, name: device.name, status: isOnline ? 'online' : 'offline', - lastSeen: lastHeartbeat ? lastHeartbeat.timestamp : null, - uptime: lastHeartbeat ? Math.floor(Date.now() / 1000) - Math.floor(new Date(lastHeartbeat.timestamp).getTime() / 1000) : null + lastSeen: latestHeartbeat ? latestHeartbeat.timestamp : null, + uptime: uptime, + metrics: latestHeartbeat ? { + cpu_usage: latestHeartbeat.cpu_usage, + memory_usage: latestHeartbeat.memory_usage, + disk_usage: latestHeartbeat.disk_usage, + uptime: latestHeartbeat.uptime + } : null }); } @@ -350,17 +372,58 @@ router.post('/devices/:id/heartbeat', async (req, res) => { if (!device.is_approved) { return res.status(403).json({ status: 'error', - message: 'Device not approved for heartbeat reporting' + message: 'Device not approved for heartbeat reporting', + approval_required: true }); } // Validate heartbeat data format - const { status, cpu_usage, memory_usage, disk_usage } = req.body; + const { timestamp, status, cpu_usage, memory_usage, disk_usage } = req.body; if (!status || typeof status !== 'string') { return res.status(400).json({ status: 'error', - message: 'Invalid heartbeat data format - status is required' + message: 'Invalid heartbeat data format - status is required and must be a string' + }); + } + + // Validate status values + const validStatuses = ['online', 'offline', 'error', 'maintenance']; + if (!validStatuses.includes(status)) { + return res.status(400).json({ + status: 'error', + message: `Invalid status value. Must be one of: ${validStatuses.join(', ')}` + }); + } + + // Validate timestamp if provided + if (timestamp && isNaN(new Date(timestamp).getTime())) { + return res.status(400).json({ + status: 'error', + message: 'Invalid timestamp format' + }); + } + + // Validate percentage fields + const percentageFields = { cpu_usage, memory_usage, disk_usage }; + for (const [field, value] of Object.entries(percentageFields)) { + if (value !== undefined && value !== null && (typeof value !== 'number' || value < 0 || value > 100)) { + return res.status(400).json({ + status: 'error', + message: `Invalid ${field} value. Must be a number between 0 and 100` + }); + } + } + + // Check for unexpected fields + const allowedFields = ['timestamp', 'status', 'cpu_usage', 'memory_usage', 'disk_usage', 'uptime']; + const receivedFields = Object.keys(req.body); + const invalidFields = receivedFields.filter(field => !allowedFields.includes(field)); + + if (invalidFields.length > 0) { + return res.status(400).json({ + status: 'error', + message: `Invalid fields in heartbeat data: ${invalidFields.join(', ')}` }); } @@ -431,18 +494,51 @@ router.get('/metrics', async (req, res) => { await sequelize.authenticate(); const dbResponseTime = Date.now() - startTime; + // Get CPU usage + const cpuUsage = process.cpuUsage(); + + // Get database statistics + const [deviceCount] = await sequelize.query( + 'SELECT COUNT(*) as count FROM "Devices" WHERE tenant_id = :tenantId', + { + replacements: { tenantId: req.user.tenant_id }, + type: sequelize.QueryTypes.SELECT + } + ); + + const [detectionCount] = await sequelize.query( + 'SELECT COUNT(*) as count FROM "DroneDetections" WHERE tenant_id = :tenantId', + { + replacements: { tenantId: req.user.tenant_id }, + type: sequelize.QueryTypes.SELECT + } + ); + res.status(200).json({ status: 'ok', - metrics: { + system: { memory: { heapUsed: Math.round((memUsage.heapUsed / 1024 / 1024) * 100) / 100, heapTotal: Math.round((memUsage.heapTotal / 1024 / 1024) * 100) / 100, external: Math.round((memUsage.external / 1024 / 1024) * 100) / 100 }, - database: { - responseTime: dbResponseTime + cpu: { + user: cpuUsage.user, + system: cpuUsage.system }, uptime: process.uptime() + }, + database: { + responseTime: dbResponseTime, + connection_pool: { + active: sequelize.connectionManager.pool._count || 0, + idle: sequelize.connectionManager.pool._idle?.length || 0, + total: sequelize.connectionManager.pool.options.max || 10 + } + }, + statistics: { + total_devices: parseInt(deviceCount.count), + total_detections: parseInt(detectionCount.count) } });