# 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, 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 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` 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('/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 `. - **WebSocket T2 Live** : le JWT est envoyé en query string (`wss://api.expria.app/t2/live?token=`) — 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 |