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'));