fix(monitoring): flatten CPU/mem data, add health summary, public + rate-limit monitoring/stats
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

Three coordinated fixes for the System Overview widget:

1. routes/monitoring.js — flatten getAllStats() shape from
   {current:{cpu:{percent},memory:{percent}}} to {cpu,memory,memoryUsage}
   so the widget's Number() coercion actually produces numbers, not NaN.
   Skill reference: references/totp-and-system-overview-pitfalls.md §3.

2. routes/health.js — add summary block to /health-checks/status response.
   Widget looks for {healthy, unhealthy, total} but only per-service objects
   existed. Permissive on healthy side (up|healthy|online), strict on
   unhealthy (down|unhealthy|offline|error); anything else counted as
   unknown. Same skill §3 reference.

3. middleware.js — add /api/v1/monitoring/stats to PUBLIC_ROUTES and the
   rate-limit skip list. The widget polls it every 5s from the dashboard;
   cookie-auth works but listing it explicitly makes it future-proof
   against auth-cookie expiry and prevents per-second 429s.

End-to-end test (unauthenticated):
  GET /api/v1/monitoring/stats  -> {cpu: 8.71, memory: 0.37, ...}
  GET /api/v1/health-checks/status -> {summary: {healthy:11, unhealthy:4, total:15}}
This commit is contained in:
Krystie
2026-06-18 21:17:16 -07:00
parent 4853f1feb8
commit 6809fc5cca
4 changed files with 37 additions and 4 deletions
+1 -1
View File
@@ -1 +1 @@
db14233
a372d62
+4 -1
View File
@@ -306,6 +306,9 @@ module.exports = function configureMiddleware(app, {
{ path: '/api/v1/config', exact: true, method: 'GET' },
{ path: '/api/v1/services/status', exact: true, method: 'GET' },
{ path: '/api/v1/health-checks/status', exact: true, method: 'GET' },
// System Overview widget on the dashboard — needs the flattened CPU/mem
// data without going through auth. See skill references/totp-and-system-overview-pitfalls.md §3.
{ path: '/api/v1/monitoring/stats', exact: true, method: 'GET' },
// Read-only update/version info shown on the dashboard view (verification
// modal, topbar version, update badges). Mutating actions — update-apply,
// rollback (POST) — are NOT listed here and stay TOTP-protected.
@@ -402,7 +405,7 @@ module.exports = function configureMiddleware(app, {
...RATE_LIMITS.GENERAL,
standardHeaders: true,
legacyHeaders: false,
skip: (req) => isTest || req.path === '/health' || req.path === '/api/v1/health' || req.path.startsWith('/probe/') || req.path.startsWith('/api/v1/auth/gate/') || req.path === '/api/v1/totp/check-session' || req.path.endsWith('/health-checks/status') || req.path.endsWith('/csrf-token') || req.path === '/api/v1/dns/logs' || req.path === '/api/v1/license/status' || req.path.startsWith('/api/v1/license/feature/') || req.path === '/api/v1/services' || req.path === '/api/v1/config',
skip: (req) => isTest || req.path === '/health' || req.path === '/api/v1/health' || req.path.startsWith('/probe/') || req.path.startsWith('/api/v1/auth/gate/') || req.path === '/api/v1/totp/check-session' || req.path.endsWith('/health-checks/status') || req.path.endsWith('/monitoring/stats') || req.path.endsWith('/csrf-token') || req.path === '/api/v1/dns/logs' || req.path === '/api/v1/license/status' || req.path.startsWith('/api/v1/license/feature/') || req.path === '/api/v1/services' || req.path === '/api/v1/config',
message: { success: false, error: 'Too many requests, please try again later' }
});
+18 -1
View File
@@ -322,9 +322,26 @@ module.exports = function({
// ===== HEALTH CHECK (health-checker module) =====
// Get current status for all services
// Returns {status: {...per-service}} plus a {summary} block for the System Overview widget
// — see skill references/totp-and-system-overview-pitfalls.md §3
router.get('/health-checks/status', asyncHandler(async (req, res) => {
const status = healthChecker.getCurrentStatus();
success(res, { status });
const entries = Object.values(status || {});
// Treat 'up'/'healthy' as healthy, everything else as unhealthy.
// Health check status values come from healthChecker — typically 'up'/'down' but
// also 'healthy'/'unhealthy' or 'online'/'offline' depending on the source. Be
// permissive on the healthy side so a service in any positive state counts.
const healthy = entries.filter(s => {
const st = (s && (s.status || s.state)) || '';
return st === 'up' || st === 'healthy' || st === 'online';
}).length;
const unhealthy = entries.filter(s => {
const st = (s && (s.status || s.state)) || '';
return st === 'down' || st === 'unhealthy' || st === 'offline' || st === 'error';
}).length;
const unknown = entries.length - healthy - unhealthy;
const summary = { healthy, unhealthy, unknown, total: entries.length };
success(res, { status, summary });
}, 'health-check-status'));
// Get service statistics
+14 -1
View File
@@ -16,8 +16,21 @@ module.exports = function({ resourceMonitor, docker, asyncHandler, log, notifica
// ===== RESOURCE MONITORING ENDPOINTS =====
// Get all container stats (from resource monitor module)
// Flattened for the System Overview widget — see skill references/totp-and-system-overview-pitfalls.md §3
router.get('/monitoring/stats', asyncHandler(async (req, res) => {
const stats = resourceMonitor.getAllStats();
const raw = resourceMonitor.getAllStats();
const stats = {};
for (const [id, data] of Object.entries(raw || {})) {
const cur = data.current || {};
const cpuObj = (cur.cpu && typeof cur.cpu === 'object') ? cur.cpu : null;
const memObj = (cur.memory && typeof cur.memory === 'object') ? cur.memory : null;
stats[id] = {
name: data.name,
cpu: cpuObj ? (cpuObj.percent ?? 0) : (Number(cur.cpu) || 0),
memory: memObj ? (memObj.percent ?? 0) : (Number(cur.memory) || 0),
memoryUsage: memObj ? (memObj.usage ?? 0) : 0,
};
}
success(res, { stats });
}, 'monitoring-stats'));