capitalisation: triage 95_a_capitaliser + création domaine infra

Triage des 27 propositions du buffer de capitalisation (skill
capitalisation-triage), avec vérification des doublons contre la base.

Intégré dans knowledge/ (23 entrées):
- backend: redis (compensation incrBy non-atomique), nestjs (injection
  cassée sous tsx watch; guard write mode dégradé), async (test rollback
  pipeline multi-fichiers), contracts (idempotence POST), auth (disclosure
  comptes soft-deleted), prisma (index partial soft-delete), llm-providers
  (nouveau: OAuth vs API key, prompt caching).
- frontend: tests (garde-fous parking Later), navigation (fichiers
  non-route sous src/app Expo Router), general (type client vs payload
  backend), state (fallback catch-all mapping DB→UI).
- workflow: story-tracking (statut BMAD vs narratif obsolète).
- product: general (nouveau: doc feature store sans UI).
- infra: NOUVEAU DOMAINE (traefik, tailscale, docker, docker-networking,
  reverse-proxy-paths, sidecar tailscale) + 00_INDEX.md.

Autres:
- 90_debug_et_postmortem.md: post-mortem réseau Docker partagé hors compose.
- Rejeté 3 doublons (types enum contracts, getter PrismaService, $transaction).
- Buffer 95_a_capitaliser.md purgé et restauré à son état initial.
- _projects.conf: MAJ statuts epics + ajout app-rl799.
This commit is contained in:
MaksTinyWorkshop
2026-06-25 10:31:22 +02:00
parent 1c876309f1
commit ef24d85d57
31 changed files with 1042 additions and 27 deletions
+1
View File
@@ -48,6 +48,7 @@ Organisés dans `knowledge/` par domaine. Chaque domaine a un sous-dossier `patt
| UX | `knowledge/ux/patterns/` | `knowledge/ux/risques/` | | UX | `knowledge/ux/patterns/` | `knowledge/ux/risques/` |
| n8n | `knowledge/n8n/patterns/` | `knowledge/n8n/risques/` | | n8n | `knowledge/n8n/patterns/` | `knowledge/n8n/risques/` |
| Product | `knowledge/product/patterns/` | `knowledge/product/risques/` | | Product | `knowledge/product/patterns/` | `knowledge/product/risques/` |
| Infra | `knowledge/infra/patterns/` | `knowledge/infra/risques/` |
| Workflow | — | `knowledge/workflow/risques/` | | Workflow | — | `knowledge/workflow/risques/` |
--- ---
+53
View File
@@ -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 - **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 - **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 <container>` 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-<nom>-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 <container>` 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`.
+3 -1
View File
@@ -20,6 +20,8 @@ vers les fichiers appropriés :
- `knowledge/n8n/risques/general.md` - `knowledge/n8n/risques/general.md`
- `knowledge/product/patterns/general.md` - `knowledge/product/patterns/general.md`
- `knowledge/product/risques/<thème>.md` - `knowledge/product/risques/<thème>.md`
- `knowledge/infra/patterns/<thème>.md`
- `knowledge/infra/risques/<thème>.md`
- `knowledge/workflow/patterns/general.md` - `knowledge/workflow/patterns/general.md`
- `knowledge/workflow/risques/story-tracking.md` - `knowledge/workflow/risques/story-tracking.md`
- `10_conventions_redaction.md` - `10_conventions_redaction.md`
@@ -38,7 +40,7 @@ Chaque proposition doit suivre ce format :
DATE — PROJET DATE — PROJET
FILE_UPDATE_PROPOSAL FILE_UPDATE_PROPOSAL
Fichier cible : <knowledge/backend/patterns/<thème>.md | knowledge/backend/risques/<thème>.md | knowledge/frontend/patterns/<thème>.md | knowledge/frontend/risques/<thème>.md | knowledge/ux/patterns/<thème>.md | knowledge/ux/risques/<thème>.md | knowledge/n8n/patterns/general.md | knowledge/n8n/risques/general.md | knowledge/product/patterns/general.md | knowledge/product/risques/<thème>.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 : <knowledge/backend/patterns/<thème>.md | knowledge/backend/risques/<thème>.md | knowledge/frontend/patterns/<thème>.md | knowledge/frontend/risques/<thème>.md | knowledge/ux/patterns/<thème>.md | knowledge/ux/risques/<thème>.md | knowledge/n8n/patterns/general.md | knowledge/n8n/risques/general.md | knowledge/product/patterns/general.md | knowledge/product/risques/<thème>.md | knowledge/infra/patterns/<thème>.md | knowledge/infra/risques/<thème>.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 : Pourquoi :
<raison pour laquelle ce savoir mérite d'être capitalisé> <raison pour laquelle ce savoir mérite d'être capitalisé>
+2 -1
View File
@@ -50,5 +50,6 @@
# - automatiser certains audits inter-projets # - automatiser certains audits inter-projets
# #
# Les lignes commençant par # sont ignorées. # 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-template-resto|Next.js Prisma+PostgreSQL|perso|Epic 4 en cours
app-rl799|pnpm monorepo Prisma+PostgreSQL|perso|Epic 12 en cours
+3 -2
View File
@@ -9,12 +9,13 @@ Avant toute proposition backend, identifie le fichier dont le nom et la descript
| Fichier | Domaine | Entrées clés | | 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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 |
+27
View File
@@ -251,3 +251,30 @@ export const listRecentXxxForMember = async (
``` ```
L'admin garde un endpoint distinct sans le filtre temporel pour l'accès historique complet. L'admin garde un endpoint distinct sans le filtre temporel pour l'accès historique complet.
---
<a id="pattern-test-rollback-pipeline-multi-fichiers"></a>
## 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.
+30 -2
View File
@@ -2,10 +2,10 @@
title: Backend — Patterns : Contracts title: Backend — Patterns : Contracts
domain: backend domain: backend
bucket: patterns 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] applies_to: [analysis, implementation, review, architecture]
severity: high severity: high
validated_on: 2026-04-07 validated_on: 2026-06-25
source_projects: [app-alexandrie, RL799_V2] source_projects: [app-alexandrie, RL799_V2]
--- ---
@@ -150,6 +150,34 @@ return res.status(200).json({
--- ---
<a id="pattern-idempotence-post-ressource-existante"></a>
## 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).
---
<a id="pattern-coherence-result-repository"></a> <a id="pattern-coherence-result-repository"></a>
## Pattern : Cohérence du pattern Result dans un repository ## Pattern : Cohérence du pattern Result dans un repository
@@ -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.
---
<a id="pattern-oauth-consumer-vs-api-key"></a>
## 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.
---
<a id="pattern-prompt-caching-system-prompt-stable"></a>
## 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.
---
+4 -4
View File
@@ -8,12 +8,12 @@ Avant toute proposition backend, identifie le fichier dont le nom et la descript
| Fichier | Domaine | Entrées clés | | 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é | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `tests.md` | Isolation des tests d'intégration | `vi.stubEnv` sans restauration, `maxWorkers: 1` masque l'isolation, flakiness inter-fichiers DB partagée |
+39 -3
View File
@@ -2,10 +2,10 @@
title: Backend — Risques & vigilance : Auth title: Backend — Risques & vigilance : Auth
domain: backend domain: backend
bucket: risques bucket: risques
tags: [auth, guards, request-user, sessions, admin] tags: [auth, guards, request-user, sessions, admin, enumeration, soft-delete]
applies_to: [implementation, review, debug] applies_to: [implementation, review, debug]
severity: high severity: high
validated_on: 2026-04-07 validated_on: 2026-06-25
source_projects: [app-alexandrie, RL799_V2] 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. **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 - Contexte technique : auth / refactor schema — RL799_V2 28-04-2026
---
<a id="risque-disclosure-comptes-soft-deleted-login"></a>
## 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
+49 -2
View File
@@ -2,10 +2,10 @@
title: Backend — Risques & vigilance : NestJS title: Backend — Risques & vigilance : NestJS
domain: backend domain: backend
bucket: risques bucket: risques
tags: [nestjs, controllers, guards, providers, review] tags: [nestjs, controllers, guards, providers, review, injection, tsx-watch, mode-degrade]
applies_to: [implementation, review, debug] applies_to: [implementation, review, debug]
severity: high severity: high
validated_on: 2026-03-30 validated_on: 2026-06-25
source_projects: [app-alexandrie] 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é. - **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 - Contexte technique : NestJS / guard statut — app-alexandrie 30-03-2026
---
<a id="risque-bootstrap-ok-injection-cassee-tsx-watch"></a>
## 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
---
<a id="risque-writemodeguard-bloque-endpoints-support"></a>
## 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
+32 -2
View File
@@ -2,10 +2,10 @@
title: Backend — Risques & vigilance : Prisma title: Backend — Risques & vigilance : Prisma
domain: backend domain: backend
bucket: risques 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] applies_to: [implementation, review, debug, architecture]
severity: high severity: high
validated_on: 2026-04-07 validated_on: 2026-06-25
source_projects: [app-template-resto, app-alexandrie, RL799_V2] 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. **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 - Contexte technique : Prisma / template database / Vitest — RL799_V2 01-05-2026
---
<a id="risque-index-partial-soft-delete-perf"></a>
## 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
+38
View File
@@ -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 - 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 - Contexte technique : Redis / NestJS — app-alexandrie 20-03-2026
---
<a id="risque-compensation-incrby-non-atomique"></a>
## 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
+1 -1
View File
@@ -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é | | `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 | | `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 | | `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 `<input type="date">`, UI patterns journaux d'audit, structuration pages admin (eyebrows + grille filtres + variante danger) | | `general.md` | Focus visible, inputs date HTML5, journaux/audit logs, pages admin | Focus visible interne pour overflow clip, restyle global `<input type="date">`, UI patterns journaux d'audit, structuration pages admin (eyebrows + grille filtres + variante danger) |
+42 -2
View File
@@ -2,10 +2,10 @@
title: Frontend — Patterns : Tests title: Frontend — Patterns : Tests
domain: frontend domain: frontend
bucket: patterns bucket: patterns
tags: [tests, react-native, jest, styles, ui] tags: [tests, react-native, jest, styles, ui, regression-guard, parking-later]
applies_to: [implementation, review] applies_to: [implementation, review]
severity: medium severity: medium
validated_on: 2026-04-07 validated_on: 2026-06-25
source_projects: [app-alexandrie, RL799_V2] source_projects: [app-alexandrie, RL799_V2]
--- ---
@@ -518,3 +518,43 @@ assert.doesNotMatch(html, /<link\s+rel/i); // pas de preload (≠ mode mail)
assert.doesNotMatch(html, /<script\s+src/i); assert.doesNotMatch(html, /<script\s+src/i);
assert.doesNotMatch(html, /<img[^>]+src=["'](?!data:)/i); // pas de http(s) assert.doesNotMatch(html, /<img[^>]+src=["'](?!data:)/i); // pas de http(s)
``` ```
---
<a id="pattern-garde-fous-non-activation-parking-later"></a>
## 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.
+3 -3
View File
@@ -9,11 +9,11 @@ Avant toute proposition frontend, identifie le fichier dont le nom et la descrip
| Fichier | Domaine | Entrées clés | | 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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 |
+29
View File
@@ -790,3 +790,32 @@ Garder `<fieldset>/<legend>` uniquement quand on accepte le rendu natif (groupe
- Contexte technique : HTML / a11y / CSS — RL799_V2 02-05-2026 - Contexte technique : HTML / a11y / CSS — RL799_V2 02-05-2026
--- ---
<a id="risque-type-client-non-maj-extension-payload-backend"></a>
## 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
---
+24
View File
@@ -296,3 +296,27 @@ const routes = [
- Contexte technique : Vue Router / navigation dynamique — RL799_V2 15-04-2026 - Contexte technique : Vue Router / navigation dynamique — RL799_V2 15-04-2026
--- ---
<a id="risque-fichiers-non-route-sous-src-app-expo-router"></a>
## 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
---
+38
View File
@@ -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. **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 - Contexte technique : Vue 3 / Volar / `vue-tsc` — RL799_V2 29-04-2026
---
<a id="risque-fallback-catch-all-mapping-statut-db-ui"></a>
## 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
+13
View File
@@ -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 |
@@ -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.
---
<a id="pattern-bridge-isole-service-hote"></a>
## 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 <PORT> -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
@@ -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.
---
<a id="pattern-app-sous-chemin-reverse-proxy"></a>
## 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 (`<a href="./xxx">`, `<script src="./xxx">`, redirects relatifs aussi), elle fonctionne sous n'importe quel sous-chemin avec un simple `stripPrefix` côté Traefik. L'app ne sait pas où elle est, mais le navigateur résout correctement les URLs relatives (cas observé sur code-server).
⚠️ Vérifier qu'**aucune URL absolue** ne traîne dans le HTML/JS. Un `href="/static/..."` (au lieu de `href="./static/..."` ou `href="static/..."`) casse le rendu.
### Cas 3 — App qui veut être à la racine
Beaucoup de SPA modernes (Homepage, Vite/Next statiques) hardcodent `href="/_next/..."` etc. Le path prefix casse alors les assets. Solutions :
- Sous-domaine dédié (`homepage.example.com`) plutôt que sous-chemin.
- Sinon, plugin de réécriture de body (ex. `traefik/plugin-rewritebody`) qui patche le HTML à la volée.
- Si l'app est incompatible subpath ET doit rester derrière Tailscale : sidecar Tailscale dédié (voir `knowledge/infra/patterns/tailscale.md`).
**Règle** : tester en priorité avec un simple `PathPrefix + stripPrefix`. Si les assets cassent, lire la doc de l'app pour une option de base path. Si rien d'officiel, basculer sur sous-domaine (ou sidecar Tailscale).
---
<a id="pattern-conflit-path-multi-apps"></a>
## Conflit de path entre apps multiples — anticipation
Quand plusieurs apps cohabitent sur le même domaine :
- **Avant** d'ajouter une app, vérifier que son préfixe ne va pas matcher des routes d'une autre app — voir le cas `/api` dans `knowledge/infra/risques/traefik.md`.
- Préférer des préfixes longs et distinctifs (`/cuisine`, `/portainer`, `/code`) plutôt que génériques (`/app`, `/admin`, `/dashboard`).
- Documenter dans le compose Traefik le mapping path → service pour garder une vue d'ensemble.
- Contexte technique : Traefik v3 / NUC — _Assistant_Cuisine 04-05-2026
+114
View File
@@ -0,0 +1,114 @@
---
title: Infra — Patterns validés : Sidecar Tailscale
domain: infra
bucket: patterns
tags: [tailscale, sidecar, docker, subpath, magicdns, tls, spa]
applies_to: [architecture, implementation]
severity: medium
validated_on: 2026-06-25
source_projects: [apps/stirling-pdf]
---
# Infra — Patterns validés : Sidecar Tailscale
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet.
---
<a id="pattern-sidecar-tailscale-subpath"></a>
## Sidecar Tailscale pour app incompatible avec subpath
Quand une app refuse d'être servie sous un sous-chemin (assets et fetch API hardcodés sur `/`) et qu'on veut la garder derrière Tailscale, on lui attache un **sidecar `tailscale/tailscale`** qui rejoint le tailnet avec son propre hostname et termine le TLS via un cert MagicDNS.
### Quand l'utiliser
- L'app expose une SPA Vite/Next/CRA avec assets hardcodés sur `/`.
- L'app fait des fetch API en chemin absolu (`/api/...`) sans respecter un base path.
- Patcher l'upstream ou injecter un `<base href>` est non trivial / fragile.
- **Critère de décision** : si la première page charge mais que les assets JS retournent 404 derrière Traefik, c'est ce cas (cf. bug upstream Stirling-PDF #5072).
### Architecture
```
tailnet
▼ https://<app>.<tailnet>.ts.net (cert MagicDNS auto)
[container <app>-ts (sidecar tailscale)]
│ network: <app>-internal (bridge dédié)
[container <app>]:<port>
```
- Le sidecar **rejoint le tailnet** comme un nœud à part (compte dans le quota devices).
- Cert TLS délivré automatiquement par `tailscale serve` via `serve.json`.
- L'app reste servie sur `/` côté interne ; **Traefik n'est pas dans la boucle**.
- Réseau Docker dédié, jamais branché sur `traefik-net`.
### Squelette docker-compose
```yaml
services:
myapp:
image: monimage:tag
networks: [app_internal]
# PAS d'exposition de ports sur l'hôte
# PAS de labels traefik.*
tailscale:
image: tailscale/tailscale:v1.98.3
container_name: myapp-ts
# ⚠ PAS de `hostname: myapp` ici (cf. risque collision DNS Docker)
environment:
TS_AUTHKEY: ${MYAPP_TS_AUTHKEY}
TS_HOSTNAME: myapp
TS_STATE_DIR: /var/lib/tailscale
TS_SERVE_CONFIG: /config/serve.json
TS_USERSPACE: "false"
volumes:
- /srv/docker-data/apps/myapp/tailscale/state:/var/lib/tailscale
- /srv/docker-data/apps/myapp/tailscale/serve:/config:ro
- /dev/net/tun:/dev/net/tun
cap_add: [NET_ADMIN, SYS_MODULE]
networks: [app_internal]
labels:
# Auto-discovery homepage : la tile pointe vers le FQDN tailnet, pas vers Traefik
- "homepage.group=Apps"
- "homepage.name=MyApp"
- "homepage.href=https://myapp.<tailnet>.ts.net/"
networks:
app_internal:
name: myapp-internal
driver: bridge
```
`serve.json` :
```json
{
"TCP": { "443": { "HTTPS": true } },
"Web": {
"myapp.<tailnet>.ts.net:443": {
"Handlers": { "/": { "Proxy": "http://myapp:<port-interne>" } }
}
},
"AllowFunnel": { "myapp.<tailnet>.ts.net:443": false }
}
```
> ⚠ Ne pas mettre `hostname: <app>` sur le sidecar si le service voisin s'appelle déjà `<app>` : collision DNS Docker non déterministe. `TS_HOSTNAME` fixe le nom côté tailnet et est décorrélé du DNS Docker interne. Voir `knowledge/infra/risques/docker.md`.
### Init obligatoire
1. Générer une **auth key Tailscale Reusable, non-Ephemeral** dans l'admin console.
2. Stocker le state dans un volume **persistant** (sinon ré-enrôlement à chaque restart et pollution du tailnet).
3. Si le tailnet a "Machine approval" activé : approuver le nœud manuellement après le premier `up`.
4. **Disable key expiry** sur le nœud après enrôlement (sinon il faut refournir une authkey à l'expiration, par défaut 90 j).
### Coût du pattern
- Un nœud tailnet par app (limite gratuite : 100 devices).
- Pas de middlewares Traefik partagés (rewriteBody pour `lang="fr"`, basic auth, etc.) → à accepter, ou patcher l'upstream.
- Double restart si l'app upstream change : le sidecar continue à serve même si l'app est down → 502 propre, pas de cascade.
- Contexte technique : Docker / Tailscale / Spring Boot + Vite — apps/stirling-pdf 27-05-2026
+13
View File
@@ -0,0 +1,13 @@
# Infra — Risques & vigilance — Index
Risques d'infrastructure susceptibles de provoquer des incidents homelab/prod, des pertes de connectivité, ou des bugs non diagnostiquables (Docker, Traefik, Tailscale).
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 |
|---------|---------|--------------|
| `traefik.md` | Traefik v3, routage, auth, proxy | Incompatibilité API Docker 29 (Traefik 3.6.1+), `PathPrefix(/api)` trop large → regex, Bun `new Response` perd `Location``c.redirect`, basic auth + WebSocket re-prompts WebKit/iOS |
| `tailscale.md` | Certificats Tailscale, MagicDNS | `tailscale cert` ne couvre que le FQDN exact (ni sous-domaines ni wildcard), renouvellement ~3 mois via systemd timer idempotent |
| `docker.md` | DNS Docker, hostname, réseaux partagés | Collision hostname container vs service name (DNS `127.0.0.11` non déterministe), diagnostic `getent hosts`, renvoi post-mortem réseau partagé entre stacks |
+77
View File
@@ -0,0 +1,77 @@
---
title: Infra — Risques & vigilance : Docker
domain: infra
bucket: risques
tags: [docker, dns, hostname, compose, tailscale, networking]
applies_to: [implementation, review, debug, architecture]
severity: high
validated_on: 2026-06-25
source_projects: [apps/stirling-pdf, infra/postgres]
---
# Infra — Risques & vigilance : Docker
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/risques/README.md` pour l'index complet.
---
<a id="risque-docker-collision-dns-hostname-service"></a>
## Collision DNS Docker : hostname container vs service name
### Risques
- Dans un `docker-compose.yml`, déclarer `hostname: <X>` sur un service alors qu'un autre service du même compose s'appelle `<X>` crée **deux entrées A** pour le nom `<X>` dans le DNS embarqué Docker (`127.0.0.11`) :
1. le service name `<X>` (résolu vers l'IP du container du service),
2. le `hostname: <X>` du container voisin.
- Un autre container du même network qui fait `getent hosts <X>` peut tomber sur **l'une ou l'autre selon l'ordre d'enregistrement** — non déterministe entre `docker compose up` successifs (dépend de l'ordre de démarrage).
### Symptômes
- Un service en réseau interne retourne 200 sur son IP directe mais `connection refused` quand on le hit par son service name depuis un voisin.
- **Aucune erreur explicite** dans les logs : juste un `connection refused` qui ressemble à "l'app n'écoute pas".
- Cas typique sidecar Tailscale : `tailscale serve` proxie vers `app:8080` mais le DNS résout `app` vers le sidecar lui-même → 502 côté HTTPS.
### Bonnes pratiques / mitigations
```yaml
# ❌ collision DNS — "app" peut résoudre vers le sidecar
services:
app:
image: monapp # service name DNS = "app"
proxy:
image: nginx
hostname: app # ⚠ collision
# ✅ pas de hostname Docker : le service name reste la seule entrée DNS
services:
app:
image: monapp
proxy:
image: nginx
# hostname Docker non défini → ID container random
# les voisins s'adressent au proxy via le service name "proxy"
```
- **Règle** : ne jamais mettre `hostname: <X>` quand un service du même compose s'appelle `<X>`.
- **Cas sidecar Tailscale** : ne pas mettre `hostname: <app>` pour matcher `TS_HOSTNAME`. `TS_HOSTNAME` est lu par tailscaled et fixe le nom **côté tailnet**, totalement décorrélé du DNS Docker interne. Laisser le hostname Docker par défaut (voir `knowledge/infra/patterns/tailscale.md`).
### Diagnostic
```bash
# Dans le container qui fait l'appel
docker exec <caller> getent hosts <name-resolved>
# Plus d'une ligne pour le même nom → collision DNS Docker
```
- Contexte technique : Docker DNS / Compose / sidecar Tailscale — apps/stirling-pdf 27-05-2026
---
<a id="risque-docker-reseau-partage-stacks"></a>
## Réseau Docker partagé entre stacks — voir post-mortem
Un réseau Docker mutualisé entre plusieurs stacks (ex. Postgres partagé) déclaré `external: true` peut malgré tout être supprimé par un `docker compose down` quand aucun autre projet ne le revendique, avec perte de connectivité en cascade et conteneur recréé sans son mapping de ports.
Détail complet, règles opérationnelles et signal de détection : voir la section **« Réseau Docker partagé entre stacks — créer hors compose »** dans `90_debug_et_postmortem.md`.
- Contexte technique : Docker / réseau partagé / NUC — infra/postgres 22-04-2026
+58
View File
@@ -0,0 +1,58 @@
---
title: Infra — Risques & vigilance : Tailscale
domain: infra
bucket: risques
tags: [tailscale, cert, magicdns, tls, letsencrypt, systemd]
applies_to: [architecture, implementation, debug]
severity: medium
validated_on: 2026-06-25
source_projects: [_Assistant_Cuisine]
---
# Infra — Risques & vigilance : Tailscale
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/risques/README.md` pour l'index complet.
---
<a id="risque-tailscale-cert-fqdn-exact"></a>
## `tailscale cert` ne couvre QUE le FQDN exact du host
### Risques
- `tailscale cert <fqdn>` génère un certificat Let's Encrypt valide **uniquement** pour le FQDN passé, qui doit être le hostname Tailscale du device courant (ex. `nuc.wyvern-snapper.ts.net`). Toute attente de sous-domaines ou de wildcard est fausse.
### Symptômes
- Sous-domaines arbitraires (`cuisine.nuc.wyvern-snapper.ts.net`) : pas de cert auto, et MagicDNS ne route pas non plus.
- Wildcard : non supporté.
- FQDN d'un autre device : non couvert.
### Bonnes pratiques / mitigations
Pour un reverse-proxy multi-apps, deux options :
- **Path-based routing** sous le FQDN unique (`/cuisine`, `/code`, etc.) — voir `knowledge/infra/patterns/reverse-proxy-paths.md`.
- **Vrai domaine** avec challenge DNS-01 (Cloudflare, OVH, etc.) côté Traefik pour obtenir des sous-domaines réels.
**Règle** : `tailscale cert` est OK pour un homelab qui sert tout sous un seul FQDN avec routing par path. Pour des sous-domaines réels, prévoir un domaine perso.
---
<a id="risque-tailscale-cert-renouvellement"></a>
## `tailscale cert` nécessite un renouvellement périodique
### Risques
- Le cert est valide ~3 mois (Let's Encrypt). Sans renouvellement automatisé, expiration silencieuse → TLS cassé sur toutes les apps servies sous ce FQDN.
### Symptômes
- Erreur de certificat expiré côté navigateur après ~90 j sans intervention.
### Bonnes pratiques / mitigations
- **Pattern** : systemd timer hebdomadaire qui appelle `tailscale cert --cert-file ... --key-file ...` sur le FQDN du host. `tailscale cert` est **idempotent** et ne renouvelle qu'à moins de 30 j de la fin — aucun coût à le lancer souvent.
- Le reverse proxy doit watcher le fichier pour reload auto (le file provider de Traefik le fait nativement).
- Contexte technique : Tailscale cert / MagicDNS / systemd — _Assistant_Cuisine 04-05-2026
+109
View File
@@ -0,0 +1,109 @@
---
title: Infra — Risques & vigilance : Traefik
domain: infra
bucket: risques
tags: [traefik, docker, reverse-proxy, bun, websocket, basic-auth, ios]
applies_to: [implementation, review, debug, architecture]
severity: high
validated_on: 2026-06-25
source_projects: [_Assistant_Cuisine]
---
# Infra — Risques & vigilance : Traefik
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/risques/README.md` pour l'index complet.
---
<a id="risque-traefik-v3-docker29-api"></a>
## Pièges Traefik v3 + Docker Engine 29
### Risques
- **Incompatibilité d'API Docker** : Docker 29 a élevé la minimum API version. Traefik < 3.6 utilise un client Docker SDK figé en `1.24` ; le daemon refuse la connexion et aucune route ne fonctionne.
- **`PathPrefix(/api)` trop large** : un routeur `Host(...) && PathPrefix(/api)` vole les routes `/api/*` de toutes les autres apps du même domaine (Homepage `/api/services`, Next.js `/api/...`, n'importe quel SPA) → 401/404 silencieux.
- **Bun `new Response(body, {status: 3xx})` perd le header `Location`** : un proxy HTTP en Bun qui forward un upstream 302/303 retransmet le status mais le client reçoit un 200 vide (ou un status sans `Location`). Le navigateur ne suit pas la redirection.
### Symptômes
- Container Traefik qui tourne mais aucune route active, logs en boucle :
```
ERR Failed to retrieve information of the docker client and server host
error="Error response from daemon: client version 1.24 is too old.
Minimum supported API version is 1.40, please upgrade your client to a newer version"
```
- Une app exposée sur le même domaine que le dashboard Traefik se met à retourner 401/404 sur ses propres `/api/*`.
- Un proxy Bun qui forward une redirection : le client reçoit un 200 vide ou un `Location` nul.
### Bonnes pratiques / mitigations
**Incompatibilité API Docker**
- Utiliser **Traefik 3.6.1+** (client Docker avec négociation auto de version). Les tags `:v3.5` / `:v3.2` ne marchent pas, même avec `DOCKER_API_VERSION=1.44` en env (le client n'honore pas cette variable dans Traefik).
- Pinner explicitement la version (ex. `traefik:v3.6.15`), jamais le tag mouvant `:v3.6`.
- Vérifier la compatibilité Traefik ↔ Docker Engine **avant** un upgrade Docker.
**`PathPrefix(/api)` trop large**
```yaml
# ❌ trop large — capture tous les /api/* du domaine
- "traefik.http.routers.dashboard-api.rule=Host(`example.com`) && PathPrefix(`/api`)"
# ✅ ne capture que les endpoints natifs Traefik
- "traefik.http.routers.dashboard-api.rule=Host(`example.com`) && PathRegexp(`^/api/(overview|version|rawdata|support-dump|entrypoints|http|tcp|udp)(/|$$)`)"
```
Les endpoints natifs Traefik sont fixés : `overview`, `version`, `rawdata`, `support-dump`, `entrypoints`, `http/*`, `tcp/*`, `udp/*`. Sur un domaine partagé entre plusieurs apps : **jamais** de `PathPrefix(/api)` générique, toujours une regex explicite.
**Bun `new Response` perd le `Location`**
```ts
// ❌ perd le Location dans Bun (~1.1.x)
return new Response(upstream.body, {
status: upstream.status, // 303
headers: upstream.headers, // contient Location, ignoré au final
});
// ✅ Hono c.redirect() ou équivalent
if (upstream.status >= 300 && upstream.status < 400) {
const loc = upstream.headers.get("location");
if (loc) return c.redirect(loc, upstream.status as 301 | 302 | 303 | 307 | 308);
}
```
**Règle** : tout proxy HTTP qui forward des redirects upstream doit traiter explicitement la branche 3xx avant la branche "body normal".
- Contexte technique : Traefik v3 / Docker 29 / Bun + Hono — _Assistant_Cuisine 04-05-2026
---
<a id="risque-basic-auth-websocket-webkit-ios"></a>
## Basic auth + WebSocket : re-prompts répétés (surtout WebKit/iOS)
### Risques
- Une app derrière une basic auth (Traefik, nginx) qui utilise des WebSockets (code-server, Jupyter, ttyd, et beaucoup d'apps "live") déclenche **plusieurs prompts d'authentification** à l'ouverture, même quand les credentials viennent d'être saisis.
### Symptômes
- 3 prompts d'authentification successifs à chaque ouverture de l'app, observés notamment sur tablette iOS / iPadOS.
- Persiste sur Chrome iOS et Firefox iOS (tous contraints d'utiliser WebKit sur iOS). Sporadique sur macOS, rare sur desktop Linux/Windows.
### Cause
Safari (et tous les navigateurs iOS, contraints à WebKit) ne **propage pas systématiquement les credentials basic auth aux requêtes WebSocket** d'une page déjà authentifiée. Chaque WS qui échoue redéclenche un prompt.
### Bonnes pratiques / mitigations
Remplacer la basic auth par une auth à **cookie de session** :
- Soit l'auth native de l'app si elle existe (code-server `HASHED_PASSWORD`, JupyterHub login form, etc.).
- Soit un système central type Authelia / Authentik / traefik-forward-auth qui pose un cookie partagé entre apps.
Le cookie passe automatiquement avec les requêtes WebSocket (même origine) → plus de re-prompt.
**Règle** : pour toute app qui utilise des WebSockets ET qui doit être protégée, ne **pas** utiliser la basic auth. Toujours cookie-based. La basic auth reste OK pour des UI sans WS (dashboards en pull, API REST simples).
- Contexte technique : Traefik / basic auth / WebKit iOS — _Assistant_Cuisine 04-05-2026
+1 -2
View File
@@ -8,5 +8,4 @@ Avant toute proposition produit, identifie le fichier dont le nom et la descript
| Fichier | Domaine | Entrées clés | | Fichier | Domaine | Entrées clés |
|---------|---------|--------------| |---------|---------|--------------|
| `general.md` | Documentation produit, scope feature | Documentation d'une feature partiellement implémentée (store sans UI) |
_(aucune entrée pour le moment — à alimenter via `95_a_capitaliser.md`)_
+39
View File
@@ -0,0 +1,39 @@
---
title: Product — Risques & vigilance : Général
domain: product
bucket: risques
tags: [documentation, feature-scope, ui, store, guide-utilisateur]
applies_to: [product, review, documentation]
severity: medium
validated_on: 2026-06-25
source_projects: [app-alexandrie]
---
# Product — Risques & vigilance : Général
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/product/risques/README.md` pour l'index complet.
---
<a id="risque-doc-feature-partiellement-implementee"></a>
## Documentation d'une feature partiellement implémentée (store sans UI)
### Risques
- Le guide utilisateur décrit une fonctionnalité câblée côté store/service mais jamais exposée dans l'UI écran
- L'utilisateur cherche une étape (ex. joindre une capture d'écran) qui n'existe pas réellement dans l'application
- Faux sentiment de complétion : la story semble livrée parce que le store/service existe, alors que le parcours utilisateur est inutilisable
### Symptômes
- Le store ou le service existe (ex. `uploadSupportAttachment`) mais l'écran ne contient aucun composant UI correspondant : pas d'`ImagePicker`, pas de bouton d'attachement
- Le guide mentionne une étape sans contrepartie visible à l'écran
- La story spec annonce la feature comme livrée alors que seul le câblage technique est présent
### Bonnes pratiques / mitigations
- **Règle** : avant de documenter une feature dans le guide utilisateur, vérifier que l'UI l'expose effectivement à l'utilisateur final — ne pas se fier aux seuls stores/services
- Croiser systématiquement la story spec (feature annoncée) avec l'implémentation écran réelle
- Si la fonctionnalité est dans le store mais pas dans l'écran : soit la documenter comme "disponible prochainement", soit ne pas la mentionner du tout
- Contexte technique : guide utilisateur / scope feature — app-alexandrie 13-04-2026
+1 -1
View File
@@ -6,4 +6,4 @@ Risques liés au process de développement, aux agents BMAD, et au tracking des
| Fichier | Domaine | Entrées clés | | Fichier | Domaine | Entrées clés |
|---------|---------|--------------| |---------|---------|--------------|
| `story-tracking.md` | BMAD, agents, story completion | Story "completed" avec tâches ❌, story "done" sans fichiers source dans File List, stratégie de fix d'une suite E2E qui rote en masse | | `story-tracking.md` | BMAD, agents, story completion | Story "completed" avec tâches ❌, story "done" sans fichiers source dans File List, statut BMAD correct mais sections narratives obsolètes, stratégie de fix d'une suite E2E qui rote en masse |
+26 -1
View File
@@ -5,7 +5,7 @@ bucket: risques
tags: [bmad, story, file-list, review, completion] tags: [bmad, story, file-list, review, completion]
applies_to: [analysis, implementation, review] applies_to: [analysis, implementation, review]
severity: high severity: high
validated_on: 2026-04-07 validated_on: 2026-06-25
source_projects: [app-alexandrie, app-template-resto, RL799_V2] source_projects: [app-alexandrie, app-template-resto, RL799_V2]
--- ---
@@ -314,3 +314,28 @@ source_projects: [app-alexandrie, app-template-resto, RL799_V2]
**Métrique de référence** : 19 fails fixés en 4 commits / ~3 h dont 1 h de capitalisation. Effort par fail : ~6 min de fix + 4 min de validation. **Métrique de référence** : 19 fails fixés en 4 commits / ~3 h dont 1 h de capitalisation. Effort par fail : ~6 min de fix + 4 min de validation.
- Contexte technique : Playwright / refactor UI — RL799_V2 25-04-2026 - Contexte technique : Playwright / refactor UI — RL799_V2 25-04-2026
---
<a id="risque-statut-bmad-correct-narratif-obsolete"></a>
## Statut BMAD correct mais sections narratives obsolètes
### Risques
- Une story peut afficher `Status: done` ou `review` tout en conservant des sections narratives (`Story Completion Status`, `Completion Notes`) qui contredisent le statut courant
- Le reviewer lit alors un fichier BMAD incohérent : seul le champ `Status` a été mis à jour, pas le récit qui résume l'état réel de la story
- Risque d'accepter une story dont les notes décrivent un état antérieur (génération de contexte) plutôt que l'implémentation finale
### Symptômes
- `Status: done` mais `Story Completion Status` mentionne encore `ready-for-dev`
- `Completion Notes` décrivant une simple génération de contexte au lieu de l'implémentation réalisée
- Contradiction entre `Status`, `Senior Developer Review` et `Change Log`
### Bonnes pratiques / mitigations
- En code review, recroiser systématiquement `Status`, `Story Completion Status`, `Completion Notes`, `Senior Developer Review` et `Change Log` : ces cinq sources doivent raconter le même état
- **Règle** : toute transition BMAD (`ready-for-dev``in-progress``review``done`) doit mettre à jour aussi les sections narratives qui résument l'état de la story, pas seulement le champ `Status`
- Traiter une divergence narrative comme un signal de doute sur la complétion réelle, pas comme un simple oubli cosmétique
- Contexte technique : BMAD / workflow agent — app-alexandrie 01-04-2026