veza/docs/archive/v0-history/PLAN_V0_503_IMPLEMENTATION.md
senke 0e7097ed1b chore(cleanup): J1 — purge 220MB debris, archive session docs (complete)
First-attempt commit 3a5c6e184 only captured the .gitignore change; the
pre-commit hook silently dropped the 343 staged moves/deletes during
lint-staged's "no matching task" path. This commit re-applies the intended
J1 content on top of bec75f143 (which was pushed in parallel).

Uses --no-verify because:
- J1 only touches .md/.json/.log/.png/binaries — zero code that would
  benefit from lint-staged, typecheck, or vitest
- The hook demonstrated it corrupts pure-rename commits in this repo
- Explicitly authorized by user for this one commit

Changes (343 total: 169 deletions + 174 renames):

Binaries purged (~167 MB):
- veza-backend-api/{server,modern-server,encrypt_oauth_tokens,seed,seed-v2}

Generated reports purged:
- 9 apps/web/lint_report*.json (~32 MB)
- 8 apps/web/tsc_*.{log,txt} + ts_*.log (TS error snapshots)
- 3 apps/web/storybook_*.json (1375+ stored errors)
- apps/web/{build_errors*,build_output,final_errors}.txt
- 70 veza-backend-api/coverage*.out + coverage_groups/ (~4 MB)
- 3 veza-backend-api/internal/handlers/*.bak

Root cleanup:
- 54 audit-*.png (visual regression baselines, ~11 MB)
- 9 stale MVP-era scripts (Jan 27, hardcoded v0.101):
  start_{iteration,mvp,recovery}.sh,
  test_{mvp_endpoints,protected_endpoints,user_journey}.sh,
  validate_v0101.sh, verify_logs_setup.sh, gen_hash.py

Session docs archived (not deleted — preserved under docs/archive/):
- 78 apps/web/*.md     → docs/archive/frontend-sessions-2026/
- 43 veza-backend-api/*.md → docs/archive/backend-sessions-2026/
- 53 docs/{RETROSPECTIVE_V,SMOKE_TEST_V,PLAN_V0_,V0_*_RELEASE_SCOPE,
          AUDIT_,PLAN_ACTION_AUDIT,REMEDIATION_PROGRESS}*.md
                        → docs/archive/v0-history/

README.md and CONTRIBUTING.md preserved in apps/web/ and veza-backend-api/.

Note: The .gitignore rules preventing recurrence were already pushed in
3a5c6e184 and remain in place — this commit does not modify .gitignore.

Refs: AUDIT_REPORT.md §11
2026-04-14 17:12:03 +02:00

26 KiB

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


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

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

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

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

# 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.

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

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

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

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

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 :

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

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

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

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

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 :

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

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

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 :

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

## 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.mddocs/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

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