import { query, queryOne } from '../utils/db' interface Client { ws: any interval: number timer: ReturnType | null autoRefresh: boolean } const clients = new Map() async function getNetworkStatus() { // pubnet 상태 const pubnetStatus = await queryOne(` SELECT ps.*, pt.name as last_target_name, pt.url as last_target_url FROM pubnet_status ps LEFT JOIN pubnet_targets pt ON ps.last_target_id = pt.id WHERE ps.id = 1 `) const pubnetLogs = await query(` SELECT pl.*, pt.name as target_name, pt.url as target_url FROM pubnet_logs pl JOIN pubnet_targets pt ON pl.target_id = pt.id ORDER BY pl.checked_at DESC LIMIT 5 `) // privnet 상태 const privnetStatus = await queryOne(` SELECT ps.*, pt.name as last_target_name, pt.url as last_target_url FROM privnet_status ps LEFT JOIN privnet_targets pt ON ps.last_target_id = pt.id WHERE ps.id = 1 `) const privnetLogs = await query(` SELECT pl.*, pt.name as target_name, pt.url as target_url FROM privnet_logs pl JOIN privnet_targets pt ON pl.target_id = pt.id ORDER BY pl.checked_at DESC LIMIT 5 `) return { pubnet: { status: pubnetStatus, logs: pubnetLogs }, privnet: { status: privnetStatus, logs: privnetLogs }, timestamp: new Date().toISOString() } } async function getHistoricalData(datetime: string) { const pubnetLogs = await query(` SELECT pl.*, pt.name as target_name, pt.url as target_url FROM pubnet_logs pl JOIN pubnet_targets pt ON pl.target_id = pt.id WHERE pl.checked_at <= $1 ORDER BY pl.checked_at DESC LIMIT 5 `, [datetime]) const privnetLogs = await query(` SELECT pl.*, pt.name as target_name, pt.url as target_url FROM privnet_logs pl JOIN privnet_targets pt ON pl.target_id = pt.id WHERE pl.checked_at <= $1 ORDER BY pl.checked_at DESC LIMIT 5 `, [datetime]) const pubnetLatest = pubnetLogs[0] || null const privnetLatest = privnetLogs[0] || null return { pubnet: { status: pubnetLatest ? { is_healthy: pubnetLatest.is_success, last_checked_at: pubnetLatest.checked_at, last_target_name: pubnetLatest.target_name } : null, logs: pubnetLogs }, privnet: { status: privnetLatest ? { is_healthy: privnetLatest.is_success, last_checked_at: privnetLatest.checked_at, last_target_name: privnetLatest.target_name } : null, logs: privnetLogs }, timestamp: datetime } } // 임계값 조회 async function getThresholds() { let rows: any[] = [] try { rows = await query(`SELECT category, metric, warning, critical, danger FROM thresholds`) } catch (e) { // 테이블 없으면 빈 배열 } const result: Record> = {} for (const row of rows) { if (!result[row.category]) result[row.category] = {} result[row.category][row.metric] = { warning: row.warning, critical: row.critical, danger: row.danger } } return result } // 레벨 계산 function getLevel(value: number | null, threshold: { warning: number; critical: number; danger: number }): string { if (value === null || value === undefined) return 'normal' if (value >= threshold.danger) return 'danger' if (value >= threshold.critical) return 'critical' if (value >= threshold.warning) return 'warning' return 'normal' } // 레벨 우선순위 const levelPriority: Record = { normal: 0, warning: 1, critical: 2, danger: 3, offline: 4, stopped: 3 } function getHighestLevel(levels: string[]): string { let highest = 'normal' for (const level of levels) { if ((levelPriority[level] || 0) > (levelPriority[highest] || 0)) { highest = level } } return highest } // 서버 대시보드 데이터 async function getServerDashboard() { const thresholds = await getThresholds() const now = new Date() const offlineThreshold = 5 * 60 * 1000 // 5분 // 서버 목록 const servers = await query(` SELECT target_id, server_name, server_ip, is_active FROM server_targets WHERE is_active = 1 ORDER BY server_name `) const serverStatuses: any[] = [] const summaryServers = { total: servers.length, normal: 0, warning: 0, critical: 0, danger: 0, offline: 0 } const summaryContainers = { total: 0, normal: 0, warning: 0, critical: 0, danger: 0, stopped: 0 } for (const server of servers) { // 최신 스냅샷 const snapshot = await queryOne(` SELECT cpu_percent, memory_percent, memory_total, memory_free, memory_used, cpu_temp, load_percent, uptime_str, collected_at FROM server_snapshots WHERE target_id = $1 AND is_online = 1 ORDER BY collected_at DESC LIMIT 1 `, [server.target_id]) // 최신 디스크 사용률 (최대값) + 용량 const diskData = await queryOne(` SELECT MAX(disk_percent) as disk_percent, SUM(disk_used) as disk_used, SUM(disk_total) as disk_total FROM server_disks WHERE target_id = $1 AND collected_at = (SELECT MAX(collected_at) FROM server_disks WHERE target_id = $1) AND device_name NOT LIKE '%loop%' AND mount_point NOT LIKE '%/snap%' AND fs_type NOT IN ('tmpfs', 'squashfs') `, [server.target_id]) // 오프라인 체크 let isOffline = true let lastCollected = null if (snapshot && snapshot.collected_at) { lastCollected = snapshot.collected_at const collectedTime = new Date(snapshot.collected_at).getTime() isOffline = (now.getTime() - collectedTime) > offlineThreshold } // 서버 레벨 계산 let serverLevel = 'offline' let cpuLevel = 'normal', memLevel = 'normal', diskLevel = 'normal' if (!isOffline && snapshot) { cpuLevel = getLevel(Number(snapshot.cpu_percent), thresholds.server?.cpu || { warning: 70, critical: 85, danger: 95 }) // 메모리: snapshot.memory_percent 직접 사용 memLevel = getLevel(Number(snapshot.memory_percent), thresholds.server?.memory || { warning: 80, critical: 90, danger: 95 }) diskLevel = getLevel(Number(diskData?.disk_percent), thresholds.server?.disk || { warning: 80, critical: 90, danger: 95 }) serverLevel = getHighestLevel([cpuLevel, memLevel, diskLevel]) } // 요약 집계 if (serverLevel === 'offline') summaryServers.offline++ else if (serverLevel === 'danger') summaryServers.danger++ else if (serverLevel === 'critical') summaryServers.critical++ else if (serverLevel === 'warning') summaryServers.warning++ else summaryServers.normal++ // 컨테이너 조회 (최신 데이터만) const containers: any[] = [] const containerSummary = { total: 0, normal: 0, warning: 0, critical: 0, stopped: 0 } if (!isOffline) { // 서버별 최신 컨테이너 (container_name별 최신 1건) const latestContainers = await query(` SELECT DISTINCT ON (container_name) container_name as name, container_status as status, cpu_percent, memory_usage, memory_limit, uptime, network_rx, network_tx FROM server_containers WHERE target_id = $1 ORDER BY container_name, collected_at DESC `, [server.target_id]) for (const c of latestContainers) { let containerLevel = 'normal' if (c.status !== 'running') { containerLevel = 'stopped' containerSummary.stopped++ } else { const cCpuLevel = getLevel(Number(c.cpu_percent), thresholds.container?.cpu || { warning: 80, critical: 90, danger: 95 }) const memPct = c.memory_limit ? (Number(c.memory_usage) / Number(c.memory_limit)) * 100 : 0 const cMemLevel = getLevel(memPct, thresholds.container?.memory || { warning: 80, critical: 90, danger: 95 }) containerLevel = getHighestLevel([cCpuLevel, cMemLevel]) if (containerLevel === 'danger') containerSummary.critical++ else if (containerLevel === 'critical') containerSummary.critical++ else if (containerLevel === 'warning') containerSummary.warning++ else containerSummary.normal++ } containers.push({ name: c.name, status: c.status || 'unknown', level: containerLevel, cpu_percent: c.cpu_percent, memory_usage: c.memory_usage, memory_limit: c.memory_limit, uptime: c.uptime, network_rx: c.network_rx, network_tx: c.network_tx }) containerSummary.total++ summaryContainers.total++ } // 전체 컨테이너 요약 집계 summaryContainers.normal += containerSummary.normal summaryContainers.warning += containerSummary.warning summaryContainers.critical += containerSummary.critical summaryContainers.stopped += containerSummary.stopped } serverStatuses.push({ target_id: server.target_id, server_name: server.server_name, server_ip: server.server_ip, level: serverLevel, cpu_percent: snapshot?.cpu_percent ?? null, cpu_level: cpuLevel, memory_percent: snapshot?.memory_percent ?? null, // snapshot의 memory_percent 직접 사용 memory_level: memLevel, memory_total: snapshot?.memory_total ?? null, memory_free: snapshot?.memory_free ?? null, memory_used: snapshot?.memory_used ?? null, disk_percent: diskData?.disk_percent ?? null, disk_level: diskLevel, disk_used: diskData?.disk_used ?? null, disk_total: diskData?.disk_total ?? null, cpu_temp: snapshot?.cpu_temp ?? null, load_percent: snapshot?.load_percent ?? null, uptime_str: snapshot?.uptime_str ?? null, last_collected: lastCollected, containers: containers, container_summary: containerSummary }) } // 서버 정렬: 장애 우선 → 컨테이너 많은 순 → 이름순 serverStatuses.sort((a, b) => { // 1. 장애 여부 (서버 장애 또는 컨테이너에 장애) const aHasIssue = a.level !== 'normal' || (a.container_summary.stopped > 0 || a.container_summary.critical > 0 || a.container_summary.warning > 0) const bHasIssue = b.level !== 'normal' || (b.container_summary.stopped > 0 || b.container_summary.critical > 0 || b.container_summary.warning > 0) if (aHasIssue !== bHasIssue) return aHasIssue ? -1 : 1 // 2. 컨테이너 수 (많은 순) const aContainers = a.container_summary.total || 0 const bContainers = b.container_summary.total || 0 if (aContainers !== bContainers) return bContainers - aContainers // 3. 서버 이름순 return a.server_name.localeCompare(b.server_name) }) return { summary: { servers: summaryServers, containers: summaryContainers }, servers: serverStatuses, timestamp: new Date().toISOString() } } function startAutoRefresh(client: Client) { if (client.timer) { clearInterval(client.timer) } if (client.autoRefresh) { client.timer = setInterval(async () => { const data = await getNetworkStatus() client.ws.send(JSON.stringify({ type: 'status', data })) const serverData = await getServerDashboard() client.ws.send(JSON.stringify({ type: 'server', data: serverData })) }, client.interval) } } export default defineWebSocketHandler({ async open(peer) { console.log('[WebSocket] Client connected') const client: Client = { ws: peer, interval: 60 * 1000, timer: null, autoRefresh: true } clients.set(peer, client) // 초기 데이터 전송 const data = await getNetworkStatus() peer.send(JSON.stringify({ type: 'status', data })) const serverData = await getServerDashboard() peer.send(JSON.stringify({ type: 'server', data: serverData })) startAutoRefresh(client) }, async message(peer, message) { const client = clients.get(peer) if (!client) return try { const msg = JSON.parse(message.text()) switch (msg.type) { case 'set_interval': client.interval = msg.interval * 60 * 1000 console.log(`[WebSocket] Interval changed to ${msg.interval} min`) startAutoRefresh(client) break case 'set_auto_refresh': client.autoRefresh = msg.enabled console.log(`[WebSocket] Auto refresh: ${msg.enabled}`) if (msg.enabled) { startAutoRefresh(client) } else if (client.timer) { clearInterval(client.timer) client.timer = null } break case 'fetch_at': const historicalData = await getHistoricalData(msg.datetime) peer.send(JSON.stringify({ type: 'historical', data: historicalData })) break case 'refresh': const currentData = await getNetworkStatus() peer.send(JSON.stringify({ type: 'status', data: currentData })) const currentServerData = await getServerDashboard() peer.send(JSON.stringify({ type: 'server', data: currentServerData })) break case 'refresh_server': const serverDashData = await getServerDashboard() peer.send(JSON.stringify({ type: 'server', data: serverDashData })) break } } catch (err) { console.error('[WebSocket] Message parse error:', err) } }, close(peer) { console.log('[WebSocket] Client disconnected') const client = clients.get(peer) if (client?.timer) { clearInterval(client.timer) } clients.delete(peer) }, error(peer, error) { console.error('[WebSocket] Error:', error) } })