aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js91
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}