Files
patchmon.net/backend/src/routes/dashboardRoutes.js
Muhammad Ibrahim c5332ce6b0 first commit
2025-09-16 15:36:42 +01:00

313 lines
7.8 KiB
JavaScript

const express = require('express');
const { PrismaClient } = require('@prisma/client');
const moment = require('moment');
const { authenticateToken } = require('../middleware/auth');
const {
requireViewDashboard,
requireViewHosts,
requireViewPackages
} = require('../middleware/permissions');
const router = express.Router();
const prisma = new PrismaClient();
// Get dashboard statistics
router.get('/stats', authenticateToken, requireViewDashboard, async (req, res) => {
try {
const now = new Date();
const twentyFourHoursAgo = moment(now).subtract(24, 'hours').toDate();
// Get all statistics in parallel for better performance
const [
totalHosts,
hostsNeedingUpdates,
totalOutdatedPackages,
erroredHosts,
securityUpdates,
osDistribution,
updateTrends
] = await Promise.all([
// Total hosts count
prisma.host.count({
where: { status: 'active' }
}),
// Hosts needing updates (distinct hosts with packages needing updates)
prisma.host.count({
where: {
status: 'active',
hostPackages: {
some: {
needsUpdate: true
}
}
}
}),
// Total outdated packages across all hosts
prisma.hostPackage.count({
where: { needsUpdate: true }
}),
// Errored hosts (not updated in 24 hours)
prisma.host.count({
where: {
status: 'active',
lastUpdate: {
lt: twentyFourHoursAgo
}
}
}),
// Security updates count
prisma.hostPackage.count({
where: {
needsUpdate: true,
isSecurityUpdate: true
}
}),
// OS distribution for pie chart
prisma.host.groupBy({
by: ['osType'],
where: { status: 'active' },
_count: {
osType: true
}
}),
// Update trends for the last 7 days
prisma.updateHistory.groupBy({
by: ['timestamp'],
where: {
timestamp: {
gte: moment(now).subtract(7, 'days').toDate()
}
},
_count: {
id: true
},
_sum: {
packagesCount: true,
securityCount: true
}
})
]);
// Format OS distribution for pie chart
const osDistributionFormatted = osDistribution.map(item => ({
name: item.osType,
count: item._count.osType
}));
// Calculate update status distribution
const updateStatusDistribution = [
{ name: 'Up to date', count: totalHosts - hostsNeedingUpdates },
{ name: 'Needs updates', count: hostsNeedingUpdates },
{ name: 'Errored', count: erroredHosts }
];
// Package update priority distribution
const packageUpdateDistribution = [
{ name: 'Security', count: securityUpdates },
{ name: 'Regular', count: totalOutdatedPackages - securityUpdates }
];
res.json({
cards: {
totalHosts,
hostsNeedingUpdates,
totalOutdatedPackages,
erroredHosts,
securityUpdates
},
charts: {
osDistribution: osDistributionFormatted,
updateStatusDistribution,
packageUpdateDistribution
},
trends: updateTrends,
lastUpdated: now.toISOString()
});
} catch (error) {
console.error('Error fetching dashboard stats:', error);
res.status(500).json({ error: 'Failed to fetch dashboard statistics' });
}
});
// Get hosts with their update status
router.get('/hosts', authenticateToken, requireViewHosts, async (req, res) => {
try {
const hosts = await prisma.host.findMany({
// Show all hosts regardless of status
select: {
id: true,
hostname: true,
ip: true,
osType: true,
osVersion: true,
lastUpdate: true,
status: true,
agentVersion: true,
autoUpdate: true,
hostGroup: {
select: {
id: true,
name: true,
color: true
}
},
_count: {
select: {
hostPackages: {
where: {
needsUpdate: true
}
}
}
}
},
orderBy: { lastUpdate: 'desc' }
});
// Get update counts for each host separately
const hostsWithUpdateInfo = await Promise.all(
hosts.map(async (host) => {
const updatesCount = await prisma.hostPackage.count({
where: {
hostId: host.id,
needsUpdate: true
}
});
return {
...host,
updatesCount,
isStale: moment(host.lastUpdate).isBefore(moment().subtract(24, 'hours'))
};
})
);
res.json(hostsWithUpdateInfo);
} catch (error) {
console.error('Error fetching hosts:', error);
res.status(500).json({ error: 'Failed to fetch hosts' });
}
});
// Get packages that need updates across all hosts
router.get('/packages', authenticateToken, requireViewPackages, async (req, res) => {
try {
const packages = await prisma.package.findMany({
where: {
hostPackages: {
some: {
needsUpdate: true
}
}
},
select: {
id: true,
name: true,
description: true,
category: true,
latestVersion: true,
hostPackages: {
where: { needsUpdate: true },
select: {
currentVersion: true,
availableVersion: true,
isSecurityUpdate: true,
host: {
select: {
id: true,
hostname: true,
osType: true
}
}
}
}
},
orderBy: {
name: 'asc'
}
});
const packagesWithHostInfo = packages.map(pkg => ({
id: pkg.id,
name: pkg.name,
description: pkg.description,
category: pkg.category,
latestVersion: pkg.latestVersion,
affectedHostsCount: pkg.hostPackages.length,
isSecurityUpdate: pkg.hostPackages.some(hp => hp.isSecurityUpdate),
affectedHosts: pkg.hostPackages.map(hp => ({
hostId: hp.host.id,
hostname: hp.host.hostname,
osType: hp.host.osType,
currentVersion: hp.currentVersion,
availableVersion: hp.availableVersion,
isSecurityUpdate: hp.isSecurityUpdate
}))
}));
res.json(packagesWithHostInfo);
} catch (error) {
console.error('Error fetching packages:', error);
res.status(500).json({ error: 'Failed to fetch packages' });
}
});
// Get detailed host information
router.get('/hosts/:hostId', authenticateToken, requireViewHosts, async (req, res) => {
try {
const { hostId } = req.params;
const host = await prisma.host.findUnique({
where: { id: hostId },
include: {
hostGroup: {
select: {
id: true,
name: true,
color: true
}
},
hostPackages: {
include: {
package: true
},
orderBy: {
needsUpdate: 'desc'
}
},
updateHistory: {
orderBy: {
timestamp: 'desc'
},
take: 10
}
}
});
if (!host) {
return res.status(404).json({ error: 'Host not found' });
}
const hostWithStats = {
...host,
stats: {
totalPackages: host.hostPackages.length,
outdatedPackages: host.hostPackages.filter(hp => hp.needsUpdate).length,
securityUpdates: host.hostPackages.filter(hp => hp.needsUpdate && hp.isSecurityUpdate).length
}
};
res.json(hostWithStats);
} catch (error) {
console.error('Error fetching host details:', error);
res.status(500).json({ error: 'Failed to fetch host details' });
}
});
module.exports = router;