# ── SQLite Patch (must be first!) ───────────────────────────── from app.sqlite_patch import apply_patch apply_patch() # ────────────────────────────────────────────────────────────── from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from contextlib import asynccontextmanager import asyncio from app.config import get_settings from app.database import IS_SQLITE, init_db from app.api.v1.router import api_router from app.flows.self_improvement_flow import self_improvement_flow from app.middleware.internal_api import InternalApiTokenMiddleware settings = get_settings() def _cors_origins() -> list[str]: base = [ settings.FRONTEND_URL, "http://localhost:3000", "http://localhost:5173", "https://dealix.sa", "https://app.dealix.sa", ] extra = [x.strip() for x in (settings.CORS_EXTRA_ORIGINS or "").split(",") if x.strip()] seen: set[str] = set() out: list[str] = [] for o in base + extra: if o not in seen: seen.add(o) out.append(o) return out def _openapi_urls() -> tuple[str | None, str | None, str | None]: if not settings.EXPOSE_OPENAPI: return None, None, None return "/api/docs", "/api/redoc", "/api/openapi.json" @asynccontextmanager async def lifespan(app: FastAPI): """Application startup and shutdown events.""" stop_event = asyncio.Event() async def _self_improvement_worker(): await asyncio.sleep(30) while not stop_event.is_set(): try: self_improvement_flow.run( tenant_id="system_tenant", input_state={ "signals": [], "bottlenecks": [], "experiments": [{"name": "always-on-ab-loop"}], "ab_results": {}, "governance_passed": True, "promoted": True, }, ) except Exception: pass await asyncio.sleep(max(60, settings.SELF_IMPROVEMENT_INTERVAL_SECONDS)) worker_task = asyncio.create_task(_self_improvement_worker()) # Startup print(f"[startup] {settings.APP_NAME} starting...") print(f" Environment: {settings.ENVIRONMENT}") print(f" LLM Primary: {settings.LLM_PRIMARY_PROVIDER}") print(f" LLM Fallback: {settings.LLM_FALLBACK_PROVIDER}") try: from app.services.posthog_client import get_posthog ph = get_posthog() print(f" PostHog: {'enabled' if getattr(ph, '_enabled', False) else 'disabled (no API key)'}") except Exception as e: print(f" PostHog: init failed ({e})") try: from app.services.dlq import dlq # noqa: F841 print(" DLQ: initialized") except Exception as e: print(f" DLQ: init failed ({e})") await init_db() yield # Shutdown stop_event.set() worker_task.cancel() print(f"[shutdown] {settings.APP_NAME} shutting down...") _docs, _redoc, _openapi = _openapi_urls() app = FastAPI( title=f"{settings.APP_NAME} API", description=( "AI-powered B2B Revenue Operating System for the Saudi market. " "Lead management, AI agents, affiliate system, meeting automation, " "deal pipeline, and commission processing — all driven by 18 specialized AI agents." ), version="2.0.0", docs_url=_docs, redoc_url=_redoc, openapi_url=_openapi, lifespan=lifespan, ) app.add_middleware(InternalApiTokenMiddleware) # CORS runs outermost (added last) so browser preflight is handled first app.add_middleware( CORSMiddleware, allow_origins=_cors_origins(), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/health") async def root_health(): """Root-level health check for Railway/load balancer healthchecks.""" return {"status": "ok"} # API Routes app.include_router(api_router, prefix="/api/v1") # ── Static marketing assets (browse + direct download) ───────── def _resolve_salesflow_root() -> Path: if settings.MARKETING_STATIC_ROOT.strip(): return Path(settings.MARKETING_STATIC_ROOT).resolve() # backend/app/main.py -> parents: app, backend, salesflow-saas return Path(__file__).resolve().parent.parent.parent _salesflow_root = _resolve_salesflow_root() _marketing_dir = _salesflow_root / "sales_assets" _presentations_dir = _salesflow_root / "presentations" / "dealix-2026-sectors" if settings.MARKETING_STATIC_ENABLED: if _marketing_dir.is_dir(): app.mount( "/dealix-marketing", StaticFiles(directory=str(_marketing_dir), html=True), name="dealix_marketing", ) print(" Marketing static: /dealix-marketing/ (index, ZIP, use cases)") if _presentations_dir.is_dir(): app.mount( "/dealix-presentations", StaticFiles(directory=str(_presentations_dir), html=True), name="dealix_presentations", ) print(" Marketing static: /dealix-presentations/ (sector HTML)")