diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-14 11:17:42 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-14 11:17:42 +0200 |
| commit | 2fb66c5baecdd8b49be904f6244f66ba04f68061 (patch) | |
| tree | 3b80940e1819a811f6ec2c04831e3616a74a8eae /packages/meshbay-hub/src/meshbay_hub | |
| parent | a294c1d338ba4c20d66873d593d1c101e69c5a40 (diff) | |
| download | meshbay-2fb66c5baecdd8b49be904f6244f66ba04f68061.tar.gz | |
feat(ui): an indexing dock above the music bar, on every page
Adding a large directory left the operator nothing to look at once they left
the Settings panel that started it, and nothing at all when it was added from
another machine. A band now sits above the music bar on every page: one row
per group with indexing under way, naming the root being walked, percent,
bytes and files, and the roots waiting their turn; "indexing finished" for a
few seconds at the end. A click opens the group's Settings, and × hides the
row until that group is idle.
Two sources feed it. On the node's own machine the desktop client polls the
loopback `GET /api/index-status` for every group, whatever the route. An
operator's group page forwards MNP `index_progress` pushes, resolving the
root from the roots table it opened; an ordinary member keeps the sidebar dot
only, and a page clears its row when it lets go of the group. Where both
describe a group, loopback wins.
Reconcile passes and watchdog bursts show only past 1 GB or 5 s, so a single
dropped file does not flash a bar. The logic lives in index-dock-model.js,
which has no imports and is tested under node. The dock publishes
`--index-dock-h` and the sidebar stops above it and the music bar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6jPTeocXA1BePekdsgPya
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
16 files changed, 566 insertions, 7 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 188b389..ee0ab07 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -18,6 +18,7 @@ import { import { GroupPage } from './group-page.js'; import { SearchPage, ConnectionPool } from './search-page.js'; import { MusicPlayerBar } from './music-player.js'; +import { IndexingDock } from './index-dock.js'; import { SettingsPage } from './settings-page.js'; import { ProfilePage } from './profile-page.js'; import { ExplorePage } from './explore-page.js'; @@ -1094,6 +1095,7 @@ function App() { ${page} </main> </div> + ${user && html`<${IndexingDock} groups=${groups} />`} ${musicQueue && html` <${MusicPlayerBar} getConnection=${getMusicConnection} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 18d3a8f..0f28adf 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -15,6 +15,7 @@ import { useStickyBand } from './sticky.js'; import { FilePreview } from './files-app.js'; import { VideoPlayer } from './video-player.js'; import { GroupSettingsPanel } from './group-settings.js'; +import { reportIndexPush } from './index-dock.js'; /** * The group shell: everything a group's "applications" (Chat, Files, and @@ -96,6 +97,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // unplugged keeps its files listed — they are frozen, not deleted — so this is // the only thing that lets the UI say which of the two it is. const [nodeRoots, setNodeRoots] = useState([]); + // Read by the index_progress handler, which connect() installs once. + const nodeRootsRef = useRef(nodeRoots); + nodeRootsRef.current = nodeRoots; const [isNodeAdmin, setIsNodeAdmin] = useState(false); // Which applications this group has enabled, from the node. Falls back to @@ -430,11 +434,18 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // False transition (daemon.py _progress_pusher), so this always // settles back to 'online' rather than getting stuck. transport.onIndexProgress = (status) => { - if (cancelled || !onPresence) return; - const pct = status.total_bytes - ? Math.min(100, Math.round(100 * status.scanned_bytes / status.total_bytes)) - : 0; - onPresence(groupId, status.scanning ? 'indexing' : 'online', pct); + if (cancelled) return; + if (onPresence) { + const pct = status.total_bytes + ? Math.min(100, Math.round(100 * status.scanned_bytes / status.total_bytes)) + : 0; + onPresence(groupId, status.scanning ? 'indexing' : 'online', pct); + } + // The operator's indexing dock. The push names no root, so the + // page that opened the roots table names it. + if (transport.memberRole === 'operator') { + reportIndexPush(groupId, status, nodeRootsRef.current); + } }; setOperatorPaired(transport.memberRole === 'operator'); @@ -503,6 +514,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, onPresence(groupId, 'online'); } } + if (transport.memberRole === 'operator' && ack.indexing) { + reportIndexPush(groupId, ack.indexing, indexMsg.roots || nodeRootsRef.current); + } } catch (err) { if (cancelled) return; @@ -554,6 +568,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, return () => { cancelled = true; + // Nothing will update this group's dock row once the page lets go of it. + reportIndexPush(groupId, null); if (transportRef.current) { // Handed over rather than closed: a download running when you leave the // group keeps its connection, and the last transfer using it closes it. diff --git a/packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js b/packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js new file mode 100644 index 0000000..bd4586b --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js @@ -0,0 +1,126 @@ +/** + * 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 } }; +} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js b/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js new file mode 100644 index 0000000..b136578 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js @@ -0,0 +1,182 @@ +import { html, useState, useEffect, useRef } from './vendor/htm-preact.js'; +import { t, getLocale } from './i18n.js'; +import * as platform from './platform.js'; +import { formatSize } from './file-utils.js'; +import { navigate, session } from './hub-client.js'; +import { useStickyBand } from './sticky.js'; +import { Icon } from './icon.js'; +import { + fromLoopback, fromPush, mergeActivity, dockRows, hideRow, percent, queuedCount, +} from './index-dock-model.js'; + +// ── Indexing dock ─────────────────────────────────────────────────────────── +// +// A node asked to index a large directory works for hours, and the only sign of +// it used to be a progress bar in the one Settings panel that had started it — +// gone the moment the operator went anywhere else, and never shown at all when +// the directory was added from another machine. This band sits above the music +// bar on every page, one row per group with indexing under way; the roots of a +// group are walked one after another, so a group's row names the one being +// walked and the ones waiting (index-dock-model.js). + +// Loopback is cheap and local; a node with nothing to do is asked less often, +// and one that is not there at all, rarely. +const BUSY_POLL_MS = 2000; +const IDLE_POLL_MS = 5000; +const ABSENT_POLL_MS = 30000; + +// MNP pushes, reported by the group page holding the connection. Module state +// rather than a prop through App: the page and the dock are far apart, and a +// push every two seconds must not re-render the whole application. +const pushed = new Map(); +const listeners = new Set(); + +/** + * Called by group-page.js for an operator's connection only — an ordinary + * member keeps the sidebar dot. `status` is the push (or the handshake ack's + * `indexing`); `roots` the roots table the page opened, which names `root_pos`. + * `null` when the page lets go of the group: nothing will update the entry any + * more, and a stale "scanning" would stay on screen for ever. + */ +export function reportIndexPush(groupId, status, roots) { + if (status) pushed.set(groupId, fromPush(groupId, status, roots)); + else pushed.delete(groupId); + for (const fn of listeners) fn(); +} + +function useLoopbackActivity() { + const [local, setLocal] = useState({}); + useEffect(() => { + if (!platform.node.available) return undefined; + let stopped = false; + let timer = null; + let last = ''; + const poll = async () => { + let delay = IDLE_POLL_MS; + try { + const data = await platform.node.call('GET', '/api/index-status'); + const next = {}; + for (const g of (data && data.groups) || []) next[g.group_id] = fromLoopback(g); + if (Object.values(next).some((j) => j.scanning || j.queued.length)) { + delay = BUSY_POLL_MS; + } + const text = JSON.stringify(next); + if (!stopped && text !== last) { last = text; setLocal(next); } + } catch { + // No node on this machine, or one older than the route. + if (!stopped && last !== '{}') { last = '{}'; setLocal({}); } + delay = ABSENT_POLL_MS; + } + if (!stopped) timer = setTimeout(poll, delay); + }; + poll(); + return () => { stopped = true; clearTimeout(timer); }; + }, []); + return local; +} + +function usePushedActivity() { + const [snapshot, setSnapshot] = useState(() => Object.fromEntries(pushed)); + useEffect(() => { + const update = () => setSnapshot(Object.fromEntries(pushed)); + listeners.add(update); + update(); + return () => { listeners.delete(update); }; + }, []); + return snapshot; +} + +function DockRow({ row, onOpen, onHide }) { + const done = row.state === 'done'; + const waiting = !done && !row.scanning; + const listing = !done && row.scanning && !row.totalBytes; + const pct = percent(row); + const number = (v) => v.toLocaleString(getLocale()); + + const status = done ? t('indexdock.done') + : waiting ? t('indexdock.waiting') + : t(`indexdock.kind_${row.kind || 'scan'}`); + const where = row.root ? `${row.groupName} › ${row.root}` : row.groupName; + + let meta = ''; + if (listing) { + meta = t('indexdock.listing'); + } else if (!done && !waiting) { + meta = [ + t('indexdock.percent', { pct }), + `${formatSize(row.scannedBytes)} / ${formatSize(row.totalBytes)}`, + row.filesTotal + ? t('indexdock.files', { done: number(row.filesDone), total: number(row.filesTotal) }) + : '', + ].filter(Boolean).join(' · '); + } + + const count = queuedCount(row); + const next = done || !count ? '' + : Array.isArray(row.queued) ? t('indexdock.next', { roots: row.queued.join(', ') }) + : t('indexdock.next_count', { count: number(count) }); + + const moving = listing || waiting; + return html` + <div class="index-dock-row ${done ? 'index-dock-done' : ''}"> + <button type="button" class="index-dock-main" title=${t('indexdock.open')} + onClick=${onOpen}> + <span class="index-dock-head"> + <span class="index-dock-title"><strong>${status}</strong> ${where}</span> + ${meta && html`<span class="index-dock-meta">${meta}</span>`} + </span> + <span class="index-progress-bar index-dock-bar"> + <span class="index-progress-fill ${moving ? 'index-dock-indeterminate' : ''}" + style="width:${done ? 100 : moving ? 30 : pct}%"></span> + </span> + ${next && html`<span class="index-dock-next">${next}</span>`} + </button> + ${!done && html` + <button type="button" class="index-dock-hide" title=${t('indexdock.hide')} + aria-label=${t('indexdock.hide')} onClick=${onHide}> + <${Icon} name="close" /> + </button> + `} + </div> + `; +} + +export function IndexingDock({ groups }) { + const local = useLoopbackActivity(); + const remote = usePushedActivity(); + const memory = useRef({}); + const [, setTick] = useState(0); + // Publishes `--index-dock-h` for the sidebar, as the music bar publishes its + // own; withdrawn when the dock renders nothing. + const band = useStickyBand('--index-dock-h'); + + const jobs = mergeActivity(local, remote, groups); + const { rows, memory: nextMemory } = dockRows(jobs, memory.current, Date.now()); + memory.current = nextMemory; + + // Time alone changes what is shown — a burst crossing five seconds, a + // "finished" row expiring — so while anything is on or pending, look again. + const pending = Object.values(nextMemory).length > 0; + useEffect(() => { + if (!pending) return undefined; + const id = setInterval(() => setTick((n) => n + 1), 1000); + return () => clearInterval(id); + }, [pending]); + + if (!rows.length) return null; + return html` + <div class="index-dock" ref=${band}> + ${rows.map((row) => html` + <${DockRow} key=${row.groupId} row=${row} + onOpen=${() => { + session.openGroupTab = { groupId: row.groupId, tab: 'settings' }; + navigate('/group/' + row.groupId); + }} + onHide=${() => { + memory.current = hideRow(memory.current, row.groupId); + setTick((n) => n + 1); + }} /> + `)} + </div> + `; +} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 9d6fc61..8607011 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -722,6 +722,19 @@ export default { 'presence.offline': 'Kein Node für diese Gruppe erreichbar', 'presence.unknown': 'Noch nicht geprüft', 'presence.indexing': 'Indizierung läuft — {pct}%', + 'indexdock.kind_scan': "Indizierung", + 'indexdock.kind_rescan': "Erneutes Einlesen", + 'indexdock.kind_reconcile': "Abgleich", + 'indexdock.kind_watch': "Neue Dateien werden indiziert", + 'indexdock.waiting': "Wartet auf Indizierung", + 'indexdock.done': "Indizierung abgeschlossen", + 'indexdock.listing': "Dateien werden aufgelistet…", + 'indexdock.percent': "{pct} %", + 'indexdock.files': "{done} / {total} Dateien", + 'indexdock.next': "Danach: {roots}", + 'indexdock.next_count': "Wartend: {count}", + 'indexdock.open': "Einstellungen dieser Gruppe öffnen", + 'indexdock.hide': "Bis zur nächsten Indizierung ausblenden", 'chat.load_older': '{n} ältere Nachrichten laden', 'chat.start_of_history': 'Anfang des Gesprächs', 'chat.today': 'Heute', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index a5dd5d4..fee6cc3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -831,6 +831,19 @@ export default { 'presence.offline': 'No node is reachable for this group', 'presence.unknown': 'Not checked yet', 'presence.indexing': 'Indexing — {pct}% done', + 'indexdock.kind_scan': "Indexing", + 'indexdock.kind_rescan': "Re-reading", + 'indexdock.kind_reconcile': "Catching up", + 'indexdock.kind_watch': "Indexing new files", + 'indexdock.waiting': "Waiting to index", + 'indexdock.done': "Indexing finished", + 'indexdock.listing': "Listing files…", + 'indexdock.percent': "{pct}%", + 'indexdock.files': "{done} / {total} files", + 'indexdock.next': "Next: {roots}", + 'indexdock.next_count': "{count} more waiting", + 'indexdock.open': "Open this group's settings", + 'indexdock.hide': "Hide until the next indexing", 'chat.load_older': 'Load {n} older messages', 'chat.start_of_history': 'Beginning of the conversation', 'chat.today': 'Today', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 6e8e191..b958da3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -717,6 +717,19 @@ export default { 'presence.offline': 'Ningún node accesible para este grupo', 'presence.unknown': 'Aún sin comprobar', 'presence.indexing': 'Indexando — {pct}%', + 'indexdock.kind_scan': "Indexando", + 'indexdock.kind_rescan': "Releyendo", + 'indexdock.kind_reconcile': "Poniéndose al día", + 'indexdock.kind_watch': "Indexando archivos nuevos", + 'indexdock.waiting': "En espera de indexar", + 'indexdock.done': "Indexación terminada", + 'indexdock.listing': "Listando archivos…", + 'indexdock.percent': "{pct}%", + 'indexdock.files': "{done} / {total} archivos", + 'indexdock.next': "Después: {roots}", + 'indexdock.next_count': "En espera: {count}", + 'indexdock.open': "Abrir los ajustes de este grupo", + 'indexdock.hide': "Ocultar hasta la próxima indexación", 'chat.load_older': 'Cargar {n} mensajes anteriores', 'chat.start_of_history': 'Inicio de la conversación', 'chat.today': 'Hoy', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 47de45f..c993d9c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -720,6 +720,19 @@ export default { 'presence.offline': 'Aucun node joignable pour ce groupe', 'presence.unknown': 'Pas encore vérifié', 'presence.indexing': 'Indexation en cours — {pct} %', + 'indexdock.kind_scan': "Indexation", + 'indexdock.kind_rescan': "Relecture", + 'indexdock.kind_reconcile': "Rattrapage", + 'indexdock.kind_watch': "Indexation de nouveaux fichiers", + 'indexdock.waiting': "En attente d'indexation", + 'indexdock.done': "Indexation terminée", + 'indexdock.listing': "Liste des fichiers…", + 'indexdock.percent': "{pct} %", + 'indexdock.files': "{done} / {total} fichiers", + 'indexdock.next': "Ensuite : {roots}", + 'indexdock.next_count': "En attente : {count}", + 'indexdock.open': "Ouvrir les paramètres de ce groupe", + 'indexdock.hide': "Masquer jusqu'à la prochaine indexation", 'chat.load_older': 'Charger {n} messages plus anciens', 'chat.start_of_history': 'Début de la conversation', 'chat.today': 'Aujourd’hui', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 660298e..3d4b502 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -719,6 +719,19 @@ export default { 'presence.offline': 'Nessun node raggiungibile per questo gruppo', 'presence.unknown': 'Non ancora verificato', 'presence.indexing': 'Indicizzazione in corso — {pct}%', + 'indexdock.kind_scan': "Indicizzazione", + 'indexdock.kind_rescan': "Rilettura", + 'indexdock.kind_reconcile': "Allineamento", + 'indexdock.kind_watch': "Indicizzazione dei nuovi file", + 'indexdock.waiting': "In attesa di indicizzazione", + 'indexdock.done': "Indicizzazione completata", + 'indexdock.listing': "Elenco dei file…", + 'indexdock.percent': "{pct}%", + 'indexdock.files': "{done} / {total} file", + 'indexdock.next': "Poi: {roots}", + 'indexdock.next_count': "In attesa: {count}", + 'indexdock.open': "Apri le impostazioni di questo gruppo", + 'indexdock.hide': "Nascondi fino alla prossima indicizzazione", 'chat.load_older': 'Carica {n} messaggi precedenti', 'chat.start_of_history': 'Inizio della conversazione', 'chat.today': 'Oggi', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index ff5b693..3140527 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -707,6 +707,19 @@ export default { 'presence.offline': 'このグループに到達できる node がありません', 'presence.unknown': '未確認', 'presence.indexing': 'インデックス中 — {pct}%', + 'indexdock.kind_scan': "インデックス中", + 'indexdock.kind_rescan': "再読み込み中", + 'indexdock.kind_reconcile': "差分を取り込み中", + 'indexdock.kind_watch': "新しいファイルをインデックス中", + 'indexdock.waiting': "インデックス待ち", + 'indexdock.done': "インデックス完了", + 'indexdock.listing': "ファイルを一覧中…", + 'indexdock.percent': "{pct}%", + 'indexdock.files': "{done} / {total} ファイル", + 'indexdock.next': "次: {roots}", + 'indexdock.next_count': "待機中: {count}", + 'indexdock.open': "このグループの設定を開く", + 'indexdock.hide': "次のインデックスまで隠す", 'chat.load_older': '以前のメッセージを {n} 件読み込む', 'chat.start_of_history': '会話のはじまり', 'chat.today': '今日', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 3c07b3e..1e55c8c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -721,6 +721,19 @@ export default { 'presence.offline': 'Geen node bereikbaar voor deze groep', 'presence.unknown': 'Nog niet gecontroleerd', 'presence.indexing': 'Bezig met indexeren — {pct}%', + 'indexdock.kind_scan': "Indexeren", + 'indexdock.kind_rescan': "Opnieuw inlezen", + 'indexdock.kind_reconcile': "Bijwerken", + 'indexdock.kind_watch': "Nieuwe bestanden indexeren", + 'indexdock.waiting': "Wacht op indexering", + 'indexdock.done': "Indexeren voltooid", + 'indexdock.listing': "Bestanden oplijsten…", + 'indexdock.percent': "{pct}%", + 'indexdock.files': "{done} / {total} bestanden", + 'indexdock.next': "Daarna: {roots}", + 'indexdock.next_count': "Wachtend: {count}", + 'indexdock.open': "Instellingen van deze groep openen", + 'indexdock.hide': "Verbergen tot de volgende indexering", 'chat.load_older': '{n} oudere berichten laden', 'chat.start_of_history': 'Begin van het gesprek', 'chat.today': 'Vandaag', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 2ff9b84..8486718 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -739,6 +739,19 @@ export default { 'presence.offline': 'Brak osiągalnego node dla tej grupy', 'presence.unknown': 'Jeszcze nie sprawdzono', 'presence.indexing': 'Indeksowanie — {pct}%', + 'indexdock.kind_scan': "Indeksowanie", + 'indexdock.kind_rescan': "Ponowny odczyt", + 'indexdock.kind_reconcile': "Uzupełnianie", + 'indexdock.kind_watch': "Indeksowanie nowych plików", + 'indexdock.waiting': "Oczekuje na indeksowanie", + 'indexdock.done': "Indeksowanie zakończone", + 'indexdock.listing': "Wyliczanie plików…", + 'indexdock.percent': "{pct}%", + 'indexdock.files': "Pliki: {done} / {total}", + 'indexdock.next': "Następnie: {roots}", + 'indexdock.next_count': "W kolejce: {count}", + 'indexdock.open': "Otwórz ustawienia tej grupy", + 'indexdock.hide': "Ukryj do następnego indeksowania", 'chat.load_older': 'Wczytaj {n} starszych wiadomości', 'chat.start_of_history': 'Początek rozmowy', 'chat.today': 'Dziś', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 33fa2be..e51df81 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -718,6 +718,19 @@ export default { 'presence.offline': 'Nenhum node acessível para este grupo', 'presence.unknown': 'Ainda não verificado', 'presence.indexing': 'Indexando — {pct}%', + 'indexdock.kind_scan': "Indexando", + 'indexdock.kind_rescan': "Relendo", + 'indexdock.kind_reconcile': "Atualizando", + 'indexdock.kind_watch': "Indexando arquivos novos", + 'indexdock.waiting': "Aguardando indexação", + 'indexdock.done': "Indexação concluída", + 'indexdock.listing': "Listando arquivos…", + 'indexdock.percent': "{pct}%", + 'indexdock.files': "{done} / {total} arquivos", + 'indexdock.next': "Em seguida: {roots}", + 'indexdock.next_count': "Na fila: {count}", + 'indexdock.open': "Abrir as configurações deste grupo", + 'indexdock.hide': "Ocultar até a próxima indexação", 'chat.load_older': 'Carregar {n} mensagens anteriores', 'chat.start_of_history': 'Início da conversa', 'chat.today': 'Hoje', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index b58ba64..cc22a70 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -694,6 +694,19 @@ export default { 'presence.offline': '本群组没有可达的 node', 'presence.unknown': '尚未检测', 'presence.indexing': '正在索引 — {pct}%', + 'indexdock.kind_scan': "正在索引", + 'indexdock.kind_rescan': "正在重新读取", + 'indexdock.kind_reconcile': "正在补录", + 'indexdock.kind_watch': "正在索引新文件", + 'indexdock.waiting': "等待索引", + 'indexdock.done': "索引完成", + 'indexdock.listing': "正在列出文件…", + 'indexdock.percent': "{pct}%", + 'indexdock.files': "{done} / {total} 个文件", + 'indexdock.next': "接下来:{roots}", + 'indexdock.next_count': "等待中:{count}", + 'indexdock.open': "打开此群组的设置", + 'indexdock.hide': "隐藏,直到下一次索引", 'chat.load_older': '加载更早的 {n} 条消息', 'chat.start_of_history': '对话开头', 'chat.today': '今天', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 871ca0a..a2ef80e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -73,6 +73,8 @@ --toolbar-h: 0px; /* Published by the music bar while it is on screen (sticky.js). */ --music-bar-h: 0px; + /* Published by the indexing dock, which sits on top of the music bar. */ + --index-dock-h: 0px; } /* ── Reset ────────────────────────────────────────────────────────────────── */ @@ -400,7 +402,7 @@ a:hover { text-decoration: underline; } display: flex; flex-direction: column; width: 240px; - height: calc(100vh - var(--nav-h) - var(--music-bar-h)); + height: calc(100vh - var(--nav-h) - var(--music-bar-h) - var(--index-dock-h)); background: var(--sidebar-bg); border-right: 1px solid var(--border); padding: 16px 0 0; @@ -2512,7 +2514,7 @@ a.transfer-name { position: fixed; left: -240px; top: 52px; - height: calc(100vh - 52px - var(--music-bar-h)); + height: calc(100vh - 52px - var(--music-bar-h) - var(--index-dock-h)); z-index: 50; transition: left 0.2s; box-shadow: none; @@ -4287,6 +4289,99 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } border-radius: 10px; } +/* ── Indexing dock (index-dock.js) ──────────────────────────────────────── + Sticky for the music bar's reason. It is rendered right before that bar, so + `bottom: var(--music-bar-h)` stacks it on top when both are pinned, and it + publishes its own height as `--index-dock-h` for the sidebar to stop above + the two of them. */ +.index-dock { + position: sticky; + bottom: var(--music-bar-h); + z-index: 50; + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 16px; +} +.index-dock-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--bg-raised); + border: 1px solid var(--border); + border-radius: 10px; +} +.index-dock-main { + flex: 1 1 auto; + min-width: 0; + display: block; + padding: 0; + border: 0; + background: none; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.index-dock-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + font-size: 0.85em; +} +.index-dock-title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.index-dock-meta { + color: var(--text-dim); + white-space: nowrap; + font-variant-numeric: tabular-nums; +} +.index-dock-bar { display: block; margin: 6px 0 0; } +.index-dock-bar .index-progress-fill { display: block; } +.index-dock-done .index-progress-fill { background: var(--success); } +/* Walking the tree, or waiting for another root: no percentage to show yet, + and a bar parked at 0 % reads as stuck. */ +.index-dock-indeterminate { animation: index-dock-sweep 1.4s ease-in-out infinite; } +@keyframes index-dock-sweep { + from { transform: translateX(-100%); } + to { transform: translateX(340%); } +} +.index-dock-next { + display: block; + margin-top: 4px; + font-size: 0.78em; + color: var(--text-dim); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.index-dock-hide { + flex-shrink: 0; + display: inline-flex; + padding: 4px; + border: 0; + border-radius: 6px; + background: none; + color: var(--text-dim); + cursor: pointer; +} +.index-dock-hide:hover { background: var(--bg-surface); } +.index-dock-hide .icon { width: 16px; height: 16px; } +@media (max-width: 640px) { + .index-dock-head { flex-direction: column; gap: 2px; } + .index-dock-title, + .index-dock-meta { white-space: normal; } +} +@media (prefers-reduced-motion: reduce) { + .index-dock-indeterminate { animation: none; } +} + .music-player-info { display: flex; align-items: center; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index a21da19..9de5407 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -3180,10 +3180,18 @@ class MeshBayTransport { if (msg.type === 'index_progress') { if (this._onIndexProgress) { + // `kind`, `root_pos`, `queued` and the file counts are absent from a + // node older than the indexing dock; index-dock-model.js reads them + // missing as idle defaults. this._onIndexProgress({ scanning: Boolean(msg.scanning), scanned_bytes: msg.scanned_bytes || 0, total_bytes: msg.total_bytes || 0, + files_done: msg.files_done || 0, + files_total: msg.files_total || 0, + kind: msg.kind || '', + root_pos: Number.isInteger(msg.root_pos) ? msg.root_pos : -1, + queued: Number.isInteger(msg.queued) ? msg.queued : 0, }); } return; |