veza/RAPPORT_EXECUTION_TECHNIQUE_2025_02.md

464 lines
19 KiB
Markdown
Raw Normal View History

# 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.