diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/node-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/node-page.js | 97 |
1 files changed, 90 insertions, 7 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js index 04f4abc..56166c6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js @@ -6,7 +6,7 @@ import { t } from './i18n.js'; import { ask } from './ask.js'; import * as platform from './platform.js'; import { Icon } from './icon.js'; -import { HUB, session } from './hub-client.js'; +import { HUB, session, hubFetch } from './hub-client.js'; // ── Node management (D5) ──────────────────────────────────────────────────── // @@ -22,18 +22,97 @@ export function pairedFrom(result) { return v === true || v === false ? v : null; } +// Why this machine's node cannot serve the signed-in account, if it cannot: +// 'other_account' when it is set up for another account or hub, 'other_node' +// when the account is linked to another node's key. Signing in links this +// node (main.js ensureNode) but never over either, since that would cut off +// the user's other machine or someone else's node; both left a node reading +// "Running" that the hub refused in a loop, with nothing here saying why +// (found on a real install, 2026-10-10). Worked out from the node and the hub +// each time the page looks, not kept from sign-in: a link made or removed on +// the Profile page or on the other machine changes the answer. +export function nodeLinkProblem(info, linkedKey, username, hub) { + if (!info || !username) return null; + const sameHub = (a, b) => String(a || '').replace(/\/+$/, '') === String(b || '').replace(/\/+$/, ''); + if ((info.username && info.username !== username) + || (info.hub_url && hub && !sameHub(info.hub_url, hub))) return 'other_account'; + if (linkedKey && info.pk_node_ed25519 && linkedKey !== info.pk_node_ed25519) return 'other_node'; + return null; +} + +function NodeLinkBanner({ info, token, username, onLinked }) { + const [linkedKey, setLinkedKey] = useState(null); + const [busy, setBusy] = useState(false); + const [err, setErr] = useState(''); + const nodeKey = info && info.pk_node_ed25519; + + useEffect(() => { + if (!username || !token) return; + let cancelled = false; + hubFetch(`/v1/users/${encodeURIComponent(username)}/pubkeys`, { token }) + .then((k) => { if (!cancelled) setLinkedKey((k && k.pk_node_ed25519) || null); }) + .catch(() => {}); + return () => { cancelled = true; }; + }, [username, token, nodeKey]); + + const problem = nodeLinkProblem(info, linkedKey, username, HUB); + if (!problem) return null; + + // Another node's key: this node's replaces it on the account, and a node + // waiting for its account signs in on its next attempt. Not node:start, + // which leaves a node that answers "running" alone, and one can (signed in + // before the account was linked elsewhere). Another account: node:start + // switches it, after asking, restarts it and links it. + const takeOver = async () => { + if (problem === 'other_node' && !await ask(t('node.link_here_confirm'))) return; + setBusy(true); + setErr(''); + try { + if (problem === 'other_node') { + await hubFetch('/v1/users/me/node_key', { + method: 'PUT', token, body: { pk_node_ed25519: nodeKey }, + }); + } else { + await platform.node.start({ hubUrl: HUB, username, token, takeOver: true }); + } + setLinkedKey(nodeKey); + if (onLinked) onLinked(); + } catch (e) { + setErr(platform.bridgeMessage(e)); + } finally { + setBusy(false); + } + }; + + return html` + <div class="node-pair-banner node-link-banner"> + <p>${problem === 'other_node' + ? t('node.link_other_node') + : t('node.link_other_account', { account: `${info.username} @ ${info.hub_url}` })}</p> + <button class="btn btn-primary btn-small" disabled=${busy} onClick=${takeOver}> + ${busy ? t('node.link_linking') + : problem === 'other_node' ? t('node.link_here') : t('node.link_take_over')}</button> + ${err && html`<p class="error-msg">${err}</p>`} + </div>`; +} + function NodeServicePanel({ onChanged, token, username }) { const [info, setInfo] = useState(null); const [busy, setBusy] = useState(''); - const [err, setErr] = useState(''); + // Two errors, not one: the status poll below cleared the message of an + // action that had just been refused, within five seconds and often before + // it was read (found switching startup mode on a real Store install). + const [pollErr, setPollErr] = useState(''); + const [actErr, setActErr] = useState(''); + const err = actErr || pollErr; const refresh = useCallback(async () => { try { const r = await platform.node.service.status(); setInfo(r); - setErr(''); + setPollErr(''); } catch (e) { - setErr(platform.bridgeMessage(e)); + setPollErr(platform.bridgeMessage(e)); } }, []); @@ -46,13 +125,13 @@ function NodeServicePanel({ onChanged, token, username }) { const act = useCallback(async (name, fn) => { setBusy(name); - setErr(''); + setActErr(''); try { await fn(); await refresh(); if (onChanged) onChanged(); } catch (e) { - setErr(platform.bridgeMessage(e)); + setActErr(platform.bridgeMessage(e)); } finally { setBusy(''); } @@ -162,7 +241,9 @@ function NodeServicePanel({ onChanged, token, username }) { ${busy === 'startupMode' && html` <p class="settings-hint">${t('node.startup_mode_updating')}</p>`} ${!info.canElevate && html` - <p class="settings-hint">${t('node.startup_mode_service_unavailable_hint')}</p>`} + <p class="settings-hint">${t(info.store + ? 'node.startup_mode_service_store_hint' + : 'node.startup_mode_service_unavailable_hint')}</p>`} `} </div>`; } @@ -718,6 +799,8 @@ export function NodePage({ groups, token, username }) { ${t('node.reload')}</button> </div> <${NodeServicePanel} onChanged=${fetchStatus} token=${token} username=${username} /> + <${NodeLinkBanner} info=${nodeInfo} token=${token} username=${username} + onLinked=${fetchStatus} /> ${actionMsg && html`<div class="node-message">${actionMsg}</div>`} ${operatorPaired === false && nodeGroups.length > 0 && html` <div class="node-pair-banner"> |