fix(StatCards): replace plan === 'free' with !hasAccess(plan, 'dashboard') (FTD-39) refactor(useAudioRecorder): move optionsRef assignment to useEffect (FTD-38) docs(TECH_DEBT): v1.27 — freeze FTD-09/33/42, close FTD-14/35/38/39
60 KiB
TECH_DEBT.md — Expria Frontend
Document de référence — Version 1.27 Ce document recense les décisions techniques prises par pragmatisme qui devront être revisitées, les stubs temporaires, et les fonctionnalités reportées. À mettre à jour après chaque session de développement.
Format : chaque entrée a un identifiant (FTD-XX pour Frontend Tech Debt, différent des
TD-XXbackend), une priorité, un statut. Priorités : 🔴 Critique (bloque la production) / 🟡 Important / 🟢 Mineur
Politique de gestion de la dette
Pour éviter que ce document devienne un cimetière de dette ignorée (le piège classique documenté dans ONBOARDING.md) :
- Maximum 15 FTD actives simultanément. Au-delà, priorisation obligatoire avant d'en ajouter une nouvelle.
- Chaque release production doit résoudre au moins 1 FTD de priorité ≥ 🟡.
- Toute FTD 🔴 doit être résolue dans la session suivante ou bloquer la release.
- Revue trimestrielle du document (Hermann + futur dev).
- Chaque FTD doit avoir une estimation de session (1h, 1 jour, 3 jours) pour permettre la priorisation.
1. Dettes héritées de l'audit backend (2026-04-17)
FTD-01 — Inconsistance des codes de validation côté backend
Priorité : 🟡 Important Statut : Ouvert — dépend du backend Estimation de session : 2h (backend uniquement) Description : Le backend utilise deux codes d'erreur pour la même classe (corps de requête invalide) :
VALIDATION_ERRORdansroutes/simulations.tsetroutes/corrections.tsINVALID_BODYdansroutes/plans.tsetroutes/stripe.ts
Le frontend gère les deux de la même manière (voir ARCHITECTURE.md §5), mais c'est une odeur de code qui devrait être unifiée.
Action côté frontend : aucune — on gère les deux codes.
Action côté backend : unifier sous VALIDATION_ERROR (plus explicite), session dédiée à ouvrir via TD-15 dans expria-backend/docs/TECH_DEBT.md.
Condition de résolution : après résolution de TD-15 backend, simplifier le switch dans les handlers frontend pour ne traiter que VALIDATION_ERROR.
FTD-02 — Header X-API-Version envoyé mais non vérifié
Priorité : 🟡 Important
Statut : Ouvert
Estimation de session : 1h (backend) + 30min (frontend)
Description : Le frontend envoie le header X-API-Version: 1.0 sur toutes les requêtes API (cf. ARCHITECTURE.md §5). Le backend ne le vérifie pas actuellement — donc en pratique, aucune incompatibilité de version ne sera détectée automatiquement.
Impact : si le backend évolue de façon breaking (ex : format de réponse de /plans/status modifié), le frontend peut recevoir un payload incompatible sans message d'erreur clair. Symptôme : bugs silencieux en production après un déploiement backend.
À faire :
- Backend : ajouter un middleware qui lit
X-API-Version, le log, et retourneHTTP 426 Upgrade Requiredavec codeAPI_VERSION_MISMATCHsi breaking change - Frontend : gérer
API_VERSION_MISMATCHdansapi-client.ts→ afficher un message "Une nouvelle version est disponible, veuillez rafraîchir la page"
Condition de résolution : avant l'arrivée d'un dev externe (qui pourrait modifier le backend sans coordination).
FTD-03 — Quirk status dans le body des erreurs de simulations/corrections
Priorité : 🟢 Mineur
Statut : Ouvert — dépend du backend
Estimation de session : 1h (backend uniquement)
Description : Les routes POST /simulations et POST /corrections/ee,eo renvoient un champ status dans le body JSON d'erreur, qui duplique le code HTTP :
{ "error": true, "code": "QUOTA_REACHED", "message": "...", "status": 403 }
Vient du pattern c.json(result, result.status) où result contient déjà status. C'est ignorable côté frontend (on ne lit pas ce champ), mais c'est du bruit.
À faire côté backend : nettoyer les objets d'erreur retournés par simulationController et correctionController pour ne pas contenir de champ status. Tracé dans TD-16 à créer dans expria-backend/docs/TECH_DEBT.md.
Condition de résolution : session de nettoyage backend, non urgente.
2. Dettes frontend propres
FTD-10 — Semgrep non intégré en CI
Priorité : 🟢 Mineur
Statut : Reporté — après MVP
Estimation de session : 2h
Description : ARCHITECTURE.md §CI mentionne semgrep scan (via plugin) dans le workflow CI cible. L'étape 10 du Sprint 0 n'a intégré que lint, format:check, typecheck, test, npm audit --audit-level=high — Semgrep a été volontairement différé pour respecter la règle de scope (max 2-3 fichiers par étape).
Impact actuel : npm audit couvre les vulnérabilités des dépendances npm, mais aucune analyse statique de sécurité (SAST) n'est faite sur le code custom du projet. Des patterns dangereux (eval, innerHTML sans DOMPurify, secrets en dur, etc.) passeraient inaperçus en CI.
À faire :
- Ajouter un step Semgrep au workflow
.github/workflows/ci.yml - Utiliser les rulesets
auto+r2c-security-audit+r2c-ci - Configurer la sortie pour bloquer sur sévérité ERROR uniquement
- Documenter dans SEC-08 de
SECURITY.md
Condition de résolution : avant l'arrivée d'un dev externe (même raisonnement que FTD-02).
FTD-14 résolu au Sprint 5.5 (2026-04-26) — voir §5 Historique des résolutions. FTD-17, FTD-18, FTD-19 résolus au Sprint 3.5 (2026-04-22) — voir §5 Historique des résolutions.
FTD-30 — Rotation token Deepgram sans grace period
Priorité : 🟢 Mineur
Statut : Gelé — Sprint 4c-3 (Deepgram live mis en pause au profit de Gemini batch backend)
Estimation de session : 0,5 jour
Description : useDeepgramLive redemande un token à T-60 s avant expiration et hot-swap la WebSocket. Si la nouvelle échoue à s'ouvrir avant l'expiration, des chunks peuvent être perdus. Code dormant depuis le Sprint 4c-3 — à ré-évaluer si Deepgram live est réactivé (cf. FTD-37).
À faire : retry policy explicite + maintien de l'ancienne connexion tant que la nouvelle n'a pas reçu son premier message. Hors scope tant que le hook reste dormant.
FTD-31 — Page EnregistrementEOPage non resumable au refresh
Priorité : 🟢 Mineur
Statut : Ouvert — introduit au Sprint 4c-1
Estimation de session : 0,5 jour
Description : Si l'utilisateur ferme l'onglet ou recharge la page pendant l'enregistrement, le transcript live et l'audio sont perdus. La simulation côté backend reste avec rapport=null mais sans contenu textuel : au resume, le provider redirige vers /simulation/eo/pre-enregistrement et l'utilisateur doit recommencer.
À faire : persister un buffer du transcript final dans localStorage à chaque is_final=true, restaurer au resume comme point de départ. Décider si on autorise la reprise « par-dessus » ou si on impose un nouveau départ.
Condition de résolution : session dédiée autosave EO post-MVP.
FTD-32 — useAudioRecorder non testé sur Safari iOS
Priorité : 🟢 Mineur
Statut : Ouvert — introduit au Sprint 4c-1
Estimation de session : 0,5 jour
Description : pickMimeType() propose un fallback audio/mp4 pour Safari, mais aucun test manuel n'a été réalisé. Le bouton « Télécharger l'audio » nomme toujours le fichier .webm même quand le mime réel est audio/mp4.
À faire : validation manuelle iOS, adapter l'extension du fichier téléchargé au mime réel via audioMimeType.
Condition de résolution : une fois la version iPhone validée par un testeur réel.
FTD-34 — Présentation T1 stockée en clair dans localStorage
Priorité : 🟢 Mineur
Statut : Ouvert — introduit au Sprint 4c-2
Estimation de session : 0,5 jour
Description : expria_eo_t1_presentation contient le texte de la présentation personnelle de l'utilisateur (prénom, âge, ville, parcours, situation familiale, projet d'immigration). Stocké en clair, accessible à tout script tiers exécuté dans le contexte du domaine. Acceptable au MVP : aucune donnée sensible au sens RGPD strict (pas de mot de passe ni numéro fiscal), mais le contenu reste personnel.
À faire : chiffrement AES-GCM avec clé dérivée du JWT Supabase, ou bascule vers IndexedDB chiffré (libs : idb-keyval + Web Crypto API). Étendre à toute persistance sensible si on en ajoute (transcripts, audio, etc.).
Condition de résolution : quand on stocke un jour des contenus plus sensibles via le même mécanisme.
FTD-35 fermée au Sprint 5.5 (2026-04-26) — subsumée par FTD-41. Voir §5 Historique.
FTD-36 — Upload audio base64 in-memory sans indicateur de progression
Priorité : 🟡 Important
Statut : Ouvert — introduit au Sprint 4c-3
Estimation de session : 1 jour
Description : EnregistrementEOPage encode le Blob audio en base64 via FileReader.readAsDataURL puis envoie le résultat dans le body JSON de POST /corrections/eo. Pour 6 minutes d'audio webm/Opus à 32 kbps ≈ 1,5 Mo binaire ≈ 2 Mo base64. Reste sous le cap 14 Mo backend, mais : (a) tout est chargé en mémoire navigateur, (b) aucun indicateur de progression d'upload (le banner « Transcription et correction en cours » couvre les ~30-60 s totales sans distinguer upload/Gemini/DeepSeek), (c) retry impossible côté navigateur si la connexion mobile coupe en cours d'upload.
À faire : passer à multipart/form-data avec XMLHttpRequest.upload.onprogress ou fetch + ReadableStream ; afficher une barre de progression upload distincte de l'état serveur.
Condition de résolution : observer un cas réel de plantage mobile/edge OU avant ouverture publique.
FTD-37 — Code Deepgram live dormant à trancher
Priorité : 🟢 Mineur Statut : Ouvert — introduit au Sprint 4c-3 Estimation de session : 1 jour (réactivation) ou 0,5 jour (suppression) Description : Sprint 4c-3 a basculé la transcription EO sur Gemini batch côté backend. Les artefacts Deepgram live restent en place mais sans consommateur :
- Frontend :
useDeepgramLive,TranscriptionDisplay,entities/transcription/api.ts+ tests associés - Backend : route
POST /transcriptions/token,lib/deepgram.ts+ tests associés Décision de garde : conservés 30 jours après la mise en prod du Sprint 4c-3 puis on tranche. Soit (a) réactivation pour réduire la latence perçue (transcription live pendant l'enregistrement vs attente serveur après stop), soit (b) suppression définitive si le retour utilisateur sur la latence Gemini est acceptable. À faire : trancher au plus tard 30 jours après la première mise en prod de cette session.
FTD-38, FTD-39 résolus au Sprint 5.5 (2026-04-26) — voir §5 Historique des résolutions.
FTD-40 — Conclusion rapport incohérente quand NCLC atteint > cible (backend)
Priorité : 🟡 Important
Statut : Ouvert — patch frontend temporaire en place (Sprint 4.5)
Estimation de session : 1h (session backend)
Description : Le prompt maître DeepSeek génère toujours un message d'encouragement vers l'objectif cible, même quand nclcObtenu > nclcCible. Le champ conseil_nclc.action_prioritaire contient alors un texte incohérent (« tu atteindras facilement le niveau 9 » pour un candidat NCLC 10). Patch frontend en place dans ConseilNclcCallout.tsx (condition depasse → texte générique). Fix robuste : modifier le prompt maître backend pour détecter nclcObtenu > nclcCible et générer un message de maintien/progression vers NCLC suivant.
Condition de résolution : prompt backend mis à jour + patch frontend retiré.
FTD-41 — Persistance présentation EO T1 en base de données
Priorité : 🔴 Critique
Statut : Ouvert — localStorage instable (FTD-35 partiellement liée)
Estimation de session : 1 jour (session fullstack)
Description : La présentation générée pour EO T1 est stockée uniquement en localStorage (expria_eo_t1_presentation). Au refresh, une redirect prématurée dans PresentationGenereeT1Page (shouldRedirect déclenché avant hydratation async complète) efface la session. Solution retenue : nouvelle colonne presentation_t1 (TEXT, nullable) sur la table productions + PATCH /simulations/:id/presentation + bouton « Sauvegarder » explicite dans PresentationGenereeT1Page. Le localStorage devient brouillon temporaire uniquement. Résout FTD-35.
Condition de résolution : migration DB + endpoint backend + bouton frontend implémentés et testés.
FTD-42 gelée au Sprint 5.5 (2026-04-26) — voir §3bis Backlog gelé.
FTD-43 — Race condition webhook post-redirect Stripe Checkout
Priorité : 🟢 Mineur
Statut : Ouvert — introduit Sprint 5c (2026-04-26)
Estimation de session : 0,5 jour
Description : Après un Stripe Checkout réussi, le frontend revient sur /dashboard?upgrade=success. useUpgradeSuccessHandler invalide PLAN_QUERY_KEY immédiatement. Mais le webhook checkout.session.completed peut arriver côté backend après le redirect frontend (latence réseau Stripe → Render typiquement 1-3 s). Si l'invalidation refetch trop tôt, usePlan() retourne encore l'ancien plan. L'utilisateur voit son ancien plan dans le dashboard pendant quelques secondes.
Mitigation actuelle : UpgradeSuccessBanner affiche explicitement « Si certaines features semblent manquer, rafraîchissez la page dans quelques secondes ». Acceptable au MVP.
À faire si remonté en prod :
- Polling court : refetch automatique de
usePlan()toutes les 2 s pendant 30 s tant que le plan reçu === ancien plan. - OU : ajouter un endpoint
GET /plans/status?wait_for_change=truecôté backend qui long-poll jusqu'au changement (max 10 s) et retourne le nouveau plan. - OU : Stripe envoie un event WebSocket via Pusher / SSE — out of scope MVP.
Condition de résolution : observer en production. Si > 5 % des upgrades produisent un message support, prioriser.
FTD-33 gelée au Sprint 5.5 (2026-04-26) — voir §3bis Backlog gelé.
FTD-24 — Pas de polling automatique pour exercices / modèle pending
Priorité : 🟡 Important
Statut : Résolu — 2026-04-23
Estimation de session : 2h
Description : Après soumission d'une correction EE, le backend génère la correction en bloquant (jusqu'à 45 s), puis retourne 200 dès que la correction est prête. Les jobs modele et exercices (fire-and-forget côté backend) peuvent mettre 10-30 s supplémentaires après la réponse HTTP. Pendant ce temps, exercices_status et modele_status valent 'pending' côté GET /simulations/:id. Côté frontend, RapportPage affiche un JobStatusFallback invitant l'utilisateur à rafraîchir manuellement la page pour voir les résultats.
Impact UX : l'utilisateur voit le rapport principal immédiatement, mais doit recharger pour voir ses exercices + production modèle. Expérience acceptable en MVP mais sous-optimale.
À faire :
- Hook
useRapport: déclencher un polling automatique via TanStack QueryrefetchInterval: 3000siexercices_status === 'pending' || modele_status === 'pending'. - Arrêt du polling dès que les deux statuts sortent de
'pending'(ready ou error). - Afficher un indicateur visuel discret pendant le polling actif (petit spinner dans JobStatusFallback).
- Timeout de polling : max 2 minutes → message "La génération prend plus de temps que prévu" + bouton Réessayer.
Lien avec TD-15 backend : si le process backend redémarre pendant un job, le statut reste indéfiniment 'pending'. Le timeout frontend atténue ce problème côté UX (on arrête de poller après 2 min).
Condition de résolution : après Sprint 3.6c (patterns) si la patience utilisateur devient un frein.
FTD-23 — useAutosave continue après correction → 400 VALIDATION_ERROR
Priorité : 🟡 Important
Statut : Résolu — 2026-04-23
Estimation de session : 30 min
Description : Le hook useAutosave (cf. src/features/simulations/hooks/useAutosave.ts) peut déclencher un PATCH /simulations/:id/contenu après que la correction a été persistée (colonne rapport !== null). Le backend refuse alors avec 400 VALIDATION_ERROR message « Cette simulation a déjà été corrigée. » (cf. simulationController.autosaveContenu backend lignes 248-255).
Scénario déclencheur :
- L'utilisateur soumet sa production →
rapportpersisté côté backend. SimulationFormpassestepà'done', mais :- Le timer d'autosave debouncé (30 s) peut encore fire après cette transition si le debounce n'est pas clear.
- Un
beforeunloadhandler peut déclencher unflush()final même une fois la correction reçue.
useAutosave.enabledest calculé comme!isSubmittingdansSimulationForm— il redevienttrueaprès la correction (quandisSubmittingrepasse àfalse).
À faire :
- Propager
enabled = !isSubmitting && step !== 'done' && step !== 'correcting'depuisSimulationForm - OU : au montage, quand
rapportdevient non null après correction, clear le timeout debouncé et retirer le handlerbeforeunloadimmédiatement. - Ajouter un test regression dans
useAutosave.test.tsqui vérifie qu'aucunautosaveContenun'est appelé aprèsstep='done'.
Impact actuel : erreur 400 dans les DevTools Network uniquement (pas d'impact UX — le texte est déjà corrigé, la sauvegarde n'est plus nécessaire). Pollue les logs frontend et backend.
Condition de résolution : session dédiée — ne bloque pas le Sprint 3.6b.
FTD-25 — Mise à jour ARCHITECTURE.md §3 (arborescence réelle)
Priorité : 🟢 Mineur
Statut : Résolu — 2026-04-25
Estimation de session : 1h
Description : ARCHITECTURE.md §3 ne liste pas entities/patterns, features/historique, features/progression, features/design-system (ajoutés aux Sprints 3.6c et 3.7). Les composants layout (AppLayout, Sidebar, MobileHeader, BottomNav, MaintenancePage) sont dans app/ alors que §3 ne prévoit que providers, router, main dans ce dossier.
À faire :
- Mettre à jour ARCHITECTURE.md §3 pour refléter l'arborescence réelle.
- Formaliser
app/comme contenant entry points + composants layout de la coquille OU déplacer versshared/components/layout/.
Condition de résolution : ARCHITECTURE.md §3 reflète l'arborescence réelle.
FTD-26 — Clarifier cohabitation shared/ui/ vs shared/components/ui/
Priorité : 🟡 Important Statut : Résolu — 2026-04-25 Estimation de session : 2h Description : Deux conventions UI cohabitent sans documentation :
src/shared/ui/{Button,Card,Badge}.tsx(PascalCase) — wrappers Expria, 40+ imports dans les features.src/shared/components/ui/{button,dialog,input,…}.tsx(kebab-case) — primitives shadcn/ui, 7 fichiers consommateurs.
Risque : confusion pour un futur dev sur quel composant utiliser.
À faire : documenter la convention dans ARCHITECTURE.md (distinction wrappers Expria / primitives shadcn) OU regrouper sous un seul dossier (ex. shared/components/ui/primitives/ + shared/components/ui/expria/).
Condition de résolution : un seul pattern documenté et appliqué.
3. Fonctionnalités reportées
FTD-07 — Sentry non intégré
Priorité : 🟡 Important Statut : Planifié — après MVP Estimation de session : 3h Description : Le monitoring frontend (erreurs JS, performances, sessions) n'est pas encore en place. Sans Sentry (ou équivalent), les bugs en production ne remontent pas — on les découvre uniquement si un utilisateur prend la peine de les signaler.
À faire :
- Créer un compte Sentry (tier gratuit suffit pour démarrer)
- Ajouter
@sentry/reactau projet - Intégrer dans
src/app/providers.tsx - Ajouter
VITE_SENTRY_DSNdans les variables d'environnement - Configurer le filtrage des données sensibles (JWT, emails)
- Mettre à jour SEC-11 dans
SECURITY.md
Condition de résolution : avant la première vague d'utilisateurs post-MVP (30 jours après lancement).
FTD-21 — Persistance session simulation
Priorité : 🔴 Critique
Statut : Partiellement résolu — /simulation/ee ✅ (2026-04-21)
Pages concernées par ordre de priorité :
✅ /simulation/ee (résolu 2026-04-21)
- Autosave contenu toutes les 30 s (
useAutosave) - Save on
beforeunload - Reprise au refresh via
localStorage(expria_simulation_id) +GET /simulations/:id PATCH /simulations/:id/contenu+PATCH /simulations/:id/sujet(Option C)getByIdtolèrerapport=null(Option A)RapportPageredirige vers/simulation/eesi simulation en cours
🟡 /simulation/eo (Sprint 4 — ouvert)
- Identique EE + état audio/enregistrement
🟡 /examen (Sprint 7 — ouvert)
- Autosave critique — timer inarrêtable + 3 tâches
- Crash pendant examen = perte totale
🟢 /sujets (inclus dans la résolution EE)
localStorage simulation_idsuffit- Pas d'autosave (pas de données saisies)
❌ Pas nécessaire : /dashboard, /rapport/:id, /historique, /progression
Résolution EE livrée (2026-04-21) :
Backend :
simulationController.createpersistesujet_idà la créationgetByIdretourneSimulationState(tolèrerapport=nullpour resume)autosaveContenu+updateSujetcontrollers (refuse sirapport !== null)- Routes
PATCH /simulations/:id/contenu+PATCH /simulations/:id/sujet - CORS :
allowMethodsétendu à PATCH/PUT/DELETE
Frontend :
useAutosave: debounce 30 s +beforeunloadflush + dedup par contenuSimulationForm: hydrateinitialContenu, affiche "Sauvegardé à HH:MM"SimulationFlowProvider: hydratation au montage depuislocalStorage→ restaure steptask-selectedsi rapport null, nettoie sinongetReportdélègue àgetSimulationStateet throwREPORT_NOT_READYsi rapport null
Condition de résolution complète : intégration EO (Sprint 4) + examen (Sprint 7).
3bis. Backlog gelé — post-MVP
Ces FTDs sont volontairement gelées : elles concernent des fonctionnalités non encore livrées (T2 Live, tests E2E) ou du confort utilisateur non bloquant (option
'system'thème). Elles ne comptent pas dans le cap de 15 FTD actives et seront réactivées quand leur sprint arrive ou quand la condition de déblocage (post-MVP) est atteinte.
FTD-06 — AudioWorklet au lieu de ScriptProcessorNode (T2 Live)
Priorité : 🟢 Mineur
Statut : Gelé — post-MVP (T2 Live non encore implémenté)
Estimation de session : 1 jour
Description : Hérité du backend (TD-09). Côté frontend, le traitement audio pour la T2 Live (capture PCM 16kHz) devra probablement utiliser AudioWorklet au lieu de ScriptProcessorNode qui est déprécié.
Impact actuel : fonctionne avec warnings dans la console. Peut poser problème sur certains navigateurs futurs.
À faire : session dédiée après le lancement MVP, pour migrer le pipeline audio vers AudioWorklet.
Condition de résolution : après 30 jours de production stable.
FTD-08 — Tests E2E non implémentés
Priorité : 🟢 Mineur
Statut : Gelé — post-MVP (accepté par design)
Estimation de session : 2 jours (Playwright setup)
Description : Actuellement, les tests de bout en bout sont manuels (via GOLDEN_DATASET.md). Une automatisation avec Playwright permettrait de détecter les régressions UI sans effort humain.
À faire : session Playwright setup après MVP, pour automatiser au minimum les 10 scénarios du Groupe Z (smoke test).
Condition de résolution : quand la maintenance manuelle du Golden Dataset devient trop chronophage.
FTD-09 — Tests de la state machine T2 Live non implémentés
Priorité : 🟡 Important
Statut : Gelé — Sprint 5.5 (2026-04-26)
Estimation de session : 3h
Description : La state machine T2 Live (src/features/t2-live/state/t2-machine.ts) n'existe pas encore. Quand elle sera créée, elle devra être testée de manière exhaustive (6+ tests couvrant les transitions d'états et les cas d'erreur).
Motif de gel : Gelé — le code n'existe pas encore, sera créé et testé au Sprint 6.
Condition de résolution : fin Sprint 6 (T2 Live).
FTD-33 — Carte EO_T2_LIVE verrouillée en dur (pas via hasAccess)
Priorité : 🟢 Mineur
Statut : Gelé — Sprint 5.5 (2026-04-26)
Estimation de session : 0,5 jour
Description : Dans TaskSelector, la carte EO_T2_LIVE a tache: null ce qui la rend inactive pour tous les plans, indépendamment de hasAccess(plan, 'oral_t2_live'). C'est volontaire tant que T2 Live n'est pas livré (Sprint 6) — un utilisateur Premium ne doit pas accéder à une feature non implémentée. À nettoyer dès que T2 Live est wired pour respecter strictement la Règle D.
Motif de gel : Gelé — condition de résolution = Sprint 6 T2 Live.
Condition de résolution : lancement de T2 Live (Sprint 6).
FTD-42 — Modal prorata Standard→Premium avec montant exact
Priorité : 🟡 Important Statut : Gelé — Sprint 5.5 (2026-04-26) Estimation de session : 1 jour Description : Le flux Standard→Premium passe actuellement par le Stripe Customer Portal (Sprint 5d). Le portal natif Stripe affiche le montant prorata + confirmation hors de l'app. Divergence avec PARCOURS_UTILISATEURS.md §3 qui prévoit une modal in-app.
Motif de gel : Gelé — Customer Portal Stripe gère nativement le prorata. Modal in-app = confort post-MVP, pas MVP-bloquant.
Condition de résolution : post-MVP, si retour utilisateur fait remonter la friction du redirect vers Customer Portal.
FTD-15 — Option 'system' manquante dans ThemeProvider
Priorité : 🟢 Mineur
Statut : Gelé — post-MVP
Estimation de session : 2h
Description : Le ThemeProvider est bi-state ('light' | 'dark'). L'option 'system' (qui suit prefers-color-scheme en temps réel via MediaQueryList.addEventListener) a été volontairement différée (décision Sprint 0.5).
À faire :
- Étendre le type
Themeà'light' | 'dark' | 'system' - Dans
ThemeProvider, sitheme === 'system': écoutermatchMedia('(prefers-color-scheme: dark)')et appliquer/retirer.darkdynamiquement ThemeToggle: cycle light → dark → system (ou un sélecteur 3 états)- Mettre à jour
getInitialTheme()pour retourner'system'si aucune préférence stockée
Condition de résolution : après MVP — confort utilisateur, pas bloquant.
4. Tests à renforcer
FTD-09 gelée au Sprint 5.5 (2026-04-26) — voir §3bis Backlog gelé.
FTD-12 — Tests automatisés manquants pour api-client.ts
Priorité : 🟡 Important
Statut : Ouvert — à faire avant intégration des features critiques
Estimation de session : 3h
Description : Le wrapper apiFetch dans src/shared/lib/api-client.ts (créé à l'étape 7b du Sprint 0) contient une logique critique non couverte par des tests automatisés : timeout via AbortSignal.timeout, retry avec backoff exponentiel, stratégie d'idempotence (retry GET/HEAD/PUT/DELETE uniquement), parsing des erreurs backend (ApiError) vs frontend (ClientError), construction des headers (Authorization, X-API-Version, Content-Type).
Impact actuel : toute régression sur ce fichier (oubli d'un header, mauvais parsing d'une erreur, boucle de retry infinie sur un edge case) passera inaperçue jusqu'aux tests manuels ou à un bug en production.
À faire :
- Créer
src/shared/lib/__tests__/api-client.test.ts - Mocker globalement
fetchviavi.fn() - Couvrir :
- Succès 2xx → retourne le payload parsé
- Erreur 4xx
ApiErrorbien parsée → throw conforme - Erreur 4xx body non-JSON → fallback
INTERNAL_ERROR - Erreur 5xx avec retry → max 2 retries puis throw
- Timeout →
ClientErrorcodeTIMEOUT - Erreur réseau (fetch reject) →
ClientErrorcodeNETWORK_ERROR+ retry - Parsing succès JSON invalide →
ClientErrorcodePARSE_ERROR - Retry désactivé sur POST/PATCH par défaut (non-idempotent)
- Bearer omis sur
/health - Bearer injecté avec token valide depuis
auth-client
- Objectif : ≥ 10 tests, couverture complète des branches
Condition de résolution : avant l'intégration des hooks TanStack Query sur des features critiques (dashboard, simulations, auth).
5. Historique des résolutions
| ID | Description | Résolu le | Comment |
|---|---|---|---|
| FTD-11 | @theme Tailwind 4 non défini — palette et typographie absentes |
2026-04-18 | Résolu au Sprint 0.5 (design system). Palette Direction H complète (canvas/surface/ink/expria/deep/semantic) + typo Plus Jakarta Sans définis dans src/index.css via @theme {} et .dark {}. shadcn/ui remappé sur ces tokens. Règle L ajoutée dans DEVELOPMENT_PRINCIPLES.md pour garantir l'usage exclusif des tokens. |
| FTD-13 | Incompatibilité Vitest 3 / Vite 8 (conflit de types Plugin<any> entre le Vite 8 top-level avec Rolldown et le Vite 7 pinné de Vitest 3.2.4 ; npm run build cassé) |
2026-04-17 | Résolu par upgrade Vitest 3.2.4 → 4.1.4 (et @vitest/coverage-v8 idem) à l'étape 12-bis du Sprint 0. Vitest 4.x supporte nativement Vite 8 Rolldown. Correctif complémentaire : script typecheck passé de tsc --noEmit -p tsconfig.app.json à tsc -b --noEmit pour couvrir aussi tsconfig.node.json (d'où vite.config.ts) et éviter qu'un bug similaire échappe à la CI. |
| FTD-16 | VITE_MAINTENANCE_MODE non lu dans le code — la variable d'env était dans env.ts mais jamais consommée |
2026-04-18 | Résolu au Sprint 1 étape 6. Ajout de isMaintenanceMode dans src/shared/config/env.ts et garde dans src/app/main.tsx : isMaintenanceMode ? <MaintenancePage /> : <Providers />. MaintenancePage est statique (aucun provider requis), tokens Direction H exclusivement. |
| FTD-22 | Code orphelin suite à la refonte UX /sujets (2026-04-21) — composant SujetSelector et helper selectSujet plus référencés après bascule dropdown → page dédiée |
2026-04-23 | Résolution complète. SujetSelector + selectSujet supprimés. Éléments conservés (choosing-subject, goToSubjectPicker) sont activement utilisés par SimulationFlowProvider et SimulationForm — ce n'est plus de la dette. |
| FTD-20 | GET /simulations/:id manquant dans le backend |
2026-04-22 | Implémenté au Sprint 3.6a (backend) — route complète avec auth, owner check, REPORT_NOT_READY. Consommé par RapportPage et useAutosave. |
| FTD-04 | Documents miroir sans automatisation de synchronisation | 2026-04-23 | Risque accepté par design (ADR 004). Mitigation en place (Règle G, commentaire SOURCE OF TRUTH, tests de parité). Condition de ré-ouverture : si une divergence silencieuse cause 2+ bugs en production. |
| FTD-05 | Ancien scaffold frontend possiblement caduc | 2026-04-23 | Audit Claude Code complet — aucun résidu scaffold Vite, aucun fichier orphelin, règles critiques (D, E, F, G, J + ADR 003/005) respectées. Désalignements documentaires traités via FTD-25 et FTD-26. |
| FTD-29 | .github/dependabot.yml dans les 2 dépôts |
2026-04-23 | Fichier créé dans expria-frontend et expria-backend. Ecosystem npm, weekly, limit 10 PRs. Dependabot alerts + security updates activés via UI GitHub. |
| FTD-27 | CI GitHub Actions pour expria-backend | 2026-04-23 | Workflow créé : npm ci → test → audit. Node 22, trigger push/PR sur main. CI verte au premier run (21s). Observations : typecheck absent (O1), ESLint absent (O2), engines.node absent (O3) — à traiter en FTDs séparées. |
| FTD-28 | Semgrep dans CI frontend + backend | 2026-04-23 | Step semgrep scan --config=auto --error --severity=ERROR ajouté aux deux workflows CI. Backend vert au 1er run. Frontend vert après correction de 4 erreurs ESLint préexistantes + fix Prettier + ajout env vars CI. |
| FTD-17 | Clé ['plan'] dupliquée entre features (usePlan, SimulationPage, RapportPage) |
2026-04-22 | Résolu au Sprint 3.5. Création de src/entities/user/query-keys.ts (constantes pures, aucun import runtime) exportant PLAN_QUERY_KEY = ['plan'] as const. features/dashboard/hooks/usePlan.ts l'importe et le re-exporte pour conserver la rétro-compatibilité de l'import PLAN_QUERY_KEY. SimulationPage.tsx et RapportPage.tsx remplacent leur useQuery inline par le hook usePlan() — dédup totale de la clé et de la config staleTime. |
| FTD-18 | SimulationForm utilise encore le shadcn Button au lieu de la primitive @/shared/ui/Button |
2026-04-22 | Résolu au Sprint 3.5. Remplacement de l'import @/shared/components/ui/button par @/shared/ui/Button dans SimulationForm.tsx. Aucun variant à adapter (usage du Button sans prop variant → primary par défaut dans les deux implémentations). Les 7 autres consommateurs shadcn (Login/RegisterPage, PaywallBanner, DesignSystemPage, ThemeToggle, dialog.tsx) restent hors scope de cette FTD. |
| FTD-23 | useAutosave continue après correction → 400 VALIDATION_ERROR |
2026-04-23 | enabled corrigé dans SimulationForm (!isSubmitting && step !== 'done' && step !== 'correcting'). Le beforeunload handler et le debounce lisent enabled via latestRef — tous deux neutralisés dès que step transite. 2 tests de régression ajoutés dans useAutosave.test.ts : (a) enabled true→false annule le debounce en cours, (b) enabled=false + beforeunload = aucun appel. |
| FTD-24 | Pas de polling automatique pour exercices / modèle pending |
2026-04-23 | Polling conditionnel dans useRapport via refetchInterval: 3000 tant que exercices_status === 'pending' || modele_status === 'pending'. Arrêt automatique dès que les deux sortent de pending (ready ou error). Timeout global 2 min → hasTimedOut = true + bouton « Réessayer » dans JobStatusFallback (primitive @/shared/ui/Button). refetch() réinitialise le flag et relance le polling. staleTime: Infinity conservé. 5 tests nouveaux dans useRapport.test.tsx. |
| FTD-25 | Mise à jour ARCHITECTURE.md §3 (arborescence réelle) | 2026-04-25 | §3 réécrite : app/ documenté avec entry points + layout (AppLayout, Sidebar, Topbar, BottomNav, MaintenancePage) ; ajout entities/{patterns,presentation,transcription} ; ajout features/{historique,progression,design-system} ; extension simulations/ (pages EO, components/rapport/, lib/, state/) ; mise à jour shared/. t2-live/ et billing/ retirés (non implémentés — voir ROADMAP). Note explicative ajoutée sous app/. Bump doc v1.1. |
| FTD-26 | Clarifier cohabitation shared/ui/ vs shared/components/ui/ |
2026-04-25 | Section dédiée ajoutée dans ARCHITECTURE.md §3 : tableau de distinction (PascalCase wrappers Expria vs kebab-case primitives shadcn) + règle d'évolution (toute nouvelle primitive Expria va dans shared/ui/, shared/components/ui/ réservé à la CLI shadcn). Aucun fichier déplacé — documentation uniquement. |
| FTD-14 | Anti-FOUC thème : script inline manquant dans <head> |
2026-04-26 | Sprint 5.5 — Script .light déjà en place dans index.html (lignes 14-20), conforme DESIGN_SYSTEM v2.0. L'exemple .dark documenté dans la fiche FTD-14 datait de la DA Boréal v1.0 (obsolète). Aucune action code requise — FTD fermée comme déjà résolue. |
| FTD-35 | PresentationGenereeT1Page : refresh sans simulation active |
2026-04-26 | Sprint 5.5 — Subsumée par FTD-41 : la résolution de FTD-41 (persistance T1 en BDD) élimine le problème de FTD-35 (localStorage instable). Aucune action propre. |
| FTD-38 | useAudioRecorder : mise à jour de ref pendant le render |
2026-04-26 | Sprint 5.5 — Refactor optionsRef.current = options (assignation pendant render + eslint-disable) en useEffect(() => { optionsRef.current = options }). Sémantique préservée : effet sans deps run après chaque commit, donc avant le prochain render qui lit la ref. eslint-disable retiré. 195 lignes de tests useAudioRecorder.test.ts toujours vertes (219/219). |
| FTD-39 | Règle D violée dans StatCards.tsx (plan === 'free' en dur) |
2026-04-26 | Sprint 5.5 — Remplacement de {plan === 'free' && ...} (ligne 90) par {!hasAccess(plan, 'dashboard') && ...}. Sémantique du gating : afficher « Renouvellement offert à l'upgrade » uniquement aux utilisateurs sans accès au dashboard complet (= Free). Import hasAccess ajouté depuis @/entities/user/lib. Tests Dashboard verts. |
| FTD-19 | Token --shadow-focus absent de src/index.css |
2026-04-22 | Résolu au Sprint 3.5. Ajout de --shadow-focus: 0 0 0 3px rgba(27, 79, 216, 0.18) dans @theme {} (valeur conforme à DESIGN_SYSTEM.md §2) et --shadow-focus: 0 0 0 3px rgba(91, 127, 255, 0.32) dans .dark {} (recalculé sur la teinte expria dark #5B7FFF). Tailwind 4 génère automatiquement l'utility shadow-focus. Migration de 5 occurrences ring-2 ring-expria/20 → shadow-focus dans Button.tsx, Card.tsx, SimulationForm.tsx (×3), SpecialCharsKeyboard.tsx. Factorisation bonus : className dupliquée des boutons secondaires de SimulationForm extraite en const secondaryActionBtn. |
6. Historique de ce document
| Version | Date | Changements |
|---|---|---|
| 1.0 | 2026-04-17 | Création initiale avec 9 FTD identifiées depuis l'audit backend et les décisions d'architecture |
| 1.1 | 2026-04-17 | Ajout FTD-10 (Semgrep CI), FTD-11 (@theme Tailwind 4), FTD-12 (tests api-client) suite à l'étape 11 du Sprint 0 |
| 1.2 | 2026-04-17 | Ajout FTD-13 résolu (incompatibilité Vitest 3 / Vite 8) suite à l'étape 12-bis du Sprint 0 |
| 1.3 | 2026-04-18 | FTD-11 résolu (design system Sprint 0.5) ; ajout FTD-14 (anti-FOUC), FTD-15 (option 'system' thème) |
| 1.4 | 2026-04-18 | FTD-16 résolu (VITE_MAINTENANCE_MODE implémenté — Sprint 1 étape 6) |
| 1.5 | 2026-04-19 | Ajout FTD-17 (clé ['plan'] dupliquée entre features — Sprint 3 étape 14) |
| 1.6 | 2026-04-20 | Ajout FTD-18 (SimulationForm shadcn Button — Sprint 0.5 bis D2) ; ajout FTD-19 (token --shadow-focus manquant — Sprint 0.5 bis D2) |
| 1.7 | 2026-04-20 | Ajout FTD-20 🔴 (GET /simulations/:id manquant backend — bloque RapportPage Sprint 3 étape 15) |
| 1.8 | 2026-04-20 | Ajout FTD-21 🔴 (persistance session simulation — prod + sujet perdus au refresh, session dédiée après G1-G5) |
| 1.9 | 2026-04-21 | FTD-22 résolu partiellement (nettoyage code orphelin refonte /sujets — SujetSelector + selectSujet supprimés ; choosing-subject + goToSubjectPicker conservés) |
| 1.10 | 2026-04-21 | FTD-21 résolu partiellement pour /simulation/ee (autosave 30 s + beforeunload + reprise via localStorage + PATCH /:id/contenu + PATCH /:id/sujet + getById tolère rapport=null) ; EO + examen restent ouverts |
| 1.11 | 2026-04-22 | Sprint 3.5 Clean — FTD-17, FTD-18, FTD-19 résolus. 15 FTD actives restantes (cap de 15 respecté) |
| 1.12 | 2026-04-22 | Sprint 3.6a — Ajout FTD-23 🟡 (useAutosave fire après correction). 16 FTD actives → cap de 15 dépassé temporairement, à revoir au prochain clean. |
| 1.13 | 2026-04-22 | Sprint 3.6b — Ajout FTD-24 🟡 (polling auto exercices/modèle pending). 17 FTD actives → cap dépassé, un clean 3.6.5 devra résoudre FTD-23/24 ensemble. |
| 1.14 | 2026-04-23 | Triage : FTD-04, FTD-05, FTD-20, FTD-22 fermées. FTD-25, FTD-26 ajoutées. 15 FTD actives (cap respecté). |
| 1.15 | 2026-04-23 | Réorg sécurité : FTD-06, FTD-08, FTD-15 gelées (backlog post-MVP). FTD-27 🔴, FTD-28 🔴, FTD-29 🟡 ajoutées (sécurité). 15 FTD actives (cap respecté). |
| 1.16 | 2026-04-23 | FTD-29 fermée (Dependabot config). 14 FTD actives. |
| 1.17 | 2026-04-23 | FTD-27 fermée (CI backend). 13 FTD actives. |
| 1.18 | 2026-04-23 | FTD-28 fermée (Semgrep CI). CI frontend verte pour la première fois. 12 FTD actives. |
| 1.19 | 2026-04-23 | FTD-23 et FTD-24 fermées (clean useAutosave après correction + polling automatique jobs pending dans useRapport). 10 FTD actives (cap 15). |
| 1.20 | 2026-04-25 | Sprint 4c-1 — Ajout FTD-30 🟡 (rotation token Deepgram sans grace period), FTD-31 🟢 (page enregistrement EO non resumable), FTD-32 🟢 (Safari iOS non testé), FTD-33 🟢 (EO_T2_LIVE verrouillé en dur). 14 FTD actives (cap 15 respecté). |
| 1.21 | 2026-04-25 | Sprint 4c-2 — Ajout FTD-34 🟢 (présentation T1 en localStorage clair), FTD-35 🟡 (refresh sans simulation active sur PresentationGenereeT1Page). 16 FTD actives — cap dépassé temporairement, accepté par Hermann pour cette session ; clean à planifier au prochain Sprint. |
| 1.22 | 2026-04-25 | Sprint 4c-3 — Ajout FTD-36 🟡 (upload audio base64 sans progression), FTD-37 🟢 (code Deepgram live dormant à trancher). FTD-30 dégradée 🟡→🟢 et passée en « gelé » (Deepgram live mis en pause). 17 FTD actives — cap toujours dépassé, clean prioritaire au Sprint suivant. |
| 1.23 | 2026-04-25 | FTD-25 et FTD-26 fermées (ARCHITECTURE.md §3 reflète l'arborescence réelle + convention shared/ui/ vs shared/components/ui/ documentée). 15 FTD actives (cap respecté). |
| 1.24 | 2026-04-25 | Sprint 4.5 Clean — Ajout FTD-38 🟢 (useAudioRecorder ref mise à jour pendant render — eslint-disable local en place) et FTD-39 🟡 (Règle D violée dans StatCards.tsx — préexistant Sprint UI Polish). 17 FTD actives — cap dépassé temporairement, à résorber au Sprint 5.5. |
| 1.25 | 2026-04-25 | Sprint 4.5 — Ajout FTD-40 🟡 (conclusion conseil_nclc backend incohérente quand NCLC atteint > cible — patch frontend en place dans ConseilNclcCallout) et FTD-41 🔴 (persistance présentation EO T1 en BDD — résout FTD-35). 19 FTD actives — cap 15 dépassé de 4. Résorption obligatoire au Sprint 5.5 avant toute nouvelle FTD. |
| 1.26 | 2026-04-26 | Sprint 5e (clean Sprint 5 Billing) — Ajout FTD-42 🟡 (modal prorata Standard→Premium avec montant exact — divergence PARCOURS_UTILISATEURS §3, actuellement Customer Portal natif sans preview in-app) et FTD-43 🟢 (race condition webhook post-redirect Stripe — usePlan() peut retourner ancien plan brièvement). 21 FTD actives — cap 15 dépassé de 6. Résorption FTD critique au Sprint 5.5 avant Sprint 6. |
| 1.27 | 2026-04-26 | Sprint 5.5 Clean — FTD-09, FTD-33, FTD-42 gelées. FTD-35 fermée (subsumée par FTD-41). FTD-14, FTD-38, FTD-39 résolues. 14 FTD actives (cap 15 respecté). |