feat(release): v0.202 — Lots G, H, F, C, D
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
Frontend CI / test (push) Failing after 0s
Storybook Audit / Build & audit Storybook (push) Failing after 0s

- Lot G: Recherche avancée (musical_key, tri pertinence, autocomplete, facettes, historique)
- Lot H: Analytics créateur (stats, charts, completion rate, export CSV/JSON)
- Lot F: Seller dashboard (GET /sell/stats, liste produits)
- Lot C: Player (crossfade, gapless preload, PiP)
- Lot D2: Autoplay (GET /tracks/recommendations, section À écouter ensuite)

Backend: GetRecommendations handler, route /tracks/recommendations
Frontend: PlayerQueue recommendations, fix TS errors (GlobalPlayer, AnalyticsViewKpiGrid, etc.)
Docs: FEATURE_STATUS, PROJECT_STATE, CHANGELOG, SCOPE_CONTROL
This commit is contained in:
senke 2026-02-20 18:16:17 +01:00
parent 2424986ebf
commit ede3546f4b
20 changed files with 508 additions and 72 deletions

View file

@ -1,10 +1,10 @@
# Règles de Développement UI - Projet SaaS
## 0. Scope v0.201 (priorité absolue)
## 0. Scope v0.202 (priorité absolue)
- **Référence** : `docs/V0_201_RELEASE_SCOPE.md` et `docs/SCOPE_CONTROL.md`
- Avant toute modification : vérifier si le changement est **dans le scope v0.201**
- **Autorisé v0.201** : lots E, F, G, H, C, D (Métadonnées, Seller, Recherche avancée, Analytics créateur, Player, Queue)
- **Référence** : `docs/V0_202_RELEASE_SCOPE.md` et `docs/SCOPE_CONTROL.md`
- Avant toute modification : vérifier si le changement est **dans le scope v0.202**
- **Autorisé v0.202** : lots G, H, F, C, D (Recherche avancée, Analytics créateur, Seller, Player, Queue)
- **Interdit** : nouvelles routes/pages hors scope, nouvelles dépendances (sauf correctif sécurité)
- En cas de doute : ne pas ajouter. Créer une issue pour une version ultérieure.

View file

@ -1,5 +1,46 @@
# Changelog - Veza
## [v0.202] - 2026-02-20
### Added
- **Lot G — Recherche avancée**
- Filtre musical_key dans track_search (G1)
- Tri pertinence (relevance) dans SearchService (G2)
- Autocomplete : GET /search/suggestions, dropdown debounced (G3)
- Facettes type (tracks/artistes/playlists/users) dans SearchPage (G4)
- Historique recherche localStorage (G5)
- **Lot H — Analytics créateur**
- GET /analytics/creator/stats, carte Completion Rate (H1)
- GET /analytics/creator/charts, graphiques (H2)
- Taux de complétion intégré dashboard (H3)
- GET /analytics/creator/export CSV/JSON (H4)
- **Lot F — Seller dashboard**
- GET /sell/stats, connexion commerceService (F1)
- Support seller_id=me dans ListProducts (F2)
- **Lot C — Player avancé**
- Crossfade configurable (112 s) depuis Settings (C1)
- Gapless préchargement via preloadTrack (C2)
- PiP (Picture-in-Picture) si supporté (C3)
- **Lot D — Autoplay**
- GET /tracks/recommendations (auth), section « À écouter ensuite » dans PlayerQueue (D2)
### Changed
- SearchPage : onglets type, suggestions dropdown, historique récent
- AnalyticsViewKpiGrid : métrique Completion Rate
- AnalyticsViewChart : graphiques creator
- SettingsPage : slider crossfade
- PlayerQueue : recommandations quand queue vide (authentifié)
- PlayerStore : crossfadeSeconds, préchargement ~5 s avant fin
### Documented
- D1 (queue collaborative) reporté v0.203+
- V0_202_RELEASE_SCOPE.md, FEATURE_STATUS.md, PROJECT_STATE.md mis à jour
---
## [v0.201] - 2026-02-20
### Added

View file

@ -63,7 +63,7 @@ export const Default: Story = {
export const Loading: Story = {
name: 'Loading',
decorators: [
(Story) => {
(_Story) => {
usePlayerStore.setState({ queue: [], currentIndex: -1, currentTrack: null });
return (
<div className="bg-background min-h-screen p-4 max-w-4xl mx-auto space-y-6">

View file

@ -202,8 +202,8 @@ export const QueueView: React.FC = () => {
>
{upNext.map((track, i) => (
<QueueSortableItem
key={sortableIds[i]}
id={sortableIds[i]}
key={sortableIds[i] ?? track.id}
id={sortableIds[i] ?? track.id}
track={track}
onPlay={playTrack}
onRemove={() => removeFromQueue(currentIndex + 1 + i)}

View file

@ -39,15 +39,14 @@ export function useEditProfile() {
first_name: p.first_name || '',
last_name: p.last_name || '',
bio: p.bio || '',
banner_url: p.banner_url || '',
banner_url: (p as { banner_url?: string }).banner_url ?? (p as { banner?: string }).banner ?? '',
location: p.location || '',
gender: p.gender || 'Prefer not to say',
birthdate: p.birthdate || '',
});
if (p.avatar_url) setAvatar(p.avatar_url);
if (p.banner_url) {
setBanner(p.banner_url);
}
const bannerUrl = (p as { banner_url?: string }).banner_url ?? (p as { banner?: string }).banner;
if (bannerUrl) setBanner(bannerUrl);
} catch (e) {
logger.error('Failed to load profile settings', {
error: e instanceof Error ? e.message : String(e),

View file

@ -49,7 +49,7 @@ export function AnalyticsViewKpiGrid({ stats }: AnalyticsViewKpiGridProps) {
: '—'
}
icon={<Target className="w-4 h-4" />}
color="green"
color="lime"
/>
</div>
);

View file

@ -1,6 +1,5 @@
import { useState, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { PictureInPicture2 } from 'lucide-react';
import { usePlayer } from '@/features/player/hooks/usePlayer';
import { usePictureInPicture } from '@/features/player/hooks/usePictureInPicture';
import { useKeyboardShortcuts } from '@/features/player/hooks/useKeyboardShortcuts';
@ -50,8 +49,8 @@ export function GlobalPlayer() {
const displayTrack = currentTrack || IDLE_TRACK;
const isIdle = !currentTrack;
const { setVideoRef: setPiPVideoRef, togglePiP, isPiPActive, isPiPSupported } = usePictureInPicture(
currentTrack?.cover_art_path ?? null,
const { setVideoRef: setPiPVideoRef, togglePiP, isPiPActive, isSupported: isPiPSupported } = usePictureInPicture(
currentTrack?.cover ?? null,
);
useMediaSession({
@ -72,7 +71,7 @@ export function GlobalPlayer() {
className="hidden w-0 h-0"
muted
playsInline
poster={currentTrack?.cover_art_path ?? undefined}
poster={currentTrack?.cover ?? undefined}
/>
)}

View file

@ -1,7 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useEffect } from 'react';
import { PlayerQueue } from './PlayerQueue';
import { usePlayerStore } from '../store/playerStore';
import { useEffect } from 'react';
import { useAuthStore } from '@/features/auth/store/authStore';
const meta: Meta<typeof PlayerQueue> = {
title: 'Components/Features/Player/PlayerQueue',
@ -65,6 +66,14 @@ export const Default: Story = {
],
};
const AuthInitializer = ({ authenticated }: { authenticated: boolean }) => {
useEffect(() => {
useAuthStore.setState({ isAuthenticated: authenticated });
return () => useAuthStore.setState({ isAuthenticated: false });
}, [authenticated]);
return null;
};
export const Empty: Story = {
args: {
isOpen: true,
@ -80,3 +89,20 @@ export const Empty: Story = {
),
],
};
export const EmptyWithRecommendations: Story = {
args: {
isOpen: true,
},
decorators: [
(Story) => (
<>
<AuthInitializer authenticated />
<StoreInitializer tracks={[]} />
<div className="h-[600px] w-full relative bg-background overflow-hidden">
<Story />
</div>
</>
),
],
};

View file

@ -1,10 +1,16 @@
import { usePlayerStore } from '../store/playerStore';
import { useUIStore } from '@/stores/ui';
import { cn } from '@/lib/utils';
import { X, GripVertical, ListMusic } from 'lucide-react';
import { X, GripVertical, ListMusic, Sparkles } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { EmptyState } from '@/components/ui/empty-state';
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '@/features/auth/store/authStore';
import { tracksApi } from '@/services/api/tracks';
import { getHLSMasterPlaylistURL } from '@/features/streaming/services/hlsService';
import type { Track as PlayerTrack } from '../types';
import type { Track as ApiTrack } from '@/features/tracks/types/track';
interface PlayerQueueProps {
isOpen: boolean;
@ -13,9 +19,31 @@ interface PlayerQueueProps {
onPlay: (track: any) => void;
}
function mapApiTrackToPlayerTrack(t: ApiTrack): PlayerTrack {
const apiTrack = t as ApiTrack & { stream_manifest_url?: string; cover_art_path?: string };
return {
id: t.id,
title: t.title,
artist: t.artist,
duration: t.duration ?? 0,
url: apiTrack.stream_manifest_url ?? getHLSMasterPlaylistURL(t.id),
cover: apiTrack.cover_art_path,
genre: t.genre,
like_count: t.like_count,
};
}
export function PlayerQueue({ isOpen, onClose, onPlay }: PlayerQueueProps) {
const { queue, currentIndex, removeFromQueue, clearQueue } = usePlayerStore();
const { sidebarOpen } = useUIStore();
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const { data: recommendations = [], isLoading: recommendationsLoading } = useQuery({
queryKey: ['track-recommendations'],
queryFn: () => tracksApi.getRecommendations({ limit: 10 }),
enabled: isOpen && isAuthenticated && queue.length === 0,
staleTime: 60_000,
});
const playerTracks = recommendations.map(mapApiTrackToPlayerTrack);
if (!isOpen) return null;
@ -56,6 +84,35 @@ export function PlayerQueue({ isOpen, onClose, onPlay }: PlayerQueueProps) {
{/* Content */}
<div className="flex-1 overflow-hidden relative">
{queue.length === 0 ? (
<div className="flex flex-col gap-4 p-4">
{playerTracks.length > 0 ? (
<>
<h4 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Sparkles className="w-4 h-4 text-primary" />
À écouter ensuite
</h4>
<ScrollArea className="max-h-layout-list">
<div className="space-y-1">
{playerTracks.map((track) => (
<div
key={track.id}
className="group flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 transition-colors cursor-pointer border border-transparent hover:border-white/5"
onClick={() => onPlay(track)}
>
<div className="w-6 text-center text-xs font-mono text-muted-foreground"></div>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium truncate text-foreground">{track.title}</h4>
<p className="text-xs text-muted-foreground truncate">{track.artist}</p>
</div>
</div>
))}
</div>
</ScrollArea>
<p className="text-xs text-muted-foreground">Cliquez pour lire</p>
</>
) : recommendationsLoading ? (
<div className="flex items-center justify-center py-8 text-muted-foreground text-sm">Chargement des suggestions</div>
) : (
<EmptyState
icon={<ListMusic className="w-full h-full" />}
title="Your queue is empty"
@ -63,6 +120,8 @@ export function PlayerQueue({ isOpen, onClose, onPlay }: PlayerQueueProps) {
size="sm"
className="border-0 shadow-none bg-transparent"
/>
)}
</div>
) : (
<ScrollArea className="h-full max-h-layout-list">
<div className="p-2 space-y-1">

View file

@ -9,7 +9,7 @@ interface SearchPageDiscoveryProps {
export function SearchPageDiscovery({ onQuerySelect }: SearchPageDiscoveryProps) {
const { getHistory, clearHistory } = useSearchHistory();
const [refreshKey, setRefreshKey] = useState(0);
const [, setRefreshKey] = useState(0);
const history = getHistory();
const handleClear = () => {

View file

@ -300,6 +300,24 @@ export async function uploadTrack(
* @returns La liste des tracks avec les métadonnées de pagination
* @throws Error si la requête échoue
*/
/**
* Get personalized track recommendations (D2 autoplay)
* Backend: GET /api/v1/tracks/recommendations (auth required)
*/
export async function getTrackRecommendations(
options?: { limit?: number; seedTrackId?: string },
): Promise<Track[]> {
const limit = Math.min(Math.max(options?.limit ?? 20, 1), 100);
const params = new URLSearchParams({ limit: String(limit) });
if (options?.seedTrackId) {
params.set('seed_track_id', options.seedTrackId);
}
const { data } = await apiClient.get<{ tracks: Track[] }>(
`/tracks/recommendations?${params.toString()}`,
);
return data?.tracks ?? [];
}
export async function getTracks(
page: number = 1,
limit: number = 20,

View file

@ -2,4 +2,6 @@ import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers);
export const worker = setupWorker(
...(handlers.filter((h): h is NonNullable<typeof h> => Boolean(h))),
);

View file

@ -53,6 +53,21 @@ export const handlersTracks = [
});
}),
http.get('*/api/v1/tracks/recommendations', ({ request }) => {
const url = new URL(request.url);
const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') || '20', 10), 1), 100);
const tracks = Array.from({ length: Math.min(limit, 5) }, (_, i) =>
mockTrack({
id: `rec-${i + 1}`,
title: `Recommended Track ${i + 1}`,
artist: 'Suggested Artist',
duration: 200 + i * 10,
cover_art_path: 'https://picsum.photos/200',
}),
);
return HttpResponse.json({ success: true, data: { tracks } });
}),
http.get('*/api/v1/tracks/suggested-tags', ({ request }) => {
const url = new URL(request.url);
const genre = url.searchParams.get('genre')?.toLowerCase() || 'default';

View file

@ -8,6 +8,7 @@
import {
uploadTrack,
getTracks,
getTrackRecommendations,
updateTrack,
getTrackStats,
getTrackHistory,
@ -54,6 +55,11 @@ export const tracksApi = {
*/
get: getTrack,
/**
* Get personalized track recommendations (D2 autoplay)
*/
getRecommendations: getTrackRecommendations,
/**
* Create/upload a new track
*/

View file

@ -1,6 +1,6 @@
# Statut des fonctionnalités — Veza
**Dernière mise à jour** : février 2026 — v0.201 (Lot E Métadonnées : BPM, key, lyrics, tags)
**Dernière mise à jour** : février 2026 — v0.202 livrée
Ce document décrit le statut réel des fonctionnalités par rapport au code.
@ -17,14 +17,14 @@ Ce document décrit le statut réel des fonctionnalités par rapport au code.
| Playlists (CRUD, collaboration) | Oui | Oui | Complet |
| Chat WebSocket | Oui | Oui | Complet (Chat Server doit être démarré) |
| Dashboard | Oui | Oui | GET /api/v1/dashboard |
| Recherche | Oui | Oui | GET /api/v1/search unifié + endpoints par ressource |
| Recherche | Oui | Oui | GET /search unifié, GET /tracks/search. v0.202 : musical_key, tri pertinence, autocomplete /suggestions, facettes type, historique localStorage |
| Social (feed, posts, groups, follows, blocks) | Oui | Oui | Complet |
| Administration | Oui | Oui | Complet |
| Marketplace | Oui | Oui | Complet (Hyperswitch) |
| Webhooks | Oui | Oui | Complet |
| Inventory / Gear | Oui | Oui | GET/POST/PUT/DELETE /api/v1/inventory/gear |
| Live Streaming (métadonnées) | Oui | Oui | GET /api/v1/live/streams — stream vidéo via Stream Server |
| Analytics | Oui | Oui | Routes /api/v1/analytics/* |
| Analytics | Oui | Oui | Routes /api/v1/analytics/*. v0.202 : creator stats/charts/export (Lot H) |
| Roles | Oui | Oui | Assign, revoke — flag ROLE_MANAGEMENT |
| Notifications | Oui | Oui | Création auto follow/like/comment |
@ -50,10 +50,29 @@ Ce document décrit le statut réel des fonctionnalités par rapport au code.
| Feature | Limitation | Version cible |
|---------|------------|--------------|
| **Go Live** (streaming vidéo) | Non implémenté — toast « coming soon » conservé | v0.703 |
| **Social Trending** (tags tendance) | SocialViewTrending utilise tags statiques ; pas d'API `/social/trending` | v0.103 |
| **Social Trending** (tags tendance) | SocialViewTrending utilise tags statiques ; pas d'API `/social/trending` | v0.203 |
| **2FA SMS** | Option « Envoyer par SMS » pendant la vérification 2FA — requiert infra Twilio + users.phone_number | v0.104 |
| **Passkeys / WebAuthn** | Login sans mot de passe — requiert go-webauthn, table webauthn_credentials, frontend navigator.credentials | v0.104 |
## Livré en v0.202
| Lot | Feature |
|-----|---------|
| G | Recherche avancée (musical_key, tri pertinence, autocomplete, facettes type, historique) |
| H | Analytics créateur (stats, graphiques, taux complétion, export CSV/JSON) |
| F | Seller dashboard (GET /sell/stats, liste produits marketplace) |
| C | Player (crossfade, gapless preload, PiP) |
| D | Autoplay (GET /tracks/recommendations, section « À écouter ensuite » dans PlayerQueue) |
## Prochaines fonctionnalités v0.203+
| Feature | Priorité |
|---------|----------|
| Queue collaborative (D1) | Basse |
| Recherche phonétique, booléenne | v0.203+ |
---
## Projets abandonnés
| Projet | Statut |

View file

@ -8,10 +8,10 @@
| Élément | Valeur |
|---------|--------|
| **Dernier tag** | v0.201 |
| **Branche release** | `release/v0.201` (taguée) |
| **Phase** | Phase 2 Contenu — Lot E livré |
| **Prochaine version** | v0.202 (Lot G, H, F, C, D) |
| **Dernier tag** | v0.202 |
| **Branche release** | `main` (v0.202 mergée) |
| **Phase** | Phase 2 Contenu — Lots G, H, F, C, D livrés |
| **Prochaine version** | v0.203 |
---
@ -26,28 +26,25 @@
- ✅ Lot E — Métadonnées : BPM, musical_key, lyrics, tags (E1E4)
- Migrations : 084 track_lyrics, 085 tracks.tags
### Non livré (reporté v0.202)
- Lot G : Recherche avancée (filtres, tri, autocomplete, facettes)
- Lot H : Analytics créateur (stats, graphiques, export)
- Lot F : Seller dashboard (stats ventes, liste produits)
- Lot C : Player (crossfade, gapless, PiP)
- Lot D : Queue collaborative, Autoplay
### v0.202 (Phase 2 Contenu — Lots G, H, F, C, D)
- Lot G : Recherche avancée (musical_key, tri pertinence, autocomplete, facettes type, historique)
- Lot H : Analytics créateur (stats, graphiques, taux complétion, export CSV/JSON)
- Lot F : Seller dashboard (GET /sell/stats, liste produits marketplace)
- Lot C : Player (crossfade, gapless preload, PiP)
- Lot D : Autoplay (GET /tracks/recommendations, section « À écouter ensuite »)
---
## 3. Prochaines étapes
### Immédiat (préparation v0.202)
1. **Créer la branche** : `release/v0.202`
2. **Mettre à jour SCOPE_CONTROL** : référence v0.202
3. **Lire** [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) — lots G, H, F, C, D
### Immédiat (préparation v0.203)
1. **Créer la branche** : `git checkout -b release/v0.203`
2. **Définir** le scope dans V0_203_RELEASE_SCOPE.md
3. **PR** : `gh pr create --base main --head release/v0.203 --title "Release v0.203"`
### Ordre d'implémentation recommandé (v0.202)
1. **Lot G** (Recherche avancée) — filtres, tri, autocomplete, facettes
2. **Lot H** (Analytics créateur) — stats, graphiques, export
3. **Lot F** (Seller dashboard) — stats vendeur, liste produits
4. **Lot C** (Player) — crossfade, gapless, PiP
5. **Lot D** (Queue) — autoplay, queue collaborative
### Cibles v0.203+
- Queue collaborative (D1)
- Recherche phonétique, booléenne
---
@ -55,7 +52,7 @@
| Document | Usage |
|----------|-------|
| [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) | Scope détaillé v0.201 |
| [V0_202_RELEASE_SCOPE.md](V0_202_RELEASE_SCOPE.md) | Scope détaillé v0.202 |
| [SCOPE_CONTROL.md](SCOPE_CONTROL.md) | Anti-scope-creep, workflow |
| [FEATURE_STATUS.md](FEATURE_STATUS.md) | Statut des features par domaine |
| [CHANGELOG.md](../CHANGELOG.md) | Historique des versions |
@ -79,5 +76,4 @@
| Métrique | Valeur |
|----------|--------|
| Features livrées (cumul) | ~270 / 600 |
| Cible v0.201 | ~330 / 600 |
| Features livrées (cumul) | ~345 / 600 |

View file

@ -1,19 +1,19 @@
# Contrôle du scope — Anti-scope-creep
**Objectif** : Éviter toute dérive de scope. Chaque modification doit être intentionnelle et traçable.
**Référence active** : [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md)
**Version précédente** : [V0_103_RELEASE_SCOPE.md](V0_103_RELEASE_SCOPE.md)
**Référence active** : [V0_202_RELEASE_SCOPE.md](V0_202_RELEASE_SCOPE.md)
**Version précédente** : [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md)
---
## 1. Règle d'or
> **Avant d'ajouter quoi que ce soit : vérifier si c'est dans le scope v0.201.**
> **Avant d'ajouter quoi que ce soit : vérifier si c'est dans le scope v0.202.**
> Si non → ne pas ajouter. Créer un ticket pour une version ultérieure.
---
## 2. Pendant la phase v0.201 (jusqu'au tag)
## 2. Pendant la phase v0.202 (jusqu'au tag)
### 2.1 Autorisé
@ -26,7 +26,7 @@
### 2.2 Interdit
- **Nouvelles features** hors scope v0.201
- **Nouvelles features** hors scope v0.202
- **Nouvelles routes** ou pages hors scope
- **Nouvelles dépendances** (sauf correctif sécurité)
- **Changements de comportement** sur les features HORS SCOPE
@ -52,7 +52,7 @@
- Non → **STOP.** Est-ce une correction de bug ? Si oui, la feature est-elle IN SCOPE ?
2. **Mon changement ajoute-t-il du code ?**
- Nouvelle route, nouveau composant, nouveau service → **STOP.** Hors scope v0.101.
- Nouvelle route, nouveau composant, nouveau service → **STOP.** Hors scope v0.202.
- Correction, refactoring, test → OK si lié à une feature IN SCOPE.
3. **Mes tests passent-ils ?**
@ -81,7 +81,7 @@ Format : `type(scope): description`
Dans chaque PR, le relecteur doit valider :
- [ ] Le changement est dans le scope v0.201 (voir [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md))
- [ ] Le changement est dans le scope v0.202 (voir [V0_202_RELEASE_SCOPE.md](V0_202_RELEASE_SCOPE.md))
- [ ] Aucune nouvelle feature ajoutée
- [ ] Aucune régression sur les flows critiques
- [ ] Les tests passent
@ -98,22 +98,22 @@ Une PR sera rejetée si :
---
## 5. Proposer une feature pour APRÈS v0.101
## 5. Proposer une feature pour APRÈS v0.202
### 5.1 Template
Utiliser le template [Feature request](.github/ISSUE_TEMPLATE/feature_request.md) avec :
- **Alignement scope** : cocher "Hors scope v0.201 — pour v0.202+"
- **Alignement scope** : cocher "Hors scope v0.202 — pour v0.203+"
- **Justification** : pourquoi cette feature est nécessaire
- **Effort estimé** : S / M / L / XL
- **Dépendances** : quelles features v0.101 doivent être stables avant
- **Dépendances** : quelles features v0.202 doivent être stables avant
### 5.2 Workflow
1. Créer une issue avec le template
2. **Ne pas implémenter** tant que v0.201 n'est pas taguée
3. Une fois v0.201 stable, prioriser les issues "v0.202" dans un nouveau document de scope
2. **Ne pas implémenter** tant que v0.202 n'est pas taguée
3. Une fois v0.202 stable, prioriser les issues "v0.203" dans un nouveau document de scope
---
@ -125,7 +125,7 @@ Si une vulnérabilité critique est identifiée :
- Correctif autorisé **immédiatement**
- Documenter dans la PR
- Pas besoin d'être dans le scope v0.201
- Pas besoin d'être dans le scope v0.202
### 6.2 Blocage production
@ -140,13 +140,13 @@ Pour tout cas ambigu :
- Ouvrir une issue "Scope clarification"
- Décision documentée dans l'issue
- Mise à jour de V0_201_RELEASE_SCOPE.md si le scope est étendu (exception rare)
- Mise à jour de V0_202_RELEASE_SCOPE.md si le scope est étendu (exception rare)
---
## 7. Après le tag d'une version
1. **Créer** le document de scope de la version suivante (ex: `V0_202_RELEASE_SCOPE.md`)
1. **Créer** le document de scope de la version suivante (ex: `V0_203_RELEASE_SCOPE.md`)
2. **Définir** explicitement les nouvelles features autorisées
3. **Mettre à jour** la référence active dans ce document (section header)
4. **Reprendre** ce processus avec le nouveau document de scope
@ -157,11 +157,12 @@ Pour tout cas ambigu :
- v0.102 : Déblocage Coming Soon, renforcement coeur produit (taguée)
- v0.103 : Complétion Phase 1 Fondation — Auth A1/A4, Profils B1-B3 (taguée)
- v0.201 : Phase 2 Contenu — Lot E Métadonnées (BPM, key, lyrics, tags) — taguée
- v0.202 : Phase 2 Contenu — Lots G, H, F, C, D — taguée
---
## 8. Rappel pour les contributeurs
- **Cursor / IA** : Les règles dans `.cursorrules` rappellent de vérifier le scope avant toute modification.
- **Humains** : Lire [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) avant de coder.
- **Humains** : Lire [V0_202_RELEASE_SCOPE.md](V0_202_RELEASE_SCOPE.md) avant de coder.
- **En doute ?** Ouvrir une issue "Scope clarification" plutôt que de coder.

View file

@ -0,0 +1,201 @@
# Scope v0.202 — Phase 2 Contenu (suite)
**Version cible** : v0.202 (X=2, Y=2)
**Prérequis** : v0.201 taguée et mergée dans main
**Objectif** : Phase 2 Contenu — Recherche avancée, Analytics créateur, Seller dashboard, Player, Queue
**Dernière mise à jour** : 20 février 2026
**Effort estimé** : 5-7 semaines de développement
---
## 1. Principe directeur
> **v0.202 = suite Phase 2 (Contenu) après v0.201.**
>
> Cinq axes principaux :
> 1. **Recherche avancée (Lot G)** : Filtres, tri, autocomplete, facettes, historique
> 2. **Analytics créateur (Lot H)** : Stats, graphiques, export
> 3. **Seller dashboard (Lot F)** : Stats ventes, liste produits
> 4. **Player avancé (Lot C)** : Crossfade, gapless, PiP
> 5. **Queue avancée (Lot D)** : Autoplay, queue collaborative
---
## 2. Contexte — État post v0.201
### 2.1 Livré en v0.201
| Lot | Feature | Statut |
|-----|---------|--------|
| E1 | BPM | ✅ |
| E2 | Musical key | ✅ |
| E3 | Lyrics | ✅ |
| E4 | Tags suggérés | ✅ |
### 2.2 Fondation existante (à enrichir)
| Domaine | Existant | À ajouter v0.202 |
|---------|----------|-------------------|
| **Recherche tracks** | `TrackSearchService`, GET /tracks/search, filtres BPM/durée/genre/format/date, tri (popularity, title, created_at) | musical_key, autocomplete, unification search unifiée, historique |
| **Search unifiée** | GET /search (q, type=track|user|playlist), SearchService | Filtres, tri, pagination |
| **Analytics** | Routes /api/v1/analytics/*, playback_analytics | Route créateur, graphiques, export |
| **Player** | MediaSession, usePlayer | Crossfade, gapless, PiP |
| **Queue** | Queue sync, PlayerQueue | Autoplay, queue collaborative |
---
## 3. Features IN SCOPE v0.202
### 3.1 Lot G — Recherche avancée (priorité haute)
**Objectif** : Enrichir la recherche existante.
**Effort** : L (5-7 jours)
**Référence** : Module 11 (veza_full_features_list)
| # | Feature | Tâche détaillée | Backend | Frontend | Critère de sortie |
|---|---------|-----------------|---------|----------|-------------------|
| G1 | **Filtres recherche** | musical_key manquant ; tags déjà dans TrackSearchParams | Ajouter `musical_key` à TrackSearchParams + handler | Vérifier UI SearchPage/TrackSearch utilise tous les filtres | Filtres appliqués, résultats mis à jour |
| G2 | **Tri avancé** | Tri pertinence, date, popularité, alphabétique | Déjà : sort_by, sort_order. Ajouter "relevance" si full-text | Dropdown tri dans SearchPage | Tri fonctionnel |
| G3 | **Autocomplete** | Suggestions pendant la frappe | Route GET /search/suggestions?q=… (tracks, users, playlists) ou extension GET /search avec limit=5 | Input avec dropdown debounced | Suggestions affichées, clic → recherche |
| G4 | **Recherche par type** | Tracks, artistes, playlists, utilisateurs | SearchService : param `type` existant. Vérifier cohérence | Onglets/facettes dans SearchPage | Résultats par catégorie |
| G5 | **Historique recherche** | Mémoriser les dernières recherches | Optionnel : table search_history. Sinon localStorage | Section "Récent" | Dernières recherches cliquables |
**Fichiers clés** :
- Backend : `track_search_service.go`, `search_service.go`, `search_handlers.go`, `routes_search.go`, `routes_tracks.go`
- Frontend : `TrackSearch.tsx`, `TrackSearchFilters`, `trackSearchService.ts`, `SearchPage`
---
### 3.2 Lot H — Analytics créateur (priorité haute)
**Objectif** : Dashboard analytics pour créateurs.
**Effort** : L (5-7 jours)
**Référence** : Module 12.1
| # | Feature | Tâche détaillée | Backend | Frontend | Critère de sortie |
|---|---------|-----------------|---------|----------|-------------------|
| H1 | **Stats d'écoute** | Plays par track, période, durée moyenne | Agrégation playback_analytics, route GET /analytics/creator/stats | Page /analytics ou section créateur | Chiffres affichés |
| H2 | **Graphiques** | Évolution plays, top tracks | Route GET /analytics/creator/charts | Charts (recharts ou équivalent) | Graphiques lisibles |
| H3 | **Taux de complétion** | % d'écoute complète par track | Calcul backend (plays complètes / total) | Affichage dans dashboard | Métrique visible |
| H4 | **Export données** | Export CSV/JSON des stats | Route GET /analytics/creator/export?format=csv|json | Bouton export | Fichier téléchargé |
**Fichiers clés** :
- Backend : `playback_analytics_handler.go`, `routes`, modèles analytics
- Frontend : Page Analytics, composants charts, export
---
### 3.3 Lot F — Seller dashboard (priorité moyenne)
**Objectif** : Dashboard vendeur fonctionnel.
**Effort** : M (3-4 jours)
**Référence** : Module 7.4
| # | Feature | Tâche détaillée | Backend | Frontend | Critère de sortie |
|---|---------|-----------------|---------|----------|-------------------|
| F1 | **Stats ventes** | Nombre ventes, revenus, période | Route GET /sell/stats ou agrégation Hyperswitch/marketplace | Page /sell | Chiffres affichés |
| F2 | **Liste produits** | Tracks/albums en vente par l'utilisateur | Extension catalogue existant | Liste dans seller dashboard | Produits listés |
---
### 3.4 Lot C — Player avancé (priorité moyenne)
**Objectif** : Améliorer l'expérience d'écoute.
**Effort** : M (2-4 jours)
**Référence** : Module 4.1
| # | Feature | Tâche détaillée | Backend | Frontend | Critère de sortie |
|---|---------|-----------------|---------|----------|-------------------|
| C1 | **Crossfade** | Transition en fondu entre tracks | N/A | Logique player (gain, overlap) | Transition fluide 1-10s configurable |
| C2 | **Gapless playback** | Lecture sans silence entre tracks | N/A | Préchargement audio, Web Audio API ou HTMLMediaElement | Pas de coupure |
| C3 | **PiP** | Picture-in-Picture (si supporté) | N/A | Media Session + PiP API (document.pictureInPictureEnabled) | Fenêtre flottante |
**Fichiers clés** : `GlobalPlayer.tsx`, `AudioPlayer.tsx`, `usePlayer.ts`, `playerStore.ts`
---
### 3.5 Lot D — Queue avancée (priorité basse)
**Objectif** : Queue collaborative et autoplay.
**Effort** : M (3-4 jours)
**Référence** : Module 4.2
| # | Feature | Tâche détaillée | Backend | Frontend | Critère de sortie |
|---|---------|-----------------|---------|----------|-------------------|
| D1 | **Queue collaborative** | Partager queue en session | Modèle session queue partagée, routes queue | UI partage | Plusieurs users, même queue |
| D2 | **Autoplay** | Recommandations quand queue vide | Route GET /recommendations ou extension playlists | Section "À écouter ensuite" | Suggestions, ajout 1 clic |
---
## 4. Récapitulatif par lot
| Lot | Nom | Priorité | Effort | Features |
|-----|-----|----------|--------|----------|
| **G** | Recherche avancée | Haute | L | 5 |
| **H** | Analytics créateur | Haute | L | 4 |
| **F** | Seller dashboard | Moyenne | M | 2 |
| **C** | Player avancé | Moyenne | M | 3 |
| **D** | Queue avancée | Basse | M | 2 |
| | **TOTAL** | | **16-27j** | **16** |
---
## 5. Features HORS SCOPE v0.202
| Feature | Raison | Version cible |
|---------|--------|---------------|
| Recherche phonétique, booléenne | Complexité | v0.203+ |
| Collaborative filtering | Nécessite historique riche | v0.203+ |
| Social Trending API | Dépend recommandations | v0.203+ |
| 2FA SMS, Passkeys | v0.104 dédié | v0.104 |
| Go Live vidéo | v0.703 | v0.703 |
---
## 6. Ordre de livraison recommandé
| Semaine | Lots | Activités |
|---------|------|-----------|
| **S1** | G (Recherche) | G1G5 : filtres musical_key, tri, autocomplete, facettes, historique |
| **S2** | H (Analytics) | H1H4 : stats, graphiques, taux complétion, export |
| **S3** | F (Seller) | F1F2 : stats ventes, liste produits |
| **S4** | C (Player) | C1C3 : crossfade, gapless, PiP |
| **S5** | D (Queue) | D1D2 : autoplay, queue collaborative |
| **S6** | Stabilisation | Tests, docs, polish |
---
## 7. Branche et workflow
- **Branche** : `release/v0.202`
- **Format commit** : `feat(scope): description` (ex: `feat(search): add musical_key filter`)
- **Référence scope** : [SCOPE_CONTROL.md](SCOPE_CONTROL.md) → v0.202
- **PR** : `gh pr create --base main --head release/v0.202 --title "Release v0.202"`
---
## 8. Critères de stabilité v0.202
### 8.1 Build & compilation
- [ ] `go build ./...` — 0 erreur
- [ ] `npm run build` — 0 erreur
- [ ] `npx tsc --noEmit` — 0 erreur
### 8.2 Tests
- [ ] `go test ./...` — pas de régression
- [ ] `npm test -- --run` — pas de régression
- [ ] E2E : recherche, analytics, seller (si applicable)
### 8.3 Documentation
- [ ] FEATURE_STATUS.md mis à jour
- [ ] CHANGELOG v0.202
- [ ] MSW handlers pour nouveaux endpoints
---
## Références
- [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) — Version précédente (Lot E livré)
- [SCOPE_CONTROL.md](SCOPE_CONTROL.md) — Processus anti-scope-creep
- [FEATURE_STATUS.md](FEATURE_STATUS.md) — Statut des features
- [PROJECT_STATE.md](PROJECT_STATE.md) — État actuel du projet

View file

@ -74,6 +74,9 @@ func (r *APIRouter) setupTrackRoutes(router *gin.RouterGroup) {
notificationService := services.NewNotificationService(r.db, r.logger)
trackHandler.SetNotificationService(notificationService)
trackRecommendationService := services.NewTrackRecommendationService(r.db.GormDB, r.logger)
trackHandler.SetTrackRecommendationService(trackRecommendationService)
tracks := router.Group("/tracks")
{
tracks.GET("", trackHandler.ListTracks)
@ -91,6 +94,8 @@ func (r *APIRouter) setupTrackRoutes(router *gin.RouterGroup) {
protected.Use(r.config.AuthMiddleware.RequireAuth())
r.applyCSRFProtection(protected)
protected.GET("/recommendations", trackHandler.GetRecommendations)
uploadGroup := protected.Group("")
uploadGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole())
uploadGroup.POST("", trackHandler.UploadTrack)

View file

@ -42,6 +42,7 @@ type TrackHandler struct {
uploadValidator *services.UploadValidator // MOD-P1-001: Added for ClamAV scan before persistence
licenseChecker services.TrackDownloadLicenseChecker // A04: Verify paid track download rights
notificationService *services.NotificationService // Phase 2.2: Optional, for like notifications
trackRecommendationService *services.TrackRecommendationService
}
// NewTrackHandler crée un nouveau handler de tracks
@ -114,6 +115,11 @@ func (h *TrackHandler) SetNotificationService(notificationService *services.Noti
h.notificationService = notificationService
}
// SetTrackRecommendationService définit le service de recommandations
func (h *TrackHandler) SetTrackRecommendationService(svc *services.TrackRecommendationService) {
h.trackRecommendationService = svc
}
// getUserID récupère l'ID utilisateur du contexte de manière sécurisée (fail-secure)
// MOD-P1-RES-003: Remplace c.MustGet() pour éviter les panics
// Retourne false si user_id est absent ou invalide (répond déjà avec 401)
@ -880,6 +886,49 @@ func (h *TrackHandler) ListTracks(c *gin.Context) {
})
}
// GetRecommendations returns personalized track recommendations (D2 autoplay)
func (h *TrackHandler) GetRecommendations(c *gin.Context) {
if h.trackRecommendationService == nil {
response.InternalServerError(c, "recommendations unavailable")
return
}
var userID uuid.UUID
if uid, exists := c.Get("user_id"); exists {
if parsed, ok := uid.(uuid.UUID); ok {
userID = parsed
}
}
limitStr := c.DefaultQuery("limit", "20")
var limit int
if _, err := fmt.Sscanf(limitStr, "%d", &limit); err != nil || limit < 1 {
limit = 20
}
if limit > 100 {
limit = 100
}
params := services.TrackRecommendationParams{
UserID: userID,
Limit: limit,
}
if seedStr := c.Query("seed_track_id"); seedStr != "" {
if sid, err := uuid.Parse(seedStr); err == nil {
params.SeedTrackID = &sid
}
}
recs, err := h.trackRecommendationService.GetRecommendations(c.Request.Context(), params)
if err != nil {
response.InternalServerError(c, "failed to get recommendations")
return
}
tracks := make([]*models.Track, 0, len(recs))
for _, r := range recs {
if r.Track != nil {
tracks = append(tracks, r.Track)
}
}
response.Success(c, gin.H{"tracks": tracks})
}
// tagSuggestionsByGenre holds static tag suggestions per genre (E4)
var tagSuggestionsByGenre = map[string][]string{
"pop": {"Pop", "Catchy", "Radio", "Mainstream", "Vocal"},