fix: move orchestrate endpoints out of admin router

- 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.
This commit is contained in:
2026-05-14 15:36:20 -07:00
parent f61515beff
commit c21741dd56
4 changed files with 67 additions and 65 deletions
+1 -64
View File
@@ -6,9 +6,6 @@ from app.security import require_admin
from app.services.scraper_service import ScraperService, enqueue_scrape from app.services.scraper_service import ScraperService, enqueue_scrape
from typing import List, Optional from typing import List, Optional
from datetime import datetime, timedelta 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)]) router = APIRouter(dependencies=[Depends(require_admin)])
@@ -182,64 +179,4 @@ def get_stats(db: Session = Depends(get_db)):
"recipes": recipe_count, "recipes": recipe_count,
"ingredients": ingredient_count, "ingredients": ingredient_count,
"meal_plans": plan_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")}
+63
View File
@@ -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")}
+2
View File
@@ -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 never_suggest as never_suggest_api
from app.api import meal_plans as meal_plans_api from app.api import meal_plans as meal_plans_api
from app.api import feedback as feedback_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(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(profile.router, prefix="/api/profile", tags=["profile"]) 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(shopping_list.router, prefix="/api/shopping-list", tags=["shopping-list"])
app.include_router(pantry.router, prefix="/api/pantry", tags=["pantry"]) app.include_router(pantry.router, prefix="/api/pantry", tags=["pantry"])
app.include_router(admin.router, prefix="/api/admin", tags=["admin"]) 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.public_router)
app.include_router(ingredients_api.admin_router) app.include_router(ingredients_api.admin_router)
app.include_router(ingredients_api._match_admin_router) app.include_router(ingredients_api._match_admin_router)
+1 -1
View File
@@ -64,7 +64,7 @@ export const mealPlannerApi = {
getMealPlans: (params?: any) => api.get('/admin/meal-plans', { params }), getMealPlans: (params?: any) => api.get('/admin/meal-plans', { params }),
getStats: () => api.get('/admin/stats'), getStats: () => api.get('/admin/stats'),
testEmail: (email: string) => api.post('/admin/test-email', null, { params: { email } }), 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: { feedback: {