add gridstack
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
"adm-zip": "^0.5.12",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"geoip-lite": "latest",
|
||||
"gridstack": "^12.6.0",
|
||||
"jose": "^6.1.3",
|
||||
"msgpackr": "1.11.8",
|
||||
"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=="],
|
||||
|
||||
"gridstack": ["gridstack@12.6.0", "", {}, "sha512-dUrqsormSybFn/2P4Dz8AgprftKD5e/IiV7UmC0XLQU+G+/WtkAeFiCSNLoAGhPDXoJ/O61Xtj3gljY/Ds83yQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"highlight.js": ["highlight.js@11.9.0", "", {}, "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw=="],
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"adm-zip": "^0.5.12",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"geoip-lite": "latest",
|
||||
"gridstack": "^12.6.0",
|
||||
"jose": "^6.1.3",
|
||||
"msgpackr": "1.11.8",
|
||||
"qrcode": "^1.5.4",
|
||||
|
||||
@@ -5,12 +5,22 @@ 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;
|
||||
|
||||
const palette = {
|
||||
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) {
|
||||
if (!onlineChart) return;
|
||||
const rows = aggregateOnlineHistory(history, snapshot);
|
||||
@@ -403,18 +646,15 @@ function initStatsToggle() {
|
||||
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));
|
||||
if (statsGrid) {
|
||||
statsGrid.cellHeight(getStatsCellHeight());
|
||||
syncStatsWidgetHeights();
|
||||
}
|
||||
applyHeightMode(height);
|
||||
resizeCharts();
|
||||
};
|
||||
@@ -429,7 +669,8 @@ function initStatsToggle() {
|
||||
localStorage.setItem(STATS_COLLAPSED_KEY, collapsed ? "true" : "false");
|
||||
};
|
||||
|
||||
initCardReorder();
|
||||
initStatsGrid($("dashboard-stats-content"));
|
||||
initStatsCardMenu();
|
||||
initPulseReorder();
|
||||
setHeight(getSavedHeight(), false);
|
||||
setCollapsed(localStorage.getItem(STATS_COLLAPSED_KEY) === "true");
|
||||
@@ -483,10 +724,9 @@ function initStatsToggle() {
|
||||
resetButton?.addEventListener("click", () => {
|
||||
setHeight(DEFAULT_STATS_HEIGHT);
|
||||
setCollapsed(false);
|
||||
resetStatsGridLayout();
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -4466,11 +4466,56 @@ body.sb-ready #top-nav {
|
||||
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;
|
||||
grid-template-columns: minmax(150px, 0.85fr) minmax(150px, 0.85fr) minmax(260px, 1.45fr) minmax(260px, 1.35fr) minmax(220px, 1fr);
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
width: 196px;
|
||||
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,
|
||||
@@ -4513,6 +4558,7 @@ body.sb-ready #top-nav {
|
||||
.dashboard-health-card {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: 1px solid rgba(51, 65, 85, 0.68);
|
||||
border-radius: 10px;
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
@@ -4540,11 +4586,44 @@ body.sb-ready #top-nav {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: var(--dashboard-stats-height);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
@@ -4603,7 +4682,6 @@ body.sb-ready #top-nav {
|
||||
|
||||
.dashboard-chart-card,
|
||||
.dashboard-health-card {
|
||||
height: var(--dashboard-stats-height);
|
||||
padding: 12px 14px 10px;
|
||||
}
|
||||
|
||||
@@ -4653,6 +4731,12 @@ body.sb-ready #top-nav {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.dashboard-chart-wrap canvas {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.dashboard-health-list {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -4741,37 +4825,7 @@ body.sb-ready #top-nav {
|
||||
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) {
|
||||
.dashboard-stats-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-chart-card,
|
||||
.dashboard-health-card {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.dashboard-stat-value {
|
||||
font-size: clamp(1.65rem, calc(var(--dashboard-stats-height) / 3.4), 3rem);
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
<link rel="stylesheet" href="/assets/tailwind.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/gridstack/gridstack.min.css" />
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
|
||||
<link rel="stylesheet" href="/assets/main.css" />
|
||||
<link rel="stylesheet" href="/assets/custom.css" />
|
||||
<script defer src="/vendor/animejs/anime.min.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&pageSize=12&q=&sort=stable&status=all&os=all&country=all&group=all" 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" />
|
||||
@@ -34,6 +36,18 @@
|
||||
<span id="server-version-text" class="dashboard-version-text">Server version: loading...</span>
|
||||
</div>
|
||||
<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">
|
||||
<i class="fa-solid fa-arrow-rotate-left"></i>
|
||||
</button>
|
||||
@@ -42,79 +56,89 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dashboard-stats-content" class="dashboard-stats-content">
|
||||
<div class="dashboard-stat-card dashboard-stat-card--online" data-dashboard-card="online" draggable="true">
|
||||
<div class="dashboard-stat-topline">
|
||||
<span class="dashboard-live-dot is-live"></span>
|
||||
<span>Online</span>
|
||||
</div>
|
||||
<div id="dash-online-count" class="dashboard-stat-value">0</div>
|
||||
<div class="dashboard-stat-subline">
|
||||
<span id="dash-total-count">0 total</span>
|
||||
<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 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 id="dashboard-stats-content" class="dashboard-stats-content grid-stack">
|
||||
<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="grid-stack-item-content dashboard-stat-card dashboard-stat-card--online">
|
||||
<div class="dashboard-stat-topline">
|
||||
<span class="dashboard-live-dot is-live"></span>
|
||||
<span>Online</span>
|
||||
</div>
|
||||
<div id="dash-online-count" class="dashboard-stat-value">0</div>
|
||||
<div class="dashboard-stat-subline">
|
||||
<span id="dash-total-count">0 total</span>
|
||||
<span id="dash-online-ratio">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-chart-card" data-dashboard-card="trend" draggable="true">
|
||||
<div class="dashboard-chart-head">
|
||||
<div>
|
||||
<span class="dashboard-chart-kicker">Past Week</span>
|
||||
<strong>Clients Online</strong>
|
||||
<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="grid-stack-item-content dashboard-stat-card dashboard-stat-card--sessions">
|
||||
<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>
|
||||
<span id="dash-trend-status" class="dashboard-chart-pill">Live</span>
|
||||
</div>
|
||||
<div class="dashboard-chart-wrap">
|
||||
<canvas id="dash-online-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-chart-card" data-dashboard-card="fleet" draggable="true">
|
||||
<div class="dashboard-chart-head">
|
||||
<div>
|
||||
<span class="dashboard-chart-kicker">Fleet Mix</span>
|
||||
<strong>Operating Systems</strong>
|
||||
<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">
|
||||
<div class="grid-stack-item-content dashboard-chart-card">
|
||||
<div class="dashboard-chart-head">
|
||||
<div>
|
||||
<span class="dashboard-chart-kicker">Past Week</span>
|
||||
<strong>Clients Online</strong>
|
||||
</div>
|
||||
<span id="dash-trend-status" class="dashboard-chart-pill">Live</span>
|
||||
</div>
|
||||
<div class="dashboard-chart-wrap">
|
||||
<canvas id="dash-online-chart"></canvas>
|
||||
</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 class="dashboard-health-card" data-dashboard-card="pulse" draggable="true">
|
||||
<div class="dashboard-chart-head">
|
||||
<div>
|
||||
<span class="dashboard-chart-kicker">Status</span>
|
||||
<strong>Server Pulse</strong>
|
||||
<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="grid-stack-item-content dashboard-chart-card">
|
||||
<div class="dashboard-chart-head">
|
||||
<div>
|
||||
<span class="dashboard-chart-kicker">Fleet Mix</span>
|
||||
<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>
|
||||
<span id="dash-last-refresh" class="dashboard-chart-pill">Waiting</span>
|
||||
</div>
|
||||
<div class="dashboard-health-list">
|
||||
<div class="dashboard-health-row" data-pulse-row="api" draggable="true">
|
||||
<span class="dashboard-live-dot is-live"></span>
|
||||
<span>API</span>
|
||||
<strong id="dash-api-status">Live</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 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="grid-stack-item-content dashboard-health-card">
|
||||
<div class="dashboard-chart-head">
|
||||
<div>
|
||||
<span class="dashboard-chart-kicker">Status</span>
|
||||
<strong>Server Pulse</strong>
|
||||
</div>
|
||||
<span id="dash-last-refresh" class="dashboard-chart-pill">Waiting</span>
|
||||
</div>
|
||||
<div class="dashboard-health-list">
|
||||
<div class="dashboard-health-row" data-pulse-row="api" draggable="true">
|
||||
<span class="dashboard-live-dot is-live"></span>
|
||||
<span>API</span>
|
||||
<strong id="dash-api-status">Live</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
|
||||
id="dashboard-stats-resizer"
|
||||
|
||||
@@ -186,6 +186,19 @@ copyFile(
|
||||
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) ──────────────────────── */
|
||||
|
||||
console.log("Bundling highlight.js ...");
|
||||
|
||||
Reference in New Issue
Block a user