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/meshbay-hub/src/meshbay_hub/static/app.js | |
| 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/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 82 |
1 files changed, 67 insertions, 15 deletions
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>`; |