diff --git a/README.md b/README.md
index 73e604c..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,171 +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/current` — nécessite le cookie de session (401 sinon). Renvoie le
- planning du foyer de l'utilisateur connecté qui couvre la date du jour (`Planning`
- dont `start_date <= aujourd'hui <= finish_date`), items inclus avec leur recette
- résolue en `{ id, name }` — ou `null` s'il n'y en a aucun (foyer sans planning en
- cours, ou profil sans foyer). `null` est une réponse **valide** (200), pas une
- erreur : aujourd'hui rien ne permet encore de créer un planning (le module « Calcul
- batch-cooking », voir [specs/batch-cooking-architecture.md](specs/batch-cooking-architecture.md),
- reste à construire), donc c'est l'état attendu tant que ce module n'existe pas.
-- Type de réponse partagé : `PlanningView` (`packages/shared/src/types/planning.ts`),
- consommé tel quel par `apps/web`.
+- `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.
-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/current`,
-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 `