Files
SecondBrain/20 Work/Ideas/Mogador/Proposition d'architecture - Content Publication Engine.md
T

1578 lines
45 KiB
Markdown

## TL;DR
Le système actuel de synchro Louise/Mogador fonctionne, mais il est devenu trop fragile: trois serveurs avec le même code, dépendance forte à Directus, peu d'observabilité, corrections d'urgence difficiles à tracer, et trop de logique implicite dans une synchro construite rapidement.
La recommandation est de ne pas tout recommencer en big bang, mais de construire un **Content Publication Engine** interne qui publie des JSON statiques versionnés vers un bucket/CDN. Les sites consommeraient ces JSON au lieu de consommer Directus directement.
Le projet doit être séparé en deux chantiers:
1. **Chantier interne, sous notre contrôle**
- produire les JSON publics;
- gérer les runs atomiques et rollback;
- intégrer JWP via un `Media Pipeline`;
- offrir une `Publication Console` interne pour corrections d'urgence;
- produire rapports et alertes.
2. **Chantier PCI, séparé**
- discuter avec PCI d'une API Louise/Mogador v2;
- demander du bulk, SSL fonctionnel, pagination, limites documentées;
- à long terme, demander des events/webhooks comme signal de changement.
Point important: le chantier interne ne doit pas attendre PCI. Il doit fonctionner avec le Mogador Toolkit actuel, puis remplacer son lecteur source par Louise API v2 si elle arrive.
Position recommandée:
```text
Louise Export / Mogador Toolkit actuel
-> Content Publication Engine interne
-> Media Pipeline JWP
-> Publication Console
-> Published Content Feed JSON
-> Bucket/CDN
-> Sites web
```
Directus peut rester temporairement pendant la transition, mais ne devrait plus être le moteur principal de publication web.
## Objectif
Ce document propose une refonte progressive de la synchronisation Louise/Mogador afin de rendre la publication vers TFO, IDELLO, ONFR et linéaire plus fiable, plus rapide à diagnostiquer et plus simple à consommer par les sites.
Le but n'est pas de jeter toute la connaissance métier existante. Le code actuel contient plusieurs règles importantes qui doivent être conservées. Par contre, l'architecture actuelle mélange trop de responsabilités:
- extraction des données officielles;
- synchronisation;
- Directus;
- permissions;
- édition d'urgence;
- publication web;
- vidéos JWP;
- rapports;
- alertes.
La refonte doit séparer ces responsabilités sans créer une usine à gaz.
## Contexte actuel
Louise est la vraie source de vérité.
Toutes les 3 heures, Louise produit un fichier SQL complet, `export_mogador.sql`. Ce fichier n'est jamais incrémental. Il commence par des `TRUNCATE`, puis réinsère toutes les données.
Flux actuel:
```text
Louise
-> export_mogador.sql complet
-> rsync vers serveur Linux
-> import dans une base MySQL structurée avec mogador.sql
-> API Mogador officielle qui parle à cette base
-> application Laravel interne
-> synchronisation vers Directus
-> sites web / fournisseurs consomment Directus ou redistribuent dans leurs propres bases
```
Aujourd'hui, il y a trois serveurs avec une copie du même code:
```text
P1: TFO + linéaire
P2: IDELLO
P3: ONFR
```
Cette séparation existe parce qu'un seul serveur prendrait trop de temps à synchroniser toutes les plateformes. L'export, le transfert, l'import SQL et la synchronisation prennent environ 1 heure par cycle sur l'architecture actuelle.
## Contraintes importantes
- La source officielle reste Louise.
- L'export Louise est un full export SQL, pas un incrémentiel.
- L'équipe dev peut regarder la base, mais la consommation officielle devrait passer par l'API Mogador pour rester conforme aux attentes contractuelles.
- L'app interne ne doit pas être exposée à Internet selon les contraintes Infra actuelles.
- Les fournisseurs ont besoin d'un accès read-only.
- Les édimestres et l'équipe interne doivent pouvoir faire des corrections d'urgence.
- Les sites veulent un modèle orienté utilisateur: collection -> saisons -> épisodes -> programmations.
- Louise/Mogador expose souvent les données dans le sens inverse: épisode -> programmation -> serie -> collection.
- Directus est utile pour les permissions et l'édition, mais difficile comme moteur relationnel/publication.
- Les programmations futures sont demandées, avec une limite v1 proposée à J+10.
- Les vidéos JWP sont gérées dans une app AdonisJS existante et doivent être intégrées sans coupler toute la publication à JWP.
## Problèmes actuels
### Architecture lourde
Le même code est déployé sur plusieurs serveurs. Cela complique:
- les mises à jour;
- les migrations;
- les alertes;
- le debugging;
- les reprises après incident;
- la compréhension globale du système.
### Directus porte trop de responsabilités
Directus sert aujourd'hui a:
- donner un accès read-only aux fournisseurs;
- permettre des corrections d'urgence;
- exposer les données aux sites;
- représenter des relations complexes entre produits, programmations, series et collections.
Le problème principal est le dernier point. Les relations entre produits, programmations, series et collections sont difficiles à maintenir dans Directus. Certaines relations ne peuvent pas être créées proprement à cause de contraintes FK ou du modèle de données.
### Le modèle Louise ne correspond pas au modèle des sites
Terminologie métier:
```text
Collection = serie pour le public
Serie = saison
Épisode = épisode
```
Les sites veulent souvent:
```json
{
"collection": {
"programmation": {
"start": "2026-08-08T06:00:00-04:00",
"end": "2026-08-08T06:24:00-04:00"
}
},
"seasons": [
{
"serie": {},
"episodes": [
{
"product": {},
"programmations": [],
"media": {},
"images": []
}
]
}
]
}
```
Mais les données Louise/Mogador partent souvent de l'épisode, puis il faut remonter vers la serie/saison et la collection.
Exemple: `is_original` peut être disponible sur les épisodes, mais pas directement sur la collection. Si le site veut cette information au niveau collection, il faut definir une règle explicite de remontee.
### Peu d'observabilité proactive
Aujourd'hui, l'équipe est souvent reactive. Les problèmes sont decouverts quand la production ou les sites signalent une erreur.
Exemples déjà rencontrès:
- supervisor down;
- worker queue down;
- migration oubliee lors d'un changement de serveur;
- programmation attendue absente du site;
- synchro incomplète;
- données manquantes dans Directus.
## Décision recommandée
La recommandation principale est de ne plus utiliser Directus comme moteur principal de publication web.
L'application interne devrait produire des JSON statiques prêts à consommer par les sites.
Architecture cible:
```text
Louise SQL full export
-> import MySQL Mogador
-> Mogador Toolkit actuel
-> Content Publication Engine interne
-> snapshots / diffs / rapports
-> génération de JSON statiques
-> publication vers bucket/CDN par plateforme
-> sites web consomment les JSON
```
L'app interne ne serait pas exposée à Internet. Les sites ne parlent pas à Laravel ou AdonisJS directement. Ils lisent des fichiers JSON publics publiés dans un bucket/CDN.
## Deux chantiers à ne pas mélanger
### Chantier A: amélioration interne de la synchro
Ce chantier est sous notre contrôle.
Il contient:
- `Content Publication Engine`;
- `Media Pipeline` AdonisJS/JWP;
- contrat interne `media_assets`;
- contrat interne `publication_jobs`;
- `Publication Console` pour corrections d'urgence;
- `Published Content Feed`, les JSON publiés vers bucket/CDN.
Ce chantier peut commencer sans attendre une refonte par PCI. Au début, il peut continuer à lire les données officielles via l'export SQL et le Mogador Toolkit actuel.
### Chantier B: discussion PCI / évolution Louise-Mogador
Ce chantier est un projet séparé.
Objectif: discuter avec PCI pour moderniser l'accès aux données officielles Louise/Mogador.
Il peut inclure:
- une API Louise/Mogador v2;
- des endpoints bulk plus efficaces;
- une meilleure compatibilité SSL;
- des limites de concurrence documentées;
- des exports plus faciles à consommer;
- à long terme, des events/webhooks comme signal de changement pour aller vers une publication presque instantanée.
Ce chantier a une relation directe avec la synchro interne, mais il ne doit pas bloquer le chantier A. La synchro interne doit être conçue pour fonctionner avec le Mogador Toolkit actuel, puis remplacer son lecteur source par Louise API v2 si elle arrive.
## Flow d'information cible
### Chantier A: publication interne
```mermaid
flowchart TD
Louise["LouiseSysteme source editorial"]
LouiseExport["Louise ExportSQL complet toutes les 3h"]
Importer["Import SQL interneBase source locale"]
Toolkit["Mogador ToolkitAPI legacy actuelle"]
Snapshot["Snapshot Collectorlit les données officielles"]
SnapshotStore["Snapshot Storeruns source + checksums"]
Diff["Diff Builderfabrique l'incrémentiel interne"]
MediaPipeline["Media PipelineAdonisJS/JWP"]
JWP["JWPplateforme video"]
MediaAssets["media_assetsétat media interne"]
Console["Publication Consolemini CMS interne"]
Overrides["overridescorrections d'urgence"]
Jobs["publication_jobsfile de republication"]
Engine["Content Publication EngineLaravel ou autre"]
Validator["Validation + rapportsattendu vs publié"]
Runs["Published Content Feedruns JSON immuables"]
Manifest["manifest.jsonpointe vers le run actif"]
Bucket["Bucket/CDNexposition publique contrôlee"]
Sites["Sites webTFO / IDELLO / ONFR / linéaire"]
Alerts["Slack / Better Stackalertes anomalies"]
Louise --> LouiseExport
LouiseExport --> Importer
Importer --> Toolkit
Toolkit --> Snapshot
Snapshot --> SnapshotStore
SnapshotStore --> Diff
Diff --> Engine
Snapshot --> MediaPipeline
MediaPipeline --> JWP
JWP --> MediaPipeline
MediaPipeline --> MediaAssets
MediaPipeline --> Jobs
Console --> Overrides
Console --> Jobs
MediaAssets --> Engine
Overrides --> Engine
Jobs --> Engine
Engine --> Validator
Validator --> Alerts
Validator --> Runs
Runs --> Manifest
Manifest --> Bucket
Runs --> Bucket
Bucket --> Sites
```
Ce schema représente ce qu'on contrôle nous-mêmes. Il ne depend pas d'une API Louise/Mogador v2.
### Chantier B: évolution PCI
```mermaid
flowchart TD
Louise["Louisesystème source"]
CurrentExport["Export SQL actuelfull truncate + insert"]
CurrentToolkit["Mogador Toolkit actuelAPI legacy"]
Discussion["Discussion PCIupgrade Louise/Mogador"]
ApiV2["Louise API v2bulk + SSL + pagination"]
Events["Events/Webhooks futurssignal de changement"]
InternalReader["Source Reader internedans le Content Publication Engine"]
Louise --> CurrentExport --> CurrentToolkit --> InternalReader
Louise --> Discussion
Discussion --> ApiV2
Discussion --> Events
ApiV2 -.remplace progressivement.-> InternalReader
Events -.declenche une relecture officielle.-> InternalReader
```
Ce deuxieme schema est un projet avec PCI. Il peut ameliorer fortement la vitesse et la fiabilite, mais il doit rester interchangeable.
Le `Content Publication Engine` devrait avoir un `Source Reader` abstrait:
```text
Version 1:
Source Reader -> Mogador Toolkit actuel -> DB importée depuis Louise Export
Version future:
Source Reader -> Louise API v2
```
Le reste du système interne ne devrait pas changer:
- diff interne;
- `media_assets`;
- `publication_jobs`;
- overrides;
- rapports;
- JSON publics;
- manifest;
- rollback.
## Comment ca marche
1. Louise produit un export complet toutes les 3 heures.
2. Le SQL est importe dans une base interne.
3. Le Mogador Toolkit expose les données officielles.
4. Le Snapshot Collector lit les données officielles.
5. Le Diff Builder compare le snapshot courant au snapshot précédent.
6. Le Media Pipeline gère JWP et écrit l'état media dans `media_assets`.
7. La Publication Console gère les corrections d'urgence et écrit les overrides.
8. Les changements créent des `publication_jobs`.
9. Le Content Publication Engine combine données officielles, media, overrides et règles métier.
10. Il génère un run JSON complet.
11. Il valide le run et produit un rapport.
12. Il publie les fichiers JSON immuables.
13. Il met à jour `manifest.json` seulement si le run est valide.
14. Les sites lisent le manifest, puis les fichiers du run actif.
## Fabrication d'un incrémental à partir d'un full export
Louise ne fournit pas d'incrémentiel. Chaque export est complet.
La solution est de sauvegarder des snapshots internes et de comparer les snapshots entre eux.
Exemple:
```text
09h00: export Louise complet
-> import MySQL
-> extraction via Mogador Toolkit
-> snapshot_09h00
12h00: export Louise complet
-> import MySQL
-> extraction via Mogador Toolkit
-> snapshot_12h00
Diff snapshot_09h00 -> snapshot_12h00:
-> programmes ajoutés
-> programmes retirés
-> produits modifiés
-> images modifiées
-> dates de programmation modifiées
-> erreurs ou anomalies
```
Cela permet:
- de savoir ce qui a changé;
- de générer un `latest.json`;
- de limiter certaines republications;
- de produire un rapport clair;
- de conserver un historique.
## Publication incrémentale protégée
Le Content Publication Engine ne devrait pas régénérer aveuglément les 8 000 ou 22 000 produits à chaque run.
Il devrait plutôt construire le nouveau run à partir du dernier run valide:
```text
Run 1:
8 000 produits publiés
Run 2:
+ 10 nouveaux produits
- 5 produits retirés
= le CPE traite les 10 nouveaux produits, retire les 5 produits, et conserve les fichiers inchangés du run 1
```
Du point de vue des sites, le run 2 reste complet. Il contient tout ce qui est publié pour la plateforme. Mais physiquement, le CPE n'a pas besoin de recalculer tous les fichiers si seulement quelques entités ont changé.
Principe:
```text
Snapshot source
= ce que Louise/Mogador dit aujourd'hui
Dernier état publié valide
= ce que les sites consomment déjà avec succès
Diff
= ce qui est ajouté, modifié, retiré ou impacté
Nouveau run
= dernier run valide + patch validé
```
Un produit inchangé ne devrait pas être republié uniquement parce qu'il existe dans le nouveau snapshot.
Il devrait être republié seulement si:
- son hash source a changé;
- sa programmation a changé;
- son media a changé;
- ses images ont changé;
- un override a changé;
- une règle de publication impactante a changé;
- une republication manuelle est demandée.
Pour les suppressions:
- les produits retirés ne sont plus inclus dans les indexes du nouveau run;
- leurs fichiers peuvent ne pas être copiés dans le nouveau run;
- ils apparaissent dans `latest.json` ou dans un rapport comme `removed`;
- une trace est conservée dans une table comme `deleted_programs` ou `publication_removed_items`.
Ce modèle résout un problème important: si PCI introduit un bug dans une version de Mogador et que plusieurs anciens produits deviennent soudainement incomplets dans la nouvelle extraction, le système ne doit pas écraser automatiquement les anciens JSON valides.
Dans ce cas, le CPE devrait:
- conserver le dernier fichier publié valide pour les produits non réellement modifiés;
- marquer les nouvelles données incomplètes comme anomalies;
- alerter l'équipe;
- empêcher la propagation massive d'une régression fournisseur;
- publier seulement les changements validés, ou bloquer le run si l'anomalie est critique.
Ce compromis rend le système plus rapide et plus fiable. On garde les bénéfices d'un run complet pour les sites, mais avec une stratégie de génération incrémentale protégée.
## Garde-fous non négociables
Pour que cette architecture soit réellement fiable, certaines règles doivent être traitées comme non négociables.
Le CPE doit:
- ne jamais publier un run incomplet;
- ne jamais mettre à jour `manifest.json` avant la validation du run;
- ne jamais écraser un produit inchangé avec une version source devenue suspecte;
- garder le dernier JSON valide pour les entités inchangées;
- alerter sur les anomalies;
- produire un rapport attendu vs publié;
- permettre un rollback en repointant `manifest.json`;
- garder un audit complet des overrides;
- garder une trace des suppressions et des entités retirées;
- rendre les publications idempotentes, donc relançables sans effet secondaire imprévu.
Le point le plus important est le suivant:
```text
Un nouveau snapshot source ne veut pas automatiquement dire que tous les JSON doivent être remplacés.
```
Le CPE doit distinguer:
- une donnée réellement modifiée;
- une donnée absente;
- une donnée incomplète;
- une donnée suspecte;
- une donnée inchangée qui doit rester publiée dans sa dernière version valide.
Cette règle est ce qui protège le système contre une régression fournisseur, une extraction incomplète ou une erreur ponctuelle dans Mogador.
## Données internes proposées
Tables possibles:
```text
sync_runs
sync_run_items
sync_anomalies
source_snapshots
products
programs
collections
series
episodes
media_assets
publication_jobs
publication_snapshots
manual_overrides
deleted_programs
```
Avec une colonne `platform`:
```text
tfo
idello
onfr
linear
```
Cela évite de dupliquer toute la logique dans des tables séparées comme `tfo_products`, `idello_products`, `onfr_products`, sauf si une contrainte technique oblige à garder cette séparation.
## Published Content Feed: JSON statiques proposés
Au lieu d'un seul gros JSON, il faut publier plusieurs fichiers spécialisés.
Exemple pour TFO:
```text
/tfo/v1/manifest.json
/tfo/v1/runs/20260730-0900/today.json
/tfo/v1/runs/20260730-0900/latest.json
/tfo/v1/runs/20260730-0900/schedule/index.json
/tfo/v1/runs/20260730-0900/schedule/2026-07-30.json
/tfo/v1/runs/20260730-0900/schedule/2026-07-31.json
/tfo/v1/runs/20260730-0900/collections/index.json
/tfo/v1/runs/20260730-0900/collections/0123456/full.json
/tfo/v1/runs/20260730-0900/products/GP123456.json
/tfo/v1/runs/20260730-0900/search/index.json
```
Pour 8 000 produits TFO et 22 000 produits IDELLO, ce volume de fichiers JSON n'est pas un problème pour un bucket/CDN. C'est préférable à un gros fichier unique.
Le site ne code pas un chemin de run en dur. Il lit toujours:
```text
/tfo/v1/manifest.json
```
Puis utilise le `base_path` retourné:
```text
/tfo/v1/runs/20260730-0900
```
### manifest.json
Le manifest indique quelle version est publiée.
```json
{
"schema_version": "1.0",
"platform": "tfo",
"published_at": "2026-07-30T09:50:00-04:00",
"run_id": "20260730-0900",
"base_path": "/tfo/v1/runs/20260730-0900"
}
```
Le manifest est le seul fichier qui doit avoir un cache court:
```text
/tfo/v1/manifest.json
Cache-Control: public, max-age=60, stale-while-revalidate=120
/tfo/v1/runs/20260730-0900/*
Cache-Control: public, max-age=31536000, immutable
```
Compromis acceptable si Infra veut réduire les appels:
```text
manifest.json: max-age=300
```
Donc 5 minutes. Trois heures serait trop long pour les corrections d'urgence, les rollbacks et les republications après média JWP.
Important: les runs sont des copies complètes et immuables. Donc oui, garder plusieurs runs multiplie le nombre de fichiers stockés.
Exemple simplifie avec TFO:
```text
8 000 produits TFO
5 runs conserves
= environ 40 000 fichiers produits
```
Et il faut ajouter les collections, schedules, indexes, `today.json`, `latest.json`, etc.
Estimation plus complète pour TFO avec 5 runs:
```text
Produits:
8 000 x 5 runs = 40 000 fichiers
Collections:
environ 300 x 5 runs = 1 500 fichiers
Schedules:
environ 11 jours x 5 runs = 55 fichiers
Indexes / today / latest / divers:
environ 50 à 150 fichiers
Total estime:
environ 41 500 à 42 000 fichiers
```
Le `manifest.json` n'est pas multiplie de la même façon: il pointe seulement vers le run actif.
Ce volume reste raisonnable pour un bucket/CDN, mais il doit être assumé dans la stratégie de rétention. C'est pour cela qu'on garde:
- les runs récents utiles au rollback;
- un minimum de 3 runs valides;
- une limite de rétention, par exemple 10 jours;
- des policies de nettoyage automatique.
Le compromis est volontaire: on accepte plus de fichiers stockés pour obtenir une publication atomique, un rollback simple, un cache agressif sur les runs, et aucun risque qu'un site lise une publication à moitié générée.
### today.json
Contient ce qui doit être en ligne aujourd'hui.
Le tableau `programs` doit contenir des objets de programmation légers. Il ne doit pas embarquer tout le produit, toutes les images ou toute la collection. Pour obtenir le détail complet, le site utilise les liens vers `products/{product_key}.json` ou `collections/{biznumber}/full.json`.
```json
{
"date": "2026-07-30",
"programs": [
{
"program_key": "190858829",
"product_key": "102828840",
"collection_biznumber": "0123456",
"serie_key": "S123",
"title": "Larguer les amarres",
"begin": "2026-07-30T06:00:00-04:00",
"end": "2026-07-30T06:24:00-04:00",
"duration_seconds": 1440,
"type": "Film",
"target": "G",
"territories": ["CA"],
"media_status": "ready",
"links": {
"product": "/tfo/v1/runs/20260730-0900/products/102828840.json",
"collection_full": "/tfo/v1/runs/20260730-0900/collections/0123456/full.json"
}
}
]
}
```
### latest.json
Contient les nouveautés et changements depuis la dernière publication.
```json
{
"run_id": "20260730-0900",
"added": [
{
"entity_type": "product",
"product_key": "102828840",
"collection_biznumber": "0123456",
"title": "Larguer les amarres",
"first_programmation": "2026-07-30T06:00:00-04:00",
"links": {
"product": "/tfo/v1/runs/20260730-0900/products/102828840.json"
}
}
],
"updated": [
{
"entity_type": "program",
"program_key": "190858829",
"product_key": "102828840",
"title": "Larguer les amarres",
"changed_fields": ["begin", "end"],
"links": {
"product": "/tfo/v1/runs/20260730-0900/products/102828840.json"
}
}
],
"removed": [
{
"entity_type": "product",
"product_key": "102000001",
"collection_biznumber": "0999999",
"title": "Ancien titre",
"reason": "not_in_current_snapshot"
}
]
}
```
### schedule/YYYY-MM-DD.json
Contient la programmation d'une date précise.
```json
{
"date": "2026-08-08",
"programs": [
{
"program_key": "190858829",
"product_key": "102828840",
"collection_biznumber": "0123456",
"serie_key": "S123",
"begin": "2026-08-08T06:00:00-04:00",
"end": "2026-08-08T06:24:00-04:00",
"title": "Larguer les amarres",
"duration_seconds": 1440,
"type": "Film",
"target": "G",
"territories": ["CA"],
"media_status": "ready",
"links": {
"product": "/tfo/v1/runs/20260730-0900/products/102828840.json",
"collection_full": "/tfo/v1/runs/20260730-0900/collections/0123456/full.json"
}
}
]
}
```
### products/{id}.json
Contient le détail d'un produit.
```json
{
"product": {},
"programmations": {
"current": [],
"upcoming": [],
"next": {
"date": "2026-08-08",
"begin": "2026-08-08T06:00:00-04:00"
}
},
"media": {},
"images": []
}
```
### collections/{biznumber}/full.json
Contient le modèle attendu par les sites.
```json
{
"collection": {
"biznumber": "0123456",
"title": "...",
"is_original": true,
"programmation": {
"start": "2026-08-08T06:00:00-04:00",
"end": "2026-08-08T06:24:00-04:00"
},
"next_programmation": {
"date": "2026-08-08",
"begin": "2026-08-08T06:00:00-04:00"
}
},
"seasons": [
{
"serie": {},
"episodes": [
{
"product": {},
"programmations": [],
"media": {},
"images": []
}
]
}
]
}
```
## Programmations futures
Pour afficher des messages comme "prochain épisode le 8 aout", il faut extraire une fenêtre future de programmation.
Limite v1 recommandée: J+10.
Configuration proposée:
```env
TFO_SCHEDULE_DAYS_AHEAD=10
IDELLO_SCHEDULE_DAYS_AHEAD=10
ONFR_SCHEDULE_DAYS_AHEAD=10
LINEAR_SCHEDULE_DAYS_BEHIND=7
LINEAR_SCHEDULE_DAYS_AHEAD=10
```
Les programmations futures sont publiées dans:
```text
/tfo/v1/runs/{run_id}/schedule/2026-08-08.json
```
Et resumees dans les JSON produit/collection:
```json
{
"next_programmation": {
"date": "2026-08-08",
"begin": "2026-08-08T06:00:00-04:00"
}
}
```
Le site peut ensuite appliquer sa logique d'affichage selon l'heure.
## Search et Algolia
Un fichier comme:
```text
/tfo/v1/runs/{run_id}/search/index.json
```
peut servir à alimenter Algolia, Meilisearch, Typesense ou un autre moteur de recherche.
Il peut contenir seulement les champs utiles:
```json
[
{
"objectID": "GP123456",
"type": "product",
"title": "...",
"slug": "...",
"image": "...",
"collection_biznumber": "0123456"
}
]
```
Si les sites ont besoin d'une vraie recherche rapide sur 8 000 à 22 000 items, un moteur dédié reste préférable à un gros fichier recherche chargé côté client.
## Publication atomique et rétention
Principe:
```text
/tfo/v1/runs/20260730-0900/...
/tfo/v1/runs/20260730-1200/...
/tfo/v1/manifest.json
```
Si la génération du run `20260730-1200` échoue, `manifest.json` continue de pointer vers `20260730-0900`. Une sync ratée ne casse pas le site.
Cette règle s'applique aussi aux corrections rapides: si une correction d'urgence ou une republication media échoue, le manifest reste sur le dernier run valide.
Retention proposée:
```text
JSON publiés:
- garder les runs des 10 derniers jours
- garder au minimum les 3 derniers runs valides
- supprimer les runs incomplets jamais publiés
Rapports:
- garder 90 jours
Anomalies importantes / audit:
- garder 1 an ou selon politique interne
```
## Versioning, documentation et validation
Les JSON deviennent un contrat avec les sites. Il faut donc versionner le format.
Version dans le chemin:
```text
/tfo/v1/manifest.json
/tfo/v1/runs/20260730-0900/products/GP123456.json
/tfo/v1/runs/20260730-0900/collections/0123456/full.json
```
Version dans chaque fichier:
```json
{
"schema_version": "1.0",
"platform": "tfo",
"run_id": "20260730-0900"
}
```
Documentation recommandée:
```text
docs/
publication-contract.md
files.md
cache-strategy.md
versioning.md
examples/
manifest.json
product.json
collection-full.json
schedule-day.json
schemas/
manifest.schema.json
product.schema.json
collection-full.schema.json
schedule-day.schema.json
```
Avant de publier un run:
```text
1. générer tous les JSON
2. valider chaque type de fichier contre son JSON Schema
3. produire le rapport
4. publier seulement si les erreurs critiques sont absentes
5. mettre à jour manifest.json
```
Swagger/OpenAPI est utile pour les endpoints internes de la Publication Console. Pour les JSON statiques, JSON Schema et exemples versionnés sont plus importants.
## Role futur de Directus
Deux options sont possibles.
### Option 1: retirer Directus progressivement
Si les sites consomment les JSON statiques et si les permissions fournisseurs sont gérées par bucket ou préfixe, Directus n'est plus nécessaire dans la chaîne de publication.
Permissions possibles:
```text
bucket-tfo
bucket-idello
bucket-onfr
```
Ou un seul bucket avec préfixes:
```text
publication/tfo/
publication/idello/
publication/onfr/
```
### Option 2: garder Directus temporairement
Directus peut rester pendant une periode de transition pour:
- consultation interne;
- édition d'urgence existante;
- accès read-only fournisseur existant;
- comparaison avec les nouveaux JSON.
Mais il ne devrait plus composer le JSON relationnel final utilisé par les sites.
## Publication Console: édition d'urgence
Si Directus est retiré ou réduit, il faut remplacer son rôle d'édition d'urgence.
La proposition est de créer une `Publication Console` interne, non exposée à Internet.
Accès proposés:
```text
Admin sync:
- peut configurer et publier
- peut gérer les utilisateurs
- peut faire rollback
Edimestre:
- peut rechercher et consulter
- peut créer une correction d'urgence
- peut voir les overrides actifs
- ne peut pas changer la configuration système
Read-only interne:
- peut consulter les données, rapports et JSON publiés
- ne peut pas modifier
```
Fonctions minimales:
- rechercher un produit, programme, collection ou serie;
- voir les données venant de Louise/Mogador;
- voir le JSON actuellement publié;
- ajouter une correction temporaire;
- mettre une raison;
- mettre une date d'expiration;
- republier les JSON concernés;
- garder un historique complet.
Exemple de table:
```text
manual_overrides
- id
- platform
- entity_type: product | program | collection | serie
- entity_key: product_key | program_key | biznumber
- field_path
- value_json
- reason
- active_from
- active_until
- created_by
- approved_by
- created_at
- updated_at
```
Important: il ne faut pas modifier les JSON publiés directement à la main. Les overrides doivent être appliqués dans le pipeline de génération, sinon ils seront perdus ou impossibles à auditer.
Pour les corrections internes, la Publication Console doit être rapide parce qu'elle ne devrait pas relancer toute la plateforme. Une correction doit créer un `publication_job` ciblé et republier seulement les entités touchées:
```text
correction collection
-> publication_job collection
-> régénération de collections/{biznumber}/full.json
-> mise à jour des indexes ou schedules impactés si nécessaire
-> nouveau run valide
-> manifest mis à jour
```
Cela permet de corriger rapidement une donnée déjà reçue ou une donnée éditoriale interne. Par contre, cela ne rend pas Louise instantané: si un changement n'est pas encore arrivé dans l'export Louise/Mogador, le CPE ne peut pas l'inventer.
Chaque override devrait inclure:
- l'utilisateur;
- le rôle;
- la raison;
- la date d'expiration;
- l'entité touchée;
- l'ancien contenu;
- le nouveau contenu;
- le run dans lequel la correction a été publiée.
## Media Pipeline: rôle de l'app AdonisJS/JWP
L'app AdonisJS qui synchronise les vidéos vers JWP ne devrait pas devenir le moteur de publication JSON complet. Son rôle naturel est:
> vérifier les vidéos, envoyer ou mettre à jour les médias dans JWP, suivre leur statut, et déclarer qu'un produit doit être republié quand le média est prêt.
Le point important n'est pas la technologie de cette app, mais le contrat entre le Media Pipeline et le Content Publication Engine.
Elle devrait faire:
- lire les infos vidéo source depuis Louise/Mogador ou depuis le snapshot interne;
- vérifier la présence du fichier vidéo source;
- créer ou mettre à jour le media dans JWP;
- gérer poster, sous-titres, métadonnées et statut d'encodage;
- maintenir une table interne `media_assets`;
- créer un job de republication quand un média change.
Elle ne devrait pas faire:
- publier les JSON finaux des sites;
- connaitre toute la logique collection/saison/épisode;
- écrire directement dans les fichiers publics;
- remplacer le Content Publication Engine.
## Limites volontaires du CPE
Le CPE doit rester un moteur de publication. C'est la condition principale pour éviter de recréer une usine à gaz.
Il ne doit pas devenir:
- un nouveau Directus complet;
- un DAM vidéo;
- un CMS généraliste;
- une API publique dynamique;
- un moteur de recherche;
- un outil de gestion PCI;
- une refonte des sites.
Sa responsabilité doit rester claire:
```text
lire la source officielle
comparer
valider
appliquer les overrides
enrichir avec media_assets
générer les JSON
publier le manifest
rapporter et alerter
```
Tout ce qui dépasse ce périmètre doit être questionné avant d'être ajouté au projet.
## Contrat entre Media Pipeline et Content Publication Engine
### Table `media_assets`
Cette table représente l'état media interne d'un produit.
Champs proposés:
```text
id
platform
product_key
biznumber
provider
provider_media_id
status
source_video_name
source_file_delivery
m3u8_url
poster_url
captions_status
error_code
error_message
metadata_json
created_at
updated_at
ready_at
```
Valeurs possibles pour `status`:
```text
missing
uploading
processing
ready
failed
disabled
```
Exemple:
```json
{
"platform": "tfo",
"product_key": "GP123456",
"biznumber": "0123456",
"provider": "jwp",
"provider_media_id": "abc123",
"status": "ready",
"m3u8_url": "https://cdn.jwplayer.com/manifests/abc123.m3u8",
"poster_url": "https://cdn.jwplayer.com/v2/media/abc123/poster.jpg",
"captions_status": "ready",
"updated_at": "2026-07-31T09:15:00-04:00"
}
```
### Table `publication_jobs`
Cette table sert à dire au Content Publication Engine: "quelque chose a changé, republie ce qui est touché".
Champs proposés:
```text
id
platform
entity_type
entity_key
reason
priority
status
attempts
payload_json
available_at
locked_at
processed_at
created_at
updated_at
```
Valeurs possibles pour `entity_type`:
```text
product
collection
schedule
all
```
Valeurs possibles pour `reason`:
```text
mogador_snapshot_changed
media_ready
media_failed
override_changed
manual_republish
rollback
```
Exemple:
```json
{
"platform": "tfo",
"entity_type": "product",
"entity_key": "GP123456",
"reason": "media_ready",
"priority": 50,
"status": "pending"
}
```
Pourquoi cette table est le meilleur compromis:
- plus fiable qu'un appel direct entre apps;
- rejouable si le Content Publication Engine est temporairement down;
- observable dans un dashboard ou rapport;
- compatible avec plusieurs workers;
- idempotent si on utilisé une cle unique par entité/reason/run;
- rapide, car le job peut être traite presque tout de suite;
- plus simple à auditer;
- ne force pas le Media Pipeline et le Content Publication Engine a être déployés ensemble.
On peut garder un scan periodique comme filet de sécurité, par exemple toutes les 15 ou 30 minutes, pour détectér un media modifie qui n'aurait pas crée de job.
## Rapport et alertes
Le rapport est une piece centrale de la v2.
Il doit comparer:
```text
ce que Louise/Mogador contient
vs
ce que le Content Publication Engine a traité
vs
ce qui a été publié en JSON
vs
éventuellement ce qui reste dans Directus pendant la transition
```
Exemple de rapport:
```text
Plateforme: TFO
Run: 20260730-0900
Export SQL reçu: oui
Import MySQL: succès
Mogador Toolkit: OK
Programmes attendus aujourd'hui: 1230
Programmes publiés: 1229
Produits attendus: 8142
Produits publiés: 8139
Ecarts:
- 1 programme absent du JSON final
- 2 produits sans video JWP
- 5 images manquantes
- 1 produit ignoré car type extrait
Publication:
- statut: publié avec avertissements
- manifest pointe vers: /tfo/v1/runs/20260730-0900
```
Alertes à mettre en place:
- export SQL non reçu;
- fichier SQL trop petit ou checksum identique de façon suspecte;
- import MySQL échoue;
- Mogador Toolkit ne repond pas;
- workers down;
- queue bloquee;
- sync trop longue;
- écart entre attendu et publié;
- programme prévu à 6h absent de la publication avant 6h;
- video JWP manquante;
- image ou transcription manquante;
- override actif proche expiration.
## Fichier SQL hors application
Le fait que `export_mogador.sql` soit hors du repertoire de l'application n'est pas un problème.
Le script d'import peut écrire un fichier de statut dans un chemin connu:
```json
{
"export_received_at": "2026-07-30T09:01:00-04:00",
"import_started_at": "2026-07-30T09:35:00-04:00",
"import_finished_at": "2026-07-30T09:48:00-04:00",
"sql_file_size": 306184192,
"checksum": "abc123",
"status": "succèss"
}
```
Chemins configurables:
```env
MOGADOR_EXPORT_PATH=/data/mogador/export_mogador.sql
MOGADOR_IMPORT_STATUS_PATH=/data/mogador/import-status.json
MOGADOR_IMPORT_LOG_PATH=/var/log/mogador-import.log
```
Le Content Publication Engine lit ce statut pour produire son rapport.
## Timeline du chantier A: synchro interne
Important: les durées ci-dessous représentent des estimations d'effort si l'équipe travaille presque à temps plein sur le projet. Si l'équipe a 35-40% d'opérations courantes, il faut convertir ces estimations en durée calendrier.
Exemple:
```text
2 semaines d'effort projet / 60% de disponibilite projet
= environ 3.3 semaines calendrier
```
En pratique, une estimation doit souvent être multipliée par environ 1.5 à 1.7 si l'équipe n'est disponible qu'à 60-65% pour le projet.
### Phase 0: contrat technique
Durée estimée: 1 semaine.
Livrables:
- schema `media_assets`;
- schema `publication_jobs`;
- structure JSON v1;
- règles de statut media;
- règles de republication;
- convention de logs et rapports.
### Phase 1: observabilité
Objectif: savoir ce qui se passe avant de tout remplacer.
- Ajouter `sync_runs`.
- Ajouter `sync_run_items`.
- Ajouter `sync_anomalies`.
- Generer un rapport par plateforme.
- Alerter sur les écarts critiques.
- Garder Directus tel quel.
### Phase 2: Media Pipeline minimal
Durée estimée: 1 à 2 semaines.
- L'app AdonisJS écrit dans `media_assets`.
- Elle crée des jobs `publication_jobs`.
- Elle détecte les médias `missing`, `processing`, `ready`, `failed`.
- Elle produit un rapport simple sur les vidéos manquantes ou en erreur.
### Phase 3: publication JSON en parallèle
Durée estimée: 2 à 3 semaines.
- Generer `/manifest.json`.
- Generer `/today.json`.
- Generer `/schedule/YYYY-MM-DD.json`.
- Generer quelques `/products/{id}.json`.
- Generer quelques `/collections/{id}/full.json`.
- Lire `media_assets`.
- Traiter `publication_jobs`.
- Comparer JSON vs Directus.
### Phase 4: premier site pilote
Durée estimée: 2 semaines.
- Choisir ONFR ou une portion TFO.
- Publier dans un bucket/prefix dédié.
- Valider performance, structure JSON, cache, rollback et médias JWP.
- Ajouter alertes Slack/Better Stack.
### Phase 5: Publication Console
Durée estimée: 2 semaines.
- Ajouter `manual_overrides`.
- Ajouter interface interne.
- Ajouter auth et rôles.
- Ajouter audit trail.
- Ajouter expiration automatique.
- Ajouter rapport des overrides actifs.
### Phase 6: reduction ou retrait de Directus
- Garder Directus seulement pendant transition.
- Migrer les usages fournisseurs vers buckets/CDN.
- Retirer progressivement les dépendances Directus.
## Timeline du chantier B: PCI / Louise-Mogador
Ce chantier peut avancer en parallèle, mais il ne devrait pas bloquer le chantier interne.
### Etape B1: clarifier les problèmes actuels
Points à documenter pour PCI:
- SSL non fonctionnel ou non compatible;
- lenteur des appels API;
- limite de concurrence actuelle;
- trop grand nombre d'appels nécessaires pour reconstruire un produit complet;
- absence d'incrémentiel;
- absence de signal de changement;
- besoin de bulk;
- besoin de pagination/cursor;
- besoin de checksums ou export id;
- besoin de documentation claire et versionnée.
### Etape B2: demander une API v2 bulk
Exemples de besoins:
- récupérer tous les produits/programmes d'une plateforme pour une fenêtre de dates;
- récupérer une collection complète;
- récupérer un produit complet;
- récupérer les horaires J+10;
- récupérer les droits et pays autorises;
- récupérer les informations vidéo source, mais pas les champs JWP.
### Etape B3: demander un mode événementiel
PCI pourrait envoyer un event quand une modification est sauvegardée dans Louise. Cet event ne ne publie rien directement. Il sert seulement à déclencher une relecture officielle par notre système interne.
Exemple:
```json
{
"event_id": "evt_123",
"séquence": 987654,
"event_type": "product.updated",
"platforms": ["tfo"],
"product_key": "GP123456",
"occurred_at": "2026-07-31T09:15:00-04:00"
}
```
Notre système reçoit l'event, relit le détail via API officielle, applique les validations, enrichit avec JWP/overrides, puis republie les JSON touchés.
## Vision long terme: publication presque instantanée
Aujourd'hui, Louise exporte un full SQL toutes les 3 heures. Tant que c'est le seul signal officiel, on ne peut pas garantir une publication instantanée d'une modification faite dans Louise.
Par contre, on peut préparer l'architecture pour le futur.
À court terme:
- snapshot toutes les 3 heures;
- diff interne;
- publication atomique;
- overrides internes rapides;
- republication rapide quand un média JWP devient prêt.
À moyen terme:
- demander à PCI une API Louise/Mogador v2 bulk fiable;
- réduire le temps de lecture;
- éviter les milliers d'appels API individuels;
- garder le snapshot complet comme filet de sécurité.
À long terme:
- demander à PCI des événements/webhooks comme signal;
- chaque changement dans Louise envoie un événement;
- notre système relit le détail officiel via API;
- le snapshot complet continue de tourner pour réconcilier les écarts.
Important: le webhook ne devrait pas publier directement vers les sites. Il devrait seulement dire: "quelque chose a changé". Notre système interne reste responsable de valider, enrichir, publier, alerter et rollback.
## Choix technique recommande pour la nouvelle app v2
La nouvelle app v2 devrait être faite en Laravel.
Raisons principales:
- le projet est surtout un système métier back-office, pas une API publique temps réel;
- Laravel gère très bien les jobs, le scheduler, les commandes, les retries et les traitements planifiés;
- Horizon donne une bonne visibilité sur les queues si Redis est utilisé correctement;
- l'écosystème Laravel est très solide pour construire une `Publication Console` interne avec auth, rôles, policies et audit;
- les migrations, seeders, commands et jobs sont bien adaptés à ce type de pipeline;
- l'équipe connait déjà le contexte Laravel de la synchro actuelle;
- la logique de publication doit être fiable et observable plus que fashionable;
- le rollback, les rapports, les overrides et la validation JSON sont des cas naturels pour Laravel.
Le Media Pipeline existant peut rester séparé. Ce qui compte, c'est qu'il respecte le contrat:
```text
Media Pipeline
-> écrit media_assets
-> crée publication_jobs
Content Publication Engine
-> lit media_assets
-> traite publication_jobs
-> génère les JSON publics
```
Avec cette séparation, le Media Pipeline peut évoluer indépendamment. La nouvelle app v2, elle, devrait être le système de publication fiable, auditable et opérationnel; Laravel est le meilleur choix pour ce rôle.
## Risques principaux
|Risque|Probabilité|Impact|Réponse|
|---|---|---|---|
|Le projet devient trop large et tente de remplacer Directus, Mogador, JWP et les sites en même temps|Élevée|Élevé|Découper en pilote, garder PCI comme chantier séparé, livrer par incréments|
|Les règles métier actuelles sont implicites dans le vieux code|Élevée|Élevé|Extraire les règles, valider avec production, ajouter rapports de comparaison|
|Les corrections d'urgence entrent en conflit avec Louise au prochain export|Élevée|Élevé|Définir précédence, expiration d'override, audit et rapport d'écart|
|Le manifest est caché trop longtemps|Moyenne|Élevé|TTL court sur manifest, runs immuables cachés longtemps, procédure purge CDN|
|Media Pipeline et Publication Engine se couplent trop fortement|Moyenne|Élevé|Passer uniquement par `media_assets` et `publication_jobs`|
|Le système publie un JSON incomplet mais valide techniquement|Moyenne|Élevé|Ajouter validations métier, rapports attendu vs publié et seuils bloquants|
|L'absence d'API PCI v2 maintient certaines lenteurs|Élevée|Moyen|Concevoir le `Source Reader` remplaçable et optimiser avec snapshots/diffs internes|
|PCI ne livre pas d'events/webhooks à court terme|Élevée|Moyen|Ne pas en faire une dépendance du MVP; garder polling 3h et overrides internes|
|Infra refuse l'exposition de l'app interne|Moyenne|Moyen|Exposer seulement les JSON via bucket/CDN; garder la console sur réseau interne|
|Migration depuis Directus cause une interruption|Moyenne|Élevé|Double-run, comparaison, pilote, rollback, aucun big bang|
## Noms recommandés
```text
Louise
Systeme source editorial.
Louise Export
Export SQL complet génère par Louise.
Mogador Toolkit
API legacy actuelle autour de la base importée.
Louise API v2
Future API PCI demandee pour lecture bulk et events.
Content Publication Engine
Notre moteur interne qui génère les JSON publics.
Media Pipeline
Notre service interne qui gère JWP et l'état media.
Publication Console
Mini CMS interne pour édimestres/admins et overrides.
Published Content Feed
Les fichiers JSON versionnés consommes par les sites.
```
Avec ces noms, on peut parler de l'architecture sans que "Mogador" veuille dire quatre choses differentes.
## Position finale
La proposition recommandée est:
```text
Content Publication Engine interne
+ snapshots
+ diff maison
+ Media Pipeline JWP
+ Publication Console
+ JSON statiques
+ bucket/CDN
+ rapports
+ alertes
```
Directus peut être garde temporairement, mais ne devrait plus être le moteur principal de publication web.
Cette approche respecte les contraintes:
- Louise reste source de vérité.
- Le Mogador Toolkit actuel peut rester utilisé au depart.
- L'app interne n'est pas exposée à Internet.
- Les sites recoivent des données simples et rapides.
- Les fournisseurs peuvent être isolés par bucket ou préfixe.
- L'équipe interne garde la capacite de corriger en urgence.
- Les erreurs deviennent visibles avant que la production les signale.
- L'architecture reste compatible avec une future API Louise/Mogador v2 de PCI.