show hardware info on clients
This commit is contained in:
@@ -429,6 +429,79 @@ p {
|
|||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
border-color: rgba(71, 85, 105, 0.55);
|
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 {
|
.card::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
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.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">${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, "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.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.ip || "-")}</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.country || "-")}</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;
|
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) {
|
function expandableCell(clientId, field, value) {
|
||||||
const text = value || "-";
|
const text = value || "-";
|
||||||
if (text === "-" || text.length <= 24) {
|
if (text === "-" || text.length <= 24) {
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ export function createRenderer({
|
|||||||
const oldCheckbox = card.querySelector(".client-checkbox");
|
const oldCheckbox = card.querySelector(".client-checkbox");
|
||||||
const wasChecked = oldCheckbox?.checked || false;
|
const wasChecked = oldCheckbox?.checked || false;
|
||||||
const wasTagNoteExpanded = card.dataset.tagNoteExpanded === "true";
|
const wasTagNoteExpanded = card.dataset.tagNoteExpanded === "true";
|
||||||
|
const wasHwExpanded = card.dataset.hwExpanded === "true";
|
||||||
|
|
||||||
card.dataset.online = String(!!client.online);
|
card.dataset.online = String(!!client.online);
|
||||||
card.dataset.os = String(client.os || "").toLowerCase();
|
card.dataset.os = String(client.os || "").toLowerCase();
|
||||||
@@ -218,6 +219,16 @@ export function createRenderer({
|
|||||||
const hasTagNote = customTag.length > 0 && customTagNote.length > 0;
|
const hasTagNote = customTag.length > 0 && customTagNote.length > 0;
|
||||||
const isTagNoteExpanded = hasTagNote && wasTagNoteExpanded;
|
const isTagNoteExpanded = hasTagNote && wasTagNoteExpanded;
|
||||||
card.dataset.tagNoteExpanded = isTagNoteExpanded ? "true" : "false";
|
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}`;
|
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
|
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>` : ""}
|
${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">
|
<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>
|
<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 ${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 ${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 ${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>
|
<span class="pill ${mons.tone}"><i class="fa ${mons.icon}"></i> ${mons.label}</span>
|
||||||
</div>
|
</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">
|
<div class="flex items-center gap-2 flex-wrap text-xs text-slate-400 font-mono">
|
||||||
<span class="pill pill-ghost">ID ${deviceId}</span>
|
<span class="pill pill-ghost">ID ${deviceId}</span>
|
||||||
${client.hwid ? `<span class="pill pill-ghost">HW ${hwid}</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
|
card
|
||||||
.querySelector(".thumb-img")
|
.querySelector(".thumb-img")
|
||||||
?.addEventListener("click", () => openModal(client.thumbnail));
|
?.addEventListener("click", () => openModal(client.thumbnail));
|
||||||
|
|||||||
Reference in New Issue
Block a user