# 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é entre `api` et `web` : schémas zod, types (`RecipeView`, `PlanningView`, `HouseView`, `SafeUserProfile`...), le contrat d'erreurs (`ErrorCode` numérique, `ApiErrorResponse`, voir [specs/error-handling.md](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 pas `express`) : `HttpError`, `ErrorHandlerService`. Séparé d'`express-tools` précisément parce que rien ici ne dépend d'Express. Détail : [specs/error-handling.md](specs/error-handling.md). - `packages/express-tools` — outillage Express générique et réutilisable : `ExpressServer` (init serveur, routes, middlewares), `wrapAsyncHandler`, `createErrorMiddleware` (adapte `ErrorHandlerService` de `error-tools` à Express) — séparé d'`apps/api`, pas de logique métier. Détail : [specs/backend-architecture.md](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 par `apps/api` (validation de date de planning) et `apps/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](specs/frontend-architecture.md#note-sur-les-fichiers-dts). ## Prérequis - Node.js 22 (voir `.nvmrc`) - pnpm 10 (`corepack enable` puis `corepack use pnpm@10.12.4`, ou installation manuelle) - Docker (pour Postgres en local) - Python 3.12+ et [`uv`](https://docs.astral.sh/uv/) (pour `services/tech-step-intent-service` en dev natif — requis, voir plus bas) ## Installation ```bash 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](#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 : ```bash pnpm --filter web exec cypress install ``` (à faire une seule fois par machine ; le binaire est mis en cache localement, hors du repo). ## Développement ```bash # 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 # Microservice de détection des techniques (spaCy) — requis, `pnpm dev:api` # ne peut plus détecter aucune technique de cuisine sans lui. Lance-le en # premier et laisse-le tourner : il s'entraîne lui-même à chaque démarrage # (~7 minutes pour le corpus actuel, voir son propre README) avant de # répondre quoi que ce soit sur /health. cd services/tech-step-intent-service uv sync cp .env.example .env # édite-le : même INTENT_SERVICE_SECRET que apps/api/.env uv run uvicorn intent_service.main:app --reload --port 8000 cd ../.. # 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 > renvoie `P1000: Authentication failed` alors que les identifiants sont corrects). > Dans ce cas, mets `POSTGRES_PORT=5433` (ou autre) dans ton `.env` **et** adapte le > port dans la `DATABASE_URL` de `apps/api/.env`. > **Toujours cibler `postgres`, jamais `docker compose up -d` tout court.** Le même > `docker-compose.yml` définit aussi le service `app` (voir [Déploiement](#déploiement)) — > celui que Portainer construit en production. Sans nom de service, `docker compose up -d` > démarre les deux : ça déclenche un `pnpm install` sur tout le monorepo (donc aussi le > `cypress` d'`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 via > `pnpm 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](specs/dev-conventions.md). ```bash 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 cinq jobs indépendants (`lint`, `test`, `intent-service-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. `test` démarre `services/tech-step-intent-service` en arrière-plan (voir ce fichier) puisque la suite Mocha ne mocke jamais un service interne. Voir aussi [Déploiement](#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 : ```bash 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. `services/tech-step-intent-service` doit aussi tourner en local avant `pnpm --filter api test` — les tests touchant `tech-step-matcher.ts` passent par le vrai service (jamais un mock, voir [specs/dev-conventions.md](specs/dev-conventions.md)) et échouent avec une erreur de connexion, pas une assertion utile, s'il n'est pas démarré. Voir la section [Développement](#développement) ci-dessus. ## 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](specs/backend-architecture.md#sources-externes--adaptateur-registre-synchronisation)), puis `node dist/server.js`. Les trois étapes sont sûres/idempotentes à répéter à chaque redémarrage du conteneur. Le duo `postgres`/`app` de `docker-compose.yml` n'expose donc qu'un seul port applicatif, `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. Les deux autres services du fichier (`tech-step-intent-service`, `tech-step-llm-worker`) n'exposent eux aucun port au host — voir leurs propres README pour leur rôle. **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` : ```bash 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 incorrect - `POST /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 }`, 401 `INVALID_CREDENTIALS` si 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 version `0.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 que > `0.31.2` fonctionne parfaitement avec la même API. Si tu montes la version, revérifie > concrètement (`argon2.hash(...)` dans un `node -e`) avant de merger, un `pnpm build` > qui 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 409 `ALREADY_HAS_HOUSE` si 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 (403 `NOT_HOUSE_ADMIN` si 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](specs/backend-architecture.md#house--foyer-adminship-code-dinvitation-sources-activées). ## Planning (apps/api) - `GET /planning?date=YYYY-MM-DD` — planning de la semaine (lundi→dimanche) couvrant `date`, 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 }`. `portions` est 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` (409 `RECIPE_IN_USE` si 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](specs/backend-architecture.md#sources-externes--adaptateur-registre-synchronisation). ## 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 de `requireAuth`) : 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](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é ; `null` efface 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éfaut `SYSTEM`). `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ée `apiClient`) : enveloppe `fetch` vers 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 via `VITE_API_URL` (voir `.env.example`). - `src/features/auth/AuthContext.tsx` — état d'auth global ; appelle `GET /auth/me` au 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é, `/login` et `/signup` redirigent 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 via `ErrorMessageService` (voir ci-dessous). Détail de l'organisation complète (dossiers, routing, SCSS/theming) : [specs/frontend-architecture.md](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 `` 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 via `RecipePickerDialog` (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/nouvelle` et `/recettes/:id/modifier` (`RecipeFormPage`) pour la création/édition manuelle. - **`/liste-de-courses`** — toujours un stub (`ComingSoonPage`), le module « Calcul batch-cooking » reste `TODO` (voir [specs/batch-cooking-architecture.md](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](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](specs/frontend-architecture.md#tests-cypress--cucumber). > **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 sur `main` aussi bien que sur une > branche de feature — pas un problème introduit par une modification du code. > `pnpm --filter web e2e` fonctionne 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:api` en local, pas le conteneur > Docker — voir [Déploiement](#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](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](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](specs/backend-architecture.md#packagesshared--assertisnever). ## i18n **i18next** + **react-i18next** — tout le texte affiché (formulaires, boutons, erreurs) vient de fichiers de locale JSON (`apps/web/src/locales//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](specs/frontend-architecture.md#i18n-internationalisation). ## 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](specs/frontend-architecture.md#thème-clairsombresystème). ## Données de test (faker.js) `apps/api` utilise [`@faker-js/faker`](https://fakerjs.dev/) 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.