From 005f3cf83559eaf84fd307584477c40676be1dd3 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 6 Sep 2026 19:27:38 +0200 Subject: fix(client): degrade against a node still speaking MNP 1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SPA is served by the hub, so deploying the hub puts this client in front of every node — including the ones not updated yet. That window is the normal state for as long as an operator takes, and for a node someone else runs it may be indefinite. Three controls were broken across it, and the failure mode is quiet: an unknown message type is logged by the node and never answered, so the click produces a thirty-second wait ending in a timeout with nothing on screen to say the node simply cannot do this. Files' Upload button read `root.writable`, which a 1.0 node does not send — it says `upload`, the same answer under the older name. The button disappeared on every un-upgraded node. It reads both now, and still respects an explicit `writable: false` rather than falling through to the legacy flag. The per-app folder pickers spoke `app_directories`. Videos, Music and Photos each had their own message before that and those still work, so the page chooses by version: an operator on an older node keeps the ability they had. `video_root` and `audio_root` hold one folder, so several are refused with a reason rather than stored as the first and silently truncated. Root management — writable, removable, eject, plug — has no older equivalent to route to, so the table goes read-only with a line saying why and pointing at the `meshbay-node root` commands. Chat's two settings are new with nothing before them and are hidden the same way. None of this was inferred from a payload's shape: `_checkNodeVersion` already parsed the node's version and threw it away, and it is kept now. Coupling a capability to whether some field happens to be present is how a flag flips because an unrelated payload changed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us --- .../src/meshbay_hub/static/group-settings.js | 24 +++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-settings.js') 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 5d0ab07..f8b4aa5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -361,7 +361,8 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, enabledApps, onEnabledApps, scanSettings, onScanSettings, entries, nodeDirs, - appSettings, onAppDirectories, onRefreshIndex, + appSettings, nodeSupportsAppOps, + onAppDirectories, onRefreshIndex, onPaired, onLeft }) { const [members, setMembers] = useState([]); const [adminId, setAdminId] = useState(''); @@ -647,7 +648,14 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, if (!transport || !transport.connected) { throw new Error(t('node.root_no_route')); } - await transport.setAppDirectories(appKey, paths, adminSignFn); + // A node too old for the generic op still answers the three per-app + // messages that came before it, so an operator on one keeps the ability + // they had rather than being handed a control that times out. + if (transport.supportsAppOps) { + await transport.setAppDirectories(appKey, paths, adminSignFn); + } else { + await transport.setAppDirectoriesLegacy(appKey, paths, adminSignFn); + } if (onAppDirectories) onAppDirectories(appKey, paths); }, [transportRef, adminSignFn, onAppDirectories]); @@ -845,11 +853,19 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,

${t('settings_node.shared_directories_hint')}

${!connected && nodeDetected && html`

${t('settings_node.roots_offline_hint')}

`} + ${/* Read-only against a node that predates the root operations: + writable, removable, eject and plug have no older equivalent + to fall back to, and an unknown message type is dropped + unanswered — a thirty-second wait ending in a timeout, with + nothing on screen to say the node simply cannot do it. */''} + ${connected && !nodeSupportsAppOps && !nodeDetected && html` +

${t('settings_node.roots_node_too_old')}

`} <${SharedDirectoriesTable} roots=${effectiveRoots} groupId=${groupId} transport=${transportRef.current} signFn=${adminSignFn} + readOnly=${connected && !nodeSupportsAppOps && !nodeDetected} nodeDetected=${nodeDetected} onRootsChange=${loadNodeInfo} onRefreshIndex=${onRefreshIndex} /> @@ -876,7 +892,9 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, disabled=${appsBusy} onChange=${() => toggleApp(app.key)} /> `}> - ${activeApps.includes(app.key) + ${!nodeSupportsAppOps && app.key === 'chat' + ? html`

${t('settings_node.app_node_too_old')}

` + : activeApps.includes(app.key) ? html`<${app.Settings} roots=${effectiveRoots} dirs=${folderOptions} settings=${appSettings} -- cgit v1.2.3