expria-frontend/docs/ARCHITECTURE.md

688 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# ARCHITECTURE.md — Expria Frontend
> **Document de référence — Version 1.0**
> Ce document décrit l'architecture technique du frontend Expria.
> À lire conjointement avec `ONBOARDING.md`, `SECURITY.md`, et les documents de référence du backend (`expria-backend/docs/`).
>
> **Règle absolue :** toute décision architecturale majeure doit être documentée ici ou dans un ADR avant d'être implémentée.
---
## 1. Vue d'ensemble
Expria Frontend est une application web SaaS, construite comme un SPA (Single Page Application) pur. Elle ne contient **aucune logique métier** : elle affiche des données reçues du backend et relaie les actions de l'utilisateur vers ce même backend.
```
Utilisateur (navigateur)
┌─────────────────────────────┐
│ EXPRIA FRONTEND │
│ React 18 + Vite 5 │
│ TypeScript 5 │
│ Tailwind + shadcn/ui │
│ TanStack Query │
│ React Router v6 │
│ Cloudflare Pages (EU + Afrique) │
└─────────────┬───────────────┘
│ HTTPS + WebSocket
│ Bearer JWT Supabase
│ X-API-Version
┌─────────────▼───────────────┐
│ EXPRIA BACKEND │
│ Hono.js sur Render (Frankfurt) │
│ api.expria.app │
└──────┬──────────────┬───────┘
│ │
┌──────▼──────┐ ┌────▼────────────────┐
│ Supabase │ │ APIs externes │
│ Auth + DB │ │ (gérées par backend)│
└─────────────┘ └─────────────────────┘
```
Le frontend communique avec Supabase **uniquement pour l'authentification** (login, logout, register, récupération de token). Toute lecture et écriture de données métier passe exclusivement par le backend Hono.
---
## 2. Stack technique
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` |
**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
- ADR 004 : Duplication des types (pas de monorepo)
- ADR 005 : `hasAccess()` typé strict + `canSimulate()` séparé
- ADR 006 : Stack React 19 / Vite 8 / TypeScript 6 / Tailwind 4 / RR7
---
## 3. Structure des dossiers
```
expria-frontend/
├── docs/
│ ├── ARCHITECTURE.md ← ce document
│ ├── ONBOARDING.md ← guide dev
│ ├── SECURITY.md ← trous identifiés + patterns
│ ├── DEVELOPMENT_PRINCIPLES.md ← règles de dev (copie adaptée du backend)
│ ├── PLANS_TARIFAIRES.md ← copie miroir du backend (source de vérité)
│ ├── PARCOURS_UTILISATEURS.md ← copie miroir du backend
│ ├── GOLDEN_DATASET.md ← tests manuels frontend
│ ├── CHANGELOG.md ← historique des sessions
│ └── adr/
│ ├── 001-cloudflare-pages-vs-vercel.md
│ ├── 002-auth-api-decoupling.md
│ ├── 003-no-zustand.md
│ ├── 004-types-duplication.md
│ └── 005-has-access-typed-strict.md
├── src/
│ ├── app/ # CONFIGURATION ET ENTRY POINTS
│ │ ├── providers.tsx # QueryClientProvider + AuthProvider + Router
│ │ ├── router.tsx # Routes déclaratives
│ │ └── main.tsx # Entry point React
│ │
│ ├── entities/ # OBJETS MÉTIER (indépendants de l'UI)
│ │ ├── user/
│ │ │ ├── types.ts # Plan, UserAccess, UserProfile
│ │ │ ├── lib.ts # hasAccess(), canSimulate()
│ │ │ ├── access.ts # IDENTIQUE à expria-backend/src/lib/access.ts
│ │ │ ├── api.ts # GET /plans/status, POST /auth/verify-token
│ │ │ └── __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
│ │ │
│ │ └── report/
│ │ ├── types.ts # Report, Critere
│ │ ├── lib.ts # Logique de floutage selon plan
│ │ ├── api.ts # POST /corrections/ee, POST /corrections/eo
│ │ └── __tests__/
│ │ └── floutage.test.ts
│ │
│ ├── features/ # UI (composants + pages + hooks)
│ │ ├── auth/
│ │ │ ├── components/ # LoginForm, RegisterForm, ProtectedRoute
│ │ │ ├── pages/ # LoginPage, RegisterPage
│ │ │ └── hooks/ # useAuth
│ │ │
│ │ ├── dashboard/
│ │ │ ├── components/ # DashboardFreeView, DashboardStandardView, DashboardPremiumView
│ │ │ ├── pages/ # DashboardPage (orchestre les vues selon le plan)
│ │ │ └── hooks/ # usePlan
│ │ │
│ │ ├── simulations/
│ │ │ ├── components/ # SimulationForm, AudioRecorder, TimerExam
│ │ │ ├── pages/ # SimulationPage, RapportPage
│ │ │ └── hooks/ # useSimulation, useExamMode
│ │ │
│ │ ├── 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 → ...)
│ │ │
│ │ └── billing/
│ │ ├── components/ # PaymentSummary
│ │ ├── pages/ # PricingPage, CheckoutPage, UpgradePage
│ │ └── hooks/ # useStripeCheckout
│ │
│ ├── shared/ # CODE RÉUTILISABLE NON MÉTIER
│ │ ├── components/
│ │ │ ├── ui/ # Button, Modal, Badge (shadcn/ui)
│ │ │ ├── PaywallModal.tsx # Blocage + boutons upgrade
│ │ │ └── Spinner.tsx
│ │ ├── hooks/ # useDebounce, useLocalStorage
│ │ ├── 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
│ │ ├── types/
│ │ │ ├── api.ts # ApiResponse<T>, ApiError
│ │ │ └── common.ts # Types utilitaires
│ │ └── config/
│ │ └── env.ts # Validation des variables d'environnement au démarrage
│ │
│ └── index.tsx # Entry point DOM
├── .github/
│ └── workflows/
│ └── ci.yml # typecheck + vitest + npm audit
├── public/ # Assets statiques (favicon, images)
├── .env.example # Template sans valeurs
├── .gitignore
├── package.json
├── tsconfig.json
├── tailwind.config.ts
├── vite.config.ts
└── README.md
```
### Règles de dépendance entre dossiers
```
app/ peut importer de : entities/, features/, shared/
features/ peut importer de : entities/, shared/
entities/ peut importer de : shared/
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.
---
## 4. Flux de données
### Authentification
```
1. User submit login form (features/auth/components/LoginForm)
2. useAuth() hook → supabase.auth.signInWithPassword()
3. Supabase returns JWT → stocké par Supabase (cookie + localStorage)
4. Frontend redirige vers /dashboard
5. TanStack Query lance automatiquement usePlan() → GET /plans/status avec Bearer JWT
6. Backend vérifie le JWT (Supabase) + retourne le plan + permissions
7. Dashboard s'affiche selon le plan
```
### Appel API typique (lecture)
```
Composant React
→ hook TanStack Query (useQuery)
→ fonction dans entities/*/api.ts
→ apiFetch() dans shared/lib/api-client.ts
→ getAccessToken() dans shared/lib/auth-client.ts
→ fetch vers api.expria.app + Bearer token + X-API-Version
← succès : payload typé directement
erreur : throw d'une ApiError typée (catch par TanStack Query)
← data typée
← rendu selon hasAccess()
```
### Upgrade Stripe (mutation)
```
1. User clique "Passer en Standard" (features/billing/pages/PricingPage)
2. useStripeCheckout() → POST /stripe/checkout
3. Backend crée session Stripe + retourne URL
4. Frontend redirige full page vers Stripe Checkout
5. User paie sur Stripe
6. Stripe redirige vers /billing/success?session_id=xxx
7. Frontend invalide queryClient.invalidateQueries(['plan'])
8. usePlan() refetch → nouveau plan affiché
```
### T2 Live (WebSocket)
```
1. User clique "Démarrer le dialogue" (features/t2-live/pages/T2LivePage)
2. useT2LiveSession() → state machine passe en 'connecting'
3. ws-client.ts ouvre wss://api.expria.app/t2/live?token=<jwt>
(JWT en query string car les navigateurs ne permettent pas de headers custom sur WS init)
4. Backend vérifie JWT + plan 'premium' + ouvre connexion Gemini
- Si JWT invalide → close code 4001 (AUTH_REQUIRED) + message {"error":true,"code":"..."}
- Si plan insuffisant → close code 4003 (PLAN_INSUFFICIENT) + message
5. State machine → 'listening' (IA parle en premier)
6. audio.ts capture PCM 16kHz → envoi via WebSocket
7. État oscille 'listening' ↔ 'speaking' pendant tout le dialogue
8. Fin de dialogue → 'processing' → backend appelle Gemini REST pour rapport
9. State machine → 'ended' + rapport affiché
```
**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" |
---
## 5. Communication frontend ↔ backend
### Format des requêtes
Toutes les requêtes HTTP vers le backend incluent :
```typescript
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwtSupabase}`, // sauf /health
'X-API-Version': '1.0', // versioning obligatoire
},
signal: AbortSignal.timeout(timeoutMs), // 5s défaut, 30s pour corrections
}
```
### Format de réponse (matche le backend réel — audit du 2026-04-17)
**Le backend ne wrappe pas les réponses dans une enveloppe.** Il renvoie directement le payload métier en cas de succès, et un objet d'erreur structuré en cas d'échec.
**Succès :** le backend renvoie directement l'objet métier.
```typescript
// Exemple : GET /plans/status → HTTP 200
{
plan: 'standard',
permissions: { dashboard: true, exam_mode: false, ... },
simulations_used: 12,
simulations_remaining: null, // null si plan illimité, number sinon
plan_expires_at: '2026-05-01T00:00:00.000Z'
}
// Exemple : POST /simulations → HTTP 201
{
id: 'uuid',
tache: 'EE_T1',
mode: 'entrainement',
created_at: '2026-04-17T10:00:00.000Z'
}
```
**Erreur :** le backend renvoie un objet structuré avec `error: true`.
```typescript
// Format standard (toutes les erreurs sauf quelques quirks)
{
error: true,
code: 'QUOTA_REACHED',
message: 'Vous avez utilisé vos 5 simulations gratuites...'
}
// Quirk connu sur POST /simulations et POST /corrections/ee,eo
// Un champ 'status' duplique le code HTTP dans le body (effet de bord du c.json(result, result.status))
{
error: true,
code: 'QUOTA_REACHED',
message: '...',
status: 403
}
```
**Type côté frontend (dans `src/shared/types/api.ts`) :**
```typescript
// SOURCE OF TRUTH: expria-backend — format réel des erreurs, confirmé par audit 2026-04-17
/**
* Forme des erreurs renvoyées par le backend.
* Le champ `status` est présent sur certaines routes (simulations, corrections) —
* il duplique le code HTTP. Ignorable côté frontend, documenté pour référence.
*/
export interface ApiError {
error: true
code: ApiErrorCode
message: string
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
// 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
```
> **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 |
### Pattern `apiFetch<T>`
Le wrapper dans `src/shared/lib/api-client.ts` retourne directement le payload typé `T` en cas de succès, ou throw une `ApiError` en cas d'échec. Le pattern standard TanStack Query catch l'erreur et la rend disponible dans `error`.
```typescript
// Côté hook
const { data, error, isLoading } = useQuery({
queryKey: ['plan'],
queryFn: () => apiFetch<PlanStatus>('/plans/status'),
})
// error est de type ApiError | null
if (error) {
switch (error.code) {
case 'AUTH_REQUIRED': redirectToLogin(); break
case 'QUOTA_REACHED': openUpgradeModal(); break
default: showGenericErrorToast()
}
}
```
### Versioning API
Le header `X-API-Version: 1.0` est envoyé par le frontend sur toutes les requêtes. Pour l'instant, le backend ne le vérifie pas (cf. FTD-02 dans `TECH_DEBT.md` frontend). Quand le backend évoluera de façon breaking, il pourra commencer à vérifier ce header et retourner `HTTP 426 Upgrade Required` avec le code `API_VERSION_MISMATCH`.
Cette discipline est critique dès qu'un dev externe rejoint l'équipe — sans elle, une modification backend silencieuse peut casser le frontend en production.
### Retry et timeout
`api-client.ts` implémente :
- **Retry** : 3 tentatives avec backoff exponentiel (500ms, 1s, 2s) pour les erreurs réseau et les 5xx. Pas de retry sur les 4xx.
- **Timeout** : 5s par défaut, configurable par appel (30s pour `/corrections/ee` et `/corrections/eo` qui appellent DeepSeek, 10s pour les autres endpoints).
- **Logging** : chaque erreur est loggée via `shared/lib/logger.ts` avec le path, le code d'erreur, et le statut HTTP (jamais le payload pour éviter de fuiter des données utilisateur).
### Authentification
- **HTTP** : le JWT Supabase est envoyé dans le header `Authorization: Bearer <jwt>`.
- **WebSocket T2 Live** : le JWT est envoyé en query string (`wss://api.expria.app/t2/live?token=<jwt>`) — les navigateurs ne permettent pas d'ajouter des headers custom à l'initiation d'une connexion WebSocket. Voir SEC-13 dans `SECURITY.md` pour les implications de sécurité.
---
## 6. Permissions et plans
### Source de vérité unique
Le fichier `src/entities/user/access.ts` est une **copie bit-à-bit** de `expria-backend/src/lib/access.ts`. Cette règle est héritée d'`ARCHITECTURE.md` backend §10 Règle 2.
Contenu exact (confirmé par audit 2026-04-17) :
```typescript
// src/entities/user/access.ts
// SOURCE OF TRUTH: expria-backend/src/lib/access.ts
// Synchronisé le : 2026-04-17
export type Plan = 'free' | 'standard' | 'premium'
export type Feature =
| 'oral_t2_live'
| 'detailed_report'
| 'tips'
| 'dashboard'
| 'exam_mode'
| 'pattern_analysis'
| 'preparation_index'
| 'basic_report'
export const PLANS = {
free: {
simulations_lifetime: 5,
oral_t2_live: false,
detailed_report: false,
tips: false,
dashboard: false,
exam_mode: false,
pattern_analysis: false,
preparation_index: false,
},
standard: {
simulations_lifetime: null,
oral_t2_live: false,
detailed_report: true,
tips: true,
dashboard: true,
exam_mode: false,
pattern_analysis: false,
preparation_index: false,
},
premium: {
simulations_lifetime: null,
oral_t2_live: true,
detailed_report: true,
tips: true,
dashboard: true,
exam_mode: true,
pattern_analysis: true,
preparation_index: true,
},
}
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
Le fichier `src/entities/user/lib.ts` ré-exporte ces fonctions sous des noms standard React (cf. ADR 005) :
```typescript
// src/entities/user/lib.ts
import {
canUserSimulate,
checkFeatureAccess,
getPlanPermissions,
} from './access'
/**
* Alias frontend-idiomatique de checkFeatureAccess.
* Vérifie si un plan a accès à une feature booléenne donnée.
*/
export const hasAccess = checkFeatureAccess
/**
* Alias frontend-idiomatique de canUserSimulate.
* Signature ergonomique (plan, used) au lieu de ({ plan, simulations_used }).
*/
export function canSimulate(plan: Plan, simulationsUsed: number) {
return canUserSimulate({ plan, simulations_used: simulationsUsed })
}
export { getPlanPermissions }
```
### API obligatoire pour vérifier une permission
Voir ADR 005 pour le détail complet. Résumé :
```typescript
import { hasAccess, canSimulate } from '@/entities/user/lib'
// ✅ Permission booléenne
if (hasAccess(plan, 'oral_t2_live')) { ... }
// ✅ Quota de simulations
const { allowed, reason } = canSimulate(plan, simulationsUsed)
// ❌ INTERDIT
if (plan === 'premium') { ... }
if (PLANS[plan].exam_mode) { ... }
```
Le backend reste l'autorité finale : toute vérification côté frontend est de l'UX (afficher ou masquer un bouton). La vraie protection est dans les middlewares backend.
---
## 7. Variables d'environnement
### Frontend (.env à la racine)
```
VITE_API_URL=https://api.expria.app
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
```
### Règle absolue
**Aucune clé privée ne doit jamais exister dans le frontend.** Les variables suivantes n'existent que dans le backend :
- `SUPABASE_SERVICE_ROLE_KEY`
- `GEMINI_API_KEY`
- `DEEPSEEK_API_KEY`
- `STRIPE_SECRET_KEY`
- `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)
### Validation au démarrage
`src/shared/config/env.ts` valide la présence de toutes les variables `VITE_*` au démarrage de l'app. Si une variable manque, l'app refuse de démarrer avec un message clair — pas de bug silencieux 3h après.
---
## 8. Déploiement
### 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) | — |
### Workflow de déploiement
```
1. Développer sur branche feature (ex: feat/dashboard-conditionnel)
2. npm run typecheck (TypeScript strict)
3. npm run test (Vitest — tous verts)
4. npm run build (Vite)
5. Commit + push sur branche feature → GitHub Actions CI
6. PR vers main → revue (ou auto-merge pour le fondateur solo)
7. Merge sur main → auto-deploy Cloudflare Pages déclenché
8. Vérifier le déploiement sur https://expria.app
9. Rejouer le Golden Dataset groupe concerné
```
### Commande de déploiement manuel (fallback)
```bash
npm run build
npx wrangler pages deploy dist --project-name=expria
```
À n'utiliser qu'en cas de panne de l'auto-deploy GitHub → Cloudflare.
---
## 9. Tests
### Philosophie
Tests ciblés sur la logique critique, pas exhaustifs. On copie la stratégie backend (cf. `TESTS_AUTOMATISES.md` backend §11) : tester ce qui vérifie un droit, modifie des données, ou calcule un résultat métier.
### 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) |
### Fichiers non testés (par design)
- Composants d'affichage (`*.tsx` dans `features/*/components/`) : trop fragiles, couverts par le Golden Dataset manuel.
- Wrappers évidents (ex : `formatDate()`).
- Supabase SDK : c'est leur responsabilité.
### CI GitHub Actions
```yaml
# .github/workflows/ci.yml
- typecheck (tsc --noEmit)
- vitest run
- npm audit --audit-level=high
- semgrep scan (via plugin)
```
Un échec sur l'un de ces jobs bloque le merge.
---
## 10. Règles de développement
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 |