19 KiB
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 :
- 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 :
- Extraire
ValidationMetricsTracker+ValidationAlerting→lib/validationMetrics.ts - Extraire
NetworkFailureTracker+ helpers →lib/networkFailureTracker.ts - Extraire retry logic →
lib/retry.ts - Extraire request interceptor →
interceptors/request.ts - Extraire response success →
interceptors/response.ts - Extraire error handler →
interceptors/error.ts - 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-visibleuniquement (clavier). - Cause : Composant
Input(input.tsx) utilisefocus-visible:ring-2etc. Les navigateurs/situations où:focus-visiblene 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 uniquementfocus-visible:. Ajouter dansindex.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 :
- Autofill :
-webkit-autofillgarde un fond jaune. Fix :box-shadow: insetpour masquer. - Flex shrink : Parents en
flexavecspace-y-*provoquentmin-width: 0sur enfants. Les inputs n'ont pasmin-w-0ouflex-shrink-0selon le besoin. - Largeur :
w-fullne suffit pas si parent amin-width: 0et flex.
- Autofill :
-
Solution : Intégrer dans les primitives du Design System :
- Autofill — Dans
index.cssoudesign-system.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);
}
}
- Input — Dans
components/ui/input.tsx, ajoutermin-w-0etflex-1si dans un flex :
className={cn(
"flex h-11 w-full min-w-0 rounded-xl ...",
// ...
)}
-
AuthInput — Vérifier qu'il a
w-full min-w-0et que le parent form utiliseflex flex-colavecspace-y-4sans conflit. -
Boutons submit — S'assurer que
AuthButtonaw-full min-w-0 flex-shrink-0. -
Supprimer
fix-login-form.cssune 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) :
// 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 :
- State : Créer
useHeaderSearch()ou utiliser un storeuiStore.searchQuery(optionnel). - Navigation : Sur submit (Enter) ou clic sur suggestion →
navigate(\/search?q=${encodeURIComponent(query)}`)`. - Composant : Remplacer l'input par un composant qui :
onKeyDown(Enter) → navigateonChange→ setQuery (pour suggestions futures)- Optionnel : dropdown avec suggestions (debounced search API)
Diff conceptuel :
// 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é : 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 :
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 :
// .eslintrc ou eslint.config.js
{
"plugins": ["tailwindcss"],
"rules": {
"tailwindcss/no-arbitrary-value": "warn"
}
}
Option B — Règle custom :
// 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.