edtech / docs /post-mortems.md
CognxSafeTrack
docs: add post-mortems for Meta API deprecation, SSE 401 and file import bugs
b8d93e2

Post-Mortems — XAMLÉ Platform

Ce document recense les bugs significatifs rencontrés en production, leurs causes racines, et les règles à appliquer pour ne pas les reproduire. À lire avant tout changement touchant l'infrastructure, les APIs tierces ou l'authentification.


Architecture de référence

Netlify (Admin Frontend — React/Vite)
    │
    │  VITE_API_URL = https://safetrack-edtech.hf.space
    ▼
HuggingFace Space (safetrack/edtech)
    └── API Fastify  [port 7860]   ← répond à toutes les requêtes REST du frontend
              │
              │  BullMQ (Redis partagé)
              ▼
Railway  "whatsapp-worker"
    ├── API Fastify  [port 7860]   ← PM2, même image Docker
    └── Worker Bridge [port 8082]  ← consommateur BullMQ, envoie les messages WhatsApp
                                      seul à pouvoir appeler graph.facebook.com
                                      (HuggingFace est bloqué réseau vers Meta)

Règle fondamentale : HuggingFace ne peut pas appeler graph.facebook.com directement. Tout appel Meta doit passer par le Worker Railway via un job BullMQ.


Post-Mortem #1 — Daily Limit affiche « — » (mai 2026)

Symptôme

La carte "Daily Limit" (Limite Quotidienne WhatsApp) sur /clients affichait au lieu du tier (TIER_1K, TIER_10K…).

Cause racine

Le service fetchMetaStatus appelait l'API Meta avec la version v19.0 :

`https://graph.facebook.com/v19.0/${phoneNumberId}?fields=messaging_limit_tier,quality_rating`

v19.0 a été dépréciée en février 2026 (2 ans après sa sortie en février 2024). Meta renvoie une erreur de dépréciation, mais le catch { /* non-fatal */ } du code la masquait silencieusement. messagingLimitTier restait undefined → affichage .

Le même problème touchait 7 fichiers avec des versions hardcodées incohérentes (v18.0 et v19.0 mélangées).

Correction appliquée

  • Toutes les URLs Meta remplacées par process.env.META_GRAPH_API_VERSION || 'v22.0'
  • Le catch silencieux remplacé par un logger.error visible
  • Log de démarrage ajouté dans l'API et le Worker : ✅ [META] Graph API version: v22.0

Règles à retenir

⚠️ Ne jamais hardcoder une version d'API Meta dans le code.
Toujours utiliser la variable d'environnement META_GRAPH_API_VERSION.

⚠️ Meta déprécie ses versions tous les ~2 ans.
Vérifier la version active sur developers.facebook.com/docs/graph-api/changelog lors de chaque mise à jour majeure du projet.

⚠️ Un catch { /* non-fatal */ } masque les bugs en production.
Tout catch doit au minimum faire un logger.error(err).

Procédure de mise à jour future

  1. Changer META_GRAPH_API_VERSION=vXX.0 dans Railway (variables d'env)
  2. Redémarrer le service → vérifier le log ✅ [META] Graph API version: vXX.0
  3. Vider le cache Redis : redis-cli --scan --pattern "meta:status:*" | xargs redis-cli del
  4. Aucun changement de code nécessaire

Post-Mortem #2 — SSE Stream : 401 Unauthorized (mai 2026)

Symptôme

safetrack-edtech.hf.space/v1/organizations/.../stream  →  401 Unauthorized

La connexion SSE (Server-Sent Events) pour les notifications en temps réel échouait systématiquement. Le badge de messages non lus ne se mettait jamais à jour.

Cause racine : deux problèmes distincts

2A — VITE_API_URL pointait vers le mauvais service À un moment, la variable pointait vers https://whatsapp-worker-production-0bc0.up.railway.app (le Worker Railway) au lieu de https://safetrack-edtech.hf.space (l'API HuggingFace). Le Worker n'expose que 2 routes (/v1/internal/whatsapp/inbound et /health). Tous les autres appels retournaient 401/404.

2B — L'API EventSource du navigateur ne peut pas envoyer de headers

// ❌ AVANT — le JWT n'arrive jamais au serveur
const es = new EventSource(`${apiBase}/v1/organizations/${orgId}/stream`);

// ✅ APRÈS — le JWT passe en query param
const es = new EventSource(`${apiBase}/v1/organizations/${orgId}/stream?token=${encodeURIComponent(token)}`);

L'API web EventSource est une limitation navigateur : elle ne supporte pas les headers personnalisés (Authorization: Bearer ...). Le serveur ne recevait donc jamais le JWT → 401.

Correction appliquée

  • Frontend (MainLayout.tsx, CrmConversationalDashboard.tsx) : ajout de ?token= dans l'URL EventSource
  • Backend (verifyJwt.ts) : injection du query param dans le header Authorization avant jwtVerify()
// verifyJwt.ts — fallback query param pour SSE
const queryToken = (request.query as Record<string, string>)?.token;
if (queryToken && !request.headers.authorization) {
    request.headers.authorization = `Bearer ${queryToken}`;
}
await request.jwtVerify();

Règles à retenir

⚠️ EventSource ne supporte pas les headers custom.
Toute connexion SSE nécessite le token en query param (?token=...). C'est le standard pour SSE — ne pas essayer de passer le JWT autrement.

⚠️ VITE_API_URL doit toujours pointer vers l'API HuggingFace, jamais vers le Worker Railway.
Valeur correcte sur Netlify : https://safetrack-edtech.hf.space

ℹ️ Le service Railway s'appelle "whatsapp-worker" mais fait tourner les deux processus (API + Worker) via PM2 sur des ports distincts (7860 et 8082). Le nom du service Railway est trompeur.


Post-Mortem #3 — Import Excel : "File chooser dialog can only be shown with a user activation" (mai 2026)

Symptôme

Cliquer sur "Parcourir mes fichiers" dans le CRM ouvrait un sélecteur de fichier, mais après sélection du fichier, une erreur console bloquait le traitement :

File chooser dialog can only be shown with a user activation.
onFileUpload @ index-BcnTfhzI.js:598

Cause racine

Deux <input type="file"> en cascade — architecture incorrecte entre CrmAIAssistant et FileImporter :

Utilisateur clique "Parcourir mes fichiers"
    └── CrmAIAssistant : <label> ouvre son propre <input type="file"> ← Input 1
              │
              └── onChange={() => onFileUpload()}  ← déclenché après sélection
                        │
                        └── document.getElementById('crm-file-upload')?.click()
                                  │                   ← Input 2 (FileImporter)
                                  └── 🚫 BLOQUÉ — .click() depuis un onChange()
                                      n'est pas une "user activation" directe
                                      (Chrome 126+, Safari 17+)

Le fichier sélectionné dans l'Input 1 était ignoré. L'Input 2 tentait de s'ouvrir depuis un onChange — ce que les navigateurs modernes refusent.

Correction appliquée

Suppression de l'input embarqué dans CrmAIAssistant. Le <label> pointe maintenant directement sur l'input de FileImporter via htmlFor :

// ❌ AVANT — double input en cascade
<label>
    Parcourir mes fichiers
    <input type="file" onChange={() => onFileUpload()} />  {/* déclenche un 2ème input */}
</label>

// ✅ APRÈS — un seul input, liaison directe
<label htmlFor="crm-file-upload">
    Parcourir mes fichiers
</label>
{/* FileImporter rend : <input id="crm-file-upload" type="file" onChange={handleFile} /> */}

Règles à retenir

⚠️ Ne jamais appeler .click() sur un <input type="file"> depuis un handler onChange, setTimeout, ou toute fonction async.
Les navigateurs modernes exigent une activation utilisateur directe (click, keydown) pour ouvrir un sélecteur de fichier.

Pattern correct : utiliser <label htmlFor="id-de-l-input"> pour déclencher un input caché. C'est natif, fiable dans tous les navigateurs, aucun JavaScript nécessaire.

⚠️ Ne jamais avoir deux <input type="file"> pour le même flux d'upload.
Un seul input, un seul handler. Si plusieurs composants doivent déclencher le même upload, ils partagent tous le même id via htmlFor.


Checklist avant tout déploiement

  • META_GRAPH_API_VERSION est défini dans les variables d'env Railway ET est ≥ v21.0
  • VITE_API_URL sur Netlify = https://safetrack-edtech.hf.space (HuggingFace, pas Railway)
  • Toute connexion SSE passe le token en ?token= dans l'URL
  • Tout catch loggue au minimum l'erreur
  • Aucun appel direct à graph.facebook.com depuis le code HuggingFace — passer par BullMQ

Variables d'environnement critiques

Variable Service Valeur correcte Risque si absent/faux
META_GRAPH_API_VERSION Railway API + Worker v22.0 (ou version récente) Daily Limit affiche , erreurs Meta silencieuses
VITE_API_URL Netlify https://safetrack-edtech.hf.space Tous les appels API retournent 401/404
JWT_SECRET Railway chaîne aléatoire ≥ 64 chars Auth compromise
ADMIN_API_KEY Railway chaîne aléatoire ≥ 32 chars Bridge Worker non sécurisé
REDIS_URL Railway + HF URL Redis partagée BullMQ ne fonctionne plus, pas de jobs
DATABASE_URL Railway + HF Neon PostgreSQL Toute l'app en erreur