diff --git a/api/routes/dashboard.ts b/api/routes/dashboard.ts index 6e97dc6..87316de 100644 --- a/api/routes/dashboard.ts +++ b/api/routes/dashboard.ts @@ -10,7 +10,7 @@ import * as calendarService from '../services/calendar.js' const router = Router() -router.get('/', async (req: Request, res: Response) => { +router.get('/', async (_req: Request, res: Response) => { try { // Date info const now = new Date() diff --git a/api/routes/diagnostic.ts b/api/routes/diagnostic.ts index a3a86c6..e13b286 100644 --- a/api/routes/diagnostic.ts +++ b/api/routes/diagnostic.ts @@ -10,7 +10,7 @@ import db from '../db.js' const router = Router() -router.get('/', async (req: Request, res: Response) => { +router.get('/', async (_req: Request, res: Response) => { const tests = [] // Test database connection diff --git a/api/routes/hetzner.ts b/api/routes/hetzner.ts index a27c622..63b64f6 100644 --- a/api/routes/hetzner.ts +++ b/api/routes/hetzner.ts @@ -54,7 +54,7 @@ router.post('/collect', async (_req: Request, res: Response) => { try { const result = await collectAllMetrics() res.json({ - success: true, + ok: true, message: `Recolhidas métricas: ${result.success} OK, ${result.failed} falharam`, ...result }) @@ -70,7 +70,7 @@ router.post('/collect', async (_req: Request, res: Response) => { // POST /api/hetzner/collect/:hetzner_id - Recolher métricas de um servidor específico router.post('/collect/:hetzner_id', async (req: Request, res: Response) => { try { - const hetzner_id = parseInt(req.params.hetzner_id) + const hetzner_id = parseInt(String(req.params.hetzner_id)) if (isNaN(hetzner_id)) { return res.status(400).json({ success: false, @@ -95,7 +95,7 @@ router.post('/collect/:hetzner_id', async (req: Request, res: Response) => { // GET /api/hetzner/history/:server_id - Histórico de métricas para gráficos router.get('/history/:server_id', async (req: Request, res: Response) => { try { - const server_id = parseInt(req.params.server_id) + const server_id = parseInt(String(req.params.server_id)) const hours = parseInt(req.query.hours as string) || 24 if (isNaN(server_id)) { diff --git a/api/routes/monitor.ts b/api/routes/monitor.ts index 024c1e3..8eba688 100644 --- a/api/routes/monitor.ts +++ b/api/routes/monitor.ts @@ -11,7 +11,7 @@ import * as monitoringService from '../services/monitoring.js' const router = Router() // Get monitoring data -router.get('/', async (req: Request, res: Response) => { +router.get('/', async (_req: Request, res: Response) => { try { const data = await monitoringService.getMonitoringData() res.json(data) @@ -22,7 +22,7 @@ router.get('/', async (req: Request, res: Response) => { }) // Trigger site availability check -router.post('/check-sites', async (req: Request, res: Response) => { +router.post('/check-sites', async (_req: Request, res: Response) => { try { console.log('[Monitor] Manual site check triggered') const result = await monitoringService.checkAllSitesAvailability() diff --git a/api/routes/server-metrics.ts b/api/routes/server-metrics.ts new file mode 100644 index 0000000..141fdd1 --- /dev/null +++ b/api/routes/server-metrics.ts @@ -0,0 +1,68 @@ +/** + * Server Metrics Routes + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ +import { Router, Request, Response } from 'express' +import { + collectAllServerMetrics, + collectSSHMetrics, + syncHetznerToMonitoring +} from '../services/server-metrics.js' + +const router = Router() + +// POST /api/server-metrics/collect - Collect all server metrics +router.post('/collect', async (_req: Request, res: Response) => { + try { + const result = await collectAllServerMetrics() + res.json({ + success: true, + message: 'Métricas recolhidas com sucesso', + ...result + }) + } catch (error) { + console.error('Error collecting metrics:', error) + res.status(500).json({ + success: false, + error: 'Failed to collect metrics' + }) + } +}) + +// POST /api/server-metrics/ssh - Collect SSH metrics only (CWP, EasyPanel) +router.post('/ssh', async (_req: Request, res: Response) => { + try { + const result = await collectSSHMetrics() + res.json({ + success: true, + message: `SSH: ${result.success} OK, ${result.failed} failed`, + ...result + }) + } catch (error) { + console.error('Error collecting SSH metrics:', error) + res.status(500).json({ + success: false, + error: 'Failed to collect SSH metrics' + }) + } +}) + +// POST /api/server-metrics/hetzner - Sync Hetzner to monitoring +router.post('/hetzner', async (_req: Request, res: Response) => { + try { + const synced = await syncHetznerToMonitoring() + res.json({ + success: true, + message: `${synced} servidores Hetzner sincronizados`, + synced + }) + } catch (error) { + console.error('Error syncing Hetzner:', error) + res.status(500).json({ + success: false, + error: 'Failed to sync Hetzner metrics' + }) + } +}) + +export default router diff --git a/api/services/server-metrics.ts b/api/services/server-metrics.ts new file mode 100644 index 0000000..c65e90e --- /dev/null +++ b/api/services/server-metrics.ts @@ -0,0 +1,219 @@ +/** + * Server Metrics Collector Service + * Recolhe métricas de todos os servidores (Hetzner API + SSH) + * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 + */ +import db from '../db.js' +import { collectAllMetrics as collectHetznerMetrics } from './hetzner.js' + +// Hetzner API Configuration +const HETZNER_API_URL = 'https://api.hetzner.cloud/v1' +const HETZNER_TOKEN = process.env.HETZNER_TOKEN || '' + +// SSH Configuration (from MCP ssh-unified) +interface SSHServer { + name: string + monitorName: string + host: string + user: string + pass: string +} + +const SSH_SERVERS: SSHServer[] = [ + { + name: 'server', + monitorName: 'CWP Server', + host: process.env.SERVER_HOST || '176.9.3.158', + user: process.env.SERVER_USER || 'root', + pass: process.env.SERVER_PASS || '' + }, + { + name: 'easy', + monitorName: 'EasyPanel', + host: process.env.EASY_HOST || '178.63.18.51', + user: process.env.EASY_USER || 'root', + pass: process.env.EASY_PASS || '' + } +] + +// Hetzner server mapping to monitoring table +const HETZNER_MAPPING: Record = { + 'gateway': 'MCP Hub', + 'meet': 'Meet', + 'whatsapp.descomplicar.pt': 'WhatsApp', + 'whatsms': 'WhatSMS' +} + +interface ServerMetrics { + cpu: number + ram: number + disk: number + load: number + containers?: number +} + +/** + * Parse SSH metrics output + */ +function parseSSHMetrics(output: string): ServerMetrics { + const lines = output.split('\n') + const metrics: ServerMetrics = { cpu: 0, ram: 0, disk: 0, load: 0 } + + for (const line of lines) { + if (line.startsWith('CPU:')) metrics.cpu = parseFloat(line.split(':')[1]) || 0 + if (line.startsWith('MEM:')) metrics.ram = parseFloat(line.split(':')[1]) || 0 + if (line.startsWith('DISK:')) metrics.disk = parseFloat(line.split(':')[1]) || 0 + if (line.startsWith('LOAD:')) metrics.load = parseFloat(line.split(':')[1]) || 0 + if (line.startsWith('CONTAINERS:')) metrics.containers = parseInt(line.split(':')[1]) || 0 + } + + return metrics +} + +/** + * Execute SSH command via subprocess (since we can't use MCP from here) + * This is a placeholder - in production, use the MCP or a proper SSH library + */ +async function executeSSH(server: SSHServer, command: string): Promise { + // For now, we'll use the database values or call an external endpoint + // In production, you'd use ssh2 library or call the MCP endpoint + const { exec } = await import('child_process') + const { promisify } = await import('util') + const execAsync = promisify(exec) + + try { + // Use sshpass for password-based auth (not ideal but works for internal servers) + const sshCommand = `sshpass -p '${server.pass}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${server.user}@${server.host} "${command}"` + const { stdout } = await execAsync(sshCommand, { timeout: 30000 }) + return stdout + } catch (error) { + console.error(`SSH error for ${server.name}:`, error) + return '' + } +} + +/** + * Collect metrics from SSH servers (CWP, EasyPanel) + */ +export async function collectSSHMetrics(): Promise<{ success: number; failed: number }> { + let success = 0 + let failed = 0 + + const metricsCommand = `echo "CPU:$(top -bn1 | grep 'Cpu(s)' | awk '{print $2}')"; echo "MEM:$(free -m | awk '/Mem:/ {printf "%.1f", $3/$2*100}')"; echo "DISK:$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')"; echo "LOAD:$(cat /proc/loadavg | awk '{print $1}')"; echo "CONTAINERS:$(docker ps -q 2>/dev/null | wc -l || echo 0)"` + + for (const server of SSH_SERVERS) { + try { + const output = await executeSSH(server, metricsCommand) + if (!output) { + failed++ + continue + } + + const metrics = parseSSHMetrics(output) + + // Update monitoring table + await db.query( + `UPDATE tbl_eal_monitoring + SET details = ?, status = 'up', last_check = NOW() + WHERE category = 'server' AND name = ?`, + [ + JSON.stringify({ + cpu: metrics.cpu, + ram: metrics.ram, + disk: metrics.disk, + load: metrics.load + }), + server.monitorName + ] + ) + + // Update containers if EasyPanel + if (server.name === 'easy' && metrics.containers !== undefined) { + // Get current container stats + const containerOutput = await executeSSH(server, 'docker ps -a --format "{{.Status}}" | grep -c "Up" || echo 0; docker ps -aq | wc -l') + const [up, total] = containerOutput.trim().split('\n').map(n => parseInt(n) || 0) + const down = total - up + + await db.query( + `UPDATE tbl_eal_monitoring + SET details = ?, status = ?, last_check = NOW() + WHERE category = 'container' AND name = 'EasyPanel Containers'`, + [ + JSON.stringify({ total, up, down, restarting: 0 }), + down > 0 ? 'warning' : 'ok' + ] + ) + } + + success++ + console.log(`✅ ${server.monitorName}: CPU=${metrics.cpu}%, RAM=${metrics.ram}%, Disk=${metrics.disk}%`) + + } catch (error) { + console.error(`❌ Failed to collect metrics from ${server.name}:`, error) + failed++ + } + } + + return { success, failed } +} + +/** + * Sync Hetzner metrics to monitoring table + */ +export async function syncHetznerToMonitoring(): Promise { + // First collect fresh Hetzner metrics + await collectHetznerMetrics() + + // Then sync to monitoring table + let synced = 0 + + for (const [hetznerName, monitorName] of Object.entries(HETZNER_MAPPING)) { + const namePattern = hetznerName.includes('.') ? hetznerName : `${hetznerName}%` + + const [result] = await db.query(` + UPDATE tbl_eal_monitoring m + JOIN v_eal_hetzner_latest h ON h.name LIKE ? + SET m.details = JSON_OBJECT( + 'cpu', ROUND(h.cpu_percent, 1), + 'ram', 0, + 'disk', 0, + 'load', ROUND(h.cpu_percent / 25, 2), + 'network_in', h.network_in_bps, + 'network_out', h.network_out_bps, + 'hetzner_id', h.hetzner_id + ), + m.status = 'up', + m.last_check = NOW() + WHERE m.category = 'server' AND m.name = ? + `, [hetznerName.includes('.') ? hetznerName : hetznerName, monitorName]) + + if ((result as any).affectedRows > 0) synced++ + } + + return synced +} + +/** + * Collect all server metrics (Hetzner + SSH) + */ +export async function collectAllServerMetrics(): Promise<{ + hetzner: { success: number; failed: number } + ssh: { success: number; failed: number } + synced: number +}> { + console.log('[METRICS] Starting server metrics collection...') + + // Collect SSH metrics (CWP, EasyPanel) + const ssh = await collectSSHMetrics() + console.log(`[SSH] ${ssh.success} OK, ${ssh.failed} failed`) + + // Collect and sync Hetzner metrics + const synced = await syncHetznerToMonitoring() + console.log(`[HETZNER] ${synced} servers synced to monitoring`) + + return { + hetzner: { success: synced, failed: 4 - synced }, + ssh, + synced + } +}