"use strict"; // ---------- Мониторинг сервера: ЦП, память, диски (круговые диаграммы, обновление каждые 5 секунд) ---------- const SVGNS = "http://www.w3.org/2000/svg"; function svgEl(tag, attrs) { const e = document.createElementNS(SVGNS, tag); for (const [k, v] of Object.entries(attrs || {})) e.setAttribute(k, v); return e; } const gb = n => n >= 1073741824 ? (n / 1073741824).toFixed(1) + " ГБ" : (n / 1048576).toFixed(0) + " МБ"; const levelColor = p => p >= 90 ? "var(--bad)" : p >= 75 ? "var(--warn)" : "var(--ok)"; // кольцевая диаграмма: доля p (0–100), подпись в центре function donut(p, center) { const r = 52, c = 2 * Math.PI * r, v = Math.max(0, Math.min(100, p ?? 0)); const svg = svgEl("svg", { viewBox: "0 0 140 140", class: "donut", role: "img", "aria-label": center }); svg.append(svgEl("circle", { cx: 70, cy: 70, r, fill: "none", stroke: "var(--line)", "stroke-width": 16 }), svgEl("circle", { cx: 70, cy: 70, r, fill: "none", stroke: levelColor(v), "stroke-width": 16, "stroke-linecap": "round", "stroke-dasharray": `${c * v / 100} ${c}`, transform: "rotate(-90 70 70)" })); const t = svgEl("text", { x: 70, y: 77, "text-anchor": "middle", class: "donut-v" }); t.textContent = p == null ? "—" : Math.round(v) + "%"; svg.append(t); return svg; } const gauge = (title, p, lines) => h("div", { class: "card gauge" }, h("h2", {}, title), donut(p, title), ...lines.filter(Boolean).map(l => h("div", { class: "small muted" }, l))); async function monitorPage() { if (!me.portal_admin) throw new Error("Мониторинг доступен администраторам портала"); const box = h("div", { class: "gauges" }), stamp = h("div", { class: "small muted", style: "margin-bottom:8px" }); fill($app, h("a", { href: "#/admin" }, "← Администрирование"), h("h1", {}, "Мониторинг сервера"), h("p", { class: "small muted" }, "Загрузка процессора, память и место на дисках сервера портала. Обновляется каждые 5 секунд, пока страница открыта."), stamp, box); const load = async () => { const m = await api("GET", "/server/monitor"); const mem = m.memory, memP = mem ? 100 * mem.used / mem.total : null; box.replaceChildren( gauge("Процессор", m.cpu, [`ядер: ${m.cpu_count || "—"}`, m.load ? `средняя нагрузка: ${m.load.map(x => x.toFixed(2)).join(" / ")}` : ""]), gauge("Память (ОЗУ)", memP, mem ? [`занято ${gb(mem.used)} из ${gb(mem.total)}`, `свободно ${gb(mem.total - mem.used)}`, mem.scope === "container" ? "лимит контейнера Docker" : ""] : ["нет данных"]), ...m.disks.map(d => gauge("Диск: " + d.labels.join(", "), 100 * d.used / d.total, [`свободно ${gb(d.free)} из ${gb(d.total)}`, h("span", { title: d.path }, d.path.length > 40 ? "…" + d.path.slice(-40) : d.path)]))); stamp.textContent = "Обновлено " + new Date().toLocaleTimeString("ru-RU"); }; await load(); timer = setInterval(guard(load), 5000); }