# Plano 19 — Media e Gallery (ingestão, digestão e `<picture>`)

> **Milestone:** M2→M3 · **Depende de:** 08, 12 · **Habilita:** 14 (SEO/perf) · **ADRs:** 018, 025
> **Objetivo:** um **fluxo único** de entrada de imagem → digestão (AVIF/WebP responsivo) →
> catálogo → **`<picture>` altamente otimizada**, sem reescrever HTML.

---

## 1. Arquitetura (dois services, ADR-015/026)

```
src/services/Media/                     # headless (ingestão/digestão/storage + componentes)
├── Providers/MediaServiceProvider.php
├── Services/MediaInputer.php           # porta de entrada única (roteia por MIME)
├── Services/Optimizers/ImageOptimizer.php   # variantes AVIF/WebP + metadata
├── Services/Optimizers/PdfOptimizer.php     # (futuro) PDF/A + linearização
├── Services/Midia/{Audio,Spreadsheet}Optimizer.php   # (futuro)
├── Views/components/{picture,input,viewer}.blade.php
└── Test/

src/services/Gallery/                   # catálogo + picker
├── Providers/GalleryServiceProvider.php
├── Services/GalleryService.php         # upload/list/find/archive/restore
├── DTOs/GalleryConfig.php              # allowedMimeTypes/maxFileSize/allowShared
├── Models/GalleryMedia.php             # variants jsonb, metadata jsonb, softDeletes
├── Migrations/…                        # gallery_media (ADR-017)
├── Routes/cms.php                      # superfície ADMIN (/cms), nunca pública
└── Views/components/gallery-picker.blade.php
```

- **Media** é agnóstico e headless; **Gallery** usa o Media e expõe **superfície administrativa**
  (refinamento do ADR-015: rotas só sob `/cms`).

## 2. Ingestão (porta única)

- [x] `MediaInputer::ingest()` valida **MIME detectado no servidor** (finfo) contra allowlist.
- [x] Storage via **Flysystem** disco **`s3`** (RustFS ↔ AWS S3 por `.env`, ADR-030), sem `StorageService` próprio; bucket idempotente via `storage:ensure-bucket`.
- [x] Digestão em **fila** (Valkey); status `PENDENTE → PROCESSANDO → CONCLUIDO | FALHA` (`media.media_uploads` + `ProcessMediaUpload`; o editor faz polling).
- [x] Log estruturado (`module`, `action`, `id`, `duration_ms`).
- [x] **Limites por tipo** (`config/media.php` + `MediaLimits`): `soft` (recomendado → **aviso**) e
      `hard` (→ **recusa** com a mensagem do limite); borda/PHP aceitam o **maior** hard
      (PDF 100 MiB) — `MediaLimitsTest` prova Apache/Caddy/PHP.
- [x] Aviso ao autor acima do recomendado no editor (nota persistente `role="status"`) e no
      catálogo (`warning` flash); a mensagem do 422 é a do tipo, não um `max` genérico.
- [x] **Lote** (F5.I4.b): 20–50 imagens = N requisições de 1 arquivo com `batch_id`; orçamento
      **50 arquivos/250 MiB**, cota **2 GiB/dia**, rate `media-upload` 120/min, concorrência 3;
      `media.media_batches` + `media_uploads.batch_id/user_id`; cliente na grade e commit do
      Gallery (alt por imagem, falha parcial, idempotente). Sem multipart de 50.

## 3. Digestão (extrema otimização)

- [x] `ImageOptimizer` gera **AVIF + WebP** nas larguras `[360,720,1200,1920,2560]` (≤ original).
- [x] **SVG** pulado; imagem < menor largura → **1 variante** na largura original (`original`).
- [x] **`width`/`height`** originais em `metadata` (jsonb) → **CLS zero**.
- [ ] Qualidade 65/75 (libvips); `stripImage()` de EXIF explícito ainda pendente (libvips já normaliza a maior parte).

## 4. Catálogo (Gallery)

- [x] Migração `gallery_media` (ADR-017: transação, `lock_timeout`, `down()`→`backup_*`).
- [x] `GalleryConfig` (readonly) + `GalleryService` (upload/list/find/archive/restore).
- [x] `is_shared`/`module` (token) — visibilidade por módulo (schema `gallery`, `gallery.gallery_media`).
- [x] **Arquivar não apaga o físico** (retenção — ADR-019).
- [x] **Lote no catálogo**: `GalleryService::catalog()` usa o `payload` já digerido (sem redigestão)
      e `GalleryBatchCommitController` cataloga `concluido` com alt, reportando `skipped`.
- [ ] `gallery-picker` (componente de uso) acessível: input drag&drop, crop, alt/descrição, grade.
      Hoje há o catálogo CMS nativo (`/cms/galeria`) com upload por formulário; o picker
      drag&drop/crop fica como follow-up (o contrato `GalleryStore` já o suporta).

## 5. Render (`<picture>`)

- [x] `<x-service-media::picture>` (`x-media::picture`): `<source type="image/avif">` + `<source type="image/webp">`
      + `<img>` com `width/height`, `decoding="async"`, `loading="lazy"`,
      `fetchpriority="high"` no LCP (anti double-download — lição do v6).
- [x] `alt` obrigatório no catálogo (Gallery); conteúdo decorativo pode usar `alt=""` (WCAG).
- [x] **Sem** middleware reescrevendo HTML (abandonar o padrão do v6) — gate `MediaNoRewriteTest`.
- [x] Integração com o **editor rico** (ADR-028): a imagem inserida no TipTap guarda a referência
      da mídia e o portal renderiza `<picture>` — a mesma otimização, sem intervenção no HTML.

## 6. Preservação e segurança (futuro → plano 17/ADR-020/019)

- [ ] PDF → **PDF/A-2a + linearização** (`qpdf`) e capa WebP.
- [ ] Scan de malware antes de processar; **crypto-shredding** para mídia sensível.
- [ ] `comando de backfill (backlog)` idempotente para backfill do acervo.

## 7. Atividades

- [x] Criar `services/Media` (provider + `MediaInputer` + `ImageOptimizer` + componentes).
- [x] Criar `services/Gallery` (migração + model + DTO + service + catálogo `/cms`).
- [x] Registrar contratos no service; nenhum service importa módulo.
- [x] Job de digestão (`ProcessMediaUpload`); comando de backfill idempotente fica como follow-up.
- [x] Componente `<picture>` + integração no CMS/portal.
- [x] `<x-service-media::input>` consumido pelo editor rico (ADR-028) para inserir imagem no AST.

## 8. Testes e evidências

- [x] Upload de JPG grande → variantes AVIF+WebP ≤ original; `<img>` com `width`/`height`.
- [x] SVG não gera variante; imagem < menor largura gera 1 variante.
- [x] `<picture>` sem variantes cai no original; `alt` vazio bloqueia publicação **no catálogo**.
- [x] Arquivar mantém o arquivo físico; restaurar reativa sem redigestão.
- [x] Fronteira: módulo usa o service **sem** importar outro módulo (teste de contrato/fake).
- [x] Nenhuma resposta HTML é reescrita por middleware; catálogo CMS sem violação de eixos/axe.

## 9. Definition of Done

- [x] Fluxo único de entrada→digestão→catálogo→`<picture>` funcionando.
- [x] Imagens leves (AVIF/WebP) sem CLS e acessíveis; reuso por ≥2 módulos.
- [x] Nativo (Flysystem/filas/jsonb); nenhuma dependência pesada de mídia.

## 10. Riscos

| Risco | Mitigação |
|---|---|
| Encoder sem AVIF | Preferir libvips; fallback Imagick/WebP |
| Digestão síncrona trava o upload | Sempre em fila + status |
| Alt text ignorado | Bloqueio na publicação (a11y) |
| Backfill pesado | Comando chunked idempotente |
| Service virar "módulo" | Só superfície admin (`/cms`), sem rotas públicas |
