From 6d4f8b834658f6d282e513cf49659f2a730894d4 Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Mon, 23 Feb 2026 18:31:38 +0000 Subject: [PATCH] feat: replace SSH with EasyPanel API for Easy server metrics --- api/services/monitoring-collector.ts | 106 ++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/api/services/monitoring-collector.ts b/api/services/monitoring-collector.ts index a9714b9..9db4138 100644 --- a/api/services/monitoring-collector.ts +++ b/api/services/monitoring-collector.ts @@ -1,6 +1,6 @@ /** * Monitoring Data Collector - * HTTP health checks for services + staleness detection for WP sites + * HTTP health checks for services + EasyPanel API metrics + staleness detection * Runs every 5 minutes via scheduler in server.ts * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ @@ -19,6 +19,14 @@ interface CheckResult { error?: string } +/** + * EasyPanel API config. + * Accessible from Docker Swarm via service name 'easypanel'. + * Token read from EASYPANEL_API_TOKEN env var. + */ +const EASYPANEL_API_URL = process.env.EASYPANEL_API_URL || 'http://easypanel:3000/api/trpc' +const EASYPANEL_API_TOKEN = process.env.EASYPANEL_API_TOKEN || '' + /** * Services to monitor via HTTP health check. * Each entry maps to a record in tbl_eal_monitoring (category='service'). @@ -33,7 +41,7 @@ const SERVICES: ServiceCheck[] = [ { name: 'Metabase', url: 'https://bi.descomplicar.pt' }, { name: 'N8N', url: 'https://automator.descomplicar.pt' }, { name: 'Outline', url: 'https://hub.descomplicar.pt' }, - { name: 'WhatSMS', url: 'https://whatsms.pt' }, + { name: 'WhatSMS', url: 'https://app.whatsms.pt' }, { name: 'MCP Gateway', url: 'http://gateway.descomplicar.pt', okStatuses: [403] }, ] @@ -154,6 +162,89 @@ export async function checkStaleness(): Promise { return (result as any).affectedRows || 0 } +/** + * Call EasyPanel tRPC API endpoint. + * Returns parsed JSON or null on failure. + */ +async function callEasyPanelAPI(endpoint: string): Promise { + if (!EASYPANEL_API_TOKEN) return null + + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 10000) + + const response = await fetch(`${EASYPANEL_API_URL}/${endpoint}`, { + headers: { 'Authorization': `Bearer ${EASYPANEL_API_TOKEN}` }, + signal: controller.signal, + }) + + clearTimeout(timeout) + if (!response.ok) return null + + const data: any = await response.json() + return data?.result?.data?.json ?? null + } catch { + return null + } +} + +/** + * Collect EasyPanel server metrics (CPU, RAM, disk) via API. + * Replaces SSH-based collection for the Easy server. + */ +export async function collectEasyPanelMetrics(): Promise { + const stats = await callEasyPanelAPI('monitor.getSystemStats') + if (!stats) return false + + const cpu = Math.round(stats.cpuInfo?.usedPercentage ?? 0) + const ram = Math.round((stats.memInfo?.usedMemPercentage ?? 0) * 10) / 10 + const disk = parseFloat(stats.diskInfo?.usedPercentage ?? '0') + const load = stats.cpuInfo?.loadavg?.[0] ?? 0 + + await upsertMonitoring('server', 'EasyPanel', 'up', { + cpu, ram, disk, load, + uptime_hours: Math.round((stats.uptime ?? 0) / 3600), + mem_total_mb: Math.round(stats.memInfo?.totalMemMb ?? 0), + mem_used_mb: Math.round(stats.memInfo?.usedMemMb ?? 0), + disk_total_gb: stats.diskInfo?.totalGb, + disk_free_gb: stats.diskInfo?.freeGb, + }) + + console.log(`[EASYPANEL] Server: CPU=${cpu}%, RAM=${ram}%, Disk=${disk}%`) + return true +} + +/** + * Collect Docker container/task stats via EasyPanel API. + * Updates the 'container' category in monitoring DB. + */ +export async function collectEasyPanelContainers(): Promise { + const tasks = await callEasyPanelAPI('monitor.getDockerTaskStats') + if (!tasks) return false + + let total = 0, up = 0, down = 0 + const unhealthy: string[] = [] + + for (const [name, info] of Object.entries(tasks) as [string, { actual: number; desired: number }][]) { + total++ + if (info.actual >= info.desired) { + up++ + } else { + down++ + unhealthy.push(name.replace('descomplicar_', '')) + } + } + + const status = down > 0 ? 'warning' : 'ok' + await upsertMonitoring('container', 'EasyPanel Containers', status, { + total, up, down, restarting: 0, + ...(unhealthy.length > 0 ? { unhealthy } : {}), + }) + + console.log(`[EASYPANEL] Containers: ${up}/${total} running${down > 0 ? `, ${down} down: ${unhealthy.join(', ')}` : ''}`) + return true +} + /** * Main collector entry point. * Called by scheduler in server.ts every 5 minutes. @@ -168,6 +259,17 @@ export async function collectMonitoringData(): Promise { console.error('[COLLECTOR] Service checks failed:', err instanceof Error ? err.message : err) } + // EasyPanel API metrics (replaces SSH for Easy server) + try { + const gotStats = await collectEasyPanelMetrics() + const gotContainers = await collectEasyPanelContainers() + if (!gotStats && !gotContainers) { + console.warn('[COLLECTOR] EasyPanel API unavailable (check EASYPANEL_API_TOKEN)') + } + } catch (err: unknown) { + console.error('[COLLECTOR] EasyPanel collection failed:', err instanceof Error ? err.message : err) + } + try { const stale = await checkStaleness() if (stale > 0) {