""" The Catalyst — FastAPI Backend Autonomous Digital Marketing Agency powered by Meta Marketing API. """ import os import json import asyncio from datetime import datetime from typing import Set from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from dotenv import load_dotenv from api.routes_catalyst import router as catalyst_router load_dotenv() # ── App ─────────────────────────────────────────────────────────────────────── app = FastAPI( title="Velocity — Catalyst Backend", description="Meta Marketing API integration for autonomous campaign management.", version="1.0.0", ) # ── CORS ────────────────────────────────────────────────────────────────────── origins = [o.strip() for o in os.getenv("CORS_ORIGINS", "http://localhost:5173").split(",")] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ── Routers ─────────────────────────────────────────────────────────────────── app.include_router(catalyst_router, prefix="/api/catalyst", tags=["Catalyst"]) # ── WebSocket — Live Optimization Feed ──────────────────────────────────────── class ConnectionManager: """Manages active WebSocket connections for live optimization broadcasts.""" def __init__(self) -> None: self.active: Set[WebSocket] = set() async def connect(self, ws: WebSocket) -> None: await ws.accept() self.active.add(ws) def disconnect(self, ws: WebSocket) -> None: self.active.discard(ws) async def broadcast(self, payload: dict) -> None: dead: Set[WebSocket] = set() for ws in self.active: try: await ws.send_text(json.dumps(payload)) except Exception: dead.add(ws) self.active -= dead manager = ConnectionManager() @app.websocket("/ws/catalyst") async def websocket_endpoint(ws: WebSocket) -> None: """ WebSocket endpoint for streaming live Claw Agent optimization events. Clients connect from in Catalyst.tsx. """ await manager.connect(ws) try: while True: # Keep-alive: wait for any incoming ping/message data = await ws.receive_text() # Echo back as acknowledgment (clients may send heartbeat pings) await ws.send_text(json.dumps({"type": "ack", "data": data})) except WebSocketDisconnect: manager.disconnect(ws) # ── Helper: broadcast a live event (called from routes after API mutations) ─── async def broadcast_live_event( event_type: str, message: str, campaign_name: str | None = None, value: str | None = None, ) -> None: payload = { "type": event_type, "message": message, "campaignName": campaign_name, "value": value, "timestamp": datetime.utcnow().isoformat(), } await manager.broadcast(payload) # Attach broadcaster so routes can call it app.state.broadcast_live_event = broadcast_live_event # ── Health check ────────────────────────────────────────────────────────────── @app.get("/health", tags=["Health"]) async def health() -> dict: return {"status": "ok", "service": "catalyst-backend", "version": "1.0.0"}