expria-frontend/docs/CHANGELOG.md
Hermann_Kitio 3016d909a6
Some checks are pending
CI / quality (push) Waiting to run
feat(t1-live): T1 Live frontend — Sprint 7b
- Add T1 state machine (8 states, presenting ⇄ interrupted)
- Add useT1LiveSession (WS /t1/live, uplink gate by ref, no context msg)
- Add T1PreparationPage, T1DialoguePage, T1SpeakingIndicator
- Add EO_T1_LIVE card in TaskSelector gated via oral_t2_live
- Extract shared t1Questionnaire.ts for batch/live DRY
- Remove T1LiveQuestionnairePage + T1LiveContext (post patch 7a)
- Simplified flow: card → preparation → dialogue
- FTD-44 frozen (cross-feature audio hooks, Sprint 7.5)
- FTD-45/46 frozen (Gemini relance quality + transcription)
- Tests: 301/301 green
2026-06-30 22:53:57 +03:00

930 lines
60 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Changelog — Expria Frontend
Toutes les modifications notables du projet frontend sont documentées dans ce fichier.
Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/).
## Convention
Chaque entrée suit ce format :
```
## [Unreleased] — YYYY-MM-DD — Session <nom>
### Added (nouveautés)
- ...
### Changed (modifications)
- ...
### Fixed (corrections)
- ...
### Removed (suppressions)
- ...
### Security (sécurité)
- ...
```
---
## [Unreleased] — 2026-06-30 — Sprint 7b — Frontend T1 Live (monologue + interruption non déterministe)
### Added
- Machine d'état T1 (`features/t1-live/state/t1-machine.ts`) — 8 états purs (`idle`, `preparing`, `connecting`, `presenting`, `interrupted`, `processing`, `ended`, `error`). Le cœur est la transition `interrupted ⇄ presenting` (interruption examinateur puis reprise candidat). +23 tests.
- `useT1LiveSession` (`features/t1-live/hooks/useT1LiveSession.ts`) — orchestrateur du dialogue T1, calqué sur `useT2LiveSession` (discipline « Voie A »). WS `wss://${API_URL}/t1/live?token=<jwt>` (PAS de `&sujet=` — T1 n'est pas subject-based). Aucun VAD micro (T1 = monologue) ; l'uplink micro est coupé/rétabli pendant une interruption via un **ref** (`uplinkMutedRef`), jamais via `setState` (leçon Voie A). Réagit aux signaux applicatifs `{type:'interruption_start'}` / `{type:'interruption_end'}`. Timer dur 180 s. Close codes 1000/4001/4003/4005/4006.
- `T1PreparationPage` + `T1DialoguePage` (`features/t1-live/pages/`) — parcours préparation → dialogue (3:00) ; écran terminal « Télécharger l'audio » + « Voir le rapport » (`/rapport/:id`). L'UI ne suppose JAMAIS qu'une relance suit (interruption non déterministe).
- `T1SpeakingIndicator` (`features/t1-live/components/`) — indicateur de prise de parole (amplitude micro réelle en `presenting`, animation décorative en `interrupted`).
- Carte `EO_T1_LIVE` dans `TaskSelector` (discriminateur `live?: 'T1' | 'T2'`, label « Tâche 1 — Live ») gatée Premium via `hasAccess(plan, 'oral_t2_live')` (TD-24 — pas de nouvelle permission, le gate couvre T1 et T2 Live) + prop `onT1LiveSelect`. `SimulationEOPage` câble `onT1LiveSelect → /simulation/eo/t1/live/preparation`.
- `features/simulations/lib/t1Questionnaire.ts` — définition partagée du questionnaire T1 (FIELDS + schéma zod + `EMPTY_REPONSES`), réutilisée par le batch `QuestionnaireT1Page`.
### Changed
- `useT1LiveSession` aligné sur le **Patch 7a backend** : plus d'envoi du message `{type:'context'}`, plus d'option `reponses`, la session audio démarre directement sur `ws.onopen` (WS_OPENED → presenting).
- Parcours T1 Live simplifié : carte `EO_T1_LIVE` → préparation → dialogue (plus d'étape questionnaire intermédiaire).
- `t1-machine` : commentaire et test nettoyés (mapping close **4004** retiré → 4006), cohérent avec la suppression du contexte côté backend.
### Removed
- `T1LiveQuestionnairePage` et `T1LiveContext` (post-Patch 7a) — le backend n'exige plus de message `context` ni de réponses pré-remplies ; ces écrans/état deviennent sans objet.
### Notes
- **FTD-44 gelée** (§3bis TECH_DEBT) — les trois hooks audio génériques sont empruntés à `features/t2-live/hooks/` (violation FSD inter-features assumée et tracée, sites marqués `// TODO(FTD-44)`), réactivée au Sprint 7.5 (factorisation Sprint 7).
- WebSocket / AudioContext non matérialisables en jsdom → validation manuelle ; la logique pure de transition est couverte par `t1-machine.test.ts`.
- Bugs amont observés au test manuel, hors contrôle frontend : **FTD-45** (relances Gemini hors-sujet, extension TD-23) et **FTD-46** (transcription Gemini Live hasardeuse).
---
## [Unreleased] — 2026-06-29 — Sprint 6e — T2 Live « Voie A » (mix audio temps réel)
### Added
- `public/pcm-record-processor.js` — AudioWorklet « tap » branché sur le mix du contexte partagé : prélève le mix (micro candidat + voix IA) en temps réel et émet des chunks Int16 vers le hook d'enregistrement. Permet un WAV aligné temporellement sur une horloge unique.
- `useAudioCapture` expose désormais `contextRef` (AudioContext partagé) + `mixNodeRef` (GainNode point de convergence) pour partager une horloge unique entre capture, playback et enregistrement.
- Indicateur de prise de parole du candidat : VAD par RMS sur le flux micro Int16 (seuils `SPEAK_RMS=500` / `SILENCE_RMS=250`, debounce 700 ms) pilotant les transitions `speaking``listening`.
- Détection `newTurn` : un chunk audio IA reçu après > 800 ms de silence IA marque la reprise de parole de l'examinateur → réalignement de l'edge-tracking micro + `USER_SILENT`.
### Changed
- **Architecture audio « Voie A »** : passage à **UN SEUL AudioContext au rate NATIF** (≈ 48 kHz), partagé par capture / playback / enregistrement. Suppression du forçage `{ sampleRate: 16000 }` et des deux contextes séparés 16 k / 24 k.
- `useAudioPlayback({ contextRef, mixNodeRef })` ne crée plus son propre contexte : la source IA est routée vers `ctx.destination` (audible) ET vers `mixGain` (captée par le tap). Buffer créé à 24 k, rééchantillonné automatiquement par le contexte natif.
- `useAudioRecording` : enregistrement via tap worklet sur le mix (`mixGain → recordNode → gain(0) → destination`, sink muet pour pull cross-navigateur). Buffer Int16 hors cycle de vie du contexte (`exportWAV()` survit à la fermeture). **WAV mono au rate natif, single-track, zéro resample** (remplace l'ancien WAV 24 k multi-piste).
- `useT2LiveSession` : cycle de vie audio aligné sur la « Voie A » — start sur `ws.onopen` après `capture.start()` résolu ; stop sur `endDialogue` (débranche le tap, buffer conservé) ; cancel ferme le contexte (buffer abandonné, aucun export).
- **Bug 6 — « Nouvelle simulation »** : le routage vers la bonne tâche s'appuie désormais sur le champ `tache` propagé dans le rapport (`report/api.ts`, `types.ts`, `RapportPage.tsx`), sans query param.
### Fixed
- **Anti-blanc EO** : suppression des silences/blancs en début de dialogue grâce à l'horloge unique et au scheduling continu de la voix IA.
- Correction de l'écho de la voix candidat (`mixGain` jamais connecté à `destination`).
- **Bug 4 — « Voir le rapport »** : la navigation vers `/rapport/:id` aboutit bien (garde `navigatingAwayRef` empêchant le cleanup/teardown d'avorter la redirection).
- **Bug 5 — « Annuler » (`cancelDialogue`)** : arrête l'enregistrement, ne déclenche aucune évaluation, ne produit aucun WAV et ne persiste aucune production (WS fermée sans message de fin).
- Stabilité de l'uplink micro : l'architecture « Voie A » supprime l'état React réactif sur la `MediaStream` (source du _starving_ d'uplink), au profit de refs stables sur le contexte/mix partagés.
### Removed
- Helpers `resample16kTo24k` et `mixTracksToInt16` de `audio-utils.ts` (rendus inutiles par l'horloge unique et le single-track). Helpers purs conservés : `arrayBufferToBase64`, `base64ToArrayBuffer`, `int16ToFloat32`, `float32ToInt16`, `concatInt16`, `buildWavHeader`.
- Instrumentation de diagnostic `[BISECT]` retirée des hooks T2 Live (logique runtime VAD / garde-fous / routage des messages conservée).
### Notes
- Tous les bugs ciblés (anti-blanc, Voie A, bugs 4/5/6, indicateur de parole) validés **à l'oreille en navigation privée** — console sans `[BISECT]`.
- Tests frontend : 259 → **269 verts (37 fichiers)**.
- AudioContext / AudioWorklet / WebSocket non matérialisables en jsdom → validation audio à l'oreille (objectif de la session). `useAudioRecording` couvert sur sa surface pure (export WAV, reset).
---
## [Unreleased] — 2026-04-26 — Sprint 6c — Frontend T2 Live UI
### Added
- `t2-machine.ts` — state machine pure T2 Live : 9 états (`idle``preparing``connecting``ready``speaking``listening``processing``ended` / `error`), 8 events. 21 tests. Résout FTD-09.
- `useT2LiveSession.ts` — hook orchestrateur : WebSocket + state machine + hooks audio (capture/playback/recording). Parse format Gemini natif (`serverContent.modelTurn`) + messages applicatifs backend (`warning`/`report`/`error`). Close codes 1000/4001/4003/4004. Timer dialogue 210 s. Ping 30 s keep-alive.
- `T2LiveContext.tsx` — Provider léger pour partager le sujet sélectionné entre les pages T2.
- `T2SujetsPage.tsx` — grille de sélection des sujets T2 (`GET /sujets?mode=EO&tache=2`).
- `T2PreparationPage.tsx` — timer 2 min, consigne affichée, zone de notes locale, bouton « Suggestions d'idées » (DeepSeek, actif immédiatement), bouton « Je suis prêt », pré-warm micro via `getUserMedia`. Transition auto vers dialogue à 0:00.
- `T2DialoguePage.tsx` — timer 3:30, indicateur d'état IA, waveform, bouton « Terminer ». Écran terminal (state `ended`) : bouton « Télécharger l'audio » (WAV mono 24 kHz) + bouton « Voir le rapport » (→ `/rapport/:id`).
- 3 routes : `/simulation/eo/t2`, `/simulation/eo/t2/preparation`, `/simulation/eo/t2/dialogue` sous `T2LiveLayout`.
### Changed
- `TaskSelector.tsx` — carte EO T2 Live déverrouillée via `hasAccess(plan, 'oral_t2_live')` + prop `onT2LiveSelect`. Résout FTD-33.
- `SimulationEOPage.tsx` — branche `onT2LiveSelect` vers `/simulation/eo/t2`.
- `entities/production/``Tache` type, labels, `mapTacheToSujetParams`, config étendus avec `EO_T2_LIVE`.
- `features/historique/``TACHE_NUMBER` étendu.
### Notes
- Tests frontend : 238 → 259 verts (+21 — tous sur t2-machine).
- FTD-09 résolue (state machine testée).
- FTD-33 résolue (carte déverrouillée via hasAccess).
- `useT2LiveSession` non testé en unit (WebSocket non supporté jsdom) — validation manuelle prévue.
---
## [Unreleased] — 2026-04-26 — Sprint 6b — Frontend audio (T2 Live)
### Added
- `public/pcm-capture-processor.js` — AudioWorklet processor : capture micro, rééchantillonnage vers 16 kHz si `sampleRate` natif différent, conversion Float32 → Int16 LE, chunks de 4096 samples (~256 ms).
- `src/shared/lib/audio-utils.ts` — 6 helpers purs : `arrayBufferToBase64`, `base64ToArrayBuffer`, `int16ToFloat32`, `float32ToInt16`, `resample16kTo24k`, `buildWavHeader`.
- `src/features/t2-live/hooks/useAudioCapture.ts` — hook capture : `getUserMedia` (mono, echoCancellation, noiseSuppression) → AudioContext 16 kHz → AudioWorklet → callback `onChunk(base64)`. Cleanup au stop/unmount.
- `src/features/t2-live/hooks/useAudioPlayback.ts` — hook playback : AudioContext 24 kHz lazy-init, scheduling séquentiel via `start(max(currentTime, lastEndTime))` pour lecture sans gaps. Cleanup au stop/unmount.
- `src/features/t2-live/hooks/useAudioRecording.ts` — hook recording : buffer chronologique unique normalisé 24 kHz (chunks candidat rééchantillonnés 16→24k), `addAIChunk(base64)` décode en interne, `exportWAV()` → Blob `audio/wav` mono 24 kHz.
- 12 tests `audio-utils.test.ts` (round-trips base64/ArrayBuffer, clamping int16/float32, interpolation resample, header WAV).
- 7 tests `useAudioRecording.test.ts` (add candidat resample, add IA, alternance, header WAV, reset, export vide, chunks vides ignorés).
### Notes
- Tests frontend : 219 → 238 verts (+19).
- `useAudioCapture` et `useAudioPlayback` dépendent de AudioContext (API navigateur) — validation manuelle au Sprint 6c.
- AudioWorklet utilisé directement (pas ScriptProcessorNode) — FTD-06 ne s'applique plus pour T2 Live.
---
## [Unreleased] — 2026-04-26 — Sprint 5.5 Clean FTD
### Changed
- `StatCards.tsx:90``plan === 'free'` remplacé par `!hasAccess(plan, 'dashboard')` (FTD-39, Règle D).
- `useAudioRecorder.ts:80` — assignation `optionsRef` pendant render refactorée en `useEffect` sans deps, eslint-disable retiré (FTD-38).
### Docs
- `TECH_DEBT.md` v1.26 → v1.27 — triage dette technique :
- Gelées : FTD-09 (state machine T2 Live), FTD-33 (carte T2 Live en dur), FTD-42 (modal prorata — Customer Portal suffit).
- Fermée : FTD-35 (subsumée par FTD-41).
- Résolues : FTD-14 (anti-FOUC déjà en place, conforme DESIGN_SYSTEM v2.0), FTD-38, FTD-39.
- 21 → 14 FTD actives (cap 15 respecté).
### Notes
- FTD-14 : le script inline `.light` était déjà présent dans `index.html` (lignes 14-20), conforme à DESIGN_SYSTEM v2.0 (dark = défaut, `.light` = override). L'exemple `.dark` documenté dans la fiche FTD-14 datait de la DA Boréal v1.0.
- Tests frontend : 219/219 verts (inchangé).
---
## [Unreleased] — 2026-04-26 — Sprint 5d — Customer Portal + page Paramètres
### Added
- `src/features/billing/hooks/useCustomerPortal.ts` — hook `{ openPortal, isLoading, error }` autour de `createCustomerPortalSession` + redirect full-page vers Stripe Customer Portal. Message d'erreur backend (`NO_ACTIVE_SUBSCRIPTION`) propagé tel quel.
- `src/features/billing/components/AccountBillingSection.tsx` — section UI `Card` : badge plan + CTA contextuel (Free → lien « Voir les plans » vers `/plan` ; Standard/Premium → bouton « Gérer mon abonnement » → Customer Portal).
- `src/features/account/pages/ParametresPage.tsx` — page conteneur `/parametres` avec section Abonnement + section Session (bouton « Se déconnecter » → `signOut()` + `queryClient.clear()` + `navigate('/login')`).
- 6 tests (3 useCustomerPortal + 3 AccountBillingSection).
### Changed
- `src/features/billing/pages/PricingPage.tsx` — branche **Standard→Premium** routée vers `useCustomerPortal.openPortal()` au lieu de Stripe Checkout direct (le Customer Portal Stripe affiche le montant prorata + confirmation native). `buildCtaConfigs` refactor : signature `(plan, isStandardPending, isPremiumPending, onUpgrade)` ; loading state combiné selon source ; erreur unifiée `checkoutError ?? portalError`.
- `src/features/billing/__tests__/PricingPage.test.tsx` — 6e test : Standard click « Passer en Premium » → `createCustomerPortalSession` appelé (et `createCheckoutSession` non appelé).
- `src/app/router.tsx``/parametres``<ParametresPage />` (sous PrivateLayout).
### Notes
- Tests : 212 → 219 verts (+7).
- Customer Portal Stripe doit être configuré côté Dashboard Stripe (hors code) pour fonctionner en prod.
---
## [Unreleased] — 2026-04-26 — Sprint 5c — Flow Checkout post-redirect
### Added
- `src/features/billing/hooks/useStripeCheckout.ts` — hook `{ checkout, isLoading, pendingPriceType, error }` autour de la mutation Stripe Checkout + redirect full-page sur succès. `pendingPriceType` permet l'affichage loading par carte sans state local.
- `src/features/dashboard/hooks/useUpgradeSuccessHandler.ts` — détecte `?upgrade=success` au mount du Dashboard, invalide le cache `PLAN_QUERY_KEY` (refetch automatique du plan), nettoie l'URL via `history.replaceState` (préserve les autres params utm\_\*, etc.).
- `src/features/dashboard/components/UpgradeSuccessBanner.tsx` — callout success-soft « Bienvenue ! Votre plan a été mis à jour. » + bouton dismiss.
- 9 tests (4 useStripeCheckout + 5 useUpgradeSuccessHandler).
### Changed
- `src/features/billing/pages/PricingPage.tsx` — migration vers `useStripeCheckout` (suppression `useMutation` inline + `pendingType` state local).
- `src/features/dashboard/pages/DashboardPage.tsx` — branche `useUpgradeSuccessHandler` + rend `<UpgradeSuccessBanner>` au-dessus de `<DashboardContent>` quand `showSuccess`.
### Cross-repo
- `expria-backend@28f8373``fix(stripe): cancel_url /tarifs → /plan`. Bug détecté lors de cette session : la route `/tarifs` n'existe pas côté frontend, les checkouts annulés aboutissaient sur un 404. Corrigé en commit séparé sur le backend.
### Notes
- Tests : 203 → 212 verts (+9).
- Race condition connue (FTD-43) : le webhook Stripe peut arriver après le redirect frontend ; `usePlan()` peut retourner l'ancien plan brièvement. Le banner indique « rafraîchissez dans quelques secondes » pour gérer ce cas.
---
## [Unreleased] — 2026-04-26 — Sprint 5b — Page tarifaire `/plan`
### Added
- `src/features/billing/api.ts``createCheckoutSession(priceType)` + `createCustomerPortalSession()` (utilisée Sprint 5d).
- `src/features/billing/components/PlanCard.tsx` — carte plan présentationnelle pure : props `cta`, `currentBadge`, `highlighted`, `ctaHint`, `errorMessage`.
- `src/features/billing/pages/PricingPage.tsx` — orchestration 3 colonnes (Découverte / Standard / Premium) avec gating dynamique selon `usePlan()`. CTA payant → Stripe Checkout (full-page redirect). Callout d'erreur sous la carte cliquée.
- 5 tests PricingPage (rendu Free/Standard/Premium + click + erreur).
### Changed
- `src/shared/config/env.ts` + `.env.example` — ajout `VITE_STRIPE_PRICE_STANDARD` + `VITE_STRIPE_PRICE_PREMIUM` (optionnels — public price_ids Stripe).
- `src/app/router.tsx``/plan``<PricingPage />` (sous PrivateLayout, donc ProtectedRoute).
- **Uniformisation CTA upgrade** : `SimulationsList`, `RapportPage`, `TaskSelector`, `DashboardFreeView`, `PaywallBanner` → libellé « Voir les plans » (au lieu de « Passer en Standard » / « Passer en Premium → » / « Voir les offres »). Cibles navigation inchangées (`/plan`).
- `SimulationsList.test.tsx` — assertion adaptée au nouveau libellé.
### Notes
- Tests : 198 → 203 verts (+5).
- `DashboardStandardView` et `BlurredProgression` conservent leurs CTA orientés (« Passer en Premium ») — sémantiquement corrects (Standard a un seul upgrade possible ; pattern_analysis est Premium-only).
---
## [Unreleased] — 2026-04-26 — Sprint 4.8 — Phonologie EO (frontend)
### Added
- `src/entities/report/__tests__/getMaxScorePerCritere.test.ts` — 7 tests (détection maxScore + mapping libellés EO).
### Changed
- `src/entities/report/lib.ts` — nouveau helper `getMaxScorePerCritere(rapport): 4 | 5` (détection sur criteres.length === 5). `CRITERE_NOM_TO_CODE` étendu avec les 4 libellés EO Sprint 4.8.
- `src/features/simulations/components/rapport/CritereCard.tsx` — nouvelle prop `maxScore` : affiche `X/4` (EO Sprint 4.8) ou `X/5` (EE, EO legacy).
- `src/features/simulations/pages/RapportPage.tsx` — calcul maxScore propagé aux CritereCard.
- `src/entities/report/types.ts` — commentaire Critere.score clarifié.
### Notes
- Rétrocompatibilité : rapports EO legacy (4 critères × /5) et EE (4 × /5) inchangés.
- Tests : 191 → 198 verts (+7).
---
## [Unreleased] — 2026-04-26 — Sprint 4.6 — UI EO (waveform + timeline)
### Added
- `RecordingWaveform.tsx` — visualiseur audio animé (AnalyserNode, fftSize 256,
smoothing 0.7, 32 barres). Visible uniquement pendant `isRecording`. Respecte
`prefers-reduced-motion` (frame statique). AudioContext fermé au cleanup.
- `RecordingTimeline.tsx` — barre de progression colorée avec seuils fixes :
vert (0 → maxSeconds-30s), orange (maxSeconds-30s → maxSeconds-15s),
rouge (maxSeconds-15s → fin). Applicable T1 et T3.
- `RecordingTimeline.test.tsx` — 7 tests (logique seuils + rendu + clamp).
### Changed
- `useAudioRecorder.ts` — expose `mediaStream: MediaStream | null` (set au
start, reset au cleanup).
- `AudioRecorder.tsx` — intègre Waveform + Timeline dans l'UI d'enregistrement.
### Notes
- Aucun changement backend.
- Tests : 166 → 173 verts (+7).
---
## [Unreleased] — 2026-04-25 — Sprint 4.5 Clean + fixes Golden Dataset
### Added
- `features/simulations/components/rapport/__tests__/ScoreHero.test.tsx` — 3 tests (un par état : depasse / atteint / !atteint)
- `features/simulations/components/rapport/__tests__/ConseilNclcCallout.test.tsx` — 3 tests (patch FTD-40)
- Test d'hydratation EO_T1 dans `simulationFlowT1.test.tsx` (resume au refresh : production + présentation)
### Changed
- `ARCHITECTURE.md` §3 — arborescence réelle reflétée (FTD-25) : note `app/` documente entry points + composants layout (AppLayout, Sidebar, Topbar, BottomNav, MaintenancePage). `t2-live/` et `billing/` retirés (non implémentés). Ajout `entities/{patterns,presentation,transcription}` et `features/{historique,progression,design-system}`. Bump v1.1.
- `ARCHITECTURE.md` §3 — convention `shared/ui/` (wrappers Expria PascalCase) vs `shared/components/ui/` (primitives shadcn kebab-case) documentée (FTD-26).
- `ConseilNclcCallout.tsx` — props `nclc` + `nclcCible` ajoutées ; patch temporaire FTD-40 (texte fixe « Excellent travail — vous avez dépassé votre objectif. Continuez sur cette lancée pour viser NCLC {nclc+1} ! » quand `nclc > nclcCible`).
- `RapportPage.tsx` — passe `nclc` + `nclcCible` à `ConseilNclcCallout`.
- `ScoreHero.tsx` — encart de conclusion à 3 états (depasse / atteint / !atteint).
- `SimulationFlowProvider.tsx``useEffect` persiste `production.id` dans `localStorage.expria_simulation_id` pour TOUS les flows (EE + EO_T1 + EO_T3) → resume au refresh fonctionnel pour EO.
### Fixed
- Sprint 4.5 Clean — 3 erreurs lint Sprint 4c corrigées :
- `useDeepgramLive.ts:152` — directive `eslint-disable-next-line` orpheline retirée
- `useAudioRecorder.test.ts:77,81` — params `_t`/`_timeslice` neutralisés via `void` (signature mock préservée)
- `useAudioRecorder.ts:73``eslint-disable-next-line react-hooks/refs` + commentaire renvoyant à FTD-38
- `QuestionnaireT1Page.test.tsx:10` — import `React` inutilisé supprimé (TS6133).
### Notes
- **TECH_DEBT.md bumps** : v1.23 (FTD-25/26 fermées) → v1.24 (FTD-38/39 ouvertes) → v1.25 (FTD-40/41 ouvertes).
- **FTD ouvertes Sprint 4.5** :
- FTD-38 🟢 — `useAudioRecorder` ref mise à jour pendant render (eslint-disable local en place)
- FTD-39 🟡 — Règle D violée dans `StatCards.tsx:90` (préexistant Sprint UI Polish)
- FTD-40 🟡 — Conclusion `conseil_nclc` backend incohérente quand NCLC atteint > cible (patch frontend en place, fix backend prompt à venir)
- FTD-41 🔴 — Persistance présentation EO T1 en BDD (résout FTD-35 ; localStorage instable)
- **FTD fermées Sprint 4.5** : FTD-25 🟢, FTD-26 🟡.
- **Cap FTD : 19/15 — dépassé de 4. Résorption obligatoire au Sprint 5.5 avant toute nouvelle FTD.**
- Tests : 159 → 166 verts (+7). Typecheck + lint : 0 erreur.
---
## [Unreleased] — 2026-04-25 — Sprint 4c — Frontend EO (T1 + T3)
### Added
- `features/simulations/pages/SimulationEOPage.tsx` — TaskSelector EO (T1, T3, T2 cadenas Premium)
- `features/simulations/pages/SujetsEOPage.tsx` — grille sujets EO_T3 + bouton aléatoire
- `features/simulations/pages/PreEnregistrementEOPage.tsx` — consigne + durée recommandée
- `features/simulations/pages/EnregistrementEOPage.tsx` — enregistrement audio + auto-submit à expiration
- `features/simulations/pages/ModeChoixT1Page.tsx` — choix Générer / Enregistrer directement
- `features/simulations/pages/QuestionnaireT1Page.tsx` — 5 champs + validation Zod + génération IA
- `features/simulations/pages/PresentationGenereeT1Page.tsx` — texte généré, modifier, copier, .txt, refaire, localStorage
- `features/simulations/hooks/useAudioRecorder.ts` — MediaRecorder, timer, maxSeconds, auto-stop, download
- `features/simulations/hooks/useDeepgramLive.ts` — conservé dormant (FTD-37)
- `features/simulations/components/AudioRecorder.tsx` — UI enregistrement, maxSeconds/onMaxReached
- `features/simulations/components/TranscriptionDisplay.tsx` — conservé dormant
- `entities/transcription/` — token Deepgram (dormant, FTD-37)
- `entities/presentation/` — generatePresentation (POST /presentations/generate)
- `shared/lib/audio.ts` — blobToBase64 helper
### Changed
- `SimulationFlowProvider` — étendu EO : submitEoAudio, presentationT1, résolution race condition step=done
- `entities/report/api.ts` — CORRECTION_EE_TIMEOUT_MS=60s / CORRECTION_EO_TIMEOUT_MS=120s
- `entities/report/types.ts` — CorrectEoPayload étendu audioBase64/mimeType
- MIME normalisé côté frontend (strip codec params)
- router.tsx — 7 nouvelles routes EO sous SimulationFlowLayout
- FTD-30 à 37 tracées dans TECH_DEBT.md
### Notes
- Transcription live Deepgram abandonnée pour le MVP — Gemini batch côté backend
- Audio non stocké côté serveur — bouton télécharger local
- Tests : 122 → 159 verts (+37)
---
## [Unreleased] — 2026-04-25 — Fix timeout API
### Fixed
- `DEFAULT_TIMEOUT_MS` augmenté de 5s à 15s dans `api-client.ts`. Le backend Render Starter a des latences occasionnelles >5s sur les premières requêtes authentifiées.
### Notes
- UptimeRobot configuré pour pinger `https://api.expria.app/` toutes les 5 minutes (keepalive serveur).
---
## [Unreleased] — 2026-04-25 — Sprint UI Polish — Sidebar + Topbar + Dashboard
### Added
- `src/app/Topbar.tsx` — topbar sticky avec backdrop-blur, breadcrumb "Expria {page}", barre de recherche (placeholder), icônes raccourcis clavier et notifications.
- `src/app/route-titles.ts` — mapping centralisé pathname → titre de page, consommé par Topbar.
- `src/features/dashboard/components/NclcHero.tsx` — carte hero NCLC avec jauge horizontale 5→10 + anneau SVG score circulaire. Supporte état placeholder (Free/vide).
- `src/features/dashboard/components/StatCards.tsx` — 3 cartes métriques (simulations restantes, NCLC estimé, dernier score avec delta coloré).
- `src/features/dashboard/components/RecentSimulations.tsx` — liste 3 dernières simulations avec badge NCLC coloré + navigation vers `/rapport/:id`.
- `src/features/dashboard/components/NextStepCard.tsx` — carte "Prochaine étape" recommandée, contenu statique par plan.
- `src/features/dashboard/components/DashboardFreeView.tsx` — vue dashboard Free (hero placeholder, stat cards, premiers pas, PaywallBanner).
- `src/features/dashboard/components/DashboardStandardView.tsx` — vue dashboard Standard (hero NCLC dernière simu, simulations récentes, NextStepCard).
- `src/features/dashboard/components/DashboardPremiumView.tsx` — vue dashboard Premium (tout Standard + MonProfilPreparation).
### Changed
- `src/app/Sidebar.tsx` — icônes lucide-react (LayoutGrid, Pencil, Mic, etc.), cadenas Lock sur items verrouillés, badge upgrade ArrowUpCircle sur "Mon plan", user footer (avatar initiales + nom + plan + ThemeToggle), logo header "EX|PRIA" avec séparateur et sous-titre.
- `src/app/AppLayout.tsx` — intégration Topbar sticky, padding reporté sur wrapper contenu.
- `src/features/dashboard/pages/DashboardPage.tsx` — refactoré en orchestrateur : routing vers Free/Standard/Premium via `hasAccess`. Aucun `plan === 'xxx'` (Règle D).
- `src/features/dashboard/components/PaywallBanner.tsx` — refonte full-width + correction tokens morts Boréal (`border-brand-100`, `dark:border-brand/20`).
### Removed
- `src/app/MobileHeader.tsx` — fonctionnalité reprise par Topbar + Sidebar (0 consommateur confirmé par grep).
### Notes
- Tests : 122/122 verts. Typecheck : 0 erreur.
- Contenu NextStepCard statique par plan (pas d'endpoint backend dédié).
- Hero NCLC : Premium → usePatterns, Standard → NCLC dernière simulation, Free → état placeholder.
- Timeout API intermittent (cold start Render) préexistant — cause le fallback temporaire plan=free au chargement initial.
---
## [Unreleased] — 2026-04-24 — Sprint DA Charcoal — Reskin complet
### Changed
- Remplacement intégral `src/index.css` par palette Charcoal (DESIGN_SYSTEM.md v2.0). Dark = thème par défaut, `.light` = override via `@custom-variant light`.
- Sidebar navy `#0C1528` permanent (identique dark et light) avec tokens `--color-sidebar-*`.
- Layout `AppLayout` : radial-gradient sur `<main>`, sidebar 230px, `max-w-[1100px]`.
- Script anti-FOUC inline dans `index.html` (détection `prefers-color-scheme` + `localStorage`).
- Renommage tokens Boréal→Charcoal sur ~45 composants (ink-1→ink-primary, expria→brand, line→border, deep→sidebar-bg, \*-bg→\*-soft, etc.).
- Inversion `dark:` → baseline + `light:` sur 5 primitives shadcn (button, badge, input, dialog, avatar).
- `DesignSystemPage` réécrite avec palette Charcoal complète.
- `docs/adr/006-stack-versions-2026.md` mis à jour : tokens Charcoal, suppression `@variant dark` et `.dark {}`.
### Fixed
- Logo Expria : wordmark forcé en `text-white` dans la sidebar (invisible en light mode sur fond navy).
### Notes
- 59 fichiers modifiés, +1173/-727 lignes.
- Tests : 122/122 verts. Typecheck : 0 erreur.
- Timeout API intermittent observé (cold start Render) — préexistant, non lié au reskin.
---
## [Unreleased] — 2026-04-23 — Clean FTD-23 + FTD-24
### Fixed
- **FTD-23 résolu** : `useAutosave` ne fire plus après correction — `enabled` propagé avec `step !== 'done' && step !== 'correcting'` depuis `SimulationForm`. 2 tests de régression ajoutés.
- **FTD-24 résolu** : polling automatique 3s dans `useRapport` quand `exercices_status` ou `modele_status === 'pending'`. Arrêt auto dès ready/error. Timeout 2 min avec message + bouton Réessayer dans `JobStatusFallback`. 5 tests ajoutés.
### Notes
- Tests frontend : 122/122 verts (+7 vs baseline 115).
- TECH_DEBT.md → v1.19. 10 FTD actives (cap 15).
---
## [Unreleased] — 2026-04-23 — FTD-28 — Semgrep CI + CI verte
### Added
- Semgrep scan (`--severity=ERROR`) dans les CI frontend et backend (FTD-28).
- Variables d'env factices dans CI frontend pour les tests.
### Fixed
- 4 erreurs ESLint corrigées : split SimulationFlowProvider (react-refresh), hook conditionnel MonProfilPreparation, ref render useTimer, setState effect AppLayout.
- Prettier format sur 7 fichiers.
- CI frontend verte pour la première fois depuis le 18 avril.
---
## [Unreleased] — 2026-04-23 — FTD-27 — CI backend
### Added
- `expria-backend/.github/workflows/ci.yml` — CI GitHub Actions (test + audit, Node 22). CI verte au premier run.
- FTD-27 fermée dans TECH_DEBT.md (v1.17).
---
## [Unreleased] — 2026-04-23 — FTD-29 — Dependabot config
### Added
- `.github/dependabot.yml` créé dans les 2 dépôts (npm, weekly, limit 10 PRs).
- FTD-29 fermée dans TECH_DEBT.md (v1.16).
---
## [Unreleased] — 2026-04-23 — Réorg sécurité TECH_DEBT v1.15
### Changed
- `TECH_DEBT.md` v1.14 → v1.15 — réorganisation sécurité.
- Gelées (backlog post-MVP) : FTD-06 (AudioWorklet), FTD-08 (Tests E2E), FTD-15 (option 'system' thème).
- Ajoutées : FTD-27 🔴 (CI backend), FTD-28 🔴 (Semgrep CI), FTD-29 🟡 (Dependabot config).
- GitHub : Dependabot alerts + security updates activés sur les deux dépôts (UI GitHub).
---
## [Unreleased] — 2026-04-23 — Triage FTD v1.14
### Changed
- `TECH_DEBT.md` v1.13 → v1.14 — triage dette technique : 17 → 15 FTD actives (cap respecté).
- 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).
---
## [Unreleased]
### Added
- Documentation initiale du projet (ARCHITECTURE, ONBOARDING, SECURITY, etc.)
- 5 ADRs pour les décisions architecturales majeures
- Code source de `src/entities/user/access.ts` et `lib.ts` avec tests
## [Unreleased] — 2026-04-22 — Sprint 3.5 — Clean post-Sprint 3
### Changed
- **FTD-17 résolu** : `PLAN_QUERY_KEY` centralisé dans `src/entities/user/query-keys.ts` (constantes pures, aucun import runtime). `usePlan` le ré-exporte ; `SimulationPage` et `RapportPage` remplacent leur `useQuery` inline par le hook `usePlan()` — déduplication totale de la clé et de la config `staleTime`.
- **FTD-18 résolu** : `SimulationForm` migré de `@/shared/components/ui/button` (shadcn) vers la primitive canonique `@/shared/ui/Button`. Aucun variant à adapter (usage sans prop `variant`).
- **FTD-19 résolu** : token `--shadow-focus` ajouté dans `@theme {}` (`0 0 0 3px rgba(27, 79, 216, 0.18)` — conforme `DESIGN_SYSTEM.md §2`) et dans `.dark {}` (recalculé sur la teinte expria dark). Migration de 5 occurrences `ring-2 ring-expria/20` → utility `shadow-focus` dans `Button`, `Card`, `SimulationForm` (×3), `SpecialCharsKeyboard`.
- Factorisation `SimulationForm` : className dupliquée des deux boutons secondaires (« Suggestions d'idées » / « Changer de sujet ») extraite en const locale `secondaryActionBtn`.
- `TECH_DEBT.md` → v1.11. 15 FTD actives (cap de 15 respecté).
### Notes
- Timeouts DeepSeek intermittents observés pendant les tests manuels Groupe B + C — cause externe (API tierce), hors périmètre refactor Sprint 3.5.
- B8 : comportement actuel diffère du spec `PARCOURS_UTILISATEURS.md §2 "Quota atteint"` — affichage d'une bannière inline au lieu du modal de blocage attendu. À corriger dans un sprint dédié (non inclus dans ce clean, qui n'introduit aucune nouvelle fonctionnalité).
## [Unreleased] — 2026-04-22 — Sprint 3.6c — Analyse patterns (Backend + Frontend)
### Added (backend)
- `GET /users/patterns` — analyse des patterns récurrents pour utilisateur Premium.
- Auth : `authMiddleware` + `planMiddleware('pattern_analysis')` (403 `PLAN_INSUFFICIENT` si Free/Standard).
- < 5 productions corrigées `200 { ready: false, minimum: 5, current: N }`.
- 5 `200 { ready: true, patterns, exercises, preparation_index, analyzed_productions, last_analysis }`.
- `patternsController.aggregatePatterns` (pure) : agrège les `erreurs_codes` sur N productions, seuil 3/5, dédoublonnage intra-prod (un même code dans un rapport ne compte qu'une fois), codes `autre` distingués par description, tri par fréquence DESC.
- `patternsController.computePreparationIndex` (pure) : 60 % score moyen normalisé + 20 % régularité (médiane des intervalles entre prod) + 20 % tendance (pente linéaire sur les 5 scores). Clamp `[0, 100]`, messages figés selon les seuils `<40` / `40-70` / `>70`.
- `patternsController.list` orchestre fetch productions + cache `pattern_analyses` + recompute + DeepSeek + INSERT. Stratégie d'invalidation : `MAX(productions.created_at) > lastAnalysis.created_at` recompute, sinon cache hit.
- `generatePatternExercices` dans `src/lib/deepseek.ts` prompt système validé par Hermann avec format `{ consigne, exemple, correction, astuce }`, température 0.4, `AbortSignal.timeout(20_000)`, validation runtime des critères via `isValidCritere`.
- Table `pattern_analyses` migration `005_sprint_3_6c_pattern_analyses.sql` : UUID PK + FK cascade user_id + `productions_ids UUID[]` + patterns/exercises JSONB + preparation_index (CHECK `[0, 100]`) + preparation_message + analyzed_count + RLS SELECT par user_id + index `(user_id, created_at DESC)`.
- 19 nouveaux tests (`patternsController.test.ts`) : 7 sur `aggregatePatterns`, 4 sur `computePreparationIndex`, 8 sur route (401, 403 free/standard, <5 prod, cache hit, cache miss + insert, no patterns, DeepSeek fail gracieux). **205 tests backend verts** (+19 vs baseline 186).
### Added (frontend)
- Page `/progression` route sous `AppLayout` + `ProtectedRoute`, remplace le placeholder `ComingSoon`.
- `ProgressionPage` orchestre `usePlan` + `usePatterns`, gate plan via `hasAccess('pattern_analysis')`.
- `ProgressionPremium` orchestrateur : si not-ready `NotReadyState` ; sinon Hero indice + patterns + exercices long terme + footer « Analyse basée sur vos N dernières productions il y a X ».
- `PreparationIndexHero` score /100 + jauge horizontale colorée (rouge <40 / ambre 40-70 / vert >70) + message.
- `PatternsList` — liste des patterns avec libellé via nouveau `CRITERE_LABELS` + badge fréquence (3/5, 4/5, 5/5).
- **`PatternExerciceCard`** — _nouveau composant lesson-style_, non interactif (contrairement à `ExerciceInteractive` du rapport individuel) : critère + diagnostic + consigne + bloc incorrect (barré rouge) côte à côte avec bloc correct (vert) + **encart astuce proéminent** (icône ampoule + fond warning).
- `NotReadyState` — barre de progression N/5 + CTA `Démarrer une simulation`.
- `BlurredProgression` — aperçu flouté pour Free/Standard + bouton upgrade Premium.
- Section Dashboard Premium `MonProfilPreparation` — MetricCard indice (score + jauge compacte + message) + nombre d'erreurs récurrentes + CTA « Voir mon profil de préparation » vers `/progression`. Garde explicite `hasAccess('pattern_analysis')` → composant retourne `null` pour Free/Standard (pas rendu dans le DOM).
- `usePatterns(plan)` — hook TanStack Query partagé entre `/progression` et dashboard ; clé `['users', 'patterns']`, `staleTime: 60s`, `enabled` conditionné par `hasAccess` pour éviter un 403 parasite.
- `entities/patterns/types.ts` + `entities/patterns/api.ts` — types miroirs du backend (`Pattern`, `PatternExercice`, `PreparationIndex`, `PatternsReady`, `PatternsNotReady`) + `getPatterns()` avec timeout 25 s.
- `CRITERE_LABELS` exporté depuis `entities/report/lib.ts` — miroir du backend pour affichage du libellé humain à partir du code taxonomie.
- 13 nouveaux tests : 6 sur `ProgressionPremium` (not-ready, ready avec indice/patterns/exercices, footer, 0 pattern) + 7 sur `MonProfilPreparation` (gating Free/Standard, Premium ready/not-ready, loading, error, 0 pattern). **115 tests frontend verts** (+13 vs baseline 102).
### Notes
- **Formule indice** arbitraire (60/20/20) — à affiner après observation prod si besoin.
- **Dégradation gracieuse DeepSeek** : si `generatePatternExercices` throw, le backend persiste quand même l'analyse avec `exercises: []` et logue l'erreur. Le frontend affiche alors la liste des patterns sans section exercices (pas de message d'erreur explicite côté UI — l'utilisateur ne sait pas qu'il manque quelque chose).
- **`ExerciceInteractive` NON réutilisé** pour les exercices long terme : les shapes et UX sont différents (lesson vs tentative). Deux composants distincts cohabitent.
- **Migration SQL à exécuter manuellement** : `cd expria-backend && supabase db push` avant les tests end-to-end Premium.
## [Unreleased] — 2026-04-22 — Sprint 3.7 — Historique (Backend + Frontend)
### Added (backend)
- `GET /simulations` — liste paginée des productions de l'utilisateur connecté.
- Query params : `page` (défaut 1, entier ≥ 1), `limit` (défaut 20, entier entre 1 et 50).
- Tri : `created_at DESC` côté Supabase.
- Filtre : `user_id = profile.id` (double-protection avec RLS).
- Projection : `id, tache, mode, score, nclc, nclc_cible, created_at` — champs lourds (`contenu`, `rapport`, `exercices`, `modele`) **exclus**.
- Réponse : `{ data: ListItem[], pagination: { page, limit, total } }`.
- Erreurs : `400 VALIDATION_ERROR` si `page`/`limit` invalide, `401 AUTH_REQUIRED` si JWT absent, `500 INTERNAL_ERROR` si DB down.
- `simulationController.list(options, profile)` + interfaces `ListOptions`, `ListItem`, `ListResult`.
- 12 nouveaux tests sur la route `GET /simulations` (186 tests backend verts, +12 vs baseline 174).
### Added (frontend)
- Page `/historique` (route sous `AppLayout` + `ProtectedRoute`, remplace le placeholder `ComingSoon`).
- `HistoriquePage` — orchestre `usePlan` + `useSimulationsList`, state local de pagination, gating plan Free via `hasAccess('dashboard')`.
- `SimulationsList` — composant liste avec :
- Empty state + CTA « Démarrer une simulation » → `/simulation/ee`
- Loading skeleton (5 barres animées)
- Error state (callout discret `border-l-danger`)
- Aperçu flouté Free + bouton `variant="upgrade"` « Passer en Standard »
- Pagination Précédent / Suivant (masquée si une seule page)
- Affichage « Page X sur Y — Z simulations »
- `SimulationListItem` — carte item : date relative, libellé de tâche (`formatTache`), score `/20`, `NCLC atteint / cible`, badges « Examen » et « En cours » (rapport non prêt). Clic → `/rapport/:id`.
- `useSimulationsList(page, limit)` — hook TanStack Query, clé `['simulations', 'list', page, limit]`, `staleTime: 30s`, `placeholderData: keepPreviousData` pour éviter le flash de squelette au changement de page.
- `listSimulations(page, limit)` dans `entities/production/api.ts` — wrap `apiFetch` + `URLSearchParams`.
- Types `SimulationListItem` et `SimulationsListResponse` dans `entities/production/types.ts`.
- `src/shared/lib/date.ts` — helper `formatRelativeDate(iso, now?)` basé sur `Intl.RelativeTimeFormat('fr', { numeric: 'auto' })`. Seuils : secondes → minutes → heures → jours → semaines → mois → années. Zéro dépendance.
- 18 nouveaux tests frontend (7 `date.test.ts` + 11 `SimulationsList.test.tsx`).
### Notes
- Les simulations avec `score === null` (en cours ou correction échouée) sont **affichées** avec un badge « En cours ». Clic → `/rapport/:id``RapportPage` gère le cas `REPORT_NOT_READY` (FTD-21) en redirigeant vers `/simulation/ee`.
- `BlurredPreview` dupliqué localement dans `SimulationsList` (pattern équivalent à `BlurredSection` de `RapportPage`). À extraire en `shared/` si le pattern se répète dans un 3ᵉ endroit — pas fait dans ce sprint.
- Pagination : Précédent/Suivant (MVP) retenu contre scroll infini. Le choix sera revu si l'historique dépasse 100 items en prod.
- Tests frontend : **102/102 verts** (+18 vs baseline 84).
## [Unreleased] — 2026-04-22 — Sprint 3.6b — Qualité correction — Frontend
### Added
- `NclcCibleSelector` (segmented control NCLC 9 / NCLC 10) dans `SimulationForm` — valeur propagée au payload `POST /corrections/ee` via `SimulationFlowProvider.submitText(texte, nclcCible)`.
- Composants `rapport/` dans `features/simulations/components/` :
- `ScoreHero` — score /20, jauge avec marqueur du seuil NCLC cible, écart vs objectif (« X points avant NCLC 9 »), badges NCLC atteint / cible.
- `RevelationCards` — 3 colonnes : ce que le candidat croit / ce que le correcteur observe / conséquence.
- `DiagnosticCallout` — callout « Ce qui freine votre progression ».
- `CritereCard` — carte enrichie par critère (exemple / suggestion / astuce + badges codes taxonomie).
- `ConseilNclcCallout` — plan d'action NCLC (objectif, écart, action prioritaire).
- `ExerciceInteractive` — carte exercice avec zone texte, bouton Indice (révélé une fois), bouton « Voir la correction » (activé après saisie), explication.
- `ProductionModeleSection` — texte final + notes pédagogiques + transformations original/amélioré + message encourageant.
- `JobStatusFallback` — fallback pour `exercices_status` / `modele_status` en `'pending'` ou `'error'`.
- Helpers dans `entities/report/lib.ts` : `groupErreursByCritere`, `ecartVsCible`, `critereCodeFromNom`.
- Tests `ExerciceInteractive.test.tsx` (6 tests) — couvre état interne : révélation unique indice, activation bouton correction, affichage correction + explication.
- FTD-24 🟡 dans `TECH_DEBT.md` — polling automatique pour exercices/modèle `pending` (refresh manuel en MVP).
### Changed
- `entities/report/types.ts` — refonte complète alignée sur le backend Sprint 3.6a : `Report` remplace l'ancien (revelation, diagnostic, criteres enrichis, conseil_nclc, erreurs_codes top-level, exercices dynamiques, modele structuré, statuts pending/ready/error). Suppression de `feedback_court`, `erreurs[]`, `modele:string`, `idees[]` (obsolètes).
- `entities/report/lib.ts``BlurableSection` réduite à `'criteres' | 'exercices' | 'modele'` : `revelation`, `diagnostic`, `conseil_nclc` deviennent visibles pour tous les plans conformément à PLANS_TARIFAIRES.md §2.
- `entities/production/types.ts``SimulationState` étendu avec `nclc_cible`, `exercices`, `exercices_status`, `modele`, `modele_status` ; `SimulationRapport` aligné sur `CorrectionRapport` backend.
- `entities/report/api.ts``getReport` recombine `SimulationState.rapport` + `exercices` + `modele` + statuts en un `Report` unifié pour `useRapport`.
- `RapportPage.tsx` — réécriture complète : câble tous les nouveaux composants, branche le gating plan via `isSectionVisible`, affiche `JobStatusFallback` pour les jobs asynchrones. Résout l'écran blanc post-Sprint 3.6a.
- `floutage.test.ts` réécrit (17 tests — matrice de visibilité + helpers lib).
### Fixed
- **Race condition `modele_status`** (backend) : l'update principal de correction écrasait `modele_status='ready'` déjà posé par `runModeleJob` (lancé en parallèle option b). `correctionController.correctEE` ne touche plus aux colonnes `*_status` — pilotées exclusivement par les jobs asynchrones.
- **Boucle infinie retour rapport → SimulationPage** : le useEffect sticky `step === 'done' → navigate('/rapport/:id')` renvoyait l'utilisateur sur le rapport à chaque tentative de retour vers `/simulation/ee`. Supprimé ; la navigation initiale vers `/rapport/:id` est déclenchée une seule fois dans `correctMutation.onSuccess` du provider.
- **Boucle retour /sujets → SimulationPage** : même pattern sticky pour `step === 'choosing-subject' → navigate('/sujets')`. Supprimé ; navigation initiale vers `/sujets` déplacée dans `createMutation.onSuccess`.
- **RapportPage hors SimulationFlowProvider** : la route `/rapport/:id` n'était pas sous `SimulationFlowLayout` — l'appel à `useSimulation()` depuis RapportPage throw. Route déplacée sous le layout, l'instance du provider est partagée avec `/simulation/ee` et `/sujets`.
### Added
- Bouton « Nouvelle simulation » en bas de `RapportPage` qui `reset()` + `navigate('/simulation/ee')`.
- `reset()` explicite dans le bouton « ← Retour » de `SujetsPage` avant la navigation, pour empêcher tout re-déclenchement de la garde sticky.
### Changed
- Navigations post-mutation déplacées dans `onSuccess` du provider (pattern cohérent pour `createMutation``/sujets` et `correctMutation``/rapport/:id`). Plus de useEffect réactif aux changements de `step` côté SimulationPage.
- `SujetsPage` : garde étendue de `!production` à `!production \|\| step === 'idle' \|\| step === 'done'` pour couvrir le cas post-rapport (évite le 400 VALIDATION_ERROR sur `PATCH /simulations/:id/sujet` d'une simulation déjà corrigée).
- `RapportPage` breadcrumb : `<Link>` remplacé par `<button>` qui `reset()` avant navigate.
### Notes
- **Option β retenue** : frontend aligné sur la structure backend réelle du Sprint 3.6a. Aucun aller-retour backend.
- `feedback_court` supprimé de l'UI ; `diagnostic` remplace la section « Retour général ».
- Polling automatique non implémenté (FTD-24) : refresh manuel de la page si `exercices_status` / `modele_status` = `'pending'`.
- Tests : **84/84 verts** (+8 vs baseline 76).
## [Unreleased] — 2026-04-22 — Sprint 3.6a — Qualité correction — Backend
### Added (backend)
- `src/lib/taxonomieErreurs.ts` : constantes des 63 codes TCF Canada + 4 codes `autre` par critère, validation runtime `isValidCode` / `isValidCritere`, et injection au prompt via `buildTaxonomyPromptSection`.
- Prompts dynamiques dans `src/lib/deepseek.ts` : `buildCorrectionPrompt` (prompt maître avec `nclc_cible` 9 ou 10, sujet, documents T3), `buildModelPrompt` (production modèle cible NCLC 9 fixe), `buildExercicesPrompt` (3 exercices ciblés sur `erreurs_codes` + extraits `exemple`, format `{difficulte, theme, diagnostic, consigne, extrait, indice, correction, explication}`).
- Post-traitement production modèle : `wordCountTCF`, `stripModelAnnotations`, `truncateToMaxWords`.
- Route `POST /corrections/ee` accepte le paramètre `nclc_cible` (optionnel, défaut 9, valeurs acceptées : 9 ou 10 ; sinon 400 VALIDATION_ERROR).
- Migration SQL `supabase/migrations/004_sprint_3_6a_qualite_correction.sql` — colonnes : `revelation`, `diagnostic`, `conseil_nclc`, `erreurs_codes`, `exercices`, `modele`, `nclc_cible`, `exercices_status`, `modele_status` + index GIN sur `erreurs_codes` (pour Sprint 3.6c).
- `controllers/__tests__/correctionController.test.ts` (7 tests) : parallélisme, statuts ready/error, `nclc_cible=10` propagé, simulation introuvable/autre user.
- `docs/TECH_DEBT.md` TD-15 🟡 : jobs fire-and-forget peuvent rester `pending` si redémarrage process.
### Changed (backend)
- `correctEE` dans `deepseek.ts` — nouvelle signature `correctEE(CorrectionInput)` + nouvelle forme `CorrectionRapport` (revelation, diagnostic, criteres[{exemple,suggestion,astuce}], conseil_nclc, erreurs_codes). `EERapport` devient alias de `CorrectionRapport`.
- `correctionController.correctEE` : lance 3 appels DeepSeek en parallèle ; await uniquement sur la correction pour répondre 200 ; modèle et exercices s'exécutent en fire-and-forget et mettent à jour `{exercices,exercices_status}` et `{modele,modele_status}` en base (pending → ready/error).
- `simulationController.getById` retourne les nouveaux champs : `nclc_cible`, `exercices`, `exercices_status`, `modele`, `modele_status` en plus du `rapport` enrichi.
- `deepseek.test.ts` réécrit — 25 tests (ancien pipeline supprimé, nouveaux tests sur correctEE/generateProductionModele/generateExercices/helpers + EO inchangé).
### Notes
- **Option A retenue** : backend renvoie uniquement la nouvelle forme. Frontend (Sprint 3.6b) casse tant que non livré — livraison groupée sans déploiement intermédiaire.
- Prompt exercices rédigé côté backend (option b), basé sur les codes taxonomie + extraits `exemple` des critères. Format aligné sur captures d'écran demandées.
- Migration SQL à exécuter manuellement via `supabase db push` — Hermann avant le premier test end-to-end.
- Tests backend : 173/173 verts (+18 vs baseline de 155).
## [Unreleased] — 2026-04-22 — Planification Sprint 3.6a/3.6b/3.6c
### Added
- Sprints 3.6a (backend prompts + taxonomie), 3.6b (frontend rapport enrichi), 3.6c (analyse patterns Premium) ajoutés à la ROADMAP entre Sprint 3.5 et Sprint 4.
- `TAXONOMIE_ERREURS.md` — 63 codes d'erreurs TCF Canada sur 4 critères + 4 codes « autre » + procédure d'enrichissement.
## 2026-04-21 — FTD-21 — Persistance session `/simulation/ee`
### Added
- `useAutosave(simulationId, contenu, enabled)` : autosave debounce 30 s + flush sur `beforeunload`, dedup par dernier contenu sauvegardé (6 tests).
- `SimulationFlowProvider` hydrate la session au montage depuis `localStorage` (`expria_simulation_id`) → `GET /simulations/:id` → restaure `step='task-selected'` + `production` + `sujet` si `rapport=null` ; nettoie la clé sinon (3 tests resume).
- Types `SimulationState`, `SimulationRapport` + API `getSimulationState`, `autosaveContenu`, `updateSujet` dans `entities/production`.
- Indicateur "Sauvegardé à HH:MM" sous la textarea `SimulationForm` (text-xs, `aria-live="polite"`).
### Changed
- `getReport` délègue désormais à `getSimulationState` et lève `REPORT_NOT_READY` si `rapport=null`. `RapportPage` catche cette erreur et redirige vers `/simulation/ee` avec message discret "Votre simulation est en cours.".
- `SimulationForm` accepte `simulationId`, `initialContenu`, `step` et persiste `expria_simulation_id` dans `localStorage` tant que la simulation est active ; nettoie la clé quand `step='done'`.
- `changeSubject` persiste le changement côté backend via `PATCH /simulations/:id/sujet` (best-effort, silencieux si échec).
### Security
- localStorage ne stocke que `simulation_id` (UUID non-sensible) — conforme SECURITY.md §2.6.
### Notes
- FTD-21 reste ouvert pour `/simulation/eo` (Sprint 4) et `/examen` (Sprint 7).
---
## 2026-04-21 — Tâche G5 — Suggestions d'idées DeepSeek
### Ajouté
- **Backend** — `POST /sujets/idees` : génère 5 suggestions
d'idées via DeepSeek pour aider l'étudiant à prolonger sa
rédaction (prompt coach TCF Canada, temperature 0.5,
timeout 15 s via AbortSignal, JSON strict
`{ idees: string[] }`)
- `generateIdees(consigne, contenu)` dans `src/lib/deepseek.ts`
(validation tableau non vide)
- 5 tests route `POST /sujets/idees` : 401 sans auth,
400 sujet_consigne manquant, 400 contenu < 30 mots,
200 succès avec idees[], 500 DeepSeek throw
- **Frontend** `getIdees(consigne, contenu)` dans
`entities/report/api.ts` (POST `/sujets/idees`,
timeoutMs 15 000)
- Hook `useIdees` `useMutation` exposant
`{ idees, isLoading, error, fetchIdees, reset }`
- Composant `IdeesSuggestions` modal shadcn Dialog avec
liste à puces, états loading/erreur/succès,
`reset()` automatique à la fermeture
- Bouton "Suggestions d'idées" (icône Lightbulb) dans
`SimulationForm` à côté de "Changer de sujet"
- Prop `plan: Plan` ajouté à `SimulationForm` (wiring
`planData.plan` depuis `SimulationPage`)
### Règles d'accès
- Règle D respectée : `hasAccess(plan, 'tips')` obligatoire
- Plan Free : bouton visible mais désactivé avec tooltip
"Disponible en Standard" (tips=false pour Free)
- Standard + Premium : bouton actif dès 30 mots écrits
- Désactivé également si `!sujet`, `isSubmitting`, ou
`idees.isLoading`
### Tests
- Backend Typecheck : 0 erreur, Vitest : 144/144 passés
(+5 tests POST /sujets/idees)
- Frontend Typecheck : 0 erreur, Vitest : 67/67 passés
- Test manuel : validé avec compte Standard (bouton actif
à 30+ mots, modal affiche 5 idées) et Free (bouton
verrouillé avec tooltip)
## 2026-04-21 — Tâche G4 + Refonte page /sujets + Fix quota simulations
### Ajouté
- **Tâche G4** choix du sujet avec dropdown intégré et bouton
aléatoire dans SimulationForm (hook `useSujets`, composant
`SujetSelector`, `getSujets()` sur `GET /sujets?mode=&tache=`)
- **Refonte UX `/sujets`** (Option A) page dédiée avec grille
de cartes `SujetCard` (responsive 1/2/3 colonnes), état partagé
via `SimulationFlowProvider` pour survivre aux navigations entre
`/simulation/ee` et `/sujets`. MVP : refresh sur `/sujets`
redirige vers `/simulation/ee`.
- Bouton "Changer de sujet" dans `SimulationForm` retour à
`/sujets` via `goToSubjectPicker`
- Prop `type: 'EE' | 'EO'` sur `TaskSelector` (EO_CARDS réservé
usage futur non routé, `/simulation/eo` reste `ComingSoon`
jusqu'au Sprint EO)
### Modifié
- `useSimulation` refacto en consommateur de
`SimulationFlowProvider` (source de vérité déplacée hors du hook)
- `SujetDisplay` redevient présentationnel (dropdown retiré)
- `TaskSelector` : retrait des cartes EO de la page
Expression Écrite (affiche uniquement EE T1/T2/T3)
### Corrigé
- **Quota simulations (backend commit `ecb478e`, expria-backend)** :
incrément `simulations_used` déplacé de
`simulationController.create()` vers `correctionController.correctEE/EO`
(Option B). Une simulation créée mais jamais corrigée ne consomme
plus le quota utilisateur.
### Supprimé
- `SujetSelector.tsx` orphelin après refonte `/sujets`
- Helper `selectSujet` de `useSimulation` orphelin
- FTD-22 tracée résolue partiellement (step `'choosing-subject'`
- `goToSubjectPicker` conservés intentionnellement)
### Tests
- Typecheck : 0 erreur
- Vitest : 67/67 passés
- Test manuel : flux complet EE T1 avec choix de sujet
(carte + aléatoire + changement de sujet) validé
## 2026-04-21 — Tâches G2+G3 — Clavier + Minuteur
### Ajouté
- Composant SpecialCharsKeyboard 30 caractères spéciaux
français en flex-wrap, sticky au scroll
- Bloc "Temps restant" sticky avec TimerDisplay MM:SS
(critique < 2min : rouge + pulse, expiré : rouge bold)
- Composant WordCountBar barre de progression colorée
(orange < cible, vert dans cible, rouge > cible)
- Hook useTimer avec 7 tests unitaires
- Config par tâche dans simulationConfig.ts
(EE T1: 10min/60-120 mots, T2: 20min/120-150,
T3: 30min/120-180)
- Auto-submit à l'expiration si ≥ 30 mots
- Bouton "Soumettre ma production" (était "Envoyer")
- Textarea auto-resize sans scroll interne
### Changed
- Compteur de caractères remplacé par WordCountBar
- Bouton soumission bloqué si < 30 mots
### Tests
- Typecheck : 0 erreur
- Vitest : 66/66 passés (+7 tests useTimer)
- Test manuel : minuteur + clavier validés sur mobile
et desktop
## 2026-04-21 — Tâche G1 — Affichage de la consigne
### Ajouté
- Interface SujetData dans entities/production/types.ts
- Production enrichie avec sujet: SujetData | null
- Composant SujetDisplay affiche consigne, rôle, contexte, doc1, doc2 selon le sujet retourné
- useSimulation expose sujet dans son retour
- SimulationForm intègre SujetDisplay au-dessus de la textarea
- FTD-21 tracée (persistance session simulation)
### Tests
- Typecheck : 0 erreur
- Vitest : 59/59 passés
- Test manuel : consigne affichée sur /simulation/ee
## 2026-04-20 — Audit frontend ↔ backend — alignement types Report
### Modifié
- `src/entities/report/types.ts` `Critere.note` `Critere.score`, `Report.exercices: Exercice[]` `Report.exercices: string[]`, JSDoc ajusté
- `src/features/simulations/pages/RapportPage.tsx` import `Exercice` retiré, `critere.note` `critere.score`, `ExerciceCard` refactoré pour consommer une `string` rendue en Markdown, clé d'itération par index
### Supprimé
- Interface `Exercice { titre, contenu }` de `entities/report/types.ts` remplacée par `string[]` pour coller au contrat backend
### Contexte (backend associé, expria-backend)
Quatre commits côté backend finalisent l'alignement du contrat `Report` :
- `feat(corrections)`: renommages `production_modele``modele`, `suggestions_idees``idees`, ajout `feedback_court` + prompts DeepSeek mis à jour + validations runtime
- `feat(corrections)`: réponse enrichie avec `simulation_id` côté `correctionController`
- `feat(simulations)`: nouvelle route `GET /simulations/:id` (auth owner, gestion `SIMULATION_NOT_FOUND`/`AUTH_REQUIRED`/`REPORT_NOT_READY`) + 4 tests
- `feat(simulations)`: sujet aléatoire (table `sujets`) retourné avec chaque production créée (EO_T2_LIVE exclu, non bloquant si aucun sujet actif)
### Tests
- Typecheck : 0 erreur
- Vitest : 59/59 passés
### À faire (hors scope — session frontend dédiée ultérieurement)
- Ajouter `sujet: SujetData | null` dans `entities/production/types.ts`
- Consommer le sujet retourné dans `SimulationPage` (affichage consigne + docs)
- Consommer `feedback_court` dans `RapportPage` (rendu toujours visible cf. PLANS_TARIFAIRES §2 déjà supporté par le type `Report`, reste à brancher dans l'UI si ce n'est pas déjà le cas)
## 2026-04-20 — Sprint 0.5 bis — AppLayout + primitives UI + refonte visuelle
### Ajouté
- `src/app/AppLayout.tsx` layout applicatif desktop/mobile (sidebar fixe 240px, drawer mobile, BottomNav)
- `src/app/Sidebar.tsx` navigation latérale avec verrouillage `hasAccess()` (Progression, Examen blanc, Historique)
- `src/app/MobileHeader.tsx` header mobile sticky (Logo, ThemeToggle, bouton menu hamburger)
- `src/app/BottomNav.tsx` navigation mobile fixe (4 items, bottom sheet "Simuler", tap target min 44px)
- `src/shared/ui/Button.tsx` primitive Button (variants: primary/secondary/ghost/upgrade ; sizes: sm/md/lg ; loading Loader2)
- `src/shared/ui/Card.tsx` primitive Card (variants: default/raised/interactive ; rendu `<button>` si `onClick` fourni)
- `src/shared/ui/Badge.tsx` primitive Badge (variants: plan/nclc/neutral ; couleur selon `planValue` pour variant plan)
### Modifié
- `src/app/router.tsx` layout routes via `PrivateLayout` (`ProtectedRoute` + `AppLayout` + `Outlet`) ; `ComingSoon` inline ; redirect `/simulation` `/simulation/ee`
- `src/features/simulations/components/TaskSelector.tsx` refonte avec `Card interactive` / `Card default opacity-60`, `Badge` "EE"/"EO", eyebrow `tracking-widest`, icône verrou
- `src/features/simulations/pages/SimulationPage.tsx` suppression header interne (Logo + ThemeToggle) ; root `<main>` ; `Button` migré vers `@/shared/ui/Button` `variant="secondary"`
- `src/features/dashboard/pages/DashboardPage.tsx` suppression header interne ; `Button` `variant="primary"` avec `navigate('/simulation/ee')` ; `Badge` `variant="plan" planValue={data.plan}` ; tout migré vers `@/shared/ui/`
### Documentation
- `docs/TECH_DEBT.md` v1.6 ajout FTD-18 (SimulationForm migration Button), FTD-19 (token `--shadow-focus` manquant)
### Tests
- Typecheck : 0 erreur
- Vitest : 59/59 passés
- Tests manuels : à valider par Hermann
---
## 2026-04-19 — Sprint 1 / Étape 6 — Maintenance mode + outillage sécurité
### Ajouté
- Page de maintenance statique (`src/app/MaintenancePage.tsx`) logo + message, tokens Direction H, zéro dépendance
- Guard `VITE_MAINTENANCE_MODE` dans `main.tsx` si `true`, aucun provider ne se monte, aucun appel réseau
- Variable `VITE_MAINTENANCE_MODE` dans `env.ts` (optionnelle, défaut `false`)
- Hook PreToolUse Claude Code (`security-check.sh`) 9 patterns SECURITY.md §2
- Hook Stop Claude Code (`check-file-size.sh`) alerte fichiers > 200 lignes
- MCP server Semgrep enregistré dans Claude Code
### Documentation
- `ARCHITECTURE.md` §7 — ajout `VITE_MAINTENANCE_MODE` dans la liste des variables
- `TECH_DEBT.md` — FTD-16 résolu (maintenance mode implémenté)
### Tests
- Typecheck : 0 erreur
- Vitest : 37/37 passés
- Test manuel : maintenance mode vérifié (page affichée, aucun appel réseau, routing bloqué)