# 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(Array(24).fill(0)); const analyserRef = useRef(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())}`); } };
setSearchQuery(e.target.value)} placeholder="What do you want to play?" // ... />
``` Temps estimé : 2–4 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 (
{Array.from({ length: 8 }).map((i) => ( ))}
); } ``` --- ## 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 | 2–4 h | Faible | | P1 | Intégrer fix-input-focus dans design system | Réduction dette | 2 h | Faible | | P2 | Découper client.ts (phases 1–3) | Maintenabilité | 2–3 j | Moyen | | P2 | fix-login-form → primitives | Réduction dette | 1 j | Moyen | | P3 | Waveform réactif (Web Audio API) | Polish player | 1–2 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.