/** * Hetzner Cloud API Service * Recolhe métricas dos VPS via API Hetzner Cloud * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import db from '../db.js' import type { RowDataPacket, ResultSetHeader } from 'mysql2' // Hetzner API Configuration const HETZNER_API_URL = 'https://api.hetzner.cloud/v1' const HETZNER_TOKEN = process.env.HETZNER_TOKEN || '' interface HetznerServer { id: number name: string status: string server_type: { name: string } datacenter: { name: string } public_net: { ipv4: { ip: string } ipv6: { ip: string } } private_net: Array<{ ip: string }> labels: Record created: string } interface HetznerMetrics { metrics: { time_series: { [key: string]: { values: Array<[number, string]> } } } } interface ServerWithMetrics { id: number hetzner_id: number name: string status: string server_type: string datacenter: string public_ipv4: string collected_at: string | null cpu_percent: number | null disk_read_iops: number | null disk_write_iops: number | null network_in_bps: number | null network_out_bps: number | null } // Helper para requests à API Hetzner async function hetznerRequest(endpoint: string): Promise { const response = await fetch(`${HETZNER_API_URL}${endpoint}`, { headers: { 'Authorization': `Bearer ${HETZNER_TOKEN}`, 'Content-Type': 'application/json' } }) if (!response.ok) { throw new Error(`Hetzner API error: ${response.status} ${response.statusText}`) } return response.json() } // Sincronizar lista de servidores export async function syncServers(): Promise { const data = await hetznerRequest<{ servers: HetznerServer[] }>('/servers') let synced = 0 for (const server of data.servers) { const [existing] = await db.query( 'SELECT id FROM tbl_eal_hetzner_servers WHERE hetzner_id = ?', [server.id] ) const serverData = { hetzner_id: server.id, name: server.name, status: server.status, server_type: server.server_type.name, datacenter: server.datacenter.name, public_ipv4: server.public_net.ipv4?.ip || null, public_ipv6: server.public_net.ipv6?.ip || null, private_ip: server.private_net?.[0]?.ip || null, labels: JSON.stringify(server.labels), created_hetzner: new Date(server.created) } if (existing.length > 0) { // Update existing await db.query( `UPDATE tbl_eal_hetzner_servers SET name = ?, status = ?, server_type = ?, datacenter = ?, public_ipv4 = ?, public_ipv6 = ?, private_ip = ?, labels = ? WHERE hetzner_id = ?`, [ serverData.name, serverData.status, serverData.server_type, serverData.datacenter, serverData.public_ipv4, serverData.public_ipv6, serverData.private_ip, serverData.labels, server.id ] ) } else { // Insert new await db.query( `INSERT INTO tbl_eal_hetzner_servers (hetzner_id, name, status, server_type, datacenter, public_ipv4, public_ipv6, private_ip, labels, created_hetzner) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ serverData.hetzner_id, serverData.name, serverData.status, serverData.server_type, serverData.datacenter, serverData.public_ipv4, serverData.public_ipv6, serverData.private_ip, serverData.labels, serverData.created_hetzner ] ) } synced++ } return synced } // Recolher métricas de um servidor export async function collectMetrics(hetzner_id: number): Promise { // Obter server_id local const [servers] = await db.query( 'SELECT id FROM tbl_eal_hetzner_servers WHERE hetzner_id = ?', [hetzner_id] ) if (servers.length === 0) { console.error(`Server ${hetzner_id} not found in database`) return false } const server_id = servers[0].id const now = new Date() const start = new Date(now.getTime() - 5 * 60 * 1000) // 5 minutos atrás try { // Obter métricas da API const metricsUrl = `/servers/${hetzner_id}/metrics?type=cpu,disk,network&start=${start.toISOString()}&end=${now.toISOString()}` const data = await hetznerRequest(metricsUrl) // Extrair valores mais recentes const getLatestValue = (series: string): number | null => { const values = data.metrics.time_series[series]?.values if (!values || values.length === 0) return null return parseFloat(values[values.length - 1][1]) } const metrics = { cpu_percent: getLatestValue('cpu'), disk_read_iops: getLatestValue('disk.0.iops.read'), disk_write_iops: getLatestValue('disk.0.iops.write'), disk_read_bps: getLatestValue('disk.0.bandwidth.read'), disk_write_bps: getLatestValue('disk.0.bandwidth.write'), network_in_bps: getLatestValue('network.0.bandwidth.in'), network_out_bps: getLatestValue('network.0.bandwidth.out'), network_in_pps: getLatestValue('network.0.pps.in'), network_out_pps: getLatestValue('network.0.pps.out') } // Inserir métricas await db.query( `INSERT INTO tbl_eal_hetzner_metrics (server_id, collected_at, cpu_percent, disk_read_iops, disk_write_iops, disk_read_bps, disk_write_bps, network_in_bps, network_out_bps, network_in_pps, network_out_pps) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ server_id, now, metrics.cpu_percent, metrics.disk_read_iops, metrics.disk_write_iops, metrics.disk_read_bps, metrics.disk_write_bps, metrics.network_in_bps, metrics.network_out_bps, metrics.network_in_pps, metrics.network_out_pps ] ) return true } catch (error) { console.error(`Error collecting metrics for server ${hetzner_id}:`, error) return false } } // Recolher métricas de todos os servidores export async function collectAllMetrics(): Promise<{ success: number; failed: number }> { const [servers] = await db.query( 'SELECT hetzner_id FROM tbl_eal_hetzner_servers WHERE status = "running"' ) let success = 0 let failed = 0 for (const server of servers) { const result = await collectMetrics(server.hetzner_id) if (result) success++ else failed++ } return { success, failed } } // Obter dados para o dashboard export async function getHetznerDashboard(): Promise<{ servers: ServerWithMetrics[] summary: { total: number; running: number; off: number } }> { // Usar a view para obter últimas métricas const [servers] = await db.query(` SELECT * FROM v_eal_hetzner_latest ORDER BY name `) // Calcular sumário const [summary] = await db.query(` SELECT COUNT(*) as total, SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running, SUM(CASE WHEN status != 'running' THEN 1 ELSE 0 END) as off FROM tbl_eal_hetzner_servers `) return { servers: servers as ServerWithMetrics[], summary: summary[0] as { total: number; running: number; off: number } } } // Limpar métricas antigas (manter últimos 7 dias) export async function cleanupOldMetrics(days: number = 7): Promise { const [result] = await db.query( `DELETE FROM tbl_eal_hetzner_metrics WHERE collected_at < DATE_SUB(NOW(), INTERVAL ? DAY)`, [days] ) return result.affectedRows } // Obter histórico de métricas para gráficos export async function getMetricsHistory( server_id: number, hours: number = 24 ): Promise { const [metrics] = await db.query(` SELECT collected_at, cpu_percent, network_in_bps, network_out_bps, disk_read_iops, disk_write_iops FROM tbl_eal_hetzner_metrics WHERE server_id = ? AND collected_at > DATE_SUB(NOW(), INTERVAL ? HOUR) ORDER BY collected_at ASC `, [server_id, hours]) return metrics }