# Plan d'implémentation v0.503 — Stream Server E2E + Chat Hardening + Cleanup **Date** : 2026-02-22 **Base** : v0.502 taguée **Durée estimée** : 5 sprints (~25 jours ouvrés) **Référence** : [V0_503_RELEASE_SCOPE.md](V0_503_RELEASE_SCOPE.md) --- ## Vue d'ensemble ``` Sprint 1 (j1-5) → CL1 : Cleanup chat-server Rust (CI/CD, config, archivage) Sprint 2 (j6-12) → CH1 : Chat hardening (Redis rate limiter, présence, FTS) Sprint 3 (j13-18) → SS1a : Stream server HLS backend (endpoints, transcoding) Sprint 4 (j19-23) → SS1b : Stream server HLS frontend (player, ABR, stories) Sprint 5 (j24-25) → QA1 : Tests, documentation, tag ``` --- ## Diagramme d'architecture cible ```mermaid flowchart TD Browser["Browser (React)"] subgraph GoBackend["Go Backend (veza-backend-api)"] GinRouter["Gin Router"] HLSHandler["HLS Handler"] StreamSvc["StreamService"] ChatHub["Chat Hub (WebSocket)"] PresenceSvc["PresenceService (Redis)"] RateLimiter["RateLimiter (Redis)"] MsgRepo["ChatMessageRepository (FTS)"] end subgraph StreamServer["Stream Server (Rust)"] HLSGen["HLS Generator"] Transcoder["Transcoding Engine"] FFmpeg["FFmpeg"] end subgraph Storage["Storage"] PostgreSQL["PostgreSQL 16"] Redis["Redis 7"] MinIO["MinIO (S3)"] end Browser -->|"GET /api/v1/tracks/:id/hls/*"| GinRouter Browser -->|"WS /api/v1/ws"| ChatHub GinRouter --> HLSHandler HLSHandler -->|"proxy"| HLSGen StreamSvc -->|"POST /internal/jobs/transcode"| Transcoder Transcoder --> FFmpeg FFmpeg -->|"segments .ts + .m3u8"| MinIO HLSGen -->|"read segments"| MinIO ChatHub --> RateLimiter ChatHub --> PresenceSvc ChatHub --> MsgRepo RateLimiter --> Redis PresenceSvc --> Redis MsgRepo -->|"ts_query FTS"| PostgreSQL ``` --- ## Sprint 1 — Cleanup chat-server Rust (jours 1-5) > **Objectif** : Supprimer toute trace opérationnelle du chat server Rust supprimé en v0.502. ### Tâche CL1-01 : Archiver le dossier `veza-chat-server/` **Action** : Supprimer le dossier du repo (l'historique Git le préserve). ```bash rm -rf veza-chat-server/ ``` **Fichiers** : `veza-chat-server/` (~78 fichiers Rust + Cargo.toml + Dockerfile) ### Tâche CL1-02 : Nettoyer CI workflow `ci.yml` **Fichier** : `.github/workflows/ci.yml` Supprimer les sections qui buildent/auditent/lintent/testent le chat-server : - Lignes ~84-101 : `Auditing Chat Server`, `cd veza-chat-server`, `cargo audit` - Lignes ~95-101 : `--filter=veza-chat-server` dans `turbo run lint`, `turbo run build`, `turbo run test` **Résultat** : Conserver uniquement `--filter=veza-stream-server` dans les commandes turbo. ### Tâche CL1-03 : Nettoyer CD workflow `cd.yml` **Fichier** : `.github/workflows/cd.yml` Supprimer toutes les références à `veza-chat-server` : - Ligne 41 : `docker build -t veza-chat-server:...` - Lignes 60-66 : Trivy scan `veza-chat-server` - Lignes 79, 92, 110 : Retirer `veza-chat-server` des boucles `for svc in` - Ligne 120 : Retirer la ligne summary chat-server - Ligne 137 : Retirer `veza-chat-server` de la boucle deploy ### Tâche CL1-04 : Nettoyer `rust-ci.yml` **Fichier** : `.github/workflows/rust-ci.yml` Supprimer les paths `veza-chat-server/**` et le job `clippy-chat` (lignes ~4-24). ### Tâche CL1-05 : Nettoyer `chat-ci.yml` **Fichier** : `.github/workflows/chat-ci.yml` Supprimer entièrement ce workflow (il est dédié au chat-server Rust). ### Tâche CL1-06 : Nettoyer Prometheus config **Fichier** : `config/prometheus.yml` Supprimer le job `veza-chat` (lignes 18-21) : ```yaml - job_name: 'veza-chat' static_configs: - targets: ['chat-server:8081'] metrics_path: '/metrics' ``` ### Tâche CL1-07 : Nettoyer Caddyfile staging **Fichier** : `config/caddy/Caddyfile.staging` Remplacer `reverse_proxy /ws chat-server:8081` par `# Chat WebSocket is now handled by Go backend at /api/v1/ws`. ### Tâche CL1-08 : Nettoyer HAProxy config **Fichier** : `config/haproxy/haproxy.cfg` Supprimer la section backend chat (`server chat1 chat-server:3000`, lignes ~90-95). ### Tâche CL1-09 : Nettoyer scripts Incus **Fichiers** : - `config/incus/deploy-service-native.sh` : retirer le case `chat-server)` (lignes ~112-457) - `config/incus/deploy-service.sh` : retirer le case `chat-server)` (lignes ~62-63) - `config/incus/build-native.sh` : retirer `build_chat_server()` et le case `chat-server)` (lignes ~48-89, ~181-182) - `config/incus/check-deployment.sh` : retirer `veza-chat-server` des listes de containers/services - `config/incus/verify-deployment.sh` : retirer `veza-chat-server` des listes - `config/incus/fix-network-now.sh` : retirer le case `*chat-server*)` - `config/incus/setup-basic-incus.sh` : retirer mention chat-server - `config/incus/haproxy.cfg` : retirer les commentaires/sections chat-server ### Tâche CL1-10 : Nettoyer Dependabot **Fichier** : `.github/dependabot.yml` Supprimer l'entrée cargo pour `/veza-chat-server`. ### Tâche CL1-11 : Nettoyer templates GitHub **Fichiers** : - `.github/pull_request_template.md` : retirer `chat-server` du scope et la checkbox `cargo test (chat-server)` - `.github/ISSUE_TEMPLATE/bug_report.md` : retirer `chat-server` de la liste des services ### Tâche CL1-12 : Vérifier `go.work` et `turbo.json` **Fichiers** : `go.work`, `turbo.json`, `package.json` (root) Retirer toute référence au chat-server de ces fichiers de configuration monorepo. ### Tâche CL1-13 : Mettre à jour documentation Incus **Fichiers** : - `config/incus/README.md` : retirer les lignes chat-server du tableau et des instructions - `config/incus/DEPLOYMENT_STATUS.md` : mettre à jour le statut - `config/incus/DEBIAN13_MIGRATION.md` : retirer mentions chat-server - `config/docker/README.md` : retirer la ligne `veza-chat-server/docker-compose.yml` - `config/incus/env/env.example` : retirer la section `=== chat-server.env ===` **Validation Sprint 1** : ```bash # Vérifier qu'aucune référence opérationnelle ne reste git grep -l 'chat-server' -- ':(exclude)docs/' ':(exclude)CHANGELOG.md' ':(exclude)*.md' # Devrait retourner 0 résultat (hors docs historiques) ``` **Commit Sprint 1** : `chore(cleanup): remove veza-chat-server and all operational references` --- ## Sprint 2 — Chat Hardening (jours 6-12) > **Objectif** : Migrer le rate limiter vers Redis, ajouter la présence persistante, et full-text search. ### Tâche CH1-01 : Rate limiter Redis **Fichier** : `veza-backend-api/internal/websocket/chat/rate_limiter.go` Remplacer l'implémentation in-memory par Redis avec fallback. ```go package chat import ( "context" "fmt" "sync" "time" "github.com/google/uuid" "github.com/redis/go-redis/v9" "go.uber.org/zap" ) type RateLimiter struct { redis *redis.Client limits map[string]rateConfig logger *zap.Logger // fallback in-memory (si Redis indisponible) fallback *inMemoryRateLimiter } type rateConfig struct { maxRequests int window time.Duration } func NewRateLimiter(redisClient *redis.Client, logger *zap.Logger) *RateLimiter { limits := map[string]rateConfig{ "send_message": {maxRequests: 10, window: time.Second}, "typing": {maxRequests: 5, window: time.Second}, "search": {maxRequests: 2, window: time.Second}, "fetch_history": {maxRequests: 5, window: time.Second}, } return &RateLimiter{ redis: redisClient, limits: limits, logger: logger, fallback: newInMemoryRateLimiter(limits), } } func (rl *RateLimiter) Allow(userID uuid.UUID, action string) bool { cfg, ok := rl.limits[action] if !ok { return true } if rl.redis == nil { return rl.fallback.Allow(userID, action) } ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() key := fmt.Sprintf("chat:ratelimit:%s:%s", userID.String(), action) now := time.Now().UnixMilli() windowStart := now - cfg.window.Milliseconds() pipe := rl.redis.Pipeline() pipe.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", windowStart)) pipe.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: now}) pipe.ZCard(ctx, key) pipe.Expire(ctx, key, cfg.window*2) results, err := pipe.Exec(ctx) if err != nil { rl.logger.Warn("Redis rate limiter failed, using fallback", zap.Error(err)) return rl.fallback.Allow(userID, action) } count := results[2].(*redis.IntCmd).Val() return count <= int64(cfg.maxRequests) } ``` L'`inMemoryRateLimiter` reprend l'ancien code (sliding window en mémoire) comme fallback. ### Tâche CH1-02 : Présence service Redis **Nouveau fichier** : `veza-backend-api/internal/websocket/chat/presence_service.go` ```go package chat import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/redis/go-redis/v9" "go.uber.org/zap" ) type PresenceInfo struct { UserID uuid.UUID `json:"user_id"` Status string `json:"status"` LastSeen time.Time `json:"last_seen"` TrackID *string `json:"track_id,omitempty"` TrackName *string `json:"track_name,omitempty"` } type ChatPresenceService struct { redis *redis.Client logger *zap.Logger ttl time.Duration } func NewChatPresenceService(redisClient *redis.Client, logger *zap.Logger) *ChatPresenceService { return &ChatPresenceService{ redis: redisClient, logger: logger, ttl: 2 * time.Minute, } } func (s *ChatPresenceService) SetOnline(ctx context.Context, userID uuid.UUID) error { key := fmt.Sprintf("chat:presence:%s", userID.String()) info := PresenceInfo{ UserID: userID, Status: "online", LastSeen: time.Now(), } data, _ := json.Marshal(info) return s.redis.Set(ctx, key, data, s.ttl).Err() } func (s *ChatPresenceService) SetOffline(ctx context.Context, userID uuid.UUID) error { key := fmt.Sprintf("chat:presence:%s", userID.String()) return s.redis.Del(ctx, key).Err() } func (s *ChatPresenceService) Heartbeat(ctx context.Context, userID uuid.UUID) error { key := fmt.Sprintf("chat:presence:%s", userID.String()) return s.redis.Expire(ctx, key, s.ttl).Err() } func (s *ChatPresenceService) GetPresence(ctx context.Context, userID uuid.UUID) (*PresenceInfo, error) { key := fmt.Sprintf("chat:presence:%s", userID.String()) data, err := s.redis.Get(ctx, key).Result() if err == redis.Nil { return &PresenceInfo{UserID: userID, Status: "offline"}, nil } if err != nil { return nil, fmt.Errorf("failed to get presence: %w", err) } var info PresenceInfo if err := json.Unmarshal([]byte(data), &info); err != nil { return nil, fmt.Errorf("failed to unmarshal presence: %w", err) } return &info, nil } ``` **Intégration Hub** : `hub.go` → appeler `presenceService.SetOnline()` sur Register et `SetOffline()` sur Unregister. ### Tâche CH1-03 : Migration FTS **Nouveau fichier** : `veza-backend-api/migrations/113_messages_fts.sql` ```sql -- Full-text search on messages ALTER TABLE messages ADD COLUMN IF NOT EXISTS content_tsv tsvector; CREATE INDEX IF NOT EXISTS idx_messages_content_fts ON messages USING GIN (content_tsv); -- Trigger to auto-update tsvector on insert/update CREATE OR REPLACE FUNCTION messages_content_tsv_trigger() RETURNS trigger AS $$ BEGIN NEW.content_tsv := to_tsvector('simple', COALESCE(NEW.content, '')); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_messages_content_tsv ON messages; CREATE TRIGGER trg_messages_content_tsv BEFORE INSERT OR UPDATE OF content ON messages FOR EACH ROW EXECUTE FUNCTION messages_content_tsv_trigger(); -- Backfill existing messages UPDATE messages SET content_tsv = to_tsvector('simple', COALESCE(content, '')) WHERE content_tsv IS NULL; ``` ### Tâche CH1-04 : Remplacer ILIKE par FTS **Fichier** : `veza-backend-api/internal/repositories/chat_message_repository.go` Modifier la méthode `Search` : ```go func (r *ChatMessageRepository) Search(ctx context.Context, roomID uuid.UUID, query string, limit, offset int) ([]models.ChatMessage, int64, error) { tsQuery := plainto_tsquery(query) // helper to sanitize var total int64 r.db.WithContext(ctx).Model(&models.ChatMessage{}). Where("room_id = ? AND is_deleted = ? AND content_tsv @@ plainto_tsquery('simple', ?)", roomID, false, query). Count(&total) var messages []models.ChatMessage err := r.db.WithContext(ctx). Where("room_id = ? AND is_deleted = ? AND content_tsv @@ plainto_tsquery('simple', ?)", roomID, false, query). Order("ts_rank(content_tsv, plainto_tsquery('simple', ?)) DESC, created_at DESC", query). Limit(limit). Offset(offset). Find(&messages).Error if err != nil { return nil, 0, fmt.Errorf("failed to search messages: %w", err) } return messages, total, nil } ``` ### Tâche CH1-05 : Mettre à jour le modèle ChatMessage GORM **Fichier** : `veza-backend-api/internal/models/message.go` Ajouter le champ `ContentTsv` (ignoré par JSON, géré par trigger) : ```go ContentTsv *string `gorm:"type:tsvector;->" json:"-"` ``` Le tag `->` indique à GORM que ce champ est en lecture seule (géré par le trigger PostgreSQL). ### Tâche CH1-06 : Intégrer Redis dans le constructeur MessageHandler **Fichier** : `veza-backend-api/internal/api/router.go` Modifier la construction du `RateLimiter` et du `MessageHandler` pour injecter le client Redis et le `ChatPresenceService`. ### Tâche CH1-07 : Tests unitaires rate limiter Redis **Nouveau fichier** : `veza-backend-api/internal/websocket/chat/rate_limiter_test.go` Tests : - `TestRedisRateLimiter_Allow` — vérifie le sliding window Redis - `TestRedisRateLimiter_FallbackInMemory` — vérifie le fallback quand Redis est nil - `TestRedisRateLimiter_UnknownAction` — toujours autorisé ### Tâche CH1-08 : Tests unitaires presence service **Nouveau fichier** : `veza-backend-api/internal/websocket/chat/presence_service_test.go` Tests : - `TestPresence_SetOnline_GetPresence` — round-trip - `TestPresence_SetOffline` — supprime la clé - `TestPresence_Heartbeat_ExtendsExpiry` — vérifie le TTL ### Tâche CH1-09 : Benchmark WebSocket **Nouveau fichier** : `veza-backend-api/internal/websocket/chat/benchmark_test.go` Benchmark : - 100 connexions simultanées - Chaque client envoie 10 messages - Mesurer le temps de bout en bout - Vérifier 0 erreur **Validation Sprint 2** : ```bash cd veza-backend-api && go test ./internal/websocket/chat/... -v -count=1 cd veza-backend-api && go test ./internal/websocket/chat/ -bench=. -benchtime=10s ``` **Commit Sprint 2** : `feat(chat): add Redis rate limiter, persistent presence, full-text search` --- ## Sprint 3 — HLS Backend Integration (jours 13-18) > **Objectif** : Rendre le pipeline HLS fonctionnel côté backend. ### État actuel | Élément | Existe ? | Fonctionne ? | |---------|----------|-------------| | `HLSHandler.GetStreamInfo` | ✅ | Retourne info track | | `HLSHandler.GetStreamStatus` | ✅ | Retourne statut transcoding | | `HLSHandler.ServeMasterPlaylist` | ✅ | Lit depuis filesystem/S3 | | `HLSHandler.ServeQualityPlaylist` | ✅ | Lit depuis filesystem/S3 | | `HLSHandler.ServeSegment` | ✅ | Lit depuis filesystem/S3 | | `HLSHandler.TriggerTranscode` | ✅ | Appelle stream server | | `StreamService.StartProcessing` | ✅ | POST /internal/jobs/transcode | | Stream server HLS endpoints | ✅ | Compilation OK | | Route `GET /tracks/:id/hls/info` | ✅ | Enregistrée dans router | | Route `GET /tracks/:id/hls/status` | ✅ | Enregistrée dans router | | Route `GET /tracks/:id/hls/master.m3u8` | ❌ | **Non enregistrée** | | Route `GET /tracks/:id/hls/quality.m3u8` | ❌ | **Non enregistrée** | | Route `GET /tracks/:id/hls/segment` | ❌ | **Non enregistrée** | | Config `HLSEnabled` | ❌ | **Absent** | | Callback HLS-ready | ❓ | **À vérifier** | ### Tâche SS1-01 : Ajouter config HLS **Fichier** : `veza-backend-api/internal/config/config.go` ```go // Ajout dans le struct Config HLSEnabled bool // Active le streaming HLS adaptatif HLSStorageDir string // Répertoire de stockage des segments HLS // Ajout dans le chargement HLSEnabled: getEnvBool("HLS_STREAMING", false), HLSStorageDir: getEnv("HLS_STORAGE_DIR", "/tmp/veza-hls"), ``` ### Tâche SS1-02 : Enregistrer les routes HLS dans le router **Fichier** : `veza-backend-api/internal/api/router.go` Ajouter les routes HLS manquantes (protégées par stream-token auth) : ```go // HLS streaming routes (stream token auth) if r.config.HLSEnabled { hlsGroup := tracks.Group("/:id/hls") hlsGroup.Use(streamTokenAuth) { hlsGroup.GET("/master.m3u8", hlsHandler.ServeMasterPlaylist) hlsGroup.GET("/:quality/playlist.m3u8", hlsHandler.ServeQualityPlaylist) hlsGroup.GET("/:quality/:segment", hlsHandler.ServeSegment) } } ``` ### Tâche SS1-03 : Étendre StreamService pour HLS **Fichier** : `veza-backend-api/internal/services/stream_service.go` Ajouter les méthodes : ```go // GetHLSStatus vérifie si les segments HLS sont disponibles pour un track func (s *StreamService) GetHLSStatus(ctx context.Context, trackID uuid.UUID) (*HLSStatus, error) // TriggerHLSTranscode déclenche le transcoding HLS adaptatif func (s *StreamService) TriggerHLSTranscode(ctx context.Context, trackID uuid.UUID, filePath string) error ``` `HLSStatus` retourne les qualités disponibles, le nombre de segments, et la durée totale. ### Tâche SS1-04 : Vérifier callback stream-ready **Fichier** : `veza-backend-api/internal/handlers/` (chercher le handler de callback interne) Vérifier que le callback `POST /api/v1/internal/tracks/:id/stream-ready` : 1. Met à jour `track.stream_manifest_url` avec l'URL HLS 2. Met à jour le statut de transcoding 3. Envoie une notification (optionnel) Si le handler n'est pas complet, le compléter pour mettre à jour le champ `stream_manifest_url` du track avec l'URL du master playlist HLS. ### Tâche SS1-05 : Mettre à jour docker-compose pour HLS **Fichiers** : `docker-compose.yml`, `docker-compose.staging.yml`, `docker-compose.prod.yml` Ajouter les variables d'environnement HLS : ```yaml environment: HLS_STREAMING: "true" HLS_STORAGE_DIR: "/data/hls" ``` Ajouter un volume partagé pour les segments HLS entre le stream server et le backend (ou utiliser MinIO/S3). ### Tâche SS1-06 : Mettre à jour Caddy pour HLS **Fichier** : `config/caddy/Caddyfile.staging` S'assurer que les routes `/hls/*` sont correctement proxiées : ``` reverse_proxy /hls/* stream-server:3001 ``` (Déjà présent, vérifier que c'est correct.) ### Tâche SS1-07 : Test E2E HLS pipeline **Nouveau fichier** : `veza-backend-api/internal/integration/e2e_hls_test.go` Test : 1. Upload un fichier audio 2. Vérifier que le transcoding est déclenché 3. Attendre le callback stream-ready 4. Vérifier que le master playlist est accessible 5. Vérifier qu'au moins 1 qualité est disponible **Validation Sprint 3** : ```bash cd veza-backend-api && go test ./internal/... -v -count=1 -run HLS cd veza-backend-api && go build ./... ``` **Commit Sprint 3** : `feat(streaming): wire HLS pipeline end-to-end (backend)` --- ## Sprint 4 — HLS Frontend Integration (jours 19-23) > **Objectif** : Le player utilise HLS quand disponible, avec ABR et quality selector. ### Tâche SS1-05 : Intégrer `useHLSPlayer` dans le player **Fichier** : `apps/web/src/features/player/hooks/usePlayer.ts` (ou équivalent) Logique : 1. Quand un track a un `stream_manifest_url` non-null → utiliser `useHLSPlayer` 2. Sinon → fallback sur l'URL directe (comportement actuel) ```typescript // Pseudo-code d'intégration const hlsPlayer = useHLSPlayer(audioRef, currentTrack?.id, currentTrack?.streamStatus); useEffect(() => { if (currentTrack?.stream_manifest_url && hlsPlayer.isHLSActive) { // HLS gère le chargement via hls.js return; } // Fallback : chargement direct audioPlayerService.loadTrack(currentTrack); }, [currentTrack]); ``` ### Tâche SS1-06 : Quality selector dans le player **Fichier** : `apps/web/src/features/player/components/` (nouveau composant ou intégré dans le player existant) Composant `QualitySelector` : - Affiche les niveaux disponibles (128k, 256k, 320k, Auto) - Appelle `hlsPlayer.setQuality(levelIndex)` au clic - Affiche le niveau actuel et la bande passante estimée - Caché si HLS non actif ### Tâche SS1-07 : Mettre à jour le type Track **Fichier** : `apps/web/src/features/` (types track) Ajouter `stream_manifest_url?: string` et `stream_status?: string` au type Track si pas déjà présent. ### Tâche SS1-08 : MSW handler HLS **Nouveau fichier** : `apps/web/src/mocks/handlers-streaming.ts` ```typescript import { http, HttpResponse } from 'msw'; export const streamingHandlers = [ http.get('/api/v1/tracks/:id/hls/info', ({ params }) => { return HttpResponse.json({ success: true, data: { track_id: params.id, hls_available: true, qualities: [ { name: 'low', bitrate: 128000 }, { name: 'medium', bitrate: 256000 }, { name: 'high', bitrate: 320000 }, ], duration: 240, }, }); }), http.get('/api/v1/tracks/:id/hls/status', ({ params }) => { return HttpResponse.json({ success: true, data: { track_id: params.id, status: 'ready', progress: 100, }, }); }), ]; ``` Enregistrer dans `handlers.ts`. ### Tâche SS1-09 : Story Storybook player HLS **Fichier** : `apps/web/src/features/player/` (story existante ou nouvelle) États : - `Default` — player avec HLS actif, quality selector visible - `Loading` — transcoding en cours (progress bar) - `Error` — HLS indisponible, fallback sur URL directe - `NoHLS` — track sans HLS, quality selector caché ### Tâche SS1-10 : Mettre à jour env.ts et .env **Fichier** : `apps/web/src/config/env.ts` Vérifier que `HLS_BASE_URL` est correctement dérivé : ```typescript export const HLS_BASE_URL = import.meta.env.VITE_STREAM_URL || deriveStreamURL(); ``` **Fichiers** : `.env.example`, `.env.storybook` Ajouter/vérifier : ``` VITE_FEATURE_HLS_STREAMING=true ``` **Validation Sprint 4** : ```bash cd apps/web && npx tsc --noEmit cd apps/web && npm run build cd apps/web && npm run storybook -- --ci ``` **Commit Sprint 4** : `feat(player): integrate HLS streaming with ABR quality switching` --- ## Sprint 5 — Tests, Documentation & Tag (jours 24-25) > **Objectif** : Finaliser, documenter, tagger. ### Tâche QA1-01 : Mettre à jour FEATURE_STATUS.md **Fichier** : `docs/FEATURE_STATUS.md` - HLS Streaming : passer de "en intégration" à "opérationnel (v0.503)" - Chat WebSocket : mettre à jour la note "Partiel" → "Opérationnel (Go, v0.502+)" - Ajouter section "Livré en v0.503" ### Tâche QA1-02 : Mettre à jour PROJECT_STATE.md **Fichier** : `docs/PROJECT_STATE.md` - Ajouter la section v0.503 dans "Ce qui est livré" - Mettre à jour "Prochaine version" → v0.601 - Mettre à jour la stack technique (retirer Chat Server Rust) - Incrémenter les indicateurs ### Tâche QA1-03 : Smoke test **Nouveau fichier** : `docs/SMOKE_TEST_V0503.md` Checklist : - [ ] HLS : upload → transcode → playback HLS - [ ] HLS : ABR switch de qualité - [ ] Chat : rate limiter Redis fonctionne - [ ] Chat : présence persistée dans Redis - [ ] Chat : recherche full-text retourne des résultats pertinents - [ ] Chat : benchmark 100 connexions OK - [ ] Cleanup : `git grep chat-server` (hors docs) = 0 résultat - [ ] CI : workflows passent sans erreur ### Tâche QA1-04 : CHANGELOG.md **Fichier** : `CHANGELOG.md` ```markdown ## v0.503 — Stream Server E2E + Chat Hardening + Cleanup ### Nouveautés - **HLS Streaming E2E** : pipeline upload → transcode → HLS player fonctionnel - **Player ABR** : quality selector (128k, 256k, 320k, Auto) - **Chat FTS** : recherche full-text PostgreSQL (tsvector + GIN index) - **Chat présence** : tracking persistant Redis avec heartbeat ### Améliorations - **Rate limiter** : migré in-memory → Redis (sliding window, fallback) - **Benchmark** : 100+ connexions WebSocket simultanées validé ### Suppressions - **veza-chat-server/** : dossier Rust archivé/supprimé du repo - **chat-ci.yml** : workflow CI dédié au chat Rust supprimé - Références chat-server retirées de CI/CD, Prometheus, Caddy, HAProxy, scripts Incus ### Infrastructure - Migration 113 : tsvector + GIN index sur messages.content ``` ### Tâche QA1-05 : Archiver scope et préparer v0.601 - Déplacer `V0_503_RELEASE_SCOPE.md` → `docs/archive/` - Créer placeholder `V0_601_RELEASE_SCOPE.md` - Mettre à jour `SCOPE_CONTROL.md` → référence v0.601 ### Tâche QA1-06 : Rétrospective **Nouveau fichier** : `docs/RETROSPECTIVE_V0503.md` ### Tâche QA1-07 : Tag ```bash git tag -a v0.503 -m "v0.503 — Stream Server E2E + Chat Hardening + Cleanup" ``` **Commit Sprint 5** : `docs(v0.503): update documentation, changelog, tag release` --- ## Résumé | Sprint | Jours | Tâches | Focus | |--------|-------|--------|-------| | 1 | 1-5 | CL1-01 à CL1-13 | Cleanup chat-server Rust | | 2 | 6-12 | CH1-01 à CH1-09 | Chat hardening (Redis, FTS) | | 3 | 13-18 | SS1-01 à SS1-07 | HLS backend E2E | | 4 | 19-23 | SS1-05 à SS1-10 | HLS frontend (player, ABR) | | 5 | 24-25 | QA1-01 à QA1-07 | Documentation & tag | **Total** : ~39 tâches, 25 jours ouvrés --- ## Dépendances entre lots ``` CL1 (Cleanup) → indépendant, peut commencer en premier CH1 (Chat Hardening) → indépendant de CL1 et SS1 SS1a (HLS Backend) → indépendant de CL1 et CH1 SS1b (HLS Frontend) → dépend de SS1a (backend endpoints disponibles) QA1 (Documentation) → dépend de CL1, CH1, SS1a, SS1b ``` --- ## Risques et mitigations | Risque | Probabilité | Impact | Mitigation | |--------|------------|--------|-----------| | Stream server HLS ne produit pas de segments | Moyenne | Élevé | Tester manuellement avec FFmpeg avant d'intégrer | | FFmpeg absent du container stream-server | Faible | Élevé | Vérifier le Dockerfile du stream server | | Redis down en production | Faible | Moyen | Fallback in-memory pour rate limiter et présence | | FTS performance sur gros volume | Faible | Moyen | Index GIN + `simple` config (pas de stemming) | | Breaking change frontend player | Moyenne | Moyen | Fallback sur URL directe si HLS échoue |