From 5b2f230544903f960434880a6b7409fa8f8fdc58 Mon Sep 17 00:00:00 2001 From: senke Date: Sun, 26 Apr 2026 23:50:07 +0200 Subject: [PATCH] =?UTF-8?q?docs(roadmap):=20add=20v1.0=20=E2=86=92=20v2.0.?= =?UTF-8?q?0-public=20launch=20roadmap=20(6=20weeks)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Living operational document tracking the path from v1.0.8 to public launch as a SoundCloud-alternative. Compresses the original 24-week plan to 6 weeks by explicit scope-control: - §2 Scope contract: IN/OUT/COMPRESSED matrix (what ships, what defers post-launch v1.1+, what's MVP-but-shippable) - §1 External actions EX-1 to EX-12 (legal, pentest, DMCA agent, DNS, TLS, CDN, OAuth secrets, Stripe live, transactional email, status page, coturn) with cycle estimates - §4 Day-by-day sprint breakdown for 6 weeks (W1 v1.0.9 + Ansible, W2 Postgres HA + obs, W3 storage HA + signature features, W4 PWA + HLS + faceted search + load test, W5 pentest + game day + canary + status page, W6 GO/NO-GO + soft launch + go-live) - §6 Risk register (R-1 to R-10) with mitigations - §7 Defended scope (refused additions during the 6 weeks) - §8 37 absolute Production-Ready criteria Daily updates expected: tick acceptance criteria as they land, commit each update with `docs: roadmap launch — done`. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/ROADMAP_V1.0_LAUNCH.md | 686 ++++++++++++++++++++++++++++++++++++ 1 file changed, 686 insertions(+) create mode 100644 docs/ROADMAP_V1.0_LAUNCH.md diff --git a/docs/ROADMAP_V1.0_LAUNCH.md b/docs/ROADMAP_V1.0_LAUNCH.md new file mode 100644 index 000000000..6f02095c2 --- /dev/null +++ b/docs/ROADMAP_V1.0_LAUNCH.md @@ -0,0 +1,686 @@ +# ROADMAP — Veza v1.0 Public Launch (4-6 semaines) + +> **Cible** : ouverture publique grand-public en 4-6 semaines, positionnée +> comme alternative sérieuse à SoundCloud, sur infra self-hosted Incus +> (R720 + débordement Hetzner si nécessaire). +> +> **Auteur** : architecte principal Veza + Claude Code Opus 4.7 +> **Dernière mise à jour** : 2026-04-26 +> **État de départ** : v1.0.8 + sprint 1 v1.0.9 partiellement livré +> (Item G subscription en cours, items 1.2/1.3/1.4/1.5/1.6 commit-prêts) + +--- + +## 0. Cadrage honnête + +**Compression** : le plan original (`/home/senke/.claude/plans/quelles-sont-les-prochaines-playful-beaver.md`) +visait 24 semaines pour atteindre une vraie maturité opérationnelle. On +compresse 4× ici. Ça implique de **trancher** sur ce qui rentre et ce +qui n'y rentre pas — la liste explicite est en §2. Si tu doutes pendant +les 6 semaines, reviens à §2 : tout ce qui n'y est pas listé comme IN +attend post-launch. + +**Réalité** : +- Solo dev, ~50h/semaine, pas de marges pour rattraper du retard +- 30 jours ouvrés disponibles, dont ~20% absorbés par l'imprévu (CI + rouge, dette qui remonte, decisions externes) +- Effort réel = 24 jours productifs sur 30 +- Budget temps perdu sur la légal/pentest/ops externes : 0 (ces tâches + doivent tourner en parallèle, JAMAIS sur le critical path code) + +**Verdict honnête** : +- 4 semaines = irréaliste sauf à accepter risques élevés (pas de pentest + externe, ToS template non-revu, infra single-instance) +- **6 semaines = atteignable si discipline scope-control + démarrage + immédiat des tâches externes** +- 8 semaines = confortable + +Je rédige pour 6 semaines avec marge à droite. + +--- + +## 1. À démarrer AUJOURD'HUI en parallèle (externe au code) + +Ces actions ont des cycles longs incompressibles. Elles doivent partir +**maintenant** sinon le lancement glisse de 2-4 semaines. + +| # | Action | Cycle | Owner | Statut | +|---|--------|-------|-------|--------| +| EX-1 | Contacter avocat pour ToS / Privacy / DMCA / DSA | 2-4 sem | toi | ⏳ | +| EX-2 | Demander devis pentest externe (ouvert sur surface v1.0.9) | 1 sem devis + 2 sem exec | toi | ⏳ | +| EX-3 | Enregistrer agent DMCA US Copyright Office (~120 USD, 2-3 sem traitement) | 2-3 sem | toi | ⏳ | +| EX-4 | Désigner point de contact DSA (UE) — soit toi soit représentant local | 1 sem | toi | ⏳ | +| EX-5 | DNS production : `veza.fr`, `api.veza.fr`, `cdn.veza.fr`, `turn.veza.fr`, `embed.veza.fr` | 1 j | toi | ⏳ | +| EX-6 | TLS certs (Let's Encrypt) pour tous les sous-domaines | 1 j | toi | ⏳ | +| EX-7 | Compte CDN edge (Cloudflare R2 + Workers ou Bunny.net) | 1 j | toi | ⏳ | +| EX-8 | Rotation OAuth secrets prod (Google, GitHub, Discord, Spotify) | 0.5 j | toi | ⏳ | +| EX-9 | Stripe / Hyperswitch live mode setup + KYC marketplace | 2-3 sem | toi | ⏳ | +| EX-10 | Email transactionnel : domaine custom + SPF/DKIM/DMARC (Mailjet/Postmark/Brevo) | 1 sem | toi | ⏳ | +| EX-11 | Status page (statuspage.io free OU Cachet self-hosted) | 1 j | toi | ⏳ | +| EX-12 | Coturn déploiement Incus (cf. `infra/coturn/README.md`) | 1 j | toi | ⏳ | + +**Critical path externe** : EX-1 (légal) et EX-2 (pentest) sont les deux +contraintes majeures. Si tu démarres EX-1 le lundi semaine 1, tu auras +les drafts ToS/Privacy semaine 3 et la version finale signée semaine 5. +EX-2 même calendrier : devis semaine 1, exécution semaines 4-5, rapport +semaine 6. Sans ces deux-là on ne tag pas v2.0.0-public. + +--- + +## 2. Scope contract — ce qui rentre vs sort + +### IN (livré avant le tag v2.0.0-public) + +**Code applicatif** +- Sprint 1 v1.0.9 (subscription Item G + items 1.2/1.3/1.4/1.5/1.6 + 1.7 flake stab) +- DMCA notice workflow (formulaire + admin queue + take-down handler) +- Embed widget `/embed/track/:id` + oEmbed +- Track sharing tokens privés (parité avec playlist share) +- Service worker offline track cache (Workbox CacheFirst) +- HLS streaming activé par défaut + ABR validé +- Faceted search UI sur `/search` et `/discover` +- Open Graph tags dynamiques sur les pages tracks/playlists +- Status page côté ops + endpoint `/api/v1/status` public + +**Infrastructure** +- IaC Ansible playbooks (Postgres, Redis, MinIO, RabbitMQ, coturn, HAProxy, backend-api, stream-server) +- Postgres streaming replica + pg_auto_failover (RTO < 60s) +- pgBackRest WAL archive + restore drill cron hebdo +- PgBouncer transaction-mode pool +- Redis Sentinel 3-node +- MinIO erasure-coded 4-node EC4+2 +- HAProxy frontal + 2 backend-api containers actif/actif (sticky WS) +- CDN edge externe (Cloudflare R2 + Workers OU Bunny.net) câblé +- OpenTelemetry collector → Tempo (traces visibles) +- Loki HA 3 réplicas + S3 backend (sur ton MinIO) +- SLO burn-rate alerts (remplacement seuils statiques) +- Backup restore drill automatisé hebdo +- Synthetic monitoring (blackbox exporter rejouant les 6 parcours) +- Game day #1 réalisé + runbooks testés +- Canary release script + +**Légal** +- Terms of Service (avocat-revu) +- Privacy Policy RGPD + CCPA + DPA template processeurs +- DMCA notice + agent enregistré +- DSA point de contact + transparency report template +- CGV vendeur marketplace +- Mentions légales + politique cookies + bannière consentement + +**Sécurité** +- Pentest externe rapport reçu, 0 critique/haut ouvert +- Secrets hors git (Vault sur conteneur Incus OU sops/age versionné) +- Rotation OAuth secrets effectuée +- JWT RS256 prod (déjà en place — vérifier rotation key) + +**Business** +- Stripe live mode actif + KYC créateurs testé E2E avec vrais fonds +- Hyperswitch live mode actif + flux paiement E2E réel +- Email transactionnel sur domaine custom (SPF/DKIM/DMARC OK) +- 5-10 testeurs beta privée semaines 5-6 avec retours documentés + +### OUT (deferred post-launch v1.1+) + +- **Native mobile (iOS/Android)** — Capacitor wrap après launch (~3 semaines) +- **Audio fingerprinting (Chromaprint)** — DMCA workflow MANUEL au launch ; fingerprinting v1.1 +- **Public API SDK** (`@veza/sdk-js`, `@veza/sdk-py`) + dev portal v2 — v1.1 +- **OAuth2 client credentials** + rate limit tiers per-app — v1.1 +- **Multi-region failover** — single region (R720 + Hetzner debordement) au launch +- **Tipping / fan support** — décision business post-launch +- **Distribution intégrale DSP** (TuneCore, CD Baby, etc.) — v1.1 +- **Subscription per-creator (Patreon-style)** — v1.1+ +- **CarPlay / Android Auto** — nécessite app native d'abord +- **Lossless tier FLAC subscriber** — codec déjà supporté, exposer post-launch +- **"Fresh from your follows" tab discovery** — déferred (chronologique du feed suffit au launch) +- **Editorial collections admin UI** — défferred (curation manuelle DB au launch) +- **Faceted search saved searches** — UI faceted suffit, "saved" v1.1 + +### COMPRESSED (livré mais minimum viable, à durcir post-launch) + +- **MinIO erasure-coded** : 4 nœuds EC4+2 mais sur la même machine R720 (pas vraiment HA matériel — ajoute Hetzner storage post-launch) +- **HAProxy sticky WS** : 2 backend-api sur la même machine R720 (pas N+1 hardware HA) +- **Loki HA** : 3 réplicas en conteneurs Incus mais sur le même hôte (single-host failure mode, accepté pour launch) +- **DMCA workflow** : workflow complet + agent enregistré, MAIS audit fingerprinting reporté → take-down purement humain +- **Pentest** : externe mais light scope (auth + paiement + DMCA + uploads), pas full deep dive +- **Game day** : 1 game day cycle complet, pas 3 cycles répétés + +--- + +## 3. Critical path & dépendances + +``` + EX-1 légal ──────────────────────────────▶ EX week 5 signature ──▶ + EX-2 pentest ────────────────────▶ EX week 4 exec ──▶ rapport week 6 + EX-9 stripe live ────────▶ KYC ──▶ test E2E vrais fonds week 5 + EX-10 email custom ──────▶ DKIM/SPF prop ────▶ délivrabilité week 4 + + Code: + W1 finir v1.0.9 + IaC ──▶ W2 Postgres HA + obs ──▶ W3 stockage + features ──▶ + W4 PWA + search + load test ──▶ W5 game day + canary ──▶ W6 go-live +``` + +**Top-3 risques de glissement** : +1. **Légal** : si EX-1 traîne, on tag mais on ne lance pas. Démarre lundi. +2. **Pentest findings critiques** : si le pentest revèle un finding HIGH semaine 6, on rétrograde de tag et on patch — peut décaler 1-2 semaines. Mitigation : faire un pentest interne semaine 4 (ZAP + nuclei + manual audit) pour pré-filtrer. +3. **Postgres HA failover en game day rouge** : si le failover n'atteint pas RTO < 60s on doit ré-architecturer. Mitigation : tester pg_auto_failover en lab Incus dès semaine 1. + +--- + +## 4. Sprint breakdown — jour par jour + +> **Convention** : chaque jour = ~6-8h productives. Acceptance criteria +> mesurables. Si un jour déborde, le suivant glisse — pas de "je +> rattraperai le week-end" autorisé sinon le burn-out garantit le slip +> de 2 semaines fin de roadmap. + +### Semaine 1 — Closer v1.0.9 + amorçage IaC + +**Objectif** : tag v1.0.9, scaffolding Ansible posé, Postgres HA en lab. + +#### Jour 1 — Lundi : Démarrer les externes + commit sprint 1 +- **AM** : Envoyer EX-1 (avocat), EX-2 (devis pentest), EX-3 (DMCA agent), EX-9 (Stripe KYC), EX-10 (email transactionnel). Ces 5 emails partent avant midi. +- **PM** : Commit + push les 4 commits préparés (`/tmp/v1.0.9-commit-messages.md`). Vérifier CI Forgejo verte. Si rouge → debug avant de continuer. +- **Acceptance** : 5 emails envoyés, 4 commits pushés, CI verte sur main. +- **Files** : `/tmp/v1.0.9-commit-messages.md` + +#### Jour 2 — Mardi : Item G subscription Phase 2 +- Webhook handler `subscription.payment_succeeded` / `payment_failed` dans `internal/services/hyperswitch/webhook_subscription.go` +- Dispatcher dans `ProcessPaymentWebhook` qui détecte le type subscription et route +- Reuse `webhook_raw_payloads` table (item E v1.0.7) pour persistence +- Tests : webhook arrive → row pending_payment → active. Webhook payment_failed → expired. +- **Acceptance** : 4 tests unitaires verts (success, failed, replay idempotent, unknown event) +- **Files** : `internal/services/hyperswitch/webhook_subscription.go` (nouveau), `internal/services/hyperswitch/webhook_subscription_test.go` (nouveau) + +#### Jour 3 — Mercredi : Item G subscription Phase 3 + E2E +- Endpoint recovery `POST /api/v1/subscriptions/complete/:id` qui retourne le `client_secret` Hyperswitch pour reprendre un paiement stalled +- Distribution gate : `distribution.checkEligibility` traite `pending_payment` comme ineligible +- E2E `@critical` `tests/e2e/subscription-pending-payment.spec.ts` : subscribe paid plan → distribution submit retourne 403 → simuler webhook → distribution OK +- Remove `TODO(v1.0.7-item-G)` annotation dans `subscription/service.go` +- **Acceptance** : E2E vert + grep TODO Item G = 0 hit +- **Files** : `internal/handlers/subscription_handler.go`, `internal/api/routes_subscription.go`, `internal/core/distribution/service.go`, `tests/e2e/28-subscription-pending-payment.spec.ts` + +#### Jour 4 — Jeudi : Item 1.7 flake stab + tag v1.0.9 +- Lire les rapports CI Forgejo des 5 derniers runs E2E sur main +- Identifier flakes : tests qui ont failé puis passé sans changement code +- Stabiliser : remplacer `waitForTimeout` par `waitFor` ciblés, ajouter retry-on-flake helpers, augmenter `expect.timeout` ciblé +- 3 cycles E2E nightly verts consécutifs avant tag +- **Acceptance** : 3 nightly E2E verts, `git tag v1.0.9` poussé +- **Files** : `tests/e2e/*.spec.ts` (stabilisations ciblées), `tests/e2e/playwright.config.ts` (timeout ciblé si nécessaire) + +#### Jour 5 — Vendredi : IaC scaffolding Ansible +- Créer `infra/ansible/` avec layout standard : `inventory/`, `group_vars/`, `host_vars/`, `roles/`, `playbooks/` +- Inventaire : 1 host R720 prod + 1 host Hetzner staging + 1 host R720 lab +- Rôle `common` : SSH hardening, fail2ban, unattended-upgrades, monitoring agent +- Rôle `incus_host` : install Incus + configuration networking +- Test : déployer le rôle common sur le host lab +- **Acceptance** : `ansible-playbook -i inventory/lab playbooks/site.yml --check` passe sans erreur +- **Files** : `infra/ansible/inventory/{lab,staging,prod}.yml`, `infra/ansible/playbooks/site.yml`, `infra/ansible/roles/common/tasks/main.yml`, `infra/ansible/roles/incus_host/tasks/main.yml`, `infra/ansible/README.md` + +**Verification gate W1** : tag v1.0.9 poussé, Ansible scaffolding fonctionne en lab, externes EX-1 à EX-10 démarrés. + +--- + +### Semaine 2 — Postgres HA + observabilité + +**Objectif** : Postgres failover automatique testé, OpenTelemetry collector wired, SLO alerts définies. + +#### Jour 6 — Lundi : Postgres HA — pg_auto_failover lab +- Rôle Ansible `postgres_ha` : 2 conteneurs Incus (primary + replica), pg_auto_failover monitor +- Configuration : sync replication, HOT_STANDBY, max_wal_senders, etc. +- Test : kill primary, vérifier failover automatique en < 60s, vérifier replica devient primary +- **Acceptance** : failover test scripté `infra/ansible/tests/test_pg_failover.sh` rouge avant fix, vert après. RTO mesuré < 60s. +- **Files** : `infra/ansible/roles/postgres_ha/{tasks,templates,defaults}/`, `infra/ansible/tests/test_pg_failover.sh` + +#### Jour 7 — Mardi : PgBouncer + connection pool +- Rôle Ansible `pgbouncer` : conteneur Incus avec PgBouncer en transaction mode +- Pool size : 50 server connections × 1000 client connections capacity +- Backend-api config : `DATABASE_URL` pointe vers PgBouncer, pas direct Postgres +- Load test : 500 connexions concurrentes simulées via `pgbench`, vérifier pas d'épuisement +- **Acceptance** : pgbench 500 clients × 30s sans erreur de connexion +- **Files** : `infra/ansible/roles/pgbouncer/{tasks,templates}/`, `veza-backend-api/internal/config/config.go` (URL pointe PgBouncer en prod) + +#### Jour 8 — Mercredi : pgBackRest + restore drill +- Rôle Ansible `pgbackrest` : install + configuration WAL archive vers MinIO bucket dédié +- Backup full hebdo + différentiels quotidiens + WAL continu +- Script `scripts/dr-drill.sh` : restore le dernier full+WAL dans un conteneur éphémère, lance `cmd/tools/seed --ci` partial check, assertion `SELECT count(*) FROM users > 0` +- Cron hebdo qui execute le script + alerte Prometheus si fail +- **Acceptance** : `bash scripts/dr-drill.sh` passe vert localement +- **Files** : `infra/ansible/roles/pgbackrest/{tasks,templates}/`, `scripts/dr-drill.sh`, `config/prometheus/alert_rules.yml` (ajout `BackupRestoreDrillFailed`) + +#### Jour 9 — Jeudi : OpenTelemetry collector + Tempo +- Rôle Ansible `otel_collector` : conteneur OpenTelemetry collector (config receivers + processors + exporters) +- Conteneur Tempo (Grafana stack) pour stocker les traces +- Backend-api : exporter OTLP vers le collector (déjà câblé en émetteur, juste activer le wire) +- Instrumenter 4 hot paths : auth login, track upload (chunked init/chunk/complete), payment webhook, search +- Dashboard Grafana basique avec service map + p99 par endpoint +- **Acceptance** : tracer une requête login → voir le span dans Tempo UI +- **Files** : `infra/ansible/roles/otel_collector/`, `config/otel/collector-config.yaml`, `veza-backend-api/internal/tracing/otlp_exporter.go` (nouveau ou extension), `config/grafana/dashboards/service-map.json` + +#### Jour 10 — Vendredi : SLO burn-rate alerts + runbooks +- Définir 3 SLO dans `config/prometheus/slo.yml` : + - `SLO_API_AVAILABILITY` : 99.5% sur `/api/v1/health` + endpoints lecture + - `SLO_API_LATENCY` : 99% p95 < 500ms sur endpoints écriture + - `SLO_PAYMENT_SUCCESS` : 99.5% des `/api/v1/orders` POST → 201 +- Multi-window burn-rate alerts (fast 1h burn 14.4× → page, slow 6h burn 6× → ticket) +- Annotations `runbook_url` sur chaque alert pointant `docs/runbooks/.md` +- Squelette runbooks pour les 6 alerts les plus probables +- **Acceptance** : `promtool check rules config/prometheus/slo.yml` vert, dashboard Grafana SLO visible avec error budget tracking +- **Files** : `config/prometheus/slo.yml`, `config/alertmanager/routes.yml` (acheminement page vs ticket), `docs/runbooks/{api-availability-slo-burn,payment-success-slo-burn,db-failover,redis-down,disk-full,cert-expiring-soon}.md` + +**Verification gate W2** : `pg_auto_failover` test rouge → vert, `pgbench` 500 connections OK, traces visibles dans Tempo, SLO burn-rate alerts définies + runbooks indexés. + +--- + +### Semaine 3 — Stockage HA + features signature + +**Objectif** : MinIO erasure-coded, CDN edge wired, embed widget + DMCA workflow + track sharing. + +#### Jour 11 — Lundi : Redis Sentinel HA + cache metrics +- Rôle Ansible `redis_sentinel` : 3 conteneurs Incus (1 master + 2 replicas + 3 sentinels colocated) +- Backend-api : `RedisClient` switch vers `NewFailoverClient` avec MasterName + SentinelAddrs +- Cache hit rate metrics : ajouter compteurs Prometheus dans rate limiter, chat pubsub, presence +- Test : kill Redis master, vérifier promotion automatique d'un replica en < 30s +- **Acceptance** : failover Redis test rouge → vert, dashboard Grafana avec hit rate par service +- **Files** : `infra/ansible/roles/redis_sentinel/`, `veza-backend-api/internal/config/config.go` (Sentinel config), `veza-backend-api/internal/metrics/cache_hit_rate.go` (nouveau) + +#### Jour 12 — Mardi : MinIO distribué + migration +- Rôle Ansible `minio_distributed` : 4 conteneurs Incus en pool EC4+2 (résiste perte 2 conteneurs sans data loss) +- Configuration : bucket `veza-prod-tracks`, lifecycle policy (versioning 30j, Glacier après 90j) +- Migration depuis le single-node actuel via `mc mirror veza-current/ veza-distributed/` +- Backend-api : pas de changement code (interface S3 reste identique) +- Smoke test : upload 100MB via `mc cp`, kill 2 nœuds, vérifier read OK, restart nœuds, vérifier rebuild +- **Acceptance** : EC4+2 résiste à 2 nœud kills, dashboard MinIO healthcheck vert +- **Files** : `infra/ansible/roles/minio_distributed/{tasks,templates}/` + +#### Jour 13 — Mercredi : CDN edge externe (Cloudflare R2 ou Bunny.net) +- Choisir : Cloudflare R2 (gratuit 10GB, $0.015/GB stockage, $0/GB egress) OU Bunny.net Stream ($0.005/GB egress audio) +- Configurer compte + bucket + cache rules + signed URL +- Câbler `internal/services/cdn_service.go` (déjà existant) pour générer URLs CDN au lieu de MinIO direct +- Trackservice `GetStorageURL` : retourner CDN signed URL pour les routes publiques (stream, download) +- Cache headers backend-api : Cache-Control: public, max-age=86400, immutable pour les segments HLS +- Test : upload track, lire via /tracks/:id/stream → assert URL = `cdn.veza.fr/...` +- **Acceptance** : 1 track lue via CDN edge, latence first byte < 200ms depuis 3 PoPs +- **Files** : `veza-backend-api/internal/services/cdn_service.go` (extend), `veza-backend-api/internal/core/track/service.go` (GetStorageURL hook CDN), `infra/ansible/roles/backend_api/templates/.env.j2` (env vars CDN) + +#### Jour 14 — Jeudi : DMCA notice handler + workflow +- Migration `987_dmca_notices.sql` : table `dmca_notices` (id, status, claimant_email, claimant_name, claimant_address, work_description, infringing_track_id, sworn_statement_at, status, takedown_at, counter_notice_at, restored_at, audit_log) +- Handler `internal/handlers/dmca_handler.go` : + - `POST /api/v1/dmca/notice` : public, rate-limited (5/IP/h), validate fields, enregistre `pending` + - `GET /api/v1/admin/dmca/notices` : admin-only, queue + pagination + - `POST /api/v1/admin/dmca/notices/:id/takedown` : admin action → set `status=takedown`, set `takedown_at=NOW()`, mark track `is_public=false` + `dmca_blocked=true` + - `POST /api/v1/admin/dmca/notices/:id/dismiss` : admin action → set `status=rejected` +- Page publique frontend `/legal/dmca` avec formulaire + agent désigné info +- Audit log dans `audit_logs` (existing v0.803 SEC2) +- **Acceptance** : E2E `tests/e2e/29-dmca-notice.spec.ts` vert (notice → admin queue → takedown → track plus accessible) +- **Files** : `veza-backend-api/migrations/987_dmca_notices.sql` + rollback, `veza-backend-api/internal/handlers/dmca_handler.go` (nouveau), `veza-backend-api/internal/api/routes_legal.go` (nouveau), `apps/web/src/features/legal/pages/DmcaNoticePage.tsx` (nouveau), `tests/e2e/29-dmca-notice.spec.ts` + +#### Jour 15 — Vendredi : Embed widget + oEmbed + track share tokens +- Route backend `GET /embed/track/:id` : retourne page HTML standalone (pas le SPA, page minimaliste avec player + waveform SVG) +- Route backend `GET /oembed?url=&format=json` : réponse oEmbed standard (type=rich, html=`