feat: add system stats card to Dashboard with CPU/RAM/Disk gauges
Full-width card with color-coded progress bars (green <70%, orange 70-90%, red >90%) and uptime display. Calls system.stats RPC on mount and refreshes every 30s. Deployed and verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e12a50f938
commit
fce67baa9c
@ -50,7 +50,7 @@
|
||||
|
||||
- [x] **BACK-01** — Add system monitoring RPC endpoints. Create `core/archipelago/src/api/rpc/system.rs` with handlers for: `system.stats` (CPU usage, RAM used/total, disk used/total, uptime, load average), `system.processes` (top 10 by CPU), `system.temperature` (if available). Read from `/proc/stat`, `/proc/meminfo`, `/proc/uptime`, `df`, and `/sys/class/thermal/` on Linux. Register in `core/archipelago/src/api/rpc/mod.rs` route table. **Acceptance**: `curl -X POST http://localhost:5678/rpc/v1 -d '{"method":"system.stats"}'` returns real metrics on dev server.
|
||||
|
||||
- [ ] **BACK-02** — Add system monitoring to frontend Dashboard. In `neode-ui/src/views/Home.vue`, add a system stats section (CPU, RAM, Disk gauges) that calls `system.stats` RPC on mount and refreshes every 30s. Use `bg-white/5 rounded-lg` sub-cards inside an existing glass container. Show percentage bars with color coding (green <70%, orange 70-90%, red >90%). **Acceptance**: Dashboard shows real CPU/RAM/Disk usage. Deploy and verify.
|
||||
- [x] **BACK-02** — Add system monitoring to frontend Dashboard. In `neode-ui/src/views/Home.vue`, add a system stats section (CPU, RAM, Disk gauges) that calls `system.stats` RPC on mount and refreshes every 30s. Use `bg-white/5 rounded-lg` sub-cards inside an existing glass container. Show percentage bars with color coding (green <70%, orange 70-90%, red >90%). **Acceptance**: Dashboard shows real CPU/RAM/Disk usage. Deploy and verify.
|
||||
|
||||
- [ ] **BACK-03** — Add WiFi/Ethernet configuration RPC endpoints. Create `core/archipelago/src/network/interfaces.rs` with: `network.list-interfaces` (lists eth0, wlan0, etc. with IP, MAC, status), `network.configure-wifi` (SSID, password, connects via `nmcli`), `network.configure-ethernet` (static IP or DHCP via `nmcli`), `network.scan-wifi` (available networks). Register in RPC router. **Acceptance**: `network.list-interfaces` returns real interface data on dev server.
|
||||
|
||||
|
||||
@ -245,6 +245,59 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- System Stats -->
|
||||
<div
|
||||
data-controller-container
|
||||
tabindex="0"
|
||||
class="home-card controller-focusable lg:col-span-2"
|
||||
:class="{ 'home-card-animate': animateCards }"
|
||||
style="--card-stagger: 4"
|
||||
>
|
||||
<div class="home-card-shell">
|
||||
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
||||
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
||||
<div class="home-card-text">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">System</h2>
|
||||
<p class="text-sm text-white/70">{{ systemUptimeDisplay }}</p>
|
||||
</div>
|
||||
<RouterLink to="/dashboard/server" class="text-white/60 hover:text-white transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="home-card-stats grid grid-cols-1 sm:grid-cols-3 gap-4 flex-1 min-h-0">
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-xs text-white/60">CPU</p>
|
||||
<p class="text-sm font-medium" :class="gaugeTextColor(systemStats.cpuPercent)">{{ systemStats.cpuPercent.toFixed(0) }}%</p>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
<div class="h-full rounded-full transition-all duration-500" :class="gaugeBarColor(systemStats.cpuPercent)" :style="{ width: systemStats.cpuPercent + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-xs text-white/60">RAM</p>
|
||||
<p class="text-sm font-medium" :class="gaugeTextColor(systemStats.memPercent)">{{ formatBytes(systemStats.memUsed) }} / {{ formatBytes(systemStats.memTotal) }}</p>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
<div class="h-full rounded-full transition-all duration-500" :class="gaugeBarColor(systemStats.memPercent)" :style="{ width: systemStats.memPercent + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-xs text-white/60">Disk</p>
|
||||
<p class="text-sm font-medium" :class="gaugeTextColor(systemStats.diskPercent)">{{ formatBytes(systemStats.diskUsed) }} / {{ formatBytes(systemStats.diskTotal) }}</p>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
<div class="h-full rounded-full transition-all duration-500" :class="gaugeBarColor(systemStats.diskPercent)" :style="{ width: systemStats.diskPercent + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Start Goals - shown in Pro mode below the overview cards -->
|
||||
@ -297,7 +350,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { computed, reactive, ref, watch, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
@ -308,6 +361,7 @@ import { playTypingSound } from '@/composables/useLoginSounds'
|
||||
import { GOALS } from '@/data/goals'
|
||||
import EasyHome from '@/components/EasyHome.vue'
|
||||
import { fileBrowserClient } from '@/api/filebrowser-client'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
const router = useRouter()
|
||||
const uiMode = useUIModeStore()
|
||||
@ -344,6 +398,7 @@ const line2Text = computed(() =>
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (typingInterval) clearInterval(typingInterval)
|
||||
if (systemStatsInterval) clearInterval(systemStatsInterval)
|
||||
})
|
||||
|
||||
watch(() => loginTransition.pendingWelcomeTyping, (pending) => {
|
||||
@ -471,8 +526,68 @@ onMounted(async () => {
|
||||
} catch {
|
||||
// FileBrowser may not be running — leave as loading
|
||||
}
|
||||
loadSystemStats()
|
||||
systemStatsInterval = setInterval(loadSystemStats, 30000)
|
||||
})
|
||||
|
||||
// System stats
|
||||
const systemStats = reactive({
|
||||
cpuPercent: 0,
|
||||
memUsed: 0,
|
||||
memTotal: 0,
|
||||
memPercent: 0,
|
||||
diskUsed: 0,
|
||||
diskTotal: 0,
|
||||
diskPercent: 0,
|
||||
uptimeSecs: 0,
|
||||
})
|
||||
|
||||
const systemUptimeDisplay = computed(() => {
|
||||
if (systemStats.uptimeSecs === 0) return 'System monitoring'
|
||||
const days = Math.floor(systemStats.uptimeSecs / 86400)
|
||||
const hours = Math.floor((systemStats.uptimeSecs % 86400) / 3600)
|
||||
if (days > 0) return `Uptime: ${days}d ${hours}h`
|
||||
const mins = Math.floor((systemStats.uptimeSecs % 3600) / 60)
|
||||
return `Uptime: ${hours}h ${mins}m`
|
||||
})
|
||||
|
||||
function gaugeTextColor(pct: number): string {
|
||||
if (pct >= 90) return 'text-red-400'
|
||||
if (pct >= 70) return 'text-orange-400'
|
||||
return 'text-green-400'
|
||||
}
|
||||
|
||||
function gaugeBarColor(pct: number): string {
|
||||
if (pct >= 90) return 'bg-red-400'
|
||||
if (pct >= 70) return 'bg-orange-400'
|
||||
return 'bg-green-400'
|
||||
}
|
||||
|
||||
let systemStatsInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
async function loadSystemStats() {
|
||||
try {
|
||||
const res = await rpcClient.call<{
|
||||
cpu_usage_percent: number
|
||||
mem_used_bytes: number
|
||||
mem_total_bytes: number
|
||||
disk_used_bytes: number
|
||||
disk_total_bytes: number
|
||||
uptime_secs: number
|
||||
}>({ method: 'system.stats' })
|
||||
systemStats.cpuPercent = res.cpu_usage_percent
|
||||
systemStats.memUsed = res.mem_used_bytes
|
||||
systemStats.memTotal = res.mem_total_bytes
|
||||
systemStats.memPercent = res.mem_total_bytes > 0 ? (res.mem_used_bytes / res.mem_total_bytes) * 100 : 0
|
||||
systemStats.diskUsed = res.disk_used_bytes
|
||||
systemStats.diskTotal = res.disk_total_bytes
|
||||
systemStats.diskPercent = res.disk_total_bytes > 0 ? (res.disk_used_bytes / res.disk_total_bytes) * 100 : 0
|
||||
systemStats.uptimeSecs = res.uptime_secs
|
||||
} catch {
|
||||
// RPC unavailable — keep defaults
|
||||
}
|
||||
}
|
||||
|
||||
function uploadFiles() {
|
||||
const pkg = packages.value['filebrowser']
|
||||
if (pkg && pkg.state === PackageState.Running) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user