aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-27 22:21:07 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-27 22:21:07 +0200
commitc7899e2218af26f3bab6a157c70acf5eb7871da6 (patch)
tree5ed08cfbf26cdd8d9dbf6ce1159fc259d611df2d /packages
parent7422a777297347dc83e6c5b2e89c1a6fa99a36b0 (diff)
downloadmeshbay-c7899e2218af26f3bab6a157c70acf5eb7871da6.tar.gz
fix(hub): the sidebar's Node section follows the node link
Reported on a real install: after the first click on Create group, the Node section (Node, Create group) disappeared from the sidebar until a reload, although the group was created and the node ran. hasNodeKey was read once per session change and never again, so a node the wizard linked stayed out of the sidebar; and that read swallowed its errors, so a session blip (a refused renewal, then the desktop app's silent device sign-in) followed by one failed request hid the section for good. The wizard now tells the app when it has linked or started a node, the app asks again then and after a group is created, and a failed read is retried -- never applied to a session that has changed meanwhile. The wizard also starts the node on its own for a node in waiting_for_hub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js33
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js10
-rw-r--r--packages/meshbay-hub/tests/test_sidebar_node_section.py55
3 files changed, 90 insertions, 8 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 6b20a63..3101be7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -924,8 +924,31 @@ function App() {
.catch(() => {});
}, [user, notifDisabled]);
+ // Whether this account has a node linked, which is what shows the sidebar's
+ // Node section. Asked again whenever a node may just have been linked (the
+ // Create Group wizard), not only when the session changes -- the section
+ // stayed hidden until a reload after the first group -- and retried rather
+ // than left false by one failed request after a session blip.
+ const nodeKeyAskedForRef = useRef(null);
+ const refreshNodeKey = useCallback(() => {
+ nodeKeyAskedForRef.current = user;
+ if (!user || !platform.capabilities.nodeAdmin) return;
+ // A late answer, or a retry, must not land on a session that has changed.
+ const current = () => nodeKeyAskedForRef.current === user;
+ const ask = (attempt) => hubFetch(`/v1/users/${user.username}/pubkeys`, { token: user.token })
+ .then(data => { if (current()) setHasNodeKey(Boolean(data.pk_node_ed25519)); })
+ .catch(() => {
+ if (attempt < 3 && current()) setTimeout(() => ask(attempt + 1), 2000 * attempt);
+ });
+ ask(1);
+ }, [user]);
+
useEffect(() => {
- if (!user) { setGroups([]); setNotifications([]); setUnreadCount(0); setHasNodeKey(false); return; }
+ if (!user) {
+ nodeKeyAskedForRef.current = null;
+ setGroups([]); setNotifications([]); setUnreadCount(0); setHasNodeKey(false);
+ return;
+ }
hubFetch('/v1/groups/mine', { token: user.token })
.then(data => setGroups(data.groups || []))
.catch(() => setGroups([]));
@@ -935,11 +958,7 @@ function App() {
if (prefs.notifications_disabled === 'true') setNotifDisabled(true);
})
.catch(() => {});
- if (platform.capabilities.nodeAdmin) {
- hubFetch(`/v1/users/${user.username}/pubkeys`, { token: user.token })
- .then(data => setHasNodeKey(Boolean(data.pk_node_ed25519)))
- .catch(() => {});
- }
+ refreshNodeKey();
fetchNotifications();
}, [user]);
@@ -1176,10 +1195,12 @@ function App() {
} else if (route === '/create-group') {
page = html`<${LazyCreateGroupPage} token=${user.token} username=${user.username}
allowPublicGroups=${allowPublicGroups}
+ onNodeLinked=${refreshNodeKey}
onCreated=${() => {
hubFetch('/v1/groups/mine', { token: user.token })
.then(data => setGroups(data.groups || []))
.catch(() => {});
+ refreshNodeKey();
}} />`;
} else if (route === '/node' && platform.capabilities.nodeAdmin && hasNodeKey) {
page = html`<${LazyNodePage} groups=${groups} token=${user.token} username=${user.username} />`;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js
index 85fe53c..7556010 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/create-group-page.js
@@ -124,7 +124,7 @@ function CreateGroupFormSimple({ token, onCreated, allowPublicGroups = true }) {
`;
}
-function CreateGroupWizard({ token, username, onCreated, allowPublicGroups = true }) {
+function CreateGroupWizard({ token, username, onCreated, onNodeLinked, allowPublicGroups = true }) {
const [step, setStep] = useState(0);
const [nodeStatus, setNodeStatus] = useState(null);
const [nodeStarting, setNodeStarting] = useState(false);
@@ -151,7 +151,10 @@ function CreateGroupWizard({ token, username, onCreated, allowPublicGroups = tru
await hubFetch('/v1/users/me/node_key', {
method: 'PUT', token, body: { pk_node_ed25519: pk },
});
- }, [token]);
+ // The sidebar's Node section depends on this link; it only re-read it at
+ // sign-in, so the first node set up here stayed out of it until a reload.
+ if (onNodeLinked) onNodeLinked();
+ }, [token, onNodeLinked]);
const detectNode = useCallback(async () => {
setNodeStatus(null);
@@ -177,6 +180,8 @@ function CreateGroupWizard({ token, username, onCreated, allowPublicGroups = tru
setError('');
try {
const result = await platform.node.start({ hubUrl: HUB, username, token });
+ // node.start links the key from the main process, out of this page's sight.
+ if (onNodeLinked) onNodeLinked();
setNodeStatus({ detected: true, ...result });
setNodeStarting(false);
setStep(1);
@@ -337,6 +342,7 @@ function CreateGroupWizard({ token, username, onCreated, allowPublicGroups = tru
&& !provisionAttempted.current
&& (nodeStatus.status === 'waiting_for_account'
|| nodeStatus.status === 'waiting_for_node_key'
+ || nodeStatus.status === 'waiting_for_hub'
|| nodeStatus.status === 'starting')) {
provisionAttempted.current = true;
startNode();
diff --git a/packages/meshbay-hub/tests/test_sidebar_node_section.py b/packages/meshbay-hub/tests/test_sidebar_node_section.py
new file mode 100644
index 0000000..371a018
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_sidebar_node_section.py
@@ -0,0 +1,55 @@
+"""
+The sidebar's Node section follows the account's node link, and must follow it
+when it changes -- not only at sign-in.
+
+Reported on a real install: after the first click on Create group, the Node
+section (Node, Create group) was gone from the sidebar until a reload, while
+the group was created and the node ran. `hasNodeKey` was read once per session
+change and never again, so a node linked by the wizard stayed out of the
+sidebar; and the one read there was swallowed its errors, so a session blip
+(a refused renewal, then the desktop app's silent device sign-in) followed by
+one failed request left the section hidden for good.
+
+Read from the source, like the rest of the SPA's wiring tests: the state lives
+inside App, and what matters is the seam between two modules.
+"""
+
+import re
+from pathlib import Path
+
+STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
+APP = (STATIC / "app.js").read_text(encoding="utf-8")
+WIZARD = (STATIC / "create-group-page.js").read_text(encoding="utf-8")
+
+
+def _body(src: str, start: str, end: str) -> str:
+ return src.split(start, 1)[1].split(end, 1)[0]
+
+
+def test_the_create_group_page_can_tell_the_app_a_node_was_linked():
+ page = _body(APP, "<${LazyCreateGroupPage}", "/>`;")
+ assert "onNodeLinked=${refreshNodeKey}" in page
+ created = _body(page, "onCreated=${() => {", "}}")
+ assert "refreshNodeKey()" in created
+
+
+def test_the_wizard_says_so_after_each_way_it_links_one():
+ link = _body(WIZARD, "const linkNodeKey = useCallback(", "}, [")
+ assert link.index("/v1/users/me/node_key") < link.index("onNodeLinked()"), (
+ "the app must be told after the hub has the key, not before")
+ start = _body(WIZARD, "const startNode = useCallback(", "}, [")
+ assert start.index("platform.node.start(") < start.index("onNodeLinked()")
+
+
+def test_one_failed_read_does_not_hide_the_section_for_good():
+ refresh = _body(APP, "const refreshNodeKey = useCallback(", "}, [user]);")
+ assert "/pubkeys" in refresh
+ assert not re.search(r"\.catch\(\(\)\s*=>\s*\{\s*\}\)", refresh), (
+ "a swallowed failure leaves hasNodeKey false until a reload")
+ assert "setTimeout(" in refresh, "a failed read is tried again"
+ assert "nodeKeyAskedForRef.current === user" in refresh, (
+ "a late answer must not land on a session that has changed")
+
+
+def test_the_session_effect_uses_the_same_read():
+ assert APP.count("/pubkeys`") == 1, "one place decides hasNodeKey"