From 2a90053eee15bd5da91e192e3ae2293a782ddc89 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sat, 30 May 2026 22:44:55 -0500 Subject: [PATCH] UI improvements --- Overlord-Client/cmd/agent/session.go | 7 +- Overlord-Client/cmd/agent/sysinfo/sysinfo.go | 8 +- .../cmd/agent/sysinfo/sysinfo_darwin.go | 48 +- .../cmd/agent/sysinfo/sysinfo_linux.go | 67 ++- .../cmd/agent/sysinfo/sysinfo_windows.go | 63 ++- Overlord-Client/cmd/agent/wire/protocol.go | 48 +- Overlord-Server/public/assets/data.js | 71 ++- Overlord-Server/public/assets/main.css | 491 +++++++++++++++++- Overlord-Server/public/assets/main.js | 109 +++- Overlord-Server/public/assets/purgatory-ui.js | 288 +++++----- Overlord-Server/public/assets/render.js | 194 +++++-- Overlord-Server/public/assets/utils.js | 14 + Overlord-Server/public/assets/viewUtils.js | 28 +- Overlord-Server/public/index.html | 167 +++--- Overlord-Server/public/purgatory.html | 44 +- Overlord-Server/scripts/seed_fake_clients.py | 161 +++++- Overlord-Server/src/db/repositories.ts | 66 ++- Overlord-Server/src/db/schema.ts | 12 + Overlord-Server/src/protocol.ts | 2 + .../src/server/routes/assets-routes.ts | 4 +- .../src/server/routes/client-routes.ts | 2 +- .../routes/websocket-lifecycle-routes.ts | 8 + Overlord-Server/src/types.ts | 2 + Overlord-Server/src/wsHandlers.ts | 14 + 24 files changed, 1498 insertions(+), 420 deletions(-) diff --git a/Overlord-Client/cmd/agent/session.go b/Overlord-Client/cmd/agent/session.go index 21c56de..aa423da 100644 --- a/Overlord-Client/cmd/agent/session.go +++ b/Overlord-Client/cmd/agent/session.go @@ -32,7 +32,6 @@ import ( "nhooyr.io/websocket" ) - func runClient(cfg config.Config) { //garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2 baseBackoff := computeBaseBackoff() @@ -294,7 +293,6 @@ func refreshServerURLsFromRaw(cfg *config.Config) bool { return true } - func createHTTPTransport(cfg config.Config) *http.Transport { tlsConfig := &tls.Config{ InsecureSkipVerify: cfg.TLSInsecureSkipVerify, @@ -608,6 +606,9 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket. if osVal == "" { osVal = runtime.GOOS } + if prettyOS := strings.TrimSpace(sysinfo.OSName()); prettyOS != "" { + osVal = prettyOS + } archVal := strings.TrimSpace(cfg.Arch) if archVal == "" { @@ -636,6 +637,8 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket. hello.CPU = hw.CPU hello.GPU = hw.GPU hello.RAM = hw.RAM + hello.BatteryPercent = hw.BatteryPercent + hello.BatteryCharging = hw.BatteryCharging hello.IsAdmin = sysinfo.IsAdmin() hello.Elevation = sysinfo.Elevation() hello.Permissions = sysinfo.DarwinPermissions() diff --git a/Overlord-Client/cmd/agent/sysinfo/sysinfo.go b/Overlord-Client/cmd/agent/sysinfo/sysinfo.go index 4f880ae..3b42cef 100644 --- a/Overlord-Client/cmd/agent/sysinfo/sysinfo.go +++ b/Overlord-Client/cmd/agent/sysinfo/sysinfo.go @@ -1,9 +1,11 @@ package sysinfo type Info struct { - CPU string `json:"cpu,omitempty"` - GPU string `json:"gpu,omitempty"` - RAM string `json:"ram,omitempty"` + CPU string `json:"cpu,omitempty"` + GPU string `json:"gpu,omitempty"` + RAM string `json:"ram,omitempty"` + BatteryPercent *int `json:"batteryPercent,omitempty"` + BatteryCharging bool `json:"batteryCharging,omitempty"` } func Collect() Info { diff --git a/Overlord-Client/cmd/agent/sysinfo/sysinfo_darwin.go b/Overlord-Client/cmd/agent/sysinfo/sysinfo_darwin.go index 4d9137e..7f60dcf 100644 --- a/Overlord-Client/cmd/agent/sysinfo/sysinfo_darwin.go +++ b/Overlord-Client/cmd/agent/sysinfo/sysinfo_darwin.go @@ -4,6 +4,7 @@ package sysinfo import ( "fmt" + "os/exec" "runtime" "strings" "unsafe" @@ -12,13 +13,54 @@ import ( ) func collectPlatform() Info { + percent, charging := batteryStatus() return Info{ - CPU: cpuName(), - GPU: gpuName(), - RAM: totalRAM(), + CPU: cpuName(), + GPU: gpuName(), + RAM: totalRAM(), + BatteryPercent: percent, + BatteryCharging: charging, } } +func OSName() string { + out, err := exec.Command("sw_vers", "-productVersion").Output() + if err != nil { + return "macOS" + } + version := strings.TrimSpace(string(out)) + if version == "" { + return "macOS" + } + return "macOS " + version +} + +func batteryStatus() (*int, bool) { + out, err := exec.Command("pmset", "-g", "batt").Output() + if err != nil { + return nil, false + } + text := string(out) + idx := strings.Index(text, "%") + if idx <= 0 { + return nil, false + } + start := idx - 1 + for start >= 0 && text[start] >= '0' && text[start] <= '9' { + start-- + } + var percent int + if _, err := fmt.Sscanf(text[start+1:idx], "%d", &percent); err != nil { + return nil, false + } + if percent < 0 || percent > 100 { + return nil, false + } + lower := strings.ToLower(text) + charging := strings.Contains(lower, "charging") || strings.Contains(lower, "charged") + return &percent, charging +} + func cpuName() string { name, err := unix.Sysctl("machdep.cpu.brand_string") if err != nil || name == "" { diff --git a/Overlord-Client/cmd/agent/sysinfo/sysinfo_linux.go b/Overlord-Client/cmd/agent/sysinfo/sysinfo_linux.go index 8429858..2c83ed0 100644 --- a/Overlord-Client/cmd/agent/sysinfo/sysinfo_linux.go +++ b/Overlord-Client/cmd/agent/sysinfo/sysinfo_linux.go @@ -11,13 +11,74 @@ import ( ) func collectPlatform() Info { + percent, charging := batteryStatus() return Info{ - CPU: cpuName(), - GPU: gpuName(), - RAM: totalRAM(), + CPU: cpuName(), + GPU: gpuName(), + RAM: totalRAM(), + BatteryPercent: percent, + BatteryCharging: charging, } } +func OSName() string { + data, err := os.ReadFile("/etc/os-release") + if err != nil { + return "Linux" + } + values := map[string]string{} + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + values[parts[0]] = strings.Trim(strings.TrimSpace(parts[1]), `"`) + } + if pretty := strings.TrimSpace(values["PRETTY_NAME"]); pretty != "" { + return pretty + } + if name := strings.TrimSpace(values["NAME"]); name != "" { + if version := strings.TrimSpace(values["VERSION_ID"]); version != "" { + return name + " " + version + } + return name + } + return "Linux" +} + +func batteryStatus() (*int, bool) { + entries, err := os.ReadDir("/sys/class/power_supply") + if err != nil { + return nil, false + } + for _, entry := range entries { + if !strings.HasPrefix(entry.Name(), "BAT") { + continue + } + base := "/sys/class/power_supply/" + entry.Name() + capacityRaw, err := os.ReadFile(base + "/capacity") + if err != nil { + continue + } + var percent int + if _, err := fmt.Sscanf(strings.TrimSpace(string(capacityRaw)), "%d", &percent); err != nil { + continue + } + if percent < 0 || percent > 100 { + continue + } + statusRaw, _ := os.ReadFile(base + "/status") + status := strings.ToLower(strings.TrimSpace(string(statusRaw))) + charging := status == "charging" || status == "full" + return &percent, charging + } + return nil, false +} + func cpuName() string { data, err := os.ReadFile("/proc/cpuinfo") if err != nil { diff --git a/Overlord-Client/cmd/agent/sysinfo/sysinfo_windows.go b/Overlord-Client/cmd/agent/sysinfo/sysinfo_windows.go index 8944ffe..784d5e5 100644 --- a/Overlord-Client/cmd/agent/sysinfo/sysinfo_windows.go +++ b/Overlord-Client/cmd/agent/sysinfo/sysinfo_windows.go @@ -14,13 +14,70 @@ import ( ) func collectPlatform() Info { + percent, charging := batteryStatus() return Info{ - CPU: cpuName(), - GPU: gpuName(), - RAM: totalRAM(), + CPU: cpuName(), + GPU: gpuName(), + RAM: totalRAM(), + BatteryPercent: percent, + BatteryCharging: charging, } } +func OSName() string { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, + `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + return "Windows" + } + defer k.Close() + + product, _, _ := k.GetStringValue("ProductName") + display, _, _ := k.GetStringValue("DisplayVersion") + build, _, _ := k.GetStringValue("CurrentBuildNumber") + + name := strings.TrimSpace(product) + if name == "" { + name = "Windows" + } + var buildNumber int + _, _ = fmt.Sscanf(strings.TrimSpace(build), "%d", &buildNumber) + if buildNumber >= 22000 && !strings.Contains(name, "11") { + name = strings.Replace(name, "Windows 10", "Windows 11", 1) + if !strings.Contains(name, "11") { + name = "Windows 11" + } + } + if display = strings.TrimSpace(display); display != "" { + name += " " + display + } + return name +} + +func batteryStatus() (*int, bool) { + type systemPowerStatus struct { + ACLineStatus byte + BatteryFlag byte + BatteryLifePercent byte + SystemStatusFlag byte + BatteryLifeTime uint32 + BatteryFullLifeTime uint32 + } + + kernel32 := windows.NewLazySystemDLL("kernel32.dll") + getSystemPowerStatus := kernel32.NewProc("GetSystemPowerStatus") + var status systemPowerStatus + ret, _, _ := getSystemPowerStatus.Call(uintptr(unsafe.Pointer(&status))) + if ret == 0 || status.BatteryLifePercent == 255 || status.BatteryFlag == 128 { + return nil, false + } + percent := int(status.BatteryLifePercent) + if percent < 0 || percent > 100 { + return nil, false + } + return &percent, status.ACLineStatus == 1 +} + func cpuName() string { k, err := registry.OpenKey(registry.LOCAL_MACHINE, `HARDWARE\DESCRIPTION\System\CentralProcessor\0`, registry.QUERY_VALUE) diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 64581c3..961c810 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -1,29 +1,31 @@ package wire type Hello struct { - Type string `msgpack:"type"` - ID string `msgpack:"id"` - HWID string `msgpack:"hwid"` - Host string `msgpack:"host"` - OS string `msgpack:"os"` - Arch string `msgpack:"arch"` - HostArch string `msgpack:"hostArch,omitempty"` - Version string `msgpack:"version"` - User string `msgpack:"user"` - Monitors int `msgpack:"monitors"` - MonitorInfo []MonitorInfo `msgpack:"monitorInfo,omitempty"` - Country string `msgpack:"country,omitempty"` - BuildTag string `msgpack:"buildTag,omitempty"` - PublicKey string `msgpack:"publicKey,omitempty"` - Signature string `msgpack:"signature,omitempty"` - InMemory bool `msgpack:"inMemory,omitempty"` - CPU string `msgpack:"cpu,omitempty"` - GPU string `msgpack:"gpu,omitempty"` - RAM string `msgpack:"ram,omitempty"` - IsAdmin bool `msgpack:"isAdmin,omitempty"` - Elevation string `msgpack:"elevation,omitempty"` - Permissions map[string]bool `msgpack:"permissions,omitempty"` - PublicIP string `msgpack:"publicIP,omitempty"` + Type string `msgpack:"type"` + ID string `msgpack:"id"` + HWID string `msgpack:"hwid"` + Host string `msgpack:"host"` + OS string `msgpack:"os"` + Arch string `msgpack:"arch"` + HostArch string `msgpack:"hostArch,omitempty"` + Version string `msgpack:"version"` + User string `msgpack:"user"` + Monitors int `msgpack:"monitors"` + MonitorInfo []MonitorInfo `msgpack:"monitorInfo,omitempty"` + Country string `msgpack:"country,omitempty"` + BuildTag string `msgpack:"buildTag,omitempty"` + PublicKey string `msgpack:"publicKey,omitempty"` + Signature string `msgpack:"signature,omitempty"` + InMemory bool `msgpack:"inMemory,omitempty"` + CPU string `msgpack:"cpu,omitempty"` + GPU string `msgpack:"gpu,omitempty"` + RAM string `msgpack:"ram,omitempty"` + BatteryPercent *int `msgpack:"batteryPercent,omitempty"` + BatteryCharging bool `msgpack:"batteryCharging,omitempty"` + IsAdmin bool `msgpack:"isAdmin,omitempty"` + Elevation string `msgpack:"elevation,omitempty"` + Permissions map[string]bool `msgpack:"permissions,omitempty"` + PublicIP string `msgpack:"publicIP,omitempty"` } type EnrollmentChallenge struct { diff --git a/Overlord-Server/public/assets/data.js b/Overlord-Server/public/assets/data.js index 58f4a3a..9d8ecb7 100644 --- a/Overlord-Server/public/assets/data.js +++ b/Overlord-Server/public/assets/data.js @@ -6,6 +6,9 @@ const FALLBACK_POLL_MS = 30000; const PREF_REFRESH_KEY = "overlord_refresh_interval_seconds"; let pollTimer = null; let render = () => {}; +let lastClientData = null; +const pageCache = new Map(); +const PAGE_CACHE_LIMIT = 8; let dashboardWs = null; let dashboardWsConnected = false; let wsReconnectTimer = null; @@ -42,6 +45,45 @@ export function registerRenderer(fn) { render = fn; } +function clientQueryParams(page = state.page) { + return new URLSearchParams({ + page: String(page), + pageSize: String(state.pageSize), + q: state.searchTerm, + sort: state.sort, + status: state.filterStatus || "all", + os: state.filterOs || "all", + country: state.filterCountry || "all", + group: state.filterGroup || "all", + }); +} + +function rememberPage(key, data) { + pageCache.set(key, data); + while (pageCache.size > PAGE_CACHE_LIMIT) { + pageCache.delete(pageCache.keys().next().value); + } +} + +async function prefetchClientPage(page) { + if (page < 1) return; + const params = clientQueryParams(page); + const key = params.toString(); + if (pageCache.has(key)) return; + try { + const res = await fetch(`/api/clients?${key}`); + if (!res.ok) return; + rememberPage(key, await res.json()); + } catch {} +} + +function prefetchAdjacentPages(data) { + const totalPages = Math.max(1, Math.ceil((Number(data.total) || 0) / (Number(data.pageSize) || state.pageSize || 1))); + const current = Number(data.page) || state.page; + if (current < totalPages) prefetchClientPage(current + 1); + if (current > 1) prefetchClientPage(current - 1); +} + export async function loadWithOptions(options = {}) { const { force = false, reorder = false } = options; if (state.isLoading) { @@ -51,19 +93,19 @@ export async function loadWithOptions(options = {}) { } state.isLoading = true; try { - const params = new URLSearchParams({ - page: String(state.page), - pageSize: String(state.pageSize), - q: state.searchTerm, - sort: state.sort, - status: state.filterStatus || "all", - os: state.filterOs || "all", - country: state.filterCountry || "all", - group: state.filterGroup || "all", - }); - const res = await fetch(`/api/clients?${params.toString()}`); + const params = clientQueryParams(); + const cacheKey = params.toString(); + const cached = pageCache.get(cacheKey); + if (force && cached) { + lastClientData = cached; + render(cached, { reorder, fromCache: true }); + } + + const res = await fetch(`/api/clients?${cacheKey}`); if (!res.ok) throw new Error(`status ${res.status}`); const data = await res.json(); + lastClientData = data; + rememberPage(cacheKey, data); updateOsFilter(data.items); @@ -73,6 +115,7 @@ export async function loadWithOptions(options = {}) { } state.lastDigest = digest; render(data, { reorder }); + prefetchAdjacentPages(data); const pag = document.getElementById("pagination"); if (pag) pag.style.visibility = ""; } catch (err) { @@ -89,6 +132,12 @@ export async function loadWithOptions(options = {}) { } } +export function renderCachedClients(options = {}) { + if (!lastClientData) return false; + render(lastClientData, { ...options, fromCache: true }); + return true; +} + function updateOsFilter(items) { const osSelect = document.getElementById("filter-os"); if (!osSelect) return; diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 0182d00..c9c6291 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -1085,9 +1085,9 @@ article.card[data-bookmarked="true"] { } .input-row:focus-within { - border-color: rgba(92, 145, 255, 0.5); + border-color: rgba(100, 116, 139, 0.82); background: rgba(255, 255, 255, 0.08); - box-shadow: 0 0 0 3px rgba(92, 145, 255, 0.1); + box-shadow: none; } .input-row i { @@ -1096,7 +1096,7 @@ article.card[data-bookmarked="true"] { } .input-row:focus-within i { - color: #7a5bff; + color: #94a3b8; } .input-row input { @@ -1252,6 +1252,25 @@ article.card[data-bookmarked="true"] { opacity: 0; } +.large-client-set .card-animate, +.large-client-set .cv-row, +.large-client-set .cv-card, +.large-client-set .cv-trow, +.large-client-set .cv-actions, +.large-client-set .cv-card-actions, +.large-client-set .cv-card-check, +.large-client-set .layout-toggle-btn, +.large-client-set .dashboard-menu summary, +.large-client-set .dashboard-menu-panel, +.large-client-set .cv-expand-btn i { + animation: none !important; + transition: none !important; +} + +.large-client-set .cv-card:hover { + transform: none; +} + @keyframes cardFlipFall { 0% { opacity: 1; @@ -1885,6 +1904,7 @@ mark.search-current { #filter-status, #filter-os, +#filter-group, #sort { transition: border-color 150ms ease, @@ -1894,10 +1914,11 @@ mark.search-current { #filter-status:focus, #filter-os:focus, +#filter-group:focus, #sort:focus { - border-color: rgba(99, 102, 241, 0.65) !important; - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18); - outline: none; + border-color: rgba(100, 116, 139, 0.82) !important; + box-shadow: none !important; + outline: none !important; } #search-shell { @@ -1908,14 +1929,207 @@ mark.search-current { } #search-shell:focus-within { - border-color: rgba(99, 102, 241, 0.65) !important; - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18); + border-color: rgba(100, 116, 139, 0.82) !important; + box-shadow: none !important; +} + +.dashboard-toolbar { + position: relative; + z-index: 20; + display: flex; + align-items: center; + gap: 10px; +} +.dashboard-search { + flex: 1 1 420px; + min-width: 240px; +} +.dashboard-toolbar-actions { + display: inline-flex; + align-items: center; + gap: 6px; + flex: 0 0 auto; +} +.dashboard-icon-btn, +.dashboard-toggle, +.dashboard-menu > summary { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 8px; + border: 1px solid rgba(71, 85, 105, 0.72); + background: #172033; + color: #cbd5e1; + cursor: pointer; + transition: + transform 150ms ease, + border-color 150ms ease, + background-color 150ms ease, + color 150ms ease; +} +.dashboard-icon-btn:hover, +.dashboard-toggle:hover, +.dashboard-menu > summary:hover { + transform: translateY(-1px); + border-color: rgba(129, 140, 248, 0.55); + background: #1f2a3e; + color: #f8fafc; +} +.dashboard-toggle input { + position: absolute; + opacity: 0; + pointer-events: none; +} +.dashboard-toggle:has(input:not(:checked)) { + color: #64748b; + background: rgba(15, 23, 42, 0.75); +} +.dashboard-menu { + position: relative; +} +.dashboard-menu > summary { + list-style: none; +} +.dashboard-menu > summary::-webkit-details-marker { + display: none; +} +.dashboard-menu[open] > summary { + color: #818cf8; + border-color: rgba(129, 140, 248, 0.55); + background: #20294a; +} +.dashboard-menu-panel { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 40; + min-width: 220px; + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px; + border: 1px solid rgba(71, 85, 105, 0.78); + border-radius: 8px; + background: #111827; + box-shadow: 0 18px 44px rgba(2, 6, 23, 0.55); +} +.dashboard-filter-panel { + min-width: 250px; +} +.dashboard-menu-panel select, +.dashboard-menu-panel button { + width: 100%; +} +.dashboard-menu-panel select { + appearance: none; + -webkit-appearance: none; + height: 36px; + padding: 0 42px 0 10px; + border: 1px solid rgba(71, 85, 105, 0.7) !important; + border-radius: 6px; + background-color: #172033 !important; + background-image: url("data:image/svg+xml,%3Csvg width='38' height='34' viewBox='0 0 38 34' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 5v24' stroke='%23475569' stroke-width='1'/%3E%3Cpath d='M14 13l5 5 5-5' fill='none' stroke='%2394a3b8' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important; + background-position: right center !important; + background-repeat: no-repeat !important; + color: #dbe4f0; + box-shadow: none !important; +} +.dashboard-menu-panel select option { + background: #111827; + color: #dbe4f0; +} +.dashboard-menu-panel select:hover { + border-color: rgba(129, 140, 248, 0.55) !important; + background-color: #1f2a3e !important; +} +.dashboard-menu-panel select:focus { + border-color: rgba(100, 116, 139, 0.82) !important; + box-shadow: none !important; + outline: none !important; +} +.dashboard-menu-panel #country-picker-btn { + justify-content: space-between; + width: 100%; + min-height: 36px; + padding: 0 10px; + background: #172033 !important; + border-color: rgba(71, 85, 105, 0.7) !important; + box-shadow: none !important; +} +.dashboard-menu-panel #country-picker-btn:hover { + border-color: rgba(129, 140, 248, 0.55) !important; + background: #1f2a3e !important; +} +.dashboard-menu-panel #country-picker-btn .fa-chevron-down { + align-self: stretch; + display: inline-flex; + align-items: center; + margin: 0 -2px 0 auto; + padding-left: 10px; + border-left: 1px solid rgba(71, 85, 105, 0.7); +} +.dashboard-menu-panel #country-picker-btn:hover .fa-chevron-down { + border-left-color: rgba(129, 140, 248, 0.45); +} +#country-picker-panel .p-2 > div { + background: #172033 !important; + border-color: transparent !important; + box-shadow: none !important; +} +#country-picker-panel .p-2 > div:focus-within { + border-color: transparent !important; + box-shadow: none !important; +} +#country-search, +#country-search:focus { + background: transparent !important; + border-color: transparent !important; + box-shadow: none !important; + outline: none !important; +} +.dashboard-fields-panel { + display: grid; + grid-template-columns: repeat(2, minmax(92px, 1fr)); + gap: 7px 10px; + min-width: 260px; +} +.dashboard-fields-panel label { + display: inline-flex; + align-items: center; + gap: 7px; + min-width: 0; + color: #cbd5e1; + font-size: 12px; +} +.dashboard-fields-panel input { + width: 14px; + height: 14px; + accent-color: #6366f1; +} + +@media (max-width: 820px) { + .dashboard-toolbar { + align-items: stretch; + flex-direction: column; + } + .dashboard-search { + flex-basis: auto; + width: 100%; + } + .dashboard-toolbar-actions { + width: 100%; + justify-content: flex-end; + flex-wrap: wrap; + } } #select-all, #clear-selection, #prev, -#next { +#next, +#page-jump-go { transition: transform 150ms ease, border-color 150ms ease, @@ -1925,12 +2139,70 @@ mark.search-current { #select-all:hover, #clear-selection:hover, #prev:hover, -#next:hover { +#next:hover, +#page-jump-go:hover { transform: translateY(-1px); border-color: rgba(99, 102, 241, 0.45) !important; box-shadow: 0 8px 18px rgba(15, 23, 42, 0.4); } +.page-jump { + display: inline-flex; + align-items: center; + gap: 5px; + height: 40px; + padding: 0 5px 0 10px; + border: 1px solid rgba(71, 85, 105, 0.62); + border-radius: 8px; + background: rgba(15, 23, 42, 0.72); + color: #94a3b8; + font-size: 12px; +} + +.page-jump label, +.page-jump-total { + line-height: 1; + white-space: nowrap; +} + +.page-jump input { + width: 4ch; + min-width: 4ch; + max-width: 10ch; + height: 28px; + padding: 0 4px; + border: 1px solid rgba(71, 85, 105, 0.72); + border-radius: 6px; + background: #0f172a; + color: #e2e8f0; + text-align: center; + font-variant-numeric: tabular-nums; + outline: none; + box-shadow: none; +} + +.page-jump input:focus { + border-color: rgba(100, 116, 139, 0.9); + box-shadow: none; +} + +.page-jump input::-webkit-outer-spin-button, +.page-jump input::-webkit-inner-spin-button { + margin: 0; +} + +.page-jump button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 28px; + border: 1px solid rgba(71, 85, 105, 0.72); + border-radius: 6px; + background: #172033; + color: #cbd5e1; +} + #bulk-toolbar { background: radial-gradient( @@ -1969,8 +2241,8 @@ mark.search-current { --ui-surface-strong: rgba(11, 17, 33, 0.9); --ui-surface-elevated: rgba(14, 22, 40, 0.94); --ui-border: rgba(148, 163, 184, 0.2); - --ui-border-strong: rgba(99, 102, 241, 0.4); - --ui-ring: rgba(99, 102, 241, 0.28); + --ui-border-strong: rgba(100, 116, 139, 0.82); + --ui-ring: transparent; --ui-shadow: 0 14px 34px rgba(2, 6, 23, 0.35); } @@ -2070,6 +2342,7 @@ textarea, #search, #filter-status, #filter-os, +#filter-group, #sort, #console-input { background-color: var(--ui-surface); @@ -2089,11 +2362,12 @@ textarea:focus, .input-row:focus-within, #filter-status:focus, #filter-os:focus, +#filter-group:focus, #sort:focus, #console-input:focus { border-color: var(--ui-border-strong) !important; - box-shadow: 0 0 0 3px var(--ui-ring); - outline: none; + box-shadow: none !important; + outline: none !important; } .pill, @@ -2691,6 +2965,14 @@ tbody tr:hover { .cv-tone-pill-arch { color: #67e8f9; } .cv-tone-pill-kali { color: #7dd3fc; } .cv-tone-pill-fedora { color: #bfdbfe; } +.cv-tone-pill-redhat { color: #fca5a5; } +.cv-tone-pill-centos { color: #c4b5fd; } +.cv-tone-pill-suse { color: #86efac; } +.cv-tone-pill-raspi { color: #f9a8d4; } +.cv-tone-pill-freebsd { color: #fca5a5; } +.cv-tone-pill-alpine { color: #7dd3fc; } +.cv-tone-pill-manjaro { color: #6ee7b7; } +.cv-tone-pill-mint { color: #86efac; } .cv-tone-pill-linux { color: #86efac; } .cv-tone-pill-x64 { color: #5eead4; } .cv-tone-pill-arm { color: #7dd3fc; } @@ -2698,6 +2980,48 @@ tbody tr:hover { .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; } +.cv-os .cv-os-icon, +.cv-card-meta-bit .cv-os-icon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 13px; + height: 13px; + flex: 0 0 13px; + font-family: inherit; + font-size: 0; + line-height: 1; +} +.cv-os .cv-os-icon::before, +.cv-card-meta-bit .cv-os-icon::before { + content: ""; + display: block; +} +.cv-os-icon-arch::before { + width: 12px; + height: 12px; + background: #67e8f9; + clip-path: polygon(50% 3%, 95% 96%, 70% 82%, 61% 62%, 50% 39%, 39% 62%, 30% 82%, 5% 96%); +} +.cv-os-icon-mint::before { + width: 13px; + height: 13px; + border: 1px solid rgba(134, 239, 172, 0.9); + border-radius: 3px; + background: #16351f; + box-shadow: inset 0 0 0 2px rgba(134, 239, 172, 0.16); +} +.cv-os-icon-mint::after { + content: "LM"; + position: absolute; + font-family: Inter, system-ui, sans-serif; + font-size: 6px; + font-weight: 800; + line-height: 1; + color: #86efac; + letter-spacing: 0; +} /* Arch chip in table variant */ .clients-table .cv-arch-chip { @@ -2720,7 +3044,7 @@ tbody tr:hover { .clients-table thead th { position: sticky; top: 0; - z-index: 1; + z-index: 0; background: var(--cv-bg) !important; color: var(--cv-text-mute); font-size: 11px; @@ -2826,7 +3150,7 @@ tbody tr:hover { .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); + box-shadow: none; } .cv-card-header { @@ -3024,6 +3348,103 @@ tbody tr:hover { } .cv-card-hw-bit i { color: var(--cv-text-dim); font-size: 10px; } +.cv-cpu-badge { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + max-width: 100%; + height: 24px; + padding: 3px 8px; + border-radius: 5px; + border: 1px solid rgba(255, 255, 255, 0.12); + font-size: 11px; + line-height: 1; + white-space: nowrap; +} + +.cv-card-hw .cv-cpu-badge { flex: 0 1 auto; } +.cv-cpu-badge span, +.cv-cpu-badge strong { + overflow: hidden; + text-overflow: ellipsis; +} +.cv-cpu-apple { + background: #050505; + color: #ffffff; +} +.cv-cpu-apple i { color: #ffffff; font-size: 13px; } +.cv-cpu-apple strong { font-weight: 800; letter-spacing: 0; } +.cv-cpu-intel { + background: #0068b5; + color: #ffffff; + text-transform: uppercase; +} +.cv-cpu-intel strong { + font-weight: 800; + text-transform: lowercase; + letter-spacing: 0; +} +.cv-cpu-amd { + background: #151515; + color: #ffffff; +} +.cv-cpu-amd strong { color: #f87171; letter-spacing: 0; } +.cv-cpu-generic { + background: rgba(255, 255, 255, 0.05); + color: var(--cv-text-mute); +} +.cv-cpu-generic i { color: var(--cv-text-dim); } + +.cv-battery { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + height: 24px; + color: var(--cv-text-mute); + white-space: nowrap; +} +.cv-battery-shell { + position: relative; + width: 28px; + height: 14px; + border: 1px solid currentColor; + border-radius: 3px; + padding: 2px; + opacity: 0.95; +} +.cv-battery-shell::after { + content: ""; + position: absolute; + top: 3px; + right: -4px; + width: 3px; + height: 6px; + border-radius: 0 2px 2px 0; + background: currentColor; +} +.cv-battery-fill { + display: block; + height: 100%; + min-width: 2px; + border-radius: 2px; + background: #86efac; +} +.cv-battery.is-low { color: #fb7185; } +.cv-battery.is-mid .cv-battery-fill { background: #fcd34d; } +.cv-battery.is-charging .cv-battery-fill { background: #60a5fa; } +.cv-battery.is-low .cv-battery-fill { background: #fb7185; } +.cv-battery-text { + display: inline-flex; + align-items: center; + gap: 3px; + font-variant-numeric: tabular-nums; + color: var(--cv-text); +} +.cv-battery-text i { color: #60a5fa; font-size: 9px; } +.cv-battery-label { color: var(--cv-text-dim); } + .cv-text-dim { color: var(--cv-text-dim); } .cv-card-actions { @@ -3293,8 +3714,7 @@ body.sb-collapsed .sb-collapse-btn i { color: #e2e8f0; } .sb-link:focus-visible { - outline: 2px solid #3b82f6; - outline-offset: 1px; + outline: none; } /* Collapsed: center icon, fill the rail */ body.sb-collapsed .sb-link { @@ -3769,8 +4189,7 @@ body.sb-open #sb-backdrop { display: block; } color: #e2e8f0; } .sb-group-btn:focus-visible { - outline: 2px solid #3b82f6; - outline-offset: 1px; + outline: none; } .sb-chevron { @@ -3868,8 +4287,7 @@ body.sb-collapsed .sb-link-child { transform: scale(1.08); } #nav-reveal-btn:focus-visible { - outline: 2px solid #3b82f6; - outline-offset: 2px; + outline: none; } #top-nav { @@ -3942,3 +4360,32 @@ body.sb-ready.nav-hidden #nav-reveal-btn { #nav-reveal-btn:hover::after { opacity: 1; } + +/* Keep dashboard controls below the app navigation while scrolling. */ +#top-nav, +html.nav-pre-sidebar #top-nav, +body.sb-ready #top-nav { + z-index: 200 !important; +} + +#nav-reveal-btn, +#sb-mobile-bar { + z-index: 210 !important; +} + +.dashboard-toolbar { + z-index: 20 !important; +} + +.dashboard-menu-panel, +.nav-dd-menu { + z-index: 40 !important; +} + +/* No browser/theme focus boxes anywhere in the app UI. */ +:where(a, button, input, select, textarea, summary, [tabindex]):focus, +:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible, +:where(.input-row, #search-shell, #country-picker-panel .p-2 > div):focus-within { + outline: none !important; + box-shadow: none !important; +} diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index eefb347..5fafd7d 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -5,6 +5,7 @@ import { openMenu, closeMenu, openModal, wireModalClose, menu } from "./ui.js"; import { registerRenderer, loadWithOptions, + renderCachedClients, startAutoRefresh, sendCommand, requestPreview, @@ -16,14 +17,18 @@ import { ThumbnailLoader } from "./thumbnail-loader.js"; const grid = document.getElementById("grid"); const totalPill = document.getElementById("total-pill"); const pageLabel = document.getElementById("page-label"); +const pageTotal = document.getElementById("page-total"); const prevBtn = document.getElementById("prev"); const nextBtn = document.getElementById("next"); +const pageJumpForm = document.getElementById("page-jump-form"); +const pageJumpInput = document.getElementById("page-jump-input"); const searchInput = document.getElementById("search"); const sortSelect = document.getElementById("sort"); const filterStatusSelect = document.getElementById("filter-status"); const filterOsSelect = document.getElementById("filter-os"); const filterGroupSelect = document.getElementById("filter-group"); const showOfflineToggle = document.getElementById("toggle-offline"); +const displayFieldsMenu = document.getElementById("display-fields-menu"); const selectAllBtn = document.getElementById("select-all"); const clearSelectionBtn = document.getElementById("clear-selection"); const logoutBtn = document.getElementById("logout-btn"); @@ -50,10 +55,38 @@ const PREF_SORT_KEY = "overlord_sort"; const PREF_FILTER_OS_KEY = "overlord_filter_os"; const PREF_FILTER_COUNTRY_KEY = "overlord_filter_country"; const PREF_FILTER_GROUP_KEY = "overlord_filter_group"; +const PREF_DISPLAY_FIELDS_KEY = "overlord_display_fields"; +const DEFAULT_DISPLAY_FIELDS = { + user: true, + ip: true, + hwid: false, + system: true, + version: true, + monitors: true, + group: true, + hardware: true, + battery: true, +}; +let displayFields = { ...DEFAULT_DISPLAY_FIELDS }; let currentUser = null; let contextCard = null; let availableOsList = new Set(); +let rendererInitialized = false; +let lastKnownTotalPages = 1; + +window.setDashboardPageBounds = (currentPage, totalPages) => { + lastKnownTotalPages = Math.max(1, Number(totalPages) || 1); + const safeCurrentPage = Math.max(1, Number(currentPage) || 1); + if (pageLabel) pageLabel.textContent = "Page"; + if (pageTotal) pageTotal.textContent = String(lastKnownTotalPages); + if (!pageJumpInput) return; + pageJumpInput.max = String(lastKnownTotalPages); + pageJumpInput.style.width = `${Math.min(8, Math.max(2, String(lastKnownTotalPages).length, String(safeCurrentPage).length)) + 2}ch`; + if (document.activeElement !== pageJumpInput) { + pageJumpInput.value = String(safeCurrentPage); + } +}; function setServerVersionLabel(version) { if (!serverVersionText) return; @@ -667,13 +700,69 @@ function setLayout(layout) { applyLayoutToggleUI(next); if (rendererSetLayout) rendererSetLayout(next); state.lastDigest = ""; - loadWithOptions({ force: true, reorder: true }); + if (!renderCachedClients({ reorder: true, force: true })) { + loadWithOptions({ force: true, reorder: true }); + } } document.querySelectorAll("#layout-toggle .layout-toggle-btn").forEach((btn) => { btn.addEventListener("click", () => setLayout(btn.dataset.layout)); }); +function loadDisplayFields() { + try { + const saved = JSON.parse(localStorage.getItem(PREF_DISPLAY_FIELDS_KEY) || "null"); + if (saved && typeof saved === "object") { + displayFields = { ...DEFAULT_DISPLAY_FIELDS, ...saved }; + } + } catch { + displayFields = { ...DEFAULT_DISPLAY_FIELDS }; + } +} + +function syncDisplayFieldInputs() { + displayFieldsMenu?.querySelectorAll("input[data-field]").forEach((input) => { + input.checked = displayFields[input.dataset.field] !== false; + }); +} + +function saveDisplayFields() { + localStorage.setItem(PREF_DISPLAY_FIELDS_KEY, JSON.stringify(displayFields)); +} + +function rerenderDisplayFields() { + state.lastDigest = ""; + if (!renderCachedClients({ reorder: true, force: true })) { + loadWithOptions({ force: true, reorder: true }); + } +} + +loadDisplayFields(); +syncDisplayFieldInputs(); + +displayFieldsMenu?.addEventListener("change", (e) => { + const input = e.target.closest("input[data-field]"); + if (!input) return; + displayFields[input.dataset.field] = input.checked; + saveDisplayFields(); + rerenderDisplayFields(); +}); + +document.addEventListener("click", (e) => { + document.querySelectorAll(".dashboard-menu[open]").forEach((details) => { + if (!details.contains(e.target)) details.removeAttribute("open"); + }); +}); + +document.querySelectorAll(".dashboard-menu").forEach((details) => { + details.addEventListener("toggle", () => { + if (!details.open) return; + document.querySelectorAll(".dashboard-menu[open]").forEach((other) => { + if (other !== details) other.removeAttribute("open"); + }); + }); +}); + let dashboardThumbnailLoader = null; async function isDashboardThumbnailEnabled() { @@ -729,6 +818,8 @@ async function setupDashboardThumbnailLoader() { } function initializeRenderer() { + if (rendererInitialized) return; + rendererInitialized = true; 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"); @@ -750,6 +841,7 @@ function initializeRenderer() { requestThumbnail, pingClient: (id) => sendCommand(id, "ping"), userRole: currentUser?.role, + getDisplayFields: () => displayFields, }); rendererSetLayout = rSetLayout; registerRenderer(renderMerge); @@ -758,7 +850,7 @@ function initializeRenderer() { loadWithOptions(); startAutoRefresh(); - if (typeof anime !== "undefined") { + if (typeof anime !== "undefined" && !document.documentElement.classList.contains("large-client-set")) { anime .timeline({ easing: "easeOutQuad" }) .add({ @@ -874,7 +966,7 @@ import("./country-picker.js").then(({ initCountryPicker }) => { } const savedSort = localStorage.getItem(PREF_SORT_KEY); - const validSorts = ["stable", "last_seen_desc", "host_asc", "ping_asc", "ping_desc", "country_asc", "country_desc", "group_asc", "group_desc"]; + const validSorts = ["stable", "last_seen_desc", "host_asc", "ping_asc", "ping_desc", "country_asc", "country_desc", "group_asc", "group_desc", "admin_first"]; if (savedSort && validSorts.includes(savedSort)) { state.sort = savedSort; if (sortSelect) sortSelect.value = savedSort; @@ -1448,6 +1540,17 @@ nextBtn?.addEventListener("click", () => { loadWithOptions({ force: true, reorder: true }); }); +pageJumpForm?.addEventListener("submit", (e) => { + e.preventDefault(); + const requested = Math.trunc(Number(pageJumpInput?.value || 1)); + const targetPage = Math.min(lastKnownTotalPages, Math.max(1, Number.isFinite(requested) ? requested : 1)); + if (pageJumpInput) pageJumpInput.value = String(targetPage); + if (targetPage === state.page) return; + state.page = targetPage; + state.lastDigest = ""; + loadWithOptions({ force: true, reorder: true }); +}); + window.addEventListener("click", (e) => { const target = e.target; if (target.closest && target.closest(".command-btn")) return; diff --git a/Overlord-Server/public/assets/purgatory-ui.js b/Overlord-Server/public/assets/purgatory-ui.js index 0851908..85efae0 100644 --- a/Overlord-Server/public/assets/purgatory-ui.js +++ b/Overlord-Server/public/assets/purgatory-ui.js @@ -1,7 +1,7 @@ -import { TabulatorFull as Tabulator } from "/vendor/tabulator/tabulator_esm.min.js"; - -const enrollmentTableEl = document.getElementById("enrollment-table"); +const body = document.getElementById("enrollment-body"); +const loadingRow = document.getElementById("enrollment-loading"); const emptyEl = document.getElementById("enrollment-empty"); +const selectAllEl = document.getElementById("select-all"); const bulkApproveBtn = document.getElementById("bulk-approve-btn"); const bulkDenyBtn = document.getElementById("bulk-deny-btn"); const statPending = document.getElementById("stat-pending"); @@ -10,9 +10,9 @@ const statDenied = document.getElementById("stat-denied"); const statBannedIps = document.getElementById("stat-banned-ips"); const searchInput = document.getElementById("search-input"); const bannedIpsSection = document.getElementById("banned-ips-section"); -const bannedIpsTableEl = document.getElementById("banned-ips-table"); +const bannedIpsBody = document.getElementById("banned-ips-body"); const bannedIpsEmpty = document.getElementById("banned-ips-empty"); -const clientsTable = document.getElementById("clients-table-wrap"); +const clientsTable = document.getElementById("enrollment-table")?.closest(".bg-slate-900\\/50"); const addBanBtn = document.getElementById("add-ban-btn"); const manualBanForm = document.getElementById("manual-ban-form"); const banIpInput = document.getElementById("ban-ip-input"); @@ -44,10 +44,8 @@ const denyReasonModalBackdrop = document.getElementById("deny-reason-modal-backd let currentFilter = "pending"; let searchQuery = ""; let clients = []; -let enrollmentTable = null; -let bannedIpsTable = null; -const selectedIds = new Set(); const expandedCells = new Set(); // tracks "clientId:field" keys +let lastEnrollmentDigest = ""; const SUSPICIOUS_FLAG_LABELS = { hwid_flood: "HWID Flood (40+ same hardware ID)", @@ -103,8 +101,9 @@ async function loadStats() { } async function loadClients() { + const hasRows = body.querySelector("tr[data-id]"); + loadingRow.classList.toggle("hidden", !!hasRows); emptyEl.classList.add("hidden"); - if (enrollmentTable) enrollmentTable.replaceData([]); const fetchFilter = currentFilter === "suspicious" ? "all" : currentFilter; try { @@ -136,26 +135,84 @@ async function loadClients() { }) : base; + loadingRow.classList.add("hidden"); + const showBulkActions = (currentFilter === "pending" || currentFilter === "suspicious") && filtered.some((c) => (c.enrollmentStatus || "pending") === "pending"); approveAllBtn.classList.toggle("hidden", !showBulkActions); denyAllBtn.classList.toggle("hidden", !showBulkActions); - const visibleIds = new Set(filtered.map((c) => c.id)); - for (const id of selectedIds) { - if (!visibleIds.has(id)) selectedIds.delete(id); - } - if (filtered.length === 0) { + lastEnrollmentDigest = `empty:${currentFilter}:${q}`; + body.querySelectorAll("tr:not(#enrollment-loading)").forEach((r) => r.remove()); emptyEl.classList.remove("hidden"); updateBulkButtons(); return; } - if (enrollmentTable) enrollmentTable.replaceData(filtered); + const nextDigest = tableDigest(filtered, [ + "id", + "host", + "user", + "os", + "cpu", + "gpu", + "ram", + "ip", + "country", + "keyFingerprint", + "lastSeen", + "enrollmentStatus", + "denyReason", + "suspiciousFlags", + ]); + if (nextDigest === lastEnrollmentDigest) { + updateBulkButtons(); + return; + } + lastEnrollmentDigest = nextDigest; + + const selectedBeforeRender = new Set(getSelectedIds()); + body.querySelectorAll("tr:not(#enrollment-loading)").forEach((r) => r.remove()); + + for (const c of filtered) { + const tr = document.createElement("tr"); + tr.className = "hover:bg-slate-800/40 transition-colors"; + tr.dataset.id = c.id; + + const statusPill = statusBadgeWithReason(c.enrollmentStatus || "pending", c.denyReason); + const fp = c.keyFingerprint ? c.keyFingerprint.substring(0, 16) + "..." : "-"; + const lastSeen = c.lastSeen ? timeAgo(c.lastSeen) : "-"; + + tr.innerHTML = ` + + ${esc(c.host || c.id)}${suspiciousBadges(c.suspiciousFlags)} + ${esc(c.user || "-")} + ${esc(c.os || "-")} + ${expandableCell(c.id, "cpu", c.cpu)} + ${c.gpu ? dedupeGpu(c.gpu) : esc("-")} + ${esc(c.ram || "-")} + ${esc(c.ip || "-")} + ${esc(c.country || "-")} + ${esc(fp)} + ${lastSeen} + ${statusPill} +
${actionButtons(c)}
+ `; + tr.classList.add("cursor-pointer"); + body.appendChild(tr); + } updateBulkButtons(); } +function tableDigest(items, fields) { + return JSON.stringify(items.map((item) => { + const row = {}; + for (const field of fields) row[field] = item[field] ?? null; + return row; + })); +} + function statusBadge(status) { const map = { pending: @@ -232,129 +289,6 @@ function expandableCell(clientId, field, value) { ``; } -function selectionHeaderFormatter() { - const rows = enrollmentTable?.getData() || []; - const allChecked = rows.length > 0 && rows.every((row) => selectedIds.has(row.id)); - return ``; -} - -function rowSelectionFormatter(cell) { - const id = cell.getRow().getData().id; - return ``; -} - -function initEnrollmentTable() { - if (!enrollmentTableEl) return; - enrollmentTable = new Tabulator(enrollmentTableEl, { - data: [], - height: "32rem", - layout: "fitDataStretch", - placeholder: "No clients in this category.", - index: "id", - virtualDom: true, - columns: [ - { - title: "", - field: "id", - width: 54, - hozAlign: "center", - headerSort: false, - headerFormatter: selectionHeaderFormatter, - formatter: rowSelectionFormatter, - }, - { - title: "Host", - field: "host", - width: 190, - formatter: (cell) => { - const c = cell.getRow().getData(); - return `${esc(c.host || c.id)}${suspiciousBadges(c.suspiciousFlags)}`; - }, - }, - { title: "User", field: "user", width: 140, formatter: (cell) => esc(cell.getValue() || "-") }, - { title: "OS", field: "os", width: 120, formatter: (cell) => esc(cell.getValue() || "-") }, - { - title: "CPU", - field: "cpu", - width: 210, - formatter: (cell) => { - const c = cell.getRow().getData(); - return expandableCell(c.id, "cpu", c.cpu); - }, - }, - { - title: "GPU", - field: "gpu", - width: 220, - formatter: (cell) => cell.getValue() ? dedupeGpu(cell.getValue()) : esc("-"), - }, - { title: "RAM", field: "ram", width: 100, formatter: (cell) => esc(cell.getValue() || "-") }, - { title: "IP", field: "ip", width: 140, formatter: (cell) => esc(cell.getValue() || "-") }, - { title: "Country", field: "country", width: 120, formatter: (cell) => esc(cell.getValue() || "-") }, - { - title: "Key Fingerprint", - field: "keyFingerprint", - width: 170, - formatter: (cell) => `${esc(cell.getValue() ? cell.getValue().substring(0, 16) + "..." : "-")}`, - }, - { - title: "Last Seen", - field: "lastSeen", - width: 120, - sorter: "number", - formatter: (cell) => `${cell.getValue() ? timeAgo(cell.getValue()) : "-"}`, - }, - { - title: "Status", - field: "enrollmentStatus", - width: 150, - formatter: (cell) => { - const c = cell.getRow().getData(); - return statusBadgeWithReason(c.enrollmentStatus || "pending", c.denyReason); - }, - }, - { - title: "Actions", - field: "id", - width: 300, - headerSort: false, - hozAlign: "right", - formatter: (cell) => `
${actionButtons(cell.getRow().getData())}
`, - }, - ], - }); -} - -function initBannedIpsTable() { - if (!bannedIpsTableEl) return; - bannedIpsTable = new Tabulator(bannedIpsTableEl, { - data: [], - height: "24rem", - layout: "fitColumns", - placeholder: "No banned IPs.", - index: "ip", - columns: [ - { title: "IP Address", field: "ip", width: 180, formatter: (cell) => `${esc(cell.getValue())}` }, - { title: "Reason", field: "reason", formatter: (cell) => esc(cell.getValue() || "-") }, - { - title: "Banned At", - field: "createdAt", - width: 150, - sorter: "number", - formatter: (cell) => `${cell.getValue() ? timeAgo(cell.getValue()) : "-"}`, - }, - { - title: "Actions", - field: "ip", - width: 120, - headerSort: false, - hozAlign: "right", - formatter: (cell) => ``, - }, - ], - }); -} - function timeAgo(ts) { const diff = Date.now() - ts; if (diff < 60000) return "just now"; @@ -401,24 +335,26 @@ async function bulkAction(action) { } catch (e) { if (window.showToast) window.showToast(`Failed: ${e.message}`, "error"); } - selectedIds.clear(); + selectAllEl.checked = false; await Promise.all([loadClients(), loadStats()]); } function getSelectedIds() { - return [...selectedIds]; + return [...document.querySelectorAll(".row-check:checked")].map( + (el) => el.dataset.id, + ); } function updateBulkButtons() { - const visibleIds = new Set((enrollmentTable?.getData() || []).map((row) => row.id)); - for (const id of selectedIds) { - if (!visibleIds.has(id)) selectedIds.delete(id); - } - const count = selectedIds.size; + const rowChecks = [...document.querySelectorAll(".row-check")]; + const selected = getSelectedIds(); + const count = selected.length; bulkApproveBtn.classList.toggle("hidden", count === 0); bulkDenyBtn.classList.toggle("hidden", count === 0); bulkBanBtn.classList.toggle("hidden", count === 0); - enrollmentTable?.redraw(true); + if (selectAllEl) { + selectAllEl.checked = rowChecks.length > 0 && rowChecks.every((cb) => cb.checked); + } } // ── Tab switching ────────────────────────────────────────────────── @@ -454,7 +390,7 @@ document.querySelectorAll(".enrollment-tab").forEach((tab) => { }); // ── Table delegation ─────────────────────────────────────────────── -enrollmentTableEl?.addEventListener("click", (e) => { +body.addEventListener("click", (e) => { // Expandable CPU/GPU cells const expander = e.target.closest(".hw-expand"); if (expander) { @@ -469,6 +405,19 @@ enrollmentTableEl?.addEventListener("click", (e) => { return; } + const row = e.target.closest("tr[data-id]"); + if ( + row && + !e.target.closest("button, input, a, select, textarea") + ) { + const checkbox = row.querySelector(".row-check"); + if (checkbox) { + checkbox.checked = !checkbox.checked; + updateBulkButtons(); + } + return; + } + const btn = e.target.closest("button"); if (!btn) return; const id = btn.dataset.id; @@ -479,24 +428,11 @@ enrollmentTableEl?.addEventListener("click", (e) => { else if (btn.classList.contains("act-ban-ip")) banClientIp(id); }); -enrollmentTableEl?.addEventListener("change", (e) => { - const selectAll = e.target.closest(".enrollment-select-all"); - if (selectAll) { - const rows = enrollmentTable?.getData() || []; - for (const row of rows) { - if (selectAll.checked) selectedIds.add(row.id); - else selectedIds.delete(row.id); - } - updateBulkButtons(); - return; - } - - const checkbox = e.target.closest(".row-check"); - if (!checkbox) return; - const id = checkbox.dataset.id; - if (!id) return; - if (checkbox.checked) selectedIds.add(id); - else selectedIds.delete(id); +body.addEventListener("change", () => updateBulkButtons()); +selectAllEl.addEventListener("change", () => { + document.querySelectorAll(".row-check").forEach((cb) => { + cb.checked = selectAllEl.checked; + }); updateBulkButtons(); }); @@ -575,8 +511,8 @@ async function banClientIp(clientId) { } async function loadBannedIps() { + bannedIpsBody.innerHTML = ""; bannedIpsEmpty.classList.add("hidden"); - if (bannedIpsTable) bannedIpsTable.replaceData([]); try { const data = await api("/api/enrollment/banned-ips"); @@ -587,13 +523,27 @@ async function loadBannedIps() { return; } - if (bannedIpsTable) bannedIpsTable.replaceData(items); + for (const entry of items) { + const tr = document.createElement("tr"); + tr.className = "hover:bg-slate-800/40 transition-colors"; + tr.innerHTML = ` + ${esc(entry.ip)} + ${esc(entry.reason || "-")} + ${entry.createdAt ? timeAgo(entry.createdAt) : "-"} + + + + `; + bannedIpsBody.appendChild(tr); + } } catch { bannedIpsEmpty.classList.remove("hidden"); } } -bannedIpsTableEl?.addEventListener("click", async (e) => { +bannedIpsBody.addEventListener("click", async (e) => { const btn = e.target.closest("button"); if (!btn) return; if (btn.classList.contains("act-unban")) { @@ -773,8 +723,6 @@ denyReasonModalConfirm.addEventListener("click", async () => { }); // ── Init ─────────────────────────────────────────────────────────── -initEnrollmentTable(); -initBannedIpsTable(); loadSettings(); loadStats(); loadClients(); diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index d5aea30..c3056bf 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -150,10 +150,64 @@ function shortenCpu(raw = "") { .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(); - if (o.includes("windows 11")) return "Win 11"; - if (o.includes("windows 10")) return "Win 10"; + 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"; @@ -175,15 +229,31 @@ export function createRenderer({ requestThumbnail, pingClient, userRole, + getDisplayFields, }) { const isViewer = userRole === "viewer"; - const MAX_ANIMATED_CARDS = 120; + 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") { @@ -194,7 +264,10 @@ export function createRenderer({ function ensureLayoutScaffold() { if (currentLayout === "table") { - if (grid.querySelector("table.clients-table")) return; + const prefs = displayFields(); + const scaffoldDigest = displayDigest(); + if (grid.querySelector("table.clients-table") && tableScaffoldDigest === scaffoldDigest) return; + tableScaffoldDigest = scaffoldDigest; grid.innerHTML = ` @@ -205,9 +278,9 @@ export function createRenderer({ - + ${prefs.system !== false ? `` : ""} - + ${prefs.group !== false ? `` : ""} @@ -215,6 +288,7 @@ export function createRenderer({
Client Status Last seenSystemSystemPingGroupGroupActions
`; } else { + tableScaffoldDigest = ""; const t = grid.querySelector("table.clients-table"); if (t) t.remove(); } @@ -453,14 +527,16 @@ export function createRenderer({ } } if (!needsReorder) return; + const byId = new Map(); + cards.forEach((card) => byId.set(card.dataset.id, card)); items.forEach((client) => { - const card = container.querySelector(`${ROW_SELECTOR}[data-id="${client.id}"]`); + const card = byId.get(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.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}|${!!c.notificationsMuted}`; + 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) { @@ -482,31 +558,40 @@ export function createRenderer({ 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)); - pageLabel.textContent = `Page ${data.page} of ${totalPages}`; + 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 hadCards = container.querySelectorAll(ROW_SELECTOR).length > 0; + 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 = container.querySelector(`${ROW_SELECTOR}[data-id="${client.id}"]`); + const existing = existingById.get(client.id); if (existing) { if (existing.classList.contains("card-uninstalling")) return; const digest = cardDigest(client); @@ -527,7 +612,7 @@ export function createRenderer({ newClients.push(client); }); - Array.from(container.querySelectorAll(ROW_SELECTOR)) + Array.from(existingCards) .filter((el) => !seen.has(el.dataset.id) && !el.classList.contains("card-uninstalling")) .forEach((el) => el.remove()); @@ -537,7 +622,7 @@ export function createRenderer({ if (newClients.length === 0) return; - const allowAnimation = !hadCards && !prefersReducedMotion && items.length <= 1000 && currentLayout !== "table"; + 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; @@ -674,17 +759,21 @@ export function createRenderer({ const displayName = nickname || client.host || deviceId; const userLine = client.user || client.host || deviceId; const hasTagNote = customTag.length > 0 && customTagNote.length > 0; - const hasHwInfo = !!(client.cpu || client.gpu || client.ram); + 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 = [ - ` ${escapeHtml(shortOsLabel(client.os))}`, - `${escapeHtml(arch.label)}`, - ` ${escapeHtml(ver.label)}`, - ` ${client.monitors || 1}`, + 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)}` : "", - client.ip ? ` ${escapeHtml(client.ip)}` : "", - hwid ? ` ${escapeHtml(hwid)}` : "", + showField("ip") && client.ip ? ` ${escapeHtml(client.ip)}` : "", + showField("hwid") && hwid ? ` ${escapeHtml(hwid)}` : "", + batteryIndicator, ].filter(Boolean); const meta = metaParts.join(metaSeparator()); @@ -708,8 +797,8 @@ export function createRenderer({ ${client.elevation === "trustedinstaller" ? `` : ""} ${client.notificationsMuted ? `` : ""} -
${escapeHtml(userLine)}
-
${meta}
+ ${showField("user") ? `
${escapeHtml(userLine)}
` : ""} + ${meta ? `
${meta}
` : ""} ${customTag ? `` : ""} ${hasTagNote ? `` : ""} @@ -717,7 +806,7 @@ export function createRenderer({ ${formatAgo(client.lastSeen)} ${formatPing(client.pingMs)} -
${groupPillHtml(client) || `—`}
+ ${showField("group") ? `
${groupPillHtml(client) || `—`}
` : ""}
${isViewer ? "" : ``} @@ -728,13 +817,14 @@ export function createRenderer({ `; @@ -754,6 +844,11 @@ export function createRenderer({ 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 = ` @@ -779,7 +874,7 @@ export function createRenderer({ ${client.elevation === "trustedinstaller" ? `` : ""} ${client.notificationsMuted ? `` : ""} - ${escapeHtml(userLine)}${client.ip ? `· ${escapeHtml(client.ip)}` : ""} + ${userMeta ? `${userMeta}` : ""}
@@ -787,11 +882,11 @@ export function createRenderer({ ${statusDot(client)} ${client.online ? "Online" : "Offline"} ${formatAgo(client.lastSeen)} - + ${showField("system") ? ` ${escapeHtml(shortOsLabel(client.os))} ${escapeHtml(arch.label)} - + ` : ""} ${formatPing(client.pingMs)} - ${groupPillHtml(client) || `—`} + ${showField("group") ? `${groupPillHtml(client) || `—`}` : ""}
${isViewer ? "" : ``} @@ -821,13 +916,15 @@ export function createRenderer({ 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 showHardware = showField("hardware"); + const cpuHtml = showHardware && client.cpu ? cpuBadgeHtml(client.cpu) : ""; + const batteryIndicator = showField("battery") ? batteryHtml(client, { compact: true }) : ""; const metaParts = [ - ` ${escapeHtml(shortOsLabel(client.os))}`, - `${escapeHtml(arch.label)}`, - ` ${client.monitors || 1}`, - client.version ? ` v${escapeHtml(client.version)}` : "", + 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 = [ @@ -863,19 +960,20 @@ export function createRenderer({ ${escapeHtml(displayName)} ${elevationBadges}
-
${escapeHtml(userLine)}${showHost ? ` @ ${escapeHtml(client.host)}` : ""}
+ ${showField("user") ? `
${escapeHtml(userLine)}${showHost ? ` @ ${escapeHtml(client.host)}` : ""}
` : ""} ${customTag ? `
${escapeHtml(customTag)}
` : ""}
- ${client.ip ? ` ${escapeHtml(client.ip)}` : ""} - ${hwidShort ? ` ${escapeHtml(hwidShort)}` : ""} + ${showField("ip") && client.ip ? ` ${escapeHtml(client.ip)}` : ""} + ${showField("hwid") && hwidShort ? ` ${escapeHtml(hwidShort)}` : ""}
-
+ ${metaParts || (showField("group") && hasGroup) ? `
${metaParts} - ${hasGroup ? `${groupPillHtml(client)}` : ""} -
- ${cpuShort || client.ram ? `
- ${cpuShort ? ` ${escapeHtml(cpuShort)}` : ""} - ${client.ram ? ` ${escapeHtml(client.ram)}` : ""} + ${showField("group") && hasGroup ? `${groupPillHtml(client)}` : ""} +
` : ""} + ${cpuHtml || (showHardware && client.ram) || batteryIndicator ? `
+ ${cpuHtml} + ${showHardware && client.ram ? ` ${escapeHtml(client.ram)}` : ""} + ${batteryIndicator}
` : ""}
${isViewer ? "" : ``} diff --git a/Overlord-Server/public/assets/utils.js b/Overlord-Server/public/assets/utils.js index 5ac84b4..e845789 100644 --- a/Overlord-Server/public/assets/utils.js +++ b/Overlord-Server/public/assets/utils.js @@ -23,6 +23,20 @@ export function digestData(data, { page, pageSize, searchTerm, sort }) { os: c.os, monitors: c.monitors, elevation: c.elevation, + bookmarked: !!c.bookmarked, + isAdmin: !!c.isAdmin, + hwid: c.hwid, + cpu: c.cpu, + gpu: c.gpu, + ram: c.ram, + batteryPercent: c.batteryPercent, + batteryCharging: c.batteryCharging, + groupId: c.groupId, + groupName: c.groupName, + groupColor: c.groupColor, + notificationsMuted: !!c.notificationsMuted, + hasThumbnail: !!c.hasThumbnail, + thumbnailVersion: c.thumbnailVersion, })) || []; return JSON.stringify({ page, diff --git a/Overlord-Server/public/assets/viewUtils.js b/Overlord-Server/public/assets/viewUtils.js index 94450af..1b5623f 100644 --- a/Overlord-Server/public/assets/viewUtils.js +++ b/Overlord-Server/public/assets/viewUtils.js @@ -37,20 +37,40 @@ export function osBadge(osRaw = "") { icon: "fa-linux", tone: "pill-unknown", }; + if (os.includes("windows 11")) + return { label: "W11", icon: "fa-brands fa-windows", tone: "pill-win" }; + if (os.includes("windows 10")) + return { label: "W10", icon: "fa-brands fa-windows", tone: "pill-win" }; if (os.includes("windows")) return { label: "Windows", icon: "fa-brands fa-windows", tone: "pill-win" }; if (os.includes("mac") || os.includes("darwin")) return { label: "macOS", icon: "fa-brands fa-apple", tone: "pill-mac" }; + if (os.includes("linux mint")) + return { label: "Mint", icon: "cv-os-icon cv-os-icon-mint", tone: "pill-mint" }; if (os.includes("ubuntu")) - return { label: "Ubuntu", icon: "fa-brands fa-linux", tone: "pill-ubuntu" }; + return { label: "Ubuntu", icon: "fa-brands fa-ubuntu", tone: "pill-ubuntu" }; if (os.includes("debian")) - return { label: "Debian", icon: "fa-brands fa-linux", tone: "pill-debian" }; + return { label: "Debian", icon: "fa-brands fa-debian", tone: "pill-debian" }; if (os.includes("arch")) - return { label: "Arch", icon: "fa-brands fa-linux", tone: "pill-arch" }; + return { label: "Arch", icon: "cv-os-icon cv-os-icon-arch", tone: "pill-arch" }; if (os.includes("kali")) return { label: "Kali", icon: "fa-brands fa-linux", tone: "pill-kali" }; if (os.includes("fedora")) - return { label: "Fedora", icon: "fa-brands fa-linux", tone: "pill-fedora" }; + return { label: "Fedora", icon: "fa-brands fa-fedora", tone: "pill-fedora" }; + if (os.includes("red hat") || os.includes("rhel")) + return { label: "RHEL", icon: "fa-brands fa-redhat", tone: "pill-redhat" }; + if (os.includes("centos")) + return { label: "CentOS", icon: "fa-brands fa-centos", tone: "pill-centos" }; + if (os.includes("suse") || os.includes("opensuse")) + return { label: "SUSE", icon: "fa-brands fa-suse", tone: "pill-suse" }; + if (os.includes("raspbian") || os.includes("raspberry")) + return { label: "Raspberry Pi", icon: "fa-brands fa-raspberry-pi", tone: "pill-raspi" }; + if (os.includes("freebsd")) + return { label: "FreeBSD", icon: "fa-brands fa-freebsd", tone: "pill-freebsd" }; + if (os.includes("alpine")) + return { label: "Alpine", icon: "fa-brands fa-linux", tone: "pill-alpine" }; + if (os.includes("manjaro")) + return { label: "Manjaro", icon: "fa-brands fa-linux", tone: "pill-manjaro" }; if (os.includes("linux")) return { label: "Linux", icon: "fa-brands fa-linux", tone: "pill-linux" }; return base; diff --git a/Overlord-Server/public/index.html b/Overlord-Server/public/index.html index f97635a..6eeca0a 100644 --- a/Overlord-Server/public/index.html +++ b/Overlord-Server/public/index.html @@ -38,10 +38,10 @@ >0 online
-
+
- - - - -
- - - -
- - - +
+ + + +
@@ -211,7 +183,14 @@ > Prev - +
+ + + / 1 + +
-
-
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + HostUserOSCPUGPURAMIPCountryKey FingerprintLast SeenStatusActions
+ Loading... +
-
+ + + + + + + + + + +
IP AddressReasonBanned AtActions