summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/WINDOWS-PORT.md32
-rw-r--r--packages/meshbay-client/src/main.js10
-rw-r--r--packages/meshbay-client/src/preload.js8
-rw-r--r--packages/meshbay-client/src/tray-icon.pngbin504 -> 829 bytes
-rw-r--r--packages/meshbay-client/src/tray-icon@2x.pngbin967 -> 1807 bytes
-rw-r--r--packages/meshbay-hub/tests/test_desktop_shell.py32
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
index 98fd63b..9b54a5d 100644
--- a/packages/meshbay-client/src/tray-icon.png
+++ b/packages/meshbay-client/src/tray-icon.png
Binary files differ
diff --git a/packages/meshbay-client/src/tray-icon@2x.png b/packages/meshbay-client/src/tray-icon@2x.png
index 9cf613d..932a69d 100644
--- a/packages/meshbay-client/src/tray-icon@2x.png
+++ b/packages/meshbay-client/src/tray-icon@2x.png
Binary files differ
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}"