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