diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-29 11:15:54 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-29 11:15:54 +0200 |
| commit | b7733e812fadd6007976d262bd1d793572a36ba7 (patch) | |
| tree | 52e2ac07c9ba4929fa50b6ba8d19f94f86e8b304 /packages/meshbay-client/src | |
| parent | 09a007937f03fad04a390323f3817f1ae7d7c2a9 (diff) | |
| download | meshbay-b7733e812fadd6007976d262bd1d793572a36ba7.tar.gz | |
feat: node workflow redesign — wizard auto-config, reset, MusicBrainz contact
Wizard (Electron):
- Auto-provisions node config (hub URL + username) from logged-in user
- node:start handles both cold start and restart of misconfigured daemon
- Waits for daemon to reach 'running', auto-links node key on hub
- probeNode accepts intermediate states for wizard progress feedback
Reset (meshbay-node reset):
- Unlinks node key from hub (DELETE /me/node_key, best-effort)
- Stops and disables daemon (systemctl --user disable --now)
- Erases ~/.config/meshbay, ~/.local/share/meshbay, ~/.local/state/meshbay
MusicBrainz contact:
- Resolved from owner's hub email instead of per-node roster config
- Removed musicbrainz_contact UI and WebRTC handshake field
- Removed set_musicbrainz_contact/musicbrainz_contact from roster
Node pairing:
- Added operator pairing banner on NodePage
- Added operator_paired flag to list_groups
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-client/src')
| -rw-r--r-- | packages/meshbay-client/src/main.js | 126 |
1 files changed, 73 insertions, 53 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index 91fc4fa..3926c01 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -829,19 +829,28 @@ function registerBridge() { { signal: AbortSignal.timeout(3000) }); if (!r.ok) return null; const status = await r.json(); - if (status.status !== 'running') return null; + const READY = ['running', 'waiting_for_node_key', 'waiting_for_account', 'starting']; + if (!READY.includes(status.status)) return null; _nodeToken = token; _nodePort = port; - return { pk_node_ed25519: status.pk_node_ed25519 || '' }; + return { pk_node_ed25519: status.pk_node_ed25519 || '', status: status.status }; } catch { return null; } } function provisionNode(hubUrl, username) { const configDir = path.join(os.homedir(), '.config', 'meshbay'); + const dataDir = path.join(os.homedir(), '.local', 'share', 'meshbay'); fs.mkdirSync(configDir, { recursive: true }); + fs.mkdirSync(dataDir, { recursive: true }); const configFile = nodeConfigPath(); - if (!fs.existsSync(configFile)) { + if (fs.existsSync(configFile)) { + const content = fs.readFileSync(configFile, 'utf8'); + const updated = content + .replace(/^url\s*=\s*"[^"]*"/m, `url = "${hubUrl}"`) + .replace(/^username\s*=\s*"[^"]*"/m, `username = "${username}"`); + fs.writeFileSync(configFile, updated, { mode: 0o600 }); + } else { const toml = [ '[hub]', `url = "${hubUrl}"`, @@ -867,7 +876,9 @@ function registerBridge() { ipcMain.handle('node:start', async (_e, opts) => { const already = await probeNode(); - if (already) return { started: true, ...already }; + if (already && already.status === 'running') { + return { started: true, ...already }; + } if (process.platform !== 'linux') { throw new Error('Automatic node start is only supported on Linux'); @@ -881,8 +892,28 @@ function registerBridge() { const configFile = nodeConfigPath(); let launched = false; + // Clear any failed state from a prior crash loop. + await new Promise((r) => { + execFile('systemctl', ['--user', 'reset-failed', 'meshbay-node'], + () => r()); + }); + + // If a daemon is running in a bad state, restart it with the fresh config. + if (already) { + try { + await new Promise((resolve, reject) => { + execFile('systemctl', ['--user', 'restart', 'meshbay-node'], + (err, _stdout, stderr) => { + if (err) return reject(new Error(stderr.trim() || err.message)); + resolve(); + }); + }); + launched = true; + } catch { /* not systemd-managed — fall through to start */ } + } + // Try systemctl first — the production path. - try { + if (!launched) try { await new Promise((resolve, reject) => { execFile('systemctl', ['--user', 'enable', '--now', 'meshbay-node'], (err, _stdout, stderr) => { @@ -890,27 +921,25 @@ function registerBridge() { resolve(); }); }); - // Give the service a moment, then check if it actually stayed up. + // Give the service a moment, then check if it 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 }; + if (result) { launched = true; break; } } - // 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()); + if (!launched) { + const isActive = await new Promise((resolve) => { + execFile('systemctl', ['--user', 'is-active', 'meshbay-node'], + (err) => resolve(!err)); }); + if (isActive) { + launched = true; + } else { + await new Promise((resolve) => { + execFile('systemctl', ['--user', 'stop', 'meshbay-node'], + () => resolve()); + }); + } } } catch { // systemctl itself failed (e.g. no unit file). @@ -930,45 +959,36 @@ function registerBridge() { child.unref(); } + // Wait for the daemon to reach 'running'. Along the way, auto-link the + // node key on the hub so the daemon can authenticate. let keyLinked = false; while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, 500)); - // If the node's admin UI is up but hub auth is stuck, link the key now. - if (!keyLinked && opts && opts.token) { + const result = await probeNode(); + if (!result) continue; + + if (result.status === 'running') { + return { started: true, ...result }; + } + + // Daemon is up but stuck on hub auth — link the key so it can proceed. + if (!keyLinked && opts && opts.token && result.pk_node_ed25519 && + (result.status === 'waiting_for_node_key' || + result.status === 'waiting_for_account')) { try { - const nc = readNodeConfig(); - const dd = nc ? nc.dataDir - : path.join(os.homedir(), '.local', 'share', 'meshbay'); - const tk = readNodeToken(dd); - if (tk) { - const port = nc ? nc.uiPort : 18000; - const sr = await fetch( - `http://127.0.0.1:${port}/api/status?t=${tk}`, - { signal: AbortSignal.timeout(3000) }); - if (sr.ok) { - const st = await sr.json(); - if (st.pk_node_ed25519 && - (st.status === 'waiting_for_node_key' || - st.status === 'waiting_for_account')) { - const lr = await fetch( - `${opts.hubUrl}/v1/users/me/node_key`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${opts.token}` }, - body: JSON.stringify({ - pk_node_ed25519: st.pk_node_ed25519 }), - signal: AbortSignal.timeout(5000), - }); - if (lr.ok) keyLinked = true; - } - } - } + const lr = await fetch( + `${opts.hubUrl}/v1/users/me/node_key`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', + 'Authorization': `Bearer ${opts.token}` }, + body: JSON.stringify({ + pk_node_ed25519: result.pk_node_ed25519 }), + signal: AbortSignal.timeout(5000), + }); + if (lr.ok) keyLinked = true; } catch { /* best effort */ } } - - const result = await probeNode(); - if (result) return { started: true, ...result }; } throw new Error( 'meshbay-node was started but did not become ready within 60 seconds'); |