diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-05 15:26:25 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-05 15:26:25 +0200 |
| commit | bd299df3468822e7b545a90bf55ffbc1bac87f58 (patch) | |
| tree | c2175e3675355148ae3eb1969983d0fa7a337dac | |
| parent | c11dd22b593358ef7932deec53c8200f5f14ed8b (diff) | |
| download | meshbay-bd299df3468822e7b545a90bf55ffbc1bac87f58.tar.gz | |
feat(client): system tray on Windows, and a clearer tray icon
The tray's Start/Stop already drove nodeService.status/stop/restart, which
had full win32 branches for both startup modes from the Node page work --
so enabling it on Windows is widening two platform gates (the `tray`
capability in preload.js, the window:minimize-to-tray handler in main.js),
not new logic.
Replaced the tray icon: the previous white chevron-in-a-box read as an
envelope at tray size. New icon is a small "M" drawn as mesh nodes and
edges, echoing the app icon's own motif, in the brand blue instead of
plain white so it stays legible on both light and dark taskbars/panels.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| -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}" |