Commit graph

6 commits

Author SHA1 Message Date
kyuno053
5d63ff9ea9
fix: corrige les bugs ouverts du repo (import TheMealDB, sidebar mobile) + doc (#59)
* fix(recipes): corrige plusieurs bugs d'import TheMealDB

- Les instructions TheMealDB numérotées sur leur propre ligne ("1\n\ntexte...\n\n2\n\ntexte...") créaient des étapes parasites ne contenant qu'un chiffre — filtrées désormais (#52).
- Un ingrédient compté sans mot d'unité dans le texte source (ex. "4 Egg Yolks") laissait l'import bloqué sur "Importer" indéfiniment, sans indication visuelle de la ligne en cause — matchUnit retombe maintenant sur l'unité générique "piece" quand une quantité a été extraite, et RecipeImportForm/RecipeFormPage surlignent désormais toute ligne dont l'unité manque, avec un message explicite (#53).
- Ajout de INGREDIENT_LABEL_SYNONYMS_EN pour reconnaître des formulations alternatives fréquentes chez les sources anglophones ("vanilla pod" en plus de "vanilla bean") sans élargir INGREDIENT_LABELS_EN à un tableau pour ses ~550 entrées (#54).
- Effet de bord découvert en vérifiant #53 de bout en bout : deux lignes source résolues vers le même ingrédient catalogue (ex. "Egg Yolks"/"Eggs" -> "Œuf") faisaient planter la création en 500 (contrainte unique recipe_id+ingredient_id) au lieu d'un 400 propre. createRecipeSchema rejette maintenant les ingredientId en double, et le formulaire d'import surligne les doublons avant même de soumettre.

Vérifié de bout en bout dans le navigateur (import réel de la recette "Flan" depuis TheMealDB, jusqu'au planning) en plus des tests ajoutés.

Closes #52, #53, #54

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(layout): la sidebar réduite écrasait la barre mobile

`isCollapsed` (rail icône seule sur desktop) persiste dans localStorage
indépendamment de la largeur de fenêtre — un utilisateur ayant réduit la
sidebar sur desktop puis ouvrant la même session sur mobile (ou réduisant
la fenêtre sous 640px) gardait `.app-sidebar.collapsed` (spécificité
0,2,0 : width 4.25rem, flex-direction column), qui l'emportait sur la
règle mobile `@media (max-width: 640px)` (spécificité 0,1,0) censée passer
la sidebar en barre horizontale pleine largeur.

Le bloc `&.collapsed` est maintenant scopé sous `@media (min-width: 641px)`
— le complément exact du breakpoint mobile — donc il ne s'applique plus du
tout en dessous.

Vérifié dans le navigateur : sidebar collapsed=true dans localStorage,
viewport 375px — la sidebar calcule bien width: 375px / flex-direction:
row (barre horizontale pleine largeur) au lieu de 4.25rem/column.

Closes #27

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(readme): documente GET /planning?date=, plus /planning/current

Le README documentait encore `GET /planning/current` (401 sans session,
couvre "aujourd'hui"), une route qui n'existe plus — `planning.routes.ts`
ne définit que `GET /planning?date=YYYY-MM-DD` depuis l'introduction de la
grille de semaine complète. Sans session, `/planning/current` renvoie un
404 générique (route inexistante), pas le 401 documenté.

Documente aussi POST/DELETE /planning/items au passage, absents jusqu'ici.

Closes #55

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: met à jour README et specs/ avec l'état réel du code

Le code avait beaucoup évolué depuis la dernière mise à jour de la
documentation (sources externes, import de recettes, planning en
grille, pages de paramètres, thème, tests Cucumber...) sans que
README.md/specs/*.md ne suivent. Tour complet du code (backend +
frontend) et réécriture :

- specs/batch-cooking-modele.md : schéma de données réécrit depuis
  schema.prisma (foyer/admin/invitation, sources, catalogue
  ingrédients/unités, techniques détectées, visibilité des recettes).
- specs/backend-architecture.md : foyer, préférences/goûts, planning,
  référence, sources externes (adaptateurs/registre/sync), matching
  ingrédients/techniques, isolation base de test, suppression de compte.
- specs/frontend-architecture.md : routing complet, sidebar/paramètres,
  thème, planning + picker, catalogue + import, composants UI partagés,
  tests Cypress+Cucumber.
- specs/batch-cooking-architecture.md : module Import passe de TODO à
  implémenté.
- specs/error-handling.md : liste complète des ~19 codes d'erreur.
- README.md : réécriture pour refléter tout ce qui précède, plus la
  note (dangereusement obsolète) sur le partage base de test/dev — le
  fix existe déjà (apps/api/.env.test), la doc décrivait encore le bug.

* feat(ingredients): ajoute jaune/blanc d'oeuf, coriandre en poudre, viandes hachées

Complète le catalogue d'ingrédients de référence (seed data) :

- jaune d'oeuf / blanc d'oeuf (dairyAndCheese/eggs, aux côtés d'"egg")
- coriandre en poudre (condimentsAndSpices/spices, aux côtés de
  corianderSeeds/freshCilantro déjà présents)
- viandes hachées manquantes : veau, porc, agneau (meatAndSeafood/meats,
  aux côtés de groundBeef déjà présent), dinde et poulet
  (meatAndSeafood/poultry)

Libellés ajoutés dans apps/web/src/locales/fr/translation.json (source
d'affichage) et packages/shared/src/data/catalog-labels-en.ts (matching
anglais pour l'import de recettes depuis des sources comme TheMealDB).
Aucune icône ni régime dédiés : héritent des défauts de leur groupe
(EGG/SPICE/MEAT/POULTRY, mêmes dietUids que leurs groupes respectifs).

282 tests apps/api toujours au vert (resetDatabase() reseed le
catalogue à chaque test).

* fix(i18n): retire le œ ligaturé des libellés français de l'œuf

"Œuf"/"Œufs" (ingrédient, sous-catégorie, allergène) et "Jaune/Blanc
d'œuf" (ajoutés par #60) s'écrivaient avec le œ ligaturé — remplacé par
"oe" (deux lettres) partout où le mot apparaît. Ne touche pas "bœuf"
(mot différent, non concerné).

Le scénario Cucumber recipe-form.feature qui sélectionne l'ingrédient
par son libellé affiché est mis à jour en conséquence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(recipes): concatène les ingrédients dupliqués à l'import

Suite au retour utilisateur sur #53 (follow-up) : au lieu de bloquer
l'import et de demander à l'utilisateur de retirer une ligne en double
à la main, deux lignes source qui résolvent vers le même ingrédient
catalogue sont désormais fusionnées automatiquement, quantité
concaténée (sommée), avant même que l'écran de revue ne s'affiche.

- mergeDuplicateIngredients (recipe-translation.ts) : même unité des
  deux côtés -> somme directe. Unité différente mais même UnitType
  (MASS/VOLUME) -> conversion via toBaseFactor avant de sommer, exprimée
  dans l'unité de la première ligne. UnitType différent, ou COUNT des
  deux côtés (une "pincée" n'est pas une fraction fixe d'une "gousse",
  cf. le commentaire de UnitView) -> jamais fusionnées, laissées en
  double (createRecipeSchema/RecipeImportForm continuent de les
  signaler, filet de sécurité déjà en place). Les lignes non résolues
  (ingredientId: null) ne sont jamais fusionnées entre elles.
- rawText concaténé ("100g Sugar + 45g Sugar") pour la traçabilité.
- Branché dans previewSourceItem (sources.service.ts), juste après
  translateRecipeIngredients — c'est le seul endroit où des doublons
  peuvent apparaître (la création manuelle ne peut pas en produire,
  IngredientPicker exclut déjà les ingrédients déjà sélectionnés).

Vérifié via l'API en local (import réel de "Flan" depuis TheMealDB) :
"100g Sugar"/"45g Sugar" -> une seule ligne Sucre, 145g.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(lint): upgrade Biome vers 2.x, active noExplicitAny/noConsole/noFloatingPromises

`@biomejs/biome` passe de 1.9.4 à 2.5.9 (config migrée via `biome migrate
--write`) — nécessaire pour noFloatingPromises, une règle type-aware
apparue en 2.0 (nursery).

- noExplicitAny : déjà "recommended", actif depuis toujours, aucun changement.
- noConsole (biome.json) : bloque tout `console.*` sauf error/warn/info/
  debug/table/assert — équivalent à "pas de console.log" sans interdire
  les niveaux nommés (voir le nouveau log service dans le prochain commit,
  qui centralise justement ces appels).
- noFloatingPromises (nursery) activé explicitement sous `rules.nursery`
  sans avoir besoin d'activer le domaine "types" au sens large (ça aurait
  aussi allumé des dizaines d'autres règles type-aware type
  noUnresolvedImports/noUnnecessaryConditions, hors scope ici).

Le reste du diff, c'est soit du reformatage automatique (import sort, 2.x
ordonne différemment de 1.9.4 — `biome check --write --unsafe`), soit les
corrections des ~20 promesses flottantes que la nouvelle règle a fait
remonter :

- La plupart sont des `navigate(...)` non attendus (react-router v7 type
  `navigate` en `void | Promise<void>`) — préfixés `void navigate(...)`,
  aucun changement de comportement.
- Trois chargements initiaux en useEffect (OnboardingAllergensPage,
  OnboardingDietPage, OnboardingHouseholdPage, HouseholdSettingsPage)
  n'avaient jamais de `.catch()` du tout — ajouté (dégradation silencieuse
  vers un état vide/par défaut, même raisonnement que le `.catch()` déjà
  présent dans OnboardingSourcesPage).
- HouseholdSettingsPage : `loadHouse` était une fonction déclarée à chaque
  render (donc une référence différente à chaque fois) utilisée comme
  dépendance de useEffect ET passée en callback à des enfants — le
  useEffect se re-déclenchait donc à chaque re-render provoqué par son
  propre fetch, un vrai bug de boucle infinie de requêtes que
  noFloatingPromises a fait remonter indirectement (via
  useExhaustiveDependencies). Corrigé avec useCallback([]).
- RecipeDetailPanel : une clé de liste `${index}-...}` sur une liste
  statique (draft.steps, sans id stable — DraftRecipeStepView n'en a pas)
  — biome-ignore justifié, pas de bug réel.
- recipe.test.ts : variable `agent` non utilisée, retirée.

Vérifié : `pnpm --filter api test` (295/295), `pnpm lint` et `pnpm build`
clean sur tout le repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(api): ajoute un log service pour les logs de fonctionnement côté serveur

Jusqu'ici, rien ne journalisait quoi que ce soit côté serveur : aucune
trace au démarrage à part un console.log ad hoc, et surtout aucune trace
des requêtes ni des erreurs gérées par ErrorHandlerService — un 500 en
production n'aurait laissé aucune trace exploitable.

- LoggerService (apps/api/src/lib/logger.service.ts) — classe (public
  debug/info/warn/error, private emit), même convention que
  ErrorHandlerService (packages/error-tools) : instance unique partagée
  exportée (`export const logger = new LoggerService()`). Émet une ligne
  JSON structurée par appel (timestamp/level/message + meta), filtrée par
  seuil selon NODE_ENV (debug complet en dev, warn+ pendant les tests
  pour ne pas alourdir la sortie de Mocha, info+ en production). Seul
  endroit du code autorisé à toucher `console` directement (biome-ignore
  justifié), toujours via une méthode nommée — jamais un console.log nu.
- requestLogger (middlewares/request-logger.ts) — une ligne par requête
  terminée (méthode/chemin/statut/durée), montée en tout premier dans
  app.ts, avant même setupCore (CORS/JSON/cookies), pour englober tout le
  pipeline. Niveau déduit du statut (info/warn/error).
- errorLogger (middlewares/error-logger.ts) — monté juste avant
  createErrorMiddleware : réutilise errorHandlerService.handle() (pur/
  sans effet de bord) pour classifier l'erreur avant que la vraie réponse
  ne soit construite, log en warn les 4xx routiniers (validation, 404,
  401...) et en error les 5xx/exceptions non prévues (avec la stack).
- error-handler.service.ts : retire le `console.error(error)` ad hoc de
  fromUnknownError — errorLogger voit désormais chaque erreur avant que
  ce service ne la mappe, donc ce console.error faisait doublon (et
  loggait en texte brut, pas en JSON structuré).
- server.ts : le console.log de démarrage passe par logger.info.

Vérifié : pnpm --filter api test (303/303, dont 8 nouveaux tests sur
LoggerService), pnpm lint/build clean, testé en live (pnpm dev:api +
curl) — logs JSON corrects pour un 200, un 404, un 401.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* style: préfixe tous les membres private/protected par _

Convention demandée par l'utilisateur : `emit` -> `_emit`, sur toutes les
classes du repo, pas seulement le nouveau code. `public` reste sans
préfixe.

- LoggerService (apps/api) : _minSeverity, _emit.
- ApiClient (apps/web) : _request (39 sites d'appel mis à jour).
- ErrorHandlerService (packages/error-tools) : _fromZodError,
  _fromHttpError, _fromUnknownError.
- ExpressServer (packages/express-tools) : _app, _registeredRoutes.

Aucun changement de comportement — pur renommage interne, aucune méthode
private/protected n'était appelée depuis l'extérieur de sa classe.

Vérifié : pnpm --filter api test (303/303), pnpm lint/build clean sur
tout le repo (apps/api, apps/web, packages/*).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(specs): documente les conventions de développement du repo

Nouveau specs/dev-conventions.md — jusqu'ici ces règles n'existaient que
dans l'historique de commits/PR (classes vs objets littéraux pour la
logique de service, préfixe _ sur private/protected, règles Biome
actives, log service, tests sans mocks de la DB, conventions git/PR...),
rien de centralisé pour un futur contributeur (humain ou Claude Code).

Référencé depuis README.md, section "Qualité / Tests".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(web): regroupe pages/ par section au lieu d'un dossier à plat

pages/ mélangeait 8 fichiers directement à sa racine (LoginPage,
SignupPage, PlanningPage+scss, RecipesPage, RecipeFormPage,
ImportRecipePage, ShoppingListPage, ComingSoonPage+scss) à côté de deux
sous-dossiers déjà groupés (onboarding/, settings/) — incohérent, et
difficile à parcourir une fois le nombre de pages monté. Un sous-dossier
par section routée, même règle que onboarding/settings existants :

- pages/auth/        — LoginPage, SignupPage
- pages/planning/     — PlanningPage + planning-page.scss
- pages/recipes/      — RecipesPage, RecipeFormPage, ImportRecipePage
- pages/shopping-list/ — ShoppingListPage

ComingSoonPage (+ .scss) déménage vers components/ui/ — ce n'est pas une
page routée elle-même (ShoppingListPage l'enveloppe), c'est un composant
UI générique réutilisable, sa place est aux côtés de Dialog/Tooltip/etc.,
pas dans pages/.

Chemins relatifs internes de chaque fichier déplacé mis à jour (un niveau
de profondeur en plus), imports dans App.tsx repointés, tri Biome
réappliqué. specs/frontend-architecture.md mis à jour (arborescence +
références de chemin).

Vérifié : pnpm build clean (apps/web, 1952 modules), pnpm lint clean sur
tout le repo, testé en live dans le navigateur (login/signup, planning,
recettes, nouvelle recette, liste de courses, paramètres) — aucune route
cassée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(web): regroupe features/recipes/ par sous-domaine au lieu d'un dossier à plat

20 fichiers à plat -> badges/ (DietTagSelect, DietBadges, AllergenBadges,
ReproducibleBadge, FavoriteStarButton), ingredients/ (IngredientPicker,
IngredientRow, ingredient-icons), steps/ (StepListEditor, StepDescription,
highlight-tech-steps), sources/ (RecipeSourcesPanel, SourceItemTable,
RecipeImportForm, recipe-import-draft, useEnabledSources).

RecipeTable/RecipeTabs/RecipeDetailPanel et recipes.scss restent à la
racine (composants transverses aux sous-dossiers, partagés par plusieurs
d'entre eux). Chemins relatifs corrigés dans les fichiers déplacés et chez
tous leurs importeurs externes (pages/recipes/*, features/planning/
RecipePickerDialog.tsx, features/profile/DislikedIngredientsField.tsx),
doc mise à jour (specs/frontend-architecture.md, specs/batch-cooking-
modele.md).

Vérifié : tsc --noEmit, biome check, build complet, 303 tests API,
vérification live navigateur (planning, /recettes, /recettes/nouvelle).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(api): regroupe lib/ par sous-domaine au lieu d'un dossier à plat

9 fichiers à plat -> recipe-sources/ (recipe-source-adapter, recipe-source-
errors, recipe-source-registry) et recipe-matching/ (recipe-translation,
ingredient-matcher, tech-step-matcher). jwt.ts, safe-profile.ts et
logger.service.ts restent à la racine de lib/ (pas de sous-domaine
partagé avec les autres).

Chemins relatifs corrigés dans les fichiers déplacés (profondeur +1 vers
db/) et chez tous leurs importeurs (modules/sources, modules/recipe,
sources/*, db/recipe-source-sync.ts, 12 fichiers de test), doc mise à
jour (specs/backend-architecture.md, specs/batch-cooking-architecture.md).

Vérifié : tsc --noEmit, biome check, build complet, 303 tests API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(api): regroupe test/ par sous-domaine, miroir de src/lib/

18 fichiers à plat -> recipe-matching/ (ingredient-matcher, recipe-
translation, tech-step-matcher — miroir de lib/recipe-matching/),
recipe-sources/ (json-ld-recipe, recipe-source, recipe-source-sync,
the-meal-db — miroir de lib/recipe-sources/), sources/ (sources,
sources-index — module + registration src/sources/index.ts).

Les tests par domaine API sans regroupement naturel (auth, health,
house, logger.service, planning, preferences, profile, recipe,
reference) restent à la racine de test/, un fichier par domaine — même
logique que jwt.ts/safe-profile.ts restés à la racine de lib/.

Chemins relatifs corrigés (../src/ -> ../../src/, ../test-support/ ->
../../test-support/ dans les fichiers déplacés qui appellent
resetDatabase). .mocharc.json ("test/**/*.test.ts") couvre déjà les
sous-dossiers, aucun changement de config nécessaire.

Vérifié : biome check, 303 tests API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(convention): impose try/catch autour de chaque await/corps async

Nouvelle règle de dev : aucun await nu, et un corps de fonction/méthode
async doit intégralement vivre dans un try/catch (pas seulement la ou
les lignes qui awaitent). Documentée dans specs/dev-conventions.md avec
son périmètre (code applicatif — services/hooks/composants/middlewares
— routes *.routes.ts exemptées car déjà couvertes par
wrapAsyncHandler ; tests et scripts one-off exemptés aussi).

Appliqué rétroactivement à tout le code applicatif qui ne l'était pas
déjà :
- api : auth/house/profile/preferences/planning/reference/recipe/
  sources .service.ts, recipe-source-sync.ts, recipe-translation.ts,
  ingredient-matcher.ts, tech-step-matcher.ts, json-ld-recipe.ts,
  the-meal-db.ts — un try/catch par fonction async, rethrow simple
  (le middleware d'erreur logge déjà tout centralement, voir
  error-logger.ts) sauf quand un catch avait déjà une logique propre
  (ex. le retry de createHouse).
- web : api/client.ts (_request), AuthContext.tsx, ThemeContext.tsx,
  AppLayout.tsx (handleLogout), HouseholdSettingsPage.tsx (handleCopy/
  handleRemove/handleDelete/handleLeave) — la plupart des handlers de
  formulaire avaient déjà ce pattern, seuls ceux qui laissaient un
  await nu ont été corrigés.

lint/complexity/noUselessCatch désactivé dans biome.json (interdisait
justement le catch-qui-rethrow que cette convention impose).

Vérifié : tsc --noEmit (api+web), biome check (0 erreur, repo entier),
build complet, 303 tests API, vérification live navigateur (thème,
déconnexion, copie du code d'invitation).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(web): corrige l'import cassé de highlight-tech-steps.cy.tsx

Oubli lors du regroupement de features/recipes/ par sous-domaine
(refactor(web): regroupe features/recipes/...) : le déplacement de
highlight-tech-steps.ts vers features/recipes/steps/ n'avait pas été
répercuté dans ce test composant Cypress (hors de apps/web/src, donc
raté par la recherche de référence externe à l'époque) — faisait
planter le job e2e en CI ("Failed to fetch dynamically imported
module").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 12:16:22 +02:00
7af98756dc API: le foyer n'est plus créé automatiquement à l'inscription + suppression de compte (step 3/8)
- signup() ne crée plus de House — houseId démarre à null, le foyer
  devient une étape optionnelle de l'onboarding (créer/rejoindre/passer)
- deleteAccount(): revérifie le mot de passe, transfère l'adminship ou
  supprime le foyer si nécessaire (leaveCurrentHouse), puis supprime
  le profil (cascade sur les allergies)
- DELETE /auth/me — nouvelle route, gated par mot de passe
- clearCookie n'envoie plus maxAge (corrige un warning de dépréciation
  Express, déjà latent sur /auth/logout)
2026-08-17 10:38:47 +02:00
1d03effc77 API: endpoints foyer/profil (nom, régime, allergènes) (step 2/6)
- GET/PATCH /house/current — renomme le foyer de l'utilisateur connecté.
  PATCH avec houseId null -> 404 HOUSE_NOT_FOUND.
- PATCH /profile/diet { dietId: number | null } — régime du profil ;
  null l'efface (étape skippable du parcours). dietId invalide ->
  404 DIET_NOT_FOUND.
- GET/PATCH /profile/allergies — allergènes/intolérances, liste d'IDs ;
  PATCH remplace l'ensemble complet (pas une fusion, cohérent avec un
  multi-select). ID invalide -> 404 ALLERGY_NOT_FOUND.
- 3 nouveaux ErrorCode (4041-4043) + libellés fr.
- Extraction de toSafeProfile() dans src/lib/safe-profile.ts —
  auparavant dupliqué dans auth.service.ts et require-auth.ts,
  profile.service.ts le réutilise aussi.
- Tests Mocha (28 passing) + Cucumber (15 scenarios) — même convention
  que le reste, doc README.

Deuxième commit de la feature profil/foyer/régime/allergènes —
composants front partagés dans le commit suivant.
2026-08-16 23:18:46 +02:00
kyuno053
de63be7ba4
Centralize error handling + code quality pass (comments, SCSS theming) (#7)
* 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>
2026-08-16 18:40:53 +02:00
kyuno053
3ad854a269
Login/signup UI (apps/web) (#6)
* 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.
2026-08-16 15:21:40 +02:00
kyuno053
42d094764f
API: signup/login (profile creation + JWT auth) (#4)
* 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`.
2026-08-16 13:45:23 +02:00