aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
diff options
context:
space:
mode:
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.js78
1 files changed, 77 insertions, 1 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 11cd07e..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,6 +22,80 @@ 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('');
@@ -725,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">