import { formatAgo, formatPing, countryToFlag, osBadge, archBadge, versionBadge, monitorsBadge, shortId, } from "./viewUtils.js"; import { applyImageSrcSmooth } from "./thumbnail-loader.js"; function wireThumbImg(img) { if (!(img instanceof HTMLImageElement) || img.dataset.thumbErrWired === "1") return; img.dataset.thumbErrWired = "1"; const handleError = () => { img.style.display = "none"; img.removeAttribute("src"); img.removeAttribute("data-thumb-url"); }; img.addEventListener("error", handleError); if (img.complete && img.src && !img.naturalWidth) handleError(); } if (typeof document !== "undefined" && typeof MutationObserver !== "undefined") { const observer = new MutationObserver((mutations) => { for (const m of mutations) { m.addedNodes.forEach((n) => { if (!(n instanceof HTMLElement)) return; if (n.matches?.("img[data-thumb-img]")) wireThumbImg(n); n.querySelectorAll?.("img[data-thumb-img]").forEach(wireThumbImg); }); } }); const start = () => { if (!document.body) return; observer.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll("img[data-thumb-img]").forEach(wireThumbImg); }; if (document.body) start(); else document.addEventListener("DOMContentLoaded", start, { once: true }); } function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } const ROW_SELECTOR = "[data-client-row]"; function pingTone(ms) { if (ms === null || ms === undefined) return "ping-unknown"; if (ms < 30) return "ping-good"; if (ms < 80) return "ping-mid"; return "ping-bad"; } function metaSeparator() { return ``; } const FAUX_PALETTES = [ ["#1e3a8a", "#0ea5e9", "#22d3ee"], ["#0f172a", "#475569", "#94a3b8"], ["#3b0764", "#a21caf", "#f472b6"], ["#064e3b", "#10b981", "#bbf7d0"], ["#7c2d12", "#ea580c", "#fdba74"], ["#082f49", "#0284c7", "#7dd3fc"], ]; function paletteFor(id) { const s = String(id || ""); let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return FAUX_PALETTES[h % FAUX_PALETTES.length]; } function fauxDesktopHtml(client, opts = {}) { const palette = paletteFor(client.id); const op = !client.online ? 0.3 : 1.0; const small = !!opts.small; const dotR = small ? 4 : 6; return `
`; } function thumbHtml(client, { width, height, small = false } = {}) { const hasThumb = !!client.hasThumbnail; const version = Number(client.thumbnailVersion) || 0; const opacityStyle = client.online ? "" : "opacity:0.35"; const idAttr = escapeHtml(client.id); const initialSrc = hasThumb ? `/api/clients/${encodeURIComponent(client.id)}/thumbnail${version ? `?v=${version}` : ""}` : ""; const initialDisplay = hasThumb ? "display:block" : "display:none"; return `
${fauxDesktopHtml(client, { small })}
`; } function statusDot(client) { return ``; } function groupPillHtml(client) { const name = String(client.groupName || "").trim(); if (!name) return ""; const color = String(client.groupColor || "").trim() || "#64748b"; return `${escapeHtml(name)}`; } function shortenCpu(raw = "") { return String(raw) .replace(/\(R\)|\(TM\)|\(tm\)|\(r\)/g, "") .replace(/\b(CPU|Processor|Genuine|Intel|AMD)\b/gi, (m) => (m.toUpperCase() === "INTEL" || m.toUpperCase() === "AMD" ? m : "")) .replace(/@\s*[\d.]+\s*GHz.*$/i, "") .replace(/\s+/g, " ") .trim() .slice(0, 38); } function cpuBrand(raw = "") { const text = String(raw).toLowerCase(); const appleMatch = text.match(/\bapple\s+(m\d+(?:\s*(?:pro|max|ultra))?)\b/i); if (appleMatch) { return { vendor: "apple", chip: appleMatch[1].replace(/\s+/g, " ").toUpperCase() }; } const intelMatch = text.match(/\b(?:intel(?:\(r\))?\s+)?(?:core\(tm\)\s+)?(i[3579]-\d{3,5}[a-z0-9]*)\b/i); if (text.includes("intel") || intelMatch) { return { vendor: "intel", chip: intelMatch ? intelMatch[1].toUpperCase() : "CPU" }; } const amdMatch = text.match(/\b(?:amd\s+)?(?:ryzen\s+)?([3579]\s+\d{4}[a-z0-9]*)\b/i); if (text.includes("amd") || text.includes("ryzen")) { return { vendor: "amd", chip: amdMatch ? amdMatch[1].replace(/\s+/g, " ").toUpperCase() : "RYZEN" }; } return null; } function cpuBadgeHtml(raw = "", { compact = false } = {}) { const brand = cpuBrand(raw); const label = shortenCpu(raw) || "CPU"; if (!brand) { return `${escapeHtml(label)}`; } if (brand.vendor === "apple") { return `${escapeHtml(brand.chip)}`; } if (brand.vendor === "intel") { return `intel${compact ? "" : `${escapeHtml(brand.chip)}`}`; } return `AMD${compact ? "" : `${escapeHtml(brand.chip)}`}`; } function batteryInfo(client) { const n = Number(client.batteryPercent); if (!Number.isFinite(n) || n <= 0 || n > 100) return null; return { percent: Math.round(n), charging: client.batteryCharging === true }; } function batteryHtml(client, { compact = false } = {}) { const battery = batteryInfo(client); if (!battery) return ""; const tone = battery.percent < 20 ? "is-low" : battery.percent <= 35 ? "is-mid" : "is-good"; const title = `Battery ${battery.percent}%${battery.charging ? " charging" : ""}`; return ` ${battery.percent}%${battery.charging ? ` ` : ""} ${compact ? "" : `${battery.charging ? "Charging" : "Battery"}`} `; } function shortOsLabel(osRaw = "") { const badge = osBadge(osRaw || ""); if (badge?.label && badge.label !== "Linux" && badge.label !== "Windows") return badge.label; const o = String(osRaw).toLowerCase(); const winVersion = o.match(/\bwindows\s+(10|11)\b/i); if (winVersion) return `W${winVersion[1]}`; if (o.includes("windows 11")) return "W11"; if (o.includes("windows 10")) return "W10"; if (o.includes("windows")) return "Win"; if (o.includes("ubuntu")) return "Ubuntu"; if (o.includes("debian")) return "Debian"; if (o.includes("arch")) return "Arch"; if (o.includes("kali")) return "Kali"; if (o.includes("fedora")) return "Fedora"; if (o.includes("mac") || o.includes("darwin")) return "macOS"; if (o.includes("linux")) return "Linux"; return osRaw || "?"; } export function createRenderer({ grid, totalPill, pageLabel, openMenu, openModal, requestPreview, requestThumbnail, pingClient, userRole, getDisplayFields, }) { const isViewer = userRole === "viewer"; const LARGE_CLIENT_THRESHOLD = 50_000; const MAX_ANIMATED_CARDS = 0; const INSERT_BATCH_SIZE = 40; const TOUCH_LONG_PRESS_MS = 520; const TOUCH_MOVE_CANCEL_PX = 10; let renderToken = 0; let gridDelegated = false; let currentLayout = (grid?.dataset.layout || "rows").toLowerCase(); let tableScaffoldDigest = ""; let currentDisplayDigest = ""; function displayFields() { return typeof getDisplayFields === "function" ? getDisplayFields() || {} : {}; } function showField(name) { return displayFields()[name] !== false; } function displayDigest() { return JSON.stringify(displayFields()); } function getCardContainer() { if (currentLayout === "table") { return grid.querySelector("tbody.clients-table-body") || grid; } return grid; } function ensureLayoutScaffold() { if (currentLayout === "table") { const prefs = displayFields(); const scaffoldDigest = displayDigest(); if (grid.querySelector("table.clients-table") && tableScaffoldDigest === scaffoldDigest) return; tableScaffoldDigest = scaffoldDigest; grid.innerHTML = ` ${prefs.system !== false ? `` : ""} ${prefs.group !== false ? `` : ""}
Client Status Last seenSystemPingGroupActions
`; } else { tableScaffoldDigest = ""; const t = grid.querySelector("table.clients-table"); if (t) t.remove(); } } function setLayout(layout) { const next = ["rows", "table", "cards"].includes(layout) ? layout : "rows"; if (currentLayout === next && grid.dataset.layout === next) return; currentLayout = next; grid.dataset.layout = next; grid.innerHTML = ""; ensureLayoutScaffold(); } /* ── Event delegation ─────────────────────────────────────────── */ function setupGridDelegation() { if (gridDelegated) return; gridDelegated = true; grid.addEventListener("click", (e) => { const card = e.target.closest(ROW_SELECTOR); if (!card) return; const clientId = card.dataset.id; if (e.target.closest(".client-checkbox")) { e.stopPropagation(); if (window.toggleClientSelection) window.toggleClientSelection(clientId); return; } const bookmarkBtn = e.target.closest(".bookmark-btn"); if (bookmarkBtn) { e.stopPropagation(); handleBookmarkClick(card, bookmarkBtn); return; } const copyBtn = e.target.closest(".copy-id-btn"); if (copyBtn) { e.stopPropagation(); const fullId = copyBtn.dataset.copy; if (fullId) { navigator.clipboard.writeText(fullId).then(() => { const icon = copyBtn.querySelector(".copy-id-icon"); if (icon) { icon.className = "fa-solid fa-check copy-id-icon"; setTimeout(() => { icon.className = "fa-regular fa-copy copy-id-icon"; }, 1200); } }).catch(() => {}); } return; } if (e.target.closest(".command-btn")) { e.stopPropagation(); const rect = e.target.closest(".command-btn").getBoundingClientRect(); openMenu(clientId, rect.right, rect.bottom); return; } if (e.target.closest(".ban-btn")) { e.stopPropagation(); if (window.banClient) window.banClient(clientId); return; } if (e.target.closest(".kebab-btn")) { e.stopPropagation(); const rect = e.target.closest(".kebab-btn").getBoundingClientRect(); openMenu(clientId, rect.right, rect.bottom); return; } if (e.target.closest(".cv-ping-btn")) { e.stopPropagation(); if (pingClient) pingClient(clientId); return; } if (e.target.closest(".client-tag-toggle")) { e.stopPropagation(); handleTagToggle(card); return; } if (e.target.closest(".hw-toggle")) { e.stopPropagation(); handleHwToggle(card); return; } if (e.target.closest(".cv-expand-btn")) { e.stopPropagation(); handleExpandToggle(card); return; } const thumbImg = e.target.closest(".thumb-img"); if (thumbImg) { if (thumbImg.src) openModal(thumbImg.src); return; } if (card._longPressTriggered) { card._longPressTriggered = false; e.preventDefault(); e.stopPropagation(); return; } if (e.target.closest("button")) return; if (e.target.closest(".client-checkbox")) return; const checkbox = card.querySelector(".client-checkbox"); if (e.ctrlKey || e.metaKey || e.shiftKey) { if (checkbox && !checkbox.disabled) { checkbox.checked = !checkbox.checked; if (window.toggleClientSelection) window.toggleClientSelection(clientId); } return; } if (card.dataset.online !== "true") return; if (pingClient) pingClient(clientId); requestThumbnail(clientId); }); grid.addEventListener("contextmenu", (e) => { const card = e.target.closest(ROW_SELECTOR); if (!card || isViewer) return; e.preventDefault(); openMenu(card.dataset.id, e.clientX, e.clientY); }); grid.addEventListener("pointerdown", (e) => { if (isViewer || e.pointerType !== "touch") return; if (e.target.closest("button") || e.target.closest(".client-checkbox")) return; const card = e.target.closest(ROW_SELECTOR); if (!card) return; card._longPressTriggered = false; card._pointerStartX = e.clientX; card._pointerStartY = e.clientY; clearTimeout(card._longPressTimer); card._longPressTimer = setTimeout(() => { card._longPressTriggered = true; openMenu(card.dataset.id, e.clientX, e.clientY); }, TOUCH_LONG_PRESS_MS); }); grid.addEventListener("pointermove", (e) => { if (e.pointerType !== "touch") return; const card = e.target.closest(ROW_SELECTOR); if (!card || !card._longPressTimer) return; if ( Math.abs(e.clientX - card._pointerStartX) > TOUCH_MOVE_CANCEL_PX || Math.abs(e.clientY - card._pointerStartY) > TOUCH_MOVE_CANCEL_PX ) { clearTimeout(card._longPressTimer); card._longPressTimer = null; } }); const clearLongPress = (e) => { if (e.pointerType !== "touch") return; const card = e.target.closest(ROW_SELECTOR); if (card) { clearTimeout(card._longPressTimer); card._longPressTimer = null; } }; grid.addEventListener("pointerup", clearLongPress); grid.addEventListener("pointercancel", clearLongPress); grid.addEventListener("pointerleave", clearLongPress, true); } async function handleBookmarkClick(card, btn) { const id = card.dataset.id; const isBookmarked = card.dataset.bookmarked === "true"; try { const res = await fetch(`/api/clients/${id}/bookmark`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ bookmarked: !isBookmarked }), }); if (res.ok) { card.dataset.bookmarked = String(!isBookmarked); const icon = btn.querySelector("i"); if (icon) { icon.classList.toggle("fa-solid", !isBookmarked); icon.classList.toggle("fa-regular", isBookmarked); } btn.classList.toggle("is-on", !isBookmarked); btn.title = !isBookmarked ? "Remove bookmark" : "Bookmark"; } } catch (err) { console.error("bookmark toggle failed", err); } } function handleTagToggle(card) { const notePanel = card.querySelector(".client-tag-note"); if (!notePanel) return; const expanded = notePanel.classList.toggle("hidden") === false; card.dataset.tagNoteExpanded = expanded ? "true" : "false"; const tagToggle = card.querySelector(".client-tag-toggle"); tagToggle?.setAttribute("aria-expanded", expanded ? "true" : "false"); const chevron = tagToggle?.querySelector(".fa-chevron-up, .fa-chevron-down"); if (chevron) { chevron.classList.toggle("fa-chevron-up", expanded); chevron.classList.toggle("fa-chevron-down", !expanded); } } function handleHwToggle(card) { 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); } } function handleExpandToggle(card) { const panel = card.querySelector(".cv-expand-panel"); if (!panel) return; const expanded = panel.classList.toggle("hidden") === false; card.dataset.expanded = expanded ? "true" : "false"; const btn = card.querySelector(".cv-expand-btn"); btn?.setAttribute("aria-expanded", expanded ? "true" : "false"); } function reorderCards(items) { const container = getCardContainer(); const cards = container.querySelectorAll(ROW_SELECTOR); let needsReorder = cards.length !== items.length; if (!needsReorder) { for (let i = 0; i < items.length; i++) { if (cards[i]?.dataset?.id !== items[i].id) { needsReorder = true; break; } } } if (!needsReorder) return; const byId = new Map(); cards.forEach((card) => byId.set(card.dataset.id, card)); items.forEach((client) => { const card = byId.get(client.id); if (card) container.appendChild(card); }); } function cardDigest(c) { return `${currentDisplayDigest}|${currentLayout}|${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${c.cpu}|${c.gpu}|${c.ram}|${c.batteryPercent}|${c.batteryCharging}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${c.groupId}|${c.groupName}|${c.groupColor}|${!!c.notificationsMuted}`; } function cardThumbDigest(c) { return `${!!c.hasThumbnail}|${Number(c.thumbnailVersion) || 0}`; } function softThumbUpdate(card, client) { const host = card.querySelector("[data-thumb-host]"); if (!host) return; const newVersion = Number(client.thumbnailVersion) || 0; host.dataset.thumbVersion = String(newVersion); host.dataset.thumbOnline = client.online ? "1" : "0"; if (!client.hasThumbnail) return; const img = host.querySelector("img[data-thumb-img]"); if (!img) return; const url = `/api/clients/${encodeURIComponent(client.id)}/thumbnail${newVersion ? `?v=${newVersion}` : ""}`; applyImageSrcSmooth(img, url); } function renderMerge(data, options = {}) { setupGridDelegation(); currentDisplayDigest = displayDigest(); ensureLayoutScaffold(); const container = getCardContainer(); const { reorder = false } = options; totalPill.textContent = `${data.online ?? data.total} online / ${data.total} total`; const totalPages = Math.max(1, Math.ceil(data.total / data.pageSize)); if (pageLabel) pageLabel.textContent = "Page"; if (typeof window.setDashboardPageBounds === "function") { window.setDashboardPageBounds(data.page, totalPages); } prevBtnState(data.page, totalPages); const items = data.items || []; const seen = new Set(); const newClients = []; const renderId = ++renderToken; const largeClientSet = (Number(data.total) || 0) >= LARGE_CLIENT_THRESHOLD; document.documentElement.classList.toggle("large-client-set", largeClientSet); const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const existingCards = container.querySelectorAll(ROW_SELECTOR); const hadCards = existingCards.length > 0; const existingById = new Map(); existingCards.forEach((card) => existingById.set(card.dataset.id, card)); const uninstallingIds = window.__uninstallingClientIds; items.forEach((client) => { seen.add(client.id); if (uninstallingIds && uninstallingIds.has(client.id)) return; const existing = existingById.get(client.id); if (existing) { if (existing.classList.contains("card-uninstalling")) return; const digest = cardDigest(client); const thumbDigest = cardThumbDigest(client); const structureChanged = existing._cardDigest !== digest; const thumbChanged = existing._thumbDigest !== thumbDigest; if (!structureChanged && !thumbChanged) return; if (structureChanged) { existing._cardDigest = digest; existing._thumbDigest = thumbDigest; updateCard(existing, client); } else { existing._thumbDigest = thumbDigest; softThumbUpdate(existing, client); } return; } newClients.push(client); }); Array.from(existingCards) .filter((el) => !seen.has(el.dataset.id) && !el.classList.contains("card-uninstalling")) .forEach((el) => el.remove()); if (reorder) { reorderCards(items); } if (newClients.length === 0) return; const allowAnimation = MAX_ANIMATED_CARDS > 0 && !largeClientSet && !hadCards && !prefersReducedMotion && items.length <= 1000 && currentLayout !== "table"; const animateLimit = Math.min(newClients.length, MAX_ANIMATED_CARDS); let idx = 0; const insertBatch = () => { if (renderId !== renderToken) return; const fragment = document.createDocumentFragment(); for ( let batch = 0; batch < INSERT_BATCH_SIZE && idx < newClients.length; batch++, idx++ ) { const client = newClients[idx]; const shouldAnimate = allowAnimation && idx < animateLimit; const card = buildCard(client, { animate: shouldAnimate, delayIndex: idx, }); fragment.appendChild(card); } container.appendChild(fragment); if (idx < newClients.length) { requestAnimationFrame(insertBatch); return; } reorderCards(items); }; insertBatch(); } function prevBtnState(currentPage, totalPages) { const prevBtn = document.getElementById("prev"); const nextBtn = document.getElementById("next"); if (prevBtn) prevBtn.disabled = currentPage <= 1; if (nextBtn) nextBtn.disabled = currentPage >= totalPages; } function setSharedDataset(el, client) { el.dataset.clientRow = currentLayout; el.dataset.id = client.id; el.dataset.hwid = client.hwid || ""; el.dataset.online = String(!!client.online); el.dataset.os = String(client.os || "").toLowerCase(); el.dataset.nickname = String(client.nickname || ""); el.dataset.customTag = String(client.customTag || ""); el.dataset.bookmarked = String(!!client.bookmarked); el.dataset.notificationsMuted = String(!!client.notificationsMuted); el.dataset.admin = String(!!client.isAdmin); el.dataset.groupId = String(client.groupId || ""); el.dataset.groupName = String(client.groupName || ""); el.dataset.groupColor = String(client.groupColor || ""); el._customTagNote = String(client.customTagNote || ""); if (!client.bookmarked && client.groupColor) { el.style.setProperty("--group-color", client.groupColor); } else { el.style.removeProperty("--group-color"); } } function buildCard(client, options = {}) { const node = currentLayout === "table" ? buildRowB(client, options) : currentLayout === "cards" ? buildCardC(client, options) : buildRowA(client, options); node._cardDigest = cardDigest(client); node._thumbDigest = cardThumbDigest(client); if (options.animate && currentLayout !== "table") { node.classList.add("card-animate"); node.style.animationDelay = `${(options.delayIndex || 0) * 0.05}s`; node.style.opacity = "0"; node.style.transform = "translateY(10px)"; if (typeof anime !== "undefined") { requestAnimationFrame(() => { anime({ targets: node, opacity: [0, 1], translateY: [10, 0], duration: 400, easing: "easeOutQuad", }); }); } } return node; } function updateCard(card, client) { const wasChecked = card.querySelector(".client-checkbox")?.checked || false; const wasTagNoteExpanded = card.dataset.tagNoteExpanded === "true"; const wasHwExpanded = card.dataset.hwExpanded === "true"; const wasExpanded = card.dataset.expanded === "true"; const fresh = buildCard(client, { animate: false }); fresh.dataset.tagNoteExpanded = wasTagNoteExpanded ? "true" : "false"; fresh.dataset.hwExpanded = wasHwExpanded ? "true" : "false"; fresh.dataset.expanded = wasExpanded ? "true" : "false"; if (wasTagNoteExpanded) fresh.querySelector(".client-tag-note")?.classList.remove("hidden"); if (wasHwExpanded) fresh.querySelector(".hw-panel")?.classList.remove("hidden"); if (wasExpanded) fresh.querySelector(".cv-expand-panel")?.classList.remove("hidden"); const cb = fresh.querySelector(".client-checkbox"); if (cb) { const isSelected = typeof window.isClientSelected === "function" ? window.isClientSelected(client.id) : false; if ((wasChecked || isSelected) && client.online) cb.checked = true; } card.replaceWith(fresh); } /* ── VARIANT A: Row cards ─────────────────────────────────────── */ function buildRowA(client, _options) { const article = document.createElement("article"); article.className = `cv-row ${client.online ? "" : "cv-offline"} ${client.bookmarked ? "is-bookmarked" : ""}`; setSharedDataset(article, client); const os = osBadge(client.os || "unknown"); const arch = archBadge(client.arch || ""); const ver = versionBadge(client.version || ""); const mons = monitorsBadge(client.monitors); const deviceId = shortId(client.id); const hwid = shortId(client.hwid || ""); const nickname = String(client.nickname || "").trim(); const customTag = String(client.customTag || "").trim(); const customTagNote = String(client.customTagNote || ""); const displayName = nickname || client.host || deviceId; const userLine = client.user || client.host || deviceId; const hasTagNote = customTag.length > 0 && customTagNote.length > 0; const showHardware = showField("hardware"); const showBattery = showField("battery"); const cpuHtml = client.cpu ? cpuBadgeHtml(client.cpu) : "—"; const batteryIndicator = showBattery ? batteryHtml(client) : ""; const verLatest = String(client.version || "").startsWith("2.0."); const metaParts = [ showField("system") ? ` ${escapeHtml(shortOsLabel(client.os))}` : "", showField("system") ? `${escapeHtml(arch.label)}` : "", showField("version") ? ` ${escapeHtml(ver.label)}` : "", showField("monitors") ? ` ${client.monitors || 1}` : "", nickname && client.host && nickname !== client.host ? ` ${escapeHtml(client.host)}` : "", showField("ip") && client.ip ? ` ${escapeHtml(client.ip)}` : "", showField("hwid") && hwid ? ` ${escapeHtml(hwid)}` : "", batteryIndicator, ].filter(Boolean); const meta = metaParts.join(metaSeparator()); article.innerHTML = `
${thumbHtml(client, { width: 168, height: 96, small: false })}
${statusDot(client)} ${countryToFlag(client.country)} ${escapeHtml(displayName)} ${client.isAdmin ? `` : ""} ${client.elevation === "system" ? `` : ""} ${client.elevation === "trustedinstaller" ? `` : ""} ${client.notificationsMuted ? `` : ""}
${showField("user") ? `
${escapeHtml(userLine)}
` : ""} ${meta ? `
${meta}
` : ""} ${customTag ? `` : ""} ${hasTagNote ? `` : ""}
${formatAgo(client.lastSeen)} ${formatPing(client.pingMs)}
${showField("group") ? `
${groupPillHtml(client) || `—`}
` : ""}
${isViewer ? "" : ``} ${isViewer ? "" : ``}
`; return article; } /* ── VARIANT B: Dense table ───────────────────────────────────── */ function buildRowB(client, _options) { const tr = document.createElement("tr"); tr.className = `cv-trow ${client.online ? "" : "cv-offline"} ${client.bookmarked ? "is-bookmarked" : ""}`; setSharedDataset(tr, client); const os = osBadge(client.os || "unknown"); const arch = archBadge(client.arch || ""); const deviceId = shortId(client.id); const nickname = String(client.nickname || "").trim(); const displayName = nickname || client.host || deviceId; const userLine = client.user || client.host || deviceId; const userMeta = [ showField("user") ? ` ${escapeHtml(userLine)}` : "", showField("ip") && client.ip ? `· ${escapeHtml(client.ip)}` : "", showField("hwid") && client.hwid ? `· ${escapeHtml(shortId(client.hwid))}` : "", ].filter(Boolean).join(""); tr.innerHTML = ` ${thumbHtml(client, { width: 80, height: 50, small: true })}
${countryToFlag(client.country)}
${escapeHtml(displayName)} ${client.isAdmin ? `` : ""} ${client.elevation === "system" ? `` : ""} ${client.elevation === "trustedinstaller" ? `` : ""} ${client.notificationsMuted ? `` : ""} ${userMeta ? `${userMeta}` : ""}
${statusDot(client)} ${client.online ? "Online" : "Offline"} ${formatAgo(client.lastSeen)} ${showField("system") ? ` ${escapeHtml(shortOsLabel(client.os))} ${escapeHtml(arch.label)} ` : ""} ${formatPing(client.pingMs)} ${showField("group") ? `${groupPillHtml(client) || `—`}` : ""}
${isViewer ? "" : ``} ${isViewer ? "" : ``}
`; return tr; } /* ── VARIANT C: Card wall ─────────────────────────────────────── */ function buildCardC(client, _options) { const article = document.createElement("article"); article.className = `cv-card ${client.online ? "" : "cv-offline"} ${client.bookmarked ? "is-bookmarked" : ""}`; setSharedDataset(article, client); const os = osBadge(client.os || "unknown"); const arch = archBadge(client.arch || ""); const ver = versionBadge(client.version || ""); const deviceId = shortId(client.id); const hwidShort = shortId(client.hwid || ""); const nickname = String(client.nickname || "").trim(); const customTag = String(client.customTag || "").trim(); const displayName = nickname || client.host || deviceId; const userLine = client.user || "unknown"; const verLatest = String(client.version || "").startsWith("2.0."); const hasGroup = !!String(client.groupName || "").trim(); const showHost = nickname && client.host && nickname !== client.host; const showHardware = showField("hardware"); const cpuHtml = showHardware && client.cpu ? cpuBadgeHtml(client.cpu) : ""; const batteryIndicator = showField("battery") ? batteryHtml(client, { compact: true }) : ""; const metaParts = [ showField("system") ? ` ${escapeHtml(shortOsLabel(client.os))}` : "", showField("system") ? `${escapeHtml(arch.label)}` : "", showField("monitors") ? ` ${client.monitors || 1}` : "", showField("version") && client.version ? ` v${escapeHtml(client.version)}` : "", ].filter(Boolean).join(""); const elevationBadges = [ client.isAdmin ? `` : "", client.elevation === "system" ? `` : "", client.elevation === "trustedinstaller" ? `` : "", client.notificationsMuted ? `` : "", ].join(""); article.innerHTML = `
${thumbHtml(client, { width: 290, height: 130, small: false })}
${isViewer ? "" : ``}
${statusDot(client)} ${client.online ? "Online" : "Offline"} · ${formatAgo(client.lastSeen)}
${formatPing(client.pingMs)}
${countryToFlag(client.country)} ${escapeHtml(displayName)} ${elevationBadges}
${showField("user") ? `
${escapeHtml(userLine)}${showHost ? ` @ ${escapeHtml(client.host)}` : ""}
` : ""} ${customTag ? `
${escapeHtml(customTag)}
` : ""}
${showField("ip") && client.ip ? ` ${escapeHtml(client.ip)}` : ""} ${showField("hwid") && hwidShort ? ` ${escapeHtml(hwidShort)}` : ""}
${metaParts || (showField("group") && hasGroup) ? `
${metaParts} ${showField("group") && hasGroup ? `${groupPillHtml(client)}` : ""}
` : ""} ${cpuHtml || (showHardware && client.ram) || batteryIndicator ? `
${cpuHtml} ${showHardware && client.ram ? ` ${escapeHtml(client.ram)}` : ""} ${batteryIndicator}
` : ""}
${isViewer ? "" : ``} ${isViewer ? "" : ``}
`; return article; } return { renderMerge, setLayout }; }