aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py55
1 files changed, 53 insertions, 2 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py
index db11a09..03db8b6 100644
--- a/packages/meshbay-node/src/meshbay_node/ui/app.py
+++ b/packages/meshbay-node/src/meshbay_node/ui/app.py
@@ -120,6 +120,10 @@ def create_ui_app(state: dict) -> FastAPI:
@app.get("/api/status")
async def api_status():
+ """
+ The daemon's state, and what it still needs: a linked key, a group, an operator, a group
+ key.
+ """
indexes = state.get("indexes", {})
total_files = sum(idx.count for idx in indexes.values())
groups_ctx = state.get("groups_ctx", {})
@@ -163,6 +167,7 @@ def create_ui_app(state: dict) -> FastAPI:
@app.delete("/api/unlink")
async def api_unlink():
+ """Unlink the node's key from its hub account."""
hub = state.get("hub")
if not hub:
raise HTTPException(status_code=503, detail="Hub not connected")
@@ -171,9 +176,13 @@ def create_ui_app(state: dict) -> FastAPI:
@app.get("/api/groups")
async def api_groups():
+ """The groups this node hosts, with live status, and whether an operator is paired."""
return await _op(lambda: ops.list_groups(state))
@app.post("/api/groups/attach")
async def attach_group(payload: dict):
+ """
+ Host a group that exists on the hub: add it to node.toml with its first folder, then reload.
+ """
result = await _op(lambda: ops.attach_group(
state,
(payload.get("name") or "").strip(),
@@ -188,6 +197,7 @@ def create_ui_app(state: dict) -> FastAPI:
@app.post("/api/groups/detach")
async def detach_group(payload: dict):
+ """Stop hosting a group: remove it from node.toml, then reload."""
result = await _op(lambda: ops.detach_group(
state,
(payload.get("name") or payload.get("group_id") or "").strip(),
@@ -199,31 +209,41 @@ def create_ui_app(state: dict) -> FastAPI:
@app.delete("/api/groups/{group_id}/files/{file_id}")
async def delete_file(group_id: str, file_id: str):
- """Milestone 14.11 — the last operator action that needed a browser."""
+ """
+ Delete a file from the group's folder on disk.
+
+ Milestone 14.11 — the last operator action that needed a browser.
+ """
return await _op(lambda: ops.delete_file(state, group_id, file_id))
@app.get("/api/denylist")
async def api_denylist():
+ """What the node currently refuses."""
return await _op(lambda: ops.read_denylist(state))
@app.post("/api/denylist/clear")
async def api_denylist_clear(subject: str = ""):
+ """Drop denylist entries: all of them, or one identifier."""
return await _op(lambda: ops.clear_denylist(state, subject=subject))
@app.get("/api/index-cache")
async def api_index_cache_stats():
+ """Size of the index cache."""
return await _op(lambda: ops.index_cache_stats(state))
@app.post("/api/index-cache/prune")
async def api_index_cache_prune():
+ """Drop index cache rows that no longer match a file on disk."""
return await _op(lambda: ops.prune_index_cache(state))
@app.post("/api/groups/{group_id}/video/rematch")
async def api_video_rematch(group_id: str):
+ """Forget the automatic matches of the group's videos, so they are looked up again."""
return await _op(lambda: ops.rematch_video(state, group_id))
@app.get("/api/groups/{group_id}/files")
async def api_group_files(group_id: str):
+ """The group's files, from its index."""
groups_ctx = state.get("groups_ctx", {})
ctx = groups_ctx.get(group_id)
if not ctx:
@@ -247,6 +267,7 @@ def create_ui_app(state: dict) -> FastAPI:
@app.get("/api/peers")
async def api_peers():
+ """The connected peers."""
webrtc = state.get("webrtc")
if not webrtc:
return {"peers": []}
@@ -275,6 +296,7 @@ def create_ui_app(state: dict) -> FastAPI:
user_id: str | None = Query(default=None),
event: str | None = Query(default=None),
):
+ """The audit log, filtered by time, account and event."""
audit = state.get("audit_store")
if not audit:
return {"entries": [], "offset": 0, "limit": limit, "has_more": False}
@@ -316,66 +338,80 @@ def create_ui_app(state: dict) -> FastAPI:
@app.post("/api/operator/pair")
async def operator_pair():
+ """A one-time code that pairs an application as this node's operator."""
return await _op(lambda: ops.pair_operator(state))
@app.get("/api/roster")
async def api_roster(group_id: str = ""):
+ """The pinned identities, for one group or all."""
return await _op(lambda: ops.read_roster(state, group_id))
@app.post("/api/groups/{group_id}/invites")
async def create_invite(group_id: str, username: str):
+ """An invitation code for one account, for this group."""
return await _op(lambda: ops.create_invite(state, group_id, username))
# Both halves, node and hub, for the CLI: an operator at the machine gets a
# whole link, not a code without a ticket.
@app.post("/api/groups/{group_id}/invite-links")
async def create_link_invite(group_id: str, email: str = ""):
+ """A whole invitation link: the node's code, then the hub's ticket."""
return await _op(lambda: ops.create_link_invitation(state, group_id, email))
@app.delete("/api/groups/{group_id}/invite-links/{invite_id}")
async def cancel_invite(group_id: str, invite_id: str):
+ """Take an invitation link back, on the node and on the hub."""
return await _op(lambda: ops.cancel_link_invitation(state, group_id, invite_id))
@app.get("/api/resolve")
async def resolve_user(username: str):
+ """Map a username to an account id, through the hub."""
return await _op(lambda: ops.resolve_user(state, username))
@app.post("/api/members/{user_id}/revoke")
async def revoke_member(user_id: str, group_id: str):
+ """Stop serving the group key to a member."""
return await _op(lambda: ops.revoke_member(state, user_id, group_id))
@app.post("/api/members/{user_id}/unpin")
async def unpin_member(user_id: str):
+ """Forget a pinned identity, so the person can pair again with a new key."""
return await _op(lambda: ops.unpin_member(state, user_id))
# ── Chat encryption (operator only, localhost) ─────────────────────────
@app.get("/api/groups/{group_id}/chat")
async def chat_status(group_id: str):
+ """What the operator needs to decide anything about the group's chat."""
return await _op(lambda: ops.chat_status(state, group_id))
@app.post("/api/groups/{group_id}/chat/epoch")
async def rotate_chat_epoch(group_id: str):
+ """Open a new chat epoch."""
return await _op(lambda: ops.open_chat_epoch(state, group_id))
@app.post("/api/groups/{group_id}/chat/encrypt-history")
async def encrypt_chat_history(group_id: str):
+ """Re-encrypt the messages written before the group's chat was encrypted."""
return await _op(lambda: ops.encrypt_chat_history(state, group_id))
@app.post("/api/groups/{group_id}/chat/prune")
async def prune_chat(group_id: str, max_age_days: int):
+ """Delete chat messages older than a number of days."""
return await _op(lambda: ops.prune_chat(state, group_id, max_age_days))
# ── GEK initialization (operator only, localhost) ──────────────────────
@app.post("/api/groups/{group_id}/gek")
async def init_gek(group_id: str, rotate: bool = False):
+ """Generate the group key, or rotate it with ?rotate=true."""
return await _op(lambda: ops.set_gek(state, group_id, rotate=rotate))
# ── Roots management (operator only, localhost) ────────────────────────
@app.post("/api/groups/{group_id}/roots")
async def add_root(group_id: str, payload: dict):
+ """Add a folder to a group."""
result = await _op(lambda: ops.add_root(
state, group_id,
(payload.get("path") or "").strip(),
@@ -392,6 +428,7 @@ def create_ui_app(state: dict) -> FastAPI:
@app.patch("/api/groups/{group_id}/roots/{root_name}")
async def update_root(group_id: str, root_name: str, payload: dict):
+ """Make a folder writable or removable, or not."""
result = await _op(lambda: ops.update_root(
state, group_id, root_name,
writable=payload.get("writable"),
@@ -404,14 +441,17 @@ def create_ui_app(state: dict) -> FastAPI:
@app.put("/api/groups/{group_id}/roots/{root_name}/eject")
async def eject_root(group_id: str, root_name: str):
+ """Eject a removable folder so its disk can be unplugged."""
return await _op(lambda: ops.eject_root(state, group_id, root_name))
@app.put("/api/groups/{group_id}/roots/{root_name}/plug")
async def plug_root(group_id: str, root_name: str):
+ """Bring an ejected folder back."""
return await _op(lambda: ops.plug_root(state, group_id, root_name))
@app.delete("/api/groups/{group_id}/roots/{root_name}")
async def remove_root(group_id: str, root_name: str):
+ """Remove a folder from a group. At least one must remain."""
result = await _op(lambda: ops.remove_root(state, group_id, root_name))
reload_fn = state.get("reload_fn")
if reload_fn:
@@ -427,6 +467,8 @@ def create_ui_app(state: dict) -> FastAPI:
@app.get("/api/groups/{group_id}/index-status")
async def index_status(group_id: str):
"""
+ One group's indexing progress.
+
Polled by the Create Group wizard and by "add a directory" in
Settings — the same source either way, since both just start a scan
on this group's indexer. `current_dir` is a basename only, and is
@@ -454,7 +496,9 @@ def create_ui_app(state: dict) -> FastAPI:
@app.get("/api/index-status")
async def index_status_all():
"""
- Every group's indexing at once, for the client's progress band — which
+ Every group's indexing progress.
+
+ All groups at once, for the client's progress band — which
is on screen whatever page the operator is on, so it cannot ask per
group. Names roots, like `current_dir` above: loopback only, the
operator's own screen. Reads state["indexers"] for the same reason.
@@ -488,6 +532,7 @@ def create_ui_app(state: dict) -> FastAPI:
@app.put("/api/groups/{group_id}/apps")
async def set_enabled_apps(group_id: str, payload: dict):
+ """Which applications members see for the group."""
apps = payload.get("apps")
if not isinstance(apps, list) or not apps:
raise HTTPException(400, "apps must be a non-empty list")
@@ -497,6 +542,7 @@ def create_ui_app(state: dict) -> FastAPI:
@app.post("/api/reload")
async def reload_config():
+ """Reload node.toml. Returns before the reload finishes."""
# start_reload, not reload_config: this must return before a
# brand-new group's synchronous initial scan finishes (minutes, not
# seconds, on a real library) — see ops.start_reload for why.
@@ -506,6 +552,7 @@ def create_ui_app(state: dict) -> FastAPI:
@app.post("/api/shutdown")
async def shutdown():
+ """Stop the daemon."""
# The graceful stop every front door tries first (the desktop app, the
# CLI, the installer): it reaches a node in any session -- a service
# node runs in session 0, where taskkill and CTRL_BREAK from the user's
@@ -521,20 +568,24 @@ def create_ui_app(state: dict) -> FastAPI:
@app.get("/api/node-settings")
async def get_node_settings():
+ """The node's effective settings."""
return await _op(lambda: ops.get_node_settings(state))
@app.put("/api/node-settings")
async def update_node_settings(payload: dict):
+ """Change node settings, written to roster.db and node.toml."""
return await _op(lambda: ops.set_node_settings(state, payload))
# ── Transfers (operator only, localhost) ───────────────────────────────
@app.get("/api/transfers")
async def get_transfers():
+ """Live transfer leases and queue depth."""
return await _op(lambda: ops.list_transfers(state))
@app.put("/api/groups/{group_id}/transfer-limits")
async def set_transfer_limits(group_id: str, payload: dict):
+ """How many transfers one member may run at once in this group."""
# The only door to `ops.set_transfer_limits` (the CLI uses it). It once
# had only a signed MNP message, which nothing anywhere sent — so the
# per-member cap sat at its default of 2 with no way to change it.