"""AI Worker Platform - FastAPI Application Entry Point.""" import os from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, FileResponse from fastapi.staticfiles import StaticFiles from app.config import settings from app.database import init_db from app.api.v1 import api_router from app.core.exceptions import AppException # Frontend dist path (relative to backend dir) FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist" @asynccontextmanager async def lifespan(app: FastAPI): """Initialize database on startup.""" init_db() from app.database import SessionLocal from app.models.tenant import Tenant db = SessionLocal() try: existing = db.query(Tenant).filter(Tenant.slug == "default").first() if not existing: tenant = Tenant(name="Default Tenant", slug="default") db.add(tenant) db.commit() print("[Startup] Created default tenant") finally: db.close() print(f"[Startup] {settings.APP_NAME} v{settings.APP_VERSION} ready") yield app = FastAPI( title=settings.APP_NAME, version=settings.APP_VERSION, description="AI Worker Project Management Platform - MVP", lifespan=lifespan, ) # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Exception handler @app.exception_handler(AppException) async def app_exception_handler(request: Request, exc: AppException): return JSONResponse( status_code=exc.status_code, content={"detail": exc.detail}, ) # Health check @app.get("/health") def health(): return {"status": "ok", "version": settings.APP_VERSION} # API routes app.include_router(api_router) # --- Serve frontend (SPA) --- if FRONTEND_DIST.exists(): # Mount static assets (js, css, images) app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets") @app.get("/{full_path:path}") async def serve_spa(full_path: str, request: Request): """Catch-all: serve index.html for SPA routing, or static files.""" # Don't intercept API routes if full_path.startswith("api/") or full_path.startswith("health"): return JSONResponse({"detail": "Not Found"}, status_code=404) # Try to serve a real file first file_path = FRONTEND_DIST / full_path if file_path.is_file(): return FileResponse(file_path) # Fallback to index.html for SPA client-side routing index = FRONTEND_DIST / "index.html" if index.exists(): return FileResponse(index) return JSONResponse({"detail": "Frontend not built"}, status_code=404) else: @app.get("/") def root(): return { "message": "AI Worker Platform API", "version": settings.APP_VERSION, "docs": "/docs", "frontend": "Run 'npm run build' in frontend/ to enable web UI", }