aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-29 11:15:54 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-29 11:15:54 +0200
commitb7733e812fadd6007976d262bd1d793572a36ba7 (patch)
tree52e2ac07c9ba4929fa50b6ba8d19f94f86e8b304 /packages/meshbay-client
parent09a007937f03fad04a390323f3817f1ae7d7c2a9 (diff)
downloadmeshbay-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')
-rw-r--r--packages/meshbay-client/src/main.js126
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');