(() => { 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); })();