Capitalisation complète — app-alexandrie & app-template-resto (23-03-2026)

Intègre ~50 entrées depuis 95_a_capitaliser.md vers les fichiers validés :
- backend risques : +15 (GET sans authz, TOCTOU tenantId, TTL UTC, AdminRoleGuard, P3014...)
- backend patterns : P2002 amendé (create+update) + 10 nouveaux (Decimal, URL safe, EN enforcement...)
- frontend risques : +21 (defaultValue/key, useTransition global, consent state, Tailwind invalide...)
- frontend patterns : +6 (click-to-load, toggle optimiste, Server Action retourne entité...)
- debug/postmortem : export{fn} ne crée pas de binding local

95_a_capitaliser.md remis à l'état initial vide.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MaksTinyWorkshop
2026-03-23 15:02:14 +01:00
parent 2e6ed9d374
commit e61e3d5ea8
6 changed files with 1485 additions and 1288 deletions

View File

@@ -8,7 +8,7 @@ Ce fichier contient **uniquement** des patterns back-end :
Objectif : éviter de réinventer la roue et réduire le temps de debug. Objectif : éviter de réinventer la roue et réduire le temps de debug.
Dernière mise à jour : 20-03-2026 Dernière mise à jour : 23-03-2026
--- ---
@@ -30,7 +30,7 @@ Dernière mise à jour : 20-03-2026
- [RedisHealthService avec cache interne court](#pattern-redis-health-cache-court) - [RedisHealthService avec cache interne court](#pattern-redis-health-cache-court)
- [Sémantique explicite `Trial` vs `Paid` dans Subscription](#pattern-subscription-trial-vs-paid) - [Sémantique explicite `Trial` vs `Paid` dans Subscription](#pattern-subscription-trial-vs-paid)
- [Restauration dachats Stripe en 3 étapes](#pattern-restauration-achats-stripe) - [Restauration dachats Stripe en 3 étapes](#pattern-restauration-achats-stripe)
- [Mapping explicite de `P2002` Prisma sur update de champ unique](#pattern-prisma-p2002-update-unique) - [Mapping explicite de `P2002` Prisma sur create/update de champ unique](#pattern-prisma-p2002-update-unique)
- [Autorisation interne minimale sans RBAC complet](#pattern-autorisation-interne-minimale) - [Autorisation interne minimale sans RBAC complet](#pattern-autorisation-interne-minimale)
- [Anti-énumération sur endpoints auth liés à un email](#pattern-anti-enumeration-auth-email) - [Anti-énumération sur endpoints auth liés à un email](#pattern-anti-enumeration-auth-email)
- [Token à usage unique — génération, hash et invalidation atomique](#pattern-token-usage-unique) - [Token à usage unique — génération, hash et invalidation atomique](#pattern-token-usage-unique)
@@ -42,6 +42,15 @@ Dernière mise à jour : 20-03-2026
- [Opérations auth sensibles — atomiques, idempotentes et cohérentes](#pattern-auth-operations-atomiques) - [Opérations auth sensibles — atomiques, idempotentes et cohérentes](#pattern-auth-operations-atomiques)
- [Réponse HTTP 200 avec payload métier pour les états d'accès](#pattern-http-200-payload-metier) - [Réponse HTTP 200 avec payload métier pour les états d'accès](#pattern-http-200-payload-metier)
- [Quota journalier Redis atomique (INCR + EXPIREAT pipeline)](#pattern-quota-redis-atomique) - [Quota journalier Redis atomique (INCR + EXPIREAT pipeline)](#pattern-quota-redis-atomique)
- [Filtrage des règles métier dans le service, pas dans le repository](#pattern-filtrage-metier-service)
- [Sérialiser les champs `Decimal` Prisma en string au niveau du repository](#pattern-decimal-prisma-serialisation)
- [Extraire les helpers de résolution tenant dans un module partagé dédié](#pattern-helper-tenant-module-partage)
- [Helper centralisé d'activation de features tenant-scoped](#pattern-helper-feature-flag-tenant)
- [Réutiliser un champ existant plutôt que créer un modèle dédié en V1](#pattern-reutiliser-champ-existant-v1)
- [Valider le protocole d'une URL externe avant de la passer à un lien public](#pattern-validation-url-externe)
- [Utilitaires purs : extraire dans un module sans `server-only`](#pattern-utilitaires-purs-module-partage)
- [EN enforcement optionnel par tenant (toggle + publish gate)](#pattern-en-enforcement-tenant)
- [Prisma — Migration manuelle sans shadow DB (P3014)](#pattern-prisma-migration-manuelle-p3014)
--- ---
@@ -686,11 +695,11 @@ handlePackWebhookEvent(event): PackWebhookResult | null
<a id="pattern-prisma-p2002-update-unique"></a> <a id="pattern-prisma-p2002-update-unique"></a>
## Pattern : mapping explicite de `P2002` Prisma sur update de champ unique ## Pattern : mapping explicite de `P2002` Prisma sur create/update de champ unique
- Objectif : transformer un conflit dunicité prévisible en erreur métier exploitable plutôt quen 500 opaque. - Objectif : transformer un conflit dunicité prévisible en erreur métier exploitable plutôt quen 500 opaque.
- Contexte : `update` Prisma sur un champ `@unique` alimenté par une source externe ou concurrente. - Contexte : `create`, `update` ou `upsert` Prisma sur un champ `@unique` alimenté par une source externe, concurrente, ou après un pre-check.
- Quand lutiliser : dès quun champ unique peut être mis à jour après création. - Quand lutiliser : dès quun champ unique peut entrer en collision — à la création ET à la modification.
- Quand léviter : jamais si le champ peut réellement entrer en collision. - Quand léviter : jamais si le champ peut réellement entrer en collision.
- Avantage : - Avantage :
- réponse client stable - réponse client stable
@@ -708,10 +717,28 @@ handlePackWebhookEvent(event): PackWebhookResult | null
- Conserver requestId et format derreur standardisé - Conserver requestId et format derreur standardisé
``` ```
### Implémentation (exemple complet)
```typescript
import { Prisma } from "@prisma/client";
try {
await prisma.item.create({ data: { ... } });
// ou: await prisma.item.update({ where: { id }, data: { ... } });
} catch (err) {
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
throw new HttpError("Un élément avec ce nom existe déjà.", { status: 409 });
}
throw err;
}
```
**Important :** un pre-check applicatif (`findUnique` avant `create`) ne suffit pas contre les race conditions. Le `try/catch P2002` est le seul garde-fou fiable. Sapplique à `create`, `update`, `updateMany`, `upsert`.
### Checklist ### Checklist
- `P2002` intercepté sur les updates sensibles - `P2002` intercepté sur les creates ET les updates sensibles
- Code derreur métier stable - Code derreur métier stable (409 Conflict)
- Pas de 500 générique sur conflit prévisible - Pas de 500 générique sur conflit prévisible
--- ---
@@ -1149,3 +1176,260 @@ if (count !== null && count > QUOTA_MAX) {
- [ ] Mode dégradé permissif si `count === null` (Redis down) - [ ] Mode dégradé permissif si `count === null` (Redis down)
- [ ] Clé nommée `{app}:quota:{action}:{userId}:{yyyy-mm-dd}` (date UTC) - [ ] Clé nommée `{app}:quota:{action}:{userId}:{yyyy-mm-dd}` (date UTC)
- [ ] Anti-pattern évité : `incrBy` + `setEx` séparés (race condition si count === 1 concurrent) - [ ] Anti-pattern évité : `incrBy` + `setEx` séparés (race condition si count === 1 concurrent)
---
<a id="pattern-filtrage-metier-service"></a>
## Pattern : Filtrage des règles métier dans le service, pas dans le repository
- Objectif : séparer la couche d'accès aux données (repository) des règles de visibilité métier (service).
- Contexte : entités publiques avec règles de filtrage (`isVisible`, `isActive`), qui varient selon le contexte appelant (public vs admin).
- Quand l'utiliser : dès qu'une règle de visibilité dépend du contexte d'appel.
- Quand l'éviter : filtres de performance (pagination, tenant scoping) — ceux-là restent dans le `where`.
- Avantage :
- la règle est testable unitairement sans Prisma (mock de données brutes)
- la requête DB reste simple et stable entre contextes
- les cas futurs (ex: admin voit les invisibles) ne nécessitent pas de modifier la requête
- Validé le : 17-03-2026
- Contexte technique : Prisma / Node.js / Next.js — app-template-resto
### Implémentation (exemple minimal)
```typescript
// Repository — charge tout ce qui est candidat
async findCategories(tenantId: string) {
return prisma.category.findMany({ where: { tenantId } }); // pas de filtre isVisible
}
// Service — applique la règle métier et mappe vers DTO
const raw = await repo.findCategories(tenantId);
return raw.filter(c => c.isVisible).map(toPublicDto);
// Admin : même repo, filtre différent dans le service admin
return raw.map(toAdminDto); // retourne tout, visible ou non
```
---
<a id="pattern-decimal-prisma-serialisation"></a>
## Pattern : Sérialiser les champs `Decimal` Prisma en string au niveau du repository
- Objectif : éviter que les objets `Decimal` Prisma traversent les couches et causent des erreurs de sérialisation JSON silencieuses.
- Contexte : tout champ `Decimal` en Prisma (ex: `price`) retourné via API ou Server Action.
- Quand l'utiliser : systématiquement sur tout champ `Decimal` dans les repositories.
- Risque si ignoré : `Decimal` n'est pas JSON-sérialisable nativement — comportement varie selon Node vs browser vs test runner.
- Validé le : 17-03-2026
- Contexte technique : Prisma / Node.js — app-template-resto
### Implémentation
```typescript
// Repository — convertir avant de retourner
return {
...dish,
price: dish.price?.toString() ?? null, // Decimal → string
};
// DTO public
type DishDto = {
price: string | null; // pas Decimal
};
```
---
<a id="pattern-helper-tenant-module-partage"></a>
## Pattern : Extraire les helpers de résolution tenant dans un module partagé dédié
- Objectif : éviter les couplages sémantiques incorrects entre domaines en centralisant les utilitaires transverses tenant.
- Contexte : toute fonction de résolution de tenant utilisée par plusieurs domaines métier.
- Quand l'utiliser : dès qu'un helper est importé par plus d'un module métier.
- Risque si ignoré : un module métier devient dépendance implicite d'un autre domaine distinct.
- Validé le : 17-03-2026
- Contexte technique : Next.js / TypeScript — app-template-resto
### Implémentation
```typescript
// ✅ src/server/tenant/resolvePublicTenant.ts
export function resolvePublicTenantSelection(env: NodeJS.ProcessEnv) { ... }
// ✅ Rétrocompatibilité depuis l'ancien emplacement si nécessaire
export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";
```
---
<a id="pattern-helper-feature-flag-tenant"></a>
## Pattern : Helper centralisé d'activation de features tenant-scoped
- Objectif : centraliser la logique d'activation/désactivation de pages ou modules par tenant dans un helper pur.
- Contexte : app multi-tenant avec features activables (pages publiques, modules optionnels, intégrations).
- Quand l'utiliser : dès qu'une feature peut être activée/désactivée par tenant.
- Avantage :
- helper pur et testable sans I/O
- comportement par défaut sain (`null`/`undefined` → tout activé)
- composants de navigation et pages importent ce helper, jamais Prisma directement
- Validé le : 17-03-2026
- Contexte technique : Next.js App Router / TypeScript — app-template-resto
### Implémentation
```typescript
// src/server/public/publicPagesConfig.ts
export function isPublicPageEnabled(
config: PublicPagesConfigRecord | null | undefined,
pageKey: PublicPageKey
): boolean {
if (!config) return true; // config absente = tout activé par défaut
return config[PAGE_KEY_TO_CONFIG_FIELD[pageKey]];
}
```
**Règle :** `null`/`undefined` → tout activé. Évite les régressions si la config n'a pas été provisionnée.
---
<a id="pattern-reutiliser-champ-existant-v1"></a>
## Pattern : Réutiliser un champ existant plutôt que créer un modèle dédié en V1
- Objectif : éviter la sur-ingénierie en V1 en réutilisant un champ existant quand le besoin est simple.
- Contexte : early-stage, besoin de stocker une configuration simple (URL, flag, valeur unique).
- Quand l'utiliser : quand la donnée a le même cycle de vie qu'un modèle existant et ne nécessite pas de relations.
- Quand l'éviter : si la configuration a son propre cycle de vie, des cardinalités multiples, ou des relations distinctes.
- Avantage : zéro migration supplémentaire, zéro scope creep
- Validé le : 17-03-2026
- Contexte technique : Prisma / Node.js — app-template-resto
### Règle
```txt
- Avant de créer un modèle ReservationConfig, vérifier si PublicHomeProfile.reservationUrl suffit
- Un champ optionnel dans le modèle le plus proche est suffisant en V1
- Ne créer un modèle dédié que si : cycle de vie distinct, relations, ou cardinalités multiples
```
---
<a id="pattern-validation-url-externe"></a>
## Pattern : Valider le protocole d'une URL externe avant de la passer à un lien public
- Objectif : prévenir les injections `javascript:` et URLs malformées dans les `<a href>` ou `<img src>` publics.
- Contexte : toute URL venant d'une config tenant, DB ou saisie utilisateur, rendue dans le HTML.
- Quand l'utiliser : systématiquement sur tout champ URL libre stocké en DB et rendu côté HTML.
- Risque si ignoré : injection `javascript:`, URL malformée, potentiel XSS.
- Validé le : 17-03-2026
- Contexte technique : Node.js / Next.js — app-template-resto
### Implémentation
```typescript
function isSafeUrl(url: string): boolean {
try {
const { protocol } = new URL(url);
return protocol === "https:" || protocol === "http:";
} catch {
return false;
}
}
// Validation complète en service/repository
if (mediaUrl) {
try { new URL(mediaUrl); } catch { throw new HttpError("URL invalide.", { status: 400 }); }
if (!mediaUrl.startsWith("https://") && !mediaUrl.startsWith("http://"))
throw new HttpError("URL doit commencer par https://.", { status: 400 });
if (mediaUrl.length > 500)
throw new HttpError("URL trop longue.", { status: 400 });
}
// Retourner null si invalide — le composant gère l'absence d'URL
```
### Checklist
- [ ] Validation format (`new URL()`) + protocole + longueur max
- [ ] Retourner `null` si invalide, jamais passer la string brute
- [ ] Composant UI reçoit `string | null`, jamais une string non vérifiée
---
<a id="pattern-utilitaires-purs-module-partage"></a>
## Pattern : Utilitaires purs — extraire dans un module sans `server-only`
- Objectif : permettre aux repositories et aux tests d'importer la même implémentation des utilitaires purs sans friction.
- Contexte : fonctions pures (slugify, formatters, validators) utilisées par des repositories qui ont `server-only`.
- Quand l'utiliser : dès qu'une fonction pure est utilisée dans un repository ET dans des tests.
- Risque si ignoré : logique dupliquée dans les tests qui diverge silencieusement de l'implémentation réelle.
- Validé le : 21-03-2026
- Contexte technique : Node.js / Next.js — app-template-resto
### Implémentation
```
src/server/menuAdmin/
allergensRepository.ts ← import { slugify } from "./slugify"
slugify.ts ← export function slugify() {} // pas de "server-only"
tests/
allergens-admin.test.ts ← import { slugify } from "../src/server/menuAdmin/slugify.ts"
```
---
<a id="pattern-en-enforcement-tenant"></a>
## Pattern : EN enforcement optionnel par tenant (toggle + publish gate)
- Objectif : permettre à un tenant d'activer l'obligation de remplir les champs traduits EN, avec une gate à la publication.
- Contexte : app multi-tenant avec internationalisation optionnelle.
- Quand l'utiliser : dès qu'un tenant peut choisir d'activer/désactiver une exigence de contenu i18n.
- Validé le : 21-03-2026
- Contexte technique : Prisma / Next.js App Router — app-template-resto
### Implémentation
```typescript
// 1. Modèle Tenant
enableEn Boolean @default(false)
// 2. Vérification dans chaque action mutante (create/update)
const { enableEn } = await getEnConfig(tenantId);
if (enableEn && !labelEn) throw new HttpError("Traduction EN requise.", { status: 400 });
// 3. Gate publish — vérification de complétude
const result = await checkEnCompleteness(tenantId); // 4 requêtes en Promise.all
// Exclut : isSystem:true, tenantId:null, isVisible:false
if (!result.complete) throw new HttpError("Contenu EN incomplet.", { status: 422 });
```
**Règles :**
- `isVisible: false` n'est pas inclus dans le check (une entité masquée ne bloque pas la publication)
- `revalidatePath` sur **toutes** les pages menu après toggle du flag (pas seulement `/settings`)
---
<a id="pattern-prisma-migration-manuelle-p3014"></a>
## Pattern : Prisma — Migration manuelle sans shadow DB (P3014)
- Objectif : créer et appliquer une migration Prisma quand la shadow database est interdite (DB managée, permissions restreintes).
- Contexte : DB managées — Supabase, PlanetScale, Railway avec rôle limité, RDS sans superuser.
- Quand l'utiliser : quand `prisma migrate dev` échoue avec `P3014 Prisma Migrate could not create the shadow database`.
- Risque si ignoré : blocage complet de la migration sur env managé.
- Validé le : 23-03-2026
- Contexte technique : Prisma v7+ — app-alexandrie / Supabase
### Implémentation
```bash
# 1. Écrire le SQL manuellement
mkdir -p prisma/migrations/<timestamp>_<nom>
# Créer migration.sql à la main
# 2. Appliquer le SQL directement en DB
npx prisma db execute --file prisma/migrations/<timestamp>_<nom>/migration.sql
# 3. Marquer la migration comme appliquée dans _prisma_migrations
npx prisma migrate resolve --applied <timestamp>_<nom>
# Note Prisma v7 : ne pas utiliser --schema= (option supprimée), utiliser prisma.config.ts
```
**Ne pas utiliser `prisma db push` en production** — il ne versionne pas les migrations.

View File

@@ -8,7 +8,7 @@ Ce fichier recense des risques back-end susceptibles de provoquer :
- régressions coûteuses, - régressions coûteuses,
- incohérences de données. - incohérences de données.
Dernière mise à jour : 20-03-2026 Dernière mise à jour : 23-03-2026
--- ---
@@ -49,6 +49,20 @@ Dernière mise à jour : 20-03-2026
- [NestJS 11 — `TooManyRequestsException` inexistante](#risque-nestjs-toomanyrequest) - [NestJS 11 — `TooManyRequestsException` inexistante](#risque-nestjs-toomanyrequest)
- [`ForbiddenException` utilisé pour des erreurs de validation](#risque-forbidden-pour-validation) - [`ForbiddenException` utilisé pour des erreurs de validation](#risque-forbidden-pour-validation)
- [PrismaService — getter explicite manquant sur nouveau modèle](#risque-prismaservice-getter-manquant) - [PrismaService — getter explicite manquant sur nouveau modèle](#risque-prismaservice-getter-manquant)
- [Endpoints GET sans contrôle d'accès sur ressource protégée](#risque-get-sans-controle-acces)
- [Divergence schéma Prisma / spec story (champ déclaré ✅ mais absent)](#risque-schema-divergence-spec-story)
- [Prisma initialisé au chargement de module — casse le build Next.js](#risque-prisma-init-module-build)
- [`server-only` dans les repositories — bloque les tests unitaires](#risque-server-only-repositories-tests)
- [Controller NestJS corrompu par insertions multiples](#risque-controller-corrompu-insertions)
- [TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)](#risque-ttl-redis-heure-locale)
- [Story "completed" avec tâches ❌ auto-déclarées](#risque-story-completed-taches-echec)
- [Story "done" sans aucun fichier source dans la File List](#risque-story-done-file-list-vide)
- [Prisma `$transaction` multi-tenant : écriture sans `tenantId` dans le WHERE (TOCTOU)](#risque-prisma-transaction-toctou-tenantid)
- [Prisma OR multi-tenant : `tenantId: null` manquant sur la branche système](#risque-prisma-or-tenantid-null)
- [Calcul de `nextOrder` hors transaction (race condition `sortOrder`)](#risque-nextorder-hors-transaction)
- [Redirect vers la page désactivée elle-même (boucle infinie feature flags)](#risque-redirect-boucle-infinie)
- [Champ `tenantId` sans FK ni relation Prisma vers `Tenant`](#risque-tenantid-sans-fk-relation)
- [NestJS `@UseGuards(AdminRoleGuard)` sans `@RequireAdminRole()` — silencieusement ouvert](#risque-adminroleguard-sans-decorateur)
--- ---
@@ -643,3 +657,378 @@ get forum() {
- **Checklist review** : à chaque nouvelle migration Prisma, vérifier que `prisma.service.ts` est mis à jour. - **Checklist review** : à chaque nouvelle migration Prisma, vérifier que `prisma.service.ts` est mis à jour.
- Contexte technique : NestJS / PrismaService encapsulé — app-alexandrie 20-03-2026 - Contexte technique : NestJS / PrismaService encapsulé — app-alexandrie 20-03-2026
---
<a id="risque-get-sans-controle-acces"></a>
## Endpoints GET sans contrôle d'accès sur ressource protégée
### Risques
- Un endpoint de lecture expose des données premium/protégées à tout utilisateur authentifié
- La règle "seuls les writes vérifient les droits" est un anti-pattern qui cause des fuites silencieuses
### Symptômes
- `getCategories`, `getThreads` ou équivalent accessible sans vérification d'entitlements
- Endpoint write protégé par `assertForumAccess` mais GET correspondant non protégé
### Bonnes pratiques / mitigations
- Tout endpoint retournant des données liées à une ressource protégée (forum pack, contenu premium) doit appeler `assertForumAccess` ou équivalent, même pour les GET
- **Checklist review** : pour chaque nouveau GET, vérifier qu'il passe par le guard/helper d'accès si la ressource appartient à un scope protégé
- Contexte technique : NestJS / app-alexandrie — 23-03-2026
---
<a id="risque-schema-divergence-spec-story"></a>
## Divergence schéma Prisma / spec story (champ déclaré ✅ mais absent)
### Risques
- Une tâche de story cochée ✅ implique un champ (ex: `consumedAt`, `tokenHash`) qui n'existe pas dans `schema.prisma`
- Le code compile ou passe en review sans que le champ soit réellement présent en DB
### Symptômes
- Erreur à l'exécution sur un champ inexistant malgré une story marquée "done"
- `schema.prisma` ne contient pas le champ mentionné dans les tâches
### Bonnes pratiques / mitigations
- Avant de marquer une tâche ✅, croiser avec `schema.prisma` pour confirmer que le champ existe réellement
- Une story peut décrire un champ comme stratégie de conception sans l'avoir intégré — toujours vérifier
- Contexte technique : Prisma / app-template-resto — 16-03-2026
---
<a id="risque-prisma-init-module-build"></a>
## Prisma initialisé au chargement de module — casse le build Next.js
### Risques
- Un import global qui initialise Prisma immédiatement peut faire échouer la collecte de pages/routes au build si `DATABASE_URL` n'est pas disponible dans l'environnement de build
### Symptômes
- `PrismaClientInitializationError` ou `Error: Environment variable not found: DATABASE_URL` au `next build`
- L'app tourne en dev mais le build CI échoue
### Bonnes pratiques / mitigations
- Préférer une initialisation lazy-safe : retarder l'accès DB au moment de l'appel métier
- Retourner un proxy qui lève une erreur claire uniquement lors du premier accès réel à la DB
- Ne jamais instancier `new PrismaClient()` au top-level d'un module importé par Next.js
- Contexte technique : Next.js App Router / Prisma — app-template-resto 16-03-2026
---
<a id="risque-server-only-repositories-tests"></a>
## `server-only` dans les repositories — bloque les tests unitaires
### Risques
- `import "server-only"` empêche l'exécution des fichiers hors runtime Next.js
- Les tests Node.js échouent avec `Error: This module cannot be imported from a Client Component module`
### Symptômes
- Tests qui passent via le dev server mais échouent via `jest` en mode node
- Erreur au `require()` d'un repository depuis un test unitaire
### Bonnes pratiques / mitigations
- Ne mettre `server-only` que dans les fichiers qui utilisent des APIs Next.js runtime (`cookies()`, `headers()`, `redirect()`)
- **Ne pas** mettre `server-only` dans les repositories purs (qui n'appellent que Prisma)
- Alternative de secours : créer un stub `node_modules/server-only/index.js` no-op pour les tests
- Contexte technique : Next.js App Router / Jest — app-template-resto 16-03-2026
---
<a id="risque-controller-corrompu-insertions"></a>
## Controller NestJS corrompu par insertions multiples
### Risques
- Des méthodes imbriquées, décorateurs orphelins ou routes dupliquées cassent la syntaxe TypeScript sans que le compilateur ne l'attrape toujours
- La story est marquée "completed" alors que le code ne compile pas
### Symptômes
- `@Get('/route')` apparaît dans le corps d'une autre méthode
- La même route est déclarée 2-3 fois dans le même controller
- Erreur NestJS au runtime mais pas à la compilation
### Bonnes pratiques / mitigations
- Quand on ajoute >3 endpoints à un controller existant, réécrire le fichier entier en partant du fichier original
- Ne jamais insérer par blocs séparés — la concaténation casse la structure AST
- **Checklist review** : grep `@Get\|@Post\|@Patch\|@Delete` dans le controller et vérifier qu'aucune route n'est dupliquée
- Contexte technique : NestJS / TypeScript — app-alexandrie 20-03-2026
---
<a id="risque-ttl-redis-heure-locale"></a>
## TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)
### Risques
- Le reset du quota journalier dérive selon le timezone du serveur, pouvant aller jusqu'à ±12h d'écart par rapport à minuit UTC
### Symptômes
- Quota qui se remet à zéro à des heures inattendues selon l'environnement de déploiement
- Comportement différent en dev local (TZ machine) et en prod (TZ container)
### Bonnes pratiques / mitigations
```typescript
// ✅ CORRECT — UTC midnight garanti
const midnight = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
);
const ttlMs = midnight.getTime() - now.getTime();
// ❌ RISQUÉ — heure locale du serveur
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
```
- Règle : tout `expireAt` ou `TTL` de quota journalier doit utiliser `Date.UTC()` — vérifier systématiquement en review
- Contexte technique : Redis / NestJS — app-alexandrie 20-03-2026
---
<a id="risque-story-completed-taches-echec"></a>
## Story "completed" avec tâches ❌ auto-déclarées
### Risques
- Un agent sette `Status: completed` alors que son propre Dev Agent Record liste des items ❌ non implémentés
- Le store mobile, service ou tests peuvent être déclarés manquants par l'agent lui-même mais la story semble terminée
### Symptômes
- Dev Agent Record contient `❌ store mobile non implémenté` mais `Status: completed`
- Code review découvre des ACs non satisfaits
### Bonnes pratiques / mitigations
- Avant de setter `Status: completed`, vérifier que le Dev Agent Record ne contient aucun ❌
- En cas de doute ou d'item manquant, setter `Status: review` pour déclencher la code review
- **Règle** : `Status: completed` = zéro ❌ auto-déclaré dans le Dev Agent Record
- Contexte technique : BMAD / workflow agent — app-alexandrie 20-03-2026
---
<a id="risque-story-done-file-list-vide"></a>
## Story "done" sans aucun fichier source dans la File List
### Risques
- Un agent peut halluciner la completion d'une story en produisant une note générique sans écrire de code
- La File List ne contient que des fichiers `_bmad-output/` mais aucun `src/`, `prisma/`, `tests/`
### Symptômes
- Completion note générique du type "Ultimate context engine analysis completed"
- File List réduite à 2 fichiers meta (story file, sprint-status)
- `git log --follow src/` ne montre aucun commit lié à la story
### Bonnes pratiques / mitigations
- Lors d'une code review, si la File List ne contient aucun fichier source : traiter comme non implémentée
- Vérifier avec `git log --follow src/` avant d'accepter le `Status: done`
- Ne pas faire confiance au status `done` sans preuve dans le code
- Contexte technique : BMAD / agent Codex — app-template-resto 21-03-2026
---
<a id="risque-prisma-transaction-toctou-tenantid"></a>
## Prisma `$transaction` multi-tenant : écriture sans `tenantId` dans le WHERE (TOCTOU)
### Risques
- Un pre-check d'appartenance tenant + une `$transaction` avec `update({ where: { id } })` sans `tenantId` crée une fenêtre TOCTOU
- Un bug upstream qui laisse passer un id cross-tenant peut contourner l'isolation
### Symptômes
- Vérification préalable OK mais écriture sur une ressource d'un autre tenant possible en race condition
- Le guard applicatif est passé mais la DB n'enforce pas au niveau de l'écriture
### Bonnes pratiques / mitigations
```typescript
// ❌ Anti-pattern — check OK mais écriture sans tenantId
const existing = await prisma.item.findMany({ where: { id: { in: ids }, tenantId } });
await prisma.$transaction(
ids.map((id, idx) => prisma.item.update({ where: { id }, data: { sortOrder: idx + 1 } }))
);
// ✅ Défense en profondeur — tenantId dans chaque écriture
await prisma.$transaction(
ids.map((id, idx) => prisma.item.updateMany({ where: { id, tenantId }, data: { sortOrder: idx + 1 } }))
);
```
- Règle : toute écriture Prisma sur une ressource tenant-aware doit inclure `tenantId` dans le WHERE, même dans une transaction précédée d'un check
- Utiliser `updateMany`/`deleteMany` (pas `update`/`delete`) pour inclure `tenantId` sans exception si 0 lignes
- Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026
---
<a id="risque-prisma-or-tenantid-null"></a>
## Prisma OR multi-tenant : `tenantId: null` manquant sur la branche système
### Risques
- Sur un modèle à `tenantId` nullable distinguant ressources "système" et "tenant", un filtre `{ isSystem: true }` sans `tenantId: null` expose des ressources corrompues à tous les tenants
### Symptômes
- Un tag `isSystem: true` avec `tenantId` non-null est exposé à tous les tenants
- Bug de sécurité difficile à détecter car le comportement nominal semble correct
### Bonnes pratiques / mitigations
```typescript
// ❌ Trop permissif
OR: [{ isSystem: true }, { tenantId, isSystem: false }]
// ✅ Défense en profondeur — double condition sur la branche système
OR: [{ isSystem: true, tenantId: null }, { tenantId, isSystem: false }]
```
- Règle : sur tout modèle `tenantId?` (nullable) + flag `isSystem`/`isGlobal`/`isPublic`, la branche "ressource publique" du filtre OR doit toujours inclure `tenantId: null`
- Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026
---
<a id="risque-nextorder-hors-transaction"></a>
## Calcul de `nextOrder` hors transaction (race condition `sortOrder`)
### Risques
- Deux requêtes concurrentes obtiennent le même `MAX(sortOrder)` et créent deux entités avec le même `sortOrder`
### Symptômes
- Deux items avec le même `sortOrder` dans la même catégorie/scope
- Bug aléatoire selon la charge — invisible en dev, présent en prod
### Bonnes pratiques / mitigations
```typescript
// ✅ Calcul dans la transaction interactive
return prisma.$transaction(async (tx) => {
const maxOrder = await tx.entity.aggregate({
where: { tenantId, scopeId },
_max: { sortOrder: true },
});
const nextOrder = (maxOrder._max.sortOrder ?? 0) + 1;
return tx.entity.create({ data: { ..., sortOrder: nextOrder } });
});
```
- Règle : ne jamais calculer `maxOrder` hors de la transaction qui crée l'entité
- Contexte technique : Prisma / transactions — app-template-resto 21-03-2026
---
<a id="risque-redirect-boucle-infinie"></a>
## Redirect vers la page désactivée elle-même (boucle infinie feature flags)
### Risques
- Une page désactivée redirige vers elle-même via le fallback — boucle infinie silencieuse absorbée par Next.js mais UX cassée
### Symptômes
- Page `/` désactivée → redirect vers `buildLocalizedPath("home")` = `/` → boucle
- Next.js absorbe la boucle mais l'utilisateur voit un écran bloqué ou vide
### Bonnes pratiques / mitigations
```typescript
// Si la page est sa propre destination de fallback, ne pas rediriger
if (pageKey === "home") return null; // évite redirect home → home
return buildLocalizedPath(locale, "home");
```
- Règle : dans tout mécanisme de redirection sur page désactivée, toujours vérifier que `pageKey !== fallbackKey`
- Retourner `null` (accès non bloqué) plutôt que de boucler
- Contexte technique : Next.js App Router / feature flags tenant — app-template-resto 17-03-2026
---
<a id="risque-tenantid-sans-fk-relation"></a>
## Champ `tenantId` sans FK ni relation Prisma vers `Tenant`
### Risques
- Un `tenantId TEXT NOT NULL` sans relation Prisma ne génère aucune FK en DB
- L'isolation multi-tenant n'est pas enforced au niveau base de données
### Symptômes
- Migration SQL sans `ALTER TABLE ... ADD CONSTRAINT ... REFERENCES "tenants"`
- Prisma ne génère pas de FK automatiquement sans `@relation` déclarée
### Bonnes pratiques / mitigations
Tout modèle tenant-scoped doit avoir les trois :
1. `tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)` dans le modèle Prisma
2. La relation inverse dans `Tenant` (ex: `menuCategories MenuCategory[]`)
3. La FK correspondante dans la migration SQL
- **Checklist review** : vérifier systématiquement que les nouveaux modèles respectent ce guardrail
- Contexte technique : Prisma / multi-tenant — app-template-resto 17-03-2026
---
<a id="risque-adminroleguard-sans-decorateur"></a>
## NestJS `@UseGuards(AdminRoleGuard)` sans `@RequireAdminRole()` — silencieusement ouvert
### Risques
- `AdminRoleGuard.canActivate()` lit la metadata `REQUIRE_ADMIN_ROLE_KEY` posée par `@RequireAdminRole()`
- Si le décorateur est absent, `requiresAdmin = false/undefined` → le guard retourne `true` et laisse passer sans vérification
### Symptômes
- Endpoint admin accessible à tout utilisateur authentifié
- Zéro erreur de compilation ou de démarrage — le bug est silencieux
### Bonnes pratiques / mitigations
```typescript
// ✅ Correct — les deux décorateurs ensemble
@Post('admin/ressource')
@UseGuards(AdminRoleGuard)
@RequireAdminRole()
async createRessource(...) {}
// ❌ Silencieusement non protégé — @RequireAdminRole() manquant
@Post('admin/ressource')
@UseGuards(AdminRoleGuard)
async createRessource(...) {}
```
- Règle : s'applique à tout guard NestJS qui délègue la décision à une metadata de décorateur
- **Checklist review** : vérifier systématiquement les endpoints admin que `@RequireAdminRole()` est présent
- Contexte technique : NestJS / guards metadata — app-alexandrie 23-03-2026

View File

@@ -12,7 +12,7 @@ Il sert de **mémoire durable** pour éviter :
- de redélibérer éternellement sur des sujets déjà tranchés, - de redélibérer éternellement sur des sujets déjà tranchés,
- de propager des “bonnes pratiques” théoriques non éprouvées. - de propager des “bonnes pratiques” théoriques non éprouvées.
Dernière mise à jour : 20-03-2026 Dernière mise à jour : 23-03-2026
--- ---
@@ -29,6 +29,11 @@ Dernière mise à jour : 20-03-2026
- [Tests de styles React Native sans renderer JSX](#pattern-tests-styles-sans-renderer) - [Tests de styles React Native sans renderer JSX](#pattern-tests-styles-sans-renderer)
- [Export des styles de composant pour réutilisation partielle](#pattern-export-styles-composant) - [Export des styles de composant pour réutilisation partielle](#pattern-export-styles-composant)
- [Token typography par usage sémantique (React Native)](#pattern-token-typography-semantique) - [Token typography par usage sémantique (React Native)](#pattern-token-typography-semantique)
- [Click-to-load strict pour les embeds tiers (iframe/widget)](#pattern-click-to-load-embeds-tiers)
- [Toggle optimiste avec rollback (React Server Action)](#pattern-toggle-optimiste-rollback)
- [Server Action retournant l'entité — élimination de `router.refresh()` sur create/edit](#pattern-server-action-retourne-entite)
- [ESLint flat config avec presets Next.js (`eslint.config.mjs`)](#pattern-eslint-flat-config-nextjs)
- [Grilles 2 colonnes FR/EN — mobile-first](#pattern-grilles-2-colonnes-mobile-first)
--- ---
@@ -634,3 +639,192 @@ mediumText12: { fontSize: 12, fontWeight: 500 }, // ambigu, réutilisé
- on met à jour la date - on met à jour la date
- on précise le nouveau contexte - on précise le nouveau contexte
- En cas de doute → le pattern nentre pas encore ici - En cas de doute → le pattern nentre pas encore ici
---
<a id="pattern-click-to-load-embeds-tiers"></a>
## Pattern : Click-to-load strict pour les embeds tiers (iframe/widget)
### Synthèse
- **Objectif** : ne charger aucun service tiers sans action explicite de lutilisateur (performance + consentement implicite).
- **Contexte** : site/webapp avec modules de réservation, map, chat ou tout embed iframe à la demande.
- **Quand lutiliser** : dès quun embed tiers est chargé à la demande (pas au premier rendu).
- **Quand léviter** : si lembed est central à la page et doit être visible immédiatement.
### Analyse
- **Avantages** :
- LCP non pollué par des tiers (performance-first)
- Aucun tiers ne reçoit de données utilisateur sans action volontaire (consentement implicite)
- Fallback toujours disponible en cas derreur iframe
- **Limites / vigilance** :
- Le fallback (lien externe + `tel:`) doit être actionnable même si lembed échoue
### Validation
- Validé le : 21-03-2026
- Contexte technique : React / Next.js — app-template-resto
### Implémentation
```tsx
const [loaded, setLoaded] = useState(false);
const [errored, setErrored] = useState(false);
if (errored) return <a href={url}>Ouvrir {label}</a>;
return (
<>
{!loaded && <button onClick={() => setLoaded(true)}>Charger {label}</button>}
{loaded && <iframe src={url} onError={() => setErrored(true)} />}
</>
);
```
---
<a id="pattern-toggle-optimiste-rollback"></a>
## Pattern : Toggle optimiste avec rollback (React Server Action)
### Synthèse
- **Objectif** : masquer la latence serveur sur un toggle boolean en mettant à jour lUI immédiatement, avec rollback en cas derreur.
- **Contexte** : toggles boolean (visibilité, disponibilité, settings) où la latence doit être masquée.
- **Quand lutiliser** : toggles sans besoin de re-fetcher lentité entière après mutation.
- **Quand léviter** : mutations qui retournent des données complexes → préférer le pattern "Server Action retournant lentité".
### Validation
- Validé le : 21-03-2026
- Contexte technique : React / Next.js App Router — app-template-resto
### Implémentation
```tsx
const [optimistic, setOptimistic] = useState(initialValue);
async function handleToggle() {
const prev = optimistic;
setOptimistic(!prev); // update immédiat
try {
await toggleAction(!prev);
router.refresh(); // synchronise le Server Component parent
} catch {
setOptimistic(prev); // rollback si erreur
}
}
```
---
<a id="pattern-server-action-retourne-entite"></a>
## Pattern : Server Action retournant lentité — élimination de `router.refresh()` sur create/edit
### Synthèse
- **Objectif** : mettre à jour létat local directement avec les données réelles retournées par le serveur, sans round-trip SSR supplémentaire.
- **Contexte** : liste ditems managée côté client (`useState`) avec création et modification via Server Actions.
- **Quand lutiliser** : create et edit dentités dans une liste. Plus performant que toggle optimiste + `router.refresh()`.
- **Quand léviter** : simples toggles boolean → le pattern optimiste avec rollback suffit.
### Analyse
- **Avantages vs toggle optimiste + `router.refresh()` :**
- Zéro aller-retour SSR supplémentaire (~500ms2s économisés sur mobile)
- État local garanti cohérent avec la DB (données réelles, pas calculées localement)
- Pas de flash de rechargement
- **Limites / vigilance** :
- `revalidatePath` reste nécessaire pour invalider le cache des pages publiques
### Validation
- Validé le : 22-03-2026
- Contexte technique : React / Next.js App Router — app-template-resto story 3.8
### Implémentation
```typescript
// Repository — retourne lentité complète
export async function createItem(tenantId: string, data: Input): Promise<ItemRow> {
return prisma.item.create({ data: { tenantId, ...data }, select: { ...fullSelect } });
}
// Action — retourne la donnée au client
export async function createItemAction(formData: FormData): Promise<ItemRow> {
const actor = await requireOwner();
const item = await createItem(actor.tenantId, input);
revalidatePath("/dashboard/..."); // invalider cache pages publiques
return item; // ← clé : retourner lentité
}
// Client — mise à jour locale sans round-trip SSR
const created = await createItemAction(formData);
setItems((prev) => [...prev, created]); // pas de router.refresh()
```
**Pour les entités avec relations :** utiliser un helper `findItemById(tenantId, id)` appelé après la mutation pour retourner la forme complète avec les relations résolues.
---
<a id="pattern-eslint-flat-config-nextjs"></a>
## Pattern : ESLint flat config avec presets Next.js (`eslint.config.mjs`)
### Synthèse
- **Objectif** : éviter les bugs de compatibilité de lancien `.eslintrc` avec Next.js récent.
- **Contexte** : projet Next.js récent utilisant déjà le flat config ESLint.
- **Quand lutiliser** : nouveau projet Next.js ou migration ESLint.
- **Quand léviter** : si le projet doit rester compatible avec des outils legacy ESLint.
### Validation
- Validé le : 16-03-2026
- Contexte technique : Next.js 16+ / ESLint flat config — app-template-resto
### Implémentation
```javascript
// eslint.config.mjs
import nextPlugin from "@next/eslint-plugin-next";
export default [
...nextPlugin.configs["core-web-vitals"],
...nextPlugin.configs["typescript"],
{
rules: {
// overrides ciblés ici
},
},
];
```
---
<a id="pattern-grilles-2-colonnes-mobile-first"></a>
## Pattern : Grilles 2 colonnes FR/EN — mobile-first
### Synthèse
- **Objectif** : afficher les champs FR + EN côte à côte sur desktop, en colonne unique sur mobile.
- **Contexte** : formulaires dashboard avec champs bilingues FR/EN côte à côte.
- **Quand lutiliser** : tout formulaire avec colonnes parallèles sur un projet mobile-first.
- **Quand léviter** : si les champs sont indépendants et nont pas de relation visuelle FR/EN.
### Validation
- Validé le : 22-03-2026
- Contexte technique : Tailwind CSS / React — app-template-resto
### Implémentation
```html
<!-- ✅ Mobile-first — colonne unique sur < 640px, 2 colonnes sur ≥ 640px -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input placeholder="Nom (FR)" />
<input placeholder="Name (EN)" />
</div>
<!-- ❌ À éviter — 2 colonnes trop étroites sur mobile -->
<div class="grid grid-cols-2 gap-4">...</div>
```

View File

@@ -7,7 +7,7 @@ Ce fichier recense des risques front-end susceptibles de provoquer :
- dette technique rapide, - dette technique rapide,
- régressions UX/perf/a11y. - régressions UX/perf/a11y.
Dernière mise à jour : 20-03-2026 Dernière mise à jour : 23-03-2026
--- ---
@@ -39,6 +39,26 @@ Dernière mise à jour : 20-03-2026
- [Dimensions d'image via tokens `spacing` (React Native)](#risque-dimensions-image-via-spacing) - [Dimensions d'image via tokens `spacing` (React Native)](#risque-dimensions-image-via-spacing)
- [Écran détail Expo Router — store vide en deep link / reload](#risque-store-vide-deep-link) - [Écran détail Expo Router — store vide en deep link / reload](#risque-store-vide-deep-link)
- [`useEffect` fetch — guard incomplet sur les états terminaux](#risque-useeffect-guard-incomplet) - [`useEffect` fetch — guard incomplet sur les états terminaux](#risque-useeffect-guard-incomplet)
- [Store Zustand : collections sans clé de contexte (navigation inter-contexte)](#risque-zustand-collection-sans-cle-contexte)
- [`useSearchParams()` sans `Suspense` casse le build Next.js App Router](#risque-usesearchparams-sans-suspense)
- [Type `ViewData` dupliqué entre couche serveur et composant UI (Next.js)](#risque-type-viewdata-duplique)
- [Composant React dans un fichier `.ts` — `React.createElement` workaround](#risque-composant-react-fichier-ts)
- [Double validation de segment dynamique App Router (layout + page)](#risque-double-validation-segment-app-router)
- [Faux test négatif — tester le helper au lieu de tester l'exclusion](#risque-faux-test-negatif)
- [État booléen UI dérivé hardcodé au lieu d'être calculé depuis le store](#risque-boolean-ui-hardcode-store)
- [Flag `isLoading` unique pour des opérations de natures différentes](#risque-flag-isloading-unique-nature-differente)
- [Consent state : `false` ambigu entre "pas de décision" et "refus explicite"](#risque-consent-state-false-ambigu)
- [Script inline : interpolation directe au lieu de `JSON.stringify`](#risque-script-inline-interpolation-directe)
- [Next.js App Router : `window.location.reload()` au lieu de `router.refresh()`](#risque-window-location-reload-nextjs)
- [`useTransition` + optimistic update : snapshot capturé après `setState`](#risque-usetransition-snapshot-apres-setstate)
- [`window.confirm()` dans une app React/Next.js](#risque-window-confirm-react)
- [`import type` depuis `src/server/**` dans un composant client](#risque-import-type-server-composant-client)
- [Inline styles dans les composants dashboard](#risque-inline-styles-dashboard)
- [Classes Tailwind invalides courantes (bugs silencieux)](#risque-tailwind-classes-invalides)
- [Next.js : `<img>` natif interdit dans les composants](#risque-img-natif-nextjs)
- [`useTransition` global pour des listes d'items interactifs](#risque-usetransition-global-liste-items)
- [`useCallback` inutile quand le callback est wrappé en inline au render](#risque-usecallback-inutile-inline)
- [Formulaire React avec `defaultValue` sans `key` prop](#risque-formulaire-defaultvalue-sans-key)
--- ---
@@ -440,3 +460,553 @@ if (forums.length > 0 || isLoading || paywallRequired) return;
**Règle** : les états "zéro résultat intentionnel" (liste vide + flag métier) doivent être traités comme "données présentes" dans le guard de fetch. **Règle** : les états "zéro résultat intentionnel" (liste vide + flag métier) doivent être traités comme "données présentes" dans le guard de fetch.
- Contexte technique : React Native / Zustand / Expo Router — app-alexandrie story 4.1, 20-03-2026 - Contexte technique : React Native / Zustand / Expo Router — app-alexandrie story 4.1, 20-03-2026
---
<a id="risque-zustand-collection-sans-cle-contexte"></a>
## Store Zustand : collections sans clé de contexte (navigation inter-contexte)
### Risques
- Un store qui stocke des collections dépendant d'un paramètre de navigation (forumSlug, threadId...) sans stocker ce paramètre affiche des données périmées lors d'une navigation inter-contexte
### Symptômes
- Naviguer du forum A vers le forum B affiche encore les catégories/threads du forum A
- Guard `if (items.length > 0) return` empêche le rechargement lors d'un changement de contexte
### Bonnes pratiques / mitigations
- Stocker la clé de contexte avec les données : `categoriesForumSlug: string | null`
- Invalider si `categoriesForumSlug !== currentForumSlug` avant de retourner depuis le cache
- Ou supprimer le guard et dépendre uniquement du changement de paramètre dans le `useEffect`
- Contexte technique : React Native / Zustand / Expo Router — app-alexandrie 23-03-2026
---
<a id="risque-usesearchparams-sans-suspense"></a>
## `useSearchParams()` sans `Suspense` casse le build Next.js App Router
### Risques
- Un composant client utilisant `useSearchParams()` peut provoquer un échec de prerender/build s'il est rendu sans boundary `Suspense` depuis la page/layout serveur
### Symptômes
- `Error: useSearchParams() should be wrapped in a suspense boundary` au `next build`
- Fonctionne en dev mais échoue à la CI/CD
### Bonnes pratiques / mitigations
- Isoler le composant client qui utilise `useSearchParams()` et le rendre sous `<Suspense fallback={...}>` au niveau de la page
- Ne jamais appeler `useSearchParams()` directement dans un composant rendu sans `Suspense` depuis un Server Component
- Contexte technique : Next.js App Router récent / Turbopack — app-template-resto 16-03-2026
---
<a id="risque-type-viewdata-duplique"></a>
## Type `ViewData` dupliqué entre couche serveur et composant UI (Next.js)
### Risques
- TypeScript accepte deux structures identiques par structural typing — si le type source évolue, la couche UI reste désynchronisée sans erreur de compilation tant que les formes correspondent
### Symptômes
- Deux définitions du même type dans `src/server/` et `src/app/`
- Champ ajouté côté serveur mais absent dans le composant UI sans warning
### Bonnes pratiques / mitigations
```typescript
// ✅ La couche UI importe et re-exporte
export type { PublicHomeViewData } from "@/server/public/getPublicHomeData";
// ❌ À éviter — redéfinition locale
export type PublicHomeViewData = { tenantName: string; ... };
```
- Règle : le type appartient à la couche qui le produit. La couche UI importe uniquement.
- Contexte technique : Next.js App Router / TypeScript — app-template-resto 16-03-2026
---
<a id="risque-composant-react-fichier-ts"></a>
## Composant React dans un fichier `.ts` — `React.createElement` workaround
### Risques
- Code illisible vs JSX natif
- Fausse impression que le fichier est "sans JSX" — peut tromper les outils de linting et les reviewers
- Empêche l'utilisation de la syntaxe JSX si on doit ajouter des enfants complexes
### Symptômes
- `React.createElement(...)` dans un fichier `.ts`
### Bonnes pratiques / mitigations
- Tout fichier exportant une fonction retournant un `ReactElement` ou utilisant React doit avoir l'extension `.tsx`
- Sans exception
- Contexte technique : TypeScript / React — app-template-resto 16-03-2026
---
<a id="risque-double-validation-segment-app-router"></a>
## Double validation de segment dynamique App Router (layout + page)
### Risques
- Si le layout fait `notFound()` sur un segment invalide ET que la page répète la même condition, les deux deviennent désynchronisés silencieusement lors d'une modification
### Symptômes
- Même condition de validation dans `layout.tsx` et `page.tsx` d'un même segment
- Modification du layout n'est pas reportée dans la page (comportement divergent)
### Bonnes pratiques / mitigations
- Si le layout garde, la page consomme — une seule responsabilité par couche
- La page doit faire confiance à son layout parent
- **Règle** : un seul composant est responsable de la garde sur un segment dynamique
- Contexte technique : Next.js App Router — app-template-resto 17-03-2026
---
<a id="risque-faux-test-negatif"></a>
## Faux test négatif — tester le helper au lieu de tester l'exclusion
### Risques
- Un test nommé "X n'utilise pas Y" qui appelle Y en interne est un test normal mal documenté, pas un test d'exclusion
- Donne une fausse confiance sur le comportement par défaut du helper
### Symptômes
- Test intitulé "sans fallback, la valeur EN vide n'est pas remplacée" mais qui appelle le helper avec fallback activé
### Bonnes pratiques / mitigations
- Un vrai test négatif vérifie que X n'importe pas Y, ou que le comportement par défaut empêche l'effet indésirable
- Pour un helper à fallback optionnel : tester explicitement le cas `fallbackToFr=false` (défaut) avec une valeur vide
- Contexte technique : TypeScript / Jest — app-template-resto 17-03-2026
---
<a id="risque-boolean-ui-hardcode-store"></a>
## État booléen UI dérivé hardcodé au lieu d'être calculé depuis le store
### Risques
- Un état toggle (`isBookmarked`, `isLiked`, `isFollowed`) initialisé à `false` en dur ne reflète jamais l'état réel
- Le bouton est toujours en mode "ajouter" sans jamais passer en mode "supprimer"
### Symptômes
- `const isBookmarked = false; // état local géré ci-dessous via state`
- Bouton bookmark/like toujours dans le même état visuel peu importe l'état réel
### Bonnes pratiques / mitigations
```typescript
// ❌ Anti-pattern — état hardcodé
const isBookmarked = false;
// ✅ Pattern correct — dérivé du store au rendu
const { bookmarks } = useCommunityStore();
const isBookmarked = bookmarks.some((b) => b.thread.id === threadId);
```
- Règle : si le store contient la liste (bookmarks, likes, follows), l'état booléen se dérive avec `.some()` ou `.has()`
- Contexte technique : React Native / Zustand — app-alexandrie story 4.4, 20-03-2026
---
<a id="risque-flag-isloading-unique-nature-differente"></a>
## Flag `isLoading` unique pour des opérations de natures différentes
### Risques
- Un même flag (ex: `isBookmarking`) utilisé à la fois pour les mutations (add/remove) et le chargement de la liste provoque des bugs visuels — spinner manquant au premier chargement si une mutation est en cours en parallèle
### Symptômes
- Spinner absent au premier chargement de la liste bookmarks
- Bouton "ajouter" désactivé alors qu'aucune mutation n'est en cours
### Bonnes pratiques / mitigations
```typescript
// ❌ Anti-pattern — un seul flag pour tout
isBookmarking: boolean;
// ✅ Pattern correct — séparation claire
isBookmarking: boolean; // mutations add/remove
isLoadingBookmarks: boolean; // chargement de la liste (GET)
```
- Contexte technique : React Native / Zustand — app-alexandrie story 4.4, 20-03-2026
---
<a id="risque-consent-state-false-ambigu"></a>
## Consent state : `false` ambigu entre "pas de décision" et "refus explicite"
### Risques
- Sans champ `decided`, `analytics: false` peut signifier "première visite" ou "refus explicite" — indistinguables
- Le banner de consentement réapparaît à chaque visite après un refus, violant l'AC de persistance du choix
### Symptômes
- Banner qui réapparaît après rechargement malgré un refus explicite
### Bonnes pratiques / mitigations
```typescript
type ConsentState = {
analytics: boolean;
decided: boolean; // true = l'utilisateur a fait un choix (cookie présent)
};
const DEFAULT: ConsentState = { analytics: false, decided: false };
// À la lecture du cookie :
if (!cookieValue) return DEFAULT; // decided=false (première visite)
return { analytics: parsed.analytics, decided: true };
```
- L'état initial du banner doit être `!decided`, pas `!analytics`
- Contexte technique : Next.js / cookies — app-template-resto 21-03-2026
---
<a id="risque-script-inline-interpolation-directe"></a>
## Script inline : interpolation directe au lieu de `JSON.stringify`
### Risques
- Injection XSS potentielle via une valeur de configuration interpolée directement dans un `<Script>` inline
- La regex de validation en amont peut évoluer et laisser passer des valeurs dangereuses
### Symptômes
- `` {`gtag('config', '${measurementId}');`} `` — interpolation directe sans échappement
### Bonnes pratiques / mitigations
```tsx
// ❌ Anti-pattern — interpolation directe
{`gtag('config', '${measurementId}');`}
// ✅ Pattern correct — JSON.stringify garantit l'échappement
{`gtag('config', ${JSON.stringify(measurementId)});`}
```
- S'applique aussi aux `dangerouslySetInnerHTML` et aux attributs `data-*` injectés en JS
- Contexte technique : Next.js / `<Script>` — app-template-resto 21-03-2026
---
<a id="risque-window-location-reload-nextjs"></a>
## Next.js App Router : `window.location.reload()` au lieu de `router.refresh()`
### Risques
- Full reload = perd l'état React, navigation complète, plus lent
- `router.refresh()` est l'outil idoine : retrigger le fetch des Server Components sans détruire l'état client
### Symptômes
- `window.location.reload()` après un Server Action dans un Client Component
- Flash de rechargement visible, perte de l'état local (scroll, focus, état de formulaire)
### Bonnes pratiques / mitigations
```tsx
// ❌ Anti-pattern — full reload
await createCategoryAction(formData);
window.location.reload();
// ✅ Pattern correct — RSC diff, préserve l'état client
const router = useRouter();
await createCategoryAction(formData);
router.refresh();
```
- `router.refresh()` refetch uniquement les Server Components affectés (via `revalidatePath`) et applique un diff. L'état des Client Components est préservé.
- Contexte technique : Next.js App Router — app-template-resto 21-03-2026
---
<a id="risque-usetransition-snapshot-apres-setstate"></a>
## `useTransition` + optimistic update : snapshot capturé après `setState`
### Risques
- Stale closure classique : le snapshot est capturé après `setState`, donc `categories` peut déjà référencer la nouvelle liste au moment du rollback
### Symptômes
- Rollback optimiste qui ne restaure pas l'ancienne valeur
- Après une erreur serveur, l'état reste sur la nouvelle liste au lieu de revenir à l'état précédent
### Bonnes pratiques / mitigations
```tsx
// ❌ Anti-pattern — snapshot capturé après setState
const newList = [...categories];
setCategories(newList);
startTransition(async () => {
try { await action(); }
catch { setCategories(categories); } // peut être newList
});
// ✅ Pattern correct — snapshot AVANT toute mutation d'état
const snapshot = categories; // capturer AVANT setCategories
setCategories(newList);
startTransition(async () => {
try { await action(); }
catch { setCategories(snapshot); } // rollback garanti
});
```
- **Règle** : toujours assigner le snapshot dans un `const` **avant** le premier `setState`
- Contexte technique : React / Next.js App Router — app-template-resto 21-03-2026
---
<a id="risque-window-confirm-react"></a>
## `window.confirm()` dans une app React/Next.js
### Risques
- Bloque le thread principal
- Ne fonctionne pas en SSR
- Non stylable, UX mobile mauvaise
### Symptômes
- `if (!confirm("Supprimer ?")) return;` dans un Client Component
### Bonnes pratiques / mitigations
```tsx
// ❌ Anti-pattern
if (!confirm("Supprimer ?")) return;
// ✅ Pattern correct — confirmation inline via état React
const [deletingId, setDeletingId] = useState<string | null>(null);
{deletingId === item.id && (
<div>
<span>Supprimer « {item.label} » ?</span>
<button onClick={() => { setDeletingId(null); doDelete(item.id); }}>Confirmer</button>
<button onClick={() => setDeletingId(null)}>Annuler</button>
</div>
)}
```
- S'applique aussi à `window.alert()` et `window.prompt()`
- Contexte technique : React / Next.js — app-template-resto 21-03-2026
---
<a id="risque-import-type-server-composant-client"></a>
## `import type` depuis `src/server/**` dans un composant client
### Risques
- Violation de boundary même si l'import est type-only (effacé à la compilation)
- Ouvre la porte à des imports runtime si le code est refactoré rapidement
- La règle ESLint `no-restricted-imports` doit couvrir les `import type` aussi
### Symptômes
- `import type { Foo } from "@/server/..."` dans un fichier `"use client"`
- Passe en review car le compilateur ne bloque pas les type-only imports
### Bonnes pratiques / mitigations
- Types partagés entre server et client doivent vivre dans `src/types/` ou `src/lib/`
- Configurer `no-restricted-imports` avec `allowTypeImports: false` pour les paths serveur
- Contexte technique : Next.js App Router / TypeScript — app-template-resto 22-03-2026
---
<a id="risque-inline-styles-dashboard"></a>
## Inline styles dans les composants dashboard
### Risques
- Contourne le système Tailwind + tokens CSS
- Crée des incohérences visuelles non détectées par le linter
### Symptômes
- `style={{ color: '#123456', marginTop: 8 }}` dans un composant dashboard
### Bonnes pratiques / mitigations
- Bloquer en code review systématiquement tout `style={{...}}` dans les composants dashboard
- Exception acceptable uniquement : animations CSS dynamiques (valeurs calculées au runtime)
- Contexte technique : React / Tailwind — app-template-resto 22-03-2026
---
<a id="risque-tailwind-classes-invalides"></a>
## Classes Tailwind invalides courantes (bugs silencieux)
### Risques
- Classes Tailwind invalides sont silencieusement ignorées — aucun warning, comportement visuellement cassé
### Symptômes
- Item masqué affiché à pleine opacité (`opacity-55` → invalide)
- Largeur incorrecte (`w-35` → invalide)
### Bonnes pratiques / mitigations
Erreurs courantes :
- `opacity-55` → invalide. Scale : 0/5/10/20/25/30/40/50/60/70/75/80/90/95/100 → utiliser `opacity-50` ou `opacity-60`
- `w-35` → invalide. Scale saute de `w-32` à `w-36` → utiliser `w-36`
- `box-border` → redondant. Tailwind Preflight applique déjà `box-sizing: border-box` globalement
- Toujours vérifier les classes custom/non-standard avec l'extension Tailwind IntelliSense
- Contexte technique : Tailwind CSS — app-template-resto 22-03-2026
---
<a id="risque-img-natif-nextjs"></a>
## Next.js : `<img>` natif interdit dans les composants
### Risques
- Warning ESLint `@next/next/no-img-element` → avec `--max-warnings=0` : erreur CI
- Pas de lazy loading, pas d'optimisation WebP, risque de layout shift (CLS)
### Symptômes
- `<img src="..." />` dans un composant Next.js
### Bonnes pratiques / mitigations
- Toujours utiliser `<Image>` de `next/image` à la place
- Exception acceptable : composants de test ou storybook uniquement
- Contexte technique : Next.js / ESLint — app-template-resto 22-03-2026
---
<a id="risque-usetransition-global-liste-items"></a>
## `useTransition` global pour des listes d'items interactifs
### Risques
- `isPending` global désactive **tous** les boutons de tous les items pendant qu'une opération est en cours sur un seul item
- Sur mobile : UX bloquée, impossible d'agir pendant qu'une autre opération tourne
### Symptômes
- Clic sur "Masquer" pour l'item A → boutons des items B et C grisés
### Bonnes pratiques / mitigations
```tsx
// ❌ Avant — bloque tout
const [isPending, startTransition] = useTransition();
// render : disabled={isPending}
// ✅ Après — per-item
const [pendingId, setPendingId] = useState<string | null>(null);
function handleToggle(id: string) {
setPendingId(id);
(async () => {
try { await toggleAction(id); }
catch (err) { handleError(err); }
finally { setPendingId(null); }
})();
}
// render : disabled={pendingId === item.id}
```
**Règles :**
- `pendingId === item.id` pour les boutons d'item (désactive uniquement l'item en cours)
- `pendingId !== null` pour les boutons globaux (ex: "Ajouter")
- `finally` garantit la réinitialisation même en cas d'erreur
- Contexte technique : React / Next.js — app-template-resto 22-03-2026
---
<a id="risque-usecallback-inutile-inline"></a>
## `useCallback` inutile quand le callback est wrappé en inline au render
### Risques
- Le handler stable est re-wrappé dans une arrow inline lors du passage en prop → nouvelle référence à chaque render → `React.memo` ne peut pas éviter le re-render
### Symptômes
```tsx
const handleToggle = useCallback((id: string) => { ... }, []); // stable ✓
// Mais au render :
<ItemCard onToggle={() => handleToggle(item.id)} />
// ↑ nouvelle closure à chaque render → memo inutile
```
### Bonnes pratiques / mitigations
- `useCallback` n'a de valeur que si le callback est passé **directement** en prop, sans re-wrapping
- Si la signature doit capturer des variables de boucle, deux options :
1. Passer les données en props et laisser l'enfant appeler le handler avec ses propres props
2. Accepter que `memo` ne soit pas protégé et supprimer le `useCallback` inutile
- Ne pas laisser un `useCallback` "pour faire bien" si son effet réel est nul
- Contexte technique : React — app-template-resto 22-03-2026
---
<a id="risque-formulaire-defaultvalue-sans-key"></a>
## Formulaire React avec `defaultValue` sans `key` prop
### Risques
- `defaultValue`, `defaultChecked`, `defaultSelected` ne s'appliquent qu'au montage
- Si le composant est réutilisé (même nœud DOM, nouvelle prop) sans être démonté, les valeurs ne se mettent pas à jour
### Symptômes
- L'utilisateur édite l'entité A, clique sur "Modifier" pour l'entité B → le formulaire affiche encore les données de A
### Bonnes pratiques / mitigations
```tsx
// Fix obligatoire : key unique basée sur l'ID de l'entité éditée
<EntityForm
key={formState.mode === "edit" ? formState.entity.id : `create-${formState.contextId}`}
...
/>
```
- **Règle** : tout formulaire d'édition réutilisé pour plusieurs entités doit avoir une `key` distincte par entité
- Contexte technique : React / Next.js — app-template-resto 21-03-2026

View File

@@ -138,3 +138,39 @@ constructor() {
### Alternative écartée ### Alternative écartée
`nest start --watch` a été testé mais a introduit des conflits ESM/CJS dans ce contexte (`exports is not defined`). `nest start --watch` a été testé mais a introduit des conflits ESM/CJS dans ce contexte (`exports is not defined`).
---
## `export { fn }` ne constitue pas un import local — détecté uniquement au build
### Contexte
Projet `app-template-resto`, story 2-4, le 17-03-2026.
Dans `getPublicHomeData.ts`, la fonction `resolvePublicTenantSelection` avait été déplacée dans `src/server/tenant/resolvePublicTenant.ts` et re-exportée depuis l'ancien emplacement.
### Symptômes
- `Cannot find name 'resolvePublicTenantSelection'` au `next build` uniquement
- ESLint et `tsc` (hors build) ne signalaient rien
- La fonction était utilisée localement dans le même fichier qui la re-exportait
### Cause
```typescript
// getPublicHomeData.ts
export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";
// puis, plus bas dans le même fichier :
const result = resolvePublicTenantSelection(env); // ← NameError au build
```
Un re-export (`export { fn } from "..."`) ne crée pas de binding local dans le fichier. La fonction est exportée vers l'extérieur mais n'est pas disponible comme variable locale.
### Correctif / règle à retenir
Si une fonction est utilisée dans le même fichier qui la re-exporte, ajouter un `import` séparé en plus du `export` :
```typescript
import { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";
export { resolvePublicTenantSelection }; // pour les appelants externes
```

File diff suppressed because it is too large Load Diff