* 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>
Incident : lancer `pnpm test` (apps/api) truncait la vraie base de dev
locale — `test-support/reset-db.ts`'s `resetDatabase()` fait un `TRUNCATE
... CASCADE` sur quasiment tout le schéma (dont `house`/`house_source`)
avant *chaque* test, et `.env`/tests partageaient le même
`DATABASE_URL` (un seul fichier `.env`, `NODE_ENV=test` ne changeait
rien). Deux lancements du test suite cette session ont ainsi effacé le
foyer, le compte et les activations de sources d'un utilisateur en
train de tester l'app en local — perte réelle, aucune récupération
possible (TRUNCATE, pas de sauvegarde).
- `config/env.ts` charge désormais `.env.test` (pas `.env`) quand
`NODE_ENV=test` — `.env.test` (local, non commité, comme `.env`)
pointe vers une base Postgres séparée (`batchcooking_test`, même
serveur/identifiants que la base de dev, juste une base différente).
`.env.test.example` documente comment la créer.
- `resetDatabase()` refuse maintenant de tourner si `NODE_ENV !== "test"`
ou si `DATABASE_URL` ne contient pas "test" — garde-fou supplémentaire
si `.env.test` est un jour absent/mal configuré, pour ne plus jamais
reproduire cet incident même en cas d'erreur de configuration.
- `.gitignore` autorise `.env.test.example` (déjà ignoré via `.env.*`,
comme `.env.example` l'est déjà pour `.env`).
Vérifié : snapshot de la base de dev (houses/house_sources/users) avant/
après un lancement complet de `pnpm test` — identique, base de dev
intacte. 282 tests toujours au vert, contre la nouvelle base de test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* 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>
- express-tools: ExpressServer.serveStaticFrontend() sert le build du
frontend (assets + fallback SPA), monté après les routes API et avant
le 404 JSON. Opt-in via FRONTEND_DIST_DIR (uniquement défini dans
l'image Docker) — le dev natif (dev:api/dev:web) est inchangé.
- apps/api/Dockerfile: build aussi apps/web, embarque son dist dans le
runtime ; corrige au passage l'oubli de packages/date-tools. Supprime
apps/web/Dockerfile et nginx.conf (plus de conteneur nginx séparé).
- docker-compose.yml: un seul service "app" (postgres + app), un seul
port APP_PORT, plus de WEB_PORT/CORS_ORIGIN à coordonner entre deux
origines. Garde `build:` (pas de registre — Portainer build depuis le
repo Git).
- ci.yml: éclate le job unique lint-and-test+e2e en 4 jobs indépendants
(lint/test/build/e2e), sans chaînage, déclenchés sur chaque push
(toute branche) + PR vers main.
- release.yml (nouveau): sur tag vX.Y.Z, sanity-build de l'image Docker,
GitHub Release avec changelog auto-généré, puis notification best-effort
du webhook Portainer (secret PORTAINER_WEBHOOK_URL).
- README: documente le conteneur unique et le pipeline de release.
* Add project specs, gitignore the source PDF
specs/batch-cooking-architecture.md and specs/batch-cooking-modele.md
are the clean markdown transcription of "Projet batch cooking.pdf"
(a scanned/image-only PDF, no extractable text). The PDF itself is
gitignored — source working document, not meant to be committed.
* Add Prisma schema for the documented data model
Models every table from specs/batch-cooking-modele.md: users/household
(user_profiles, house, diet, allergy, category), planning (planning,
planning_item), and recipes (recipe, ingredients, step, tech_step,
tech_step_mapping, sources).
Two deliberate deviations from the literal spec doc, per project
discussion:
- recipe_ingredient (recipe <-> ingredients) carries quantity + unit.
The spec describes a plain many-to-many with no extra fields, but a
shopping list / batch-cooking calculation needs quantities.
- step is modeled one-to-many from recipe (not many-to-many as labeled
in the doc): the documented `order` column only makes sense scoped
to a single recipe, which isn't reconcilable with steps being
shared across recipes.
Everything else follows the doc as-is, including field nullability
choices made where the doc doesn't specify (e.g. user_profiles.house_id
optional, recipe.source_id optional) and onDelete behavior (Cascade
for owned child records, SetNull for optional references) — first
draft, not meant as final production hardening.
Verified: `prisma validate`, `prisma generate`, and a real
`prisma migrate dev` against a local Postgres (via docker-compose) —
the migration applies cleanly and produces the expected schema.
README: documents the migrate command and a Postgres port-conflict
gotcha hit during validation (a native Postgres service on this
machine was already bound to 5432, intercepting the Docker container's
connections).
* Add COMMENT ON for every table and column in the init migration
Descriptions pulled from specs/batch-cooking-modele.md's per-table
field tables. The two tables not in the original spec (join tables
recipe_ingredient, user_profile_allergy) get a comment explaining
why they exist.
Amends the still-unmerged init migration directly rather than adding
a follow-up migration, since it hasn't been applied anywhere but this
local dev database.
Verified: `prisma migrate reset --force` reapplies cleanly, and a
query against pg_description confirms every column of every project
table has a comment (only Prisma's own internal _prisma_migrations
table is uncommented, out of scope).