* 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>
|
||
|---|---|---|
| .claude | ||
| .github/workflows | ||
| apps | ||
| packages | ||
| services/tech-step-llm-worker | ||
| specs | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .nvmrc | ||
| biome.json | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.base.json | ||
batchCooking
Structure
Monorepo pnpm workspaces :
apps/api— backend Express/TypeScript : auth, foyer, planning (grille de la semaine), catalogue de recettes (favoris/perso/foyer/publique), import de recettes depuis des sources externes, préférences (thème, régime, allergies, ingrédients détestés). Tests Mocha (base Postgres réelle, isolée de la base de dev — voir plus bas).apps/web— frontend React/Vite/TypeScript, prêt à être embarqué par Capacitor plus tard. Espace connecté complet (planning, recettes, réglages) derrière une sidebar, wizard d'inscription, thème clair/sombre/système.packages/shared— code partagé entreapietweb: schémas zod, types (RecipeView,PlanningView,HouseView,SafeUserProfile...), le contrat d'erreurs (ErrorCodenumérique,ApiErrorResponse, voir specs/error-handling.md) et les libellés anglais du catalogue d'ingrédients (data/catalog-labels-en.ts, utilisés par le matching de recettes importées — voir plus bas) — même règles des deux côtés, pas de risque de dérive entre front et back.packages/error-tools— gestion des erreurs, indépendante de tout framework HTTP (n'importe pasexpress) :HttpError,ErrorHandlerService. Séparé d'express-toolsprécisément parce que rien ici ne dépend d'Express. Détail : specs/error-handling.md.packages/express-tools— outillage Express générique et réutilisable :ExpressServer(init serveur, routes, middlewares),wrapAsyncHandler,createErrorMiddleware(adapteErrorHandlerServicedeerror-toolsà Express) — séparé d'apps/api, pas de logique métier. Détail : specs/backend-architecture.md.packages/date-tools— utilitaires de date partagés (Luxon) : convention "date-only = minuit UTC" (parseDateOnly/formatDateOnly/toDateOnly), calcul de semaine lundi-first (getWeekStart/addWeeks/buildCalendarMonth) — utilisés à la fois parapps/api(validation de date de planning) etapps/web(grille/navigateur de semaine).
packages/shared, packages/error-tools, packages/express-tools et
packages/date-tools ont un vrai build (tsc → dist/, voir leur
package.json) : consommés en JS compilé, pas en TS brut — nécessaire pour un
runtime Node pur (Docker, pas de transpilation à la volée), voir la note dans
specs/frontend-architecture.md.
Prérequis
- Node.js 22 (voir
.nvmrc) - pnpm 10 (
corepack enablepuiscorepack use pnpm@10.12.4, ou installation manuelle) - Docker (pour Postgres en local)
Installation
pnpm install
cp .env.example .env
cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.env
Puis édite ces deux .env pour renseigner de vrais POSTGRES_USER/POSTGRES_PASSWORD
(et la DATABASE_URL correspondante dans apps/api/.env) : les fichiers .env.example
ne contiennent volontairement aucun identifiant réel (juste changeme), et
docker-compose.yml refuse de démarrer tant que POSTGRES_USER/PASSWORD/DB ne
sont pas définis dans .env — pas de valeur par défaut en dur dans les fichiers commités.
Même règle pour apps/api/.env : JWT_SECRET est requis, sans défaut (génère le
tien, voir le commentaire dans apps/api/.env.example).
Si tu comptes lancer pnpm --filter api test (voir Qualité / Tests),
crée aussi apps/api/.env.test — voir la section dédiée plus bas.
Cypress : téléchargement du binaire
pnpm install installe le package cypress mais pas forcément son binaire (le
téléchargement du .exe/binaire natif peut être ignoré selon l'environnement où
pnpm install a été lancé — ex. un environnement sandboxé/CI dont le cache ne
correspond pas à celui de ta machine). Si pnpm --filter web e2e échoue avec une
erreur du type :
No version of Cypress is installed in: ...\AppData\Local\Cypress\Cache\...
Please reinstall Cypress by running: cypress install
lance simplement, depuis ta machine :
pnpm --filter web exec cypress install
(à faire une seule fois par machine ; le binaire est mis en cache localement, hors du repo).
Développement
# Base de données Postgres locale
docker compose up -d postgres
# Applique le schéma (première fois / après un changement de prisma/schema.prisma)
pnpm --filter api exec prisma migrate dev
# Peuple les données de référence (régimes, allergènes, ingrédients, unités,
# techniques...) — automatique après `prisma migrate reset`, sinon à la main :
pnpm --filter api prisma:seed
# Backend (http://localhost:3000)
pnpm dev:api
# Frontend (http://localhost:5173)
pnpm dev:web
Conflit de port possible sur
5432: si tu as déjà un Postgres natif installé sur ta machine (service Windows, Homebrew, etc.), il peut occuper le port 5432 et intercepter les connexions à la place du conteneur Docker (symptôme : Prisma renvoieP1000: Authentication failedalors que les identifiants sont corrects). Dans ce cas, metsPOSTGRES_PORT=5433(ou autre) dans ton.envet adapte le port dans laDATABASE_URLdeapps/api/.env.
Toujours cibler
postgres, jamaisdocker compose up -dtout court. Le mêmedocker-compose.ymldéfinit aussi le serviceapp(voir Déploiement) — celui que Portainer construit en production. Sans nom de service,docker compose up -ddémarre les deux : ça déclenche unpnpm installsur tout le monorepo (donc aussi lecypressd'apps/web, avec son téléchargement de binaire) rien que pour builder une image dont le dev local n'a pas besoin (on sert le front/back directement viapnpm dev:web/pnpm dev:api, pas ce conteneur).
Qualité / Tests
Conventions de code (classes vs objets littéraux, préfixe _ sur les membres
privés, règles Biome actives, logs côté serveur, etc.) :
specs/dev-conventions.md.
pnpm lint # Biome (lint + format check)
pnpm lint:fix # Biome --write
pnpm test # tests unitaires/intégration (Mocha, apps/api)
pnpm --filter web e2e # tests e2e (Cypress + Cucumber, démarre le serveur dev automatiquement)
pnpm --filter web cy:run:component # tests de composant UI isolés (Cypress component testing)
pnpm build # build de tous les workspaces
La CI GitHub Actions (.github/workflows/ci.yml) exécute quatre jobs indépendants (lint, test, build, e2e — ce dernier lance aussi cy:run:component) en parallèle, sur chaque push (toutes branches) et sur chaque PR vers main — pas de chaînage entre eux, chacun apparaît comme son propre check. Voir aussi Déploiement pour le pipeline de release (.github/workflows/release.yml).
Base de test isolée de la base de dev (apps/api)
pnpm --filter api test exécute une TRUNCATE ... CASCADE sur presque tout le
schéma avant chaque test (test-support/reset-db.ts). Pour ne jamais
risquer de vider une vraie base de dev locale, NODE_ENV=test (posé par le
script test) fait charger apps/api/.env.test au lieu de .env — un
fichier à créer toi-même, pas fourni automatiquement :
cp apps/api/.env.test.example apps/api/.env.test
# puis édite-le : mêmes identifiants Postgres que ton .env, mais une base
# différente (ex. batchcooking_test) — .env.test.example documente les
# commandes exactes pour la créer et lui appliquer le schéma.
Un garde-fou (assertRunningAgainstTestDatabase()) refuse d'exécuter
resetDatabase() si DATABASE_URL ne contient ni "test" ni "ci" — la
seule base qu'il doit rejeter est ta vraie base de dev.
Déploiement
Une seule image Docker (apps/api/Dockerfile) sert à la fois l'API et le frontend
buildé — plus de conteneur nginx séparé pour apps/web. Le stage build compile
apps/api et apps/web (pnpm --filter web build), le stage runtime copie
le résultat (apps/web/dist) à côté de l'API ; au démarrage, apps/api/src/app.ts
sert ce dossier statique (fallback SPA compris, pour le routing react-router côté
client) via FRONTEND_DIST_DIR — voir packages/express-tools/src/express-server.ts
(serveStaticFrontend). Cette variable n'est renseignée que dans l'image Docker :
en dev natif (pnpm dev:api), elle reste vide et pnpm dev:web continue de servir
le frontend via son propre serveur Vite (HMR), sur un port séparé, comme avant.
Le CMD de l'image enchaîne trois étapes, chacune dans son propre processus
node : prisma migrate deploy (applique les migrations), puis
node dist/scripts/seed-runtime.js (seed des données de référence et
synchronisation de la table sources depuis le registre d'adaptateurs de code
— nécessaire à chaque démarrage : le registre en mémoire peuplé par
server.ts ne survit pas au changement de processus, voir
specs/backend-architecture.md),
puis node dist/server.js. Les trois étapes sont sûres/idempotentes à
répéter à chaque redémarrage du conteneur.
docker-compose.yml ne définit donc que deux services : postgres et app (un
seul port, APP_PORT, défaut 3000 — plus de WEB_PORT/CORS_ORIGIN à
coordonner entre deux origines, le frontend et l'API sont désormais servis depuis
la même origine).
Pas de registre d'image dans cette configuration : l'instance Portainer de
production est reliée directement au dépôt Git et reconstruit elle-même
docker-compose.yml/apps/api/Dockerfile à chaque déploiement — la CI ne pousse
donc aucune image nulle part.
Release (.github/workflows/release.yml)
Déclenchée par un tag vX.Y.Z :
git tag vX.Y.Z
git push --tags
Le pipeline enchaîne trois jobs : sanity-build (build de l'image Docker sans
push, juste pour vérifier qu'elle build encore à ce tag avant de laisser Portainer
redéployer dessus), github-release (crée une Release GitHub avec changelog
auto-généré à partir des PRs mergées), puis notify-portainer — envoie une requête
au webhook de redeploy de Portainer si le secret de dépôt PORTAINER_WEBHOOK_URL
est configuré (sinon Portainer se resynchronise simplement à son prochain
polling Git). Pour l'activer : récupérer l'URL du webhook depuis les réglages du
stack Portainer, puis l'ajouter comme secret GitHub PORTAINER_WEBHOOK_URL.
Auth (apps/api)
Inscription (création de profil + foyer) et connexion, JWT dans un cookie httpOnly.
POST /auth/signup—{ firstName, lastName, email, password }→ crée le foyer (house) et le profil (user_profiles) en une transaction, pose le cookie de session, renvoie le profil (201)POST /auth/login—{ email, password }→ pose le cookie de session, renvoie le profil (200) ; message d'erreur volontairement générique (401) que ce soit l'email ou le mot de passe qui soit incorrectPOST /auth/logout— efface le cookie (204)GET /auth/me— profil courant, nécessite le cookie de session (401 sinon)DELETE /auth/me— supprime définitivement le compte après re-saisie du mot de passe ({ password }, 401INVALID_CREDENTIALSsi incorrect) ; gère le départ/transfert d'adminship du foyer avant suppression (voir Foyer plus bas)
Mots de passe hachés avec argon2. UserProfile.tokenVersion existe pour
invalider les JWT déjà émis (ex. futur changement de mot de passe) mais rien
ne l'incrémente encore — pas de route de changement d'email/mot de passe
aujourd'hui, seulement la suppression de compte.
argon2 : version pinnée à
0.31.2, pas de^. La version0.45.1(dernière au moment de l'écriture) segfault au runtime sur au moins une configuration Windows — reproduit de façon stable (bash sandboxé, bash non-sandboxé, PowerShell), alors que0.31.2fonctionne parfaitement avec la même API. Si tu montes la version, revérifie concrètement (argon2.hash(...)dans unnode -e) avant de merger, unpnpm buildqui passe ne suffit pas à détecter un crash runtime.
Les tests (Mocha) tournent avec un coût argon2 réduit
(NODE_ENV=test, voir auth.service.ts) — le coût par défaut est volontairement
élevé (sécurité), ce qui rendrait la suite de tests lente/instable sinon. La CI
provisionne un vrai Postgres de service (.github/workflows/ci.yml) et exécute
prisma migrate deploy avant les tests.
Foyer — création, invitation, admin, sources externes (apps/api)
Un foyer (house) a un admin (adminId) et un code d'invitation à 8
caractères (inviteCode, alphabet sans caractères ambigus 0/O/1/I).
GET/PATCH /house/current— foyer courant.PATCH { name }ouvert à tout membre.POST /house— crée un foyer (l'appelant devient admin) ;POST /house/join { inviteCode }— rejoint un foyer existant. Les deux 409ALREADY_HAS_HOUSEsi le profil a déjà un foyer.POST /house/leave— quitte le foyer courant. Si le partant était l'admin, l'adminship passe au membre restant le plus ancien ; si plus personne ne reste, le foyer est supprimé (un foyer ne peut jamais rester sans admin).DELETE /house/current— supprime le foyer (403NOT_HOUSE_ADMINsi appelé par un non-admin).DELETE /house/members/:id— retire un membre (admin seulement, pas de self-retrait par cette route, utiliser/leave).GET/PATCH /house/current/sources— quelles sources externes de recettes (voir plus bas) le foyer voit dans son catalogue —{ sourceIds: number[] }, remplace (pas de fusion), opt-in (aucune source activée par défaut).
Détail complet (génération du code, transfert d'adminship) : specs/backend-architecture.md.
Planning (apps/api)
GET /planning?date=YYYY-MM-DD— planning de la semaine (lundi→dimanche) couvrantdate, pour le foyer de l'utilisateur connecté —PlanningView | null(null= pas de foyer, ou aucun planning pour cette semaine, deux cas normaux confondus, jamais une erreur).POST /planning/items— ajoute une recette à un créneau :{ date, weekDay, meal, recipeId, portions }.portionsest saisi indépendamment du rendement propre de la recette (Recipe.portions) — un créneau peut mettre à l'échelle.DELETE /planning/items/:id— retire un item du planning.
Le planning d'une semaine est créé à la demande (première recette ajoutée), jamais en avance.
Recettes — catalogue, favoris, import depuis une source externe (apps/api)
GET /recipes?tab=favoris|perso|foyer|publique&search=&suitableForHousehold=&ingredientIds=&dietIds=— catalogue filtré par onglet + filtres optionnels.PERSONAL/HOUSE/PUBLIC(Recipe.visibility) contrôlent qui peut lire une recette (jamais qui peut l'éditer, toujours réservé à l'auteur) ; les recettes issues d'une source externe non activée pour le foyer du viewer sont masquées de tous les onglets.GET /recipes/:id,POST /recipes,PATCH /recipes/:id,DELETE /recipes/:id(409RECIPE_IN_USEsi encore référencée par un planning),POST/DELETE /recipes/:id/favorite.- Import depuis une source externe (
/sources) :GET /sources/:sourceKey/browse(parcourir),GET /sources/:sourceKey/preview/:externalId(prévisualiser sans sauvegarder — ingrédients/unités/techniques déjà résolus contre les catalogues),POST /sources/:sourceKey/import/:externalId(finaliser — même payload qu'une création manuelle). Un item de source n'est sauvegardé qu'en conséquence de son ajout au planning (import transparent si tout est résolu) ou d'une revue manuelle (ingrédients ambigus à choisir à la main) — jamais un bouton "importer" isolé. Une seule source concrète aujourd'hui : TheMealDB (API officielle, catalogue anglais).
Détail complet (adaptateurs, algorithmes de matching ingrédients/techniques,
synchronisation de la table sources) :
specs/backend-architecture.md.
Données de référence — régimes, allergènes, ingrédients, unités, techniques (apps/api)
GET /reference/diets,/allergies,/ingredients,/units,/tech-steps,/sources— tous publics (pas derequireAuth) : ce sont des données de référence, pas des données de foyer, et le wizard d'inscription doit pouvoir les lire avant qu'un compte n'existe.
Données seedées via apps/api/src/db/reference-seed-data.ts (pnpm --filter api prisma:seed, ou automatiquement après prisma migrate reset) — jamais
créées/éditées/supprimées via l'API applicative. key/name sont @unique
pour permettre un seed idempotent (upsert). Le catalogue d'ingrédients
(400+) est organisé en 7 rayons/sous-catégories façon supermarché français, et
chaque allergène est classé ALLERGY (immunitaire) ou INTOLERANCE
(Gluten/Sulfites). Détail complet du schéma :
specs/batch-cooking-modele.md.
Foyer & profil — régime, allergènes, ingrédients détestés (apps/api)
Nécessitent tous une session (requireAuth) — données propres à
l'utilisateur/au foyer, pas des données de référence.
PATCH /profile/diet { dietId: number | null }— régime du profil connecté ;nullefface le régime.GET/PATCH /profile/allergies— allergènes/intolérances (medical), liste d'IDs, remplace (pas de fusion).GET/PATCH /profile/disliked-ingredients— ingrédients personnellement "pas aimés" (goût, pas médical — ne déclenche jamais un avertissement de sécurité, juste un rappel discret sur la fiche recette), même contrat de remplacement.GET/PATCH /preferences { theme: "LIGHT"|"DARK"|"SYSTEM" }— préférence d'affichage, upsert (pas de ligne tant que rien n'a été choisi, défautSYSTEM).
apps/api/src/lib/safe-profile.ts centralise le retrait du passwordHash
(toSafeProfile).
Page de connexion / inscription (apps/web)
src/api/client.ts—ApiClient(classe, instance unique exportéeapiClient) : enveloppefetchvers l'API (credentials: "include", requis pour que le cookie de session httpOnly parte/revienne — l'API et le front sont sur des origines différentes). URL configurable viaVITE_API_URL(voir.env.example).src/features/auth/AuthContext.tsx— état d'auth global ; appelleGET /auth/meau chargement pour restaurer la session depuis le cookie ;deleteAccount()pour la suppression de compte.src/features/auth/RequireAuth.tsx/RedirectIfAuthenticated.tsx— gardes de route (react-router-dom) : l'espace connecté exige d'être connecté,/loginet/signupredirigent vers/si on l'est déjà.src/pages/{Login,Signup}Page.tsx— validation client instantanée via les schémas zod partagés (packages/shared), erreurs API traduites viaErrorMessageService(voir ci-dessous).
Détail de l'organisation complète (dossiers, routing, SCSS/theming) : specs/frontend-architecture.md.
Sidebar, planning, recettes & sections (apps/web)
Une fois connecté, l'utilisateur atterrit sur src/layouts/AppLayout.tsx —
sidebar (nav Planning/Recettes/Liste de courses, sous-menu Paramètres
repliable, menu compte en pied) et <Outlet /> pour la route active — montée
une seule fois comme route parente de tout l'espace authentifié (App.tsx).
/—PlanningPage: grille complète de la semaine (7 jours × 5 repas), navigation par semaine avec mini-calendrier, ajout viaRecipePickerDialog(parcourir le catalogue et les sources externes, prévisualiser avant de confirmer, import transparent en un clic si la recette d'une source n'est pas encore résolue automatiquement, sinon revue intégrée dans le même dialogue)./recettes(+/recettes/:id,/recettes/sources/:sourceKey/:externalId) —RecipesPage, vue maître-détail : onglets favoris/perso/foyer/publique plus un onglet par source externe activée pour le foyer, tableau + panneau de détail (surlignage des techniques détectées avec infobulle, icônes d'ingrédients génériques, badges régime/allergènes/reproductible)./recettes/nouvelleet/recettes/:id/modifier(RecipeFormPage) pour la création/édition manuelle./liste-de-courses— toujours un stub (ComingSoonPage), le module « Calcul batch-cooking » resteTODO(voir specs/batch-cooking-architecture.md)./parametres/*— Compte (identité + suppression), Préférences (régime/allergies/ingrédients détestés), Foyer (création/invitation, membres, sources activées), Préférences utilisateur (thème clair/sombre/système), Crédits (attribution des icônes CC BY 4.0).
Détail complet (pourquoi une seule route parente, le flux d'import détaillé,
les composants UI partagés Dialog/Checkbox/Radio/Tooltip) :
specs/frontend-architecture.md.
Parcours d'inscription — onboarding (apps/web)
Wizard de 4 écrans lancé une fois juste après l'inscription :
/onboarding/regime → /onboarding/foyer → /onboarding/sources
(conditionnelle, sautée si aucun foyer n'a été créé/rejoint à l'étape
précédente) → /onboarding/allergenes. Chaque étape a un unique bouton
"Continuer" qui envoie la valeur courante (y compris "aucune" pour
régime/allergènes) — pas de bouton "Passer" séparé, skip implicite. Routes
top-level RequireAuth, pas nichées sous AppLayout : wizard plein écran
sans sidebar, même langage visuel que /login//signup. Les mêmes réglages
restent modifiables à tout moment depuis /parametres/* (hot saving, pas de
bouton "Enregistrer" — chaque champ sauvegarde peu après la dernière
modification).
Tests Cypress (apps/web/cypress/e2e/*.cy.ts et *.feature +
@badeball/cypress-cucumber-preprocessor) : mockent l'API via cy.intercept
plutôt que de dépendre d'un vrai backend — le job e2e de la CI ne provisionne
pas de Postgres/API, seulement le serveur de dev Vite. Le comportement réel de
l'API est couvert par la suite Mocha d'apps/api (contre une vraie base).
Détail du dispositif de test (Gherkin + steps partagés, tests de composant) :
specs/frontend-architecture.md.
Cypress ne peut pas tourner en local dans un environnement Windows sandboxé : Chromium/Electron headless plante au lancement du process GPU (
GPU process isn't usable), reproductible surmainaussi bien que sur une branche de feature — pas un problème introduit par une modification du code.pnpm --filter web e2efonctionne normalement en CI (GitHub Actions) et sur une machine de dev classique ; dans cet environnement précis, vérifier manuellement via le serveur de dev (pnpm dev:web+pnpm dev:apien local, pas le conteneur Docker — voir Déploiement — qui sert le frontend buildé, pas le serveur de dev Vite).
Gestion des erreurs (API ↔ web)
Contrat d'erreurs partagé via packages/shared (ErrorCode, énumération
numérique groupée par famille — 4000 validation, 401x auth, 402x
conflit/état invalide, 403x autorisation, 404x not found, 500x interne
— et ApiErrorResponse) : l'API renvoie toujours
{ code, message, details? } (message en anglais, dev-facing — jamais affiché tel
quel), et le client traduit code en libellé français via i18next
(ErrorMessageService, apps/web/src/services/error-message.service.ts →
apps/web/src/locales/fr/translation.json). Côté API, ErrorHandlerService
(packages/error-tools) et createErrorMiddleware (packages/express-tools)
centralisent la transformation de toute erreur levée en réponse HTTP conforme —
aucune valeur ErrorCode codée en dur nulle part (toujours ErrorCode.XXX, y
compris dans les mocks Cypress).
Détail complet (schéma, liste des ~19 codes actuels, exemples, comment ajouter un nouveau code d'erreur) : specs/error-handling.md.
Le profil authentifié (requireAuth) passe par res.locals.userProfile
(typé via AuthLocals), pas par une augmentation du namespace global Express —
voir specs/backend-architecture.md pour le détail
et le pourquoi.
packages/shared fournit aussi assertIsNever (vérification d'exhaustivité de
switch/if-chain sur une union, erreur de compilation si un cas est oublié) —
voir specs/backend-architecture.md.
i18n
i18next + react-i18next — tout le texte affiché (formulaires, boutons,
erreurs) vient de fichiers de locale JSON (apps/web/src/locales/<lng>/translation.json),
jamais codé en dur dans un composant. Une seule langue existe aujourd'hui (fr) ;
en ajouter une est une question de fichier de locale, pas de code. Les
libellés des tables de référence (régimes, allergènes, ingrédients, unités,
techniques) vivent aussi dans ce fichier (catalog.*, par key stable de
schema.prisma), jamais stockés en base. Détail :
specs/frontend-architecture.md.
Thème clair / sombre / système (apps/web)
Préférence par utilisateur, persistée côté serveur (GET/PATCH /preferences, pas localStorage). Tokens de design en custom properties CSS
(apps/web/src/styles/_theme.scss) redéfinies sous [data-theme="dark"]
(choix explicite) ou sous prefers-color-scheme: dark quand aucun
data-theme n'est posé (choix "système", le défaut). Détail :
specs/frontend-architecture.md.
Données de test (faker.js)
apps/api utilise @faker-js/faker pour toutes les données
de test dans test/*.test.ts (Mocha) — jamais de nom/email qui ressemble à une
vraie personne en dur dans un fixture.