veza/RAPPORT_EXECUTION_TECHNIQUE_2025_02.md
senke 8a0f008345 chore: playwright workflow, docs, rapports audit, visual-tests, tmt unit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 22:19:34 +01:00

463 lines
19 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.

# Rapport d'Exécution Technique — Veza v1.0
**Date** : 2025-02-10
**Équipe** : Lead Frontend Architect (ex-Spotify), Design Engineer (ex-Discord), Performance Expert
**Objectif** : Valider, affiner et prioriser l'exécution des points de l'audit exhaustif pour atteindre une qualité "Premium Audio Platform".
---
## 1. Verdict Technique Immédiat — Fichiers Critiques
### 1.1 `client.ts` (2238 lignes)
| Critère | Verdict | Détail |
|---------|---------|--------|
| **Maintenabilité** | ⛔ Critique | Fichier monolithique. 5 classes, 2 interceptors, retry, refresh, validation, cache, offline, déduplication. Impossibilité de tester unitairement des parties isolées. |
| **Architecture** | ⚠️ Acceptable | Logique correcte (retry, refresh, CSRF, validation Zod). Mais tout dans un seul fichier. |
| **Risque** | Moyen | Refactoring sans changer les types exportés possible. Dépendances circulaires à éviter. |
**Pourquoi ça fait amateur :** Un fichier de 2200+ lignes suggère un manque de discipline architecturale. Les géants (Spotify, Discord) découpent : interceptors dans des modules, retry dans un utilitaire, refresh dans un service auth dédié.
**Comment font les géants :** Architecture en couches. `apiClient` = instance axios + composition d'interceptors. Chaque interceptor est un fichier < 150 lignes. Les helpers (retry, sanitize) sont des modules testables.
---
### 1.2 `AuthContext.tsx` (108 lignes)
| Critère | Verdict | Détail |
|---------|---------|--------|
| **Rôle** | Redondant | Duplique authStore + useUser. `authService` utilisé directement (authStore utilise `api/auth`). |
| **Usage app** | Aucun | App ne wrap pas avec `AuthContext.Provider`. Utilisé uniquement dans **Storybook** (decorators). |
| **Conflit** | Oui | 3 fichiers app utilisent `useAuth` de AuthContext : `ProfileView`, `useAuthView`, `useEditProfile`. Ces composants **crashent** si rendus hors Storybook (pas de Provider). |
**Bug confirmé :** `EditProfile` (Settings > Profile) utilise `useEditProfile``useAuth` (AuthContext). L'app n'a pas `AuthContext.Provider`. Donc **Settings > Profile tab = crash** en production.
**Pourquoi ça fait amateur :** Double source de vérité. Storybook fonctionne (AuthProvider), l'app non. Composants qui marchent en Storybook mais crashent en prod.
---
### 1.3 `authStore.ts` (328 lignes)
| Critère | Verdict | Détail |
|---------|---------|--------|
| **Design** | ✅ Solide | Zustand + persist + broadcastSync. Pas de `user` (délégué à React Query via `useUser`). |
| **API** | Propre | `login`, `register`, `logout`, `refreshUser`, `logoutLocal`. Types explicites. |
| **État** | Source principale | Utilisé par App, Header, Sidebar, LoginForm, ProtectedRoute, client.ts (401 → logoutLocal). |
**Points forts :** Gestion httpOnly cookies, refresh avec préservation d'état sur erreurs réseau, sync cross-tabs.
**Pourquoi c'est bien :** Une seule source pour `isAuthenticated`, `isLoading`, `error`. User via React Query (cache, déduplication).
---
### 1.4 `GlobalPlayer.tsx` (301 lignes)
| Critère | Verdict | Détail |
|---------|---------|--------|
| **UI** | ✅ Correcte | Barre glass, gradients, layout responsive. |
| **Waveform** | ❌ Décoratif | `Waveform` utilise `Math.random()` pour hauteurs. Pas de lecture audio. Animation `eq-bounce` décorative. |
| **Problèmes** | Variés | `z-[100]` arbitraire. `translate-y-[200%]` arbitraire. Waveform visible uniquement sur `xl`. |
**Pourquoi ça fait amateur :** Spotify/SoundCloud : waveform = visualisation réelle du signal audio. Veza : barres qui bougent au hasard, indépendantes du son.
**Comment font les géants :** Web Audio API (AnalyserNode) ou Wavesurfer.js pour extraire les données audio et dessiner le waveform. Le seek est possible en cliquant sur le waveform.
---
## 2. Analyse de la Dette Structurelle (Deep Dive)
### 2.1 Auth Dual-Source — Zones de conflit
| Fichier | Import | Comportement |
|---------|--------|--------------|
| `ProtectedRoute.tsx` | `useAuth` (features), `useAuthStore` | OK — useAuth = useAuthStore + useUser |
| `PublicRoute.tsx` | Idem | OK |
| `ProfileView.tsx` | `useAuth` (context) | ❌ Crashe hors Storybook |
| `useAuthView.ts` | `useAuth` (context) | ❌ AuthView utilisé en Storybook seulement |
| `useEditProfile.ts` | `useAuth` (context) | ❌ **EditProfile utilisé dans Settings → CRASH** |
**Plan de migration — Fichier par fichier :**
| Ordre | Fichier | Action |
|-------|---------|--------|
| 1 | `useEditProfile.ts` | Remplacer `import { useAuth } from '@/context/AuthContext'` par `import { useUser } from '@/features/auth/hooks/useUser'`. Utiliser `const { data: user } = useUser()`. |
| 2 | `ProfileView.tsx` | Idem. `const { data: currentUser } = useUser()` à la place de `useAuth()`. |
| 3 | `useAuthView.ts` | `useAuth()` appelé sans destructuring (juste pour vérifier contexte). Remplacer par `useAuthStore()` + `useUser()` si besoin, ou supprimer l'appel si inutile. |
| 4 | `StorybookDecorator` | Remplacer `AuthProvider` (context) par un mock `useAuthStore.setState` ou laisser QueryClient + MSW (useUser fonctionne avec MSW). |
| 5 | `AuthContext.tsx` | Supprimer le fichier. |
| 6 | `AuthContext.test.tsx` | Supprimer ou migrer vers tests authStore. |
**Diff conceptuel pour useEditProfile :**
```diff
- import { useAuth } from '@/context/AuthContext';
+ import { useUser } from '@/features/auth/hooks/useUser';
export function useEditProfile() {
- const { user } = useAuth();
+ const { data: user } = useUser();
// ...
}
```
---
### 2.2 Client API Monolithique — Schéma de découpage
**Structure proposée :**
```
services/api/
├── client.ts # Instance axios + attachement interceptors (150 lignes max)
├── clientConfig.ts # API_TIMEOUTS, baseURL, création instance
├── interceptors/
│ ├── request.ts # CSRF, X-API-Version, validation request (120 lignes)
│ ├── response.ts # Unwrap, validation response, cache, rate limit (200 lignes)
│ └── error.ts # Retry, refresh, 401, toast, offline (250 lignes)
├── lib/
│ ├── validationMetrics.ts # ValidationMetricsTracker, ValidationAlerting
│ ├── networkFailureTracker.ts
│ ├── retry.ts # isRetryableError, getRetryDelay, exponential backoff
│ └── sanitize.ts # sanitizeForLogging, getRequestId
├── auth/
│ ├── refreshQueue.ts # failedQueue, processQueue, isRefreshing
│ └── index.ts
└── index.ts # Re-exports apiClient, deduplicatedApiClient
```
**Garantie types :** `apiClient` reste exporté avec la même signature. Les interceptors sont attachés dans `client.ts`. Les types générés (`ApiResponse`, etc.) ne changent pas.
**Ordre de migration :**
1. Extraire `ValidationMetricsTracker` + `ValidationAlerting``lib/validationMetrics.ts`
2. Extraire `NetworkFailureTracker` + helpers → `lib/networkFailureTracker.ts`
3. Extraire retry logic → `lib/retry.ts`
4. Extraire request interceptor → `interceptors/request.ts`
5. Extraire response success → `interceptors/response.ts`
6. Extraire error handler → `interceptors/error.ts`
7. Réduire `client.ts` à l'assemblage.
---
### 2.3 Correctifs CSS — Analyse et intégration
**fix-input-focus.css :**
- **Problème :** Tailwind `focus:` applique ring/border sur tous les inputs (souris + clavier). Souhait : `focus-visible` uniquement (clavier).
- **Cause :** Composant `Input` (input.tsx) utilise `focus-visible:ring-2` etc. Les navigateurs/situations où `:focus-visible` ne s'applique pas correctement sont comblés par le fix.
- **Solution :** Intégrer dans le composant Input. Ne pas utiliser `focus:` dans les classes Tailwind. Utiliser uniquement `focus-visible:`. Ajouter dans `index.css` :
```css
/* Design System: focus only for keyboard (focus-visible) */
@layer base {
input:focus:not(:focus-visible),
textarea:focus:not(:focus-visible),
select:focus:not(:focus-visible) {
outline: none;
--tw-ring-width: 0;
}
}
```
Puis supprimer `fix-input-focus.css`.
**fix-login-form.css (391 lignes) :**
- **Problème :** Formulaires (login, register) ont des inputs/boutons qui se rétrécissent en flex, des largeurs incorrectes, autofill jaune.
- **Cause racine :**
1. **Autofill** : `-webkit-autofill` garde un fond jaune. Fix : `box-shadow: inset` pour masquer.
2. **Flex shrink** : Parents en `flex` avec `space-y-*` provoquent `min-width: 0` sur enfants. Les inputs n'ont pas `min-w-0` ou `flex-shrink-0` selon le besoin.
3. **Largeur** : `w-full` ne suffit pas si parent a `min-width: 0` et flex.
- **Solution :** Intégrer dans les primitives du Design System :
1. **Autofill** — Dans `index.css` ou `design-system.css` :
```css
@layer base {
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 1000px var(--background) inset;
-webkit-text-fill-color: var(--foreground);
}
}
```
2. **Input** — Dans `components/ui/input.tsx`, ajouter `min-w-0` et `flex-1` si dans un flex :
```tsx
className={cn(
"flex h-11 w-full min-w-0 rounded-xl ...",
// ...
)}
```
3. **AuthInput** — Vérifier qu'il a `w-full min-w-0` et que le parent form utilise `flex flex-col` avec `space-y-4` sans conflit.
4. **Boutons submit** — S'assurer que `AuthButton` a `w-full min-w-0 flex-shrink-0`.
5. Supprimer `fix-login-form.css` une fois les primitives corrigées.
---
## 3. Analyse "Perception Premium" (UI/UX)
### 3.1 Anatomie du Player — Waveform réactif
**État actuel :** 24 barres avec `height: 20 + Math.random() * 80` et animation CSS. Aucun lien avec l'audio.
**Stack technique pour waveform réactif :**
| Option | Complexité | Qualité | Commentaire |
|--------|------------|---------|-------------|
| **Web Audio API** | Moyenne | Élevée | AnalyserNode + getByteFrequencyData. Barres = niveaux de fréquence. |
| **Wavesurfer.js** | Faible | Très élevée | Waveform canvas, seek intégré. Dépendance ~50kb. |
| **Peaks.js (BBC)** | Moyenne | Élevée | Pré-calcul server-side. Nécessite backend. |
**Recommandation :** **Wavesurfer.js** pour le player expanded (vue détaillée). Pour la barre compacte, garder un EQ animé (Web Audio API) plus léger.
**Implémentation Web Audio API (EQ bars) :**
```tsx
// useAudioAnalyser.ts
const useAudioAnalyser = (audioElement: HTMLAudioElement | null, playing: boolean) => {
const [levels, setLevels] = useState<number[]>(Array(24).fill(0));
const analyserRef = useRef<AnalyserNode | null>(null);
useEffect(() => {
if (!audioElement) return;
const ctx = new AudioContext();
const source = ctx.createMediaElementSource(audioElement);
const analyser = ctx.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
analyser.connect(ctx.destination);
analyserRef.current = analyser;
const data = new Uint8Array(analyser.frequencyBinCount);
let raf: number;
const update = () => {
if (!analyserRef.current || !playing) return;
analyserRef.current.getByteFrequencyData(data);
const step = Math.floor(data.length / 24);
const newLevels = Array.from({ length: 24 }, (_, i) => data[i * step] / 255);
setLevels(newLevels);
raf = requestAnimationFrame(update);
};
if (playing) update();
return () => cancelAnimationFrame(raf);
}, [audioElement, playing]);
return levels;
};
```
---
### 3.2 Recherche Header — Plan d'implémentation
**État actuel :** Input avec placeholder "What do you want to play?" sans `onChange` ni navigation.
**Plan :**
1. **State** : Créer `useHeaderSearch()` ou utiliser un store `uiStore.searchQuery` (optionnel).
2. **Navigation** : Sur submit (Enter) ou clic sur suggestion → `navigate(\`/search?q=${encodeURIComponent(query)}\`)`.
3. **Composant** : Remplacer l'input par un composant qui :
- `onKeyDown` (Enter) → navigate
- `onChange` → setQuery (pour suggestions futures)
- Optionnel : dropdown avec suggestions (debounced search API)
**Diff conceptuel :**
```tsx
// Header.tsx
const [searchQuery, setSearchQuery] = useState('');
const navigate = useNavigate();
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {
navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`);
}
};
<form onSubmit={handleSearch} className="flex-1 max-w-md">
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="What do you want to play?"
// ...
/>
</form>
```
Temps estimé : 24 h.
---
### 3.3 Skeleton & Transitions — Routes sans LoadingState
| Route | Composant | État actuel | Skeleton proposé |
|-------|------------|-------------|-------------------|
| `/dashboard` | DashboardPage | Present | Déjà OK |
| `/library` | LibraryPage | Mix | LibraryPageSkeleton (grille cartes) |
| `/marketplace` | MarketplaceHome | Spinner générique | MarketplaceSkeleton (grille produits) |
| `/search` | SearchPage | SearchPageSkeleton | OK |
| `/playlists` | PlaylistRoutes | Variable | PlaylistGridSkeleton |
| `/sell` | SellerDashboardView | Spinner | SellerDashboardSkeleton (cards + table) |
| `/profile` | UserProfilePage | UserProfilePageSkeleton | OK |
| `/settings` | SettingsPage | SettingsPageSkeleton | OK |
| `/chat` | ChatPage | Variable | ChatSkeleton (sidebar + messages) |
**Structure type MarketplaceSkeleton :**
```tsx
export function MarketplaceSkeleton() {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{Array.from({ length: 8 }).map((i) => (
<Card key={i} className="overflow-hidden">
<Skeleton className="aspect-square w-full" />
<CardContent className="p-4">
<Skeleton className="h-5 w-3/4 mb-2" />
<Skeleton className="h-4 w-1/2" />
</CardContent>
</Card>
))}
</div>
);
}
```
---
## 4. Audit de Rigueur (Design System)
### 4.1 Fichiers avec valeurs arbitraires
Commande : `node scripts/report-arbitrary-values.mjs`
Résultat du grep manuel : **~40 fichiers** avec `w-[`, `h-[`, `gap-[`, `rounded-[`, etc.
Exemples critiques :
- `GlobalPlayer.tsx` : `z-[100]`, `translate-y-[200%]`, `duration-[var(--duration-normal)]`
- `PlayerControls.tsx` : `text-[8px]` (badge repeat)
- `ChatMessage.tsx`, `Header.tsx`, etc.
### 4.2 Règle ESLint pour bloquer les arbitraires
**Option A — eslint-plugin-tailwindcss :**
```json
// .eslintrc ou eslint.config.js
{
"plugins": ["tailwindcss"],
"rules": {
"tailwindcss/no-arbitrary-value": "warn"
}
}
```
**Option B — Règle custom :**
```js
// eslint-rules/no-arbitrary-tailwind.js
module.exports = {
meta: {
type: 'suggestion',
docs: { description: 'Disallow arbitrary values in Tailwind classes' },
schema: [],
},
create(context) {
const arbitraryPattern = /\b(w|h|gap|p|m|rounded|text|top|left|right|bottom|z)-\[[^\]]+\]/;
return {
JSXAttribute(node) {
if (node.name.name === 'className' && node.value?.type === 'Literal') {
const value = node.value.value || '';
if (arbitraryPattern.test(value)) {
context.report({
node: node.value,
message: 'Avoid arbitrary Tailwind values. Use design tokens or Tailwind scale.',
});
}
}
},
};
},
};
```
**Option C — Tailwind config (recommended) :**
Dans `tailwind.config.ts`, restreindre les valeurs arbitraires via `safelist` ou en n'utilisant que des classes prédéfinies. Approche plus stricte.
**Recommandation :** `eslint-plugin-tailwindcss` avec `no-arbitrary-value: "warn"` en premier. Migration progressive. Puis passer à `"error"` une fois les 40 fichiers corrigés.
---
## 5. Rapport d'Exécution Immédiate — Format "Pourquoi / Comment / Diff"
### 5.1 Recherche Header non fonctionnelle
| | |
|---|---|
| **Pourquoi ça fait amateur** | L'utilisateur voit une barre de recherche évidente (comme Spotify) mais rien ne se passe. Perception de feature incomplète. |
| **Comment font les géants** | Spotify : recherche globale, navigation vers /search, suggestions en temps réel. Discord : command palette Cmd+K. |
| **Diff conceptuel** | Voir section 3.2. Ajouter `value`, `onChange`, `onSubmit``navigate('/search?q=' + query)`. |
---
### 5.2 EditProfile crash (AuthContext)
| | |
|---|---|
| **Pourquoi ça fait amateur** | Un onglet Settings (Profile) qui crashe = bug critique. L'utilisateur ne peut pas éditer son profil. |
| **Comment font les géants** | Une seule source d'auth. Pas de Context qui n'existe que dans les tests. |
| **Diff conceptuel** | `useEditProfile` : remplacer `useAuth` (context) par `useUser` (React Query). 1 ligne modifiée. |
---
### 5.3 Waveform décoratif
| | |
|---|---|
| **Pourquoi ça fait amateur** | Les barres bougent au hasard. L'utilisateur ne voit pas le lien avec la musique. SoundCloud/Spotify : waveform = audio réel. |
| **Comment font les géants** | Web Audio API ou Wavesurfer. Visualisation = données audio réelles. |
| **Diff conceptuel** | Intégrer `useAudioAnalyser` avec `AnalyserNode`. Remplacer `Math.random()` par `levels[i]` du canal fréquentiel. |
---
### 5.4 fix-input-focus.css
| | |
|---|---|
| **Pourquoi ça fait amateur** | Un fichier "fix" indique que le design system n'a pas résolu le problème à la source. |
| **Comment font les géants** | Focus géré dans les composants. `:focus-visible` uniquement. Pas de patch global. |
| **Diff conceptuel** | Ajouter règle `@layer base` dans index.css. Supprimer fix-input-focus.css. Vérifier Input n'utilise pas `focus:` sans `visible`. |
---
### 5.5 fix-login-form.css (391 lignes)
| | |
|---|---|
| **Pourquoi ça fait amateur** | 391 lignes de correctifs avec `!important` = guerre de spécificité. Layout non maîtrisé. |
| **Comment font les géants** | Formulaires avec primitives correctes : `min-w-0`, `flex-1`, autofill géré en amont. |
| **Diff conceptuel** | Intégrer autofill dans design-system.css. Corriger Input, AuthInput, AuthButton avec `min-w-0`. Supprimer fix-login-form.css. |
---
## 6. Priorisation Exécution v1.0
| Priorité | Action | Impact | Effort | Risque |
|----------|--------|--------|--------|--------|
| P0 | Migrer useEditProfile, ProfileView, useAuthView vers useUser | Fix crash Settings | 2 h | Faible |
| P0 | Supprimer AuthContext, mettre à jour Storybook | Une source auth | 2 h | Moyen |
| P1 | Recherche Header fonctionnelle | UX immédiate | 24 h | Faible |
| P1 | Intégrer fix-input-focus dans design system | Réduction dette | 2 h | Faible |
| P2 | Découper client.ts (phases 13) | Maintenabilité | 23 j | Moyen |
| P2 | fix-login-form → primitives | Réduction dette | 1 j | Moyen |
| P3 | Waveform réactif (Web Audio API) | Polish player | 12 j | Moyen |
| P3 | Skeletons Marketplace, SellerDashboard | Perception loaded | 4 h | Faible |
| P4 | ESLint no-arbitrary-value | Rigueur long terme | 2 h | Faible |
---
**Prochaine étape recommandée :** Exécuter P0 (migration AuthContext → useUser) pour éliminer le crash critique avant toute autre action.