From 3c44f55f6b0aba77c7ad57d0a5ebe3e55b409473 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 25 Aug 2026 18:18:48 +0200 Subject: fix(hub,node): Create Group wizard silently skipped apps, and lost track of scanning progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_013XSohfUQQiaE77qyFLgSv3 --- .../meshbay-hub/src/meshbay_hub/static/platform.js | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/platform.js') 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 @@ -327,6 +327,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. -- cgit v1.2.3