From ece4b01405b8edcf5cbe6367a2882433db1491b5 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 9 Aug 2026 04:12:34 +0200 Subject: feat(node): add config, daemon, and local web UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packages/meshbay-node/src/meshbay_node/config.py | 123 +++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 packages/meshbay-node/src/meshbay_node/config.py (limited to 'packages/meshbay-node/src/meshbay_node/config.py') diff --git a/packages/meshbay-node/src/meshbay_node/config.py b/packages/meshbay-node/src/meshbay_node/config.py new file mode 100644 index 0000000..27301af --- /dev/null +++ b/packages/meshbay-node/src/meshbay_node/config.py @@ -0,0 +1,123 @@ +""" +MeshBay Node configuration. + +Config file: ~/.config/meshbay/node.toml +All values have sensible defaults and can be overridden by env vars +prefixed with MESHBAY_ (e.g. MESHBAY_HUB_URL). +""" + +import os +from dataclasses import dataclass, field +from pathlib import Path + +try: + import tomllib # Python 3.11+ +except ImportError: + import tomli as tomllib # type: ignore[no-redef] + +DEFAULT_CONFIG_PATH = Path.home() / ".config" / "meshbay" / "node.toml" + +EXAMPLE_CONFIG = """\ +# MeshBay Node configuration +# See: https://meshbay.org/docs/node-config + +[hub] +url = "http://meshbay.org" +username = "myusername" +# password stored separately via keystore + +[node] +port = 19000 +ui_port = 18000 +shared_dirs = ["/home/user/MeshBay"] + +[group] +id = "" # set after joining a group +name = "" + +[keystore] +# unlock_file = "~/.config/meshbay/unlock.key" +# or set MESHBAY_UNLOCK_KEY env var +""" + + +@dataclass +class HubConfig: + url: str = "http://meshbay.org" + username: str = "" + password: str = "" # loaded from keystore or env; never written to TOML + + +@dataclass +class NodeConfig: + port: int = 19000 + ui_port: int = 18000 + shared_dirs: list[str] = field(default_factory=list) + + +@dataclass +class GroupConfig: + id: str = "" + name: str = "" + + +@dataclass +class KeystoreConfig: + path: Path = field(default_factory=lambda: DEFAULT_CONFIG_PATH.parent / "keystore.enc") + unlock_file: Path | None = None + + +@dataclass +class Config: + hub: HubConfig = field(default_factory=HubConfig) + node: NodeConfig = field(default_factory=NodeConfig) + group: GroupConfig = field(default_factory=GroupConfig) + keystore: KeystoreConfig = field(default_factory=KeystoreConfig) + + +def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config: + """ + Load config from TOML file. Missing file returns defaults. + Env vars override file values. + """ + cfg = Config() + + if path.exists(): + raw = tomllib.loads(path.read_text()) + hub = raw.get("hub", {}) + cfg.hub.url = hub.get("url", cfg.hub.url) + cfg.hub.username = hub.get("username", cfg.hub.username) + + nd = raw.get("node", {}) + cfg.node.port = nd.get("port", cfg.node.port) + cfg.node.ui_port = nd.get("ui_port", cfg.node.ui_port) + cfg.node.shared_dirs = nd.get("shared_dirs", cfg.node.shared_dirs) + + grp = raw.get("group", {}) + cfg.group.id = grp.get("id", cfg.group.id) + cfg.group.name = grp.get("name", cfg.group.name) + + ks = raw.get("keystore", {}) + if "path" in ks: + cfg.keystore.path = Path(ks["path"]).expanduser() + if "unlock_file" in ks: + cfg.keystore.unlock_file = Path(ks["unlock_file"]).expanduser() + + # Env var overrides + if url := os.environ.get("MESHBAY_HUB_URL"): + cfg.hub.url = url + if user := os.environ.get("MESHBAY_USERNAME"): + cfg.hub.username = user + if pwd := os.environ.get("MESHBAY_PASSWORD"): + cfg.hub.password = pwd + if port := os.environ.get("MESHBAY_PORT"): + cfg.node.port = int(port) + + return cfg + + +def write_example_config(path: Path = DEFAULT_CONFIG_PATH) -> None: + """Write an example config file if none exists.""" + if not path.exists(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(EXAMPLE_CONFIG) -- cgit v1.2.3