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** :
```typescript
`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](https://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**
```typescript
// ❌ 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()`
```typescript
// 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` :
```tsx
// ❌ 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 |