Commit graph

62 commits

Author SHA1 Message Date
53d415fddb feat(tech-steps): fiabilise la detection des tech steps (corpus + LLM + corrections utilisateur)
Une seule feature livree en une seule PR, en 5 phases :

- Phase 1 : enrichit le corpus NLP (tech-step-training-data.ts) et ajoute
  un harness d'evaluation (precision/rappel/F1) avec un jeu de test etiquete
  - la premiere metrique objective de qualite pour ce classifieur.
- Phase 2 : schema Prisma (StepTechStepCorrection, TechStepTrainingSuggestion)
  + endpoints utilisateur (POST/GET corrections, ouverts a tout viewer, pas
  seulement l'auteur) + endpoints internes /internal/tech-steps/* proteges
  par secret partage (requireInternalWorker).
- Phase 3 : UI de highlight/correction cote web (selection de texte ->
  association a une technique, ou clic sur un highlight existant pour le
  corriger/supprimer) - verifiee via Cypress (component + e2e, en Chrome
  reel).
- Phase 4 : worker LLM autonome (services/tech-step-llm-worker, hors du
  monorepo pnpm comme experiments/llm-tech-step-poc) qui audite les clauses
  a faible confiance et transforme les corrections utilisateur en
  suggestions d'entrainement, sans jamais toucher le chemin interactif.
- Phase 5 : script retrain-tech-steps.ts (gate de regression F1 + backfill)
  et list-pending-training-suggestions.ts pour la revue humaine avant
  application au corpus.

Verification effectuee cette session : tsc/biome sur l'ensemble du repo,
build complet (pnpm build), suite Cypress complete (component 39/39, e2e
75/76 - le seul echec est preexistant et sans rapport, cote
recipe-form.feature/ingredient-picker), tests unitaires du worker (6/6) et
son install/typecheck reels contre node-llama-cpp. Les tests Mocha
d'apps/api (Phases 1 et 2) n'ont pas pu etre executes dans cette session
(pas de Postgres local disponible) - a lancer avant merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 09:48:02 +02:00
kyuno053
0e0fd81563
feat(api): remplace la détection des tech steps par un pipeline NLP (node-nlp) (#63)
* feat(api): remplace la détection des tech steps par un pipeline NLP (node-nlp)

Le matching par regex ne généralisait jamais au-delà de son propre
vocabulaire — une étape décrivant la fonte du beurre comme "jusqu'à ce
que le beurre ait disparu dans la poêle" ne contient aucun verbe sur
lequel une regex pourrait s'ancrer, alors que le sens est sans
ambiguïté.

Nouveau pipeline en 3 étapes (TechStepClassifierService, node-nlp
4.27.0 — la 5.x est encore alpha, non retenue) :
1. NER (entités enum) trouve les mentions candidates + leur position
   exacte, à partir de listes de synonymes (tech-step-training-data.ts)
   plutôt que de regex écrites à la main. ner.threshold: 1 (exact,
   après normalisation) — le défaut à 0.8 faisait matcher "faire" (verbe
   auxiliaire omniprésent) contre "frire" par pure proximité de chaîne.
2. La description est découpée en clauses autour de ces candidats
   (splitIntoClauses, pure/testable sans modèle).
3. Le NlpManager classe chaque clause individuellement, entraîné sur
   des phrases qui n'emploient jamais le verbe de la technique — c'est
   ce qui apporte la compréhension du sens. En dessous de
   CONFIDENCE_THRESHOLD (0.65, ajusté empiriquement), retombe sur la
   technique impliquée par l'ancre NER plutôt que d'abandonner un match
   clairement ancré sur un mot-clé.

TechStepMapping (table de regex par technique/locale) supprimée —
migration 20260821130000_drop_tech_step_mapping — plus aucune table
n'est interrogée à l'exécution, les données de matching vivent en code.
TECH_STEPS (reference-seed-data.ts) simplifié en simple liste de uid,
les mappings ayant disparu.

Deux pièges trouvés en construisant ce pipeline, corrigés à la source :
- db/prisma.ts construisait PrismaClient sans importer config/env.ts —
  un run de test isolé pouvait faire gagner la course au .env interne
  de Prisma (dev) contre .env.test. Fixé en important config/env.js en
  tout premier, pour effet de bord.
- NlpManager a autoSave/autoLoad: true par défaut — persiste le modèle
  entraîné dans model.nlp et le recharge au lieu de ré-entraîner au
  prochain démarrage. Les deux désactivés explicitement (sinon un
  modèle obsolète masquerait silencieusement toute mise à jour du
  corpus/seuil) ; model.nlp ajouté au .gitignore en garde-fou.

apps/api/src/db/prisma.ts, recipe.service.ts, sources.service.ts et
recipe-translation.ts adaptés à la matching async (le classifieur
entraîné remplace le couple loadTechStepMappingRules+matchTechStepSpans
synchrone) ; server.ts appelle techStepClassifier.warmUp() avant
d'accepter du trafic (le tout premier appel réel à
NlpManager.process() charge les ressources par langue de node-nlp,
plusieurs secondes).

Vérifié : tsc --noEmit, biome check (0 erreur), build complet des 6
packages, 308 tests API (dont un test-support/reset-db.ts corrigé —
référençait encore tech_step_mapping dans son TRUNCATE).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(api): ajoute la délimitation de contexte aux tech steps et étoffe le vocabulaire du classifieur

Deux évolutions du pipeline NLP de détection des tech steps (PR #63) :

1. Délimitation de contexte — en plus du mot-clé qui déclenche un match
   (start/end), chaque TechStepMatch porte maintenant contextStart/
   contextEnd : la clause complète autour du mot-clé (ex : "poêle chaude"
   comme mot-clé, "Dans une poêle chaude" comme contexte). Persisté sur
   StepTechStep (colonnes nullables, migration dédiée), exposé via
   StepTechStepView, et rendu côté web avec un style plus discret que le
   mot-clé (StepDescription.tsx, .step-tech-step-context). splitIntoClauses
   coupe désormais sur l'espace le plus proche du milieu de l'écart entre
   deux candidats plutôt que sur le milieu brut, pour ne jamais couper un
   mot en deux (findGapSplitPoint).

2. Vocabulaire du classifieur — synonymes et locutions supplémentaires par
   technique (FR/EN) pour fiabiliser la détection sur des formulations que
   le corpus initial ne couvrait pas. Plusieurs bugs de fond trouvés et
   corrigés en cours de route, tous confirmés par la suite de tests
   complète (309 tests) :
   - un synonyme multi-mots qui est un préfixe-mot d'un synonyme plus court
     déjà enregistré pour la même technique fait matcher les deux comme
     candidats NER distincts et chevauchants, corrompant le découpage en
     clauses (parfois jusqu'à une mauvaise classification) — retiré
     partout où ce motif a été repéré (cook, fry, deglaze, simmer, boil,
     roast, chop, mince, marinate, preheat, bake, plate, coat) ;
   - "poêlé"/"poêlée" comme synonymes de panFry sont réduits à la même
     racine que le nom "poêle" par le stemmer français de node-nlp,
     provoquant un faux positif sur toute mention nue de "poêle" (dont
     celle de preheat) — retiré ;
   - "Fouetter les blancs en neige" était mal classé en foldIn (la phrase
     d'entraînement de foldIn partage la même locution) — corrigé en
     ajoutant des phrases d'entraînement dédiées à whisk ;
   - "Émincer les tomates" est passé sous le seuil de confiance vers melt
     après l'ajout du nouveau vocabulaire ailleurs dans le corpus — corrigé
     en élargissant les phrases d'entraînement de mince à un autre légume.

Le test unitaire de splitIntoClauses avec un point de coupure obsolète
(pré-datant findGapSplitPoint) est aussi corrigé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(api): corrige la découpe des clauses et le seuil de confiance du classifieur de tech steps

Trouvé en examinant des vraies recettes déjà en base après le dernier
étoffement du vocabulaire : plusieurs étapes bien réelles se faisaient
classer sur la mauvaise technique, sans lien avec un mot-clé manquant.

- splitIntoClauses coupe désormais sur la limite de phrase (juste après
  un ".", "!" ou "?") la plus proche du milieu de l'écart entre deux
  candidats quand il y en a une, plutôt que sur l'espace brut le plus
  proche du milieu. Une description à deux techniques dans deux phrases
  distinctes ("Préchauffer le four à 180°C. Dans un saladier, mettre le
  beurre... et mélanger.") ne coupait qu'au milieu brut, ce qui pouvait
  trancher en pleine deuxième phrase et envoyer au classifieur une
  clause tronquée ("...(thermostat 6). Dans un saladier, mettre" sans
  complément) — assez éloignée des phrases d'entraînement courtes et
  complètes pour se faire mal classer avec confiance (préchauffer prédit
  "mix", mélanger prédit "melt").
- CONFIDENCE_THRESHOLD passe de 0.65 à 0.75 : du texte anglais passé
  dans le classifieur français (qui doit ne rien trouver, garanti par
  le test d'isolation des locales) scorait 0.69 sur "boil" — du bruit
  de petit corpus, pas un vrai verdict. Les cas réels que ce seuil sert
  à faire confiance scorent 0.91 à 1.0 en pratique ; 0.75 sépare
  proprement le bruit du signal sans rien casser (309 tests toujours
  verts).
- Deux phrases d'entraînement ajoutées à `cook` pour deux clauses
  réelles mal classées (feu doux + remuant, découvert + laisser cuire)
  qui n'avaient pourtant pas de mot-clé manquant.

Ajoute aussi src/scripts/backfill-tech-steps.ts : la détection ne
tourne qu'à la création/modification d'une recette, jamais
rétroactivement — ce script recalcule le start/end/contextStart/
contextEnd de chaque étape existante contre le classifieur actuel,
pour ne pas avoir à rouvrir et resauvegarder chaque recette à la main
après un changement de corpus.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(web): rend le contexte des tech steps réellement visible

Le style existant (fond teinté à 6% d'opacité, sans autre indice
visuel) était structurellement correct — vérifié en base, l'API et le
DOM contenaient bien les spans de contexte — mais imperceptible à
l'œil sur ce thème sombre : --color-primary n'est pas assez saturé
pour qu'une teinte de quelques % se distingue du fond de la carte.
Vérifié en créant une recette test dans le navigateur et en zoomant le
texte rendu : littéralement aucune différence visible avant, un
rectangle net après.

Passe à 10% de fond + une bordure basse pleine à 45% d'opacité comme
second indice visuel indépendant, tout en gardant le mot-clé
(soulignement pointillé + fond à 14% + curseur + tooltip) nettement
plus marqué que son contexte.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(web): retire l'affichage visuel du contexte des tech steps

Ne touche que le rendu — le backend continue de calculer et de
persister contextStart/contextEnd (tech-step-matcher.ts, StepTechStep),
et splitDescriptionByTechSteps continue de découper la description
autour du contexte (segments isKeyword: false). StepDescription.tsx
rend désormais ces segments comme du texte brut, comme un segment sans
technique — plus d'encadré/bordure autour de la clause, seul le
mot-clé reste surligné avec sa tooltip.

.step-tech-step-context (CSS) retirée, devenue inutilisée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 00:17:39 +02:00
kyuno053
5d63ff9ea9
fix: corrige les bugs ouverts du repo (import TheMealDB, sidebar mobile) + doc (#59)
* 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>

* fix(layout): la sidebar réduite écrasait la barre mobile

`isCollapsed` (rail icône seule sur desktop) persiste dans localStorage
indépendamment de la largeur de fenêtre — un utilisateur ayant réduit la
sidebar sur desktop puis ouvrant la même session sur mobile (ou réduisant
la fenêtre sous 640px) gardait `.app-sidebar.collapsed` (spécificité
0,2,0 : width 4.25rem, flex-direction column), qui l'emportait sur la
règle mobile `@media (max-width: 640px)` (spécificité 0,1,0) censée passer
la sidebar en barre horizontale pleine largeur.

Le bloc `&.collapsed` est maintenant scopé sous `@media (min-width: 641px)`
— le complément exact du breakpoint mobile — donc il ne s'applique plus du
tout en dessous.

Vérifié dans le navigateur : sidebar collapsed=true dans localStorage,
viewport 375px — la sidebar calcule bien width: 375px / flex-direction:
row (barre horizontale pleine largeur) au lieu de 4.25rem/column.

Closes #27

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(readme): documente GET /planning?date=, plus /planning/current

Le README documentait encore `GET /planning/current` (401 sans session,
couvre "aujourd'hui"), une route qui n'existe plus — `planning.routes.ts`
ne définit que `GET /planning?date=YYYY-MM-DD` depuis l'introduction de la
grille de semaine complète. Sans session, `/planning/current` renvoie un
404 générique (route inexistante), pas le 401 documenté.

Documente aussi POST/DELETE /planning/items au passage, absents jusqu'ici.

Closes #55

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: met à jour README et specs/ avec l'état réel du code

Le code avait beaucoup évolué depuis la dernière mise à jour de la
documentation (sources externes, import de recettes, planning en
grille, pages de paramètres, thème, tests Cucumber...) sans que
README.md/specs/*.md ne suivent. Tour complet du code (backend +
frontend) et réécriture :

- specs/batch-cooking-modele.md : schéma de données réécrit depuis
  schema.prisma (foyer/admin/invitation, sources, catalogue
  ingrédients/unités, techniques détectées, visibilité des recettes).
- specs/backend-architecture.md : foyer, préférences/goûts, planning,
  référence, sources externes (adaptateurs/registre/sync), matching
  ingrédients/techniques, isolation base de test, suppression de compte.
- specs/frontend-architecture.md : routing complet, sidebar/paramètres,
  thème, planning + picker, catalogue + import, composants UI partagés,
  tests Cypress+Cucumber.
- specs/batch-cooking-architecture.md : module Import passe de TODO à
  implémenté.
- specs/error-handling.md : liste complète des ~19 codes d'erreur.
- README.md : réécriture pour refléter tout ce qui précède, plus la
  note (dangereusement obsolète) sur le partage base de test/dev — le
  fix existe déjà (apps/api/.env.test), la doc décrivait encore le bug.

* feat(ingredients): ajoute jaune/blanc d'oeuf, coriandre en poudre, viandes hachées

Complète le catalogue d'ingrédients de référence (seed data) :

- jaune d'oeuf / blanc d'oeuf (dairyAndCheese/eggs, aux côtés d'"egg")
- coriandre en poudre (condimentsAndSpices/spices, aux côtés de
  corianderSeeds/freshCilantro déjà présents)
- viandes hachées manquantes : veau, porc, agneau (meatAndSeafood/meats,
  aux côtés de groundBeef déjà présent), dinde et poulet
  (meatAndSeafood/poultry)

Libellés ajoutés dans apps/web/src/locales/fr/translation.json (source
d'affichage) et packages/shared/src/data/catalog-labels-en.ts (matching
anglais pour l'import de recettes depuis des sources comme TheMealDB).
Aucune icône ni régime dédiés : héritent des défauts de leur groupe
(EGG/SPICE/MEAT/POULTRY, mêmes dietUids que leurs groupes respectifs).

282 tests apps/api toujours au vert (resetDatabase() reseed le
catalogue à chaque test).

* fix(i18n): retire le œ ligaturé des libellés français de l'œuf

"Œuf"/"Œufs" (ingrédient, sous-catégorie, allergène) et "Jaune/Blanc
d'œuf" (ajoutés par #60) s'écrivaient avec le œ ligaturé — remplacé par
"oe" (deux lettres) partout où le mot apparaît. Ne touche pas "bœuf"
(mot différent, non concerné).

Le scénario Cucumber recipe-form.feature qui sélectionne l'ingrédient
par son libellé affiché est mis à jour en conséquence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(recipes): concatène les ingrédients dupliqués à l'import

Suite au retour utilisateur sur #53 (follow-up) : au lieu de bloquer
l'import et de demander à l'utilisateur de retirer une ligne en double
à la main, deux lignes source qui résolvent vers le même ingrédient
catalogue sont désormais fusionnées automatiquement, quantité
concaténée (sommée), avant même que l'écran de revue ne s'affiche.

- mergeDuplicateIngredients (recipe-translation.ts) : même unité des
  deux côtés -> somme directe. Unité différente mais même UnitType
  (MASS/VOLUME) -> conversion via toBaseFactor avant de sommer, exprimée
  dans l'unité de la première ligne. UnitType différent, ou COUNT des
  deux côtés (une "pincée" n'est pas une fraction fixe d'une "gousse",
  cf. le commentaire de UnitView) -> jamais fusionnées, laissées en
  double (createRecipeSchema/RecipeImportForm continuent de les
  signaler, filet de sécurité déjà en place). Les lignes non résolues
  (ingredientId: null) ne sont jamais fusionnées entre elles.
- rawText concaténé ("100g Sugar + 45g Sugar") pour la traçabilité.
- Branché dans previewSourceItem (sources.service.ts), juste après
  translateRecipeIngredients — c'est le seul endroit où des doublons
  peuvent apparaître (la création manuelle ne peut pas en produire,
  IngredientPicker exclut déjà les ingrédients déjà sélectionnés).

Vérifié via l'API en local (import réel de "Flan" depuis TheMealDB) :
"100g Sugar"/"45g Sugar" -> une seule ligne Sucre, 145g.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(lint): upgrade Biome vers 2.x, active noExplicitAny/noConsole/noFloatingPromises

`@biomejs/biome` passe de 1.9.4 à 2.5.9 (config migrée via `biome migrate
--write`) — nécessaire pour noFloatingPromises, une règle type-aware
apparue en 2.0 (nursery).

- noExplicitAny : déjà "recommended", actif depuis toujours, aucun changement.
- noConsole (biome.json) : bloque tout `console.*` sauf error/warn/info/
  debug/table/assert — équivalent à "pas de console.log" sans interdire
  les niveaux nommés (voir le nouveau log service dans le prochain commit,
  qui centralise justement ces appels).
- noFloatingPromises (nursery) activé explicitement sous `rules.nursery`
  sans avoir besoin d'activer le domaine "types" au sens large (ça aurait
  aussi allumé des dizaines d'autres règles type-aware type
  noUnresolvedImports/noUnnecessaryConditions, hors scope ici).

Le reste du diff, c'est soit du reformatage automatique (import sort, 2.x
ordonne différemment de 1.9.4 — `biome check --write --unsafe`), soit les
corrections des ~20 promesses flottantes que la nouvelle règle a fait
remonter :

- La plupart sont des `navigate(...)` non attendus (react-router v7 type
  `navigate` en `void | Promise<void>`) — préfixés `void navigate(...)`,
  aucun changement de comportement.
- Trois chargements initiaux en useEffect (OnboardingAllergensPage,
  OnboardingDietPage, OnboardingHouseholdPage, HouseholdSettingsPage)
  n'avaient jamais de `.catch()` du tout — ajouté (dégradation silencieuse
  vers un état vide/par défaut, même raisonnement que le `.catch()` déjà
  présent dans OnboardingSourcesPage).
- HouseholdSettingsPage : `loadHouse` était une fonction déclarée à chaque
  render (donc une référence différente à chaque fois) utilisée comme
  dépendance de useEffect ET passée en callback à des enfants — le
  useEffect se re-déclenchait donc à chaque re-render provoqué par son
  propre fetch, un vrai bug de boucle infinie de requêtes que
  noFloatingPromises a fait remonter indirectement (via
  useExhaustiveDependencies). Corrigé avec useCallback([]).
- RecipeDetailPanel : une clé de liste `${index}-...}` sur une liste
  statique (draft.steps, sans id stable — DraftRecipeStepView n'en a pas)
  — biome-ignore justifié, pas de bug réel.
- recipe.test.ts : variable `agent` non utilisée, retirée.

Vérifié : `pnpm --filter api test` (295/295), `pnpm lint` et `pnpm build`
clean sur tout le repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(api): ajoute un log service pour les logs de fonctionnement côté serveur

Jusqu'ici, rien ne journalisait quoi que ce soit côté serveur : aucune
trace au démarrage à part un console.log ad hoc, et surtout aucune trace
des requêtes ni des erreurs gérées par ErrorHandlerService — un 500 en
production n'aurait laissé aucune trace exploitable.

- LoggerService (apps/api/src/lib/logger.service.ts) — classe (public
  debug/info/warn/error, private emit), même convention que
  ErrorHandlerService (packages/error-tools) : instance unique partagée
  exportée (`export const logger = new LoggerService()`). Émet une ligne
  JSON structurée par appel (timestamp/level/message + meta), filtrée par
  seuil selon NODE_ENV (debug complet en dev, warn+ pendant les tests
  pour ne pas alourdir la sortie de Mocha, info+ en production). Seul
  endroit du code autorisé à toucher `console` directement (biome-ignore
  justifié), toujours via une méthode nommée — jamais un console.log nu.
- requestLogger (middlewares/request-logger.ts) — une ligne par requête
  terminée (méthode/chemin/statut/durée), montée en tout premier dans
  app.ts, avant même setupCore (CORS/JSON/cookies), pour englober tout le
  pipeline. Niveau déduit du statut (info/warn/error).
- errorLogger (middlewares/error-logger.ts) — monté juste avant
  createErrorMiddleware : réutilise errorHandlerService.handle() (pur/
  sans effet de bord) pour classifier l'erreur avant que la vraie réponse
  ne soit construite, log en warn les 4xx routiniers (validation, 404,
  401...) et en error les 5xx/exceptions non prévues (avec la stack).
- error-handler.service.ts : retire le `console.error(error)` ad hoc de
  fromUnknownError — errorLogger voit désormais chaque erreur avant que
  ce service ne la mappe, donc ce console.error faisait doublon (et
  loggait en texte brut, pas en JSON structuré).
- server.ts : le console.log de démarrage passe par logger.info.

Vérifié : pnpm --filter api test (303/303, dont 8 nouveaux tests sur
LoggerService), pnpm lint/build clean, testé en live (pnpm dev:api +
curl) — logs JSON corrects pour un 200, un 404, un 401.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* style: préfixe tous les membres private/protected par _

Convention demandée par l'utilisateur : `emit` -> `_emit`, sur toutes les
classes du repo, pas seulement le nouveau code. `public` reste sans
préfixe.

- LoggerService (apps/api) : _minSeverity, _emit.
- ApiClient (apps/web) : _request (39 sites d'appel mis à jour).
- ErrorHandlerService (packages/error-tools) : _fromZodError,
  _fromHttpError, _fromUnknownError.
- ExpressServer (packages/express-tools) : _app, _registeredRoutes.

Aucun changement de comportement — pur renommage interne, aucune méthode
private/protected n'était appelée depuis l'extérieur de sa classe.

Vérifié : pnpm --filter api test (303/303), pnpm lint/build clean sur
tout le repo (apps/api, apps/web, packages/*).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(specs): documente les conventions de développement du repo

Nouveau specs/dev-conventions.md — jusqu'ici ces règles n'existaient que
dans l'historique de commits/PR (classes vs objets littéraux pour la
logique de service, préfixe _ sur private/protected, règles Biome
actives, log service, tests sans mocks de la DB, conventions git/PR...),
rien de centralisé pour un futur contributeur (humain ou Claude Code).

Référencé depuis README.md, section "Qualité / Tests".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(web): regroupe pages/ par section au lieu d'un dossier à plat

pages/ mélangeait 8 fichiers directement à sa racine (LoginPage,
SignupPage, PlanningPage+scss, RecipesPage, RecipeFormPage,
ImportRecipePage, ShoppingListPage, ComingSoonPage+scss) à côté de deux
sous-dossiers déjà groupés (onboarding/, settings/) — incohérent, et
difficile à parcourir une fois le nombre de pages monté. Un sous-dossier
par section routée, même règle que onboarding/settings existants :

- pages/auth/        — LoginPage, SignupPage
- pages/planning/     — PlanningPage + planning-page.scss
- pages/recipes/      — RecipesPage, RecipeFormPage, ImportRecipePage
- pages/shopping-list/ — ShoppingListPage

ComingSoonPage (+ .scss) déménage vers components/ui/ — ce n'est pas une
page routée elle-même (ShoppingListPage l'enveloppe), c'est un composant
UI générique réutilisable, sa place est aux côtés de Dialog/Tooltip/etc.,
pas dans pages/.

Chemins relatifs internes de chaque fichier déplacé mis à jour (un niveau
de profondeur en plus), imports dans App.tsx repointés, tri Biome
réappliqué. specs/frontend-architecture.md mis à jour (arborescence +
références de chemin).

Vérifié : pnpm build clean (apps/web, 1952 modules), pnpm lint clean sur
tout le repo, testé en live dans le navigateur (login/signup, planning,
recettes, nouvelle recette, liste de courses, paramètres) — aucune route
cassée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(web): regroupe features/recipes/ par sous-domaine au lieu d'un dossier à plat

20 fichiers à plat -> badges/ (DietTagSelect, DietBadges, AllergenBadges,
ReproducibleBadge, FavoriteStarButton), ingredients/ (IngredientPicker,
IngredientRow, ingredient-icons), steps/ (StepListEditor, StepDescription,
highlight-tech-steps), sources/ (RecipeSourcesPanel, SourceItemTable,
RecipeImportForm, recipe-import-draft, useEnabledSources).

RecipeTable/RecipeTabs/RecipeDetailPanel et recipes.scss restent à la
racine (composants transverses aux sous-dossiers, partagés par plusieurs
d'entre eux). Chemins relatifs corrigés dans les fichiers déplacés et chez
tous leurs importeurs externes (pages/recipes/*, features/planning/
RecipePickerDialog.tsx, features/profile/DislikedIngredientsField.tsx),
doc mise à jour (specs/frontend-architecture.md, specs/batch-cooking-
modele.md).

Vérifié : tsc --noEmit, biome check, build complet, 303 tests API,
vérification live navigateur (planning, /recettes, /recettes/nouvelle).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* 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>

* 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>

* feat(convention): impose try/catch autour de chaque await/corps async

Nouvelle règle de dev : aucun await nu, et un corps de fonction/méthode
async doit intégralement vivre dans un try/catch (pas seulement la ou
les lignes qui awaitent). Documentée dans specs/dev-conventions.md avec
son périmètre (code applicatif — services/hooks/composants/middlewares
— routes *.routes.ts exemptées car déjà couvertes par
wrapAsyncHandler ; tests et scripts one-off exemptés aussi).

Appliqué rétroactivement à tout le code applicatif qui ne l'était pas
déjà :
- api : auth/house/profile/preferences/planning/reference/recipe/
  sources .service.ts, recipe-source-sync.ts, recipe-translation.ts,
  ingredient-matcher.ts, tech-step-matcher.ts, json-ld-recipe.ts,
  the-meal-db.ts — un try/catch par fonction async, rethrow simple
  (le middleware d'erreur logge déjà tout centralement, voir
  error-logger.ts) sauf quand un catch avait déjà une logique propre
  (ex. le retry de createHouse).
- web : api/client.ts (_request), AuthContext.tsx, ThemeContext.tsx,
  AppLayout.tsx (handleLogout), HouseholdSettingsPage.tsx (handleCopy/
  handleRemove/handleDelete/handleLeave) — la plupart des handlers de
  formulaire avaient déjà ce pattern, seuls ceux qui laissaient un
  await nu ont été corrigés.

lint/complexity/noUselessCatch désactivé dans biome.json (interdisait
justement le catch-qui-rethrow que cette convention impose).

Vérifié : tsc --noEmit (api+web), biome check (0 erreur, repo entier),
build complet, 303 tests API, vérification live navigateur (thème,
déconnexion, copie du code d'invitation).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(web): corrige l'import cassé de highlight-tech-steps.cy.tsx

Oubli lors du regroupement de features/recipes/ par sous-domaine
(refactor(web): regroupe features/recipes/...) : le déplacement de
highlight-tech-steps.ts vers features/recipes/steps/ n'avait pas été
répercuté dans ce test composant Cypress (hors de apps/web/src, donc
raté par la recherche de référence externe à l'époque) — faisait
planter le job e2e en CI ("Failed to fetch dynamically imported
module").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 12:16:22 +02:00
17b6b9151a fix(api): la garde resetDatabase() accepte aussi la base CI
DATABASE_URL en CI (ci.yml) pointe vers "batchcooking_ci", pas
"batchcooking_test" — la garde ne cherchait que "test" et rejetait donc
la base CI légitime, faisant échouer les 282 tests. Accepte "test" ou
"ci" désormais ; seul le nom réel de la base de dev ("batchcooking")
ne matche toujours ni l'un ni l'autre, ce qui reste le seul cas que
cette garde doit effectivement bloquer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 19:59:16 +02:00
61ac2993db fix(api): isole la base de test de la base de dev
Incident : lancer `pnpm test` (apps/api) truncait la vraie base de dev
locale — `test-support/reset-db.ts`'s `resetDatabase()` fait un `TRUNCATE
... CASCADE` sur quasiment tout le schéma (dont `house`/`house_source`)
avant *chaque* test, et `.env`/tests partageaient le même
`DATABASE_URL` (un seul fichier `.env`, `NODE_ENV=test` ne changeait
rien). Deux lancements du test suite cette session ont ainsi effacé le
foyer, le compte et les activations de sources d'un utilisateur en
train de tester l'app en local — perte réelle, aucune récupération
possible (TRUNCATE, pas de sauvegarde).

- `config/env.ts` charge désormais `.env.test` (pas `.env`) quand
  `NODE_ENV=test` — `.env.test` (local, non commité, comme `.env`)
  pointe vers une base Postgres séparée (`batchcooking_test`, même
  serveur/identifiants que la base de dev, juste une base différente).
  `.env.test.example` documente comment la créer.
- `resetDatabase()` refuse maintenant de tourner si `NODE_ENV !== "test"`
  ou si `DATABASE_URL` ne contient pas "test" — garde-fou supplémentaire
  si `.env.test` est un jour absent/mal configuré, pour ne plus jamais
  reproduire cet incident même en cas d'erreur de configuration.
- `.gitignore` autorise `.env.test.example` (déjà ignoré via `.env.*`,
  comme `.env.example` l'est déjà pour `.env`).

Vérifié : snapshot de la base de dev (houses/house_sources/users) avant/
après un lancement complet de `pnpm test` — identique, base de dev
intacte. 282 tests toujours au vert, contre la nouvelle base de test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 19:51:21 +02:00
e4c8d910d4 feat(recipes): écran de revue et finalisation de l'import (étape 3/4)
Backend :
- `createRecipe` refactorisé en fine enveloppe autour d'un nouvel
  helper interne `createRecipeInternal`, paramétré par une source
  d'import optionnelle ; nouvelle fonction exportée
  `createImportedRecipe` qui réutilise toute la validation
  ingrédients/unités/diets et le matching des tech steps, sans
  dupliquer cette logique.
- La locale de l'adaptateur source est propagée jusqu'au chargement
  des `TechStepMapping`, pour que le texte anglais (TheMealDB, etc.)
  soit matché contre le bon jeu de règles au lieu du défaut français.
- Nouvel endpoint `POST /sources/:sourceKey/import/:externalId` —
  valide le payload via `createRecipeSchema` (même schéma qu'une
  création manuelle) et persiste une vraie `Recipe` liée à la source
  (`sourceId`/`externalId`).
- Nouveau code d'erreur `RECIPE_ALREADY_IMPORTED` (4022) quand
  l'item a déjà été importé pour ce foyer.

Frontend :
- `ImportRecipePage` (nouvelle page, `/recettes/importer/:sourceKey/:externalId`) —
  pré-remplit le formulaire depuis `previewSourceItem`, en miroir de
  `RecipeFormPage` (mêmes sous-composants : `IngredientRow`,
  `IngredientPicker`, `StepListEditor`, `DietTagSelect`). Ajoute une
  section dédiée aux lignes d'ingrédients non résolues automatiquement :
  l'utilisateur choisit un ingrédient réel via l'`IngredientPicker`
  existant ou retire la ligne — aucune recette invalide n'est jamais
  soumise, le bouton d'import reste désactivé tant qu'il en reste.
- `SourceItemPreviewPanel` gagne un lien « Importer cette recette »
  vers cet écran.

Tests :
- Mocha (`apps/api/test/sources.test.ts`) : 6 nouveaux tests sur
  `POST /sources/:sourceKey/import/:externalId` (payload valide,
  ingrédient/unité inconnus, déjà importé, deux foyers distincts,
  locale de la source respectée pour les tech steps). 282 tests
  passent au total, aucune régression.
- Cypress : nouveau scénario Gherkin bout-en-bout dans
  `recipe-sources.feature` (parcourir → prévisualiser → importer →
  résoudre un ingrédient non reconnu → confirmer → atterrir sur la
  recette sauvegardée). Steps d'édition d'ingrédients/étapes
  génériques déplacés de `recipe-form.ts` vers
  `cypress/support/step_definitions/common.steps.ts`, réutilisables
  par ce nouveau scénario.

Suite : étape 4 (ajouter au planning déclenche l'import si nécessaire).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 16:56:06 +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
d0900ad7fd feat(recipes): surligne les tech steps détectés dans les étapes, avec tooltip
Expose côté UI ce que tech-step-matcher.ts détecte déjà à la sauvegarde
(Step.techSteps) mais qui restait backend-only : dans le panneau détail
d'une recette, les mots exacts ayant déclenché une technique sont
surlignés, avec un tooltip (survol/focus clavier) donnant son nom.

- tech-step-matcher.ts : matchTechStepSpans(description, mappings) expose
  désormais {techStepId, start, end} en plus de la simple séquence d'ids
  (déjà calculé en interne, jusqu'ici jeté). matchTechSteps devient un
  wrapper fin dessus — aucun changement à ses ~12 tests existants ni à
  recipe-translation.ts.
- StepTechStep gagne start/end (nullable, pas de backfill — même leçon que
  l'incident de migration ingredient_unit_catalog : NOT NULL sans défaut
  sur une table déjà peuplée casse le déploiement). Une ligne pré-existante
  sans span est simplement omise de la réponse API plutôt que de fuiter un
  null, jusqu'à ce que la recette soit resauvegardée.
- recipe.service.ts : createRecipe/updateRecipe persistent start/end ;
  StepView expose techSteps: { techStep: {id,key}, start, end }[]. Le
  recalcul complet à chaque édition (ajout/modif/suppression d'étape) était
  déjà garanti par le delete-then-recreate existant d'updateRecipe — testé
  explicitement (nouveau test "recomputes techniques from scratch...").
- Frontend : StepDescription.tsx (découpe le texte via
  highlight-tech-steps.ts, pur et testé) remplace le <p> brut dans
  RecipeDetailPanel. Nouveau Tooltip.tsx (composants/ui, CSS pur, aucune
  lib externe — même esprit que Dialog.tsx) : un <button> (focusable
  nativement, pas de tabIndex sur un <mark> non interactif) affiche le nom
  de la technique (catalog.techSteps.<key>) au survol/focus.

Tests : matchTechStepSpans (spans corrects, chevauchement résolu),
recipe.test.ts (forme API + recalcul complet sur modif/ajout/suppression
d'étape, avec vérification que les anciennes lignes StepTechStep sont bien
supprimées), splitDescriptionByTechSteps (tri, bornes invalides ignorées,
chevauchement résiduel ignoré), scénario Cypress recipes.feature
(surlignage + tooltip au focus).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 14:46:15 +02:00
deec91c5a3 fix(recipes): synchronise les sources en base au démarrage de l'image de prod
Le conteneur de prod ne peuplait jamais la table Source : seed-runtime.ts
(l'entrée seed de l'image Docker, exécutée après `prisma migrate deploy`)
n'appelait que seedReferenceData(), jamais registerAllRecipeSources()/
syncRecipeSources() — contrairement à prisma/seed.ts (dev). server.ts
enregistre bien les adaptateurs dans son propre registre en mémoire, mais
c'est un processus distinct de celui qui lance seed-runtime.js dans la
chaîne CMD du Dockerfile ; sans ce sync, GET /reference/sources renvoyait
toujours [], et HouseholdSettingsPage masquait silencieusement toute la
section sources (sources.length === 0 → return null). C'est ce que
l'utilisateur a remarqué : impossible de paramétrer les sources visibles
du foyer en prod.

Vérifié en local : Source/HouseSource vidées, seed-runtime.js compilé
relancé exactement comme le ferait le conteneur (migrate deploy déjà
appliqué, puis ce script) → les deux sources (TheMealDB, JSON-LD) sont
bien resynchronisées.

Ajoute aussi la couverture Cypress du parcours "sources" qui manquait :
- onboarding.feature : nouveau scénario où le catalogue de sources n'est
  pas vide — l'étape /onboarding/sources s'affiche et se soumet, au lieu
  du seul scénario existant qui la voyait toujours skippée (catalogue
  vide).
- household-settings.feature : nouveaux scénarios pour la section sources
  de /parametres/foyer — affichage + sauvegarde (autosave incluse) quand
  des sources existent, et disparition complète de la section quand le
  catalogue est vide.
- Nouvelles steps partagées (reference-data.steps.ts pour le catalogue,
  household-mutations.steps.ts pour la sélection par foyer).

Non exécutés localement : Chromium/Electron headless plante au lancement
du process GPU dans cet environnement (limitation documentée du README,
reproductible sur main, sans lien avec ce changement) — vérifiés par
relecture attentive contre le code source réel (libellés de traduction,
routes, formes de requête/réponse) et en suivant le même gabarit que les
scénarios existants déjà verts en CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 13:48:29 +02:00
89722ba790 feat(recipes): matching anglais pour les tech steps et les ingrédients
- Ajoute une expression régulière anglaise à chacun des 26 TechStepMapping
  du catalogue (locale "en"), en plus du "fr" existant — les recettes en
  anglais (TheMealDB, etc.) peuvent désormais matcher leurs étapes.
- Nouveau apps/api/src/lib/ingredient-matcher.ts : moteur de matching pur
  (nom d'ingrédient, unité, quantité) contre les catalogues Ingredient/Unit,
  à partir de labels anglais écrits à la main (packages/shared/src/data/
  catalog-labels-en.ts — 546 INGREDIENT_LABELS_EN + 17 UNIT_LABELS_EN avec
  synonymes/abréviations). Tokenise et stem naïvement les deux côtés pour
  tolérer pluriels et mots descriptifs superflus ; la correspondance la
  plus spécifique (le plus de mots) l'emporte en cas de recoupement.
- extractQuantity() : lit un nombre en tête de texte libre (entier,
  décimal, fraction simple ou nombre mixte) pour déduire la quantité et
  l'unité quand la source ne les fournit pas séparément.
- Étend recipe-translation.ts : translateRecipe(recipe, locale) résout
  aussi ingredientId/unitId/quantity de chaque ligne d'ingrédient — mais
  uniquement pour locale "en" (seules langue avec des labels), pour ne pas
  interroger la base inutilement ni halluciner un match dans une autre
  langue.
- Ajoute cup/ounce/pound au catalogue Unit (toBaseFactor réel), absents
  jusqu'ici alors que très fréquents dans les recettes anglaises.
- Vérifié en conditions réelles contre TheMealDB (Teriyaki Chicken
  Casserole) : 8/9 ingrédients résolus avec la bonne quantité/unité, le
  seul raté ("stir-fry vegetables") étant un mélange sans entrée dédiée au
  catalogue — dégradation gracieuse (unitId/ingredientId: null) comme prévu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 13:10:00 +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
kyuno053
b9f148a674
Merge pull request #39 from kyuno053/feat/recipe-step-translation
feat(recipes): traduction des étapes d'une recette importée en tech steps
2026-08-20 11:57:45 +02:00
5b868e4422 feat(recipes): traduction des étapes d'une recette importée en tech steps
Ajoute la brique "Traduction en étapes" du pipeline d'import décrit
dans specs/batch-cooking-architecture.md — prend un ParsedRecipe
(sortie de parse() d'un adaptateur, recipe-source-adapter.ts) et
déclare, pour chaque étape, sa séquence de tech steps détectée.

- recipe-translation.ts : TranslatedRecipe/TranslatedRecipeStep
  (ParsedRecipe/ParsedRecipeStep + techStepIds: number[], même forme
  que Step.techSteps/StepTechStep). translateRecipeSteps() est pure
  (prend les mappings en argument, comme matchTechSteps lui-même) ;
  translateRecipe() est le wrapper qui charge le catalogue depuis la
  DB pour une locale donnée — même séparation pur/DB que
  tech-step-matcher.ts.
- Ne touche pas aux ingrédients (résolution vers Ingredient/Unit
  toujours hors scope) ni ne produit une Recipe sauvegardable (pas de
  dietIds/visibility/auteur) — une seule brique du pipeline, pas tout
  le pipeline.
- Documente explicitement la limite actuelle : le catalogue de tech
  steps n'a que des mappings "fr", donc une source anglophone comme
  TheMealDB traduite avec cette locale obtient des séquences vides
  sur toutes ses étapes (vérifié par un test dédié avec du texte
  réel de TheMealDB).

8 nouveaux tests (partie pure + partie DB avec le vrai catalogue
"fr" seedé). 180 tests passent au total. Build et lint propres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 11:39:15 +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
kyuno053
0b1c102418
Merge pull request #35 from kyuno053/feat/recipe-source-adapter
feat(recipes): module générique d'adaptateurs de sources de recettes
2026-08-20 08:51:12 +02:00
3eadb4db41 fix(recipes): une étape peut porter une séquence de tech steps
Corrige le modèle de données suite à une review sur la PR #34 :
"Dans une poêle chaude, faire chauffer une noix de beurre" combine
deux techniques (preheat + melt), or Step.techStepId ne pouvait en
porter qu'une seule (FK simple nullable).

- Step.techStepId (FK simple) remplacé par StepTechStep, une table de
  jointure ordonnée (stepId, techStepId, order) — @@id([stepId,
  order]) garantit une séquence propre par étape.
- tech-step-matcher.ts : matchTechStep(...) → number|null devient
  matchTechSteps(...) → number[]. Nouvel algorithme : chaque mapping
  qui matche devient un candidat avec sa position dans le texte ; on
  garde le meilleur candidat par technique (poids, puis position),
  on résout les chevauchements entre techniques différentes par poids
  décroissant (ex: "cuire au four" ne garde que `bake`, pas `cook` en
  plus), puis on trie le résultat par ordre d'apparition dans le
  texte — une séquence qui se lit dans le même ordre que l'instruction.
- Ajout de la technique "melt" (faire fondre) au catalogue, pour
  pouvoir tester le cas concret du commentaire de review de bout en
  bout (préchauffer + faire fondre).
- recipe.service.ts : câble StepTechStep via un create imbriqué à la
  place du champ scalaire.

Tests étendus dans tech-step-matcher.test.ts (séquences non
chevauchantes, résolution de chevauchement combinée à une technique
distincte, etc.) et recipe.test.ts (nouveau test de bout en bout avec
deux techniques dans une même étape). 133 tests passent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 07:58:29 +02:00
aedeb257ce feat(recipes): relie les recettes à leur source (sourceId + externalId)
Répond au besoin identifié précédemment : la table `sources` devient
un vrai catalogue des sources implémentées, et une recette importée
pourra être reliée à l'item source dont elle provient.

- Source.key (unique) — même convention que Diet.key/Unit.key/
  TechStep.key. Le catalogue est désormais synchronisé depuis le
  registre d'adaptateurs (recipe-source-registry.ts) via
  syncRecipeSources() (nouveau apps/api/src/db/recipe-source-sync.ts),
  plutôt que maintenu à la main comme DIETS/UNITS — reste vide tant
  qu'aucun adaptateur concret n'est enregistré.
- Recipe.externalId (nullable) — l'identifiant de la recette côté
  source. Contrainte @@unique([sourceId, externalId]) : empêche
  d'importer deux fois la même recette (les recettes manuelles, aux
  deux colonnes nulles, ne sont jamais en conflit entre elles).
- findImportedExternalIds(prisma, sourceKey, externalIds) — le
  pendant DB de markAlreadyImported (recipe-source-adapter.ts),
  ferme la boucle commencée dans la PR précédente pour distinguer les
  recettes déjà intégrées lors du browse.
- syncRecipeSources() appelé après seedReferenceData() dans
  prisma/seed.ts et test-support/reset-db.ts.

Toujours pas de route HTTP ni de champ sourceId/externalId exposé
dans createRecipeSchema — la sauvegarde effective d'une recette
importée reste pour une PR ultérieure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 07:43:51 +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
ea1dfc5ad7 test(recipes): couverture Mocha complète pour les tech steps
Étend les tests ajoutés pour le catalogue de techniques culinaires :

- tech-step-matcher.test.ts : normalizeText (cédille/ligature, texte
  déjà normalisé, chaîne vide), matchTechStep (listes/description
  vides, plusieurs mappings pointant vers le même techStep, respect
  des frontières de mot — évite les faux positifs type "recuire"/
  "précuit"), et loadTechStepMappingRules (filtrage par locale contre
  une vraie base, locale sans mapping).
- reference.test.ts : ordre alphabétique par key, idempotence du
  reseed (pas de doublon en rappelant seedReferenceData sans
  truncate).
- recipe.test.ts : détection indépendante par étape sur une recette à
  plusieurs étapes (ordre préservé, y compris une étape sans match),
  et résolution de bout en bout du mapping le plus spécifique quand
  une description matche plusieurs techniques.

129 tests passent (13 nouveaux).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 06:38:00 +02:00
5026a23bd5 feat(recipes): catalogue de techniques culinaires (tech steps)
Rend opérationnel le squelette TechStep/TechStepMapping/Step.techStepId
présent dans le schéma depuis le premier commit mais jamais implémenté :

- TechStep gagne un `key` unique (camelCase, même convention que
  Diet/Unit) ; TechStepMapping gagne un `locale` pour pouvoir porter
  plusieurs jeux de règles de matching par langue.
- Catalogue statique de 25 techniques françaises courantes (Cuire,
  Frire, Déglacer, Mijoter, ...), chacune associée à une ou plusieurs
  expressions régulières + un poids, seedées de façon idempotente dans
  reference-seed-data.ts.
- Nouveau moteur de matching (apps/api/src/lib/tech-step-matcher.ts) :
  normalisation accents/casse (NFD) puis test des expressions,
  résolution du meilleur match par poids. Pur et testé unitairement.
- Câblé dans recipe.service.ts : à la création/modification d'une
  recette, chaque étape voit son techStepId calculé automatiquement à
  partir de sa description (locale "fr" en dur pour l'instant, faute
  de préférence de langue utilisateur dans l'app).
- Reste backend-only : StepView n'expose pas encore techStepId,
  conformément au commentaire existant.
- Endpoint GET /reference/tech-steps + TechStepView, en cohérence avec
  les autres catalogues de référence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 23:29:46 +02:00
de500e1a8a feat(recipes): catalogue de référence pour les unités d'ingrédients
Remplace l'unité texte libre de RecipeIngredient (max 20 caractères,
"g"/"grammes"/"G"... jamais fiable à additionner) par une référence
vers un nouveau catalogue Unit (id/key/type/toBaseFactor), même
traitement que Diet/Allergy/Ingredient : GET /reference/units, seedé
par reference-seed-data.ts (14 unités : gram/kilogram/milliliter/
centiliter/liter/tablespoon/teaspoon/piece/pinch/slice/clove/bunch/
sachet/sprig), sélectionnable uniquement via un <select> dans le
formulaire recette (plus de saisie libre).

`toBaseFactor` (combien d'unités de base — gramme pour MASS,
millilitre pour VOLUME — vaut une unité) pose les bases d'une future
fonctionnalité de conversion (ex. liste de courses additionnant
"500g" + "0.5kg") sans construire cette fonctionnalité elle-même —
les unités COUNT restent à toBaseFactor=1, non convertibles entre
elles (une "pincée" n'est pas une fraction fixe d'une "gousse").

Migration : recipe_ingredient.unit → unit_id (FK), breaking change
sans backfill assumé (pas de recette réelle en prod actuellement,
voir commentaire de migration) — mêmes garde-fous service-side que
ingredientId (404 UNIT_NOT_FOUND) et mêmes tests de couverture.
2026-08-19 22:34:57 +02:00
0cb1dccd92 feat(recipes): ajouter le nombre de portions couvertes par une recette
Ajoute Recipe.portions (combien de portions la recette produit telle
qu'écrite) — formulaire de création/édition, fiche détail, migration
Prisma (backfill à 4, même pattern que planning_item.portions).

Le sélecteur de recette du planning pré-remplit désormais son propre
champ "portions" depuis cette valeur au lieu de toujours démarrer à 1
(RecipeSummaryView.portions), tout en gardant PlanningItem.portions
indépendant (une recette peut être mise à l'échelle pour un créneau).

Couverture : tests API (création/édition/validation), scénarios
cypress (formulaire + préchargement en édition).
2026-08-19 21:44:01 +02:00
77b55d7aeb refactor(catalog): authoring 100% anglais — uid camelCase, plus de français dans le code
Le catalogue de référence (diets/allergènes/ingrédients) était écrit en
français dans reference-seed-data.ts, avec une table de correspondance
séparée (catalog-en-keys.ts, 666 lignes, ~563 entrées) traduisant chaque
libellé français vers une clé anglaise snake_case, elle-même utilisée pour
peupler la colonne `key` en base et régénérer translation.json. Décision :
remplacer par un authoring 100% anglais camelCase directement dans le seed
— plus de détour, plus de table de correspondance.

- `Ingredient.name`/`allergenNames`/`dietNames` → `uid`/`allergenUids`/
  `dietUids`, valeurs en camelCase directement (ex: "Tomate" → "tomato",
  "Fruits à coque" → "treeNuts").
- `IngredientCategory`/`IngredientSubcategory` (enums Prisma) renommés du
  français SCREAMING_SNAKE_CASE (`PRODUITS_FRAIS`, `LEGUMES`...) vers
  l'anglais camelCase (`freshProduce`, `vegetables`...) — même mécanique
  de migration que le renommage d'enum précédent
  (20260818113250_ingredient_taxonomy_rework) : nouvelle colonne avec
  valeur par défaut sûre, jamais de cast direct (aucune valeur commune
  entre ancien et nouvel enum), seedReferenceData() corrige chaque ligne
  au démarrage suivant.
- Migration `20260819180000_catalog_camel_case_uids` : renomme les clés
  existantes (diet/category/ingredients, même mécanique que
  20260818193000_catalog_keys_to_english) + swap des deux enums. Un cas
  particulier corrigé à la main : "sesame_seeds" était à la fois la clé
  d'un allergène (Category) et d'un ingrédient qui se référence lui-même
  ("Graines de sésame") — les deux tables ont besoin de leur propre
  UPDATE.
- `catalog-en-keys.ts`, `slugify.ts`, `generate-catalog-i18n.ts`,
  `validate-catalog-en-keys.ts` — supprimés (plus de raison d'être).
  Conséquence assumée : `translation.json` n'est plus régénéré
  automatiquement, c'est désormais la seule source du texte FR, tenue à
  jour à la main en parallèle du uid (même clé qui les relie).
- `packages/shared/src/types/reference.ts`, `apps/web`'s
  `ingredient-icons.tsx` (CATEGORY_ICON/SUBCATEGORY_ICON), fixtures
  Cypress codées en dur — mis à jour avec les nouveaux noms.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:40:55 +02:00
36fbe02299 Merge branch 'main' into feat/ingredient-reproducible-flag
Resolves the conflict from PR #28 (Ciqual ingredient expansion) merging
first: both branches touched reference-seed-data.ts's SAUCES group.
Also flags the three newly-merged sauces (Aïoli, Sauce vinaigrette,
Houmous) and Gressin as reproducible — these were in the original
curation plan but skipped earlier since they didn't exist on this
branch's base yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 19:32:14 +02:00
978fe71a11 feat(recipes): flag les ingrédients faisables maison + suggestion de recherche
Remplace la FK morte `Ingredient.alternateRecipeId` (jamais branchée nulle
part — confirmé par exploration : zéro usage en dehors de schema.prisma)
par un flag booléen `reproducible`, plus simple : pas de liaison
recette↔ingrédient en base, juste une info "ça vaut le coup d'être fait
maison" plus un raccourci de recherche.

- Migration : drop `alternate_recipe` (colonne + FK), ajoute
  `reproducible BOOLEAN NOT NULL DEFAULT false` sur `ingredients`.
- `reference-seed-data.ts` : `IngredientSeed` gagne `reproducible?`,
  threadé dans le flatten + la réconciliation `seedReferenceData`. Premier
  lot de 27 ingrédients marqués (pains, pâtes à cuire, sauces de base,
  bouillons/fonds) — même logique que la curation Ciqual : un lot solide
  plutôt qu'exhaustif sur les 546 ingrédients.
- `IngredientView` (shared) + les deux endroits qui la construisent
  (`reference.service.ts`, `recipe.service.ts`) gagnent `reproducible`.
- `ReproducibleBadge` (nouveau) : pastille "Faisable maison" — simple
  dans `IngredientPicker` (avec son propre toggle d'affichage), lien
  cliquable dans `IngredientRow` vers `/recettes?search=<nom>` ouvert
  dans un nouvel onglet (pour ne jamais perdre le formulaire de recette
  en cours — pas de persistance de brouillon dans `RecipeFormPage`).
- `RecipesPage` lit `?search=` au montage pour permettre ce deep-link.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 19:09:39 +02:00
84178a9f5a feat(catalog): deuxième passe Ciqual — 53 ingrédients supplémentaires
Suite de la première passe (55 ajouts) : relecture complète des 815
concepts restants après élimination du bruit commercial/branded, pour
combler d'autres trous réels du catalogue.

53 nouveaux ingrédients :
- 13 légumes (Chicorée rouge, Chou romanesco, Chou-rave, Chou chinois,
  Céleri-rave, Gombo, Oignon nouveau, Potimarron, Rutabaga, Salicorne,
  Salsifis, Mâche, Scarole).
- 4 fruits (Groseille, Kaki, Nectarine, Tamarin).
- 5 viandes/abats (Cheval, Cœur de bœuf, Foie gras, Museau de bœuf,
  Viande des Grisons).
- 3 volailles/gibier à plumes (Chapon, Pigeon, Faisan).
- 3 poissons — allergène Poissons (Morue, Limande, Rascasse).
- 5 crustacés/mollusques (Écrevisse, Crevette grise — Crustacés ;
  Coque, Escargot, Seiche — Mollusques).
- 7 crémerie (Edam, Gouda, Mimolette, Maroilles, Mont d'or — allergène
  Lait ; Kéfir, Yaourt à la grecque).
- 1 boulangerie (Gressin — allergène Gluten).
- 3 sauces (Aïoli — allergène Œufs ; Sauce vinaigrette ; Houmous —
  allergène Graines de sésame).
- 7 assaisonnements (Huile de maïs/pépins de raisin/palme ; Huile de
  soja — allergène Soja ; Olives noires/vertes ; Vin rosé (cuisine) —
  allergène Sulfites).
- 2 légumineuses (Haricots flageolets, Lentilles blondes).

Mêmes conventions que la première passe : clé anglaise dans
catalog-en-keys.ts, translation.json régénéré via
`tsx scripts/generate-catalog-i18n.ts`, aucune migration nécessaire.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 17:07:34 +02:00
1b787d0cd2 feat(catalog): étoffe le catalogue d'ingrédients à partir de la table Ciqual 2025
Source : « Table des aliments moyens Ciqual 2025 » (ANSES, ciqual.anses.fr),
utilisée comme source de noms pour combler des trous du catalogue plutôt
qu'importée telle quelle — le fichier est une table de correspondance
"aliment moyen ↔ aliments contributeurs" très granulaire (états de cuisson,
produits de marque, boissons/snacks industriels), pas directement
réutilisable comme liste d'ingrédients génériques pour une app de recettes.

55 nouveaux ingrédients ajoutés à reference-seed-data.ts, catégorisés et
allergène-tagués selon les conventions existantes :
- 26 fromages régionaux français (Brie, Camembert, Roquefort, Munster,
  Reblochon, Cantal, Beaufort, Saint-Nectaire, Cancoillotte, Tomme,
  Époisses, etc.) — allergène Lait.
- 18 charcuteries/abats/gibiers (Andouille, Boudin blanc/noir, Rillettes,
  Foie de veau, Ris de veau, Tripes, Cerf, Chevreuil, Sanglier…).
- 4 volailles (Caille, Pintade, Oie, Foie de volaille).
- 2 poissons — allergène Poissons (Anguille, Carrelet).
- 2 crustacés/mollusques — allergène Crustacés/Mollusques (Araignée de
  mer, Bigorneau).
- 3 fruits/légumes (Cardon, Cassis, Canneberge).

Chaque ajout a sa clé anglaise dans catalog-en-keys.ts ; translation.json
régénéré via `tsx scripts/generate-catalog-i18n.ts` (pas d'édition
manuelle). Pas de migration Prisma nécessaire — pure donnée de référence,
upsertée par `seedReferenceData` au prochain déploiement/seed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 16:34:19 +02:00
c29648e293 fix(ci): corrige lint/tests cassés par la migration portions
- Dialog.tsx : remplace le div role="dialog" par un <dialog> natif
  (showModal) — corrige lint/a11y/useSemanticElements, récupère
  gratuitement le piège de focus et l'Échap natifs. Le clic extérieur
  est rebranché en imperative addEventListener pour éviter
  lint/a11y/useKeyWithClickEvents sur un élément non interactif.
- RecipePickerDialog.tsx : retire l'autoFocus (lint/a11y/noAutofocus),
  ordre des imports/formatage corrigés par `biome check --write`.
- apps/api/test/planning.test.ts, apps/api/test/recipe.test.ts :
  les fixtures qui créent un `PlanningItem` directement via Prisma
  n'avaient pas le nouveau champ `portions` requis.
- apps/web/cypress/e2e/planning-page.cy.ts : ajoute `portions` aux
  items mockés pour rester fidèle au contrat `PlanningItemView`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 14:56:50 +02:00
8bdbfda3ae feat(planning): dialog de sélection de recette, création de planning, assignation avec portions
Ajoute le chaînon manquant entre le catalogue de recettes et le planning
hebdomadaire :

- Backend : `PlanningItem.portions` (nouvelle colonne + migration),
  `POST /planning/items` / `DELETE /planning/items/:id` (créent la
  semaine de planning à la volée si besoin), `GET /recipes` gagne les
  filtres `ingredientIds`/`dietIds` (ET) en plus de `suitableForHousehold`
  (déjà préparé).
- Frontend : nouveau `Dialog` générique (premier modal de l'app),
  `RecipePickerDialog` qui réutilise le même affichage que le catalogue
  (`RecipeTabs`/`RecipeTable`) avec recherche par nom, filtre ingrédients,
  filtre régime alimentaire, toggle "convient à tout le foyer", puis une
  étape de saisie du nombre de portions.
- `PlanningPage` : le bouton "+" de chaque case ouvre le dialog, le
  bouton "✕" retire la recette (optimiste, avec rollback si l'appel
  échoue), les portions s'affichent sur chaque chip.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 14:44:02 +02:00
ffb1e06130 chore(api): retire l'intégration Cucumber/Gherkin
Retire la suite BDD Gherkin (apps/api/features/) — pas cassée, mais
mise de côté pour l'instant en même temps que l'intégration côté
Cypress (voir PR #23, fermée). À reprendre plus tard, probablement
avec une approche différente des deux côtés.

Vérifié avant retrait que la suite Mocha (apps/api/test/) couvre déjà
les mêmes scénarios, domaine par domaine (santé, référentiels,
authentification, profil, foyer, planning, recettes, préférences) —
souvent avec plus de détail (ex. reference.feature ne couvre pas
/reference/ingredients, contrairement à test/reference.test.ts). Pas
de perte de couverture réelle.

- apps/api/features/ (scénarios .feature + step-definitions + world/hooks)
  et cucumber.cjs supprimés
- apps/api/package.json : retire le script `test:bdd` et la
  dépendance @cucumber/cucumber (@faker-js/faker conservé — aussi
  utilisé par la suite Mocha)
- .github/workflows/ci.yml : retire l'étape `pnpm --filter api test:bdd`
- README : retire toute mention Cucumber/Gherkin/BDD (section dédiée,
  commandes, description du monorepo, note faker.js)

Le côté Cypress était déjà propre sur main (l'intégration testée sur
la branche feat/cypress-cucumber n'a jamais été mergée — PR fermée,
branche supprimée).
2026-08-19 12:41:17 +02:00
kyuno053
4b988706cc
fix(docker): n'installe plus/ne build plus Cypress dans l'image de prod (#22)
- README : le dev local cible désormais `docker compose up -d postgres`
  explicitement. `docker-compose.yml` définit aussi le service `app`
  (celui que Portainer build en prod) — sans nom de service,
  `docker compose up -d` construisait et démarrait aussi ce conteneur en
  local, déclenchant un `pnpm install` sur tout le monorepo (donc le
  `cypress` d'apps/web, avec son téléchargement de binaire) pour une image
  dont le dev local n'a pas besoin.
- Dockerfile (apps/api) : `CYPRESS_INSTALL_BINARY=0` sur le stage `build`
  — évite le téléchargement du binaire Cypress (~200 Mo, Electron) à
  chaque build de l'image de prod, qu'il vienne de `docker compose` ou du
  pipeline de release. Le package `cypress` lui-même reste installé (vraie
  entrée du lockfile) ; `pnpm prune --prod` puis un wipe+reinstall complet
  ont été testés pour l'éliminer aussi, mais se sont révélés dangereux
  dans ce monorepo pnpm (prune ignore les node_modules des autres
  packages du workspace ; le wipe+reinstall a fait sauter le binding
  natif compilé d'argon2, crash silencieux au démarrage du conteneur) —
  pas justifié pour ~10 Mo de JS inerte une fois le téléchargement du
  binaire évité.
- apps/api/package.json : déplace `prisma` (le CLI, invoqué directement
  par le `CMD` du conteneur pour `migrate deploy`) de devDependencies
  vers dependencies — classification plus correcte indépendamment du
  point ci-dessus.

Vérifié par un vrai `docker build` + `docker run` contre un Postgres réel
(migrations, seed, démarrage du serveur, signup avec hash argon2 réel).
2026-08-19 09:45:04 +02:00
kyuno053
c34eaa89d0
chore(web): session de polish global — version, checkbox, danger zone, icônes (#20)
* chore(web): session de polish global — version, checkbox, danger zone, icônes

- Affiche le numéro de version (package.json, injecté via Vite) en bas de
  la sidebar, masqué en mode collapse et en mobile.
- Factorise les checkbox/radio dupliqués (AllergySelect, DietTagSelect,
  IngredientPicker, UserPreferencesPage) en composants partagés
  CheckboxOption/RadioOption (components/ui/), et inverse le layout pour
  que la case soit à gauche du label.
- Teinte la "zone de danger" de suppression de compte en rouge (fond +
  bordure), pas seulement le bouton.
- Migre les icônes de navigation générale vers lucide-react (nav-icons.tsx
  devient un fichier de ré-export) ; les pictogrammes d'ingrédients métier
  restent en SVG custom (pas d'équivalents fins côté lucide).

Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle manuelle (sidebar desktop/collapsed/mobile, light/dark).

* feat(web): icônes d'ingrédients depuis foodiconpack.com + page de crédits

- Remplace 19 des 22 pictogrammes génériques d'ingrédients par des icônes
  curées du pack gratuit "Common ingredient icons"/"Common Utensils" de
  foodiconpack.com (CC BY 4.0) : carotte, pomme, basilic, bœuf, poulet,
  saumon, crevette, riz, pois chiches, amandes, lait, cheddar, œufs,
  cannelle, miel, huile d'olive, bière, marmite, sucre.
- BREAD/DOUGH/SPROUT restent en SVG custom : pas d'équivalent net dans le
  pack (packs "ingrédients"/"ustensiles"/"plats"/"boissons" vérifiés).
  Architecture inchangée : `icon` reste un enum de 22 valeurs partagées en
  base (pas de migration, pas de mapping par ingrédient — cf. le
  commentaire du fichier sur l'historique emoji→enum générique).
- Nouveau wrapper FilledIcon (fill="currentColor", viewBox 2048) à côté du
  wrapper Icon existant (stroke) — les deux stylent au même endroit via
  CSS, donc le mélange des 19+3 icônes reste visuellement homogène.
- Ajoute /parametres/credits (CreditsPage) créditant foodiconpack.com et
  liant la licence CC BY 4.0, requis par la licence des icônes utilisées ;
  nouvelle entrée de nav "Crédits" (icône lucide Info).

Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle (grille des 22 icônes dans le picker, page crédits).

* feat(web,api): zone dangereuse rouge, préférences élargies, onglet favoris par défaut, e2e recettes, catalogue en uid+i18n

- Zone dangereuse (compte) : le bouton "Supprimer mon compte" est rouge.
- Pages préférences/paramétrage : contenu centré et élargi (32rem -> 56rem)
  au lieu de coller à gauche sur un écran large.
- Page recettes : l'onglet "Favoris" est sélectionné par défaut.
- Ajout de apps/web/cypress/e2e/recipes.cy.ts (onglets, recherche, sélection
  master-detail, favori, suppression, lien nouvelle recette).
- Catalogue de référence (ingrédients/régimes/allergènes) : la colonne
  `name` (le libellé français, utilisé comme clé unique) devient `key`, un
  slug stable et opaque au sens produit (ex. "vegetarien", "boeuf_hache").
  Le libellé lui-même déménage entièrement côté client, dans
  apps/web/src/locales/fr/translation.json sous le namespace `catalog.*`,
  résolu via `t(\`catalog.ingredients.${key}\`)` etc. — même schéma que
  IngredientCategory/IngredientSubcategory. Migration Prisma
  (rename + backfill des ~456 lignes déjà seedées), seed/service/tests API
  et composants web mis à jour en conséquence.
  - apps/api/src/utils/slugify.ts + scripts/generate-catalog-i18n.ts
    (regénère le fichier de traduction depuis reference-seed-data.ts).
  - 102 tests Mocha + 32 scénarios Cucumber passent contre la base migrée.

Note : cypress run plante dans cet environnement (le processus GPU
Chromium/Electron crash même headless, indépendamment des flags) — les
recipes.cy.ts n'ont pas pu être exécutés ici ; vérifiés par lecture du code
source des composants visés et par un passage manuel dans le navigateur de
prévisualisation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(api): les uids du catalogue sont en anglais, pas des slugs français

reference-seed-data.ts reste rédigé en français (c'est juste le libellé
d'autoring, jamais stocké/exposé), mais la clé stable (`Diet.key`/
`Category.key`/`Ingredient.key`) qu'on en dérive doit elle-même être un
identifiant anglais, indépendant de la langue d'autoring — pas juste le
même texte français passé à slugify().

- apps/api/src/db/catalog-en-keys.ts : dictionnaire écrit à la main
  (label français -> clé anglaise) pour les 5 régimes, 14 allergènes et
  437 ingrédients ; getEnglishKey() lève une erreur explicite si un
  nouvel élément n'a pas encore d'entrée plutôt que de retomber sur un
  slug français silencieux.
- scripts/validate-catalog-en-keys.ts : vérifie que chaque diet/allergène/
  ingrédient de reference-seed-data.ts a une entrée, et que les clés
  anglaises résultantes sont uniques (437/437, 14/14, 5/5 — zéro manquant,
  zéro collision).
- reference-seed-data.ts et scripts/generate-catalog-i18n.ts utilisent
  désormais getEnglishKey() au lieu de slugify(nom français).
- Nouvelle migration (20260818193000_catalog_keys_to_english) qui
  remappe les lignes déjà seedées avec un slug français (par la migration
  précédente) vers leur clé anglaise définitive.
- apps/web/src/locales/fr/translation.json régénéré : catalog.* est
  maintenant indexé par clé anglaise ("vegetarian", "eggs",
  "ground_beef"...), toujours avec le libellé français en valeur.
- Tests/step-definitions mis à jour (getEnglishKey() au lieu de
  slugify()) ; 102 tests Mocha + 32 scénarios Cucumber passent contre la
  base migrée. Vérifié aussi en direct via GET /reference/diets et
  /reference/allergies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(web): crypto.randomUUID plante hors contexte sécurisé, empêchant d'associer un ingrédient

Écran noir + "TypeError: crypto.randomUUID is not a function" au clic sur
une carte d'ingrédient dans le formulaire de recette. crypto.randomUUID()
n'est défini que dans un "contexte sécurisé" (https, ou littéralement le
host "localhost") — il est absent sur une IP locale (test sur un vrai
appareil), dans une WebView Capacitor (l'enrobage mobile prévu pour cette
app), ou en http sur un vrai domaine. RecipeFormPage/StepListEditor s'en
servaient pour générer l'identité React (`key`) de chaque ligne
d'ingrédient/étape en brouillon.

- apps/web/src/lib/client-key.ts : remplace par un générateur qui ne
  touche jamais `crypto` — un compteur + Math.random suffit, cette valeur
  n'a besoin d'être unique que le temps de la session de rendu, jamais
  envoyée au serveur.
- apps/web/cypress/e2e/recipe-form.cy.ts : couvre l'association d'un
  ingrédient (recherche, sélection, exclusion du picker une fois
  sélectionné, retrait), la création et l'édition d'une recette, et un
  test de non-régression dédié qui supprime crypto.randomUUID avant le
  chargement de la page (comme le ferait un vrai contexte non sécurisé)
  pour vérifier que l'ajout de plusieurs ingrédients/étapes ne plante
  plus.

Vérifié en direct dans le navigateur de prévisualisation en supprimant
crypto.randomUUID à la main (reproduit le crash), puis en confirmant que
l'ajout d'ingrédient fonctionne à nouveau après le correctif. cypress run
ne peut toujours pas s'exécuter dans cet environnement (voir le commit
précédent) — non exécutés avec Cypress lui-même, mais vérifiés par
lecture des sélecteurs réels et rejoués à la main dans le navigateur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ci): corrige les specs Cypress cassées par le refactor uid+i18n, applique biome

- onboarding.cy.ts / preferences.cy.ts / recipes.cy.ts mockaient encore
  GET /reference/diets|allergies avec l'ancienne forme {id, name}. Depuis
  les deux derniers commits l'API renvoie {id, key} (uid anglais) et le
  composant résout le libellé via i18n (t(`catalog.diets.${key}`)) — avec
  key manquant, ça affichait littéralement "catalog.diets.undefined" au
  lieu de "Végétarien"/"Omnivore"/etc., faisant échouer cy.select()/
  cy.contains() dans ces 3 specs. Corrigé pour mocker {key: "vegetarian"},
  {key: "peanuts"}, etc.
- recipes.cy.ts : le test "shows a not-found message" utilisait le
  mauvais code d'erreur (4041 au lieu de ErrorCode.RECIPE_NOT_FOUND =
  4045), donc RecipeDetailPanel tombait dans son état d'erreur générique
  au lieu du message "Cette recette n'existe pas." — bug dans mon propre
  test, sans rapport avec le refactor.
- pnpm lint (biome) : les fichiers touchés par le refactor précédent
  avaient quelques soucis de formatage/tri d'imports (des sed multi-
  fichiers, pas d'édition via l'outil habituel) — corrigés par
  `biome check --write`.

Vérifié : ces 3 specs + recipe-form.cy.ts passent maintenant dans le job
CI GitHub Actions (Linux, Cypress s'y exécute réellement — contrairement
à cet environnement Windows sandboxé, voir les commits précédents) ; 102
tests Mocha + 32 scénarios Cucumber toujours au vert en local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:52:12 +02:00
d761a3795f fix(migrations): rend la migration ingredient_taxonomy_rework safe sur des données existantes
La version générée automatiquement par `prisma migrate diff` castait
directement chaque valeur `category` existante (ancien enum à 18
valeurs) vers le nouvel enum à 7 valeurs — échoue pour toute ligne déjà
seedée, puisqu'aucune ancienne valeur n'existe dans le nouvel enum.
En local ça passait inaperçu (reset complet sur une base vide), mais en
production (données déjà seedées) la migration échoue avec "invalid
input value for enum".

Réécrite pour ajouter les nouvelles colonnes avec une valeur par défaut
sûre (aucun cast des données existantes), puis les substituer aux
anciennes — même logique que les défauts `@default(...)` déjà
documentés dans schema.prisma : seedReferenceData() (relancée à chaque
démarrage du conteneur, voir apps/api/Dockerfile) corrige tout de suite
après la vraie catégorie/sous-catégorie de chaque ligne.

Ajoute aussi un DROP TYPE IF EXISTS défensif : une tentative précédente
de cette migration laisse le type IngredientSubcategory orphelin (son
CREATE TYPE s'exécute hors de la transaction qui échoue plus loin), une
nouvelle tentative sans ce garde-fou échouerait différemment ("type
already exists").

Vérifiée en rejouant l'historique complet des migrations sur une base
de test jetable, avec des lignes portant les anciennes valeurs d'enum
insérées à la main pour reproduire exactement l'échec signalé — la
version corrigée s'applique proprement et préserve les id existants
(donc toute vraie ligne RecipeIngredient qui y référence).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 12:47:14 +02:00
78e0f89503 feat(recipes): remplace les emoji par des icônes SVG épurées
Deux usages d'emoji supprimés, remplacés par un vrai jeu d'icônes SVG
(style ligne, cohérent avec layouts/nav-icons.tsx) :

- Catégories/sous-catégories du picker (auparavant préfixées d'un
  emoji dans les libellés) : nouvelles icônes CategoryIcon/
  SubcategoryIcon, une par catégorie/sous-catégorie.
- Icône par ingrédient (auparavant un emoji différent pour chacun des
  437 ingrédients) : remplacé par un petit vocabulaire de ~22
  pictogrammes génériques ("un légume", "une bouteille", "un
  fromage"...) réutilisés selon la nature de l'ingrédient plutôt
  qu'un dessin par ingrédient (irréaliste à la main pour 437 items).

- Schéma : nouvel enum IngredientIcon (22 valeurs), colonne
  Ingredient.icon passe de String? (texte libre) à IngredientIcon
  (non nullable, toujours une valeur générique pertinente désormais).
- reference-seed-data.ts : chaque groupe porte un defaultIcon (calqué
  sur sa sous-catégorie), avec override par ingrédient pour les
  exceptions (ex. les fromages dans "produits laitiers", les jus/cafés
  dans "assaisonnements").
- apps/web/src/features/recipes/ingredient-icons.tsx (nouveau) :
  22 icônes SVG + CategoryIcon/SubcategoryIcon (réutilisent le même
  vocabulaire pour représenter chaque catégorie/sous-catégorie).
- packages/shared : IngredientView.icon devient IngredientIcon (union
  de 22 valeurs) au lieu de string | null.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 12:30:25 +02:00
9775cbe38f feat(recipes): refonte de la catégorisation des ingrédients façon rayons de supermarché
Remplace les 18 catégories plates (mélangeant rayons génériques et
cuisines d'origine — un ingrédient pouvait finir dans "légumes" ou
"cuisine italienne" selon l'angle choisi) par une hiérarchie à 2 niveaux
sur le modèle d'un supermarché français :

- 🥦 Produits frais (légumes, fruits, herbes fraîches)
- 🥩 Boucherie & poissonnerie (viandes, volailles, poissons,
  crustacés & fruits de mer)
- 🥫 Épicerie sèche (féculents, légumineuses, graines & fruits secs,
  autres produits secs/conserves)
- 🍞 Boulangerie (pains, pâtes à cuire — 4 nouveaux ingrédients :
  pâte feuilletée/brisée/à pizza/à tarte sablée)
- 🧈 Crémerie & fromage (produits laitiers, œufs, alternatives
  végétales)
- 🧂 Condiments & épices (épices, sauces, assaisonnements)
- 🍳 Aides culinaires (bases, épaississants, sucres)

- Schéma : nouvel enum IngredientCategory (7 valeurs) + nouvel enum
  IngredientSubcategory (22 valeurs) + colonne Ingredient.subcategory.
  Migration appliquée via reset (données de référence, aucune perte
  réelle) car les anciennes valeurs d'enum n'existent plus dans les
  nouvelles.
- reference-seed-data.ts entièrement réorganisé par (catégorie, sous-
  catégorie), tous les 437 ingrédients recatégorisés un par un.
- packages/shared : INGREDIENT_CATEGORIES (7), INGREDIENT_SUBCATEGORIES
  (22) et INGREDIENT_CATEGORY_SUBCATEGORIES (mapping catégorie →
  sous-catégories, pour piloter le picker) remplacent l'ancienne liste
  plate.
- IngredientPicker : drill-down à 2 niveaux — la ligne de sous-
  catégories (couleur --color-tag, visuellement subordonnée) apparaît
  une fois une catégorie choisie, et se réinitialise au changement de
  catégorie.
- i18n : nouvelles clés recipes.form.category.* (7) et
  recipes.form.subcategory.* (22).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:47:47 +02:00
5890c62462 fix(recipes): overflow des préférences + régimes liés aux ingrédients
- Fix overflow horizontal sur /parametres/preferences : <fieldset> a un
  min-width: min-content par défaut du navigateur, ce qui empêchait la
  grille de l'IngredientPicker de wrapper (page entière poussée à
  ~3100px de large). Reset min-width: 0 sur .disliked-ingredients-field.
- Recatégorise les laits/crèmes végétaux (coco, amande, avoine) de
  PRODUITS_LAITIERS_OEUFS vers LIQUIDES_BOISSONS — ce ne sont pas des
  produits laitiers.
- Nouveau modèle IngredientDiet (many-to-many ingrédient <-> régime) :
  quels régimes (Végétarien, Végan, Pescétarien) chaque ingrédient
  respecte. Omnivore volontairement absent (trivial) et Sans gluten
  aussi (déjà dérivable de l'allergène Gluten existant).
- reference-seed-data.ts : chaque groupe de catégorie porte un
  defaultDiets, avec dietNames en override pour les exceptions
  (fromages, viandes, poissons, sauces à base d'œuf/poisson...).
- IngredientView.diets exposé par /reference/ingredients et /recipes,
  affiché via DietBadges dans IngredientPicker et IngredientRow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:01:57 +02:00
ea86a4a7f1 feat(recipes): catégories d'ingrédients + nouveau sélecteur
- Prisma: enum IngredientCategory (18 valeurs) + Ingredient.category,
  migration appliquée
- reference-seed-data.ts restructuré en 18 groupes de catégories
- packages/shared: INGREDIENT_CATEGORIES + IngredientView.category
- API: category exposé par /reference/ingredients et /recipes
- Web: nouveau IngredientPicker (chips catégories + recherche + grille
  de cartes) remplaçant IngredientAutocomplete, branché dans le
  formulaire de recette et le champ aliments-pas-aimés
- i18n: clés recipes.form.category.* et libellés du picker
- Fix test reference.test.ts pour la nouvelle forme d'IngredientView

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 10:24:12 +02:00
acab18ac4a feat(recipes): catalogue v2 - visibilité, favoris, régimes et catalogue d'ingrédients exhaustif
Recipe catalog v2:
- Recipe gagne visibility (PERSONAL/HOUSE/PUBLIC), authorId, authorHouseId
- Favoris par utilisateur (RecipeFavorite), régimes associés (RecipeDiet)
- Aliments "pas aimés" par utilisateur (UserProfileDislikedIngredient),
  distinct des allergies médicales
- API: GET /recipes?tab=favoris|perso|foyer|publique avec contrôle d'accès
  complet, POST/DELETE /recipes/:id/favorite, édition/suppression réservées
  à l'auteur (403 NOT_RECIPE_AUTHOR), GET/PATCH /profile/disliked-ingredients
- Frontend: vue maître-détail (onglets + tableau + panneau détail),
  formulaire enrichi (visibilité, régimes), section préférences pour les
  aliments pas aimés

Catalogue d'ingrédients de référence:
- Extension du seed de 39 à ~430 ingrédients (viandes, poissons/fruits de
  mer, légumes, fruits, féculents, condiments/sauces, épices/herbes, pains
  à sandwich, cuisines italienne/asiatique/mexicaine/maghrébine, liquides
  et boissons de cuisine, bouillons/fonds)
- Chaque ingrédient lié à ses allergènes UE (IngredientAllergy) — les 14
  allergènes réglementaires restent tous couverts
- Seeding optimisé en requêtes groupées (createMany/diff ciblé) plutôt
  qu'un upsert par ligne, pour garder resetDatabase() rapide en test

Tests: 102 tests Mocha + 32 scénarios BDD, tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 09:29:11 +02:00
fff8c0da26 fix(api): seed reference data (diets/allergies) on container start
Found on http://batch.dev.kyuno.fr/: GET /reference/diets and
/reference/allergies both returned [] — onboarding's "régime
alimentaire" step and the profile's food-preferences tab had nothing to
show. Cause: the Docker image's CMD only ran `prisma migrate deploy`
(schema), never the seed that populates Diet/Category/Allergy.

Adds src/scripts/seed-runtime.ts — a runtime-only seed entry point
(distinct from prisma/seed.ts, the dev-time one wired to `prisma db
seed`/`prisma migrate reset` via tsx importing from ../src, which isn't
shipped in the runtime image). This one lives under src/ so tsc compiles
it into dist/ alongside everything else, and runs via plain `node`,
reusing the same idempotent seedReferenceData() (upserts by unique name)
already used by prisma/seed.ts and test-support/reset-db.ts.

Dockerfile CMD now runs it between migrate deploy and starting the
server — safe on every container start/restart, confirmed idempotent
locally (no duplicates, no error on a second run against an
already-seeded database).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 00:04:14 +02:00
266f540d88 fix(api): make the session cookie's Secure flag overridable
Found on http://batch.dev.kyuno.fr/: login/signup succeeded (200/201,
profile in the body) but every subsequent request 401'd. Cause: the
session cookie is `secure: NODE_ENV === "production"`, and
docker-compose.yml sets NODE_ENV=production regardless of whether the
deployment actually has TLS in front of it. A Secure cookie is silently
never sent back by the browser over plain HTTP — no error, just a cookie
that never round-trips.

Adds COOKIE_SECURE, independent from NODE_ENV, to override the flag per
deployment. Unset (default) keeps prior behavior — secure in production.
Set COOKIE_SECURE=false only for a deployment reachable over plain HTTP
(no TLS yet), like this dev instance.

Verified locally: docker compose up with COOKIE_SECURE=false persists
and round-trips the cookie (signup -> /auth/me 200); without it, the
cookie still gets Secure as before. Full pnpm --filter api test / test:bdd
suites still pass (66 + 25 scenarios).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:49:05 +02:00
c2cc773c7b Fusionner web+api en une image, séparer la CI, ajouter la release
- express-tools: ExpressServer.serveStaticFrontend() sert le build du
  frontend (assets + fallback SPA), monté après les routes API et avant
  le 404 JSON. Opt-in via FRONTEND_DIST_DIR (uniquement défini dans
  l'image Docker) — le dev natif (dev:api/dev:web) est inchangé.
- apps/api/Dockerfile: build aussi apps/web, embarque son dist dans le
  runtime ; corrige au passage l'oubli de packages/date-tools. Supprime
  apps/web/Dockerfile et nginx.conf (plus de conteneur nginx séparé).
- docker-compose.yml: un seul service "app" (postgres + app), un seul
  port APP_PORT, plus de WEB_PORT/CORS_ORIGIN à coordonner entre deux
  origines. Garde `build:` (pas de registre — Portainer build depuis le
  repo Git).
- ci.yml: éclate le job unique lint-and-test+e2e en 4 jobs indépendants
  (lint/test/build/e2e), sans chaînage, déclenchés sur chaque push
  (toute branche) + PR vers main.
- release.yml (nouveau): sur tag vX.Y.Z, sanity-build de l'image Docker,
  GitHub Release avec changelog auto-généré, puis notification best-effort
  du webhook Portainer (secret PORTAINER_WEBHOOK_URL).
- README: documente le conteneur unique et le pipeline de release.
2026-08-17 23:11:26 +02:00
9dd90359c9 Tests: dates fixes pour Planning + refonte des radio/checkbox
- apps/api: les tests Planning (mocha et cucumber) lisaient l'horloge
  systeme (new Date()/DateTime.utc()) pour construire leurs fixtures et
  interroger /planning, ce qui les rendait non deterministes. Ajoute
  test-support/reference-date.ts (TEST_REFERENCE_DATE, une date UTC
  fixe) et l'utilise dans planning.test.ts / planning.steps.ts a la
  place du systeme.

- apps/web: nouveau style global pour tous les radio/checkbox de
  l'app (theme-select, allergy-select, onboarding) - "carte
  selectionnable" : le controle natif reste reel/accessible mais
  visuellement cache, toute la ligne devient la surface interactive
  (bordure + fond teinte + coche au survol/selection). Corrige au
  passage le bug de fond qui causait le desalignement des radios sur
  /parametres/preferences-utilisateur (la regle generique
  input, select { width: 100% } de profile-forms.scss s'appliquait
  aussi aux checkbox/radio) et une regression de font-weight ou les
  lignes non selectionnees du theme apparaissaient en gras comme si
  elles l'etaient.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 22:36:02 +02:00
3acde696f5 API: module preferences — GET/PATCH /preferences (step 2/4)
- getPreferences: SYSTEM par défaut si aucune ligne (même logique que
  dietId/allergies : absent = valeur par défaut, pas une omission)
- updatePreferences: upsert (crée la ligne au premier PATCH)
- Tests Mocha + Cucumber : 401, valeur invalide, défaut, création à la
  volée, cloisonnement entre profils
2026-08-17 15:55:45 +02:00
e3811dc280 Shared + migration: table user_preference (thème clair/sombre/système) (step 1/4)
- Modèle Prisma UserPreference (1-1 avec UserProfile, onDelete Cascade),
  enum ThemePreference (LIGHT/DARK/SYSTEM, défaut SYSTEM)
- packages/shared: THEME_PREFERENCES/ThemePreference, PreferencesView,
  updatePreferencesSchema
2026-08-17 15:55:18 +02:00
9b2b2c2e28 API: GET /planning?date= remplace GET /planning/current (step 2/4)
- getPlanningForDate(houseId, date: DateTime) — paramétré au lieu de
  toujours "aujourd'hui", même logique de recherche sinon
- GET /planning?date=YYYY-MM-DD, validation de forme (zod) puis de
  validité calendaire (parseDateOnly, 400 VALIDATION_ERROR sinon) —
  un seul endpoint générique au lieu de deux qui se recouvrent
- Tests Mocha + Cucumber adaptés, + cas date manquante/malformée/
  impossible et "semaine différente d'aujourd'hui"
2026-08-17 14:17:46 +02:00
3363cfad75 Tests API: couverture Mocha + Cucumber pour le foyer et la suppression de compte (step 4/8)
- house.test.ts réécrit (le foyer n'est plus auto-créé) + POST /house,
  POST /house/join, POST /house/leave, DELETE /house/current,
  DELETE /house/members/:id
- auth.test.ts: signup renvoie houseId=null, DELETE /auth/me (mauvais
  mot de passe, suppression, transfert d'admin)
- planning.test.ts/steps.ts: création explicite du foyer (POST /house)
- household.feature: scénarios créer/rejoindre/quitter/supprimer/
  retirer un membre, via un second agent (CustomWorld.secondAgent)
- auth.feature: scénarios de suppression de compte
2026-08-17 10:40:23 +02:00
7af98756dc API: le foyer n'est plus créé automatiquement à l'inscription + suppression de compte (step 3/8)
- signup() ne crée plus de House — houseId démarre à null, le foyer
  devient une étape optionnelle de l'onboarding (créer/rejoindre/passer)
- deleteAccount(): revérifie le mot de passe, transfère l'adminship ou
  supprime le foyer si nécessaire (leaveCurrentHouse), puis supprime
  le profil (cascade sur les allergies)
- DELETE /auth/me — nouvelle route, gated par mot de passe
- clearCookie n'envoie plus maxAge (corrige un warning de dépréciation
  Express, déjà latent sur /auth/logout)
2026-08-17 10:38:47 +02:00