summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-25 18:18:48 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-25 18:18:48 +0200
commit3c44f55f6b0aba77c7ad57d0a5ebe3e55b409473 (patch)
tree6b79cb92c785378b855da3862a3bf371abe701bc /packages/meshbay-hub/src
parentbbc76ad4da3ca61f820b9605ec9228c7a9357352 (diff)
downloadmeshbay-3c44f55f6b0aba77c7ad57d0a5ebe3e55b409473.tar.gz
fix(hub,node): Create Group wizard silently skipped apps, and lost track of scanning progress
Two real-world bugs found together while testing multi-root group creation: - CreateGroupWizard only sent the enabled-apps PUT when the operator had *unchecked* something, assuming "every box left checked" already matched the node's own default (Roster.DEFAULT_APPS = chat, files). It doesn't — so leaving every app checked, the common case, silently left Videos/Music/Photos disabled on the node. Now sent unconditionally. - The wizard's "add extra roots" step never polled index-status, so once step 3 (which only watches the first/upload root) finished, the progress bar froze while the node kept scanning the remaining roots for minutes, unwatched. Added waitForRootsIndexed (platform.js), mirroring waitForGroupHosted's own race handling. That fix exposed a deeper one: indexer.py's _scan_root() only flipped `progress.scanning` on *after* walking the directory and stat()-ing every file — both off-loop, but slow enough on a large root that a poller's grace period (waitForRootsIndexed's 5s) could expire before ever observing `scanning: true` (confirmed against production logs: a GEK-init step fired 5.058s after a root started scanning, matching the grace period almost exactly). The stat() pass was also a synchronous loop directly on the asyncio event loop — blocking the whole daemon (WebRTC, chat, admin UI) for as long as it took on a root with many files. Both fixed: `scanning` now flips on before the walk starts, and stat()-ing is now off-loop too (_size_files). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XSohfUQQiaE77qyFLgSv3
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js37
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js40
2 files changed, 64 insertions, 13 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 1530ff5..5ec1ac8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -926,11 +926,15 @@ function CreateGroupWizard({ token, username, onCreated }) {
{ label: t('wizard.step_attach'), status: 'pending' },
];
steps.push({ label: t('wizard.step_index'), status: 'pending' });
- // Only a step at all when it does something — the common case (every
- // app left on, the default) has nothing to set and no reason to show a
- // step for it. After indexing (see the execution order below for why).
- if (enabledApps.length < APPS.length)
- steps.push({ label: t('wizard.step_apps'), status: 'pending' });
+ // Always a step: the node's own default for a brand-new group is
+ // `chat, files` only (Roster.DEFAULT_APPS) — narrower than "every app
+ // checked" here, which is this wizard's own default. Skipping this call
+ // whenever nothing was *unchecked* used to assume those two defaults
+ // agreed; they don't, so leaving every box checked — the common,
+ // recommended case — silently left Videos/Music/Photos disabled on the
+ // node (found live 2026-08-25: no `set_enabled_apps`/"Enabled apps for
+ // group" ever logged for a group created with every app left on).
+ steps.push({ label: t('wizard.step_apps'), status: 'pending' });
if (roots.length > 1)
steps.push({ label: t('wizard.step_add_roots'), status: 'pending' });
steps.push({ label: t('wizard.step_gek'), status: 'pending' });
@@ -1009,7 +1013,10 @@ function CreateGroupWizard({ token, username, onCreated }) {
update('done');
advance();
- // 4. Narrow the enabled apps down, if the operator unchecked any.
+ // 4. Set the enabled apps — unconditionally (see the step-list
+ // comment above on why "only if narrowed" was wrong: the node's own
+ // default is not "every app", so leaving every box checked must still
+ // be told to the node explicitly).
// Used to run *before* the scan above, reasoning that a member
// joining mid-scan should never briefly see an app meant to be off —
// but nobody can join before the group is hosted either (same
@@ -1019,13 +1026,11 @@ function CreateGroupWizard({ token, username, onCreated }) {
// indefinite "Group not hosted on this node" (found live against a
// real, several-thousand-file library: withRetry's five attempts
// don't come close to covering a scan that takes minutes).
- if (enabledApps.length < APPS.length) {
- update('running');
- await withRetry(() => platform.node.call(
- 'PUT', `/api/groups/${gid}/apps`, { apps: enabledApps }));
- update('done');
- advance();
- }
+ update('running');
+ await withRetry(() => platform.node.call(
+ 'PUT', `/api/groups/${gid}/apps`, { apps: enabledApps }));
+ update('done');
+ advance();
// 5. Add extra roots (if >1)
if (roots.length > 1) {
@@ -1038,6 +1043,12 @@ function CreateGroupWizard({ token, username, onCreated }) {
upload: i === uploadIdx,
}));
}
+ // Each add above only schedules its scan (platform.js's
+ // waitForRootsIndexed docstring) — wait for it to actually finish,
+ // reusing the same progress bar step 3 fed, or this step reports
+ // "done" while the node is still hashing gigabytes behind the
+ // scenes (found live 2026-08-25).
+ await platform.waitForRootsIndexed(gid, setIndexProgress);
update('done');
advance();
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index a5312e1..78e27eb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -328,6 +328,46 @@ export async function waitForGroupHosted(groupId, onProgress,
}
/**
+ * Create Group wizard only: after adding extra roots (step 5), wait for
+ * whatever scanning that triggers to actually finish, showing progress along
+ * the way. `POST /api/groups/{id}/roots` (ui/app.py) schedules its rescan as
+ * a detached background task and returns as soon as the config write is
+ * done — "every add-root call resolved" is not "the node is done indexing".
+ * Found live (2026-08-25): a multi-root group's later, larger roots kept
+ * scanning for minutes after the wizard had already moved on to GEK init and
+ * pairing, with no progress shown anywhere — the disk was working, the UI
+ * just never asked again.
+ *
+ * Same race as waitForGroupHosted above, one level down: the very first
+ * poll can land in the gap between "the last add-root call returned" and
+ * "its background reload actually started scanning", which reads as "not
+ * scanning" for the wrong reason (nothing left to do) rather than the right
+ * one (hasn't started yet). Waits up to `graceMs` for scanning to be
+ * observed at least once before trusting a "not scanning" answer — after
+ * that, the first "not scanning" really does mean finished, because the
+ * node scans one root at a time (indexer.py's single-worker executor) and
+ * nothing here adds more roots once this call starts.
+ */
+export async function waitForRootsIndexed(groupId, onProgress,
+ { intervalMs = 500, graceMs = 5000 } = {}) {
+ const graceDeadline = Date.now() + graceMs;
+ let sawScanning = false;
+ for (;;) {
+ let status;
+ try {
+ status = await node.call('GET', `/api/groups/${groupId}/index-status`);
+ } catch {
+ return; // the node went away mid-poll — same stance as watchIndexProgress
+ }
+ if (onProgress) onProgress(status);
+ if (status.scanning) sawScanning = true;
+ if (sawScanning && !status.scanning) return;
+ if (!sawScanning && Date.now() > graceDeadline) return;
+ await new Promise((r) => setTimeout(r, intervalMs));
+ }
+}
+
+/**
* LAN cast relay — re-serve decrypted video segments over HTTP so a
* Chromecast or Smart TV on the same Wi-Fi can play the stream.
*