const $ = (id) => document.getElementById(id);
const HISTORY = { cpu: [], mem: [] };
const MAX_HISTORY = 60;
const DEMO = () => {
const rnd = (a, b) => Math.round(a + Math.random() * (b - a));
const memPct = rnd(35, 55);
return {
hostname: "demo-server", os: "linux", release: "6.x", arch: "x64",
uptime: 3 * 86400 + 7 * 3600 + 42 * 60,
loadavg: [0.4, 0.3, 0.2],
cpu: { usage: rnd(5, 25), model: "Пример данных (Предпросмотр)", cores: 8, speed: 2.4 },
mem: { total: 16 * 1024 ** 3, used: Math.round(memPct / 100 * 16 * 1024 ** 3), free: Math.round((100 - memPct) / 100 * 16 * 1024 ** 3), percent: memPct },
disk: { total: 100 * 1024 ** 3, used: 64 * 1024 ** 3, free: 36 * 1024 ** 3, percent: 64 },
network: "1.2 Гбит/с",
processes: [
{ pid: 1234, name: "node", cpu: 12.3, mem: 4.2 },
{ pid: 5678, name: "nginx", cpu: 3.1, mem: 1.0 },
{ pid: 9012, name: "python3", cpu: 2.4, mem: 5.7 },
{ pid: 3456, name: "postgres", cpu: 1.8, mem: 12.5 },
{ pid: 7890, name: "redis", cpu: 0.9, mem: 0.8 },
{ pid: 1112, name: "docker", cpu: 0.6, mem: 1.4 },
],
};
};
function fmtBytes(n) {
if (n == null || isNaN(n)) return "—";
const u = ["Б", "КБ", "МБ", "ГБ", "ТБ"];
let i = 0;
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
return n.toFixed(i >= 2 ? 1 : 0) + " " + u[i];
}
function fmtUptime(sec) {
if (sec == null) return "—";
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
let s = m + " мин";
if (h > 0) s = h + " ч " + s;
if (d > 0) s = d + " дн " + s;
return s;
}
function level(p) {
if (p >= 90) return { c: "bad", w: "bad" };
if (p >= 70) return { c: "warn", w: "warn" };
return { c: "ok", w: "" };
}
function spark(id, arr) {
const el = $(id);
if (!arr.length) return;
const w = 100, h = 30, pad = 2;
const min = Math.min(...arr), max = Math.max(...arr);
const rng = max - min || 1;
const pts = arr.map((v, i) => {
const x = pad + (i / (arr.length - 1)) * (w - pad * 2);
const y = h - pad - ((v - min) / rng) * (h - pad * 2);
return x + "," + y;
});
const path = "M" + pts.join(" L");
const last = pts[pts.length - 1].split(",");
el.innerHTML = '' +
'';
}
function pushHistory(m) {
HISTORY.cpu.push(m.cpu.usage);
HISTORY.mem.push(m.mem.percent);
if (HISTORY.cpu.length > MAX_HISTORY) { HISTORY.cpu.shift(); HISTORY.mem.shift(); }
}
function render(m) {
const status = m.status || "ok";
const dot = $("statusDot");
dot.className = "dot " + status;
$("hostname").textContent = m.hostname || "—";
$("osinfo").textContent = (m.os || "") + " " + (m.release || "") + " · " + (m.arch || "");
$("lastUpd").textContent = "последнее обновление: " + new Date().toLocaleTimeString("ru-RU");
$("cpuPct").textContent = Math.round(m.cpu.usage);
$("cpuBar").style.width = Math.min(100, m.cpu.usage) + "%";
$("cpuBar").className = level(m.cpu.usage).w;
$("cpuSub").textContent = m.cpu.cores + " ядер" + (m.cpu.speed > 0 ? " · " + m.cpu.speed + " ГГц" : "");
$("cpuModel").textContent = m.cpu.model || "—";
$("cpuCard").classList.toggle("dim", false);
$("cpuSpark").style.display = "block";
spark("cpuSpark", HISTORY.cpu);
$("memPct").textContent = Math.round(m.mem.percent);
$("memBar").style.width = Math.min(100, m.mem.percent) + "%";
$("memBar").className = level(m.mem.percent).w;
$("memSub").textContent = "использовано " + fmtBytes(m.mem.used) + " из " + fmtBytes(m.mem.total);
spark("memSpark", HISTORY.mem);
$("diskPct").textContent = Math.round(m.disk.percent);
$("diskBar").style.width = Math.min(100, m.disk.percent) + "%";
$("diskBar").className = level(m.disk.percent).w;
$("diskSub").textContent = "свободно " + fmtBytes(m.disk.free) + " из " + fmtBytes(m.disk.total);
$("uptime").textContent = fmtUptime(m.uptime);
$("loadavg").textContent = (m.loadavg || []).map((x) => x.toFixed ? x.toFixed(2) : x).join(" / ");
$("network").textContent = m.network || "—";
$("cores").textContent = (m.cpu.cores || "—") + " · " + (m.arch || "—");
const body = $("procBody");
if (!m.processes || !m.processes.length) {
body.innerHTML = '
| нет данных |
';
return;
}
body.innerHTML = m.processes.map((p) =>
'' +
'| ' + p.pid + ' | ' +
'' + p.name + ' | ' +
'' + Number(p.cpu).toFixed(1) + '% | ' +
'' + Number(p.mem).toFixed(1) + '% | ' +
'
'
).join("");
}
async function load() {
let m;
try {
const r = await fetch("/api/metrics", { cache: "no-store" });
if (!r.ok) throw new Error("http " + r.status);
m = await r.json();
} catch (e) {
m = DEMO();
$("cpuModel").textContent = "Сервер не запущен — показаны демо-данные";
$("statusDot").className = "dot warn";
}
pushHistory(m);
render(m);
}
load();
setInterval(load, 3000);