From 6809fc5cca1c6ed5d177f10dfc19721cebeee9e5 Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 18 Jun 2026 21:17:15 -0700 Subject: [PATCH] fix(monitoring): flatten CPU/mem data, add health summary, public + rate-limit monitoring/stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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}} --- dashcaddy-api/VERSION | 2 +- dashcaddy-api/middleware.js | 5 ++++- dashcaddy-api/routes/health.js | 19 ++++++++++++++++++- dashcaddy-api/routes/monitoring.js | 15 ++++++++++++++- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/dashcaddy-api/VERSION b/dashcaddy-api/VERSION index 3395ff3..e3492e2 100644 --- a/dashcaddy-api/VERSION +++ b/dashcaddy-api/VERSION @@ -1 +1 @@ -db14233 +a372d62 diff --git a/dashcaddy-api/middleware.js b/dashcaddy-api/middleware.js index beff43c..719c7ff 100644 --- a/dashcaddy-api/middleware.js +++ b/dashcaddy-api/middleware.js @@ -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' } }); diff --git a/dashcaddy-api/routes/health.js b/dashcaddy-api/routes/health.js index e5c112d..7001a89 100644 --- a/dashcaddy-api/routes/health.js +++ b/dashcaddy-api/routes/health.js @@ -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 diff --git a/dashcaddy-api/routes/monitoring.js b/dashcaddy-api/routes/monitoring.js index ae27a39..28e2255 100644 --- a/dashcaddy-api/routes/monitoring.js +++ b/dashcaddy-api/routes/monitoring.js @@ -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'));