Commit graph

6 commits

Author SHA1 Message Date
a488704a77 feat(recipes): permet d'associer ingredients/ustensiles a une correction de technique
Etend le flux de correction existant (TechStepCorrectionPopover) pour que
l'utilisateur associe lui-meme des ingredients (avec quantite/unite) et
des ustensiles a la technique qu'il corrige, avec le meme marquage
source: "manual" que la technique elle-meme.

Backend :
- submitTechStepCorrectionSchema (packages/shared) accepte des tableaux
  ingredients/utensils optionnels, chacun avec son propre span [start,end)
  selectionne par l'utilisateur. Omis = ne touche pas aux metadonnees
  existantes ; tableau (meme vide) = remplace tout ce qui existait sur
  cette occurrence (auto ET manuel precedent - decision validee avec
  l'utilisateur).
- applyManualCorrection (recipe-tech-step-correction.service.ts) ecrit
  les nouvelles lignes StepTechStepIngredient/StepTechStepUtensil apres
  avoir vide celles de l'occurrence via deleteMany - meme chemin de code
  que ce soit une creation ou une mise a jour de la technique.
- Nouveaux asserts d'existence (ingredient/unite/ustensile) + validation
  de span, nouveau code d'erreur UTENSIL_NOT_FOUND.
- source ajoute a StepTechStepIngredientView/StepTechStepUtensilView
  (le calque manquait ce que la colonne DB portait deja).

Frontend :
- TechStepCorrectionPopover passe d'un clic = soumission immediate a un
  flux selection-puis-confirmation, avec deux nouvelles sections
  Ingredients/Ustensiles pre-remplies avec l'existant.
- Ajouter un ingredient/ustensile demande une selection de texte dediee
  dans la description encore visible (StepDescription geree via un
  nouvel etat pendingSpanRequest/resolvedMetadataSpan) - pas de raccourci
  sur le span de la correction elle-meme.
- Nouveau CatalogSearchPicker.tsx, plus leger que IngredientPicker pour
  ce contexte de popover, reutilise pour les deux catalogues.
- getUtensils() ajoute a apiClient.

Tests : nouveaux cas Mocha (attache/remplace/omission/validations) dans
recipe-tech-step-correction.test.ts, TechStepCorrectionPopover.cy.tsx
etendu avec le nouveau flux, recipes.ts (e2e) ajuste au clic Valider
supplementaire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 21:08:18 +02:00
kyuno053
92bea914e8
feat(tech-steps): fiabilise la détection des tech steps (corpus + LLM + corrections utilisateur) (#66)
* feat(tech-steps): fiabilise la detection des tech steps (corpus + LLM + corrections utilisateur)

Une seule feature livree en une seule PR, en 5 phases :

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

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

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

* fix(tech-steps): calibre le seuil F1 sur une vraie execution et corrige un bug de comptage

Docker etant redevenu disponible dans cette session, j'ai pu lancer pour de
vrai la suite Mocha d'apps/api (334/334, y compris les tests Phase 1/2
qui n'avaient pu etre executes precedemment) ainsi que les scripts de la
Phase 5 contre une vraie base de test.

- tech-step-eval-dataset.ts : corrige un vrai bug d'auteur - "Take the
  plates..." collisionnait avec le synonyme anglais enregistre "plates"
  (technique plate), invalidant ce cas negatif. Remplace par "dishes".
- tech-step-eval-runner.ts : F1 reel mesure = 0.815 (33 TP / 9 FP / 6 FN).
  Documente ce chiffre et les vraies erreurs de classification decouvertes
  (ex: "Blanchissez les haricots verts..." classifie a tort comme "peel")
  - des faiblesses reelles du classifieur que ce harness est cense
  detecter, pas a masquer en ajustant le jeu de test.
- retrain-tech-steps.ts : le script loggait `appliedIds.length`/
  `rejectedIds.length` (ce qui a ete demande) au lieu du `count` reel
  retourne par `updateMany` (ce qui a vraiment ete modifie) - un id
  inexistant faisait afficher un faux succes. Decouvert en executant le
  script pour de vrai avec des ids partiellement invalides.

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

* fix(tech-steps): corrige un span de correction incorrect sur un highlight existant

Bug reel trouve en lancant l'application pour de vrai et en cliquant sur
un highlight existant : la correction soumise couvrait presque toute la
description au lieu du seul mot-cle cliqué (ex: [6, 56) au lieu de [6, 13)
pour "mijoter").

Cause : StepDescription.tsx capturait `start` dans un `const` par
iteration de `.map()` (correct), mais utilisait `offset` directement (la
variable mutable partagee, pas une valeur capturee) pour `end` dans le
gestionnaire onClick - une fermeture classique sur variable de boucle
encore mutee. Par le temps ou l'utilisateur clique reellement (bien apres
la fin du rendu), `offset` contient sa valeur finale (fin de la
description entiere), pas celle du segment concerne.

Corrige en capturant `end` dans un `const` au meme endroit que `start`.
Renforce aussi l'assertion e2e correspondante (recipes.ts) qui ne
verifiait auparavant que la requete avait ete faite, jamais son contenu -
elle serait passee malgre ce bug.

Verifie en conditions reelles : recette creee via l'UI, correction
soumise, span persiste verifie directement en base (start=6, end=13,
previous=simmer, corrected=grill).

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

* chore: ignore les telechargements Cypress (artefact de run local)

* feat(tech-steps): distingue les corrections manuelles des détections auto

Les corrections utilisateur (via TechStepCorrectionPopover) sont
désormais écrites directement dans StepTechStep, avec une colonne
`source` ("auto" | "manual") qui les distingue des matches du
classifieur NLP :

- Migration `step_tech_step_source` ajoutant `source` (défaut "auto")
- `applyManualCorrection`/`renumberStepTechSteps` dans
  recipe-tech-step-correction.service.ts : une correction met à jour
  ou crée l'entrée StepTechStep concernée (source "manual"), la
  réponse de l'endpoint inclut désormais le techSteps à jour du step
  (SubmitTechStepCorrectionResult), pas seulement l'audit de
  correction
- backfill-tech-steps.ts préserve les entrées "manual" existantes :
  seules les entrées "auto" sont recalculées, et un nouveau match
  auto chevauchant une correction manuelle est ignoré plutôt
  qu'inséré en doublon — vérifié en base réelle (une correction
  manuelle survit intacte à un backfill complet)
- Le front distingue visuellement les deux (StepDescription.tsx,
  recipes.scss : `.step-tech-step--manual`, couleur Turmeric au lieu
  de Basil), avec un tooltip "(correction manuelle)" et un indicateur
  de découvrabilité de la fonctionnalité dans RecipeDetailPanel

Corrige aussi deux bugs trouvés en testant en conditions réelles :
- StepDescription.tsx : le clic sur un highlight existant lisait la
  variable `offset` (mutable, partagée par la boucle) au lieu d'une
  valeur capturée, envoyant un `end` erroné (fin de la description
  entière au lieu du span du mot cliqué)
- backfill-tech-steps.ts : le garde `import.meta.url ===
  file://${process.argv[1]}` ne matche jamais sur Windows (chemins à
  antislash), le script ne faisait donc rien en exécution directe ;
  remplacé par `pathToFileURL(process.argv[1]).href`

335 tests apps/api passants, 40/40 composants Cypress, 75/76 e2e
Cypress (1 flake pré-existant sans rapport, non touché ici).

* fix(worker): corrige le build Docker de tech-step-llm-worker

docker compose build tech-step-llm-worker échouait sur deux problèmes
en cascade, tous deux liés à l'isolation volontaire de ce service hors
du monorepo pnpm (seul son propre package.json/tsconfig.json est copié
dans son contexte de build) :

- pnpm install --ignore-workspace --frozen-lockfile échouait
  (ERR_PNPM_IGNORED_BUILDS) : sans "packageManager" dans son
  package.json, corepack télécharge le pnpm le plus récent
  (11.22.0), qui a durci en erreur bloquante ce qui n'était qu'un
  avertissement sur les builds de dépendances ignorés
  (esbuild/node-llama-cpp). Le reste du repo est épargné parce que
  apps/api/Dockerfile copie le package.json racine, qui pinne déjà
  pnpm@10.12.4 — ce pin ne pouvait pas atteindre ce service isolé.
  Fixé en pinnant la même version ici.
- tsc échouait ensuite (TS5083 puis erreurs en cascade dans les .d.ts
  de node-llama-cpp) : tsconfig.json de ce service extends le
  tsconfig.base.json racine (skipLibCheck notamment), jamais copié
  dans le contexte de build. Fixé en le copiant avant tsconfig.json.

Vérifié : `docker compose build tech-step-llm-worker` complet en local.

* fix(tech-steps): empêche le contexte d'un match d'avaler une correction manuelle voisine

La correction manuelle ne s'affichait pas quand elle portait sur du texte
qui n'était pas une technique à l'origine — reproduit en live : une
description avec un seul match auto-détecté ("mijoter") voit son
contexte de clause s'étendre sur toute la description dès que
splitIntoClauses (tech-step-matcher.ts) n'a trouvé qu'un seul candidat
NER (le cas courant), même quand ce candidat n'a aucun rapport avec le
reste du texte. splitDescriptionByTechSteps avançait alors son curseur
jusqu'à la fin de ce contexte large, ce qui faisait purement et
simplement disparaître (silencieusement, sans erreur) toute correction
manuelle ajoutée plus loin dans la même description — un mot pourtant
sans aucun rapport avec la technique auto-détectée.

Le contexte d'un match est purement cosmétique (StepDescription.tsx le
rend identique à du texte brut depuis que sa mise en valeur dédiée a
été désactivée) et ne doit donc jamais coûter son propre highlight à
un *autre* match. splitDescriptionByTechSteps distingue maintenant
deux notions : le chevauchement entre les spans *keyword* stricts de
deux entrées (toujours un vrai conflit, l'entrée la plus tardive est
toujours ignorée, comportement inchangé) et le chevauchement du
contexte *cosmétique* d'une entrée sur le keyword d'une autre (jamais
un vrai conflit désormais : le contexte est simplement rogné pour
laisser la place, plutôt que l'entrée voisine entière étant abandonnée).

Vérifié en conditions réelles (Docker) : une correction manuelle sur
"materiel" dans "Faire mijoter la sauce, puis ranger le materiel."
s'affiche maintenant correctement à côté du highlight auto "mijoter",
et survit à un rechargement complet de la page.

Nouveau test de régression dans highlight-tech-steps.cy.tsx
reproduisant exactement ce cas ; les 18 tests du fichier (dont tous
les cas de contexte/malformation déjà couverts) passent toujours.

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

Closes #52, #53, #54

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

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

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

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

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

Closes #27

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

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

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

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

Closes #55

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* refactor(api): regroupe lib/ par sous-domaine au lieu d'un dossier à plat

9 fichiers à plat -> recipe-sources/ (recipe-source-adapter, recipe-source-
errors, recipe-source-registry) et recipe-matching/ (recipe-translation,
ingredient-matcher, tech-step-matcher). jwt.ts, safe-profile.ts et
logger.service.ts restent à la racine de lib/ (pas de sous-domaine
partagé avec les autres).

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

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

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

* refactor(api): regroupe test/ par sous-domaine, miroir de src/lib/

18 fichiers à plat -> recipe-matching/ (ingredient-matcher, recipe-
translation, tech-step-matcher — miroir de lib/recipe-matching/),
recipe-sources/ (json-ld-recipe, recipe-source, recipe-source-sync,
the-meal-db — miroir de lib/recipe-sources/), sources/ (sources,
sources-index — module + registration src/sources/index.ts).

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 12:16:22 +02:00
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
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