#!/usr/bin/env python3 """ Minimal backend for Circuit Builder UI. Serves the static UI and runs entropyk-cli validate/run on POST. Run from repository root: python tools/circuit-builder-ui/server.py Then open http://localhost:8765 """ import json import os import subprocess import tempfile from http.server import HTTPServer, BaseHTTPRequestHandler from pathlib import Path from urllib.parse import parse_qs, urlparse PORT = 8765 # Path to entropyk-cli from repo root (run server from repo root) REPO_ROOT = Path(__file__).resolve().parent.parent.parent CLI_PATH = REPO_ROOT / "target" / "release" / "entropyk-cli" UI_DIR = Path(__file__).resolve().parent def run_cli(subcommand, config_json_str, run_output=None): """Run entropyk-cli validate or run; config_json_str written to temp file.""" if not CLI_PATH.exists(): return False, f"CLI not found. Build first: cargo build --release -p entropyk-cli\nExpected: {CLI_PATH}", "" with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: f.write(config_json_str) tmp_path = f.name try: cmd = [str(CLI_PATH), subcommand, "--config", tmp_path] if run_output is not None: cmd.extend(["-o", run_output]) r = subprocess.run( cmd, capture_output=True, text=True, timeout=60, cwd=str(REPO_ROOT), ) stdout = r.stdout or "" stderr = r.stderr or "" if r.returncode != 0: return False, stdout or stderr or f"Exit code {r.returncode}", stderr return True, stdout, stderr except subprocess.TimeoutExpired: return False, "Timeout (60s)", "" except Exception as e: return False, str(e), "" finally: try: os.unlink(tmp_path) except Exception: pass class Handler(BaseHTTPRequestHandler): def do_GET(self): path = urlparse(self.path).path if path == "/" or path == "/index.html": self.serve_file(UI_DIR / "index.html", "text/html") elif path == "/examples/simple_working.json": self.serve_file(REPO_ROOT / "crates" / "cli" / "examples" / "simple_working.json", "application/json") elif path == "/examples/chiller_r410a_minimal.json": self.serve_file(REPO_ROOT / "crates" / "cli" / "examples" / "chiller_r410a_minimal.json", "application/json") else: self.send_error(404) def serve_file(self, filepath, content_type): try: with open(filepath, "rb") as f: data = f.read() except FileNotFoundError: self.send_error(404) return self.send_response(200) self.send_header("Content-Type", content_type) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data) def do_POST(self): path = urlparse(self.path).path if path != "/validate" and path != "/run": self.send_error(404) return content_len = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(content_len) try: config_str = body.decode("utf-8") json.loads(config_str) except Exception as e: self.send_json(400, {"ok": False, "error": f"Invalid JSON: {e}"}) return if path == "/validate": ok, out, err = run_cli("validate", config_str) else: ok, out, err = run_cli("run", config_str, run_output=os.path.devnull) self.send_json(200, {"ok": ok, "stdout": out, "stderr": err}) def send_json(self, status, obj): body = json.dumps(obj).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, format, *args): print(f"[{self.log_date_time_string()}] {format % args}") def main(): os.chdir(REPO_ROOT) print(f"Entropyk Circuit Builder UI") print(f" UI: http://localhost:{PORT}/") print(f" CLI: {CLI_PATH} {'(found)' if CLI_PATH.exists() else '(not found — run: cargo build --release -p entropyk-cli)'}") print(f" Root: {REPO_ROOT}") server = HTTPServer(("", PORT), Handler) try: server.serve_forever() except KeyboardInterrupt: print("\nBye.") server.server_close() if __name__ == "__main__": main()