232 lines
8 KiB
JavaScript
232 lines
8 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
// ---------- tabs ----------
|
|
const tabBtns = document.querySelectorAll(".tab-btn");
|
|
const sections = document.querySelectorAll(".tab");
|
|
|
|
function showTab(name) {
|
|
tabBtns.forEach((b) => b.classList.toggle("active", b.dataset.tab === name));
|
|
sections.forEach((s) => s.classList.toggle("hidden", s.id !== "tab-" + name));
|
|
try {
|
|
localStorage.setItem("dash.tab", name);
|
|
} catch (e) {}
|
|
}
|
|
tabBtns.forEach((b) => b.addEventListener("click", () => showTab(b.dataset.tab)));
|
|
try {
|
|
const saved = localStorage.getItem("dash.tab");
|
|
if (saved && document.getElementById("tab-" + saved)) showTab(saved);
|
|
} catch (e) {}
|
|
|
|
// ---------- helpers ----------
|
|
function fmtBytes(n, digits) {
|
|
if (n == null || isNaN(n)) return "—";
|
|
if (digits == null) digits = 1;
|
|
const u = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
let i = 0;
|
|
while (Math.abs(n) >= 1024 && i < u.length - 1) {
|
|
n /= 1024;
|
|
i++;
|
|
}
|
|
return n.toFixed(digits) + " " + u[i];
|
|
}
|
|
|
|
function fmtTime(ts) {
|
|
const d = new Date(ts * 1000);
|
|
return d.toLocaleTimeString([], { hour12: false });
|
|
}
|
|
|
|
// ---------- charts ----------
|
|
const charts = {};
|
|
|
|
function baseOpts(extra) {
|
|
const o = {
|
|
animation: false,
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: { mode: "index", intersect: false },
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
x: { ticks: { maxTicksLimit: 7, maxRotation: 0, color: "#7d8a9c" }, grid: { display: false } },
|
|
y: { beginAtZero: true, ticks: { color: "#7d8a9c" }, grid: { color: "rgba(42,51,66,.5)" } },
|
|
},
|
|
};
|
|
if (extra) Object.assign(o.scales.y, extra);
|
|
return o;
|
|
}
|
|
|
|
function envelopeDs(name, color, s) {
|
|
s = s || {};
|
|
return [
|
|
{ label: name, data: s.max || [], borderColor: color, backgroundColor: color + "2e", borderWidth: 1, pointRadius: 0, tension: 0, fill: false, order: 0 },
|
|
{ label: name + " min", band: true, data: s.min || [], borderColor: color, backgroundColor: color, borderWidth: 1, pointRadius: 0, tension: 0, fill: false, order: 1 },
|
|
];
|
|
}
|
|
|
|
const LEGEND = { display: true, labels: { boxWidth: 10, color: "#7d8a9c", filter: (item, data) => !data.datasets[item.datasetIndex]?.band } };
|
|
|
|
function initCharts() {
|
|
if (typeof Chart === "undefined") return;
|
|
charts.cpu = new Chart(document.getElementById("chart-cpu"), {
|
|
type: "line",
|
|
data: {
|
|
labels: [],
|
|
datasets: [].concat(envelopeDs("CPU", "#4fc3f7"), envelopeDs("GPU", "#ba68c8")),
|
|
},
|
|
options: baseOpts({ max: 100 }),
|
|
});
|
|
charts.cpu.options.plugins.legend = LEGEND;
|
|
charts.mem = new Chart(document.getElementById("chart-mem"), {
|
|
type: "line",
|
|
data: {
|
|
labels: [],
|
|
datasets: [].concat(envelopeDs("Memory", "#81c784"), envelopeDs("VRAM", "#ffb74d")),
|
|
},
|
|
options: baseOpts({ max: 100 }),
|
|
});
|
|
charts.mem.options.plugins.legend = LEGEND;
|
|
charts.io = new Chart(document.getElementById("chart-io"), {
|
|
type: "line",
|
|
data: {
|
|
labels: [],
|
|
datasets: [].concat(envelopeDs("read", "#e57373"), envelopeDs("write", "#fff176")),
|
|
},
|
|
options: baseOpts({
|
|
ticks: { color: "#7d8a9c", callback: (v) => fmtBytes(v, 0) },
|
|
beginAtZero: true,
|
|
}),
|
|
});
|
|
charts.io.options.plugins.legend = LEGEND;
|
|
}
|
|
|
|
function updateCharts(hist) {
|
|
if (!hist || !hist.ts || !hist.ts.length) return;
|
|
const labels = hist.ts.map(fmtTime);
|
|
const series = hist.series || {};
|
|
const build = (chart, pairs) => {
|
|
chart.data.labels = labels;
|
|
chart.data.datasets = [];
|
|
pairs.forEach(([name, color, key], i) => {
|
|
const ds = envelopeDs(name, color, series[key]);
|
|
ds[0].fill = i * 2 + 1;
|
|
chart.data.datasets.push(...ds);
|
|
});
|
|
chart.update("none");
|
|
};
|
|
build(charts.cpu, [["CPU", "#4fc3f7", "cpu"], ["GPU", "#ba68c8", "gpu"]]);
|
|
build(charts.mem, [["Memory", "#81c784", "mem_pct"], ["VRAM", "#ffb74d", "vram_pct"]]);
|
|
build(charts.io, [["read", "#e57373", "io_read"], ["write", "#fff176", "io_write"]]);
|
|
}
|
|
|
|
function pollHistory() {
|
|
fetch("/api/history")
|
|
.then((r) => r.json())
|
|
.then(updateCharts)
|
|
.catch(() => {});
|
|
}
|
|
|
|
initCharts();
|
|
pollHistory();
|
|
setInterval(pollHistory, 2000);
|
|
|
|
// ---------- journal ----------
|
|
const journalLog = document.getElementById("journal-log");
|
|
const journalCursor = document.getElementById("journal-cursor");
|
|
const journalStatus = document.getElementById("journal-status");
|
|
const MAX_LINES = 500;
|
|
|
|
document.body.addEventListener("htmx:afterSwap", (e) => {
|
|
if (e.target.id !== "journal-log") return;
|
|
// surface one-shot errors in the status bar instead of the log
|
|
document.querySelectorAll("#journal-log .j-error-once").forEach((el) => {
|
|
journalStatus.textContent = el.textContent.trim();
|
|
el.remove();
|
|
});
|
|
// track cursor from the most recent line
|
|
const lines = journalLog.querySelectorAll(".jline");
|
|
const last = lines[lines.length - 1];
|
|
if (last && last.dataset.cursor) journalCursor.value = last.dataset.cursor;
|
|
// trim
|
|
const extra = lines.length - MAX_LINES;
|
|
if (extra > 0) {
|
|
for (let i = 0; i < extra; i++) lines[i].remove();
|
|
}
|
|
// auto-scroll if user is at the bottom (within 40px)
|
|
const wrap = document.getElementById("journal-logwrap");
|
|
const atBottom = wrap.scrollHeight - wrap.scrollTop - wrap.clientHeight < 40;
|
|
if (atBottom) wrap.scrollTop = wrap.scrollHeight;
|
|
});
|
|
|
|
// reset cursor when the filter form is submitted
|
|
const journalFilters = document.getElementById("journal-filters");
|
|
journalFilters.addEventListener("submit", () => {
|
|
journalCursor.value = "";
|
|
journalStatus.textContent = "";
|
|
journalLog.innerHTML = "";
|
|
});
|
|
|
|
// ---------- table sorting (processes, services) ----------
|
|
document.body.addEventListener("click", (e) => {
|
|
const link = e.target.closest(".sortlink");
|
|
if (!link) return;
|
|
e.preventDefault();
|
|
const formId = link.dataset.form || "proc-controls";
|
|
const form = document.getElementById(formId);
|
|
if (!form) return;
|
|
const sortSel = form.querySelector("select[name=sort]");
|
|
const orderSel = form.querySelector("select[name=order]");
|
|
if (sortSel) sortSel.value = link.dataset.sort;
|
|
if (orderSel) orderSel.value = link.dataset.order;
|
|
htmx.trigger(form, "submit");
|
|
});
|
|
|
|
// ---------- services accordion ----------
|
|
const openSvc = new Set();
|
|
|
|
function loadSvcDetail(unit, target) {
|
|
htmx.ajax("GET", "/api/services/" + encodeURIComponent(unit) + "/detail", { target });
|
|
}
|
|
|
|
function toggleSvcRow(row) {
|
|
const detailRow = row.nextElementSibling;
|
|
if (!detailRow || !detailRow.classList.contains("svc-detail-row")) return;
|
|
const unit = row.dataset.unit;
|
|
if (openSvc.has(unit)) {
|
|
openSvc.delete(unit);
|
|
row.classList.remove("open");
|
|
detailRow.hidden = true;
|
|
} else {
|
|
openSvc.add(unit);
|
|
row.classList.add("open");
|
|
detailRow.hidden = false;
|
|
loadSvcDetail(unit, detailRow.querySelector(".svc-detail"));
|
|
}
|
|
}
|
|
|
|
document.body.addEventListener("click", (e) => {
|
|
const row = e.target.closest("#services tr.svc-row");
|
|
if (!row || e.target.closest("button")) return;
|
|
toggleSvcRow(row);
|
|
});
|
|
|
|
document.body.addEventListener("keydown", (e) => {
|
|
const el = e.target.closest(".svc-name");
|
|
if (el && (e.key === "Enter" || e.key === " ")) {
|
|
e.preventDefault();
|
|
toggleSvcRow(el.closest("tr.svc-row"));
|
|
}
|
|
});
|
|
|
|
document.body.addEventListener("htmx:afterSwap", (e) => {
|
|
if (e.target.id !== "services" || !openSvc.size) return;
|
|
for (const row of e.target.querySelectorAll("tr.svc-row")) {
|
|
const unit = row.dataset.unit;
|
|
if (!openSvc.has(unit)) continue;
|
|
const detailRow = row.nextElementSibling;
|
|
if (!detailRow || !detailRow.classList.contains("svc-detail-row")) continue;
|
|
row.classList.add("open");
|
|
detailRow.hidden = false;
|
|
loadSvcDetail(unit, detailRow.querySelector(".svc-detail"));
|
|
}
|
|
});
|
|
})();
|