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 ` +
| + | + | + | Client | +Status | +Last seen | +System | +Ping | +Group | +Actions | +
|---|