From 14911aae21ec4975f7a805b91e3c4867cb41974b Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 12 Oct 2026 00:17:32 +0200 Subject: 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 --- docs/MESHBAY_DESIGN.md | 35 +- docs/MESHBAY_HTTP_API.md | 2 + docs/MESHBAY_NODE_PROTOCOL.md | 21 +- docs/USERGUIDE.md | 8 + .../kotlin/org/meshbay/client/keys/Transcripts.kt | 2 +- packages/meshbay-client/src/main.js | 2 + packages/meshbay-client/src/transcripts.js | 2 +- .../meshbay-common/src/meshbay_common/__init__.py | 8 +- .../meshbay-common/src/meshbay_common/adminop.py | 5 + .../meshbay-common/src/meshbay_common/protocol.py | 6 + .../src/meshbay_hub/static/group-page.js | 8 +- .../src/meshbay_hub/static/group-settings.js | 31 +- .../src/meshbay_hub/static/index-dock-model.js | 25 +- .../src/meshbay_hub/static/index-dock.js | 62 +- .../src/meshbay_hub/static/locales/de.js | 4 + .../src/meshbay_hub/static/locales/en.js | 4 + .../src/meshbay_hub/static/locales/es.js | 4 + .../src/meshbay_hub/static/locales/fr.js | 4 + .../src/meshbay_hub/static/locales/it.js | 4 + .../src/meshbay_hub/static/locales/ja.js | 4 + .../src/meshbay_hub/static/locales/nl.js | 4 + .../src/meshbay_hub/static/locales/pl.js | 4 + .../src/meshbay_hub/static/locales/pt-BR.js | 4 + .../src/meshbay_hub/static/locales/zh-CN.js | 4 + .../meshbay-hub/src/meshbay_hub/static/style.css | 2 + .../src/meshbay_hub/static/transport-admin.js | 21 + .../src/meshbay_hub/static/transport.js | 10 +- packages/meshbay-hub/tests/test_indexing_dock.py | 34 +- .../meshbay-node/src/meshbay_node/cli/groups.py | 29 +- .../meshbay-node/src/meshbay_node/cli/parser.py | 4 +- packages/meshbay-node/src/meshbay_node/daemon.py | 17 +- .../src/meshbay_node/indexer/indexer.py | 281 ++++-- .../meshbay-node/src/meshbay_node/ops/__init__.py | 4 + .../meshbay-node/src/meshbay_node/ops/roots.py | 73 +- packages/meshbay-node/src/meshbay_node/roots.py | 15 +- packages/meshbay-node/src/meshbay_node/roster.py | 25 + .../src/meshbay_node/transport/webrtc/admin.py | 4 + .../src/meshbay_node/transport/webrtc/dispatch.py | 2 + .../src/meshbay_node/transport/webrtc/node_ops.py | 62 +- packages/meshbay-node/src/meshbay_node/ui/app.py | 10 + packages/meshbay-node/tests/golden/cli.json | 4 +- packages/meshbay-node/tests/golden/dispatch.json | 982 ++++++++++++++++++--- .../meshbay-node/tests/test_root_availability.py | 2 +- .../tests/test_root_paths_are_operator_only.py | 2 +- packages/meshbay-node/tests/test_root_pause.py | 313 +++++++ packages/meshbay-node/tests/test_roots.py | 13 +- 46 files changed, 1919 insertions(+), 247 deletions(-) create mode 100644 packages/meshbay-node/tests/test_root_pause.py diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 9c63c52..d0b5475 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -16,7 +16,7 @@ > them — it names the invariant that holds today, not the incident that produced > it. §13 is the register of those labels. > -> Wire versions at the time of writing: **MNP 6.1** (oldest peer accepted 4.0), +> Wire versions at the time of writing: **MNP 6.2** (oldest peer accepted 4.0), > **MHP 0.1**, packages **0.19.0**. The normative source for the wire format is > `MESHBAY_NODE_PROTOCOL.md`; this document states the design the protocol > serves, not its byte layout. @@ -1231,7 +1231,7 @@ from anything in the response. | `invite_link_create`, `invite_cancel` | the operator | | group key init and rotation | **local admin API or CLI only**; the node generates the key itself | | root add, root `writable`/`removable`, hosting a group, transfer limits | **local admin API or CLI only** (MNP 6.0) | -| root remove/eject/plug, `apps_enabled`, app directories | operator-signed | +| root remove/eject/plug/pause/resume, `apps_enabled`, app directories | operator-signed | | ~~`gek_bundle_store`~~ | **the message does not exist.** No member ever hands the node key material | `gek_bundle_store` was deleted rather than gated. The operator's X25519 public key @@ -1641,8 +1641,9 @@ A signed op proves that the operator's key signed, not that they meant it: in a browser that key is driven by code the hub serves (T3), and in the desktop application by a renderer that parses content from nodes. Either could otherwise have shared any folder on the machine, writable, from anywhere. The operator still -sees every root and its flags from any browser; removing, ejecting and plugging stay -signed ops, because they narrow what is shared or restore what already was. +sees every root and its flags from any browser; removing, ejecting, plugging, pausing +and resuming stay signed ops, because they narrow what is shared or restore what +already was. > **There is one answer to "may this member write", and it is the root.** A single > flag over the group cannot express "this library is published read-only and that @@ -1670,6 +1671,12 @@ states: - **`available`** — computed as `not ejected and is_live()`. This is what clients and the indexer see. +A third, **`paused`**, is the operator's alone and independent of both: the root is +readable and served, and its indexing is stopped (§6.3). Persisted in `roster.db` +beside `ejected`, for the same reason: a pause that a restart undid would start the +scan the operator stopped. Removing a root forgets its pause, so a directory added +back under the same name starts indexed. + The distinction matters: between clicking eject and physically unplugging, `is_live()` is still true, and without `ejected` the availability sweep would immediately flip the root back. @@ -1755,6 +1762,24 @@ reason to show progress. `index_progress` is deliberately **not** sealed: counters only, every 2 s. +**A walk looks at its root before every file.** A root removed from the set or +ejected while it is walked is not read to the end: its entries are dropped, or +frozen on a drive that is leaving. The walk reads the root from the current set, +not the object it started with, because a reload replaces every root. + +**A pause outlasts the walk it stops.** Reconcile indexes every file the index +lacks, so a walk that was merely cancelled is redone at the next tick. A paused +root is therefore not watched, not swept by reconcile, and not hashed by any walk: + +- **A walk that meets a pause finishes from the hash cache alone.** What the node + already hashed stays listed; nothing new is read. Stopping dead is not enough, + because the index is rebuilt at every start and a rescan drops the root's + entries before it walks — either would leave a paused root half empty. +- **Resuming runs the reconcile sweep over that root, never a scan.** A scan + replaces every entry it meets, and with them what the enrichment passes put + there; the sweep indexes only what the index lacks. The request returns once + the walk is queued, and the progress band shows it. + ### 6.4 Uploads Five protections, and they are the substance: @@ -1989,7 +2014,7 @@ before its key is linked or a group exists. ``` meshbay-node status meshbay-node group list|add|remove -meshbay-node root list|add|remove|set|eject|plug +meshbay-node root list|add|remove|set|eject|plug|pause|resume meshbay-node gek init|rotate meshbay-node operator pair meshbay-node member list|invite [--link]|cancel|revoke|unpin diff --git a/docs/MESHBAY_HTTP_API.md b/docs/MESHBAY_HTTP_API.md index 1bcb55d..73d079d 100644 --- a/docs/MESHBAY_HTTP_API.md +++ b/docs/MESHBAY_HTTP_API.md @@ -229,6 +229,8 @@ draws a new one at each start and deletes the file when it stops. | PATCH | `/api/groups/{group_id}/roots/{root_name}` | Make a folder writable or removable, or not. | | PUT | `/api/groups/{group_id}/roots/{root_name}/eject` | Eject a removable folder so its disk can be unplugged. | | PUT | `/api/groups/{group_id}/roots/{root_name}/plug` | Bring an ejected folder back. | +| PUT | `/api/groups/{group_id}/roots/{root_name}/pause` | Stop indexing a folder. | +| PUT | `/api/groups/{group_id}/roots/{root_name}/resume` | Index a paused folder again, from where it stopped. | | DELETE | `/api/groups/{group_id}/roots/{root_name}` | Remove a folder from a group. | | GET | `/api/groups/{group_id}/index-status` | One group's indexing progress. | | GET | `/api/index-status` | Every group's indexing progress. | diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index 994c908..207d640 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -1168,6 +1168,7 @@ broadcast, every connected peer in the group learns the change without reconnect | `musicbrainz_enabled` | `enabled` | operator | `musicbrainz_enabled_ack{enabled}` | yes | | `root_remove` | the root name | operator | `root_remove_ack` | no | | `root_eject`, `root_plug` | the root name | operator | `root_eject_ack` / `root_plug_ack` | yes | +| `root_pause`, `root_resume` | the root name | operator | `root_pause_ack` / `root_resume_ack` | yes | | `app_directories` | `:,,...` | operator | `app_directories_ack{app, dirs}` | yes | | `chat_directory` | the path | operator | `chat_directory_ack{path}` | yes | | `chat_link_preview` | `on\|off` | operator | `chat_link_preview_ack{enabled}` | yes | @@ -1189,7 +1190,8 @@ proves that the operator's key signed, not that the operator meant it: in a brow that key is driven by code the hub serves (T3), and in the desktop application by a renderer that parses content from nodes. Either could have shared any folder on the operator's machine, writable, from anywhere. What is left here narrows (`root_remove`, -`root_eject`) or restores what the operator already shared (`root_plug`). The +`root_eject`, `root_pause`) or restores what the operator already shared (`root_plug`, +`root_resume`). The operator still sees the table from any browser: it rides in `index_sync` and `index_delta`, which is also how a change made on the node's machine reaches every open page. @@ -1327,11 +1329,13 @@ folder just created, or one emptied, does not exist as far as the UI is concerne a member cannot tell "the drive is unplugged" from "it is all still there" — an unavailable root is listed, with its content frozen rather than hidden. -Each root is described as `{name, kind, available, writable, removable, ejected}` — and -never a path: a member is told what exists and whether it is readable, never where on -the operator's disk it lives. `roots` rides on `index_delta` as well as `index_sync`, +Each root is described as `{name, kind, available, writable, removable, ejected, +paused}` — and never a path: a member is told what exists and whether it is readable, +never where on the operator's disk it lives. `paused` is the operator having stopped +the root's indexing: its files are served as before, and nothing new is indexed until +it is resumed. `roots` rides on `index_delta` as well as `index_sync`, because a full index is only ever sent on request: without it, a root added, removed, -ejected or plugged would leave every connected client's directory table stale until +ejected, plugged or paused would leave every connected client's directory table stale until somebody reloaded the page, and the delta that tells them something changed would be the one message unable to say what. @@ -2138,6 +2142,7 @@ it back (§3.5). | `chat_link_preview` / `_ack` | C→N / N⇒C | signed | whether the node unfurls posted links | | `search_listed` / `_ack` | C→N / N⇒C | signed | whether members' cross-group Search lists this group | | `root_eject` / `_ack`, `root_plug` / `_ack` | C→N / N⇒C | signed | take a removable root offline, put it back | +| `root_pause` / `_ack`, `root_resume` / `_ack` | C→N / N⇒C | signed | stop a root's indexing, start it again from where it stopped (6.2) | | `apps_enabled` / `_ack` | C→N / N⇒C | signed | which group apps are shown | | `set_scan_settings` / `_ack` | C→N / N⇒C | signed | reconcile interval and debounce | | `tmdb_config` / `_ack` | C→N / N⇒C | signed | node-wide TMDB token and language | @@ -2182,7 +2187,7 @@ message: ## 13. Versioning and compatibility -MNP versions independently of the package version. Current: **`6.1`**; oldest peer +MNP versions independently of the package version. Current: **`6.2`**; oldest peer accepted: **`4.0`**. 4.0 is the floor: a member presents a short-lived node-audience token bound to one node @@ -2208,6 +2213,10 @@ script in its page either. 6.1 adds the signed `chat_purge` (§10.4), additively: a 6.0 node gives no answer, as for any unknown type, and nothing else changes. +6.2 adds the signed `root_pause` and `root_resume` (§10.4) and `paused` on each root of +the roots table, additively: a 6.1 node gives no answer to either message, and a client +that ignores `paused` shows a paused root as an ordinary one. + The two numbers are separate on purpose. `MNP_VERSION` says what this build speaks; `MNP_MIN_SUPPORTED` says what it will talk to, and moving the second is a decision about whether an older peer can still do anything useful: diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index 5649b30..42103d6 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -695,6 +695,13 @@ catalogued. Group → Settings has the re-check interval and how long the node waits after a file changes before indexing it. Members see indexing progress while it runs. +**Pausing.** Indexing a large drive takes hours. The pause button on the +indexing band, or **Pause indexing** beside the directory in Group → Settings, +stops it at the next file. The files already indexed stay visible to members. +Nothing else in that directory is indexed until you click **Resume indexing**, +not even a file a member uploads into it, and the pause survives a restart. +Resuming picks up where it stopped: files already indexed are not read again. + One thing that surprises people: **two identical files in one group are one entry**, because a file is identified by its content. A scan that reports ten files and indexes nine has not lost anything. @@ -1058,6 +1065,7 @@ meshbay-node reset erase all node state (destructive) meshbay-node group list|add|remove groups this node hosts meshbay-node root list|add|remove|set directories in a group meshbay-node root eject|plug removable drives +meshbay-node root pause|resume stop and restart a directory's indexing meshbay-node gek init|rotate the group's encryption key meshbay-node operator pair authorise a browser diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/keys/Transcripts.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/keys/Transcripts.kt index a48c4cf..b2d3d68 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/keys/Transcripts.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/keys/Transcripts.kt @@ -171,7 +171,7 @@ class Transcripts(private val now: () -> Double = { System.currentTimeMillis() / "invite_cancel", "member_revoke", "apps_enabled", "set_scan_settings", "tmdb_config", "tmdb_enabled", "tmdb_override", "tmdb_rematch", "musicbrainz_enabled", "root_remove", "root_eject", "root_plug", - "app_directories", "chat_directory", "chat_link_preview", "search_listed", + "root_pause", "root_resume", "app_directories", "chat_directory", "chat_link_preview", "search_listed", "chat_epoch", "chat_purge", ) const val PREFIX_JOIN = "meshbay:join:v1" diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index 1af4386..33388e8 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -2396,6 +2396,8 @@ Its log: ${nodeLogHint()}`); updateRoot: (a) => ['PATCH', root(a), anObject(a.updates)], ejectRoot: (a) => ['PUT', `${root(a)}/eject`], plugRoot: (a) => ['PUT', `${root(a)}/plug`], + pauseRoot: (a) => ['PUT', `${root(a)}/pause`], + resumeRoot: (a) => ['PUT', `${root(a)}/resume`], removeRoot: (a) => ['DELETE', root(a)], initGek: (a) => ['POST', `${group(a)}/gek${a.rotate ? '?rotate=true' : ''}`], pairOperator: () => ['POST', '/api/operator/pair'], diff --git a/packages/meshbay-client/src/transcripts.js b/packages/meshbay-client/src/transcripts.js index ffba3bf..8119d71 100644 --- a/packages/meshbay-client/src/transcripts.js +++ b/packages/meshbay-client/src/transcripts.js @@ -42,7 +42,7 @@ const ADMIN_OPS = new Set([ 'invite_cancel', 'member_revoke', 'apps_enabled', 'set_scan_settings', 'tmdb_config', 'tmdb_enabled', 'tmdb_override', 'tmdb_rematch', 'musicbrainz_enabled', 'root_remove', 'root_eject', 'root_plug', - 'app_directories', 'chat_directory', 'chat_link_preview', 'search_listed', + 'root_pause', 'root_resume', 'app_directories', 'chat_directory', 'chat_link_preview', 'search_listed', 'chat_epoch', 'chat_purge', ]); diff --git a/packages/meshbay-common/src/meshbay_common/__init__.py b/packages/meshbay-common/src/meshbay_common/__init__.py index e8a9579..b86dec2 100644 --- a/packages/meshbay-common/src/meshbay_common/__init__.py +++ b/packages/meshbay-common/src/meshbay_common/__init__.py @@ -276,5 +276,11 @@ __version__ = "0.19.0" # 6.1 (2026-10-08) adds the signed `chat_purge` / `chat_purge_ack`: the operator # deletes a group's stored chat. Additive — a 6.0 node answers nothing, as for # any unknown type, and nothing else changes. The floor stays at 4.0. -MNP_VERSION = "6.1" +# +# 6.2 (2026-10-11) adds the signed `root_pause` / `root_resume` and their acks: +# the operator stops a root's indexing and starts it again, and each root in +# the roots table carries `paused`. Additive — a 6.1 node answers nothing to +# either, and a client that ignores `paused` shows a paused root as an ordinary +# one. The floor stays at 4.0. +MNP_VERSION = "6.2" MHP_VERSION = "0.1" diff --git a/packages/meshbay-common/src/meshbay_common/adminop.py b/packages/meshbay-common/src/meshbay_common/adminop.py index f59b420..43d2a7f 100644 --- a/packages/meshbay-common/src/meshbay_common/adminop.py +++ b/packages/meshbay-common/src/meshbay_common/adminop.py @@ -113,6 +113,11 @@ OP_CHAT_EPOCH = "chat_epoch" OP_CHAT_PURGE = "chat_purge" OP_ROOT_EJECT = "root_eject" OP_ROOT_PLUG = "root_plug" +# Stop and restart the indexing of one root (MNP 6.2). The subject is the root +# name, like eject and plug. Pausing narrows what the node does and resuming +# returns to what the operator already shared, so neither widens anything. +OP_ROOT_PAUSE = "root_pause" +OP_ROOT_RESUME = "root_resume" # Also gone with 6.0, because no client ever sent them: `gek_rotate`, # `member_unpin`, `transfer_limits` and `group_detach`. Rotating the group key, # forgetting an identity, the per-member transfer caps and no longer hosting a diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py index 5073b66..e5ceaa7 100644 --- a/packages/meshbay-common/src/meshbay_common/protocol.py +++ b/packages/meshbay-common/src/meshbay_common/protocol.py @@ -252,6 +252,12 @@ class MNP: ROOT_EJECT_ACK = "root_eject_ack" ROOT_PLUG = "root_plug" # operator → node: re-enable an ejected root ROOT_PLUG_ACK = "root_plug_ack" + # Operator → node: stop / restart indexing one root (MNP 6.2). The acks are + # broadcast, like eject's, so every open table shows the new state. + ROOT_PAUSE = "root_pause" + ROOT_PAUSE_ACK = "root_pause_ack" + ROOT_RESUME = "root_resume" + ROOT_RESUME_ACK = "root_resume_ack" # Member → node: who is in this group and which device keys they hold, with # the countersignature that admitted each one. Scoped to one group and # answers any member of it, because the point is that a member 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, ${t('group.root_ejected')}`} ${!isLocal && r.available === false && !r.ejected && html` ${t('node.unavailable')}`} + ${!isLocal && r.paused && html` + ${t('node.root_paused')}`} ${hasPaths && html` ${r.path || ''}`} @@ -346,6 +361,14 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn, `} + ${canEdit && !isLocal && html` + + `} ${canEdit && !isLocal && html` + ${!done && onPause && html` + + `} ${!done && html`