From a8b8b6369a8dd8ef275bd96dc563f8fc71b1915a Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sun, 31 May 2026 15:53:50 -0500 Subject: [PATCH] better dashboard --- .../public/assets/dashboard-stats.js | 290 +++++++++++++++++- Overlord-Server/public/assets/main.css | 159 +++++++++- Overlord-Server/public/assets/main.js | 14 +- Overlord-Server/public/index.html | 53 ++-- 4 files changed, 465 insertions(+), 51 deletions(-) diff --git a/Overlord-Server/public/assets/dashboard-stats.js b/Overlord-Server/public/assets/dashboard-stats.js index b1a54cc..61ff587 100644 --- a/Overlord-Server/public/assets/dashboard-stats.js +++ b/Overlord-Server/public/assets/dashboard-stats.js @@ -3,6 +3,14 @@ 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 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 palette = { text: "#cbd5e1", @@ -10,7 +18,7 @@ const palette = { border: "rgba(100, 116, 139, 0.18)", panel: "rgba(15, 23, 42, 0.96)", cyan: "#22d3ee", - emerald: "#34d399", + emerald: "#22c55e", sky: "#38bdf8", indigo: "#818cf8", amber: "#fbbf24", @@ -166,8 +174,8 @@ function makeCharts() { datasets: [{ label: "Online", data: [], - borderColor: palette.cyan, - backgroundColor: createGradient(onlineCtx, palette.cyan), + borderColor: palette.emerald, + backgroundColor: createGradient(onlineCtx, palette.emerald), borderWidth: 2, fill: true, tension: 0.38, @@ -184,7 +192,7 @@ function makeCharts() { legend: { display: false }, tooltip: { backgroundColor: palette.panel, - borderColor: "rgba(56, 189, 248, 0.32)", + borderColor: "rgba(34, 197, 94, 0.34)", borderWidth: 1, titleColor: "#e2e8f0", bodyColor: palette.text, @@ -225,6 +233,9 @@ function makeCharts() { maintainAspectRatio: false, animation: { duration: 260 }, cutout: "66%", + layout: { + padding: { top: 2, right: 4, bottom: 10, left: 4 }, + }, plugins: { legend: { position: "right", @@ -249,6 +260,37 @@ function makeCharts() { 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 updateOnlineChart(history, snapshot) { if (!onlineChart) return; const rows = aggregateOnlineHistory(history, snapshot); @@ -345,7 +387,9 @@ export function initDashboardStats() { if (initialized) return; initialized = true; if (!makeCharts()) { - setTimeout(makeCharts, 150); + setTimeout(() => { + if (makeCharts()) applyHeightMode(getSavedHeight()); + }, 150); } initStatsToggle(); fetchDashboardMetrics(); @@ -355,8 +399,26 @@ export function initDashboardStats() { 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 resizeCharts = () => { + requestAnimationFrame(() => { + onlineChart?.resize(); + osChart?.resize(); + }); + }; + + 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)); + applyHeightMode(height); + resizeCharts(); + }; + const setCollapsed = (collapsed) => { shell.classList.toggle("is-collapsed", collapsed); button.setAttribute("aria-expanded", collapsed ? "false" : "true"); @@ -367,16 +429,222 @@ function initStatsToggle() { localStorage.setItem(STATS_COLLAPSED_KEY, collapsed ? "true" : "false"); }; + initCardReorder(); + 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); + localStorage.removeItem(STATS_ORDER_KEY); + localStorage.removeItem(PULSE_ORDER_KEY); + const content = $("dashboard-stats-content"); + if (content) applySavedOrder(content); + 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) { - requestAnimationFrame(() => { - onlineChart?.resize(); - osChart?.resize(); - }); - } + 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(); + }); }); } diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 156a889..18bd091 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -4378,9 +4378,10 @@ body.sb-ready #top-nav { } .dashboard-stats-shell { + --dashboard-stats-height: 142px; display: flex; flex-direction: column; - gap: 10px; + gap: 8px; } .dashboard-stats-bar { @@ -4405,6 +4406,46 @@ body.sb-ready #top-nav { color: #38bdf8; } +.dashboard-version-text { + display: inline-flex; + align-items: center; + min-height: 22px; + border-left: 1px solid rgba(100, 116, 139, 0.42); + padding-left: 10px; + color: #64748b; + font-size: 0.74rem; + font-weight: 600; + letter-spacing: 0; + text-transform: none; +} + +.dashboard-version-text .server-version-number { + color: #22c55e; + margin-left: 4px; + font-variant-numeric: tabular-nums; + font-weight: 800; +} + +.dashboard-version-text .server-version-number-warning { + color: #fbbf24; + margin-left: 4px; + font-variant-numeric: tabular-nums; + font-weight: 800; +} + +.dashboard-version-text .server-version-number-mismatch { + color: #f87171; + margin-left: 4px; + font-variant-numeric: tabular-nums; + font-weight: 800; +} + +.dashboard-stats-controls { + display: inline-flex; + align-items: center; + gap: 8px; +} + .dashboard-stats-toggle { display: inline-flex; align-items: center; @@ -4432,10 +4473,41 @@ body.sb-ready #top-nav { align-items: stretch; } -.dashboard-stats-shell.is-collapsed .dashboard-stats-content { +.dashboard-stats-shell.is-collapsed .dashboard-stats-content, +.dashboard-stats-shell.is-collapsed .dashboard-stats-resizer { display: none; } +.dashboard-stats-shell.is-resizing { + user-select: none; +} + +.dashboard-stats-resizer { + display: flex; + align-items: center; + justify-content: center; + height: 14px; + border-radius: 8px; + cursor: ns-resize; + touch-action: none; + color: #64748b; +} + +.dashboard-stats-resizer span { + width: 92px; + height: 4px; + border-radius: 999px; + background: rgba(71, 85, 105, 0.78); + transition: background 140ms ease, width 140ms ease; +} + +.dashboard-stats-resizer:hover span, +.dashboard-stats-resizer:focus-visible span, +.dashboard-stats-shell.is-resizing .dashboard-stats-resizer span { + width: 132px; + background: rgba(56, 189, 248, 0.78); +} + .dashboard-stat-card, .dashboard-chart-card, .dashboard-health-card { @@ -4446,6 +4518,7 @@ body.sb-ready #top-nav { background: rgba(15, 23, 42, 0.72); box-shadow: none; overflow: hidden; + cursor: grab; } .dashboard-stat-card::before, @@ -4454,11 +4527,24 @@ body.sb-ready #top-nav { display: none; } +.dashboard-stat-card:active, +.dashboard-chart-card:active, +.dashboard-health-card:active { + cursor: grabbing; +} + +.dashboard-stat-card.is-dragging, +.dashboard-chart-card.is-dragging, +.dashboard-health-card.is-dragging { + opacity: 0.52; + border-color: rgba(56, 189, 248, 0.62); +} + .dashboard-stat-card { display: flex; flex-direction: column; justify-content: space-between; - min-height: 142px; + height: var(--dashboard-stats-height); padding: 14px; } @@ -4466,6 +4552,15 @@ body.sb-ready #top-nav { border-color: rgba(34, 211, 238, 0.22); } +.dashboard-stat-card--online .dashboard-stat-value { + color: #f8fafc; + text-shadow: none; +} + +.dashboard-stat-card--online .dashboard-stat-subline span:last-child { + color: #86efac; +} + .dashboard-stat-card--sessions { border-color: rgba(129, 140, 248, 0.24); } @@ -4489,7 +4584,7 @@ body.sb-ready #top-nav { .dashboard-stat-value { margin-top: 10px; color: #f8fafc; - font-size: 2.4rem; + font-size: clamp(1.8rem, calc(var(--dashboard-stats-height) / 3.2), 3.8rem); line-height: 1; font-weight: 800; font-variant-numeric: tabular-nums; @@ -4508,7 +4603,7 @@ body.sb-ready #top-nav { .dashboard-chart-card, .dashboard-health-card { - min-height: 142px; + height: var(--dashboard-stats-height); padding: 12px 14px 10px; } @@ -4549,12 +4644,13 @@ body.sb-ready #top-nav { .dashboard-chart-wrap { position: relative; z-index: 1; - height: 88px; + height: max(56px, calc(var(--dashboard-stats-height) - 54px)); min-width: 0; } .dashboard-chart-wrap--donut { - height: 92px; + height: max(54px, calc(var(--dashboard-stats-height) - 62px)); + padding-bottom: 8px; } .dashboard-health-list { @@ -4562,6 +4658,9 @@ body.sb-ready #top-nav { z-index: 1; display: grid; gap: 7px; + align-content: start; + max-height: calc(var(--dashboard-stats-height) - 52px); + overflow: hidden; } .dashboard-health-row { @@ -4576,6 +4675,16 @@ body.sb-ready #top-nav { padding: 5px 8px; color: #9db1cc; font-size: 0.78rem; + cursor: grab; +} + +.dashboard-health-row.is-hidden { + display: none; +} + +.dashboard-health-row.is-dragging { + opacity: 0.5; + border-color: rgba(56, 189, 248, 0.58); } .dashboard-health-row strong { @@ -4584,6 +4693,26 @@ body.sb-ready #top-nav { font-variant-numeric: tabular-nums; } +.dashboard-stats-shell.is-compact .dashboard-chart-kicker { + display: none; +} + +.dashboard-stats-shell.is-compact .dashboard-chart-head { + margin-bottom: 5px; +} + +.dashboard-stats-shell.is-compact .dashboard-chart-pill { + max-width: 7rem; +} + +.dashboard-stats-shell.is-tiny .dashboard-stat-subline { + display: none; +} + +.dashboard-stats-shell.is-tiny .dashboard-chart-pill { + display: none; +} + .dashboard-live-dot { width: 8px; height: 8px; @@ -4593,8 +4722,8 @@ body.sb-ready #top-nav { } .dashboard-live-dot.is-live { - background: #34d399; - box-shadow: 0 0 0 4px rgba(52, 211, 153, 0.12), 0 0 16px rgba(52, 211, 153, 0.34); + background: #22c55e; + box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.13), 0 0 16px rgba(34, 197, 94, 0.36); } .dashboard-live-dot.is-warn { @@ -4644,8 +4773,18 @@ body.sb-ready #top-nav { } .dashboard-stat-value { - font-size: 2rem; + font-size: clamp(1.65rem, calc(var(--dashboard-stats-height) / 3.4), 3rem); } + + .dashboard-stats-bar { + align-items: flex-start; + } + + .dashboard-stats-controls { + flex-wrap: wrap; + justify-content: flex-end; + } + } .dashboard-menu-panel, diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index 2722ca2..6eb66f7 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -89,11 +89,15 @@ window.setDashboardPageBounds = (currentPage, totalPages) => { } }; -function setServerVersionLabel(version) { +function setServerVersionLabel(version, tone = "ok") { if (!serverVersionText) return; serverVersionText.textContent = "Server version: "; const value = document.createElement("span"); - value.className = "server-version-number"; + value.className = tone === "bad" + ? "server-version-number-mismatch" + : tone === "warn" + ? "server-version-number-warning" + : "server-version-number"; value.textContent = version; serverVersionText.appendChild(value); } @@ -103,16 +107,16 @@ async function loadServerVersion() { try { const res = await fetch("/api/version", { credentials: "include" }); if (!res.ok) { - serverVersionText.textContent = "Server version: unavailable"; + setServerVersionLabel("unavailable", "bad"); return; } const payload = await res.json(); const version = typeof payload?.version === "string" && payload.version.trim() ? payload.version.trim() : "unknown"; - setServerVersionLabel(version); + setServerVersionLabel(version, version === "unknown" ? "warn" : "ok"); } catch { - serverVersionText.textContent = "Server version: unavailable"; + setServerVersionLabel("unavailable", "bad"); } } diff --git a/Overlord-Server/public/index.html b/Overlord-Server/public/index.html index b025db9..cf9071b 100644 --- a/Overlord-Server/public/index.html +++ b/Overlord-Server/public/index.html @@ -25,32 +25,25 @@
-
-
-

Clients

-

KDOT wants YOU to have a good day!

-

- Server version: loading... -

-
- 0 online -
+
Status overview + Server version: loading... +
+
+ +
-
-
+
Online @@ -61,7 +54,7 @@ 0%
-
+
Sessions @@ -72,7 +65,7 @@ 0 errors
-
+
Past Week @@ -84,7 +77,7 @@
-
+
Fleet Mix @@ -96,7 +89,7 @@
-
+
Status @@ -105,17 +98,17 @@ Waiting
-
+
API Live
-
+
Memory 0%
-
+
Ping Avg - @@ -123,6 +116,16 @@
+