from fastapi import FastAPI, Depends, HTTPException, Request from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session from sqlalchemy import text from app.database import get_db from app.config import settings from app.security import require_session import logging logging.basicConfig(level=settings.LOG_LEVEL) logger = logging.getLogger(__name__) app = FastAPI( title="MealPlanner", description="Self-hosted meal planning system", version="0.1.0", ) # Serve generated recipe images from local filesystem. app.mount("/static", StaticFiles(directory="static"), name="static") def _requires_session(path: str, method: str) -> bool: if method == "OPTIONS" or not path.startswith("/api/"): return False if path.startswith("/api/auth/") or path.startswith("/api/admin/"): return False # Email approval links carry their own signed, single-use token. if path.startswith("/api/meals/vote/"): return False return True @app.middleware("http") async def require_family_session(request: Request, call_next): if _requires_session(request.url.path, request.method): try: require_session(request) except HTTPException as exc: return JSONResponse( status_code=exc.status_code, content={"detail": exc.detail}, headers=getattr(exc, "headers", None), ) return await call_next(request) @app.get("/health") def health_check(db: Session = Depends(get_db)): return {"status": "ok"} @app.get("/health/db") def health_check_db(db: Session = Depends(get_db)): try: db.execute(text("SELECT 1")) return {"status": "ok", "database": "connected"} except Exception as e: return {"status": "error", "database": "disconnected", "error": str(e)} from app.api import profile, meals, shopping_list, pantry, admin, auth from app.api import ingredients as ingredients_api from app.api import recipe_search as recipe_search_api from app.api import recipes as recipes_api from app.api import never_suggest as never_suggest_api from app.api import meal_plans as meal_plans_api from app.api import feedback as feedback_api from app.api import orchestrate as orchestrate_api from app.api import llm_plan as llm_plan_api app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) app.include_router(profile.router, prefix="/api/profile", tags=["profile"]) app.include_router(meals.router, prefix="/api/meals", tags=["meals"]) app.include_router(shopping_list.router, prefix="/api/shopping-list", tags=["shopping-list"]) app.include_router(pantry.router, prefix="/api/pantry", tags=["pantry"]) app.include_router(admin.router, prefix="/api/admin", tags=["admin"]) app.include_router(orchestrate_api.router, prefix="/api/orchestrate", tags=["orchestrate"]) # Sprint 12: external recipe search (Spoonacular) + import. # Mounted BEFORE recipes_api.public_router so /search and /import are # not shadowed by the WIP's GET /{recipe_id} (which would 422 on a # non-UUID "search" path segment). See sprint15-verification.md. app.include_router(recipe_search_api.router, prefix="/api/recipes", tags=["recipes"]) app.include_router(ingredients_api.public_router) app.include_router(ingredients_api.admin_router) app.include_router(ingredients_api._match_admin_router) app.include_router(recipes_api.public_router) app.include_router(recipes_api.admin_router) app.include_router(never_suggest_api.public_router) app.include_router(never_suggest_api.admin_router) app.include_router(meal_plans_api.admin_router) app.include_router(meal_plans_api.public_router) app.include_router(feedback_api.router, prefix="/api/feedback", tags=["feedback"]) # Sprint 13: F9-lite — free-text meal-plan synthesis via Ollama Cloud. app.include_router(llm_plan_api.router, prefix="/api/llm", tags=["llm"])