summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/daemon.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-01 14:08:32 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-01 14:08:32 +0200
commitcfc91e0a424163869c64d30e55d55a53f18a3dbf (patch)
tree04ed3bbec11690f62f1e2a738bf89f4b763332e5 /packages/meshbay-node/src/meshbay_node/daemon.py
parentba45a3c94806f612fa62812e0b36d08b581a2e47 (diff)
downloadmeshbay-cfc91e0a424163869c64d30e55d55a53f18a3dbf.tar.gz
refactor(node): JSON-only control API, Node page absorbs the admin dashboard
Remove the node daemon's server-rendered admin UI (GET / and /audit, the _render_* helpers and inline templates) and the `meshbay-node ui` CLI verb. The loopback control API stays; it is now JSON only, ruff-clean, and 453 lines (was 1074). Also drop three never-wired endpoints (/api/config, /api/chat/history, /ws/chat, plus broadcast_chat) and the pointless 18000/tcp firewall profiles. The desktop client's Node page (static/node-page.js) takes over what the dashboard showed, reorganised into six tabs (Overview, Groups, Roster, Peers, Audit, Settings): - Overview: version, node id, QUIC port, hub, index-cache maintenance - Roster: node-wide view with unpin - Peers and Audit: auto-load on open, no Load button - Audit: real usernames and group names (resolved from the roster and node.toml), Previous/Next pagination newest-first, Export CSV of every matching row - Settings: node settings, STUN, ICE, denylist, then Unlink from hub Backend: audit.get_entries gains `offset`; /api/audit and /api/peers resolve ids to names via a new _display_names helper; CSP tightened to default-src 'none' now that no HTML is served. draft-v6 sections 2.11 and 2.12 corrected -- the Node page uses the loopback API, not MNP. One capability is intentionally dropped: browser-based admin on a headless server. The CLI covers every operation there. See docs/refactor-node-ui.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQCaZnde4Bjjdu84dhSuF5
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py59
1 files changed, 24 insertions, 35 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 2130440..056371a 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -12,14 +12,13 @@ Startup sequence:
8. Start QUIC chunk server (LAN / port-forwarded / hub-less direct access)
9. (Phase 11.5: the unauthenticated HTTP file API and the TCP+TLS server were removed)
10. Start hub WebSocket (signaling, revocations, WebRTC offers)
- 11. Start local web UI on node.ui_port (localhost only)
+ 11. Start local control API on node.ui_port (loopback only, token-gated)
12. Run until SIGINT/SIGTERM
Usage:
meshbay-node # interactive password prompt
meshbay-node --config /path # custom config
meshbay-node status # node state + public key (works while stopped)
- meshbay-node ui # print the local admin UI URL
meshbay-node gek-init # initialise the group key (no browser needed)
meshbay-node init # write example config + create keystore
meshbay-node --calibrate-argon2 # benchmark Argon2id, suggest parameters
@@ -201,19 +200,21 @@ class NodeDaemon:
)
log.info("Keys loaded: %s", keys.pk_ed25519_b64[:16])
- # 2. Start admin UI early (so operator can copy node key before hub login)
+ # 2. Start the local control API early (so the operator can read the
+ # node key before hub login). It is JSON-only, loopback-only, and both
+ # the CLI and the desktop client's Node page are its clients.
self._state["pk_node_ed25519"] = keys.pk_ed25519_b64
self._state["config"] = self._config
# Where it came from, so `group add` appends to the file this process
# actually read rather than guessing at the default.
self._state["config_path"] = str(self._config_path)
- # Per-run token for the local admin UI (11.5.3). Not a password: it keeps
+ # Per-run token for the control API (11.5.3). Not a password: it keeps
# other local processes and rebound browser pages out of an API that can
# re-initialise group keys.
ui_token = base64.urlsafe_b64encode(os.urandom(18)).decode().rstrip("=")
self._state["ui_token"] = ui_token
- # Persisted so `meshbay-node ui` can open the browser. Nobody should ever
- # have to copy a token out of a log or a terminal — that is not a workflow.
+ # Persisted so the CLI and the desktop client can read it — nobody
+ # should ever copy a token out of a log or a terminal.
self._config.data_dir.mkdir(parents=True, exist_ok=True)
self._ui_token_file = self._config.data_dir / "ui-token"
self._ui_token_file.write_text(ui_token)
@@ -228,7 +229,7 @@ class NodeDaemon:
)
ui_server = uvicorn.Server(ui_cfg)
self._tasks.append(asyncio.create_task(ui_server.serve()))
- log.info("Admin UI ready — open it with: meshbay-node ui")
+ log.info("Control API on 127.0.0.1:%d", self._config.node.ui_port)
# 3. Hub connection (Ed25519 auth — retries until node key is linked)
hub_cfg = HubConfig(
@@ -415,7 +416,7 @@ class NodeDaemon:
}
if not groups_ctx:
- log.warning("No groups configured yet — admin UI and hub "
+ log.warning("No groups configured yet — the control API and hub "
"connection stay up; attach a group to go live")
# 5. Chat stores (one SQLite DB per group)
@@ -608,7 +609,7 @@ class NodeDaemon:
# authentication, for private groups too — finding C1. Every client path now
# goes through the MNP handshake (JWT + group claim + GEK proof).
- # 10. Update admin UI state (UI already running from step 2)
+ # 10. Update control API state (already running from step 2)
self._state["groups_ctx"] = groups_ctx
self._state["audit_store"] = self._audit_store
self._state["bundle_store"] = self._bundle_store
@@ -908,17 +909,19 @@ class NodeDaemon:
except _httpx.HTTPStatusError as e:
body = e.response.text if hasattr(e.response, 'text') else ''
# Any 401 here needs a human at a browser, and the operator needs
- # this daemon alive to read its public key out of the local admin
- # UI. Exiting would take that UI down and strand them — which is
- # exactly what happened when a node was started before its owner
- # had registered.
+ # this daemon alive to read its public key (via `meshbay-node
+ # status` or the desktop client, both of which query the control
+ # API). Exiting would strand them — which is exactly what
+ # happened when a node was started before its owner had
+ # registered.
if e.response.status_code == 401:
if "No node key" in body:
self._state["status"] = "waiting_for_node_key"
log.warning(
- "Node key not linked. Open the admin UI, copy this "
- "node's key, and paste it in Settings > Link Node on "
- "%s. Retrying in 5s...",
+ "Node key not linked. Get it from `meshbay-node "
+ "status` and paste it in Settings > Link Node on %s "
+ "(the desktop client links it automatically). "
+ "Retrying in 5s...",
self._config.hub.url,
)
else:
@@ -1500,8 +1503,8 @@ def _daemon_api(cfg: Config, path: str, method: str = "GET",
The daemon owns the roster, the hub session and the live group contexts, so
the CLI asks it to act rather than opening its databases behind its back. It
- also means every operator action goes through the same authorization as the
- admin UI (the per-run session token, 11.5.3).
+ also means every operator action goes through the control API's per-run
+ session token (11.5.3), the same gate the desktop client's Node page passes.
"""
import json as _json
import urllib.error
@@ -1611,13 +1614,13 @@ def main() -> None:
parser = argparse.ArgumentParser(description="MeshBay Node daemon")
parser.add_argument("command", nargs="?",
- choices=["init", "reset", "status", "ui", "gek-init",
+ choices=["init", "reset", "status", "gek-init",
"gek", "operator", "member", "group", "file",
"video", "denylist", "stun", "reload",
"restart-daemon", "calibrate-argon2"],
help="init: provision config + keystore | reset: erase all "
"node state | status: node state and keys "
- "| ui: print the admin UI URL | operator pair: pair a "
+ "| operator pair: pair a "
"browser with this node | member list|invite|revoke|unpin "
"| group list|add|remove | gek init|rotate | file list|rm "
"| video rematch: re-resolve TMDB matches for a group's "
@@ -1655,7 +1658,7 @@ def main() -> None:
args = parser.parse_args()
# Query commands print a report; library logging would interleave with it.
- quiet = args.command in ("status", "ui", "gek-init", "gek", "operator",
+ quiet = args.command in ("status", "gek-init", "gek", "operator",
"member", "group", "file", "video", "denylist",
"stun", "reload", "restart-daemon", "reset")
logging.basicConfig(
@@ -1843,7 +1846,6 @@ def main() -> None:
print(f"groups {live.get('group_count', 0)}"
f" files {live.get('total_files', 0)}"
f" peers {live.get('webrtc_peers', 0)}")
- print(f"admin UI meshbay-node ui")
needs = live.get("needs", [])
if needs:
@@ -2331,19 +2333,6 @@ def main() -> None:
print(f"also written to {path}")
return
- if args.command == "ui":
- cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
- token_file = cfg.data_dir / "ui-token"
- if not token_file.exists():
- print("Node does not appear to be running — start it with: meshbay-node")
- sys.exit(1)
- print(f"http://127.0.0.1:{cfg.node.ui_port}"
- f"/?t={token_file.read_text().strip()}")
- print()
- print("The UI listens on loopback only. From another machine:")
- print(f" ssh -L {cfg.node.ui_port}:127.0.0.1:{cfg.node.ui_port} <this-host>")
- return
-
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
if not cfg.hub.username:
print("Error: hub.username not set in config. Run: meshbay-node init")