diff options
Diffstat (limited to 'packages/meshbay-hub')
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" |