aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-11 14:17:55 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-11 17:51:51 +0200
commitd4935aa2a28fcbab8c3556e3532e53667092701e (patch)
tree2898874e80493bb7067401b4462b51edba267e69
parentbca6fc3f0884fcdb0455b502ee4495b04945baee (diff)
downloadmeshbay-d4935aa2a28fcbab8c3556e3532e53667092701e.tar.gz
feat: gate the create-group wizard on whether a node is bundled
MeshBay Light has no bundled meshbay-node.exe, so the create-group wizard (which assumes it can start a local node) needs its own signal, not just platform.node.available. main.js exposes it over IPC (node:bundled) by checking the packaged resources directory rather than trusting a build-time constant; preload.js and platform.js carry it through the usual contextBridge/wrapper path. winCanElevateServiceMode() replaces the two prior 'app.isPackaged' checks for whether the app can offer service-mode elevation -- Light is packaged but has no service-mode.ps1 to elevate into, so packaged alone was already the wrong test even before this target existed. create-group-page.js gates the wizard step that starts a node on the new capability instead of hiding the whole feature; node-page.js's comment fix is unrelated cosmetic drift caught in the same pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-rw-r--r--packages/meshbay-client/src/main.js50
-rw-r--r--packages/meshbay-client/src/preload.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js27
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/node-page.js9
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js19
5 files changed, 91 insertions, 18 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js
index 83cf772..d7e3e7b 100644
--- a/packages/meshbay-client/src/main.js
+++ b/packages/meshbay-client/src/main.js
@@ -984,6 +984,22 @@ function registerBridge() {
}
});
+ // Does THIS build ship its own node, as opposed to one merely reachable on
+ // PATH (findNodeBinary's fallback, meant for a dev venv -- not "this
+ // installer provisioned a node"). The "Light" target (electron-builder
+ // .light.yml) ships no node-runtime extraResource at all; the renderer
+ // uses this to fall back to the browser-only "create a group" form
+ // instead of the wizard that assumes a local node it can link right there
+ // (create-group-page.js) -- see C:\Users\admin\devel\light-client.md 5.3.
+ // Unaffected off win32 and in dev: only a packaged Windows build can even
+ // be Light, so everything else keeps today's behaviour unconditionally.
+ function hasBundledNode() {
+ if (process.platform === 'win32' && app.isPackaged) {
+ return fs.existsSync(path.join(process.resourcesPath, 'node-runtime', 'meshbay-node.exe'));
+ }
+ return true;
+ }
+
function findNodeBinary() {
if (process.platform === 'win32') {
// A packaged Windows build carries the frozen daemon as an
@@ -1232,6 +1248,8 @@ function registerBridge() {
return { installed: Boolean(bin) };
});
+ ipcMain.handle('node:bundled', () => hasBundledNode());
+
// The systemd unit's own view of the node, for the status panel at the top
// of the Node page. Deliberately not `probeNode()`: that asks the daemon's
// own HTTP API, which cannot answer while the daemon is stopped or crash-
@@ -1245,6 +1263,18 @@ function registerBridge() {
ipcMain.handle('node:service-status', () => nodeServiceStatus());
+ // service-mode.ps1 is an extraResource present in a packaged Full build,
+ // absent from a packaged Light one (nothing to run as a service) and from
+ // an unpackaged dev run. app.isPackaged alone used to gate this, which is
+ // wrong for Light: it would enable the "background service" option and
+ // only fail when actually clicked (winElevateServiceMode's own existsSync
+ // check below, with an actionable error) -- correct but a dead click the
+ // Node page should not offer in the first place.
+ function winCanElevateServiceMode() {
+ return app.isPackaged
+ && fs.existsSync(path.join(process.resourcesPath, 'service-mode.ps1'));
+ }
+
async function nodeServiceStatus() {
if (process.platform === 'win32') {
const svc = await winServiceTaskStatus();
@@ -1259,11 +1289,12 @@ function registerBridge() {
activeState: running ? 'active' : 'inactive',
subState: svc.state,
// Whether switching startup mode can actually elevate right now —
- // service-mode.ps1 is an extraResource, only present in a packaged
- // build. Already installed here, so removing it always works
- // regardless; this only gates the Node page offering to switch
- // *into* service mode.
- canElevate: app.isPackaged,
+ // service-mode.ps1 is an extraResource, present in a packaged Full
+ // build but not a Light one (no node to run as a service at all).
+ // Already installed here, so removing it always works regardless;
+ // this only gates the Node page offering to switch *into* service
+ // mode.
+ canElevate: winCanElevateServiceMode(),
};
}
// Per-user Startup mode. `installed` used to be winAutostartInstalled(),
@@ -1275,12 +1306,17 @@ function registerBridge() {
const [p, bin] = await Promise.all([probeNode(), findNodeBinary()]);
return {
supported: true,
- mode: 'startup',
+ // Only claim "startup mode" once a node was actually found (bundled
+ // or on PATH) -- a Light install with none at all would otherwise
+ // show a working-looking autostart dropdown for a node that does
+ // not exist. `showStartupRow` in node-page.js is gated on this being
+ // a string, so `null` here hides that whole row.
+ mode: bin ? 'startup' : null,
installed: Boolean(bin),
autostart: winAutostartInstalled(),
activeState: p ? 'active' : 'inactive',
subState: p ? 'running' : '',
- canElevate: app.isPackaged,
+ canElevate: winCanElevateServiceMode(),
};
}
if (process.platform !== 'linux') return { supported: false };
diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js
index 13d4f37..c9c9fe0 100644
--- a/packages/meshbay-client/src/preload.js
+++ b/packages/meshbay-client/src/preload.js
@@ -105,6 +105,10 @@ contextBridge.exposeInMainWorld('meshbay', {
node: {
detect: () => ipcRenderer.invoke('node:detect'),
installed: () => ipcRenderer.invoke('node:installed'),
+ // Whether THIS build ships its own node (Full) or not (Light) --
+ // distinct from `installed`, which also counts one merely found on
+ // PATH. Windows only; other platforms always resolve true.
+ bundled: () => ipcRenderer.invoke('node:bundled'),
start: (opts) => ipcRenderer.invoke('node:start', opts),
call: (method, path, body) => ipcRenderer.invoke('node:call', method, path, body),
pairingCode: () => ipcRenderer.invoke('node:pairing-code'),
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js
index c3b542f..f81247d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js
@@ -8,7 +8,32 @@ import { Icon } from './icon.js';
import { SharedDirectoriesTable } from './group-settings.js';
export function CreateGroupPage(props) {
- if (platform.node.available) return html`<${CreateGroupWizard} ...${props} />`;
+ // The wizard assumes a LOCAL node it can link right there (waiting_for_
+ // account / waiting_for_node_key, below) -- exactly what a "Light" desktop
+ // build (electron-builder.light.yml, no bundled node-runtime) does not
+ // have. Without this check that polling loop hangs forever, the same
+ // "Detecting local node…" failure mode already found and fixed once for
+ // Full (see the Windows-port history around linkNodeKeyAndAwaitRunning).
+ // `bundled` starts null (unknown) and resolves once via IPC; a plain
+ // browser has no bridge at all and skips straight to false, the same
+ // outcome `platform.node.available` already gave it. Falling back to
+ // CreateGroupFormSimple is not a lesser feature for Light -- it is the
+ // exact form a browser-only member already uses to create a group with no
+ // node of their own; hosting it can be linked from a device that has one.
+ const [bundled, setBundled] = useState(null);
+ useEffect(() => {
+ if (!platform.node.available) { setBundled(false); return undefined; }
+ let cancelled = false;
+ platform.node.bundled().then((b) => { if (!cancelled) setBundled(b); });
+ return () => { cancelled = true; };
+ }, []);
+
+ if (platform.node.available && bundled === null) {
+ return html`<div class="page-message">
+ <span class="spinner"></span>${' '}${t('node.service_checking')}
+ </div>`;
+ }
+ if (platform.node.available && bundled) return html`<${CreateGroupWizard} ...${props} />`;
return html`<${CreateGroupFormSimple} ...${props} />`;
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
index c1d57f9..6dcaa58 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
@@ -55,11 +55,10 @@ function NodeServicePanel({ onChanged }) {
return i.autostart ? 'signin' : 'off';
};
- // Switching mode itself — the installer's own choice is effectively one-shot
- // (it skips the question once the firewall rules exist for any reason, and
- // per-user mode sets those up on its own with no Scheduled Task), so this is
- // the only way back in if service mode was declined, or out if it is no
- // longer wanted. One elevation, task + firewall together, same script.
+ // Switching mode itself — the installer's own radio page only runs once,
+ // at install time, so this is the only way back in if service mode was
+ // declined there, or out if it is no longer wanted. One elevation, task +
+ // firewall together, same script the installer runs.
//
// The two mechanisms are mutually exclusive by construction here: never
// both installed at once, which would start the daemon twice (once at
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index e93ffb3..43b8ba8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -231,6 +231,16 @@ export const node = {
async installed() {
return bridge && bridge.node ? bridge.node.installed() : { installed: false };
},
+ /**
+ * Does THIS build ship its own node (Full) or not (Light)? Distinct from
+ * `installed`, which also counts one merely found on PATH. A browser has
+ * no bridge at all, so it resolves false the same as `available` does --
+ * create-group-page.js already falls back to the node-free form for that
+ * case, and Light should take the same fallback.
+ */
+ async bundled() {
+ return bridge && bridge.node ? bridge.node.bundled() : false;
+ },
async start(opts) {
if (!bridge || !bridge.node) throw new Error('Node bridge not available');
return bridge.node.start(opts);
@@ -288,11 +298,10 @@ export const node = {
},
/**
* Windows only: switch INTO or OUT OF service mode after install — the
- * installer's own choice is effectively one-shot (build/installer.nsh skips
- * it once the firewall rules exist for any reason, and per-user mode sets
- * those up on its own with no Scheduled Task), so this is the only way back
- * in if it was declined, or out if it was chosen and no longer wanted. One
- * elevation, task + firewall together — same script the installer runs.
+ * installer's own radio page (build/installer.nsh) only runs once, at
+ * install time, so this is the only way back in if a different mode was
+ * chosen there and is no longer wanted. One elevation, task + firewall
+ * together — same script the installer runs.
*/
serviceMode: {
available: Boolean(bridge && bridge.node && bridge.node.serviceMode),