diff --git a/.cursor/skills/frontend-design/SKILL.md b/.cursor/skills/frontend-design/SKILL.md new file mode 100644 index 0000000..600b6db --- /dev/null +++ b/.cursor/skills/frontend-design/SKILL.md @@ -0,0 +1,42 @@ +--- +name: frontend-design +description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics. +license: Complete terms in LICENSE.txt +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. +- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. + +NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. + +Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. \ No newline at end of file diff --git a/.env.example b/.env.example index 570d1d0..40d3f2a 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,8 @@ VITE_ENABLE_T2_LIVE=false # Optionnel — DSN Sentry pour monitoring prod (laisser commenté en dev local) # VITE_SENTRY_DSN=https://xxxxxx@o000000.ingest.sentry.io/0000000 + +# Sprint 5b — price_ids Stripe publics (Dashboard Stripe → Produits → Plan → Tarif). +# Requis en dev/prod ; absents en CI tests (tests mockent features/billing/api.ts). +VITE_STRIPE_PRICE_STANDARD=price_xxx +VITE_STRIPE_PRICE_PREMIUM=price_xxx diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2ba298d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8484718..173c4bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,10 @@ on: jobs: quality: runs-on: ubuntu-latest + env: + VITE_API_URL: "http://localhost:3000" + VITE_SUPABASE_URL: "https://fake.supabase.co" + VITE_SUPABASE_ANON_KEY: "fake-anon-key-for-ci" steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -21,3 +25,7 @@ jobs: - run: npm run typecheck - run: npm run test - run: npm audit --audit-level=high + - name: Install Semgrep + run: python3 -m pip install --user semgrep + - name: Semgrep scan + run: semgrep scan --config=auto --error --severity=ERROR diff --git a/.gitignore b/.gitignore index 4591520..1862d22 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ dist-ssr # Claude Code local config .claude/ + +# Exploration DA temporaire — supprimer une fois la direction choisie +design-exploration/ diff --git a/design-reference/direction-h-dark.html b/design-reference/direction-h-dark.html new file mode 100644 index 0000000..4a52f4a --- /dev/null +++ b/design-reference/direction-h-dark.html @@ -0,0 +1,927 @@ + + + + + +Expria — Tableau de bord · Direction H : Mode sombre + + + + + +
+
+
+
Direction H — Mode sombre
+
Inversion réfléchie de la version claire. Fond bleu-nuit désaturé, cards qui ressortent, bleu Expria remonté en luminance pour rester lisible.
+
+
Mode sombre
+
+
+ +
+ + + + + +
+ + + + +
+ +
+
Simulations restantes
+
3 / 5
+
+
Plan Découverte — renouvellement à chaque upgrade
+
+ +
+
Niveau estimé
+
NCLC 8
+
+
Moyenne des 3 dernières simulations · Objectif NCLC 9
+
+ +
+
Plan actuel
+
Découverte
+
Débloquez des simulations illimitées, le Mode Examen et le T2 Live avec Standard ou Premium.
+ + Passer à Standard + + +
+ +
+ + +
+ + +
+
Simulations récentes
+
Vos 3 dernières corrections
+ +
+ +
+
+ +
+
+
Expression écrite — Tâche 2
+
Aujourd'hui · 09:42
+
+
NCLC 9
+
16/20
+
+ +
+
+ +
+
+
Expression orale — Tâche 1
+
Il y a 2 jours
+
+
NCLC 8
+
14/20
+
+ +
+
+ +
+
+
Expression écrite — Tâche 3
+
Il y a 5 jours
+
+
NCLC 9
+
15/20
+
+ +
+
+ + +
+
Prochaine étape recommandée
+
Travaillez la tâche 2 à l'oral
+
+ Votre dernier score à l'EO T2 (14/20) est en dessous de votre moyenne. Une simulation de 10 minutes suffit pour consolider. +
+ +
+
+
Durée
+
10 min
+
+
+
Difficulté
+
Modérée
+
+
+ + +
+ +
+ + +
+
Palette de la direction H — mode sombre
+
+
+
+
#0D1220
+
Fond principal
+
+
+
+
#182238
+
Cards surface
+
+
+
+
#5B7FFF
+
Bleu Expria — remonté
+
+
+
+
#27324B
+
Hairlines
+
+
+
+
#F1F4FA
+
Titres
+
+
+
+
#A8B2C7
+
Corps secondaire
+
+
+
+
#3DD68C
+
Succès
+
+
+
+
#F5B849
+
Attention
+
+
+
+ +
+ +
+ + + diff --git a/design-reference/direction-h-juste-milieu.html b/design-reference/direction-h-juste-milieu.html new file mode 100644 index 0000000..87a062d --- /dev/null +++ b/design-reference/direction-h-juste-milieu.html @@ -0,0 +1,921 @@ + + + + + +Expria — Tableau de bord · Direction H : Juste milieu + + + + + +
+
+
+
Direction H — Juste milieu
+
Entre Boréal (trop blanc) et Cadence (trop sombre). Fond gris-bleuté, cards blanches en relief, bleu Expria pivot, accents bleu-nuit.
+
+
Version recommandée
+
+
+ +
+ + + + + +
+ + + + +
+ +
+
Simulations restantes
+
3 / 5
+
+
Plan Découverte — renouvellement à chaque upgrade
+
+ +
+
Niveau estimé
+
NCLC 8
+
+
Moyenne des 3 dernières simulations · Objectif NCLC 9
+
+ +
+
Plan actuel
+
Découverte
+
Débloquez des simulations illimitées, le Mode Examen et le T2 Live avec Standard ou Premium.
+ + Passer à Standard + + +
+ +
+ + +
+ + +
+
Simulations récentes
+
Vos 3 dernières corrections
+ +
+ +
+
+ +
+
+
Expression écrite — Tâche 2
+
Aujourd'hui · 09:42
+
+
NCLC 9
+
16/20
+
+ +
+
+ +
+
+
Expression orale — Tâche 1
+
Il y a 2 jours
+
+
NCLC 8
+
14/20
+
+ +
+
+ +
+
+
Expression écrite — Tâche 3
+
Il y a 5 jours
+
+
NCLC 9
+
15/20
+
+ +
+
+ + +
+
Prochaine étape recommandée
+
Travaillez la tâche 2 à l'oral
+
+ Votre dernier score à l'EO T2 (14/20) est en dessous de votre moyenne. Une simulation de 10 minutes suffit pour consolider. +
+ +
+
+
Durée
+
10 min
+
+
+
Difficulté
+
Modérée
+
+
+ + +
+ +
+ + +
+
Palette de la direction H
+
+
+
+
#EEF2F8
+
Fond principal
+
+
+
+
#FFFFFF
+
Cards (blanc franc)
+
+
+
+
#1B4FD8
+
Bleu Expria — pivot
+
+
+
+
#0B1F5C
+
Bleu nuit — premium
+
+
+
+
#0F172A
+
Titres
+
+
+
+
#475569
+
Corps
+
+
+
+
#0E9F6E
+
Succès
+
+
+
+
#C77A00
+
Attention
+
+
+
+ +
+ +
+ + + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ad7c542..2714d4b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -47,24 +47,25 @@ Le frontend communique avec Supabase **uniquement pour l'authentification** (log Versions officielles au 2026-04-17 (cf. ADR 006 pour la justification) : -| Domaine | Choix | Version | Justification | -|---|---|---|---| -| Framework UI | React | 19.2.x | Compilateur React, Actions, useOptimistic | -| Build tool | Vite | 8.0.x | HMR rapide, moteur Rolldown, config minimale | -| Langage | TypeScript (strict mode) | 6.0.x | Typage fort obligatoire pour détecter les bugs de permissions à la compilation | -| Styling | Tailwind CSS | 4.2.x | Configuration CSS-first via `@theme`, moteur Oxide (builds microseconde) | -| UI components | shadcn/ui | CLI latest | Copy-paste, total contrôle, supporte Tailwind 4 + React 19 depuis 2025 | -| Routing | React Router | v7.14.x | Compatible API v6, data loaders disponibles | -| État serveur | TanStack Query | 5.x | Cache, refetch, invalidation, remplace Redux/SWR | -| État local | `useState` / `useReducer` | React 19 built-in | Pas de store global pour la V2 (voir ADR 003) | -| Auth | Supabase JS | 2.103.x | Côté frontend : auth uniquement. Cf. `ARCHITECTURE.md` backend §2 | -| Validation | Zod | latest | Validation des inputs formulaires (cf. SECURITY.md SEC-04) | -| Rendu Markdown | react-markdown | latest | Rendu sécurisé des rapports IA (cf. SECURITY.md SEC-05) | -| Tests | Vitest + React Testing Library | latest | Parité avec backend (qui utilise Vitest) | -| Lint + Format | ESLint + Prettier | 9.x + latest | Standard | -| CI | GitHub Actions | — | Typecheck + tests + `npm audit` | +| Domaine | Choix | Version | Justification | +| -------------- | ------------------------------ | ----------------- | ------------------------------------------------------------------------------ | +| Framework UI | React | 19.2.x | Compilateur React, Actions, useOptimistic | +| Build tool | Vite | 8.0.x | HMR rapide, moteur Rolldown, config minimale | +| Langage | TypeScript (strict mode) | 6.0.x | Typage fort obligatoire pour détecter les bugs de permissions à la compilation | +| Styling | Tailwind CSS | 4.2.x | Configuration CSS-first via `@theme`, moteur Oxide (builds microseconde) | +| UI components | shadcn/ui | CLI latest | Copy-paste, total contrôle, supporte Tailwind 4 + React 19 depuis 2025 | +| Routing | React Router | v7.14.x | Compatible API v6, data loaders disponibles | +| État serveur | TanStack Query | 5.x | Cache, refetch, invalidation, remplace Redux/SWR | +| État local | `useState` / `useReducer` | React 19 built-in | Pas de store global pour la V2 (voir ADR 003) | +| Auth | Supabase JS | 2.103.x | Côté frontend : auth uniquement. Cf. `ARCHITECTURE.md` backend §2 | +| Validation | Zod | latest | Validation des inputs formulaires (cf. SECURITY.md SEC-04) | +| Rendu Markdown | react-markdown | latest | Rendu sécurisé des rapports IA (cf. SECURITY.md SEC-05) | +| Tests | Vitest + React Testing Library | latest | Parité avec backend (qui utilise Vitest) | +| Lint + Format | ESLint + Prettier | 9.x + latest | Standard | +| CI | GitHub Actions | — | Typecheck + tests + `npm audit` | **Choix motivés par ADR :** + - ADR 001 : Cloudflare Pages (hébergement) - ADR 002 : Découplage `auth-client` / `api-client` - ADR 003 : Pas de Zustand pour la V2 @@ -95,10 +96,16 @@ expria-frontend/ │ └── 005-has-access-typed-strict.md │ ├── src/ -│ ├── app/ # CONFIGURATION ET ENTRY POINTS -│ │ ├── providers.tsx # QueryClientProvider + AuthProvider + Router +│ ├── app/ # ENTRY POINTS + LAYOUT DE LA COQUILLE +│ │ ├── main.tsx # Entry point React (montage DOM) +│ │ ├── providers.tsx # QueryClientProvider + ThemeProvider + Router │ │ ├── router.tsx # Routes déclaratives -│ │ └── main.tsx # Entry point React +│ │ ├── route-titles.ts # Mapping route → titre (breadcrumb Topbar) +│ │ ├── AppLayout.tsx # Coquille app (sidebar + topbar + outlet) +│ │ ├── Sidebar.tsx # Navigation desktop (navy permanent) +│ │ ├── Topbar.tsx # Topbar sticky (breadcrumb, recherche, theme toggle) +│ │ ├── BottomNav.tsx # Navigation mobile (< 1024px) +│ │ └── MaintenancePage.tsx # Page affichée si VITE_MAINTENANCE_MODE=true │ │ │ ├── entities/ # OBJETS MÉTIER (indépendants de l'UI) │ │ ├── user/ @@ -106,67 +113,109 @@ expria-frontend/ │ │ │ ├── lib.ts # hasAccess(), canSimulate() │ │ │ ├── access.ts # IDENTIQUE à expria-backend/src/lib/access.ts │ │ │ ├── api.ts # GET /plans/status, POST /auth/verify-token +│ │ │ ├── query-keys.ts # Constantes de clés TanStack (PLAN_QUERY_KEY) │ │ │ └── __tests__/ -│ │ │ ├── hasAccess.test.ts -│ │ │ └── canSimulate.test.ts │ │ │ │ │ ├── production/ │ │ │ ├── types.ts # Production, Tache, Mode │ │ │ ├── lib.ts # helpers (format tache, etc.) -│ │ │ └── api.ts # POST /simulations, GET /simulations/:id +│ │ │ └── api.ts # POST /simulations, GET /simulations/:id, PATCH /:id/contenu │ │ │ -│ │ └── report/ -│ │ ├── types.ts # Report, Critere -│ │ ├── lib.ts # Logique de floutage selon plan -│ │ ├── api.ts # POST /corrections/ee, POST /corrections/eo -│ │ └── __tests__/ -│ │ └── floutage.test.ts +│ │ ├── report/ +│ │ │ ├── types.ts # Report, Critere, Revelation, Diagnostic +│ │ │ ├── lib.ts # Logique de floutage selon plan +│ │ │ ├── api.ts # POST /corrections/ee, POST /corrections/eo +│ │ │ └── __tests__/ +│ │ │ +│ │ ├── patterns/ # Sprint 3.6c — analyse patterns Premium +│ │ │ ├── types.ts # Pattern, PatternAnalysis, PreparationIndex +│ │ │ └── api.ts # GET /users/patterns +│ │ │ +│ │ ├── presentation/ # Sprint 4c-2 — présentation T1 EO +│ │ │ ├── types.ts +│ │ │ └── api.ts # POST /presentations/generate +│ │ │ +│ │ └── transcription/ # Sprint 4c — code Deepgram dormant (cf. FTD-37) +│ │ ├── types.ts +│ │ └── api.ts │ │ │ ├── features/ # UI (composants + pages + hooks) │ │ ├── auth/ -│ │ │ ├── components/ # LoginForm, RegisterForm, ProtectedRoute +│ │ │ ├── components/ # ProtectedRoute │ │ │ ├── pages/ # LoginPage, RegisterPage │ │ │ └── hooks/ # useAuth │ │ │ │ │ ├── dashboard/ -│ │ │ ├── components/ # DashboardFreeView, DashboardStandardView, DashboardPremiumView +│ │ │ ├── components/ # DashboardFreeView/StandardView/PremiumView, +│ │ │ │ # NclcHero, StatCards, RecentSimulations, +│ │ │ │ # NextStepCard, PaywallBanner, MonProfilPreparation │ │ │ ├── pages/ # DashboardPage (orchestre les vues selon le plan) │ │ │ └── hooks/ # usePlan │ │ │ │ │ ├── simulations/ -│ │ │ ├── components/ # SimulationForm, AudioRecorder, TimerExam -│ │ │ ├── pages/ # SimulationPage, RapportPage -│ │ │ └── hooks/ # useSimulation, useExamMode +│ │ │ ├── components/ # SimulationForm, AudioRecorder, TimerDisplay, +│ │ │ │ │ # TaskSelector, SujetCard/Display, IdeesSuggestions, +│ │ │ │ │ # NclcCibleSelector, SpecialCharsKeyboard, +│ │ │ │ │ # WordCountBar, TranscriptionDisplay +│ │ │ │ └── rapport/ # ScoreHero, RevelationCards, CritereCard, +│ │ │ │ # DiagnosticCallout, ConseilNclcCallout, +│ │ │ │ # ExerciceInteractive, ProductionModeleSection, +│ │ │ │ # JobStatusFallback +│ │ │ ├── pages/ # EE : SimulationPage, SujetsPage, RapportPage +│ │ │ │ # EO : SujetsEOPage, PreEnregistrementEOPage, +│ │ │ │ # EnregistrementEOPage, SimulationEOPage, +│ │ │ │ # ModeChoixT1Page, QuestionnaireT1Page, +│ │ │ │ # PresentationGenereeT1Page +│ │ │ ├── hooks/ # useSimulation, useSujets, useRapport, useTimer, +│ │ │ │ # useAutosave, useIdees, useAudioRecorder, +│ │ │ │ # useDeepgramLive (dormant — FTD-37) +│ │ │ ├── lib/ # simulationConfig.ts (durées, mots cibles, etc.) +│ │ │ └── state/ # SimulationFlowProvider + simulationFlow (machine d'état) │ │ │ -│ │ ├── t2-live/ -│ │ │ ├── components/ # DialogueView, AudioVisualizer -│ │ │ ├── pages/ # T2LivePage -│ │ │ ├── hooks/ # useT2LiveSession -│ │ │ ├── lib/ -│ │ │ │ ├── ws-client.ts # WebSocket + reconnexion -│ │ │ │ └── audio.ts # Capture PCM + lecture réponse -│ │ │ └── state/ -│ │ │ └── t2-machine.ts # State machine (idle → connecting → listening → ...) +│ │ ├── historique/ # Sprint 3.7 — liste des productions +│ │ │ ├── components/ # SimulationsList, SimulationListItem +│ │ │ ├── pages/ # HistoriquePage +│ │ │ └── hooks/ # useSimulationsList │ │ │ -│ │ └── billing/ -│ │ ├── components/ # PaymentSummary -│ │ ├── pages/ # PricingPage, CheckoutPage, UpgradePage -│ │ └── hooks/ # useStripeCheckout +│ │ ├── progression/ # Sprint 3.6c — analyse patterns Premium +│ │ │ ├── components/ # PreparationIndexHero, PatternsList, +│ │ │ │ # PatternExerciceCard, ProgressionPremium, +│ │ │ │ # BlurredProgression, NotReadyState +│ │ │ ├── pages/ # ProgressionPage +│ │ │ └── hooks/ # usePatterns +│ │ │ +│ │ └── design-system/ # Page interne de référence visuelle (DA Charcoal) +│ │ └── DesignSystemPage.tsx │ │ │ ├── shared/ # CODE RÉUTILISABLE NON MÉTIER +│ │ ├── ui/ # PRIMITIVES EXPRIA (PascalCase) — voir note ci-dessous +│ │ │ ├── Button.tsx +│ │ │ ├── Card.tsx +│ │ │ └── Badge.tsx │ │ ├── components/ -│ │ │ ├── ui/ # Button, Modal, Badge (shadcn/ui) -│ │ │ ├── PaywallModal.tsx # Blocage + boutons upgrade -│ │ │ └── Spinner.tsx -│ │ ├── hooks/ # useDebounce, useLocalStorage +│ │ │ ├── ui/ # PRIMITIVES SHADCN BRUTES (kebab-case) — voir note +│ │ │ │ ├── avatar.tsx +│ │ │ │ ├── badge.tsx +│ │ │ │ ├── button.tsx +│ │ │ │ ├── dialog.tsx +│ │ │ │ ├── input.tsx +│ │ │ │ ├── label.tsx +│ │ │ │ ├── progress.tsx +│ │ │ │ └── separator.tsx +│ │ │ ├── Logo.tsx +│ │ │ └── ThemeToggle.tsx +│ │ ├── hooks/ # useTheme │ │ ├── lib/ │ │ │ ├── auth-client.ts # Supabase Auth uniquement (ADR 002) │ │ │ ├── api-client.ts # Fetch + retry + timeout + logging (ADR 002) │ │ │ ├── query-client.ts # Configuration TanStack Query -│ │ │ └── logger.ts # Logging structuré frontend +│ │ │ ├── logger.ts # Logging structuré frontend +│ │ │ ├── theme.ts # getInitialTheme / applyTheme / persistTheme +│ │ │ ├── audio.ts # Helpers MediaRecorder + mime detection +│ │ │ ├── date.ts # formatRelativeDate (Intl.RelativeTimeFormat) +│ │ │ └── utils.ts # cn() — clsx + tailwind-merge │ │ ├── types/ -│ │ │ ├── api.ts # ApiResponse, ApiError -│ │ │ └── common.ts # Types utilitaires +│ │ │ └── api.ts # ApiError, ApiErrorCode, FrontendErrorCode │ │ └── config/ │ │ └── env.ts # Validation des variables d'environnement au démarrage │ │ @@ -186,6 +235,29 @@ expria-frontend/ └── README.md ``` +### Note sur `app/` — entry points + layout + +Le dossier `app/` contient les entry points React (`main.tsx`, `providers.tsx`, `router.tsx`) **ET** les composants layout de la coquille applicative (`AppLayout`, `Sidebar`, `Topbar`, `BottomNav`, `MaintenancePage`). Ces composants ne sont rattachés à aucune feature : ils définissent la structure visuelle globale de l'app et orchestrent l'affichage des routes. Leur rôle structurel justifie leur place dans `app/` plutôt que dans `shared/components/` ou dans une feature dédiée. + +> Note : `t2-live/` (Sprint 6) et `billing/` (Sprint 5) ne sont pas encore implémentés et n'apparaissent volontairement pas dans cette arborescence. Voir `ROADMAP.md` pour le calendrier. + +### Convention `shared/ui/` vs `shared/components/ui/` + +Deux dossiers UI cohabitent dans `shared/`. **La distinction est volontaire :** + +| Dossier | Convention | Contenu | Usage | +| ----------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `shared/ui/` | PascalCase (`Button.tsx`) | **Wrappers Expria maison** : tokens DA Charcoal appliqués, API simplifiée, variants métier (`primary`, `secondary`, `ghost`, `upgrade`). | **À utiliser par défaut dans toutes les features.** | +| `shared/components/ui/` | kebab-case (`button.tsx`) | **Primitives shadcn/ui brutes** générées par la CLI shadcn. | À utiliser **uniquement** comme base interne pour construire un wrapper Expria, ou quand une primitive Radix (Dialog, Popover) est nécessaire directement. | + +**Règle d'évolution :** + +- Toute nouvelle primitive Expria va dans `shared/ui/`. +- Aucune nouvelle primitive ne doit être ajoutée manuellement dans `shared/components/ui/` — ce dossier est réservé aux fichiers générés par la CLI shadcn. +- Si un wrapper Expria s'appuie sur une primitive shadcn, il l'importe depuis `shared/components/ui/` et l'expose sous une API simplifiée dans `shared/ui/.tsx`. + +Cette dualité est tracée dans `TECH_DEBT.md` (FTD-26) — documentée, pas à fusionner. + ### Règles de dépendance entre dossiers ``` @@ -197,6 +269,10 @@ shared/ ne doit RIEN importer des autres dossiers Cette hiérarchie garantit que la logique métier (`entities/`) ne dépend jamais de l'UI (`features/`), et que les briques réutilisables (`shared/`) restent portables. +**Exception documentée — cross-entity report → user :** +`entities/report/lib.ts` importe `hasAccess` et `Plan` depuis `entities/user/lib`. +Justification : la logique de floutage des rapports dépend intrinsèquement des permissions utilisateur. Utiliser `hasAccess()` est obligatoire (Règle D) ; le déplacer vers `shared/` briserait la cohésion du domaine `user`. Cette exception est volontaire et ne doit pas être généralisée à d'autres paires d'entities. + --- ## 4. Flux de données @@ -260,12 +336,12 @@ Composant React **Gestion des close codes côté frontend :** -| Close code | Cause | Action côté frontend | -|---|---|---| -| 1000 | Fermeture normale | State → 'ended', afficher le rapport | -| 4001 | AUTH_REQUIRED | State → 'error', redirect `/login` | -| 4003 | PLAN_INSUFFICIENT | State → 'error', afficher PaywallModal Premium | -| Autre | Erreur réseau ou serveur | State → 'error', message générique + bouton "Réessayer" | +| Close code | Cause | Action côté frontend | +| ---------- | ------------------------ | ------------------------------------------------------- | +| 1000 | Fermeture normale | State → 'ended', afficher le rapport | +| 4001 | AUTH_REQUIRED | State → 'error', redirect `/login` | +| 4003 | PLAN_INSUFFICIENT | State → 'error', afficher PaywallModal Premium | +| Autre | Erreur réseau ou serveur | State → 'error', message générique + bouton "Réessayer" | --- @@ -345,44 +421,44 @@ export interface ApiError { error: true code: ApiErrorCode message: string - status?: number // quirk backend (simulations, corrections) + status?: number // quirk backend (simulations, corrections) } export type ApiErrorCode = - | 'AUTH_REQUIRED' // 401 — JWT absent, invalide ou expiré - | 'PLAN_INSUFFICIENT' // 403 — feature non disponible pour ce plan - | 'QUOTA_REACHED' // 403 — quota de simulations Free épuisé - | 'VALIDATION_ERROR' // 400 — corps de requête invalide (simulations, corrections) - | 'INVALID_BODY' // 400 — corps de requête invalide (plans, stripe) — voir note - | 'INVALID_PLAN' // 400 — valeur de plan inconnue - | 'NO_ACTIVE_SUBSCRIPTION' // 400 — tentative d'upgrade sans abonnement actif - | 'SIMULATION_NOT_FOUND' // 404 — simulation inexistante ou non accessible - | 'STRIPE_WEBHOOK_INVALID' // 400 — signature webhook invalide - | 'INTERNAL_ERROR' // 500 — erreur serveur inattendue + | 'AUTH_REQUIRED' // 401 — JWT absent, invalide ou expiré + | 'PLAN_INSUFFICIENT' // 403 — feature non disponible pour ce plan + | 'QUOTA_REACHED' // 403 — quota de simulations Free épuisé + | 'VALIDATION_ERROR' // 400 — corps de requête invalide (simulations, corrections) + | 'INVALID_BODY' // 400 — corps de requête invalide (plans, stripe) — voir note + | 'INVALID_PLAN' // 400 — valeur de plan inconnue + | 'NO_ACTIVE_SUBSCRIPTION' // 400 — tentative d'upgrade sans abonnement actif + | 'SIMULATION_NOT_FOUND' // 404 — simulation inexistante ou non accessible + | 'STRIPE_WEBHOOK_INVALID' // 400 — signature webhook invalide + | 'INTERNAL_ERROR' // 500 — erreur serveur inattendue // Erreurs générées côté frontend uniquement (pas envoyées par le backend) export type FrontendErrorCode = - | 'TIMEOUT' // timeout côté client (AbortController) - | 'NETWORK_ERROR' // pas de réponse réseau - | 'PARSE_ERROR' // réponse non-JSON + | 'TIMEOUT' // timeout côté client (AbortController) + | 'NETWORK_ERROR' // pas de réponse réseau + | 'PARSE_ERROR' // réponse non-JSON ``` > **Note sur `VALIDATION_ERROR` vs `INVALID_BODY`** : le backend utilise deux codes pour la même classe d'erreur (corps invalide). `VALIDATION_ERROR` dans les routes simulations/corrections, `INVALID_BODY` dans les routes plans/stripe. Cette inconsistance est documentée dans `TECH_DEBT.md` backend (TD-15 à créer). Côté frontend, les deux codes sont gérés de la même manière dans l'UI. ### Codes d'erreur — mapping HTTP -| Code backend | HTTP | Signification | Routes émettrices | -|---|---|---|---| -| `AUTH_REQUIRED` | 401 | JWT absent, invalide, expiré, ou profil introuvable | middleware, corrections | -| `PLAN_INSUFFICIENT` | 403 | Feature réservée à un plan supérieur | middleware, simulations | -| `QUOTA_REACHED` | 403 | 5/5 simulations utilisées (plan Free) | simulations | -| `VALIDATION_ERROR` | 400 | Corps de requête invalide (simulations, corrections) | simulations, corrections | -| `INVALID_BODY` | 400 | Corps de requête invalide (plans, stripe) | plans, stripe | -| `INVALID_PLAN` | 400 | Valeur de plan inconnue dans le payload | plans, stripe | -| `NO_ACTIVE_SUBSCRIPTION` | 400 | Upgrade prorata sans abonnement actif | plans | -| `SIMULATION_NOT_FOUND` | 404 | Simulation inexistante ou non accessible | corrections | -| `STRIPE_WEBHOOK_INVALID` | 400 | Signature webhook invalide | stripe | -| `INTERNAL_ERROR` | 500 | Erreur serveur inattendue | plans, stripe, corrections, simulations | +| Code backend | HTTP | Signification | Routes émettrices | +| ------------------------ | ---- | ---------------------------------------------------- | --------------------------------------- | +| `AUTH_REQUIRED` | 401 | JWT absent, invalide, expiré, ou profil introuvable | middleware, corrections | +| `PLAN_INSUFFICIENT` | 403 | Feature réservée à un plan supérieur | middleware, simulations | +| `QUOTA_REACHED` | 403 | 5/5 simulations utilisées (plan Free) | simulations | +| `VALIDATION_ERROR` | 400 | Corps de requête invalide (simulations, corrections) | simulations, corrections | +| `INVALID_BODY` | 400 | Corps de requête invalide (plans, stripe) | plans, stripe | +| `INVALID_PLAN` | 400 | Valeur de plan inconnue dans le payload | plans, stripe | +| `NO_ACTIVE_SUBSCRIPTION` | 400 | Upgrade prorata sans abonnement actif | plans | +| `SIMULATION_NOT_FOUND` | 404 | Simulation inexistante ou non accessible | corrections | +| `STRIPE_WEBHOOK_INVALID` | 400 | Signature webhook invalide | stripe | +| `INTERNAL_ERROR` | 500 | Erreur serveur inattendue | plans, stripe, corrections, simulations | ### Pattern `apiFetch` @@ -398,9 +474,14 @@ const { data, error, isLoading } = useQuery({ // error est de type ApiError | null if (error) { switch (error.code) { - case 'AUTH_REQUIRED': redirectToLogin(); break - case 'QUOTA_REACHED': openUpgradeModal(); break - default: showGenericErrorToast() + case 'AUTH_REQUIRED': + redirectToLogin() + break + case 'QUOTA_REACHED': + openUpgradeModal() + break + default: + showGenericErrorToast() } } ``` @@ -484,9 +565,18 @@ export const PLANS = { }, } -export function getPlanPermissions(plan: Plan) { /* ... */ } -export function canUserSimulate(user: { plan: string; simulations_used: number }): { allowed, reason? } { /* ... */ } -export function checkFeatureAccess(plan: Plan, feature: Feature): boolean { /* ... */ } +export function getPlanPermissions(plan: Plan) { + /* ... */ +} +export function canUserSimulate(user: { plan: string; simulations_used: number }): { + allowed + reason? +} { + /* ... */ +} +export function checkFeatureAccess(plan: Plan, feature: Feature): boolean { + /* ... */ +} ``` ### Alias frontend-idiomatiques @@ -495,11 +585,7 @@ Le fichier `src/entities/user/lib.ts` ré-exporte ces fonctions sous des noms st ```typescript // src/entities/user/lib.ts -import { - canUserSimulate, - checkFeatureAccess, - getPlanPermissions, -} from './access' +import { canUserSimulate, checkFeatureAccess, getPlanPermissions } from './access' /** * Alias frontend-idiomatique de checkFeatureAccess. @@ -550,6 +636,7 @@ VITE_SUPABASE_URL=https://xxx.supabase.co VITE_SUPABASE_ANON_KEY=xxx VITE_ENABLE_T2_LIVE=false # flag pour cacher T2 en prod tant que pas prêt VITE_SENTRY_DSN=xxx # optionnel, pour monitoring +VITE_MAINTENANCE_MODE=false # true = affiche MaintenancePage avant tout provider ``` ### Règle absolue @@ -563,6 +650,7 @@ VITE_SENTRY_DSN=xxx # optionnel, pour monitoring - `STRIPE_WEBHOOK_SECRET` Cette règle est vérifiée par : + - Le plugin Security Guidance de Claude Code (voir `SECURITY.md`) - Une règle Semgrep dans la CI - Le scan de secrets GitHub (Dependabot) @@ -577,12 +665,12 @@ Cette règle est vérifiée par : ### Infrastructure cible (cf. ADR 001) -| Composant | Plateforme | URL | -|---|---|---| -| Frontend | Cloudflare Pages | `https://expria.app` | -| Backend API | Render (Frankfurt) | `https://api.expria.app` | -| DNS | Vercel (actuellement) | — | -| Base de données | Supabase (Frankfurt) | — | +| Composant | Plateforme | URL | +| --------------- | --------------------- | ------------------------ | +| Frontend | Cloudflare Pages | `https://expria.app` | +| Backend API | Render (Frankfurt) | `https://api.expria.app` | +| DNS | Vercel (actuellement) | — | +| Base de données | Supabase (Frankfurt) | — | ### Workflow de déploiement @@ -617,13 +705,13 @@ Tests ciblés sur la logique critique, pas exhaustifs. On copie la stratégie ba ### Fichiers obligatoirement couverts -| Fichier | Nombre de tests minimum | -|---|---| -| `src/entities/user/__tests__/hasAccess.test.ts` | 14+ | -| `src/entities/user/__tests__/canSimulate.test.ts` | 7 | -| `src/entities/report/__tests__/floutage.test.ts` | 8+ (un par critère à flouter × 3 plans) | -| `src/features/t2-live/state/__tests__/t2-machine.test.ts` | 6+ (transitions d'états) | -| `src/features/dashboard/hooks/__tests__/usePlan.test.ts` | 3+ (cache, refetch, invalidation) | +| Fichier | Nombre de tests minimum | +| --------------------------------------------------------- | --------------------------------------- | +| `src/entities/user/__tests__/hasAccess.test.ts` | 14+ | +| `src/entities/user/__tests__/canSimulate.test.ts` | 7 | +| `src/entities/report/__tests__/floutage.test.ts` | 8+ (un par critère à flouter × 3 plans) | +| `src/features/t2-live/state/__tests__/t2-machine.test.ts` | 6+ (transitions d'états) | +| `src/features/dashboard/hooks/__tests__/usePlan.test.ts` | 3+ (cache, refetch, invalidation) | ### Fichiers non testés (par design) @@ -650,39 +738,50 @@ Un échec sur l'un de ces jobs bloque le merge. Ces règles sont héritées de `DEVELOPMENT_PRINCIPLES.md` backend et adaptées au frontend. ### Règle 1 — Séparation stricte + Le frontend affiche des données et relaie des actions. Aucune logique métier. ### Règle 2 — Source de vérité unique pour les plans + `src/entities/user/access.ts` est identique à `expria-backend/src/lib/access.ts`. Toute modification se fait dans les deux dépôts, dans le même commit logique. ### Règle 3 — Maximum 3 fichiers par étape + Hérité du backend. Si une tâche nécessite plus de 3 fichiers, elle est découpée. ### Règle 4 — Plan avant code + Aucune session Claude Code ne commence à coder sans plan validé. ### Règle 5 — Tests verts avant de continuer + `npm run test` et `npm run typecheck` doivent passer après chaque étape. ### Règle 6 — Jamais de clé privée dans le frontend + Variables `VITE_*` uniquement. Cf. section 7. ### Règle 7 — Jamais de `if (plan === 'xxx')` + Toute vérification de permission passe par `hasAccess()` ou `canSimulate()`. Cf. ADR 005. ### Règle 8 — Jamais de logique métier dans `features/` + Les règles de floutage, de quotas, de permissions vivent dans `entities/*/lib.ts`. Les composants de `features/` appellent ces fonctions. ### Règle 9 — Jamais d'appel direct à Supabase pour les données métier + Supabase côté frontend est **uniquement** pour l'authentification. Toute lecture/écriture passe par le backend Hono. ### Règle 10 — Signaler tout écart par rapport au plan + Identique à la Règle H backend. --- ## 11. Historique des versions de ce document -| Version | Date | Auteur | Changements | -|---|---|---|---| -| 1.0 | 2026-04-17 | Hermann (avec assistance Claude) | Création initiale | +| Version | Date | Auteur | Changements | +| ------- | ---------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1.0 | 2026-04-17 | Hermann (avec assistance Claude) | Création initiale | +| 1.1 | 2026-04-25 | Hermann (avec assistance Claude) | FTD-25 + FTD-26 — §3 reflète l'arborescence réelle ; ajout note `app/` (entry points + layout) ; ajout convention `shared/ui/` vs `shared/components/ui/` | diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b0c2276..fb6b15f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -29,9 +29,923 @@ Chaque entrée suit ce format : --- +## [Unreleased] — 2026-07-02 — Sprint 7.5 — Clean FTD-44 + +### Changed + +- Hooks audio génériques `useAudioCapture`, `useAudioPlayback`, `useAudioRecording` (+ test) déplacés de `features/t2-live/hooks/` vers `shared/lib/audio/`. Imports mis à jour dans `useT2LiveSession.ts` et `useT1LiveSession.ts`. Résout FTD-44. + +### Removed + +- Marqueurs `// TODO(FTD-44)` retirés de `useT1LiveSession.ts`. + +### Notes + +- 4 fichiers déplacés (3 hooks + 1 test) + 2 fichiers d'imports édités. Aucun changement fonctionnel (pipeline audio « Voie A » intact). +- 301/301 tests verts, 0 erreur typecheck. +- Validation manuelle : T2 Live D2-D5 verts ; T1 Live parcours complet vert (préparation → dialogue → présentation → interruption → fin → téléchargement audio → rapport), navigation privée. D6 non rejoué (optionnel, état pré-existant partiel, sans lien avec cette refacto). +- Commit `d9160c4`. +- **TECH_DEBT.md v1.30 → v1.31** : FTD-44 résolue (fermée) ; ajout FTD-47 🟢 (sessions T1 Live non taguées dans l'historique — découverte pendant cette session, racine Sprint 7a backend). **14 → 15 FTD actives — cap de 15 atteint.** +- **GOLDEN_DATASET.md** : Groupe D étendu avec D12-D16 (T1 Live) — libellés vérifiés dans le code réel (`T1PreparationPage.tsx`, `T1DialoguePage.tsx`, `t1-machine.ts`). + +--- + +## [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=` (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` → `` (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 `` au-dessus de `` 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` → `` (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 `
`, 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 : `` remplacé par ` +``` + +**Bouton secondaire :** +```tsx + +``` + +**Badge sémantique :** +```tsx + + {children} + +``` + +### Règles d'implémentation + +- Chaque primitive **accepte `className`** en plus de ses props typées, pour overrides ponctuels. +- Chaque primitive **expose ses props via un type exporté** (`ButtonProps`, `CardProps`, etc.). +- Aucune primitive ne contient de logique métier ou d'appel API. Elles reçoivent tout par props. +- Les icônes sont importées de `lucide-react` et passées comme composant, jamais par nom de string. + +--- + +## 6. Layout principal — `AppLayout` + +```tsx +
+ {/* fixed, w-[230px], bg sidebar navy */} +
+
+ {children} +
+
+
+``` + +### Breakpoints + +| Breakpoint | Comportement | +|---|---| +| `< 1024px` | Sidebar masquée, `BottomNav` fixe en bas, padding horizontal 20px | +| `≥ 1024px` | Sidebar 230px + contenu centré 1100px max, padding 36px | +| `≥ 1440px` | Contenu centré 1100px max (pas d'élargissement) | + +### Densité verticale + +- Padding vertical section : 24px mobile, 32px desktop. +- Gap inter-cards : 12px mobile, 16px desktop. +- Marge sous `PageHeader` : 20px mobile, 28px desktop. + +--- + +## 7. Données mock + +Avant branchement API, fournir les données via `src/shared/api/mock/dashboard.ts`. Données crédibles, françaises, alignées sur l'audience réelle. + +```typescript +export const mockDashboard = { + user: { + firstName: 'Yacine', + plan: 'decouverte' as const, + planLabel: 'Plan Découverte', + }, + metrics: { + nclcEstimated: 7.5, + nclcTarget: 9, + simulationsUsed: 2, + simulationsQuota: 5, + lastScore: { value: 16, max: 20, type: 'ecrit' as const }, + }, + recentSimulations: [ + { id: 's-001', type: 'ecrit', relativeDate: 'il y a 2 jours', score: 16, max: 20, nclc: 8 }, + { id: 's-002', type: 'oral', relativeDate: 'il y a 5 jours', score: 14, max: 20, nclc: 7 }, + { id: 's-003', type: 'ecrit', relativeDate: 'il y a 1 semaine', score: 15, max: 20, nclc: 7 }, + ], + nextStep: { + title: 'Cible une simulation orale cette semaine', + body: 'Ton écrit est solide (NCLC 8). L\'oral reste à consolider pour sécuriser ton NCLC 9.', + action: { label: 'Démarrer Expression Orale', to: '/simulation/orale' }, + }, +} as const; +``` + +**Règles contenu :** +- Aucun "Lorem ipsum", aucune date absolue — relatif uniquement. +- Les prénoms mocks reflètent l'audience : Yacine, Aminata, Kenza, Bilal, Fatou, Kévin. +- Les scores suivent une progression crédible (pas de 20/20 ni de 5/20). + +--- + +## 8. Accessibilité — plancher + +- Contraste minimum **WCAG AA** sur tous les couples texte/fond (vérifié dark ET light). +- Tous les éléments interactifs ont un `:focus-visible` avec `--shadow-focus` (halo bleu 3px). +- Les icônes décoratives portent `aria-hidden="true"`. +- Les icônes fonctionnelles (sans label visible) portent `aria-label`. +- Les landmarks sémantiques : `
`, `