First-attempt commit3a5c6e184only 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 ofbec75f143(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 in3a5c6e184and remain in place — this commit does not modify .gitignore. Refs: AUDIT_REPORT.md §11
24 KiB
Plan d'implémentation v0.703 — Go Live & Streaming Complet
État des lieux
Le module Live Streaming est partiellement implémenté :
| Feature | Backend | Frontend | Route | MSW | Tests |
|---|---|---|---|---|---|
| List streams | ✅ GET /live/streams | ✅ LiveView | ✅ /live | ❌ | ❌ |
| Get stream | ✅ GET /live/streams/:id | ✅ LiveViewPlayer | ✅ | ❌ | ❌ |
| Create stream | ✅ POST /live/streams | ❌ | ❌ | ❌ | ❌ |
| Stream key | ❌ field exists, never generated | ❌ | ❌ | ❌ | ❌ |
| Update stream | ❌ | ❌ | ❌ | ❌ | ❌ |
| Go Live page | ❌ | ❌ | ❌ | ❌ | ❌ |
| Live chat | ❌ | ⚠️ mock data | ❌ | ❌ | ❌ |
| VOD replays | ❌ | ❌ | ❌ | ❌ | ❌ |
| Playback speed | ❌ | ❌ | — | — | ❌ |
| Media Session | ❌ | ❌ | — | — | ❌ |
Ce plan ajoute le Go Live complet, le chat live connecté, et les améliorations player.
Fichiers existants clés
- Model :
live_stream.go(LiveStream struct, StreamKey field L18) - Service :
live_stream_service.go(Create L35, SetIsLive L79, UpdateViewerCount L97) - Handler :
live_stream_handler.go(ListLiveStreams L38, GetLiveStream L55, CreateLiveStream L70) - Repository :
live_stream_repository.go - Routes :
routes_live.go(GET /streams L19, GET /streams/:id L20, POST /streams L26) - Stream events :
stream_events_handler.go - LiveView :
LiveView.tsx(limitation A6 L4) - LiveViewChat :
LiveViewChat.tsx(mock data) - LiveViewPlayer :
LiveViewPlayer.tsx - Navbar :
Navbar.tsx(Go Live toast L223) - Lazy exports :
lazyExports.ts - Routes :
routeConfig.tsx - Live service :
liveService.ts - Player hooks :
apps/web/src/features/player/hooks/ - Player audio :
apps/web/src/features/player/components/audio-player/
Step 1 : Migration + Model update (GL1-01, GL1-02)
Fichier : veza-backend-api/migrations/117_live_streams_go_live.sql (nouveau)
ALTER TABLE live_streams
ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS stream_url TEXT DEFAULT '',
ADD COLUMN IF NOT EXISTS is_vod BOOLEAN NOT NULL DEFAULT false;
UPDATE live_streams SET stream_key = gen_random_uuid()::text
WHERE stream_key = '' OR stream_key IS NULL;
ALTER TABLE live_streams ALTER COLUMN stream_key SET NOT NULL;
ALTER TABLE live_streams ALTER COLUMN stream_key SET DEFAULT gen_random_uuid()::text;
CREATE INDEX IF NOT EXISTS idx_live_streams_user_id ON live_streams(user_id);
CREATE INDEX IF NOT EXISTS idx_live_streams_is_live ON live_streams(is_live) WHERE is_live = true;
CREATE INDEX IF NOT EXISTS idx_live_streams_scheduled ON live_streams(scheduled_at)
WHERE scheduled_at IS NOT NULL;
Fichier : veza-backend-api/internal/models/live_stream.go — ajouter 3 champs après Tags :
ScheduledAt *time.Time `json:"scheduled_at,omitempty" db:"scheduled_at"`
StreamURL string `gorm:"type:text;default:''" json:"stream_url,omitempty" db:"stream_url"`
IsVOD bool `gorm:"default:false" json:"is_vod" db:"is_vod"`
Commit : feat(live): add migration 117 and model fields for Go Live
Step 2 : Service — Stream key generation, ListByUser (GL1-03, GL1-04)
Fichier : veza-backend-api/internal/services/live_stream_service.go
Modifier Create pour générer la stream key :
func (s *LiveStreamService) Create(ctx context.Context, userID uuid.UUID, stream *models.LiveStream) (*models.LiveStream, error) {
if stream.Title == "" {
return nil, errors.New("title is required")
}
stream.UserID = userID
stream.StreamKey = uuid.New().String()
if stream.StreamerName == "" {
stream.StreamerName = "Streamer"
}
if err := s.repo.Create(ctx, stream); err != nil {
return nil, err
}
return stream, nil
}
Ajouter ListByUser :
func (s *LiveStreamService) ListByUser(ctx context.Context, userID uuid.UUID) ([]*models.LiveStream, error) {
return s.repo.ListByUserID(ctx, userID)
}
Ajouter RegenerateStreamKey :
func (s *LiveStreamService) RegenerateStreamKey(ctx context.Context, id, userID uuid.UUID) (string, error) {
existing, err := s.repo.GetByID(ctx, id)
if err != nil {
return "", err
}
if existing.UserID != userID {
return "", errors.New("stream not found")
}
existing.StreamKey = uuid.New().String()
if err := s.repo.Update(ctx, existing); err != nil {
return "", err
}
return existing.StreamKey, nil
}
Commit : feat(live): stream key generation, ListByUser, RegenerateStreamKey
Step 3 : Handler — Endpoints me, me/key, regenerate, PUT (GL1-04 to GL1-07)
Fichier : veza-backend-api/internal/handlers/live_stream_handler.go — ajouter :
// GetMyStreams returns the authenticated user's streams (including stream_key)
func (h *LiveStreamHandler) GetMyStreams(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok { return }
streams, err := h.service.ListByUser(c.Request.Context(), userID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to list streams", err))
return
}
type streamWithKey struct {
*models.LiveStream
StreamKey string `json:"stream_key"`
}
result := make([]streamWithKey, len(streams))
for i, s := range streams {
result[i] = streamWithKey{LiveStream: s, StreamKey: s.StreamKey}
}
RespondSuccess(c, http.StatusOK, gin.H{"streams": result})
}
// GetMyStreamKey returns the user's stream key (creates draft if none exist)
func (h *LiveStreamHandler) GetMyStreamKey(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok { return }
streams, err := h.service.ListByUser(c.Request.Context(), userID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get streams", err))
return
}
var streamKey string
if len(streams) > 0 {
streamKey = streams[0].StreamKey
} else {
draft := &models.LiveStream{Title: "My Stream"}
created, createErr := h.service.Create(c.Request.Context(), userID, draft)
if createErr != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create stream", createErr))
return
}
streamKey = created.StreamKey
}
RespondSuccess(c, http.StatusOK, gin.H{
"stream_key": streamKey,
"rtmp_url": "rtmp://stream.veza.app/live",
})
}
// RegenerateStreamKey generates a new stream key for the user's first stream
func (h *LiveStreamHandler) RegenerateStreamKey(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok { return }
streams, err := h.service.ListByUser(c.Request.Context(), userID)
if err != nil || len(streams) == 0 {
RespondWithAppError(c, apperrors.NewNotFoundError("no stream found"))
return
}
newKey, err := h.service.RegenerateStreamKey(c.Request.Context(), streams[0].ID, userID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to regenerate key", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{
"stream_key": newKey,
"rtmp_url": "rtmp://stream.veza.app/live",
})
}
// UpdateLiveStream updates a live stream's metadata (ownership check)
func (h *LiveStreamHandler) UpdateLiveStream(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok { return }
id, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid stream ID"))
return
}
var req CreateLiveStreamRequest
if appErr := NewCommonHandler(h.logger).BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
stream := &models.LiveStream{
Title: req.Title, Description: req.Description,
Category: req.Category, ThumbnailURL: req.ThumbnailURL,
StreamerName: req.StreamerName, Tags: req.Tags,
}
updated, updateErr := h.service.Update(c.Request.Context(), id, userID, stream)
if updateErr != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update stream", updateErr))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"stream": updated})
}
Fichier : veza-backend-api/internal/api/routes_live.go — ajouter dans le bloc protected :
protected.GET("/streams/me", liveStreamHandler.GetMyStreams)
protected.GET("/streams/me/key", liveStreamHandler.GetMyStreamKey)
protected.POST("/streams/me/key/regenerate", liveStreamHandler.RegenerateStreamKey)
protected.PUT("/streams/:id", liveStreamHandler.UpdateLiveStream)
Commit : feat(live): add handler endpoints for Go Live (me, key, regenerate, update)
Step 4 : Tests backend live stream (GL1-10)
Fichier : veza-backend-api/internal/services/live_stream_service_test.go (nouveau)
Cas de test (SQLite in-memory, AutoMigrate LiveStream) :
TestCreate_GeneratesStreamKey— Create → stream.StreamKey non vide, format UUIDTestCreate_RequiresTitle— Create avec title="" → erreurTestListByUser_ReturnsOnlyUserStreams— 2 users, 3 streams → ListByUser retourne seulement les streams de l'userTestRegenerateStreamKey_Success— RegenerateStreamKey → nouveau key != ancien keyTestRegenerateStreamKey_WrongUser— RegenerateStreamKey avec mauvais userID → erreurTestSetIsLive_SetsStartedAt— SetIsLive(true) → StartedAt set, EndedAt nilTestSetIsLive_SetsEndedAt— SetIsLive(false) → EndedAt setTestUpdateViewerCount_Clamps— UpdateViewerCount(-100) → viewer_count = 0 (pas négatif)TestUpdate_OwnershipCheck— Update avec mauvais userID → erreur
package services
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"veza-backend-api/internal/models"
"veza-backend-api/internal/repositories"
)
func setupLiveStreamTest(t *testing.T) (*LiveStreamService, *gorm.DB) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.LiveStream{}))
repo := repositories.NewLiveStreamRepository(db)
svc := NewLiveStreamService(repo)
return svc, db
}
func TestCreate_GeneratesStreamKey(t *testing.T) {
svc, _ := setupLiveStreamTest(t)
stream := &models.LiveStream{Title: "Test Stream"}
created, err := svc.Create(context.Background(), uuid.New(), stream)
require.NoError(t, err)
assert.NotEmpty(t, created.StreamKey)
_, parseErr := uuid.Parse(created.StreamKey)
assert.NoError(t, parseErr, "stream key should be valid UUID")
}
func TestCreate_RequiresTitle(t *testing.T) {
svc, _ := setupLiveStreamTest(t)
_, err := svc.Create(context.Background(), uuid.New(), &models.LiveStream{})
assert.Error(t, err)
}
Commit : test(live): add live stream service unit tests
Step 5 : Frontend GoLivePage + GoLiveView (GL2-01 to GL2-04)
Fichier : apps/web/src/features/live/pages/go-live-page/GoLiveView.tsx (nouveau)
import { useState } from 'react';
import { Copy, Eye, EyeOff, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import toast from '@/utils/toast';
interface GoLiveViewProps {
streamKey: string | null;
rtmpUrl: string;
onCreateStream: (data: { title: string; description: string; category: string; tags: string[] }) => Promise<void>;
onRegenerateKey: () => Promise<void>;
isLoading: boolean;
}
export function GoLiveView({ streamKey, rtmpUrl, onCreateStream, onRegenerateKey, isLoading }: GoLiveViewProps) {
const [showKey, setShowKey] = useState(false);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
toast.success('Copied to clipboard');
};
// ... form and stream key display ...
}
export function GoLiveViewSkeleton() {
return <div className="animate-pulse space-y-4">/* skeleton */</div>;
}
Fichier : apps/web/src/features/live/pages/GoLivePage.tsx (nouveau)
import { useState, useEffect } from 'react';
import { GoLiveView, GoLiveViewSkeleton } from './go-live-page/GoLiveView';
import { ErrorDisplay } from '@/components/ui/ErrorDisplay';
import { liveService } from '@/services/liveService';
export function GoLivePage() {
const [streamKey, setStreamKey] = useState<string | null>(null);
const [rtmpUrl, setRtmpUrl] = useState('rtmp://stream.veza.app/live');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
liveService.getMyStreamKey()
.then((res) => { setStreamKey(res.stream_key); setRtmpUrl(res.rtmp_url); })
.catch((e) => setError(e instanceof Error ? e : new Error(String(e))))
.finally(() => setLoading(false));
}, []);
if (loading) return <GoLiveViewSkeleton />;
if (error) return <ErrorDisplay error={error} onRetry={() => window.location.reload()} />;
return (
<GoLiveView
streamKey={streamKey}
rtmpUrl={rtmpUrl}
onCreateStream={async (data) => { await liveService.createStream(data); }}
onRegenerateKey={async () => {
const res = await liveService.regenerateStreamKey();
setStreamKey(res.stream_key);
}}
isLoading={false}
/>
);
}
Fichier : apps/web/src/components/ui/lazy-component/lazyExports.ts — ajouter :
export const LazyGoLive = createLazyComponent(
() => import('@/features/live/pages/GoLivePage').then((m) => ({ default: m.GoLivePage })),
undefined,
'Go Live',
);
Fichier : apps/web/src/router/routeConfig.tsx — ajouter après /live :
{ path: '/live/go-live', element: wrapProtected(<LazyGoLive />) },
Commit : feat(live): add GoLivePage, GoLiveView, lazy export, route
Step 6 : Navbar + liveService + MSW (GL2-05 to GL2-07)
Fichier : apps/web/src/components/layout/Navbar.tsx L217-224 — remplacer le onClick Go Live :
onClick={() => {
onNavigate('go-live');
setShowUserMenu(false);
}}
Fichier : apps/web/src/services/liveService.ts — ajouter :
async getMyStreams(): Promise<{ streams: LiveStream[] }> {
const response = await api.get('/live/streams/me');
return response.data.data;
},
async getMyStreamKey(): Promise<{ stream_key: string; rtmp_url: string }> {
const response = await api.get('/live/streams/me/key');
return response.data.data;
},
async regenerateStreamKey(): Promise<{ stream_key: string; rtmp_url: string }> {
const response = await api.post('/live/streams/me/key/regenerate');
return response.data.data;
},
async updateStream(id: string, data: Partial<LiveStream>): Promise<LiveStream> {
const response = await api.put(`/live/streams/${id}`, data);
return response.data.data.stream;
},
async createStream(data: { title: string; description: string; category: string; tags: string[] }): Promise<LiveStream> {
const response = await api.post('/live/streams', data);
return response.data.data.stream;
},
Fichier : apps/web/src/mocks/handlers-live.ts (nouveau)
import { http, HttpResponse } from 'msw';
export const liveHandlers = [
http.get('*/api/v1/live/streams/me/key', () => {
return HttpResponse.json({
success: true,
data: {
stream_key: 'sk-abc123-def456-ghi789',
rtmp_url: 'rtmp://stream.veza.app/live',
},
});
}),
http.get('*/api/v1/live/streams/me', () => {
return HttpResponse.json({
success: true,
data: { streams: [/* mock stream with key */] },
});
}),
http.post('*/api/v1/live/streams/me/key/regenerate', () => {
return HttpResponse.json({
success: true,
data: {
stream_key: 'sk-new-' + Date.now(),
rtmp_url: 'rtmp://stream.veza.app/live',
},
});
}),
http.put('*/api/v1/live/streams/:id', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ success: true, data: { stream: body } });
}),
];
Enregistrer dans apps/web/src/mocks/handlers.ts : import { liveHandlers } from './handlers-live'; et ajouter ...liveHandlers dans le tableau.
Commit : feat(live): wire Navbar Go Live, liveService methods, MSW handlers
Step 7 : Live Chat WebSocket (GL3-01 to GL3-03)
Fichier : apps/web/src/features/live/pages/live-page/LiveViewChat.tsx — remplacer les données mock par le hook useChat existant :
- Utiliser
JoinConversationavec le stream ID comme room identifier - Écouter les messages entrants via le WebSocket hub existant
- Afficher les messages en temps réel
- Input de message connecté à
SendMessage
Fichier : apps/web/src/features/live/pages/live-page/LiveViewPlayer.tsx — écouter les events viewer count via WebSocket ou polling GET /live/streams/:id
Commit : feat(live): connect LiveViewChat to WebSocket, real-time viewer count
Step 8 : Player enhancements (GL4-01 to GL4-03)
Fichier : apps/web/src/features/player/components/audio-player/ — ajouter un composant PlaybackSpeedButton :
const SPEED_OPTIONS = [0.5, 0.75, 1, 1.25, 1.5, 2] as const;
export function PlaybackSpeedButton({ speed, onSpeedChange }: {
speed: number;
onSpeedChange: (speed: number) => void;
}) {
const cycleSpeed = () => {
const idx = SPEED_OPTIONS.indexOf(speed as typeof SPEED_OPTIONS[number]);
const next = SPEED_OPTIONS[(idx + 1) % SPEED_OPTIONS.length];
onSpeedChange(next);
};
return (
<button onClick={cycleSpeed} className="text-xs font-mono px-1.5 py-0.5 rounded hover:bg-muted">
{speed}x
</button>
);
}
Fichier : apps/web/src/features/player/hooks/useMediaSession.ts (nouveau)
import { useEffect } from 'react';
interface MediaSessionActions {
onPlay: () => void;
onPause: () => void;
onPrevious: () => void;
onNext: () => void;
onSeekBackward: () => void;
onSeekForward: () => void;
}
export function useMediaSession(
track: { title: string; artist?: string; coverUrl?: string } | null,
actions: MediaSessionActions,
) {
useEffect(() => {
if (!('mediaSession' in navigator) || !track) return;
navigator.mediaSession.metadata = new MediaMetadata({
title: track.title,
artist: track.artist ?? 'Unknown',
artwork: track.coverUrl ? [{ src: track.coverUrl, sizes: '512x512', type: 'image/png' }] : [],
});
navigator.mediaSession.setActionHandler('play', actions.onPlay);
navigator.mediaSession.setActionHandler('pause', actions.onPause);
navigator.mediaSession.setActionHandler('previoustrack', actions.onPrevious);
navigator.mediaSession.setActionHandler('nexttrack', actions.onNext);
navigator.mediaSession.setActionHandler('seekbackward', actions.onSeekBackward);
navigator.mediaSession.setActionHandler('seekforward', actions.onSeekForward);
return () => {
navigator.mediaSession.metadata = null;
};
}, [track?.title, track?.artist, track?.coverUrl]);
}
Commit : feat(player): add playback speed control and Media Session API
Step 9 : Stories GoLiveView (GL2-08)
Fichier : apps/web/src/features/live/pages/go-live-page/GoLiveView.stories.tsx (nouveau)
import type { Meta, StoryObj } from '@storybook/react';
import { GoLiveView, GoLiveViewSkeleton } from './GoLiveView';
import { ErrorDisplay } from '@/components/ui/ErrorDisplay';
const meta: Meta<typeof GoLiveView> = {
title: 'Features/Live/GoLiveView',
component: GoLiveView,
};
export default meta;
type Story = StoryObj<typeof GoLiveView>;
export const Default: Story = {
args: {
streamKey: 'sk-abc123-def456-ghi789',
rtmpUrl: 'rtmp://stream.veza.app/live',
onCreateStream: async () => {},
onRegenerateKey: async () => {},
isLoading: false,
},
};
export const Loading: Story = {
render: () => <GoLiveViewSkeleton />,
};
export const Error: Story = {
name: 'Erreur',
render: () => (
<ErrorDisplay
error={new Error('Failed to load stream key')}
onRetry={() => window.location.reload()}
/>
),
};
export const StreamKeyVisible: Story = {
args: {
...Default.args,
streamKey: 'sk-visible-key-for-testing',
},
};
Commit : feat(storybook): add GoLiveView stories (Default, Loading, Error, StreamKeyVisible)
Step 10 : API Reference + Documentation (QA1)
Fichier : docs/API_REFERENCE.md — ajouter section "Live Streaming" :
## Live Streaming
### GET /live/streams
List live streams (public). Optional `?is_live=true` filter.
### GET /live/streams/:id
Get stream details.
### POST /live/streams
Create a new stream (auth required). Body: `{ "title": "...", "description": "..." }`.
### GET /live/streams/me
List authenticated user's streams (includes stream_key). Auth required.
### GET /live/streams/me/key
Get user's stream key + RTMP URL. Creates draft stream if none exist. Auth required.
### POST /live/streams/me/key/regenerate
Regenerate stream key. Auth required.
### PUT /live/streams/:id
Update stream metadata (ownership check). Auth required.
Fichier : CHANGELOG.md — ajouter section v0.703
Fichier : docs/PROJECT_STATE.md — mettre à jour Dernier tag → v0.703, Prochaine version → v0.801, ajouter section v0.703
Fichier : docs/FEATURE_STATUS.md — mettre à jour limitation Go Live (supprimée), ajouter section "Livré en v0.703"
Commit : docs: update API_REFERENCE, CHANGELOG, PROJECT_STATE, FEATURE_STATUS for v0.703
Step 11 : Rétrospective + Scope Control + placeholder v0.801
Fichier : docs/RETROSPECTIVE_V0703.md (nouveau)
Fichier : docs/V0_801_RELEASE_SCOPE.md (nouveau, placeholder)
Fichier : docs/SCOPE_CONTROL.md — Référence active → V0_801_RELEASE_SCOPE.md, historique v0.703 taguée
Fichier : .cursorrules — Scope v0.801
Commit : docs: add RETROSPECTIVE_V0703, placeholder V0_801, update SCOPE_CONTROL
Step 12 : Archive + tag
mv docs/V0_703_RELEASE_SCOPE.md docs/archive/V0_703_RELEASE_SCOPE.md
git add .
git commit -m "chore(docs): archive V0_703_RELEASE_SCOPE"
git tag v0.703
Dépendances entre steps
graph TD
S1[Step1 Migration+Model] --> S2[Step2 Service]
S2 --> S3[Step3 Handler+Routes]
S3 --> S4[Step4 Tests backend]
S5[Step5 GoLivePage] --> S6[Step6 Navbar+MSW]
S6 --> S7[Step7 LiveChat WS]
S8[Step8 Player] --> S9[Step9 Stories]
S4 --> S10[Step10 Docs]
S7 --> S10
S9 --> S10
S10 --> S11[Step11 Retro]
S11 --> S12[Step12 Tag]
Validation finale
cd veza-backend-api && go build ./... && go vet ./... && go test ./... -v
cd apps/web && npm run build
git tag v0.703