# Rapport — Finalisation du module Vidéos

Rédigé le 22 juillet 2026. Conformément à la consigne, la direction artistique terminée précédemment
(voir [docs/rapport-direction-artistique.md](rapport-direction-artistique.md)) n'a pas été altérée :
mêmes couleurs, même typographie, mêmes animations discrètes, même approche narrative. Aucun module
Mouvement, ATL, Contact, Médiathèque ou Chronologie n'a été modifié.

---

## 1. État initial du module vidéo (audit)

Un module vidéo existait déjà et a été entièrement réutilisé :

- **Modèle** `App\Models\Video` : `youtube_id`, `title`, `description`, `category`, `published_on`,
  `featured`, `sort_order`, `published`. Méthodes `thumbnailUrl()` (hqdefault uniquement),
  `watchUrl()`, `embedUrl()` (déjà sur `youtube-nocookie.com`, sans gestion du temps de départ).
- **Migration** `2026_07_20_205342_create_videos_table.php` : table simple, sans soft delete, sans
  champ de temps de démarrage.
- **Contrôleur public** `VideoController@index` : logique déjà correcte — `$videos->firstWhere('featured', true) ?? $videos->first()` (vidéo principale avec repli sur la première publiée).
- **Route publique** `GET /videos` (`videos.index`) : déjà en place, déjà dans la navigation
  principale et le footer.
- **Vue publique** `resources/views/videos/index.blade.php` : existait, mais avec un iframe brut
  toujours chargé (pas de façade), sans filigrane ni typographie de la nouvelle direction
  artistique.
- **Intégration accueil** : `HomeController` exposait déjà `$video` (vidéo « featured »), affichée
  dans `home.blade.php` via un iframe brut équivalent.
- **Statistiques** : le rapport de direction artistique comptait déjà les vidéos publiées dans « Un
  parcours en quelques chiffres » (`Video::where('published', true)->count()`, combiné aux
  documents).
- **Aucune administration** : aucun contrôleur, aucune route, aucune vue `/admin/videos` n'existait.
- **Seeder** `VideoSeeder` : une seule vidéo précédemment identifiée (`L43z6VfRm0k`).
- **Tests** : une seule assertion (`PublicPagesTest::test_vision_mouvement_videos_presse_pages`)
  vérifiant que `/videos` répond 200 et affiche « telithotenge73 ».

**Décision de départ, conforme à la règle finale** : aucune seconde table ni second système vidéo
n'a été créé. Le modèle, la migration, le contrôleur et la route publics existants ont été complétés,
jamais remplacés.

## 2. Architecture réutilisée

- Table `videos`, modèle `Video`, contrôleur `VideoController`, route `videos.index`, vue
  `videos/index.blade.php`, intégration dans `HomeController`/`home.blade.php`, statistique
  « documents et vidéos d'archives » de l'accueil, navigation et footer publics (le lien « Vidéos »
  y figurait déjà).
- Convention d'administration (contrôleur/routes/vues) calquée sur `AlbumAdminController` déjà
  existant, pour rester cohérent avec le reste de l'admin.
- Journal d'activité : réutilisation de `MediaActivityLog::record()` (déjà utilisé par tous les
  autres modules admin), avec `subject_type = 'Video'`.

## 3. Champs ajoutés (une seule migration, aucune nouvelle table)

Migration `2026_07_22_192358_add_playback_fields_to_videos_table.php`, qui **complète** la table
`videos` existante :

| Champ ajouté | Raison |
|---|---|
| `provider` (string, défaut `youtube`) | Prépare une éventuelle diversification future, sans usage actif hors YouTube |
| `start_seconds` (unsigned int, défaut 0) | Temps de démarrage extrait du lien (ex. 14) |
| `original_url` (string, nullable) | Conserve le lien brut collé par l'administrateur, pour affichage/audit — jamais utilisé pour construire l'iframe (toujours reconstruite depuis `youtube_id` + `start_seconds`, validés) |
| `deleted_at` (soft delete) | Suppression logique + restauration, demandées explicitement |

**Champs délibérément non ajoutés**, pour ne pas dupliquer l'existant ni sur-ingénierer :

- `video_id` — déjà couvert par `youtube_id`.
- `is_featured` — déjà couvert par `featured`.
- `display_order` — déjà couvert par `sort_order`.
- `published_at` — le champ `published` (booléen) suffit ; `published_on` couvre déjà la date/contexte éditorial.
- `thumbnail_url` — toujours dérivable de `youtube_id` (aucun intérêt à le stocker, risque de
  désynchronisation) ; le repli maxres → hq → mq est géré côté client (voir §8).
- `slug` — aucune page de détail par vidéo n'existe ni n'est demandée ; l'identifiant numérique suffit
  pour les routes d'administration.

## 4. Vidéo principale configurée

`https://www.youtube.com/watch?v=Be4wQTgC0G0&t=14s` a été ajoutée (via `VideoSeeder`, exécuté sur la
base de développement réelle) et définie comme vidéo principale (`featured = true`) :

- `youtube_id = Be4wQTgC0G0`
- `start_seconds = 14`
- `original_url` conservé tel que collé
- `embedUrl()` retourne `https://www.youtube-nocookie.com/embed/Be4wQTgC0G0?start=14`

La vidéo précédemment identifiée (`L43z6VfRm0k`) **n'a pas été supprimée** : elle reste en base,
dépubliée du statut « principale » mais toujours publiée et visible dans la section « à voir aussi ».

## 5. Route publique

`GET /videos` (nom de route `videos.index`) — conservée telle quelle, déjà correcte et déjà dans la
navigation. Aucune nouvelle route publique nécessaire.

## 6. Interface d'administration

Nouveau module `/admin/videos` (`VideoAdminController`, routes ajoutées dans le groupe
`admin`+middleware `admin` existant) :

| Route | Action |
|---|---|
| `GET /admin/videos` | Liste (avec filtre corbeille) |
| `GET /admin/videos/create` | Formulaire de création |
| `POST /admin/videos` | Création |
| `GET /admin/videos/{video}/edit` | Édition + aperçu (façade cliquable, jamais de lecture automatique) |
| `PUT /admin/videos/{video}` | Mise à jour |
| `POST /admin/videos/{video}/feature` | Définir comme vidéo principale |
| `DELETE /admin/videos/{video}` | Suppression logique |
| `POST /admin/videos/{id}/restore` | Restauration |

Lien « Vidéos » ajouté dans le menu latéral de l'administration
(`resources/views/components/admin-layout.blade.php`), juste après « Chronologie ».
L'administrateur ne saisit jamais qu'un lien YouTube complet — jamais un identifiant seul, jamais un
iframe.

## 7. Validation des liens YouTube

`Video::parseYoutubeUrl(string $url)` (méthode statique du modèle, testée indépendamment) :

- Domaines acceptés : `youtube.com`, `www.youtube.com`, `m.youtube.com`, `youtu.be`, `www.youtu.be`,
  `youtube-nocookie.com`. Tout autre domaine est rejeté.
- Formes reconnues : `watch?v=...`, `youtu.be/...`, `embed/...`.
- Identifiant validé par motif strict (`^[A-Za-z0-9_-]{11}$`).
- Le lien est d'abord passé à `strip_tags()` : toute balise HTML/JS présente dans le champ fait
  échouer la validation immédiatement (protection contre l'injection).
- Message d'erreur clair renvoyé comme erreur de validation Laravel classique (`$errors->first()`),
  jamais une page technique.

## 8. Extraction de l'identifiant et du temps de démarrage

Le temps de départ est lu depuis les paramètres `t` ou `start` de l'URL, avec prise en charge de
trois formats : secondes brutes (`14`), suffixées (`14s`), ou composées (`1h2m3s`, `1m30s`). Un
paramètre de temps non reconnu vaut `0` plutôt que de faire échouer tout le lien pour un détail
secondaire.

Stratégie de repli pour la miniature (`maxresdefault` → `hqdefault` → `mqdefault`) : implémentée
**côté client** (JavaScript, `resources/js/app.js`) via l'évènement `error` de l'`<img>`, plutôt que
par une vérification serveur de l'existence du fichier distant (ce qui aurait ajouté une requête
HTTP externe bloquante à chaque chargement de page — fragile et lent). Aucun téléchargement ni
réhébergement de la vidéo ou de ses images n'est effectué.

## 9. Mise en scène (façade vidéo)

Composant `resources/views/components/youtube-facade.blade.php` + logique dans
`resources/js/app.js` : la page affiche uniquement une miniature et un bouton Lecture accessible
(`aria-label`) tant que le visiteur n'a pas cliqué. L'iframe YouTube (domaine
`youtube-nocookie.com`, `autoplay=1` uniquement injecté **après** le clic explicite de l'utilisateur)
n'est créée qu'à ce moment — aucune requête vers YouTube, aucun cookie tiers, aucune lecture
automatique non sollicitée au chargement de la page. Réutilisée à trois endroits : page `/videos`
(vidéo principale), page d'accueil, et aperçu dans l'administration.

Page `/videos` restructurée selon la direction artistique : hero sombre avec filigrane du logo,
vidéo principale mise en scène (légende, date si connue, lien YouTube), section « à voir aussi »
pour les autres vidéos publiées (uniquement si elles existent réellement en base — aucun contenu
inventé), appel final vers Parcours / Galerie / Mouvement.

## 10. Performances

- Aucune bibliothèque externe ajoutée (façade en JavaScript natif, cohérent avec le reste du site).
- Aucune requête réseau vers YouTube avant le clic explicite sur Lecture.
- Miniatures en `loading="lazy"`.
- `<iframe loading="lazy">` volontairement omis sur l'iframe injectée dynamiquement : la façade
  elle-même est une stratégie de lazy-loading plus forte que l'attribut natif (chargement différé
  jusqu'à l'interaction, pas seulement jusqu'à la proximité du viewport).
- Aucun impact CLS : la façade occupe un ratio `aspect-video` fixe dès le rendu serveur : l'iframe
  injecté ensuite prend exactement le même espace.
- Build de production vérifié (`npm run build`) après chaque étape, sans erreur.

## 11. Accessibilité

- Bouton Lecture : élément `<button>` natif, utilisable au clavier, `aria-label` explicite incluant
  le titre de la vidéo.
- Texte alternatif pertinent sur la miniature (`alt="Miniature de la vidéo « {titre} »"`).
- Titre explicite sur l'iframe injectée (`title` = titre de la vidéo).
- `:focus-visible` global du site conservé, aucun style ne le supprime sur le bouton Lecture.
- Aucun piège clavier (un seul élément interactif, pas de modale).
- Contenu compréhensible sans animation (miniature statique + bouton, aucune information portée
  uniquement par un mouvement).

## 12. SEO et partage

Page `/videos` : titre, meta description, Open Graph (image = miniature de la vidéo principale),
balises Twitter (déjà utilisées ailleurs sur le site), URL canonique (mécanisme déjà présent dans
`x-layout`). JSON-LD `VideoObject` ajouté (`name`, `description`, `thumbnailUrl` avec ses trois
niveaux de repli, `embedUrl`), avec `uploadDate` **uniquement** si `published_on` est renseigné en
base — jamais de date inventée (vérifié par test : `test_videos_page_json_ld_never_invents_an_upload_date`).
Le bloc JSON-LD n'est jamais visible comme texte (même mécanisme `{!! !!}` avec échappement de
`</script` déjà utilisé ailleurs sur le site, vérifié par test).

## 13. Tests

96 tests existants + **38 nouveaux tests** (`tests/Feature/VideoTest.php`,
`tests/Feature/VideoAdminTest.php`) = **134 tests, 338 assertions, tous passants**.

Couverture ajoutée : affichage de la page vidéos, affichage de la vidéo principale, identifiant
`Be4wQTgC0G0`, démarrage à 14 secondes, domaine `youtube-nocookie.com`, absence d'autoplay/iframe au
chargement initial, accès admin protégé (redirection si non authentifié, refus si non-admin),
modification du lien, extraction depuis `youtube.com/watch`, `youtu.be`, paramètres `t=14s` et
`start=14`, rejet d'un domaine non YouTube, rejet d'un identifiant invalide, exclusivité de la vidéo
principale (y compris après plusieurs bascules successives), vidéo non publiée invisible
publiquement, état vide sans erreur, suppression logique et restauration, journalisation
(création et mise en avant), JSON-LD non visible comme texte et sans date inventée, conservation de
la statistique vidéo de l'accueil.

## 14. Résultats et bug corrigé en cours de route

Suite complète verte. Un bug réel a été détecté et corrigé pendant la validation manuelle réelle : le
journal d'activité (`MediaActivityLog::record()`, utilisé par **tous** les modules d'administration,
pas seulement les vidéos) pouvait lever une exception si une valeur journalisée contenait une
séquence UTF-8 invalide, provoquant une erreur 500 **après** que la modification métier avait déjà
été enregistrée en base — un échec de journalisation (préoccupation secondaire) ne doit jamais faire
échouer ni laisser incohérente l'action principale déjà effectuée. Corrigé en encapsulant l'écriture
du journal dans un bloc `try/catch` (échec journalisé via les logs applicatifs, jamais remonté à
l'utilisateur) et en nettoyant automatiquement toute chaîne mal encodée avant l'encodage JSON
(`mb_scrub`). Cette correction bénéficie à tous les modules admin existants, pas seulement aux
vidéos.

## 15. Limitations

- **Validation visuelle réelle non réalisée** : l'outil de navigateur intégré à cet environnement de
  développement bloque les captures d'écran sur le domaine personnalisé `tlt.test` derrière une
  carte d'approbation manuelle que je ne peux pas m'accorder moi-même (limitation déjà rencontrée et
  documentée lors des phases précédentes). La vérification s'est donc appuyée sur des requêtes HTTP
  réelles (`curl`), l'inspection du HTML rendu, et une relecture du code — pas sur une inspection
  visuelle directe. **Une vérification humaine finale du rendu (notamment mobile, et le comportement
  réel du bouton Lecture dans un vrai navigateur) reste recommandée avant mise en production.**
- La bannière de couverture par période de la chronologie (phase précédente) et la façade vidéo
  partagent la même limitation de vérification visuelle.
- Aucune vidéo autre que celle demandée et celle déjà présente n'a été ajoutée ou inventée.

## 16. Procédure pour remplacer la vidéo depuis l'administration

1. Se connecter à `/admin/login` avec un compte administrateur.
2. Aller dans « Vidéos » (menu latéral) ou directement `/admin/videos`.
3. Cliquer sur la vidéo à modifier (ou « + Ajouter une vidéo » pour en créer une nouvelle).
4. Coller le nouveau lien YouTube complet dans le champ « Lien YouTube » (aucune manipulation
   d'identifiant ou d'iframe requise).
5. Ajuster le titre, la description, la catégorie ou la date si nécessaire.
6. Cocher « Définir comme vidéo principale » si cette vidéo doit remplacer l'actuelle vidéo mise en
   avant (l'ancienne est automatiquement désélectionnée, jamais supprimée).
7. Cliquer sur « Enregistrer les modifications » — la page d'édition affiche un aperçu cliquable
   (aucune lecture automatique) pour vérifier immédiatement le résultat.
8. Décocher « Publiée » pour retirer temporairement une vidéo de l'affichage public sans la
   supprimer.

---

## 17. Problème de lecture intégrée YouTube — vérification anti-robot

### Symptôme signalé

Après clic sur « Lecture » dans la façade vidéo, l'iframe YouTube se charge correctement mais
affiche ensuite, à l'intérieur du cadre, le message « Connectez-vous pour confirmer que vous n'êtes
pas un robot ». La vidéo (`Be4wQTgC0G0`, démarrage à 14s) reste inaccessible en lecture directe sur
le site pour le visiteur concerné.

### Diagnostic

Avant toute modification, l'état réel du système a été vérifié point par point :

- **URL injectée avant correctif** : `https://www.youtube-nocookie.com/embed/{id}?start={n}&autoplay=1`
  — domaine correct, mais paramètres minimaux (aucun `rel`, `playsinline`, `modestbranding`), et le
  choix du domaine était câblé en dur dans `resources/js/app.js`, dupliquant une décision qui existait
  déjà partiellement dans `App\Models\Video::embedUrl()` — non conforme à « ne pas dupliquer cette
  logique dans plusieurs vues ».
- **Attribut `allow`** : présent, mais sans `web-share`. Aucun attribut `sandbox` n'a jamais été
  utilisé sur cette iframe — donc aucune restriction de ce type ne pouvait être en cause (vérifié en
  lisant `resources/js/app.js` avant modification : pas de `iframe.sandbox` dans le code).
- **Referrer-Policy** : l'attribut `referrerpolicy="strict-origin-when-cross-origin"` était déjà
  posé sur l'iframe elle-même, mais **aucun en-tête HTTP `Referrer-Policy` ni `Content-Security-Policy`
  n'existait au niveau du site** (vérifié par `curl -D -` sur `/videos` avant correctif : aucune de
  ces deux en-têtes n'était renvoyée).
- **Origine transmise à YouTube** : aucun paramètre `origin` n'était transmis (l'API IFrame n'était
  pas utilisée), donc sans effet sur le comportement observé.
- **CSP** : absente avant correctif — ne pouvait donc pas être la cause du blocage (une CSP absente
  n'empêche rien).
- **Fonctionnement sur `http://tlt.test`** : la façade se charge et affiche correctement la miniature
  YouTube (vérifié par requête HTTP réelle). Le comportement au clic (côté iframe, document
  cross-origine) n'est pas observable par une requête HTTP simple ni par le JavaScript du site.

### Tests réalisés (§9)

Réalisés en dehors de `tlt.test`, sur une page neutre (`https://example.com`, via injection DOM
contrôlée), pour isoler la variable "notre application" de la variable "YouTube/réseau" :

| Test | Résultat |
|---|---|
| Intégration `youtube-nocookie.com/embed/Be4wQTgC0G0?start=14&rel=0&playsinline=1&modestbranding=1` dans une page neutre | Se charge correctement : miniature et bouton de lecture YouTube natifs visibles. |
| Intégration `youtube.com/embed/Be4wQTgC0G0?start=14&rel=0&playsinline=1&modestbranding=1` dans une page neutre | Se charge également correctement, rendu similaire. |
| Navigation directe (hors iframe) vers l'URL d'embed | Retourne une erreur YouTube distincte (« Erreur 153 — erreur de configuration du lecteur »), attendue : YouTube refuse qu'une URL `/embed/` soit ouverte comme page de premier niveau au lieu d'être réellement intégrée dans un `<iframe>` — ce comportement est normal et indépendant du problème signalé. |
| Clic effectif sur le bouton de lecture pour déclencher la vérification anti-robot | **Non concluant** : l'automatisation de clic dans un `<iframe>` cross-origine profondément imbriqué (le lecteur YouTube charge lui-même un second niveau d'iframe interne) s'est révélée peu fiable dans cet environnement d'outillage — le clic ne s'est pas propagé de façon certaine jusqu'au bouton natif de YouTube. Ce point n'a donc **pas pu être reproduit de façon certaine par l'automatisation**, ce qui est documenté ici honnêtement plutôt que de prétendre le contraire. |
| Chrome normal / navigation privée / Firefox / Edge / sans VPN / sans bloqueur / autre réseau | **Non testés** : cet environnement de développement ne dispose que d'un seul navigateur automatisé sans accès à un second réseau, VPN ou téléphone physique. Cette limite est documentée ci-dessous plutôt que passée sous silence. |

**Conclusion du diagnostic** : les deux domaines (`youtube.com` et `youtube-nocookie.com`) chargent
correctement l'iframe d'intégration depuis une origine neutre, sans aucune configuration
particulière — ce qui confirme qu'**il ne s'agit pas d'un blocage lié à l'intégration elle-même**
(pas de CSP, pas de `sandbox`, pas de paramètre manquant qui expliquerait un refus total de charger).
Le message « Connectez-vous pour confirmer que vous n'êtes pas un robot » est un contrôle **propre à
YouTube**, déclenché côté serveur Google selon la réputation de l'adresse IP/réseau du visiteur (IP
partagée, hébergeur cloud, VPN, volume de requêtes récent vers l'infrastructure YouTube) — un
mécanisme que le site ne peut ni observer (document cross-origine, politique de même origine),
ni influencer, ni contourner légitimement. C'est exactement la limite que la demande elle-même
anticipait.

### Domaine retenu et configuration ajoutée

`config('services.youtube.privacy_enhanced')` (variable `.env` : `YOUTUBE_PRIVACY_ENHANCED`,
documentée dans `.env` et `.env.example`) — **`true` par défaut** (`youtube-nocookie.com`). Aucun des
deux domaines testés n'a montré de différence de comportement à l'intégration elle-même ; le choix
reste donc configurable pour permettre de tester `youtube.com` classique en production si le réseau
réel des visiteurs se comporte différemment de cet environnement de développement. **Un seul point
de décision** : `App\Models\Video::embedUrl()` / `playerEmbedUrl()` / `embedDomain()` — plus aucune
logique de domaine dans les vues ou `resources/js/app.js`.

### Paramètres d'intégration retenus

`start` (uniquement si > 0), `rel=0`, `playsinline=1`, `modestbranding=1` (toujours actifs sur les
deux domaines à ce jour), `enablejsapi=1` (uniquement sur `playerEmbedUrl()`, utilisée par la façade
interactive — jamais sur l'URL "propre" utilisée pour le JSON-LD). `origin` est ajouté **côté
client** au moment du clic (`window.location.origin`), pour toujours refléter l'origine réellement
visitée par le navigateur plutôt qu'une valeur calculée côté serveur. `autoplay=1` n'est ajouté
qu'à ce même moment, après le clic explicite sur Lecture — jamais avant.

### Attributs de l'iframe

`allowfullscreen`, `title` (titre de la vidéo), `referrerpolicy="strict-origin-when-cross-origin"`,
`allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture;
web-share"`. Aucun attribut `sandbox` n'a jamais été utilisé sur cette iframe (vérifié avant
modification) — rien n'a donc été retiré à ce niveau.

### Content-Security-Policy ajoutée

Nouveau middleware `App\Http\Middleware\AddSecurityHeaders` (enregistré globalement dans
`bootstrap/app.php`), volontairement minimal et additif :

```
Content-Security-Policy: frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com; img-src 'self' data: https://i.ytimg.com https://img.youtube.com
Referrer-Policy: strict-origin-when-cross-origin
```

Aucune directive `script-src`, `default-src` ou `style-src` n'a été ajoutée : en leur absence, le
navigateur n'applique aucune restriction sur ces points, ce qui garantit qu'aucun script existant du
site (scripts inline de détection `reveal-ready`/`intro-skip`, bundle Vite, administration) n'est
affecté. Aucun joker `*` n'est utilisé. Cette CSP ne résout pas le contrôle anti-robot de YouTube
(qui se produit sur le domaine de YouTube, pas sur le nôtre) mais garantit, en défense en
profondeur, que seules les origines YouTube légitimes peuvent être intégrées et que seules les
sources d'images nécessaires sont autorisées.

### Repli obligatoire ajouté

Le composant `resources/views/components/youtube-facade.blade.php` affiche désormais, **en
permanence sous le lecteur, sans attendre une erreur** :

- Un texte sobre : « Si YouTube demande une connexion ou empêche la lecture intégrée, ouvrez la
  vidéo directement sur YouTube. »
- Un bouton « Regarder directement sur YouTube » (`target="_blank" rel="noopener noreferrer"`),
  vers `https://www.youtube.com/watch?v={id}`.

Ce bouton est présent dès le premier chargement de la page, pas seulement après une erreur détectée.

### Détection d'erreur (API YouTube IFrame)

`resources/js/app.js` charge, uniquement après le premier clic sur Lecture (jamais avant, pour ne
rien alourdir avant que ce soit nécessaire), le script officiel `https://www.youtube.com/iframe_api`
et attache un lecteur `YT.Player` à l'iframe injectée. Les codes d'erreur officiels suivants
déclenchent l'affichage d'une carte de repli (« La lecture intégrée n'est pas disponible dans ce
navigateur. » + bouton « Regarder sur YouTube ») à la place de l'iframe :

- `2` — paramètre de requête invalide (identifiant malformé)
- `5` — erreur du lecteur HTML5
- `100` — vidéo introuvable, privée ou supprimée
- `101` / `150` — intégration désactivée par le propriétaire de la vidéo

**Limite assumée et documentée explicitement dans le code** : le message « Connectez-vous pour
confirmer que vous n'êtes pas un robot » s'affiche à l'intérieur du document de l'iframe
(cross-origine) et **n'est signalé par aucun code d'erreur officiel de l'API** — YouTube considère
que le lecteur a chargé correctement, seule une vérification humaine est requise avant lecture. Ce
cas **ne peut donc pas être détecté** par `onError`, ni par aucun autre mécanisme accessible à notre
JavaScript (politique de même origine). C'est précisément pour cette raison que le bouton de repli
décrit ci-dessus reste **permanent et non conditionnel** à une détection d'erreur.

### Administration

La page d'édition d'une vidéo (`/admin/videos/{id}/edit`) affiche désormais, sous l'aperçu
cliquable : le domaine d'intégration actuellement utilisé (reflète `services.youtube.privacy_enhanced`
en temps réel), le temps de départ, le lien direct YouTube, un bouton « Tester sur YouTube », et un
texte explicite : la vidéo est correctement enregistrée et une éventuelle demande de connexion vient
de YouTube, pas nécessairement d'une erreur du site.

### Procédure de test en production

1. Ouvrir `/videos` sur le domaine de production, cliquer sur Lecture.
2. Si le message anti-robot apparaît : basculer `YOUTUBE_PRIVACY_ENHANCED=false` dans `.env` de
   production, vider le cache de configuration (`php artisan config:clear`), retester — cela change
   uniquement le domaine d'intégration (`youtube.com` au lieu de `youtube-nocookie.com`), sans
   garantir la disparition du contrôle (celui-ci dépend de la réputation IP/réseau du visiteur, pas
   du domaine).
3. Dans tous les cas, le bouton « Regarder directement sur YouTube », toujours visible sous le
   lecteur, permet au visiteur de voir la vidéo sans interruption perçue comme une panne du site.
4. Vérifier depuis l'administration (`/admin/videos/{id}/edit`) que la vidéo est bien enregistrée et
   que le domaine/temps de départ affichés correspondent à l'attendu — ce qui confirme que
   l'application fonctionne correctement indépendamment du comportement de YouTube.

### Ce qui n'a pas été fait (conformément à la consigne)

Aucun téléchargement de la vidéo, aucune tentative de contournement de la vérification anti-robot,
aucun service proxy, aucune extraction du flux vidéo. La façade existante n'a pas été supprimée, le
démarrage à 14 secondes fonctionne toujours (couvert par les tests de non-régression), et
l'administration existante n'a pas été cassée (144 tests, 367 assertions, tous passants après ce
correctif).
