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.
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.
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).
É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.
* 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).
* 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.
* 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>
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().
- 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é
- 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é
- 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)
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).
- 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.
- 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.
* 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.
* Scaffold generic pnpm monorepo (api + web + shared)
Sets up the initial project infrastructure only, no business modules yet:
- apps/api: Express/TypeScript backend skeleton (healthcheck route, zod-validated
env config, error handling, Prisma initialized with no models yet, Postgres
as the target DB)
- apps/web: React/Vite/TypeScript frontend skeleton, Capacitor-ready for the
future mobile app
- packages/shared: empty placeholder for types/schemas shared between api and
web once the data model is defined
- Tooling: Biome (lint/format), Mocha+Chai+Supertest (api tests), Cypress
(web e2e smoke test), GitHub Actions CI (lint + test + build + e2e)
- docker-compose.yml for local Postgres
- README documents setup steps, including the Cypress binary caveat (pnpm
install doesn't always fetch the native binary — needs `cypress install`
run locally per machine)
Fixes along the way:
- apps/web/cypress.config.ts: disable GPU on browser launch for
headless/sandboxed environments
- apps/web/tsconfig.*: split into solution/app/node tsconfig files (standard
Vite pattern) — the previous single-file setup caused `tsc -b` to emit
compiled .js/.d.ts next to vite.config.ts and cypress.config.ts
* Remove hardcoded credentials from committed env/compose files
.env.example and apps/api/.env.example had a real usable default
credential pair (batchcooking/batchcooking) baked in, and
docker-compose.yml fell back to the same values via ${VAR:-default}
if .env was missing. Neither should ship a working credential:
- .env.example / apps/api/.env.example now use "changeme" placeholders
that must be edited before use.
- docker-compose.yml uses ${VAR:?...} instead of ${VAR:-default} for
POSTGRES_USER/PASSWORD/DB, so compose fails loudly if .env isn't set
up rather than silently falling back to a guessable credential.
Healthcheck reads the container's own env var ($$POSTGRES_USER)
instead of duplicating the value in the compose file.
- README updated to say .env.example must be edited, not just copied.
Verified: `docker compose config` fails with a clear message when
.env is absent, and resolves correctly once .env is filled in.
* Fix CI: remove pnpm version conflict with packageManager field
pnpm/action-setup@v4 errored with "Multiple versions of pnpm
specified" because the workflow pinned version: 10 while
package.json's packageManager field pins pnpm@10.12.4. The action
already reads packageManager automatically, so drop the redundant
version input.
* Fix CI: install Cypress binary explicitly before running e2e
Same root cause as the README caveat: pnpm install doesn't reliably
trigger Cypress's postinstall binary download, so `cypress run` failed
in CI with "The cypress npm package is installed, but the Cypress
binary is missing." Add an explicit `cypress install` step, and cache
~/.cache/Cypress keyed on the lockfile so subsequent runs don't
re-download it.
* Fix Cypress config loading: give the solution tsconfig a module system
apps/web/tsconfig.json (the tsc -b "solution" file) had no
compilerOptions, only files/references. Cypress's bundled ts-node
picks the nearest tsconfig.json to transpile cypress.config.ts, and
with no "module" specified it defaulted to CommonJS while
package.json declares "type": "module" — causing:
ReferenceError: exports is not defined in ES module scope
Adding module/moduleResolution to the solution config (harmless for
tsc -b itself, since it only builds the referenced projects) fixes
the mismatch. This regressed after the earlier fix for the stray
vite.config.js emission and was never re-verified against Cypress
until CI caught it.