From 14a0ed1ced25aa36236c525ba0e02eacf213ae13 Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Thu, 7 May 2026 06:44:41 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20admin=20orchestrate=20endpoints=20?= =?UTF-8?q?=E2=80=94=20run-week,=20per-step,=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- backend/app/api/admin.py | 65 +++++++++++++++++++++++++++++- backend/tests/test_orchestrator.py | 49 ++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 0a55c4b..3e759cb 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -6,6 +6,9 @@ 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)]) @@ -161,4 +164,64 @@ def get_stats(db: Session = Depends(get_db)): "recipes": recipe_count, "ingredients": ingredient_count, "meal_plans": plan_count - } \ No newline at end of file + } + + +_VALID_STEPS = {"scrape", "generate", "email", "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 diff --git a/backend/tests/test_orchestrator.py b/backend/tests/test_orchestrator.py index c0ced39..9689765 100644 --- a/backend/tests/test_orchestrator.py +++ b/backend/tests/test_orchestrator.py @@ -474,3 +474,52 @@ def test_run_week_calls_all_steps(monkeypatch): from app.services.orchestrator.runner import run_week run_week(WEEK) assert called == ["scrape", "generate", "email", "deadline", "finalize"] + + +# --------------------------------------------------------------------------- +# Admin endpoint tests +# --------------------------------------------------------------------------- + +@pytest.fixture() +def admin_client(db): + from fastapi.testclient import TestClient + from app.main import app + from app.database import get_db + + app.dependency_overrides[get_db] = lambda: db + client = TestClient(app) + yield client + app.dependency_overrides.clear() + + +ADMIN_HEADERS = {"Authorization": "Bearer test-admin-token"} + + +def test_orchestrate_status_returns_list(admin_client, family, monkeypatch): + from app.config import settings + monkeypatch.setattr(settings, "ADMIN_TOKEN", "test-admin-token") + resp = admin_client.get("/api/admin/orchestrate/status", headers=ADMIN_HEADERS) + assert resp.status_code == 200 + assert "runs" in resp.json() + + +def test_orchestrate_step_invalid_returns_400(admin_client, monkeypatch): + from app.config import settings + monkeypatch.setattr(settings, "ADMIN_TOKEN", "test-admin-token") + resp = admin_client.post( + "/api/admin/orchestrate/bogus", headers=ADMIN_HEADERS + ) + assert resp.status_code == 400 + + +def test_orchestrate_run_week_returns_202(admin_client, monkeypatch): + from app.config import settings + monkeypatch.setattr(settings, "ADMIN_TOKEN", "test-admin-token") + monkeypatch.setattr( + "app.api.admin.run_week", + lambda week=None: None, + ) + resp = admin_client.post( + "/api/admin/orchestrate/run-week", headers=ADMIN_HEADERS + ) + assert resp.status_code == 202