Changement d'architecture demande par l'utilisateur : le dataset d'entrainement (TECH_STEP_TRAINING_DATA) quitte apps/api pour vivre entierement dans services/tech-step-intent-service (intent_service/training_data.py). Ce service est desormais autonome : il s'entraine lui-meme une seule fois, a son propre demarrage (PipelineRegistry.initialize, dans le lifespan FastAPI), sans plus dependre d'un POST /v1/train pousse par apps/api (route supprimee). apps/api ne connait plus aucune technique/synonyme, uniquement le resultat de POST /v1/process. Corpus enrichi avec les 48 techniques du lexique fourni (Arroser, Appertiser, Braiser, Caraméliser, Confire, Julienne/Brunoise/Mirepoix/ Paysanne, Cuire à blanc/au bain-marie/à l'étouffée, Déglacer variantes, Emulsionner, Glacer, Pocher, Réduire, Suer, Zester, etc.), soit 74 techniques au total (26 + 48). Integration complete bout en bout : - reference-seed-data.ts : 48 nouvelles entrees TECH_STEPS - apps/web/locales/fr/translation.json : libelles francais correspondants - "Mitonner" fondu comme synonyme de simmer (pas une technique distincte, sa propre definition le dit) - "Blanchir un oeuf" (whiskPale) distingue de "Blanchir un legume" (blanch, existant) via des synonymes en phrase complete plutot qu'au mot nu — filter_spans (deja en place) resout la collision par specificite Impact performance mesure : le corpus elargi (74 classes vs 26) rend l'entrainement bien plus lent a nombre d'iterations egal (150 iterations depassait 17 minutes par run de test) — reduit a 40 iterations apres mesures repetees en local (~200s/locale, ~400s pour fr+en combines). docker-compose.yml (healthcheck start_period 600s), CI (timeout curl 600s) et le README du service documentent ce nouveau temps de demarrage. CONFIDENCE_THRESHOLD recalibre a 0.2 par verification manuelle (0.75 puis 0.45 ne tenaient plus compte tenu du nombre de classes) — marque explicitement comme placeholder en attendant une vraie repasse de calibrate-tech-step-threshold.ts (necessite Postgres, indisponible dans cet environnement). Verifie : 28/28 tests pytest du service (suite complete re-ecrite pour s'entrainer une seule fois par session sur le vrai corpus, fixture partagee dans conftest.py), lint + build complets du monorepo. La suite Mocha d'apps/api reste a confirmer via CI (le root hook mocha n'attend plus l'entrainement, seulement CI's propre attente sur /health). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
501 lines
27 KiB
Markdown
501 lines
27 KiB
Markdown
# 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 `<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 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/<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](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.
|