diff --git a/config.example.json b/config.example.json index d6430c9..a30c7e2 100644 --- a/config.example.json +++ b/config.example.json @@ -20,5 +20,5 @@ }, "log": "data/decisions.jsonl", "trace": "data/runs.jsonl", - "dashboard": {"port": 8765} + "dashboard": {"port": 8765, "host": "0.0.0.0"} } \ No newline at end of file diff --git a/semif_agent/cli.py b/semif_agent/cli.py index cac7228..7df0613 100644 --- a/semif_agent/cli.py +++ b/semif_agent/cli.py @@ -138,6 +138,11 @@ def main(argv: list[str] | None = None) -> int: dash_p = sub.add_parser("dashboard", help="run the local browser dashboard") dash_p.add_argument("--port", type=int, default=int(config.get("dashboard", {}).get("port", 8765))) + dash_p.add_argument( + "--host", + default=str(config.get("dashboard", {}).get("host", "127.0.0.1")), + help="bind address (0.0.0.0 to expose on the LAN)", + ) dash_p.add_argument( "--replay", action="store_true", @@ -166,7 +171,7 @@ def main(argv: list[str] | None = None) -> int: if args.replay: print("replay mode: reading decision log + trace; engine not warmed.") - serve(scheduler, port=args.port) + serve(scheduler, port=args.port, host=args.host) else: parser.print_help() return 0 diff --git a/semif_agent/dashboard.py b/semif_agent/dashboard.py index 8301981..054d233 100644 --- a/semif_agent/dashboard.py +++ b/semif_agent/dashboard.py @@ -226,10 +226,25 @@ class DashboardHandler(BaseHTTPRequestHandler): def serve(scheduler: Scheduler, port: int = 8765, host: str = "127.0.0.1") -> None: handler = type("Handler", (DashboardHandler,), {"scheduler": scheduler, "lock": threading.Lock()}) server = ThreadingHTTPServer((host, port), handler) - print(f"semif dashboard on http://{host}:{port}/ (Ctrl-C to stop)") + bound = server.server_address[0] + url = f"http://{bound}:{server.server_address[1]}/" if bound != "0.0.0.0" else _lan_url(port) + print(f"semif dashboard on {url} (Ctrl-C to stop)") try: server.serve_forever() except KeyboardInterrupt: pass finally: - server.server_close() \ No newline at end of file + server.server_close() + + +def _lan_url(port: int) -> str: + import socket + + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.connect(("8.8.8.8", 80)) + ip = sock.getsockname()[0] + sock.close() + except OSError: + ip = "127.0.0.1" + return f"http://{ip}:{port}/" \ No newline at end of file