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>
- 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"
- 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)
Retour de CI (job e2e, PR #10) : "creates a profile and lands on the
home page" échouait avec une exception non gérée ("Failed to fetch")
depuis OnboardingHouseholdPage's GET /house/current — jamais mocké
dans ce test.
Cause : ce test n'avait jamais été mis à jour quand le signup a commencé
à rediriger vers /onboarding/foyer au lieu de "/" directement (fait dans
le commit du wizard d'inscription, plus tôt sur cette PR) — seul le
texte "Bonjour Alice Martin" -> "Bonjour Alice" avait été corrigé à
l'époque, pas la destination réelle de la redirection. Invisible
localement : je n'ai pas pu faire tourner Cypress dans ce sandbox
(crash GPU headless, confirmé pré-existant), donc ce test n'avait
jamais réellement été exécuté après ce changement — seulement
typechecké.
Fix : le test vérifie maintenant qu'il atterrit sur /onboarding/foyer
(pas la home) avec le nom du foyer préaffiché, et mocke GET
/house/current comme le fait déjà onboarding.cy.ts pour ce même écran.
Vérifié via tsc --noEmit (Cypress toujours injouable localement ici) —
CI à re-checker après push.
Retour fonctionnel : allergies et intolérances doivent être distinguées
dans l'UI, et /foyer doit sauvegarder à la volée plutôt que via des
boutons "Enregistrer".
- AllergySelect prend un `legend` en prop au lieu d'un libellé interne
fixe — le même composant est rendu deux fois par chaque page
consommatrice (HouseholdPage, OnboardingAllergensPage), une fois par
`kind` (ALLERGY / INTOLERANCE), la sélection restant une seule liste
d'IDs partagée.
- HouseholdPage : suppression des boutons "Enregistrer", autosave
déclenché depuis le handler onChange de chaque champ (jamais un
useEffect générique sur la valeur — se déclencherait aussi au
chargement initial, sans distinction propre "chargé" vs "modifié").
Nom du foyer et allergènes/intolérances debouncés (600ms/500ms),
régime sauvegardé immédiatement (sélection discrète). Validation
client (nom vide) empêche l'autosave plutôt que de déclencher un
aller-retour API voué à l'échec.
- i18n : household.form.allergiesLabel devient "Allergies" (au lieu de
"Allergies & intolérances"), nouvelle clé intolerancesLabel, save/
saved remplacés par saving/saved (plus de bouton à libeller).
- Cypress (household.cy.ts réécrit, onboarding.cy.ts mis à jour) +
specs/frontend-architecture.md + README.md.
Vérifié dans le navigateur : wizard d'inscription affiche bien les
deux groupes (12 allergies / 2 intolérances) ; /foyer sans aucun
bouton, chaque section sauvegarde automatiquement (vérifié en base
après édition du nom du foyer et du régime) ; compte de test nettoyé.
Clôt le retour fonctionnel sur la feature profil/foyer/régime/
allergènes (8 commits au total sur cette PR).
Retour fonctionnel : les allergies et intolérances doivent être
distinguées, pas listées ensemble.
- schema.prisma: enum AllergenKind (ALLERGY|INTOLERANCE) + Category.kind
(@default(ALLERGY), migration écrite à la main comme précédemment —
`migrate dev` refuse en environnement non-interactif ici — SQL généré
via `prisma migrate diff`).
- reference-seed-data.ts: classification par substance (Gluten et
Sulfites = INTOLERANCE, les 12 autres = ALLERGY — réaction
non-immunitaire documentée vs réaction immunitaire classique).
Corrige au passage l'upsert : `update: { kind }` au lieu de `update:
{}` — un reseed doit pouvoir corriger `kind` sur une Category déjà
existante, pas juste no-op.
- reference.service.ts / packages/shared: AllergyView gagne `kind`.
PATCH /profile/allergies ne change pas (une seule liste d'IDs, kind
ne sert qu'au groupement d'affichage côté client).
- Tests Mocha (29 passing) + Cucumber (15 scenarios, inchangés).
Classifié par substance (pas par utilisateur) — documenté comme
limitation connue dans le README. Web (split UI + hot saving sur
/foyer) dans le commit suivant.
- apps/web/cypress/e2e/onboarding.cy.ts — parcours complet (rempli et
entièrement skippé) signup → 3 étapes → home, mêmes conventions
cy.intercept que le reste.
- apps/web/cypress/e2e/household.cy.ts — /foyer : préremplissage, et
sauvegarde indépendante de chacune des 3 sections.
- specs/frontend-architecture.md : nouvelle section "Parcours profil —
foyer, régime, allergènes" (diagramme mermaid, les deux bugs de state
trouvés en testant dans le navigateur), arborescence et namespaces
i18n à jour.
- README.md : nouvelle section "Parcours profil — foyer, régime,
allergènes", section sidebar mise à jour (Foyer & profil n'est plus
un stub).
Cypress lui-même ne peut pas tourner en local dans ce sandbox (voir la
note existante dans le README) — vérifié via `tsc --noEmit` sur les
specs + parcours manuel complet dans le navigateur (les deux à travers
les 5 commits précédents de cette feature).
Clôt la feature profil/foyer/régime/allergènes (6 commits, cette PR) :
seed+référence -> endpoints foyer/profil -> composants partagés ->
wizard d'inscription -> page /foyer -> ce commit.
- HouseholdPage remplace le stub ComingSoonPage : 3 sections
indépendamment sauvegardées (nom du foyer, régime, allergènes/
intolérances), mêmes composants partagés que le wizard d'inscription.
Chaque section a son propre bouton "Enregistrer" (3 ressources API
distinctes, pas de raison qu'une modification attende les autres).
- AuthContext : ajout de refreshUser() — re-fetch GET /auth/me et met
à jour `user`.
- Bug trouvé et corrigé en testant l'aller-retour SPA dans le
navigateur (sidebar → Recettes → Foyer, sans rechargement complet) :
la valeur du régime revenait à l'ancienne après sauvegarde. Cause :
la page initialisait dietId depuis useAuth().user.dietId, un instantané
jamais rafraîchi après une modification faite directement via
apiClient (qui ne touche pas AuthContext). Fix : la page fetch son
propre profil frais (apiClient.me()) au montage plutôt que de
dépendre du contexte, et appelle refreshUser() après une sauvegarde
réussie du régime pour que le reste de l'app reste cohérent aussi.
- household.comingSoon (clé i18n) supprimée, plus utilisée.
Vérifié dans le navigateur : préremplissage, sauvegarde par section,
persistance après rechargement ET après navigation SPA aller-retour ;
nettoyage du compte de test.
- SignupPage: après signup(), navigate("/onboarding/foyer") au lieu de
"/" — la home reste inchangée, seule la destination change.
- pages/onboarding/: 3 routes top-level RequireAuth-gated (PAS nichées
sous AppLayout — wizard plein écran sans sidebar, même langage visuel
que /login|/signup) :
- /onboarding/foyer — HouseNameField, préremplie avec le nom
auto-généré du foyer (continuer sans éditer = skip implicite).
- /onboarding/regime — DietSelect, valeur initiale depuis
useAuth().user.dietId (pas de fetch supplémentaire nécessaire).
- /onboarding/allergenes — AllergySelect, termine sur navigate("/").
- Bug trouvé et corrigé en testant dans le navigateur : RedirectIfAuthenticated
redirigeait vers "/" en course avec le navigate() explicite de
SignupPage — `user` devient non-null (via signup()) pendant que
SignupPage est encore monté sous ce guard, qui réagit et redirige
avant que le navigate("/onboarding/foyer") ne prenne effet. Latent
depuis le début (invisible avant car l'ancien SignupPage naviguait
aussi vers "/", donc les deux redirections concordaient). Fix : la
décision de redirection est verrouillée une seule fois, au moment où
la vérification initiale (`isLoading`) se termine, plutôt que
réévaluée à chaque changement de `user`.
Vérifié de bout en bout dans le navigateur (inscription → 3 étapes →
home), données confirmées en base (foyer renommé, régime + 2 allergènes
enregistrés), puis nettoyage des comptes de test.
- ApiClient: getDiets/getAllergies (référence), getCurrentHouse/
renameHouse, updateDiet, getAllergyIds/updateAllergyIds.
- features/profile/: HouseNameField, DietSelect (toujours une option
"aucun régime" -> null, étape skippable), AllergySelect (checkboxes
en grille + fieldset/legend, pas un <select multiple> — plus
tapable/accessible, notamment sur mobile). Tous "dumb"/contrôlés :
reçoivent leurs données (diets/allergies) en props plutôt que de les
fetcher eux-mêmes — le fetch/état de chargement reste à la page
appelante.
- profile-forms.scss partagé par les trois (même split que
features/auth/auth-form.scss vs Login/SignupPage : styles de champs
ici, layout de page dans chaque page consommatrice).
Pas encore utilisés (aucune page ne les importe) — le wizard
d'inscription (étape suivante) et la page /foyer les cablent.
- GET/PATCH /house/current — renomme le foyer de l'utilisateur connecté.
PATCH avec houseId null -> 404 HOUSE_NOT_FOUND.
- PATCH /profile/diet { dietId: number | null } — régime du profil ;
null l'efface (étape skippable du parcours). dietId invalide ->
404 DIET_NOT_FOUND.
- GET/PATCH /profile/allergies — allergènes/intolérances, liste d'IDs ;
PATCH remplace l'ensemble complet (pas une fusion, cohérent avec un
multi-select). ID invalide -> 404 ALLERGY_NOT_FOUND.
- 3 nouveaux ErrorCode (4041-4043) + libellés fr.
- Extraction de toSafeProfile() dans src/lib/safe-profile.ts —
auparavant dupliqué dans auth.service.ts et require-auth.ts,
profile.service.ts le réutilise aussi.
- Tests Mocha (28 passing) + Cucumber (15 scenarios) — même convention
que le reste, doc README.
Deuxième commit de la feature profil/foyer/régime/allergènes —
composants front partagés dans le commit suivant.
- schema.prisma: Diet.name/Category.name deviennent @unique (pas dans le
doc spec d'origine — ajouté pour que le seed soit idempotent par
upsert). Migration écrite à la main + appliquée via `migrate deploy`
(`migrate dev` refuse en environnement non-interactif ici) — SQL
généré via `prisma migrate diff` pour matcher exactement les
conventions Prisma.
- src/db/reference-seed-data.ts: seedReferenceData() — 5 régimes, 14
allergènes (règlement UE 1169/2011 annexe II). Chaque allergène = une
Category (upsert par nom) + une unique Allergy sous cette catégorie
(Allergy elle-même ne porte pas de nom, voir schema.prisma).
Réutilisée par prisma/seed.ts (CLI, `prisma db seed`) ET
test-support/reset-db.ts (chaque test repart avec ces données de
référence, pas des tables vides).
- modules/reference/: GET /reference/diets, GET /reference/allergies —
publics (pas de requireAuth), lisibles avant qu'un compte existe
(wizard d'inscription).
- packages/shared: DietView, AllergyView (name résolu côté serveur
depuis Category, le split Allergy/Category reste invisible du client).
- Tests Mocha + Cucumber, doc README.
Premier commit de la feature profil/foyer/régime/allergènes (planifiée
en chat) — endpoints foyer/profil dans le commit suivant.
- New apps/web/cypress/e2e/home-planning.cy.ts (mocked API, same
cy.intercept convention as auth.cy.ts):
- sidebar nav between sections + active-link highlighting
- user name + logout from the sidebar footer
- home planning: empty / loaded (table rows) / error states
- specs/frontend-architecture.md: documents AppLayout (single
RequireAuth+AppLayout parent route, nested routes via <Outlet />),
the ComingSoonPage stub pattern, updated folder tree and i18n
namespace list, updated routing diagram.
- README.md: new "Accueil, sidebar & sections" section; notes the
Cypress-can't-run-headless-here environment limitation (confirmed
pre-existing on main) and the docker-vs-local-dev CORS_ORIGIN gotcha
hit while manually verifying this feature.
Closes out the home-page-after-login feature (5 commits, this PR):
GET /planning/current -> AppLayout -> routing/stub pages -> HomePage
planning view -> this commit.
- ApiClient.getCurrentPlanning() — GET /planning/current.
- HomePage.tsx: replaces the old greeting card (now redundant with
AppLayout's sidebar) with the household's current planning — loading /
error / empty ("aucun planning pour cette semaine") / loaded (table of
weekDay/meal/recipe) states, modeled as a discriminated union so an
impossible combination (e.g. loading with data) can't be represented.
No invented weekday/meal grid — the API's `weekDay`/`meal` are free-form
strings (no enum exists yet in the schema), so this renders the items
as returned rather than assuming a specific vocabulary.
- locales/fr/translation.json: home.* replaced (title/loading/error/
empty/table.*), old greeting/logout keys removed (superseded by
layout.greeting/layout.logout from the AppLayout commit).
- cypress/e2e/auth.cy.ts: updated the now-stale "Bonjour Alice Martin"
assertions (greeting moved to the sidebar, first name only) and added
GET /planning/current intercepts so these specs don't depend on a real
backend. Manually verified end-to-end against a real API in the browser
preview (empty state + a seeded planning) — Cypress itself can't run
headless Chromium in this sandboxed dev environment (confirmed
pre-existing on main, unrelated to this change); CI runs the real
suite.
Verified with `pnpm --filter web build` after each of steps 2-4 to keep
every commit in this sequence independently buildable.
- App.tsx: every authenticated route now nests under one
RequireAuth + AppLayout parent route (react-router nested
routes/<Outlet />) instead of each page wrapping its own guard.
- New routes/pages: /recettes, /liste-de-courses, /foyer, each a thin
wrapper around a shared ComingSoonPage ("cette section arrive
bientôt.") — no backend behind them yet, matches the "pages stub
dédiées" choice discussed in chat.
- locales/fr/translation.json: nav labels + stub copy.
HomePage still shows its own greeting/logout card here (unchanged,
now nested inside AppLayout's <Outlet /> — briefly duplicating the
sidebar's greeting/logout) — replaced by the actual planning view in
the next commit.
Adds the shell for authenticated pages: a fixed-width sidebar with
brand, section nav (Planning/Recettes/Liste de courses/Foyer & profil),
and the signed-in user's name + logout at the bottom, plus a main
content area rendering the matched child route via react-router's
<Outlet />. Follows the "Mise en Place" theme tokens; collapses to a
top bar under 640px (this app is meant to be embedded via Capacitor
later, see the root README).
Not wired into App.tsx yet (next commit) — self-contained and builds/
typechecks on its own.
- packages/shared: PlanningView/PlanningItemView, exported.
- apps/api: planning module (service + route), mounted at /planning.
GET /planning/current returns the authenticated user's household's
planning covering today, or null (no error) when there isn't one yet —
the expected state until planning creation exists.
- Tests: Mocha (apps/api/test/planning.test.ts) + Cucumber
(features/planning.feature), same conventions as auth.
- packages/express-tools: fixed AsyncRequestHandler/wrapAsyncHandler's
Locals generic constraint (Record<string, unknown> -> Record<string,
any>, matching Express's own Response<ResBody, LocalsObj>) — the first
endpoint combining requireAuth/AuthLocals with an async handler exposed
that the stricter constraint rejected plain interfaces Response itself
accepts fine.
- Docs: README.md ("Planning" section) + specs/backend-architecture.md.
First commit of the home-page-after-login feature (see plan discussed in
chat) — frontend layout/routing/HomePage follow in subsequent commits on
this same branch/PR.
Fills in the design tokens proposed for the app (palette, type scale,
shadows/radii, and a real dark theme) and applies them to the existing
auth and home pages:
- _theme.scss: full token set — basil/vermillion/turmeric/raspberry
palette, heading type scale (was missing above --font-size-base),
elevation/radius scale, and a working dark theme via
prefers-color-scheme (color-scheme: light dark was declared but
unused). Includes a 3-tier allergen/intolerance color scale, kept
distinct from --color-error, for when the recipe/allergy feature
lands — not yet consumed by any component.
- global.scss: box-sizing reset, headings on the display font stack,
visible focus ring.
- auth-form.scss / HomePage.scss(+tsx): both screens now render their
content on a raised card (--color-surface, radius, shadow) instead
of directly on the page background, so login/signup and home read
as one coherent app.
Also adds .claude/launch.json (pnpm --filter web dev, port 5173) used
to preview the change locally.
Verified: `vite build` passes; computed styles checked live against
the token values in both themes.
* Centralize error handling (shared codes + API/client services), code quality pass
## Error handling
Requested: a centralized error-handling service on the API, custom error
codes shared across apps, and a client-side error service for i18n labels.
- packages/shared/src/errors/error-codes.ts — ErrorCode enum + ApiErrorResponse
contract. Single source of truth: neither side hardcodes a raw error string
the other has to guess at.
- apps/api: HttpError now carries an ErrorCode (not just a message).
ErrorHandlerService (new) centralizes every "how do we turn a thrown error
into an HTTP response" decision — app.ts's error middleware is now a thin
adapter calling into it. API messages reverted to English/dev-facing (they
were French from an earlier pass) since user-facing text is now generated
client-side from the code.
- apps/web: ApiClient (class, singleton instance) throws ApiError carrying
the code. ErrorMessageService (new) maps every ErrorCode to a localized
label, structured with a Locale type from the start (only "fr" exists, but
adding a language later is "add a locale to the map", not "hunt down every
hardcoded string"). LoginPage/SignupPage now display
errorMessageService.getLabel(err.code), never err.message directly.
- Tests strengthened to assert on `code`, not just HTTP status (Mocha +
Cucumber, new "the response error code should be" step). Cypress mocks
updated to the new {code, message} response shape.
## Code quality pass
Per explicit feedback: heavy JSDoc on every interface/type/class/function/
method/member touched in this PR, explicit public/private visibility on
every class member (ApiClient, ErrorMessageService, ErrorHandlerService,
HttpError), no HTML/logic mixing (styling extracted out of components
entirely, never inline).
ApiClient/ErrorMessageService were initially written as static-only classes;
switched to instance-based singletons (matching ErrorHandlerService's
existing pattern) after Biome's noStaticOnlyClass rule flagged the
static-only shape as an anti-pattern — same "class with visibility
modifiers" outcome, without fighting the linter.
## SCSS + theming
- apps/web/src/styles/_theme.scss — design tokens as CSS custom properties
on :root (colors, spacing, typography), not plain Sass variables — makes
them available at runtime, not just compile time, so a future theme
switch (e.g. dark mode) is "redefine these variables" rather than
rebuilding stylesheets.
- apps/web/src/styles/global.scss replaces the old single index.css:
reset + theme import only, loaded once from main.tsx.
- Per-page/component styles colocated (HomePage.tsx + HomePage.scss);
styles shared by multiple pages within one feature live in that feature's
folder (features/auth/auth-form.scss, used by both Login/SignupPage) —
not duplicated per page, not dumped in the global stylesheet either.
- Component-level .scss files intentionally don't `@use` the theme
partial: they only consume CSS custom properties (global at runtime via
global.scss), not Sass-level symbols, so importing it would do nothing —
documented inline rather than left as a silently-redundant import.
- vite.config.ts opts into Sass's modern compiler API to silence a
legacy-js-api deprecation warning on every build.
## specs/ updates
- New specs/error-handling.md — the ErrorCode/ApiErrorResponse contract,
both services, with a flow diagram.
- New specs/frontend-architecture.md — apps/web folder structure, routing/
auth-guard flow, SCSS/theming conventions.
- specs/batch-cooking-architecture.md links to both (original doc content
otherwise untouched — it's the user's own hand-authored source doc).
## Verification
Full lint/mocha/cucumber/build green. Manually re-verified the whole auth
flow in a real browser against native dev servers (not just the automated
suites): signup, the EMAIL_ALREADY_IN_USE → "Cet email est déjà utilisé"
translation end-to-end (confirmed the raw API response carries the English
dev message + code, and the UI shows the French label), wrong-password
INVALID_CREDENTIALS → its label, and confirmed the theme tokens actually
apply (computed button background-color matches --color-primary, card
max-width matches the token value) rather than trusting the build succeeding.
* Address review: no .d.ts, express-tools package, faker fixtures, numeric codes, real i18n lib
Five explicit review points, addressed on this same PR branch (not a new
PR) per updated preference.
## No .d.ts files in the codebase
- apps/web: vite-env.d.ts removed — its /// <reference types="vite/client" />
is replaced by "types": ["vite/client"] in tsconfig.app.json, same effect.
- apps/api: src/types/express.d.ts renamed to express-request.augment.ts —
`declare global` module augmentation works identically in a plain .ts
file as long as it has a top-level import (making it a module); the
.d.ts extension wasn't doing anything for us here.
## packages/express-tools — separate package for Express tooling
Moved HttpError and ErrorHandlerService out of apps/api into a new
workspace package, plus a new createErrorMiddleware() factory (the actual
Express 4-arg error-handling middleware, previously inlined in app.ts).
apps/api now just consumes @batch-cooking/express-tools. Has a real build
(tsc -> dist/, same pattern as packages/shared) — required for the same
reason shared needed one: apps/api's Docker image runs plain `node
dist/server.js`, no tsx. apps/api/Dockerfile updated to COPY the new
package's dist alongside shared's.
## faker.js for test fixtures
apps/api/test/auth.test.ts: replaced the hardcoded "Nicolas
Lefevre"/nicolas@example.com fixture (looked like real user data) with
@faker-js/faker, generated fresh per test via buildSignupPayload().
features/step-definitions/auth.steps.ts: fakerized the filler
firstName/lastName/password used for background state the scenarios
don't actually read.
Deliberately did NOT fakerize the literal example values inside
auth.feature itself (alice@example.com etc.) — those are the readable,
illustrative Gherkin examples that are the whole point of BDD scenarios,
not real PII, and randomizing them would make the scenarios harder to
read for no real gain. Flagged this reasoning in the README in case that
call should go the other way.
Caught a real bug while wiring this up: faker.internet.email() sometimes
capitalizes parts of the address, but signupSchema/loginSchema normalize
emails to lowercase — the test fixture needs to match what's actually
stored, so buildSignupPayload() lowercases the generated email too.
Found by actually running the suite repeatedly, not just once.
## ErrorCode: numeric enum, zero hardcoded values
packages/shared/src/errors/error-codes.ts: ErrorCode is now a numeric
enum (4000 VALIDATION_ERROR, 4001 EMAIL_ALREADY_IN_USE, 4010
INVALID_CREDENTIALS, 4011 NOT_AUTHENTICATED, 4040 NOT_FOUND, 5000
INTERNAL_ERROR — grouped by family like HTTP status codes).
Audited and fixed every place that hardcoded a raw code value instead of
referencing the enum: ApiClient's fallback (`"INTERNAL_ERROR" as
ErrorCode` — would no longer even type-check once the enum went numeric,
which is exactly the point), and the Cypress mock bodies (now import
ErrorCode from @batch-cooking/shared instead of typing the string).
Cucumber's "the response error code should be {string}" step still takes
the *name* in the .feature file (readable: "EMAIL_ALREADY_IN_USE") and
resolves it to the real numeric value via ErrorCode[name] — TypeScript's
reverse enum mapping — before comparing, so the Gherkin stays readable
without the step hardcoding a number either.
## Real i18n library (i18next), not a hand-rolled label map
apps/web: added i18next + react-i18next. New locales/fr/translation.json
holds every user-facing string — not just error labels (errors.*), but
the login/signup/home pages' labels, buttons and headings too
(auth.login.*, auth.signup.*, home.*) — via useTranslation()/t() in each
page. ErrorMessageService no longer owns its own label map; it converts
the numeric ErrorCode to its enum member name and delegates the actual
lookup to i18next (errors.<MEMBER_NAME>). Adding a language is now
"add a locale file", not a code change anywhere.
## specs/ and README updated
specs/error-handling.md and specs/frontend-architecture.md rewritten for
the new package, numeric codes, and i18next. New "i18n" and "no .d.ts"
sections. README covers the same, plus a note on the faker.js scope
decision (feature-file literals excluded, on purpose).
## Verification
Full lint/mocha (x3 runs)/cucumber/build green. Re-verified
express-tools' extraction against a real risk (not just tsc passing):
ran `node dist/server.js` standalone (mirrors the Docker runtime, no
tsx) and hit /health, a 404 (confirmed numeric code 4040 over the wire),
and a real signup + duplicate-email 409 (confirmed numeric 4001). Then
re-verified the full pipeline in a real browser against native dev
servers: signup, EMAIL_ALREADY_IN_USE -> i18next -> "Cet email est déjà
utilisé" end-to-end, home page i18next interpolation
({{firstName}}/{{lastName}}) rendering correctly.
* Address second review round: interface comments, res.locals, ExpressServer, assertIsNever
Five more explicit review points, on the same PR branch.
## Every interface key commented
Audited all 6 interfaces in the codebase. Two had partially-commented
members (violates the "every key gets /** */" rule): AuthResult
(apps/api/auth.service.ts) and SafeUserProfile (packages/shared) — both
now fully commented. The other four (AuthTokenPayload,
AuthContextValue, ErrorHandlingResult, ApiErrorResponse) were already
compliant.
## Removed the Express namespace augmentation
apps/api/src/types/express.d.ts (renamed to express-request.augment.ts
in the last round) is gone entirely. requireAuth now attaches the
authenticated profile to `res.locals.userProfile` — Express's own
built-in per-request mechanism for exactly this — typed via a new
AuthLocals interface and `Response<unknown, AuthLocals>`, instead of a
project-wide `declare global` silently changing every Request's type
whether or not it went through the middleware.
## ErrorHandlerService confirmed framework-agnostic
It already had zero Express import. Documented this explicitly (in the
package's index.ts and the new backend-architecture.md spec) as a
deliberate split: ErrorHandlerService is framework-agnostic (would work
behind Fastify too), ExpressServer/createErrorMiddleware are the actual
Express integration layer.
## packages/express-tools: server init + route/middleware utilities
New ExpressServer class, modeled on the pattern shared as a reference
(adapted, not copied 1:1 — deliberately left out the reference's custom
runtime param-type-validation system, since zod already does that job
in this codebase and running two parallel validation mechanisms would
be redundant, not "propre"):
- setupCore() — the common cors/json/cookie-parser stack
- addRoute() — registers a route, warns+skips instead of silently
double-registering the same method+path
- addMiddleware() / mountRouter() / setErrorHandler()
- listen()
- .instance — the raw Express app, for supertest
Also added wrapAsyncHandler() — forwards a thrown/rejected error from an
async handler to next(err) automatically, removing the manual
try/catch/next(err) every route needed.
apps/api/src/app.ts now builds via ExpressServer (createServer(),
consumed by both server.ts's .listen() and createApp()'s .instance for
tests). auth.routes.ts's signup/login handlers use wrapAsyncHandler
instead of manual try/catch. cookie-parser/cors moved out of apps/api's
own dependencies entirely — they're express-tools' concern now.
## assertIsNever (packages/shared/src/tools/)
Exhaustiveness-check helper for switch/if-chains over a union: takes a
`never`-typed value and throws, so a forgotten case in a later-added
union member becomes a compile error instead of a silent runtime
fallthrough. Verified for real (not just written and assumed correct):
wrote a throwaway switch missing a case and confirmed `tsc` rejects it
with the exact expected error, then deleted the scratch file. No
existing switch/if-chain over a union in the codebase yet to retrofit
it into — noted as ready for when one appears (e.g. the not-yet-built
batch-cooking calculation module or recipe-import pipeline).
## specs/ updated
New specs/backend-architecture.md — ExpressServer, wrapAsyncHandler,
the res.locals decision (with the "why not declare global" reasoning
spelled out), assertIsNever. error-handling.md and
frontend-architecture.md cross-link to it instead of duplicating.
README covers the same, briefly.
## Verification
Full lint/mocha/cucumber/build green. Re-ran `node dist/server.js`
standalone (mirrors Docker, no tsx) after the ExpressServer refactor:
/health, a 404 (numeric 4040), and a real signup + GET /me round trip
confirming res.locals-based auth actually works at runtime, not just
that tsc accepts the types.
* refactor: move ErrorHandlerService/HttpError out of express-tools
ErrorHandlerService has zero dependency on Express — it's a plain
"map an error to {status, body}" service that works identically
behind any HTTP framework. It had no business living in a package
named express-tools.
Extracted HttpError, ErrorHandlerService, and ErrorHandlingResult
into a new packages/error-tools package (same tsc-build-to-dist
pattern as shared/express-tools). express-tools now only keeps the
actual Express-specific layer: ExpressServer, wrapAsyncHandler, and
createErrorMiddleware (which adapts ErrorHandlerService, imported
from error-tools, onto Express).
- packages/error-tools: new package, depends on shared + zod
- packages/express-tools: drops zod dependency, adds error-tools
dependency for error-middleware.ts's type import
- apps/api: adds error-tools dependency; app.ts, auth.service.ts,
require-auth.ts now import HttpError/errorHandlerService from
error-tools instead of express-tools
- apps/api/Dockerfile: adds COPY for packages/error-tools in the
runtime stage
- specs/error-handling.md, specs/backend-architecture.md, README.md
updated to reflect the new package split
Verified: pnpm lint, pnpm build (all packages, correct dependency
order), pnpm test (9/9 Mocha), pnpm test:bdd (5/5 Cucumber), full
Docker rebuild + compose up (no crash-loop), curl + browser checks
of /health, unknown-route 404, signup (201), duplicate-email 409
(code 4001 EMAIL_ALREADY_IN_USE) — all going through the moved
ErrorHandlerService/HttpError correctly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Add login/signup UI (apps/web)
Wires the frontend to the existing auth API: signup, login, logout,
session restore on load.
- src/api/client.ts — fetch wrapper, credentials: "include" (required
for the httpOnly session cookie — api and web run on different
origins)
- src/features/auth/AuthContext.tsx — global auth state; calls
GET /auth/me on mount to restore the session from the cookie
- src/features/auth/RequireAuth.tsx / RedirectIfAuthenticated.tsx —
react-router-dom route guards (/ requires auth, /login and /signup
redirect away if already authenticated)
- src/pages/{Login,Signup,Home}Page.tsx — forms with client-side
validation via the shared zod schemas, API errors displayed as-is
Moves signupSchema/loginSchema from apps/api into packages/shared
(new SafeUserProfile type too) so frontend and backend validate with
the exact same rules — this is what that package was scaffolded for.
apps/api's auth.schema.ts is gone, auth.routes.ts/auth.service.ts now
import from @batch-cooking/shared directly.
Translated all user-facing API error messages and zod validation
messages to French (were English, inconsistent with the rest of the
UI) — found by actually clicking through the flow in a browser, not
just reading the code.
zod pinned to the same v3 range across api/web/shared on purpose:
apps/web's `pnpm add zod` initially resolved v4, which would have let
a major-version mismatch slip in silently (zod v3 and v4 aren't drop-in
compatible) since shared's schemas are built with v3.
Cypress specs updated for the new routing (unauthenticated visitors
now land on /login, not the old placeholder) and a new auth.cy.ts
mocking the API via cy.intercept — the e2e CI job has no live
backend, so these test frontend behavior only. Real API behavior is
covered by apps/api's Mocha/Cucumber suites against a real database.
Verified end-to-end in a real browser (not just curl): signup, session
persistence across reload, logout (confirmed the cookie was actually
cleared server-side, not just client state), wrong-password error
display, client-side validation blocking short passwords without a
network round trip, duplicate-email conflict. Full lint/mocha/
cucumber/build suite green.
* Fix packages/shared: build to dist/ instead of shipping raw TS
Found by Docker-packaging apps/api and actually running the container:
it crash-looped with "Cannot find module
'/repo/packages/shared/src/schemas/auth.js'" — Node's plain ESM loader
(node dist/server.js, no tsx/ts-node registered) can't execute .ts
source files.
This was invisible everywhere else: tsx (dev, mocha, cucumber) and
Vite both transpile TS on the fly regardless of what package.json
points to, so every dev/test/build path masked the problem. The
Docker container is the first place this code path actually runs
through a plain Node runtime — exactly the kind of thing "package
every change in Docker and run it" is supposed to catch.
Fix: give packages/shared a real build (tsc emitting to dist/, with
.d.ts), and point package.json's main/types/exports at dist/ instead
of src/index.ts. Added as a postinstall (same pattern as apps/api's
`prisma generate`) so dist/ regenerates automatically after any
`pnpm install`; after editing packages/shared's source directly,
`pnpm --filter shared build` (or `pnpm build`) is needed before the
change is visible to consumers pointing at the compiled dist/.
Verified: `node dist/server.js` (plain node, no tsx — mirrors exactly
what the Docker container runs) starts and responds on /health.
Rebuilt the Docker images and re-ran a full signup through the
containerized stack end-to-end. Full lint/mocha/cucumber/build suite
still green.
apps/api/Dockerfile: multi-stage build on node:22-slim (not alpine —
avoids musl-vs-glibc native binding surprises for argon2/Prisma's
engine binaries; same base image family for build and runtime stages
keeps "native" binaries compatible across stages). Runtime stage
copies the monorepo structure as-is rather than flattening to a single
package, so pnpm's symlinked node_modules stay valid. Container runs
`prisma migrate deploy` on startup before starting the server, so the
review environment's schema is always in sync automatically.
Installs openssl explicitly in the base image: without it, Prisma
can't detect the right engine binary and silently defaults to a guess
that may not match what's actually on the image — caught by checking
the build log, not just a successful build.
apps/web/Dockerfile: builds with Vite, serves the static output via
nginx (not a Node static server) — avoids the devDependency problem of
needing `vite preview` in a --prod-deployed image, and is the more
standard way to serve a built SPA. nginx.conf has an SPA fallback
(try_files ... /index.html) ready for when client-side routing lands.
docker-compose.yml: adds `api` and `web` services alongside the
existing `postgres`. api's DATABASE_URL targets the `postgres` service
name over the compose network (not localhost/POSTGRES_PORT, which is
only the host-side mapping). Both new services require JWT_SECRET/
ports via env vars with no defaults, consistent with the project's
existing no-hardcoded-credentials rule.
.dockerignore added — without it, the Windows-built node_modules
(with Windows-specific native binaries) would get copied into the
Linux build context.
Verified: full build (api + web images), `docker compose up -d`
brought up all three containers, curled /health and the web root,
ran a real signup through the containerized stack end-to-end.
* Add signup/login (profile creation + JWT auth)
API:
- POST /auth/signup — creates a house + user_profile (transactional),
hashes the password with argon2, sets a JWT in an httpOnly cookie
- POST /auth/login — verifies credentials (generic 401 for both wrong
email and wrong password, doesn't leak which), sets the cookie
- POST /auth/logout — clears the cookie
- GET /auth/me — current profile, behind requireAuth middleware
- requireAuth verifies the JWT and re-checks tokenVersion against the
DB, so a stateless JWT can still be invalidated (password change /
logout-everywhere, not built yet but the field is in place)
Schema: user_profiles gets password_hash + token_version (not in the
original spec doc — required for auth). New migration, with
COMMENT ON for the new columns per the established pattern.
Decisions from the auth planning discussion: JWT in httpOnly cookie
(not server-side sessions), first profile created also creates its
house, argon2 for hashing.
argon2 pinned to 0.31.2 (not ^, deliberately): 0.45.1 segfaults at
runtime on this Windows machine — reproduced consistently across bash
(sandboxed and unsandboxed) and PowerShell, while 0.31.2 works fine
with the same API. Documented in the README as a trap for future
upgrades, since `tsc`/`prisma generate` succeeding doesn't catch a
runtime native-binding crash.
Tests: Mocha (unit-style, apps/api/test/auth.test.ts) and a Cucumber
feature (apps/api/features/auth.feature) covering the full signup →
authenticated flow, duplicate email, wrong password. Both share
test-support/reset-db.ts (TRUNCATE ... CASCADE) to start each
test/scenario from a clean slate. Test-only argon2 cost parameters
(NODE_ENV=test) keep the suite fast — argon2's real cost is
deliberately expensive, which made hashing dozens of times per run
slow and occasionally timeout-flaky at default cost.
CI: added a Postgres service container to lint-and-test (previously
none — tests didn't touch a real DB), runs `prisma migrate deploy`
before the test steps.
Verified end-to-end manually against the dev server (curl): signup,
duplicate email (409), wrong password (401), valid login (200),
validation errors (400), /me with and without cookie, logout (204) —
all behave as intended. Full suite (lint, mocha, cucumber, build) run
multiple times locally with no flakiness after the timeout/cost fixes.
* Fix CI: generate Prisma Client via postinstall
CI failed with "@prisma/client did not initialize yet" — pnpm install
never ran `prisma generate`, and `prisma migrate deploy` (unlike
`migrate dev`) doesn't do it either. Worked locally only because prior
`prisma migrate dev` runs had already generated the client as a side
effect.
Adding a postinstall script fixes it for CI and for anyone cloning the
repo fresh and running plain `pnpm install`.
* Add project specs, gitignore the source PDF
specs/batch-cooking-architecture.md and specs/batch-cooking-modele.md
are the clean markdown transcription of "Projet batch cooking.pdf"
(a scanned/image-only PDF, no extractable text). The PDF itself is
gitignored — source working document, not meant to be committed.
* Add Prisma schema for the documented data model
Models every table from specs/batch-cooking-modele.md: users/household
(user_profiles, house, diet, allergy, category), planning (planning,
planning_item), and recipes (recipe, ingredients, step, tech_step,
tech_step_mapping, sources).
Two deliberate deviations from the literal spec doc, per project
discussion:
- recipe_ingredient (recipe <-> ingredients) carries quantity + unit.
The spec describes a plain many-to-many with no extra fields, but a
shopping list / batch-cooking calculation needs quantities.
- step is modeled one-to-many from recipe (not many-to-many as labeled
in the doc): the documented `order` column only makes sense scoped
to a single recipe, which isn't reconcilable with steps being
shared across recipes.
Everything else follows the doc as-is, including field nullability
choices made where the doc doesn't specify (e.g. user_profiles.house_id
optional, recipe.source_id optional) and onDelete behavior (Cascade
for owned child records, SetNull for optional references) — first
draft, not meant as final production hardening.
Verified: `prisma validate`, `prisma generate`, and a real
`prisma migrate dev` against a local Postgres (via docker-compose) —
the migration applies cleanly and produces the expected schema.
README: documents the migrate command and a Postgres port-conflict
gotcha hit during validation (a native Postgres service on this
machine was already bound to 5432, intercepting the Docker container's
connections).
* Add COMMENT ON for every table and column in the init migration
Descriptions pulled from specs/batch-cooking-modele.md's per-table
field tables. The two tables not in the original spec (join tables
recipe_ingredient, user_profile_allergy) get a comment explaining
why they exist.
Amends the still-unmerged init migration directly rather than adding
a follow-up migration, since it hasn't been applied anywhere but this
local dev database.
Verified: `prisma migrate reset --force` reapplies cleanly, and a
query against pg_description confirms every column of every project
table has a comment (only Prisma's own internal _prisma_migrations
table is uncommented, out of scope).
Coexists with Mocha (kept for unit-style tests) and Cypress (unchanged,
web e2e). Adds:
- apps/api/features/*.feature — Gherkin scenarios
- apps/api/features/step-definitions/*.steps.ts — step implementations
- apps/api/features/support/world.ts — per-scenario World, spins up the
Express app in-process via createApp() + supertest (no real server
needed, same approach as the existing Mocha health test)
- apps/api/cucumber.cjs — config, deliberately .cjs (not .js) to avoid
the same ESM/CJS config-loading mismatch that broke
apps/web/cypress.config.ts earlier
- `test:bdd` script (cross-env + tsx via NODE_OPTIONS=--import=tsx, for
cross-platform ESM+TS loading)
- health.feature/steps as a working example, mirroring the existing
Mocha health test so both suites cover the same behavior in their
respective styles
CI: runs `pnpm --filter api test:bdd` alongside the existing test step.
README: documents the new test layer and the TS/ESM config-loading
caveat for future tool configs.
Verified locally: lint, mocha, cucumber, and full build all pass.