feat: документация из Nextcloud, график у сотрудника, автоочистка фото, мониторинг, итог мастера, фильтр по местам; доп. сервисы в docker-compose
- docker-compose.yml: профили docs (Nextcloud), office (ONLYOFFICE Document Server), ai (Ollama + скачивание модели) - автонастройка: адреса Nextcloud, ONLYOFFICE, портала и модели подставляются из .env или по именам сервисов во внутренней сети Docker; значения из интерфейса важнее - локальная модель в своей сети (имя сервиса, частный IP) подключается без ключа API - .env.example, docs/DEPLOY.md (раздел 7а), README
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
"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);
|
||||
}
|
||||
Reference in New Issue
Block a user