Commit graph

106 commits

Author SHA1 Message Date
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
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
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
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
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
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
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
30fffd59ad fix(web): corrige l'e2e onboarding cassé par l'étape sources
GET /reference/sources n'était intercepté par aucun scénario Cypress
menant à la création/jonction d'un foyer — la requête réelle restait
en attente indéfiniment, laissant OnboardingSourcesPage bloqué sur
/onboarding/sources au lieu de s'auto-sauter vers /onboarding/allergenes.

- Ajout du Given "the sources reference list is empty" (même pattern
  que les intercepts diets/allergies existants), câblé dans les deux
  scénarios qui créent/rejoignent un foyer.
- Mise à jour de l'assertion "Étape 3 sur 3" → "Étape 4 sur 4" : un
  foyer étant créé dans ce scénario, l'étape allergènes affiche
  désormais le total dynamique (4 étapes) comme prévu.
- OnboardingSourcesPage.tsx : redirige aussi vers /onboarding/allergenes
  en cas d'échec réseau sur getSources(), pas seulement quand la liste
  est vide — le wizard ne doit pas bloquer l'utilisateur sur une étape
  optionnelle à cause d'un problème transitoire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 09:47:17 +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
0ff8775cf6 fix(recipes): élargir le sélecteur d'unité pour les labels longs
.ingredient-row__unit était encore dimensionné pour l'ancien champ
texte libre (6rem, suffisant pour "g"/"unité") — trop étroit
maintenant que c'est un <select> sur le catalogue, dont le plus long
label ("cuillère à soupe") était tronqué.
2026-08-19 22:40:02 +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
4a6f586866 fix(e2e): update layout test for settings-page's dropped max-width
layout.cy.ts still asserted .settings-page was a centered column
capped at 896px (max-width: 56rem) — that cap was removed in
4647a82 (settings pages were cramped on wide viewports) without
updating this regression test, breaking CI on main.

Moves the settings-page assertion into the full-bleed group
(assertFillsContentWidth, same as .planning-page/.recipes-page)
instead of the centered-column one, matching current behavior.
2026-08-19 22:01:22 +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
4647a82942 fix(settings): remove max-width cap on settings pages
.settings-page was capped at 56rem, leaving Compte, Préférences
alimentaires (foyer), and Préférences utilisateur cramped in a narrow
centered column on wide viewports. Drop the max-width/margin so the
content fills .app-content's full width like other pages.
2026-08-19 21:15:53 +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
kyuno053
f1fefc1f38
refactor: sépare les tests Cypress en parcours utilisateur / layout / composants (#25)
* chore: point de départ pour le refactor des tests Cypress

Sépare les tests Cypress en trois catégories, comme discuté :

1. Parcours utilisateur (cypress/e2e/) — scénarios Gherkin/Cucumber,
   pilotés par @badeball/cypress-cucumber-preprocessor@22.2.0 (validé
   sur experiment/cucumber-cypress, mergée). Ex. "En tant
   qu'utilisateur, je peux créer un compte".

2. Layout applicatif (nouveau dossier à définir) — specs Cypress
   classiques (pas de Gherkin), cy.visit() sur une vraie page, mais
   centrées sur la disposition/visibilité des éléments, indépendamment
   d'un parcours utilisateur scripté.

3. Composants génériques (cypress/component/, nouveau) — vrai
   Component Testing Cypress, composant React monté isolément (pas de
   routeur, pas de backend). Composants concernés aujourd'hui :
   components/ui/{Checkbox,Radio,Dialog}.tsx.

Premier pas : mise en place de l'infra Component Testing (config +
devServer Vite + adapter React 18), validée sur un composant simple
avant de construire le reste.

* feat(web): infra Cypress Component Testing, premier test sur CheckboxOption

Étape 1 du refactor (voir PR) : met en place le vrai mode Component
Testing de Cypress, séparé de l'e2e — monte un composant React isolé
(pas de routeur, pas de backend), pour tester les composants
génériques (components/ui/) indépendamment de tout parcours
utilisateur.

- cypress.config.ts : nouveau bloc `component` (devServer Vite, réutilise
  vite.config.ts de l'appli — même plugin React, même Sass). Le hook
  GPU-disable est factorisé (`disableGpu`) puisque e2e et component ont
  chacun leur propre `setupNodeEvents`, pas de config partagée par défaut.
- cypress/support/component.ts + component-index.html : fichiers de
  support standards Cypress CT — importe le vrai global.scss de l'appli
  (les composants génériques sont stylés via lui, pas de CSS scopé à eux).
- cypress/component/CheckboxOption.cy.tsx : 4 scénarios sur
  components/ui/Checkbox.tsx (rendu du label, reflet du prop `checked`
  sur l'input natif + la classe `is-selected`, callback `onChange` avec
  la valeur inversée, comportement contrôlé via un wrapper avec état).

Dépendance ajoutée, épinglée : @cypress/vite-dev-server@5.2.1 (dernière
version sans peer dependency cypress >=14 — 6.0.3+ l'exige explicitement,
on est sur cypress@13.17.0).

Testé en local jusqu'au mur GPU/Electron habituel (config + devServer
Vite chargent sans erreur) — l'exécution réelle du montage reste à
vérifier via la CI.

* ci: exécute les tests de composants Cypress

Sans ça, `cypress/component/CheckboxOption.cy.tsx` (commit précédent)
ne tournait jamais en CI : `pnpm --filter web e2e` lance `cypress run`
sans `--component`, donc uniquement la suite e2e par défaut.

- apps/web/package.json : nouveau script `cy:run:component`
- ci.yml : étape dédiée après `pnpm --filter web e2e`, sans
  start-server-and-test (Cypress lance son propre dev server Vite en
  interne pour le component testing, pas besoin d'attendre l'appli
  comme pour l'e2e)

* test(web): refactor user journeys into Cucumber scenarios

Convertit les parcours utilisateur (goal-driven, "en tant que X je peux
Y") en scénarios Gherkin, en réutilisant l'infra Cucumber déjà validée
par le smoke test (PR #24). Retire le smoke test jetable maintenant
superflu.

8 fichiers .feature ajoutés, chacun avec son fichier de step definitions
au même basename (convention de découverte du préprocesseur — voir
login-smoke.ts) :

- auth.feature : inscription (succès, erreur validation, email déjà
  pris), connexion (succès, identifiants invalides), déconnexion
- onboarding.feature : les 3 scénarios déjà couverts (wizard complet,
  étapes sautées, rejoindre un foyer pendant l'onboarding) — dépend de
  household-settings.ts et preferences.ts pour ses steps de
  création/rejoint de foyer et de sélection de régime/allergies
- household-settings.feature : créer un foyer, rejoindre par code
  d'invitation, renommer (autosave), retirer un membre, supprimer le
  foyer, quitter le foyer
- account.feature : suppression de compte (mauvais mot de passe,
  succès, annulation)
- recipe-form.feature : les 4 scénarios déjà couverts inchangés (ajout
  d'ingrédient + création, régression crypto.randomUUID, exclusion/
  réinclusion d'ingrédient, préchargement + édition d'une recette
  existante)
- recipes.feature : bascule favori, suppression d'une recette
- preferences.feature : autosave du régime, autosave des allergies
- user-preferences.feature : changement de thème (autosave)

En contrepartie, les anciens .cy.ts perdent uniquement les it() migrés
vers Gherkin — les scénarios de layout/affichage pur (catalogue de
recettes, tabs, recherche, panneau de détail, sidebar, planning grid,
etc.) restent en Cypress classique, conformément au découpage
"parcours utilisateur (Cucumber) vs layout (Cypress pur)" déjà en
place pour les component tests. auth.cy.ts, onboarding.cy.ts et
recipe-form.cy.ts sont supprimés : 100% de leur contenu a migré.

Les commentaires "voir auth.cy.ts pour la justification" désormais
obsolètes (fichier supprimé) sont remplacés par une explication
autonome du mock cy.intercept.

Vérifié statiquement : les 246 steps Gherkin des 8 .feature résolvent
chacun vers exactement une définition (0 non résolu, 0 ambigu) et
`pnpm exec biome check` est propre sur tout cypress/. Reste à confirmer
en CI que les scénarios passent réellement (pas seulement qu'ils se
résolvent).

* fix(web): fix cross-feature step discovery and slash-alternation bug

La CI de la refonte précédente (commit aaace15) a échoué : la
découverte par défaut du préprocesseur ne charge, pour un fichier
`foo.feature`, QUE `foo.ts` (co-localisé, même basename) et
`cypress/support/step_definitions/**` — pas les autres `.ts` du
dossier `cypress/e2e/`. onboarding.feature référençait donc des steps
qui ne vivaient que dans preferences.ts, household-settings.ts et
auth.ts, introuvables lors de son propre run.

Déplace les steps réellement partagés entre plusieurs .feature vers
cypress/support/step_definitions/ (chargé pour toutes les features) :
- reference-data.steps.ts : mocks des listes de référence régimes/
  allergies (options ou vides) — partagé entre auth.feature et
  onboarding.feature
- household-mutations.steps.ts : création/adhésion à un foyer et leurs
  assertions — partagé entre household-settings.feature et
  onboarding.feature
- profile-mutations.steps.ts : sélection du régime, mise à jour des
  allergies et leur assertion — partagé entre preferences.feature et
  onboarding.feature

Les définitions d'origine sont retirées de auth.ts/household-
settings.ts/onboarding.ts/preferences.ts pour éviter un step
"Ambiguous" (chargé deux fois pour la feature qui les définissait déjà
elle-même).

Corrige aussi un second bug distinct révélé par la même CI :
"the ingredient/diet catalog is available" (recipe-form.feature)
contient un "/" non échappé — en syntaxe Cucumber Expression, "/" hors
d'un paramètre {..} signifie une alternative de texte ("ingredient" OU
"diet catalog is available"), jamais le caractère littéral. Le texte
du .feature ne pouvait donc jamais matcher. Renommé sans "/" :
"the ingredient and diet catalog is available".

Le script de vérification statique utilisé pour valider aaace15 avant
push donnait une fausse confiance : il regroupait tous les steps de
tous les fichiers comme disponibles globalement pour chaque feature,
sans respecter ce scoping réel. Réécrit pour ne charger, par feature,
que son fichier co-localisé + step_definitions/ — et pour détecter les
patterns contenant un "/" non échappé. Résultat : toujours 246 steps,
0 non résolu, 0 ambigu, 0 pattern à slash non échappé, cette fois avec
un modèle de résolution fidèle au comportement réel du préprocesseur.

* test(web): cover every CheckboxOption/RadioOption behavior

Complète les component tests des deux seuls composants UI génériques
committés (Dialog.tsx est un WIP non commité d'une autre fonctionnalité
en cours — hors scope ici) pour couvrir tout leur comportement, pas
seulement le cas heureux.

CheckboxOption — 4 tests existants (rendu, checked/is-selected, onChange
au clic depuis unchecked, contrôlé) complétés par :
- onChange(false) au clic depuis l'état checked (symétrique du test
  existant, qui ne couvrait que checked=false → true)
- fusion du className de l'appelant avec is-selected, dans les deux
  sens (juste className, className+is-selected)
- class="" (chaîne vide, pas "false"/"null") quand aucun className
  n'est passé et que checked=false — pin le comportement exact du
  `.filter(Boolean).join(" ")`
- le clic sur le texte du label (pas seulement l'input) déclenche aussi
  onChange — comportement natif du HTML dont la "carte sélectionnable"
  de global.scss dépend entièrement
- le span .check-mark est aria-hidden

RadioOption — aucun test avant ce commit. Ajouté en couvrant en plus
ce qui distingue vraiment un radio d'un checkbox :
- name/value posés sur l'input natif
- onChange(value) au clic depuis unchecked
- AUCUN onChange au clic sur un radio déjà checked (contrairement à un
  checkbox, un radio natif ne réémet pas `change` si l'état ne change
  pas réellement)
- clic sur le label, className/is-selected, aria-hidden — mêmes
  scénarios que CheckboxOption
- comportement de groupe mutuellement exclusif : 3 RadioOption
  partageant `name="theme"` (mirroring UserPreferencesPage), un seul
  sélectionné à la fois, y compris via `input:checked` natif du
  navigateur

Non exécutable en local (limitation GPU/sandbox Electron documentée
dans le README, pré-existante) — à vérifier en CI.

* test(web): add layout/style regression suite for the app shell

Troisième catégorie du découpage des tests (parcours via Cucumber,
composants génériques via Component Testing, et maintenant layout pur
— indépendant de tout parcours utilisateur). Cypress classique, pas de
Gherkin : ce fichier teste la structure/l'apparence du shell
(AppLayout) lui-même, pas le contenu d'une page donnée.

Couvre spécifiquement les 4 axes demandés :

- Positionnement : la sidebar garde une largeur fixe (240px déplié,
  68px replié) plaquée au coin haut-gauche, sur n'importe quelle page.
- Scroll : régression directe pour #21 — `.app-layout` reste borné
  exactement à la hauteur du viewport (overflow: hidden), et une page
  plus haute que le viewport scrolle uniquement dans `.app-content`
  (via un spacer synthétique de 3000px injecté après le mount, pour
  rester indépendant du contenu réel d'une page donnée) sans jamais
  déplacer la sidebar ni scroller le document lui-même.
- Largeur des pages : autre régression directe pour #21 — le planning
  et le catalogue de recettes remplissent toute la largeur disponible
  de `.app-content`, tandis que la page "Liste de courses" et les
  pages de paramètres restent centrées avec un espace égal de chaque
  côté (le bug original : collées à gauche avec un grand vide à
  droite).
- Breakpoint responsive (< 640px) : la sidebar bascule en barre
  horizontale pleine largeur, masque le bouton collapse/la version,
  et garde chaque lien de nav pleinement lisible (icône + label, avec
  scroll horizontal) plutôt que de les écraser en pastilles de ~16px
  sans texte — un mode de régression explicitement documenté en
  commentaire dans AppLayout.scss mais jusqu'ici non testé.
- Thème de couleur : va au-delà de l'attribut `data-theme` déjà
  couvert par user-preferences.cy.ts — vérifie les vraies valeurs de
  couleur calculées (`getComputedStyle`) sur la sidebar, le lien de
  nav actif et le fond de page, en clair et en sombre, confirmant que
  la cascade CSS des tokens (_theme.scss) atteint réellement le rendu,
  pas seulement que le JS pose le bon attribut.

Non exécutable en local (limitation GPU/sandbox Electron documentée
dans le README, pré-existante) — à vérifier en CI.
2026-08-19 14:31:17 +02:00
kyuno053
fc886a79cb
experiment: Cucumber/Gherkin + Cypress (#24)
* chore: point de départ pour l'expérimentation Cucumber/Gherkin + Cypress

Repart de zéro (pas de reprise du travail précédent sur
feat/cypress-cucumber, fermée/supprimée) — voir la discussion sur la
PR pour le contexte : bug amont dans
@badeball/cypress-cucumber-preprocessor@26.0.0 (require() synchrone de
dépendances @cucumber/* désormais ESM pur, plusieurs incompatibilités
de schéma trouvées en épinglant d'anciennes versions).

* feat(web): installe cypress-cucumber-preprocessor@22.2.0, versions figées

Étape 1 de l'expérimentation. Versions exactes (pas de ^), comme
demandé, pour éviter que le prochain `pnpm install` fasse dériver la
résolution vers des patchs plus récents :

- @badeball/cypress-cucumber-preprocessor@22.2.0
- @bahmutov/cypress-esbuild-preprocessor@2.2.8
- cypress@13.17.0
- esbuild@0.21.5

Contrairement à v26.0.0 (utilisé dans la tentative précédente,
fermée), cette version charge sans ERR_REQUIRE_ESM — aucun
pnpm.overrides nécessaire cette fois. Vérifié :
- `import { addCucumberPreprocessorPlugin }` : OK
- `addCucumberPreprocessorPlugin(on, config)` avec un contexte Cypress
  minimal : résout sans erreur, enregistre tous ses event handlers
  (before:run, after:run, before:spec, after:spec, after:screenshot,
  task)

À vérifier ensuite : cypress.config.ts + un vrai fichier .feature.

* test(web): feature Gherkin jetable pour valider le pipeline preprocessor

Étape 2 de l'expérimentation — un seul scénario minimal (remplir
email/password sur l'écran de connexion) pour vérifier que
addCucumberPreprocessorPlugin@22.2.0 + l'esbuild plugin fonctionnent
de bout en bout, pas juste au chargement. Steps volontairement
autonomes dans login-smoke.steps.ts (pas de fichier partagé) — tout
ce fichier est prévu pour être supprimé une fois validé.

cypress.config.ts : specPattern couvre maintenant *.cy.ts ET *.feature
en parallèle (le reste de la suite reste en .cy.ts classique pour
l'instant).

Testé en local jusqu'au mur GPU/Electron habituel de cet
environnement (chargement de la config + bundling esbuild passent,
pas d'ERR_REQUIRE_ESM) — la vraie exécution du scénario reste à
vérifier via la CI.

* fix(web): renomme login-smoke.steps.ts en login-smoke.ts

Le vrai (et seul) problème du run précédent : "Step implementation
missing for 'I am not signed in'" — pas un souci ESM/CJS cette fois,
juste une convention de nommage. Le pattern stepDefinitions par défaut
du preprocessor cherche, pour cypress/e2e/login-smoke.feature :

  - cypress/e2e/login-smoke/**/*.{js,mjs,ts,tsx}
  - cypress/e2e/login-smoke.{js,mjs,ts,tsx}   <- même basename, SANS ".steps"
  - cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}

`login-smoke.steps.ts` ne correspond à aucun des trois. Confirmé par
le message d'erreur lui-même (Cypress liste les 3 patterns essayés).
2026-08-19 13:04:21 +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
c63bc15ea4
fix(web): sidebar reste fixe sur page longue, page Liste de courses centrée (#21)
* fix(web): sidebar reste fixe sur page longue, page Liste de courses centrée

- AppLayout: .app-layout passe de min-height:100vh à height:100vh +
  overflow:hidden, et .app-content devient le vrai conteneur de scroll
  (overflow-y:auto). Avant, une page plus haute que le viewport faisait
  défiler tout le document, entraînant la sidebar avec lui au lieu de la
  garder fixe.
- ComingSoonPage (page "Liste de courses"): ajoute margin:0 auto, comme
  .settings-page, au lieu de rester collée au bord gauche avec un grand
  espace vide à droite sur les écrans larges.

* fix(web): la grille recettes stretche le panneau de détail sur toute sa hauteur

Régression révélée par le fix du layout précédent (AppLayout.scss) : tant
que `.app-layout` grandissait sans limite (`min-height: 100vh`), rien
dans la chaîne de hauteurs (.app-content → .recipes-page →
.recipes-page__catalog → .recipe-detail-panel) n'avait de hauteur
réellement définie, donc `.recipe-detail-panel { height: 100% }` ne
contraignait jamais rien — le panneau grandissait pour tout afficher,
masquant silencieusement que sa grille parente (`&__catalog`, display:
grid) n'a jamais eu de `grid-template-rows` explicite.

Avec `.app-layout` maintenant fixé à `height: 100vh`, `&__catalog` a
enfin une hauteur définie — mais sans `grid-template-rows`, sa seule
ligne implicite reste dimensionnée à son contenu (auto) au lieu de
s'étirer sur toute la hauteur disponible, comme l'intention documentée
en commentaire (« both stretched to the same full height ») le disait
déjà. Résultat concret : le panneau de détail recette rendait plus
court que son propre contenu, qui se retrouvait coupé par son
`overflow-y: auto` — repéré via l'échec CI de recipes.cy.ts sur
"opens a recipe's detail alongside the table" (timeout sur la
description, `<p>` non visible car clippée par un parent en overflow).

Vérifié en réinjectant les mêmes fixtures dans le navigateur (mock
fetch temporaire) : le panneau passe de 296px à 448px de haut, la
description reste entièrement dans ses limites.

* fix(web): restaure le layout table+panneau empilé sous 1024px

Le commit précédent (grid-template-rows sur .recipes-page__catalog)
corrigeait bien le layout desktop (2 colonnes, 1 ligne étirée sur
toute la hauteur), mais la même règle s'appliquait aussi telle quelle
au breakpoint mobile/tablette (@media max-width: 1024px), où la grille
passe à 1 seule colonne — donc à 2 lignes empilées (table puis
panneau), pas 1. Forcer ces 2 lignes dans un seul track `minmax(0, 1fr)`
coupait la hauteur disponible en deux et donnait 0px à la ligne du
tableau, dont le contenu se retrouvait entièrement clippé par son
propre `overflow-y: auto` — repéré via le nouvel échec CI de
recipes.cy.ts (Ratatouille non visible), le viewport par défaut de
Cypress (1000×660) tombant sous ce seuil de 1024px.

Réinitialise `grid-template-rows: auto` dans ce même bloc `@media`, à
côté du `height: auto` qui y était déjà (même logique : plus de ligne
unique à hauteur fixe à ce breakpoint). Revérifié dans le navigateur
aux deux largeurs (1000px et 1600px) avec les mêmes fixtures que le
test.

* test(web): agrandit le viewport du test de détail recette

Dernier morceau de la même régression : la grille catalogue+panneau
(master-detail) est un layout desktop, comme la grille de planning
(voir planning-page.cy.ts's `cy.viewport(1600, 900)`, même
justification). Au viewport par défaut de Cypress (1000×660), le
panneau de détail n'a tout simplement pas assez de hauteur pour
afficher photo + en-tête + ingrédients + étapes sans avoir besoin de
scroller en interne — c'est le comportement voulu de
`.recipe-detail-panel` (`overflow-y: auto`, voir son commentaire dans
recipes.scss), pas un bug. Ce n'était jamais visible avant que
`.app-layout` ait une vraie hauteur bornée (voir les 2 commits
précédents) : tout grandissait sans limite, donc rien n'avait jamais
besoin de scroller pour être visible.

Reproduit en rejouant le flow exact du test dans le navigateur (visite
/recettes, clic sur la ligne Omelette, pas juste une navigation directe
vers /recettes/2 — la simple navigation directe ne reproduisait pas
l'échec) : à 1000×660 la description était bien coupée
(descWithinPanel: false) ; à 1600×900 tout rentre.
2026-08-19 10:42:30 +02:00