"""Front-facing API the website talks to. The browser: - opens a WebSocket to /events to receive live chat events from every profile (each event is tagged with the originating profile name); - uses REST to spawn/stop profiles and to send commands to cli profiles. This process sits between the website and the SimpleX binaries; it is the only thing that touches the binaries. Run: uvicorn supervisor.server:app """ import asyncio import contextlib from fastapi import FastAPI, WebSocket, WebSocketDisconnect from .supervisor import Supervisor app = FastAPI(title="SimpleX Orchestrate") _browser_clients: set[WebSocket] = set() async def _broadcast(name: str, event: dict) -> None: dead = [] for ws in _browser_clients: try: await ws.send_json({"profile": name, "event": event}) except Exception: dead.append(ws) for ws in dead: _browser_clients.discard(ws) sup = Supervisor(on_event=_broadcast) @app.on_event("shutdown") async def _shutdown() -> None: await sup.stop_all() # ── Control (REST) ────────────────────────────────────────────────────────────── @app.get("/profiles") async def profiles() -> dict: return {"profiles": sup.list()} @app.post("/profiles/{name}/start-cli") async def start_cli(name: str) -> dict: await sup.start_cli(name) return {"ok": True} @app.post("/profiles/{name}/start-directory") async def start_directory(name: str, body: dict) -> dict: await sup.start_directory(name, body["super_users"], body["web_folder"]) return {"ok": True} @app.post("/profiles/{name}/start-broadcast") async def start_broadcast(name: str, body: dict) -> dict: await sup.start_broadcast(name, body["display_name"], body["publishers"]) return {"ok": True} @app.post("/profiles/{name}/cmd") async def cmd(name: str, body: dict) -> dict: """Send a raw chat command string to a cli profile (e.g. '/_send @1 text hi').""" return {"resp": await sup.send(name, body["cmd"])} @app.post("/profiles/{name}/stop") async def stop(name: str) -> dict: await sup.stop(name) return {"ok": True} # ── Live events (WebSocket) ────────────────────────────────────────────────────── @app.websocket("/events") async def events(ws: WebSocket) -> None: await ws.accept() _browser_clients.add(ws) try: while True: await ws.receive_text() # keep the socket open; ignore inbound except WebSocketDisconnect: pass finally: _browser_clients.discard(ws)