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);