expria-frontend/docs/TECH_DEBT.md
Hermann_Kitio a0352457dc docs(tech-debt): triage FTD v1.14 — 17→15 actives
Fermées : FTD-04 (miroir docs, accepté ADR 004), FTD-05 (scaffold
caduc, audit clean), FTD-20 (GET /simulations/:id livré Sprint 3.6a),
FTD-22 (code orphelin /sujets, résolution complète).

Ajoutées : FTD-25 (ARCHITECTURE.md §3 désaligné), FTD-26
(cohabitation shared/ui vs shared/components/ui).

Cap de 15 FTD actives respecté.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 01:42:11 +03:00

26 KiB
Raw Blame History

TECH_DEBT.md — Expria Frontend

Document de référence — Version 1.14 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-XX backend), 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_ERROR dans routes/simulations.ts et routes/corrections.ts
  • INVALID_BODY dans routes/plans.ts et routes/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 retourne HTTP 426 Upgrade Required avec code API_VERSION_MISMATCH si breaking change
  • Frontend : gérer API_VERSION_MISMATCH dans api-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)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 — Anti-FOUC thème : script inline manquant dans <head>

Priorité : 🟡 Important Statut : Ouvert — à faire avant déploiement production Estimation de session : 30 min Description : Le ThemeProvider applique la classe .dark sur <html> après l'hydratation React (useEffect). Entre le premier paint du navigateur et l'exécution de React, la page s'affiche brièvement en mode clair même si l'utilisateur a choisi le mode sombre — c'est le FOUC (Flash Of Unstyled Content).

Fix : ajouter un script inline bloquant dans le <head> de index.html qui lit localStorage.getItem('expria-theme') (et prefers-color-scheme en fallback) et applique .dark sur document.documentElement avant le premier paint. Ce script doit être minifié et inliné (non-async, non-defer) pour garantir l'exécution avant le CSS.

<script>
  (function(){var t=localStorage.getItem('expria-theme');
  if(t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme:dark)').matches))
  document.documentElement.classList.add('dark')})()
</script>

Impact actuel : visible uniquement pour les utilisateurs en mode sombre — bref flash de fond clair au chargement. Acceptable en dev, indésirable en production.

Condition de résolution : avant la première mise en production (Sprint 1 ou avant).


FTD-15 — Option 'system' manquante dans ThemeProvider

Priorité : 🟢 Mineur Statut : Reporté — après 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, si theme === 'system' : écouter matchMedia('(prefers-color-scheme: dark)') et appliquer/retirer .dark dynamiquement
  • 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.


FTD-17, FTD-18, FTD-19 résolus au Sprint 3.5 (2026-04-22) — voir §5 Historique des résolutions.


FTD-24 — Pas de polling automatique pour exercices / modèle pending

Priorité : 🟡 Important Statut : Ouvert — accepté au Sprint 3.6b, refresh manuel requis entre-temps 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 Query refetchInterval: 3000 si exercices_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 : Ouvert — pré-existant au Sprint 3.6a, détecté lors des tests manuels 3.6a 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 :

  1. L'utilisateur soumet sa production → rapport persisté côté backend.
  2. SimulationForm passe step à 'done', mais :
    • Le timer d'autosave debouncé (30 s) peut encore fire après cette transition si le debounce n'est pas clear.
    • Un beforeunload handler peut déclencher un flush() final même une fois la correction reçue.
  3. useAutosave.enabled est calculé comme !isSubmitting dans SimulationForm — il redevient true après la correction (quand isSubmitting repasse à false).

À faire :

  • Propager enabled = !isSubmitting && step !== 'done' && step !== 'correcting' depuis SimulationForm
  • OU : au montage, quand rapport devient non null après correction, clear le timeout debouncé et retirer le handler beforeunload immédiatement.
  • Ajouter un test regression dans useAutosave.test.ts qui vérifie qu'aucun autosaveContenu n'est appelé après step='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 : Ouvert 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 vers shared/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 : Ouvert 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-06 — AudioWorklet au lieu de ScriptProcessorNode (T2 Live)

Priorité : 🟢 Mineur Statut : Reporté à après le lancement MVP 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-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/react au projet
  • Intégrer dans src/app/providers.tsx
  • Ajouter VITE_SENTRY_DSN dans 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-08 — Tests E2E non implémentés

Priorité : 🟢 Mineur Statut : Reporté — 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-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)
  • getById tolère rapport=null (Option A)
  • RapportPage redirige vers /simulation/ee si 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_id suffit
  • 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.create persiste sujet_id à la création
  • getById retourne SimulationState (tolère rapport=null pour resume)
  • autosaveContenu + updateSujet controllers (refuse si rapport !== null)
  • Routes PATCH /simulations/:id/contenu + PATCH /simulations/:id/sujet
  • CORS : allowMethods étendu à PATCH/PUT/DELETE

Frontend :

  • useAutosave : debounce 30 s + beforeunload flush + dedup par contenu
  • SimulationForm : hydrate initialContenu, affiche "Sauvegardé à HH:MM"
  • SimulationFlowProvider : hydratation au montage depuis localStorage → restaure step task-selected si rapport null, nettoie sinon
  • getReport délègue à getSimulationState et throw REPORT_NOT_READY si rapport null

Condition de résolution complète : intégration EO (Sprint 4) + examen (Sprint 7).


4. Tests à renforcer

FTD-09 — Tests de la state machine T2 Live non implémentés

Priorité : 🟡 Important Statut : Planifié — à créer au Sprint 2.5 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).

À faire au Sprint 2.5 (spike T2 Live) :

  • Créer t2-machine.test.ts avec tests des transitions : idle → connecting, connecting → listening, listening ↔ speaking, * → error, * → ended
  • Tests des messages d'erreur (close code 4001, 4003, autre)

Condition de résolution : fin Sprint 2.5.


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 fetch via vi.fn()
  • Couvrir :
    • Succès 2xx → retourne le payload parsé
    • Erreur 4xx ApiError bien parsée → throw conforme
    • Erreur 4xx body non-JSON → fallback INTERNAL_ERROR
    • Erreur 5xx avec retry → max 2 retries puis throw
    • Timeout → ClientError code TIMEOUT
    • Erreur réseau (fetch reject) → ClientError code NETWORK_ERROR + retry
    • Parsing succès JSON invalide → ClientError code PARSE_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-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 variantprimary 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-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/20shadow-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 /sujetsSujetSelector + 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é).