from fastapi import APIRouter, HTTPException, Request from app.services.repository import repo from app.services.recipe_search import recipe_search from app.services.store import JsonStore router = APIRouter(prefix="/api", tags=["dashboard"]) store = JsonStore() 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} @router.get("/dashboard/summary") async def summary(): profile = repo.get_profile() 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"), "saved": sum(1 for item in suggestions if item.get("status") == "saved"), "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, "profile": profile, "counts": counts, "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), } @router.get("/failed-searches") async def failed_searches(): return {"status": "ok", "items": repo.list_failed_searches()} @router.get("/failed-searches/{failed_search_id}/results") async def failed_search_results(failed_search_id: str): item = next((entry for entry in repo.list_failed_searches() if str(entry.get("id")) == str(failed_search_id)), None) if not item: raise HTTPException(status_code=404, detail="Failed search not found.") query = str(item.get("query") or "").strip() if not query: raise HTTPException(status_code=400, detail="Failed search query is empty.") all_results = recipe_search.search(query, limit=10) results = usable_results(all_results) updates = { "last_checked_at": store.now(), "updated_at": store.now(), "result_count": len(results), "sample_titles": [str(entry.get("title") or "") for entry in results[:4]], } if results: updates["status"] = "resolved-with-recipes" updates["resolved_at"] = str(item.get("resolved_at") or store.now()) saved = repo.update_failed_search(str(item.get("id")), updates) or {**item, **updates} return {"status": "ok", "item": saved, "results": results} @router.post("/failed-searches/refresh") async def refresh_failed_searches(): items = repo.list_failed_searches() refreshed = [] for item in items: query = str(item.get("query") or "").strip() status = str(item.get("status") or "open") if not query or status != "open": refreshed.append(item) continue all_results = recipe_search.search(query, limit=10) results = usable_results(all_results) preview_titles = [str(entry.get("title") or "") for entry in results[:4] if str(entry.get("title") or "").strip()] result_count = len(results) updates = { "last_checked_at": store.now(), "result_count": result_count, "sample_titles": preview_titles, } if result_count > 0: updates["status"] = "resolved-with-recipes" updates["resolved_at"] = store.now() 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}