Files
_Assistant_Lead_Tech/90_debug_et_postmortem.md
T
MaksTinyWorkshop ef24d85d57 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.
2026-06-25 10:31:22 +02:00

16 KiB
Raw Blame History

Debug & post-mortems

Ce fichier sert à capitaliser sur les problèmes rencontrés.

À documenter

  • bug pénible
  • mauvaise compréhension
  • fausse hypothèse
  • solution finale

Objectif

Ne plus jamais perdre du temps sur le même problème.


Postmortems

SQL Server qui crash dans un conteneur LXC Proxmox

Contexte

NUC personnel sous Proxmox avec plusieurs services en conteneurs LXC. Un conteneur SQL Server (Microsoft SQL Server Linux) ne démarrait plus.

Symptômes

  • sqlcmd impossible → timeout
  • service mssql-server en boucle de restart
  • logs contenant :
Operation not permitted
chmod: changing permissions of '/var/opt/mssql/log/...'
  • crash + génération de core dump

Cause probable

SQL Server utilise certaines opérations système qui sont mal supportées dans les conteneurs LXC (permissions, filesystem, capabilities).

Dans un environnement Proxmox LXC, cela peut casser après :

  • une mise à jour
  • un changement de permissions
  • un changement de configuration du conteneur

Conclusion

SQL Server n'est pas un bon candidat pour un conteneur LXC Proxmox.

Décision architecturale

Pour un homelab ou un petit serveur :

  • éviter SQL Server en LXC
  • préférer :
    • PostgreSQL
    • MariaDB / MySQL

Si SQL Server est nécessaire :

  • utiliser une VM complète plutôt qu'un conteneur.

Règle à retenir

Éviter les bases lourdes nécessitant des capabilities système avancées dans des conteneurs LXC.


Suppression silencieuse due à deux éditions concurrentes sur le même fichier

Contexte

Un même fichier a été modifié par deux mécanismes proches dans le temps : édition en cours dagent et passe outillée/linter/formatteur.

Symptômes

  • bloc de code disparu sans erreur explicite
  • diff final incohérent avec lintention de modification
  • impression de “régression fantôme” après une édition pourtant correcte

Cause probable

Deux processus ont réécrit le même fichier sans coordination, le second écrasant silencieusement une partie du travail du premier.

Correctif / règle à retenir

  • éviter deux passes d’écriture concurrentes sur le même fichier
  • relire le diff immédiatement après toute passe automatique
  • privilégier une séquence stricte : édition, puis lint/format, puis vérification

tsx + NestJS : injection par type cassée silencieusement

Contexte

Projet app-alexandrie, Epic 3, le 10-03-2026. Le backend NestJS tournait avec tsx watch dans un contexte ESM (module: nodenext), notamment pour rester compatible avec Prisma v7.

Symptômes

  • TypeError: Cannot read properties of undefined (reading 'get') dans le constructeur dun service
  • ConfigService injecté par type mais undefined au runtime
  • @Injectable() et ConfigModule correctement configurés, sans erreur de compilation

Cause probable

tsx repose sur esbuild pour transpiler TypeScript. Dans ce contexte, emitDecoratorMetadata est ignoré même sil est activé dans tsconfig.json. NestJS ne peut donc plus résoudre correctement certaines injections par type, notamment constructor(private readonly config: ConfigService).

Correctif / règle à retenir

  • ne pas supposer que emitDecoratorMetadata fonctionne avec tsx
  • dans ce contexte, éviter linjection par type de ConfigService pour les services dinfra
  • lire explicitement les variables via process.env, après chargement amont de ConfigModule.forRoot()

Exemple :

// AVANT
constructor(private readonly config: ConfigService) {
  const host = this.config.get('REDIS_HOST');
}

// APRES
constructor() {
  const host = process.env['REDIS_HOST'] ?? 'localhost';
}

Alternative écartée

nest start --watch a été testé mais a introduit des conflits ESM/CJS dans ce contexte (exports is not defined).


export { fn } ne constitue pas un import local — détecté uniquement au build

Contexte

Projet app-template-resto, story 2-4, le 17-03-2026. Dans getPublicHomeData.ts, la fonction resolvePublicTenantSelection avait été déplacée dans src/server/tenant/resolvePublicTenant.ts et re-exportée depuis l'ancien emplacement.

Symptômes

  • Cannot find name 'resolvePublicTenantSelection' au next build uniquement
  • ESLint et tsc (hors build) ne signalaient rien
  • La fonction était utilisée localement dans le même fichier qui la re-exportait

Cause

// getPublicHomeData.ts
export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";

// puis, plus bas dans le même fichier :
const result = resolvePublicTenantSelection(env); // ← NameError au build

Un re-export (export { fn } from "...") ne crée pas de binding local dans le fichier. La fonction est exportée vers l'extérieur mais n'est pas disponible comme variable locale.

Correctif / règle à retenir

Si une fonction est utilisée dans le même fichier qui la re-exporte, ajouter un import séparé en plus du export :

import { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";
export { resolvePublicTenantSelection }; // pour les appelants externes

CLI npm globale qui ne se met pas à jour (prefix / permissions / contexte projet)

Contexte

Mise à jour de @openai/codex via la CLI (codex update), sur une machine avec installation npm globale utilisateur (~/.npm-global) et exécution depuis un repo contenant un .npmrc non standard.

Symptômes

  • Message dupdate CLI affiché mais version inchangée après npm install -g
  • codex --version reste sur une ancienne version
  • Installation via sudo ne change rien
  • which codex et npm root -g pointent vers des chemins différents

Cause

  • Décalage entre :
    • le prefix npm utilisé pour installer
    • le binaire exécuté
  • Ancienne installation toujours active dans le bon prefix utilisateur
  • Contexte projet (.npmrc) pouvant influencer le comportement de npm

Correctif / règle à retenir

  • Ne jamais utiliser sudo npm install -g
  • Sassurer que :
    • npm config get prefix = dossier utilisateur (ex : ~/.npm-global)
    • which <cli> pointe vers ce même prefix
  • Faire les installs globales hors dun repo (éviter .npmrc projet)
  • En cas de doute, nettoyer :
rm -rf ~/.npm-global/lib/node_modules/<package>
rm -f ~/.npm-global/bin/<cli>
npm install -g <package>@latest

Commandes de diagnostic utiles

  • npm config get prefix
  • which <cli>
  • npm root -g
  • npm ls -g --depth=0 <package> | npm list -g @openai/codex --depth=0
  • --version

Sub-agents Claude Code — Write indisponible dans la sandbox Explore

Contexte

Workflow BMAD testarch-test-review sur RL799_V2 (24-04-2026) utilisant 4 sub-agents subagent_type=Explore pour évaluer 4 dimensions qualité en parallèle. Chaque sub-agent devait écrire un fichier JSON dans /tmp/.

Symptômes

  • Les 4 sub-agents ont terminé leur analyse avec succès mais aucun n'a réussi à écrire son fichier JSON
  • Messages de retour : "Je rencontre une limitation d'outillage… je suis en mode READ-ONLY… je génère le rapport directement en texte."

Cause

Le sub-agent type Explore n'a pas accès à l'outil Write dans sa sandbox (spec : "Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit"). Non documenté clairement dans les workflows TEA qui demandent pourtant d'écrire en JSON.

Correctif / règle à retenir

  1. Ne pas demander aux sub-agents Explore d'utiliser Write — briefer explicitement "retourne le JSON en bloc dans ta réponse finale"
  2. L'orchestrateur matérialise les fichiers de sortie pour le compte des sub-agents
  3. Alternative : utiliser subagent_type=general-purpose qui a accès à tous les tools (mais plus cher en tokens et moins spécialisé pour l'exploration)

Extrait de brief corrigé pour futur usage :

Ta mission : analyse X dans les fichiers Y.
Format de sortie : JSON structuré selon le schéma ci-dessous.
IMPORTANT : retourne le JSON directement dans ta réponse finale, entre blocs ```json```.
Ne tente pas d'écrire de fichier (Write indisponible dans ta sandbox).
L'orchestrateur matérialisera le fichier à partir de ton retour.

Effet iceberg en CI — patcher en cascade jusqu'au fond du puits

Contexte

Quand un fix CI structurant rétablit un pipeline qui foirait depuis longtemps, plusieurs bugs latents en aval peuvent apparaître en cascade : ils étaient tous présents avant, juste invisibles parce que le runner s'arrêtait à l'échec amont. Vécu sur RL799_V2 le 30-04 / 01-05-2026, 8 étages d'iceberg fixés en cascade.

Symptômes

# Phase Symptôme Cause Fix
1 CI tests Cannot find module '@org/shared' dist/lib non bâti avant test:api Build workspace en amont
2 CI tests Module '@prisma/client' has no exported member 'X' Client Prisma non généré Inverser prisma generatepnpm build
3 CI tests Seed incomplet : 0 users / N attendus Étape seed manquante Ajouter prisma db seed après prisma migrate deploy
4 CI tests <env> non configuré (requis hors dev) Variable d'env applicative manquante en CI Définir au bloc env: du job
5 CI tests 14×500 sur endpoints qui chiffrent ENCRYPTION_KEY manquante Idem
6 CI tests (PDF) Could not find Chrome Puppeteer cherche son cache local absent du runner PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable
7 CD prod (migrate) Cannot find module '/app/scripts/check-node-version.mjs' pnpm run prisma:migrate appelle un script absent de l'image API Appel direct du binaire Prisma
8 CI tests Test attend 50,00 € reçoit 1,19 € waitForNotification mal scopé (filtre par type mais pas par recipientId) — masquée par les étages 1-7 Re-run OU patch chirurgical du where:

Chaque étage masquait le suivant. Aucun n'était nouveau — tous présents avant la session, mais invisibles à cause des étages amont.

Cause

  • Local ≠ CI : en local, dist/ traîne, le client Prisma est généré, la DB est seedée d'une session précédente, le .env est complet. Le bug est invisible
  • Pipeline early-exit : un échec à l'étape N ne laisse rien tourner aux étapes N+1, N+2, …
  • Effet additif des sessions : plus le pipeline est cassé depuis longtemps, plus le code applicatif a évolué sans validation CI

Correctif / règle à retenir

  1. Validation locale stricte avant push CI structurant : simuler les conditions CI vierges (rm -rf node_modules/.prisma packages/*/dist apps/*/.next + relancer la chaîne complète)
  2. Lecture honnête des nouveaux failures : après un fix CI structurant, ne pas présumer que les nouveaux failures sont des régressions du fix. Probablement des bugs latents
  3. Tableau iceberg : noter au fil de la session le tableau (étage / symptôme / cause / fix). Ne pas se laisser submerger par "ça casse encore"
  4. Push après chaque étage : ne pas attendre d'avoir tout fixé. Chaque fix structurant mérite son commit thématique
  5. Ne pas stopper trop tôt : un seul push ne révèle qu'un étage. Tant qu'il y a des bugs latents, le pipeline cassera

Signal pour repérer un effet iceberg

  • Le pipeline était cassé depuis ≥ 1 semaine
  • Le fix d'aujourd'hui touche une étape précoce du workflow (install, build, generate, migrate)
  • Les commits récents ont ajouté des features sans valider en CI
  • Sentiment vague de "ça pourrait casser plein d'autres trucs" — c'est probablement vrai

Prisma migrate inclut les diffs cosmétiques (RenameIndex)

Contexte

prisma migrate dev --create-only --name add_lodge_settings peut générer une migration qui contient (1) le changement attendu mais aussi (2) un side-effect cosmétique pré-existant entre le schema Prisma et la DB qui n'avait jamais été nettoyé. RL799_V2 — migration 20260427120920_add_lodge_settings qui ramassait un ALTER INDEX … RENAME TO … orphelin.

Symptômes

  • Migration thématique qui contient un rename d'index sans rapport avec le scope de la story
  • Un dev qui regarde la migration ne comprend pas pourquoi cet ALTER INDEX est là

Options et décision

Option Pro Con
Garder le rename dans la migration thématique avec commentaire la prochaine prisma migrate dev ne re-générera pas ce rename le commit "thématique" contient un side-effect cosmétique
Retirer le rename commit propre la prochaine migration thématique l'inclura à nouveau → piège pour le prochain dev
Migration de cleanup séparée plus propre nécessite 2 migrations + 2 PRs

Décision recommandée : option 1 avec commentaire explicite dans le .sql :

-- RenameIndex (réalignement DB ↔ schema, dérive cosmétique pré-existante)
ALTER INDEX "tronc_entries_tenue_idx" RENAME TO "tronc_entries_tenue_id_idx";

Correctif / règle à retenir

  • 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 <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.

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 :

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.