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>
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).