import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from app.config import settings from app.routers import analytics, auth, distractions, proactive, sessions, steps, tasks from app.services.db import close_pool, get_pool logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): await get_pool() print(f"APNs config → KEY_ID={settings.APNS_KEY_ID or '(empty)'!r} " f"TEAM_ID={settings.APNS_TEAM_ID or '(empty)'!r} " f"P8_PATH={settings.APNS_P8_PATH or '(empty)'!r} " f"SANDBOX={settings.APNS_SANDBOX} " f"BUNDLE={settings.APPLE_BUNDLE_ID}") yield await close_pool() app = FastAPI( title="LockInBro API", version="1.0.0", root_path="/api/v1", lifespan=lifespan, ) @app.exception_handler(Exception) async def llm_error_handler(request: Request, exc: Exception): # Surface LLM provider errors as 502 instead of 500 exc_name = type(exc).__name__ if "ClientError" in exc_name or "APIError" in exc_name or "APIConnectionError" in exc_name: return JSONResponse(status_code=502, content={"detail": f"LLM provider error: {exc}"}) if isinstance(exc, RuntimeError) and "No LLM API key" in str(exc): return JSONResponse(status_code=503, content={"detail": str(exc)}) raise exc @app.middleware("http") async def log_client_info(request: Request, call_next): real_ip = request.headers.get("cf-connecting-ip", request.headers.get("x-forwarded-for", "unknown")) ua = request.headers.get("user-agent", "unknown") response = await call_next(request) if request.url.path != "/api/v1/health": print(f"[REQ] {request.method} {request.url.path} → {response.status_code} | ip={real_ip} ua={ua[:80]}") return response app.include_router(auth.router) app.include_router(tasks.router) app.include_router(steps.router) app.include_router(sessions.router) app.include_router(distractions.router) app.include_router(proactive.router) app.include_router(analytics.router) @app.get("/health") async def health(): return {"status": "ok"}