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));