From 2353c40832665d74c62e3c39dc0ea8d77bd03a21 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sat, 9 May 2026 20:47:44 -0500 Subject: [PATCH] new dashboard UI --- .../cmd/agent/handlers/screenshot.go | 2 +- Overlord-Server/public/assets/data.js | 12 +- Overlord-Server/public/assets/main.css | 860 ++++++++++++++++++ Overlord-Server/public/assets/main.js | 35 +- Overlord-Server/public/assets/render.js | 640 +++++++++---- Overlord-Server/public/index.html | 36 +- Overlord-Server/src/thumbnails.ts | 6 +- 7 files changed, 1399 insertions(+), 192 deletions(-) diff --git a/Overlord-Client/cmd/agent/handlers/screenshot.go b/Overlord-Client/cmd/agent/handlers/screenshot.go index f9a2f0e..586377d 100644 --- a/Overlord-Client/cmd/agent/handlers/screenshot.go +++ b/Overlord-Client/cmd/agent/handlers/screenshot.go @@ -58,7 +58,7 @@ func HandleScreenshot(ctx context.Context, env *rt.Env, cmdID string, allDisplay } var buf bytes.Buffer - opts := &jpeg.Options{Quality: 85} + opts := &jpeg.Options{Quality: 92} if err := jpeg.Encode(&buf, img, opts); err != nil { log.Printf("screenshot: jpeg encode failed: %v", err) return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ diff --git a/Overlord-Server/public/assets/data.js b/Overlord-Server/public/assets/data.js index 95e6983..bad3848 100644 --- a/Overlord-Server/public/assets/data.js +++ b/Overlord-Server/public/assets/data.js @@ -21,20 +21,22 @@ function moveClientCardImmediately(msg) { ? CSS.escape(clientId) : clientId; const grid = document.getElementById("grid"); - const card = grid?.querySelector(`article[data-id="${selectorId}"]`); + const card = grid?.querySelector(`[data-client-row][data-id="${selectorId}"]`); if (!grid || !card) return; + const parent = card.parentNode || grid; + if (msg.event === "client_online") { card.dataset.online = "true"; - card.classList.remove("card-offline"); - grid.prepend(card); + card.classList.remove("card-offline", "cv-offline"); + parent.prepend(card); return; } if (msg.event === "client_offline") { card.dataset.online = "false"; - card.classList.add("card-offline"); - grid.appendChild(card); + card.classList.add("card-offline", "cv-offline"); + parent.appendChild(card); } } diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 21ce985..cc00b53 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -2079,6 +2079,866 @@ tbody tr:hover { } } +/* ============================================================ + CLIENT VIEWS — Variants A (rows) / B (table) / C (card wall) + ============================================================ */ + +:root { + --cv-bg: #13161f; + --cv-bg-selected: #1a2238; + --cv-bg-hover: rgba(255, 255, 255, 0.03); + --cv-surface2: #1b2030; + --cv-border: rgba(148, 163, 184, 0.16); + --cv-border-strong: rgba(148, 163, 184, 0.32); + --cv-text: #e8edf2; + --cv-text-mute: #9aa6bd; + --cv-text-dim: #6b7488; + --cv-accent: #818cf8; + --cv-accent-bg: rgba(99, 102, 241, 0.18); + --cv-accent-border: rgba(129, 140, 248, 0.55); + --cv-danger: #ef9a9a; + --cv-warn: #fbbf24; +} + +/* ── Layout toggle (segmented control) ─────────────────────── */ + +#layout-toggle { + gap: 2px; +} +.layout-toggle-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 6px; + border: 0; + background: transparent; + color: var(--cv-text-dim); + cursor: pointer; + transition: background-color 140ms ease, color 140ms ease; +} +.layout-toggle-btn:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--cv-text); +} +.layout-toggle-btn.is-active { + background: var(--cv-accent-bg); + color: var(--cv-accent); + box-shadow: inset 0 0 0 1px var(--cv-accent-border); +} +.layout-toggle-btn i { font-size: 13px; } + +/* ── Grid container — switches mode ────────────────────────── */ + +#grid.clients-grid[data-layout="rows"] { + display: flex; + flex-direction: column; + gap: 8px; + padding: 0; +} +#grid.clients-grid[data-layout="table"] { + display: block; + padding: 0; +} +#grid.clients-grid[data-layout="cards"] { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); + gap: 14px; + padding: 8px 0; +} + +/* ── Shared atoms ──────────────────────────────────────────── */ + +.cv-mid { color: var(--cv-text-dim); margin: 0 6px; } +.cv-mono { font-family: ui-monospace, "SFMono-Regular", "Menlo", monospace; } +.cv-tab-num { font-variant-numeric: tabular-nums; } +.cv-warn { color: var(--cv-warn) !important; } + +.cv-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + background: #475569; + box-shadow: 0 0 0 2px rgba(148, 163, 184, 0.12); +} +.cv-dot.is-online { + background: #22c55e; + box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.18), 0 0 8px rgba(34, 197, 94, 0.45); +} +.cv-dot.is-offline { + background: #64748b; +} + +.cv-flag { font-size: 14px; line-height: 1; } +.cv-name { + font-size: 14px; + font-weight: 600; + color: var(--cv-text); + letter-spacing: -0.2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +.cv-mini-pill { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 4px; + font-size: 10px; + border: 1px solid transparent; +} +.cv-pill-admin { background: rgba(234, 179, 8, 0.12); color: #fde68a; border-color: rgba(234, 179, 8, 0.3); } +.cv-pill-system { background: rgba(239, 68, 68, 0.12); color: #fca5a5; border-color: rgba(239, 68, 68, 0.3); } +.cv-pill-ti { background: rgba(168, 85, 247, 0.12); color: #d8b4fe; border-color: rgba(168, 85, 247, 0.3); } + +.cv-group { + display: inline-flex; + align-items: center; + font-size: 11px; + font-weight: 500; + padding: 2px 8px; + border-radius: 4px; + background: color-mix(in srgb, var(--gc, #64748b) 15%, transparent); + color: var(--gc, #cbd5e1); + border: 1px solid color-mix(in srgb, var(--gc, #64748b) 30%, transparent); + white-space: nowrap; +} +.cv-group-empty { color: var(--cv-text-dim); font-size: 11px; } + +/* Custom checkbox */ +.cv-checkbox { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + cursor: pointer; + flex-shrink: 0; +} +.cv-checkbox input { + position: absolute; + inset: 0; + opacity: 0; + margin: 0; + cursor: pointer; + z-index: 1; +} +.cv-checkbox input:disabled { cursor: not-allowed; } +.cv-checkbox-box { + width: 16px; + height: 16px; + border-radius: 4px; + border: 1.5px solid var(--cv-border-strong); + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + color: transparent; + font-size: 9px; + transition: all 140ms ease; +} +.cv-checkbox input:checked + .cv-checkbox-box { + background: var(--cv-accent); + border-color: var(--cv-accent); + color: #fff; +} +.cv-checkbox input:disabled + .cv-checkbox-box { opacity: 0.4; } + +/* Star button */ +.cv-star { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + background: transparent; + border: 0; + color: var(--cv-text-dim); + cursor: pointer; + font-size: 14px; + transition: color 120ms ease, transform 120ms ease; +} +.cv-star:hover { color: #fde68a; transform: scale(1.1); } +.cv-star.is-on { color: #fbbf24; } + +/* Thumbnails */ +.cv-thumb, +.cv-thumb-img { + border-radius: 6px; + overflow: hidden; + flex-shrink: 0; + background: #0d1018; + border: 1px solid var(--cv-border); +} +.cv-thumb-img { object-fit: cover; cursor: pointer; } +.cv-thumb { position: relative; } + +.cv-faux { + position: absolute; + inset: 0; + background: linear-gradient(135deg, var(--p1) 0%, var(--p2) 55%, var(--p3) 100%); + opacity: var(--op, 1); + overflow: hidden; +} +.cv-faux-dots { + position: absolute; + top: 6px; + left: 8px; + display: flex; + gap: 4px; +} +.cv-faux-dots i { + display: inline-block; + border-radius: 50%; + background: rgba(255, 255, 255, 0.55); +} +.cv-faux-dots i:nth-child(1) { background: rgba(255, 90, 90, 0.78); } +.cv-faux-dots i:nth-child(2) { background: rgba(255, 200, 60, 0.68); } +.cv-faux-dots i:nth-child(3) { background: rgba(70, 220, 110, 0.64); } +.cv-faux-window { + position: absolute; + top: 22px; + left: 14px; + right: 14px; + bottom: 24px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 4px; + padding: 8px 8px; + display: flex; + flex-direction: column; + gap: 5px; + overflow: hidden; +} +.cv-faux-window span { + display: block; + height: 4px; + background: rgba(255, 255, 255, 0.34); + border-radius: 1px; +} +.cv-faux-window span:nth-child(2) { background: rgba(255, 255, 255, 0.26); } +.cv-faux-window span:nth-child(3) { background: rgba(255, 255, 255, 0.20); } +.cv-faux-window span:nth-child(4) { background: rgba(255, 255, 255, 0.16); } +.cv-faux-window span:nth-child(5) { background: rgba(255, 255, 255, 0.12); } +.cv-faux-bar { + position: absolute; + bottom: 4px; + left: 8px; + right: 8px; + height: 14px; + background: rgba(0, 0, 0, 0.40); + border-radius: 3px; + display: flex; + align-items: center; + gap: 4px; + padding: 0 5px; +} +.cv-faux-bar i { + display: inline-block; + width: 8px; + height: 8px; + background: rgba(255, 255, 255, 0.36); + border-radius: 1px; +} + +/* Buttons */ +.cv-btn-primary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + height: 28px; + padding: 0 12px; + border-radius: 6px; + background: var(--cv-accent); + color: #fff; + font-size: 12px; + font-weight: 500; + border: 0; + cursor: pointer; + transition: background-color 140ms ease, transform 140ms ease; +} +.cv-btn-primary:hover { background: #6366f1; } +.cv-btn-primary i { font-size: 12px; } +.cv-btn-primary.cv-btn-sm { height: 26px; padding: 0 10px; font-size: 11px; } +.cv-btn-primary.cv-btn-flex { flex: 1; } + +.cv-icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + background: var(--cv-surface2); + border: 1px solid var(--cv-border); + color: var(--cv-text-mute); + cursor: pointer; + font-size: 12px; + transition: all 140ms ease; +} +.cv-icon-btn:hover { + background: rgba(255, 255, 255, 0.07); + color: var(--cv-text); + border-color: var(--cv-border-strong); +} +.cv-icon-btn:disabled { opacity: 0.4; cursor: not-allowed; } +.cv-icon-btn.cv-icon-sm { width: 26px; height: 26px; font-size: 11px; } +.cv-icon-btn.cv-icon-danger { color: var(--cv-danger); border-color: rgba(239, 68, 68, 0.25); } +.cv-icon-btn.cv-icon-danger:hover { background: rgba(239, 68, 68, 0.10); border-color: rgba(239, 68, 68, 0.45); } + +.cv-actions { display: inline-flex; align-items: center; gap: 4px; } + +/* ════════════════════════════════════════════════════════════ + VARIANT A — Row cards + ════════════════════════════════════════════════════════════ */ + +.cv-row { + position: relative; + display: flex; + align-items: center; + gap: 16px; + background: var(--cv-bg); + border: 1px solid var(--cv-border); + border-radius: 12px; + padding: 18px 22px 18px 25px; + min-height: 116px; + flex-wrap: wrap; + transition: background-color 140ms ease, border-color 140ms ease; +} +.cv-row .cv-edge { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 4px; + background: var(--group-color, transparent); + border-top-left-radius: 12px; + border-bottom-left-radius: 12px; +} +.cv-row.cv-offline { opacity: 0.65; } +.cv-row.is-bookmarked { border-color: rgba(202, 138, 4, 0.5); } +.cv-row:has(.client-checkbox:checked) { + background: var(--cv-bg-selected); + border-color: var(--cv-accent-border); +} + +.cv-row .cv-primary { + flex: 1 1 320px; + min-width: 240px; + display: flex; + flex-direction: column; + gap: 4px; +} +.cv-row .cv-name-line { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + min-width: 0; +} +.cv-row .cv-name { font-size: 16px; } +.cv-row .cv-user-line { + font-size: 12px; + font-family: ui-monospace, "SFMono-Regular", "Menlo", monospace; + color: var(--cv-text-mute); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.cv-row .cv-user-line i { margin-right: 4px; opacity: 0.6; } +.cv-row .cv-meta-line { + font-size: 12px; + color: var(--cv-text-dim); + display: flex; + align-items: center; + flex-wrap: wrap; + margin-top: 4px; +} +.cv-row .cv-meta-line i { font-size: 11px; margin-right: 3px; } + +.cv-row .cv-tag { + display: inline-flex; + align-items: center; + gap: 5px; + margin-top: 4px; + padding: 2px 8px; + font-size: 11px; + background: rgba(245, 158, 11, 0.12); + color: #fcd34d; + border: 1px solid rgba(245, 158, 11, 0.3); + border-radius: 4px; + cursor: default; + align-self: flex-start; +} +.cv-row .cv-tag.has-note { cursor: pointer; } +.cv-row .cv-tag.has-note:hover { background: rgba(245, 158, 11, 0.18); } + +.cv-row .client-tag-note { + flex-basis: 100%; + margin-top: 6px; + padding: 8px 10px; + background: var(--cv-surface2); + border: 1px solid var(--cv-border); + border-radius: 6px; + font-size: 12px; + color: #fde68a; + white-space: pre-wrap; + word-break: break-word; + max-height: 200px; + overflow: auto; +} +.cv-row .client-tag-note.hidden { display: none; } + +.cv-row .cv-time { + flex: 0 0 120px; + display: flex; + flex-direction: column; + gap: 6px; + font-size: 12px; + color: var(--cv-text-dim); +} +.cv-row .cv-time-line { display: inline-flex; align-items: center; gap: 6px; } +.cv-row .cv-time i { font-size: 11px; } +.cv-row .cv-ping-line { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: ui-monospace, monospace; + font-variant-numeric: tabular-nums; +} + +.cv-row .cv-group-cell { flex: 0 0 auto; min-width: 60px; } +.cv-row .cv-spacer { flex: 1 1 auto; } + +.cv-row .cv-actions { opacity: 0.85; transition: opacity 120ms ease; } +.cv-row:hover .cv-actions, +.cv-row:has(.client-checkbox:checked) .cv-actions { opacity: 1; } +.cv-row .cv-expand-btn[aria-expanded="true"] i { transform: rotate(180deg); transition: transform 180ms ease; } +.cv-row .cv-expand-btn i { transition: transform 180ms ease; } + +.cv-row .cv-expand-panel { + flex-basis: 100%; + margin: 14px -22px -18px -25px; + padding: 12px 22px 16px 25px; + border-top: 1px solid var(--cv-border); + background: var(--cv-surface2); + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; +} +.cv-row .cv-expand-panel.hidden { display: none; } +.cv-expand-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} +.cv-field { display: flex; flex-direction: column; gap: 3px; min-width: 0; } +.cv-field-wide { grid-column: span 2; } +.cv-field-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--cv-text-mute); + font-weight: 600; +} +.cv-field-value { + font-size: 12px; + color: var(--cv-text); + word-break: break-word; + overflow-wrap: anywhere; +} +.cv-field-value.copy-id-btn { cursor: pointer; } +.cv-field-value.copy-id-btn:hover { color: var(--cv-accent); } + +/* Ping tones */ +.ping-good { color: #86efac; } +.ping-mid { color: #fcd34d; } +.ping-bad { color: #fca5a5; } +.ping-unknown { color: var(--cv-text-dim); } + +/* OS / arch tones — applied to icon containers across all variants */ +.cv-tone-pill-win { color: #93c5fd; } +.cv-tone-pill-mac { color: #f1f5f9; } +.cv-tone-pill-ubuntu { color: #fdba74; } +.cv-tone-pill-debian { color: #f9a8d4; } +.cv-tone-pill-arch { color: #67e8f9; } +.cv-tone-pill-kali { color: #7dd3fc; } +.cv-tone-pill-fedora { color: #bfdbfe; } +.cv-tone-pill-linux { color: #86efac; } +.cv-tone-pill-x64 { color: #5eead4; } +.cv-tone-pill-arm { color: #7dd3fc; } +.cv-tone-pill-ghost, +.cv-tone-pill-unknown { color: var(--cv-text-mute); } +.cv-os, .cv-arch { display: inline-flex; align-items: center; gap: 4px; } +.cv-os i { font-size: 11px; } + +/* Arch chip in table variant */ +.clients-table .cv-arch-chip { + background: rgba(255, 255, 255, 0.04); +} + +/* ════════════════════════════════════════════════════════════ + VARIANT B — Dense table + ════════════════════════════════════════════════════════════ */ + +.clients-table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--cv-border); + border-radius: 10px; + overflow: hidden; + background: var(--cv-bg) !important; + backdrop-filter: none; +} +.clients-table thead th { + position: sticky; + top: 0; + z-index: 1; + background: var(--cv-bg) !important; + color: var(--cv-text-mute); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 500; + text-align: left; + padding: 8px 10px; + border-bottom: 1px solid var(--cv-border); +} +.clients-table th.cv-th-check { width: 36px; padding-left: 12px; } +.clients-table th.cv-th-star { width: 30px; } +.clients-table th.cv-th-thumb { width: 80px; } +.clients-table th.cv-th-status { width: 110px; } +.clients-table th.cv-th-last { width: 130px; } +.clients-table th.cv-th-system { width: 140px; } +.clients-table th.cv-th-ping { width: 90px; text-align: right; } +.clients-table th.cv-th-group { width: 90px; } +.clients-table th.cv-th-actions { width: 160px; text-align: right; padding-right: 12px; } + +.clients-table tbody tr, +.clients-table tbody tr:nth-child(even) { + background: transparent; + border-bottom: 1px solid var(--cv-border); + transition: background-color 100ms ease; +} +.clients-table tbody tr:hover { + background: var(--cv-bg-hover); +} +.clients-table tbody tr:has(.client-checkbox:checked) { + background: var(--cv-bg-selected) !important; +} +.clients-table tbody tr.cv-trow.cv-offline { opacity: 0.65; } +.clients-table tbody tr.is-bookmarked td:first-child { box-shadow: inset 3px 0 0 0 rgba(202, 138, 4, 0.55); } +.clients-table td { + padding: 8px 10px; + vertical-align: middle; + border-bottom: 1px solid var(--cv-border); + color: var(--cv-text); + font-size: 13px; +} +.clients-table td.cv-td-check { padding-left: 12px; } +.clients-table td.cv-td-actions { padding-right: 12px; text-align: right; } +.clients-table td.cv-td-ping { text-align: right; } +.clients-table tbody tr:last-child td { border-bottom: 0; } + +.cv-tcell-client { display: flex; align-items: center; gap: 8px; min-width: 0; } +.cv-tcell-stack { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; } +.cv-tcell-name-row { display: flex; align-items: center; gap: 5px; min-width: 0; } +.cv-tcell-stack .cv-name { font-size: 13px; } +.cv-tcell-stack .cv-user-line { + font-size: 11px; + color: var(--cv-text-mute); + display: flex; + align-items: center; + gap: 4px; + flex-wrap: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.cv-tcell-stack .cv-user-line i { font-size: 10px; opacity: 0.7; } + +.cv-status-cell { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--cv-text-dim); } +.cv-trow[data-online="true"] .cv-status-cell { color: var(--cv-text); } + +.cv-td-last { color: var(--cv-text-mute); font-size: 12px; } + +.cv-system-cell { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; } +.cv-arch-chip { + display: inline-flex; + align-items: center; + font-size: 10px; + padding: 1px 5px; + border-radius: 3px; + border: 1px solid var(--cv-border); + color: var(--cv-text-mute); + margin-left: 2px; +} + +.cv-actions-table { justify-content: flex-end; opacity: 0.55; transition: opacity 120ms ease; } +.clients-table tbody tr:hover .cv-actions-table, +.clients-table tbody tr:has(.client-checkbox:checked) .cv-actions-table { opacity: 1; } + +.clients-table .cv-thumb, +.clients-table .cv-thumb-img { border-radius: 4px; } + +/* ════════════════════════════════════════════════════════════ + VARIANT C — Card wall + ════════════════════════════════════════════════════════════ */ + +.cv-card { + position: relative; + display: flex; + flex-direction: column; + background: var(--cv-bg); + border: 1px solid var(--cv-border); + border-radius: 10px; + overflow: hidden; + transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; +} +.cv-card:hover { transform: translateY(-2px); } +.cv-card.cv-offline { opacity: 0.85; } +.cv-card.is-bookmarked { border-color: rgba(202, 138, 4, 0.5); } +.cv-card:has(.client-checkbox:checked) { + border-color: var(--cv-accent-border); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18); +} + +.cv-card-header { + position: relative; + height: 130px; + overflow: hidden; + background: #0d1018; +} +.cv-card-header .cv-thumb, +.cv-card-header .cv-thumb-img { + position: absolute; + inset: 0; + width: 100% !important; + height: 100% !important; + border-radius: 0; + border: 0; +} +.cv-card.cv-offline .cv-card-header > .cv-thumb, +.cv-card.cv-offline .cv-card-header > .cv-thumb-img { opacity: 0.3 !important; } + +.cv-card-check { + position: absolute; + top: 8px; + left: 8px; + z-index: 2; + width: 18px; + height: 18px; + opacity: 0; + transition: opacity 140ms ease; + background: rgba(0, 0, 0, 0.45); + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + border-radius: 4px; + padding: 0; +} +.cv-card-check .cv-checkbox-box { + border-color: rgba(255, 255, 255, 0.5); + background: transparent; +} +.cv-card:hover .cv-card-check, +.cv-card:has(.client-checkbox:checked) .cv-card-check { opacity: 1; } + +.cv-card-chips { + position: absolute; + top: 8px; + right: 8px; + z-index: 2; + display: flex; + gap: 4px; +} +.cv-chip-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 5px; + background: rgba(0, 0, 0, 0.45); + border: 1px solid rgba(255, 255, 255, 0.12); + color: #f1f5f9; + cursor: pointer; + font-size: 11px; + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + transition: background-color 140ms ease, color 140ms ease; +} +.cv-chip-btn:hover { background: rgba(0, 0, 0, 0.6); } +.cv-chip-btn.is-on { color: #fbbf24; } + +.cv-card-status { + position: absolute; + bottom: 8px; + left: 8px; + z-index: 2; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 8px; + border-radius: 999px; + background: rgba(0, 0, 0, 0.45); + border: 1px solid rgba(255, 255, 255, 0.08); + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + color: #fff; + font-size: 11px; +} +.cv-card-status .cv-card-ago { color: #cbd2e0; } + +.cv-card-ping { + position: absolute; + bottom: 8px; + right: 8px; + z-index: 2; + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 8px; + border-radius: 999px; + background: rgba(0, 0, 0, 0.45); + border: 1px solid rgba(255, 255, 255, 0.08); + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + font-size: 11px; + color: #fff; +} +.cv-card-ping i { font-size: 10px; } + +.cv-card { + /* let cards in the same row stretch so actions can sit on the same baseline */ +} +.cv-card-body { + display: flex; + flex-direction: column; + gap: 2px; + padding: 10px 12px 12px; + flex: 1 1 auto; + min-height: 0; +} +.cv-card-body .cv-name-line { display: flex; align-items: center; gap: 6px; min-width: 0; } +.cv-card-body .cv-name { font-size: 14px; } +.cv-card-body .cv-user-line { + font-size: 11px; + color: var(--cv-text-mute); + margin-top: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.cv-card-body .cv-user-line i { margin-right: 4px; opacity: 0.6; } + +.cv-card-net { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; + font-size: 11px; + color: var(--cv-text-dim); + flex-wrap: wrap; +} +.cv-card-net:empty { display: none; } +.cv-card-net-bit { display: inline-flex; align-items: center; gap: 5px; } +.cv-card-net-bit i { font-size: 10px; opacity: 0.7; } + +.cv-card-tag { + display: inline-flex; + align-items: center; + gap: 5px; + margin-top: 6px; + align-self: flex-start; + padding: 2px 8px; + font-size: 11px; + background: rgba(245, 158, 11, 0.12); + color: #fcd34d; + border: 1px solid rgba(245, 158, 11, 0.3); + border-radius: 4px; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cv-card-meta { + margin-top: 10px; + display: flex; + align-items: center; + gap: 10px; + font-size: 11px; + color: var(--cv-text-dim); + flex-wrap: wrap; +} +.cv-card-meta-bit { display: inline-flex; align-items: center; gap: 4px; } +.cv-card-meta-bit i { font-size: 10px; } +.cv-group-spacer { margin-left: auto; } + +.cv-card-hw { + display: flex; + align-items: center; + gap: 12px; + margin-top: 6px; + padding: 6px 8px; + background: var(--cv-surface2); + border-radius: 6px; + font-size: 11px; + color: var(--cv-text-mute); + flex-wrap: wrap; +} +.cv-card-hw-bit { + display: inline-flex; + align-items: center; + gap: 5px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.cv-card-hw-bit i { color: var(--cv-text-dim); font-size: 10px; } + +.cv-text-dim { color: var(--cv-text-dim); } + +.cv-card-actions { + margin-top: auto; + padding-top: 10px; + border-top: 1px solid var(--cv-border); + display: flex; + align-items: center; + gap: 4px; + opacity: 0.85; + transition: opacity 120ms ease; +} +.cv-card-meta { margin-bottom: 6px; } +.cv-card:hover .cv-card-actions, +.cv-card:has(.client-checkbox:checked) .cv-card-actions { opacity: 1; } + +/* ── Mobile tweaks ─────────────────────────────────────────── */ +@media (max-width: 720px) { + .cv-row { + padding: 10px 12px 10px 15px; + gap: 10px; + } + .cv-row .cv-primary { flex-basis: 100%; } + .cv-row .cv-time, .cv-row .cv-group-cell { flex-basis: auto; } + .cv-expand-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .clients-table th.cv-th-thumb, + .clients-table td.cv-td-thumb, + .clients-table th.cv-th-system, + .clients-table td.cv-td-system { display: none; } + #grid.clients-grid[data-layout="cards"] { + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + } +} + /* ============================================================ SIDEBAR LAYOUT ============================================================ */ diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index 29ea315..5756b4e 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -115,7 +115,7 @@ function detectClientPlatform(clientId) { typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(clientId) : clientId; - const card = document.querySelector(`article[data-id="${selectorId}"]`); + const card = document.querySelector(`[data-client-row][data-id="${selectorId}"]`); const os = String(card?.dataset?.os || "").toLowerCase(); if (os.includes("windows")) return "windows"; if (os.includes("darwin") || os.includes("mac")) return "mac"; @@ -129,7 +129,7 @@ function getClientCard(clientId) { typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(clientId) : clientId; - return document.querySelector(`article[data-id="${selectorId}"]`); + return document.querySelector(`[data-client-row][data-id="${selectorId}"]`); } window.__uninstallingClientIds = window.__uninstallingClientIds || new Set(); @@ -558,8 +558,36 @@ async function loadPluginsForClient(clientId) { } } +const PREF_LAYOUT_KEY = "overlord_layout"; +let rendererSetLayout = null; + +function applyLayoutToggleUI(layout) { + document.querySelectorAll("#layout-toggle .layout-toggle-btn").forEach((btn) => { + const active = btn.dataset.layout === layout; + btn.classList.toggle("is-active", active); + btn.setAttribute("aria-pressed", active ? "true" : "false"); + }); +} + +function setLayout(layout) { + const next = ["rows", "table", "cards"].includes(layout) ? layout : "rows"; + localStorage.setItem(PREF_LAYOUT_KEY, next); + applyLayoutToggleUI(next); + if (rendererSetLayout) rendererSetLayout(next); + state.lastDigest = ""; + loadWithOptions({ force: true, reorder: true }); +} + +document.querySelectorAll("#layout-toggle .layout-toggle-btn").forEach((btn) => { + btn.addEventListener("click", () => setLayout(btn.dataset.layout)); +}); + function initializeRenderer() { - const { renderMerge } = createRenderer({ + const savedLayout = localStorage.getItem(PREF_LAYOUT_KEY) || "rows"; + if (grid) grid.dataset.layout = ["rows", "table", "cards"].includes(savedLayout) ? savedLayout : "rows"; + applyLayoutToggleUI(grid?.dataset.layout || "rows"); + + const { renderMerge, setLayout: rSetLayout } = createRenderer({ grid, totalPill, pageLabel, @@ -574,6 +602,7 @@ function initializeRenderer() { pingClient: (id) => sendCommand(id, "ping"), userRole: currentUser?.role, }); + rendererSetLayout = rSetLayout; registerRenderer(renderMerge); refreshGroupFilter(); loadWithOptions(); diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index 1992c75..4b4a22c 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -15,8 +15,102 @@ function escapeHtml(text) { return div.innerHTML; } -function cardDigest(c) { - return `${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.thumbnail}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${c.cpu}|${c.gpu}|${c.ram}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${JSON.stringify(c.permissions)}|${c.groupId}|${c.groupName}|${c.groupColor}`; +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 } = {}) { + if (client.thumbnail) { + return `preview`; + } + 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 shortOsLabel(osRaw = "") { + const o = String(osRaw).toLowerCase(); + if (o.includes("windows 11")) return "Win 11"; + if (o.includes("windows 10")) return "Win 10"; + 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({ @@ -37,6 +131,51 @@ export function createRenderer({ const TOUCH_MOVE_CANCEL_PX = 10; let renderToken = 0; let gridDelegated = false; + let currentLayout = (grid?.dataset.layout || "rows").toLowerCase(); + + function getCardContainer() { + if (currentLayout === "table") { + return grid.querySelector("tbody.clients-table-body") || grid; + } + return grid; + } + + function ensureLayoutScaffold() { + if (currentLayout === "table") { + if (grid.querySelector("table.clients-table")) return; + grid.innerHTML = ` + + + + + + + + + + + + + + + + +
ClientStatusLast seenSystemPingGroupActions
+ `; + } else { + 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 ─────────────────────────────────────────── */ @@ -45,7 +184,7 @@ export function createRenderer({ gridDelegated = true; grid.addEventListener("click", (e) => { - const card = e.target.closest("article[data-id]"); + const card = e.target.closest(ROW_SELECTOR); if (!card) return; const clientId = card.dataset.id; @@ -88,6 +227,19 @@ export function createRenderer({ 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); @@ -100,6 +252,12 @@ export function createRenderer({ 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); @@ -128,7 +286,7 @@ export function createRenderer({ }); grid.addEventListener("contextmenu", (e) => { - const card = e.target.closest("article[data-id]"); + const card = e.target.closest(ROW_SELECTOR); if (!card || isViewer) return; e.preventDefault(); openMenu(card.dataset.id, e.clientX, e.clientY); @@ -137,7 +295,7 @@ export function createRenderer({ 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("article[data-id]"); + const card = e.target.closest(ROW_SELECTOR); if (!card) return; card._longPressTriggered = false; card._pointerStartX = e.clientX; @@ -151,7 +309,7 @@ export function createRenderer({ grid.addEventListener("pointermove", (e) => { if (e.pointerType !== "touch") return; - const card = e.target.closest("article[data-id]"); + const card = e.target.closest(ROW_SELECTOR); if (!card || !card._longPressTimer) return; if ( Math.abs(e.clientX - card._pointerStartX) > TOUCH_MOVE_CANCEL_PX || @@ -164,7 +322,7 @@ export function createRenderer({ const clearLongPress = (e) => { if (e.pointerType !== "touch") return; - const card = e.target.closest("article[data-id]"); + const card = e.target.closest(ROW_SELECTOR); if (card) { clearTimeout(card._longPressTimer); card._longPressTimer = null; } }; grid.addEventListener("pointerup", clearLongPress); @@ -184,21 +342,12 @@ export function createRenderer({ if (res.ok) { card.dataset.bookmarked = String(!isBookmarked); const icon = btn.querySelector("i"); - if (!isBookmarked) { - btn.classList.remove("border-slate-700", "bg-slate-800/50", "text-slate-500"); - btn.classList.add("border-yellow-600", "bg-yellow-900/50", "text-yellow-300"); - card.classList.remove("border-slate-800"); - card.classList.add("border-yellow-600/60"); - if (icon) { icon.classList.remove("fa-regular"); icon.classList.add("fa-solid"); } - btn.title = "Remove bookmark"; - } else { - btn.classList.remove("border-yellow-600", "bg-yellow-900/50", "text-yellow-300"); - btn.classList.add("border-slate-700", "bg-slate-800/50", "text-slate-500"); - card.classList.remove("border-yellow-600/60"); - card.classList.add("border-slate-800"); - if (icon) { icon.classList.remove("fa-solid"); icon.classList.add("fa-regular"); } - btn.title = "Bookmark"; + 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); @@ -233,9 +382,18 @@ export function createRenderer({ } } + 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 cards = grid.querySelectorAll("article[data-id]"); + 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++) { @@ -244,14 +402,19 @@ export function createRenderer({ } if (!needsReorder) return; items.forEach((client) => { - const card = grid.querySelector(`article[data-id="${client.id}"]`); - if (card) grid.appendChild(card); + const card = container.querySelector(`${ROW_SELECTOR}[data-id="${client.id}"]`); + if (card) container.appendChild(card); }); } + function cardDigest(c) { + return `${currentLayout}|${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.thumbnail}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${c.cpu}|${c.gpu}|${c.ram}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${c.groupId}|${c.groupName}|${c.groupColor}`; + } function renderMerge(data, options = {}) { setupGridDelegation(); + 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)); @@ -267,14 +430,14 @@ export function createRenderer({ window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; - const hadCards = grid.querySelectorAll("article[data-id]").length > 0; + const hadCards = container.querySelectorAll(ROW_SELECTOR).length > 0; const uninstallingIds = window.__uninstallingClientIds; items.forEach((client) => { seen.add(client.id); if (uninstallingIds && uninstallingIds.has(client.id)) return; - const existing = grid.querySelector(`article[data-id="${client.id}"]`); + const existing = container.querySelector(`${ROW_SELECTOR}[data-id="${client.id}"]`); if (existing) { if (existing.classList.contains("card-uninstalling")) return; const digest = cardDigest(client); @@ -286,7 +449,7 @@ export function createRenderer({ newClients.push(client); }); - Array.from(grid.querySelectorAll("article[data-id]")) + Array.from(container.querySelectorAll(ROW_SELECTOR)) .filter((el) => !seen.has(el.dataset.id) && !el.classList.contains("card-uninstalling")) .forEach((el) => el.remove()); @@ -294,11 +457,9 @@ export function createRenderer({ reorderCards(items); } - if (newClients.length === 0) { - return; - } + if (newClients.length === 0) return; - const allowAnimation = !hadCards && !prefersReducedMotion && items.length <= 1000; + const allowAnimation = !hadCards && !prefersReducedMotion && items.length <= 1000 && currentLayout !== "table"; const animateLimit = Math.min(newClients.length, MAX_ANIMATED_CARDS); let idx = 0; @@ -318,7 +479,7 @@ export function createRenderer({ }); fragment.appendChild(card); } - grid.appendChild(fragment); + container.appendChild(fragment); if (idx < newClients.length) { requestAnimationFrame(insertBatch); return; @@ -336,23 +497,46 @@ export function createRenderer({ 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.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 card = document.createElement("article"); - card.dataset.id = client.id; - card.dataset.hwid = client.hwid || ""; + const node = + currentLayout === "table" + ? buildRowB(client, options) + : currentLayout === "cards" + ? buildCardC(client, options) + : buildRowA(client, options); - updateCard(card, client); - card._cardDigest = cardDigest(client); + node._cardDigest = cardDigest(client); - if (options.animate) { - card.classList.add("card-animate"); - card.style.animationDelay = `${(options.delayIndex || 0) * 0.05}s`; - card.style.opacity = "0"; - card.style.transform = "translateY(10px)"; + 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: card, + targets: node, opacity: [0, 1], translateY: [10, 0], duration: 400, @@ -362,25 +546,42 @@ export function createRenderer({ } } - return card; + return node; } function updateCard(card, client) { - const oldCheckbox = card.querySelector(".client-checkbox"); - const wasChecked = oldCheckbox?.checked || false; + 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); - card.dataset.online = String(!!client.online); - card.dataset.os = String(client.os || "").toLowerCase(); - card.dataset.nickname = String(client.nickname || ""); - card.dataset.customTag = String(client.customTag || ""); - card.dataset.bookmarked = String(!!client.bookmarked); - card.dataset.admin = String(!!client.isAdmin); - card.dataset.groupId = String(client.groupId || ""); - card.dataset.groupName = String(client.groupName || ""); - card.dataset.groupColor = String(client.groupColor || ""); - card._customTagNote = String(client.customTagNote || ""); const os = osBadge(client.os || "unknown"); const arch = archBadge(client.arch || ""); const ver = versionBadge(client.version || ""); @@ -391,136 +592,217 @@ export function createRenderer({ 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 isTagNoteExpanded = hasTagNote && wasTagNoteExpanded; - card.dataset.tagNoteExpanded = isTagNoteExpanded ? "true" : "false"; - const groupName = String(client.groupName || "").trim(); - const groupColor = String(client.groupColor || "").trim(); 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); - const borderClass = client.bookmarked ? "border-yellow-600/60" : "border-slate-800"; - card.className = `card rounded-xl border ${borderClass} p-4 ${client.online ? "" : "card-offline"}`; - if (!client.bookmarked && groupColor) { - card.style.setProperty("--group-color", groupColor); - } else { - card.style.removeProperty("--group-color"); - } - const cardThumb = client.thumbnail - ? (() => { - const wrapper = document.createElement("div"); - wrapper.className = "flex-shrink-0"; - const img = document.createElement("img"); - img.className = - "w-40 h-24 rounded-lg object-contain cursor-pointer thumb-img"; - img.alt = "preview"; - img.src = client.thumbnail; - wrapper.appendChild(img); - return wrapper; - })() - : (() => { - const wrapper = document.createElement("div"); - wrapper.className = "flex-shrink-0"; - wrapper.innerHTML = `
`; - return wrapper; - })(); + const verLatest = String(client.version || "").startsWith("2.0."); - card.innerHTML = ` -
-
- + const metaParts = [ + ` ${escapeHtml(shortOsLabel(client.os))}`, + `${escapeHtml(arch.label)}`, + ` ${escapeHtml(ver.label)}`, + ` ${client.monitors || 1}`, + nickname && client.host && nickname !== client.host ? ` ${escapeHtml(client.host)}` : "", + client.ip ? ` ${escapeHtml(client.ip)}` : "", + hwid ? ` ${escapeHtml(hwid)}` : "", + ].filter(Boolean); + const meta = metaParts.join(metaSeparator()); + + article.innerHTML = ` + + + +
${thumbHtml(client, { width: 126, height: 80, small: false })}
+
+
+ ${statusDot(client)} + ${countryToFlag(client.country)} + ${escapeHtml(displayName)} + ${client.isAdmin ? `` : ""} + ${client.elevation === "system" ? `` : ""} + ${client.elevation === "trustedinstaller" ? `` : ""}
-
-
-
- ${countryToFlag(client.country)} - ${escapeHtml(displayName)} - ${nickname && client.host ? ` ${escapeHtml(client.host)}` : ""} - ${customTag ? `` : ""} - ${groupName ? ` ${escapeHtml(groupName)}` : ""} - ${escapeHtml(client.user || "unknown")} - - - ${client.online ? "Online" : "Offline"} - - ${client.isAdmin ? ` Admin` : ""} - ${client.elevation === "system" ? ` SYSTEM` : ""} - ${client.elevation === "trustedinstaller" ? ` TrustedInstaller` : ""} - ${client.os === "darwin" && client.permissions ? (() => { - const p = client.permissions; - const pills = []; - if (p.screenRecording === true) pills.push(' Screen'); - else pills.push(' No Screen'); - if (p.accessibility === true) pills.push(' Accessibility'); - else pills.push(' No Accessibility'); - if (p.fullDiskAccess === true) pills.push(' FDA'); - else pills.push(' No FDA'); - return pills.join(""); - })() : ""} - ${!client.online && client.disconnectReason && client.disconnectReason !== "normal" ? (() => { - const iconMap = { panic: "fa-skull-crossbones", crash: "fa-skull", timeout: "fa-clock", network: "fa-plug-circle-xmark" }; - const colorMap = { panic: "text-red-400", crash: "text-red-400", timeout: "text-amber-400", network: "text-slate-400" }; - const icon = iconMap[client.disconnectReason] || "fa-circle-exclamation"; - const color = colorMap[client.disconnectReason] || "text-slate-400"; - const detail = client.disconnectDetail ? escapeHtml(client.disconnectDetail) : ""; - return ` ${escapeHtml(client.disconnectReason)}`; - })() : ""} -
- ${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}` : ""} - ${client.ip ? ` ${escapeHtml(client.ip)}` : ""} -
-
-
- - ${formatPing(client.pingMs)} - ${isViewer ? "" : ``} - ${isViewer ? "" : ``} +
${escapeHtml(userLine)}
+
${meta}
+ ${customTag ? `` : ""} + ${hasTagNote ? `` : ""} +
+
+ ${formatAgo(client.lastSeen)} + ${formatPing(client.pingMs)} +
+
${groupPillHtml(client) || `—`}
+
+
+ ${isViewer ? "" : ``} + + ${isViewer ? "" : ``} + +
+ `; - - const thumbSlots = card.querySelectorAll(".flex-shrink-0"); - if (thumbSlots.length >= 2) { - thumbSlots[1].replaceWith(cardThumb); - } - - const checkbox = card.querySelector(".client-checkbox"); - if (checkbox) { - const isSelected = - typeof window.isClientSelected === "function" - ? window.isClientSelected(client.id) - : false; - - if ((wasChecked || isSelected) && client.online) { - checkbox.checked = true; - } - } + return article; } - return { renderMerge }; + /* ── 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; + + tr.innerHTML = ` + + + + + + + ${thumbHtml(client, { width: 56, height: 35, small: true })} + +
+ ${countryToFlag(client.country)} +
+ + ${escapeHtml(displayName)} + ${client.isAdmin ? `` : ""} + ${client.elevation === "system" ? `` : ""} + ${client.elevation === "trustedinstaller" ? `` : ""} + + ${escapeHtml(userLine)}${client.ip ? `· ${escapeHtml(client.ip)}` : ""} +
+
+ + + ${statusDot(client)} ${client.online ? "Online" : "Offline"} + + ${formatAgo(client.lastSeen)} + + ${escapeHtml(shortOsLabel(client.os))} ${escapeHtml(arch.label)} + + ${formatPing(client.pingMs)} + ${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 cpuShort = client.cpu ? shortenCpu(client.cpu) : ""; + + const metaParts = [ + ` ${escapeHtml(shortOsLabel(client.os))}`, + `${escapeHtml(arch.label)}`, + ` ${client.monitors || 1}`, + client.version ? ` v${escapeHtml(client.version)}` : "", + ].filter(Boolean).join(""); + + const elevationBadges = [ + client.isAdmin ? `` : "", + client.elevation === "system" ? `` : "", + client.elevation === "trustedinstaller" ? `` : "", + ].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} +
+
${escapeHtml(userLine)}${showHost ? ` @ ${escapeHtml(client.host)}` : ""}
+ ${customTag ? `
${escapeHtml(customTag)}
` : ""} +
+ ${client.ip ? ` ${escapeHtml(client.ip)}` : ""} + ${hwidShort ? ` ${escapeHtml(hwidShort)}` : ""} +
+
+ ${metaParts} + ${hasGroup ? `${groupPillHtml(client)}` : ""} +
+ ${cpuShort || client.ram ? `
+ ${cpuShort ? ` ${escapeHtml(cpuShort)}` : ""} + ${client.ram ? ` ${escapeHtml(client.ram)}` : ""} +
` : ""} +
+ ${isViewer ? "" : ``} + + ${isViewer ? "" : ``} +
+
+ `; + return article; + } + + return { renderMerge, setLayout }; } diff --git a/Overlord-Server/public/index.html b/Overlord-Server/public/index.html index 9715467..bf5201a 100644 --- a/Overlord-Server/public/index.html +++ b/Overlord-Server/public/index.html @@ -111,6 +111,40 @@ > Clear selection +
+ + + +
-
+