show hardware info on clients
This commit is contained in:
@@ -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">×${n}</span>` : esc(name)).join(", ");
|
||||
}
|
||||
|
||||
function expandableCell(clientId, field, value) {
|
||||
const text = value || "-";
|
||||
if (text === "-" || text.length <= 24) {
|
||||
|
||||
@@ -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">×${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));
|
||||
|
||||
Reference in New Issue
Block a user