add gridstack

This commit is contained in:
KingKDot
2026-06-03 23:07:50 -05:00
parent 68134a69c2
commit 25b90005ea
6 changed files with 445 additions and 110 deletions
+3
View File
@@ -10,6 +10,7 @@
"adm-zip": "^0.5.12", "adm-zip": "^0.5.12",
"ansi-to-html": "0.7.2", "ansi-to-html": "0.7.2",
"geoip-lite": "latest", "geoip-lite": "latest",
"gridstack": "^12.6.0",
"jose": "^6.1.3", "jose": "^6.1.3",
"msgpackr": "1.11.8", "msgpackr": "1.11.8",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
@@ -316,6 +317,8 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"gridstack": ["gridstack@12.6.0", "", {}, "sha512-dUrqsormSybFn/2P4Dz8AgprftKD5e/IiV7UmC0XLQU+G+/WtkAeFiCSNLoAGhPDXoJ/O61Xtj3gljY/Ds83yQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"highlight.js": ["highlight.js@11.9.0", "", {}, "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw=="], "highlight.js": ["highlight.js@11.9.0", "", {}, "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw=="],
+1
View File
@@ -21,6 +21,7 @@
"adm-zip": "^0.5.12", "adm-zip": "^0.5.12",
"ansi-to-html": "0.7.2", "ansi-to-html": "0.7.2",
"geoip-lite": "latest", "geoip-lite": "latest",
"gridstack": "^12.6.0",
"jose": "^6.1.3", "jose": "^6.1.3",
"msgpackr": "1.11.8", "msgpackr": "1.11.8",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
+250 -10
View File
@@ -5,12 +5,22 @@ let osChart = null;
const STATS_COLLAPSED_KEY = "overlord_dashboard_stats_collapsed"; const STATS_COLLAPSED_KEY = "overlord_dashboard_stats_collapsed";
const STATS_HEIGHT_KEY = "overlord_dashboard_stats_height"; const STATS_HEIGHT_KEY = "overlord_dashboard_stats_height";
const STATS_ORDER_KEY = "overlord_dashboard_stats_order"; 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 PULSE_ORDER_KEY = "overlord_dashboard_pulse_order";
const DEFAULT_STATS_HEIGHT = 142; const DEFAULT_STATS_HEIGHT = 142;
const MIN_STATS_HEIGHT = 112; const MIN_STATS_HEIGHT = 112;
const MAX_STATS_HEIGHT = 280; const MAX_STATS_HEIGHT = 280;
const DEFAULT_CARD_ORDER = ["online", "sessions", "trend", "fleet", "pulse"]; const DEFAULT_CARD_ORDER = ["online", "sessions", "trend", "fleet", "pulse"];
const DEFAULT_PULSE_ORDER = ["api", "memory", "ping"]; 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;
const palette = { const palette = {
text: "#cbd5e1", text: "#cbd5e1",
@@ -291,6 +301,239 @@ function applyHeightMode(height) {
} }
} }
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) { function updateOnlineChart(history, snapshot) {
if (!onlineChart) return; if (!onlineChart) return;
const rows = aggregateOnlineHistory(history, snapshot); const rows = aggregateOnlineHistory(history, snapshot);
@@ -403,18 +646,15 @@ function initStatsToggle() {
const resizer = $("dashboard-stats-resizer"); const resizer = $("dashboard-stats-resizer");
if (!shell || !button) return; if (!shell || !button) return;
const resizeCharts = () => {
requestAnimationFrame(() => {
onlineChart?.resize();
osChart?.resize();
});
};
const setHeight = (value, persist = true) => { const setHeight = (value, persist = true) => {
const height = normalizeHeight(value); const height = normalizeHeight(value);
shell.style.setProperty("--dashboard-stats-height", `${height}px`); shell.style.setProperty("--dashboard-stats-height", `${height}px`);
resizer?.setAttribute("aria-valuenow", String(height)); resizer?.setAttribute("aria-valuenow", String(height));
if (persist) localStorage.setItem(STATS_HEIGHT_KEY, String(height)); if (persist) localStorage.setItem(STATS_HEIGHT_KEY, String(height));
if (statsGrid) {
statsGrid.cellHeight(getStatsCellHeight());
syncStatsWidgetHeights();
}
applyHeightMode(height); applyHeightMode(height);
resizeCharts(); resizeCharts();
}; };
@@ -429,7 +669,8 @@ function initStatsToggle() {
localStorage.setItem(STATS_COLLAPSED_KEY, collapsed ? "true" : "false"); localStorage.setItem(STATS_COLLAPSED_KEY, collapsed ? "true" : "false");
}; };
initCardReorder(); initStatsGrid($("dashboard-stats-content"));
initStatsCardMenu();
initPulseReorder(); initPulseReorder();
setHeight(getSavedHeight(), false); setHeight(getSavedHeight(), false);
setCollapsed(localStorage.getItem(STATS_COLLAPSED_KEY) === "true"); setCollapsed(localStorage.getItem(STATS_COLLAPSED_KEY) === "true");
@@ -483,10 +724,9 @@ function initStatsToggle() {
resetButton?.addEventListener("click", () => { resetButton?.addEventListener("click", () => {
setHeight(DEFAULT_STATS_HEIGHT); setHeight(DEFAULT_STATS_HEIGHT);
setCollapsed(false); setCollapsed(false);
resetStatsGridLayout();
localStorage.removeItem(STATS_ORDER_KEY); localStorage.removeItem(STATS_ORDER_KEY);
localStorage.removeItem(PULSE_ORDER_KEY); localStorage.removeItem(PULSE_ORDER_KEY);
const content = $("dashboard-stats-content");
if (content) applySavedOrder(content);
const pulseList = document.querySelector(".dashboard-health-list"); const pulseList = document.querySelector(".dashboard-health-list");
if (pulseList) applyPulseOrder(pulseList); if (pulseList) applyPulseOrder(pulseList);
}); });
+90 -36
View File
@@ -4466,11 +4466,56 @@ body.sb-ready #top-nav {
border-color: rgba(71, 85, 105, 0.8); border-color: rgba(71, 85, 105, 0.8);
} }
.dashboard-stats-content { .dashboard-card-menu {
position: relative;
}
.dashboard-card-menu > summary {
list-style: none;
}
.dashboard-card-menu > summary::-webkit-details-marker {
display: none;
}
.dashboard-card-menu-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 45;
display: grid; display: grid;
grid-template-columns: minmax(150px, 0.85fr) minmax(150px, 0.85fr) minmax(260px, 1.45fr) minmax(260px, 1.35fr) minmax(220px, 1fr); gap: 6px;
gap: 12px; width: 196px;
align-items: stretch; padding: 10px;
border: 1px solid rgba(51, 65, 85, 0.82);
border-radius: 8px;
background: rgba(15, 23, 42, 0.98);
box-shadow: 0 18px 38px rgba(2, 6, 23, 0.42);
}
.dashboard-card-menu-panel label {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
padding: 6px 7px;
border-radius: 6px;
color: #cbd5e1;
font-size: var(--text-xs);
font-weight: 700;
cursor: pointer;
}
.dashboard-card-menu-panel label:hover {
background: rgba(51, 65, 85, 0.62);
}
.dashboard-card-menu-panel input {
accent-color: #38bdf8;
}
.dashboard-stats-content {
min-height: calc(var(--dashboard-stats-height) + 12px);
} }
.dashboard-stats-shell.is-collapsed .dashboard-stats-content, .dashboard-stats-shell.is-collapsed .dashboard-stats-content,
@@ -4513,6 +4558,7 @@ body.sb-ready #top-nav {
.dashboard-health-card { .dashboard-health-card {
position: relative; position: relative;
min-width: 0; min-width: 0;
height: 100%;
border: 1px solid rgba(51, 65, 85, 0.68); border: 1px solid rgba(51, 65, 85, 0.68);
border-radius: 10px; border-radius: 10px;
background: rgba(15, 23, 42, 0.72); background: rgba(15, 23, 42, 0.72);
@@ -4540,11 +4586,44 @@ body.sb-ready #top-nav {
border-color: rgba(56, 189, 248, 0.62); border-color: rgba(56, 189, 248, 0.62);
} }
.dashboard-stats-content .grid-stack-item-content {
overflow: hidden !important;
}
.dashboard-stats-content .grid-stack-item.dashboard-card-hidden {
display: none;
}
.dashboard-stats-content .grid-stack-item.ui-draggable-dragging .grid-stack-item-content,
.dashboard-stats-content .grid-stack-item.ui-resizable-resizing .grid-stack-item-content {
border-color: rgba(56, 189, 248, 0.62);
box-shadow: 0 18px 42px rgba(2, 6, 23, 0.42);
}
.dashboard-stats-content .grid-stack-placeholder > .placeholder-content,
.dashboard-stats-content .grid-stack-placeholder > .grid-stack-item-content {
border: 1px dashed rgba(56, 189, 248, 0.58);
border-radius: 10px;
background: rgba(8, 47, 73, 0.22);
}
.dashboard-stats-content .ui-resizable-se {
right: 8px !important;
bottom: 8px !important;
width: 14px;
height: 14px;
opacity: 0;
transition: opacity 140ms ease;
}
.dashboard-stats-content .grid-stack-item:hover .ui-resizable-se {
opacity: 0.72;
}
.dashboard-stat-card { .dashboard-stat-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
height: var(--dashboard-stats-height);
padding: 14px; padding: 14px;
} }
@@ -4603,7 +4682,6 @@ body.sb-ready #top-nav {
.dashboard-chart-card, .dashboard-chart-card,
.dashboard-health-card { .dashboard-health-card {
height: var(--dashboard-stats-height);
padding: 12px 14px 10px; padding: 12px 14px 10px;
} }
@@ -4653,6 +4731,12 @@ body.sb-ready #top-nav {
padding-bottom: 8px; padding-bottom: 8px;
} }
.dashboard-chart-wrap canvas {
display: block;
max-width: 100%;
max-height: 100%;
}
.dashboard-health-list { .dashboard-health-list {
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -4741,37 +4825,7 @@ body.sb-ready #top-nav {
box-shadow: 0 0 0 4px rgba(100, 116, 139, 0.1); box-shadow: 0 0 0 4px rgba(100, 116, 139, 0.1);
} }
@media (max-width: 1380px) {
.dashboard-stats-content {
grid-template-columns: repeat(2, minmax(150px, 1fr)) repeat(2, minmax(260px, 1.35fr));
}
.dashboard-health-card {
grid-column: span 2;
}
}
@media (max-width: 940px) {
.dashboard-stats-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dashboard-chart-card,
.dashboard-health-card {
grid-column: span 2;
}
}
@media (max-width: 620px) { @media (max-width: 620px) {
.dashboard-stats-content {
grid-template-columns: 1fr;
}
.dashboard-chart-card,
.dashboard-health-card {
grid-column: auto;
}
.dashboard-stat-value { .dashboard-stat-value {
font-size: clamp(1.65rem, calc(var(--dashboard-stats-height) / 3.4), 3rem); font-size: clamp(1.65rem, calc(var(--dashboard-stats-height) / 3.4), 3rem);
} }
+88 -64
View File
@@ -12,11 +12,13 @@
<link rel="stylesheet" href="/assets/tailwind.css" /> <link rel="stylesheet" href="/assets/tailwind.css" />
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" /> <link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" />
<link rel="stylesheet" href="/vendor/flag-icons/css/flag-icons.min.css" /> <link rel="stylesheet" href="/vendor/flag-icons/css/flag-icons.min.css" />
<link rel="stylesheet" href="/vendor/gridstack/gridstack.min.css" />
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
<link rel="stylesheet" href="/assets/main.css" /> <link rel="stylesheet" href="/assets/main.css" />
<link rel="stylesheet" href="/assets/custom.css" /> <link rel="stylesheet" href="/assets/custom.css" />
<script defer src="/vendor/animejs/anime.min.js"></script> <script defer src="/vendor/animejs/anime.min.js"></script>
<script defer src="/vendor/chart.js/chart.umd.js"></script> <script defer src="/vendor/chart.js/chart.umd.js"></script>
<script defer src="/vendor/gridstack/gridstack-all.js"></script>
<link rel="preload" href="/api/clients?page=1&amp;pageSize=12&amp;q=&amp;sort=stable&amp;status=all&amp;os=all&amp;country=all&amp;group=all" as="fetch" crossorigin="use-credentials" /> <link rel="preload" href="/api/clients?page=1&amp;pageSize=12&amp;q=&amp;sort=stable&amp;status=all&amp;os=all&amp;country=all&amp;group=all" as="fetch" crossorigin="use-credentials" />
<link rel="preload" href="/api/auth/me" as="fetch" crossorigin="use-credentials" /> <link rel="preload" href="/api/auth/me" as="fetch" crossorigin="use-credentials" />
<link rel="preload" href="/api/groups" as="fetch" crossorigin="use-credentials" /> <link rel="preload" href="/api/groups" as="fetch" crossorigin="use-credentials" />
@@ -34,6 +36,18 @@
<span id="server-version-text" class="dashboard-version-text">Server version: loading...</span> <span id="server-version-text" class="dashboard-version-text">Server version: loading...</span>
</div> </div>
<div class="dashboard-stats-controls" aria-label="Status overview controls"> <div class="dashboard-stats-controls" aria-label="Status overview controls">
<details id="dashboard-card-menu" class="dashboard-card-menu">
<summary class="dashboard-stats-toggle" title="Dashboard cards" aria-label="Dashboard cards">
<i class="fa-solid fa-table-cells-large"></i>
</summary>
<div id="dashboard-card-menu-panel" class="dashboard-card-menu-panel">
<label><input type="checkbox" data-dashboard-card-toggle="online" checked /> Online</label>
<label><input type="checkbox" data-dashboard-card-toggle="sessions" checked /> Sessions</label>
<label><input type="checkbox" data-dashboard-card-toggle="trend" checked /> Clients Online</label>
<label><input type="checkbox" data-dashboard-card-toggle="fleet" checked /> Operating Systems</label>
<label><input type="checkbox" data-dashboard-card-toggle="pulse" checked /> Server Pulse</label>
</div>
</details>
<button id="dashboard-stats-reset" class="dashboard-stats-toggle" type="button" title="Reset status overview"> <button id="dashboard-stats-reset" class="dashboard-stats-toggle" type="button" title="Reset status overview">
<i class="fa-solid fa-arrow-rotate-left"></i> <i class="fa-solid fa-arrow-rotate-left"></i>
</button> </button>
@@ -42,79 +56,89 @@
</button> </button>
</div> </div>
</div> </div>
<div id="dashboard-stats-content" class="dashboard-stats-content"> <div id="dashboard-stats-content" class="dashboard-stats-content grid-stack">
<div class="dashboard-stat-card dashboard-stat-card--online" data-dashboard-card="online" draggable="true"> <div class="grid-stack-item" data-dashboard-card="online" gs-id="online" gs-x="0" gs-y="0" gs-w="2" gs-h="2" gs-min-w="2" gs-min-h="1">
<div class="dashboard-stat-topline"> <div class="grid-stack-item-content dashboard-stat-card dashboard-stat-card--online">
<span class="dashboard-live-dot is-live"></span> <div class="dashboard-stat-topline">
<span>Online</span> <span class="dashboard-live-dot is-live"></span>
</div> <span>Online</span>
<div id="dash-online-count" class="dashboard-stat-value">0</div> </div>
<div class="dashboard-stat-subline"> <div id="dash-online-count" class="dashboard-stat-value">0</div>
<span id="dash-total-count">0 total</span> <div class="dashboard-stat-subline">
<span id="dash-online-ratio">0%</span> <span id="dash-total-count">0 total</span>
</div> <span id="dash-online-ratio">0%</span>
</div> </div>
<div class="dashboard-stat-card dashboard-stat-card--sessions" data-dashboard-card="sessions" draggable="true"> </div>
<div class="dashboard-stat-topline">
<i class="fa-solid fa-wave-square"></i>
<span>Sessions</span>
</div>
<div id="dash-session-count" class="dashboard-stat-value">0</div>
<div class="dashboard-stat-subline">
<span id="dash-command-minute">0 cmd/min</span>
<span id="dash-http-errors">0 errors</span>
</div> </div>
</div> <div class="grid-stack-item" data-dashboard-card="sessions" gs-id="sessions" gs-x="2" gs-y="0" gs-w="2" gs-h="2" gs-min-w="2" gs-min-h="1">
<div class="dashboard-chart-card" data-dashboard-card="trend" draggable="true"> <div class="grid-stack-item-content dashboard-stat-card dashboard-stat-card--sessions">
<div class="dashboard-chart-head"> <div class="dashboard-stat-topline">
<div> <i class="fa-solid fa-wave-square"></i>
<span class="dashboard-chart-kicker">Past Week</span> <span>Sessions</span>
<strong>Clients Online</strong> </div>
<div id="dash-session-count" class="dashboard-stat-value">0</div>
<div class="dashboard-stat-subline">
<span id="dash-command-minute">0 cmd/min</span>
<span id="dash-http-errors">0 errors</span>
</div>
</div> </div>
<span id="dash-trend-status" class="dashboard-chart-pill">Live</span>
</div> </div>
<div class="dashboard-chart-wrap"> <div class="grid-stack-item" data-dashboard-card="trend" gs-id="trend" gs-x="4" gs-y="0" gs-w="3" gs-h="2" gs-min-w="2" gs-min-h="1">
<canvas id="dash-online-chart"></canvas> <div class="grid-stack-item-content dashboard-chart-card">
</div> <div class="dashboard-chart-head">
</div> <div>
<div class="dashboard-chart-card" data-dashboard-card="fleet" draggable="true"> <span class="dashboard-chart-kicker">Past Week</span>
<div class="dashboard-chart-head"> <strong>Clients Online</strong>
<div> </div>
<span class="dashboard-chart-kicker">Fleet Mix</span> <span id="dash-trend-status" class="dashboard-chart-pill">Live</span>
<strong>Operating Systems</strong> </div>
<div class="dashboard-chart-wrap">
<canvas id="dash-online-chart"></canvas>
</div>
</div> </div>
<span id="dash-os-leader" class="dashboard-chart-pill">None</span>
</div>
<div class="dashboard-chart-wrap dashboard-chart-wrap--donut">
<canvas id="dash-os-chart"></canvas>
</div> </div>
</div> <div class="grid-stack-item" data-dashboard-card="fleet" gs-id="fleet" gs-x="7" gs-y="0" gs-w="3" gs-h="2" gs-min-w="2" gs-min-h="1">
<div class="dashboard-health-card" data-dashboard-card="pulse" draggable="true"> <div class="grid-stack-item-content dashboard-chart-card">
<div class="dashboard-chart-head"> <div class="dashboard-chart-head">
<div> <div>
<span class="dashboard-chart-kicker">Status</span> <span class="dashboard-chart-kicker">Fleet Mix</span>
<strong>Server Pulse</strong> <strong>Operating Systems</strong>
</div>
<span id="dash-os-leader" class="dashboard-chart-pill">None</span>
</div>
<div class="dashboard-chart-wrap dashboard-chart-wrap--donut">
<canvas id="dash-os-chart"></canvas>
</div>
</div> </div>
<span id="dash-last-refresh" class="dashboard-chart-pill">Waiting</span>
</div> </div>
<div class="dashboard-health-list"> <div class="grid-stack-item" data-dashboard-card="pulse" gs-id="pulse" gs-x="10" gs-y="0" gs-w="2" gs-h="2" gs-min-w="2" gs-min-h="1">
<div class="dashboard-health-row" data-pulse-row="api" draggable="true"> <div class="grid-stack-item-content dashboard-health-card">
<span class="dashboard-live-dot is-live"></span> <div class="dashboard-chart-head">
<span>API</span> <div>
<strong id="dash-api-status">Live</strong> <span class="dashboard-chart-kicker">Status</span>
</div> <strong>Server Pulse</strong>
<div class="dashboard-health-row" data-pulse-row="memory" draggable="true"> </div>
<span id="dash-memory-dot" class="dashboard-live-dot is-live"></span> <span id="dash-last-refresh" class="dashboard-chart-pill">Waiting</span>
<span>Memory</span> </div>
<strong id="dash-memory-status">0%</strong> <div class="dashboard-health-list">
</div> <div class="dashboard-health-row" data-pulse-row="api" draggable="true">
<div class="dashboard-health-row" data-pulse-row="ping" draggable="true"> <span class="dashboard-live-dot is-live"></span>
<span id="dash-ping-dot" class="dashboard-live-dot is-muted"></span> <span>API</span>
<span>Ping Avg</span> <strong id="dash-api-status">Live</strong>
<strong id="dash-ping-status">-</strong> </div>
<div class="dashboard-health-row" data-pulse-row="memory" draggable="true">
<span id="dash-memory-dot" class="dashboard-live-dot is-live"></span>
<span>Memory</span>
<strong id="dash-memory-status">0%</strong>
</div>
<div class="dashboard-health-row" data-pulse-row="ping" draggable="true">
<span id="dash-ping-dot" class="dashboard-live-dot is-muted"></span>
<span>Ping Avg</span>
<strong id="dash-ping-status">-</strong>
</div>
</div>
</div> </div>
</div> </div>
</div>
</div> </div>
<div <div
id="dashboard-stats-resizer" id="dashboard-stats-resizer"
+13
View File
@@ -186,6 +186,19 @@ copyFile(
path.join(VENDOR, "tabulator", "tabulator_midnight.min.css"), path.join(VENDOR, "tabulator", "tabulator_midnight.min.css"),
); );
/* ── GridStack ───────────────────────────────────────────────────── */
console.log("Copying GridStack ...");
const gridstackRoot = path.join(NM, "gridstack", "dist");
copyFile(
path.join(gridstackRoot, "gridstack-all.js"),
path.join(VENDOR, "gridstack", "gridstack-all.js"),
);
copyFile(
path.join(gridstackRoot, "gridstack.min.css"),
path.join(VENDOR, "gridstack", "gridstack.min.css"),
);
/* ── highlight.js (bundle core + languages) ──────────────────────── */ /* ── highlight.js (bundle core + languages) ──────────────────────── */
console.log("Bundling highlight.js ..."); console.log("Bundling highlight.js ...");