688 lines
27 KiB
Markdown
688 lines
27 KiB
Markdown
# 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 |
|