diff --git a/backend/routes/_ws.ts b/backend/routes/_ws.ts
index a4b2bba..fde8319 100644
--- a/backend/routes/_ws.ts
+++ b/backend/routes/_ws.ts
@@ -176,16 +176,19 @@ async function getServerDashboard() {
for (const server of servers) {
// 최신 스냅샷
const snapshot = await queryOne(`
- SELECT cpu_percent, memory_percent, collected_at
+ 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
+ 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)
@@ -291,8 +294,16 @@ async function getServerDashboard() {
cpu_level: cpuLevel,
memory_percent: snapshot?.memory_percent ?? null,
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
diff --git a/frontend/components/ServerPortlet.vue b/frontend/components/ServerPortlet.vue
index 888e498..9787d40 100644
--- a/frontend/components/ServerPortlet.vue
+++ b/frontend/components/ServerPortlet.vue
@@ -40,22 +40,28 @@
+
+
+
CPU
- {{ server.cpu_percent?.toFixed(0) || '-' }}
+ {{ server.cpu_percent?.toFixed(0) || '-' }}%
MEM
- {{ server.memory_percent?.toFixed(0) || '-' }}
+ {{ calcMemPercent(server).toFixed(0) }}%
- {{ server.disk_percent?.toFixed(0) || '-' }}
+ {{ server.disk_percent?.toFixed(0) || '-' }}%
+
+
+
+
+
+
@@ -162,8 +186,16 @@ interface ServerStatus {
cpu_level: string
memory_percent: number | null
memory_level: string
+ memory_total: number | null
+ memory_free: number | null
+ memory_used: number | null
disk_percent: number | null
disk_level: string
+ disk_used: number | null
+ disk_total: number | null
+ cpu_temp: number | null
+ load_percent: number | null
+ uptime_str: string | null
last_collected: string | null
containers: ContainerStatus[]
container_summary: { total: number; normal: number; warning: number; critical: number; stopped: number }
@@ -265,6 +297,35 @@ function isContainerChanged(serverId: number, containerName: string, metric: str
return changedKeys.value.has(`container-${serverId}-${containerName}-${metric}`)
}
+// 서버 메모리 퍼센트 계산: (total - free) / total * 100
+function calcMemPercent(server: ServerStatus): number {
+ const total = Number(server.memory_total) || 0
+ const free = Number(server.memory_free) || 0
+ if (total === 0) return 0
+ return ((total - free) / total) * 100
+}
+
+// 서버 메모리 용량 포맷: used/total GB
+function formatServerMem(server: ServerStatus): string {
+ const total = Number(server.memory_total) || 0
+ const free = Number(server.memory_free) || 0
+ if (total === 0) return '-'
+ const used = total - free
+ const usedGB = (used / (1024 * 1024 * 1024)).toFixed(1)
+ const totalGB = (total / (1024 * 1024 * 1024)).toFixed(1)
+ return `${usedGB}/${totalGB}G`
+}
+
+// 서버 디스크 용량 포맷: used/total GB
+function formatServerDisk(server: ServerStatus): string {
+ const used = Number(server.disk_used) || 0
+ const total = Number(server.disk_total) || 0
+ if (total === 0) return '-'
+ const usedGB = (used / (1024 * 1024 * 1024)).toFixed(0)
+ const totalGB = (total / (1024 * 1024 * 1024)).toFixed(0)
+ return `${usedGB}/${totalGB}G`
+}
+
function sortContainers(containers: ContainerStatus[]) {
return [...containers].sort((a, b) => a.name.localeCompare(b.name))
}
@@ -378,12 +439,16 @@ function formatTimeAgo(datetime: string | null): string {
.progress-fill.warning { background: #eab308; }
.progress-fill.critical { background: #f97316; }
.progress-fill.danger { background: #ef4444; }
-.metric-value { font-size: 17px; font-weight: 700; width: 36px; text-align: right; transition: all 0.3s; padding: 2px 4px; border-radius: 4px; }
+.metric-value { font-size: 17px; font-weight: 700; width: 44px; text-align: right; transition: all 0.3s; padding: 2px 4px; border-radius: 4px; }
.metric-value.normal { color: #16a34a; }
.metric-value.warning { color: #ca8a04; }
.metric-value.critical { color: #ea580c; }
.metric-value.danger { color: #dc2626; }
+.extra-row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
+.extra-icon { font-size: 12px; width: 16px; text-align: center; }
+.extra-value { font-size: 12px; font-weight: 600; color: var(--text-secondary); font-family: monospace; }
+
.offline-info { text-align: center; padding: 24px 0; color: var(--text-muted); }
.offline-text { font-size: 18px; margin-bottom: 8px; }
.offline-time { font-size: 15px; opacity: 0.7; }
diff --git a/frontend/server/history.vue b/frontend/server/history.vue
index fecc4f7..eaf3dea 100644
--- a/frontend/server/history.vue
+++ b/frontend/server/history.vue
@@ -405,9 +405,19 @@ async function fetchSnapshots() {
load: validLoad.length ? (validLoad.reduce((a: number, b: number) => a + b, 0) / validLoad.length).toFixed(1) : '-'
}
- // Memory/Swap 라인 차트
- const memData = data.map((d: any) => d.memory_percent || 0)
- const swapData = data.map((d: any) => d.swap_percent || 0)
+ // Memory/Swap 라인 차트 - 퍼센트 계산: (total - free) / total * 100
+ const memData = data.map((d: any) => {
+ const total = Number(d.memory_total) || 0
+ const free = Number(d.memory_free) || 0
+ if (total === 0) return 0
+ return ((total - free) / total) * 100
+ })
+ const swapData = data.map((d: any) => {
+ const total = Number(d.swap_total) || 0
+ const used = Number(d.swap_used) || 0
+ if (total === 0) return 0
+ return (used / total) * 100
+ })
memChart = createLineChart(memChartRef.value!, labels, [
{ label: 'Memory %', data: memData, borderColor: chartColors[1] },
{ label: 'Swap %', data: swapData, borderColor: chartColors[2] }
@@ -415,6 +425,7 @@ async function fetchSnapshots() {
// 평균 계산 (Memory, Swap) + 사용량/전체용량 (BigInt는 문자열로 반환되므로 Number로 변환)
// 메모리 사용량 = total - free (free가 있으면 사용, 없으면 used 사용)
+ // 메모리 퍼센트도 (total - free) / total * 100 으로 계산
const validMem = memData.filter((v: number) => v > 0)
const validSwap = swapData.filter((v: number) => v >= 0)
const memUsedData = data.map((d: any) => {