* 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>
* 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>
* 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>
Trois ajustements successifs sur le dialogue de sélection de recette
(RecipePickerDialog), demandés en continu après le premier correctif
de débordement :
1. Dialogue élargi et à hauteur fixe (95vw plafonné à 85rem, 80vh) au
lieu de dépendre du contenu, avec la répartition liste/détail
redéfinie en fractions du dialogue lui-même (3fr/2fr) plutôt qu'en
vw — cette dernière suivait la largeur du viewport, sans rapport
avec la largeur désormais fixe du dialogue.
2. Le formulaire de revue d'import (ex-ImportRecipePage) est extrait
dans un composant partagé, RecipeImportForm — toujours monté en
page autonome (route directe/rechargement), mais désormais aussi
intégré comme une étape du dialogue lui-même quand un item de
source a besoin d'une résolution manuelle, au lieu de naviguer et
perdre le contexte du picker (recherche, filtres, créneau).
3. Cliquer sur une recette dans le dialogue ne fait plus que la
sélectionner/prévisualiser (RecipeDetailPanel, comme /recettes) —
plus de saut automatique vers l'étape suivante. Un nouveau pied de
dialogue (Dialog.tsx gagne une prop ) porte Confirmer/
Fermer : Confirmer agit sur la sélection en cours (recette réelle
→ étape portions existante ; item de source pas encore importé →
import transparent ou formulaire intégré, point 2). Les onglets
réguliers gagnent leur propre paire maître-détail (RecipeTable +
RecipeDetailPanel, showActions=false) sur ce même modèle ; les
onglets source prévisualisent désormais aussi les items déjà
importés en interne (RecipeSourcesPanel), plus de saut direct.
Cypress (planning.feature/planning.ts) mis à jour en conséquence :
sélectionner puis confirmer sont deux étapes distinctes, le clic sur
la ligne ne déclenche plus rien tout seul.
Bug pré-existant trouvé en testant en direct (sans rapport avec ce qui
précède) : l'import d'une recette source plante avec une contrainte
d'unicité Prisma dès que deux lignes d'ingrédient se résolvent au même
ingrédient catalogue — signalé séparément (tâche en arrière-plan), pas
corrigé ici.
Vérifié en direct (navigateur, comptes de test) : sélection sans saut
d'écran, pied de dialogue activé/désactivé correctement, Confirmer sur
un item de source non résolu bascule vers le formulaire intégré,
Fermer ferme bien le dialogue.
pnpm exec tsc -b --force (web) — propre.
pnpm exec biome check — propre.
pnpm --filter web build — propre.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
92vw/75rem -> 95vw/85rem — vérifié en direct, dialogue passe de 1178px à
1216px sur un viewport 1280px.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.recipes-page__catalog dimensionne son panneau de détail avec
minmax(35vw, 38vw) — une fraction du *viewport*, cohérent sur /recettes
(cette grille occupe quasiment toute la page) mais sans rapport avec ce
dialogue, désormais de largeur fixe (92vw plafonné à 75rem, voir commit
précédent) : à un viewport large, 38vw dépasse largement la largeur réelle
du dialogue et déséquilibre la répartition liste/détail.
Remplacé, sous .recipe-picker-dialog uniquement, par un ratio 3fr/2fr
relatif au dialogue lui-même — la répartition (60/40) reste identique
quelle que soit la largeur de l'écran, vérifié en direct à 1280px et
1600px de large (ratio mesuré 1.50 dans les deux cas).
pnpm --filter web build — propre.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Suite au correctif précédent (débordement du catalogue) : le dialogue lui-
même passe de 56rem/hauteur-au-contenu à 92vw plafonné à 75rem, avec une
hauteur fixe de 80vh (au lieu du simple plafond hérité de Dialog.tsx) —
la zone de navigation garde désormais une taille généreuse et constante
quel que soit l'onglet/la quantité de contenu, plutôt que de dépendre de
ce qui tient à l'écran.
Vérifié en direct (navigateur, 1280×800) : dialogue 1178×640px (92vw ×
80vh), catalogue/tableau/détail correctement bornés à l'intérieur.
pnpm --filter web build — propre.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
.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é.
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.
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).
.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.
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>
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>
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>
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>
- 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>
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>
* 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.
* chore(web): session de polish global — version, checkbox, danger zone, icônes
- Affiche le numéro de version (package.json, injecté via Vite) en bas de
la sidebar, masqué en mode collapse et en mobile.
- Factorise les checkbox/radio dupliqués (AllergySelect, DietTagSelect,
IngredientPicker, UserPreferencesPage) en composants partagés
CheckboxOption/RadioOption (components/ui/), et inverse le layout pour
que la case soit à gauche du label.
- Teinte la "zone de danger" de suppression de compte en rouge (fond +
bordure), pas seulement le bouton.
- Migre les icônes de navigation générale vers lucide-react (nav-icons.tsx
devient un fichier de ré-export) ; les pictogrammes d'ingrédients métier
restent en SVG custom (pas d'équivalents fins côté lucide).
Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle manuelle (sidebar desktop/collapsed/mobile, light/dark).
* feat(web): icônes d'ingrédients depuis foodiconpack.com + page de crédits
- Remplace 19 des 22 pictogrammes génériques d'ingrédients par des icônes
curées du pack gratuit "Common ingredient icons"/"Common Utensils" de
foodiconpack.com (CC BY 4.0) : carotte, pomme, basilic, bœuf, poulet,
saumon, crevette, riz, pois chiches, amandes, lait, cheddar, œufs,
cannelle, miel, huile d'olive, bière, marmite, sucre.
- BREAD/DOUGH/SPROUT restent en SVG custom : pas d'équivalent net dans le
pack (packs "ingrédients"/"ustensiles"/"plats"/"boissons" vérifiés).
Architecture inchangée : `icon` reste un enum de 22 valeurs partagées en
base (pas de migration, pas de mapping par ingrédient — cf. le
commentaire du fichier sur l'historique emoji→enum générique).
- Nouveau wrapper FilledIcon (fill="currentColor", viewBox 2048) à côté du
wrapper Icon existant (stroke) — les deux stylent au même endroit via
CSS, donc le mélange des 19+3 icônes reste visuellement homogène.
- Ajoute /parametres/credits (CreditsPage) créditant foodiconpack.com et
liant la licence CC BY 4.0, requis par la licence des icônes utilisées ;
nouvelle entrée de nav "Crédits" (icône lucide Info).
Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle (grille des 22 icônes dans le picker, page crédits).
* feat(web,api): zone dangereuse rouge, préférences élargies, onglet favoris par défaut, e2e recettes, catalogue en uid+i18n
- Zone dangereuse (compte) : le bouton "Supprimer mon compte" est rouge.
- Pages préférences/paramétrage : contenu centré et élargi (32rem -> 56rem)
au lieu de coller à gauche sur un écran large.
- Page recettes : l'onglet "Favoris" est sélectionné par défaut.
- Ajout de apps/web/cypress/e2e/recipes.cy.ts (onglets, recherche, sélection
master-detail, favori, suppression, lien nouvelle recette).
- Catalogue de référence (ingrédients/régimes/allergènes) : la colonne
`name` (le libellé français, utilisé comme clé unique) devient `key`, un
slug stable et opaque au sens produit (ex. "vegetarien", "boeuf_hache").
Le libellé lui-même déménage entièrement côté client, dans
apps/web/src/locales/fr/translation.json sous le namespace `catalog.*`,
résolu via `t(\`catalog.ingredients.${key}\`)` etc. — même schéma que
IngredientCategory/IngredientSubcategory. Migration Prisma
(rename + backfill des ~456 lignes déjà seedées), seed/service/tests API
et composants web mis à jour en conséquence.
- apps/api/src/utils/slugify.ts + scripts/generate-catalog-i18n.ts
(regénère le fichier de traduction depuis reference-seed-data.ts).
- 102 tests Mocha + 32 scénarios Cucumber passent contre la base migrée.
Note : cypress run plante dans cet environnement (le processus GPU
Chromium/Electron crash même headless, indépendamment des flags) — les
recipes.cy.ts n'ont pas pu être exécutés ici ; vérifiés par lecture du code
source des composants visés et par un passage manuel dans le navigateur de
prévisualisation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(api): les uids du catalogue sont en anglais, pas des slugs français
reference-seed-data.ts reste rédigé en français (c'est juste le libellé
d'autoring, jamais stocké/exposé), mais la clé stable (`Diet.key`/
`Category.key`/`Ingredient.key`) qu'on en dérive doit elle-même être un
identifiant anglais, indépendant de la langue d'autoring — pas juste le
même texte français passé à slugify().
- apps/api/src/db/catalog-en-keys.ts : dictionnaire écrit à la main
(label français -> clé anglaise) pour les 5 régimes, 14 allergènes et
437 ingrédients ; getEnglishKey() lève une erreur explicite si un
nouvel élément n'a pas encore d'entrée plutôt que de retomber sur un
slug français silencieux.
- scripts/validate-catalog-en-keys.ts : vérifie que chaque diet/allergène/
ingrédient de reference-seed-data.ts a une entrée, et que les clés
anglaises résultantes sont uniques (437/437, 14/14, 5/5 — zéro manquant,
zéro collision).
- reference-seed-data.ts et scripts/generate-catalog-i18n.ts utilisent
désormais getEnglishKey() au lieu de slugify(nom français).
- Nouvelle migration (20260818193000_catalog_keys_to_english) qui
remappe les lignes déjà seedées avec un slug français (par la migration
précédente) vers leur clé anglaise définitive.
- apps/web/src/locales/fr/translation.json régénéré : catalog.* est
maintenant indexé par clé anglaise ("vegetarian", "eggs",
"ground_beef"...), toujours avec le libellé français en valeur.
- Tests/step-definitions mis à jour (getEnglishKey() au lieu de
slugify()) ; 102 tests Mocha + 32 scénarios Cucumber passent contre la
base migrée. Vérifié aussi en direct via GET /reference/diets et
/reference/allergies.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(web): crypto.randomUUID plante hors contexte sécurisé, empêchant d'associer un ingrédient
Écran noir + "TypeError: crypto.randomUUID is not a function" au clic sur
une carte d'ingrédient dans le formulaire de recette. crypto.randomUUID()
n'est défini que dans un "contexte sécurisé" (https, ou littéralement le
host "localhost") — il est absent sur une IP locale (test sur un vrai
appareil), dans une WebView Capacitor (l'enrobage mobile prévu pour cette
app), ou en http sur un vrai domaine. RecipeFormPage/StepListEditor s'en
servaient pour générer l'identité React (`key`) de chaque ligne
d'ingrédient/étape en brouillon.
- apps/web/src/lib/client-key.ts : remplace par un générateur qui ne
touche jamais `crypto` — un compteur + Math.random suffit, cette valeur
n'a besoin d'être unique que le temps de la session de rendu, jamais
envoyée au serveur.
- apps/web/cypress/e2e/recipe-form.cy.ts : couvre l'association d'un
ingrédient (recherche, sélection, exclusion du picker une fois
sélectionné, retrait), la création et l'édition d'une recette, et un
test de non-régression dédié qui supprime crypto.randomUUID avant le
chargement de la page (comme le ferait un vrai contexte non sécurisé)
pour vérifier que l'ajout de plusieurs ingrédients/étapes ne plante
plus.
Vérifié en direct dans le navigateur de prévisualisation en supprimant
crypto.randomUUID à la main (reproduit le crash), puis en confirmant que
l'ajout d'ingrédient fonctionne à nouveau après le correctif. cypress run
ne peut toujours pas s'exécuter dans cet environnement (voir le commit
précédent) — non exécutés avec Cypress lui-même, mais vérifiés par
lecture des sélecteurs réels et rejoués à la main dans le navigateur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): corrige les specs Cypress cassées par le refactor uid+i18n, applique biome
- onboarding.cy.ts / preferences.cy.ts / recipes.cy.ts mockaient encore
GET /reference/diets|allergies avec l'ancienne forme {id, name}. Depuis
les deux derniers commits l'API renvoie {id, key} (uid anglais) et le
composant résout le libellé via i18n (t(`catalog.diets.${key}`)) — avec
key manquant, ça affichait littéralement "catalog.diets.undefined" au
lieu de "Végétarien"/"Omnivore"/etc., faisant échouer cy.select()/
cy.contains() dans ces 3 specs. Corrigé pour mocker {key: "vegetarian"},
{key: "peanuts"}, etc.
- recipes.cy.ts : le test "shows a not-found message" utilisait le
mauvais code d'erreur (4041 au lieu de ErrorCode.RECIPE_NOT_FOUND =
4045), donc RecipeDetailPanel tombait dans son état d'erreur générique
au lieu du message "Cette recette n'existe pas." — bug dans mon propre
test, sans rapport avec le refactor.
- pnpm lint (biome) : les fichiers touchés par le refactor précédent
avaient quelques soucis de formatage/tri d'imports (des sed multi-
fichiers, pas d'édition via l'outil habituel) — corrigés par
`biome check --write`.
Vérifié : ces 3 specs + recipe-form.cy.ts passent maintenant dans le job
CI GitHub Actions (Linux, Cypress s'y exécute réellement — contrairement
à cet environnement Windows sandboxé, voir les commits précédents) ; 102
tests Mocha + 32 scénarios Cucumber toujours au vert en local.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
- .ingredient-picker__display-toggle svg : 1.1rem -> 1.5rem, trop
petite dans son bouton 2.25rem.
- .ingredient-picker__display-menu : neutralise le margin-left: auto
du check-mark app-wide (global.scss, pensé pour une grille de chips
avec coche en bout de ligne) pour ce menu vertical de préférences —
coche à gauche, libellé à droite, plus classique pour ce type de menu.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Remplace les deux boutons icône seule (régimes/allergènes) par un
bouton réglages unique ouvrant un menu avec deux cases à cocher
labellisées — les icônes seules n'étaient pas assez explicites sur ce
qu'elles activaient/désactivaient.
En chemin, corrige un bug réel découvert pendant l'implémentation : les
cases à cocher rendaient invisibles (la règle globale "selectable
card" de global.scss masque le <input type="checkbox"> natif et
attend un <span class="check-mark"> + une classe is-selected sur le
<label> pour dessiner l'état coché — mes cases n'avaient ni l'un ni
l'autre). Corrigé en suivant exactement le même pattern que
AllergySelect.tsx.
Supprime AllergenIcon (nav-icons.tsx), devenu inutilisé.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Les boutons de filtre (catégorie/sous-catégorie/toggles) et les cartes
d'ingrédients n'avaient pas user-select: none — un clic qui bouge d'un
pixel (clic rapide, trackpad) est alors lu comme un glisser de
sélection de texte, et le surlignage natif du navigateur s'affiche à
la place de l'état .active attendu. Vu de l'extérieur ça ressemble à
un bug de CSS de sélection flaky ; c'est en fait ce surlignage natif
qui apparaît par intermittence.
Ajouté sur .ingredient-picker (hérité par tous ses boutons enfants),
avec un user-select: text explicite ré-appliqué sur le champ de
recherche pour ne pas casser le copier/coller dedans.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deux usages d'emoji supprimés, remplacés par un vrai jeu d'icônes SVG
(style ligne, cohérent avec layouts/nav-icons.tsx) :
- Catégories/sous-catégories du picker (auparavant préfixées d'un
emoji dans les libellés) : nouvelles icônes CategoryIcon/
SubcategoryIcon, une par catégorie/sous-catégorie.
- Icône par ingrédient (auparavant un emoji différent pour chacun des
437 ingrédients) : remplacé par un petit vocabulaire de ~22
pictogrammes génériques ("un légume", "une bouteille", "un
fromage"...) réutilisés selon la nature de l'ingrédient plutôt
qu'un dessin par ingrédient (irréaliste à la main pour 437 items).
- Schéma : nouvel enum IngredientIcon (22 valeurs), colonne
Ingredient.icon passe de String? (texte libre) à IngredientIcon
(non nullable, toujours une valeur générique pertinente désormais).
- reference-seed-data.ts : chaque groupe porte un defaultIcon (calqué
sur sa sous-catégorie), avec override par ingrédient pour les
exceptions (ex. les fromages dans "produits laitiers", les jus/cafés
dans "assaisonnements").
- apps/web/src/features/recipes/ingredient-icons.tsx (nouveau) :
22 icônes SVG + CategoryIcon/SubcategoryIcon (réutilisent le même
vocabulaire pour représenter chaque catégorie/sous-catégorie).
- packages/shared : IngredientView.icon devient IngredientIcon (union
de 22 valeurs) au lieu de string | null.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deux boutons icône en bout de la barre de recherche de l'IngredientPicker
(régimes/feuille, allergènes/triangle), même pattern bouton-icône +
état actif que FavoriteStarButton. Préférence d'affichage locale au
picker (non persistée) — n'affecte pas quels ingrédients apparaissent,
seulement si leurs badges allergènes/régimes sont visibles sur les
cartes. Nouvelle icône AllergenIcon dans nav-icons.tsx, réutilise
DietPreferencesIcon existante.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Fix overflow horizontal sur /parametres/preferences : <fieldset> a un
min-width: min-content par défaut du navigateur, ce qui empêchait la
grille de l'IngredientPicker de wrapper (page entière poussée à
~3100px de large). Reset min-width: 0 sur .disliked-ingredients-field.
- Recatégorise les laits/crèmes végétaux (coco, amande, avoine) de
PRODUITS_LAITIERS_OEUFS vers LIQUIDES_BOISSONS — ce ne sont pas des
produits laitiers.
- Nouveau modèle IngredientDiet (many-to-many ingrédient <-> régime) :
quels régimes (Végétarien, Végan, Pescétarien) chaque ingrédient
respecte. Omnivore volontairement absent (trivial) et Sans gluten
aussi (déjà dérivable de l'allergène Gluten existant).
- reference-seed-data.ts : chaque groupe de catégorie porte un
defaultDiets, avec dietNames en override pour les exceptions
(fromages, viandes, poissons, sauces à base d'œuf/poisson...).
- IngredientView.diets exposé par /reference/ingredients et /recipes,
affiché via DietBadges dans IngredientPicker et IngredientRow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Prisma: enum IngredientCategory (18 valeurs) + Ingredient.category,
migration appliquée
- reference-seed-data.ts restructuré en 18 groupes de catégories
- packages/shared: INGREDIENT_CATEGORIES + IngredientView.category
- API: category exposé par /reference/ingredients et /recipes
- Web: nouveau IngredientPicker (chips catégories + recherche + grille
de cartes) remplaçant IngredientAutocomplete, branché dans le
formulaire de recette et le champ aliments-pas-aimés
- i18n: clés recipes.form.category.* et libellés du picker
- Fix test reference.test.ts pour la nouvelle forme d'IngredientView
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found live on http://batch.dev.kyuno.fr/: every API call went to
http://localhost:3000 in the visitor's own browser instead of the
deployed origin, because API_BASE_URL fell back to a hardcoded
"http://localhost:3000" whenever VITE_API_URL wasn't set at build time
— which is exactly the case for the production Docker image (apps/web/.env
is excluded by .dockerignore, so Vite never sees VITE_API_URL there).
Now defaults to "" (same origin as the page), correct for the merged
single-container deployment where the API serves this very frontend
build. Native dev (pnpm dev:web) is unaffected — apps/web/.env still sets
VITE_API_URL=http://localhost:3000 explicitly there.
Verified: built the Docker image, confirmed "localhost:3000" no longer
appears in the bundled JS, and docker compose up shows /auth/me called
against the container's own origin.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- apps/api: les tests Planning (mocha et cucumber) lisaient l'horloge
systeme (new Date()/DateTime.utc()) pour construire leurs fixtures et
interroger /planning, ce qui les rendait non deterministes. Ajoute
test-support/reference-date.ts (TEST_REFERENCE_DATE, une date UTC
fixe) et l'utilise dans planning.test.ts / planning.steps.ts a la
place du systeme.
- apps/web: nouveau style global pour tous les radio/checkbox de
l'app (theme-select, allergy-select, onboarding) - "carte
selectionnable" : le controle natif reste reel/accessible mais
visuellement cache, toute la ligne devient la surface interactive
(bordure + fond teinte + coche au survol/selection). Corrige au
passage le bug de fond qui causait le desalignement des radios sur
/parametres/preferences-utilisateur (la regle generique
input, select { width: 100% } de profile-forms.scss s'appliquait
aussi aux checkbox/radio) et une regression de font-weight ou les
lignes non selectionnees du theme apparaissaient en gras comme si
elles l'etaient.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Design validé via une maquette HTML itérée avec l'utilisateur (voir
historique de conversation) avant implémentation.
- nav-icons.tsx: jeu d'icônes trait fin (24x24, inline SVG) pour les 3
items de nav principaux, les 4 pages de Paramètres et le chevron de
repli — pas de librairie d'icônes pour une poignée de glyphes
- AppLayout: bouton replier/déplier à côté du logo — la sidebar passe
de 15rem à un rail 4.25rem icônes-seules ; un seul toggle de classe
CSS sur le <aside> pilote tout (aucun sous-composant n'a besoin de
savoir que la sidebar est repliée), état persisté en localStorage
- Logo : monogramme "bC" en repli plutôt qu'une icône arbitraire à la
place du nom complet
- Avatar rond (initiale) dans le menu compte, popover recalée en
largeur fixe quand la sidebar est repliée (sinon écrasée à 4.25rem)
- title="" sur chaque item pour l'infobulle native une fois replié
- ThemeContext (features/theme/) : charge/applique le thème du profil
connecté via l'attribut data-theme (SYSTEM = pas d'attribut, laisse
la media query prefers-color-scheme décider) ; échec réseau non
bloquant (worst case reste au thème courant, pas d'unhandled rejection)
- UserPreferencesPage, routée /parametres/preferences-utilisateur,
hot-save (3 boutons radio Clair/Sombre/Système)
- layout.settings.nav.preferences renommé "Préférences alimentaires"
(évite la confusion avec ce nouveau concept plus large), nouvelle
entrée "Préférences utilisateur"
- _theme.scss: commentaires mis à jour (l'attribut data-theme est
désormais réellement posé, plus une simple anticipation)
- Remplace HomePage (table jour unique) par PlanningPage : grille
7 jours × 5 repas, groupés Matin/Midi/Après-midi/Soir (séparateurs
pleins, plus épais entre groupes), recettes en pastilles pleine
largeur, bouton "+" pleine largeur sans bordure (pas encore branché
— pas de catalogue de recettes côté API, tâche future)
- WeekNavigator + CalendarPopover (sur date-tools) : flèches semaine
précédente/suivante, popover calendrier (mois navigable, clic sur
un jour → sa semaine), fermeture au clic extérieur
- apiClient.getPlanningForWeek(date) remplace getCurrentPlanning()
- i18n: namespace home → planning (+ nouvelles clés jours/repas/
calendrier), common.loadError factorisé (repris par les pages
Foyer/Préférences qui réutilisaient l'ancien home.error)
- Ordre: régime (1) → foyer (2) → allergènes (3), au lieu de
foyer → régime → allergènes
- L'étape foyer est désormais optionnelle: créer, rejoindre par code
d'invitation, ou passer (aucun foyer n'est créé au signup)