Sans ça, `cypress/component/CheckboxOption.cy.tsx` (commit précédent)
ne tournait jamais en CI : `pnpm --filter web e2e` lance `cypress run`
sans `--component`, donc uniquement la suite e2e par défaut.
- apps/web/package.json : nouveau script `cy:run:component`
- ci.yml : étape dédiée après `pnpm --filter web e2e`, sans
start-server-and-test (Cypress lance son propre dev server Vite en
interne pour le component testing, pas besoin d'attendre l'appli
comme pour l'e2e)
Étape 1 du refactor (voir PR) : met en place le vrai mode Component
Testing de Cypress, séparé de l'e2e — monte un composant React isolé
(pas de routeur, pas de backend), pour tester les composants
génériques (components/ui/) indépendamment de tout parcours
utilisateur.
- cypress.config.ts : nouveau bloc `component` (devServer Vite, réutilise
vite.config.ts de l'appli — même plugin React, même Sass). Le hook
GPU-disable est factorisé (`disableGpu`) puisque e2e et component ont
chacun leur propre `setupNodeEvents`, pas de config partagée par défaut.
- cypress/support/component.ts + component-index.html : fichiers de
support standards Cypress CT — importe le vrai global.scss de l'appli
(les composants génériques sont stylés via lui, pas de CSS scopé à eux).
- cypress/component/CheckboxOption.cy.tsx : 4 scénarios sur
components/ui/Checkbox.tsx (rendu du label, reflet du prop `checked`
sur l'input natif + la classe `is-selected`, callback `onChange` avec
la valeur inversée, comportement contrôlé via un wrapper avec état).
Dépendance ajoutée, épinglée : @cypress/vite-dev-server@5.2.1 (dernière
version sans peer dependency cypress >=14 — 6.0.3+ l'exige explicitement,
on est sur cypress@13.17.0).
Testé en local jusqu'au mur GPU/Electron habituel (config + devServer
Vite chargent sans erreur) — l'exécution réelle du montage reste à
vérifier via la CI.
Sépare les tests Cypress en trois catégories, comme discuté :
1. Parcours utilisateur (cypress/e2e/) — scénarios Gherkin/Cucumber,
pilotés par @badeball/cypress-cucumber-preprocessor@22.2.0 (validé
sur experiment/cucumber-cypress, mergée). Ex. "En tant
qu'utilisateur, je peux créer un compte".
2. Layout applicatif (nouveau dossier à définir) — specs Cypress
classiques (pas de Gherkin), cy.visit() sur une vraie page, mais
centrées sur la disposition/visibilité des éléments, indépendamment
d'un parcours utilisateur scripté.
3. Composants génériques (cypress/component/, nouveau) — vrai
Component Testing Cypress, composant React monté isolément (pas de
routeur, pas de backend). Composants concernés aujourd'hui :
components/ui/{Checkbox,Radio,Dialog}.tsx.
Premier pas : mise en place de l'infra Component Testing (config +
devServer Vite + adapter React 18), validée sur un composant simple
avant de construire le reste.
* 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).
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).
* 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.
- 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).
* 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>
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>
- .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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
push (all branches) already fires for a PR branch on this repo; keeping
pull_request too just ran every job twice on the same commit. GitHub
still attaches the push-triggered run to the PR by SHA regardless of
which event fired it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 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.
- 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>
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().
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é
- 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é
- 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)
- 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
- 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é
- 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)
- 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"
- packages/date-tools: parseDateOnly/formatDateOnly/toDateOnly,
getWeekStart/addWeeks/buildCalendarMonth, sur Luxon DateTime (UTC)
- packages/shared: WEEK_DAYS/WeekDay, MEALS/Meal (contrat de valeurs
documenté pour PlanningItemView.weekDay/.meal, pas encore enforcé
en base), getPlanningByDateSchema (validation de forme de ?date=)
- 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)
- 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)
- 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
- 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)
- 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
- 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)