veza/docs/archive/root-md/RAPPORT_EXECUTION_TECHNIQUE_2025_02.md
senke 43af35fd93 chore(audit 2.2, 2.3): nettoyer .md et .json à la racine
- Archiver 131 .md dans docs/archive/root-md/
- Archiver 22 .json dans docs/archive/root-json/
- Conserver 7 .md utiles (README, CONTRIBUTING, CHANGELOG, etc.)
- Conserver package.json, package-lock.json, turbo.json
- Ajouter README d'index dans chaque archive
2026-02-15 14:35:08 +01:00

19 KiB
Raw Blame 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 useEditProfileuseAuth (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 :

  1. Extraire ValidationMetricsTracker + ValidationAlertinglib/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 :
/* 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 :
@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);
  }
}
  1. Input — Dans components/ui/input.tsx, ajouter min-w-0 et flex-1 si dans un flex :
className={cn(
  "flex h-11 w-full min-w-0 rounded-xl ...",
  // ...
)}
  1. 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.

  2. Boutons submit — S'assurer que AuthButton a w-full min-w-0 flex-shrink-0.

  3. 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) :

// 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 :

// 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 :

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, onSubmitnavigate('/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.