File size: 9,413 Bytes
b8d93e2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | # 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 |
|