diff --git a/README.md b/README.md index ce79f96..627e39d 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,21 @@ Monorepo pnpm workspaces : -- `apps/api` — backend Express/TypeScript (squelette générique : healthcheck, config env, Prisma non modélisé, tests Mocha) -- `apps/web` — frontend React/Vite/TypeScript, prêt à être embarqué par Capacitor plus tard. - Page de connexion/inscription en place ; le reste est encore un squelette générique. -- `packages/shared` — code partagé entre `api` et `web` : schémas zod (`signupSchema`, - `loginSchema`), types (`SafeUserProfile`), et le contrat d'erreurs (`ErrorCode` - numérique, `ApiErrorResponse`, voir [specs/error-handling.md](specs/error-handling.md)) — - même règles des deux côtés, pas de risque de dérive entre front et back. +- `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 : @@ -19,11 +27,16 @@ Monorepo pnpm workspaces : (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` et `packages/express-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 +`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 @@ -49,6 +62,9 @@ sont pas définis dans `.env` — pas de valeur par défaut en dur dans les fich 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 @@ -80,6 +96,10 @@ 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 @@ -105,14 +125,34 @@ pnpm dev:web ## Qualité / Tests ```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, démarre le serveur dev automatiquement) -pnpm build # build de tous les workspaces +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`) 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](#déploiement) pour le pipeline de release (`.github/workflows/release.yml`). +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](#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. ## Déploiement @@ -126,6 +166,16 @@ client) via `FRONTEND_DIST_DIR` — voir `packages/express-tools/src/express-ser 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. + `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 @@ -166,10 +216,14 @@ Inscription (création de profil + foyer) et connexion, JWT dans un cookie httpO 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. Le hash est indépendant du foyer : un profil crée -toujours son propre foyer à l'inscription (rejoindre un foyer existant n'est pas -encore implémenté). +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 — @@ -184,173 +238,178 @@ Les tests (Mocha) tournent avec un coût argon2 réduit provisionne un vrai Postgres de service (`.github/workflows/ci.yml`) et exécute `prisma migrate deploy` avant les tests. -> **Les tests automatisés et `pnpm dev:api` partagent la même base Postgres locale.** -> Lancer `pnpm test` **vide `user_profiles`/`house`** (`TRUNCATE ... CASCADE`, -> voir `test-support/reset-db.ts`) — si tu es en train de tester manuellement à la main -> (via le navigateur ou curl) contre le serveur de dev, un run de tests en parallèle -> efface tes données de test sans prévenir. Pas un bug, juste à savoir. +## 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` — nécessite le cookie de session (401 sinon). - Renvoie le planning du foyer de l'utilisateur connecté qui couvre `date` - (`Planning` dont `start_date <= date <= finish_date`), items inclus avec leur - recette résolue en `{ id, name }` — ou `null` s'il n'y en a aucun (foyer sans - planning couvrant cette semaine, ou profil sans foyer). `null` est une - réponse **valide** (200), pas une erreur. -- `POST /planning/items` — ajoute une recette à un créneau (jour/repas) du - planning du foyer connecté, créant la semaine correspondante à la volée si - besoin. +- `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. -- Type de réponse partagé : `PlanningView`/`PlanningItemView` - (`packages/shared/src/types/planning.ts`), consommé tel quel par `apps/web`. -Détail de `AsyncRequestHandler`/`wrapAsyncHandler` (`packages/express-tools`) — -premier endpoint à combiner `requireAuth`/`AuthLocals` avec un handler async, ce qui -a mis au jour une contrainte générique trop stricte, corrigée à la source : -[specs/backend-architecture.md](specs/backend-architecture.md). +Le planning d'une semaine est créé à la demande (première recette ajoutée), +jamais en avance. -## Données de référence — régimes & allergènes (apps/api) +## Recettes — catalogue, favoris, import depuis une source externe (apps/api) -- `GET /reference/diets` — liste des régimes alimentaires (`Diet`, 5 valeurs seedées). -- `GET /reference/allergies` — liste des allergènes sélectionnables, `{ id, name }` - (le nom vient de `Category.name` — la table `allergy` elle-même ne porte pas de - nom, voir `schema.prisma` — chaque allergène = une `Category` + une unique - `Allergy` sous cette catégorie). +- `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). -Les deux sont **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 (donc une session) n'existe. +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 seedées via `apps/api/prisma/seed.ts` (`pnpm --filter api prisma:seed`, ou -automatiquement après `prisma migrate reset` — config `prisma.seed` dans -`package.json`). La logique réelle (listes + upsert idempotent) vit dans -`src/db/reference-seed-data.ts`, partagée avec `test-support/reset-db.ts` : chaque -test repart d'une base **avec** ces données de référence, pas de tables vides — -nécessaire pour tester `dietId`/`allergyIds` sur de vraies lignes. +## Données de référence — régimes, allergènes, ingrédients, unités, techniques (apps/api) -`Diet.name` et `Category.name` sont `@unique` — ajouté à ce schéma (pas dans le doc -spec d'origine) précisément pour permettre cet upsert idempotent par nom. +- `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. -Liste des 14 allergènes : ceux du règlement UE 1169/2011 (annexe II) — liste -standard, pas inventée. +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). -**Allergies vs intolérances** (retour fonctionnel, pas dans le doc spec d'origine) : -`Category.kind` (`AllergenKind` — `ALLERGY` | `INTOLERANCE`) classe chaque allergène. -Seuls `Gluten` et `Sulfites` sont en `INTOLERANCE` (réaction non-immunitaire -documentée) ; les 12 autres en `ALLERGY` (réaction immunitaire classique). Classifié -par substance, pas par utilisateur — un même foyer ne peut pas déclarer "allergie au -lait" pour un membre et "intolérance au lait" pour un autre ; a suffi pour le besoin -exprimé, à revoir si ça devient un problème réel. `GET /reference/allergies` renvoie -`kind` dans chaque `AllergyView` ; `PATCH /profile/allergies` ne change pas (une -seule liste d'IDs, `kind` ne sert qu'à grouper l'affichage côté client). +## Foyer & profil — régime, allergènes, ingrédients détestés (apps/api) -## Foyer & profil — nom, régime, allergènes (apps/api) +Nécessitent tous une session (`requireAuth`) — données propres à +l'utilisateur/au foyer, pas des données de référence. -Nécessitent tous une session (`requireAuth`) — contrairement aux endpoints de -référence ci-dessus, ce sont des données propres à l'utilisateur/au foyer. - -- `GET`/`PATCH /house/current` — foyer de l'utilisateur connecté. `GET` renvoie - `null` si le profil n'a pas encore de foyer (cas théorique : le signup en crée - toujours un) ; `PATCH { name }` le renomme (`404 HOUSE_NOT_FOUND` si le profil - n'a pas de foyer). - `PATCH /profile/diet { dietId: number | null }` — régime du profil connecté ; - `null` efface le régime (étape "skippable" du parcours). `404 DIET_NOT_FOUND` si - `dietId` ne correspond à aucun régime de référence. -- `GET`/`PATCH /profile/allergies` — allergènes/intolérances du profil connecté, - sous forme de liste d'IDs (`number[]`). `PATCH { allergyIds }` **remplace** - l'ensemble (pas une fusion — le client renvoie toujours la sélection complète, - cohérent avec un composant de multi-sélection). `404 ALLERGY_NOT_FOUND` si un ID - ne correspond à aucun allergène de référence. + `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`), auparavant dupliqué dans `auth.service.ts` et -`require-auth.ts` — `profile.service.ts` le réutilise aussi. +(`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. -- `src/features/auth/RequireAuth.tsx` / `RedirectIfAuthenticated.tsx` — gardes de route - (react-router-dom) : `/` exige d'être connecté, `/login` et `/signup` redirigent vers - `/` si on l'est déjà. -- `src/pages/{Login,Signup,Home}Page.tsx` — validation client instantanée via les +- `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). -## Accueil, sidebar & sections (apps/web) +## Sidebar, planning, recettes & sections (apps/web) -Une fois connecté, l'utilisateur atterrit sur `src/layouts/AppLayout.tsx` — sidebar -(nav Planning/Recettes/Liste de courses/Foyer & profil + nom/déconnexion en pied) et -`` pour la route active — montée une seule fois comme route parente de tout -l'espace authentifié (`App.tsx`), pas dupliquée par page. `src/pages/HomePage.tsx` -(routée sur `/`) affiche le planning de la semaine du foyer (`GET /planning?date=`, -voir plus haut) avec ses états chargement/erreur/vide/rempli ; `Recettes` et `Liste de -courses` n'ont pas encore de backend dédié et rendent pour l'instant le même -composant `ComingSoonPage` — `Foyer & profil` (`src/pages/HouseholdPage.tsx`), lui, -est une vraie page (voir section suivante). Détail complet (pourquoi une seule route -parente, pourquoi un composant stub partagé) : -[specs/frontend-architecture.md](specs/frontend-architecture.md#applayout--sidebar-commune-à-lespace-connecté). +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`). -## Parcours profil — foyer, régime, allergènes (apps/web) +- **`/` — `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). -- `src/features/profile/` — `HouseNameField`, `DietSelect`, `AllergySelect` : champs - contrôlés et "dumb" (reçoivent leurs données en props, ne fetchent rien - eux-mêmes), partagés par les deux surfaces ci-dessous. `AllergySelect` utilise une - grille de cases à cocher dans un `
`/`` plutôt qu'un - `