feat: add Doris Kitchen import review workflow
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from app.services.repository import repo
|
||||
from app.services.recipe_search import recipe_search
|
||||
@@ -14,6 +13,27 @@ def usable_results(items):
|
||||
return [entry for entry in items if str(entry.get("status") or "suggested") != "rejected"]
|
||||
|
||||
|
||||
def _find_suggestion(suggestion_id: str | None):
|
||||
if not suggestion_id:
|
||||
return None
|
||||
return next((item for item in repo.list_suggestions() if str(item.get("id")) == str(suggestion_id)), None)
|
||||
|
||||
|
||||
def _find_existing_open_import_flag(recipe_url: str, kitchenowl_recipe_id: str | int | None):
|
||||
recipe_url = str(recipe_url or "").strip().lower()
|
||||
recipe_id = str(kitchenowl_recipe_id or "").strip()
|
||||
for item in repo.list_import_flags():
|
||||
if str(item.get("status") or "open") != "open":
|
||||
continue
|
||||
item_url = str(item.get("recipe_url") or "").strip().lower()
|
||||
item_recipe_id = str(item.get("kitchenowl_recipe_id") or "").strip()
|
||||
if recipe_id and item_recipe_id and recipe_id == item_recipe_id:
|
||||
return item
|
||||
if recipe_url and item_url and recipe_url == item_url:
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok", "backend": repo.backend}
|
||||
@@ -25,6 +45,7 @@ async def summary():
|
||||
suggestions = repo.list_suggestions()
|
||||
plans = repo.list_meal_plans()
|
||||
failed_searches = repo.recent_failed_searches(limit=6)
|
||||
import_flags = repo.list_import_flags()
|
||||
counts = {
|
||||
"suggested": sum(1 for item in suggestions if item.get("status") == "suggested"),
|
||||
"approved": sum(1 for item in suggestions if item.get("status") == "approved"),
|
||||
@@ -32,6 +53,7 @@ async def summary():
|
||||
"rejected": sum(1 for item in suggestions if item.get("status") == "rejected"),
|
||||
"plans": len(plans),
|
||||
"failed_searches": len(repo.list_failed_searches()),
|
||||
"open_import_flags": sum(1 for item in import_flags if str(item.get("status") or "open") == "open"),
|
||||
}
|
||||
return {
|
||||
"backend": repo.backend,
|
||||
@@ -40,6 +62,7 @@ async def summary():
|
||||
"recent_suggestions": repo.recent_suggestions(),
|
||||
"recent_plans": list(reversed(plans[-4:])),
|
||||
"recent_failed_searches": failed_searches,
|
||||
"recent_import_flags": repo.recent_import_flags(limit=6),
|
||||
}
|
||||
|
||||
|
||||
@@ -96,3 +119,87 @@ async def refresh_failed_searches():
|
||||
saved = repo.update_failed_search(str(item.get("id")), updates) or {**item, **updates}
|
||||
refreshed.append(saved)
|
||||
return {"status": "ok", "items": refreshed}
|
||||
|
||||
|
||||
@router.get("/import-flags")
|
||||
async def list_import_flags():
|
||||
return {"status": "ok", "items": repo.list_import_flags()}
|
||||
|
||||
|
||||
@router.post("/import-flags")
|
||||
async def create_import_flag(request: Request):
|
||||
payload = await request.json()
|
||||
suggestion_id = str(payload.get("suggestion_id") or "").strip() or None
|
||||
suggestion = _find_suggestion(suggestion_id)
|
||||
if suggestion_id and not suggestion:
|
||||
raise HTTPException(status_code=404, detail="Suggestion not found.")
|
||||
|
||||
recipe_title = str(payload.get("recipe_title") or (suggestion or {}).get("title") or ((suggestion or {}).get("payload") or {}).get("name") or "").strip()
|
||||
recipe_url = str(payload.get("recipe_url") or ((suggestion or {}).get("payload") or {}).get("source") or (suggestion or {}).get("url") or "").strip()
|
||||
issue_type = str(payload.get("issue_type") or "formatting").strip().lower() or "formatting"
|
||||
notes = str(payload.get("notes") or "").strip()
|
||||
kitchenowl_recipe_id = payload.get("kitchenowl_recipe_id")
|
||||
if kitchenowl_recipe_id in {None, "", 0}:
|
||||
kitchenowl_recipe_id = (((suggestion or {}).get("kitchenowl_result") or {}).get("recipe") or {}).get("id")
|
||||
|
||||
if not notes:
|
||||
raise HTTPException(status_code=400, detail="Issue notes are required.")
|
||||
if not recipe_title and not recipe_url and not kitchenowl_recipe_id:
|
||||
raise HTTPException(status_code=400, detail="Provide at least a recipe title, recipe URL, or KitchenOwl recipe id.")
|
||||
|
||||
existing = _find_existing_open_import_flag(recipe_url, kitchenowl_recipe_id)
|
||||
if existing:
|
||||
history = list(existing.get("notes_history") or [])
|
||||
history.append({"at": store.now(), "issue_type": issue_type, "notes": notes})
|
||||
updates = {
|
||||
"updated_at": store.now(),
|
||||
"last_flagged_at": store.now(),
|
||||
"issue_type": issue_type,
|
||||
"notes": notes,
|
||||
"notes_history": history[-20:],
|
||||
"flag_count": int(existing.get("flag_count") or 1) + 1,
|
||||
"recipe_title": recipe_title or existing.get("recipe_title"),
|
||||
"recipe_url": recipe_url or existing.get("recipe_url"),
|
||||
"kitchenowl_recipe_id": kitchenowl_recipe_id or existing.get("kitchenowl_recipe_id"),
|
||||
"suggestion_id": suggestion_id or existing.get("suggestion_id"),
|
||||
"kitchenowl_status": (suggestion or {}).get("kitchenowl_status") or existing.get("kitchenowl_status"),
|
||||
}
|
||||
saved = repo.update_import_flag(str(existing.get("id")), updates) or {**existing, **updates}
|
||||
return {"status": "ok", "item": saved, "deduplicated": True}
|
||||
|
||||
item = {
|
||||
"id": store.new_id("import-flag"),
|
||||
"status": "open",
|
||||
"issue_type": issue_type,
|
||||
"notes": notes,
|
||||
"notes_history": [{"at": store.now(), "issue_type": issue_type, "notes": notes}],
|
||||
"flag_count": 1,
|
||||
"created_at": store.now(),
|
||||
"updated_at": store.now(),
|
||||
"last_flagged_at": store.now(),
|
||||
"suggestion_id": suggestion_id,
|
||||
"recipe_title": recipe_title,
|
||||
"recipe_url": recipe_url,
|
||||
"kitchenowl_recipe_id": kitchenowl_recipe_id,
|
||||
"kitchenowl_status": (suggestion or {}).get("kitchenowl_status"),
|
||||
}
|
||||
saved = repo.log_import_flag(item)
|
||||
return {"status": "ok", "item": saved, "deduplicated": False}
|
||||
|
||||
|
||||
@router.post("/import-flags/{import_flag_id}/resolve")
|
||||
async def resolve_import_flag(import_flag_id: str, request: Request):
|
||||
payload = await request.json()
|
||||
current = next((entry for entry in repo.list_import_flags() if str(entry.get("id")) == str(import_flag_id)), None)
|
||||
if not current:
|
||||
raise HTTPException(status_code=404, detail="Import flag not found.")
|
||||
resolution_notes = str(payload.get("resolution_notes") or "").strip()
|
||||
updates = {
|
||||
"status": "resolved",
|
||||
"updated_at": store.now(),
|
||||
"resolved_at": store.now(),
|
||||
}
|
||||
if resolution_notes:
|
||||
updates["resolution_notes"] = resolution_notes
|
||||
saved = repo.update_import_flag(import_flag_id, updates) or {**current, **updates}
|
||||
return {"status": "ok", "item": saved}
|
||||
|
||||
Reference in New Issue
Block a user