Commit graph

5 commits

Author SHA1 Message Date
9083770d57 fix(recipes): corrige plusieurs bugs d'import TheMealDB
- Les instructions TheMealDB numérotées sur leur propre ligne ("1\n\ntexte...\n\n2\n\ntexte...") créaient des étapes parasites ne contenant qu'un chiffre — filtrées désormais (#52).
- Un ingrédient compté sans mot d'unité dans le texte source (ex. "4 Egg Yolks") laissait l'import bloqué sur "Importer" indéfiniment, sans indication visuelle de la ligne en cause — matchUnit retombe maintenant sur l'unité générique "piece" quand une quantité a été extraite, et RecipeImportForm/RecipeFormPage surlignent désormais toute ligne dont l'unité manque, avec un message explicite (#53).
- Ajout de INGREDIENT_LABEL_SYNONYMS_EN pour reconnaître des formulations alternatives fréquentes chez les sources anglophones ("vanilla pod" en plus de "vanilla bean") sans élargir INGREDIENT_LABELS_EN à un tableau pour ses ~550 entrées (#54).
- Effet de bord découvert en vérifiant #53 de bout en bout : deux lignes source résolues vers le même ingrédient catalogue (ex. "Egg Yolks"/"Eggs" -> "Œuf") faisaient planter la création en 500 (contrainte unique recipe_id+ingredient_id) au lieu d'un 400 propre. createRecipeSchema rejette maintenant les ingredientId en double, et le formulaire d'import surligne les doublons avant même de soumettre.

Vérifié de bout en bout dans le navigateur (import réel de la recette "Flan" depuis TheMealDB, jusqu'au planning) en plus des tests ajoutés.

Closes #52, #53, #54

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 08:14:37 +02:00
44ef5e071f feat(recipes): parcourir et prévisualiser les sources externes (étape 1/4)
Première étape du chantier "onglet Sources" (parcourir toutes les
recettes externes des sources activées par le foyer, importées ou non,
et déclencher leur import à l'ajout au planning) — celle-ci pose les
endpoints backend de lecture seule, rien n'est encore sauvegardé.

- RecipeSourceAdapter gagne `locale` (theMealDbAdapter: "en") — nécessaire
  pour que translateRecipe/matchTechStepSpans sachent contre quel jeu de
  TechStepMapping/labels d'ingrédients traduire une source donnée.
- findImportedExternalIds (recipe-source-sync.ts) devient
  findImportedRecipeIds : renvoie une Map<externalId, recipeId> au lieu
  d'un simple Set — son premier vrai appelant (le parcours) a besoin de
  l'id réel pour naviguer directement vers la recette déjà importée, pas
  seulement savoir qu'elle l'est.
- Nouveau module apps/api/src/modules/sources/ :
  - GET /sources/:sourceKey/browse — appelle list() de l'adaptateur,
    flague chaque item alreadyImported/recipeId. Restreint aux sources
    activées par le foyer courant (HouseSource) ; 404 SOURCE_NOT_FOUND
    sinon, même si la source existe (même posture que la visibilité des
    recettes : "pas trouvée" plutôt que "pas autorisée").
  - GET /sources/:sourceKey/preview/:externalId — fetchDetail + parse +
    résolution complète (translateRecipeIngredients, matchTechStepSpans
    avec spans réels) contre la locale de la source, sans rien
    sauvegarder. Ingrédients non résolus → null plutôt qu'une erreur.
- Nouveaux types partagés (packages/shared/src/types/sources.ts) :
  BrowsableSourceItemView, RecipeImportDraftView (+ Draft*View).

Vérifié en conditions réelles contre TheMealDB (recette "Chicken Handi") :
ingrédients résolus avec la bonne quantité/unité (1.2 kg de poulet, 8
gousses d'ail...), non-résolus corrects (huile végétale, piment vert),
et chaque étape avec ses techniques détectées et leurs spans exacts
(cook/fry/plate/setAside sur la même phrase, etc.).

Tests : 276 passing (+8 nouveaux, sources.test.ts). Étape suivante (2/4) :
l'UI de parcours (onglet Sources) — voir le plan de session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 15:51:16 +02:00
73206c442b fix(recipes): la source générique JSON-LD n'apparaît plus comme source
jsonLdRecipeAdapter était enregistré (et donc synchronisé comme Source
sélectionnable par un foyer) au même titre que theMealDbAdapter — mais
c'est une structure générique de parsing schema.org destinée à être
déclinée par site web scrappé, pas une source qu'on peut raisonnablement
« activer » ou « faire confiance » en tant que telle (aucun catalogue à
parcourir : list() renvoie toujours vide).

registerAllRecipeSources() ne l'enregistre donc plus — elle reste
utilisable directement (un futur adapter par site l'utiliserait en
interne, ou un futur flux « importer depuis une URL » l'appellerait
directement), simplement plus comme Source autonome du registre.

Corrige aussi le mock Cypress qui prétendait « mirrorer ce qui est
vraiment seedé » avec cette même source — remplacé par un second exemple
clairement illustratif (Marmiton), qui ne correspond à aucun adapter réel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 15:10:51 +02:00
8ab652206d feat(recipes): source générique JSON-LD (schema.org/Recipe)
Deuxième adaptateur concret, cette fois générique plutôt que lié à un
site précis : la plupart des sites de recettes embarquent des
données structurées JSON-LD (schema.org/Recipe) pour le SEO/Google
Rich Results — un seul adaptateur peut donc couvrir une grande partie
des sites, sans scraper le DOM site par site.

- apps/api/src/sources/json-ld-recipe.ts : official: false (on lit du
  HTML arbitraire, pas une API dédiée maintenue par l'éditeur), pas de
  catalogue à parcourir (list() renvoie toujours vide) — fetchDetail()
  prend directement une URL comme externalId, prête pour un futur
  flux "importer depuis une URL".
- Extraction JSON-LD par regex (pas de nouvelle dépendance — un tag
  <script> ne contient jamais de HTML imbriqué, donc pas besoin d'un
  vrai parseur DOM), tolérante aux blocs multiples et aux JSON
  malformés (ignorés plutôt que de faire échouer toute la page).
  Gère les variantes réelles de schema.org : @type en tableau, @graph,
  recipeInstructions en string/HowToStep[]/HowToSection imbriquées,
  image en string/ImageObject/tableau, recipeYield en nombre/texte/
  tableau.

Vérifié contre une vraie page (bbcgoodfood.com, HTML téléchargé +
fetch live) : nom, description, image, portions, 8 ingrédients et 2
étapes correctement extraits de bout en bout.

26 nouveaux tests (tous avec fetch stubbé, aucun appel réseau réel
dans la suite automatisée). 221 tests passent au total. Build et
lint propres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 12:17:49 +02:00
b725ec1016 feat(recipes): première source concrète (TheMealDB) + icône de source
Marmiton s'est avéré inaccessible pour du scraping (bloqué même via
WebFetch, signe de protection anti-bot) — TheMealDB (themealdb.com)
est une vraie API JSON publique et gratuite, sans scraping, testée
en conditions réelles (list → fetchDetail → parse fonctionnent
bout en bout contre l'API live).

- Source.iconUrl (nullable) + RecipeSourceAdapter.iconUrl (requis,
  même convention que `official`) synchronisé par syncRecipeSources.
- apps/api/src/sources/the-meal-db.ts : premier RecipeSourceAdapter
  réel — official: true (API officielle, pas de scraping), utilise
  fetch natif (aucune dépendance ajoutée). list() fait une recherche
  (pas de vrai "browse" côté TheMealDB, mais une requête vide renvoie
  un échantillon de secours) ; parse() éclate les instructions en
  étapes par ligne et ignore les emplacements d'ingrédients vides.
- apps/api/src/sources/index.ts : registerAllRecipeSources(), appelé
  par server.ts (process réel) et prisma/seed.ts — délibérément PAS
  importé par app.ts, pour ne jamais dépendre de l'ordre des tests.
- SourceSelect (web) affiche désormais le logo de la source à côté
  de son nom.

Vérifié en conditions réelles : seed → table sources peuplée avec le
vrai logo TheMealDB ; endpoint /reference/sources sur serveur réel ;
parcours navigateur complet (onboarding → étape sources visible avec
icône chargée → activation → paramètres foyer reflète le choix).

186 tests passent (16 nouveaux, dont le moteur TheMealDB testé avec
un stub de fetch — aucun appel réseau réel dans la suite automatisée).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 11:16:13 +02:00