* 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>
14 KiB
Conventions de développement — Projet Batch-cooking
Règles de style/architecture qui s'appliquent à tout le code du monorepo, transverses aux specs par domaine (backend-architecture.md, frontend-architecture.md, error-handling.md). Ce document répond à « comment j'écris du code ici », les autres répondent à « comment tel module est construit ».
TypeScript — style général
- Commenter le plus possible : chaque interface, type, classe, fonction,
méthode et membre de classe porte un commentaire JSDoc (
/** ... */). Le commentaire explique le pourquoi (la décision, le compromis, le piège évité), pas juste une paraphrase de la signature — la plupart des exemples de ce repo font ça abondamment, c'est la norme attendue, pas une option. - Jamais de mélange HTML/TypeScript : le JSX vit uniquement dans les
fichiers
.tsxde composant. Toute logique (appels API, validation, dérivation d'état, formatage) est extraite dans un.tsplain importé par le composant. Pas destyle={{}}inline ni de CSS-in-JS — le style va dans un fichier.scss(voir plus bas). - Modificateurs de visibilité explicites : dans toute classe, chaque
membre et méthode porte
public,privateouprotected— jamais le défaut implicite (public) de TypeScript.
Classes, jamais d'objets littéraux pour de la logique de service
Un objet qui regroupe du comportement (un logger, la gestion d'erreurs, un
client API, une résolution de libellés — dès qu'il y a un verbe, pas
seulement de la donnée) est une vraie class, jamais un objet littéral
assigné à un const :
// ❌ jamais ça, même pour un singleton sans état avec un seul appelant
export const logger = {
info(message: string) { /* ... */ },
};
// ✅ toujours ça
export class LoggerService {
public info(message: string): void { /* ... */ }
private _emit(level: LogLevel, message: string): void { /* ... */ }
}
/** Instance unique partagée — ce service est sans état, pas besoin d'une par appelant. */
export const logger = new LoggerService();
Exemples dans le repo : LoggerService (apps/api/src/lib/logger.service.ts),
ErrorHandlerService (packages/error-tools), ExpressServer
(packages/express-tools), ApiClient/ErrorMessageService (apps/web).
Un objet purement data (une table de correspondance, un objet qui implémente
une interface orientée donnée sans logique propre — ex. theMealDbAdapter,
INGREDIENT_LABELS_EN) n'est pas concerné, il reste un objet littéral.
React fonctionnel (hooks, function components, contexts) n'est pas forcé dans des classes — cette règle vise spécifiquement la logique de service, pas l'idiome React.
Préfixe _ sur tout membre private/protected
Tout champ ou méthode private/protected d'une classe porte le préfixe _
— emit → _emit, app → _app. public ne porte jamais ce préfixe.
Rétroactif : appliqué à chaque classe existante du repo (pas seulement au
nouveau code) — voir LoggerService._emit/_minSeverity,
ApiClient._request, ErrorHandlerService._fromZodError/_fromHttpError/
_fromUnknownError, ExpressServer._app/_registeredRoutes.
await toujours encapsulé dans try/catch
Aucun await nu (non encapsulé) : chaque appel await vit dans un bloc
try/catch. Conséquence directe — une fonction/méthode async contient
forcément au moins un await, donc tout son corps vit dans un
try/catch, pas seulement la ou les lignes qui awaitent. L'erreur
attrapée doit être traitée de façon utile pour ce point d'appel (log via
LoggerService, throw/retour d'une erreur typée, dégradation
gracieuse) — jamais avalée silencieusement — en cohérence avec le
traitement d'erreur déjà en place ailleurs dans le fichier (ex. les
chaînes .catch() du front). Rétroactif, même logique que les deux règles
ci-dessus.
Périmètre : code applicatif uniquement (routes, services, composants,
hooks, middlewares). Les fichiers de test (apps/api/test/**) et les
scripts one-off (prisma/seed.ts, apps/api/src/scripts/seed-runtime.ts,
cypress.config.ts) en sont exclus — un test s'appuie sur la propagation
du rejet d'un await non encapsulé pour faire échouer le test
(idiome chai/mocha) ; l'encapsuler forcerait soit un re-throw inutile,
soit risquerait d'avaler un vrai échec de test.
Deuxième exception : les handlers de route Express (*.routes.ts) passés à
wrapAsyncHandler (packages/express-tools/src/async-handler.ts) — son
rôle documenté est justement de transmettre une erreur/rejet à next(err)
pour qu'elle atteigne le middleware d'erreur centralisé, sans try/catch
répété dans chaque route. La règle vise le code service/hook/composant
qui n'est pas déjà filtré par ce mécanisme.
lint/complexity/noUselessCatch est désactivé dans biome.json pour
cette raison précise : cette règle Biome interdit un catch qui ne fait
que throw err;, exactement la forme que prend un try/catch ajouté
uniquement pour respecter la convention ci-dessus quand rien de plus
utile n'est à faire au niveau de cet appel (le middleware d'erreur logge
déjà tout centralement — voir plus bas). Les deux règles sont
mutuellement exclusives ; la convention du repo l'emporte.
Élégance avant rapidité
Préférer une solution propre, bien structurée et délibérée à une solution rapide/bricolée — y compris quand le correctif rapide "marche". C'est le fil conducteur des règles ci-dessus, pas une règle séparée.
Lint & format — Biome
biome.json à la racine, un seul config pour tout le monorepo (pnpm lint /
pnpm lint:fix / pnpm format à la racine, jamais un linter par package).
Formatage : indentation 2 espaces, largeur de ligne 100, guillemets
doubles, point-virgules toujours, virgules finales partout
(trailingCommas: "all"), tri automatique des imports (assist.organizeImports).
Règles actives au-delà du preset recommended :
| Règle | Effet |
|---|---|
suspicious.noExplicitAny |
interdit any explicite (déjà dans recommended, listée ici pour la visibilité) — utiliser un type précis ou unknown + vérification |
suspicious.noConsole (allow: ["error","warn","info","debug","table","assert"]) |
interdit console.log nu ; les niveaux nommés restent autorisés, mais uniquement depuis LoggerService (voir plus bas) — le reste du code ne touche jamais console directement |
nursery.noFloatingPromises |
interdit une Promise ni await-ée, ni .then(ok, err)/.catch(), ni void-ée, ni return-ée — un useEffect qui lance un fetch fire-and-forget doit soit avoir un .catch(), soit être explicitement void navigate(...) |
noFloatingPromises est une règle type-aware (nursery, nécessite Biome
≥ 2.0) — activée directement sous rules.nursery sans passer par
linter.domains (qui allumerait tout un lot d'autres règles type-aware hors
scope, ex. noUnresolvedImports).
Pour un flottant légitime (effet de bord volontairement non attendu, ex. une
navigation après un catch qui ne peut pas échouer utilement) : void maPromise(); — jamais un // biome-ignore pour ce cas précis, seulement pour
les rares cas où la règle se trompe vraiment (voir le commentaire au-dessus de
chaque biome-ignore existant, jamais silencieux).
Logs de fonctionnement côté serveur
apps/api/src/lib/logger.service.ts — LoggerService, instance unique
export const logger = new LoggerService(). Seul endroit du code autorisé à
appeler console.* (via un biome-ignore justifié) ; partout ailleurs,
noConsole l'interdit — le but est qu'un log traverse toujours
logger.debug/info/warn/error(message, meta?), jamais un console.log nu.
- Émet une ligne JSON structurée (
timestamp,level,message, +metafusionné) — pas du texte libre, pour rester grep/parse-able pardocker logs/Portainer ou un agrégateur de logs. - Seuil filtré par
NODE_ENV:debugcomplet en développement,warn+ en test (pour ne pas alourdir la sortie de Mocha),info+ en production. apps/api/src/middlewares/request-logger.ts— une ligne par requête terminée (méthode/chemin/statut/durée), montée en tout premier dansapp.ts, avant mêmesetupCore(CORS/JSON/cookies).apps/api/src/middlewares/error-logger.ts— monté juste avantcreateErrorMiddleware; réutiliseerrorHandlerService.handle()(pur, sans effet de bord) pour classifier l'erreur avant que la réponse ne soit construite :warnpour un4xxroutinier (validation, 404, 401...),error(avec la stack) pour un5xx/une exception non prévue.
apps/web n'a pas d'équivalent aujourd'hui — noConsole s'y applique aussi,
mais rien ne journalise encore côté client (hors ce que le navigateur capture
lui-même).
SCSS
- SCSS (
sass), jamais de CSS brut ni de CSS-in-JS. - Commenter le SCSS aussi librement que le TypeScript — expliquer ce qu'une règle affecte et pourquoi, pas juste ce qu'elle fait.
- Styles globaux (reset, tokens de thème) dans une feuille globale ;
styles de page/composant colocalisés au même niveau que les autres fichiers
du composant (
LoginPage.tsx+LoginPage.scsscôte à côte). - Un fichier de tokens réutilisable (
_theme.scssou équivalent : couleurs, espacements, typographie) importé partout où nécessaire, pour qu'un re-stylage reste un changement centralisé.
i18n
Tout texte affiché (formulaires, boutons, messages d'erreur) vient d'un
fichier de locale JSON (apps/web/src/locales/<lng>/translation.json),
jamais codé en dur dans un composant — voir
frontend-architecture.md
pour le détail (i18next/react-i18next, une seule langue aujourd'hui fr).
Gestion des erreurs
Contrat complet documenté à part : error-handling.md.
En bref — ErrorCode (numérique, jamais codé en dur), HttpError
(packages/error-tools, framework-agnostic), ErrorHandlerService.handle()
comme unique point de mapping erreur → réponse HTTP, ErrorMessageService
côté web pour traduire un code en libellé affiché (jamais err.message
directement à l'utilisateur).
Tests
apps/api— Mocha + Chai, contre une vraie base Postgres isolée (.env.test, jamais la même base quepnpm dev:api), pas de mocks de la base ou des services internes. Seule exception : le premier module à parler à un vrai réseau externe (the-meal-db.ts) stubglobalThis.fetch— pas de librairie de mock HTTP dans le repo, un simplefetchréassignable suffit pour l'unique cas qui en a besoin.- Données de test générées, jamais des données réalistes en dur :
@faker-js/fakerpour tout nom/email/mot de passe de fixture — jamais un nom/email qui ressemble à une vraie personne codé en dur dans un test. apps/web— Cypress (+ Cucumber pour certains scénarios), mocke l'API viacy.interceptplutôt que de dépendre d'un vrai backend ; le comportement réel de l'API est couvert côtéapps/api. Toute valeurErrorCodeutilisée dans un mock passe parErrorCode.XXX, jamais une valeur numérique/chaîne codée en dur — même règle que le code applicatif.- Un module pur (pas de DB/réseau) est toujours testable isolément de sa
variante DB-backed — convention systématique dans ce repo :
matchXxx()pur +loadXxxCatalog()qui touche la base, jamais les deux mélangés dans la même fonction (voiringredient-matcher.ts,tech-step-matcher.ts,recipe-translation.ts).
Monorepo & packages partagés
pnpm-workspace.yaml : apps/* + packages/*. packages/shared,
packages/error-tools, packages/express-tools et packages/date-tools ont
un vrai build (tsc → dist/) et sont consommés en JS compilé, jamais en
TS brut — nécessaire pour un runtime Node pur sans transpilation à la volée
(l'image Docker de prod). Concrètement : après avoir modifié un fichier dans
un de ces packages, il faut le rebuilder (pnpm --filter <package> build, ou
juste pnpm build à la racine) pour que apps/api/apps/web voient le
changement en dev — un tsx watch/Vite ne recompile pas le package pour vous.
Séparation stricte par dépendance, pas par convention de nommage :
packages/error-tools n'importe jamais express (sinon il vivrait dans
express-tools) ; packages/express-tools dépend d'error-tools, jamais
l'inverse.
Git & Pull Requests
- Jamais de commit direct sur
main— toujours une branche de feature, même pour un correctif d'une ligne. Tout ce qui arrive surmainpasse par une PR revue (l'historique demainn'est fait que de commits de merge de PR). - Messages de commit en français, format
type(scope): description(feat,fix,style,docs,chore...), scope = le domaine touché (recipes,planning,api,layout...). Toujours un trailerCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>quand le commit vient de Claude Code. - PRs plutôt petites et scopées à un sujet — l'historique de ce repo est fait
de nombreuses PRs courtes (
feat(recipes): ...,fix(planning): ...) plutôt que de grosses PRs multi-sujets, sauf demande explicite contraire.
Variables d'environnement & secrets
- Aucun secret n'a de valeur par défaut codée en dur (
JWT_SECRET,POSTGRES_PASSWORD...) —docker-compose.yml/config/env.tséchouent bruyamment si absent, jamais un fallback devinable. .env.examplene contient que des valeurs bidon (changeme), jamais de vrai identifiant, même de dev..env(dev) et.env.test(suite Mocha) pointent vers des bases Postgres différentes —resetDatabase()fait unTRUNCATEavant chaque test, partager la base viderait les données de dev à chaque run de tests.