diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-14 10:17:49 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-14 10:17:49 +0200 |
| commit | 80ac882346b474bb7ea4e8c8afd4694bd13dd9dd (patch) | |
| tree | 3aebf475a23dc3e44dff1a6c577d8af82418d024 /packages/meshbay-hub | |
| parent | af542a40aec6ffe0ac06798d99abd00a24eb9c50 (diff) | |
| download | meshbay-80ac882346b474bb7ea4e8c8afd4694bd13dd9dd.tar.gz | |
feat(node): skip a file the disk stops answering for, then pause its root
A Windows tester's scan stuck for good on one directory of a large disk:
one read that never returned held the indexer's only thread, and with it
every root behind that one.
- A disk call is timed by its silence, not its length: each directory
entry listed and each megabyte read counts as an answer. After 60 s of
silence the call is abandoned on its daemon thread, the file is noted
unread and the scan goes on.
- Three silent files in a row pause the root (not persisted). Resume
skips the files already known to give no answer; a restart, a replug
or a change to the file retries them.
- The walk is per directory with os.scandir: a subdirectory that cannot
be listed costs itself, and a listing that stops answering pauses the
root at once. A scan no longer stats anything from the event loop.
- The log names the file after 10 s of silence, the indexing band shows
it, and Group settings shows the paused root, where it stuck and the
unread files with their reason, on the node's machine only.
- generate_http_api.py writes LF on Windows too.
Verified with real kernel-blocked opens (an unacknowledged oplock) in the
installed frozen app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub')
15 files changed, 208 insertions, 2 deletions
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 a666c29..8ed8eec 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -46,6 +46,9 @@ async function readLocalNode(node, groupId, attempts = NODE_CHECK_ATTEMPTS, // ── Shared Directories Table ──────────────────────────────────────────── +// How often the table asks its node what it could not read. +const TROUBLE_POLL_MS = 10000; + /** * A group's root directories, and the operator's controls over them. * @@ -98,6 +101,10 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, // reason nothing happened. const [msg, setMsg] = useState(null); const [indexProgress, setIndexProgress] = useState(null); + // What the node could not read and the folders it paused by itself, as its + // loopback API says them; `troubleTick` asks again after a change here. + const [trouble, setTrouble] = useState(null); + const [troubleTick, setTroubleTick] = useState(0); const say = useCallback((text) => setMsg(text ? { text, error: false } : null), []); const refuse = useCallback((text) => setMsg({ text, error: true }), []); @@ -161,6 +168,26 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, const canEdit = isLocal || overMnp || overLoopback; const canWiden = isLocal || onNodeMachine; + // Paths, so only where the loopback API answers: on the node's own machine. + // An operator elsewhere sees the folder paused, as any member does. + useEffect(() => { + if (!onNodeMachine) { setTrouble(null); return undefined; } + let stopped = false; + const read = async () => { + try { + const s = await platform.node.op('groupIndexStatus', { groupId }); + const next = { unread: s.unread || [], total: s.unread_total || 0, + stalled: s.stalled || [] }; + if (!stopped) { + setTrouble((prev) => JSON.stringify(prev) === JSON.stringify(next) ? prev : next); + } + } catch { /* busy, or mid-reload: the next poll asks again */ } + }; + read(); + const id = setInterval(read, TROUBLE_POLL_MS); + return () => { stopped = true; clearInterval(id); }; + }, [onNodeMachine, groupId, troubleTick]); + // Deliberately no index refresh after a root change. // // Adding a root makes the node reload, which rescans — minutes on a real @@ -182,7 +209,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, } catch (err) { refuse(platform.bridgeMessage(err)); return false; - } finally { setBusy(false); } + } finally { setBusy(false); setTroubleTick((n) => n + 1); } }, [onRootsChange, refuse]); const doUpdateRoot = useCallback(async (rootName, updates) => { @@ -296,6 +323,34 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, <p class="settings-hint" style="margin-top:8px"> ${t('settings_node.roots_local_only_hint')}</p>`; + // A folder the node paused because its disk stopped answering, and the + // files it gave up on. Under the table, with the paths: this is the list an + // operator used to need the node's log for. + const stalledAt = {}; + for (const s of (trouble && trouble.stalled) || []) { + if (displayRoots.some((r) => r.name === s.root && r.paused)) stalledAt[s.root] = s.path; + } + const troubleNote = (isLocal || !trouble) ? '' : html` + ${Object.entries(stalledAt).map(([name, file]) => html` + <p class="error-msg" role="status" style="margin-top:10px" key=${name}> + ${t('node.root_stalled_hint', { name, file })}</p>`)} + ${trouble.total > 0 && html` + <details class="unread-files"> + <summary>${t('node.unread_summary', { count: trouble.total })}</summary> + <ul> + ${trouble.unread.map((u) => html` + <li key=${u.path}> + <span class="unread-path">${u.path}</span> + <span class="unread-reason">${u.reason === 'no_answer' + ? t('node.unread_no_answer') : u.reason}</span> + </li>`)} + </ul> + ${trouble.total > trouble.unread.length && html` + <p class="settings-hint">${t('node.unread_more', + { count: trouble.total - trouble.unread.length })}</p>`} + </details>`} + `; + if (!displayRoots.length) { return html` <div class="shared-directories-table"> @@ -336,6 +391,9 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, <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>`} + ${!isLocal && stalledAt[r.name] && html` + <span class="node-root-badge node-root-badge-warn" + title=${stalledAt[r.name]}>${t('node.root_stalled')}</span>`} </td> ${hasPaths && html` <td class="sdt-col-path" title=${r.path || ''}>${r.path || ''}</td>`} @@ -390,6 +448,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, `; })} </tbody> </table> + ${troubleNote} ${localOnlyHint} ${addControls} ${message} 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 ed81e7c..f0f9aa0 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 @@ -35,9 +35,18 @@ export function fromLoopback(g) { filesDone: g.files_done || 0, filesTotal: g.files_total || 0, queued: Array.isArray(g.queued) ? g.queued : [], + // What the disk has given no answer on, and for how long. A path, so the + // loopback answer only: a push carries counters and nothing else. + slowPath: g.slow_path || '', + slowSecs: g.slow_secs || 0, }; } +/** The last segment of a path, whichever way its separators lean. */ +export function baseName(path) { + return String(path || '').split(/[\\/]/).filter(Boolean).pop() || ''; +} + 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]; @@ -52,6 +61,8 @@ export function fromPush(groupId, s, roots) { filesDone: s.files_done || 0, filesTotal: s.files_total || 0, queued: Number.isInteger(s.queued) ? s.queued : 0, + slowPath: '', + slowSecs: 0, }; } 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 47a5818..3a9a539 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/index-dock.js @@ -7,6 +7,7 @@ import { useStickyBand } from './sticky.js'; import { Icon } from './icon.js'; import { fromLoopback, fromPush, mergeActivity, dockRows, hideRow, pauseRow, percent, queuedCount, + baseName, } from './index-dock-model.js'; // ── Indexing dock ─────────────────────────────────────────────────────────── @@ -117,9 +118,14 @@ function DockRow({ row, error, onOpen, onPause, onHide }) { : t(`indexdock.kind_${row.kind || 'scan'}`); const where = row.root ? `${row.groupName} › ${row.root}` : row.groupName; + // A scan that sits still says what it is sitting on. It used to show a bar + // that had stopped, and the name of a folder. + const slow = !done && !error && row.slowPath; let meta = ''; if (error) { meta = error; + } else if (slow) { + meta = t('indexdock.slow', { secs: number(row.slowSecs), file: baseName(row.slowPath) }); } else if (listing) { meta = t('indexdock.listing'); } else if (!done && !waiting) { @@ -144,7 +150,8 @@ function DockRow({ row, error, onOpen, onPause, 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 ${error ? 'index-dock-error' : ''}" + ${meta && html`<span class="index-dock-meta ${error || slow ? 'index-dock-error' : ''}" + title=${slow ? row.slowPath : null} role=${error ? 'alert' : null}>${meta}</span>`} </span> <span class="index-progress-bar index-dock-bar"> 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 afa1ba0..1fe2b36 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.open': "Einstellungen dieser Gruppe öffnen", 'indexdock.hide': "Bis zur nächsten Indizierung ausblenden", 'indexdock.pause': "Indexierung dieses Ordners pausieren", + 'indexdock.slow': "Seit {secs} s keine Antwort vom Datenträger: {file}", 'chat.load_older': '{n} ältere Nachrichten laden', 'chat.start_of_history': 'Anfang des Gesprächs', 'chat.today': 'Heute', @@ -1002,6 +1003,11 @@ export default { 'node.root_pause': "Indexierung pausieren", 'node.root_resume': "Indexierung fortsetzen", 'node.root_paused': "Indexierung pausiert", + 'node.root_stalled': "Datenträger antwortet nicht", + 'node.root_stalled_hint': "Der Node hat die Indexierung von {name} pausiert: Der Datenträger hat bei {file} nicht geantwortet. Prüfen Sie den Datenträger oder verschieben Sie diese Datei, und setzen Sie dann die Indexierung fort.", + 'node.unread_summary': "Dateien, die nicht gelesen werden konnten: {count}", + 'node.unread_no_answer': "keine Antwort vom Datenträger", + 'node.unread_more': "Und {count} weitere.", '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 b4346b9..642d3e2 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.open': "Open this group's settings", 'indexdock.hide': "Hide until the next indexing", 'indexdock.pause': "Pause indexing this folder", + 'indexdock.slow': "No answer from the disk for {secs} s: {file}", 'chat.load_older': 'Load {n} older messages', 'chat.start_of_history': 'Beginning of the conversation', 'chat.today': 'Today', @@ -1070,6 +1071,11 @@ export default { 'node.root_pause': "Pause indexing", 'node.root_resume': "Resume indexing", 'node.root_paused': "indexing paused", + 'node.root_stalled': "disk not answering", + 'node.root_stalled_hint': "The node paused indexing of {name}: its disk gave no answer at {file}. Check the disk, or move that file out, then resume indexing.", + 'node.unread_summary': "Files that could not be read: {count}", + 'node.unread_no_answer': "no answer from the disk", + 'node.unread_more': "And {count} more.", '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 9bb26e1..fe66af4 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.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", + 'indexdock.slow': "El disco no responde desde hace {secs} s: {file}", 'chat.load_older': 'Cargar {n} mensajes anteriores', 'chat.start_of_history': 'Inicio de la conversación', 'chat.today': 'Hoy', @@ -996,6 +997,11 @@ export default { 'node.root_pause': "Pausar la indexación", 'node.root_resume': "Reanudar la indexación", 'node.root_paused': "indexación en pausa", + 'node.root_stalled': "el disco no responde", + 'node.root_stalled_hint': "El nodo pausó la indexación de {name}: su disco no respondió en {file}. Revisa el disco, o saca ese archivo de la carpeta, y reanuda la indexación.", + 'node.unread_summary': "Archivos que no se pudieron leer: {count}", + 'node.unread_no_answer': "el disco no respondió", + 'node.unread_more': "Y {count} más.", '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 9f32a19..6296a8b 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.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", + 'indexdock.slow': "Pas de réponse du disque depuis {secs} s : {file}", 'chat.load_older': 'Charger {n} messages plus anciens', 'chat.start_of_history': 'Début de la conversation', 'chat.today': 'Aujourd’hui', @@ -1005,6 +1006,11 @@ export default { 'node.root_pause': "Mettre l'indexation en pause", 'node.root_resume': "Reprendre l'indexation", 'node.root_paused': "indexation en pause", + 'node.root_stalled': "le disque ne répond pas", + 'node.root_stalled_hint': "Le node a mis en pause l'indexation de {name} : son disque n'a pas répondu sur {file}. Vérifiez le disque, ou sortez ce fichier du dossier, puis reprenez l'indexation.", + 'node.unread_summary': "Fichiers qui n'ont pas pu être lus : {count}", + 'node.unread_no_answer': "pas de réponse du disque", + 'node.unread_more': "Et {count} autres.", '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 5578028..0751692 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.open': "Apri le impostazioni di questo gruppo", 'indexdock.hide': "Nascondi fino alla prossima indicizzazione", 'indexdock.pause': "Sospendi l'indicizzazione di questa cartella", + 'indexdock.slow': "Nessuna risposta dal disco da {secs} s: {file}", 'chat.load_older': 'Carica {n} messaggi precedenti', 'chat.start_of_history': 'Inizio della conversazione', 'chat.today': 'Oggi', @@ -1004,6 +1005,11 @@ export default { 'node.root_pause': "Sospendi l'indicizzazione", 'node.root_resume': "Riprendi l'indicizzazione", 'node.root_paused': "indicizzazione sospesa", + 'node.root_stalled': "il disco non risponde", + 'node.root_stalled_hint': "Il node ha sospeso l'indicizzazione di {name}: il disco non ha risposto su {file}. Controlla il disco, o sposta quel file altrove, poi riprendi l'indicizzazione.", + 'node.unread_summary': "File che non è stato possibile leggere: {count}", + 'node.unread_no_answer': "nessuna risposta dal disco", + 'node.unread_more': "E altri {count}.", '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 b5e091c..c3d4fa8 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.open': "このグループの設定を開く", 'indexdock.hide': "次のインデックスまで隠す", 'indexdock.pause': "このフォルダーのインデックス作成を一時停止", + 'indexdock.slow': "ディスクが {secs} 秒間応答していません: {file}", 'chat.load_older': '以前のメッセージを {n} 件読み込む', 'chat.start_of_history': '会話のはじまり', 'chat.today': '今日', @@ -990,6 +991,11 @@ export default { 'node.root_pause': "インデックス作成を一時停止", 'node.root_resume': "インデックス作成を再開", 'node.root_paused': "インデックス作成を一時停止中", + 'node.root_stalled': "ディスクが応答しません", + 'node.root_stalled_hint': "ノードが {name} のインデックス作成を一時停止しました。{file} でディスクが応答しませんでした。ディスクを確認するか、そのファイルを移動してから、インデックス作成を再開してください。", + 'node.unread_summary': "読み取れなかったファイル: {count}", + 'node.unread_no_answer': "ディスクからの応答なし", + 'node.unread_more': "ほか {count} 件。", '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 86e01a2..c02be99 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.open': "Instellingen van deze groep openen", 'indexdock.hide': "Verbergen tot de volgende indexering", 'indexdock.pause': "Indexeren van deze map pauzeren", + 'indexdock.slow': "Al {secs} s geen antwoord van de schijf: {file}", 'chat.load_older': '{n} oudere berichten laden', 'chat.start_of_history': 'Begin van het gesprek', 'chat.today': 'Vandaag', @@ -1006,6 +1007,11 @@ export default { 'node.root_pause': "Indexeren pauzeren", 'node.root_resume': "Indexeren hervatten", 'node.root_paused': "indexeren gepauzeerd", + 'node.root_stalled': "schijf antwoordt niet", + 'node.root_stalled_hint': "De node heeft het indexeren van {name} gepauzeerd: de schijf gaf geen antwoord bij {file}. Controleer de schijf, of verplaats dat bestand, en hervat daarna het indexeren.", + 'node.unread_summary': "Bestanden die niet gelezen konden worden: {count}", + 'node.unread_no_answer': "geen antwoord van de schijf", + 'node.unread_more': "En nog {count}.", '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 090be3f..97e8b53 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.open': "Otwórz ustawienia tej grupy", 'indexdock.hide': "Ukryj do następnego indeksowania", 'indexdock.pause': "Wstrzymaj indeksowanie tego folderu", + 'indexdock.slow': "Brak odpowiedzi dysku od {secs} s: {file}", 'chat.load_older': 'Wczytaj {n} starszych wiadomości', 'chat.start_of_history': 'Początek rozmowy', 'chat.today': 'Dziś', @@ -1030,6 +1031,11 @@ export default { 'node.root_pause': "Wstrzymaj indeksowanie", 'node.root_resume': "Wznów indeksowanie", 'node.root_paused': "indeksowanie wstrzymane", + 'node.root_stalled': "dysk nie odpowiada", + 'node.root_stalled_hint': "Węzeł wstrzymał indeksowanie {name}: dysk nie odpowiedział przy {file}. Sprawdź dysk albo przenieś ten plik, a potem wznów indeksowanie.", + 'node.unread_summary': "Pliki, których nie udało się odczytać: {count}", + 'node.unread_no_answer': "brak odpowiedzi dysku", + 'node.unread_more': "I jeszcze {count}.", '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 bfac600..8ff36a5 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.open': "Abrir as configurações deste grupo", 'indexdock.hide': "Ocultar até a próxima indexação", 'indexdock.pause': "Pausar a indexação desta pasta", + 'indexdock.slow': "Sem resposta do disco há {secs} s: {file}", 'chat.load_older': 'Carregar {n} mensagens anteriores', 'chat.start_of_history': 'Início da conversa', 'chat.today': 'Hoje', @@ -997,6 +998,11 @@ export default { 'node.root_pause': "Pausar a indexação", 'node.root_resume': "Retomar a indexação", 'node.root_paused': "indexação pausada", + 'node.root_stalled': "o disco não responde", + 'node.root_stalled_hint': "O node pausou a indexação de {name}: o disco não respondeu em {file}. Verifique o disco, ou tire esse arquivo da pasta, e retome a indexação.", + 'node.unread_summary': "Arquivos que não puderam ser lidos: {count}", + 'node.unread_no_answer': "sem resposta do disco", + 'node.unread_more': "E mais {count}.", '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 367efa0..d4ad64f 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.open': "打开此群组的设置", 'indexdock.hide': "隐藏,直到下一次索引", 'indexdock.pause': "暂停索引此文件夹", + 'indexdock.slow': "磁盘已 {secs} 秒没有响应:{file}", 'chat.load_older': '加载更早的 {n} 条消息', 'chat.start_of_history': '对话开头', 'chat.today': '今天', @@ -978,6 +979,11 @@ export default { 'node.root_pause': "暂停索引", 'node.root_resume': "继续索引", 'node.root_paused': "索引已暂停", + 'node.root_stalled': "磁盘没有响应", + 'node.root_stalled_hint': "节点已暂停索引 {name}:磁盘在 {file} 处没有响应。请检查磁盘,或将该文件移出,然后恢复索引。", + 'node.unread_summary': "无法读取的文件:{count}", + 'node.unread_no_answer': "磁盘没有响应", + 'node.unread_more': "还有 {count} 个。", '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 586f36d..a14bdc3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -3681,6 +3681,17 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } font-size: 0.8em; color: var(--text-dim); } +/* Files a scan could not read, with their paths: names somebody else chose. */ +.unread-files { margin-top: 10px; font-size: 0.9em; } +.unread-files summary { cursor: pointer; color: var(--warn, #d97706); } +.unread-files ul { + margin: 6px 0 0; + padding-left: 18px; + max-height: 240px; + overflow-y: auto; +} +.unread-files li { word-break: break-word; margin-bottom: 2px; } +.unread-files .unread-reason { color: var(--text-dim); margin-left: 6px; } .node-root-unavailable { opacity: 0.6; } .node-message { diff --git a/packages/meshbay-hub/tests/test_indexing_dock.py b/packages/meshbay-hub/tests/test_indexing_dock.py index 1619b57..4510dfc 100644 --- a/packages/meshbay-hub/tests/test_indexing_dock.py +++ b/packages/meshbay-hub/tests/test_indexing_dock.py @@ -224,3 +224,61 @@ def test_the_dock_polls_the_node_wide_route_not_one_group(): source = DOCK.read_text(encoding="utf-8") assert "node.op('indexStatus')" in source assert "groupIndexStatus" not in source + + +# ── A disk that stops answering ───────────────────────────────────────────── + +@needs_node +def test_a_silent_disk_is_named_by_the_loopback_answer_only(tmp_path): + """A scan that sat still showed a bar that had stopped and a folder's name. + What it is waiting on is a path: the node's own machine, never a push.""" + paths = ["C:\\Music\\Some Album\\track 02.flac", "/srv/music/some album/track 02.flac", ""] + out = _run(tmp_path, f""" + const paths = {json.dumps(paths)}; + console.log(JSON.stringify({{ + local: m.fromLoopback({{ group_id: 'g', scanning: true, slow_path: paths[0], + slow_secs: 23 }}), + idle: m.fromLoopback({{ group_id: 'g', scanning: true }}), + push: m.fromPush('g', {{ scanning: true, slow_path: paths[0], slow_secs: 23 }}, []), + names: paths.map(m.baseName), + }})); + """) + assert (out["local"]["slowPath"], out["local"]["slowSecs"]) == (paths[0], 23) + assert (out["idle"]["slowPath"], out["idle"]["slowSecs"]) == ("", 0) + assert (out["push"]["slowPath"], out["push"]["slowSecs"]) == ("", 0), ( + "a member's connection must not be able to put a path on screen") + assert out["names"] == ["track 02.flac", "track 02.flac", ""], "either separator" + + +def test_the_row_says_what_the_disk_is_silent_on(): + dock = DOCK.read_text(encoding="utf-8") + row = dock[dock.index("function DockRow("):dock.index("export function IndexingDock(")] + assert ("t('indexdock.slow', { secs: number(row.slowSecs), " + "file: baseName(row.slowPath) })") in row + # Before the percentage: a bar that no longer moves is not the news. + assert row.index("else if (slow)") < row.index("else if (listing)") + assert "title=${slow ? row.slowPath : null}" in row, "the whole path, on hover" + + +def test_group_settings_list_what_the_node_could_not_read(): + """With paths, so asked of the loopback API and of nothing else: an operator + elsewhere sees the folder paused, as any member does.""" + settings = (STATIC / "group-settings.js").read_text(encoding="utf-8") + effect = settings[settings.index("if (!onNodeMachine) { setTrouble(null)"):] + effect = effect[:effect.index("}, [onNodeMachine, groupId, troubleTick]);")] + assert "platform.node.op('groupIndexStatus', { groupId })" in effect + assert "transport." not in effect + + note = settings[settings.index("const troubleNote ="):] + note = note[:note.index("if (!displayRoots.length)")] + assert "t('node.root_stalled_hint', { name, file })" in note + assert "t('node.unread_summary', { count: trouble.total })" in note + assert "u.reason === 'no_answer'" in note and "t('node.unread_no_answer')" in note + # Only for a folder that is still paused: resumed, the note has had its day. + assert "r.name === s.root && r.paused" in settings + assert "${troubleNote}" in settings[settings.index('<table class="shared-dirs-tbl">'):] + + css = (STATIC / "style.css").read_text(encoding="utf-8") + rule = css[css.index(".unread-files li {"):] + assert "word-break: break-word" in rule[:rule.index("}")], ( + "a path has no spaces to break at, and unpins the page on a phone") |