summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-21 17:50:14 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-21 17:50:14 +0200
commit81c7d1a34c6b1505f0a75b39c2232f92b1ca4512 (patch)
tree5782835ae06d3f84c4a9c11c8babcec0d8126157
parent8730281d739d9ed1d6f2d366772582eea8ba0294 (diff)
parent73490d331179cb640828b8625abb75208cd7ae5f (diff)
downloadmeshbay-81c7d1a34c6b1505f0a75b39c2232f92b1ca4512.tar.gz
Merge feat/d6-first-run-wizard: first-run onboarding wizard (D.6)
-rw-r--r--packages/meshbay-client/src/main.js114
-rw-r--r--packages/meshbay-client/src/preload.js2
-rw-r--r--packages/meshbay-client/test/test-node-start.js268
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js82
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js7
15 files changed, 594 insertions, 19 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js
index 45e8462..22e0162 100644
--- a/packages/meshbay-client/src/main.js
+++ b/packages/meshbay-client/src/main.js
@@ -22,6 +22,7 @@
const { app, BrowserWindow, dialog, ipcMain, protocol, safeStorage, shell } =
require('electron');
+const { execFile, spawn } = require('node:child_process');
const crypto = require('node:crypto');
const fs = require('node:fs');
const fsp = require('node:fs/promises');
@@ -691,27 +692,132 @@ function registerBridge() {
ipcMain.handle('node:detect', async () => {
const nc = readNodeConfig();
- if (!nc) return { detected: false };
+ if (!nc) return { detected: false, configured: false };
const token = readNodeToken(nc.dataDir);
- if (!token) return { detected: false };
+ if (!token) return { detected: false, configured: true };
_nodeToken = token;
_nodePort = nc.uiPort;
try {
const r = await fetch(
`http://127.0.0.1:${_nodePort}/api/status?t=${_nodeToken}`,
{ signal: AbortSignal.timeout(3000) });
- if (!r.ok) return { detected: false };
+ if (!r.ok) return { detected: false, configured: true };
const status = await r.json();
return {
detected: true,
+ configured: true,
status: status.status,
pk_node_ed25519: status.pk_node_ed25519 || '',
};
} catch {
- return { detected: false };
+ return { detected: false, configured: true };
}
});
+ ipcMain.handle('node:installed', async () => {
+ if (process.platform !== 'linux') return { installed: false };
+ return new Promise((resolve) => {
+ execFile('systemctl', ['--user', 'show', 'meshbay-node.service',
+ '--property=LoadState'], (err, stdout) => {
+ if (err) return resolve({ installed: false });
+ resolve({ installed: stdout.trim() === 'LoadState=loaded' });
+ });
+ });
+ });
+
+ async function probeNode() {
+ const nc = readNodeConfig();
+ const dataDir = nc ? nc.dataDir
+ : path.join(os.homedir(), '.local', 'share', 'meshbay');
+ const port = nc ? nc.uiPort : 18000;
+ const token = readNodeToken(dataDir);
+ if (!token) return null;
+ try {
+ const r = await fetch(
+ `http://127.0.0.1:${port}/api/status?t=${token}`,
+ { signal: AbortSignal.timeout(3000) });
+ if (!r.ok) return null;
+ const status = await r.json();
+ _nodeToken = token;
+ _nodePort = port;
+ return { pk_node_ed25519: status.pk_node_ed25519 || '' };
+ } catch { return null; }
+ }
+
+ ipcMain.handle('node:start', async () => {
+ const already = await probeNode();
+ if (already) return { started: true, ...already };
+
+ if (process.platform !== 'linux') {
+ throw new Error('Automatic node start is only supported on Linux');
+ }
+
+ const deadline = Date.now() + 15000;
+ const configFile = nodeConfigPath();
+ let launched = false;
+
+ // Try systemctl first — the production path.
+ try {
+ await new Promise((resolve, reject) => {
+ execFile('systemctl', ['--user', 'enable', '--now', 'meshbay-node'],
+ (err, _stdout, stderr) => {
+ if (err) return reject(new Error(stderr.trim() || err.message));
+ resolve();
+ });
+ });
+ // Give the service a moment, then check if it actually stayed up.
+ for (let i = 0; i < 6 && Date.now() < deadline; i++) {
+ await new Promise((r) => setTimeout(r, 500));
+ const result = await probeNode();
+ if (result) return { started: true, ...result };
+ }
+ // The unit may be stuck in auto-restart (e.g. ExecStart points to
+ // /usr/bin which doesn't exist in dev). is-active returns 0 only when
+ // the service is genuinely running.
+ const isActive = await new Promise((resolve) => {
+ execFile('systemctl', ['--user', 'is-active', 'meshbay-node'],
+ (err) => resolve(!err));
+ });
+ if (isActive) {
+ launched = true;
+ } else {
+ // Stop the broken unit so it doesn't compete with the direct start.
+ await new Promise((resolve) => {
+ execFile('systemctl', ['--user', 'stop', 'meshbay-node'],
+ () => resolve());
+ });
+ }
+ } catch {
+ // systemctl itself failed (e.g. no unit file).
+ }
+
+ // Fallback: start the binary directly (dev mode, or no unit installed).
+ if (!launched) {
+ const binPath = await new Promise((resolve) => {
+ execFile('which', ['meshbay-node'], (err, stdout) => {
+ resolve(err ? null : stdout.trim());
+ });
+ });
+ if (!binPath) {
+ throw new Error(
+ 'meshbay-node is not installed');
+ }
+ const child = spawn(binPath, ['--config', configFile], {
+ detached: true,
+ stdio: 'ignore',
+ });
+ child.unref();
+ }
+
+ while (Date.now() < deadline) {
+ await new Promise((r) => setTimeout(r, 500));
+ const result = await probeNode();
+ if (result) return { started: true, ...result };
+ }
+ throw new Error(
+ 'meshbay-node was started but did not become ready within 15 seconds');
+ });
+
ipcMain.handle('node:call', async (_e, method, apiPath, body) => {
if (!_nodeToken) throw new Error('Node not detected');
const sep = apiPath.includes('?') ? '&' : '?';
diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js
index e9fd375..c8553a5 100644
--- a/packages/meshbay-client/src/preload.js
+++ b/packages/meshbay-client/src/preload.js
@@ -86,6 +86,8 @@ contextBridge.exposeInMainWorld('meshbay', {
// pattern as hub:fetch.
node: {
detect: () => ipcRenderer.invoke('node:detect'),
+ installed: () => ipcRenderer.invoke('node:installed'),
+ start: () => ipcRenderer.invoke('node:start'),
call: (method, path, body) => ipcRenderer.invoke('node:call', method, path, body),
pairingCode: () => ipcRenderer.invoke('node:pairing-code'),
setPairingCode: (code) => ipcRenderer.invoke('node:set-pairing-code', code),
diff --git a/packages/meshbay-client/test/test-node-start.js b/packages/meshbay-client/test/test-node-start.js
new file mode 100644
index 0000000..1e91419
--- /dev/null
+++ b/packages/meshbay-client/test/test-node-start.js
@@ -0,0 +1,268 @@
+#!/usr/bin/env node
+/**
+ * Integration test for the node:start logic in main.js.
+ *
+ * Exercises each step of the startup sequence on the real system:
+ * 1. probeNode — can we reach a running daemon?
+ * 2. systemctl enable --now — does the unit start the daemon?
+ * 3. is-active check — did the unit crash (e.g. wrong ExecStart path)?
+ * 4. direct start fallback — find binary in PATH, spawn it
+ * 5. probeNode poll — does the daemon respond after direct start?
+ *
+ * Run: node test/test-node-start.js
+ *
+ * The test leaves the daemon running on success so subsequent Electron
+ * testing can pick it up. Pass --cleanup to kill it after the test.
+ */
+
+'use strict';
+
+const { execFile, spawn } = require('node:child_process');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+
+const CLEANUP = process.argv.includes('--cleanup');
+
+let passed = 0;
+let failed = 0;
+
+function ok(label) { passed++; console.log(` \x1b[32m✓\x1b[0m ${label}`); }
+function fail(label, detail) {
+ failed++;
+ console.log(` \x1b[31m✗\x1b[0m ${label}`);
+ if (detail) console.log(` ${detail}`);
+}
+function info(msg) { console.log(` \x1b[36mℹ\x1b[0m ${msg}`); }
+
+// ── helpers (same logic as main.js) ──────────────────────────────────────────
+
+function nodeConfigPath() {
+ return path.join(os.homedir(), '.config', 'meshbay', 'node.toml');
+}
+
+function readNodeConfig() {
+ try {
+ const text = fs.readFileSync(nodeConfigPath(), 'utf8');
+ let dataDir = path.join(os.homedir(), '.local', 'share', 'meshbay');
+ let uiPort = 18000;
+ const dataMatch = text.match(/^\s*data_dir\s*=\s*"([^"]+)"/m);
+ if (dataMatch) dataDir = dataMatch[1].replace(/^~/, os.homedir());
+ const portMatch = text.match(/^\s*ui_port\s*=\s*(\d+)/m);
+ if (portMatch) uiPort = parseInt(portMatch[1], 10);
+ return { dataDir, uiPort };
+ } catch { return null; }
+}
+
+function readNodeToken(dataDir) {
+ try {
+ return fs.readFileSync(path.join(dataDir, 'ui-token'), 'utf8').trim();
+ } catch { return null; }
+}
+
+async function probeNode() {
+ const nc = readNodeConfig();
+ const dataDir = nc ? nc.dataDir
+ : path.join(os.homedir(), '.local', 'share', 'meshbay');
+ const port = nc ? nc.uiPort : 18000;
+ const token = readNodeToken(dataDir);
+ if (!token) return null;
+ try {
+ const r = await fetch(
+ `http://127.0.0.1:${port}/api/status?t=${token}`,
+ { signal: AbortSignal.timeout(3000) });
+ if (!r.ok) return null;
+ const status = await r.json();
+ return { port, pk_node_ed25519: status.pk_node_ed25519 || '' };
+ } catch { return null; }
+}
+
+function exec(cmd, args) {
+ return new Promise((resolve) => {
+ execFile(cmd, args, (err, stdout, stderr) => {
+ resolve({ err, stdout: stdout?.trim(), stderr: stderr?.trim() });
+ });
+ });
+}
+
+async function pollProbe(seconds) {
+ const deadline = Date.now() + seconds * 1000;
+ while (Date.now() < deadline) {
+ await new Promise((r) => setTimeout(r, 500));
+ const result = await probeNode();
+ if (result) return result;
+ }
+ return null;
+}
+
+// ── kill any running daemon ──────────────────────────────────────────────────
+
+async function killDaemon() {
+ const { stdout } = await exec('pgrep', ['-f', 'meshbay.node.daemon']);
+ if (stdout) {
+ for (const pid of stdout.split('\n').filter(Boolean)) {
+ try { process.kill(Number(pid), 'SIGTERM'); } catch {}
+ }
+ await new Promise((r) => setTimeout(r, 1000));
+ }
+ await exec('systemctl', ['--user', 'stop', 'meshbay-node']);
+}
+
+// ── tests ────────────────────────────────────────────────────────────────────
+
+async function main() {
+ console.log('\nnode:start integration tests\n');
+
+ // ── Prerequisites ──
+ console.log('Prerequisites:');
+
+ const configFile = nodeConfigPath();
+ if (fs.existsSync(configFile)) {
+ ok(`Config exists: ${configFile}`);
+ } else {
+ fail(`Config missing: ${configFile}`);
+ process.exit(1);
+ }
+
+ const nc = readNodeConfig();
+ info(`dataDir = ${nc.dataDir}`);
+ info(`uiPort = ${nc.uiPort}`);
+
+ const { stdout: binPath } = await exec('which', ['meshbay-node']);
+ if (binPath) {
+ ok(`Binary found: ${binPath}`);
+ } else {
+ fail('meshbay-node not in PATH');
+ process.exit(1);
+ }
+
+ const unitExists = fs.existsSync(
+ path.join(os.homedir(), '.config', 'systemd', 'user', 'meshbay-node.service'));
+ info(`systemd unit installed: ${unitExists}`);
+
+ if (unitExists) {
+ const { stdout: execStart } = await exec('systemctl', [
+ '--user', 'show', 'meshbay-node', '--property=ExecStart']);
+ const execPath = execStart?.match(/path=([^ ;]+)/)?.[1] || '?';
+ const execExists = fs.existsSync(execPath);
+ if (execExists) {
+ ok(`Unit ExecStart binary exists: ${execPath}`);
+ } else {
+ info(`Unit ExecStart binary MISSING: ${execPath} (fallback will be needed)`);
+ }
+ }
+
+ // ── Test 1: probeNode when daemon is stopped ──
+ console.log('\n1. probeNode with daemon stopped:');
+ await killDaemon();
+ const probeDown = await probeNode();
+ if (!probeDown) {
+ ok('probeNode returns null when daemon is stopped');
+ } else {
+ fail('probeNode returned a result but daemon should be stopped', JSON.stringify(probeDown));
+ }
+
+ // ── Test 2: systemctl enable --now ──
+ if (unitExists) {
+ console.log('\n2. systemctl enable --now:');
+ const { err: sysErr, stderr: sysStderr } = await exec(
+ 'systemctl', ['--user', 'enable', '--now', 'meshbay-node']);
+ if (sysErr) {
+ info(`systemctl returned error: ${sysStderr || sysErr.message}`);
+ } else {
+ ok('systemctl enable --now returned 0');
+ }
+
+ // Wait a moment, then check
+ await new Promise((r) => setTimeout(r, 2000));
+
+ const { err: activeErr, stdout: activeState } = await exec(
+ 'systemctl', ['--user', 'is-active', 'meshbay-node']);
+ info(`is-active: "${activeState}" (exit ${activeErr ? activeErr.code : 0})`);
+
+ const isActive = !activeErr;
+ if (isActive) {
+ ok('Service is active');
+ const probeUp = await pollProbe(5);
+ if (probeUp) {
+ ok(`probeNode succeeds (pk: ${probeUp.pk_node_ed25519?.slice(0, 16)}...)`);
+ } else {
+ fail('probeNode returned null despite service being active');
+ }
+ } else {
+ info('Service is NOT active — this is expected in dev mode (ExecStart mismatch)');
+
+ // Verify is-failed vs is-active difference (the bug we fixed)
+ const { err: failedErr, stdout: failedState } = await exec(
+ 'systemctl', ['--user', 'is-failed', 'meshbay-node']);
+ info(`is-failed: "${failedState}" (exit ${failedErr ? failedErr.code : 0})`);
+ const isFailed = !failedErr;
+ if (!isFailed && activeState === 'activating') {
+ ok('Confirmed bug scenario: is-failed=false while is-active=activating (auto-restart loop)');
+ info('Old code would NOT fall back; new code uses is-active and DOES fall back');
+ }
+
+ // Stop the broken unit
+ await exec('systemctl', ['--user', 'stop', 'meshbay-node']);
+ await exec('systemctl', ['--user', 'disable', 'meshbay-node']);
+ ok('Stopped and disabled broken unit');
+ }
+ } else {
+ console.log('\n2. systemctl (skipped — no unit file)');
+ }
+
+ // ── Test 3: Direct start fallback ──
+ console.log('\n3. Direct start fallback:');
+ await killDaemon();
+ await new Promise((r) => setTimeout(r, 500));
+
+ const child = spawn(binPath, ['--config', configFile], {
+ detached: true,
+ stdio: 'ignore',
+ });
+ child.unref();
+ ok(`Spawned ${binPath} --config ${configFile} (pid ${child.pid})`);
+
+ const probeAfter = await pollProbe(15);
+ if (probeAfter) {
+ ok(`probeNode succeeds after direct start (pk: ${probeAfter.pk_node_ed25519?.slice(0, 16)}...)`);
+ } else {
+ fail('probeNode returned null after 15 seconds of polling');
+ // Debug info
+ const token = readNodeToken(nc.dataDir);
+ info(`Token file present: ${!!token}`);
+ const { stdout: pgrep } = await exec('pgrep', ['-af', 'meshbay']);
+ info(`meshbay processes: ${pgrep || 'none'}`);
+ }
+
+ // ── Test 4: probeNode when daemon is already running ──
+ console.log('\n4. probeNode when daemon already running:');
+ if (probeAfter) {
+ const probeAgain = await probeNode();
+ if (probeAgain) {
+ ok('probeNode returns immediately for running daemon');
+ } else {
+ fail('probeNode returned null for running daemon');
+ }
+ } else {
+ info('Skipped — daemon did not start');
+ }
+
+ // ── Cleanup ──
+ if (CLEANUP) {
+ console.log('\nCleanup:');
+ await killDaemon();
+ ok('Daemon stopped');
+ } else {
+ info('\nDaemon left running for Electron testing. Pass --cleanup to stop it.');
+ }
+
+ // ── Summary ──
+ console.log(`\n${passed} passed, ${failed} failed\n`);
+ process.exit(failed > 0 ? 1 : 0);
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 7780d08..4b5ccd3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -906,7 +906,13 @@ function NotificationFeed({ notifications, onMarkRead, onPurge }) {
}
function HomePage({ groups, notifications, onMarkRead, onPurge }) {
+ const [setupDismissed, setSetupDismissed] = useState(false);
+
if (groups.length === 0) {
+ if (platform.isNative && !setupDismissed) {
+ return html`<${SetupWelcome}
+ onDismiss=${() => setSetupDismissed(true)} />`;
+ }
return html`
<div>
<h2>${t('home.welcome')}</h2>
@@ -1033,6 +1039,22 @@ function ExplorePage({ token, myGroupIds }) {
`;
}
+// ── First-run welcome (Electron-only, shown once on empty home) ─────────────
+
+function SetupWelcome({ onDismiss }) {
+ return html`<div class="page-content">
+ <h2>${t('setup.welcome_title')}</h2>
+ <p class="page-message" style="margin-bottom:24px">
+ ${t('setup.welcome_message')}</p>
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
+ <a class="btn btn-primary" href="#/create-group">
+ ${t('setup.create_group')}</a>
+ <button class="btn btn-secondary" onClick=${onDismiss}>
+ ${t('setup.dismiss')}</button>
+ </div>
+ </div>`;
+}
+
// ── Create Group Page ────────────────────────────────────────────────────────
function CreateGroupPage(props) {
@@ -1130,6 +1152,7 @@ function CreateGroupFormSimple({ token, onCreated }) {
function CreateGroupWizard({ token, onCreated }) {
const [step, setStep] = useState(0); // 0=node check, 1=details, 2=setup, 3=done
const [nodeStatus, setNodeStatus] = useState(null); // null=loading, object=result
+ const [nodeStarting, setNodeStarting] = useState(false);
const [error, setError] = useState('');
// Step 1 fields
@@ -1144,30 +1167,50 @@ function CreateGroupWizard({ token, onCreated }) {
const [setupError, setSetupError] = useState('');
const [groupId, setGroupId] = useState('');
+ const linkNodeKey = useCallback(async (pk) => {
+ if (!pk) return;
+ try {
+ await hubFetch('/v1/users/me/node_key', {
+ method: 'PUT', token, body: { pk_node_ed25519: pk },
+ });
+ } catch { /* already linked or same key */ }
+ }, [token]);
+
// Step 0: detect node
const detectNode = useCallback(async () => {
setNodeStatus(null);
setError('');
try {
const result = await platform.node.detect();
- setNodeStatus(result);
if (result.detected) {
- // Auto-link node key to hub if not already done
- if (result.pk_node_ed25519) {
- try {
- await hubFetch('/v1/users/me/node_key', {
- method: 'PUT', token,
- body: { pk_node_ed25519: result.pk_node_ed25519 },
- });
- } catch { /* already linked or same key */ }
- }
+ await linkNodeKey(result.pk_node_ed25519);
+ setNodeStatus(result);
setStep(1);
+ return;
}
+ // Not responding — check if the unit is installed (for the Start button)
+ const inst = await platform.node.installed();
+ setNodeStatus({ detected: false, configured: result.configured, installed: inst.installed });
} catch (err) {
setError(err.message);
setNodeStatus({ detected: false });
}
- }, [token]);
+ }, [token, linkNodeKey]);
+
+ const startNode = useCallback(async () => {
+ setNodeStarting(true);
+ setError('');
+ try {
+ const result = await platform.node.start();
+ await linkNodeKey(result.pk_node_ed25519);
+ setNodeStatus({ detected: true, ...result });
+ setNodeStarting(false);
+ setStep(1);
+ } catch (err) {
+ setError(platform.bridgeMessage(err));
+ setNodeStarting(false);
+ }
+ }, [linkNodeKey]);
useEffect(() => { detectNode(); }, [detectNode]);
@@ -1279,12 +1322,21 @@ function CreateGroupWizard({ token, onCreated }) {
</div>`;
}
if (!nodeStatus.detected) {
+ const canStart = nodeStatus.installed || nodeStatus.configured;
return html`<div class="page-content">
<h2>${t('wizard.title')}</h2>
- <p class="page-message">${t('wizard.node_not_found')}</p>
- ${error && html`<div class="error-msg">${error}</div>`}
- <div style="display:flex;gap:8px;margin-top:16px">
- <button class="btn btn-primary" onClick=${detectNode}>
+ <p class="page-message">${canStart
+ ? t('setup.node_not_running')
+ : t('setup.node_not_installed')}</p>
+ ${!canStart && html`<p class="page-message" style="margin-top:8px">
+ ${t('setup.node_install_hint')}</p>`}
+ ${error && html`<div class="error-msg" style="margin-top:8px">${error}</div>`}
+ <div style="display:flex;gap:8px;margin-top:16px;flex-wrap:wrap">
+ ${canStart && html`
+ <button class="btn btn-primary" disabled=${nodeStarting}
+ onClick=${startNode}>
+ ${nodeStarting ? t('setup.node_starting') : t('setup.node_start')}</button>`}
+ <button class="btn btn-secondary" onClick=${detectNode}>
${t('wizard.retry')}</button>
</div>
</div>`;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index e309834..da2ada4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -467,4 +467,18 @@ export default {
'group.unmute': 'Unmute notifications',
'profile.title': 'Profil',
'usermenu.profile': 'Profil',
+
+ // First-run setup wizard
+ 'setup.welcome_title': 'Willkommen bei MeshBay',
+ 'setup.welcome_message': 'Ihr Node hostet Ihre Dateien — verschlüsselt, auf Ihrem Rechner. Starten wir ihn und erstellen Ihre erste Gruppe.',
+ 'setup.node_detected': 'Lokaler Node erkannt und läuft.',
+ 'setup.node_not_running': 'Ihr Node ist installiert, läuft aber nicht.',
+ 'setup.node_start': 'Node starten',
+ 'setup.node_starting': 'Node wird gestartet…',
+ 'setup.node_started': 'Node gestartet.',
+ 'setup.node_not_installed': 'meshbay-node ist auf diesem Rechner nicht installiert.',
+ 'setup.node_install_hint': 'Installieren Sie es mit Ihrem Paketmanager und kommen Sie hierher zurück.',
+ 'setup.skip': 'Überspringen — Gruppe ohne lokalen Node erstellen',
+ 'setup.create_group': 'Erste Gruppe erstellen',
+ 'setup.dismiss': 'Einrichtung überspringen',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 9a5058e..3ddd7f1 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -366,6 +366,20 @@ export default {
'wizard.done_message': 'Your group is ready. Your node is hosting it and encryption is set up.',
'wizard.go_to_group': 'Go to group',
+ // First-run setup wizard (Electron-only)
+ 'setup.welcome_title': 'Welcome to MeshBay',
+ 'setup.welcome_message': 'Your node hosts your files — encrypted, on your machine. Let’s get it running and create your first group.',
+ 'setup.node_detected': 'Local node detected and running.',
+ 'setup.node_not_running': 'Your node is installed but not running.',
+ 'setup.node_start': 'Start node',
+ 'setup.node_starting': 'Starting node…',
+ 'setup.node_started': 'Node started.',
+ 'setup.node_not_installed': 'meshbay-node is not installed on this machine.',
+ 'setup.node_install_hint': 'Install it with your package manager, then come back here.',
+ 'setup.skip': 'Skip — create a group without a local node',
+ 'setup.create_group': 'Create your first group',
+ 'setup.dismiss': 'Skip setup',
+
// Unified Group Settings (node sections)
'settings_node.roots': 'Shared directories',
'settings_node.detach_failed_continue': 'Could not detach the group from the node. Delete on hub anyway?',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 5a95347..90e310d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -462,4 +462,18 @@ export default {
'group.unmute': 'Unmute notifications',
'profile.title': 'Perfil',
'usermenu.profile': 'Perfil',
+
+ // First-run setup wizard
+ 'setup.welcome_title': 'Bienvenido a MeshBay',
+ 'setup.welcome_message': 'Su node aloja sus archivos — cifrados, en su máquina. Vamos a ponerlo en marcha y crear su primer grupo.',
+ 'setup.node_detected': 'Node local detectado y en funcionamiento.',
+ 'setup.node_not_running': 'Su node está instalado pero no se está ejecutando.',
+ 'setup.node_start': 'Iniciar node',
+ 'setup.node_starting': 'Iniciando node…',
+ 'setup.node_started': 'Node iniciado.',
+ 'setup.node_not_installed': 'meshbay-node no está instalado en esta máquina.',
+ 'setup.node_install_hint': 'Instálelo con su gestor de paquetes y vuelva aquí.',
+ 'setup.skip': 'Omitir — crear un grupo sin node local',
+ 'setup.create_group': 'Crear su primer grupo',
+ 'setup.dismiss': 'Omitir configuración',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 4139fc9..fa7468f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -466,4 +466,18 @@ export default {
'group.unmute': 'Réactiver les notifications',
'profile.title': 'Profil',
'usermenu.profile': 'Profil',
+
+ // First-run setup wizard
+ 'setup.welcome_title': 'Bienvenue sur MeshBay',
+ 'setup.welcome_message': 'Votre node héberge vos fichiers — chiffrés, sur votre machine. Démarrons-le et créons votre premier groupe.',
+ 'setup.node_detected': 'Node local détecté et en fonctionnement.',
+ 'setup.node_not_running': 'Votre node est installé mais ne tourne pas.',
+ 'setup.node_start': 'Démarrer le node',
+ 'setup.node_starting': 'Démarrage du node…',
+ 'setup.node_started': 'Node démarré.',
+ 'setup.node_not_installed': 'meshbay-node n\'est pas installé sur cette machine.',
+ 'setup.node_install_hint': 'Installez-le avec votre gestionnaire de paquets, puis revenez ici.',
+ 'setup.skip': 'Passer — créer un groupe sans node local',
+ 'setup.create_group': 'Créer votre premier groupe',
+ 'setup.dismiss': 'Passer la configuration',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 531c8f7..6d349b9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -464,4 +464,18 @@ export default {
'group.unmute': 'Unmute notifications',
'profile.title': 'Profilo',
'usermenu.profile': 'Profilo',
+
+ // First-run setup wizard
+ 'setup.welcome_title': 'Benvenuto su MeshBay',
+ 'setup.welcome_message': 'Il suo node ospita i suoi file — cifrati, sulla sua macchina. Avviamolo e creiamo il suo primo gruppo.',
+ 'setup.node_detected': 'Node locale rilevato e in funzione.',
+ 'setup.node_not_running': 'Il suo node è installato ma non è in esecuzione.',
+ 'setup.node_start': 'Avvia node',
+ 'setup.node_starting': 'Avvio del node…',
+ 'setup.node_started': 'Node avviato.',
+ 'setup.node_not_installed': 'meshbay-node non è installato su questa macchina.',
+ 'setup.node_install_hint': 'Lo installi con il suo gestore di pacchetti, poi torni qui.',
+ 'setup.skip': 'Salta — crea un gruppo senza node locale',
+ 'setup.create_group': 'Crea il suo primo gruppo',
+ 'setup.dismiss': 'Salta la configurazione',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 1a68827..6f74073 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -452,4 +452,18 @@ export default {
'group.unmute': 'Unmute notifications',
'profile.title': 'プロフィール',
'usermenu.profile': 'プロフィール',
+
+ // First-run setup wizard
+ 'setup.welcome_title': 'MeshBay へようこそ',
+ 'setup.welcome_message': 'お使いの node がファイルを暗号化してお使いのマシン上に保管します。node を起動して最初のグループを作成しましょう。',
+ 'setup.node_detected': 'ローカル node が検出され、実行中です。',
+ 'setup.node_not_running': 'node はインストールされていますが、実行されていません。',
+ 'setup.node_start': 'node を起動',
+ 'setup.node_starting': 'node を起動中…',
+ 'setup.node_started': 'Node が起動しました。',
+ 'setup.node_not_installed': 'この端末に meshbay-node がインストールされていません。',
+ 'setup.node_install_hint': 'パッケージマネージャーでインストールし、こちらにお戻りください。',
+ 'setup.skip': 'スキップ — ローカル node なしでグループを作成',
+ 'setup.create_group': '最初のグループを作成',
+ 'setup.dismiss': 'セットアップをスキップ',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index d0c9649..f1c2b75 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -466,4 +466,18 @@ export default {
'group.unmute': 'Unmute notifications',
'profile.title': 'Profiel',
'usermenu.profile': 'Profiel',
+
+ // First-run setup wizard
+ 'setup.welcome_title': 'Welkom bij MeshBay',
+ 'setup.welcome_message': 'Uw node host uw bestanden — versleuteld, op uw machine. Laten we hem starten en uw eerste groep aanmaken.',
+ 'setup.node_detected': 'Lokale node gedetecteerd en actief.',
+ 'setup.node_not_running': 'Uw node is geïnstalleerd maar draait niet.',
+ 'setup.node_start': 'Node starten',
+ 'setup.node_starting': 'Node wordt gestart…',
+ 'setup.node_started': 'Node gestart.',
+ 'setup.node_not_installed': 'meshbay-node is niet geïnstalleerd op deze machine.',
+ 'setup.node_install_hint': 'Installeer het met uw pakketbeheerder en kom hier terug.',
+ 'setup.skip': 'Overslaan — groep aanmaken zonder lokale node',
+ 'setup.create_group': 'Eerste groep aanmaken',
+ 'setup.dismiss': 'Installatie overslaan',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 630548b..e3f9270 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -479,4 +479,18 @@ export default {
'group.unmute': 'Unmute notifications',
'profile.title': 'Profil',
'usermenu.profile': 'Profil',
+
+ // First-run setup wizard
+ 'setup.welcome_title': 'Witamy w MeshBay',
+ 'setup.welcome_message': 'Pana/Pani node przechowuje pliki — zaszyfrowane, na Pana/Pani maszynie. Uruchommy go i utwórzmy pierwszą grupę.',
+ 'setup.node_detected': 'Wykryto lokalny node — działa.',
+ 'setup.node_not_running': 'Node jest zainstalowany, ale nie działa.',
+ 'setup.node_start': 'Uruchom node',
+ 'setup.node_starting': 'Uruchamianie node…',
+ 'setup.node_started': 'Node uruchomiony.',
+ 'setup.node_not_installed': 'meshbay-node nie jest zainstalowany na tej maszynie.',
+ 'setup.node_install_hint': 'Zainstaluj go za pomocą menedżera pakietów, a potem wróć tutaj.',
+ 'setup.skip': 'Pomiń — utwórz grupę bez lokalnego node',
+ 'setup.create_group': 'Utwórz pierwszą grupę',
+ 'setup.dismiss': 'Pomiń konfigurację',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index 9a6809a..2b003c2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -463,4 +463,18 @@ export default {
'group.unmute': 'Unmute notifications',
'profile.title': 'Perfil',
'usermenu.profile': 'Perfil',
+
+ // First-run setup wizard
+ 'setup.welcome_title': 'Bem-vindo ao MeshBay',
+ 'setup.welcome_message': 'Seu node hospeda seus arquivos — criptografados, na sua máquina. Vamos iniciá-lo e criar seu primeiro grupo.',
+ 'setup.node_detected': 'Node local detectado e em execução.',
+ 'setup.node_not_running': 'Seu node está instalado mas não está em execução.',
+ 'setup.node_start': 'Iniciar node',
+ 'setup.node_starting': 'Iniciando node…',
+ 'setup.node_started': 'Node iniciado.',
+ 'setup.node_not_installed': 'meshbay-node não está instalado nesta máquina.',
+ 'setup.node_install_hint': 'Instale-o com seu gerenciador de pacotes e volte aqui.',
+ 'setup.skip': 'Pular — criar um grupo sem node local',
+ 'setup.create_group': 'Criar seu primeiro grupo',
+ 'setup.dismiss': 'Pular configuração',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 549b77e..bab41e1 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -438,4 +438,18 @@ export default {
'group.unmute': 'Unmute notifications',
'profile.title': '个人资料',
'usermenu.profile': '个人资料',
+
+ // First-run setup wizard
+ 'setup.welcome_title': '欢迎使用 MeshBay',
+ 'setup.welcome_message': '您的 node 在您自己的机器上加密存储文件。让我们启动它并创建您的第一个群组。',
+ 'setup.node_detected': '已检测到本地 node,正在运行。',
+ 'setup.node_not_running': '您的 node 已安装但未运行。',
+ 'setup.node_start': '启动 node',
+ 'setup.node_starting': '正在启动 node…',
+ 'setup.node_started': 'Node 已启动。',
+ 'setup.node_not_installed': '此机器上未安装 meshbay-node。',
+ 'setup.node_install_hint': '请使用包管理器安装,然后回到此处。',
+ 'setup.skip': '跳过 — 不使用本地 node 创建群组',
+ 'setup.create_group': '创建第一个群组',
+ 'setup.dismiss': '跳过设置',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index d400b4e..eeffd49 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -224,6 +224,13 @@ export const node = {
async detect() {
return bridge && bridge.node ? bridge.node.detect() : { detected: false };
},
+ async installed() {
+ return bridge && bridge.node ? bridge.node.installed() : { installed: false };
+ },
+ async start() {
+ if (!bridge || !bridge.node) throw new Error('Node bridge not available');
+ return bridge.node.start();
+ },
async call(method, path, body) {
if (!bridge || !bridge.node) throw new Error('Node bridge not available');
return bridge.node.call(method, path, body);