import { getDb } from '../utils/db' interface Client { ws: any interval: number timer: ReturnType | null autoRefresh: boolean } const clients = new Map() function getNetworkStatus() { const db = getDb() // pubnet 상태 const pubnetStatus = db.prepare(` 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 `).get() const pubnetLogs = db.prepare(` 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 `).all() // privnet 상태 const privnetStatus = db.prepare(` 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 `).get() const privnetLogs = db.prepare(` 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 `).all() return { pubnet: { status: pubnetStatus, logs: pubnetLogs }, privnet: { status: privnetStatus, logs: privnetLogs }, timestamp: new Date().toISOString() } } function getHistoricalData(datetime: string) { const db = getDb() // 특정 시간 이전의 로그 조회 const pubnetLogs = db.prepare(` 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 <= @datetime ORDER BY pl.checked_at DESC LIMIT 5 `).all({ datetime }) const privnetLogs = db.prepare(` 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 <= @datetime ORDER BY pl.checked_at DESC LIMIT 5 `).all({ 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 } } // 임계값 조회 function getThresholds() { const db = getDb() const rows = db.prepare(`SELECT category, metric, warning, critical, danger FROM thresholds`).all() as any[] 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 } // 서버 대시보드 데이터 function getServerDashboard() { const db = getDb() const thresholds = getThresholds() const now = new Date() const offlineThreshold = 5 * 60 * 1000 // 5분 // 서버 목록 const servers = db.prepare(`SELECT target_id, server_name, is_active FROM server_targets WHERE is_active = 1 ORDER BY server_name`).all() as any[] 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 = db.prepare(` SELECT cpu_percent, memory_percent, collected_at FROM server_snapshots WHERE target_id = ? ORDER BY collected_at DESC LIMIT 1 `).get(server.target_id) as any // 디스크 정보 조회 (루트 마운트 또는 최대 사용률) const disk = db.prepare(` SELECT disk_percent FROM server_disks WHERE target_id = ? ORDER BY CASE WHEN mount_point = '/' THEN 0 ELSE 1 END, disk_percent DESC LIMIT 1 `).get(server.target_id) as any // 오프라인 체크 let isOffline = true let lastCollected = null if (snapshot && snapshot.collected_at) { lastCollected = snapshot.collected_at const collectedTime = new Date(snapshot.collected_at.replace(' ', 'T') + '+09:00').getTime() isOffline = (now.getTime() - collectedTime) > offlineThreshold } // 서버 레벨 계산 let serverLevel = 'offline' let cpuLevel = 'normal', memLevel = 'normal', diskLevel = 'normal' if (!isOffline && snapshot) { cpuLevel = getLevel(snapshot.cpu_percent, thresholds.server?.cpu || { warning: 70, critical: 85, danger: 95 }) memLevel = getLevel(snapshot.memory_percent, thresholds.server?.memory || { warning: 80, critical: 90, danger: 95 }) diskLevel = getLevel(disk?.disk_percent ?? null, thresholds.server?.disk || { warning: 80, critical: 90, danger: 95 }) serverLevel = getHighestLevel([cpuLevel, memLevel, diskLevel]) } // 컨테이너 조회 (최신 데이터, 중복 제거) const containers = db.prepare(` SELECT container_name, container_status, cpu_percent, memory_usage, memory_limit, uptime, network_rx, network_tx FROM server_containers WHERE target_id = ? AND collected_at = ( SELECT MAX(collected_at) FROM server_containers WHERE target_id = ? ) GROUP BY container_name ORDER BY container_name `).all(server.target_id, server.target_id) as any[] const containerStatuses: any[] = [] const containerSummary = { total: containers.length, normal: 0, warning: 0, critical: 0, danger: 0, stopped: 0 } for (const c of containers) { let containerLevel = 'normal' if (c.container_status !== 'running') { containerLevel = 'stopped' containerSummary.stopped++ } else { const cCpuLevel = getLevel(c.cpu_percent, thresholds.container?.cpu || { warning: 80, critical: 90, danger: 95 }) const cMemPercent = c.memory_limit ? (c.memory_usage / c.memory_limit * 100) : null const cMemLevel = getLevel(cMemPercent, thresholds.container?.memory || { warning: 80, critical: 90, danger: 95 }) containerLevel = getHighestLevel([cCpuLevel, cMemLevel]) if (containerLevel === 'normal') containerSummary.normal++ else if (containerLevel === 'warning') containerSummary.warning++ else if (containerLevel === 'critical') containerSummary.critical++ else if (containerLevel === 'danger') containerSummary.danger++ } containerStatuses.push({ name: c.container_name, status: c.container_status, 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 }) } // 요약 집계 summaryContainers.total += containerSummary.total summaryContainers.normal += containerSummary.normal summaryContainers.warning += containerSummary.warning summaryContainers.critical += containerSummary.critical summaryContainers.danger += containerSummary.danger summaryContainers.stopped += containerSummary.stopped 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++ serverStatuses.push({ target_id: server.target_id, server_name: server.server_name, level: serverLevel, cpu_percent: snapshot?.cpu_percent ?? null, cpu_level: cpuLevel, memory_percent: snapshot?.memory_percent ?? null, memory_level: memLevel, disk_percent: disk?.disk_percent ?? null, disk_level: diskLevel, last_collected: lastCollected, containers: containerStatuses, container_summary: containerSummary }) } // 서버 정렬: 컨테이너 많은 순 → 이름 순 serverStatuses.sort((a, b) => { const containerDiff = (b.container_summary?.total || 0) - (a.container_summary?.total || 0) if (containerDiff !== 0) return containerDiff 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(() => { const data = getNetworkStatus() client.ws.send(JSON.stringify({ type: 'status', data })) // 서버 대시보드 데이터도 전송 const serverData = getServerDashboard() client.ws.send(JSON.stringify({ type: 'server', data: serverData })) }, client.interval) } } export default defineWebSocketHandler({ open(peer) { console.log('[WebSocket] Client connected') const client: Client = { ws: peer, interval: 60 * 1000, // 기본 1분 timer: null, autoRefresh: true } clients.set(peer, client) // 초기 데이터 전송 const data = getNetworkStatus() peer.send(JSON.stringify({ type: 'status', data })) // 서버 대시보드 데이터 전송 const serverData = getServerDashboard() peer.send(JSON.stringify({ type: 'server', data: serverData })) // 자동 갱신 시작 startAutoRefresh(client) }, 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': // 자동 갱신 ON/OFF 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 = getHistoricalData(msg.datetime) peer.send(JSON.stringify({ type: 'historical', data: historicalData })) break case 'refresh': // 즉시 갱신 요청 const currentData = getNetworkStatus() peer.send(JSON.stringify({ type: 'status', data: currentData })) // 서버 데이터도 전송 const currentServerData = getServerDashboard() peer.send(JSON.stringify({ type: 'server', data: currentServerData })) break case 'refresh_server': // 서버 대시보드만 즉시 갱신 const serverDashData = 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) } })