diff --git a/components/SystemMetricsChart.vue b/components/SystemMetricsChart.vue
new file mode 100644
index 0000000..1ff1d98
--- /dev/null
+++ b/components/SystemMetricsChart.vue
@@ -0,0 +1,333 @@
+
+
+
+
+
+
+
diff --git a/components/SystemResources.vue b/components/SystemResources.vue
new file mode 100644
index 0000000..ee9a0bd
--- /dev/null
+++ b/components/SystemResources.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+ {{ metric.label }}
+ {{ metric.detail }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ metric.percent }}%
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/index.vue b/pages/index.vue
index a17bc8a..3a603f5 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -36,6 +36,21 @@
+
+
+
+
+
@@ -75,10 +90,23 @@ type DiagramItem = {
totalText: string
}
+type SystemMetrics = {
+ cpuPercent: number
+ memoryPercent: number
+ totalMemory: number
+ usedMemory: number
+ incomingMbps: number
+ outgoingMbps: number
+ sampledAt: string
+}
+
const rawResults = ref([])
const loading = ref(false)
+const systemMetrics = ref(null)
+const systemLoading = ref(true)
const chartRadius = 52
const chartCircumference = 2 * Math.PI * chartRadius
+let systemTimer: ReturnType | null = null
const getHostName = (output: string, fallback: string) => {
const hostMatch = output.match(/Name:\s*(.+)/i)
@@ -167,8 +195,27 @@ const runScript = async () => {
}
}
+const loadSystemMetrics = async () => {
+ try {
+ systemMetrics.value = await $fetch("/api/system")
+ } catch {
+ systemMetrics.value = null
+ } finally {
+ systemLoading.value = false
+ }
+}
+
onMounted(() => {
runScript()
+ loadSystemMetrics()
+ systemTimer = setInterval(loadSystemMetrics, 2000)
+})
+
+onBeforeUnmount(() => {
+ if (systemTimer) {
+ clearInterval(systemTimer)
+ systemTimer = null
+ }
})
@@ -228,6 +275,14 @@ onMounted(() => {
align-items: start;
}
+.metrics-row {
+ margin-top: 1.5rem;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 280px;
+ gap: 1.5rem;
+ align-items: start;
+}
+
.grid-left,
.grid-middle,
.grid-right {
@@ -259,7 +314,8 @@ onMounted(() => {
.storage-grid,
.content-grid,
- .dashboard-grid {
+ .dashboard-grid,
+ .metrics-row {
grid-template-columns: 1fr;
}
diff --git a/server/api/system.get.ts b/server/api/system.get.ts
new file mode 100644
index 0000000..7ce0fb1
--- /dev/null
+++ b/server/api/system.get.ts
@@ -0,0 +1,165 @@
+import fs from "node:fs"
+import os from "node:os"
+
+type CpuTimesSnapshot = {
+ idle: number
+ total: number
+}
+
+type NetworkSnapshot = {
+ rxBytes: number
+ txBytes: number
+ timestamp: number
+}
+
+let previousNetworkSnapshot: NetworkSnapshot | null = null
+
+function getCpuSnapshot(): CpuTimesSnapshot {
+ const cpus = os.cpus()
+
+ return cpus.reduce(
+ (snapshot, cpu) => {
+ const total = Object.values(cpu.times).reduce((sum, value) => sum + value, 0)
+
+ snapshot.idle += cpu.times.idle
+ snapshot.total += total
+
+ return snapshot
+ },
+ {idle: 0, total: 0}
+ )
+}
+
+function wait(durationMs: number) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, durationMs)
+ })
+}
+
+async function getCpuUsagePercent(sampleMs: number) {
+ const start = getCpuSnapshot()
+ await wait(sampleMs)
+ const end = getCpuSnapshot()
+
+ const idleDelta = end.idle - start.idle
+ const totalDelta = end.total - start.total
+
+ if (totalDelta <= 0) {
+ return 0
+ }
+
+ return Math.max(0, Math.min(100, Math.round((1 - idleDelta / totalDelta) * 100)))
+}
+
+function getNetworkTotals() {
+ try {
+ const content = fs.readFileSync("/proc/net/dev", "utf8")
+ const totals = content
+ .split("\n")
+ .slice(2)
+ .map((line) => line.trim())
+ .filter(Boolean)
+ .reduce(
+ (accumulator, line) => {
+ const [namePart, valuesPart] = line.split(":")
+ const interfaceName = namePart?.trim()
+
+ if (!interfaceName || interfaceName === "lo" || !valuesPart) {
+ return accumulator
+ }
+
+ const values = valuesPart.trim().split(/\s+/)
+ const rxBytes = Number.parseInt(values[0] || "0", 10)
+ const txBytes = Number.parseInt(values[8] || "0", 10)
+
+ if (Number.isFinite(rxBytes)) {
+ accumulator.rxBytes += rxBytes
+ }
+
+ if (Number.isFinite(txBytes)) {
+ accumulator.txBytes += txBytes
+ }
+
+ return accumulator
+ },
+ {rxBytes: 0, txBytes: 0}
+ )
+
+ return totals
+ } catch {
+ return null
+ }
+}
+
+function getNetworkRatesMbps() {
+ const totals = getNetworkTotals()
+ const now = Date.now()
+
+ if (!totals) {
+ previousNetworkSnapshot = null
+ return {
+ incomingMbps: 0,
+ outgoingMbps: 0
+ }
+ }
+
+ const currentSnapshot: NetworkSnapshot = {
+ ...totals,
+ timestamp: now
+ }
+
+ if (!previousNetworkSnapshot) {
+ previousNetworkSnapshot = currentSnapshot
+
+ return {
+ incomingMbps: 0,
+ outgoingMbps: 0
+ }
+ }
+
+ const elapsedSeconds = (currentSnapshot.timestamp - previousNetworkSnapshot.timestamp) / 1000
+
+ if (elapsedSeconds <= 0) {
+ previousNetworkSnapshot = currentSnapshot
+
+ return {
+ incomingMbps: 0,
+ outgoingMbps: 0
+ }
+ }
+
+ const incomingMbps = Math.max(
+ 0,
+ Number((((currentSnapshot.rxBytes - previousNetworkSnapshot.rxBytes) * 8) / elapsedSeconds / 1000000).toFixed(2))
+ )
+ const outgoingMbps = Math.max(
+ 0,
+ Number((((currentSnapshot.txBytes - previousNetworkSnapshot.txBytes) * 8) / elapsedSeconds / 1000000).toFixed(2))
+ )
+
+ previousNetworkSnapshot = currentSnapshot
+
+ return {
+ incomingMbps,
+ outgoingMbps
+ }
+}
+
+export default defineEventHandler(async () => {
+ const totalMemory = os.totalmem()
+ const freeMemory = os.freemem()
+ const usedMemory = totalMemory - freeMemory
+ const memoryPercent = totalMemory > 0 ? Math.round((usedMemory / totalMemory) * 100) : 0
+ const cpuPercent = await getCpuUsagePercent(200)
+ const {incomingMbps, outgoingMbps} = getNetworkRatesMbps()
+
+ return {
+ cpuPercent,
+ memoryPercent,
+ totalMemory,
+ usedMemory,
+ incomingMbps,
+ outgoingMbps,
+ sampledAt: new Date().toISOString()
+ }
+})