Commit graph

164 commits

Author SHA1 Message Date
ae80537b7c 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>
2026-08-21 11:15:17 +02:00
23d4fd4966 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>
2026-08-21 10:53:17 +02:00
eab28aa017 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>
2026-08-21 10:37:58 +02:00
c5282ba4a7 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>
2026-08-21 10:32:52 +02:00
22582536a6 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>
2026-08-21 10:19:27 +02:00
37a044a267 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>
2026-08-21 10:11:57 +02:00
a53e8aff4a 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>
2026-08-21 09:26:59 +02:00
3160ed2dac 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>
2026-08-21 09:26:32 +02:00
d4d1bc38b9 Merge PR #60 (docs + catalogue ingrédients) dans #59
# Conflicts:
#	README.md
2026-08-21 08:38:47 +02:00
bc1f14f1cc 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).
2026-08-21 08:25:12 +02:00
4f509865c1 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.
2026-08-21 08:24:58 +02:00
c6553dc9a3 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>
2026-08-21 08:14:37 +02:00
8475dd3d2d 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>
2026-08-21 08:14:37 +02:00
9083770d57 fix(recipes): corrige plusieurs bugs d'import TheMealDB
- Les instructions TheMealDB numérotées sur leur propre ligne ("1\n\ntexte...\n\n2\n\ntexte...") créaient des étapes parasites ne contenant qu'un chiffre — filtrées désormais (#52).
- Un ingrédient compté sans mot d'unité dans le texte source (ex. "4 Egg Yolks") laissait l'import bloqué sur "Importer" indéfiniment, sans indication visuelle de la ligne en cause — matchUnit retombe maintenant sur l'unité générique "piece" quand une quantité a été extraite, et RecipeImportForm/RecipeFormPage surlignent désormais toute ligne dont l'unité manque, avec un message explicite (#53).
- Ajout de INGREDIENT_LABEL_SYNONYMS_EN pour reconnaître des formulations alternatives fréquentes chez les sources anglophones ("vanilla pod" en plus de "vanilla bean") sans élargir INGREDIENT_LABELS_EN à un tableau pour ses ~550 entrées (#54).
- Effet de bord découvert en vérifiant #53 de bout en bout : deux lignes source résolues vers le même ingrédient catalogue (ex. "Egg Yolks"/"Eggs" -> "Œuf") faisaient planter la création en 500 (contrainte unique recipe_id+ingredient_id) au lieu d'un 400 propre. createRecipeSchema rejette maintenant les ingredientId en double, et le formulaire d'import surligne les doublons avant même de soumettre.

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

Closes #52, #53, #54

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 08:14:37 +02:00
kyuno053
f7d7664397
Merge pull request #51 from kyuno053/feat/unify-source-detail-view
fix(recipes): retire l'import manuel, le planning importe seul
2026-08-21 00:09:30 +02:00
85fd9bae7d feat(planning): le picker prévisualise avant de confirmer, formulaire de revue intégré
Trois ajustements successifs sur le dialogue de sélection de recette
(RecipePickerDialog), demandés en continu après le premier correctif
de débordement :

1. Dialogue élargi et à hauteur fixe (95vw plafonné à 85rem, 80vh) au
   lieu de dépendre du contenu, avec la répartition liste/détail
   redéfinie en fractions du dialogue lui-même (3fr/2fr) plutôt qu'en
   vw — cette dernière suivait la largeur du viewport, sans rapport
   avec la largeur désormais fixe du dialogue.

2. Le formulaire de revue d'import (ex-ImportRecipePage) est extrait
   dans un composant partagé, RecipeImportForm — toujours monté en
   page autonome (route directe/rechargement), mais désormais aussi
   intégré comme une étape du dialogue lui-même quand un item de
   source a besoin d'une résolution manuelle, au lieu de naviguer et
   perdre le contexte du picker (recherche, filtres, créneau).

3. Cliquer sur une recette dans le dialogue ne fait plus que la
   sélectionner/prévisualiser (RecipeDetailPanel, comme /recettes) —
   plus de saut automatique vers l'étape suivante. Un nouveau pied de
   dialogue (Dialog.tsx gagne une prop ) porte Confirmer/
   Fermer : Confirmer agit sur la sélection en cours (recette réelle
   → étape portions existante ; item de source pas encore importé →
   import transparent ou formulaire intégré, point 2). Les onglets
   réguliers gagnent leur propre paire maître-détail (RecipeTable +
   RecipeDetailPanel, showActions=false) sur ce même modèle ; les
   onglets source prévisualisent désormais aussi les items déjà
   importés en interne (RecipeSourcesPanel), plus de saut direct.

Cypress (planning.feature/planning.ts) mis à jour en conséquence :
sélectionner puis confirmer sont deux étapes distinctes, le clic sur
la ligne ne déclenche plus rien tout seul.

Bug pré-existant trouvé en testant en direct (sans rapport avec ce qui
précède) : l'import d'une recette source plante avec une contrainte
d'unicité Prisma dès que deux lignes d'ingrédient se résolvent au même
ingrédient catalogue — signalé séparément (tâche en arrière-plan), pas
corrigé ici.

Vérifié en direct (navigateur, comptes de test) : sélection sans saut
d'écran, pied de dialogue activé/désactivé correctement, Confirmer sur
un item de source non résolu bascule vers le formulaire intégré,
Fermer ferme bien le dialogue.

pnpm exec tsc -b --force (web) — propre.
pnpm exec biome check — propre.
pnpm --filter web build — propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 00:05:17 +02:00
260bc3dc05 style(planning): dialogue de sélection encore un peu plus large
92vw/75rem -> 95vw/85rem — vérifié en direct, dialogue passe de 1178px à
1216px sur un viewport 1280px.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 23:26:51 +02:00
b0c1d46acc style(planning): répartit la largeur du dialogue par rapport à lui-même
.recipes-page__catalog dimensionne son panneau de détail avec
minmax(35vw, 38vw) — une fraction du *viewport*, cohérent sur /recettes
(cette grille occupe quasiment toute la page) mais sans rapport avec ce
dialogue, désormais de largeur fixe (92vw plafonné à 75rem, voir commit
précédent) : à un viewport large, 38vw dépasse largement la largeur réelle
du dialogue et déséquilibre la répartition liste/détail.

Remplacé, sous .recipe-picker-dialog uniquement, par un ratio 3fr/2fr
relatif au dialogue lui-même — la répartition (60/40) reste identique
quelle que soit la largeur de l'écran, vérifié en direct à 1280px et
1600px de large (ratio mesuré 1.50 dans les deux cas).

pnpm --filter web build — propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 23:24:50 +02:00
933882a527 style(planning): dialogue de sélection plus large, hauteur fixe 80vh
Suite au correctif précédent (débordement du catalogue) : le dialogue lui-
même passe de 56rem/hauteur-au-contenu à 92vw plafonné à 75rem, avec une
hauteur fixe de 80vh (au lieu du simple plafond hérité de Dialog.tsx) —
la zone de navigation garde désormais une taille généreuse et constante
quel que soit l'onglet/la quantité de contenu, plutôt que de dépendre de
ce qui tient à l'écran.

Vérifié en direct (navigateur, 1280×800) : dialogue 1178×640px (92vw ×
80vh), catalogue/tableau/détail correctement bornés à l'intérieur.

pnpm --filter web build — propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 23:21:08 +02:00
fc8f38afde fix(planning): corrige le débordement du dialogue de sélection pour les sources
Le dialogue de sélection de recette (RecipePickerDialog) affichait bien les
données renvoyées par le serveur, mais rien ne s'affichait dans la fenêtre
tant que le nombre d'items dépassait sa hauteur — reproduit en direct via
le compte de test : la tab TheMealDB (25 items, contrairement aux quelques
recettes perso habituelles) rend le bug visible alors qu'il touchait déjà
le tableau simple des autres onglets, juste sans jamais assez de lignes
pour le déclencher.

Cause : .recipes-page__catalog et .recipe-table-wrap se dimensionnent via
flex: 1; min-height: 0, ce qui ne fonctionne que si leur parent est
display: flex — vrai sur /recettes (.recipes-page l'établit), faux dans ce
dialogue où le contenu est monté directement dans .dialog-panel__body
(Dialog.tsx), un simple conteneur en flux bloc. Sans ce contexte, la zone
catalogue grossissait à la taille de son contenu (1499px mesuré en direct
contre 605px de hauteur réellement disponible) au lieu d'être bornée et
scrollable dans ses propres limites.

Correctif scoping .dialog-panel__body en display: flex; flex-direction:
column uniquement sous .recipe-picker-dialog (tous les autres appelants de
Dialog gardent le flux bloc par défaut), plus flex: 1; min-height: 0 sur
.recipe-table-wrap pour qu'il profite du même contexte.

Vérifié en direct (navigateur, compte de test, tab TheMealDB) : la zone
catalogue passe de 1499px à 450px (borné), les lignes redeviennent
visibles/cliquables sans affecter les autres onglets ni l'étape de
confirmation des portions (rendue par un Dialog séparé, sans la classe
recipe-picker-dialog). Tenu aussi en viewport réduit (1024x640).

pnpm exec tsc -b --force (web) — propre.
pnpm --filter web build — propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 23:19:21 +02:00
f24b289cd4 fix css 2026-08-20 23:17:25 +02:00
73ae8169a1 fix(recipes): retire l'import manuel, le planning importe seul
Correction de comportement sur la gestion des recettes de sources
externes — l'implémentation précédente avait dérivé d'une lecture
erronée du besoin :

- Plus aucun bouton d'import nulle part. Parcourir une source
  (RecipesPage, hors planning) ne fait plus jamais que prévisualiser
  — RecipeDetailPanel n'affiche plus de lien "Importer cette
  recette", seulement un bouton icône discret vers la page d'origine
  quand la recette en a une (nouveau .recipe-detail-panel__source-link,
  même emplacement que l'étoile favori).
- Une recette externe n'est importée dans la base qu'au moment où
  quelqu'un l'ajoute effectivement à son planning — jamais avant.
  RecipePickerDialog.handleSelectDraftItem est désormais le seul
  endroit de toute l'appli qui importe quoi que ce soit : cliquer sur
  un item pas encore importé y déclenche une tentative d'import
  transparente (POST /sources/.../import puis POST /planning/items),
  sans écran intermédiaire, dès que rien ne manque
  (tryBuildCompleteImport, nouveau apps/web/src/features/recipes/
  recipe-import-draft.ts). Seul un ingrédient non résolu (ou une
  erreur réseau) fait encore basculer vers l'écran de revue existant
  (ImportRecipePage), pré-rempli, pour compléter ce qui manque.
- RecipeSourcesPanel gagne onSelectDraftItem (remplace planningSlot,
  qui n'a plus de raison d'être puisqu'il n'y a plus de lien d'import
  à qui le transmettre) : quand ce callback est fourni
  (RecipePickerDialog uniquement), un item pas encore importé n'est
  plus prévisualisé sur place, il est remonté tel quel à l'appelant.

Tests :
- planning.feature : le scénario existant retire l'étape "je clique
  le lien Importer cette recette" (redirection désormais automatique
  puisque le draft de test a un ingrédient non résolu) ; nouveau
  scénario pour le chemin transparent (draft entièrement résolu,
  aucun écran de revue).
- recipe-sources.feature : le scénario qui important depuis /recettes
  (hors planning) est supprimé — cette capacité n'existe plus hors
  planning. Le scénario de deep-link vérifie maintenant l'absence du
  bouton d'import et la présence du lien discret.
- pnpm exec tsc -b --force (web) — propre.
- pnpm exec biome check — propre.
- pnpm --filter web build — propre.
- Cypress non exécutable localement sur cette machine (crash GPU
  Electron connu) — scénarios vérifiés par relecture attentive
  contre le markup/les clés i18n réels ; CI (GitHub Actions) fera
  foi à l'exécution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 21:14:10 +02:00
kyuno053
0f5abb1749
Merge pull request #49 from kyuno053/feat/unify-source-detail-view
feat(recipes): unifie l'affichage des recettes externes avec les recettes réelles
2026-08-20 21:06:51 +02:00
4381e63045 feat(recipes): chaque source activée devient sa propre tab
Nouvelle correction demandée sur cette PR : l'onglet générique
« Sources » (avec un <select> interne quand le foyer en a activé
plusieurs) devient une tab à part entière par source activée — au même
niveau que Favoris/Perso/Foyer/Publique, plus transparent qu'un
sélecteur caché dans un sous-menu.

- `RecipeTabs` accepte désormais `sources: SourceView[]` et rend une
  tab par source (icône propre à la source si elle en a une, sinon
  l'icône générique `SourcesIcon` ; libellé = le nom réel de la
  source, pas une clé i18n). Nouveau type `RecipesPageTab` en
  `RecipeTab | "source:<key>"`, avec `sourceTabValue`/
  `parseSourceTabValue`/`isSourceTab` comme seul point d'assemblage/
  lecture de ce format.
- Nouveau hook partagé `useEnabledSources` (déplacé hors de
  `RecipeSourcesPanel`, maintenant utilisé par `RecipesPage` ET
  `RecipePickerDialog` pour construire leurs tabs).
- `RecipeSourcesPanel` simplifié : `sourceKey` devient une prop requise
  (fournie par la tab elle-même) au lieu d'un état interne avec son
  propre sélecteur — plus de `<select>`, plus de message « aucune
  source activée » (une tab qui n'existe pas ne peut plus être
  cliquée). Remonté via `key={sourceKey}` par l'appelant au changement
  de tab, même convention que `RecipePickerDialog`/`CalendarPopover`
  ailleurs dans l'app.
- Un bug distinct trouvé en écrivant ce changement : passer tel quel
  `initialSelection` (dérivé de l'URL) au panneau nouvellement monté
  en changeant directement de tab source à tab source aurait fait
  prévisualiser l'ancien item contre la nouvelle source. Gardé en ne
  transmettant `initialSelection` que lorsqu'il appartient réellement
  à `activeSourceKey`.

Aucun changement backend.

Tests :
- Vérifié manuellement en local (foyer avec TheMealDB activé) :
  tab dédiée dans /recettes et dans le sélecteur du planning, parcours
  d'un item, aperçu unifié, aucune régression console.
- Cypress : `recipe-sources.feature`/`planning.feature` mis à jour
  (« I click the button "Sources" » → « ... "TheMealDB" »), scénario
  « aucune source activée » réécrit pour vérifier l'absence de tab
  plutôt qu'un message dans un onglet qui n'existe plus.
- `pnpm exec tsc -b --force` (web) — propre.
- `pnpm exec biome check` — propre.
- `pnpm --filter web build` — propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 20:31:09 +02:00
kyuno053
174e16bf13
Merge pull request #50 from kyuno053/fix/isolate-test-database
fix(api): isole la base de test de la base de dev
2026-08-20 20:06:31 +02:00
d5172c2c66 Merge branch 'fix/isolate-test-database' into feat/unify-source-detail-view 2026-08-20 20:04:02 +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
368ea08960 feat(recipes): unifie l'affichage des recettes externes avec les recettes réelles
Suite au retour utilisateur sur le plan « onglet Sources » livré
précédemment (#44-#48) : navigation transparente, page recette pour un
item externe, comportement d'ajout au planning déjà importé.

L'onglet « Sources » (RecipesPage) reste un onglet à part (décision
explicite : pas de fusion des listes perso/foyer/publique/externe) —
mais son affichage se comporte désormais « comme si c'était importé » :

- `SourceItemPreviewPanel` est supprimé, fusionné dans
  `RecipeDetailPanel` lui-même (nouvel état `"loaded-draft"`) : un item
  pas encore importé se voit exactement comme une vraie recette — même
  en-tête, même mise en page description/étapes — la seule différence
  étant les actions proposées (« Importer cette recette » là où une
  vraie recette montre Modifier/Supprimer). La liste brute des
  ingrédients et l'indice « non résolu » disparaissent de cette vue :
  cette complexité reste réservée à l'écran de revue d'import
  (ImportRecipePage), pas à un simple aperçu.
- Un item pas encore importé gagne une vraie URL adressable —
  `/recettes/sources/:sourceKey/:externalId` (nouvelle route,
  RecipesPage) — au même titre qu'une vraie recette a `/recettes/:id`.
  Avant, le sélectionner ne changeait que de l'état React local dans
  `RecipeSourcesPanel`, sans URL propre : ni lien direct, ni retour
  arrière/rafraîchissement possibles. `RecipeSourcesPanel` gagne
  `initialSelection`/`onItemSelected` pour rester piloté par cette URL
  sans avoir à connaître le routage lui-même — `RecipePickerDialog`
  (qui prévisualise dans une modale sans URL propre) laisse les deux
  non renseignés et garde son comportement inchangé.
- `onSelectImportedRecipe` (déjà présent) continue de traiter un item
  déjà importé exactement comme une vraie recette — c'est justement ce
  qui rend la navigation transparente pour ce cas.

Le troisième point du retour (vérifier si la recette est déjà en base
avant de l'ajouter au planning, ne rien faire si oui, l'importer sinon)
était déjà le comportement de #48 — inchangé ici, aucune régression:
`RecipePickerDialog` résout un item déjà importé vers sa vraie recette
sans ré-import, et n'importe que les items qui ne le sont pas encore.

Aucun changement backend.

Tests :
- Cypress : nouvelle assertion d'URL dans le scénario « Previews a
  not-yet-imported item » de recipe-sources.feature, et nouveau
  scénario « Deep-links straight to a not-yet-imported item's own
  page » — la CI confirmera.
- `pnpm --filter api test` — 282 tests toujours au vert (aucun
  changement backend).
- `pnpm exec tsc -b --force` (web) — propre.
- `pnpm exec biome check` — propre.
- `pnpm -r build` — propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 19:30:35 +02:00
kyuno053
be0d885269
Merge pull request #44 from kyuno053/fix/json-ld-not-a-browsable-source
fix(recipes): la source générique JSON-LD n'apparaît plus comme source
2026-08-20 18:51:47 +02:00
kyuno053
b383abdfdf
Merge pull request #45 from kyuno053/feat/browse-sources-backend
feat(recipes): parcourir et prévisualiser les sources externes (étape 1/4)
2026-08-20 18:45:45 +02:00
kyuno053
5f4c1c25ed
Merge pull request #46 from kyuno053/feat/browse-sources-ui
feat(recipes): onglet Sources — parcourir les recettes externes (étape 2/4)
2026-08-20 18:40:51 +02:00
kyuno053
c7f63c9834
Merge pull request #47 from kyuno053/feat/import-recipe-review
feat(recipes): écran de revue et finalisation de l'import (étape 3/4)
2026-08-20 18:37:00 +02:00
kyuno053
45661f58b0
Merge pull request #48 from kyuno053/feat/plan-import-flow
feat(planning): ajouter au planning déclenche l'import si besoin (étape 4/4)
2026-08-20 18:32:58 +02:00
81848179bf fix(web-tests): simplifie le scénario planning, retire une vérif redondante/instable
Retire le dernier volet du scénario (rouvrir le sélecteur, rebrowser la
source, vérifier le badge « déjà importée ») — la CI l'a fait échouer
(le badge n'apparaissait jamais sur le second passage) sans qu'une
relecture du code de RecipePickerDialog/RecipeSourcesPanel/de leurs
mocks stateful n'explique pourquoi à distance, sans accès aux
screenshots Cypress (non remontés en artifact CI). Plutôt que
d'itérer à l'aveugle sur un mock complexe, on retire ce volet : le
comportement qu'il vérifiait (« browse marque bien alreadyImported une
fois la Recipe créée ») est déjà entièrement couvert côté Mocha
(sources.test.ts, étape 1) — cette assertion Cypress était redondante,
pas la seule preuve du comportement.

Le reste du scénario (le vrai objet de cette étape : sélectionner un
item non importé depuis le planning, l'importer en résolvant un
ingrédient, et le voir atterrir dans le bon créneau après le retour
sur "/") est inchangé et n'a jamais été mis en cause par cet échec.

Simplifie `planning.ts` en conséquence : `browsing TheMealDB returns
some items`/`importing the previewed item ...` redeviennent des mocks
statiques (le flag `fishPieImported` qu'ils entretenaient n'a plus de
lecteur), et le step Then devenu mort est retiré.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 17:51:44 +02:00
991f91bc0e feat(planning): ajouter au planning déclenche l'import si besoin (étape 4/4)
Dernière étape du plan « onglet Sources » : le sélecteur de recette du
planning (`RecipePickerDialog`) gagne l'onglet « Sources », jusqu'ici
volontairement exclu faute d'écran de revue à qui transmettre un item
choisi (voir étape 3, #47).

- Sélectionner un item déjà importé se comporte exactement comme
  choisir cette même recette depuis un onglet normal (résolue via
  `GET /recipes/:id`, direction vers l'étape « combien de portions ? »
  du dialogue, sans navigation).
- Sélectionner un item pas encore importé bascule vers l'écran de
  revue existant (`ImportRecipePage`), avec le créneau du planning
  porté par la query string (`?planningDate=&planningWeekDay=&planningMeal=`).
  Un import réussi y ajoute alors automatiquement la recette
  fraîchement créée à ce créneau (`POST /planning/items`, avec les
  portions du formulaire) avant de revenir sur le planning — plutôt que
  d'atterrir sur la page de la recette comme le fait un import « classique ».
- `RecipeSourcesPanel`/`SourceItemPreviewPanel` généralisés en
  conséquence : la première ne navigue plus elle-même vers la recette
  déjà importée (`onSelectImportedRecipe` renvoie l'id, chaque appelant
  décide), la seconde propage le créneau optionnel sur son lien
  d'import.

Aucun changement backend : `POST /sources/:sourceKey/import/:externalId`
et `POST /planning/items` existaient déjà et suffisent tels quels — une
fois la recette importée, `GET /sources/:sourceKey/browse` la marque
déjà `alreadyImported` automatiquement (logique déjà couverte par
`sources.test.ts`). 282 tests API toujours au vert, aucune régression.

Tests :
- Cypress : nouveau scénario Gherkin bout-en-bout
  (`cypress/e2e/planning.feature`/`planning.ts`) — ouvrir le
  sélecteur depuis un créneau vide, parcourir Sources, importer un
  item non résolu (ingrédient à compléter compris), vérifier que la
  requête d'ajout au planning porte bien le bon créneau/les bonnes
  portions, que la recette apparaît dans la bonne case de la grille
  après le retour sur "/", puis que rebrowser la source la marque
  désormais comme déjà importée.

Suite : plan « onglet Sources » terminé (étapes 1 à 4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 17:42:43 +02:00
01eee1ae31 fix(web-tests): utilise le bon step Cucumber pour le lien d'import
Le lien « Importer cette recette » de SourceItemPreviewPanel est un
<Link> (donc un <a>), pas un <button> — le scénario utilisait "I click
the button ..." qui ne cible que button, d'où l'échec en CI
("Expected to find content: 'Importer cette recette' within the
selector: 'button' but never did"). Bascule vers le step générique "I
click the link ..." déjà présent dans common.steps.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 17:00:33 +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
a860363438 fix(web-tests): résout les steps Cucumber manquants dans recipe-sources.feature
La CI a révélé que la résolution des steps Cucumber n'est pas globale sur
tout cypress/e2e/ comme je le pensais — seuls le fichier/dossier de même
nom que la .feature et cypress/support/step_definitions/ sont cherchés
(voir le message d'erreur de la CI). recipes.ts vit directement dans
cypress/e2e/ (pas dans step_definitions/), donc ses steps ne résolvaient
que pour recipes.feature — recipe-sources.feature qui les réutilisait
plantait avec "Step implementation missing".

- Les steps génériques réellement partagés entre les deux features
  (heading du panneau détail, technique surlignée, tooltip) migrent vers
  common.steps.ts (step_definitions/, cherché globalement) plutôt que
  d'être dupliqués une deuxième fois.
- Les steps propres à un fixture précis (recipe 2's detail, disliked
  ingredients, catalogue vide) sont redéclarés localement dans
  recipe-sources.ts avec leurs propres données minimales — même
  précédent que recipes.cy.ts, qui duplique déjà indépendamment le
  fixture omeletteDetail plutôt que de dépendre de recipes.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 16:22:12 +02:00
b5a12cf489 feat(recipes): onglet Sources — parcourir les recettes externes (étape 2/4)
Deuxième étape du chantier "onglet Sources" : l'UI de parcours, construite
contre les endpoints backend de l'étape 1 (#45). L'onglet désactivé
placeholder de RecipeTabs devient un vrai onglet fonctionnel.

- RecipeTabs.tsx : nouveau type RecipesPageTab (RecipeTab | "sources") —
  gardé hors du type partagé RecipeTab puisque l'API n'a pas de
  tab=sources à valider. Un prop `tabs` optionnel restreint quels onglets
  s'affichent — RecipePickerDialog (choix d'une recette pour un planning)
  s'y restreint aux 4 onglets réels, parcourir des sources externes en
  plein milieu de ce dialogue n'a pas de sens sans le flux de revue/import.
- Nouveau RecipeSourcesPanel.tsx : contenu de l'onglet "Sources" —
  autonome (son propre master-detail), ne partage pas le fetching
  RecipeTab de RecipesPage puisqu'il parcourt le catalogue *live* d'une
  source (GET /sources/:key/browse), pas la table Recipe sauvegardée.
  Sélecteur de source si le foyer en a activé plusieurs ; sélectionner un
  item déjà importé navigue directement vers la vraie recette
  (SourceItemTable + navigate), un item pas encore importé affiche un
  aperçu en lecture seule (SourceItemPreviewPanel, réutilise
  StepDescription — les tech steps sont donc déjà surlignés dans
  l'aperçu).
- Bug trouvé et corrigé en écrivant le scénario Cypress : cliquer un item
  déjà importé changeait l'URL mais restait affiché sur l'onglet Sources
  (RecipesPage ne rend RecipeDetailPanel/RecipeTable qu'en dehors de
  l'onglet "sources"). RecipeSourcesPanel prend maintenant un callback
  `onViewImportedRecipe` pour repasser sur un onglet réel avant de
  naviguer.

Tests : nouveau recipe-sources.feature (parcours utilisateur complet —
onglet vide, parcours avec items importés/non importés, aperçu avec
surlignage de technique) ; recipes.cy.ts corrigé (assertion obsolète sur
l'ancien placeholder désactivé). Étape suivante (3/4) : écran de revue
(corriger les ingrédients non résolus) + finalisation de l'import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 16:14:20 +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
kyuno053
9e5e950873
Merge pull request #43 from kyuno053/feat/highlight-tech-steps
feat(recipes): surligne les tech steps détectés dans les étapes, avec tooltip
2026-08-20 15:07:19 +02:00
0619fafd28 fix(web-tests): scroll the highlighted technique into view before asserting
Same class of failure as the household-settings sources scenario earlier
this session: the steps section sits below the detail panel's
header/photo/description, off the fold of .app-content's own scroll — a
bare .should("be.visible") doesn't auto-scroll.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 14:53:04 +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
kyuno053
f58ac82a07
Merge pull request #42 from kyuno053/fix/runtime-seed-source-sync
fix(recipes): synchronise les sources en base au démarrage de l'image de prod
2026-08-20 14:00:27 +02:00
4db2085826 fix(web-tests): scroll to the sources section before asserting visibility
CI's e2e run confirmed the real cause: SourcesSection renders near the
bottom of /parametres/foyer (after name, invite code, members), inside
.app-content's own scrollable region — a bare `.should("be.visible")`
doesn't auto-scroll (only interaction commands like `.click()`/`.check()`
do), so the checkbox row was still clipped by that container's overflow
when the assertion ran. Adds a dedicated "I scroll to the section" step
and uses it before the sources-section assertions in household-settings.feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 13:56:42 +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
kyuno053
6178205603
Merge pull request #41 from kyuno053/feat/english-matching
feat(recipes): matching anglais pour les tech steps et les ingrédients
2026-08-20 13:13:57 +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