Commit graph

9 commits

Author SHA1 Message Date
kyuno053
5d63ff9ea9
fix: corrige les bugs ouverts du repo (import TheMealDB, sidebar mobile) + doc (#59)
* fix(recipes): corrige plusieurs bugs d'import TheMealDB

- Les instructions TheMealDB numérotées sur leur propre ligne ("1\n\ntexte...\n\n2\n\ntexte...") créaient des étapes parasites ne contenant qu'un chiffre — filtrées désormais (#52).
- Un ingrédient compté sans mot d'unité dans le texte source (ex. "4 Egg Yolks") laissait l'import bloqué sur "Importer" indéfiniment, sans indication visuelle de la ligne en cause — matchUnit retombe maintenant sur l'unité générique "piece" quand une quantité a été extraite, et RecipeImportForm/RecipeFormPage surlignent désormais toute ligne dont l'unité manque, avec un message explicite (#53).
- Ajout de INGREDIENT_LABEL_SYNONYMS_EN pour reconnaître des formulations alternatives fréquentes chez les sources anglophones ("vanilla pod" en plus de "vanilla bean") sans élargir INGREDIENT_LABELS_EN à un tableau pour ses ~550 entrées (#54).
- Effet de bord découvert en vérifiant #53 de bout en bout : deux lignes source résolues vers le même ingrédient catalogue (ex. "Egg Yolks"/"Eggs" -> "Œuf") faisaient planter la création en 500 (contrainte unique recipe_id+ingredient_id) au lieu d'un 400 propre. createRecipeSchema rejette maintenant les ingredientId en double, et le formulaire d'import surligne les doublons avant même de soumettre.

Vérifié de bout en bout dans le navigateur (import réel de la recette "Flan" depuis TheMealDB, jusqu'au planning) en plus des tests ajoutés.

Closes #52, #53, #54

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(layout): la sidebar réduite écrasait la barre mobile

`isCollapsed` (rail icône seule sur desktop) persiste dans localStorage
indépendamment de la largeur de fenêtre — un utilisateur ayant réduit la
sidebar sur desktop puis ouvrant la même session sur mobile (ou réduisant
la fenêtre sous 640px) gardait `.app-sidebar.collapsed` (spécificité
0,2,0 : width 4.25rem, flex-direction column), qui l'emportait sur la
règle mobile `@media (max-width: 640px)` (spécificité 0,1,0) censée passer
la sidebar en barre horizontale pleine largeur.

Le bloc `&.collapsed` est maintenant scopé sous `@media (min-width: 641px)`
— le complément exact du breakpoint mobile — donc il ne s'applique plus du
tout en dessous.

Vérifié dans le navigateur : sidebar collapsed=true dans localStorage,
viewport 375px — la sidebar calcule bien width: 375px / flex-direction:
row (barre horizontale pleine largeur) au lieu de 4.25rem/column.

Closes #27

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(readme): documente GET /planning?date=, plus /planning/current

Le README documentait encore `GET /planning/current` (401 sans session,
couvre "aujourd'hui"), une route qui n'existe plus — `planning.routes.ts`
ne définit que `GET /planning?date=YYYY-MM-DD` depuis l'introduction de la
grille de semaine complète. Sans session, `/planning/current` renvoie un
404 générique (route inexistante), pas le 401 documenté.

Documente aussi POST/DELETE /planning/items au passage, absents jusqu'ici.

Closes #55

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: met à jour README et specs/ avec l'état réel du code

Le code avait beaucoup évolué depuis la dernière mise à jour de la
documentation (sources externes, import de recettes, planning en
grille, pages de paramètres, thème, tests Cucumber...) sans que
README.md/specs/*.md ne suivent. Tour complet du code (backend +
frontend) et réécriture :

- specs/batch-cooking-modele.md : schéma de données réécrit depuis
  schema.prisma (foyer/admin/invitation, sources, catalogue
  ingrédients/unités, techniques détectées, visibilité des recettes).
- specs/backend-architecture.md : foyer, préférences/goûts, planning,
  référence, sources externes (adaptateurs/registre/sync), matching
  ingrédients/techniques, isolation base de test, suppression de compte.
- specs/frontend-architecture.md : routing complet, sidebar/paramètres,
  thème, planning + picker, catalogue + import, composants UI partagés,
  tests Cypress+Cucumber.
- specs/batch-cooking-architecture.md : module Import passe de TODO à
  implémenté.
- specs/error-handling.md : liste complète des ~19 codes d'erreur.
- README.md : réécriture pour refléter tout ce qui précède, plus la
  note (dangereusement obsolète) sur le partage base de test/dev — le
  fix existe déjà (apps/api/.env.test), la doc décrivait encore le bug.

* feat(ingredients): ajoute jaune/blanc d'oeuf, coriandre en poudre, viandes hachées

Complète le catalogue d'ingrédients de référence (seed data) :

- jaune d'oeuf / blanc d'oeuf (dairyAndCheese/eggs, aux côtés d'"egg")
- coriandre en poudre (condimentsAndSpices/spices, aux côtés de
  corianderSeeds/freshCilantro déjà présents)
- viandes hachées manquantes : veau, porc, agneau (meatAndSeafood/meats,
  aux côtés de groundBeef déjà présent), dinde et poulet
  (meatAndSeafood/poultry)

Libellés ajoutés dans apps/web/src/locales/fr/translation.json (source
d'affichage) et packages/shared/src/data/catalog-labels-en.ts (matching
anglais pour l'import de recettes depuis des sources comme TheMealDB).
Aucune icône ni régime dédiés : héritent des défauts de leur groupe
(EGG/SPICE/MEAT/POULTRY, mêmes dietUids que leurs groupes respectifs).

282 tests apps/api toujours au vert (resetDatabase() reseed le
catalogue à chaque test).

* fix(i18n): retire le œ ligaturé des libellés français de l'œuf

"Œuf"/"Œufs" (ingrédient, sous-catégorie, allergène) et "Jaune/Blanc
d'œuf" (ajoutés par #60) s'écrivaient avec le œ ligaturé — remplacé par
"oe" (deux lettres) partout où le mot apparaît. Ne touche pas "bœuf"
(mot différent, non concerné).

Le scénario Cucumber recipe-form.feature qui sélectionne l'ingrédient
par son libellé affiché est mis à jour en conséquence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(recipes): concatène les ingrédients dupliqués à l'import

Suite au retour utilisateur sur #53 (follow-up) : au lieu de bloquer
l'import et de demander à l'utilisateur de retirer une ligne en double
à la main, deux lignes source qui résolvent vers le même ingrédient
catalogue sont désormais fusionnées automatiquement, quantité
concaténée (sommée), avant même que l'écran de revue ne s'affiche.

- mergeDuplicateIngredients (recipe-translation.ts) : même unité des
  deux côtés -> somme directe. Unité différente mais même UnitType
  (MASS/VOLUME) -> conversion via toBaseFactor avant de sommer, exprimée
  dans l'unité de la première ligne. UnitType différent, ou COUNT des
  deux côtés (une "pincée" n'est pas une fraction fixe d'une "gousse",
  cf. le commentaire de UnitView) -> jamais fusionnées, laissées en
  double (createRecipeSchema/RecipeImportForm continuent de les
  signaler, filet de sécurité déjà en place). Les lignes non résolues
  (ingredientId: null) ne sont jamais fusionnées entre elles.
- rawText concaténé ("100g Sugar + 45g Sugar") pour la traçabilité.
- Branché dans previewSourceItem (sources.service.ts), juste après
  translateRecipeIngredients — c'est le seul endroit où des doublons
  peuvent apparaître (la création manuelle ne peut pas en produire,
  IngredientPicker exclut déjà les ingrédients déjà sélectionnés).

Vérifié via l'API en local (import réel de "Flan" depuis TheMealDB) :
"100g Sugar"/"45g Sugar" -> une seule ligne Sucre, 145g.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(lint): upgrade Biome vers 2.x, active noExplicitAny/noConsole/noFloatingPromises

`@biomejs/biome` passe de 1.9.4 à 2.5.9 (config migrée via `biome migrate
--write`) — nécessaire pour noFloatingPromises, une règle type-aware
apparue en 2.0 (nursery).

- noExplicitAny : déjà "recommended", actif depuis toujours, aucun changement.
- noConsole (biome.json) : bloque tout `console.*` sauf error/warn/info/
  debug/table/assert — équivalent à "pas de console.log" sans interdire
  les niveaux nommés (voir le nouveau log service dans le prochain commit,
  qui centralise justement ces appels).
- noFloatingPromises (nursery) activé explicitement sous `rules.nursery`
  sans avoir besoin d'activer le domaine "types" au sens large (ça aurait
  aussi allumé des dizaines d'autres règles type-aware type
  noUnresolvedImports/noUnnecessaryConditions, hors scope ici).

Le reste du diff, c'est soit du reformatage automatique (import sort, 2.x
ordonne différemment de 1.9.4 — `biome check --write --unsafe`), soit les
corrections des ~20 promesses flottantes que la nouvelle règle a fait
remonter :

- La plupart sont des `navigate(...)` non attendus (react-router v7 type
  `navigate` en `void | Promise<void>`) — préfixés `void navigate(...)`,
  aucun changement de comportement.
- Trois chargements initiaux en useEffect (OnboardingAllergensPage,
  OnboardingDietPage, OnboardingHouseholdPage, HouseholdSettingsPage)
  n'avaient jamais de `.catch()` du tout — ajouté (dégradation silencieuse
  vers un état vide/par défaut, même raisonnement que le `.catch()` déjà
  présent dans OnboardingSourcesPage).
- HouseholdSettingsPage : `loadHouse` était une fonction déclarée à chaque
  render (donc une référence différente à chaque fois) utilisée comme
  dépendance de useEffect ET passée en callback à des enfants — le
  useEffect se re-déclenchait donc à chaque re-render provoqué par son
  propre fetch, un vrai bug de boucle infinie de requêtes que
  noFloatingPromises a fait remonter indirectement (via
  useExhaustiveDependencies). Corrigé avec useCallback([]).
- RecipeDetailPanel : une clé de liste `${index}-...}` sur une liste
  statique (draft.steps, sans id stable — DraftRecipeStepView n'en a pas)
  — biome-ignore justifié, pas de bug réel.
- recipe.test.ts : variable `agent` non utilisée, retirée.

Vérifié : `pnpm --filter api test` (295/295), `pnpm lint` et `pnpm build`
clean sur tout le repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(api): ajoute un log service pour les logs de fonctionnement côté serveur

Jusqu'ici, rien ne journalisait quoi que ce soit côté serveur : aucune
trace au démarrage à part un console.log ad hoc, et surtout aucune trace
des requêtes ni des erreurs gérées par ErrorHandlerService — un 500 en
production n'aurait laissé aucune trace exploitable.

- LoggerService (apps/api/src/lib/logger.service.ts) — classe (public
  debug/info/warn/error, private emit), même convention que
  ErrorHandlerService (packages/error-tools) : instance unique partagée
  exportée (`export const logger = new LoggerService()`). Émet une ligne
  JSON structurée par appel (timestamp/level/message + meta), filtrée par
  seuil selon NODE_ENV (debug complet en dev, warn+ pendant les tests
  pour ne pas alourdir la sortie de Mocha, info+ en production). Seul
  endroit du code autorisé à toucher `console` directement (biome-ignore
  justifié), toujours via une méthode nommée — jamais un console.log nu.
- requestLogger (middlewares/request-logger.ts) — une ligne par requête
  terminée (méthode/chemin/statut/durée), montée en tout premier dans
  app.ts, avant même setupCore (CORS/JSON/cookies), pour englober tout le
  pipeline. Niveau déduit du statut (info/warn/error).
- errorLogger (middlewares/error-logger.ts) — monté juste avant
  createErrorMiddleware : réutilise errorHandlerService.handle() (pur/
  sans effet de bord) pour classifier l'erreur avant que la vraie réponse
  ne soit construite, log en warn les 4xx routiniers (validation, 404,
  401...) et en error les 5xx/exceptions non prévues (avec la stack).
- error-handler.service.ts : retire le `console.error(error)` ad hoc de
  fromUnknownError — errorLogger voit désormais chaque erreur avant que
  ce service ne la mappe, donc ce console.error faisait doublon (et
  loggait en texte brut, pas en JSON structuré).
- server.ts : le console.log de démarrage passe par logger.info.

Vérifié : pnpm --filter api test (303/303, dont 8 nouveaux tests sur
LoggerService), pnpm lint/build clean, testé en live (pnpm dev:api +
curl) — logs JSON corrects pour un 200, un 404, un 401.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* style: préfixe tous les membres private/protected par _

Convention demandée par l'utilisateur : `emit` -> `_emit`, sur toutes les
classes du repo, pas seulement le nouveau code. `public` reste sans
préfixe.

- LoggerService (apps/api) : _minSeverity, _emit.
- ApiClient (apps/web) : _request (39 sites d'appel mis à jour).
- ErrorHandlerService (packages/error-tools) : _fromZodError,
  _fromHttpError, _fromUnknownError.
- ExpressServer (packages/express-tools) : _app, _registeredRoutes.

Aucun changement de comportement — pur renommage interne, aucune méthode
private/protected n'était appelée depuis l'extérieur de sa classe.

Vérifié : pnpm --filter api test (303/303), pnpm lint/build clean sur
tout le repo (apps/api, apps/web, packages/*).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(specs): documente les conventions de développement du repo

Nouveau specs/dev-conventions.md — jusqu'ici ces règles n'existaient que
dans l'historique de commits/PR (classes vs objets littéraux pour la
logique de service, préfixe _ sur private/protected, règles Biome
actives, log service, tests sans mocks de la DB, conventions git/PR...),
rien de centralisé pour un futur contributeur (humain ou Claude Code).

Référencé depuis README.md, section "Qualité / Tests".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(web): regroupe pages/ par section au lieu d'un dossier à plat

pages/ mélangeait 8 fichiers directement à sa racine (LoginPage,
SignupPage, PlanningPage+scss, RecipesPage, RecipeFormPage,
ImportRecipePage, ShoppingListPage, ComingSoonPage+scss) à côté de deux
sous-dossiers déjà groupés (onboarding/, settings/) — incohérent, et
difficile à parcourir une fois le nombre de pages monté. Un sous-dossier
par section routée, même règle que onboarding/settings existants :

- pages/auth/        — LoginPage, SignupPage
- pages/planning/     — PlanningPage + planning-page.scss
- pages/recipes/      — RecipesPage, RecipeFormPage, ImportRecipePage
- pages/shopping-list/ — ShoppingListPage

ComingSoonPage (+ .scss) déménage vers components/ui/ — ce n'est pas une
page routée elle-même (ShoppingListPage l'enveloppe), c'est un composant
UI générique réutilisable, sa place est aux côtés de Dialog/Tooltip/etc.,
pas dans pages/.

Chemins relatifs internes de chaque fichier déplacé mis à jour (un niveau
de profondeur en plus), imports dans App.tsx repointés, tri Biome
réappliqué. specs/frontend-architecture.md mis à jour (arborescence +
références de chemin).

Vérifié : pnpm build clean (apps/web, 1952 modules), pnpm lint clean sur
tout le repo, testé en live dans le navigateur (login/signup, planning,
recettes, nouvelle recette, liste de courses, paramètres) — aucune route
cassée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(web): regroupe features/recipes/ par sous-domaine au lieu d'un dossier à plat

20 fichiers à plat -> badges/ (DietTagSelect, DietBadges, AllergenBadges,
ReproducibleBadge, FavoriteStarButton), ingredients/ (IngredientPicker,
IngredientRow, ingredient-icons), steps/ (StepListEditor, StepDescription,
highlight-tech-steps), sources/ (RecipeSourcesPanel, SourceItemTable,
RecipeImportForm, recipe-import-draft, useEnabledSources).

RecipeTable/RecipeTabs/RecipeDetailPanel et recipes.scss restent à la
racine (composants transverses aux sous-dossiers, partagés par plusieurs
d'entre eux). Chemins relatifs corrigés dans les fichiers déplacés et chez
tous leurs importeurs externes (pages/recipes/*, features/planning/
RecipePickerDialog.tsx, features/profile/DislikedIngredientsField.tsx),
doc mise à jour (specs/frontend-architecture.md, specs/batch-cooking-
modele.md).

Vérifié : tsc --noEmit, biome check, build complet, 303 tests API,
vérification live navigateur (planning, /recettes, /recettes/nouvelle).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(api): regroupe lib/ par sous-domaine au lieu d'un dossier à plat

9 fichiers à plat -> recipe-sources/ (recipe-source-adapter, recipe-source-
errors, recipe-source-registry) et recipe-matching/ (recipe-translation,
ingredient-matcher, tech-step-matcher). jwt.ts, safe-profile.ts et
logger.service.ts restent à la racine de lib/ (pas de sous-domaine
partagé avec les autres).

Chemins relatifs corrigés dans les fichiers déplacés (profondeur +1 vers
db/) et chez tous leurs importeurs (modules/sources, modules/recipe,
sources/*, db/recipe-source-sync.ts, 12 fichiers de test), doc mise à
jour (specs/backend-architecture.md, specs/batch-cooking-architecture.md).

Vérifié : tsc --noEmit, biome check, build complet, 303 tests API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(api): regroupe test/ par sous-domaine, miroir de src/lib/

18 fichiers à plat -> recipe-matching/ (ingredient-matcher, recipe-
translation, tech-step-matcher — miroir de lib/recipe-matching/),
recipe-sources/ (json-ld-recipe, recipe-source, recipe-source-sync,
the-meal-db — miroir de lib/recipe-sources/), sources/ (sources,
sources-index — module + registration src/sources/index.ts).

Les tests par domaine API sans regroupement naturel (auth, health,
house, logger.service, planning, preferences, profile, recipe,
reference) restent à la racine de test/, un fichier par domaine — même
logique que jwt.ts/safe-profile.ts restés à la racine de lib/.

Chemins relatifs corrigés (../src/ -> ../../src/, ../test-support/ ->
../../test-support/ dans les fichiers déplacés qui appellent
resetDatabase). .mocharc.json ("test/**/*.test.ts") couvre déjà les
sous-dossiers, aucun changement de config nécessaire.

Vérifié : biome check, 303 tests API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(convention): impose try/catch autour de chaque await/corps async

Nouvelle règle de dev : aucun await nu, et un corps de fonction/méthode
async doit intégralement vivre dans un try/catch (pas seulement la ou
les lignes qui awaitent). Documentée dans specs/dev-conventions.md avec
son périmètre (code applicatif — services/hooks/composants/middlewares
— routes *.routes.ts exemptées car déjà couvertes par
wrapAsyncHandler ; tests et scripts one-off exemptés aussi).

Appliqué rétroactivement à tout le code applicatif qui ne l'était pas
déjà :
- api : auth/house/profile/preferences/planning/reference/recipe/
  sources .service.ts, recipe-source-sync.ts, recipe-translation.ts,
  ingredient-matcher.ts, tech-step-matcher.ts, json-ld-recipe.ts,
  the-meal-db.ts — un try/catch par fonction async, rethrow simple
  (le middleware d'erreur logge déjà tout centralement, voir
  error-logger.ts) sauf quand un catch avait déjà une logique propre
  (ex. le retry de createHouse).
- web : api/client.ts (_request), AuthContext.tsx, ThemeContext.tsx,
  AppLayout.tsx (handleLogout), HouseholdSettingsPage.tsx (handleCopy/
  handleRemove/handleDelete/handleLeave) — la plupart des handlers de
  formulaire avaient déjà ce pattern, seuls ceux qui laissaient un
  await nu ont été corrigés.

lint/complexity/noUselessCatch désactivé dans biome.json (interdisait
justement le catch-qui-rethrow que cette convention impose).

Vérifié : tsc --noEmit (api+web), biome check (0 erreur, repo entier),
build complet, 303 tests API, vérification live navigateur (thème,
déconnexion, copie du code d'invitation).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(web): corrige l'import cassé de highlight-tech-steps.cy.tsx

Oubli lors du regroupement de features/recipes/ par sous-domaine
(refactor(web): regroupe features/recipes/...) : le déplacement de
highlight-tech-steps.ts vers features/recipes/steps/ n'avait pas été
répercuté dans ce test composant Cypress (hors de apps/web/src, donc
raté par la recherche de référence externe à l'époque) — faisait
planter le job e2e en CI ("Failed to fetch dynamically imported
module").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 12:16:22 +02:00
kyuno053
c34eaa89d0
chore(web): session de polish global — version, checkbox, danger zone, icônes (#20)
* chore(web): session de polish global — version, checkbox, danger zone, icônes

- Affiche le numéro de version (package.json, injecté via Vite) en bas de
  la sidebar, masqué en mode collapse et en mobile.
- Factorise les checkbox/radio dupliqués (AllergySelect, DietTagSelect,
  IngredientPicker, UserPreferencesPage) en composants partagés
  CheckboxOption/RadioOption (components/ui/), et inverse le layout pour
  que la case soit à gauche du label.
- Teinte la "zone de danger" de suppression de compte en rouge (fond +
  bordure), pas seulement le bouton.
- Migre les icônes de navigation générale vers lucide-react (nav-icons.tsx
  devient un fichier de ré-export) ; les pictogrammes d'ingrédients métier
  restent en SVG custom (pas d'équivalents fins côté lucide).

Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle manuelle (sidebar desktop/collapsed/mobile, light/dark).

* feat(web): icônes d'ingrédients depuis foodiconpack.com + page de crédits

- Remplace 19 des 22 pictogrammes génériques d'ingrédients par des icônes
  curées du pack gratuit "Common ingredient icons"/"Common Utensils" de
  foodiconpack.com (CC BY 4.0) : carotte, pomme, basilic, bœuf, poulet,
  saumon, crevette, riz, pois chiches, amandes, lait, cheddar, œufs,
  cannelle, miel, huile d'olive, bière, marmite, sucre.
- BREAD/DOUGH/SPROUT restent en SVG custom : pas d'équivalent net dans le
  pack (packs "ingrédients"/"ustensiles"/"plats"/"boissons" vérifiés).
  Architecture inchangée : `icon` reste un enum de 22 valeurs partagées en
  base (pas de migration, pas de mapping par ingrédient — cf. le
  commentaire du fichier sur l'historique emoji→enum générique).
- Nouveau wrapper FilledIcon (fill="currentColor", viewBox 2048) à côté du
  wrapper Icon existant (stroke) — les deux stylent au même endroit via
  CSS, donc le mélange des 19+3 icônes reste visuellement homogène.
- Ajoute /parametres/credits (CreditsPage) créditant foodiconpack.com et
  liant la licence CC BY 4.0, requis par la licence des icônes utilisées ;
  nouvelle entrée de nav "Crédits" (icône lucide Info).

Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle (grille des 22 icônes dans le picker, page crédits).

* feat(web,api): zone dangereuse rouge, préférences élargies, onglet favoris par défaut, e2e recettes, catalogue en uid+i18n

- Zone dangereuse (compte) : le bouton "Supprimer mon compte" est rouge.
- Pages préférences/paramétrage : contenu centré et élargi (32rem -> 56rem)
  au lieu de coller à gauche sur un écran large.
- Page recettes : l'onglet "Favoris" est sélectionné par défaut.
- Ajout de apps/web/cypress/e2e/recipes.cy.ts (onglets, recherche, sélection
  master-detail, favori, suppression, lien nouvelle recette).
- Catalogue de référence (ingrédients/régimes/allergènes) : la colonne
  `name` (le libellé français, utilisé comme clé unique) devient `key`, un
  slug stable et opaque au sens produit (ex. "vegetarien", "boeuf_hache").
  Le libellé lui-même déménage entièrement côté client, dans
  apps/web/src/locales/fr/translation.json sous le namespace `catalog.*`,
  résolu via `t(\`catalog.ingredients.${key}\`)` etc. — même schéma que
  IngredientCategory/IngredientSubcategory. Migration Prisma
  (rename + backfill des ~456 lignes déjà seedées), seed/service/tests API
  et composants web mis à jour en conséquence.
  - apps/api/src/utils/slugify.ts + scripts/generate-catalog-i18n.ts
    (regénère le fichier de traduction depuis reference-seed-data.ts).
  - 102 tests Mocha + 32 scénarios Cucumber passent contre la base migrée.

Note : cypress run plante dans cet environnement (le processus GPU
Chromium/Electron crash même headless, indépendamment des flags) — les
recipes.cy.ts n'ont pas pu être exécutés ici ; vérifiés par lecture du code
source des composants visés et par un passage manuel dans le navigateur de
prévisualisation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(api): les uids du catalogue sont en anglais, pas des slugs français

reference-seed-data.ts reste rédigé en français (c'est juste le libellé
d'autoring, jamais stocké/exposé), mais la clé stable (`Diet.key`/
`Category.key`/`Ingredient.key`) qu'on en dérive doit elle-même être un
identifiant anglais, indépendant de la langue d'autoring — pas juste le
même texte français passé à slugify().

- apps/api/src/db/catalog-en-keys.ts : dictionnaire écrit à la main
  (label français -> clé anglaise) pour les 5 régimes, 14 allergènes et
  437 ingrédients ; getEnglishKey() lève une erreur explicite si un
  nouvel élément n'a pas encore d'entrée plutôt que de retomber sur un
  slug français silencieux.
- scripts/validate-catalog-en-keys.ts : vérifie que chaque diet/allergène/
  ingrédient de reference-seed-data.ts a une entrée, et que les clés
  anglaises résultantes sont uniques (437/437, 14/14, 5/5 — zéro manquant,
  zéro collision).
- reference-seed-data.ts et scripts/generate-catalog-i18n.ts utilisent
  désormais getEnglishKey() au lieu de slugify(nom français).
- Nouvelle migration (20260818193000_catalog_keys_to_english) qui
  remappe les lignes déjà seedées avec un slug français (par la migration
  précédente) vers leur clé anglaise définitive.
- apps/web/src/locales/fr/translation.json régénéré : catalog.* est
  maintenant indexé par clé anglaise ("vegetarian", "eggs",
  "ground_beef"...), toujours avec le libellé français en valeur.
- Tests/step-definitions mis à jour (getEnglishKey() au lieu de
  slugify()) ; 102 tests Mocha + 32 scénarios Cucumber passent contre la
  base migrée. Vérifié aussi en direct via GET /reference/diets et
  /reference/allergies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(web): crypto.randomUUID plante hors contexte sécurisé, empêchant d'associer un ingrédient

Écran noir + "TypeError: crypto.randomUUID is not a function" au clic sur
une carte d'ingrédient dans le formulaire de recette. crypto.randomUUID()
n'est défini que dans un "contexte sécurisé" (https, ou littéralement le
host "localhost") — il est absent sur une IP locale (test sur un vrai
appareil), dans une WebView Capacitor (l'enrobage mobile prévu pour cette
app), ou en http sur un vrai domaine. RecipeFormPage/StepListEditor s'en
servaient pour générer l'identité React (`key`) de chaque ligne
d'ingrédient/étape en brouillon.

- apps/web/src/lib/client-key.ts : remplace par un générateur qui ne
  touche jamais `crypto` — un compteur + Math.random suffit, cette valeur
  n'a besoin d'être unique que le temps de la session de rendu, jamais
  envoyée au serveur.
- apps/web/cypress/e2e/recipe-form.cy.ts : couvre l'association d'un
  ingrédient (recherche, sélection, exclusion du picker une fois
  sélectionné, retrait), la création et l'édition d'une recette, et un
  test de non-régression dédié qui supprime crypto.randomUUID avant le
  chargement de la page (comme le ferait un vrai contexte non sécurisé)
  pour vérifier que l'ajout de plusieurs ingrédients/étapes ne plante
  plus.

Vérifié en direct dans le navigateur de prévisualisation en supprimant
crypto.randomUUID à la main (reproduit le crash), puis en confirmant que
l'ajout d'ingrédient fonctionne à nouveau après le correctif. cypress run
ne peut toujours pas s'exécuter dans cet environnement (voir le commit
précédent) — non exécutés avec Cypress lui-même, mais vérifiés par
lecture des sélecteurs réels et rejoués à la main dans le navigateur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ci): corrige les specs Cypress cassées par le refactor uid+i18n, applique biome

- onboarding.cy.ts / preferences.cy.ts / recipes.cy.ts mockaient encore
  GET /reference/diets|allergies avec l'ancienne forme {id, name}. Depuis
  les deux derniers commits l'API renvoie {id, key} (uid anglais) et le
  composant résout le libellé via i18n (t(`catalog.diets.${key}`)) — avec
  key manquant, ça affichait littéralement "catalog.diets.undefined" au
  lieu de "Végétarien"/"Omnivore"/etc., faisant échouer cy.select()/
  cy.contains() dans ces 3 specs. Corrigé pour mocker {key: "vegetarian"},
  {key: "peanuts"}, etc.
- recipes.cy.ts : le test "shows a not-found message" utilisait le
  mauvais code d'erreur (4041 au lieu de ErrorCode.RECIPE_NOT_FOUND =
  4045), donc RecipeDetailPanel tombait dans son état d'erreur générique
  au lieu du message "Cette recette n'existe pas." — bug dans mon propre
  test, sans rapport avec le refactor.
- pnpm lint (biome) : les fichiers touchés par le refactor précédent
  avaient quelques soucis de formatage/tri d'imports (des sed multi-
  fichiers, pas d'édition via l'outil habituel) — corrigés par
  `biome check --write`.

Vérifié : ces 3 specs + recipe-form.cy.ts passent maintenant dans le job
CI GitHub Actions (Linux, Cypress s'y exécute réellement — contrairement
à cet environnement Windows sandboxé, voir les commits précédents) ; 102
tests Mocha + 32 scénarios Cucumber toujours au vert en local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:52:12 +02:00
78e0f89503 feat(recipes): remplace les emoji par des icônes SVG épurées
Deux usages d'emoji supprimés, remplacés par un vrai jeu d'icônes SVG
(style ligne, cohérent avec layouts/nav-icons.tsx) :

- Catégories/sous-catégories du picker (auparavant préfixées d'un
  emoji dans les libellés) : nouvelles icônes CategoryIcon/
  SubcategoryIcon, une par catégorie/sous-catégorie.
- Icône par ingrédient (auparavant un emoji différent pour chacun des
  437 ingrédients) : remplacé par un petit vocabulaire de ~22
  pictogrammes génériques ("un légume", "une bouteille", "un
  fromage"...) réutilisés selon la nature de l'ingrédient plutôt
  qu'un dessin par ingrédient (irréaliste à la main pour 437 items).

- Schéma : nouvel enum IngredientIcon (22 valeurs), colonne
  Ingredient.icon passe de String? (texte libre) à IngredientIcon
  (non nullable, toujours une valeur générique pertinente désormais).
- reference-seed-data.ts : chaque groupe porte un defaultIcon (calqué
  sur sa sous-catégorie), avec override par ingrédient pour les
  exceptions (ex. les fromages dans "produits laitiers", les jus/cafés
  dans "assaisonnements").
- apps/web/src/features/recipes/ingredient-icons.tsx (nouveau) :
  22 icônes SVG + CategoryIcon/SubcategoryIcon (réutilisent le même
  vocabulaire pour représenter chaque catégorie/sous-catégorie).
- packages/shared : IngredientView.icon devient IngredientIcon (union
  de 22 valeurs) au lieu de string | null.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 12:30:25 +02:00
ea86a4a7f1 feat(recipes): catégories d'ingrédients + nouveau sélecteur
- Prisma: enum IngredientCategory (18 valeurs) + Ingredient.category,
  migration appliquée
- reference-seed-data.ts restructuré en 18 groupes de catégories
- packages/shared: INGREDIENT_CATEGORIES + IngredientView.category
- API: category exposé par /reference/ingredients et /recipes
- Web: nouveau IngredientPicker (chips catégories + recherche + grille
  de cartes) remplaçant IngredientAutocomplete, branché dans le
  formulaire de recette et le champ aliments-pas-aimés
- i18n: clés recipes.form.category.* et libellés du picker
- Fix test reference.test.ts pour la nouvelle forme d'IngredientView

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 10:24:12 +02:00
acab18ac4a feat(recipes): catalogue v2 - visibilité, favoris, régimes et catalogue d'ingrédients exhaustif
Recipe catalog v2:
- Recipe gagne visibility (PERSONAL/HOUSE/PUBLIC), authorId, authorHouseId
- Favoris par utilisateur (RecipeFavorite), régimes associés (RecipeDiet)
- Aliments "pas aimés" par utilisateur (UserProfileDislikedIngredient),
  distinct des allergies médicales
- API: GET /recipes?tab=favoris|perso|foyer|publique avec contrôle d'accès
  complet, POST/DELETE /recipes/:id/favorite, édition/suppression réservées
  à l'auteur (403 NOT_RECIPE_AUTHOR), GET/PATCH /profile/disliked-ingredients
- Frontend: vue maître-détail (onglets + tableau + panneau détail),
  formulaire enrichi (visibilité, régimes), section préférences pour les
  aliments pas aimés

Catalogue d'ingrédients de référence:
- Extension du seed de 39 à ~430 ingrédients (viandes, poissons/fruits de
  mer, légumes, fruits, féculents, condiments/sauces, épices/herbes, pains
  à sandwich, cuisines italienne/asiatique/mexicaine/maghrébine, liquides
  et boissons de cuisine, bouillons/fonds)
- Chaque ingrédient lié à ses allergènes UE (IngredientAllergy) — les 14
  allergènes réglementaires restent tous couverts
- Seeding optimisé en requêtes groupées (createMany/diff ciblé) plutôt
  qu'un upsert par ligne, pour garder resetDatabase() rapide en test

Tests: 102 tests Mocha + 32 scénarios BDD, tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 09:29:11 +02:00
9dd90359c9 Tests: dates fixes pour Planning + refonte des radio/checkbox
- apps/api: les tests Planning (mocha et cucumber) lisaient l'horloge
  systeme (new Date()/DateTime.utc()) pour construire leurs fixtures et
  interroger /planning, ce qui les rendait non deterministes. Ajoute
  test-support/reference-date.ts (TEST_REFERENCE_DATE, une date UTC
  fixe) et l'utilise dans planning.test.ts / planning.steps.ts a la
  place du systeme.

- apps/web: nouveau style global pour tous les radio/checkbox de
  l'app (theme-select, allergy-select, onboarding) - "carte
  selectionnable" : le controle natif reste reel/accessible mais
  visuellement cache, toute la ligne devient la surface interactive
  (bordure + fond teinte + coche au survol/selection). Corrige au
  passage le bug de fond qui causait le desalignement des radios sur
  /parametres/preferences-utilisateur (la regle generique
  input, select { width: 100% } de profile-forms.scss s'appliquait
  aussi aux checkbox/radio) et une regression de font-weight ou les
  lignes non selectionnees du theme apparaissaient en gras comme si
  elles l'etaient.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 22:36:02 +02:00
7abe030bb9 Web: pages Compte/Préférences/Foyer, client API et déplacement des menus (step 6/8)
- pages/settings/AccountSettingsPage: identité + suppression de compte
  (confirmation en deux temps, mot de passe requis)
- pages/settings/PreferencesPage: régime + allergies/intolérances,
  sorti de HouseholdPage (attribut du profil, pas du foyer)
- pages/settings/HouseholdSettingsPage: sans foyer → créer/rejoindre ;
  avec foyer → renommer (hot-save), code d'invitation, membres,
  retirer un membre / supprimer le foyer (admin) ou le quitter
- HouseholdPage.tsx/.scss supprimés (contenu réparti ci-dessus)
- apiClient: createHouse/joinHouse/leaveHouse/deleteHouse/
  removeHouseMember/deleteAccount
- AuthContext: deleteAccount()
- i18n: namespaces account/preferences réorganisés, household réduit
  au foyer, common.saving/saved factorisées
2026-08-17 10:45:58 +02:00
8f25e53f11 Web: allergies/intolérances séparées + hot saving sur /foyer (step 8/8)
Retour fonctionnel : allergies et intolérances doivent être distinguées
dans l'UI, et /foyer doit sauvegarder à la volée plutôt que via des
boutons "Enregistrer".

- AllergySelect prend un `legend` en prop au lieu d'un libellé interne
  fixe — le même composant est rendu deux fois par chaque page
  consommatrice (HouseholdPage, OnboardingAllergensPage), une fois par
  `kind` (ALLERGY / INTOLERANCE), la sélection restant une seule liste
  d'IDs partagée.
- HouseholdPage : suppression des boutons "Enregistrer", autosave
  déclenché depuis le handler onChange de chaque champ (jamais un
  useEffect générique sur la valeur — se déclencherait aussi au
  chargement initial, sans distinction propre "chargé" vs "modifié").
  Nom du foyer et allergènes/intolérances debouncés (600ms/500ms),
  régime sauvegardé immédiatement (sélection discrète). Validation
  client (nom vide) empêche l'autosave plutôt que de déclencher un
  aller-retour API voué à l'échec.
- i18n : household.form.allergiesLabel devient "Allergies" (au lieu de
  "Allergies & intolérances"), nouvelle clé intolerancesLabel, save/
  saved remplacés par saving/saved (plus de bouton à libeller).
- Cypress (household.cy.ts réécrit, onboarding.cy.ts mis à jour) +
  specs/frontend-architecture.md + README.md.

Vérifié dans le navigateur : wizard d'inscription affiche bien les
deux groupes (12 allergies / 2 intolérances) ; /foyer sans aucun
bouton, chaque section sauvegarde automatiquement (vérifié en base
après édition du nom du foyer et du régime) ; compte de test nettoyé.

Clôt le retour fonctionnel sur la feature profil/foyer/régime/
allergènes (8 commits au total sur cette PR).
2026-08-17 00:09:09 +02:00
e512c33ffc Web: composants partagés foyer/régime/allergènes (step 3/6)
- ApiClient: getDiets/getAllergies (référence), getCurrentHouse/
  renameHouse, updateDiet, getAllergyIds/updateAllergyIds.
- features/profile/: HouseNameField, DietSelect (toujours une option
  "aucun régime" -> null, étape skippable), AllergySelect (checkboxes
  en grille + fieldset/legend, pas un <select multiple> — plus
  tapable/accessible, notamment sur mobile). Tous "dumb"/contrôlés :
  reçoivent leurs données (diets/allergies) en props plutôt que de les
  fetcher eux-mêmes — le fetch/état de chargement reste à la page
  appelante.
- profile-forms.scss partagé par les trois (même split que
  features/auth/auth-form.scss vs Login/SignupPage : styles de champs
  ici, layout de page dans chaque page consommatrice).

Pas encore utilisés (aucune page ne les importe) — le wizard
d'inscription (étape suivante) et la page /foyer les cablent.
2026-08-16 23:22:19 +02:00