# Spec — Médiathèque de photos réutilisables

- **Date** : 2026-06-16
- **Contexte** : extension du module « Médias des pages » (cf.
  `docs/superpowers/specs/2026-06-16-medias-pages-design.md`).
- **Objectif** : offrir au mainteneur une **médiathèque** (réserve de photos importées)
  affichée à droite de l'écran d'édition d'une page, pour **réutiliser** une photo sur
  plusieurs emplacements/pages sans la re-télécharger à chaque fois.

## 1. Problème

Aujourd'hui chaque emplacement (`page.php`) impose un upload à l'unité. Une même photo
réutilisée sur plusieurs pages doit être re-téléchargée. Aucune table ne répertorie les
images uploadées (`public/uploads/pages/…`) : pas de réserve consultable.

## 2. Périmètre (décidé)

- **Bibliothèque gérée** : table dédiée `t_media_library` (nom convivial, date, dimensions).
- **Alimentation** : (a) import dédié « Importer dans le catalogue » (sans placement) ;
  (b) récupération automatique — tout upload fait via un emplacement est aussi enregistré
  dans la médiathèque (insert idempotent sur `image_url`).
- **Affectation** : bouton **« Choisir dans le catalogue »** par emplacement → arme la cible →
  bouton **« Utiliser ici »** sous une vignette l'affecte (single = remplace ; collection =
  ajoute). Le `label` de la photo sert de texte alternatif par défaut.
- **Gestion dans le panneau de droite** : importer, renommer/légender, supprimer (avec garde
  si la photo est encore utilisée).

### Hors périmètre (YAGNI)

- Dossiers / tags / recherche dans la médiathèque (liste simple, la plus récente d'abord).
- Recadrage / édition d'image (l'`ImageUploadService` redimensionne déjà).
- Médiathèque pour les images d'entités (salles/profs/événements) — hors de ce lot.
- Sélection multiple pour affectation en lot (un clic = une affectation).

## 3. Données — table `t_media_library` (migration 034, idempotente)

`CREATE TABLE IF NOT EXISTS` (pas de PREPARE/EXECUTE → sûr avec `migrate.php`). Encodage
`utf8mb4_unicode_ci`.

| colonne       | type                          | rôle                                       |
|---------------|-------------------------------|--------------------------------------------|
| `library_ID`  | BIGINT UNSIGNED PK AUTO_INCREMENT |                                        |
| `image_url`   | VARCHAR(255) NOT NULL, **UNIQUE** | `/uploads/pages/…` (dédoublonnage)     |
| `label`       | VARCHAR(255) NULL             | nom convivial / légende                    |
| `width`       | SMALLINT UNSIGNED NULL        | dimension capturée à l'import              |
| `height`      | SMALLINT UNSIGNED NULL        | dimension capturée à l'import              |
| `created_at`  | TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP |                             |

> Rappel : appliquer la migration 034 à la main sur `aldanadb` (local) et en prod **avant**
> de tester (cf. limitation `migrate.php`).

## 4. Architecture

### 4.1 Repository — `App\Repositories\MediaLibraryRepository`

PDO préparé (vérif `?`/params). Méthodes :

- `listAll(): array` — toutes les images, `ORDER BY created_at DESC, library_ID DESC`.
- `findByID(int $id): ?array`.
- `create(string $url, ?string $label, ?int $w, ?int $h): int`.
- `registerIfAbsent(string $url, ?string $label, ?int $w, ?int $h): void` — `INSERT … ON
  DUPLICATE KEY UPDATE library_ID = library_ID` (no-op si l'URL existe déjà). Sert à la
  récupération automatique des uploads d'emplacement.
- `rename(int $id, ?string $label): void`.
- `delete(int $id): void`.
- `isReferenced(string $url): int` — nombre d'emplacements (`t_media_overrides`) utilisant
  cette URL (garde de suppression).

### 4.2 Contrôleur — ajouts à `App\Controllers\Admin\MediaController`

- `libraryImport()` — `POST` : upload via `ImageUploadService` (entité `pages`, slug
  `library`), capture des dimensions (`getimagesize` sur le fichier stocké), `create()`.
  Accepte plusieurs fichiers (`image_file[]`) ; chaque fichier valide est enregistré, les
  erreurs par fichier sont accumulées en flash. Redirige vers la page d'origine
  (`return` param) ou l'index.
- `libraryRename()` — `POST` : `library_ID` + `label` → `rename()`.
- `libraryDelete()` — `POST` : `library_ID` → si `isReferenced(url) > 0`, refuse avec flash
  (« utilisée sur N emplacement(s) ») ; sinon `delete()` + `ImageUploadService::delete(url)`.
- `assignFromLibrary()` — `POST` : `slot_key` + `library_ID`. Valide `slot_key` via
  `slotDef()` (rejet si inconnue). Résout l'URL + `label` de la bibliothèque. Pour un slot
  `single` → `MediaOverrideRepository::upsertSingle(slotKey, url, label)` ; pour `collection`
  → `addToCollection(slotKey, url, label)`. Redirige vers
  `/admin/settings/medias/page?key=<pageKey>`.
- **Enrichissement** de `saveSingle()` et `collectionAdd()` : après un upload de fichier
  réussi, appeler `registerIfAbsent(url, null, w, h)` pour alimenter la médiathèque.
- `page()` et éventuellement `index()` passent `library => (new MediaLibraryRepository())->listAll()` à la vue.

### 4.3 Routes (`config/routes.php`, middleware `admin`, CSRF global)

```
POST /admin/settings/medias/library/import  → MediaController::libraryImport
POST /admin/settings/medias/library/rename  → MediaController::libraryRename
POST /admin/settings/medias/library/delete  → MediaController::libraryDelete
POST /admin/settings/medias/assign          → MediaController::assignFromLibrary
```

### 4.4 Vues

- `views/pages/admin/medias/page.php` : passe en **deux colonnes** (CSS flex/grid) — gauche =
  emplacements existants (chaque slot reçoit un bouton « Choisir dans le catalogue » avec
  `data-choose-slot`/`data-choose-label`) ; droite = inclusion du partial.
- Nouveau partial `views/partials/admin/_media_library_panel.php` : formulaire d'import,
  grille de vignettes (chaque vignette : aperçu, champ rename, bouton supprimer, bouton
  « Utiliser ici »). Sticky.
- **JS vanilla** (dans `page.php`) : clic sur « Choisir » → mémorise `activeSlot`/label,
  affiche « Placer dans : <label> », révèle les boutons « Utiliser ici » ; à la soumission
  d'un « Utiliser ici », injecte `activeSlot` dans le champ caché `slot_key` ; si aucun slot
  armé, bloque avec un message.

## 5. Sécurité

- **CSRF** : tous les nouveaux POST passent par le `CsrfMiddleware` global ; chaque formulaire
  contient `csrf_field()`.
- **Upload** : `ImageUploadService` (P7) inchangé (MIME réel, anti-bombe, re-encode WebP,
  filename forgé, confinement `public/uploads/`).
- **`slot_key`** : revalidée contre le registre dans `assignFromLibrary` (pas d'écriture
  arbitraire).
- **Suppression fichier** : via `ImageUploadService::delete()` (confiné `/uploads/`), et
  seulement si non référencée.
- **`library_ID`** : entier ; `findByID` borne l'accès.

## 6. Critères d'acceptation

1. Depuis l'écran d'édition d'une page, un panneau de droite liste les photos de la
   médiathèque (vignette + nom + dimensions), la plus récente d'abord.
2. « Importer dans le catalogue » ajoute une/des photo(s) à la réserve sans les placer.
3. Toute photo uploadée via un emplacement apparaît aussi dans la médiathèque (sans doublon).
4. « Choisir dans le catalogue » sur un emplacement puis « Utiliser ici » sur une vignette
   affecte la photo : remplacement (single) ou ajout (collection), avec le nom comme alt par
   défaut.
5. Renommer une photo met à jour son nom ; le nom sert d'alt par défaut à l'affectation.
6. Supprimer une photo non utilisée la retire (table + fichier) ; supprimer une photo utilisée
   est refusé avec un message indiquant le nombre d'emplacements concernés.
7. Tous les POST sont protégés CSRF ; uploads soumis au durcissement P7 ; `slot_key` revalidée.
8. Base sans la table 034 → l'écran ne plante pas (dégradation : panneau vide / message).

## 7. Tests

Logique majoritairement BDD/contrôleur (pas de BDD de test dans le harnais). On s'appuie sur
la recette manuelle (critères §6) et les tests existants du registre. Si une partie pure est
isolable (ex. validation `slot_key` déjà couverte par `slotDef`/registre), pas de nouveau test
nécessaire. Vérification PHPStan niveau 6 sur les nouveaux fichiers + `php -l`.

## 8. Plan de livraison (indicatif)

1. Migration 034 + `MediaLibraryRepository`.
2. Contrôleur : `assignFromLibrary`, `libraryImport`, `libraryRename`, `libraryDelete`,
   enrichissement `saveSingle`/`collectionAdd`, passage de `library` aux vues.
3. Routes.
4. Partial `_media_library_panel.php` + réorg 2 colonnes de `page.php` + JS d'armement.
5. Recette manuelle + rappel migration prod.
