Commit graph

7 commits

Author SHA1 Message Date
82c09331dc refactor(api): regroupe test/ par sous-domaine, miroir de src/lib/
18 fichiers à plat -> recipe-matching/ (ingredient-matcher, recipe-
translation, tech-step-matcher — miroir de lib/recipe-matching/),
recipe-sources/ (json-ld-recipe, recipe-source, recipe-source-sync,
the-meal-db — miroir de lib/recipe-sources/), sources/ (sources,
sources-index — module + registration src/sources/index.ts).

Les tests par domaine API sans regroupement naturel (auth, health,
house, logger.service, planning, preferences, profile, recipe,
reference) restent à la racine de test/, un fichier par domaine — même
logique que jwt.ts/safe-profile.ts restés à la racine de lib/.

Chemins relatifs corrigés (../src/ -> ../../src/, ../test-support/ ->
../../test-support/ dans les fichiers déplacés qui appellent
resetDatabase). .mocharc.json ("test/**/*.test.ts") couvre déjà les
sous-dossiers, aucun changement de config nécessaire.

Vérifié : biome check, 303 tests API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 11:27:06 +02:00
4db8e1369f refactor(api): regroupe lib/ par sous-domaine au lieu d'un dossier à plat
9 fichiers à plat -> recipe-sources/ (recipe-source-adapter, recipe-source-
errors, recipe-source-registry) et recipe-matching/ (recipe-translation,
ingredient-matcher, tech-step-matcher). jwt.ts, safe-profile.ts et
logger.service.ts restent à la racine de lib/ (pas de sous-domaine
partagé avec les autres).

Chemins relatifs corrigés dans les fichiers déplacés (profondeur +1 vers
db/) et chez tous leurs importeurs (modules/sources, modules/recipe,
sources/*, db/recipe-source-sync.ts, 12 fichiers de test), doc mise à
jour (specs/backend-architecture.md, specs/batch-cooking-architecture.md).

Vérifié : tsc --noEmit, biome check, build complet, 303 tests API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 11:21:49 +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
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
766d48eaa5 feat(recipes): préférences de sources par foyer + distinction officielle/non-officielle
Répond à deux besoins : permettre à chaque foyer de choisir quelles
sources apparaissent dans ses onglets de recettes, et distinguer les
sources à API officielle des sources scrapées.

- RecipeSourceAdapter.official (booléen, sans défaut — chaque
  adaptateur doit le déclarer explicitement) synchronisé sur
  Source.official par syncRecipeSources.
- HouseSource : table de jointure opt-in (House <-> Source) — aucune
  ligne = source masquée. Un foyer nouvellement créé ne voit aucune
  source tant qu'il ne les active pas explicitement.
- GET /reference/sources (catalogue des sources implémentées, avec le
  flag officiel).
- GET/PATCH /house/current/sources (lecture/remplacement complet des
  sources activées par le foyer courant).
- recipe.service.ts : sourceVisibilityWhere() filtre désormais TOUS
  les onglets (perso/foyer/publique/favoris) — une recette sans
  source reste toujours visible ; une recette importée ne l'est que
  si sa source est activée pour le foyer du viewer. Un viewer sans
  foyer ne voit aucune recette sourcée.

Côté web :
- Nouvelle étape /onboarding/sources dans le wizard d'inscription,
  atteinte uniquement si un foyer vient d'être créé/rejoint (sinon on
  saute direct aux allergènes) ; s'auto-saute aussi si aucune source
  n'est encore implémentée (catalogue vide aujourd'hui).
- Nouvelle section « Sources de recettes » dans /parametres/foyer
  (masquée dans les mêmes conditions), avec sauvegarde à la volée
  (même pattern que les autres préférences hot-saved).
- SourceSelect (features/house/), grille de cases à cocher avec badge
  officiel/non-officielle, sur le même principe qu'AllergySelect.

172 tests backend passent (dont 25 nouveaux). Build et lint propres.
Vérifié manuellement en navigateur : le parcours d'onboarding saute
bien l'étape sources (catalogue vide) et affiche « 4 sur 4 » quand un
foyer a été créé ; la section paramètres reste invisible tant
qu'aucune source n'existe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 09:22:54 +02:00
dd747af70f feat(recipes): distingue les recettes déjà importées lors du browse
Ajoute markAlreadyImported(items, importedExternalIds) et le type
BrowsableRecipeItem à recipe-source-adapter.ts : quand on parcourt le
catalogue d'une source (list()), on peut désormais annoter chaque
item pour savoir s'il correspond à une recette déjà intégrée dans
notre base ou non.

Reste une fonction pure, volontairement séparée de list() : un
adaptateur ne connaît que sa source, jamais notre base — même
séparation I/O/pur que tech-step-matcher.ts. La constitution du set
d'externalId déjà importés (où/comment on persiste ce lien) est
laissée à une future couche, pas encore décidée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 07:24:44 +02:00
4a1e46b283 feat(recipes): module générique d'adaptateurs de sources de recettes
Pose les bases du pipeline d'import décrit dans
specs/batch-cooking-architecture.md (Import depuis source → Traduction
en étapes → Sauvegarde), en commençant par le premier maillon :
récupérer et parser des recettes brutes depuis une source externe,
indépendamment du site/API concerné.

- RecipeSourceAdapter<TRawDetail> (recipe-source-adapter.ts) : contrat
  générique par source — list() pour parcourir un catalogue de façon
  paginée (l'utilisateur "browse" les recettes disponibles), puis
  fetchDetail(externalId) une fois une recette sélectionnée, puis
  parse(raw) pour la transformer en ParsedRecipe. parse() est pure et
  synchrone (même séparation I/O vs pur que tech-step-matcher.ts), ce
  qui la rend testable sans réseau.
- ParsedRecipe est volontairement distinct de CreateRecipeInput : les
  ingrédients restent en texte libre (pas d'ingredientId/unitId) — la
  résolution vers nos catalogues Ingredient/Unit est un sujet séparé,
  pas encore construit.
- recipe-source-registry.ts : registre en mémoire des adaptateurs,
  identifiés par une clé stable (même convention que Diet.key/
  Unit.key/TechStep.key), distinct de la table Source (schema.prisma)
  qui documente la provenance d'une recette déjà sauvegardée.
- recipe-source-errors.ts : RecipeSourceFetchError/RecipeSourceParseError,
  vocabulaire d'erreur dédié en attendant qu'une route les traduise en
  HttpError/ErrorCode.

Pas de route HTTP, pas d'écriture en base, pas d'implémentation
concrète pour l'instant — uniquement le module générique, validé par
un adaptateur factice dans les tests. Le câblage (endpoint, sourceId,
un vrai parseur) sera une PR suivante.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 07:15:50 +02:00