show hardware info on clients

This commit is contained in:
KingKDot
2026-04-03 20:24:34 -05:00
parent 1d85c89d77
commit 48be7748d2
3 changed files with 115 additions and 1 deletions
+73
View File
@@ -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;
@@ -110,7 +110,7 @@ async function loadClients() {
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.user || "-")}</td>
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.os || "-")}</td>
<td class="px-4 py-3 text-sm text-slate-400">${expandableCell(c.id, "cpu", c.cpu)}</td>
<td class="px-4 py-3 text-sm text-slate-400">${expandableCell(c.id, "gpu", c.gpu)}</td>
<td class="px-4 py-3 text-sm text-slate-400">${c.gpu ? dedupeGpu(c.gpu) : esc("-")}</td>
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.ram || "-")}</td>
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.ip || "-")}</td>
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.country || "-")}</td>
@@ -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)} <span class="hw-gpu-count">&times;${n}</span>` : esc(name)).join(", ");
}
function expandableCell(clientId, field, value) {
const text = value || "-";
if (text === "-" || text.length <= 24) {
+34
View File
@@ -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} <span class="hw-gpu-count">&times;${n}</span>` : 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 ? `<div class="client-tag-note rounded-lg border border-amber-900/60 bg-amber-950/20 px-3 py-2 text-sm text-amber-100 whitespace-pre-wrap break-words max-h-48 overflow-auto ${isTagNoteExpanded ? "" : "hidden"}">${escapeHtml(customTagNote)}</div>` : ""}
<div class="flex items-center gap-2 flex-wrap text-sm text-slate-300">
<span class="pill pill-ghost"><i class="fa-regular fa-clock"></i> ${formatAgo(client.lastSeen)}</span>
${hasHwInfo ? `<button type="button" class="hw-toggle pill pill-hw cursor-pointer" aria-expanded="${isHwExpanded ? "true" : "false"}"><i class="fa-solid fa-microchip"></i> Hardware <i class="fa-solid ${isHwExpanded ? "fa-chevron-up" : "fa-chevron-down"}"></i></button>` : ""}
<span class="pill ${os.tone}"><i class="fa ${os.icon}"></i> ${os.label}</span>
<span class="pill ${arch.tone}"><i class="fa ${arch.icon}"></i> ${arch.label}</span>
<span class="pill ${ver.tone}"><i class="fa ${ver.icon}"></i> ${ver.label}</span>
<span class="pill ${mons.tone}"><i class="fa ${mons.icon}"></i> ${mons.label}</span>
</div>
${hasHwInfo ? `<div class="hw-panel rounded-lg border border-indigo-900/50 bg-indigo-950/20 px-3 py-2 text-sm text-slate-200 ${isHwExpanded ? "" : "hidden"}">
<div class="hw-panel-grid">
${client.cpu ? `<div class="hw-row"><span class="hw-label hw-label-cpu"><i class="fa-solid fa-microchip"></i> CPU</span><span class="hw-value">${escapeHtml(client.cpu)}</span></div>` : ""}
${gpuHtml ? `<div class="hw-row"><span class="hw-label hw-label-gpu"><i class="fa-solid fa-display"></i> GPU</span><span class="hw-value">${gpuHtml}</span></div>` : ""}
${client.ram ? `<div class="hw-row"><span class="hw-label hw-label-ram"><i class="fa-solid fa-memory"></i> RAM</span><span class="hw-value">${escapeHtml(client.ram)}</span></div>` : ""}
</div>
</div>` : ""}
<div class="flex items-center gap-2 flex-wrap text-xs text-slate-400 font-mono">
<span class="pill pill-ghost">ID ${deviceId}</span>
${client.hwid ? `<span class="pill pill-ghost">HW ${hwid}</span>` : ""}
@@ -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));