diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-21 17:50:08 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-21 17:50:08 +0200 |
| commit | 73490d331179cb640828b8625abb75208cd7ae5f (patch) | |
| tree | 5782835ae06d3f84c4a9c11c8babcec0d8126157 /packages | |
| parent | 8730281d739d9ed1d6f2d366772582eea8ba0294 (diff) | |
| download | meshbay-73490d331179cb640828b8625abb75208cd7ae5f.tar.gz | |
feat: D.6 first-run wizard — detect, start, link, group, gek-init, pair
Desktop client guides new users through the full onboarding sequence
without a terminal: detect local node → start daemon (systemd with
direct-start fallback for dev) → link node key to hub → create group →
attach directories → gek-init → operator pair.
- node:detect returns configured field to distinguish missing vs stopped
- node:start tries systemctl first, checks is-active, falls back to
spawning the binary directly when the unit crashes (dev mode)
- probeNode() extracts the shared config→token→fetch pattern
- SetupWelcome on empty HomePage links to /create-group
- CreateGroupWizard step 0 handles node detection and start
- platform.js exposes node.installed() and node.start()
- 12 setup.* i18n keys added to all 10 locales
- Integration test for node:start fallback logic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages')
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); |