gpu_memory_percent is serialized as JSON null (not omitted) when no GPU is present, but the template checked `!== undefined`, which is true for null too. Calling .toFixed() on that null then threw during render, blanking the whole widget instead of just hiding the GPU row. Only showed up on GPU-less hosts like a Lightsail instance, not on this dev machine which has a GPU.
123 lines
2.7 KiB
Vue
123 lines
2.7 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
interface MemoryStats {
|
|
cpu_percent: number
|
|
memory_percent: number
|
|
memory_used_mb: number
|
|
memory_total_mb: number
|
|
gpu_memory_used_mb?: number
|
|
gpu_memory_total_mb?: number
|
|
gpu_memory_percent?: number
|
|
}
|
|
|
|
const stats = ref<MemoryStats | null>(null)
|
|
let intervalId: number | null = null
|
|
|
|
const fetchMemoryStats = async () => {
|
|
try {
|
|
const response = await fetch('/api/system/memory')
|
|
stats.value = await response.json()
|
|
} catch (error) {
|
|
console.error('Failed to fetch memory stats:', error)
|
|
}
|
|
}
|
|
|
|
const formatMemory = (mb: number) => {
|
|
if (mb > 1024) {
|
|
return `${(mb / 1024).toFixed(1)}GB`
|
|
}
|
|
return `${Math.round(mb)}MB`
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchMemoryStats()
|
|
intervalId = window.setInterval(fetchMemoryStats, 10000)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (intervalId) {
|
|
clearInterval(intervalId)
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="memory-monitor" v-if="stats">
|
|
<div class="stat">
|
|
<span class="label">CPU</span>
|
|
<div class="bar">
|
|
<div class="fill" :style="{ width: stats.cpu_percent + '%' }"></div>
|
|
</div>
|
|
<span class="value">{{ stats.cpu_percent.toFixed(1) }}%</span>
|
|
</div>
|
|
|
|
<div class="stat">
|
|
<span class="label">RAM</span>
|
|
<div class="bar">
|
|
<div class="fill" :style="{ width: stats.memory_percent + '%' }"></div>
|
|
</div>
|
|
<span class="value">{{ stats.memory_percent.toFixed(1) }}% ({{ formatMemory(stats.memory_used_mb) }} / {{ formatMemory(stats.memory_total_mb) }})</span>
|
|
</div>
|
|
|
|
<div class="stat" v-if="stats.gpu_memory_percent != null">
|
|
<span class="label">GPU</span>
|
|
<div class="bar">
|
|
<div class="fill gpu" :style="{ width: stats.gpu_memory_percent + '%' }"></div>
|
|
</div>
|
|
<span class="value">{{ stats.gpu_memory_percent.toFixed(1) }}% ({{ formatMemory(stats.gpu_memory_used_mb!) }} / {{ formatMemory(stats.gpu_memory_total_mb!) }})</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.memory-monitor {
|
|
background: #1a1a1a;
|
|
border-top: 1px solid #333;
|
|
padding: 12px;
|
|
font-size: 11px;
|
|
font-family: monospace;
|
|
gap: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.stat {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.label {
|
|
width: 45px;
|
|
color: #888;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.bar {
|
|
flex: 1;
|
|
height: 16px;
|
|
background: #2a2a2a;
|
|
border-radius: 3px;
|
|
border: 1px solid #444;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #4CAF50, #8BC34A);
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.fill.gpu {
|
|
background: linear-gradient(90deg, #2196F3, #64B5F6);
|
|
}
|
|
|
|
.value {
|
|
width: 120px;
|
|
text-align: right;
|
|
color: #aaa;
|
|
}
|
|
</style>
|