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.
|
- [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.
|
- [ ] **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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Start Goals - shown in Pro mode below the overview cards -->
|
<!-- Quick Start Goals - shown in Pro mode below the overview cards -->
|
||||||
@ -297,7 +350,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { RouterLink, useRouter } from 'vue-router'
|
||||||
import { useAppStore } from '../stores/app'
|
import { useAppStore } from '../stores/app'
|
||||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||||
@ -308,6 +361,7 @@ import { playTypingSound } from '@/composables/useLoginSounds'
|
|||||||
import { GOALS } from '@/data/goals'
|
import { GOALS } from '@/data/goals'
|
||||||
import EasyHome from '@/components/EasyHome.vue'
|
import EasyHome from '@/components/EasyHome.vue'
|
||||||
import { fileBrowserClient } from '@/api/filebrowser-client'
|
import { fileBrowserClient } from '@/api/filebrowser-client'
|
||||||
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const uiMode = useUIModeStore()
|
const uiMode = useUIModeStore()
|
||||||
@ -344,6 +398,7 @@ const line2Text = computed(() =>
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (typingInterval) clearInterval(typingInterval)
|
if (typingInterval) clearInterval(typingInterval)
|
||||||
|
if (systemStatsInterval) clearInterval(systemStatsInterval)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => loginTransition.pendingWelcomeTyping, (pending) => {
|
watch(() => loginTransition.pendingWelcomeTyping, (pending) => {
|
||||||
@ -471,8 +526,68 @@ onMounted(async () => {
|
|||||||
} catch {
|
} catch {
|
||||||
// FileBrowser may not be running — leave as loading
|
// 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() {
|
function uploadFiles() {
|
||||||
const pkg = packages.value['filebrowser']
|
const pkg = packages.value['filebrowser']
|
||||||
if (pkg && pkg.state === PackageState.Running) {
|
if (pkg && pkg.state === PackageState.Running) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user