diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-09 04:12:34 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-09 04:12:34 +0200 |
| commit | ece4b01405b8edcf5cbe6367a2882433db1491b5 (patch) | |
| tree | 339844449dc68a9675da47195665ad7d3d6fced7 /packages/meshbay-node/src/meshbay_node/ui | |
| parent | 6abb68ae95f6c4da4a66453398006183a73db9d9 (diff) | |
| download | meshbay-ece4b01405b8edcf5cbe6367a2882433db1491b5.tar.gz | |
feat(node): add config, daemon, and local web UI
config.py: TOML + env var overrides, sane defaults.
daemon.py: full startup sequence (keystore→hub→GEK→indexer→
server→UI), SIGINT/SIGTERM shutdown, calibrate-argon2 command.
ui/app.py: FastAPI on localhost:18000, status+files JSON API,
HTML status page (auto-refresh 10s). All bound to 127.0.0.1.
Full suite: 29/29 tests.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ui')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ui/__init__.py | 4 | ||||
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ui/app.py | 134 |
2 files changed, 138 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ui/__init__.py b/packages/meshbay-node/src/meshbay_node/ui/__init__.py index e69de29..a7ba872 100644 --- a/packages/meshbay-node/src/meshbay_node/ui/__init__.py +++ b/packages/meshbay-node/src/meshbay_node/ui/__init__.py @@ -0,0 +1,4 @@ +"""Local web UI served on localhost:18000.""" +from .app import create_ui_app + +__all__ = ["create_ui_app"] diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py new file mode 100644 index 0000000..f8978d1 --- /dev/null +++ b/packages/meshbay-node/src/meshbay_node/ui/app.py @@ -0,0 +1,134 @@ +""" +MeshBay Node — local web UI (localhost:18000). + +Minimal FastAPI app providing: + GET / → status page (HTML) + GET /api/status → JSON status + GET /api/files → JSON file list from the group index + GET /api/config → JSON config summary (no secrets) + +Served only on 127.0.0.1 — not exposed to the network. +""" + +import logging +from typing import TYPE_CHECKING + +from fastapi import FastAPI +from fastapi.responses import HTMLResponse + +from meshbay_node import __version__ + +if TYPE_CHECKING: + from meshbay_node.indexer import GroupIndex + +log = logging.getLogger(__name__) + + +def create_ui_app(state: dict) -> FastAPI: + """ + Create the UI FastAPI app. + + state dict is updated by the daemon and read by UI endpoints: + state["status"] : str — "starting" | "running" | "error" + state["group_id"] : str + state["group_name"] : str + state["hub_url"] : str + state["username"] : str + state["node_port"] : int + state["index"] : GroupIndex | None + state["endpoint_hint"]: str | None + """ + app = FastAPI( + title="MeshBay Node UI", + version=__version__, + docs_url=None, # disable Swagger on local UI + redoc_url=None, + ) + + @app.get("/api/status") + async def api_status(): + index = state.get("index") + return { + "version": __version__, + "status": state.get("status", "starting"), + "hub_url": state.get("hub_url", ""), + "username": state.get("username", ""), + "group_id": state.get("group_id", ""), + "group_name": state.get("group_name", ""), + "node_port": state.get("node_port", 0), + "endpoint_hint": state.get("endpoint_hint"), + "file_count": index.count if index else 0, + "index_version": index.version if index else 0, + } + + @app.get("/api/files") + async def api_files(): + index = state.get("index") + if not index: + return {"files": []} + return { + "files": [ + { + "id": e.id[:16] + "…", + "name": e.name, + "path": e.path, + "size": e.size, + "type": e.type, + "duration": e.duration, + } + for e in index.entries + ] + } + + @app.get("/", response_class=HTMLResponse) + async def root(): + index = state.get("index") + status = state.get("status", "starting") + file_count = index.count if index else 0 + status_color = {"running": "#22c55e", "error": "#ef4444"}.get(status, "#f59e0b") + + files_html = "" + if index: + rows = "".join( + f"<tr><td>{e.name}</td><td>{e.type}</td>" + f"<td>{e.size // 1024} KB</td><td>{e.path or '/'}</td></tr>" + for e in index.entries + ) + files_html = f""" + <h2>Files ({file_count})</h2> + <table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%"> + <thead><tr><th>Name</th><th>Type</th><th>Size</th><th>Path</th></tr></thead> + <tbody>{rows}</tbody> + </table>""" + + return f"""<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>MeshBay Node</title> + <style> + body {{ font-family: monospace; max-width: 900px; margin: 40px auto; padding: 0 20px; }} + .badge {{ display:inline-block; padding:3px 10px; border-radius:4px; + color:#fff; background:{status_color}; font-weight:bold; }} + table {{ font-size: 0.9em; }} + th {{ background: #f3f4f6; }} + </style> + <meta http-equiv="refresh" content="10"> +</head> +<body> + <h1>🔗 MeshBay Node <span class="badge">{status}</span></h1> + <p> + <b>Hub:</b> {state.get("hub_url", "—")} | + <b>User:</b> {state.get("username", "—")} | + <b>Group:</b> {state.get("group_name") or state.get("group_id") or "—"} | + <b>Port:</b> {state.get("node_port", "—")} + </p> + <p><b>Endpoint:</b> {state.get("endpoint_hint") or "unknown"}</p> + {files_html} + <hr> + <small>MeshBay Node v{__version__} — <a href="/api/status">JSON status</a> + — <a href="/api/files">JSON files</a></small> +</body> +</html>""" + + return app |