diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-settings.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/group-settings.js | 111 |
1 files changed, 110 insertions, 1 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 a14fd21..0b64a03 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -22,6 +22,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, isNodeAdmin, userId, operatorPaired, connected, memberUpload, onMemberUpload, enabledApps, onEnabledApps, + scanSettings, onScanSettings, onPaired, onLeft }) { const [members, setMembers] = useState([]); const [adminId, setAdminId] = useState(''); @@ -36,6 +37,10 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, const [nodeGroupName, setNodeGroupName] = useState(''); const [nodeBusy, setNodeBusy] = useState(false); const [nodeMsg, setNodeMsg] = useState(''); + // Bytes-based indexing progress while a newly added directory is being + // scanned — same source as the Create Group wizard's step, see + // platform.watchIndexProgress. + const [nodeIndexProgress, setNodeIndexProgress] = useState(null); const loadNodeInfo = useCallback(async () => { if (!platform.node.available) return; @@ -194,6 +199,51 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, } }, [transportRef, onEnabledApps, activeApps]); + const [scanBusy, setScanBusy] = useState(false); + const [scanMsg, setScanMsg] = useState(''); + const [reconcileMinutes, setReconcileMinutes] = useState( + scanSettings ? Math.round(scanSettings.reconcile_interval_secs / 60) : 10); + const [debounceSeconds, setDebounceSeconds] = useState( + scanSettings ? Math.round(scanSettings.debounce_secs) : 2); + // The node is the source of truth; once it has answered, the fields track + // it rather than whatever this browser guessed before connecting. + useEffect(() => { + if (!scanSettings) return; + setReconcileMinutes(Math.round(scanSettings.reconcile_interval_secs / 60)); + setDebounceSeconds(Math.round(scanSettings.debounce_secs)); + }, [scanSettings]); + + /** + * How often the reconciliation backstop runs, and how long a changed file + * is left alone before being hashed. Same shape as toggleApp: signed, and + * the fields do not claim success until the node has confirmed it. + */ + const saveScanSettings = useCallback(async () => { + const transport = transportRef && transportRef.current; + setScanMsg(''); + setScanBusy(true); + try { + if (!transport || !transport.connected) { + throw new Error('Not connected to the node'); + } + const sk = transport.sessionKeys && transport.sessionKeys.skEdB64; + const signFn = (sk && window.MeshBayKeys) + ? (transcript) => window.MeshBayKeys.signBytes(sk, transcript) + : null; + await transport.setScanSettings(reconcileMinutes * 60, debounceSeconds, signFn); + const applied = { + reconcile_interval_secs: reconcileMinutes * 60, + debounce_secs: debounceSeconds, + }; + if (onScanSettings) onScanSettings(applied); + setScanMsg(t('settings_node.scan_saved')); + } catch (err) { + setScanMsg(err.message); + } finally { + setScanBusy(false); + } + }, [transportRef, onScanSettings, reconcileMinutes, debounceSeconds]); + const [removing, setRemoving] = useState(''); /** @@ -382,6 +432,37 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, </div> `} + ${/* How hard the node works watching its own disk — indexer.py + DirectoryIndexer. A performance knob, not a permission: it + changes nothing about who can see or do what. */ + isNodeAdmin && connected && html` + <div class="settings-section"> + <h3 class="settings-heading">${t('settings_node.scan_title')}</h3> + <p class="settings-hint">${t('settings_node.scan_hint')}</p> + <div class="settings-row"> + <label class="settings-label"> + ${t('settings_node.scan_reconcile_label')} + <input type="number" min="1" max="1440" step="1" + value=${reconcileMinutes} disabled=${scanBusy} + onInput=${e => setReconcileMinutes(Number(e.target.value))} /> + </label> + </div> + <div class="settings-row"> + <label class="settings-label"> + ${t('settings_node.scan_debounce_label')} + <input type="number" min="0" max="300" step="1" + value=${debounceSeconds} disabled=${scanBusy} + onInput=${e => setDebounceSeconds(Number(e.target.value))} /> + </label> + </div> + <button class="btn btn-small btn-secondary" style="margin-top:8px" + disabled=${scanBusy} onClick=${saveScanSettings}> + ${scanBusy ? t('settings_node.scan_saving') : t('settings_node.scan_save')} + </button> + ${scanMsg && html`<p class="settings-hint">${scanMsg}</p>`} + </div> + `} + ${/* Roots management (Electron-only, when node is local) */ nodeDetected && nodeRoots.length > 0 && html` <div class="settings-section"> @@ -425,12 +506,18 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, onClick=${async () => { const chosen = await platform.rootPicker.choose(); if (!chosen) return; - setNodeBusy(true); setNodeMsg(''); + setNodeBusy(true); setNodeMsg(''); setNodeIndexProgress(null); try { await platform.node.call('POST', '/api/groups/' + groupId + '/roots', { path: chosen.path, name: chosen.name }); await platform.node.call('POST', '/api/reload'); + // The root is already scanning in the background on the + // node regardless of whether anyone watches this — see + // the "closing the client" test in test_hot_reload_*.py. + // This is only about not leaving the operator staring at + // an unchanged screen while it happens. + await platform.watchIndexProgress(groupId, setNodeIndexProgress); setNodeMsg(t('node.root_added')); await loadNodeInfo(); } catch (err) { setNodeMsg(platform.bridgeMessage(err)); } @@ -438,6 +525,28 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, }}> <${Icon} name="folder-plus" /> ${t('node.add_root')} </button> + ${nodeIndexProgress && nodeIndexProgress.scanning && html` + <div class="index-progress" style="margin-top:8px"> + <div class="index-progress-bar"> + <div class="index-progress-fill" style="width:${ + nodeIndexProgress.total_bytes + ? Math.min(100, Math.round( + 100 * nodeIndexProgress.scanned_bytes / nodeIndexProgress.total_bytes)) + : 0}%"></div> + </div> + <div class="index-progress-label">${t('wizard.indexing_progress', { + pct: nodeIndexProgress.total_bytes + ? Math.min(100, Math.round( + 100 * nodeIndexProgress.scanned_bytes / nodeIndexProgress.total_bytes)) + : 0, + })}</div> + ${nodeIndexProgress.current_dir && html` + <div class="index-progress-dir"> + ${t('wizard.indexing_current_dir', { dir: nodeIndexProgress.current_dir })} + </div> + `} + </div> + `} </div> </div> `} |