diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-12 00:17:32 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-12 00:17:32 +0200 |
| commit | 14911aae21ec4975f7a805b91e3c4867cb41974b (patch) | |
| tree | 8c00230a88053905fd77938f3a9539efeb4c3094 /packages/meshbay-hub/src | |
| parent | f6f107e02c7a4aa7326c9a82310a239327d81ed6 (diff) | |
| download | meshbay-14911aae21ec4975f7a805b91e3c4867cb41974b.tar.gz | |
feat: let the operator pause and resume a root's indexing (MNP 6.2)
A whole-root walk now checks its root before every file: removed or
ejected, it stops instead of reading the drive to the end; paused, it
finishes from the hash cache alone. A paused root is not watched, not
swept by reconcile and survives a restart (roster), its indexed files
stay served. Resume runs the reconcile sweep over the root, so entries
and their enrichment are kept.
Signed root_pause / root_resume over MNP, loopback routes, CLI
`root pause|resume`, a pause button on the indexing dock and
pause/resume in the roots table of Group settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src')
17 files changed, 175 insertions, 24 deletions
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 48b1c80..3e3b483 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -510,6 +510,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, setNodeRoots(msg.roots); } }; + // The dock's pause button, for a node that is not on this machine. + const pauseFromDock = (rootName) => + transport.pauseRoot(groupId, rootName, transport.signFn); // The node's own scan (a root added while we were already connected, // or reconcile catching one back up) — never the entries, just // enough to animate the sidebar dot. Guaranteed a final push at the @@ -526,7 +529,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // 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); + reportIndexPush(groupId, status, nodeRootsRef.current, pauseFromDock); } }; setOperatorPaired(transport.memberRole === 'operator'); @@ -637,7 +640,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, } } if (transport.memberRole === 'operator' && ack.indexing) { - reportIndexPush(groupId, ack.indexing, indexMsg.roots || nodeRootsRef.current); + reportIndexPush(groupId, ack.indexing, indexMsg.roots || nodeRootsRef.current, + pauseFromDock); } } catch (err) { if (cancelled) return; 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 1a367be..a666c29 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -55,8 +55,9 @@ async function readLocalNode(node, groupId, attempts = NODE_CHECK_ATTEMPTS, * mode="live" — a hosted group. What widens the node's sharing — adding * a directory, its `writable` and `removable` switches — goes * through the loopback API only, so only on the node's own - * machine. Removing, ejecting and plugging are signed ops - * over MNP, or the loopback API when there is no connection. + * machine. Removing, ejecting, plugging, pausing and + * resuming are signed ops over MNP, or the loopback API when + * there is no connection. * mode="local" — the wizard, before the group exists. Changes are held in * an array the caller owns; nothing is persisted until the * group is attached. @@ -72,7 +73,7 @@ async function readLocalNode(node, groupId, attempts = NODE_CHECK_ATTEMPTS, * * Props: * roots — the node's current roots: { name, path, writable, - * removable, ejected, available, kind } + * removable, ejected, paused, available, kind } * groupId — the group id * transport — MeshBayTransport instance, or null when not connected * signFn — signing function for admin ops @@ -153,7 +154,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, // signature proves that the operator's key signed, not that the operator // meant it: in a browser that key is driven by code the hub serves. // - // Narrowing — remove, eject, plug — MNP first, then loopback. + // Narrowing — remove, eject, plug, pause, resume — MNP first, then loopback. const overMnp = !isLocal && transport && transport.connected; const overLoopback = !isLocal && !overMnp && nodeAvail; const onNodeMachine = !isLocal && nodeAvail; @@ -220,6 +221,18 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, else throw new Error(t('node.root_no_route')); }), [overMnp, overLoopback, transport, groupId, signFn, run]); + const doPauseRoot = useCallback((rootName) => run(async () => { + if (overMnp) await transport.pauseRoot(groupId, rootName, signFn); + else if (overLoopback) await platform.node.op('pauseRoot', { groupId, rootName }); + else throw new Error(t('node.root_no_route')); + }), [overMnp, overLoopback, transport, groupId, signFn, run]); + + const doResumeRoot = useCallback((rootName) => run(async () => { + if (overMnp) await transport.resumeRoot(groupId, rootName, signFn); + else if (overLoopback) await platform.node.op('resumeRoot', { groupId, rootName }); + else throw new Error(t('node.root_no_route')); + }), [overMnp, overLoopback, transport, groupId, signFn, run]); + const doRemoveRoot = useCallback(async (rootName) => { if (isLocal) { if (onLocalRootsChange) { @@ -321,6 +334,8 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, <span class="node-root-badge node-root-badge-warn">${t('group.root_ejected')}</span>`} ${!isLocal && r.available === false && !r.ejected && html` <span class="node-root-badge node-root-badge-warn">${t('node.unavailable')}</span>`} + ${!isLocal && r.paused && html` + <span class="node-root-badge">${t('node.root_paused')}</span>`} </td> ${hasPaths && html` <td class="sdt-col-path" title=${r.path || ''}>${r.path || ''}</td>`} @@ -347,6 +362,14 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, `} <td class="sdt-col-actions"> ${canEdit && !isLocal && html` + <button class="sdt-action-btn" disabled=${busy || !!r.ejected} + title=${r.paused ? t('node.root_resume') : t('node.root_pause')} + aria-label=${r.paused ? t('node.root_resume') : t('node.root_pause')} + onClick=${() => r.paused ? doResumeRoot(r.name) : doPauseRoot(r.name)}> + <${Icon} name=${r.paused ? 'play' : 'pause'} /> + </button> + `} + ${canEdit && !isLocal && html` <button class="sdt-action-btn" disabled=${busy || !r.removable} title=${r.ejected ? t('group.root_plug') : t('group.root_eject')} onClick=${() => r.ejected ? doPlugRoot(r.name) : doEjectRoot(r.name)}> 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 index bd4586b..ed81e7c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js @@ -91,8 +91,9 @@ function loud(job, since, now) { * * `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. + * whether the operator hid it, which walk they paused, 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 = {}; @@ -105,9 +106,12 @@ export function dockRows(jobs, memory, now) { 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) { + // The node takes a moment to stop, and may go on to the next root + // queued: only the walk that was paused stays off screen. + const paused = prev.busy && prev.pausedSig === sig ? sig : ''; + next[gid] = { busy: true, sig, since, shown, hidden, pausedSig: paused, doneAt: 0 }; + if (shown && !hidden && !paused) rows.push({ ...job, state: 'running' }); + } else if (prev.busy && prev.shown && !prev.hidden && !prev.pausedSig) { next[gid] = { busy: false, doneAt: now }; rows.push({ ...job, state: 'done' }); } else if (prev.doneAt && now - prev.doneAt < DONE_MS) { @@ -124,3 +128,14 @@ export function hideRow(memory, groupId) { if (!prev || !prev.busy) return memory; return { ...memory, [groupId]: { ...prev, hidden: true } }; } + +/** + * The operator paused the walk this row shows. Its row goes at once rather than + * when the node's next report arrives, and no "finished" follows it — the root + * is not finished, it is paused, and its settings row says so. + */ +export function pauseRow(memory, groupId) { + const prev = memory[groupId]; + if (!prev || !prev.busy) return memory; + return { ...memory, [groupId]: { ...prev, pausedSig: prev.sig } }; +} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js b/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js index 6c89eb6..47a5818 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js @@ -6,7 +6,7 @@ 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, + fromLoopback, fromPush, mergeActivity, dockRows, hideRow, pauseRow, percent, queuedCount, } from './index-dock-model.js'; // ── Indexing dock ─────────────────────────────────────────────────────────── @@ -29,6 +29,9 @@ const ABSENT_POLL_MS = 30000; // 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(); +// How to pause a root of that group over the page's connection, signed by its +// operator — the dock holds no transport of its own. +const pushedPause = new Map(); const listeners = new Set(); /** @@ -36,11 +39,17 @@ const listeners = new Set(); * 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. + * more, and a stale "scanning" would stay on screen for ever. `pause(rootName)` + * is what the row's pause button calls when no node answers on this machine. */ -export function reportIndexPush(groupId, status, roots) { - if (status) pushed.set(groupId, fromPush(groupId, status, roots)); - else pushed.delete(groupId); +export function reportIndexPush(groupId, status, roots, pause) { + if (status) { + pushed.set(groupId, fromPush(groupId, status, roots)); + if (pause) pushedPause.set(groupId, pause); + } else { + pushed.delete(groupId); + pushedPause.delete(groupId); + } for (const fn of listeners) fn(); } @@ -96,7 +105,7 @@ function usePushedActivity() { return snapshot; } -function DockRow({ row, onOpen, onHide }) { +function DockRow({ row, error, onOpen, onPause, onHide }) { const done = row.state === 'done'; const waiting = !done && !row.scanning; const listing = !done && row.scanning && !row.totalBytes; @@ -109,7 +118,9 @@ function DockRow({ row, onOpen, onHide }) { const where = row.root ? `${row.groupName} › ${row.root}` : row.groupName; let meta = ''; - if (listing) { + if (error) { + meta = error; + } else if (listing) { meta = t('indexdock.listing'); } else if (!done && !waiting) { meta = [ @@ -133,7 +144,8 @@ function DockRow({ row, onOpen, onHide }) { 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>`} + ${meta && html`<span class="index-dock-meta ${error ? 'index-dock-error' : ''}" + role=${error ? 'alert' : null}>${meta}</span>`} </span> <span class="index-progress-bar index-dock-bar"> <span class="index-progress-fill ${moving ? 'index-dock-indeterminate' : ''}" @@ -141,6 +153,12 @@ function DockRow({ row, onOpen, onHide }) { </span> ${next && html`<span class="index-dock-next">${next}</span>`} </button> + ${!done && onPause && html` + <button type="button" class="index-dock-hide" title=${t('indexdock.pause')} + aria-label=${t('indexdock.pause')} onClick=${onPause}> + <${Icon} name="pause" /> + </button> + `} ${!done && html` <button type="button" class="index-dock-hide" title=${t('indexdock.hide')} aria-label=${t('indexdock.hide')} onClick=${onHide}> @@ -156,6 +174,7 @@ export function IndexingDock({ groups }) { const remote = usePushedActivity(); const memory = useRef({}); const [, setTick] = useState(0); + const [errors, setErrors] = useState({}); // 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'); @@ -173,11 +192,36 @@ export function IndexingDock({ groups }) { return () => clearInterval(id); }, [pending]); + // A whole-root walk names its root; a burst of file events does not, and + // there is nothing to pause. The local node first, as for the figures. + const pauser = (row) => { + if (row.state !== 'running' || !row.scanning || !row.root) return null; + if (local[row.groupId]) { + return (rootName) => platform.node.op('pauseRoot', { groupId: row.groupId, rootName }); + } + return pushedPause.get(row.groupId) || null; + }; + + const pause = async (row, via) => { + const before = memory.current; + memory.current = pauseRow(memory.current, row.groupId); + setErrors((e) => ({ ...e, [row.groupId]: '' })); + setTick((n) => n + 1); + try { + await via(row.root); + } catch (err) { + // Still running: put the row back, and say why. + memory.current = { ...memory.current, [row.groupId]: before[row.groupId] }; + setErrors((e) => ({ ...e, [row.groupId]: platform.bridgeMessage(err) })); + } + }; + if (!rows.length) return null; return html` <div class="index-dock" ref=${band}> ${rows.map((row) => html` - <${DockRow} key=${row.groupId} row=${row} + <${DockRow} key=${row.groupId} row=${row} error=${errors[row.groupId] || ''} + onPause=${pauser(row) && (() => pause(row, pauser(row)))} onOpen=${() => { session.openGroupTab = { groupId: row.groupId, tab: 'settings' }; navigate('/group/' + row.groupId); 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 c190efa..264cd5b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -880,6 +880,7 @@ export default { 'indexdock.next_count': "Wartend: {count}", 'indexdock.open': "Einstellungen dieser Gruppe öffnen", 'indexdock.hide': "Bis zur nächsten Indizierung ausblenden", + 'indexdock.pause': "Indexierung dieses Ordners pausieren", 'chat.load_older': '{n} ältere Nachrichten laden', 'chat.start_of_history': 'Anfang des Gesprächs', 'chat.today': 'Heute', @@ -1001,6 +1002,9 @@ export default { 'node.remove_root': 'Entfernen', 'node.root_no_signing_key': 'Kein Signaturschlüssel verfügbar — koppeln Sie dieses Gerät zuerst mit dem Node', 'node.root_no_route': 'Keine Verbindung zum Node — verbinden Sie sich damit oder verwenden Sie die App auf dem Rechner, der ihn hostet', + 'node.root_pause': "Indexierung pausieren", + 'node.root_resume': "Indexierung fortsetzen", + 'node.root_paused': "Indexierung pausiert", 'node.root_remove_last': 'Eine Gruppe braucht mindestens ein Verzeichnis', 'node.root_path': 'Pfad', 'node.directory': 'Verzeichnis', 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 df85791..a2d7699 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -993,6 +993,7 @@ export default { 'indexdock.next_count': "{count} more waiting", 'indexdock.open': "Open this group's settings", 'indexdock.hide': "Hide until the next indexing", + 'indexdock.pause': "Pause indexing this folder", 'chat.load_older': 'Load {n} older messages', 'chat.start_of_history': 'Beginning of the conversation', 'chat.today': 'Today', @@ -1069,6 +1070,9 @@ export default { 'node.remove_root': 'Remove', 'node.root_no_signing_key': 'No signing key available — pair this device with the node first', 'node.root_no_route': 'No route to the node — connect to it, or use the app on the machine hosting it', + 'node.root_pause': "Pause indexing", + 'node.root_resume': "Resume indexing", + 'node.root_paused': "indexing paused", 'node.root_remove_last': 'A group needs at least one directory', 'node.root_path': 'Path', 'node.root_added': 'Directory added.', 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 50c08cc..e7d313e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -874,6 +874,7 @@ export default { 'indexdock.next_count': "En espera: {count}", 'indexdock.open': "Abrir los ajustes de este grupo", 'indexdock.hide': "Ocultar hasta la próxima indexación", + 'indexdock.pause': "Pausar la indexación de esta carpeta", 'chat.load_older': 'Cargar {n} mensajes anteriores', 'chat.start_of_history': 'Inicio de la conversación', 'chat.today': 'Hoy', @@ -995,6 +996,9 @@ export default { 'node.remove_root': 'Eliminar', 'node.root_no_signing_key': 'No hay clave de firma disponible: empareja primero este dispositivo con el nodo', 'node.root_no_route': 'No hay ruta al nodo: conéctate a él o usa la aplicación en la máquina que lo aloja', + 'node.root_pause': "Pausar la indexación", + 'node.root_resume': "Reanudar la indexación", + 'node.root_paused': "indexación en pausa", 'node.root_remove_last': 'Un grupo necesita al menos un directorio', 'node.root_path': 'Ruta', 'node.directory': 'Directorio', 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 a56c9ef..e1994dd 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -877,6 +877,7 @@ export default { 'indexdock.next_count': "En attente : {count}", 'indexdock.open': "Ouvrir les paramètres de ce groupe", 'indexdock.hide': "Masquer jusqu'à la prochaine indexation", + 'indexdock.pause': "Mettre en pause l'indexation de ce dossier", 'chat.load_older': 'Charger {n} messages plus anciens', 'chat.start_of_history': 'Début de la conversation', 'chat.today': 'Aujourd’hui', @@ -1004,6 +1005,9 @@ export default { 'node.remove_root': 'Retirer', 'node.root_no_signing_key': 'Aucune clé de signature disponible — appairez d\'abord cet appareil avec le nœud', 'node.root_no_route': 'Aucune route vers le nœud — connectez-vous à lui, ou utilisez l\'application sur la machine qui l\'héberge', + 'node.root_pause': "Mettre l'indexation en pause", + 'node.root_resume': "Reprendre l'indexation", + 'node.root_paused': "indexation en pause", 'node.root_remove_last': 'Un groupe a besoin d\'au moins un répertoire', 'node.root_path': 'Chemin', 'node.root_added': 'Répertoire ajouté.', 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 de071ee..eb96a49 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -876,6 +876,7 @@ export default { 'indexdock.next_count': "In attesa: {count}", 'indexdock.open': "Apri le impostazioni di questo gruppo", 'indexdock.hide': "Nascondi fino alla prossima indicizzazione", + 'indexdock.pause': "Sospendi l'indicizzazione di questa cartella", 'chat.load_older': 'Carica {n} messaggi precedenti', 'chat.start_of_history': 'Inizio della conversazione', 'chat.today': 'Oggi', @@ -1003,6 +1004,9 @@ export default { 'node.remove_root': 'Rimuovi', 'node.root_no_signing_key': 'Nessuna chiave di firma disponibile: associa prima questo dispositivo al nodo', 'node.root_no_route': 'Nessuna via verso il nodo: connettiti a esso oppure usa l\'applicazione sulla macchina che lo ospita', + 'node.root_pause': "Sospendi l'indicizzazione", + 'node.root_resume': "Riprendi l'indicizzazione", + 'node.root_paused': "indicizzazione sospesa", 'node.root_remove_last': 'Un gruppo ha bisogno di almeno una directory', 'node.root_path': 'Percorso', 'node.directory': 'Directory', 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 d7261c6..2a4584a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -864,6 +864,7 @@ export default { 'indexdock.next_count': "待機中: {count}", 'indexdock.open': "このグループの設定を開く", 'indexdock.hide': "次のインデックスまで隠す", + 'indexdock.pause': "このフォルダーのインデックス作成を一時停止", 'chat.load_older': '以前のメッセージを {n} 件読み込む', 'chat.start_of_history': '会話のはじまり', 'chat.today': '今日', @@ -989,6 +990,9 @@ export default { 'node.remove_root': '削除', 'node.root_no_signing_key': '署名鍵がありません — 先にこの端末をノードとペアリングしてください', 'node.root_no_route': 'ノードへの経路がありません — 接続するか、ノードを動かしているマシンでアプリを使ってください', + 'node.root_pause': "インデックス作成を一時停止", + 'node.root_resume': "インデックス作成を再開", + 'node.root_paused': "インデックス作成を一時停止中", 'node.root_remove_last': 'グループには少なくとも 1 つのディレクトリが必要です', 'node.root_path': 'パス', 'node.directory': 'ディレクトリ', 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 a628fb9..ce4acbe 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -878,6 +878,7 @@ export default { 'indexdock.next_count': "Wachtend: {count}", 'indexdock.open': "Instellingen van deze groep openen", 'indexdock.hide': "Verbergen tot de volgende indexering", + 'indexdock.pause': "Indexeren van deze map pauzeren", 'chat.load_older': '{n} oudere berichten laden', 'chat.start_of_history': 'Begin van het gesprek', 'chat.today': 'Vandaag', @@ -1005,6 +1006,9 @@ export default { 'node.remove_root': 'Verwijderen', 'node.root_no_signing_key': 'Geen ondertekeningssleutel beschikbaar — koppel dit apparaat eerst aan de node', 'node.root_no_route': 'Geen route naar de node — maak verbinding, of gebruik de app op de machine die hem host', + 'node.root_pause': "Indexeren pauzeren", + 'node.root_resume': "Indexeren hervatten", + 'node.root_paused': "indexeren gepauzeerd", 'node.root_remove_last': 'Een groep heeft minstens één map nodig', 'node.root_path': 'Pad', 'node.directory': 'Map', 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 9014664..72ace3a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -898,6 +898,7 @@ export default { 'indexdock.next_count': "W kolejce: {count}", 'indexdock.open': "Otwórz ustawienia tej grupy", 'indexdock.hide': "Ukryj do następnego indeksowania", + 'indexdock.pause': "Wstrzymaj indeksowanie tego folderu", 'chat.load_older': 'Wczytaj {n} starszych wiadomości', 'chat.start_of_history': 'Początek rozmowy', 'chat.today': 'Dziś', @@ -1029,6 +1030,9 @@ export default { 'node.remove_root': 'Usuń', 'node.root_no_signing_key': 'Brak klucza podpisu — najpierw sparuj to urządzenie z węzłem', 'node.root_no_route': 'Brak połączenia z węzłem — połącz się z nim albo użyj aplikacji na komputerze, który go hostuje', + 'node.root_pause': "Wstrzymaj indeksowanie", + 'node.root_resume': "Wznów indeksowanie", + 'node.root_paused': "indeksowanie wstrzymane", 'node.root_remove_last': 'Grupa wymaga co najmniej jednego katalogu', 'node.root_path': 'Ścieżka', 'node.directory': 'Katalog', 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 e6972fb..ad46dc2 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 @@ -875,6 +875,7 @@ export default { 'indexdock.next_count': "Na fila: {count}", 'indexdock.open': "Abrir as configurações deste grupo", 'indexdock.hide': "Ocultar até a próxima indexação", + 'indexdock.pause': "Pausar a indexação desta pasta", 'chat.load_older': 'Carregar {n} mensagens anteriores', 'chat.start_of_history': 'Início da conversa', 'chat.today': 'Hoje', @@ -996,6 +997,9 @@ export default { 'node.remove_root': 'Remover', 'node.root_no_signing_key': 'Nenhuma chave de assinatura disponível — pareie este dispositivo com o nó primeiro', 'node.root_no_route': 'Sem rota até o nó — conecte-se a ele ou use o aplicativo na máquina que o hospeda', + 'node.root_pause': "Pausar a indexação", + 'node.root_resume': "Retomar a indexação", + 'node.root_paused': "indexação pausada", 'node.root_remove_last': 'Um grupo precisa de pelo menos um diretório', 'node.root_path': 'Caminho', 'node.directory': 'Diretório', 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 96f7f5f..ccaf059 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 @@ -852,6 +852,7 @@ export default { 'indexdock.next_count': "等待中:{count}", 'indexdock.open': "打开此群组的设置", 'indexdock.hide': "隐藏,直到下一次索引", + 'indexdock.pause': "暂停索引此文件夹", 'chat.load_older': '加载更早的 {n} 条消息', 'chat.start_of_history': '对话开头', 'chat.today': '今天', @@ -977,6 +978,9 @@ export default { 'node.remove_root': '移除', 'node.root_no_signing_key': '没有可用的签名密钥 — 请先将本设备与节点配对', 'node.root_no_route': '无法连接到节点 — 请先连接,或在运行该节点的机器上使用应用', + 'node.root_pause': "暂停索引", + 'node.root_resume': "继续索引", + 'node.root_paused': "索引已暂停", 'node.root_remove_last': '每个群组至少需要一个目录', 'node.root_path': '路径', 'node.directory': '目录', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 67d52b9..f6cb57c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -3591,6 +3591,7 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } .sdt-action-btn:hover:not(:disabled) { background: var(--bg-raised); color: var(--text); } .sdt-action-btn:disabled { opacity: 0.3; cursor: default; } .sdt-action-danger:hover:not(:disabled) { color: var(--error); border-color: var(--error); } +.sdt-action-btn .icon { width: 1em; height: 1em; display: block; } .sdt-dir-name { display: inline-flex; align-items: center; gap: 6px; font-weight: 500; } .sdt-dir-name .icon { width: 16px; height: 16px; flex-shrink: 0; } .sdt-row-ejected { opacity: 0.5; } @@ -4832,6 +4833,7 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } white-space: nowrap; font-variant-numeric: tabular-nums; } +.index-dock-meta.index-dock-error { color: var(--error); white-space: normal; } .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); } diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js index 7bc77af..aea156b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js @@ -279,6 +279,27 @@ extendTransport(class { return msg; } + // Stop / restart indexing one root (MNP 6.2). Already-indexed files stay + // listed while it is paused. + async pauseRoot(groupId, rootName, signFn) { + return this._rootIndexOp('root_pause', groupId, rootName, signFn); + } + + async resumeRoot(groupId, rootName, signFn) { + return this._rootIndexOp('root_resume', groupId, rootName, signFn); + } + + async _rootIndexOp(type, groupId, rootName, signFn) { + const msg = await this._sendAndWait({ + type, v: '6.2', group_id: groupId, root_name: rootName, + }); + if (msg.type === 'error') throw new Error(msg.detail); + if (msg.type === 'admin_challenge') { + return this._authorizeAdminOp(msg, type, rootName, signFn); + } + return msg; + } + /** * Ask the node for a one-time pairing code admitting `userId` to this group. * diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index ee6645f..c86d668 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -196,7 +196,7 @@ function _aborted() { // unsaved-looking draft after a save that had worked. const BROADCAST_ACK_TYPES = new Set([ 'root_eject_ack', 'root_plug_ack', 'root_remove_ack', - 'app_directories_ack', 'chat_directory_ack', 'chat_link_preview_ack', + 'root_pause_ack', 'root_resume_ack', 'app_directories_ack', 'chat_directory_ack', 'chat_link_preview_ack', 'chat_epoch_ack', 'chat_purge_ack', 'search_listed_ack', ]); @@ -223,7 +223,7 @@ const ADMIN_OP_TYPES = new Set([ 'tmdb_override', 'tmdb_rematch', 'tmdb_config', 'tmdb_enabled', 'musicbrainz_enabled', 'file_delete', 'dir_delete', 'apps_enabled', 'set_scan_settings', 'member_revoke', - 'root_remove', 'root_eject', 'root_plug', + 'root_remove', 'root_eject', 'root_plug', 'root_pause', 'root_resume', 'app_directories', 'chat_directory', 'chat_link_preview', 'chat_epoch', 'chat_purge', 'search_listed', 'invite_create', 'invite_link_create', 'invite_cancel', ]); @@ -2141,11 +2141,13 @@ class MeshBayTransport { this._onMusicbrainzEnabled(Boolean(msg.enabled)); } - // A root was ejected, plugged or removed. Broadcast by the node to every - // peer, so everyone's table updates without waiting for the next index_sync. + // A root was ejected, plugged, paused, resumed or removed. Broadcast by the + // node to every peer, so everyone's table updates without waiting for the + // next index_sync. // A root added or a flag changed — on the node's own machine, never over // MNP — arrives in the index_delta the node pushes. if (msg.type === 'root_eject_ack' || msg.type === 'root_plug_ack' + || msg.type === 'root_pause_ack' || msg.type === 'root_resume_ack' || msg.type === 'root_remove_ack') { if (this._onRootsChanged) this._onRootsChanged(msg); } |