(() => {
const root = document.querySelector("[data-dashboard-metrics]");
if (!root) return;
const memoryChart = root.querySelector("[data-memory-chart]");
const logChart = root.querySelector("[data-log-chart]");
const logSummary = root.querySelector("[data-log-summary]");
const status = root.querySelector("[data-metrics-status]");
const memoryScopeLabel = root.querySelector("[data-memory-scope-label]");
const logScopeLabel = root.querySelector("[data-log-scope-label]");
const scopeButtons = Array.from(root.querySelectorAll("[data-dashboard-scope]"));
let activeScope = window.localStorage.getItem("lumi-dashboard-scope") || "5m";
const scopeLabels = { "5m": "5 minutes", "24h": "24 hours", "7d": "7 days" };
const levels = [
["error", "Errors", "danger"],
["warn", "Warnings", "warning"],
["info", "Info", "info"],
["debug", "Debug", "text-muted"]
];
const bytes = (value) => {
const mb = Number(value || 0) / 1048576;
return mb >= 1024 ? `${(mb / 1024).toFixed(1)} GB` : `${mb.toFixed(0)} MB`;
};
const duration = (seconds) => {
const total = Number(seconds || 0);
const hours = Math.floor(total / 3600);
const minutes = Math.floor((total % 3600) / 60);
return hours ? `${hours}h ${minutes}m` : `${minutes}m`;
};
const setMetric = (name, value) => {
const target = root.querySelector(`[data-metric="${name}"]`);
if (target) target.textContent = value;
};
const escapeAttr = (value) => String(value ?? "").replace(/[&"<]/g, (char) => ({
"&": "&",
"\"": """,
"<": "<"
})[char]);
const scaleX = (sample, samples, scopeMs, now) => {
if (samples.length <= 1) return 290;
const min = now - scopeMs;
return 10 + Math.max(0, Math.min(1, (sample.sampled_at - min) / scopeMs)) * 250;
};
const scaleY = (value, max) => 104 - (Number(value || 0) / Math.max(max, 1)) * 82;
const drawMemory = (data) => {
if (!memoryChart) return;
const samples = data.memory_scopes?.[activeScope] || [];
const scopeMs = data.scopes?.[activeScope]?.duration_ms || 5 * 60 * 1000;
const now = Number(data.sampled_at || Date.now());
const max = Math.max(...samples.map((sample) => sample.rss), data.memory?.rss || 0, 1);
const gridValues = [0, 0.25, 0.5, 0.75, 1].map((ratio) => Math.round(max * ratio));
const points = samples.map((sample) => `${scaleX(sample, samples, scopeMs, now).toFixed(1)},${scaleY(sample.rss, max).toFixed(1)}`).join(" ");
const labelSamples = samples.length <= 3
? samples
: [samples[0], samples[Math.floor(samples.length / 2)], samples[samples.length - 1]];
const grid = gridValues.map((value) => {
const y = scaleY(value, max);
return `${bytes(value)}`;
}).join("");
const vertical = [0, 0.5, 1].map((ratio) => {
const x = 10 + ratio * 250;
const label = ratio === 0 ? `-${scopeLabels[activeScope] || activeScope}` : ratio === 1 ? "now" : "mid";
return `${label}`;
}).join("");
const labels = labelSamples.map((sample) => {
const x = scaleX(sample, samples, scopeMs, now);
const y = scaleY(sample.rss, max);
return `${bytes(sample.rss)}`;
}).join("");
memoryChart.innerHTML = `
${grid}
${vertical}
${points ? `` : ""}
${labels}
`;
};
const drawLogs = (logs) => {
if (!logChart) return;
const counts = logs.levels || logs;
const entries = levels.map(([level, label, token]) => [level, label, token, Number(counts[level] || 0)]);
const max = Math.max(...entries.map(([, , , value]) => value), 1);
const range = encodeURIComponent(String(logs.scope_ms || 5 * 60 * 1000));
logChart.innerHTML = entries.map(([level, label, token, value], index) => {
const height = Math.max(4, (value / max) * 76);
const x = 22 + index * 68;
const y = 92 - height;
const href = `/admin/logs?range=${range}&level=${encodeURIComponent(level)}&limit=500`;
return `${value}${level}`;
}).join("");
if (logSummary) {
logSummary.innerHTML = entries.map(([level, label, , value]) => {
const href = `/admin/logs?range=${range}&level=${encodeURIComponent(level)}&limit=500`;
const badge = level === "error" ? "danger" : level === "warn" ? "warning" : level === "info" ? "info" : "muted";
return `${label}: ${value}`;
}).join("");
}
};
const syncScopeButtons = () => {
scopeButtons.forEach((button) => {
const active = button.dataset.dashboardScope === activeScope;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", active ? "true" : "false");
});
if (memoryScopeLabel) memoryScopeLabel.textContent = scopeLabels[activeScope] || activeScope;
if (logScopeLabel) logScopeLabel.textContent = scopeLabels[activeScope] || activeScope;
};
const refresh = async () => {
try {
syncScopeButtons();
const response = await fetch(`/api/admin/dashboard-metrics?scope=${encodeURIComponent(activeScope)}`, { cache: "no-store" });
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Metrics unavailable.");
setMetric("uptime", duration(data.uptime_seconds));
setMetric("rss", bytes(data.memory.rss));
setMetric("heap", `${bytes(data.memory.heap_used)} / ${bytes(data.memory.heap_total)}`);
setMetric("plugins", `${data.plugins.enabled} / ${data.plugins.total}`);
setMetric("users", data.counts.users);
setMetric("commands", data.counts.commands);
if (status) {
status.textContent = "Live";
status.className = "status-indicator status-success";
}
drawMemory(data);
drawLogs(data.logs);
} catch (error) {
if (status) {
status.textContent = error.message;
status.className = "status-indicator status-danger";
}
}
};
scopeButtons.forEach((button) => {
button.addEventListener("click", () => {
activeScope = button.dataset.dashboardScope || "5m";
window.localStorage.setItem("lumi-dashboard-scope", activeScope);
refresh();
});
});
refresh();
window.setInterval(refresh, 10000);
})();