diff --git a/backend/package-lock.json b/backend/package-lock.json index b92dbce..5d41b0c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -34,6 +34,7 @@ "proxy-agent": "^6.5.0", "signale": "1.4.0", "sqlite3": "^5.1.7", + "systeminformation": "^5.31.3", "temp-write": "^6.0.1" }, "devDependencies": { @@ -5137,6 +5138,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/systeminformation": { + "version": "5.31.3", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.3.tgz", + "integrity": "sha512-vX0eeI7oGIr79NLiJRWnK8SyxDjyiNOEanaQnHRNyb5ep8QcpD8QMDvrukdrxV4pV4AKjwUDfaypXnWHMC/65A==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", diff --git a/backend/package.json b/backend/package.json index e31bf22..9749168 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,6 +38,7 @@ "proxy-agent": "^6.5.0", "signale": "1.4.0", "sqlite3": "^5.1.7", + "systeminformation": "^5.31.3", "temp-write": "^6.0.1" }, "devDependencies": { diff --git a/backend/routes/reports.js b/backend/routes/reports.js index df9962a..bff826f 100644 --- a/backend/routes/reports.js +++ b/backend/routes/reports.js @@ -1,3 +1,4 @@ +import si from "systeminformation"; import express from "express"; import internalReport from "../internal/report.js"; import jwtdecode from "../lib/express/jwt-decode.js"; @@ -29,4 +30,36 @@ router } }); + /** + * GET /reports/system + */ +router + .route("/system") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .get(async (req, res, next) => { + try { + const [cpuTotal, memData, networkStats] = await Promise.all([ + si.currentLoad(), + si.mem(), + si.networkStats("*"), + ]); + + // Grab eth0 or the first active interface + const activeNet = networkStats.find(n => n.operstate === 'up' && n.iface !== 'lo') || networkStats[0] || {}; + + res.status(200).json({ + cpu: Math.round(cpuTotal.currentLoad), + memory: Math.round((memData.active / memData.total) * 100), + networkRx: (activeNet.rx_sec / 1024 / 1024 * 8).toFixed(2), // Mbps + networkTx: (activeNet.tx_sec / 1024 / 1024 * 8).toFixed(2), // Mbps + }); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + export default router; diff --git a/frontend/src/components/SiteFooter.tsx b/frontend/src/components/SiteFooter.tsx index 984cb6d..728ae57 100644 --- a/frontend/src/components/SiteFooter.tsx +++ b/frontend/src/components/SiteFooter.tsx @@ -1,20 +1,63 @@ +import { useEffect, useState } from "react"; +import { IconCpu, IconServer, IconArrowsDownUp } from "@tabler/icons-react"; +import * as api from "../api/backend/base"; + export function SiteFooter() { + const [sysStats, setSysStats] = useState({ + cpu: 0, + memory: 0, + networkRx: "0.00", + networkTx: "0.00" + }); + + useEffect(() => { + let isMounted = true; + + const fetchStats = async () => { + try { + const data = await api.get({ url: "/reports/system" }); + if (isMounted && data) { + setSysStats(data); + } + } catch (err) { + // Silently fail polling to prevent console flood + } + }; + + // Initial fetch + fetchStats(); + + // Poll every 1 second + const interval = setInterval(fetchStats, 1000); + return () => { + isMounted = false; + clearInterval(interval); + }; + }, []); return (