Files
drone-detector/server/routes/dashboard.js
2025-09-13 20:58:09 +02:00

361 lines
11 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { DroneDetection, Device, Heartbeat, Tenant } = require('../models');
const { Op } = require('sequelize');
const { sequelize } = require('../models');
const { authenticateToken } = require('../middleware/auth');
const { MultiTenantAuth } = require('../middleware/multiTenantAuth');
// GET /api/dashboard/overview - Get dashboard overview statistics
router.get('/overview', authenticateToken, async (req, res) => {
try {
const { hours = 24 } = req.query;
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
// Initialize multi-tenant auth to determine tenant
const multiTenantAuth = new MultiTenantAuth();
const tenantId = await multiTenantAuth.determineTenant(req);
if (!tenantId) {
return res.status(403).json({
success: false,
message: 'Access denied: No tenant context'
});
}
// Create base filter for tenant devices
const tenantDeviceFilter = { tenant_id: tenantId };
// Get basic statistics - filtered by tenant
const [
totalDevices,
activeDevices,
totalDetections,
recentDetections,
uniqueDronesDetected
] = await Promise.all([
Device.count({ where: tenantDeviceFilter }),
Device.count({ where: { ...tenantDeviceFilter, is_active: true } }),
DroneDetection.count({
include: [{
model: Device,
where: tenantDeviceFilter,
attributes: []
}],
where: { drone_type: { [Op.ne]: 0 } }
}),
DroneDetection.count({
include: [{
model: Device,
where: tenantDeviceFilter,
attributes: []
}],
where: {
server_timestamp: { [Op.gte]: timeWindow },
drone_type: { [Op.ne]: 0 }
}
}),
DroneDetection.count({
include: [{
model: Device,
where: tenantDeviceFilter,
attributes: []
}],
where: {
server_timestamp: { [Op.gte]: timeWindow },
drone_type: { [Op.ne]: 0 }
},
distinct: true,
col: 'drone_id'
})
]);
// Get device status breakdown - filtered by tenant
const devices = await Device.findAll({
where: tenantDeviceFilter,
attributes: ['id', 'last_heartbeat', 'heartbeat_interval', 'is_active']
});
const now = new Date();
let onlineDevices = 0;
let offlineDevices = 0;
devices.forEach(device => {
if (!device.is_active) return;
const timeSinceLastHeartbeat = device.last_heartbeat
? (now - new Date(device.last_heartbeat)) / 1000
: null;
const expectedInterval = device.heartbeat_interval || 300;
const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < (expectedInterval * 2);
if (isOnline) {
onlineDevices++;
} else {
offlineDevices++;
}
});
// Get recent alerts count
// This would require AlertLog model which we haven't imported yet
// const recentAlerts = await AlertLog.count({
// where: { created_at: { [Op.gte]: timeWindow } }
// });
res.json({
success: true,
data: {
summary: {
total_devices: totalDevices,
active_devices: activeDevices,
online_devices: onlineDevices,
offline_devices: offlineDevices,
total_detections: totalDetections,
recent_detections: recentDetections,
unique_drones_detected: uniqueDronesDetected,
// recent_alerts: recentAlerts || 0,
time_window_hours: hours
},
device_status: {
total: totalDevices,
active: activeDevices,
online: onlineDevices,
offline: offlineDevices,
inactive: totalDevices - activeDevices
}
}
});
} catch (error) {
console.error('Error fetching dashboard overview:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch dashboard overview',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/dashboard/activity - Get recent activity feed
router.get('/activity', authenticateToken, async (req, res) => {
try {
const { limit = 50, hours = 24 } = req.query;
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
// Initialize multi-tenant auth to determine tenant
const multiTenantAuth = new MultiTenantAuth();
const tenantId = await multiTenantAuth.determineTenant(req);
if (!tenantId) {
return res.status(403).json({
success: false,
message: 'Access denied: No tenant context'
});
}
// Get recent detections with device info - filtered by tenant
const recentDetections = await DroneDetection.findAll({
where: {
server_timestamp: { [Op.gte]: timeWindow },
drone_type: { [Op.ne]: 0 }
},
include: [{
model: Device,
as: 'device',
where: { tenant_id: tenantId },
attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description']
}],
limit: Math.min(parseInt(limit), 200),
order: [['server_timestamp', 'DESC']]
});
// Get recent heartbeats - filtered by tenant
const recentHeartbeats = await Heartbeat.findAll({
where: { received_at: { [Op.gte]: timeWindow } },
include: [{
model: Device,
as: 'device',
where: { tenant_id: tenantId },
attributes: ['id', 'name', 'geo_lat', 'geo_lon']
}],
limit: Math.min(parseInt(limit), 50),
order: [['received_at', 'DESC']]
});
// Combine and sort activities
const activities = [
...recentDetections.map(detection => ({
type: 'detection',
timestamp: detection.server_timestamp,
data: {
detection_id: detection.id,
device_id: detection.device_id,
device_name: detection.device.name || `Device ${detection.device_id}`,
drone_id: detection.drone_id,
drone_type: detection.drone_type,
rssi: detection.rssi,
freq: detection.freq,
location: detection.device.location_description ||
`${detection.device.geo_lat}, ${detection.device.geo_lon}`
}
})),
...recentHeartbeats.map(heartbeat => ({
type: 'heartbeat',
timestamp: heartbeat.received_at,
data: {
device_id: heartbeat.device_id,
device_name: heartbeat.device.name || `Device ${heartbeat.device_id}`,
battery_level: heartbeat.battery_level,
signal_strength: heartbeat.signal_strength,
temperature: heartbeat.temperature
}
}))
];
// Sort by timestamp descending
activities.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
res.json({
success: true,
data: activities.slice(0, parseInt(limit))
});
} catch (error) {
console.error('Error fetching dashboard activity:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch dashboard activity',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/dashboard/charts/detections - Get detection chart data
router.get('/charts/detections', authenticateToken, async (req, res) => {
try {
const { hours = 24, interval = 'hour' } = req.query;
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
// Initialize multi-tenant auth to determine tenant
const multiTenantAuth = new MultiTenantAuth();
const tenantId = await multiTenantAuth.determineTenant(req);
if (!tenantId) {
return res.status(403).json({
success: false,
message: 'Access denied: No tenant context'
});
}
let groupBy;
switch (interval) {
case 'minute':
groupBy = "DATE_TRUNC('minute', server_timestamp)";
break;
case 'hour':
groupBy = "DATE_TRUNC('hour', server_timestamp)";
break;
case 'day':
groupBy = "DATE_TRUNC('day', server_timestamp)";
break;
default:
groupBy = "DATE_TRUNC('hour', server_timestamp)";
}
const detectionCounts = await DroneDetection.findAll({
include: [{
model: Device,
where: { tenant_id: tenantId },
attributes: []
}],
where: {
server_timestamp: { [Op.gte]: timeWindow },
drone_type: { [Op.ne]: 0 }
},
attributes: [
[sequelize.fn('DATE_TRUNC', interval, sequelize.col('server_timestamp')), 'time_bucket'],
[sequelize.fn('COUNT', '*'), 'count']
],
group: ['time_bucket'],
order: [['time_bucket', 'ASC']],
raw: true
});
res.json({
success: true,
data: detectionCounts.map(item => ({
timestamp: item.time_bucket,
count: parseInt(item.count)
}))
});
} catch (error) {
console.error('Error fetching detection chart data:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch detection chart data',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/dashboard/charts/devices - Get device activity chart data
router.get('/charts/devices', authenticateToken, async (req, res) => {
try {
const { hours = 24 } = req.query;
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
// Initialize multi-tenant auth to determine tenant
const multiTenantAuth = new MultiTenantAuth();
const tenantId = await multiTenantAuth.determineTenant(req);
if (!tenantId) {
return res.status(403).json({
success: false,
message: 'Access denied: No tenant context'
});
}
const deviceActivity = await DroneDetection.findAll({
where: {
server_timestamp: { [Op.gte]: timeWindow },
drone_type: { [Op.ne]: 0 }
},
attributes: [
'device_id',
[sequelize.fn('COUNT', '*'), 'detection_count']
],
include: [{
model: Device,
as: 'device',
where: { tenant_id: tenantId },
attributes: ['name', 'location_description']
}],
group: ['device_id', 'device.id', 'device.name', 'device.location_description'],
order: [[sequelize.fn('COUNT', '*'), 'DESC']],
raw: false
});
res.json({
success: true,
data: deviceActivity.map(item => ({
device_id: item.device_id,
device_name: item.device.name || `Device ${item.device_id}`,
location: item.device.location_description,
detection_count: parseInt(item.dataValues.detection_count)
}))
});
} catch (error) {
console.error('Error fetching device chart data:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch device chart data',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
module.exports = router;