aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/app.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-21 17:50:08 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-21 17:50:08 +0200
commit73490d331179cb640828b8625abb75208cd7ae5f (patch)
tree5782835ae06d3f84c4a9c11c8babcec0d8126157 /packages/meshbay-hub/src/meshbay_hub/static/app.js
parent8730281d739d9ed1d6f2d366772582eea8ba0294 (diff)
downloadmeshbay-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.js82
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>`;