feat: real-time system monitor in footer

This commit is contained in:
xtcnet 2026-03-08 20:35:06 +07:00
parent 34020bc562
commit e48fef3154
4 changed files with 109 additions and 5 deletions

View file

@ -34,6 +34,7 @@
"proxy-agent": "^6.5.0", "proxy-agent": "^6.5.0",
"signale": "1.4.0", "signale": "1.4.0",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"systeminformation": "^5.31.3",
"temp-write": "^6.0.1" "temp-write": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {
@ -5137,6 +5138,32 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/tar": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",

View file

@ -38,6 +38,7 @@
"proxy-agent": "^6.5.0", "proxy-agent": "^6.5.0",
"signale": "1.4.0", "signale": "1.4.0",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"systeminformation": "^5.31.3",
"temp-write": "^6.0.1" "temp-write": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,3 +1,4 @@
import si from "systeminformation";
import express from "express"; import express from "express";
import internalReport from "../internal/report.js"; import internalReport from "../internal/report.js";
import jwtdecode from "../lib/express/jwt-decode.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; export default router;

View file

@ -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() { 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 ( return (
<footer className="footer d-print-none py-3"> <footer className="footer d-print-none py-3">
<div className="container-xl"> <div className="container-xl">
<div className="row text-center align-items-center flex-row-reverse"> <div className="row text-center align-items-center flex-row-reverse">
<div className="col-lg-auto ms-lg-auto"> <div className="col-lg-auto ms-lg-auto d-flex gap-3 align-items-center text-muted small">
<div title="CPU Usage" className="d-flex align-items-center gap-1">
<IconCpu size={16} />
<span>{sysStats.cpu}%</span>
</div>
<div title="Memory Usage" className="d-flex align-items-center gap-1">
<IconServer size={16} />
<span>{sysStats.memory}%</span>
</div>
<div title="Network Bandwidth" className="d-flex align-items-center gap-1">
<IconArrowsDownUp size={16} />
<span>{sysStats.networkRx} {sysStats.networkTx} Mbps</span>
</div>
</div> </div>
<div className="col-12 col-lg-auto mt-3 mt-lg-0"> <div className="col-12 col-lg-auto mt-3 mt-lg-0">
<ul className="list-inline list-inline-dots mb-0"> <ul className="list-inline list-inline-dots mb-0">
<li className="list-inline-item"> <li className="list-inline-item">
© D3V.AC 2026{" "} © D3V.AC 2026{" "}
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>