diff --git a/backend/api/server/history/snapshots.get.ts b/backend/api/server/history/snapshots.get.ts index 816b639..ceb0a0d 100644 --- a/backend/api/server/history/snapshots.get.ts +++ b/backend/api/server/history/snapshots.get.ts @@ -32,7 +32,15 @@ export default defineEventHandler(async (event) => { SELECT snapshot_id, cpu_percent, + cpu_temp, + load_percent, memory_percent, + memory_total, + memory_used, + memory_free, + swap_percent, + swap_total, + swap_used, is_online, collected_at FROM server_snapshots diff --git a/backend/utils/server-scheduler.ts b/backend/utils/server-scheduler.ts index ba3e0a5..03a9ce6 100644 --- a/backend/utils/server-scheduler.ts +++ b/backend/utils/server-scheduler.ts @@ -455,10 +455,10 @@ async function collectServerData(target: ServerTarget) { await execute(` INSERT INTO server_snapshots ( target_id, os_name, os_version, host_name, uptime_seconds, uptime_str, ip_address, - cpu_name, cpu_count, cpu_percent, memory_total, memory_used, memory_percent, + cpu_name, cpu_count, cpu_percent, memory_total, memory_used, memory_free, memory_percent, swap_total, swap_used, swap_percent, is_online, api_version, cpu_temp, load_1, load_5, load_15, load_percent, collected_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25) `, [ target.target_id, system?.os_name || system?.linux_distro || null, @@ -472,6 +472,7 @@ async function collectServerData(target: ServerTarget) { cpu?.total ?? quicklook?.cpu ?? null, mem?.total || null, mem?.used || null, + mem?.free || null, mem?.percent || null, memswap?.total || null, memswap?.used || null, diff --git a/frontend/server/history.vue b/frontend/server/history.vue index 9800bac..fecc4f7 100644 --- a/frontend/server/history.vue +++ b/frontend/server/history.vue @@ -413,15 +413,21 @@ async function fetchSnapshots() { { label: 'Swap %', data: swapData, borderColor: chartColors[2] } ]) - // 평균 계산 (Memory, Swap) + 사용량/전체용량 + // 평균 계산 (Memory, Swap) + 사용량/전체용량 (BigInt는 문자열로 반환되므로 Number로 변환) + // 메모리 사용량 = total - free (free가 있으면 사용, 없으면 used 사용) const validMem = memData.filter((v: number) => v > 0) const validSwap = swapData.filter((v: number) => v >= 0) - const memUsedData = data.map((d: any) => d.memory_used || 0).filter((v: number) => v > 0) + const memUsedData = data.map((d: any) => { + const total = Number(d.memory_total) || 0 + const free = Number(d.memory_free) || 0 + const used = Number(d.memory_used) || 0 + return free > 0 ? (total - free) : used + }).filter((v: number) => v > 0) const avgMemUsedGB = memUsedData.length ? (memUsedData.reduce((a: number, b: number) => a + b, 0) / memUsedData.length / (1024 * 1024 * 1024)).toFixed(1) : '-' - const memTotalGB = data[0]?.memory_total ? (data[0].memory_total / (1024 * 1024 * 1024)).toFixed(1) : '-' - const swapUsedData = data.map((d: any) => d.swap_used || 0).filter((v: number) => v >= 0) + const memTotalGB = data[0]?.memory_total ? (Number(data[0].memory_total) / (1024 * 1024 * 1024)).toFixed(1) : '-' + const swapUsedData = data.map((d: any) => Number(d.swap_used) || 0).filter((v: number) => v >= 0) const avgSwapUsedGB = swapUsedData.length ? (swapUsedData.reduce((a: number, b: number) => a + b, 0) / swapUsedData.length / (1024 * 1024 * 1024)).toFixed(1) : '0' - const swapTotalGB = data[0]?.swap_total ? (data[0].swap_total / (1024 * 1024 * 1024)).toFixed(1) : '0' + const swapTotalGB = data[0]?.swap_total ? (Number(data[0].swap_total) / (1024 * 1024 * 1024)).toFixed(1) : '0' memAvg.value = { mem: validMem.length ? (validMem.reduce((a: number, b: number) => a + b, 0) / validMem.length).toFixed(1) : '-', swap: validSwap.length ? (validSwap.reduce((a: number, b: number) => a + b, 0) / validSwap.length).toFixed(1) : '-', @@ -449,10 +455,10 @@ async function fetchDisks() { diskChart?.destroy() diskChart = createLineChart(diskChartRef.value!, timeLabels, datasets) - // 평균 계산 (전체 디스크) + 사용량/전체용량 + // 평균 계산 (전체 디스크) + 사용량/전체용량 (BigInt는 문자열로 반환되므로 Number로 변환) const allPercents = data.map((d: any) => d.disk_percent || 0).filter((v: number) => v > 0) - const allUsed = data.map((d: any) => d.disk_used || 0).filter((v: number) => v > 0) - const allTotal = data.map((d: any) => d.disk_total || 0).filter((v: number) => v > 0) + const allUsed = data.map((d: any) => Number(d.disk_used) || 0).filter((v: number) => v > 0) + const allTotal = data.map((d: any) => Number(d.disk_total) || 0).filter((v: number) => v > 0) const avgUsedGB = allUsed.length ? (allUsed.reduce((a: number, b: number) => a + b, 0) / allUsed.length / (1024 * 1024 * 1024)).toFixed(1) : '-' const avgTotalGB = allTotal.length ? (allTotal.reduce((a: number, b: number) => a + b, 0) / allTotal.length / (1024 * 1024 * 1024)).toFixed(1) : '-' diskAvg.value = { @@ -489,14 +495,14 @@ async function fetchContainers() { const cpuValues = containerRows.map((d: any) => d.cpu_percent || 0) const cpuAvgVal = cpuValues.length ? (cpuValues.reduce((a: number, b: number) => a + b, 0) / cpuValues.length).toFixed(1) : '0' - // Memory 평균 (bytes -> MB) - const memValues = containerRows.map((d: any) => (d.memory_usage || 0) / 1024 / 1024) + // Memory 평균 (bytes -> MB, BigInt는 문자열로 반환되므로 Number로 변환) + const memValues = containerRows.map((d: any) => (Number(d.memory_usage) || 0) / 1024 / 1024) const memAvgVal = memValues.length ? (memValues.reduce((a: number, b: number) => a + b, 0) / memValues.length) : 0 - const memLimit = latest.memory_limit ? (latest.memory_limit / 1024 / 1024 / 1024).toFixed(1) + ' GB' : '-' + const memLimit = latest.memory_limit ? (Number(latest.memory_limit) / 1024 / 1024 / 1024).toFixed(1) + ' GB' : '-' // Network 평균 (bytes/s -> KB/s) - const rxValues = containerRows.map((d: any) => (d.network_rx || 0) / 1024) - const txValues = containerRows.map((d: any) => (d.network_tx || 0) / 1024) + const rxValues = containerRows.map((d: any) => (Number(d.network_rx) || 0) / 1024) + const txValues = containerRows.map((d: any) => (Number(d.network_tx) || 0) / 1024) const rxAvgVal = rxValues.length ? (rxValues.reduce((a: number, b: number) => a + b, 0) / rxValues.length) : 0 const txAvgVal = txValues.length ? (txValues.reduce((a: number, b: number) => a + b, 0) / txValues.length) : 0 @@ -526,22 +532,22 @@ async function fetchContainers() { if (!containerCharts[name]) containerCharts[name] = {} - // CPU 차트 (0-100%) + // CPU 차트 (0-200% - 컨테이너는 멀티코어로 100% 초과 가능) if (refs.cpu) { const cpuData = containerRows.map((d: any) => d.cpu_percent || 0) - containerCharts[name].cpu = createLineChart(refs.cpu, timeLabels, [{ label: 'CPU %', data: cpuData, borderColor: '#3b82f6' }], 100) + containerCharts[name].cpu = createLineChart(refs.cpu, timeLabels, [{ label: 'CPU %', data: cpuData, borderColor: '#3b82f6' }], 200) } - // Memory 차트 (MB 단위) - 자동 스케일 + // Memory 차트 (MB 단위) - 자동 스케일 (BigInt 문자열 변환) if (refs.mem) { - const memData = containerRows.map((d: any) => (d.memory_usage || 0) / 1024 / 1024) + const memData = containerRows.map((d: any) => (Number(d.memory_usage) || 0) / 1024 / 1024) containerCharts[name].mem = createLineChart(refs.mem, timeLabels, [{ label: 'Memory MB', data: memData, borderColor: '#22c55e' }], null) } - // Network 차트 (KB/s 단위) - 자동 스케일 + // Network 차트 (KB/s 단위) - 자동 스케일 (BigInt 문자열 변환) if (refs.net) { - const rxData = containerRows.map((d: any) => (d.network_rx || 0) / 1024) - const txData = containerRows.map((d: any) => (d.network_tx || 0) / 1024) + const rxData = containerRows.map((d: any) => (Number(d.network_rx) || 0) / 1024) + const txData = containerRows.map((d: any) => (Number(d.network_tx) || 0) / 1024) containerCharts[name].net = createLineChart(refs.net, timeLabels, [ { label: 'RX KB/s', data: rxData, borderColor: '#06b6d4' }, { label: 'TX KB/s', data: txData, borderColor: '#f59e0b' }