Public Access
Two changes:
1. Sprint 12 latent-bug fix: backend/app/main.py mount order.
The pre-existing WIP backend/app/api/recipes.py:212 registers
GET /{recipe_id} (UUID-typed) under /api/recipes. Sprint 12's
recipe_search_api.router also mounts under /api/recipes. FastAPI
matches routes in registration order, so the WIP's /{recipe_id}
was catching /api/recipes/search and treating 'search' as a UUID,
returning 422. This was a latent bug: Sprint 12 hasn't been
deployed yet so the user hasn't seen the failure, but the
frontend's 'Search the web' feature would 422 on every query.
Fix: moved the recipe_search_api.router import to line 39 (with
the other api imports) and the include_router call to BEFORE
recipes_api.public_router. 3-line comment explains the why.
Verified live: GET /api/recipes/search?q=chicken+parmesan&limit=2
returns 200 with 2 hits. The WIP's GET /api/recipes/{uuid} still
works (it just no longer shadows the /search and /import routes).
2. Sprint 15 content op: scripts/seed_recipes.py (NEW, ~150 lines).
User direction (2026-06-05): 'Lets build out recipes for the
coming 4 weeks in advance. In order to do this, lets add more
recipes to the list of available ones.'
The script seeds family-friendly recipes from Spoonacular into
the local library. 50 queries (5 cuisines x 10 each: Italian,
Mexican, Asian, American, Mediterranean/Middle Eastern).
For each query: hit Spoonacular's complexSearch directly (avoids
the broken backend route and the backend's quota counter), take
the top hit, POST to the local backend's /api/recipes/import
(which does the 1-pt /information call + idempotent ingredient
upserts + Recipe insert). Idempotent: 409 from the import
endpoint is logged and skipped. 1.5 sec sleep between queries.
Stops cleanly on Spoonacular 402 (quota exhausted).
Result: 18 recipes imported today. Spoonacular's free tier is
50 pts/day (not 150 as I assumed; the _DAILY_LIMIT=140 in
recipe_search.py:48 should drop to 45 — follow-up ticket).
At 28 queries the script hit the cap. Re-running tomorrow will
yield ~30 more (after the 18 already imported count toward 50).
DB went 31 -> 49 total recipes. 19 Spoonacular + 30 manual.
LLM test (Sprint 13 endpoint, week 2026-07-06):
{picked_count: 0, filled_count: 19, failed_count: 2}
The library fill covered 19/21 slots. The LLM (kimi-k2.6:cloud)
returned 0 picks. Sprint 13 tolerance worked as designed.
No pre-existing WIP files touched (recipes.py, schemas/recipe.py,
nginx.conf unchanged). Only main.py was reordered (one-line + 3-line
comment). scripts/seed_recipes.py is a new file in the existing
scripts/ directory.
Deploy: git pull + docker compose up -d --build backend frontend.
The 18 new recipes are already in the DB. Re-run the seed script
on later days for the remaining 32 (after the cap resets).
70 lines
2.9 KiB
Python
70 lines
2.9 KiB
Python
from fastapi import FastAPI, Depends
|
|
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
|
|
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")
|
|
|
|
|
|
@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"])
|