"""Petite interface web d'administration, greffée sur le serveur FastMCP (même port). Permet d'éditer à la main les paramètres « métier » sans rebuild ni édition JSON : - la liste des ingrédients à exclure (coco & co) ; - les préférences de scoring (liked / disliked). Les écritures passent par `hellofresh.filter` (mêmes fichiers que le serveur MCP), donc le serveur prend en compte les changements au prochain appel — pas de redémarrage. Routes enregistrées via `register(mcp)` depuis server.py : GET / -> page HTML GET /api/config -> {excludes, prefs} GET /api/auth-status -> état de connexion HelloFresh (vérifié contre l'API) PUT /api/excludes -> sauve la liste d'exclusion PUT /api/prefs -> sauve liked/disliked PUT /api/allow-premium -> sauve le réglage « recettes premium autorisées » """ from __future__ import annotations import anyio from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, Response from . import auth as hf_auth from . import filter as hf_filter def _clean_list(value) -> list[str]: """Normalise une entrée en liste de chaînes non vides, dédupliquées (ordre conservé).""" if not isinstance(value, list): return [] out: list[str] = [] seen: set[str] = set() for v in value: s = str(v).strip() key = hf_filter.normalize(s) if s and key not in seen: seen.add(key) out.append(s) return out async def _config(request: Request) -> Response: return JSONResponse({ "excludes": hf_filter.load_excludes(), "prefs": hf_filter.load_prefs(), }) async def _put_excludes(request: Request) -> Response: try: body = await request.json() except Exception: return JSONResponse({"error": "JSON invalide"}, status_code=400) terms = _clean_list(body.get("exclude")) hf_filter.save_excludes(terms) return JSONResponse({"exclude": terms}) async def _put_prefs(request: Request) -> Response: try: body = await request.json() except Exception: return JSONResponse({"error": "JSON invalide"}, status_code=400) liked = _clean_list(body.get("liked")) disliked = _clean_list(body.get("disliked")) saved = hf_filter.save_prefs(liked, disliked) return JSONResponse(saved) async def _put_allow_premium(request: Request) -> Response: try: body = await request.json() except Exception: return JSONResponse({"error": "JSON invalide"}, status_code=400) value = hf_filter.save_allow_premium(bool(body.get("allow_premium"))) return JSONResponse({"allow_premium": value}) async def _auth_status(request: Request) -> Response: """État de connexion HelloFresh, vérifié par un vrai appel API (peut être lent). Déporté dans un thread : `auth.auth_status` fait des I/O réseau bloquantes (httpx sync) et ne doit pas figer la boucle asyncio du serveur. """ status = await anyio.to_thread.run_sync(hf_auth.auth_status) return JSONResponse(status) async def _index(request: Request) -> Response: return HTMLResponse(_PAGE) def register(mcp) -> None: """Enregistre les routes web sur le serveur FastMCP.""" mcp.custom_route("/", methods=["GET"], include_in_schema=False)(_index) mcp.custom_route("/api/config", methods=["GET"], include_in_schema=False)(_config) mcp.custom_route("/api/auth-status", methods=["GET"], include_in_schema=False)(_auth_status) mcp.custom_route("/api/excludes", methods=["PUT"], include_in_schema=False)(_put_excludes) mcp.custom_route("/api/prefs", methods=["PUT"], include_in_schema=False)(_put_prefs) mcp.custom_route("/api/allow-premium", methods=["PUT"], include_in_schema=False)(_put_allow_premium) _PAGE = """
Vérification…
Les recettes « premium » coûtent un supplément hors abonnement. Décoché, elles sont écartées des propositions et refusées à la confirmation (garde-fou anti-surcoût). Coché, elles sont autorisées par défaut.
Toute recette contenant l'un de ces termes (nom d'ingrédient ou allergène, sans accent ni casse) est refusée à la sélection.
Mots-clés cherchés dans le nom, le titre et les ingrédients. « Aimé » remonte la recette dans les propositions, « pas aimé » la fait descendre (sans l'exclure).