/** * What the indexing dock shows, worked out from what nodes report. No DOM and no * imports, so tests/test_indexing_dock.py runs this file under node as it is. * * Two sources describe a group's indexing: * * - The loopback API (`GET /api/index-status`, the node's ui/app.py) answers on * the node's own machine, for every group it hosts, whatever page is open, * and names the roots. * - MNP `index_progress` pushes reach an operator anywhere, but only while that * group's page holds a connection, and carry counters only (decision D3): the * root under way is a position in the roots table the page already opened, * and the roots waiting are a count. * * Where both describe a group, the loopback answer wins. It is the fuller one. */ // A reconcile pass or a burst of watchdog events is usually a file or two, done // in less time than it takes to read a bar. Shown only past either threshold; // a root walked for the first time, or again, is always shown. export const NOISE_BYTES = 1024 ** 3; export const NOISE_MS = 5000; // How long "indexing finished" stays on screen. export const DONE_MS = 4000; export function fromLoopback(g) { return { groupId: g.group_id, groupName: g.group_name || '', scanning: Boolean(g.scanning), kind: g.kind || '', root: g.root || '', scannedBytes: g.scanned_bytes || 0, totalBytes: g.total_bytes || 0, filesDone: g.files_done || 0, filesTotal: g.files_total || 0, queued: Array.isArray(g.queued) ? g.queued : [], }; } export function fromPush(groupId, s, roots) { const pos = Number.isInteger(s.root_pos) ? s.root_pos : -1; const known = pos >= 0 && Array.isArray(roots) && roots[pos]; return { groupId, groupName: '', scanning: Boolean(s.scanning), kind: s.kind || '', root: known ? (roots[pos].name || '') : '', scannedBytes: s.scanned_bytes || 0, totalBytes: s.total_bytes || 0, filesDone: s.files_done || 0, filesTotal: s.files_total || 0, queued: Number.isInteger(s.queued) ? s.queued : 0, }; } export function queuedCount(job) { return Array.isArray(job.queued) ? job.queued.length : (job.queued || 0); } export function percent(job) { return job.totalBytes ? Math.min(100, Math.round(100 * job.scannedBytes / job.totalBytes)) : 0; } /** One job per group, loopback first, named after the hub's group list. */ export function mergeActivity(local, pushed, groups) { const names = new Map((groups || []).map((g) => [g.id, g.name])); const ids = [...new Set([...Object.keys(local), ...Object.keys(pushed)])]; return ids.map((gid) => { const job = local[gid] || pushed[gid]; return { ...job, groupName: names.get(gid) || job.groupName || gid.slice(0, 8) }; }); } function busy(job) { return job.scanning || queuedCount(job) > 0; } function loud(job, since, now) { // '' while scanning is a node older than `kind`: shown, as it always was. return job.kind === 'scan' || job.kind === 'rescan' || job.kind === '' || queuedCount(job) > 0 || job.totalBytes >= NOISE_BYTES || now - since >= NOISE_MS; } /** * The rows to draw, and what to remember for the next call. * * `memory` is per group: when the current walk was first seen, whether its row * has been shown (a burst that crossed the threshold stays up until it ends), * whether the operator hid it, and when it finished. Pure: same arguments, same * answer, so a render that runs twice does not flash anything twice. */ export function dockRows(jobs, memory, now) { const next = {}; const rows = []; for (const job of jobs) { const gid = job.groupId; const prev = memory[gid] || {}; if (busy(job)) { const sig = `${job.kind}|${job.root}`; const since = prev.busy && prev.sig === sig ? prev.since : now; const shown = Boolean(prev.busy && prev.shown) || loud(job, since, now); const hidden = Boolean(prev.busy && prev.hidden); next[gid] = { busy: true, sig, since, shown, hidden, doneAt: 0 }; if (shown && !hidden) rows.push({ ...job, state: 'running' }); } else if (prev.busy && prev.shown && !prev.hidden) { next[gid] = { busy: false, doneAt: now }; rows.push({ ...job, state: 'done' }); } else if (prev.doneAt && now - prev.doneAt < DONE_MS) { next[gid] = prev; rows.push({ ...job, state: 'done' }); } } return { rows, memory: next }; } /** Hidden until that group has nothing left to index. */ export function hideRow(memory, groupId) { const prev = memory[groupId]; if (!prev || !prev.busy) return memory; return { ...memory, [groupId]: { ...prev, hidden: true } }; }