# Media

Gestionnaire de médias CMS — bibliothèque centrale pour les images, PDFs, vidéos et autres fichiers attachés au contenu publié sur le site (pages, articles, modules tiers).

> `media` est conçu pour les **assets de contenu web** (la plupart privés, certains publics), pas pour le stockage personnel de fichiers utilisateurs.

## Activation

Le module est **désactivé par défaut**. Pour l'activer :

```bash
# .env.local
ZEN_MEDIA=true
```

Puis appliquer le schéma BD :

```bash
npx zen-db init
```

L'item « Médias » apparaît comme entrée top-level de la sidebar admin (sans section parente), accessible aux utilisateurs ayant la permission `media.view`.

## Architecture

| Fichier | Rôle |
|---|---|
| [`schema.server.js`](./schema.server.js) | Tables `zen_media` et `zen_media_references` + seed des permissions media. |
| [`api.server.js`](./api.server.js) | CRUD + attach/detach. Source de vérité unique pour la BD et S3. |
| [`routes.server.js`](./routes.server.js) | Routes API admin + route publique de service de fichier. |
| [`navigation.js`](./navigation.js) | Enregistre la section sidebar (gated par `ZEN_MEDIA`). |
| [`permissions.js`](./permissions.js) | Catalogue de permissions (`media.view`, `media.upload`, `media.delete`). |
| [`pages/MediaPage.client.js`](./pages/MediaPage.client.js) | Page admin `/admin/media`. |
| [`components/MediaPicker.client.js`](./components/MediaPicker.client.js) | Composant réutilisable par les modules tiers. |
| [`components/MediaDetailsModal.client.js`](./components/MediaDetailsModal.client.js) | Modale d'édition d'un média (visibilité, alt, caption). Réutilisable hors de la page admin Médias — utilisée notamment par le bloc image du BlockEditor. Accepte `media={…}` ou `slug="…"`. |
| [`components/MediaImage.client.js`](./components/MediaImage.client.js) | Wrapper d'affichage qui dispatche `next/image` ↔ `<img>` selon la visibilité. |

### Modèle de données

`zen_media` — registre central des médias (slug URL-safe, clé S3, MIME, taille, visibilité, owner, alt/caption).

`zen_media_references` — table de jointure (media_id, source_type, source_id, field). FK `ON DELETE RESTRICT` empêche la suppression d'un média référencé par un contenu existant.

Conventions de `field` connues :
- `featured_image` / `gallery` / *etc.* — champs nommés des modules consommateurs (cf. `attachMedia`).
- `block_image` — image insérée dans du contenu BlockEditor. Synchronisée
  automatiquement par [`syncBlockImageReferences`](../../shared/components/BlockEditor/mediaLink.server.js)
  à chaque save du document hôte. Voir [BlockEditor README — Liaison avec la médiathèque](../../shared/components/BlockEditor/README.md#liaison-avec-la-médiathèque-mediaslug).

### Stockage

Tous les uploads sont stockés sous `media/<yyyy>/<mm>/<filename-unique>` via [`@zen/core/storage`](../../core/storage). **Tous les objets S3 sont privés** au niveau du bucket — l'accès public passe exclusivement par notre route HTTP qui valide la `visibility` en BD avant de proxyfier le contenu.

## URLs

- **Service de fichier** : `GET /zen/api/media/file/:slug`
  - `visibility = 'public'`  → servi sans authentification (cache long)
  - `visibility = 'private'` → requiert une session avec `media.view`
- **CRUD admin** : `GET|POST|PATCH|DELETE /zen/api/media[/:id]`
- **Lecture par slug (admin)** : `GET /zen/api/media/by-slug/:slug` — utile depuis les composants qui n'ont que le slug (ex. bloc image du BlockEditor).

Le slug est un identifiant aléatoire de 12 caractères (base64url) — non énumérable.

## API serveur (pour les modules consommateurs)

```js
import {
  uploadMedia,
  getMediaById,
  getMediaBySlug,
  listMedia,
  updateMedia,
  deleteMedia,
  attachMedia,
  detachMedia,
  detachAllForSource,
  buildMediaUrl,
  isMediaEnabled,
} from '@zen/core/features/media';

// Upload depuis une server action / API handler :
const { success, media } = await uploadMedia({
  file,                       // File ou Buffer
  uploadedBy: session.user.id,
  visibility: 'public',
  altText: 'Logo de la conférence 2026',
});

// Au moment de sauvegarder un post avec featured image :
await attachMedia({
  mediaId: media.id,
  sourceType: 'post',
  sourceId: post.id,
  field: 'featured_image',
});

// Au moment de supprimer le post : libérer toutes les références.
await detachAllForSource({ sourceType: 'post', sourceId: post.id });
```

## MediaPicker (composant client)

```jsx
'use client';
import { useState } from 'react';
import MediaPicker from '@zen/core/features/media/picker';

function FeaturedImageField() {
  const [open, setOpen] = useState(false);
  const [media, setMedia] = useState(null);

  return (
    <>
      <button onClick={() => setOpen(true)}>Choisir une image</button>
      <MediaPicker
        isOpen={open}
        onClose={() => setOpen(false)}
        accept="image/*"
        visibility="public"
        uploadVisibility="public"
        onSelect={(m) => { setMedia(m); }}
      />
      {media && <img src={`/zen/api/media/file/${media.slug}`} alt={media.alt_text} />}
    </>
  );
}
```

| Prop | Type | Description |
|---|---|---|
| `isOpen` | `boolean` | Contrôlé par le parent. |
| `onClose` | `() => void` | |
| `onSelect` | `(media \| media[]) => void` | Reçoit l'objet média (ou tableau si `multiple`). |
| `accept` | `string` | Filtre MIME pour l'`<input type="file">` et le filtre par défaut de la grille. |
| `visibility` | `'public' \| 'private' \| 'any'` | Filtre par défaut. |
| `uploadVisibility` | `'public' \| 'private'` | Visibilité appliquée aux nouveaux uploads. |
| `multiple` | `boolean` | Sélection multiple avec validation explicite. |

## Affichage des images : `<MediaImage>`

Pour afficher un média image dans n'importe quel composant client, utiliser le wrapper `MediaImage` plutôt qu'un `<img>` direct :

```jsx
import MediaImage from '@zen/core/features/media/components/MediaImage.client';

<MediaImage media={media} width={800} height={600} alt="..." />
// ou en mode "remplir le conteneur parent" (parent doit être `position: relative`) :
<MediaImage media={media} fill sizes="(max-width: 768px) 100vw, 50vw" className="object-cover" />
```

Le wrapper choisit automatiquement entre `next/image` et `<img>` natif :

- `media.visibility === 'public'` → `next/image` (srcset AVIF/WebP, lazy, cache CDN).
- `media.visibility === 'private'` → `<img>` natif. **C'est volontaire** : `/_next/image` met en cache la réponse optimisée par URL au niveau Next/CDN. Si on optimisait un média privé, l'image serait servie en clair à tout visiteur après le premier hit, contournant la vérification de session faite par `/zen/api/media/file/:slug`.

Côté projet hôte, **aucune configuration `next.config.js` n'est requise** : `/zen/api/media/file/...` est same-origin et ne nécessite pas de `remotePatterns`.

## Permissions

| Clé | Description |
|---|---|
| `media.view` | Voir la liste et accéder aux médias privés. |
| `media.upload` | Téléverser et modifier les métadonnées. |
| `media.delete` | Supprimer un média non référencé. |

Toutes sont auto-attribuées au rôle `admin` au moment du seed.

## Limites

- Pas de génération de thumbnails — l'original est servi tel quel (le navigateur fait le redimensionnement). Un v2 pourrait intégrer `sharp` ou déléguer à Cloudflare Images.
- Pas de dossiers ni de tags — liste plate avec recherche par nom + filtre par type/visibilité.
- Pas d'édition d'image (crop, rotation).
- SVG bloqué par `validateUpload` (risque XSS).

## Distinction avec un futur module `files`

Le nom **`files`** est réservé à un futur module type Drive/Dropbox/Nextcloud (stockage personnel privé, partage, dossiers, versioning). Ce module-ci (`media`) est strictement orienté **assets attachés au contenu publié**.
