diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css
index 09ca346..f83dc50 100644
--- a/Overlord-Server/public/assets/main.css
+++ b/Overlord-Server/public/assets/main.css
@@ -429,6 +429,79 @@ p {
color: #94a3b8;
border-color: rgba(71, 85, 105, 0.55);
}
+
+/* Hardware info toggle & panel */
+.pill-hw {
+ background: rgba(99, 102, 241, 0.14);
+ color: #c7d2fe;
+ border-color: rgba(99, 102, 241, 0.3);
+ transition: background 0.15s ease;
+}
+.pill-hw:hover {
+ background: rgba(99, 102, 241, 0.25);
+}
+.hw-panel {
+ margin-top: 4px;
+ background: linear-gradient(135deg, rgba(30, 35, 60, 0.55), rgba(20, 24, 44, 0.65));
+ border-color: rgba(99, 102, 241, 0.18);
+ backdrop-filter: blur(4px);
+ padding: 10px 14px;
+}
+.hw-panel-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.hw-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 4px 8px;
+ border-radius: var(--radius-sm);
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.04);
+}
+.hw-row:hover {
+ background: rgba(255, 255, 255, 0.06);
+}
+.hw-label {
+ flex-shrink: 0;
+ width: 56px;
+ font-size: var(--text-xs);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+.hw-label i {
+ font-size: 12px;
+ width: 14px;
+ text-align: center;
+}
+.hw-label-cpu { color: #93c5fd; }
+.hw-label-cpu i { color: #60a5fa; }
+.hw-label-gpu { color: #c4b5fd; }
+.hw-label-gpu i { color: #a78bfa; }
+.hw-label-ram { color: #6ee7b7; }
+.hw-label-ram i { color: #34d399; }
+.hw-value {
+ font-size: var(--text-xs);
+ color: var(--color-text);
+ word-break: break-word;
+}
+.hw-gpu-count {
+ font-size: 0.65rem;
+ padding: 1px 5px;
+ border-radius: var(--radius-full);
+ background: rgba(167, 139, 250, 0.18);
+ color: #c4b5fd;
+ border: 1px solid rgba(167, 139, 250, 0.25);
+ margin-left: 4px;
+ font-weight: 600;
+}
+
.card::before {
content: "";
position: absolute;
diff --git a/Overlord-Server/public/assets/purgatory-ui.js b/Overlord-Server/public/assets/purgatory-ui.js
index 53cee49..cf7c59f 100644
--- a/Overlord-Server/public/assets/purgatory-ui.js
+++ b/Overlord-Server/public/assets/purgatory-ui.js
@@ -110,7 +110,7 @@ async function loadClients() {
${esc(c.user || "-")} |
${esc(c.os || "-")} |
${expandableCell(c.id, "cpu", c.cpu)} |
- ${expandableCell(c.id, "gpu", c.gpu)} |
+ ${c.gpu ? dedupeGpu(c.gpu) : esc("-")} |
${esc(c.ram || "-")} |
${esc(c.ip || "-")} |
${esc(c.country || "-")} |
@@ -160,6 +160,13 @@ function esc(s) {
return d.innerHTML;
}
+function dedupeGpu(raw) {
+ if (!raw) return null;
+ const counts = new Map();
+ raw.split(",").map(s => s.trim()).filter(Boolean).forEach(g => counts.set(g, (counts.get(g) || 0) + 1));
+ return [...counts.entries()].map(([name, n]) => n > 1 ? `${esc(name)} ×${n}` : esc(name)).join(", ");
+}
+
function expandableCell(clientId, field, value) {
const text = value || "-";
if (text === "-" || text.length <= 24) {
diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js
index 02298b8..726ba13 100644
--- a/Overlord-Server/public/assets/render.js
+++ b/Overlord-Server/public/assets/render.js
@@ -198,6 +198,7 @@ export function createRenderer({
const oldCheckbox = card.querySelector(".client-checkbox");
const wasChecked = oldCheckbox?.checked || false;
const wasTagNoteExpanded = card.dataset.tagNoteExpanded === "true";
+ const wasHwExpanded = card.dataset.hwExpanded === "true";
card.dataset.online = String(!!client.online);
card.dataset.os = String(client.os || "").toLowerCase();
@@ -218,6 +219,16 @@ export function createRenderer({
const hasTagNote = customTag.length > 0 && customTagNote.length > 0;
const isTagNoteExpanded = hasTagNote && wasTagNoteExpanded;
card.dataset.tagNoteExpanded = isTagNoteExpanded ? "true" : "false";
+ const hasHwInfo = !!(client.cpu || client.gpu || client.ram);
+ const isHwExpanded = hasHwInfo && wasHwExpanded;
+ card.dataset.hwExpanded = isHwExpanded ? "true" : "false";
+ const dedupeGpu = (raw) => {
+ if (!raw) return null;
+ const counts = new Map();
+ raw.split(",").map(s => s.trim()).filter(Boolean).forEach(g => counts.set(g, (counts.get(g) || 0) + 1));
+ return [...counts.entries()].map(([name, n]) => n > 1 ? `${name} ×${n}` : escapeHtml(name)).join(", ");
+ };
+ const gpuHtml = dedupeGpu(client.gpu);
card.className = `card rounded-xl border ${client.bookmarked ? "border-yellow-600/60" : "border-slate-800"} bg-slate-900/70 p-4 shadow-lg ${client.online ? "" : "card-offline"} tone-${os.tone}`;
const cardThumb = client.thumbnail
? (() => {
@@ -259,11 +270,19 @@ export function createRenderer({
${hasTagNote ? `${escapeHtml(customTagNote)}
` : ""}
${formatAgo(client.lastSeen)}
+ ${hasHwInfo ? `` : ""}
${os.label}
${arch.label}
${ver.label}
${mons.label}
+ ${hasHwInfo ? `
+
+ ${client.cpu ? `
CPU${escapeHtml(client.cpu)}
` : ""}
+ ${gpuHtml ? `
GPU${gpuHtml}
` : ""}
+ ${client.ram ? `
RAM${escapeHtml(client.ram)}
` : ""}
+
+
` : ""}
ID ${deviceId}
${client.hwid ? `HW ${hwid}` : ""}
@@ -366,6 +385,21 @@ export function createRenderer({
}
});
+ card.querySelector(".hw-toggle")?.addEventListener("click", (e) => {
+ e.stopPropagation();
+ const hwPanel = card.querySelector(".hw-panel");
+ if (!hwPanel) return;
+ const expanded = hwPanel.classList.toggle("hidden") === false;
+ card.dataset.hwExpanded = expanded ? "true" : "false";
+ const hwToggle = card.querySelector(".hw-toggle");
+ hwToggle?.setAttribute("aria-expanded", expanded ? "true" : "false");
+ const chevron = hwToggle?.querySelector(".fa-chevron-up, .fa-chevron-down");
+ if (chevron) {
+ chevron.classList.toggle("fa-chevron-up", expanded);
+ chevron.classList.toggle("fa-chevron-down", !expanded);
+ }
+ });
+
card
.querySelector(".thumb-img")
?.addEventListener("click", () => openModal(client.thumbnail));