let initialized = false; let metricsTimer = null; let onlineChart = null; let osChart = null; const STATS_COLLAPSED_KEY = "overlord_dashboard_stats_collapsed"; const STATS_HEIGHT_KEY = "overlord_dashboard_stats_height"; const STATS_ORDER_KEY = "overlord_dashboard_stats_order"; const STATS_LAYOUT_KEY = "overlord_dashboard_stats_gridstack_layout"; const STATS_HIDDEN_KEY = "overlord_dashboard_stats_hidden"; const PULSE_ORDER_KEY = "overlord_dashboard_pulse_order"; const DEFAULT_STATS_HEIGHT = 142; const MIN_STATS_HEIGHT = 112; const MAX_STATS_HEIGHT = 280; const DEFAULT_CARD_ORDER = ["online", "sessions", "trend", "fleet", "pulse"]; const DEFAULT_PULSE_ORDER = ["api", "memory", "ping"]; const DEFAULT_GRID_LAYOUT = { online: { x: 0, y: 0, w: 2, h: 2 }, sessions: { x: 2, y: 0, w: 2, h: 2 }, trend: { x: 4, y: 0, w: 3, h: 2 }, fleet: { x: 7, y: 0, w: 3, h: 2 }, pulse: { x: 10, y: 0, w: 2, h: 2 }, }; let statsGrid = null; function teardownDashboardStats() { if (metricsTimer) clearInterval(metricsTimer); metricsTimer = null; onlineChart?.destroy?.(); osChart?.destroy?.(); onlineChart = null; osChart = null; statsGrid?.destroy?.(false); statsGrid = null; initialized = false; } const palette = { text: "#cbd5e1", muted: "#94a3b8", border: "rgba(100, 116, 139, 0.18)", panel: "rgba(15, 23, 42, 0.96)", cyan: "#22d3ee", emerald: "#22c55e", sky: "#38bdf8", indigo: "#818cf8", amber: "#fbbf24", rose: "#fb7185", violet: "#a78bfa", slate: "#64748b", }; const osColors = [ palette.sky, palette.emerald, palette.violet, palette.amber, palette.rose, palette.cyan, palette.indigo, palette.slate, ]; function $(id) { return document.getElementById(id); } function setText(id, value) { const el = $(id); if (el) el.textContent = value; } function formatNumber(value) { const n = Number(value) || 0; return n.toLocaleString(); } function formatBytes(bytes) { const n = Number(bytes) || 0; if (n <= 0) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; const idx = Math.min(units.length - 1, Math.floor(Math.log(n) / Math.log(1024))); return `${(n / Math.pow(1024, idx)).toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`; } function formatTime(timestamp) { return new Date(timestamp || Date.now()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); } function formatWeekLabel(timestamp) { const date = new Date(timestamp || Date.now()); return date.toLocaleDateString([], { weekday: "short", hour: "2-digit", }); } function compactRepeatedTickLabel(value, index, ticks) { const labelFor = (tickValue) => { const labels = this?.chart?.data?.labels || []; return labels[Number(tickValue)] ?? this?.getLabelForValue?.(tickValue) ?? ""; }; const labels = (ticks || []).map((tick) => labelFor(tick.value)); const current = labels[index] || labelFor(value); if (!current) return ""; const isRunStart = index === 0 || labels[index - 1] !== current; const isRunEnd = index === labels.length - 1 || labels[index + 1] !== current; return isRunStart || isRunEnd ? current : ""; } function aggregateOnlineHistory(history, snapshot) { const now = Date.now(); const weekAgo = now - 7 * 24 * 60 * 60 * 1000; const source = Array.isArray(history) ? history.filter((row) => Number(row?.timestamp) >= weekAgo) : []; const rows = source.length ? source : [{ timestamp: now, clientsOnline: snapshot?.clients?.online || 0 }]; const maxPoints = 84; if (rows.length <= maxPoints) return rows; const bucketMs = Math.max( 60 * 60 * 1000, Math.ceil((now - weekAgo) / maxPoints), ); const buckets = new Map(); for (const row of rows) { const ts = Number(row.timestamp) || now; const key = Math.floor(ts / bucketMs) * bucketMs; const bucket = buckets.get(key) || { timestamp: key, total: 0, count: 0, peak: 0 }; const online = Number(row.clientsOnline) || 0; bucket.total += online; bucket.count += 1; bucket.peak = Math.max(bucket.peak, online); buckets.set(key, bucket); } return Array.from(buckets.values()) .sort((a, b) => a.timestamp - b.timestamp) .map((bucket) => ({ timestamp: bucket.timestamp, clientsOnline: Math.round(bucket.total / Math.max(1, bucket.count)), clientsPeak: bucket.peak, })); } function setDotTone(id, tone) { const el = $(id); if (!el) return; el.classList.toggle("is-live", tone === "live"); el.classList.toggle("is-warn", tone === "warn"); el.classList.toggle("is-bad", tone === "bad"); el.classList.toggle("is-muted", tone === "muted"); } function topEntries(record, limit = 6) { return Object.entries(record || {}) .map(([key, value]) => [key || "Unknown", Number(value) || 0]) .filter(([, value]) => value > 0) .sort((a, b) => b[1] - a[1]) .slice(0, limit); } function compactOsLabel(os) { const text = String(os || "Unknown"); const lower = text.toLowerCase(); if (lower.includes("windows 11")) return "Win 11"; if (lower.includes("windows 10")) return "Win 10"; if (lower.includes("windows")) return "Windows"; if (lower.includes("darwin") || lower.includes("mac")) return "macOS"; if (lower.includes("ubuntu")) return "Ubuntu"; if (lower.includes("debian")) return "Debian"; if (lower.includes("kali")) return "Kali"; if (lower.includes("fedora")) return "Fedora"; if (lower.includes("linux")) return "Linux"; return text.length > 16 ? `${text.slice(0, 15)}...` : text; } function configureChartDefaults() { if (typeof Chart === "undefined") return false; Chart.defaults.color = palette.text; Chart.defaults.borderColor = palette.border; Chart.defaults.font.family = "Inter, Segoe UI, system-ui, sans-serif"; return true; } function createGradient(ctx, color) { const gradient = ctx.createLinearGradient(0, 0, 0, 150); gradient.addColorStop(0, `${color}55`); gradient.addColorStop(0.72, `${color}12`); gradient.addColorStop(1, `${color}00`); return gradient; } function makeCharts() { if (!configureChartDefaults()) return false; const onlineCanvas = $("dash-online-chart"); const osCanvas = $("dash-os-chart"); if (!onlineCanvas || !osCanvas) return false; const onlineCtx = onlineCanvas.getContext("2d"); onlineChart = new Chart(onlineCtx, { type: "line", data: { labels: [], datasets: [{ label: "Online", data: [], borderColor: palette.emerald, backgroundColor: createGradient(onlineCtx, palette.emerald), borderWidth: 2, fill: true, tension: 0.38, pointRadius: 0, pointHoverRadius: 4, }], }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 260 }, interaction: { intersect: false, mode: "index" }, plugins: { legend: { display: false }, tooltip: { backgroundColor: palette.panel, borderColor: "rgba(34, 197, 94, 0.34)", borderWidth: 1, titleColor: "#e2e8f0", bodyColor: palette.text, displayColors: false, callbacks: { label: (ctx) => `Avg online: ${ctx.parsed.y}`, }, }, }, scales: { x: { grid: { display: false }, ticks: { color: palette.muted, maxRotation: 0, maxTicksLimit: 5, callback: compactRepeatedTickLabel, }, }, y: { beginAtZero: true, grid: { color: "rgba(100, 116, 139, 0.1)" }, ticks: { color: palette.muted, precision: 0, maxTicksLimit: 4 }, }, }, }, }); osChart = new Chart(osCanvas, { type: "doughnut", data: { labels: [], datasets: [{ data: [], backgroundColor: osColors, borderWidth: 0, hoverBorderWidth: 0, hoverOffset: 5, }], }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 260 }, cutout: "66%", layout: { padding: { top: 2, right: 4, bottom: 10, left: 4 }, }, plugins: { legend: { position: "right", labels: { boxWidth: 9, boxHeight: 9, color: palette.text, padding: 10, font: { size: 11 }, }, }, tooltip: { backgroundColor: palette.panel, borderColor: "rgba(129, 140, 248, 0.32)", borderWidth: 1, titleColor: "#e2e8f0", bodyColor: palette.text, }, }, }, }); return true; } function getSavedHeight() { return normalizeHeight(localStorage.getItem(STATS_HEIGHT_KEY) || DEFAULT_STATS_HEIGHT); } function normalizeHeight(value) { const n = Math.round(Number(value)); if (!Number.isFinite(n)) return DEFAULT_STATS_HEIGHT; return Math.min(MAX_STATS_HEIGHT, Math.max(MIN_STATS_HEIGHT, n)); } function applyHeightMode(height) { const shell = $("dashboard-stats"); if (!shell) return; shell.classList.toggle("is-compact", height < 142); shell.classList.toggle("is-tiny", height < 122); const healthRows = Array.from(shell.querySelectorAll(".dashboard-health-row")); const visibleCount = height < 122 ? 1 : height < 146 ? 2 : healthRows.length; healthRows.forEach((row, index) => { row.classList.toggle("is-hidden", index >= visibleCount); }); if (osChart) { const nextDisplay = height >= 172; if (osChart.options.plugins.legend.display !== nextDisplay) { osChart.options.plugins.legend.display = nextDisplay; osChart.update("none"); } } } function getStatsCellHeight() { return Math.max(48, Math.round(getSavedHeight() / 2)); } function resizeCharts() { requestAnimationFrame(() => { onlineChart?.resize(); osChart?.resize(); }); } function syncStatsWidgetHeights() { if (!statsGrid?.engine?.nodes) return; const cellHeight = Number(statsGrid.opts?.cellHeight) || getStatsCellHeight(); for (const node of statsGrid.engine.nodes) { const content = node.el?.querySelector?.(".grid-stack-item-content"); if (!content) continue; const estimated = Math.max(64, (Number(node.h) || 2) * cellHeight - 12); content.style.setProperty("--dashboard-stats-height", `${estimated}px`); } } function readSavedGridLayout() { try { const parsed = JSON.parse(localStorage.getItem(STATS_LAYOUT_KEY) || "[]"); if (!Array.isArray(parsed)) return []; return parsed .filter((item) => DEFAULT_CARD_ORDER.includes(item?.id)) .map((item) => ({ id: item.id, x: Math.max(0, Number(item.x) || 0), y: Math.max(0, Number(item.y) || 0), w: Math.max(1, Number(item.w) || DEFAULT_GRID_LAYOUT[item.id]?.w || 2), h: Math.max(1, Number(item.h) || DEFAULT_GRID_LAYOUT[item.id]?.h || 2), })); } catch { return []; } } function readHiddenStatsCards() { try { const parsed = JSON.parse(localStorage.getItem(STATS_HIDDEN_KEY) || "[]"); if (!Array.isArray(parsed)) return []; return parsed.filter((id) => DEFAULT_CARD_ORDER.includes(id)); } catch { return []; } } function saveHiddenStatsCards(hidden) { const cleaned = Array.from(hidden).filter((id) => DEFAULT_CARD_ORDER.includes(id)); localStorage.setItem(STATS_HIDDEN_KEY, JSON.stringify(cleaned)); } function isStatsGridManaged(el) { return !!statsGrid?.engine?.nodes?.some((node) => node.el === el); } function saveStatsGridLayout() { if (!statsGrid?.engine?.nodes) return; const layoutMap = new Map( DEFAULT_CARD_ORDER.map((id) => [id, { id, ...DEFAULT_GRID_LAYOUT[id] }]), ); for (const item of readSavedGridLayout()) { layoutMap.set(item.id, item); } for (const node of statsGrid.engine.nodes) { const id = node.el?.dataset?.dashboardCard || node.id; if (!DEFAULT_CARD_ORDER.includes(id)) continue; layoutMap.set(id, { id, x: node.x, y: node.y, w: node.w, h: node.h }); } const layout = DEFAULT_CARD_ORDER.map((id) => layoutMap.get(id)); localStorage.setItem(STATS_LAYOUT_KEY, JSON.stringify(layout)); } function applyGridLayout(layout) { if (!statsGrid) return; statsGrid.batchUpdate(); for (const item of layout) { const el = document.querySelector(`[data-dashboard-card="${item.id}"]`); if (el) statsGrid.update(el, { x: item.x, y: item.y, w: item.w, h: item.h }); } statsGrid.batchUpdate(false); syncStatsWidgetHeights(); resizeCharts(); } function setStatsCardHidden(id, hidden, persist = true) { if (!DEFAULT_CARD_ORDER.includes(id)) return; const el = document.querySelector(`[data-dashboard-card="${id}"]`); if (!el) return; if (hidden) { if (statsGrid && isStatsGridManaged(el)) statsGrid.removeWidget(el, false); el.classList.add("dashboard-card-hidden"); } else { el.classList.remove("dashboard-card-hidden"); if (statsGrid && !isStatsGridManaged(el)) { statsGrid.makeWidget(el); const layout = readSavedGridLayout().find((item) => item.id === id); statsGrid.update(el, { ...(layout || DEFAULT_GRID_LAYOUT[id]) }); } } if (persist) { const nextHidden = new Set(readHiddenStatsCards()); if (hidden) nextHidden.add(id); else nextHidden.delete(id); saveHiddenStatsCards(nextHidden); saveStatsGridLayout(); } syncStatsWidgetHeights(); resizeCharts(); } function applyHiddenStatsCards() { const hidden = new Set(readHiddenStatsCards()); for (const id of DEFAULT_CARD_ORDER) { setStatsCardHidden(id, hidden.has(id), false); } } function syncStatsCardMenu() { const panel = $("dashboard-card-menu-panel"); if (!panel) return; const hidden = new Set(readHiddenStatsCards()); panel.querySelectorAll("[data-dashboard-card-toggle]").forEach((input) => { const id = input.dataset.dashboardCardToggle; input.checked = !hidden.has(id); }); } function initStatsCardMenu() { const menu = $("dashboard-card-menu"); const panel = $("dashboard-card-menu-panel"); if (!panel || panel.dataset.bound === "true") return; panel.dataset.bound = "true"; syncStatsCardMenu(); panel.querySelectorAll("[data-dashboard-card-toggle]").forEach((input) => { input.addEventListener("change", () => { setStatsCardHidden(input.dataset.dashboardCardToggle, !input.checked); }); }); menu?.addEventListener("toggle", () => { if (menu.open) syncStatsCardMenu(); }); } function applySavedGridLayout() { const saved = readSavedGridLayout(); if (saved.length) { applyGridLayout(saved); return; } // Preserve older drag-only ordering by converting it into default-size widgets. const order = readSavedOrder(); if (!order.length || order.join("|") === DEFAULT_CARD_ORDER.join("|")) return; const migrated = order.map((id, index) => ({ id, ...(DEFAULT_GRID_LAYOUT[id] || { w: 2, h: 2 }), x: index === 0 ? 0 : undefined, y: index === 0 ? 0 : undefined, })); let x = 0; for (const item of migrated) { const width = item.w || 2; if (x + width > 12) x = 0; item.x = x; item.y = 0; x += width; } applyGridLayout(migrated); } function resetStatsGridLayout() { localStorage.removeItem(STATS_LAYOUT_KEY); localStorage.removeItem(STATS_HIDDEN_KEY); for (const id of DEFAULT_CARD_ORDER) { setStatsCardHidden(id, false, false); } syncStatsCardMenu(); if (!statsGrid) { const content = $("dashboard-stats-content"); if (content) applySavedOrder(content); return; } applyGridLayout(DEFAULT_CARD_ORDER.map((id) => ({ id, ...DEFAULT_GRID_LAYOUT[id] }))); } function initStatsGrid(content) { if (!content || content.dataset.gridstackBound === "true") return; const api = window.GridStack?.GridStack || window.GridStack; if (typeof api?.init !== "function") { applySavedOrder(content); applyHiddenStatsCards(); return; } content.dataset.gridstackBound = "true"; statsGrid = api.init({ column: 12, cellHeight: getStatsCellHeight(), margin: 6, animate: true, float: false, handle: ".grid-stack-item-content", draggable: { handle: ".grid-stack-item-content", appendTo: "body", scroll: true, }, resizable: { handles: "se" }, }, content); applySavedGridLayout(); applyHiddenStatsCards(); statsGrid.on("change dragstop resizestop", () => { syncStatsWidgetHeights(); saveStatsGridLayout(); resizeCharts(); }); syncStatsWidgetHeights(); resizeCharts(); } function updateOnlineChart(history, snapshot) { if (!onlineChart) return; const rows = aggregateOnlineHistory(history, snapshot); onlineChart.data.labels = rows.map((row) => formatWeekLabel(row.timestamp)); onlineChart.data.datasets[0].data = rows.map((row) => Number(row.clientsOnline) || 0); onlineChart.update("none"); } function updateOsChart(byOS) { if (!osChart) return; const entries = topEntries(byOS, 7); if (!entries.length) { osChart.data.labels = ["No clients"]; osChart.data.datasets[0].data = [1]; osChart.data.datasets[0].backgroundColor = ["rgba(100, 116, 139, 0.35)"]; setText("dash-os-leader", "None"); osChart.update("none"); return; } osChart.data.labels = entries.map(([label]) => compactOsLabel(label)); osChart.data.datasets[0].data = entries.map(([, count]) => count); osChart.data.datasets[0].backgroundColor = entries.map((_, index) => osColors[index % osColors.length]); setText("dash-os-leader", `${compactOsLabel(entries[0][0])} ${entries[0][1]}`); osChart.update("none"); } function updateSummary(snapshot) { const clients = snapshot?.clients || {}; const online = Number(clients.online) || 0; const total = Number(clients.total) || 0; const ratio = total > 0 ? Math.round((online / total) * 100) : 0; const sessions = snapshot?.sessions || {}; const activeSessions = (Number(sessions.console) || 0) + (Number(sessions.remoteDesktop) || 0) + (Number(sessions.fileBrowser) || 0) + (Number(sessions.process) || 0); setText("dash-online-count", formatNumber(online)); setText("dash-total-count", `${formatNumber(total)} total`); setText("dash-online-ratio", `${ratio}%`); setText("dash-session-count", formatNumber(activeSessions)); setText("dash-command-minute", `${formatNumber(snapshot?.commands?.lastMinute)} cmd/min`); setText("dash-http-errors", `${formatNumber(snapshot?.http?.lastMinuteErrors)} errors`); const mem = Number(snapshot?.server?.systemMemory?.usedPercent) || 0; setText("dash-memory-status", `${Math.round(mem)}%`); setDotTone("dash-memory-dot", mem >= 90 ? "bad" : mem >= 75 ? "warn" : "live"); const avgPing = snapshot?.ping?.avg; if (Number.isFinite(avgPing)) { const rounded = Math.round(avgPing); setText("dash-ping-status", `${rounded} ms`); setDotTone("dash-ping-dot", rounded >= 150 ? "bad" : rounded >= 80 ? "warn" : "live"); } else { setText("dash-ping-status", "-"); setDotTone("dash-ping-dot", "muted"); } setText("dash-trend-status", `${formatBytes(snapshot?.bandwidth?.sentPerSecond || 0)}/s out`); setText("dash-last-refresh", formatTime(Date.now())); } export function updateDashboardStatsFromClients(data) { if (!data) return; const online = Number(data.online) || 0; const total = Number(data.total) || 0; const ratio = total > 0 ? Math.round((online / total) * 100) : 0; setText("dash-online-count", formatNumber(online)); setText("dash-total-count", `${formatNumber(total)} total`); setText("dash-online-ratio", `${ratio}%`); } async function fetchDashboardMetrics() { try { const res = await fetch("/api/metrics", { credentials: "include" }); if (!res.ok) throw new Error(`metrics ${res.status}`); const data = await res.json(); const snapshot = data?.snapshot || {}; updateSummary(snapshot); updateOnlineChart(data?.history || [], snapshot); updateOsChart(snapshot?.clients?.byOS || {}); setText("dash-api-status", "Live"); } catch (err) { console.warn("dashboard stats failed", err); setText("dash-api-status", "Error"); setText("dash-last-refresh", "Error"); } } export function initDashboardStats() { if (initialized) return; initialized = true; window.removeEventListener("pagehide", teardownDashboardStats); window.addEventListener("pagehide", teardownDashboardStats); if (!makeCharts()) { setTimeout(() => { if (makeCharts()) applyHeightMode(getSavedHeight()); }, 150); } initStatsToggle(); fetchDashboardMetrics(); metricsTimer = setInterval(fetchDashboardMetrics, 5000); } function initStatsToggle() { const shell = $("dashboard-stats"); const button = $("dashboard-stats-toggle"); const resetButton = $("dashboard-stats-reset"); const resizer = $("dashboard-stats-resizer"); if (!shell || !button) return; const setHeight = (value, persist = true) => { const height = normalizeHeight(value); shell.style.setProperty("--dashboard-stats-height", `${height}px`); resizer?.setAttribute("aria-valuenow", String(height)); if (persist) localStorage.setItem(STATS_HEIGHT_KEY, String(height)); if (statsGrid) { statsGrid.cellHeight(getStatsCellHeight()); syncStatsWidgetHeights(); } applyHeightMode(height); resizeCharts(); }; const setCollapsed = (collapsed) => { shell.classList.toggle("is-collapsed", collapsed); button.setAttribute("aria-expanded", collapsed ? "false" : "true"); button.title = collapsed ? "Show status overview" : "Hide status overview"; button.innerHTML = collapsed ? '' : ''; localStorage.setItem(STATS_COLLAPSED_KEY, collapsed ? "true" : "false"); }; initStatsGrid($("dashboard-stats-content")); initStatsCardMenu(); initPulseReorder(); setHeight(getSavedHeight(), false); setCollapsed(localStorage.getItem(STATS_COLLAPSED_KEY) === "true"); let dragStartY = 0; let dragStartHeight = DEFAULT_STATS_HEIGHT; const stopResize = () => { shell.classList.remove("is-resizing"); resizer?.releasePointerCapture?.(resizer._activePointerId); resizer._activePointerId = null; }; resizer?.setAttribute("aria-valuemin", String(MIN_STATS_HEIGHT)); resizer?.setAttribute("aria-valuemax", String(MAX_STATS_HEIGHT)); resizer?.addEventListener("pointerdown", (e) => { if (shell.classList.contains("is-collapsed")) return; dragStartY = e.clientY; dragStartHeight = getSavedHeight(); resizer._activePointerId = e.pointerId; resizer.setPointerCapture?.(e.pointerId); shell.classList.add("is-resizing"); e.preventDefault(); }); resizer?.addEventListener("pointermove", (e) => { if (!shell.classList.contains("is-resizing")) return; setHeight(dragStartHeight + (e.clientY - dragStartY)); }); resizer?.addEventListener("pointerup", stopResize); resizer?.addEventListener("pointercancel", stopResize); resizer?.addEventListener("keydown", (e) => { if (shell.classList.contains("is-collapsed")) return; const current = getSavedHeight(); if (e.key === "ArrowUp") { e.preventDefault(); setHeight(current - 10); } else if (e.key === "ArrowDown") { e.preventDefault(); setHeight(current + 10); } else if (e.key === "Home") { e.preventDefault(); setHeight(MIN_STATS_HEIGHT); } else if (e.key === "End") { e.preventDefault(); setHeight(MAX_STATS_HEIGHT); } }); resetButton?.addEventListener("click", () => { setHeight(DEFAULT_STATS_HEIGHT); setCollapsed(false); resetStatsGridLayout(); localStorage.removeItem(STATS_ORDER_KEY); localStorage.removeItem(PULSE_ORDER_KEY); const pulseList = document.querySelector(".dashboard-health-list"); if (pulseList) applyPulseOrder(pulseList); }); button.addEventListener("click", () => { const nextCollapsed = !shell.classList.contains("is-collapsed"); setCollapsed(nextCollapsed); if (!nextCollapsed) resizeCharts(); }); } function readSavedPulseOrder() { try { const parsed = JSON.parse(localStorage.getItem(PULSE_ORDER_KEY) || "[]"); if (!Array.isArray(parsed)) return DEFAULT_PULSE_ORDER; const cleaned = parsed.filter((id) => DEFAULT_PULSE_ORDER.includes(id)); return [...cleaned, ...DEFAULT_PULSE_ORDER.filter((id) => !cleaned.includes(id))]; } catch { return DEFAULT_PULSE_ORDER; } } function savePulseOrder(list) { const order = Array.from(list.querySelectorAll("[data-pulse-row]")) .map((row) => row.dataset.pulseRow) .filter(Boolean); localStorage.setItem(PULSE_ORDER_KEY, JSON.stringify(order)); } function applyPulseOrder(list) { const byId = new Map(); list.querySelectorAll("[data-pulse-row]").forEach((row) => { byId.set(row.dataset.pulseRow, row); }); for (const id of readSavedPulseOrder()) { const row = byId.get(id); if (row) list.appendChild(row); } applyHeightMode(getSavedHeight()); } function initPulseReorder() { const list = document.querySelector(".dashboard-health-list"); if (!list || list.dataset.reorderBound === "true") return; list.dataset.reorderBound = "true"; applyPulseOrder(list); let dragged = null; list.addEventListener("dragstart", (e) => { const row = e.target.closest("[data-pulse-row]"); if (!row) return; e.stopPropagation(); dragged = row; row.classList.add("is-dragging"); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", row.dataset.pulseRow || ""); }); list.addEventListener("dragover", (e) => { if (!dragged) return; const target = e.target.closest("[data-pulse-row]"); if (!target || target === dragged) return; e.preventDefault(); e.stopPropagation(); const rect = target.getBoundingClientRect(); const after = e.clientY > rect.top + rect.height / 2; list.insertBefore(dragged, after ? target.nextSibling : target); applyHeightMode(getSavedHeight()); }); list.addEventListener("drop", (e) => { if (!dragged) return; e.preventDefault(); e.stopPropagation(); savePulseOrder(list); applyHeightMode(getSavedHeight()); }); list.addEventListener("dragend", (e) => { e.stopPropagation(); dragged?.classList.remove("is-dragging"); dragged = null; savePulseOrder(list); applyHeightMode(getSavedHeight()); }); } function readSavedOrder() { try { const parsed = JSON.parse(localStorage.getItem(STATS_ORDER_KEY) || "[]"); if (!Array.isArray(parsed)) return DEFAULT_CARD_ORDER; const cleaned = parsed.filter((id) => DEFAULT_CARD_ORDER.includes(id)); return [...cleaned, ...DEFAULT_CARD_ORDER.filter((id) => !cleaned.includes(id))]; } catch { return DEFAULT_CARD_ORDER; } } function saveCurrentOrder(content) { const order = Array.from(content.querySelectorAll("[data-dashboard-card]")) .map((card) => card.dataset.dashboardCard) .filter(Boolean); localStorage.setItem(STATS_ORDER_KEY, JSON.stringify(order)); } function applySavedOrder(content) { const byId = new Map(); content.querySelectorAll("[data-dashboard-card]").forEach((card) => { byId.set(card.dataset.dashboardCard, card); }); for (const id of readSavedOrder()) { const card = byId.get(id); if (card) content.appendChild(card); } } function initCardReorder() { const content = $("dashboard-stats-content"); if (!content || content.dataset.reorderBound === "true") return; content.dataset.reorderBound = "true"; applySavedOrder(content); let dragged = null; content.addEventListener("dragstart", (e) => { const card = e.target.closest("[data-dashboard-card]"); if (!card) return; dragged = card; card.classList.add("is-dragging"); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", card.dataset.dashboardCard || ""); }); content.addEventListener("dragover", (e) => { if (!dragged) return; const target = e.target.closest("[data-dashboard-card]"); if (!target || target === dragged) return; e.preventDefault(); const rect = target.getBoundingClientRect(); const after = e.clientY > rect.top + rect.height / 2 || e.clientX > rect.left + rect.width / 2; content.insertBefore(dragged, after ? target.nextSibling : target); }); content.addEventListener("drop", (e) => { if (!dragged) return; e.preventDefault(); saveCurrentOrder(content); }); content.addEventListener("dragend", () => { dragged?.classList.remove("is-dragging"); dragged = null; saveCurrentOrder(content); requestAnimationFrame(() => { onlineChart?.resize(); osChart?.resize(); }); }); } if (typeof window !== "undefined") { window.addEventListener("beforeunload", () => { if (metricsTimer) clearInterval(metricsTimer); }); }