Sprint 5.5 Clean FTD — triage 21→14 actives (cap 15)

fix(StatCards): replace plan === 'free' with !hasAccess(plan, 'dashboard') (FTD-39)
refactor(useAudioRecorder): move optionsRef assignment to useEffect (FTD-38)
docs(TECH_DEBT): v1.27 — freeze FTD-09/33/42, close FTD-14/35/38/39
This commit is contained in:
Hermann_Kitio 2026-04-26 18:57:50 +03:00
parent 3a3fa6272d
commit 5a31819bca
4 changed files with 78 additions and 106 deletions

View file

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

View file

@ -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 `<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.
```html
<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-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, '<feature>')` 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 `<head>` | 2026-04-26 | Sprint 5.5 — Script `.light` déjà en place dans `index.html` (lignes 14-20), conforme DESIGN_SYSTEM v2.0. L'exemple `.dark` documenté dans la fiche FTD-14 datait de la DA Boréal v1.0 (obsolète). Aucune action code requise — FTD fermée comme déjà résolue. |
| FTD-35 | `PresentationGenereeT1Page` : refresh sans simulation active | 2026-04-26 | Sprint 5.5 — Subsumée par FTD-41 : la résolution de FTD-41 (persistance T1 en BDD) élimine le problème de FTD-35 (localStorage instable). Aucune action propre. |
| FTD-38 | `useAudioRecorder` : mise à jour de ref pendant le render | 2026-04-26 | Sprint 5.5 — Refactor `optionsRef.current = options` (assignation pendant render + eslint-disable) en `useEffect(() => { optionsRef.current = options })`. Sémantique préservée : effet sans deps run après chaque commit, donc avant le prochain render qui lit la ref. eslint-disable retiré. 195 lignes de tests `useAudioRecorder.test.ts` toujours vertes (219/219). |
| FTD-39 | Règle D violée dans `StatCards.tsx` (`plan === 'free'` en dur) | 2026-04-26 | Sprint 5.5 — Remplacement de `{plan === 'free' && ...}` (ligne 90) par `{!hasAccess(plan, 'dashboard') && ...}`. Sémantique du gating : afficher « Renouvellement offert à l'upgrade » uniquement aux utilisateurs sans accès au dashboard complet (= Free). Import `hasAccess` ajouté depuis `@/entities/user/lib`. Tests Dashboard verts. |
| FTD-19 | Token `--shadow-focus` absent de `src/index.css` | 2026-04-22 | Résolu au Sprint 3.5. Ajout de `--shadow-focus: 0 0 0 3px rgba(27, 79, 216, 0.18)` dans `@theme {}` (valeur conforme à `DESIGN_SYSTEM.md §2`) et `--shadow-focus: 0 0 0 3px rgba(91, 127, 255, 0.32)` dans `.dark {}` (recalculé sur la teinte expria dark `#5B7FFF`). Tailwind 4 génère automatiquement l'utility `shadow-focus`. Migration de 5 occurrences `ring-2 ring-expria/20``shadow-focus` dans `Button.tsx`, `Card.tsx`, `SimulationForm.tsx` (×3), `SpecialCharsKeyboard.tsx`. Factorisation bonus : className dupliquée des boutons secondaires de `SimulationForm` extraite en const `secondaryActionBtn`. |
---
@ -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é). |