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 |