feat(hub): show server CPU memory disk and network status on monitor page
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -67,6 +67,153 @@
|
||||
let monitorBoardSlowHintTimer = null;
|
||||
let boardEventSource = null;
|
||||
let sseReconnectTimer = null;
|
||||
let hostStatusTimer = null;
|
||||
const HOST_STATUS_POLL_MS = 5000;
|
||||
|
||||
function fmtHostBytes(n) {
|
||||
const v = Number(n);
|
||||
if (!Number.isFinite(v)) return "—";
|
||||
const abs = Math.abs(v);
|
||||
if (abs >= 1e12) return (v / 1e12).toFixed(2) + " TB";
|
||||
if (abs >= 1e9) return (v / 1e9).toFixed(2) + " GB";
|
||||
if (abs >= 1e6) return (v / 1e6).toFixed(2) + " MB";
|
||||
if (abs >= 1e3) return (v / 1e3).toFixed(1) + " KB";
|
||||
return v.toFixed(0) + " B";
|
||||
}
|
||||
|
||||
function fmtHostUptime(sec) {
|
||||
const s = Math.max(0, Number(sec) || 0);
|
||||
const d = Math.floor(s / 86400);
|
||||
const h = Math.floor((s % 86400) / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
if (d > 0) return d + "天" + h + "时";
|
||||
if (h > 0) return h + "时" + m + "分";
|
||||
return m + "分";
|
||||
}
|
||||
|
||||
function hostMetricLevel(percent) {
|
||||
const p = Number(percent);
|
||||
if (!Number.isFinite(p)) return "ok";
|
||||
if (p >= 90) return "bad";
|
||||
if (p >= 75) return "warn";
|
||||
return "ok";
|
||||
}
|
||||
|
||||
function setHostMetricBar(fillEl, percent) {
|
||||
if (!fillEl) return;
|
||||
const p = Math.max(0, Math.min(100, Number(percent) || 0));
|
||||
const level = hostMetricLevel(p);
|
||||
fillEl.style.width = p + "%";
|
||||
fillEl.classList.remove("warn", "bad");
|
||||
if (level === "warn") fillEl.classList.add("warn");
|
||||
if (level === "bad") fillEl.classList.add("bad");
|
||||
}
|
||||
|
||||
function renderHostStatusBar(data) {
|
||||
const bar = document.getElementById("host-status-bar");
|
||||
if (!bar) return;
|
||||
if (!data || !data.ok) {
|
||||
bar.classList.remove("hidden");
|
||||
const dot = document.getElementById("host-status-dot");
|
||||
const name = document.getElementById("host-status-name");
|
||||
const uptime = document.getElementById("host-status-uptime");
|
||||
if (dot) {
|
||||
dot.className = "host-status-dot bad";
|
||||
}
|
||||
if (name) name.textContent = "服务器";
|
||||
if (uptime) uptime.textContent = (data && data.msg) || "状态不可用";
|
||||
const cpuVal = document.getElementById("host-cpu-val");
|
||||
const memVal = document.getElementById("host-mem-val");
|
||||
const diskVal = document.getElementById("host-disk-val");
|
||||
const netVal = document.getElementById("host-net-val");
|
||||
if (cpuVal) cpuVal.textContent = "—";
|
||||
if (memVal) memVal.textContent = "—";
|
||||
if (diskVal) diskVal.textContent = "—";
|
||||
if (netVal) netVal.textContent = "—";
|
||||
return;
|
||||
}
|
||||
bar.classList.remove("hidden");
|
||||
const cpu = data.cpu || {};
|
||||
const mem = data.memory || {};
|
||||
const disk = data.disk || {};
|
||||
const net = data.network || {};
|
||||
const levels = [
|
||||
hostMetricLevel(cpu.percent),
|
||||
hostMetricLevel(mem.percent),
|
||||
hostMetricLevel(disk.percent),
|
||||
];
|
||||
let overall = "ok";
|
||||
if (levels.includes("bad")) overall = "bad";
|
||||
else if (levels.includes("warn")) overall = "warn";
|
||||
const dot = document.getElementById("host-status-dot");
|
||||
const name = document.getElementById("host-status-name");
|
||||
const uptime = document.getElementById("host-status-uptime");
|
||||
if (dot) dot.className = "host-status-dot " + overall;
|
||||
if (name) name.textContent = data.hostname || "服务器";
|
||||
if (uptime) {
|
||||
uptime.textContent =
|
||||
"运行 " +
|
||||
fmtHostUptime(data.uptime_sec) +
|
||||
(data.updated_at ? " · " + data.updated_at : "");
|
||||
}
|
||||
setHostMetricBar(document.getElementById("host-cpu-fill"), cpu.percent);
|
||||
setHostMetricBar(document.getElementById("host-mem-fill"), mem.percent);
|
||||
setHostMetricBar(document.getElementById("host-disk-fill"), disk.percent);
|
||||
const cpuVal = document.getElementById("host-cpu-val");
|
||||
const memVal = document.getElementById("host-mem-val");
|
||||
const diskVal = document.getElementById("host-disk-val");
|
||||
const netVal = document.getElementById("host-net-val");
|
||||
if (cpuVal) {
|
||||
cpuVal.textContent =
|
||||
(cpu.percent != null ? cpu.percent + "%" : "—") +
|
||||
(cpu.count ? " · " + cpu.count + "核" : "");
|
||||
}
|
||||
if (memVal) {
|
||||
memVal.textContent =
|
||||
(mem.percent != null ? mem.percent + "%" : "—") +
|
||||
" · " +
|
||||
fmtHostBytes(mem.used_bytes) +
|
||||
"/" +
|
||||
fmtHostBytes(mem.total_bytes);
|
||||
}
|
||||
if (diskVal) {
|
||||
diskVal.textContent =
|
||||
(disk.percent != null ? disk.percent + "%" : "—") +
|
||||
" · " +
|
||||
fmtHostBytes(disk.used_bytes) +
|
||||
"/" +
|
||||
fmtHostBytes(disk.total_bytes);
|
||||
}
|
||||
if (netVal) {
|
||||
const up = fmtHostBytes(net.sent_rate_bps) + "/s";
|
||||
const down = fmtHostBytes(net.recv_rate_bps) + "/s";
|
||||
netVal.textContent = "↑" + up + " ↓" + down;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHostStatus() {
|
||||
if (currentPage() !== "monitor") return;
|
||||
try {
|
||||
const r = await apiFetch("/api/host/status", { credentials: "same-origin" });
|
||||
const data = await r.json();
|
||||
renderHostStatusBar(data);
|
||||
} catch (err) {
|
||||
renderHostStatusBar({ ok: false, msg: String(err && err.message ? err.message : err) });
|
||||
}
|
||||
}
|
||||
|
||||
function stopHostStatusPoll() {
|
||||
if (hostStatusTimer) {
|
||||
clearInterval(hostStatusTimer);
|
||||
hostStatusTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startHostStatusPoll() {
|
||||
stopHostStatusPoll();
|
||||
void fetchHostStatus();
|
||||
hostStatusTimer = setInterval(fetchHostStatus, HOST_STATUS_POLL_MS);
|
||||
}
|
||||
|
||||
async function apiFetch(url, opts) {
|
||||
const r = await fetch(url, opts);
|
||||
@@ -744,6 +891,7 @@
|
||||
|
||||
function stopMonitorPoll() {
|
||||
closeMonitorBoardStream();
|
||||
stopHostStatusPoll();
|
||||
if (sseReconnectTimer) {
|
||||
clearTimeout(sseReconnectTimer);
|
||||
sseReconnectTimer = null;
|
||||
@@ -886,6 +1034,7 @@
|
||||
const hadCache = restoreMonitorBoardFromCache();
|
||||
void fetchMonitorBoardSnapshot({ showLoading: !hadCache });
|
||||
connectMonitorBoardStream();
|
||||
startHostStatusPoll();
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
|
||||
Reference in New Issue
Block a user