feat: real-time system monitor in footer
This commit is contained in:
parent
34020bc562
commit
e48fef3154
4 changed files with 109 additions and 5 deletions
27
backend/package-lock.json
generated
27
backend/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue