Commit graph

59 commits

Author SHA1 Message Date
kyuno053
fc886a79cb
experiment: Cucumber/Gherkin + Cypress (#24)
* chore: point de départ pour l'expérimentation Cucumber/Gherkin + Cypress

Repart de zéro (pas de reprise du travail précédent sur
feat/cypress-cucumber, fermée/supprimée) — voir la discussion sur la
PR pour le contexte : bug amont dans
@badeball/cypress-cucumber-preprocessor@26.0.0 (require() synchrone de
dépendances @cucumber/* désormais ESM pur, plusieurs incompatibilités
de schéma trouvées en épinglant d'anciennes versions).

* feat(web): installe cypress-cucumber-preprocessor@22.2.0, versions figées

Étape 1 de l'expérimentation. Versions exactes (pas de ^), comme
demandé, pour éviter que le prochain `pnpm install` fasse dériver la
résolution vers des patchs plus récents :

- @badeball/cypress-cucumber-preprocessor@22.2.0
- @bahmutov/cypress-esbuild-preprocessor@2.2.8
- cypress@13.17.0
- esbuild@0.21.5

Contrairement à v26.0.0 (utilisé dans la tentative précédente,
fermée), cette version charge sans ERR_REQUIRE_ESM — aucun
pnpm.overrides nécessaire cette fois. Vérifié :
- `import { addCucumberPreprocessorPlugin }` : OK
- `addCucumberPreprocessorPlugin(on, config)` avec un contexte Cypress
  minimal : résout sans erreur, enregistre tous ses event handlers
  (before:run, after:run, before:spec, after:spec, after:screenshot,
  task)

À vérifier ensuite : cypress.config.ts + un vrai fichier .feature.

* test(web): feature Gherkin jetable pour valider le pipeline preprocessor

Étape 2 de l'expérimentation — un seul scénario minimal (remplir
email/password sur l'écran de connexion) pour vérifier que
addCucumberPreprocessorPlugin@22.2.0 + l'esbuild plugin fonctionnent
de bout en bout, pas juste au chargement. Steps volontairement
autonomes dans login-smoke.steps.ts (pas de fichier partagé) — tout
ce fichier est prévu pour être supprimé une fois validé.

cypress.config.ts : specPattern couvre maintenant *.cy.ts ET *.feature
en parallèle (le reste de la suite reste en .cy.ts classique pour
l'instant).

Testé en local jusqu'au mur GPU/Electron habituel de cet
environnement (chargement de la config + bundling esbuild passent,
pas d'ERR_REQUIRE_ESM) — la vraie exécution du scénario reste à
vérifier via la CI.

* fix(web): renomme login-smoke.steps.ts en login-smoke.ts

Le vrai (et seul) problème du run précédent : "Step implementation
missing for 'I am not signed in'" — pas un souci ESM/CJS cette fois,
juste une convention de nommage. Le pattern stepDefinitions par défaut
du preprocessor cherche, pour cypress/e2e/login-smoke.feature :

  - cypress/e2e/login-smoke/**/*.{js,mjs,ts,tsx}
  - cypress/e2e/login-smoke.{js,mjs,ts,tsx}   <- même basename, SANS ".steps"
  - cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}

`login-smoke.steps.ts` ne correspond à aucun des trois. Confirmé par
le message d'erreur lui-même (Cypress liste les 3 patterns essayés).
2026-08-19 13:04:21 +02:00
ffb1e06130 chore(api): retire l'intégration Cucumber/Gherkin
Retire la suite BDD Gherkin (apps/api/features/) — pas cassée, mais
mise de côté pour l'instant en même temps que l'intégration côté
Cypress (voir PR #23, fermée). À reprendre plus tard, probablement
avec une approche différente des deux côtés.

Vérifié avant retrait que la suite Mocha (apps/api/test/) couvre déjà
les mêmes scénarios, domaine par domaine (santé, référentiels,
authentification, profil, foyer, planning, recettes, préférences) —
souvent avec plus de détail (ex. reference.feature ne couvre pas
/reference/ingredients, contrairement à test/reference.test.ts). Pas
de perte de couverture réelle.

- apps/api/features/ (scénarios .feature + step-definitions + world/hooks)
  et cucumber.cjs supprimés
- apps/api/package.json : retire le script `test:bdd` et la
  dépendance @cucumber/cucumber (@faker-js/faker conservé — aussi
  utilisé par la suite Mocha)
- .github/workflows/ci.yml : retire l'étape `pnpm --filter api test:bdd`
- README : retire toute mention Cucumber/Gherkin/BDD (section dédiée,
  commandes, description du monorepo, note faker.js)

Le côté Cypress était déjà propre sur main (l'intégration testée sur
la branche feat/cypress-cucumber n'a jamais été mergée — PR fermée,
branche supprimée).
2026-08-19 12:41:17 +02:00
kyuno053
c63bc15ea4
fix(web): sidebar reste fixe sur page longue, page Liste de courses centrée (#21)
* fix(web): sidebar reste fixe sur page longue, page Liste de courses centrée

- AppLayout: .app-layout passe de min-height:100vh à height:100vh +
  overflow:hidden, et .app-content devient le vrai conteneur de scroll
  (overflow-y:auto). Avant, une page plus haute que le viewport faisait
  défiler tout le document, entraînant la sidebar avec lui au lieu de la
  garder fixe.
- ComingSoonPage (page "Liste de courses"): ajoute margin:0 auto, comme
  .settings-page, au lieu de rester collée au bord gauche avec un grand
  espace vide à droite sur les écrans larges.

* fix(web): la grille recettes stretche le panneau de détail sur toute sa hauteur

Régression révélée par le fix du layout précédent (AppLayout.scss) : tant
que `.app-layout` grandissait sans limite (`min-height: 100vh`), rien
dans la chaîne de hauteurs (.app-content → .recipes-page →
.recipes-page__catalog → .recipe-detail-panel) n'avait de hauteur
réellement définie, donc `.recipe-detail-panel { height: 100% }` ne
contraignait jamais rien — le panneau grandissait pour tout afficher,
masquant silencieusement que sa grille parente (`&__catalog`, display:
grid) n'a jamais eu de `grid-template-rows` explicite.

Avec `.app-layout` maintenant fixé à `height: 100vh`, `&__catalog` a
enfin une hauteur définie — mais sans `grid-template-rows`, sa seule
ligne implicite reste dimensionnée à son contenu (auto) au lieu de
s'étirer sur toute la hauteur disponible, comme l'intention documentée
en commentaire (« both stretched to the same full height ») le disait
déjà. Résultat concret : le panneau de détail recette rendait plus
court que son propre contenu, qui se retrouvait coupé par son
`overflow-y: auto` — repéré via l'échec CI de recipes.cy.ts sur
"opens a recipe's detail alongside the table" (timeout sur la
description, `<p>` non visible car clippée par un parent en overflow).

Vérifié en réinjectant les mêmes fixtures dans le navigateur (mock
fetch temporaire) : le panneau passe de 296px à 448px de haut, la
description reste entièrement dans ses limites.

* fix(web): restaure le layout table+panneau empilé sous 1024px

Le commit précédent (grid-template-rows sur .recipes-page__catalog)
corrigeait bien le layout desktop (2 colonnes, 1 ligne étirée sur
toute la hauteur), mais la même règle s'appliquait aussi telle quelle
au breakpoint mobile/tablette (@media max-width: 1024px), où la grille
passe à 1 seule colonne — donc à 2 lignes empilées (table puis
panneau), pas 1. Forcer ces 2 lignes dans un seul track `minmax(0, 1fr)`
coupait la hauteur disponible en deux et donnait 0px à la ligne du
tableau, dont le contenu se retrouvait entièrement clippé par son
propre `overflow-y: auto` — repéré via le nouvel échec CI de
recipes.cy.ts (Ratatouille non visible), le viewport par défaut de
Cypress (1000×660) tombant sous ce seuil de 1024px.

Réinitialise `grid-template-rows: auto` dans ce même bloc `@media`, à
côté du `height: auto` qui y était déjà (même logique : plus de ligne
unique à hauteur fixe à ce breakpoint). Revérifié dans le navigateur
aux deux largeurs (1000px et 1600px) avec les mêmes fixtures que le
test.

* test(web): agrandit le viewport du test de détail recette

Dernier morceau de la même régression : la grille catalogue+panneau
(master-detail) est un layout desktop, comme la grille de planning
(voir planning-page.cy.ts's `cy.viewport(1600, 900)`, même
justification). Au viewport par défaut de Cypress (1000×660), le
panneau de détail n'a tout simplement pas assez de hauteur pour
afficher photo + en-tête + ingrédients + étapes sans avoir besoin de
scroller en interne — c'est le comportement voulu de
`.recipe-detail-panel` (`overflow-y: auto`, voir son commentaire dans
recipes.scss), pas un bug. Ce n'était jamais visible avant que
`.app-layout` ait une vraie hauteur bornée (voir les 2 commits
précédents) : tout grandissait sans limite, donc rien n'avait jamais
besoin de scroller pour être visible.

Reproduit en rejouant le flow exact du test dans le navigateur (visite
/recettes, clic sur la ligne Omelette, pas juste une navigation directe
vers /recettes/2 — la simple navigation directe ne reproduisait pas
l'échec) : à 1000×660 la description était bien coupée
(descWithinPanel: false) ; à 1600×900 tout rentre.
2026-08-19 10:42:30 +02:00
kyuno053
4b988706cc
fix(docker): n'installe plus/ne build plus Cypress dans l'image de prod (#22)
- README : le dev local cible désormais `docker compose up -d postgres`
  explicitement. `docker-compose.yml` définit aussi le service `app`
  (celui que Portainer build en prod) — sans nom de service,
  `docker compose up -d` construisait et démarrait aussi ce conteneur en
  local, déclenchant un `pnpm install` sur tout le monorepo (donc le
  `cypress` d'apps/web, avec son téléchargement de binaire) pour une image
  dont le dev local n'a pas besoin.
- Dockerfile (apps/api) : `CYPRESS_INSTALL_BINARY=0` sur le stage `build`
  — évite le téléchargement du binaire Cypress (~200 Mo, Electron) à
  chaque build de l'image de prod, qu'il vienne de `docker compose` ou du
  pipeline de release. Le package `cypress` lui-même reste installé (vraie
  entrée du lockfile) ; `pnpm prune --prod` puis un wipe+reinstall complet
  ont été testés pour l'éliminer aussi, mais se sont révélés dangereux
  dans ce monorepo pnpm (prune ignore les node_modules des autres
  packages du workspace ; le wipe+reinstall a fait sauter le binding
  natif compilé d'argon2, crash silencieux au démarrage du conteneur) —
  pas justifié pour ~10 Mo de JS inerte une fois le téléchargement du
  binaire évité.
- apps/api/package.json : déplace `prisma` (le CLI, invoqué directement
  par le `CMD` du conteneur pour `migrate deploy`) de devDependencies
  vers dependencies — classification plus correcte indépendamment du
  point ci-dessus.

Vérifié par un vrai `docker build` + `docker run` contre un Postgres réel
(migrations, seed, démarrage du serveur, signup avec hash argon2 réel).
2026-08-19 09:45:04 +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
6f02cc0922 fix(e2e): mock GET /reference/ingredients et /profile/disliked-ingredients
preferences.cy.ts échouait en CI (3/4 tests) sur des assertions sans
rapport apparent avec le contenu changé cette session (#diet
introuvable, 'Arachides' introuvable) : PreferencesPage.tsx charge
aussi ces deux endpoints dans le même Promise.all que
diets/allergies (pour DislikedIngredientsField), mais le test ne les
mockait pas — la requête réseau réelle échoue en CI (pas de backend),
le Promise.all rejette, la page entière rend son état d'erreur au lieu
du formulaire, ce qui fait disparaître #diet et les cases à cocher
d'allergies avec.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 14:11:29 +02:00
655c10ad95 fix(recipes): agrandit l'icône réglages, aligne les cases à cocher à gauche
- .ingredient-picker__display-toggle svg : 1.1rem -> 1.5rem, trop
  petite dans son bouton 2.25rem.
- .ingredient-picker__display-menu : neutralise le margin-left: auto
  du check-mark app-wide (global.scss, pensé pour une grille de chips
  avec coche en bout de ligne) pour ce menu vertical de préférences —
  coche à gauche, libellé à droite, plus classique pour ce type de menu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 13:47:52 +02:00
8d1c763add fix(recipes): menu d'options d'affichage explicite dans le picker d'ingrédients
Remplace les deux boutons icône seule (régimes/allergènes) par un
bouton réglages unique ouvrant un menu avec deux cases à cocher
labellisées — les icônes seules n'étaient pas assez explicites sur ce
qu'elles activaient/désactivaient.

En chemin, corrige un bug réel découvert pendant l'implémentation : les
cases à cocher rendaient invisibles (la règle globale "selectable
card" de global.scss masque le <input type="checkbox"> natif et
attend un <span class="check-mark"> + une classe is-selected sur le
<label> pour dessiner l'état coché — mes cases n'avaient ni l'un ni
l'autre). Corrigé en suivant exactement le même pattern que
AllergySelect.tsx.

Supprime AllergenIcon (nav-icons.tsx), devenu inutilisé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 13:35:02 +02:00
84003d2009 fix(recipes): empêche la sélection de texte native sur les chips/cartes du picker
Les boutons de filtre (catégorie/sous-catégorie/toggles) et les cartes
d'ingrédients n'avaient pas user-select: none — un clic qui bouge d'un
pixel (clic rapide, trackpad) est alors lu comme un glisser de
sélection de texte, et le surlignage natif du navigateur s'affiche à
la place de l'état .active attendu. Vu de l'extérieur ça ressemble à
un bug de CSS de sélection flaky ; c'est en fait ce surlignage natif
qui apparaît par intermittence.

Ajouté sur .ingredient-picker (hérité par tous ses boutons enfants),
avec un user-select: text explicite ré-appliqué sur le champ de
recherche pour ne pas casser le copier/coller dedans.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 13:21:45 +02:00
d761a3795f fix(migrations): rend la migration ingredient_taxonomy_rework safe sur des données existantes
La version générée automatiquement par `prisma migrate diff` castait
directement chaque valeur `category` existante (ancien enum à 18
valeurs) vers le nouvel enum à 7 valeurs — échoue pour toute ligne déjà
seedée, puisqu'aucune ancienne valeur n'existe dans le nouvel enum.
En local ça passait inaperçu (reset complet sur une base vide), mais en
production (données déjà seedées) la migration échoue avec "invalid
input value for enum".

Réécrite pour ajouter les nouvelles colonnes avec une valeur par défaut
sûre (aucun cast des données existantes), puis les substituer aux
anciennes — même logique que les défauts `@default(...)` déjà
documentés dans schema.prisma : seedReferenceData() (relancée à chaque
démarrage du conteneur, voir apps/api/Dockerfile) corrige tout de suite
après la vraie catégorie/sous-catégorie de chaque ligne.

Ajoute aussi un DROP TYPE IF EXISTS défensif : une tentative précédente
de cette migration laisse le type IngredientSubcategory orphelin (son
CREATE TYPE s'exécute hors de la transaction qui échoue plus loin), une
nouvelle tentative sans ce garde-fou échouerait différemment ("type
already exists").

Vérifiée en rejouant l'historique complet des migrations sur une base
de test jetable, avec des lignes portant les anciennes valeurs d'enum
insérées à la main pour reproduire exactement l'échec signalé — la
version corrigée s'applique proprement et préserve les id existants
(donc toute vraie ligne RecipeIngredient qui y référence).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 12:47:14 +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
9775cbe38f feat(recipes): refonte de la catégorisation des ingrédients façon rayons de supermarché
Remplace les 18 catégories plates (mélangeant rayons génériques et
cuisines d'origine — un ingrédient pouvait finir dans "légumes" ou
"cuisine italienne" selon l'angle choisi) par une hiérarchie à 2 niveaux
sur le modèle d'un supermarché français :

- 🥦 Produits frais (légumes, fruits, herbes fraîches)
- 🥩 Boucherie & poissonnerie (viandes, volailles, poissons,
  crustacés & fruits de mer)
- 🥫 Épicerie sèche (féculents, légumineuses, graines & fruits secs,
  autres produits secs/conserves)
- 🍞 Boulangerie (pains, pâtes à cuire — 4 nouveaux ingrédients :
  pâte feuilletée/brisée/à pizza/à tarte sablée)
- 🧈 Crémerie & fromage (produits laitiers, œufs, alternatives
  végétales)
- 🧂 Condiments & épices (épices, sauces, assaisonnements)
- 🍳 Aides culinaires (bases, épaississants, sucres)

- Schéma : nouvel enum IngredientCategory (7 valeurs) + nouvel enum
  IngredientSubcategory (22 valeurs) + colonne Ingredient.subcategory.
  Migration appliquée via reset (données de référence, aucune perte
  réelle) car les anciennes valeurs d'enum n'existent plus dans les
  nouvelles.
- reference-seed-data.ts entièrement réorganisé par (catégorie, sous-
  catégorie), tous les 437 ingrédients recatégorisés un par un.
- packages/shared : INGREDIENT_CATEGORIES (7), INGREDIENT_SUBCATEGORIES
  (22) et INGREDIENT_CATEGORY_SUBCATEGORIES (mapping catégorie →
  sous-catégories, pour piloter le picker) remplacent l'ancienne liste
  plate.
- IngredientPicker : drill-down à 2 niveaux — la ligne de sous-
  catégories (couleur --color-tag, visuellement subordonnée) apparaît
  une fois une catégorie choisie, et se réinitialise au changement de
  catégorie.
- i18n : nouvelles clés recipes.form.category.* (7) et
  recipes.form.subcategory.* (22).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:47:47 +02:00
11b03b0e56 feat(recipes): toggles afficher/masquer allergènes et régimes dans le picker
Deux boutons icône en bout de la barre de recherche de l'IngredientPicker
(régimes/feuille, allergènes/triangle), même pattern bouton-icône +
état actif que FavoriteStarButton. Préférence d'affichage locale au
picker (non persistée) — n'affecte pas quels ingrédients apparaissent,
seulement si leurs badges allergènes/régimes sont visibles sur les
cartes. Nouvelle icône AllergenIcon dans nav-icons.tsx, réutilise
DietPreferencesIcon existante.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:17:55 +02:00
5890c62462 fix(recipes): overflow des préférences + régimes liés aux ingrédients
- Fix overflow horizontal sur /parametres/preferences : <fieldset> a un
  min-width: min-content par défaut du navigateur, ce qui empêchait la
  grille de l'IngredientPicker de wrapper (page entière poussée à
  ~3100px de large). Reset min-width: 0 sur .disliked-ingredients-field.
- Recatégorise les laits/crèmes végétaux (coco, amande, avoine) de
  PRODUITS_LAITIERS_OEUFS vers LIQUIDES_BOISSONS — ce ne sont pas des
  produits laitiers.
- Nouveau modèle IngredientDiet (many-to-many ingrédient <-> régime) :
  quels régimes (Végétarien, Végan, Pescétarien) chaque ingrédient
  respecte. Omnivore volontairement absent (trivial) et Sans gluten
  aussi (déjà dérivable de l'allergène Gluten existant).
- reference-seed-data.ts : chaque groupe de catégorie porte un
  defaultDiets, avec dietNames en override pour les exceptions
  (fromages, viandes, poissons, sauces à base d'œuf/poisson...).
- IngredientView.diets exposé par /reference/ingredients et /recipes,
  affiché via DietBadges dans IngredientPicker et IngredientRow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:01:57 +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
fff8c0da26 fix(api): seed reference data (diets/allergies) on container start
Found on http://batch.dev.kyuno.fr/: GET /reference/diets and
/reference/allergies both returned [] — onboarding's "régime
alimentaire" step and the profile's food-preferences tab had nothing to
show. Cause: the Docker image's CMD only ran `prisma migrate deploy`
(schema), never the seed that populates Diet/Category/Allergy.

Adds src/scripts/seed-runtime.ts — a runtime-only seed entry point
(distinct from prisma/seed.ts, the dev-time one wired to `prisma db
seed`/`prisma migrate reset` via tsx importing from ../src, which isn't
shipped in the runtime image). This one lives under src/ so tsc compiles
it into dist/ alongside everything else, and runs via plain `node`,
reusing the same idempotent seedReferenceData() (upserts by unique name)
already used by prisma/seed.ts and test-support/reset-db.ts.

Dockerfile CMD now runs it between migrate deploy and starting the
server — safe on every container start/restart, confirmed idempotent
locally (no duplicates, no error on a second run against an
already-seeded database).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 00:04:14 +02:00
266f540d88 fix(api): make the session cookie's Secure flag overridable
Found on http://batch.dev.kyuno.fr/: login/signup succeeded (200/201,
profile in the body) but every subsequent request 401'd. Cause: the
session cookie is `secure: NODE_ENV === "production"`, and
docker-compose.yml sets NODE_ENV=production regardless of whether the
deployment actually has TLS in front of it. A Secure cookie is silently
never sent back by the browser over plain HTTP — no error, just a cookie
that never round-trips.

Adds COOKIE_SECURE, independent from NODE_ENV, to override the flag per
deployment. Unset (default) keeps prior behavior — secure in production.
Set COOKIE_SECURE=false only for a deployment reachable over plain HTTP
(no TLS yet), like this dev instance.

Verified locally: docker compose up with COOKIE_SECURE=false persists
and round-trips the cookie (signup -> /auth/me 200); without it, the
cookie still gets Secure as before. Full pnpm --filter api test / test:bdd
suites still pass (66 + 25 scenarios).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:49:05 +02:00
44e9245a0f fix(web): default API_BASE_URL to same origin, not localhost:3000
Found live on http://batch.dev.kyuno.fr/: every API call went to
http://localhost:3000 in the visitor's own browser instead of the
deployed origin, because API_BASE_URL fell back to a hardcoded
"http://localhost:3000" whenever VITE_API_URL wasn't set at build time
— which is exactly the case for the production Docker image (apps/web/.env
is excluded by .dockerignore, so Vite never sees VITE_API_URL there).

Now defaults to "" (same origin as the page), correct for the merged
single-container deployment where the API serves this very frontend
build. Native dev (pnpm dev:web) is unaffected — apps/web/.env still sets
VITE_API_URL=http://localhost:3000 explicitly there.

Verified: built the Docker image, confirmed "localhost:3000" no longer
appears in the bundled JS, and docker compose up shows /auth/me called
against the container's own origin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 23:28:24 +02:00
c2cc773c7b Fusionner web+api en une image, séparer la CI, ajouter la release
- express-tools: ExpressServer.serveStaticFrontend() sert le build du
  frontend (assets + fallback SPA), monté après les routes API et avant
  le 404 JSON. Opt-in via FRONTEND_DIST_DIR (uniquement défini dans
  l'image Docker) — le dev natif (dev:api/dev:web) est inchangé.
- apps/api/Dockerfile: build aussi apps/web, embarque son dist dans le
  runtime ; corrige au passage l'oubli de packages/date-tools. Supprime
  apps/web/Dockerfile et nginx.conf (plus de conteneur nginx séparé).
- docker-compose.yml: un seul service "app" (postgres + app), un seul
  port APP_PORT, plus de WEB_PORT/CORS_ORIGIN à coordonner entre deux
  origines. Garde `build:` (pas de registre — Portainer build depuis le
  repo Git).
- ci.yml: éclate le job unique lint-and-test+e2e en 4 jobs indépendants
  (lint/test/build/e2e), sans chaînage, déclenchés sur chaque push
  (toute branche) + PR vers main.
- release.yml (nouveau): sur tag vX.Y.Z, sanity-build de l'image Docker,
  GitHub Release avec changelog auto-généré, puis notification best-effort
  du webhook Portainer (secret PORTAINER_WEBHOOK_URL).
- README: documente le conteneur unique et le pipeline de release.
2026-08-17 23:11:26 +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
cdca6f61f4 Tests: couverture Cypress pour le repli/déploiement de la sidebar
Repli/déploiement via le bouton dédié, labels masqués (pas retirés du
DOM — restent pour le title="") mais liens toujours visibles/cliquables
icône seule, persistance via localStorage vérifiée après cy.reload().
2026-08-17 16:17:06 +02:00
73bfcd8fac Web: sidebar avec icônes + repli/déploiement
Design validé via une maquette HTML itérée avec l'utilisateur (voir
historique de conversation) avant implémentation.

- nav-icons.tsx: jeu d'icônes trait fin (24x24, inline SVG) pour les 3
  items de nav principaux, les 4 pages de Paramètres et le chevron de
  repli — pas de librairie d'icônes pour une poignée de glyphes
- AppLayout: bouton replier/déplier à côté du logo — la sidebar passe
  de 15rem à un rail 4.25rem icônes-seules ; un seul toggle de classe
  CSS sur le <aside> pilote tout (aucun sous-composant n'a besoin de
  savoir que la sidebar est repliée), état persisté en localStorage
- Logo : monogramme "bC" en repli plutôt qu'une icône arbitraire à la
  place du nom complet
- Avatar rond (initiale) dans le menu compte, popover recalée en
  largeur fixe quand la sidebar est repliée (sinon écrasée à 4.25rem)
- title="" sur chaque item pour l'infobulle native une fois replié
2026-08-17 16:16:42 +02:00
a8d897dfab Tests: couverture Cypress pour les préférences utilisateur (step 4/4)
- user-preferences.cy.ts: défaut SYSTEM, thème sauvegardé restitué et
  appliqué au document, autosave sans bouton "Enregistrer"
- sidebar.cy.ts: 4 pages de paramètres (libellé "Préférences
  alimentaires" renommé + nouvelle entrée "Préférences utilisateur")
- auth.cy.ts/onboarding.cy.ts: corrige un intercept **/planning/current
  périmé (route supprimée dans la PR précédente) — masqué jusqu'ici car
  aucune assertion n'en dépendait directement, mais provoquait un appel
  réseau non mocké
2026-08-17 15:56:36 +02:00
4bdc9c0470 Web: UserPreferencesPage (thème) + renommage "Préférences alimentaires" (step 3/4)
- ThemeContext (features/theme/) : charge/applique le thème du profil
  connecté via l'attribut data-theme (SYSTEM = pas d'attribut, laisse
  la media query prefers-color-scheme décider) ; échec réseau non
  bloquant (worst case reste au thème courant, pas d'unhandled rejection)
- UserPreferencesPage, routée /parametres/preferences-utilisateur,
  hot-save (3 boutons radio Clair/Sombre/Système)
- layout.settings.nav.preferences renommé "Préférences alimentaires"
  (évite la confusion avec ce nouveau concept plus large), nouvelle
  entrée "Préférences utilisateur"
- _theme.scss: commentaires mis à jour (l'attribut data-theme est
  désormais réellement posé, plus une simple anticipation)
2026-08-17 15:56:12 +02:00
3acde696f5 API: module preferences — GET/PATCH /preferences (step 2/4)
- getPreferences: SYSTEM par défaut si aucune ligne (même logique que
  dietId/allergies : absent = valeur par défaut, pas une omission)
- updatePreferences: upsert (crée la ligne au premier PATCH)
- Tests Mocha + Cucumber : 401, valeur invalide, défaut, création à la
  volée, cloisonnement entre profils
2026-08-17 15:55:45 +02:00
e3811dc280 Shared + migration: table user_preference (thème clair/sombre/système) (step 1/4)
- Modèle Prisma UserPreference (1-1 avec UserProfile, onDelete Cascade),
  enum ThemePreference (LIGHT/DARK/SYSTEM, défaut SYSTEM)
- packages/shared: THEME_PREFERENCES/ThemePreference, PreferencesView,
  updatePreferencesSchema
2026-08-17 15:55:18 +02:00
9d4de338e3 Tests: couverture Cypress pour la grille de planning (step 4/4)
- home-planning.cy.ts → planning-page.cy.ts : nav sidebar reprise
  telle quelle, nouveaux cas pour la grille (case vide, recettes
  placées dans les bonnes cases, colonne du jour courant, navigation
  semaine précédente/suivante, popover calendrier)
- cy.clock fige "aujourd'hui" (2026-08-17, un lundi) pour des
  assertions de date déterministes ; cy.viewport élargi (desktop-only,
  cf. décision produit) pour que les 7 colonnes tiennent sans scroll
  horizontal lors des assertions de visibilité
2026-08-17 14:19:39 +02:00
07c3851957 Web: PlanningPage — grille hebdomadaire + navigation semaine (step 3/4)
- Remplace HomePage (table jour unique) par PlanningPage : grille
  7 jours × 5 repas, groupés Matin/Midi/Après-midi/Soir (séparateurs
  pleins, plus épais entre groupes), recettes en pastilles pleine
  largeur, bouton "+" pleine largeur sans bordure (pas encore branché
  — pas de catalogue de recettes côté API, tâche future)
- WeekNavigator + CalendarPopover (sur date-tools) : flèches semaine
  précédente/suivante, popover calendrier (mois navigable, clic sur
  un jour → sa semaine), fermeture au clic extérieur
- apiClient.getPlanningForWeek(date) remplace getCurrentPlanning()
- i18n: namespace home → planning (+ nouvelles clés jours/repas/
  calendrier), common.loadError factorisé (repris par les pages
  Foyer/Préférences qui réutilisaient l'ancien home.error)
2026-08-17 14:19:16 +02:00
9b2b2c2e28 API: GET /planning?date= remplace GET /planning/current (step 2/4)
- getPlanningForDate(houseId, date: DateTime) — paramétré au lieu de
  toujours "aujourd'hui", même logique de recherche sinon
- GET /planning?date=YYYY-MM-DD, validation de forme (zod) puis de
  validité calendaire (parseDateOnly, 400 VALIDATION_ERROR sinon) —
  un seul endpoint générique au lieu de deux qui se recouvrent
- Tests Mocha + Cucumber adaptés, + cas date manquante/malformée/
  impossible et "semaine différente d'aujourd'hui"
2026-08-17 14:17:46 +02:00
26453dd757 Tests: couverture Cypress pour les paramètres, l'onboarding et la sidebar (step 8/8)
- household.cy.ts scindé en preferences.cy.ts (régime/allergies) et
  household-settings.cy.ts (nom, créer/rejoindre/membres/suppression/
  quitter, avec et sans droits admin)
- onboarding.cy.ts réordonné (régime → foyer → allergènes), avec les
  cas skip / créer / rejoindre à l'étape foyer
- account.cy.ts: identité, suppression de compte (erreur de mot de
  passe, succès, annulation)
- sidebar.cy.ts: menu Paramètres (3 liens) et menu compte (Mon compte,
  déconnexion), redirection /foyer → /parametres/foyer
- auth.cy.ts/home-planning.cy.ts: adaptés au nouveau menu compte et au
  premier écran d'onboarding (régime, plus foyer)
2026-08-17 10:49:19 +02:00
60ca850042 Web: parcours d'inscription réordonné — régime, foyer (optionnel), allergènes (step 7/8)
- Ordre: régime (1) → foyer (2) → allergènes (3), au lieu de
  foyer → régime → allergènes
- L'étape foyer est désormais optionnelle: créer, rejoindre par code
  d'invitation, ou passer (aucun foyer n'est créé au signup)
2026-08-17 10:48:31 +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
8e9457297d Web: menu Paramètres en bas de la sidebar + menu compte utilisateur (step 5/8)
- AppLayout: retire "Foyer & profil" de la nav principale
- SettingsMenu: bloc repliable (Compte/Préférences/Foyer), ouvert par
  défaut si la route courante est sous /parametres
- AccountMenu: remplace le footer statique (salutation + déconnexion)
  par un petit menu déroulant (Mon compte, Se déconnecter)
2026-08-17 10:42:36 +02:00
3363cfad75 Tests API: couverture Mocha + Cucumber pour le foyer et la suppression de compte (step 4/8)
- house.test.ts réécrit (le foyer n'est plus auto-créé) + POST /house,
  POST /house/join, POST /house/leave, DELETE /house/current,
  DELETE /house/members/:id
- auth.test.ts: signup renvoie houseId=null, DELETE /auth/me (mauvais
  mot de passe, suppression, transfert d'admin)
- planning.test.ts/steps.ts: création explicite du foyer (POST /house)
- household.feature: scénarios créer/rejoindre/quitter/supprimer/
  retirer un membre, via un second agent (CustomWorld.secondAgent)
- auth.feature: scénarios de suppression de compte
2026-08-17 10:40:23 +02:00
7af98756dc API: le foyer n'est plus créé automatiquement à l'inscription + suppression de compte (step 3/8)
- signup() ne crée plus de House — houseId démarre à null, le foyer
  devient une étape optionnelle de l'onboarding (créer/rejoindre/passer)
- deleteAccount(): revérifie le mot de passe, transfère l'adminship ou
  supprime le foyer si nécessaire (leaveCurrentHouse), puis supprime
  le profil (cascade sur les allergies)
- DELETE /auth/me — nouvelle route, gated par mot de passe
- clearCookie n'envoie plus maxAge (corrige un warning de dépréciation
  Express, déjà latent sur /auth/logout)
2026-08-17 10:38:47 +02:00
7d5a6c05bb API: administrateur de foyer, code d'invitation, créer/rejoindre/quitter/supprimer (step 2/8)
- House.adminId / House.inviteCode (migration Prisma)
- house.service: createHouse, joinHouse, leaveCurrentHouse (transfert
  d'admin ou suppression du foyer si dernier membre), deleteHouse
  (admin-only), removeMember (admin-only)
- house.routes: POST /house, POST /house/join, POST /house/leave,
  DELETE /house/current, DELETE /house/members/:memberId
- GET/PATCH /house/current renvoient désormais membres + admin + code
2026-08-17 10:38:16 +02:00
189621c306 Test: fix auth.cy.ts signup test — stale post-onboarding-wizard assertions
Retour de CI (job e2e, PR #10) : "creates a profile and lands on the
home page" échouait avec une exception non gérée ("Failed to fetch")
depuis OnboardingHouseholdPage's GET /house/current — jamais mocké
dans ce test.

Cause : ce test n'avait jamais été mis à jour quand le signup a commencé
à rediriger vers /onboarding/foyer au lieu de "/" directement (fait dans
le commit du wizard d'inscription, plus tôt sur cette PR) — seul le
texte "Bonjour Alice Martin" -> "Bonjour Alice" avait été corrigé à
l'époque, pas la destination réelle de la redirection. Invisible
localement : je n'ai pas pu faire tourner Cypress dans ce sandbox
(crash GPU headless, confirmé pré-existant), donc ce test n'avait
jamais réellement été exécuté après ce changement — seulement
typechecké.

Fix : le test vérifie maintenant qu'il atterrit sur /onboarding/foyer
(pas la home) avec le nom du foyer préaffiché, et mocke GET
/house/current comme le fait déjà onboarding.cy.ts pour ce même écran.

Vérifié via tsc --noEmit (Cypress toujours injouable localement ici) —
CI à re-checker après push.
2026-08-17 07:48:33 +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
6cfa71730c API: séparer allergies et intolérances (kind sur Category) (step 7/8)
Retour fonctionnel : les allergies et intolérances doivent être
distinguées, pas listées ensemble.

- schema.prisma: enum AllergenKind (ALLERGY|INTOLERANCE) + Category.kind
  (@default(ALLERGY), migration écrite à la main comme précédemment —
  `migrate dev` refuse en environnement non-interactif ici — SQL généré
  via `prisma migrate diff`).
- reference-seed-data.ts: classification par substance (Gluten et
  Sulfites = INTOLERANCE, les 12 autres = ALLERGY — réaction
  non-immunitaire documentée vs réaction immunitaire classique).
  Corrige au passage l'upsert : `update: { kind }` au lieu de `update:
  {}` — un reseed doit pouvoir corriger `kind` sur une Category déjà
  existante, pas juste no-op.
- reference.service.ts / packages/shared: AllergyView gagne `kind`.
  PATCH /profile/allergies ne change pas (une seule liste d'IDs, kind
  ne sert qu'au groupement d'affichage côté client).
- Tests Mocha (29 passing) + Cucumber (15 scenarios, inchangés).

Classifié par substance (pas par utilisateur) — documenté comme
limitation connue dans le README. Web (split UI + hot saving sur
/foyer) dans le commit suivant.
2026-08-17 00:01:19 +02:00
9722f4a27b Tests + docs: onboarding/foyer Cypress coverage, specs updates (step 6/6)
- apps/web/cypress/e2e/onboarding.cy.ts — parcours complet (rempli et
  entièrement skippé) signup → 3 étapes → home, mêmes conventions
  cy.intercept que le reste.
- apps/web/cypress/e2e/household.cy.ts — /foyer : préremplissage, et
  sauvegarde indépendante de chacune des 3 sections.
- specs/frontend-architecture.md : nouvelle section "Parcours profil —
  foyer, régime, allergènes" (diagramme mermaid, les deux bugs de state
  trouvés en testant dans le navigateur), arborescence et namespaces
  i18n à jour.
- README.md : nouvelle section "Parcours profil — foyer, régime,
  allergènes", section sidebar mise à jour (Foyer & profil n'est plus
  un stub).

Cypress lui-même ne peut pas tourner en local dans ce sandbox (voir la
note existante dans le README) — vérifié via `tsc --noEmit` sur les
specs + parcours manuel complet dans le navigateur (les deux à travers
les 5 commits précédents de cette feature).

Clôt la feature profil/foyer/régime/allergènes (6 commits, cette PR) :
seed+référence -> endpoints foyer/profil -> composants partagés ->
wizard d'inscription -> page /foyer -> ce commit.
2026-08-16 23:42:54 +02:00
5d88e28810 Web: page /foyer réelle (édition foyer/profil) (step 5/6)
- HouseholdPage remplace le stub ComingSoonPage : 3 sections
  indépendamment sauvegardées (nom du foyer, régime, allergènes/
  intolérances), mêmes composants partagés que le wizard d'inscription.
  Chaque section a son propre bouton "Enregistrer" (3 ressources API
  distinctes, pas de raison qu'une modification attende les autres).
- AuthContext : ajout de refreshUser() — re-fetch GET /auth/me et met
  à jour `user`.
- Bug trouvé et corrigé en testant l'aller-retour SPA dans le
  navigateur (sidebar → Recettes → Foyer, sans rechargement complet) :
  la valeur du régime revenait à l'ancienne après sauvegarde. Cause :
  la page initialisait dietId depuis useAuth().user.dietId, un instantané
  jamais rafraîchi après une modification faite directement via
  apiClient (qui ne touche pas AuthContext). Fix : la page fetch son
  propre profil frais (apiClient.me()) au montage plutôt que de
  dépendre du contexte, et appelle refreshUser() après une sauvegarde
  réussie du régime pour que le reste de l'app reste cohérent aussi.
- household.comingSoon (clé i18n) supprimée, plus utilisée.

Vérifié dans le navigateur : préremplissage, sauvegarde par section,
persistance après rechargement ET après navigation SPA aller-retour ;
nettoyage du compte de test.
2026-08-16 23:38:48 +02:00
7bc81f51fb Web: wizard d'inscription (foyer/régime/allergènes) (step 4/6)
- SignupPage: après signup(), navigate("/onboarding/foyer") au lieu de
  "/" — la home reste inchangée, seule la destination change.
- pages/onboarding/: 3 routes top-level RequireAuth-gated (PAS nichées
  sous AppLayout — wizard plein écran sans sidebar, même langage visuel
  que /login|/signup) :
  - /onboarding/foyer — HouseNameField, préremplie avec le nom
    auto-généré du foyer (continuer sans éditer = skip implicite).
  - /onboarding/regime — DietSelect, valeur initiale depuis
    useAuth().user.dietId (pas de fetch supplémentaire nécessaire).
  - /onboarding/allergenes — AllergySelect, termine sur navigate("/").
- Bug trouvé et corrigé en testant dans le navigateur : RedirectIfAuthenticated
  redirigeait vers "/" en course avec le navigate() explicite de
  SignupPage — `user` devient non-null (via signup()) pendant que
  SignupPage est encore monté sous ce guard, qui réagit et redirige
  avant que le navigate("/onboarding/foyer") ne prenne effet. Latent
  depuis le début (invisible avant car l'ancien SignupPage naviguait
  aussi vers "/", donc les deux redirections concordaient). Fix : la
  décision de redirection est verrouillée une seule fois, au moment où
  la vérification initiale (`isLoading`) se termine, plutôt que
  réévaluée à chaque changement de `user`.

Vérifié de bout en bout dans le navigateur (inscription → 3 étapes →
home), données confirmées en base (foyer renommé, régime + 2 allergènes
enregistrés), puis nettoyage des comptes de test.
2026-08-16 23:31:59 +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
1d03effc77 API: endpoints foyer/profil (nom, régime, allergènes) (step 2/6)
- GET/PATCH /house/current — renomme le foyer de l'utilisateur connecté.
  PATCH avec houseId null -> 404 HOUSE_NOT_FOUND.
- PATCH /profile/diet { dietId: number | null } — régime du profil ;
  null l'efface (étape skippable du parcours). dietId invalide ->
  404 DIET_NOT_FOUND.
- GET/PATCH /profile/allergies — allergènes/intolérances, liste d'IDs ;
  PATCH remplace l'ensemble complet (pas une fusion, cohérent avec un
  multi-select). ID invalide -> 404 ALLERGY_NOT_FOUND.
- 3 nouveaux ErrorCode (4041-4043) + libellés fr.
- Extraction de toSafeProfile() dans src/lib/safe-profile.ts —
  auparavant dupliqué dans auth.service.ts et require-auth.ts,
  profile.service.ts le réutilise aussi.
- Tests Mocha (28 passing) + Cucumber (15 scenarios) — même convention
  que le reste, doc README.

Deuxième commit de la feature profil/foyer/régime/allergènes —
composants front partagés dans le commit suivant.
2026-08-16 23:18:46 +02:00
9b7c955019 API: seed régimes/allergènes + GET /reference/diets, /reference/allergies (step 1/6)
- schema.prisma: Diet.name/Category.name deviennent @unique (pas dans le
  doc spec d'origine — ajouté pour que le seed soit idempotent par
  upsert). Migration écrite à la main + appliquée via `migrate deploy`
  (`migrate dev` refuse en environnement non-interactif ici) — SQL
  généré via `prisma migrate diff` pour matcher exactement les
  conventions Prisma.
- src/db/reference-seed-data.ts: seedReferenceData() — 5 régimes, 14
  allergènes (règlement UE 1169/2011 annexe II). Chaque allergène = une
  Category (upsert par nom) + une unique Allergy sous cette catégorie
  (Allergy elle-même ne porte pas de nom, voir schema.prisma).
  Réutilisée par prisma/seed.ts (CLI, `prisma db seed`) ET
  test-support/reset-db.ts (chaque test repart avec ces données de
  référence, pas des tables vides).
- modules/reference/: GET /reference/diets, GET /reference/allergies —
  publics (pas de requireAuth), lisibles avant qu'un compte existe
  (wizard d'inscription).
- packages/shared: DietView, AllergyView (name résolu côté serveur
  depuis Category, le split Allergy/Category reste invisible du client).
- Tests Mocha + Cucumber, doc README.

Premier commit de la feature profil/foyer/régime/allergènes (planifiée
en chat) — endpoints foyer/profil dans le commit suivant.
2026-08-16 23:09:02 +02:00
72472581d6 Tests + docs: sidebar/planning Cypress coverage, frontend-architecture.md (step 5/5)
- New apps/web/cypress/e2e/home-planning.cy.ts (mocked API, same
  cy.intercept convention as auth.cy.ts):
  - sidebar nav between sections + active-link highlighting
  - user name + logout from the sidebar footer
  - home planning: empty / loaded (table rows) / error states
- specs/frontend-architecture.md: documents AppLayout (single
  RequireAuth+AppLayout parent route, nested routes via <Outlet />),
  the ComingSoonPage stub pattern, updated folder tree and i18n
  namespace list, updated routing diagram.
- README.md: new "Accueil, sidebar & sections" section; notes the
  Cypress-can't-run-headless-here environment limitation (confirmed
  pre-existing on main) and the docker-vs-local-dev CORS_ORIGIN gotcha
  hit while manually verifying this feature.

Closes out the home-page-after-login feature (5 commits, this PR):
GET /planning/current -> AppLayout -> routing/stub pages -> HomePage
planning view -> this commit.
2026-08-16 21:09:31 +02:00
ac7f8d9685 Web: HomePage becomes the weekly planning view (step 4/5)
- ApiClient.getCurrentPlanning() — GET /planning/current.
- HomePage.tsx: replaces the old greeting card (now redundant with
  AppLayout's sidebar) with the household's current planning — loading /
  error / empty ("aucun planning pour cette semaine") / loaded (table of
  weekDay/meal/recipe) states, modeled as a discriminated union so an
  impossible combination (e.g. loading with data) can't be represented.
  No invented weekday/meal grid — the API's `weekDay`/`meal` are free-form
  strings (no enum exists yet in the schema), so this renders the items
  as returned rather than assuming a specific vocabulary.
- locales/fr/translation.json: home.* replaced (title/loading/error/
  empty/table.*), old greeting/logout keys removed (superseded by
  layout.greeting/layout.logout from the AppLayout commit).
- cypress/e2e/auth.cy.ts: updated the now-stale "Bonjour Alice Martin"
  assertions (greeting moved to the sidebar, first name only) and added
  GET /planning/current intercepts so these specs don't depend on a real
  backend. Manually verified end-to-end against a real API in the browser
  preview (empty state + a seeded planning) — Cypress itself can't run
  headless Chromium in this sandboxed dev environment (confirmed
  pre-existing on main, unrelated to this change); CI runs the real
  suite.

Verified with `pnpm --filter web build` after each of steps 2-4 to keep
every commit in this sequence independently buildable.
2026-08-16 21:06:15 +02:00
c07f8ad82e Web: wire AppLayout into routing + stub section pages (step 3/5)
- App.tsx: every authenticated route now nests under one
  RequireAuth + AppLayout parent route (react-router nested
  routes/<Outlet />) instead of each page wrapping its own guard.
- New routes/pages: /recettes, /liste-de-courses, /foyer, each a thin
  wrapper around a shared ComingSoonPage ("cette section arrive
  bientôt.") — no backend behind them yet, matches the "pages stub
  dédiées" choice discussed in chat.
- locales/fr/translation.json: nav labels + stub copy.

HomePage still shows its own greeting/logout card here (unchanged,
now nested inside AppLayout's <Outlet /> — briefly duplicating the
sidebar's greeting/logout) — replaced by the actual planning view in
the next commit.
2026-08-16 21:04:51 +02:00
f4ab482b52 Web: AppLayout sidebar component (step 2/5)
Adds the shell for authenticated pages: a fixed-width sidebar with
brand, section nav (Planning/Recettes/Liste de courses/Foyer & profil),
and the signed-in user's name + logout at the bottom, plus a main
content area rendering the matched child route via react-router's
<Outlet />. Follows the "Mise en Place" theme tokens; collapses to a
top bar under 640px (this app is meant to be embedded via Capacitor
later, see the root README).

Not wired into App.tsx yet (next commit) — self-contained and builds/
typechecks on its own.
2026-08-16 21:03:14 +02:00