diff --git a/Overlord-Server/public/assets/dashboard-stats.js b/Overlord-Server/public/assets/dashboard-stats.js
new file mode 100644
index 0000000..b1a54cc
--- /dev/null
+++ b/Overlord-Server/public/assets/dashboard-stats.js
@@ -0,0 +1,387 @@
+let initialized = false;
+let metricsTimer = null;
+let onlineChart = null;
+let osChart = null;
+const STATS_COLLAPSED_KEY = "overlord_dashboard_stats_collapsed";
+
+const palette = {
+ text: "#cbd5e1",
+ muted: "#94a3b8",
+ border: "rgba(100, 116, 139, 0.18)",
+ panel: "rgba(15, 23, 42, 0.96)",
+ cyan: "#22d3ee",
+ emerald: "#34d399",
+ sky: "#38bdf8",
+ indigo: "#818cf8",
+ amber: "#fbbf24",
+ rose: "#fb7185",
+ violet: "#a78bfa",
+ slate: "#64748b",
+};
+
+const osColors = [
+ palette.sky,
+ palette.emerald,
+ palette.violet,
+ palette.amber,
+ palette.rose,
+ palette.cyan,
+ palette.indigo,
+ palette.slate,
+];
+
+function $(id) {
+ return document.getElementById(id);
+}
+
+function setText(id, value) {
+ const el = $(id);
+ if (el) el.textContent = value;
+}
+
+function formatNumber(value) {
+ const n = Number(value) || 0;
+ return n.toLocaleString();
+}
+
+function formatBytes(bytes) {
+ const n = Number(bytes) || 0;
+ if (n <= 0) return "0 B";
+ const units = ["B", "KB", "MB", "GB", "TB"];
+ const idx = Math.min(units.length - 1, Math.floor(Math.log(n) / Math.log(1024)));
+ return `${(n / Math.pow(1024, idx)).toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`;
+}
+
+function formatTime(timestamp) {
+ return new Date(timestamp || Date.now()).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+function formatWeekLabel(timestamp) {
+ const date = new Date(timestamp || Date.now());
+ return date.toLocaleDateString([], {
+ weekday: "short",
+ hour: "2-digit",
+ });
+}
+
+function aggregateOnlineHistory(history, snapshot) {
+ const now = Date.now();
+ const weekAgo = now - 7 * 24 * 60 * 60 * 1000;
+ const source = Array.isArray(history)
+ ? history.filter((row) => Number(row?.timestamp) >= weekAgo)
+ : [];
+ const rows = source.length
+ ? source
+ : [{ timestamp: now, clientsOnline: snapshot?.clients?.online || 0 }];
+ const maxPoints = 84;
+ if (rows.length <= maxPoints) return rows;
+
+ const bucketMs = Math.max(
+ 60 * 60 * 1000,
+ Math.ceil((now - weekAgo) / maxPoints),
+ );
+ const buckets = new Map();
+ for (const row of rows) {
+ const ts = Number(row.timestamp) || now;
+ const key = Math.floor(ts / bucketMs) * bucketMs;
+ const bucket = buckets.get(key) || { timestamp: key, total: 0, count: 0, peak: 0 };
+ const online = Number(row.clientsOnline) || 0;
+ bucket.total += online;
+ bucket.count += 1;
+ bucket.peak = Math.max(bucket.peak, online);
+ buckets.set(key, bucket);
+ }
+
+ return Array.from(buckets.values())
+ .sort((a, b) => a.timestamp - b.timestamp)
+ .map((bucket) => ({
+ timestamp: bucket.timestamp,
+ clientsOnline: Math.round(bucket.total / Math.max(1, bucket.count)),
+ clientsPeak: bucket.peak,
+ }));
+}
+
+function setDotTone(id, tone) {
+ const el = $(id);
+ if (!el) return;
+ el.classList.toggle("is-live", tone === "live");
+ el.classList.toggle("is-warn", tone === "warn");
+ el.classList.toggle("is-bad", tone === "bad");
+ el.classList.toggle("is-muted", tone === "muted");
+}
+
+function topEntries(record, limit = 6) {
+ return Object.entries(record || {})
+ .map(([key, value]) => [key || "Unknown", Number(value) || 0])
+ .filter(([, value]) => value > 0)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, limit);
+}
+
+function compactOsLabel(os) {
+ const text = String(os || "Unknown");
+ const lower = text.toLowerCase();
+ if (lower.includes("windows 11")) return "Win 11";
+ if (lower.includes("windows 10")) return "Win 10";
+ if (lower.includes("windows")) return "Windows";
+ if (lower.includes("darwin") || lower.includes("mac")) return "macOS";
+ if (lower.includes("ubuntu")) return "Ubuntu";
+ if (lower.includes("debian")) return "Debian";
+ if (lower.includes("kali")) return "Kali";
+ if (lower.includes("fedora")) return "Fedora";
+ if (lower.includes("linux")) return "Linux";
+ return text.length > 16 ? `${text.slice(0, 15)}...` : text;
+}
+
+function configureChartDefaults() {
+ if (typeof Chart === "undefined") return false;
+ Chart.defaults.color = palette.text;
+ Chart.defaults.borderColor = palette.border;
+ Chart.defaults.font.family = "Inter, Segoe UI, system-ui, sans-serif";
+ return true;
+}
+
+function createGradient(ctx, color) {
+ const gradient = ctx.createLinearGradient(0, 0, 0, 150);
+ gradient.addColorStop(0, `${color}55`);
+ gradient.addColorStop(0.72, `${color}12`);
+ gradient.addColorStop(1, `${color}00`);
+ return gradient;
+}
+
+function makeCharts() {
+ if (!configureChartDefaults()) return false;
+ const onlineCanvas = $("dash-online-chart");
+ const osCanvas = $("dash-os-chart");
+ if (!onlineCanvas || !osCanvas) return false;
+
+ const onlineCtx = onlineCanvas.getContext("2d");
+ onlineChart = new Chart(onlineCtx, {
+ type: "line",
+ data: {
+ labels: [],
+ datasets: [{
+ label: "Online",
+ data: [],
+ borderColor: palette.cyan,
+ backgroundColor: createGradient(onlineCtx, palette.cyan),
+ borderWidth: 2,
+ fill: true,
+ tension: 0.38,
+ pointRadius: 0,
+ pointHoverRadius: 4,
+ }],
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: { duration: 260 },
+ interaction: { intersect: false, mode: "index" },
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ backgroundColor: palette.panel,
+ borderColor: "rgba(56, 189, 248, 0.32)",
+ borderWidth: 1,
+ titleColor: "#e2e8f0",
+ bodyColor: palette.text,
+ displayColors: false,
+ callbacks: {
+ label: (ctx) => `Avg online: ${ctx.parsed.y}`,
+ },
+ },
+ },
+ scales: {
+ x: {
+ grid: { display: false },
+ ticks: { color: palette.muted, maxRotation: 0, maxTicksLimit: 5 },
+ },
+ y: {
+ beginAtZero: true,
+ grid: { color: "rgba(100, 116, 139, 0.1)" },
+ ticks: { color: palette.muted, precision: 0, maxTicksLimit: 4 },
+ },
+ },
+ },
+ });
+
+ osChart = new Chart(osCanvas, {
+ type: "doughnut",
+ data: {
+ labels: [],
+ datasets: [{
+ data: [],
+ backgroundColor: osColors,
+ borderColor: "rgba(2, 6, 23, 0.9)",
+ borderWidth: 3,
+ hoverOffset: 5,
+ }],
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: { duration: 260 },
+ cutout: "66%",
+ plugins: {
+ legend: {
+ position: "right",
+ labels: {
+ boxWidth: 9,
+ boxHeight: 9,
+ color: palette.text,
+ padding: 10,
+ font: { size: 11 },
+ },
+ },
+ tooltip: {
+ backgroundColor: palette.panel,
+ borderColor: "rgba(129, 140, 248, 0.32)",
+ borderWidth: 1,
+ titleColor: "#e2e8f0",
+ bodyColor: palette.text,
+ },
+ },
+ },
+ });
+ return true;
+}
+
+function updateOnlineChart(history, snapshot) {
+ if (!onlineChart) return;
+ const rows = aggregateOnlineHistory(history, snapshot);
+
+ onlineChart.data.labels = rows.map((row) => formatWeekLabel(row.timestamp));
+ onlineChart.data.datasets[0].data = rows.map((row) => Number(row.clientsOnline) || 0);
+ onlineChart.update("none");
+}
+
+function updateOsChart(byOS) {
+ if (!osChart) return;
+ const entries = topEntries(byOS, 7);
+ if (!entries.length) {
+ osChart.data.labels = ["No clients"];
+ osChart.data.datasets[0].data = [1];
+ osChart.data.datasets[0].backgroundColor = ["rgba(100, 116, 139, 0.35)"];
+ setText("dash-os-leader", "None");
+ osChart.update("none");
+ return;
+ }
+
+ osChart.data.labels = entries.map(([label]) => compactOsLabel(label));
+ osChart.data.datasets[0].data = entries.map(([, count]) => count);
+ osChart.data.datasets[0].backgroundColor = entries.map((_, index) => osColors[index % osColors.length]);
+ setText("dash-os-leader", `${compactOsLabel(entries[0][0])} ${entries[0][1]}`);
+ osChart.update("none");
+}
+
+function updateSummary(snapshot) {
+ const clients = snapshot?.clients || {};
+ const online = Number(clients.online) || 0;
+ const total = Number(clients.total) || 0;
+ const ratio = total > 0 ? Math.round((online / total) * 100) : 0;
+ const sessions = snapshot?.sessions || {};
+ const activeSessions =
+ (Number(sessions.console) || 0) +
+ (Number(sessions.remoteDesktop) || 0) +
+ (Number(sessions.fileBrowser) || 0) +
+ (Number(sessions.process) || 0);
+
+ setText("dash-online-count", formatNumber(online));
+ setText("dash-total-count", `${formatNumber(total)} total`);
+ setText("dash-online-ratio", `${ratio}%`);
+ setText("dash-session-count", formatNumber(activeSessions));
+ setText("dash-command-minute", `${formatNumber(snapshot?.commands?.lastMinute)} cmd/min`);
+ setText("dash-http-errors", `${formatNumber(snapshot?.http?.lastMinuteErrors)} errors`);
+
+ const mem = Number(snapshot?.server?.systemMemory?.usedPercent) || 0;
+ setText("dash-memory-status", `${Math.round(mem)}%`);
+ setDotTone("dash-memory-dot", mem >= 90 ? "bad" : mem >= 75 ? "warn" : "live");
+
+ const avgPing = snapshot?.ping?.avg;
+ if (Number.isFinite(avgPing)) {
+ const rounded = Math.round(avgPing);
+ setText("dash-ping-status", `${rounded} ms`);
+ setDotTone("dash-ping-dot", rounded >= 150 ? "bad" : rounded >= 80 ? "warn" : "live");
+ } else {
+ setText("dash-ping-status", "-");
+ setDotTone("dash-ping-dot", "muted");
+ }
+
+ setText("dash-trend-status", `${formatBytes(snapshot?.bandwidth?.sentPerSecond || 0)}/s out`);
+ setText("dash-last-refresh", formatTime(Date.now()));
+}
+
+export function updateDashboardStatsFromClients(data) {
+ if (!data) return;
+ const online = Number(data.online) || 0;
+ const total = Number(data.total) || 0;
+ const ratio = total > 0 ? Math.round((online / total) * 100) : 0;
+ setText("dash-online-count", formatNumber(online));
+ setText("dash-total-count", `${formatNumber(total)} total`);
+ setText("dash-online-ratio", `${ratio}%`);
+}
+
+async function fetchDashboardMetrics() {
+ try {
+ const res = await fetch("/api/metrics", { credentials: "include" });
+ if (!res.ok) throw new Error(`metrics ${res.status}`);
+ const data = await res.json();
+ const snapshot = data?.snapshot || {};
+ updateSummary(snapshot);
+ updateOnlineChart(data?.history || [], snapshot);
+ updateOsChart(snapshot?.clients?.byOS || {});
+ setText("dash-api-status", "Live");
+ } catch (err) {
+ console.warn("dashboard stats failed", err);
+ setText("dash-api-status", "Error");
+ setText("dash-last-refresh", "Error");
+ }
+}
+
+export function initDashboardStats() {
+ if (initialized) return;
+ initialized = true;
+ if (!makeCharts()) {
+ setTimeout(makeCharts, 150);
+ }
+ initStatsToggle();
+ fetchDashboardMetrics();
+ metricsTimer = setInterval(fetchDashboardMetrics, 5000);
+}
+
+function initStatsToggle() {
+ const shell = $("dashboard-stats");
+ const button = $("dashboard-stats-toggle");
+ if (!shell || !button) return;
+
+ const setCollapsed = (collapsed) => {
+ shell.classList.toggle("is-collapsed", collapsed);
+ button.setAttribute("aria-expanded", collapsed ? "false" : "true");
+ button.title = collapsed ? "Show status overview" : "Hide status overview";
+ button.innerHTML = collapsed
+ ? ''
+ : '';
+ localStorage.setItem(STATS_COLLAPSED_KEY, collapsed ? "true" : "false");
+ };
+
+ setCollapsed(localStorage.getItem(STATS_COLLAPSED_KEY) === "true");
+ button.addEventListener("click", () => {
+ const nextCollapsed = !shell.classList.contains("is-collapsed");
+ setCollapsed(nextCollapsed);
+ if (!nextCollapsed) {
+ requestAnimationFrame(() => {
+ onlineChart?.resize();
+ osChart?.resize();
+ });
+ }
+ });
+}
+
+if (typeof window !== "undefined") {
+ window.addEventListener("beforeunload", () => {
+ if (metricsTimer) clearInterval(metricsTimer);
+ });
+}
diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css
index c9c6291..156a889 100644
--- a/Overlord-Server/public/assets/main.css
+++ b/Overlord-Server/public/assets/main.css
@@ -4377,6 +4377,277 @@ body.sb-ready #top-nav {
z-index: 20 !important;
}
+.dashboard-stats-shell {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.dashboard-stats-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.dashboard-stats-title {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: #94a3b8;
+ font-size: 0.8rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+
+.dashboard-stats-title i {
+ color: #38bdf8;
+}
+
+.dashboard-stats-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 34px;
+ height: 34px;
+ border: 1px solid rgba(51, 65, 85, 0.68);
+ border-radius: 8px;
+ background: rgba(30, 41, 59, 0.7);
+ color: #94a3b8;
+ cursor: pointer;
+ transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
+}
+
+.dashboard-stats-toggle:hover {
+ background: rgba(51, 65, 85, 0.78);
+ color: #e2e8f0;
+ border-color: rgba(71, 85, 105, 0.8);
+}
+
+.dashboard-stats-content {
+ 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;
+}
+
+.dashboard-stats-shell.is-collapsed .dashboard-stats-content {
+ display: none;
+}
+
+.dashboard-stat-card,
+.dashboard-chart-card,
+.dashboard-health-card {
+ position: relative;
+ min-width: 0;
+ border: 1px solid rgba(51, 65, 85, 0.68);
+ border-radius: 10px;
+ background: rgba(15, 23, 42, 0.72);
+ box-shadow: none;
+ overflow: hidden;
+}
+
+.dashboard-stat-card::before,
+.dashboard-chart-card::before,
+.dashboard-health-card::before {
+ display: none;
+}
+
+.dashboard-stat-card {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ min-height: 142px;
+ padding: 14px;
+}
+
+.dashboard-stat-card--online {
+ border-color: rgba(34, 211, 238, 0.22);
+}
+
+.dashboard-stat-card--sessions {
+ border-color: rgba(129, 140, 248, 0.24);
+}
+
+.dashboard-stat-topline,
+.dashboard-chart-kicker {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ color: #94a3b8;
+ font-size: 0.72rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.dashboard-stat-topline i {
+ color: #818cf8;
+}
+
+.dashboard-stat-value {
+ margin-top: 10px;
+ color: #f8fafc;
+ font-size: 2.4rem;
+ line-height: 1;
+ font-weight: 800;
+ font-variant-numeric: tabular-nums;
+}
+
+.dashboard-stat-subline {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-top: 12px;
+ color: #9db1cc;
+ font-size: 0.78rem;
+ font-variant-numeric: tabular-nums;
+}
+
+.dashboard-chart-card,
+.dashboard-health-card {
+ min-height: 142px;
+ padding: 12px 14px 10px;
+}
+
+.dashboard-chart-head {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 8px;
+}
+
+.dashboard-chart-head strong {
+ display: block;
+ margin-top: 2px;
+ color: #e2e8f0;
+ font-size: 0.94rem;
+ font-weight: 700;
+}
+
+.dashboard-chart-pill {
+ flex-shrink: 0;
+ max-width: 9rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border: 1px solid rgba(56, 189, 248, 0.22);
+ border-radius: 999px;
+ background: rgba(8, 47, 73, 0.32);
+ color: #bae6fd;
+ padding: 3px 8px;
+ font-size: 0.7rem;
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+}
+
+.dashboard-chart-wrap {
+ position: relative;
+ z-index: 1;
+ height: 88px;
+ min-width: 0;
+}
+
+.dashboard-chart-wrap--donut {
+ height: 92px;
+}
+
+.dashboard-health-list {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ gap: 7px;
+}
+
+.dashboard-health-row {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ gap: 8px;
+ min-height: 26px;
+ border: 1px solid rgba(100, 116, 139, 0.16);
+ border-radius: 8px;
+ background: rgba(15, 23, 42, 0.46);
+ padding: 5px 8px;
+ color: #9db1cc;
+ font-size: 0.78rem;
+}
+
+.dashboard-health-row strong {
+ color: #e2e8f0;
+ font-size: 0.78rem;
+ font-variant-numeric: tabular-nums;
+}
+
+.dashboard-live-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 999px;
+ background: #64748b;
+ box-shadow: 0 0 0 4px rgba(100, 116, 139, 0.12);
+}
+
+.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);
+}
+
+.dashboard-live-dot.is-warn {
+ background: #fbbf24;
+ box-shadow: 0 0 0 4px rgba(251, 191, 36, 0.12), 0 0 16px rgba(251, 191, 36, 0.3);
+}
+
+.dashboard-live-dot.is-bad {
+ background: #fb7185;
+ box-shadow: 0 0 0 4px rgba(251, 113, 133, 0.12), 0 0 16px rgba(251, 113, 133, 0.3);
+}
+
+.dashboard-live-dot.is-muted {
+ background: #64748b;
+ 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: 2rem;
+ }
+}
+
.dashboard-menu-panel,
.nav-dd-menu {
z-index: 40 !important;
diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js
index 5fafd7d..2722ca2 100644
--- a/Overlord-Server/public/assets/main.js
+++ b/Overlord-Server/public/assets/main.js
@@ -13,6 +13,7 @@ import {
markManualDisconnect,
} from "./data.js";
import { ThumbnailLoader } from "./thumbnail-loader.js";
+import { initDashboardStats, updateDashboardStatsFromClients } from "./dashboard-stats.js";
const grid = document.getElementById("grid");
const totalPill = document.getElementById("total-pill");
@@ -844,7 +845,11 @@ function initializeRenderer() {
getDisplayFields: () => displayFields,
});
rendererSetLayout = rSetLayout;
- registerRenderer(renderMerge);
+ registerRenderer((data, options) => {
+ renderMerge(data, options);
+ updateDashboardStatsFromClients(data);
+ });
+ initDashboardStats();
setupDashboardThumbnailLoader();
refreshGroupFilter();
loadWithOptions();
diff --git a/Overlord-Server/public/index.html b/Overlord-Server/public/index.html
index 6eeca0a..b025db9 100644
--- a/Overlord-Server/public/index.html
+++ b/Overlord-Server/public/index.html
@@ -16,6 +16,7 @@
+
@@ -38,6 +39,91 @@
>0 online
+