diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 91 |
1 files changed, 80 insertions, 11 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index d18d9a4..044457b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -298,7 +298,7 @@ function Nav({ user, theme, onThemeChange, onLogout, onMenuToggle, unreadCount, // ── Sidebar ────────────────────────────────────────────────────────────────── -function Sidebar({ groups, presence, route, menuOpen, role, hasNodeKey }) { +function Sidebar({ groups, presence, indexProgressPct, route, menuOpen, role, hasNodeKey }) { const isStaff = role === 'moderator' || role === 'admin'; return html` <aside class="sidebar ${menuOpen ? 'open' : ''}"> @@ -339,13 +339,16 @@ function Sidebar({ groups, presence, route, menuOpen, role, hasNodeKey }) { // actually cares about. Anything else is "not known yet". const state = presence[g.id] ?? (g.node_online === true ? 'online' : g.node_online === false ? 'offline' : 'unknown'); + const label = state === 'indexing' + ? t('presence.indexing', { pct: indexProgressPct[g.id] ?? 0 }) + : t('presence.' + state); return html` <a key=${g.id} class="sidebar-item ${route === '/group/' + g.id ? 'active' : ''}" href="#/group/${g.id}"> <span class="presence presence-${state}" - title="${t('presence.' + state)}" - aria-label="${t('presence.' + state)}"></span> + title="${label}" + aria-label="${label}"></span> <span class="sidebar-item-name">${g.name}</span> </a> `; @@ -840,6 +843,9 @@ function CreateGroupWizard({ token, username, onCreated }) { const [setupSteps, setSetupSteps] = useState([]); const [setupError, setSetupError] = useState(''); const [groupId, setGroupId] = useState(''); + // Bytes-based, not file-count-based: one 20 GB file finishing last must + // not read as "8 of 9 done" while it is still the only thing running. + const [indexProgress, setIndexProgress] = useState(null); const linkNodeKey = useCallback(async (pk) => { if (!pk) return; @@ -908,12 +914,14 @@ function CreateGroupWizard({ token, username, onCreated }) { const steps = [ { label: t('wizard.step_create_hub'), status: 'pending' }, { label: t('wizard.step_attach'), status: 'pending' }, + { label: t('wizard.step_index'), status: 'pending' }, ]; if (roots.length > 1) steps.push({ label: t('wizard.step_add_roots'), status: 'pending' }); steps.push({ label: t('wizard.step_gek'), status: 'pending' }); steps.push({ label: t('wizard.step_pair'), status: 'pending' }); setSetupSteps([...steps]); + setIndexProgress(null); let si = 0; const update = (status) => { @@ -922,6 +930,27 @@ function CreateGroupWizard({ token, username, onCreated }) { }; const advance = () => { si++; }; + // Every step from here on is scoped to the group the node just attached. + // The node hot-loads a brand-new group synchronously — scan included — + // before it is added to groups_ctx or its own in-memory config + // (daemon.py _reload_config_inner: the config swap is the *last* thing + // that function does, a beat after the scan, not atomic with it) — so a + // call that lands in that beat gets refused even though the wait above + // already reported the scan as done. A handful of short retries absorbs + // that gap without a real cross-process synchronization primitive. + const withRetry = async (fn, attempts = 5, delayMs = 400) => { + for (let i = 0; i < attempts; i++) { + try { + return await fn(); + } catch (err) { + const msg = String((err && err.message) || ''); + const notHostedYet = /not configured on this node|not hosted on this node/i.test(msg); + if (!notHostedYet || i === attempts - 1) throw err; + await new Promise((r) => setTimeout(r, delayMs)); + } + } + }; + try { // 1. Create group on hub update('running'); @@ -942,32 +971,48 @@ function CreateGroupWizard({ token, username, onCreated }) { attachBody.upload_dir = mainRoot.path; } await platform.node.call('POST', '/api/groups/attach', attachBody); + // Fire-and-forget on the node's side (ui/app.py) — this call itself + // returns immediately, well before the scan below finishes. It used + // to be the thing the wizard waited on, which is exactly what made + // "Attaching to node" time out on a real library (see ops.start_reload). await platform.node.call('POST', '/api/reload'); update('done'); advance(); - // 3. Add extra roots (if >1) + // 3. Wait for the node's own initial scan of this group to finish — + // the group is not usable for anything below (extra roots, GEK) until + // this finishes, so nobody lands on a page that looks broken, or hits + // a "not configured" error from racing ahead of it. Can take tens of + // minutes on a slow disk (see the StarWars benchmark) — the node + // keeps scanning on its own either way (test_hot_reload_survives_ + // client_close.py); this step is only about not lying about it. + update('running'); + await platform.waitForGroupHosted(gid, setIndexProgress); + update('done'); + advance(); + + // 4. Add extra roots (if >1) if (roots.length > 1) { update('running'); for (let i = 0; i < roots.length; i++) { if (i === (uploadIdx < roots.length ? uploadIdx : 0)) continue; const r = roots[i]; - await platform.node.call('POST', `/api/groups/${gid}/roots`, { + await withRetry(() => platform.node.call('POST', `/api/groups/${gid}/roots`, { path: r.path, name: r.name, upload: i === uploadIdx, - }); + })); } update('done'); advance(); } - // 4. GEK init + // 5. GEK init update('running'); - await platform.node.call('POST', `/api/groups/${gid}/gek`); + await withRetry(() => platform.node.call('POST', `/api/groups/${gid}/gek`)); update('done'); advance(); - // 5. Generate pairing code + // 6. Generate pairing code update('running'); const pairResult = await platform.node.call('POST', '/api/operator/pair'); if (pairResult && pairResult.code) { @@ -976,7 +1021,7 @@ function CreateGroupWizard({ token, username, onCreated }) { } update('done'); - // Reload node config so it picks up the new group + // Reload once more so any roots added at step 4 are picked up. try { await platform.node.call('POST', '/api/reload'); } catch { /* best effort */ } setStep(3); @@ -1105,6 +1150,9 @@ function CreateGroupWizard({ token, username, onCreated }) { // Step 2: Automatic setup progress if (step === 2) { + const pct = indexProgress && indexProgress.total_bytes + ? Math.min(100, Math.round(100 * indexProgress.scanned_bytes / indexProgress.total_bytes)) + : 0; return html`<div class="page-content"> <h2>${t('wizard.title')}</h2> <p class="page-message">${t('wizard.setting_up')}</p> @@ -1120,6 +1168,19 @@ function CreateGroupWizard({ token, username, onCreated }) { </div> `)} </div> + ${indexProgress && indexProgress.scanning && html` + <div class="index-progress" style="margin-top:12px"> + <div class="index-progress-bar"> + <div class="index-progress-fill" style="width:${pct}%"></div> + </div> + <div class="index-progress-label">${t('wizard.indexing_progress', { pct })}</div> + ${indexProgress.current_dir && html` + <div class="index-progress-dir"> + ${t('wizard.indexing_current_dir', { dir: indexProgress.current_dir })} + </div> + `} + </div> + `} ${setupError && html` <div class="error-msg" style="margin-top:16px">${setupError}</div> <div style="display:flex;gap:8px;margin-top:8px"> @@ -2835,8 +2896,15 @@ function App() { // for the session only: it is a cache of observations, not a source of truth, // and a reload should go back to asking. const [presence, setPresence] = useState({}); - const notePresence = useCallback((gid, state) => { + // Percentage alongside 'indexing' presence — kept separate from `presence` + // itself so a changing % does not require treating every tick as a new + // presence state (see the Sidebar dot's title/aria-label). + const [indexProgressPct, setIndexProgressPct] = useState({}); + const notePresence = useCallback((gid, state, pct) => { setPresence(prev => (prev[gid] === state ? prev : { ...prev, [gid]: state })); + if (pct !== undefined) { + setIndexProgressPct(prev => (prev[gid] === pct ? prev : { ...prev, [gid]: pct })); + } }, []); const handleLeftGroup = useCallback((gid) => { @@ -3084,6 +3152,7 @@ function App() { ${user && html`<${Sidebar} groups=${groups} presence=${presence} + indexProgressPct=${indexProgressPct} route=${route} menuOpen=${menuOpen} role=${user.role} |