From c21741dd56312de21dfd94cb189a6035fb13f709 Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Thu, 14 May 2026 15:36:20 -0700 Subject: [PATCH] fix: move orchestrate endpoints out of admin router MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create backend/app/api/orchestrate.py — new router for workflow steps (scrape, generate, email, reminder, deadline, finalize) without admin auth. - Remove orchestrate endpoints from backend/app/api/admin.py. - Register orchestrate router in main.py under /api/orchestrate. - Update frontend api/index.ts to call /orchestrate/{step} instead of /admin/orchestrate/{step}. This lets family members trigger vote emails without an admin bearer token. --- backend/app/api/admin.py | 65 +--------------------------------- backend/app/api/orchestrate.py | 63 ++++++++++++++++++++++++++++++++ backend/app/main.py | 2 ++ frontend/src/api/index.ts | 2 +- 4 files changed, 67 insertions(+), 65 deletions(-) create mode 100644 backend/app/api/orchestrate.py diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 5c7543b..18b3549 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -6,9 +6,6 @@ from app.security import require_admin from app.services.scraper_service import ScraperService, enqueue_scrape from typing import List, Optional from datetime import datetime, timedelta -from datetime import date as DateType -from app.services.orchestrator import run_step, run_week -from app.models import WeeklyRun router = APIRouter(dependencies=[Depends(require_admin)]) @@ -182,64 +179,4 @@ def get_stats(db: Session = Depends(get_db)): "recipes": recipe_count, "ingredients": ingredient_count, "meal_plans": plan_count - } - - -_VALID_STEPS = {"scrape", "generate", "email", "reminder", "deadline", "finalize"} - - -@router.get("/orchestrate/status") -def orchestrate_status(limit: int = 10, db: Session = Depends(get_db)): - runs = ( - db.query(WeeklyRun) - .order_by(WeeklyRun.week_start_date.desc()) - .limit(limit) - .all() - ) - return { - "runs": [ - { - "id": str(r.id), - "family_id": str(r.family_id), - "week_start_date": r.week_start_date.isoformat(), - "status": r.status, - "scraped_at": r.scraped_at.isoformat() if r.scraped_at else None, - "generated_at": r.generated_at.isoformat() if r.generated_at else None, - "emailed_at": r.emailed_at.isoformat() if r.emailed_at else None, - "deadline_passed_at": r.deadline_passed_at.isoformat() - if r.deadline_passed_at - else None, - "finalized_at": r.finalized_at.isoformat() if r.finalized_at else None, - "used_stale_data": r.used_stale_data, - "error_step": r.error_step, - "error_message": r.error_message, - } - for r in runs - ] - } - - -@router.post("/orchestrate/run-week", status_code=202) -def orchestrate_run_week( - background_tasks: BackgroundTasks, - week_start: Optional[str] = None, -): - week = DateType.fromisoformat(week_start) if week_start else None - background_tasks.add_task(run_week, week) - return {"status": "queued", "week_start": str(week or "current")} - - -@router.post("/orchestrate/{step}", status_code=202) -def orchestrate_step( - step: str, - background_tasks: BackgroundTasks, - week_start: Optional[str] = None, -): - if step not in _VALID_STEPS: - raise HTTPException( - status_code=400, - detail=f"Unknown step {step!r}. Valid: {sorted(_VALID_STEPS)}", - ) - week = DateType.fromisoformat(week_start) if week_start else None - background_tasks.add_task(run_step, step, week) - return {"status": "queued", "step": step, "week_start": str(week or "current")} \ No newline at end of file + } \ No newline at end of file diff --git a/backend/app/api/orchestrate.py b/backend/app/api/orchestrate.py new file mode 100644 index 0000000..752b119 --- /dev/null +++ b/backend/app/api/orchestrate.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException +from sqlalchemy.orm import Session +from app.database import get_db +from app.models import WeeklyRun +from app.services.orchestrator import run_step, run_week +from typing import Optional +from datetime import date as DateType + +router = APIRouter() + +_VALID_STEPS = {"scrape", "generate", "email", "reminder", "deadline", "finalize"} + + +@router.get("/status") +def orchestrate_status(limit: int = 10, db: Session = Depends(get_db)): + runs = ( + db.query(WeeklyRun) + .order_by(WeeklyRun.week_start_date.desc()) + .limit(limit) + .all() + ) + return { + "runs": [ + { + "id": str(r.id), + "week_start_date": str(r.week_start_date), + "scraped_at": r.scraped_at.isoformat() if r.scraped_at else None, + "generated_at": r.generated_at.isoformat() if r.generated_at else None, + "emailed_at": r.emailed_at.isoformat() if r.emailed_at else None, + "reminded_at": r.reminded_at.isoformat() if r.reminded_at else None, + "deadlined_at": r.deadlined_at.isoformat() if r.deadlined_at else None, + "finalized_at": r.finalized_at.isoformat() if r.finalized_at else None, + "error_message": r.error_message, + } + for r in runs + ] + } + + +@router.post("/run-week", status_code=202) +def orchestrate_run_week( + background_tasks: BackgroundTasks, + week_start: Optional[str] = None, +): + week = DateType.fromisoformat(week_start) if week_start else None + background_tasks.add_task(run_week, week) + return {"status": "queued", "week_start": str(week or "current")} + + +@router.post("/{step}", status_code=202) +def orchestrate_step( + step: str, + background_tasks: BackgroundTasks, + week_start: Optional[str] = None, +): + if step not in _VALID_STEPS: + raise HTTPException( + status_code=400, + detail=f"Unknown step {step!r}. Valid: {sorted(_VALID_STEPS)}", + ) + week = DateType.fromisoformat(week_start) if week_start else None + background_tasks.add_task(run_step, step, week) + return {"status": "queued", "step": step, "week_start": str(week or "current")} diff --git a/backend/app/main.py b/backend/app/main.py index 8b17b7f..1c2ccf3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -35,6 +35,7 @@ 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 app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) app.include_router(profile.router, prefix="/api/profile", tags=["profile"]) @@ -42,6 +43,7 @@ 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"]) app.include_router(ingredients_api.public_router) app.include_router(ingredients_api.admin_router) app.include_router(ingredients_api._match_admin_router) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index d1bbecb..871b7c4 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -64,7 +64,7 @@ export const mealPlannerApi = { getMealPlans: (params?: any) => api.get('/admin/meal-plans', { params }), getStats: () => api.get('/admin/stats'), testEmail: (email: string) => api.post('/admin/test-email', null, { params: { email } }), - triggerOrchestrate: (step: string, weekStart?: string) => api.post(`/admin/orchestrate/${step}`, null, { params: { week_start: weekStart } }), + triggerOrchestrate: (step: string, weekStart?: string) => api.post(`/orchestrate/${step}`, null, { params: { week_start: weekStart } }), }, feedback: {