From 9c136a0e37add42f5d0c8675a797969cfe690de0 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 22 Aug 2026 16:40:54 +0200 Subject: fix: first-run wizard reliability and node startup performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node daemon no longer blocks startup on slow directory scans — initial indexing runs in the background so the node reaches "running" immediately after transports are up. Fixes the wizard failing to detect the node when large USB/NAS roots take minutes to scan. Also: wizard key-linking deadlock resolved (main.js links during poll), invite form stays in DOM during reconnects (disabled instead of destroyed), pairing code bridges to renderer, and firewall docs for LAN casting added. Co-Authored-By: Claude Opus 4.6 --- packages/meshbay-node/src/meshbay_node/daemon.py | 64 +++++++++++++----------- 1 file changed, 35 insertions(+), 29 deletions(-) (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py') diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index e11a654..5fc70fe 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -241,14 +241,13 @@ class NodeDaemon: gek=gek, on_change=self._on_index_change, ) - await indexer.start() + await indexer.start(defer_scan=True) self._indexers.append(indexer) self._state["indexes"][group_cfg.id] = indexer.index self._state["indexers"][group_cfg.id] = indexer - log.info("Indexing group %s: %s (%d files)", + log.info("Group %s configured: %s (scan deferred)", group_cfg.name, - ", ".join(f"{r.name}={r.path}" for r in roots), - indexer.index.count) + ", ".join(f"{r.name}={r.path}" for r in roots)) groups_ctx[group_cfg.id] = { "gek": gek, @@ -268,8 +267,8 @@ class NodeDaemon: } if not groups_ctx: - log.error("No valid groups configured — exiting") - return + log.warning("No groups configured yet — admin UI and hub " + "connection stay up; attach a group to go live") # 5. Chat stores (one SQLite DB per group) for gid in groups_ctx: @@ -290,14 +289,14 @@ class NodeDaemon: denylist = self._denylist # 6. WebRTC transport (browser clients) - first = next(iter(groups_ctx.values())) + first = next(iter(groups_ctx.values()), None) if WEBRTC_AVAILABLE: self._webrtc = WebRTCTransport( sk_node=keys.sk_ed25519, hub_pk_pem=session.hub_pk_pem, - gek=first["gek"], - roots=first["roots"], - index=first["index"], + gek=first["gek"] if first else None, + roots=first["roots"] if first else None, + index=first["index"] if first else None, groups=groups_ctx, denylist=denylist, max_concurrent_streams=self._config.node.max_concurrent_streams, @@ -307,6 +306,7 @@ class NodeDaemon: # _group_ctx(). Assigning the first group's store transport-wide # sent every group's chat to one database and served it back to # members of every other group (finding H1). + self._webrtc._ctx["groups"] = groups_ctx self._webrtc._ctx["hub_ws"] = _WsSender(hub) self._webrtc._ctx["node_user_id"] = session.user_id self._webrtc._ctx["audit_store"] = self._audit_store @@ -342,14 +342,15 @@ class NodeDaemon: self._quic_server = QuicChunkServer( sk_node=keys.sk_ed25519, hub_pk_pem=session.hub_pk_pem, - gek=first["gek"], - roots=first["roots"], - index=first["index"], + gek=first["gek"] if first else None, + roots=first["roots"] if first else None, + index=first["index"] if first else None, host="::", port=self._config.node.quic_port, groups=groups_ctx, denylist=denylist, ) + self._quic_server._ctx["groups"] = groups_ctx await self._quic_server.start() log.info("QUIC server on port %d (%d groups)", self._config.node.quic_port, len(groups_ctx)) @@ -434,18 +435,23 @@ class NodeDaemon: "yes" if self._webrtc else "no", "yes" if self._quic_server else "no") - # 11. Initial swarm registration — PUBLIC groups only. - # Finding H7: registering every group's hashes hands the hub a content - # fingerprint of every private file on the node, which is exactly the - # metadata the "hub stores no content metadata" claim rules out. It also - # lets anyone confirm whether a known file exists in the network. - endpoint = f"webrtc:{self._config.node.quic_port}" - for gctx in groups_ctx.values(): - if gctx.get("visibility") != "public": - continue - hashes = [e.id for e in gctx["index"].entries] - if hashes: - asyncio.ensure_future(self._register_swarm(hashes, endpoint)) + # 11. Background initial scan — files appear progressively. + async def _bg_scan(indexer, name, gctx): + await indexer.initial_scan() + log.info("Background scan complete for %s: %d files", + name, indexer.index.count) + # Swarm registration for public groups (after files are known). + if gctx.get("visibility") == "public": + endpoint = f"webrtc:{self._config.node.quic_port}" + hashes = [e.id for e in gctx["index"].entries] + if hashes: + await self._register_swarm(hashes, endpoint) + + for idx, group_cfg in zip(self._indexers, self._config.groups): + gctx = groups_ctx.get(group_cfg.id) + if gctx: + self._tasks.append(asyncio.create_task( + _bg_scan(idx, group_cfg.name, gctx))) # 12. Wait for shutdown stop_event = asyncio.Event() @@ -486,7 +492,7 @@ class NodeDaemon: log.error("Reload failed, keeping the running config: %s", e) return - groups_ctx = self._state.get("groups_ctx") or {} + groups_ctx = self._state.get("groups_ctx", {}) hosted = set(groups_ctx) incoming = {g.id for g in fresh.groups if g.id} @@ -647,7 +653,7 @@ class NodeDaemon: log.warning( "Node key not linked. Open the admin UI, copy this " "node's key, and paste it in Settings > Link Node on " - "%s. Retrying in 30s...", + "%s. Retrying in 5s...", self._config.hub.url, ) else: @@ -655,10 +661,10 @@ class NodeDaemon: log.warning( "Hub rejected the node credentials for user %r. " "Register that account on %s first, then link this " - "node's key. Retrying in 30s...", + "node's key. Retrying in 5s...", self._config.hub.username, self._config.hub.url, ) - await asyncio.sleep(30) + await asyncio.sleep(5) else: raise except Exception as e: -- cgit v1.2.3