aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
diff options
context:
space:
mode:
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.js111
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>
`}