diff --git a/00_INDEX.md b/00_INDEX.md index 21afc3d..1937335 100644 --- a/00_INDEX.md +++ b/00_INDEX.md @@ -48,6 +48,7 @@ Organisés dans `knowledge/` par domaine. Chaque domaine a un sous-dossier `patt | UX | `knowledge/ux/patterns/` | `knowledge/ux/risques/` | | n8n | `knowledge/n8n/patterns/` | `knowledge/n8n/risques/` | | Product | `knowledge/product/patterns/` | `knowledge/product/risques/` | +| Infra | `knowledge/infra/patterns/` | `knowledge/infra/risques/` | | Workflow | — | `knowledge/workflow/risques/` | --- diff --git a/90_debug_et_postmortem.md b/90_debug_et_postmortem.md index 3e37f10..86d1c23 100644 --- a/90_debug_et_postmortem.md +++ b/90_debug_et_postmortem.md @@ -330,3 +330,56 @@ ALTER INDEX "tronc_entries_tenue_idx" RENAME TO "tronc_entries_tenue_id_idx"; - **Préventif** : `prisma migrate diff` régulièrement (CI/CD ou pré-commit) pour détecter la dérive AVANT qu'elle ne pollue une migration thématique - **Curatif** : inspecter manuellement le SQL généré par `--create-only` avant de l'appliquer en migration thématique + +--- + +## Réseau Docker partagé entre stacks supprimé par `compose down` malgré `external: true` + +### Contexte + +NUC sous Proxmox, le 22-04-2026. Plusieurs stacks Docker partagent un réseau commun : un Postgres mutualisé (`shared-postgres`) auquel se connectent plusieurs apps via leur IP Tailscale. Chaque stack déclare le réseau en `external: true` dans son `docker-compose.yml`. Après une coupure de courant et redémarrage de la stack, le conteneur Postgres tournait en `Up (healthy)` mais plus aucune app ne pouvait s'y connecter. + +### Symptômes + +- Conteneur `shared-postgres` en `Up (healthy)`, mais `docker port ` retourne vide et `docker inspect ... NetworkSettings.Ports` retourne `{}`. +- Les autres stacks connectées au réseau ont perdu toute connectivité. + +### Cause + +Double piège : + +1. **`external: true` ne protège pas toujours contre la destruction.** Un `docker compose down` exécuté plus tôt a supprimé le réseau `shared-postgres-net` malgré son `external: true`, parce qu'aucun autre projet ne le "possédait" au moment de la commande. Le flag `external` protège contre la *création*, pas toujours contre la *destruction* — surtout quand le réseau a initialement été créé par un `compose up` (il porte alors les labels Compose et appartient au projet). +2. **Config figée à la création.** Le conteneur relancé par `restart: unless-stopped` garde sa config figée au moment de sa création : les modifications ultérieures du `docker-compose.yml` (ajout du bloc `ports:`, changement de réseau) ne sont pas prises en compte tant qu'on ne fait pas `up --force-recreate`. Le conteneur s'est donc recréé sans réseau ni mapping de ports. + +### Correctif / règle à retenir + +**Un réseau Docker partagé entre plusieurs stacks doit être créé explicitement hors compose, pas par une stack.** + +```bash +docker network create shared--net +``` + +Le réseau n'a alors aucun label Compose, donc aucun `compose down` ne peut le supprimer. + +**Figer le nom de projet Compose.** Par défaut, le nom de projet Compose = nom du dossier. Si le `container_name` diffère du nom de dossier (ex. dossier `postgres/` mais `container_name: shared-postgres`), un `compose down` peut "ne rien voir à supprimer" car il cherche dans le mauvais projet. Solution : déclarer `name:` au top-level : + +```yaml +name: shared-postgres + +services: + postgres: + container_name: shared-postgres + ... +``` + +**Règles opérationnelles pour les stacks partagées critiques :** + +- Ne jamais faire `docker compose down` sur la stack qui "porte" le réseau partagé. +- Utiliser `stop` / `start` / `up -d --force-recreate` pour les maintenances. +- Documenter la procédure de recréation du réseau dans le README de la stack. + +### Signal de détection + +Conteneur `Up (healthy)` mais `docker port ` vide, ou `docker inspect ... NetworkSettings.Ports` à `{}` → le conteneur a été recréé sans sa config de ports à jour. Fix : `compose up -d --force-recreate` (après avoir vérifié que le réseau externe existe). + +> Risque connexe côté DNS Docker : voir `knowledge/infra/risques/docker.md`. diff --git a/95_a_capitaliser.md b/95_a_capitaliser.md index 0389eb4..ae46619 100644 --- a/95_a_capitaliser.md +++ b/95_a_capitaliser.md @@ -20,6 +20,8 @@ vers les fichiers appropriés : - `knowledge/n8n/risques/general.md` - `knowledge/product/patterns/general.md` - `knowledge/product/risques/.md` +- `knowledge/infra/patterns/.md` +- `knowledge/infra/risques/.md` - `knowledge/workflow/patterns/general.md` - `knowledge/workflow/risques/story-tracking.md` - `10_conventions_redaction.md` @@ -38,7 +40,7 @@ Chaque proposition doit suivre ce format : DATE — PROJET FILE_UPDATE_PROPOSAL -Fichier cible : .md | knowledge/backend/risques/.md | knowledge/frontend/patterns/.md | knowledge/frontend/risques/.md | knowledge/ux/patterns/.md | knowledge/ux/risques/.md | knowledge/n8n/patterns/general.md | knowledge/n8n/risques/general.md | knowledge/product/patterns/general.md | knowledge/product/risques/.md | knowledge/workflow/patterns/general.md | knowledge/workflow/risques/story-tracking.md | 10_conventions_redaction.md | 40_decisions_et_archi.md | 90_debug_et_postmortem.md> +Fichier cible : .md | knowledge/backend/risques/.md | knowledge/frontend/patterns/.md | knowledge/frontend/risques/.md | knowledge/ux/patterns/.md | knowledge/ux/risques/.md | knowledge/n8n/patterns/general.md | knowledge/n8n/risques/general.md | knowledge/product/patterns/general.md | knowledge/product/risques/.md | knowledge/infra/patterns/.md | knowledge/infra/risques/.md | knowledge/workflow/patterns/general.md | knowledge/workflow/risques/story-tracking.md | 10_conventions_redaction.md | 40_decisions_et_archi.md | 90_debug_et_postmortem.md> Pourquoi : diff --git a/_projects.conf b/_projects.conf index cc8849a..532f291 100644 --- a/_projects.conf +++ b/_projects.conf @@ -50,5 +50,6 @@ # - automatiser certains audits inter-projets # # Les lignes commençant par # sont ignorées. -app-alexandrie|NestJS + Expo (React Native) + Prisma + pnpm monorepo|mindleaf|Epic 8 en cours +app-alexandrie|NestJS + Expo (React Native) + Prisma + pnpm monorepo|mindleaf|Epic 1 en cours app-template-resto|Next.js Prisma+PostgreSQL|perso|Epic 4 en cours +app-rl799|pnpm monorepo Prisma+PostgreSQL|perso|Epic 12 en cours diff --git a/knowledge/backend/patterns/README.md b/knowledge/backend/patterns/README.md index 3d30c72..42a3784 100644 --- a/knowledge/backend/patterns/README.md +++ b/knowledge/backend/patterns/README.md @@ -9,12 +9,13 @@ Avant toute proposition backend, identifie le fichier dont le nom et la descript | Fichier | Domaine | Entrées clés | |---------|---------|--------------| | `auth.md` | Auth, sessions, tokens, erreurs API, corrélation | Format erreur standardisé, middleware requestId, anti-énumération, token usage unique, autorisation interne, opérations atomiques | -| `contracts.md` | Contrats API, Zod, error codes, HTTP sémantique | Contracts-First/Zod-Infer/No-DTO, error codes comme contrat, HTTP 200 payload métier | +| `contracts.md` | Contrats API, Zod, error codes, HTTP sémantique | Contracts-First/Zod-Infer/No-DTO, error codes comme contrat, HTTP 200 payload métier, idempotence POST = retour ressource existante | | `prisma.md` | Prisma, DB, migrations, pagination | Soft delete, pagination cursor, idempotency key, P2002 unique, Decimal sérialisation, migration manuelle P3014, filtrage métier dans service | | `stripe.md` | Stripe, paiements, webhooks entrants, subscriptions | Provider-Strategy, metadata subscription_data, parsing webhook unique, restauration achats, Trial vs Paid | | `nestjs.md` | NestJS, guards, Redis, quotas | Guard global APP_GUARD, RedisHealthService cache court, quota INCR+EXPIREAT atomique | | `multi-tenant.md` | Multi-tenant, isolation, feature flags | 403 vs 404, repository tenant-aware, tenantId dans updates, helper tenant partagé, feature flag tenant, EN enforcement | | `nextjs.md` | Next.js App Router, Server Actions, isolation | Runtime-only logique pure, server-only isolation, utilitaires purs sans server-only, réutiliser champ V1, validation URL externe | -| `async.md` | Jobs async, webhooks sortants, queues | Exécution asynchrone outbox light, webhooks sortants HMAC + retries idempotents, hooks fire-and-forget après création DB, fanout notification avec filtre grade, auto-purge fenêtre temporelle SQL | +| `async.md` | Jobs async, webhooks sortants, queues | Exécution asynchrone outbox light, webhooks sortants HMAC + retries idempotents, hooks fire-and-forget après création DB, fanout notification avec filtre grade, auto-purge fenêtre temporelle SQL, test de rollback pipeline multi-fichiers atomique | | `general.md` | Architecture générale, helpers, RBAC | Helper auth centralisé enrichissable, ordre canonique des gates HTTP, délégation agrégat → endpoint agrégé, anti-énumération DELETE 204, lazy init memoizé, cap LRU par-user, convention dot-notation audit, whitelist explicite audit, singleton DB config, invalidation cache avant mutation, pipeline CI/CD GitHub Actions → VPS | | `tests.md` | Tests d'intégration DB, isolation, atomicité | `cleanup.track()` LIFO, `globalSetup` purge, template database Postgres, helper `waitForX()` polling-borné, test d'atomicité transaction, convention `describe()` 2 niveaux, refactor itératif d'un fichier monolithe | +| `llm-providers.md` | Fournisseurs LLM, auth, coûts | OAuth consumer ≠ API key, prompt caching system prompt stable, budget cap fournisseur | diff --git a/knowledge/backend/patterns/async.md b/knowledge/backend/patterns/async.md index 98568bd..a9610f3 100644 --- a/knowledge/backend/patterns/async.md +++ b/knowledge/backend/patterns/async.md @@ -251,3 +251,30 @@ export const listRecentXxxForMember = async ( ``` L'admin garde un endpoint distinct sans le filtre temporel pour l'accès historique complet. + +--- + + +## Pattern : Test de rollback pour pipelines multi-fichiers atomiques + +- Objectif : garantir qu'un pipeline qui écrit N fichiers avec rollback nettoie réellement l'état partiel quand le Nème échoue. +- Contexte : opération qui persiste plusieurs artefacts (ex : variantes/dérivés d'une image, fichiers d'un export) en gardant la liste des chemins déjà écrits pour pouvoir les supprimer en cas d'échec. +- Quand l'utiliser : tout pipeline « tout ou rien » sur N écritures de fichiers avec compensation manuelle (pas de transaction native). +- Quand l'éviter : écriture d'un seul fichier, ou stockage qui offre une vraie transactionnalité. +- Avantage : + - couvre le cas limite réel (échec en cours de pipeline) plutôt que le seul chemin nominal + - détecte une compensation incomplète (fichiers orphelins) ou excessive (suppression d'un fichier non écrit par ce pipeline) +- Limites / vigilance : + - le test doit être mis à jour quand N change (ajout d'un variant) pour couvrir le nouveau cas limite +- Validé le : 03-04-2026 +- Contexte technique : Node.js / écriture fichiers — app-template-resto story 4.3 + +### Règle + +Tout pipeline qui écrit N fichiers avec rollback (suppression des déjà-écrits si un échec survient) doit avoir un test unitaire couvrant le cas **« N-1 fichiers écrits + le Nème échoue »**. Ce test vérifie : + +- que les N-1 fichiers déjà écrits sont **exactement** supprimés (ni plus, ni moins) ; +- que la phase de finalisation (`finalize()` ou équivalent) n'est **pas** appelée ; +- que l'erreur est bien propagée à l'appelant. + +Quand le nombre d'artefacts change (ajout d'un variant), mettre à jour ce test pour couvrir le nouveau N. diff --git a/knowledge/backend/patterns/contracts.md b/knowledge/backend/patterns/contracts.md index df08f10..28c490e 100644 --- a/knowledge/backend/patterns/contracts.md +++ b/knowledge/backend/patterns/contracts.md @@ -2,10 +2,10 @@ title: Backend — Patterns : Contracts domain: backend bucket: patterns -tags: [contracts, zod, api, error-codes, monorepo] +tags: [contracts, zod, api, error-codes, monorepo, idempotence, http-semantics] applies_to: [analysis, implementation, review, architecture] severity: high -validated_on: 2026-04-07 +validated_on: 2026-06-25 source_projects: [app-alexandrie, RL799_V2] --- @@ -150,6 +150,34 @@ return res.status(200).json({ --- + +## Pattern : Idempotence POST = retour de la ressource existante + +- Objectif : garantir qu'un POST dont l'AC demande l'idempotence produit le **même résultat quel que soit le nombre d'appels**. +- Contexte : endpoint de création où une duplication est détectable (ressource déjà créée pour le même sujet, ex : demande d'export, déclenchement de job unique). Complément du pattern « HTTP 200 + payload métier » ci-dessus, appliqué au cas duplication. +- Quand l'utiliser : POST déclaré idempotent (retries client, double-tap mobile, rejeu réseau). +- Quand l'éviter : création stricte où une duplication est une vraie erreur métier que l'appelant doit traiter explicitement. +- Validé le : 13-04-2026 +- Contexte technique : NestJS / contrat API — app-alexandrie story 9.2 + +### Règle + +Un POST idempotent **retourne la ressource existante** (HTTP 200 / 201) quand une duplication est détectée, sans lever d'erreur. Un `409 CONFLICT` est un comportement de **blocage**, pas d'idempotence : il force l'appelant à gérer un cas d'erreur et casse le contrat « N appels = 1 résultat ». + +```typescript +// ✅ IDEMPOTENT — retourne la ressource existante +if (existing) return this.serialize(existing); + +// ❌ NON-IDEMPOTENT — lève une erreur de blocage +if (existing) throw new HttpException({ error: { code: 'ALREADY_EXISTS' } }, 409); +``` + +### Côté client + +Dédupliquer par `id` avant d'insérer dans la liste/store local, pour éviter les doublons en cas de race condition (deux requêtes parallèles recevant la même ressource). + +--- + ## Pattern : Cohérence du pattern Result dans un repository diff --git a/knowledge/backend/patterns/llm-providers.md b/knowledge/backend/patterns/llm-providers.md new file mode 100644 index 0000000..abe0afd --- /dev/null +++ b/knowledge/backend/patterns/llm-providers.md @@ -0,0 +1,59 @@ +--- +title: "Backend — Patterns : Fournisseurs LLM" +domain: backend +bucket: patterns +tags: [llm, anthropic, openai, api-key, oauth, prompt-caching, couts] +applies_to: [analysis, implementation, architecture] +severity: medium +validated_on: 2026-06-25 +source_projects: [_Assistant_Cuisine] +--- + +# Backend — Patterns : Fournisseurs LLM + +> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet. + +--- + + +## OAuth abonnement consumer ≠ API key — ne pas mélanger les deux usages + +Tous les fournisseurs LLM (Anthropic, OpenAI, Google) séparent strictement deux mondes : + +| Type | Auth | Usage prévu | Coût | +|---|---|---|---| +| **Abonnement consumer** (Claude Pro/Max, ChatGPT Plus) | OAuth via app officielle | usage interactif personnel via clients officiels (desktop, web, plugin IDE) | forfait mensuel | +| **API platform** | API key | usage programmatique, multi-users, services tiers, prod | pay-per-token | + +### Règle + +- Ne **jamais** utiliser les credentials OAuth d'un abonnement consumer pour servir une application à plusieurs utilisateurs. Anthropic l'interdit explicitement (ToS) ; les autres le tolèrent moins ouvertement mais le principe est identique. Risque concret : bannissement du compte si détecté. +- Pour toute application applicative (chatbot, intégration produit), prendre une **API key séparée**, indépendante de l'abonnement consumer. + +--- + + +## Prompt caching obligatoire dès qu'un gros system prompt est stable + +Tout système ayant un system prompt volumineux et stable (knowledge base, format de sortie, persona) DOIT activer le prompt caching côté fournisseur. La portion cachée est facturée à une fraction du tarif normal (ordre de grandeur : ~10 %), ce qui divise le coût input par 5 à 10 sur un usage où le system prompt domine. + +```ts +// OpenAI : promptCacheKey via provider metadata +streamText({ + model: openai("gpt-4.1-mini"), + system: bigStaticPrompt, + experimental_providerMetadata: { + openai: { promptCacheKey: "myapp-system-v1" }, + }, +}); + +// Anthropic : marquage explicite des blocs cacheables dans messages (cache_control) +``` + +### Conséquences sur les coûts + +- Le **coût marginal** d'un nouvel utilisateur est quasi nul tant que le system prompt domine le volume de tokens. +- Pour un usage léger (quelques conversations/jour, prompt caching actif), prévoir un budget mensuel de l'ordre de quelques euros. +- Toujours poser un **budget cap** côté console fournisseur pour éviter toute dérive. + +--- diff --git a/knowledge/backend/risques/README.md b/knowledge/backend/risques/README.md index d58d49a..4347f1d 100644 --- a/knowledge/backend/risques/README.md +++ b/knowledge/backend/risques/README.md @@ -8,12 +8,12 @@ Avant toute proposition backend, identifie le fichier dont le nom et la descript | Fichier | Domaine | Entrées clés | |---------|---------|--------------| -| `auth.md` | Auth, sessions, guards, accès | AuthN/AuthZ dispersée, guard global manquant, null-check request.user, AdminRoleGuard sans @RequireAdminRole, GET sans contrôle accès, cookie après révocation, mock session sans expiresAt, buildApp partagé e2e, champ absent JWT, email login vs contact | +| `auth.md` | Auth, sessions, guards, accès | AuthN/AuthZ dispersée, guard global manquant, null-check request.user, AdminRoleGuard sans @RequireAdminRole, GET sans contrôle accès, cookie après révocation, mock session sans expiresAt, buildApp partagé e2e, champ absent JWT, email login vs contact, disclosure comptes soft-deleted dans login() | | `contracts.md` | Contrats, validation, codes erreur | Contrats implicites, erreurs non standardisées, duplication constantes, schema orphelin, code erreur générique 409, ForbiddenException pour validation, process.env direct, statut métier non propagé | -| `prisma.md` | Prisma, DB, transactions, migrations | @unique nullable, TOCTOU transaction, OR tenantId null, nextOrder race condition, tenantId sans FK, schema divergence spec, getter manquant, init module build, clearAllMocks imbriqué, cursor non validé, enum-like String, migration manuelle hors git, relation 1:1 sans unique | +| `prisma.md` | Prisma, DB, transactions, migrations | @unique nullable, TOCTOU transaction, OR tenantId null, nextOrder race condition, tenantId sans FK, schema divergence spec, getter manquant, init module build, clearAllMocks imbriqué, cursor non validé, enum-like String, migration manuelle hors git, relation 1:1 sans unique, index partial soft-delete (perf) | | `stripe.md` | Stripe, paiements, webhooks, subscriptions | billing_cycle_anchor vs current_period_end, list() sans has_more, concurrence trial→payant, non-idempotence, 200 pendant processing | -| `nestjs.md` | NestJS, controllers, providers | TooManyRequestsException NestJS 11, controller corrompu insertions, repository dead layer, interface provider incomplète | -| `redis.md` | Redis, cache, quotas, TTL | Thrash connexion sous charge, entitlements TTL > SLA, compteurs in-memory, TTL heure locale ±12h | +| `nestjs.md` | NestJS, controllers, providers | TooManyRequestsException NestJS 11, controller corrompu insertions, repository dead layer, interface provider incomplète, guard multi-statut READ_METHODS, bootstrap OK mais injection cassée (tsx watch), guard écriture mode dégradé bloque le support | +| `redis.md` | Redis, cache, quotas, TTL | Thrash connexion sous charge, entitlements TTL > SLA, compteurs in-memory, TTL heure locale ±12h, compensation incrBy non-atomique (quota fantôme) | | `nextjs.md` | Next.js, build, routing | Prisma init au chargement module, server-only dans repositories, redirect boucle infinie feature flags, dossiers `_*` exclus du routing App Router | | `general.md` | Observabilité, migrations, performance, architecture | Observabilité insuffisante, migrations non reproductibles, upsert N+1, authorize-after-fetch, valeur sentinelle DTO, idempotence endpoint, fichier orphelin, mélange Date UTC/locale, champ fantôme Zod, catch vide, params non validés, cast TS brut, chevauchement temporel, TOCTOU, biais agrégation, couplage types erreur, service HTTP-aware, count sans filtre, env top-level, dérive DTO liste vs détail, notification linkUrl rôle-aware, matrice documentée vs code, format `User.id` mixte, Web Push topic > 32 chars, lib npm types non embarqués, form HTML POST dans un mail, env vars frontend-facing fail-fast | | `tests.md` | Isolation des tests d'intégration | `vi.stubEnv` sans restauration, `maxWorkers: 1` masque l'isolation, flakiness inter-fichiers DB partagée | diff --git a/knowledge/backend/risques/auth.md b/knowledge/backend/risques/auth.md index 7ebb613..04a7e21 100644 --- a/knowledge/backend/risques/auth.md +++ b/knowledge/backend/risques/auth.md @@ -2,10 +2,10 @@ title: Backend — Risques & vigilance : Auth domain: backend bucket: risques -tags: [auth, guards, request-user, sessions, admin] +tags: [auth, guards, request-user, sessions, admin, enumeration, soft-delete] applies_to: [implementation, review, debug] severity: high -validated_on: 2026-04-07 +validated_on: 2026-06-25 source_projects: [app-alexandrie, RL799_V2] --- @@ -458,4 +458,40 @@ Quand on prévoit de supprimer un flag auth profondément câblé : **Anti-pattern** : déprécier en douceur en gardant le flag avec un commentaire `// @deprecated` sans supprimer les usages. Le code mort s'accumule, les futurs devs hésitent à le nettoyer ("pourquoi c'est encore là ?"), la dépréciation ne se finit jamais. -- Contexte technique : auth / refactor schema — RL799_V2 28-04-2026 \ No newline at end of file +- Contexte technique : auth / refactor schema — RL799_V2 28-04-2026 + +--- + + +## Information disclosure sur comptes soft-deleted dans `login()` + +### Risques + +- Retourner un code d'erreur distinct (ex : `ACCOUNT_DELETED`) pour les comptes supprimés dans `login()` permet l'**énumération** : un attaquant distingue « compte supprimé » de « identifiants invalides » par email. +- Complément du pattern anti-énumération (cf. `pattern-...anti-énumération` dans `patterns/auth.md`) appliqué au cas soft-delete. + +### Symptômes + +- `login()` lève `ACCOUNT_DELETED` au lieu de `INVALID_CREDENTIALS` pour un compte soft-deleted +- L'existence (et le statut) d'un compte fuite via la réponse d'authentification + +### Bonnes pratiques / mitigations + +```typescript +// ❌ DANGEREUX — révèle l'existence d'un compte supprimé +if (user.deletedAt !== null) { + throw new UnauthorizedException({ error: { code: 'ACCOUNT_DELETED' } }); +} + +// ✅ CORRECT — même code que des identifiants invalides +if (user.deletedAt !== null) { + throw new UnauthorizedException({ + error: { code: 'INVALID_CREDENTIALS', message: 'Email ou mot de passe invalide.' }, + }); +} +``` + +- **Règle** : dans `login()`, toujours répondre `INVALID_CREDENTIALS` pour un compte soft-deleted — jamais un code spécifique. +- **Nuance** : un code `ACCOUNT_DELETED` reste acceptable dans un flux `exchange()` OAuth, où le provider a déjà confirmé l'identité (pas d'énumération possible côté attaquant). + +- Contexte technique : auth / soft-delete / anti-énumération — app-alexandrie 13-04-2026 \ No newline at end of file diff --git a/knowledge/backend/risques/nestjs.md b/knowledge/backend/risques/nestjs.md index b95c366..8500644 100644 --- a/knowledge/backend/risques/nestjs.md +++ b/knowledge/backend/risques/nestjs.md @@ -2,10 +2,10 @@ title: Backend — Risques & vigilance : NestJS domain: backend bucket: risques -tags: [nestjs, controllers, guards, providers, review] +tags: [nestjs, controllers, guards, providers, review, injection, tsx-watch, mode-degrade] applies_to: [implementation, review, debug] severity: high -validated_on: 2026-03-30 +validated_on: 2026-06-25 source_projects: [app-alexandrie] --- @@ -144,3 +144,50 @@ if (user.sessionStatus === 'BLOCKED') throw new HttpException(...); - **Règle** : définir une whitelist explicite des writes autorisés même en état bloqué. - Contexte technique : NestJS / guard statut — app-alexandrie 30-03-2026 + +--- + + +## Bootstrap NestJS OK mais injection runtime cassée sous `tsx watch` + +### Risques + +- En dev, un serveur NestJS peut afficher `Nest application successfully started` et exposer ses routes, tout en cassant à la **première requête** : des dépendances injectées (`Reflector`, services consommés par les guards globaux) arrivent à `undefined` au runtime. +- Le symptôme initial ressemble à un problème réseau/mobile alors que la cause réelle est l'injection backend. + +### Symptômes + +- `GET /` ou une route d'auth répond `500` alors que le bootstrap est nominal +- Stack runtime du type `Cannot read properties of undefined (reading 'getAllAndOverride')` +- Les guards globaux (`AuthGuard`, guards basés sur `Reflector`) sont les premiers à tomber + +### Bonnes pratiques / mitigations + +- Ne pas conclure trop vite à un problème réseau dès que le port écoute : traiter « le port écoute » et « les requêtes fonctionnent » comme **deux validations distinctes**. +- Tester immédiatement `GET /`, l'endpoint OpenAPI et une route publique métier, puis lire la stack runtime **après le premier hit** (pas seulement les logs de bootstrap). +- Si un lanceur `tsx watch` est utilisé avec NestJS, vérifier explicitement la compatibilité avec l'injection runtime ; en cas de doute, expliciter les injections critiques avec `@Inject(...)` sur guards et services exposés dès le premier hit. + +- Contexte technique : NestJS / tsx watch / injection — app-alexandrie 01-04-2026 + +--- + + +## Guard d'écriture en mode dégradé bloque les endpoints de support + +### Risques + +- Un guard global qui refuse toutes les écritures (`POST/PUT/PATCH/DELETE`) quand un service critique est down (ex : Redis) bloque aussi les endpoints dont l'utilité est **maximale en panne** : soumission de ticket de support, feedback, remontée de logs d'erreur client. +- Cas typique : un endpoint de ticketing (`POST /support/...`) devient inaccessible précisément au moment où l'utilisateur veut signaler le dysfonctionnement qu'il subit. + +### Symptômes + +- Soumission de ticket de support impossible quand le service de quota/cache est indisponible +- Le guard rejette des routes qui devraient rester ouvertes en mode dégradé + +### Bonnes pratiques / mitigations + +- Lors de l'ajout d'un guard d'écriture en mode dégradé, **recenser explicitement les endpoints critiques qui doivent rester disponibles** : support, feedback, logs d'erreur client. +- Whitelister ces préfixes dès la création du guard — pas lors de la code review d'une story ultérieure. Exemple de whitelist : `/auth`, `/billing/.../webhooks`, `/support`. +- **Why** : un service de ticketing bloqué quand le backend dysfonctionne est contre-productif — l'utilisateur ne peut pas signaler le problème qu'il est en train de subir. + +- Contexte technique : NestJS / guard mode dégradé — app-alexandrie 01-04-2026 diff --git a/knowledge/backend/risques/prisma.md b/knowledge/backend/risques/prisma.md index b1dd4f0..fda3ba7 100644 --- a/knowledge/backend/risques/prisma.md +++ b/knowledge/backend/risques/prisma.md @@ -2,10 +2,10 @@ title: Backend — Risques & vigilance : Prisma domain: backend bucket: risques -tags: [prisma, transactions, tenant, schema, race-condition] +tags: [prisma, transactions, tenant, schema, race-condition, index, soft-delete, performance] applies_to: [implementation, review, debug, architecture] severity: high -validated_on: 2026-04-07 +validated_on: 2026-06-25 source_projects: [app-template-resto, app-alexandrie, RL799_V2] --- @@ -619,3 +619,33 @@ const resolveDbUrl = (): string | undefined => { **Règle générale** : toute stratégie template-based doit auditer le chemin du `DB_URL` à travers les sub-processes de bootstrap. Le bootstrap ouvre une connexion sur la template, mais le seed transitif exécuté via un sub-process peut être sujet à des transformations agressives du DSN qui le redirigent ailleurs. - Contexte technique : Prisma / template database / Vitest — RL799_V2 01-05-2026 + +--- + + +## Index partial sur colonnes soft-delete (nuance perf) + +### Risques + +- Un index plein sur une colonne soft-delete nullable (`deleted_at`) indexe **tous les NULL**, c'est-à-dire la quasi-totalité des lignes (les comptes actifs). L'index est volumineux et peu sélectif pour les requêtes qui ne ciblent que les lignes supprimées. +- Distinct de l'index **unique** partiel déjà documenté (cf. `risque-prisma-unique-nullable`, qui traite de l'idempotence) : ici l'enjeu est purement la **performance / le coût d'indexation**. + +### Symptômes + +- `CREATE INDEX ... ON ("deleted_at")` sans clause `WHERE` sur une table où >99 % des lignes ont `deleted_at IS NULL` +- Index gros pour un bénéfice quasi nul sur les requêtes « lister les comptes supprimés » + +### Bonnes pratiques / mitigations + +```sql +-- ❌ Index plein — indexe la masse des NULL (lignes actives) +CREATE INDEX "users_deleted_at_idx" ON "users"("deleted_at"); + +-- ✅ Index partial — n'indexe que les lignes effectivement supprimées +CREATE INDEX "users_deleted_at_idx" ON "users"("deleted_at") + WHERE "deleted_at" IS NOT NULL; +``` + +- **Règle** : pour une colonne soft-delete nullable à majorité `NULL`, préférer un index partial `WHERE deleted_at IS NOT NULL`. + +- Contexte technique : Prisma / PostgreSQL / index partial — app-alexandrie 13-04-2026 diff --git a/knowledge/backend/risques/redis.md b/knowledge/backend/risques/redis.md index 8cc6640..18ce0bd 100644 --- a/knowledge/backend/risques/redis.md +++ b/knowledge/backend/risques/redis.md @@ -103,3 +103,41 @@ 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 + +--- + + +## Compensation `incrBy(-1)` non-atomique après dépassement de quota + +### Risques + +- Pattern courant de quota Redis : `INCR` → vérifier `> limit` → si oui, décrémenter (`incrBy(-1)`) et lever 429. La compensation n'est PAS atomique avec le check. +- Si Redis devient indisponible (flap) entre l'incrément et la compensation, l'appel de décrément échoue silencieusement et retourne `null`. Le compteur reste incrémenté → quota fantôme jusqu'à l'expiration de la clé. + +### Symptômes + +- Utilisateur bloqué par le quota alors qu'il n'a pas atteint la limite réelle +- Aucune erreur visible : la valeur de retour `null` du décrément n'est pas vérifiée +- Comportement intermittent corrélé aux instabilités Redis + +### Bonnes pratiques / mitigations + +```typescript +// ❌ Compensation non vérifiée — échec silencieux possible +await redis.incr(key); +if (current > limit) { + await redis.incrBy(key, -1); // retourne null si Redis flap → compensation perdue + throw quotaExceeded(); +} + +// ✅ Vérifier le retour de la compensation et loguer +const compensated = await redis.incrBy(key, -1); +if (compensated === null) { + logger.error({ type: 'quota', event: 'compensation_failed', key }); +} +``` + +- **Règle** : toujours vérifier le retour du décrément de compensation et loguer explicitement si `null`. Documenter ce choix dans les Dev Notes de la story (comportement intentionnel vs bug). +- **Solution robuste** : encapsuler incrément + compensation conditionnelle dans le même pipeline `MULTI/EXEC` ou un script Lua (atomicité garantie), au prix d'une complexité plus élevée. + +- Contexte technique : Redis / NestJS — app-alexandrie 01-04-2026 diff --git a/knowledge/frontend/patterns/README.md b/knowledge/frontend/patterns/README.md index d45d8e3..9fa4216 100644 --- a/knowledge/frontend/patterns/README.md +++ b/knowledge/frontend/patterns/README.md @@ -13,5 +13,5 @@ Avant toute proposition frontend, identifie le fichier dont le nom et la descrip | `navigation.md` | Navigation, routing, Expo Router, intégrations tierces | Navigation réactive post-action async, link-out page locale canonique, factorisation page mode dynamique via `meta.mode` typé | | `design-tokens.md` | Design tokens, typographie, spacing, Tailwind, RN StyleSheet | Tokens TypeScript Expo/RN, typography sémantique, export styles composant, grilles 2 colonnes | | `nextjs.md` | Next.js App Router, embeds, ESLint | Click-to-load embeds tiers, ESLint flat config Next.js | -| `tests.md` | Tests styles React Native, smoke checks, mount + mock composable | Tests de styles sans renderer JSX, smoke checks `readFileSync`, classe CSS modifier vs texte, cleanup E2E best-effort, helpers SW purs, mount + mock composable, assertions React Email | +| `tests.md` | Tests styles React Native, smoke checks, mount + mock composable | Tests de styles sans renderer JSX, smoke checks `readFileSync`, classe CSS modifier vs texte, cleanup E2E best-effort, helpers SW purs, mount + mock composable, assertions React Email, garde-fous de non-activation feature parking Later | | `general.md` | Focus visible, inputs date HTML5, journaux/audit logs, pages admin | Focus visible interne pour overflow clip, restyle global ``, UI patterns journaux d'audit, structuration pages admin (eyebrows + grille filtres + variante danger) | diff --git a/knowledge/frontend/patterns/tests.md b/knowledge/frontend/patterns/tests.md index c631354..3454e1d 100644 --- a/knowledge/frontend/patterns/tests.md +++ b/knowledge/frontend/patterns/tests.md @@ -2,10 +2,10 @@ title: Frontend — Patterns : Tests domain: frontend bucket: patterns -tags: [tests, react-native, jest, styles, ui] +tags: [tests, react-native, jest, styles, ui, regression-guard, parking-later] applies_to: [implementation, review] severity: medium -validated_on: 2026-04-07 +validated_on: 2026-06-25 source_projects: [app-alexandrie, RL799_V2] --- @@ -518,3 +518,43 @@ assert.doesNotMatch(html, /]+src=["'](?!data:)/i); // pas de http(s) ``` + +--- + + +## Pattern : Garde-fous de non-activation pour features "parking Later" + +### Synthèse + +- **Objectif** : protéger une feature explicitement décidée "parking Later" (non implémentée en V1) contre une activation accidentelle, via des tests de non-régression couvrant **tous** ses points d'entrée futurs. +- **Contexte** : une décision de design liste N fonctions/écrans comme points d'injection futurs de la feature (ex : N fonctions de fetch où un futur filtre par auteur sera ajouté). +- **Quand l'utiliser** : dès qu'une feature est parkée mais que son chemin d'activation futur est déjà identifié. +- **Quand l'éviter** : feature sans aucun point d'entrée prévu (rien à protéger). + +### Analyse + +- **Avantages** : + - documente l'intention de non-activation à l'endroit où le dev futur travaillera + - détecte une activation partielle ou accidentelle dans n'importe lequel des points d'entrée +- **Limites / vigilance** : + - **un garde-fou partiel donne une fausse confiance** : couvrir une seule fonction laisse les autres points d'injection sans protection + - **règle** : si le design futur liste N fonctions/écrans, écrire N tests de non-présence correspondants (N fonctions → N tests) + +### Validation + +- Validé le : 25-06-2026 +- Contexte technique : React Native / Jest — app-alexandrie + +### Implémentation + +```typescript +// ✅ CORRECT : tous les points d'entrée du design futur couverts +it('fetchEntries ne filtre pas par auteur', ...) +it('loadMoreEntries ne filtre pas par auteur', ...) +it('fetchEntriesByCategory ne filtre pas par auteur', ...) + +// ❌ INSUFFISANT : un seul point d'entrée couvert +it('fetchEntries ne filtre pas par auteur', ...) +``` + +Le dev qui activera la feature plus tard ajoutera le comportement dans les N fonctions simultanément ; chaque point d'entrée doit avoir son test de non-activation. diff --git a/knowledge/frontend/risques/README.md b/knowledge/frontend/risques/README.md index 72a46cd..a62531c 100644 --- a/knowledge/frontend/risques/README.md +++ b/knowledge/frontend/risques/README.md @@ -9,11 +9,11 @@ Avant toute proposition frontend, identifie le fichier dont le nom et la descrip | Fichier | Domaine | Entrées clés | |---------|---------|--------------| | `auth.md` | Auth, guards de rôle, entitlements, OAuth | Auth côté client, loading infini écran gated, bouton OAuth vide, guard rôle flash UX | -| `state.md` | Zustand, state management, erreurs async, optimistic UI | Erreurs silencieuses, catch sans feedback, auto-reset état dégradé, fire-and-forget refresh, boolean UI hardcodé, flag isLoading unique, erreur sans rethrow, optimistic update sous-listes | -| `navigation.md` | Expo Router, Vue Router, deep link, useEffect fetch, contexte store | Store vide deep link/reload, guard incomplet états terminaux, collection sans clé contexte, double route racine, router-link disabled, état local query param | +| `state.md` | Zustand, state management, erreurs async, optimistic UI | Erreurs silencieuses, catch sans feedback, auto-reset état dégradé, fire-and-forget refresh, boolean UI hardcodé, flag isLoading unique, erreur sans rethrow, optimistic update sous-listes, fallback catch-all mapping statut DB → UI | +| `navigation.md` | Expo Router, Vue Router, deep link, useEffect fetch, contexte store | Store vide deep link/reload, guard incomplet états terminaux, collection sans clé contexte, double route racine, router-link disabled, état local query param, fichiers non-route sous `src/app` Expo Router | | `design-tokens.md` | Design tokens, spacing, Tailwind, StyleSheet RN | Double système espacement, dimensions via spacing, inline styles dashboard, classes Tailwind invalides | | `nextjs.md` | Next.js App Router, SSR, Server Actions, sécurité | useSearchParams sans Suspense, type ViewData dupliqué, composant React .ts, double validation segment, consent state ambigu, script inline XSS, window.location.reload, useTransition snapshot, window.confirm, import type server, img natif, useTransition global liste, formulaire defaultValue sans key | | `tests.md` | Jest, ts-jest, tests React Native, Vue | Config node bloque .tsx, faux test négatif, helpers copiés, test écran indirect, test façade flux réel, tests présence textuelle | | `performance.md` | Re-renders, memoization, useCallback, fetch | Sur-renders bundle non maîtrisé, useCallback inutile inline, fetch sans timeout | | `react-native.md` | React Native, fetch, ScrollView, TextInput | Focus ring TextInput, contentInset iOS-only, fetch sans response.ok | -| `general.md` | Accessibilité, regex, patterns transversaux, monorepo | Accessibilité oubliée a11y, regex globale singleton lastIndex, Alert.prompt iOS-only, primitive UI couplée, migration partielle classes legacy, ARIA roles sans clavier, duplication logique métier monorepo, event listeners globaux modales, boutons imbriqués, fire-and-forget sans feedback | +| `general.md` | Accessibilité, regex, patterns transversaux, monorepo | Accessibilité oubliée a11y, regex globale singleton lastIndex, Alert.prompt iOS-only, primitive UI couplée, migration partielle classes legacy, ARIA roles sans clavier, duplication logique métier monorepo, event listeners globaux modales, boutons imbriqués, fire-and-forget sans feedback, type client non MAJ extension payload backend | diff --git a/knowledge/frontend/risques/general.md b/knowledge/frontend/risques/general.md index f40e7c9..1037064 100644 --- a/knowledge/frontend/risques/general.md +++ b/knowledge/frontend/risques/general.md @@ -790,3 +790,32 @@ Garder `
/` uniquement quand on accepte le rendu natif (groupe - Contexte technique : HTML / a11y / CSS — RL799_V2 02-05-2026 --- + + +## Type client non mis à jour lors d'une extension de payload backend + +### Risques + +- Quand un nouveau champ est ajouté dans un objet retourné par une Server Action, le type TS côté client n'est pas toujours mis à jour en parallèle +- Si le retour de la Server Action est casté vers un type plus étroit, TypeScript accepte l'incompatibilité **silencieusement** : le champ existe au runtime mais reste invisible pour les consommateurs TS + +### Symptômes + +- Le payload contient un champ supplémentaire en runtime, mais le type client ne l'expose pas +- Une tâche "mettre à jour le test si le payload expose le nouveau champ" est marquée done en vérifiant le test, sans vérifier le type source qui l'alimente +- Aucune erreur de compile car le cast masque la divergence + +### Bonnes pratiques / mitigations + +Pour toute extension d'un payload backend (ajout de champ), appliquer une **checklist de propagation** : + +1. Le type TS côté client (ex : type d'item média, type de l'entité) +2. Le contrat Zod si présent +3. Les fixtures de test qui typent le payload +4. Les composants qui déstructurent le payload (vérifier les accès au nouveau champ manquants) + +- Règle : ne pas laisser cette propagation à la décision implicite du dev — l'expliciter comme checklist dans la story / la PR. + +- Contexte technique : Next.js / Server Actions / TypeScript / Zod — app-template-resto 25-06-2026 + +--- diff --git a/knowledge/frontend/risques/navigation.md b/knowledge/frontend/risques/navigation.md index 92d3336..9b2ed81 100644 --- a/knowledge/frontend/risques/navigation.md +++ b/knowledge/frontend/risques/navigation.md @@ -296,3 +296,27 @@ const routes = [ - Contexte technique : Vue Router / navigation dynamique — RL799_V2 15-04-2026 --- + + +## Fichiers non-route sous `src/app` avec Expo Router + +### Risques + +- Expo Router scanne récursivement `src/app` pour construire les routes : tout fichier TypeScript laissé dans cet arbre (`*.spec.ts`, `*.utils.ts`, `*.screen-logic.ts`) est traité comme une route potentielle, même s'il s'agit d'un test, d'un helper ou d'une logique d'écran +- Symptômes initiaux trompeurs : une succession de warnings au build puis un crash runtime qui ressemble à un problème d'app, alors que la cause est un simple fichier mal placé + +### Symptômes + +- Warning `Route "...spec.ts" is missing the required default export` +- Warning `Route "...screen-logic.ts" is missing the required default export` +- Erreur runtime `Property 'describe' doesn't exist` ou `Property 'jest' doesn't exist` dans Expo Go (le runtime de l'app charge un fichier de test) + +### Bonnes pratiques / mitigations + +- `src/app` ne doit contenir **que** des fichiers de routing Expo Router +- Déplacer helpers, utils, tests et logique d'écran ailleurs (`src/domains`, `src/features`, `src/shared`) +- **Diagnostic** : en debug mobile, si Expo Go remonte `describe` / `jest` au runtime, vérifier immédiatement qu'aucun fichier de test n'est resté sous `src/app` + +- Contexte technique : Expo Router — app-alexandrie 25-06-2026 + +--- diff --git a/knowledge/frontend/risques/state.md b/knowledge/frontend/risques/state.md index 5706e10..1c7ce29 100644 --- a/knowledge/frontend/risques/state.md +++ b/knowledge/frontend/risques/state.md @@ -555,3 +555,41 @@ Trois cas typiques quand l'erreur apparaît après extraction : **Recommandation outillage** : `vue-tsc` plutôt que `tsc` pur en typecheck (cf. `risque-templates-vue-references-orphelines`). Ce genre de divergence aurait été détecté **avant** le refactor. - Contexte technique : Vue 3 / Volar / `vue-tsc` — RL799_V2 29-04-2026 + +--- + + +## Fallback catch-all dans le mapping statut DB → UI + +### Risques + +- Un mapping statut DB → statut UI avec un `return` catch-all final accepte implicitement **toute** nouvelle valeur d'enum DB sans alerte +- Une nouvelle valeur (ex : `pending`) atterrit alors dans la branche par défaut (ex : rendue comme `processing`) avec un libellé incorrect, violant silencieusement le contrat d'affichage + +### Symptômes + +- Une valeur d'enum DB non explicitement gérée affiche le mauvais état UI (ex : "En cours…" au lieu de "En attente") +- L'ajout d'une valeur d'enum côté DB ne déclenche aucune erreur de compile ni de test ; le mauvais libellé passe inaperçu + +### Bonnes pratiques / mitigations + +```ts +// ❌ Le catch-all masque silencieusement "pending" sous "processing" +function dbStatusToUiStatus(s: DBStatus): UIStatus { + if (s === "ready") return "ready"; + if (s === "failed") return "failed"; + return "processing"; // "pending" atterrit ici sans libellé correct +} + +// ✅ Mapping exhaustif : un if par valeur d'enum +function dbStatusToUiStatus(s: DBStatus): UIStatus { + if (s === "ready") return "ready"; + if (s === "failed") return "failed"; + if (s === "pending") return "pending"; + return "processing"; +} +``` + +- Règle : chaque valeur de l'enum DB doit avoir son propre `if` dans le mapping. Éviter le `return` catch-all final qui absorbe les valeurs non prévues. + +- Contexte technique : frontend / mapping statut DB → UI — app-template-resto 25-06-2026 diff --git a/knowledge/infra/patterns/README.md b/knowledge/infra/patterns/README.md new file mode 100644 index 0000000..fec790b --- /dev/null +++ b/knowledge/infra/patterns/README.md @@ -0,0 +1,13 @@ +# Infra — Patterns validés — Index + +Patterns d'infrastructure (Docker, reverse proxy, Tailscale, homelab NUC) testés et validés en conditions réelles. + +Avant toute proposition infra, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le. + +--- + +| Fichier | Domaine | Entrées clés | +|---------|---------|--------------| +| `docker-networking.md` | Bridges Docker, communication container ↔ hôte | Bridges isolés / service hôte injoignable, conteneuriser plutôt que firewall, `network_mode: host`, anti-pattern `host-gateway` | +| `reverse-proxy-paths.md` | Servir une app sous un sous-chemin (Traefik) | App consciente du préfixe (stripPrefix ou non), chemins relatifs, app racine, conflits de path multi-apps | +| `tailscale.md` | Sidecar Tailscale pour app incompatible subpath | Quand l'utiliser, architecture, squelette compose, `serve.json`, init obligatoire, coûts | diff --git a/knowledge/infra/patterns/docker-networking.md b/knowledge/infra/patterns/docker-networking.md new file mode 100644 index 0000000..4b29309 --- /dev/null +++ b/knowledge/infra/patterns/docker-networking.md @@ -0,0 +1,57 @@ +--- +title: Infra — Patterns validés : Docker networking +domain: infra +bucket: patterns +tags: [docker, networking, bridge, traefik, host-gateway, firewall] +applies_to: [architecture, implementation, debug] +severity: medium +validated_on: 2026-06-25 +source_projects: [_Assistant_Cuisine] +--- + +# Infra — Patterns validés : Docker networking + +> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet. + +--- + + +## Bridges Docker isolés — un container ne peut pas joindre un service hôte par défaut + +### Constat + +Un container connecté à un bridge custom (ex. `traefik-net` en `172.21.0.0/16`) ne peut **pas** atteindre par défaut : + +- l'IP hôte côté `docker0` (`172.17.0.1`) +- l'IP hôte sur d'autres interfaces (ex. IP Tailscale `100.x.x.x`) +- `host.docker.internal` (qui résout vers `172.17.0.1`, donc même blocage) + +Même quand l'hôte écoute bien sur `0.0.0.0:PORT`, l'appel depuis le container part en timeout. Cause : sous Docker 29 + iptables/nftables strict, les bridges sont isolés entre eux et le forward bridge → host est bloqué par défaut. + +**Conséquence** : faire pointer un router Traefik vers `host.docker.internal:PORT` pour atteindre un service systemd hôte ne fonctionne pas en bridge mode. + +### Bonnes pratiques / solutions (par ordre de propreté) + +**Solution 1 (préférée) — Conteneuriser le service hôte** + +Mettre le service dans un container connecté au même bridge (`traefik-net`). La communication passe par le DNS Docker interne (`servicename:port`). Aucune complexité réseau, fonctionne nativement. C'est presque toujours possible (ex. code-server via l'image `lscr.io/linuxserver/code-server`). + +**Solution 2 — Traefik en `network_mode: host`** + +Traefik abandonne son bridge custom et écoute directement sur les interfaces hôte. Il atteint tous les services hôte trivialement, MAIS perd l'accès aux containers via bridge : il faut alors leur publier des ports hôte, ce qui défait l'intérêt du reverse proxy interne. Réservé aux setups simples où Traefik ne sert que des services hôte. + +**Solution 3 — Ouvrir manuellement le firewall** + +```bash +sudo iptables -I DOCKER-USER -s 172.21.0.0/16 -d 172.21.0.1 -p tcp --dport -j ACCEPT +``` + +Fragile : règle volatile, à refaire au reboot ou à l'upgrade Docker. Si on prend ce chemin, documenter et provisionner via une unit systemd. + +### Anti-pattern : `extra_hosts: host.docker.internal:host-gateway` + +Cette ligne déclare seulement un alias DNS dans le `/etc/hosts` du container ; elle ne crée **aucune route réseau**. Si le firewall Docker bloque, l'alias ne sert à rien : le container résout `host.docker.internal` mais le SYN reste bloqué. + +**Règle** : avant de partir sur la Solution 2 ou 3, toujours vérifier qu'on ne peut pas conteneuriser le service. C'est presque toujours possible et toujours plus propre. + +- Contexte technique : Docker 29 / Traefik / NUC — _Assistant_Cuisine 04-05-2026 diff --git a/knowledge/infra/patterns/reverse-proxy-paths.md b/knowledge/infra/patterns/reverse-proxy-paths.md new file mode 100644 index 0000000..abe13e5 --- /dev/null +++ b/knowledge/infra/patterns/reverse-proxy-paths.md @@ -0,0 +1,57 @@ +--- +title: Infra — Patterns validés : Reverse proxy & sous-chemins +domain: infra +bucket: patterns +tags: [traefik, reverse-proxy, pathprefix, stripprefix, subpath, spa] +applies_to: [architecture, implementation, debug] +severity: medium +validated_on: 2026-06-25 +source_projects: [_Assistant_Cuisine] +--- + +# Infra — Patterns validés : Reverse proxy & sous-chemins + +> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet. + +--- + + +## Servir une app sous un sous-chemin via reverse proxy + +Le pattern qui fonctionne dépend de la façon dont l'app gère ses URLs internes (assets, API, redirects). Trois cas. + +### Cas 1 — App nativement consciente du préfixe + +L'app prend une option de base path et émet ses URLs internes préfixées. Côté Traefik, lire la doc de l'app pour savoir si un `stripPrefix` est requis — il n'y a **pas de règle universelle** : + +- **App qui reçoit le path complet et sait quoi en faire** (ex. Cooklang `cook server --url-prefix /cuisine`) : `PathPrefix(/cuisine)` **sans** `stripPrefix`. +- **App qui veut recevoir `/` mais émet ses assets sous le préfixe** (ex. Portainer `--base-url=/portainer`) : `PathPrefix(/portainer)` **avec** `stripPrefix /portainer`. + +### Cas 2 — App qui sert uniquement des chemins relatifs + +Si l'app utilise des URLs relatives partout (``, `