140 lines
5.2 KiB
JavaScript
140 lines
5.2 KiB
JavaScript
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 = '<path d="' + path + '"/>' +
|
||
'<circle cx="' + last[0] + '" cy="' + last[1] + '" r="2"/>';
|
||
}
|
||
|
||
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 = '<tr><td colspan="4" class="muted">нет данных</td></tr>';
|
||
return;
|
||
}
|
||
body.innerHTML = m.processes.map((p) =>
|
||
'<tr>' +
|
||
'<td class="pid">' + p.pid + '</td>' +
|
||
'<td>' + p.name + '</td>' +
|
||
'<td class="num-cell"><span class="cpu-cell"><span class="minibar"><i style="width:' + Math.min(100, p.cpu) + '%"></i></span>' + Number(p.cpu).toFixed(1) + '%</span></td>' +
|
||
'<td class="num-cell">' + Number(p.mem).toFixed(1) + '%</td>' +
|
||
'</tr>'
|
||
).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);
|