better dashboard
This commit is contained in:
@@ -3,6 +3,14 @@ let metricsTimer = null;
|
|||||||
let onlineChart = null;
|
let onlineChart = null;
|
||||||
let osChart = null;
|
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_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 = {
|
const palette = {
|
||||||
text: "#cbd5e1",
|
text: "#cbd5e1",
|
||||||
@@ -10,7 +18,7 @@ const palette = {
|
|||||||
border: "rgba(100, 116, 139, 0.18)",
|
border: "rgba(100, 116, 139, 0.18)",
|
||||||
panel: "rgba(15, 23, 42, 0.96)",
|
panel: "rgba(15, 23, 42, 0.96)",
|
||||||
cyan: "#22d3ee",
|
cyan: "#22d3ee",
|
||||||
emerald: "#34d399",
|
emerald: "#22c55e",
|
||||||
sky: "#38bdf8",
|
sky: "#38bdf8",
|
||||||
indigo: "#818cf8",
|
indigo: "#818cf8",
|
||||||
amber: "#fbbf24",
|
amber: "#fbbf24",
|
||||||
@@ -166,8 +174,8 @@ function makeCharts() {
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
label: "Online",
|
label: "Online",
|
||||||
data: [],
|
data: [],
|
||||||
borderColor: palette.cyan,
|
borderColor: palette.emerald,
|
||||||
backgroundColor: createGradient(onlineCtx, palette.cyan),
|
backgroundColor: createGradient(onlineCtx, palette.emerald),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.38,
|
tension: 0.38,
|
||||||
@@ -184,7 +192,7 @@ function makeCharts() {
|
|||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: palette.panel,
|
backgroundColor: palette.panel,
|
||||||
borderColor: "rgba(56, 189, 248, 0.32)",
|
borderColor: "rgba(34, 197, 94, 0.34)",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
titleColor: "#e2e8f0",
|
titleColor: "#e2e8f0",
|
||||||
bodyColor: palette.text,
|
bodyColor: palette.text,
|
||||||
@@ -225,6 +233,9 @@ function makeCharts() {
|
|||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
animation: { duration: 260 },
|
animation: { duration: 260 },
|
||||||
cutout: "66%",
|
cutout: "66%",
|
||||||
|
layout: {
|
||||||
|
padding: { top: 2, right: 4, bottom: 10, left: 4 },
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
position: "right",
|
position: "right",
|
||||||
@@ -249,6 +260,37 @@ function makeCharts() {
|
|||||||
return true;
|
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) {
|
function updateOnlineChart(history, snapshot) {
|
||||||
if (!onlineChart) return;
|
if (!onlineChart) return;
|
||||||
const rows = aggregateOnlineHistory(history, snapshot);
|
const rows = aggregateOnlineHistory(history, snapshot);
|
||||||
@@ -345,7 +387,9 @@ export function initDashboardStats() {
|
|||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
initialized = true;
|
initialized = true;
|
||||||
if (!makeCharts()) {
|
if (!makeCharts()) {
|
||||||
setTimeout(makeCharts, 150);
|
setTimeout(() => {
|
||||||
|
if (makeCharts()) applyHeightMode(getSavedHeight());
|
||||||
|
}, 150);
|
||||||
}
|
}
|
||||||
initStatsToggle();
|
initStatsToggle();
|
||||||
fetchDashboardMetrics();
|
fetchDashboardMetrics();
|
||||||
@@ -355,8 +399,26 @@ export function initDashboardStats() {
|
|||||||
function initStatsToggle() {
|
function initStatsToggle() {
|
||||||
const shell = $("dashboard-stats");
|
const shell = $("dashboard-stats");
|
||||||
const button = $("dashboard-stats-toggle");
|
const button = $("dashboard-stats-toggle");
|
||||||
|
const resetButton = $("dashboard-stats-reset");
|
||||||
|
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 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) => {
|
const setCollapsed = (collapsed) => {
|
||||||
shell.classList.toggle("is-collapsed", collapsed);
|
shell.classList.toggle("is-collapsed", collapsed);
|
||||||
button.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
button.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||||
@@ -367,16 +429,222 @@ function initStatsToggle() {
|
|||||||
localStorage.setItem(STATS_COLLAPSED_KEY, collapsed ? "true" : "false");
|
localStorage.setItem(STATS_COLLAPSED_KEY, collapsed ? "true" : "false");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
initCardReorder();
|
||||||
|
initPulseReorder();
|
||||||
|
setHeight(getSavedHeight(), false);
|
||||||
setCollapsed(localStorage.getItem(STATS_COLLAPSED_KEY) === "true");
|
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", () => {
|
button.addEventListener("click", () => {
|
||||||
const nextCollapsed = !shell.classList.contains("is-collapsed");
|
const nextCollapsed = !shell.classList.contains("is-collapsed");
|
||||||
setCollapsed(nextCollapsed);
|
setCollapsed(nextCollapsed);
|
||||||
if (!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(() => {
|
requestAnimationFrame(() => {
|
||||||
onlineChart?.resize();
|
onlineChart?.resize();
|
||||||
osChart?.resize();
|
osChart?.resize();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4378,9 +4378,10 @@ body.sb-ready #top-nav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-stats-shell {
|
.dashboard-stats-shell {
|
||||||
|
--dashboard-stats-height: 142px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-stats-bar {
|
.dashboard-stats-bar {
|
||||||
@@ -4405,6 +4406,46 @@ body.sb-ready #top-nav {
|
|||||||
color: #38bdf8;
|
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 {
|
.dashboard-stats-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -4432,10 +4473,41 @@ body.sb-ready #top-nav {
|
|||||||
align-items: stretch;
|
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;
|
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-stat-card,
|
||||||
.dashboard-chart-card,
|
.dashboard-chart-card,
|
||||||
.dashboard-health-card {
|
.dashboard-health-card {
|
||||||
@@ -4446,6 +4518,7 @@ body.sb-ready #top-nav {
|
|||||||
background: rgba(15, 23, 42, 0.72);
|
background: rgba(15, 23, 42, 0.72);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-stat-card::before,
|
.dashboard-stat-card::before,
|
||||||
@@ -4454,11 +4527,24 @@ body.sb-ready #top-nav {
|
|||||||
display: none;
|
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 {
|
.dashboard-stat-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 142px;
|
height: var(--dashboard-stats-height);
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4466,6 +4552,15 @@ body.sb-ready #top-nav {
|
|||||||
border-color: rgba(34, 211, 238, 0.22);
|
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 {
|
.dashboard-stat-card--sessions {
|
||||||
border-color: rgba(129, 140, 248, 0.24);
|
border-color: rgba(129, 140, 248, 0.24);
|
||||||
}
|
}
|
||||||
@@ -4489,7 +4584,7 @@ body.sb-ready #top-nav {
|
|||||||
.dashboard-stat-value {
|
.dashboard-stat-value {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
font-size: 2.4rem;
|
font-size: clamp(1.8rem, calc(var(--dashboard-stats-height) / 3.2), 3.8rem);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
@@ -4508,7 +4603,7 @@ body.sb-ready #top-nav {
|
|||||||
|
|
||||||
.dashboard-chart-card,
|
.dashboard-chart-card,
|
||||||
.dashboard-health-card {
|
.dashboard-health-card {
|
||||||
min-height: 142px;
|
height: var(--dashboard-stats-height);
|
||||||
padding: 12px 14px 10px;
|
padding: 12px 14px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4549,12 +4644,13 @@ body.sb-ready #top-nav {
|
|||||||
.dashboard-chart-wrap {
|
.dashboard-chart-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
height: 88px;
|
height: max(56px, calc(var(--dashboard-stats-height) - 54px));
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-chart-wrap--donut {
|
.dashboard-chart-wrap--donut {
|
||||||
height: 92px;
|
height: max(54px, calc(var(--dashboard-stats-height) - 62px));
|
||||||
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-health-list {
|
.dashboard-health-list {
|
||||||
@@ -4562,6 +4658,9 @@ body.sb-ready #top-nav {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
|
align-content: start;
|
||||||
|
max-height: calc(var(--dashboard-stats-height) - 52px);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-health-row {
|
.dashboard-health-row {
|
||||||
@@ -4576,6 +4675,16 @@ body.sb-ready #top-nav {
|
|||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
color: #9db1cc;
|
color: #9db1cc;
|
||||||
font-size: 0.78rem;
|
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 {
|
.dashboard-health-row strong {
|
||||||
@@ -4584,6 +4693,26 @@ body.sb-ready #top-nav {
|
|||||||
font-variant-numeric: tabular-nums;
|
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 {
|
.dashboard-live-dot {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@@ -4593,8 +4722,8 @@ body.sb-ready #top-nav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-live-dot.is-live {
|
.dashboard-live-dot.is-live {
|
||||||
background: #34d399;
|
background: #22c55e;
|
||||||
box-shadow: 0 0 0 4px rgba(52, 211, 153, 0.12), 0 0 16px rgba(52, 211, 153, 0.34);
|
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 {
|
.dashboard-live-dot.is-warn {
|
||||||
@@ -4644,8 +4773,18 @@ body.sb-ready #top-nav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-stat-value {
|
.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,
|
.dashboard-menu-panel,
|
||||||
|
|||||||
@@ -89,11 +89,15 @@ window.setDashboardPageBounds = (currentPage, totalPages) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function setServerVersionLabel(version) {
|
function setServerVersionLabel(version, tone = "ok") {
|
||||||
if (!serverVersionText) return;
|
if (!serverVersionText) return;
|
||||||
serverVersionText.textContent = "Server version: ";
|
serverVersionText.textContent = "Server version: ";
|
||||||
const value = document.createElement("span");
|
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;
|
value.textContent = version;
|
||||||
serverVersionText.appendChild(value);
|
serverVersionText.appendChild(value);
|
||||||
}
|
}
|
||||||
@@ -103,16 +107,16 @@ async function loadServerVersion() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch("/api/version", { credentials: "include" });
|
const res = await fetch("/api/version", { credentials: "include" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
serverVersionText.textContent = "Server version: unavailable";
|
setServerVersionLabel("unavailable", "bad");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = await res.json();
|
const payload = await res.json();
|
||||||
const version = typeof payload?.version === "string" && payload.version.trim()
|
const version = typeof payload?.version === "string" && payload.version.trim()
|
||||||
? payload.version.trim()
|
? payload.version.trim()
|
||||||
: "unknown";
|
: "unknown";
|
||||||
setServerVersionLabel(version);
|
setServerVersionLabel(version, version === "unknown" ? "warn" : "ok");
|
||||||
} catch {
|
} catch {
|
||||||
serverVersionText.textContent = "Server version: unavailable";
|
setServerVersionLabel("unavailable", "bad");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,32 +25,25 @@
|
|||||||
<header id="top-nav"></header>
|
<header id="top-nav"></header>
|
||||||
<main class="px-5 py-6">
|
<main class="px-5 py-6">
|
||||||
<div id="clients-shell" class="w-full flex flex-col gap-5">
|
<div id="clients-shell" class="w-full flex flex-col gap-5">
|
||||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
<span id="total-pill" class="hidden">0 online</span>
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-semibold">Clients</h1>
|
|
||||||
<p class="text-slate-400">KDOT wants YOU to have a good day!</p>
|
|
||||||
<p id="server-version-text" class="text-xs text-slate-500 mt-1">
|
|
||||||
Server version: loading...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-full bg-slate-800/70 text-slate-300 border border-slate-600/60"
|
|
||||||
id="total-pill"
|
|
||||||
>0 online</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<section id="dashboard-stats" class="dashboard-stats-shell" aria-label="Dashboard status">
|
<section id="dashboard-stats" class="dashboard-stats-shell" aria-label="Dashboard status">
|
||||||
<div class="dashboard-stats-bar">
|
<div class="dashboard-stats-bar">
|
||||||
<div class="dashboard-stats-title">
|
<div class="dashboard-stats-title">
|
||||||
<i class="fa-solid fa-chart-simple"></i>
|
<i class="fa-solid fa-chart-simple"></i>
|
||||||
<span>Status overview</span>
|
<span>Status overview</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">
|
||||||
|
<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>
|
||||||
<button id="dashboard-stats-toggle" class="dashboard-stats-toggle" type="button" aria-expanded="true" title="Hide status overview">
|
<button id="dashboard-stats-toggle" class="dashboard-stats-toggle" type="button" aria-expanded="true" title="Hide status overview">
|
||||||
<i class="fa-solid fa-eye-slash"></i>
|
<i class="fa-solid fa-eye-slash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div id="dashboard-stats-content" class="dashboard-stats-content">
|
<div id="dashboard-stats-content" class="dashboard-stats-content">
|
||||||
<div class="dashboard-stat-card dashboard-stat-card--online">
|
<div class="dashboard-stat-card dashboard-stat-card--online" data-dashboard-card="online" draggable="true">
|
||||||
<div class="dashboard-stat-topline">
|
<div class="dashboard-stat-topline">
|
||||||
<span class="dashboard-live-dot is-live"></span>
|
<span class="dashboard-live-dot is-live"></span>
|
||||||
<span>Online</span>
|
<span>Online</span>
|
||||||
@@ -61,7 +54,7 @@
|
|||||||
<span id="dash-online-ratio">0%</span>
|
<span id="dash-online-ratio">0%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-stat-card dashboard-stat-card--sessions">
|
<div class="dashboard-stat-card dashboard-stat-card--sessions" data-dashboard-card="sessions" draggable="true">
|
||||||
<div class="dashboard-stat-topline">
|
<div class="dashboard-stat-topline">
|
||||||
<i class="fa-solid fa-wave-square"></i>
|
<i class="fa-solid fa-wave-square"></i>
|
||||||
<span>Sessions</span>
|
<span>Sessions</span>
|
||||||
@@ -72,7 +65,7 @@
|
|||||||
<span id="dash-http-errors">0 errors</span>
|
<span id="dash-http-errors">0 errors</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-chart-card">
|
<div class="dashboard-chart-card" data-dashboard-card="trend" draggable="true">
|
||||||
<div class="dashboard-chart-head">
|
<div class="dashboard-chart-head">
|
||||||
<div>
|
<div>
|
||||||
<span class="dashboard-chart-kicker">Past Week</span>
|
<span class="dashboard-chart-kicker">Past Week</span>
|
||||||
@@ -84,7 +77,7 @@
|
|||||||
<canvas id="dash-online-chart"></canvas>
|
<canvas id="dash-online-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-chart-card">
|
<div class="dashboard-chart-card" data-dashboard-card="fleet" draggable="true">
|
||||||
<div class="dashboard-chart-head">
|
<div class="dashboard-chart-head">
|
||||||
<div>
|
<div>
|
||||||
<span class="dashboard-chart-kicker">Fleet Mix</span>
|
<span class="dashboard-chart-kicker">Fleet Mix</span>
|
||||||
@@ -96,7 +89,7 @@
|
|||||||
<canvas id="dash-os-chart"></canvas>
|
<canvas id="dash-os-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-health-card">
|
<div class="dashboard-health-card" data-dashboard-card="pulse" draggable="true">
|
||||||
<div class="dashboard-chart-head">
|
<div class="dashboard-chart-head">
|
||||||
<div>
|
<div>
|
||||||
<span class="dashboard-chart-kicker">Status</span>
|
<span class="dashboard-chart-kicker">Status</span>
|
||||||
@@ -105,17 +98,17 @@
|
|||||||
<span id="dash-last-refresh" class="dashboard-chart-pill">Waiting</span>
|
<span id="dash-last-refresh" class="dashboard-chart-pill">Waiting</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-health-list">
|
<div class="dashboard-health-list">
|
||||||
<div class="dashboard-health-row">
|
<div class="dashboard-health-row" data-pulse-row="api" draggable="true">
|
||||||
<span class="dashboard-live-dot is-live"></span>
|
<span class="dashboard-live-dot is-live"></span>
|
||||||
<span>API</span>
|
<span>API</span>
|
||||||
<strong id="dash-api-status">Live</strong>
|
<strong id="dash-api-status">Live</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-health-row">
|
<div class="dashboard-health-row" data-pulse-row="memory" draggable="true">
|
||||||
<span id="dash-memory-dot" class="dashboard-live-dot is-live"></span>
|
<span id="dash-memory-dot" class="dashboard-live-dot is-live"></span>
|
||||||
<span>Memory</span>
|
<span>Memory</span>
|
||||||
<strong id="dash-memory-status">0%</strong>
|
<strong id="dash-memory-status">0%</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-health-row">
|
<div class="dashboard-health-row" data-pulse-row="ping" draggable="true">
|
||||||
<span id="dash-ping-dot" class="dashboard-live-dot is-muted"></span>
|
<span id="dash-ping-dot" class="dashboard-live-dot is-muted"></span>
|
||||||
<span>Ping Avg</span>
|
<span>Ping Avg</span>
|
||||||
<strong id="dash-ping-status">-</strong>
|
<strong id="dash-ping-status">-</strong>
|
||||||
@@ -123,6 +116,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
id="dashboard-stats-resizer"
|
||||||
|
class="dashboard-stats-resizer"
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="horizontal"
|
||||||
|
tabindex="0"
|
||||||
|
title="Drag to resize status overview"
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="dashboard-toolbar">
|
<div class="dashboard-toolbar">
|
||||||
<label
|
<label
|
||||||
|
|||||||
Reference in New Issue
Block a user