jsonLdRecipeAdapter était enregistré (et donc synchronisé comme Source
sélectionnable par un foyer) au même titre que theMealDbAdapter — mais
c'est une structure générique de parsing schema.org destinée à être
déclinée par site web scrappé, pas une source qu'on peut raisonnablement
« activer » ou « faire confiance » en tant que telle (aucun catalogue à
parcourir : list() renvoie toujours vide).
registerAllRecipeSources() ne l'enregistre donc plus — elle reste
utilisable directement (un futur adapter par site l'utiliserait en
interne, ou un futur flux « importer depuis une URL » l'appellerait
directement), simplement plus comme Source autonome du registre.
Corrige aussi le mock Cypress qui prétendait « mirrorer ce qui est
vraiment seedé » avec cette même source — remplacé par un second exemple
clairement illustratif (Marmiton), qui ne correspond à aucun adapter réel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same class of failure as the household-settings sources scenario earlier
this session: the steps section sits below the detail panel's
header/photo/description, off the fold of .app-content's own scroll — a
bare .should("be.visible") doesn't auto-scroll.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expose côté UI ce que tech-step-matcher.ts détecte déjà à la sauvegarde
(Step.techSteps) mais qui restait backend-only : dans le panneau détail
d'une recette, les mots exacts ayant déclenché une technique sont
surlignés, avec un tooltip (survol/focus clavier) donnant son nom.
- tech-step-matcher.ts : matchTechStepSpans(description, mappings) expose
désormais {techStepId, start, end} en plus de la simple séquence d'ids
(déjà calculé en interne, jusqu'ici jeté). matchTechSteps devient un
wrapper fin dessus — aucun changement à ses ~12 tests existants ni à
recipe-translation.ts.
- StepTechStep gagne start/end (nullable, pas de backfill — même leçon que
l'incident de migration ingredient_unit_catalog : NOT NULL sans défaut
sur une table déjà peuplée casse le déploiement). Une ligne pré-existante
sans span est simplement omise de la réponse API plutôt que de fuiter un
null, jusqu'à ce que la recette soit resauvegardée.
- recipe.service.ts : createRecipe/updateRecipe persistent start/end ;
StepView expose techSteps: { techStep: {id,key}, start, end }[]. Le
recalcul complet à chaque édition (ajout/modif/suppression d'étape) était
déjà garanti par le delete-then-recreate existant d'updateRecipe — testé
explicitement (nouveau test "recomputes techniques from scratch...").
- Frontend : StepDescription.tsx (découpe le texte via
highlight-tech-steps.ts, pur et testé) remplace le <p> brut dans
RecipeDetailPanel. Nouveau Tooltip.tsx (composants/ui, CSS pur, aucune
lib externe — même esprit que Dialog.tsx) : un <button> (focusable
nativement, pas de tabIndex sur un <mark> non interactif) affiche le nom
de la technique (catalog.techSteps.<key>) au survol/focus.
Tests : matchTechStepSpans (spans corrects, chevauchement résolu),
recipe.test.ts (forme API + recalcul complet sur modif/ajout/suppression
d'étape, avec vérification que les anciennes lignes StepTechStep sont bien
supprimées), splitDescriptionByTechSteps (tri, bornes invalides ignorées,
chevauchement résiduel ignoré), scénario Cypress recipes.feature
(surlignage + tooltip au focus).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI's e2e run confirmed the real cause: SourcesSection renders near the
bottom of /parametres/foyer (after name, invite code, members), inside
.app-content's own scrollable region — a bare `.should("be.visible")`
doesn't auto-scroll (only interaction commands like `.click()`/`.check()`
do), so the checkbox row was still clipped by that container's overflow
when the assertion ran. Adds a dedicated "I scroll to the section" step
and uses it before the sources-section assertions in household-settings.feature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le conteneur de prod ne peuplait jamais la table Source : seed-runtime.ts
(l'entrée seed de l'image Docker, exécutée après `prisma migrate deploy`)
n'appelait que seedReferenceData(), jamais registerAllRecipeSources()/
syncRecipeSources() — contrairement à prisma/seed.ts (dev). server.ts
enregistre bien les adaptateurs dans son propre registre en mémoire, mais
c'est un processus distinct de celui qui lance seed-runtime.js dans la
chaîne CMD du Dockerfile ; sans ce sync, GET /reference/sources renvoyait
toujours [], et HouseholdSettingsPage masquait silencieusement toute la
section sources (sources.length === 0 → return null). C'est ce que
l'utilisateur a remarqué : impossible de paramétrer les sources visibles
du foyer en prod.
Vérifié en local : Source/HouseSource vidées, seed-runtime.js compilé
relancé exactement comme le ferait le conteneur (migrate deploy déjà
appliqué, puis ce script) → les deux sources (TheMealDB, JSON-LD) sont
bien resynchronisées.
Ajoute aussi la couverture Cypress du parcours "sources" qui manquait :
- onboarding.feature : nouveau scénario où le catalogue de sources n'est
pas vide — l'étape /onboarding/sources s'affiche et se soumet, au lieu
du seul scénario existant qui la voyait toujours skippée (catalogue
vide).
- household-settings.feature : nouveaux scénarios pour la section sources
de /parametres/foyer — affichage + sauvegarde (autosave incluse) quand
des sources existent, et disparition complète de la section quand le
catalogue est vide.
- Nouvelles steps partagées (reference-data.steps.ts pour le catalogue,
household-mutations.steps.ts pour la sélection par foyer).
Non exécutés localement : Chromium/Electron headless plante au lancement
du process GPU dans cet environnement (limitation documentée du README,
reproductible sur main, sans lien avec ce changement) — vérifiés par
relecture attentive contre le code source réel (libellés de traduction,
routes, formes de requête/réponse) et en suivant le même gabarit que les
scénarios existants déjà verts en CI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Ajoute une expression régulière anglaise à chacun des 26 TechStepMapping
du catalogue (locale "en"), en plus du "fr" existant — les recettes en
anglais (TheMealDB, etc.) peuvent désormais matcher leurs étapes.
- Nouveau apps/api/src/lib/ingredient-matcher.ts : moteur de matching pur
(nom d'ingrédient, unité, quantité) contre les catalogues Ingredient/Unit,
à partir de labels anglais écrits à la main (packages/shared/src/data/
catalog-labels-en.ts — 546 INGREDIENT_LABELS_EN + 17 UNIT_LABELS_EN avec
synonymes/abréviations). Tokenise et stem naïvement les deux côtés pour
tolérer pluriels et mots descriptifs superflus ; la correspondance la
plus spécifique (le plus de mots) l'emporte en cas de recoupement.
- extractQuantity() : lit un nombre en tête de texte libre (entier,
décimal, fraction simple ou nombre mixte) pour déduire la quantité et
l'unité quand la source ne les fournit pas séparément.
- Étend recipe-translation.ts : translateRecipe(recipe, locale) résout
aussi ingredientId/unitId/quantity de chaque ligne d'ingrédient — mais
uniquement pour locale "en" (seules langue avec des labels), pour ne pas
interroger la base inutilement ni halluciner un match dans une autre
langue.
- Ajoute cup/ounce/pound au catalogue Unit (toBaseFactor réel), absents
jusqu'ici alors que très fréquents dans les recettes anglaises.
- Vérifié en conditions réelles contre TheMealDB (Teriyaki Chicken
Casserole) : 8/9 ingrédients résolus avec la bonne quantité/unité, le
seul raté ("stir-fry vegetables") étant un mélange sans entrée dédiée au
catalogue — dégradation gracieuse (unitId/ingredientId: null) comme prévu.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deuxième adaptateur concret, cette fois générique plutôt que lié à un
site précis : la plupart des sites de recettes embarquent des
données structurées JSON-LD (schema.org/Recipe) pour le SEO/Google
Rich Results — un seul adaptateur peut donc couvrir une grande partie
des sites, sans scraper le DOM site par site.
- apps/api/src/sources/json-ld-recipe.ts : official: false (on lit du
HTML arbitraire, pas une API dédiée maintenue par l'éditeur), pas de
catalogue à parcourir (list() renvoie toujours vide) — fetchDetail()
prend directement une URL comme externalId, prête pour un futur
flux "importer depuis une URL".
- Extraction JSON-LD par regex (pas de nouvelle dépendance — un tag
<script> ne contient jamais de HTML imbriqué, donc pas besoin d'un
vrai parseur DOM), tolérante aux blocs multiples et aux JSON
malformés (ignorés plutôt que de faire échouer toute la page).
Gère les variantes réelles de schema.org : @type en tableau, @graph,
recipeInstructions en string/HowToStep[]/HowToSection imbriquées,
image en string/ImageObject/tableau, recipeYield en nombre/texte/
tableau.
Vérifié contre une vraie page (bbcgoodfood.com, HTML téléchargé +
fetch live) : nom, description, image, portions, 8 ingrédients et 2
étapes correctement extraits de bout en bout.
26 nouveaux tests (tous avec fetch stubbé, aucun appel réseau réel
dans la suite automatisée). 221 tests passent au total. Build et
lint propres.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ajoute la brique "Traduction en étapes" du pipeline d'import décrit
dans specs/batch-cooking-architecture.md — prend un ParsedRecipe
(sortie de parse() d'un adaptateur, recipe-source-adapter.ts) et
déclare, pour chaque étape, sa séquence de tech steps détectée.
- recipe-translation.ts : TranslatedRecipe/TranslatedRecipeStep
(ParsedRecipe/ParsedRecipeStep + techStepIds: number[], même forme
que Step.techSteps/StepTechStep). translateRecipeSteps() est pure
(prend les mappings en argument, comme matchTechSteps lui-même) ;
translateRecipe() est le wrapper qui charge le catalogue depuis la
DB pour une locale donnée — même séparation pur/DB que
tech-step-matcher.ts.
- Ne touche pas aux ingrédients (résolution vers Ingredient/Unit
toujours hors scope) ni ne produit une Recipe sauvegardable (pas de
dietIds/visibility/auteur) — une seule brique du pipeline, pas tout
le pipeline.
- Documente explicitement la limite actuelle : le catalogue de tech
steps n'a que des mappings "fr", donc une source anglophone comme
TheMealDB traduite avec cette locale obtient des séquences vides
sur toutes ses étapes (vérifié par un test dédié avec du texte
réel de TheMealDB).
8 nouveaux tests (partie pure + partie DB avec le vrai catalogue
"fr" seedé). 180 tests passent au total. Build et lint propres.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Marmiton s'est avéré inaccessible pour du scraping (bloqué même via
WebFetch, signe de protection anti-bot) — TheMealDB (themealdb.com)
est une vraie API JSON publique et gratuite, sans scraping, testée
en conditions réelles (list → fetchDetail → parse fonctionnent
bout en bout contre l'API live).
- Source.iconUrl (nullable) + RecipeSourceAdapter.iconUrl (requis,
même convention que `official`) synchronisé par syncRecipeSources.
- apps/api/src/sources/the-meal-db.ts : premier RecipeSourceAdapter
réel — official: true (API officielle, pas de scraping), utilise
fetch natif (aucune dépendance ajoutée). list() fait une recherche
(pas de vrai "browse" côté TheMealDB, mais une requête vide renvoie
un échantillon de secours) ; parse() éclate les instructions en
étapes par ligne et ignore les emplacements d'ingrédients vides.
- apps/api/src/sources/index.ts : registerAllRecipeSources(), appelé
par server.ts (process réel) et prisma/seed.ts — délibérément PAS
importé par app.ts, pour ne jamais dépendre de l'ordre des tests.
- SourceSelect (web) affiche désormais le logo de la source à côté
de son nom.
Vérifié en conditions réelles : seed → table sources peuplée avec le
vrai logo TheMealDB ; endpoint /reference/sources sur serveur réel ;
parcours navigateur complet (onboarding → étape sources visible avec
icône chargée → activation → paramètres foyer reflète le choix).
186 tests passent (16 nouveaux, dont le moteur TheMealDB testé avec
un stub de fetch — aucun appel réseau réel dans la suite automatisée).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GET /reference/sources n'était intercepté par aucun scénario Cypress
menant à la création/jonction d'un foyer — la requête réelle restait
en attente indéfiniment, laissant OnboardingSourcesPage bloqué sur
/onboarding/sources au lieu de s'auto-sauter vers /onboarding/allergenes.
- Ajout du Given "the sources reference list is empty" (même pattern
que les intercepts diets/allergies existants), câblé dans les deux
scénarios qui créent/rejoignent un foyer.
- Mise à jour de l'assertion "Étape 3 sur 3" → "Étape 4 sur 4" : un
foyer étant créé dans ce scénario, l'étape allergènes affiche
désormais le total dynamique (4 étapes) comme prévu.
- OnboardingSourcesPage.tsx : redirige aussi vers /onboarding/allergenes
en cas d'échec réseau sur getSources(), pas seulement quand la liste
est vide — le wizard ne doit pas bloquer l'utilisateur sur une étape
optionnelle à cause d'un problème transitoire.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Répond à deux besoins : permettre à chaque foyer de choisir quelles
sources apparaissent dans ses onglets de recettes, et distinguer les
sources à API officielle des sources scrapées.
- RecipeSourceAdapter.official (booléen, sans défaut — chaque
adaptateur doit le déclarer explicitement) synchronisé sur
Source.official par syncRecipeSources.
- HouseSource : table de jointure opt-in (House <-> Source) — aucune
ligne = source masquée. Un foyer nouvellement créé ne voit aucune
source tant qu'il ne les active pas explicitement.
- GET /reference/sources (catalogue des sources implémentées, avec le
flag officiel).
- GET/PATCH /house/current/sources (lecture/remplacement complet des
sources activées par le foyer courant).
- recipe.service.ts : sourceVisibilityWhere() filtre désormais TOUS
les onglets (perso/foyer/publique/favoris) — une recette sans
source reste toujours visible ; une recette importée ne l'est que
si sa source est activée pour le foyer du viewer. Un viewer sans
foyer ne voit aucune recette sourcée.
Côté web :
- Nouvelle étape /onboarding/sources dans le wizard d'inscription,
atteinte uniquement si un foyer vient d'être créé/rejoint (sinon on
saute direct aux allergènes) ; s'auto-saute aussi si aucune source
n'est encore implémentée (catalogue vide aujourd'hui).
- Nouvelle section « Sources de recettes » dans /parametres/foyer
(masquée dans les mêmes conditions), avec sauvegarde à la volée
(même pattern que les autres préférences hot-saved).
- SourceSelect (features/house/), grille de cases à cocher avec badge
officiel/non-officielle, sur le même principe qu'AllergySelect.
172 tests backend passent (dont 25 nouveaux). Build et lint propres.
Vérifié manuellement en navigateur : le parcours d'onboarding saute
bien l'étape sources (catalogue vide) et affiche « 4 sur 4 » quand un
foyer a été créé ; la section paramètres reste invisible tant
qu'aucune source n'existe.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Corrige le modèle de données suite à une review sur la PR #34 :
"Dans une poêle chaude, faire chauffer une noix de beurre" combine
deux techniques (preheat + melt), or Step.techStepId ne pouvait en
porter qu'une seule (FK simple nullable).
- Step.techStepId (FK simple) remplacé par StepTechStep, une table de
jointure ordonnée (stepId, techStepId, order) — @@id([stepId,
order]) garantit une séquence propre par étape.
- tech-step-matcher.ts : matchTechStep(...) → number|null devient
matchTechSteps(...) → number[]. Nouvel algorithme : chaque mapping
qui matche devient un candidat avec sa position dans le texte ; on
garde le meilleur candidat par technique (poids, puis position),
on résout les chevauchements entre techniques différentes par poids
décroissant (ex: "cuire au four" ne garde que `bake`, pas `cook` en
plus), puis on trie le résultat par ordre d'apparition dans le
texte — une séquence qui se lit dans le même ordre que l'instruction.
- Ajout de la technique "melt" (faire fondre) au catalogue, pour
pouvoir tester le cas concret du commentaire de review de bout en
bout (préchauffer + faire fondre).
- recipe.service.ts : câble StepTechStep via un create imbriqué à la
place du champ scalaire.
Tests étendus dans tech-step-matcher.test.ts (séquences non
chevauchantes, résolution de chevauchement combinée à une technique
distincte, etc.) et recipe.test.ts (nouveau test de bout en bout avec
deux techniques dans une même étape). 133 tests passent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Répond au besoin identifié précédemment : la table `sources` devient
un vrai catalogue des sources implémentées, et une recette importée
pourra être reliée à l'item source dont elle provient.
- Source.key (unique) — même convention que Diet.key/Unit.key/
TechStep.key. Le catalogue est désormais synchronisé depuis le
registre d'adaptateurs (recipe-source-registry.ts) via
syncRecipeSources() (nouveau apps/api/src/db/recipe-source-sync.ts),
plutôt que maintenu à la main comme DIETS/UNITS — reste vide tant
qu'aucun adaptateur concret n'est enregistré.
- Recipe.externalId (nullable) — l'identifiant de la recette côté
source. Contrainte @@unique([sourceId, externalId]) : empêche
d'importer deux fois la même recette (les recettes manuelles, aux
deux colonnes nulles, ne sont jamais en conflit entre elles).
- findImportedExternalIds(prisma, sourceKey, externalIds) — le
pendant DB de markAlreadyImported (recipe-source-adapter.ts),
ferme la boucle commencée dans la PR précédente pour distinguer les
recettes déjà intégrées lors du browse.
- syncRecipeSources() appelé après seedReferenceData() dans
prisma/seed.ts et test-support/reset-db.ts.
Toujours pas de route HTTP ni de champ sourceId/externalId exposé
dans createRecipeSchema — la sauvegarde effective d'une recette
importée reste pour une PR ultérieure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ajoute markAlreadyImported(items, importedExternalIds) et le type
BrowsableRecipeItem à recipe-source-adapter.ts : quand on parcourt le
catalogue d'une source (list()), on peut désormais annoter chaque
item pour savoir s'il correspond à une recette déjà intégrée dans
notre base ou non.
Reste une fonction pure, volontairement séparée de list() : un
adaptateur ne connaît que sa source, jamais notre base — même
séparation I/O/pur que tech-step-matcher.ts. La constitution du set
d'externalId déjà importés (où/comment on persiste ce lien) est
laissée à une future couche, pas encore décidée.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pose les bases du pipeline d'import décrit dans
specs/batch-cooking-architecture.md (Import depuis source → Traduction
en étapes → Sauvegarde), en commençant par le premier maillon :
récupérer et parser des recettes brutes depuis une source externe,
indépendamment du site/API concerné.
- RecipeSourceAdapter<TRawDetail> (recipe-source-adapter.ts) : contrat
générique par source — list() pour parcourir un catalogue de façon
paginée (l'utilisateur "browse" les recettes disponibles), puis
fetchDetail(externalId) une fois une recette sélectionnée, puis
parse(raw) pour la transformer en ParsedRecipe. parse() est pure et
synchrone (même séparation I/O vs pur que tech-step-matcher.ts), ce
qui la rend testable sans réseau.
- ParsedRecipe est volontairement distinct de CreateRecipeInput : les
ingrédients restent en texte libre (pas d'ingredientId/unitId) — la
résolution vers nos catalogues Ingredient/Unit est un sujet séparé,
pas encore construit.
- recipe-source-registry.ts : registre en mémoire des adaptateurs,
identifiés par une clé stable (même convention que Diet.key/
Unit.key/TechStep.key), distinct de la table Source (schema.prisma)
qui documente la provenance d'une recette déjà sauvegardée.
- recipe-source-errors.ts : RecipeSourceFetchError/RecipeSourceParseError,
vocabulaire d'erreur dédié en attendant qu'une route les traduise en
HttpError/ErrorCode.
Pas de route HTTP, pas d'écriture en base, pas d'implémentation
concrète pour l'instant — uniquement le module générique, validé par
un adaptateur factice dans les tests. Le câblage (endpoint, sourceId,
un vrai parseur) sera une PR suivante.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Étend les tests ajoutés pour le catalogue de techniques culinaires :
- tech-step-matcher.test.ts : normalizeText (cédille/ligature, texte
déjà normalisé, chaîne vide), matchTechStep (listes/description
vides, plusieurs mappings pointant vers le même techStep, respect
des frontières de mot — évite les faux positifs type "recuire"/
"précuit"), et loadTechStepMappingRules (filtrage par locale contre
une vraie base, locale sans mapping).
- reference.test.ts : ordre alphabétique par key, idempotence du
reseed (pas de doublon en rappelant seedReferenceData sans
truncate).
- recipe.test.ts : détection indépendante par étape sur une recette à
plusieurs étapes (ordre préservé, y compris une étape sans match),
et résolution de bout en bout du mapping le plus spécifique quand
une description matche plusieurs techniques.
129 tests passent (13 nouveaux).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rend opérationnel le squelette TechStep/TechStepMapping/Step.techStepId
présent dans le schéma depuis le premier commit mais jamais implémenté :
- TechStep gagne un `key` unique (camelCase, même convention que
Diet/Unit) ; TechStepMapping gagne un `locale` pour pouvoir porter
plusieurs jeux de règles de matching par langue.
- Catalogue statique de 25 techniques françaises courantes (Cuire,
Frire, Déglacer, Mijoter, ...), chacune associée à une ou plusieurs
expressions régulières + un poids, seedées de façon idempotente dans
reference-seed-data.ts.
- Nouveau moteur de matching (apps/api/src/lib/tech-step-matcher.ts) :
normalisation accents/casse (NFD) puis test des expressions,
résolution du meilleur match par poids. Pur et testé unitairement.
- Câblé dans recipe.service.ts : à la création/modification d'une
recette, chaque étape voit son techStepId calculé automatiquement à
partir de sa description (locale "fr" en dur pour l'instant, faute
de préférence de langue utilisateur dans l'app).
- Reste backend-only : StepView n'expose pas encore techStepId,
conformément au commentaire existant.
- Endpoint GET /reference/tech-steps + TechStepView, en cohérence avec
les autres catalogues de référence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.ingredient-row__unit était encore dimensionné pour l'ancien champ
texte libre (6rem, suffisant pour "g"/"unité") — trop étroit
maintenant que c'est un <select> sur le catalogue, dont le plus long
label ("cuillère à soupe") était tronqué.
Remplace l'unité texte libre de RecipeIngredient (max 20 caractères,
"g"/"grammes"/"G"... jamais fiable à additionner) par une référence
vers un nouveau catalogue Unit (id/key/type/toBaseFactor), même
traitement que Diet/Allergy/Ingredient : GET /reference/units, seedé
par reference-seed-data.ts (14 unités : gram/kilogram/milliliter/
centiliter/liter/tablespoon/teaspoon/piece/pinch/slice/clove/bunch/
sachet/sprig), sélectionnable uniquement via un <select> dans le
formulaire recette (plus de saisie libre).
`toBaseFactor` (combien d'unités de base — gramme pour MASS,
millilitre pour VOLUME — vaut une unité) pose les bases d'une future
fonctionnalité de conversion (ex. liste de courses additionnant
"500g" + "0.5kg") sans construire cette fonctionnalité elle-même —
les unités COUNT restent à toBaseFactor=1, non convertibles entre
elles (une "pincée" n'est pas une fraction fixe d'une "gousse").
Migration : recipe_ingredient.unit → unit_id (FK), breaking change
sans backfill assumé (pas de recette réelle en prod actuellement,
voir commentaire de migration) — mêmes garde-fous service-side que
ingredientId (404 UNIT_NOT_FOUND) et mêmes tests de couverture.
layout.cy.ts still asserted .settings-page was a centered column
capped at 896px (max-width: 56rem) — that cap was removed in
4647a82 (settings pages were cramped on wide viewports) without
updating this regression test, breaking CI on main.
Moves the settings-page assertion into the full-bleed group
(assertFillsContentWidth, same as .planning-page/.recipes-page)
instead of the centered-column one, matching current behavior.
Ajoute Recipe.portions (combien de portions la recette produit telle
qu'écrite) — formulaire de création/édition, fiche détail, migration
Prisma (backfill à 4, même pattern que planning_item.portions).
Le sélecteur de recette du planning pré-remplit désormais son propre
champ "portions" depuis cette valeur au lieu de toujours démarrer à 1
(RecipeSummaryView.portions), tout en gardant PlanningItem.portions
indépendant (une recette peut être mise à l'échelle pour un créneau).
Couverture : tests API (création/édition/validation), scénarios
cypress (formulaire + préchargement en édition).
.settings-page was capped at 56rem, leaving Compte, Préférences
alimentaires (foyer), and Préférences utilisateur cramped in a narrow
centered column on wide viewports. Drop the max-width/margin so the
content fills .app-content's full width like other pages.
Le catalogue de référence (diets/allergènes/ingrédients) était écrit en
français dans reference-seed-data.ts, avec une table de correspondance
séparée (catalog-en-keys.ts, 666 lignes, ~563 entrées) traduisant chaque
libellé français vers une clé anglaise snake_case, elle-même utilisée pour
peupler la colonne `key` en base et régénérer translation.json. Décision :
remplacer par un authoring 100% anglais camelCase directement dans le seed
— plus de détour, plus de table de correspondance.
- `Ingredient.name`/`allergenNames`/`dietNames` → `uid`/`allergenUids`/
`dietUids`, valeurs en camelCase directement (ex: "Tomate" → "tomato",
"Fruits à coque" → "treeNuts").
- `IngredientCategory`/`IngredientSubcategory` (enums Prisma) renommés du
français SCREAMING_SNAKE_CASE (`PRODUITS_FRAIS`, `LEGUMES`...) vers
l'anglais camelCase (`freshProduce`, `vegetables`...) — même mécanique
de migration que le renommage d'enum précédent
(20260818113250_ingredient_taxonomy_rework) : nouvelle colonne avec
valeur par défaut sûre, jamais de cast direct (aucune valeur commune
entre ancien et nouvel enum), seedReferenceData() corrige chaque ligne
au démarrage suivant.
- Migration `20260819180000_catalog_camel_case_uids` : renomme les clés
existantes (diet/category/ingredients, même mécanique que
20260818193000_catalog_keys_to_english) + swap des deux enums. Un cas
particulier corrigé à la main : "sesame_seeds" était à la fois la clé
d'un allergène (Category) et d'un ingrédient qui se référence lui-même
("Graines de sésame") — les deux tables ont besoin de leur propre
UPDATE.
- `catalog-en-keys.ts`, `slugify.ts`, `generate-catalog-i18n.ts`,
`validate-catalog-en-keys.ts` — supprimés (plus de raison d'être).
Conséquence assumée : `translation.json` n'est plus régénéré
automatiquement, c'est désormais la seule source du texte FR, tenue à
jour à la main en parallèle du uid (même clé qui les relie).
- `packages/shared/src/types/reference.ts`, `apps/web`'s
`ingredient-icons.tsx` (CATEGORY_ICON/SUBCATEGORY_ICON), fixtures
Cypress codées en dur — mis à jour avec les nouveaux noms.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolves the conflict from PR #28 (Ciqual ingredient expansion) merging
first: both branches touched reference-seed-data.ts's SAUCES group.
Also flags the three newly-merged sauces (Aïoli, Sauce vinaigrette,
Houmous) and Gressin as reproducible — these were in the original
curation plan but skipped earlier since they didn't exist on this
branch's base yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Remplace la FK morte `Ingredient.alternateRecipeId` (jamais branchée nulle
part — confirmé par exploration : zéro usage en dehors de schema.prisma)
par un flag booléen `reproducible`, plus simple : pas de liaison
recette↔ingrédient en base, juste une info "ça vaut le coup d'être fait
maison" plus un raccourci de recherche.
- Migration : drop `alternate_recipe` (colonne + FK), ajoute
`reproducible BOOLEAN NOT NULL DEFAULT false` sur `ingredients`.
- `reference-seed-data.ts` : `IngredientSeed` gagne `reproducible?`,
threadé dans le flatten + la réconciliation `seedReferenceData`. Premier
lot de 27 ingrédients marqués (pains, pâtes à cuire, sauces de base,
bouillons/fonds) — même logique que la curation Ciqual : un lot solide
plutôt qu'exhaustif sur les 546 ingrédients.
- `IngredientView` (shared) + les deux endroits qui la construisent
(`reference.service.ts`, `recipe.service.ts`) gagnent `reproducible`.
- `ReproducibleBadge` (nouveau) : pastille "Faisable maison" — simple
dans `IngredientPicker` (avec son propre toggle d'affichage), lien
cliquable dans `IngredientRow` vers `/recettes?search=<nom>` ouvert
dans un nouvel onglet (pour ne jamais perdre le formulaire de recette
en cours — pas de persistance de brouillon dans `RecipeFormPage`).
- `RecipesPage` lit `?search=` au montage pour permettre ce deep-link.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Source : « Table des aliments moyens Ciqual 2025 » (ANSES, ciqual.anses.fr),
utilisée comme source de noms pour combler des trous du catalogue plutôt
qu'importée telle quelle — le fichier est une table de correspondance
"aliment moyen ↔ aliments contributeurs" très granulaire (états de cuisson,
produits de marque, boissons/snacks industriels), pas directement
réutilisable comme liste d'ingrédients génériques pour une app de recettes.
55 nouveaux ingrédients ajoutés à reference-seed-data.ts, catégorisés et
allergène-tagués selon les conventions existantes :
- 26 fromages régionaux français (Brie, Camembert, Roquefort, Munster,
Reblochon, Cantal, Beaufort, Saint-Nectaire, Cancoillotte, Tomme,
Époisses, etc.) — allergène Lait.
- 18 charcuteries/abats/gibiers (Andouille, Boudin blanc/noir, Rillettes,
Foie de veau, Ris de veau, Tripes, Cerf, Chevreuil, Sanglier…).
- 4 volailles (Caille, Pintade, Oie, Foie de volaille).
- 2 poissons — allergène Poissons (Anguille, Carrelet).
- 2 crustacés/mollusques — allergène Crustacés/Mollusques (Araignée de
mer, Bigorneau).
- 3 fruits/légumes (Cardon, Cassis, Canneberge).
Chaque ajout a sa clé anglaise dans catalog-en-keys.ts ; translation.json
régénéré via `tsx scripts/generate-catalog-i18n.ts` (pas d'édition
manuelle). Pas de migration Prisma nécessaire — pure donnée de référence,
upsertée par `seedReferenceData` au prochain déploiement/seed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Dialog.tsx : remplace le div role="dialog" par un <dialog> natif
(showModal) — corrige lint/a11y/useSemanticElements, récupère
gratuitement le piège de focus et l'Échap natifs. Le clic extérieur
est rebranché en imperative addEventListener pour éviter
lint/a11y/useKeyWithClickEvents sur un élément non interactif.
- RecipePickerDialog.tsx : retire l'autoFocus (lint/a11y/noAutofocus),
ordre des imports/formatage corrigés par `biome check --write`.
- apps/api/test/planning.test.ts, apps/api/test/recipe.test.ts :
les fixtures qui créent un `PlanningItem` directement via Prisma
n'avaient pas le nouveau champ `portions` requis.
- apps/web/cypress/e2e/planning-page.cy.ts : ajoute `portions` aux
items mockés pour rester fidèle au contrat `PlanningItemView`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ajoute le chaînon manquant entre le catalogue de recettes et le planning
hebdomadaire :
- Backend : `PlanningItem.portions` (nouvelle colonne + migration),
`POST /planning/items` / `DELETE /planning/items/:id` (créent la
semaine de planning à la volée si besoin), `GET /recipes` gagne les
filtres `ingredientIds`/`dietIds` (ET) en plus de `suitableForHousehold`
(déjà préparé).
- Frontend : nouveau `Dialog` générique (premier modal de l'app),
`RecipePickerDialog` qui réutilise le même affichage que le catalogue
(`RecipeTabs`/`RecipeTable`) avec recherche par nom, filtre ingrédients,
filtre régime alimentaire, toggle "convient à tout le foyer", puis une
étape de saisie du nombre de portions.
- `PlanningPage` : le bouton "+" de chaque case ouvre le dialog, le
bouton "✕" retire la recette (optimiste, avec rollback si l'appel
échoue), les portions s'affichent sur chaque chip.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore: point de départ pour le refactor des tests Cypress
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.
* feat(web): infra Cypress Component Testing, premier test sur CheckboxOption
É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.
* ci: exécute les tests de composants Cypress
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)
* test(web): refactor user journeys into Cucumber scenarios
Convertit les parcours utilisateur (goal-driven, "en tant que X je peux
Y") en scénarios Gherkin, en réutilisant l'infra Cucumber déjà validée
par le smoke test (PR #24). Retire le smoke test jetable maintenant
superflu.
8 fichiers .feature ajoutés, chacun avec son fichier de step definitions
au même basename (convention de découverte du préprocesseur — voir
login-smoke.ts) :
- auth.feature : inscription (succès, erreur validation, email déjà
pris), connexion (succès, identifiants invalides), déconnexion
- onboarding.feature : les 3 scénarios déjà couverts (wizard complet,
étapes sautées, rejoindre un foyer pendant l'onboarding) — dépend de
household-settings.ts et preferences.ts pour ses steps de
création/rejoint de foyer et de sélection de régime/allergies
- household-settings.feature : créer un foyer, rejoindre par code
d'invitation, renommer (autosave), retirer un membre, supprimer le
foyer, quitter le foyer
- account.feature : suppression de compte (mauvais mot de passe,
succès, annulation)
- recipe-form.feature : les 4 scénarios déjà couverts inchangés (ajout
d'ingrédient + création, régression crypto.randomUUID, exclusion/
réinclusion d'ingrédient, préchargement + édition d'une recette
existante)
- recipes.feature : bascule favori, suppression d'une recette
- preferences.feature : autosave du régime, autosave des allergies
- user-preferences.feature : changement de thème (autosave)
En contrepartie, les anciens .cy.ts perdent uniquement les it() migrés
vers Gherkin — les scénarios de layout/affichage pur (catalogue de
recettes, tabs, recherche, panneau de détail, sidebar, planning grid,
etc.) restent en Cypress classique, conformément au découpage
"parcours utilisateur (Cucumber) vs layout (Cypress pur)" déjà en
place pour les component tests. auth.cy.ts, onboarding.cy.ts et
recipe-form.cy.ts sont supprimés : 100% de leur contenu a migré.
Les commentaires "voir auth.cy.ts pour la justification" désormais
obsolètes (fichier supprimé) sont remplacés par une explication
autonome du mock cy.intercept.
Vérifié statiquement : les 246 steps Gherkin des 8 .feature résolvent
chacun vers exactement une définition (0 non résolu, 0 ambigu) et
`pnpm exec biome check` est propre sur tout cypress/. Reste à confirmer
en CI que les scénarios passent réellement (pas seulement qu'ils se
résolvent).
* fix(web): fix cross-feature step discovery and slash-alternation bug
La CI de la refonte précédente (commit aaace15) a échoué : la
découverte par défaut du préprocesseur ne charge, pour un fichier
`foo.feature`, QUE `foo.ts` (co-localisé, même basename) et
`cypress/support/step_definitions/**` — pas les autres `.ts` du
dossier `cypress/e2e/`. onboarding.feature référençait donc des steps
qui ne vivaient que dans preferences.ts, household-settings.ts et
auth.ts, introuvables lors de son propre run.
Déplace les steps réellement partagés entre plusieurs .feature vers
cypress/support/step_definitions/ (chargé pour toutes les features) :
- reference-data.steps.ts : mocks des listes de référence régimes/
allergies (options ou vides) — partagé entre auth.feature et
onboarding.feature
- household-mutations.steps.ts : création/adhésion à un foyer et leurs
assertions — partagé entre household-settings.feature et
onboarding.feature
- profile-mutations.steps.ts : sélection du régime, mise à jour des
allergies et leur assertion — partagé entre preferences.feature et
onboarding.feature
Les définitions d'origine sont retirées de auth.ts/household-
settings.ts/onboarding.ts/preferences.ts pour éviter un step
"Ambiguous" (chargé deux fois pour la feature qui les définissait déjà
elle-même).
Corrige aussi un second bug distinct révélé par la même CI :
"the ingredient/diet catalog is available" (recipe-form.feature)
contient un "/" non échappé — en syntaxe Cucumber Expression, "/" hors
d'un paramètre {..} signifie une alternative de texte ("ingredient" OU
"diet catalog is available"), jamais le caractère littéral. Le texte
du .feature ne pouvait donc jamais matcher. Renommé sans "/" :
"the ingredient and diet catalog is available".
Le script de vérification statique utilisé pour valider aaace15 avant
push donnait une fausse confiance : il regroupait tous les steps de
tous les fichiers comme disponibles globalement pour chaque feature,
sans respecter ce scoping réel. Réécrit pour ne charger, par feature,
que son fichier co-localisé + step_definitions/ — et pour détecter les
patterns contenant un "/" non échappé. Résultat : toujours 246 steps,
0 non résolu, 0 ambigu, 0 pattern à slash non échappé, cette fois avec
un modèle de résolution fidèle au comportement réel du préprocesseur.
* test(web): cover every CheckboxOption/RadioOption behavior
Complète les component tests des deux seuls composants UI génériques
committés (Dialog.tsx est un WIP non commité d'une autre fonctionnalité
en cours — hors scope ici) pour couvrir tout leur comportement, pas
seulement le cas heureux.
CheckboxOption — 4 tests existants (rendu, checked/is-selected, onChange
au clic depuis unchecked, contrôlé) complétés par :
- onChange(false) au clic depuis l'état checked (symétrique du test
existant, qui ne couvrait que checked=false → true)
- fusion du className de l'appelant avec is-selected, dans les deux
sens (juste className, className+is-selected)
- class="" (chaîne vide, pas "false"/"null") quand aucun className
n'est passé et que checked=false — pin le comportement exact du
`.filter(Boolean).join(" ")`
- le clic sur le texte du label (pas seulement l'input) déclenche aussi
onChange — comportement natif du HTML dont la "carte sélectionnable"
de global.scss dépend entièrement
- le span .check-mark est aria-hidden
RadioOption — aucun test avant ce commit. Ajouté en couvrant en plus
ce qui distingue vraiment un radio d'un checkbox :
- name/value posés sur l'input natif
- onChange(value) au clic depuis unchecked
- AUCUN onChange au clic sur un radio déjà checked (contrairement à un
checkbox, un radio natif ne réémet pas `change` si l'état ne change
pas réellement)
- clic sur le label, className/is-selected, aria-hidden — mêmes
scénarios que CheckboxOption
- comportement de groupe mutuellement exclusif : 3 RadioOption
partageant `name="theme"` (mirroring UserPreferencesPage), un seul
sélectionné à la fois, y compris via `input:checked` natif du
navigateur
Non exécutable en local (limitation GPU/sandbox Electron documentée
dans le README, pré-existante) — à vérifier en CI.
* test(web): add layout/style regression suite for the app shell
Troisième catégorie du découpage des tests (parcours via Cucumber,
composants génériques via Component Testing, et maintenant layout pur
— indépendant de tout parcours utilisateur). Cypress classique, pas de
Gherkin : ce fichier teste la structure/l'apparence du shell
(AppLayout) lui-même, pas le contenu d'une page donnée.
Couvre spécifiquement les 4 axes demandés :
- Positionnement : la sidebar garde une largeur fixe (240px déplié,
68px replié) plaquée au coin haut-gauche, sur n'importe quelle page.
- Scroll : régression directe pour #21 — `.app-layout` reste borné
exactement à la hauteur du viewport (overflow: hidden), et une page
plus haute que le viewport scrolle uniquement dans `.app-content`
(via un spacer synthétique de 3000px injecté après le mount, pour
rester indépendant du contenu réel d'une page donnée) sans jamais
déplacer la sidebar ni scroller le document lui-même.
- Largeur des pages : autre régression directe pour #21 — le planning
et le catalogue de recettes remplissent toute la largeur disponible
de `.app-content`, tandis que la page "Liste de courses" et les
pages de paramètres restent centrées avec un espace égal de chaque
côté (le bug original : collées à gauche avec un grand vide à
droite).
- Breakpoint responsive (< 640px) : la sidebar bascule en barre
horizontale pleine largeur, masque le bouton collapse/la version,
et garde chaque lien de nav pleinement lisible (icône + label, avec
scroll horizontal) plutôt que de les écraser en pastilles de ~16px
sans texte — un mode de régression explicitement documenté en
commentaire dans AppLayout.scss mais jusqu'ici non testé.
- Thème de couleur : va au-delà de l'attribut `data-theme` déjà
couvert par user-preferences.cy.ts — vérifie les vraies valeurs de
couleur calculées (`getComputedStyle`) sur la sidebar, le lien de
nav actif et le fond de page, en clair et en sombre, confirmant que
la cascade CSS des tokens (_theme.scss) atteint réellement le rendu,
pas seulement que le JS pose le bon attribut.
Non exécutable en local (limitation GPU/sandbox Electron documentée
dans le README, pré-existante) — à vérifier en CI.
* 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>