# Spec — Gestionnaire de médias de page (« Médias des pages »)

- **Date** : 2026-06-16
- **Phase** : reprise existant / CMS léger (cf. plan mise en production)
- **Objectif** : permettre au mainteneur du site de remplacer, depuis la console admin, les
  images et photos présentes sur les pages publiques (et de gérer les images d'entités au
  même endroit), sans toucher au code.

## 1. Problème

Aujourd'hui les images des pages publiques (thème `public` et thème `emerald`) sont écrites
**en dur** dans les templates PHP, par ex. le carrousel de 10 slides dans
`views/pages/emerald/home.php` (`/assets/img/emerald/slide-1.jpg` … `slide-10.jpg`).
Changer une image impose une modification de code + déploiement. Le mainteneur non-technique
ne peut rien changer.

L'`ImageUploadService` existant (P7) gère déjà upload + re-encodage WebP + durcissement
sécurité, mais uniquement pour des **entités BDD** (salles, profs, événements, pratiques,
vidéos) liées à des fiches — pas pour les images décoratives des pages.

## 2. Périmètre (décidé)

- **Toutes** les images de page deviennent remplaçables (recensement exhaustif requis).
- Zones à image unique → **remplacement 1:1**.
- Zones à images multiples (carrousels/galeries) → liste gérable : **ajouter / supprimer /
  réordonner / remplacer**.
- Gestion du **texte alternatif (alt)** par image (SEO + accessibilité).
- Module **intégré dans « Site & contenu »** (`/admin/settings`), pas de nouvelle entrée
  sidebar. Sous-pages sous `/admin/settings/medias` (la rubrique « Site & contenu » reste
  surlignée car le matcher est `str_starts_with`).
- **Centralisation des médias d'entités** : le module expose aussi les images des fiches
  (salles, profs, événements) en **lecture/écriture sur leurs colonnes existantes** via les
  repositories actuels — **pas de stockage dupliqué**, juste une UI agrégée alternative.

### Hors périmètre (YAGNI)

- Gestion du **texte rédactionnel** des pages (ce n'est pas un CMS de contenu, uniquement
  les médias). Le texte reste géré par `t_site_settings` / templates.
- Recadrage / éditeur d'image dans le navigateur (l'`ImageUploadService` redimensionne déjà).
- Versioning / historique des images.
- Bibliothèque média réutilisable (une image = un slot, pas de médiathèque partagée).

## 3. Architecture

### 3.1 Principe de résolution

Chaque image remplaçable a une **clé de slot stable** :
`<theme>.<page>.<slot>` (ex. `emerald.home.carousel`, `public.le-lieu.hero`).

Les templates n'écrivent plus de chemin en dur ; ils appellent des helpers qui résolvent
**override BDD si présent → sinon défaut codé**. Conséquences :

- Le site fonctionne toujours même base vide (les défauts restent dans le code = filet de
  sécurité, conforme au playbook ALDANA).
- « Revenir à l'original » = supprimer la/les ligne(s) d'override.

### 3.2 Helpers globaux (ajout dans `src/Helpers/functions.php`)

```php
// Image unique : renvoie l'URL (override BDD ou défaut codé).
function media(string $slotKey, string $defaultUrl = ''): string

// Texte alternatif d'une image unique (override BDD ou défaut codé).
function media_alt(string $slotKey, string $defaultAlt = ''): string

// Collection ordonnée. $defaults = liste de strings (URL) ou de ['url'=>, 'alt'=>].
// Renvoie une liste de ['url' => string, 'alt' => string].
function media_list(string $slotKey, array $defaults = []): array
```

**Sémantique collection** : si ≥1 ligne d'override existe pour la clé, la liste BDD remplace
**entièrement** les défauts ; sinon, fallback sur les défauts codés.

**Cache statique par requête** : au premier appel d'un helper `media*`, un unique
`SELECT * FROM t_media_overrides` charge tous les overrides en mémoire statique ; les
résolutions suivantes lisent ce cache (pas de N+1). Le cache n'est jamais peuplé avec un
résultat vide « caché » de façon persistante : il est seulement à portée de requête HTTP.

### 3.3 Registre déclaratif — `config/media_slots.php`

Source de vérité des emplacements, groupés par page. Chargé via le helper `config()` existant.

```php
return [
  'emerald.home' => [
    'label' => 'Accueil (Emerald)',
    'route' => '/',
    'slots' => [
      'carousel' => [
        'label'       => 'Carrousel principal',
        'type'        => 'collection',          // 'single' | 'collection'
        'recommended' => '1920×1080',
        'defaults'    => [
          ['url' => '/assets/img/emerald/slide-1.jpg', 'alt' => 'Forêt de pins'],
          // … 10 slides
        ],
      ],
    ],
  ],
  // … toutes les pages recensées (voir 3.6)
];
```

Champs d'un slot : `label` (FR), `type`, `recommended` (dimensions conseillées, affichage),
`default` (single) ou `defaults` (collection), `default_alt` (single, optionnel).

### 3.4 Données — table `t_media_overrides`

Migration `033_create_media_overrides.sql`, **idempotente** (pattern `IF NOT EXISTS` /
vérif `INFORMATION_SCHEMA`, cf. migrations 018/030/032). Encodage `utf8mb4_unicode_ci`.

| colonne        | type                          | rôle                                            |
|----------------|-------------------------------|-------------------------------------------------|
| `override_ID`  | BIGINT UNSIGNED PK AUTO_INCREMENT |                                             |
| `slot_key`     | VARCHAR(191) NOT NULL         | ex. `emerald.home.carousel`                     |
| `position`     | INT UNSIGNED NOT NULL DEFAULT 0 | ordre dans une collection ; 0 pour un single  |
| `image_url`    | VARCHAR(255) NOT NULL         | `/uploads/pages/...` ou URL externe             |
| `alt_text`     | VARCHAR(255) NULL             | texte alternatif                                |
| `created_at`   | TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP |                                  |
| `updated_at`   | TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP |       |

Contrainte : `UNIQUE (slot_key, position)`. Index implicite sur `slot_key` (préfixe de
l'unique) suffisant pour le chargement.

> Rappel migrations : `migrate.php` casse sur le pattern PREPARE/EXECUTE (cf. mémoire
> projet). La migration 033 sera appliquée **à la main** via mysql/phpMyAdmin sur `aldanadb`
> (local) et en prod. **Rappeler d'appliquer la migration AVANT de tester.**

### 3.5 Stockage des fichiers

Ajouter `'pages'` à la constante `ImageUploadService::ENTITIES`. Les uploads de médias de
page atterrissent dans `public/uploads/pages/`, protégé par le `.htaccess` durci P7.
Le `slug` passé à l'uploader est dérivé de la clé de slot (ex. `emerald-home-carousel`).

### 3.6 Recensement des emplacements (livrable d'audit)

Avant de peupler le registre, auditer **tous** les templates de pages pour lister chaque
référence image (`<img>`, `background-image`, `url(...)`, tableaux de slides). Sources
connues à couvrir au minimum :

- `views/pages/emerald/` : `home.php` (carrousel 10 slides + autres), `events.php`,
  `offres.php`, `yoga-et-moi.php`.
- `views/pages/public/` : `home.php`, `le-lieu.php`, `pratiques.php`,
  `cours-particuliers.php`, `evenements.php`, `yoga-et-moi.php`, `videos/`.

Le résultat est consigné dans `docs/audit-medias-pages.md` (clé, page, type, chemin actuel,
dimensions). Ce document pilote le contenu de `config/media_slots.php` et la migration des
templates.

## 4. Module admin « Médias des pages »

Accès via la rubrique **Configuration → Site & contenu**. La page `SettingsController::index`
gagne des liens vers les écrans médias (ou un onglet). Routes sous `/admin/settings/medias`.

### 4.1 Composants

- **`MediaController`** (`App\Controllers\Admin`) — utilise le trait `HandlesPhotoUpload`.
  - `index()` — liste des pages depuis le registre (navigation par page) + section
    « Médias d'entités ».
  - `page()` — `?key=emerald.home` : tous les slots de la page avec vignette(s) actuelle(s),
    champ upload (style `_photo_field`), champ alt. Collections : ajouter/supprimer/
    réordonner + « revenir aux images d'origine ».
  - `saveSingle()` — upsert/suppression d'un slot single.
  - `collectionAdd()` / `collectionDelete()` / `collectionReorder()` — gestion d'une collection.
  - `revert()` — supprime tous les overrides (et fichiers locaux) d'un slot → retour au défaut.
  - Section entités : `entityImage()` (édition) déléguée aux repositories existants
    (RoomRepository, TeacherRepository, EventRepository) — écrit dans la colonne image de la
    fiche, **sans** passer par `t_media_overrides`.
- **`MediaOverrideRepository`** (`App\Repositories`) — PDO préparé, vérif
  `substr_count($sql,'?') === count($params)`. Méthodes : `loadAll()`, `findBySlot()`,
  `upsertSingle()`, `addToCollection()`, `deleteByID()`, `deleteBySlot()`, `reorder()`,
  `maxPosition()`.

### 4.2 Routes (ajouts dans `config/routes.php`, middleware `admin`)

```
GET  /admin/settings/medias                  → MediaController::index
GET  /admin/settings/medias/page             → MediaController::page
POST /admin/settings/medias/single           → MediaController::saveSingle
POST /admin/settings/medias/collection/add   → MediaController::collectionAdd
POST /admin/settings/medias/collection/del   → MediaController::collectionDelete
POST /admin/settings/medias/collection/order → MediaController::collectionReorder
POST /admin/settings/medias/revert           → MediaController::revert
POST /admin/settings/medias/entity           → MediaController::entityImage
```

### 4.3 Vues

`views/pages/admin/medias/index.php`, `views/pages/admin/medias/page.php`. Réutilisent le
style du partial `_photo_field` pour la cohérence visuelle.

## 5. Migration des templates

Pour chaque emplacement recensé, remplacer le chemin en dur par l'appel helper en
**passant l'ancien chemin comme défaut** :

```php
// Avant
'/assets/img/emerald/slide-1.jpg',
// Après (le défaut reste = filet de sécurité)
media_list('emerald.home.carousel', $defaultSlides)
```

Aucune image n'est supprimée du repo : les fichiers `/assets/img/...` restent les défauts.

## 6. Sécurité

- **CSRF obligatoire** sur tous les POST : `csrf_field()` dans les formulaires + vérification
  du token dans le contrôleur (cohérent avec le reste de l'admin).
- Toute la chaîne de durcissement P7 réutilisée telle quelle (MIME réel + cross-check
  getimagesize, anti decompression bomb, re-encodage WebP destructeur de polyglots,
  filename forgé serveur, confinement `public/uploads/`).
- `slot_key` côté contrôleur : validé contre le **registre** (`config/media_slots.php`) —
  une clé inconnue est rejetée (pas d'écriture de slot arbitraire).
- `position` : entier non signé borné.
- URL externe optionnelle : même politique que `_photo_field` (l'upload prime sur l'URL).
- Suppression de fichier via `ImageUploadService::delete()` (confinement + idempotent).

## 7. Tests (PHPUnit)

- `media()` / `media_alt()` : override présent → renvoie override ; absent → renvoie défaut.
- `media_list()` : collection avec overrides → liste BDD ordonnée ; sans override → défauts ;
  défauts fournis en strings **ou** en `['url','alt']`.
- Cache statique : un seul accès BDD pour N appels dans la même requête.
- Repository : upsert single (une seule ligne `position=0`), add/delete/reorder collection,
  `deleteBySlot` (revert) supprime toutes les lignes.
- Sémantique revert : après `revert`, le helper retombe sur les défauts codés.
- Validation `slot_key` inconnue rejetée.

## 8. Critères d'acceptation

1. Depuis « Site & contenu », le mainteneur voit la liste des pages et leurs images actuelles.
2. Il peut remplacer une image unique (upload), elle s'affiche sur la page publique.
3. Il peut ajouter, supprimer, réordonner les images d'un carrousel.
4. Il peut saisir un texte alternatif par image, rendu dans le HTML public.
5. « Revenir à l'original » restaure l'image codée par défaut.
6. Les images d'entités (salles/profs/événements) sont éditables depuis le même module,
   écrivant dans leurs colonnes existantes (aucun doublon de stockage).
7. Base vide → le site affiche les images par défaut sans erreur.
8. Tous les POST sont protégés CSRF ; uploads soumis au durcissement P7.

## 9. Plan de livraison (indicatif, détaillé dans le plan d'implémentation)

1. Migration 033 + `MediaOverrideRepository` + tests repo.
2. Helpers `media* ` + cache statique + tests helpers.
3. Audit des emplacements → `docs/audit-medias-pages.md` → `config/media_slots.php`.
4. `MediaController` + routes + vues admin (section pages).
5. Migration des templates vers les helpers (défauts conservés).
6. Section « Médias d'entités » (vue agrégée sur repos existants).
7. Recette manuelle + rappel application migration prod.
