better dashboard
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,32 +25,25 @@
|
||||
<header id="top-nav"></header>
|
||||
<main class="px-5 py-6">
|
||||
<div id="clients-shell" class="w-full flex flex-col gap-5">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<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>
|
||||
<span id="total-pill" class="hidden">0 online</span>
|
||||
<section id="dashboard-stats" class="dashboard-stats-shell" aria-label="Dashboard status">
|
||||
<div class="dashboard-stats-bar">
|
||||
<div class="dashboard-stats-title">
|
||||
<i class="fa-solid fa-chart-simple"></i>
|
||||
<span>Status overview</span>
|
||||
<span id="server-version-text" class="dashboard-version-text">Server version: loading...</span>
|
||||
</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">
|
||||
<i class="fa-solid fa-eye-slash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
<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">
|
||||
<span class="dashboard-live-dot is-live"></span>
|
||||
<span>Online</span>
|
||||
@@ -61,7 +54,7 @@
|
||||
<span id="dash-online-ratio">0%</span>
|
||||
</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">
|
||||
<i class="fa-solid fa-wave-square"></i>
|
||||
<span>Sessions</span>
|
||||
@@ -72,7 +65,7 @@
|
||||
<span id="dash-http-errors">0 errors</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-chart-card">
|
||||
<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>
|
||||
@@ -84,7 +77,7 @@
|
||||
<canvas id="dash-online-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-chart-card">
|
||||
<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>
|
||||
@@ -96,7 +89,7 @@
|
||||
<canvas id="dash-os-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-health-card">
|
||||
<div class="dashboard-health-card" data-dashboard-card="pulse" draggable="true">
|
||||
<div class="dashboard-chart-head">
|
||||
<div>
|
||||
<span class="dashboard-chart-kicker">Status</span>
|
||||
@@ -105,17 +98,17 @@
|
||||
<span id="dash-last-refresh" class="dashboard-chart-pill">Waiting</span>
|
||||
</div>
|
||||
<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>API</span>
|
||||
<strong id="dash-api-status">Live</strong>
|
||||
</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>Memory</span>
|
||||
<strong id="dash-memory-status">0%</strong>
|
||||
</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>Ping Avg</span>
|
||||
<strong id="dash-ping-status">-</strong>
|
||||
@@ -123,6 +116,16 @@
|
||||
</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>
|
||||
<div class="dashboard-toolbar">
|
||||
<label
|
||||
|
||||
Reference in New Issue
Block a user