diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e610a63..dc1da9f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -29,6 +29,28 @@ Chaque entrée suit ce format : --- +## [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 diff --git a/docs/TECH_DEBT.md b/docs/TECH_DEBT.md index bf1f37c..0cfebe2 100644 --- a/docs/TECH_DEBT.md +++ b/docs/TECH_DEBT.md @@ -1,6 +1,6 @@ # TECH_DEBT.md — Expria Frontend -> **Document de référence — Version 1.26** +> **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. > @@ -101,31 +101,7 @@ Vient du pattern `c.json(result, result.status)` où `result` contient déjà `s --- -### FTD-14 — Anti-FOUC thème : script inline manquant dans `` - -**Priorité :** 🟡 Important -**Statut :** Ouvert — à faire avant déploiement production -**Estimation de session :** 30 min -**Description :** Le `ThemeProvider` applique la classe `.dark` sur `` 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 `` 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. - -```html - -``` - -**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-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. --- @@ -173,14 +149,7 @@ Vient du pattern `c.json(result, result.status)` où `result` contient déjà `s --- -### FTD-35 — `PresentationGenereeT1Page` : refresh sans simulation active - -**Priorité :** 🟡 Important -**Statut :** Ouvert — introduit au Sprint 4c-2 -**Estimation de session :** 0,5 jour -**Description :** Le hydrate du provider lit `expria_simulation_id` et `expria_eo_t1_presentation` indépendamment. Si l'ID a expiré côté backend (`getSimulationState` rejette en 404) mais que la présentation reste en localStorage, l'utilisateur est redirigé vers `/simulation/eo` au mount du provider, puis la page `PresentationGenereeT1Page` peut être atteinte via reload direct sans `production` valide → la garde `shouldRedirect` envoie vers `/simulation/eo/t1/mode`, qui à son tour redirige vers `/simulation/eo`. Le résultat est correct mais l'utilisateur n'a pas de feedback explicite (« votre simulation a expiré »). -**À faire :** afficher un toast / bandeau dédié quand la résolution `getSimulationState` échoue avec une présentation localStorage présente, et proposer un bouton « Recommencer » qui efface la présentation également. -**Condition de résolution :** Sprint 4c-3 ou clean post-MVP. +> FTD-35 fermée au Sprint 5.5 (2026-04-26) — subsumée par FTD-41. Voir §5 Historique. --- @@ -209,33 +178,7 @@ Vient du pattern `c.json(result, result.status)` où `result` contient déjà `s --- -### FTD-38 — `useAudioRecorder` : mise à jour de ref pendant le render - -**Priorité :** 🟢 Mineur -**Statut :** Ouvert — introduit Sprint 4c-1, signalé Sprint 4.5 -**Estimation de session :** 1h -**Description :** `optionsRef.current = options` est exécuté pendant le render (ligne 73 de `useAudioRecorder.ts`) pour capturer les callbacks inline frais (`onMaxReached`) sans réabonner les effets à chaque render. ESLint signale ce pattern via la règle `react-hooks/refs` (« Cannot update ref during render »). Désactivation locale en place avec commentaire explicatif renvoyant à cette FTD. - -**À faire :** refactorer en `useEffect(() => { optionsRef.current = options })` ou équivalent — l'effet s'exécute après commit, donc avant le prochain render qui utilisera la ref. Risque : 195 lignes de tests sur ce hook (`useAudioRecorder.test.ts`) — vérifier que la sémantique reste identique (auto-stop à `maxSeconds`, callback `onMaxReached` invoqué une seule fois, etc.). - -**Condition de résolution :** session dédiée — non bloquant pour la prod (le pattern fonctionne, c'est un avertissement de bonnes pratiques React 19). - ---- - -### FTD-39 — Règle D violée dans `StatCards.tsx` (`plan === 'free'` en dur) - -**Priorité :** 🟡 Important -**Statut :** Ouvert — introduit Sprint UI Polish (commit `4005673`), signalé Sprint 4.5 -**Estimation de session :** 30 min -**Description :** [`src/features/dashboard/components/StatCards.tsx:90`](../src/features/dashboard/components/StatCards.tsx) contient `{plan === 'free' && (...)}` — viole la Règle D de `DEVELOPMENT_PRINCIPLES.md` (interdiction de comparer `plan === 'xxx'` en dur dans les composants). Doit passer par `hasAccess(plan, '')` ou `canSimulate(plan, used)` selon le sens du gating. - -**À faire :** - -- Identifier la sémantique du gating (probablement « afficher le compteur de simulations restantes uniquement aux Free » → utiliser le résultat de `canSimulate(plan, simulationsUsed)`). -- Remplacer la condition par le helper approprié. -- Vérifier que les tests Dashboard (Free/Standard/Premium) restent verts. - -**Condition de résolution :** session dédiée Dashboard — pas de refacto sans plan validé (Règle A). +> FTD-38, FTD-39 résolus au Sprint 5.5 (2026-04-26) — voir §5 Historique des résolutions. --- @@ -261,26 +204,7 @@ Vient du pattern `c.json(result, result.status)` où `result` contient déjà `s --- -### FTD-42 — Modal prorata Standard→Premium avec montant exact - -**Priorité :** 🟡 Important -**Statut :** Ouvert — introduit Sprint 5d (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 bien le montant prorata + confirmation, mais hors de l'app : l'utilisateur quitte expria.app pendant la confirmation. **Divergence avec PARCOURS_UTILISATEURS.md §3** qui prévoit une modal in-app avec : - -> « Vous paierez [montant calculé]€ aujourd'hui pour accéder au plan Premium jusqu'au [date de fin]. » [Confirmer] [Annuler] - -Endpoint backend déjà disponible : `POST /plans/upgrade-prorata` retourne `{ amount, currency, newPlanExpiry }` calculé par Stripe (preview). - -**À faire :** - -- Hook `usePreviewProrata()` → consomme `POST /plans/upgrade-prorata` au mount de la modal. -- Composant `ProrataConfirmationModal` (montant + date affichés + bouton Confirmer). -- Bouton Confirmer → appel d'un nouvel endpoint backend `POST /plans/upgrade-prorata/confirm` qui réalise effectivement le `subscription.update()` Stripe. -- Tests : 5+ (preview, confirm, erreurs). -- Décision côté UX : remplacer entièrement le flux Customer Portal pour Standard→Premium, OU garder le Customer Portal en fallback « Gérer mon abonnement » ailleurs ? - -**Condition de résolution :** modal in-app livrée + Standard→Premium ne passe plus par Customer Portal mais par cette modal. +> FTD-42 gelée au Sprint 5.5 (2026-04-26) — voir §3bis Backlog gelé. --- @@ -303,13 +227,7 @@ Endpoint backend déjà disponible : `POST /plans/upgrade-prorata` retourne `{ a --- -### FTD-33 — Carte EO_T2_LIVE verrouillée en dur (pas via `hasAccess`) - -**Priorité :** 🟢 Mineur -**Statut :** Ouvert — introduit au Sprint 4c-1 -**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 (pas de logique de plan en dur). -**Condition de résolution :** lancement de T2 Live (Sprint 6). +> FTD-33 gelée au Sprint 5.5 (2026-04-26) — voir §3bis Backlog gelé. --- @@ -503,6 +421,45 @@ Frontend : --- +### 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 @@ -523,19 +480,7 @@ Frontend : ## 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-09 gelée au Sprint 5.5 (2026-04-26) — voir §3bis Backlog gelé. --- @@ -589,6 +534,10 @@ Frontend : | 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 `` | 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`. | --- @@ -624,3 +573,4 @@ Frontend : | 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é). | diff --git a/src/features/dashboard/components/StatCards.tsx b/src/features/dashboard/components/StatCards.tsx index 2957cbe..53c1ede 100644 --- a/src/features/dashboard/components/StatCards.tsx +++ b/src/features/dashboard/components/StatCards.tsx @@ -15,7 +15,7 @@ import { Card } from '@/shared/ui/Card' import { formatRelativeDate } from '@/shared/lib/date' import { isEcrit } from '@/entities/production/lib' import type { SimulationListItem } from '@/entities/production/types' -import type { Plan } from '@/entities/user/lib' +import { hasAccess, type Plan } from '@/entities/user/lib' interface StatCardsProps { plan: Plan @@ -87,7 +87,7 @@ function SimulationsRestantesCard({ style={{ width: `${pct}%` }} /> - {plan === 'free' && ( + {!hasAccess(plan, 'dashboard') && (

Renouvellement offert à l'upgrade

)} diff --git a/src/features/simulations/hooks/useAudioRecorder.ts b/src/features/simulations/hooks/useAudioRecorder.ts index 674096d..d636a3b 100644 --- a/src/features/simulations/hooks/useAudioRecorder.ts +++ b/src/features/simulations/hooks/useAudioRecorder.ts @@ -76,10 +76,10 @@ export function useAudioRecorder(options: UseAudioRecorderOptions = {}): UseAudi // Capture options dans une ref pour éviter de réabonner les effets sur // chaque render (les callers fournissent souvent des fonctions inline). - // Refacto propre via useEffect tracé en FTD-38. const optionsRef = useRef(options) - // eslint-disable-next-line react-hooks/refs - optionsRef.current = options + useEffect(() => { + optionsRef.current = options + }) const maxReachedFiredRef = useRef(false) const cleanupTimer = useCallback(() => {