From 06c101154d544290d27f18d6fc08fb5f58a4e5d5 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 25 Aug 2026 00:29:03 +0200 Subject: fix(hub): poll for a root count change instead of trusting one fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Point 1 (directory list not updating without a full page reload) turned out to still reproduce after the earlier onRefreshIndex fix — that one addressed the Videos/Music root pickers (nodeDirs), but this section's own list reads a different field entirely (ops.list_groups returns the *runtime* root set, groups_ctx[gid]["roots"]) that only gets replaced once _reload_config_inner's retarget actually finishes. /api/reload itself is fire-and-forget on the node by design (ops.start_reload's own docstring — a brand-new group's initial scan can take minutes, the caller must not block on it), so a single loadNodeInfo() call right after can land in the gap before that replacement happens and show the pre-change count. Both the add and remove handlers now poll /api/groups (up to ~4s, every 400ms) until the root count actually matches what the action should have produced, rather than fetching once and hoping the timing worked out. --- .../src/meshbay_hub/static/group-settings.js | 36 ++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js index 34f94f9..a4db4cb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -157,6 +157,36 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, }, [groupId]); useEffect(() => { loadNodeInfo(); }, [loadNodeInfo]); + + /** + * Poll /api/groups until the root count actually matches what an + * add/remove just did, instead of trusting a single loadNodeInfo() call + * right after /api/reload. Found live: /api/reload is fire-and-forget on + * the node (ops.start_reload schedules the real work and returns + * immediately, deliberately — a brand-new group's initial scan can take + * minutes, see its own docstring) — and the list this section renders + * (ops.list_groups) reads the *runtime* root set + * (groups_ctx[gid]["roots"]), which only gets replaced once + * _reload_config_inner's retarget actually finishes, not the config-file + * list add_root/remove_root already updated synchronously. A single + * fetch right after can land in that gap and show the old count. + */ + const waitForRootCount = useCallback(async (expectedCount) => { + for (let i = 0; i < 10; i++) { + try { + const data = await platform.node.call('GET', '/api/groups'); + const ng = (data.groups || []).find(g => g.id === groupId); + const roots = (ng && ng.roots) || []; + if (roots.length === expectedCount) { + setNodeRoots(roots); + if (ng) setNodeGroupName(ng.name || ''); + return true; + } + } catch { /* keep trying — the node may be mid-reload */ } + await new Promise(r => setTimeout(r, 400)); + } + return false; + }, [groupId]); const [inviteCode, setInviteCode] = useState(null); const [pairCode, setPairCode] = useState(''); const [pairStatus, setPairStatus] = useState(''); @@ -976,13 +1006,14 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, disabled=${nodeBusy} onClick=${async () => { if (!confirm(t('node.root_remove_confirm', { name: r.name }))) return; + const countBefore = nodeRoots.length; setNodeBusy(true); setNodeMsg(''); try { await platform.node.call('DELETE', '/api/groups/' + groupId + '/roots/' + encodeURIComponent(r.name)); await platform.node.call('POST', '/api/reload'); setNodeMsg(t('node.root_removed')); - await loadNodeInfo(); + await waitForRootCount(countBefore - 1); // Folders (unlike files) only ever arrive via a full // index_sync, never index_delta (daemon.py's ongoing // push has no `dirs` field) — without this, the @@ -1000,6 +1031,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, onClick=${async () => { const chosen = await platform.rootPicker.choose(); if (!chosen) return; + const countBefore = nodeRoots.length; setNodeBusy(true); setNodeMsg(''); setNodeIndexProgress(null); try { await platform.node.call('POST', @@ -1013,7 +1045,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, // an unchanged screen while it happens. await platform.watchIndexProgress(groupId, setNodeIndexProgress); setNodeMsg(t('node.root_added')); - await loadNodeInfo(); + await waitForRootCount(countBefore + 1); // See the matching comment on root removal above — a new // folder needs a full index_sync to show up anywhere that // reads `nodeDirs` (the Videos/Music root pickers), not -- cgit v1.2.3