diff options
| -rw-r--r-- | docs/WINDOWS-PORT.md | 32 | ||||
| -rw-r--r-- | packages/meshbay-client/src/main.js | 10 | ||||
| -rw-r--r-- | packages/meshbay-client/src/preload.js | 8 | ||||
| -rw-r--r-- | packages/meshbay-client/src/tray-icon.png | bin | 504 -> 829 bytes | |||
| -rw-r--r-- | packages/meshbay-client/src/tray-icon@2x.png | bin | 967 -> 1807 bytes | |||
| -rw-r--r-- | packages/meshbay-hub/tests/test_desktop_shell.py | 32 |
6 files changed, 71 insertions, 11 deletions
diff --git a/docs/WINDOWS-PORT.md b/docs/WINDOWS-PORT.md index 8fc169b..e33b6f5 100644 --- a/docs/WINDOWS-PORT.md +++ b/docs/WINDOWS-PORT.md @@ -295,14 +295,25 @@ same no-admin-by-default reason service mode below needs one prompt. #### Service mode (one admin confirmation) -A Scheduled Task via **S4U** (Service For User) logon — -`schtasks /create ... /sc onstart /ru <user> /rp ""`, no `/it` — not a real -Windows Service (`pywin32`/NSSM), because a Service runs under +A Scheduled Task via **S4U** (Service For User) logon — not a real Windows +Service (`pywin32`/NSSM), because a Service runs under LocalSystem/NetworkService, accounts with no normal user profile, so `%LOCALAPPDATA%\meshbay\` would not exist for it. S4U starts at boot with no sign-in and no stored password, and — the whole reason S4U and not LocalSystem — loads the signed-in user's own profile, so nothing in `platform.py` needed to change to support it. + +Registered via `Register-ScheduledTask -LogonType S4U` (the `ScheduledTasks` +PowerShell module), **not** `schtasks /create`: schtasks has no flag naming +the logon type, only inference from whether `/rp` is present, and both +readings broke live on a blank-password account (common on a personal PC, +confirmed 2026-09-05) — `/rp ""` fails schtasks' own credential validation +under Windows' default blank-password policy, and omitting `/rp` registers +"Interactive only" instead of S4U, which installs cleanly but never actually +launches the process, at boot or on demand. `-LogonType S4U` names it +explicitly. Verified live end-to-end after the fix: install, manual start, +and unattended boot-time start (the process already running on next login, +no manual action taken). `packaging/win/service.ps1` + `service-mode.ps1` (one elevation, folds the firewall rules into the same UAC prompt) + `meshbay_node.platform.service_install/_remove/_status/_run/_end` + CLI @@ -554,6 +565,21 @@ alone — Task Scheduler owns that process's creation flags and its own stop semantics are a separate, unverified surface, not something `service.ps1` controls. +### 5.10 System tray (2026-09-05) + +**Scope:** `preload.js` + `src/main.js` (two lines) + `test_desktop_shell.py` + +The GNOME tray (minimise-to-indicator, Start/Stop menu) was built against +`nodeService.status()/stop()/restart()` — the same abstraction the Node +page's own Stop/Restart buttons use, which already had full win32 branches +(§5.3) covering both startup modes. So the tray needed no new logic for +Windows, only its two platform gates widened from Linux-only to +Linux-or-win32: the `tray` capability in `preload.js`, and the +`window:minimize-to-tray` IPC handler in `main.js`. Reuses the existing +`tray-icon.png`/`@2x.png` (already packaged for every OS via `package.json`'s +`files`, no separate Windows list). `test_desktop_shell.py` now pins that +both gates name the same platform set — nothing did before. + --- ## 6. Execution order diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index 273d151..74c8203 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -328,12 +328,13 @@ const HUB_FETCH_TIMEOUT_MS = 30000; let mainWindow = null; // ── System tray ────────────────────────────────────────────────────────────── -// Linux only for now; Windows is being done on that OS. +// Linux and Windows. // // The context menu is not optional. Under libappindicator -- which is how GNOME // shows a tray at all, via the AppIndicator extension -- `tray.on('click')` // never fires: the indicator only opens its menu. A tray whose only affordance -// was a click would be inert on the one desktop this targets. +// was a click would be inert there -- Windows does send it, so the same handler +// restores the window on a plain left click. // // Labels arrive from the renderer rather than being translated here. The locale // files are the interface's, the main process has no i18n, and a second string @@ -413,7 +414,7 @@ function ensureTray(labels) { tray = new Tray(path.join(__dirname, 'tray-icon.png')); tray.setToolTip('MeshBay'); refreshTrayMenu(); - // Harmless where it does nothing (GNOME), useful on desktops that do send it. + // No-op on GNOME (never fires there), the left-click restore on Windows. tray.on('click', showFromTray); if (!trayTimer) trayTimer = setInterval(refreshTrayMenu, TRAY_POLL_MS); return tray; @@ -638,7 +639,8 @@ function registerBridge() { // make "minimise to tray" mean "exit". A hidden window keeps the session, the // transfers and the node connection exactly as they were. ipcMain.handle('window:minimize-to-tray', (_e, labels) => { - if (process.platform !== 'linux' || !mainWindow) return false; + const trayOS = process.platform === 'linux' || process.platform === 'win32'; + if (!trayOS || !mainWindow) return false; ensureTray(labels); mainWindow.hide(); return true; diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js index 1aa16e5..b390bfb 100644 --- a/packages/meshbay-client/src/preload.js +++ b/packages/meshbay-client/src/preload.js @@ -41,10 +41,10 @@ contextBridge.exposeInMainWorld('meshbay', { localFolders: true, nativeSave: true, lanCast: true, - // Linux only for now -- Windows is being done on that OS. Declared per - // platform rather than always: a button that hides the window somewhere the - // desktop shows no indicator would hide it for good. - tray: process.platform === 'linux', + // Linux and Windows have a tray to hide into; declared per platform + // rather than always, so the button never appears somewhere the desktop + // shows no indicator, which would hide the window for good. + tray: process.platform === 'linux' || process.platform === 'win32', }, // Labels are passed in because the main process has no i18n; see main.js. diff --git a/packages/meshbay-client/src/tray-icon.png b/packages/meshbay-client/src/tray-icon.png Binary files differindex 98fd63b..9b54a5d 100644 --- a/packages/meshbay-client/src/tray-icon.png +++ b/packages/meshbay-client/src/tray-icon.png diff --git a/packages/meshbay-client/src/tray-icon@2x.png b/packages/meshbay-client/src/tray-icon@2x.png Binary files differindex 9cf613d..932a69d 100644 --- a/packages/meshbay-client/src/tray-icon@2x.png +++ b/packages/meshbay-client/src/tray-icon@2x.png diff --git a/packages/meshbay-hub/tests/test_desktop_shell.py b/packages/meshbay-hub/tests/test_desktop_shell.py index f1dd399..45e3ed4 100644 --- a/packages/meshbay-hub/tests/test_desktop_shell.py +++ b/packages/meshbay-hub/tests/test_desktop_shell.py @@ -376,3 +376,35 @@ def test_package_json_does_not_define_a_second_linux_package(): assert "electron-builder" not in dist, ( "the dist script builds packages with electron-builder again; it " "should delegate to packaging/build/build-client.sh") + + +# ── System tray is cross-platform ──────────────────────────────────────────── + +def test_tray_capability_and_ipc_handler_agree_on_which_platforms(): + """ + The tray (GNOME, then Windows) has exactly two platform gates: the + `tray` capability preload.js exposes, which is all app.js's nav button + is keyed on, and the `window:minimize-to-tray` IPC handler in main.js + that actually creates it. Both must name the same platform set, or one + of two broken states results: a button that renders but does nothing + (handler stricter than the capability), or a hidden window with no way + back (capability stricter than the handler, on a desktop with no tray + to have hidden it into). Everything else the tray uses (`ensureTray`, + `tray.on('click')`, the poll-refresh, `nodeService.status/stop/restart`) + is unconditional Electron/abstraction code with no platform check of its + own -- these two lines are the whole gate. + """ + preload = _preload() + cap_line = next( + (line for line in preload.splitlines() if "tray:" in line + and "process.platform" in line), None) + assert cap_line, "no platform-gated `tray:` capability found in preload.js" + + lines = _main().splitlines() + handler_start = next( + i for i, line in enumerate(lines) if "'window:minimize-to-tray'" in line) + handler_block = "\n".join(lines[handler_start:handler_start + 5]) + + for plat in ("'linux'", "'win32'"): + assert plat in cap_line, f"tray capability must name {plat}" + assert plat in handler_block, f"minimize-to-tray handler must name {plat}" |