""" WebSocket hub for real-time updates: agent status, logs, project state changes. """ import asyncio import json import logging from typing import Set from fastapi import WebSocket, WebSocketDisconnect log = logging.getLogger("foxy.ws") class ConnectionManager: """Manages active WebSocket connections and broadcasts messages.""" def __init__(self): self.active_connections: Set[WebSocket] = set() self._lock = asyncio.Lock() async def connect(self, websocket: WebSocket): await websocket.accept() async with self._lock: self.active_connections.add(websocket) log.info(f"WebSocket connected ({len(self.active_connections)} active)") async def disconnect(self, websocket: WebSocket): async with self._lock: self.active_connections.discard(websocket) log.info(f"WebSocket disconnected ({len(self.active_connections)} active)") async def broadcast(self, message_type: str, data: dict): """Broadcast a message to all connected clients.""" payload = json.dumps({"type": message_type, "data": data}) async with self._lock: dead = set() for ws in self.active_connections: try: await ws.send_text(payload) except Exception: dead.add(ws) self.active_connections -= dead async def broadcast_agent_status(self, agent_name: str, status: str, project: str = ""): await self.broadcast("agent_status", { "agent": agent_name, "status": status, "project": project, }) async def broadcast_log(self, agent: str, message: str, level: str = "info"): await self.broadcast("log", { "agent": agent, "message": message, "level": level, }) async def broadcast_project_update(self, project_id: int, status: str, name: str = ""): await self.broadcast("project_update", { "project_id": project_id, "status": status, "name": name, }) # Singleton manager = ConnectionManager()