# Backlog — Upgrade Diário Oficial v6 (migrar melhorias v5 preservando revisor/auditoria)

> **Data:** 2026-09-11
> **Branch:** `update_diario_v5_to_v6`
> **Fonte de comparação (v5):** `diario-oficial-reimplementacao/`
> **Documento base:** `ESTRUTURA_DIARIO_ATUAL.md`
> **Rastreio (seeds):** Epic `v6_diario_emergencial-e4cb` · Plan `pl-8443` · 18 issues `v6_diario_emergencial-*`
> **Objetivo:** trazer as **melhorias/otimizações** do módulo v5 para a estrutura v6 atual, **preservando** as features v6 (workflow 4 olhos/revisor, auditoria pública LGPD, `cover_image_id`, geração com polling, download).

---

## 1. Decisões aprovadas

| Decisão | Escolha |
|--------|---------|
| Escopo | **Fases 1–3** (Fase 4 fica como backlog) |
| Motor de anexos | **Manter o grid 4-up atual** (`gridBoxesForColumns`/`printAnexoMateria`) **+ adicionar `mergePdfs`** |
| Editor de capa | **Não** restaurar cropper/rotação/texto; manter upload/preview/remove e apenas corrigir a qualidade |
| Features v6 | **Intocadas** — revisor/4 olhos, auditoria pública, `cover_image_id`, polling, download |

---

## 2. Estrutura de rastreio (seeds)

- **Epic:** `v6_diario_emergencial-e4cb` — *Upgrade Diário Oficial v6: migrar melhorias do v5 preservando revisor/auditoria*
- **Plan:** `pl-8443` — *Upgrade Diário Oficial v6: migrar melhorias v5 (Fases 1-3)* — status `approved`, rev 1
- **Filhos:** 18 issues (ver tabela §3)

Comandos úteis:

```bash
sd plan show pl-8443
sd ready                 # próximos passos desbloqueados
sd list --label fase-1   # itens da fase 1
sd show <id>             # detalhe de uma issue
```

---

## 3. Backlog por fase (issues)

### Fase 1 — Resiliência da geração de PDF (prioridade 1)

| # | Issue | Tipo | Arquivo-alvo principal | Depende de |
|---|-------|------|------------------------|-----------|
| F1.1 | `v6_diario_emergencial-4862` — Repo: timeout, QR/progresso com try-catch, logging, escrita atômica e `isPublished` | task | `app/Repositories/Eloquent/Prefecture/DiarioOficial/DiarioOficialRepository.php` | — |
| F1.2 | `v6_diario_emergencial-2177` — FPdfService: assinatura digital somente em edições publicadas | task | `app/Services/FPdfService.php` | — |
| F1.3 | `v6_diario_emergencial-f26e` — DiarioOficialComponent: filtrar certificado não deletado e tolerar inválido | task | `app/Http/Components/Prefecture/Contents/Preview/DiarioOficialComponent.php` | — |
| F1.4 | `v6_diario_emergencial-f52f` — printAnexoMateria: fallback para imagens em falha de import | task | `DiarioOficialRepository.php` | — |

### Fase 2 — Importação/otimização e ajustes (prioridade 2–3)

| # | Issue | Tipo | Arquivo-alvo principal | Depende de |
|---|-------|------|------------------------|-----------|
| F2.1 | `v6_diario_emergencial-10cf` — `mergePdfs`: anexos consecutivos mesmo escopo (grid 4-up) | feature | `DiarioOficialRepository.php` | F1.1, F1.4 |
| F2.2 | `v6_diario_emergencial-7aac` — Capa: compressão `min(altura,1366)` + transação | task | `app/Http/Controllers/Prefecture/DiarioController.php` | — |
| F2.3 | `v6_diario_emergencial-6207` — Geração: resetar `generation_progress` + logar `dispatchGeneration` | task | `DiarioController.php` | — (bloqueia F3.1) |
| F2.4 | `v6_diario_emergencial-ded5` — Certificados: validar/logar import A1 (CRT/pkcs12) | task | `app/Http/Controllers/Prefecture/DiarioCertificadosController.php` | — |
| F2.5 | `v6_diario_emergencial-10f8` — Portal: filtro em SQL + restaurar filtro por Edição | task | `app/Http/Controllers/Portal/DiarioController.php` + blade + js | — |

### Fase 3 — Paridade de UX (prioridade 2–3)

| # | Issue | Tipo | Arquivo-alvo principal | Depende de |
|---|-------|------|------------------------|-----------|
| F3.1 | `v6_diario_emergencial-76b0` — CMS create: Processar PDF + polling 1s + progresso granular + gating | feature | `resources/views/prefecture/contents/diario-oficial/create.blade.php` | F2.3 |
| F3.2 | `v6_diario_emergencial-bef6` — `generation-status.js` robusto + `data-status` no badge | task | `public/assets/js/custom/contents/diario-oficial/generation-status.js`, `datatable.js` | F3.1 |
| F3.3 | `v6_diario_emergencial-1ec5` — `areas.js`: feedback de erro, `removeLoad`, guard/revert Sortable, 4 colunas | bug | `public/assets/js/custom/contents/diario-oficial/areas.js` | — |
| F3.4 | `v6_diario_emergencial-83cc` — `select-image.js`: restaurar `navMenu()` e guards do `selectSize` | bug | `public/assets/js/custom/utilities/helpers/select-image.js` | — |
| F3.5 | `v6_diario_emergencial-76df` — `form.blade`: pré-preencher autor/e-mail/data | task | `resources/views/prefecture/contents/diario-oficial/form.blade.php` | — |
| F3.6 | `v6_diario_emergencial-3f67` — `show-pdf`: OpenGraph + bloco de assinatura | task | `resources/views/portal/layouts/diario-oficial/show-pdf.blade.php` | — |

### Fase 4 — Backlog (opcional / arquitetural)

| # | Issue | Tipo | Descrição |
|---|-------|------|-----------|
| F4.1 | `v6_diario_emergencial-4370` — Limpeza | task | Remover orphans de adendo (`#adendo_container`/`areas.js`) e views duplicadas mortas |
| F4.2 | `v6_diario_emergencial-d436` — Manutenibilidade | task | Enum de tipos, fallback de extensão no `FileTrait`, `fillable`/casts do `Diario`, nome de rota, seeds de menu |
| F4.3 | `v6_diario_emergencial-0365` — Spike | task | Avaliar adoção do motor de colunas v5 (`LayoutManager`/`ImageProcessor`) vs grid 4-up |

### Grafo de dependências

```
F1.1 (4862) ─┐
F1.4 (f52f) ─┴─▶ F2.1 (10cf) mergePdfs
F2.3 (6207) ───▶ F3.1 (76b0) ──▶ F3.2 (bef6)
```

---

## 4. Inventário completo das diferenças (v5 → v6)

Legenda: **(A)** melhoria v5 ausente/regredida na v6 → migrar · **(B)** feature v6 → preservar · **(C)** neutro/infra → sem ação.

### 4.1 Geração de PDF — confiabilidade/performance (A)

| Item | v5 | v6 atual | Issue |
|------|----|----------|-------|
| Remove timeout PHP | `@set_time_limit(0)` `DiarioOficialRepository.php:109` | ausente | F1.1 |
| Escrita atômica | `.tmp.<uniqid>` → `Storage::move` `:456-467` | `Storage::put` direto `:263` | F1.1 |
| Limpeza de temporários | remove `_merged_*` sucesso+erro `:469-492` | ausente | F1.1 / F2.1 |
| Guard do QR Code | try/catch + log `:202-206` | sem guard `:157` | F1.1 |
| Guard do progresso | try/catch + `Log` `:280-298` | só callback `:211-223` | F1.1 |
| Assinatura só publicado | `isPublished` + gate `HeaderRenderer:51` | assina sempre `FPdfService.php:190-193` | F1.2 |
| Fallback import→imagem | FPDI falha → `convertPdfToImages` `:395-425` | engole exceção `:349-356` | F1.4 |
| Observabilidade | `Log::info/warning/error` extensivo | ausente | F1.1 |
| Componente de preview | `where('deleted_at', null)` + try/catch `:30,33-38` | sem filtro/guard `:30-38` | F1.3 |

### 4.2 Importação de arquivos / anexos (A)

| Item | v5 | v6 atual | Issue |
|------|----|----------|-------|
| **Merge de anexos consecutivos** (mesmo `id_area`+`id_subarea`) | `mergePdfs()` `:144-179,551-582` | ausente (cada anexo inicia nova página do grid) | F2.1 |
| Render de imagens | base64 + `writeHTML` em colunas `:593-613` | grid 4-up com `Image()` `:300-317` | **decisão: manter v6** |
| `checkIfIsNeedBreakPage` ciente de colunas + altura dinâmica | `LayoutManager.php:130-177` | `y>240`/`x>91` `FPdfService.php:263-277` | F4.3 (spike) |

### 4.3 Capa (A)

| Item | v5 | v6 atual | Issue |
|------|----|----------|-------|
| Qualidade | `compress(..., null, null, min($h,1366), true)` `DiarioController.php:88-91` | `compress($path, null, 1024, 500, null, true)` → reduz altura a 500px `:215` | F2.2 |
| Atomicidade | transação no attach `:83-101` | sem transação `attachCoverImage:197-219` | F2.2 |
| Editor de imagem | cropper/rotação/texto `form.blade.php:96,207,267-269` | não wired (só upload/preview) | **fora do escopo** |

### 4.4 Controllers / API (A)

| Item | v5 | v6 atual | Issue |
|------|----|----------|-------|
| `show()` ciente de geração | stream se `completed`, JSON 202/500 senão `:127-167` | regenera síncrono `:118-121` | (avaliar; fora das fases) |
| Reset `generation_progress` | `:427,437` | não reseta `:512-565` | F2.3 |
| `dispatchGeneration` defensivo | try/catch+log `:528-543` | silencioso `:228-237` | F2.3 |
| `refresh_PDF` adiado | defere para edit `:384-388` | força regeneração síncrona `:501-511` | (avaliar) |
| Validação/log do certificado A1 | `DiarioCertificadosController.php:54-126` | ignora falhas `:53-93` | F2.4 |
| Filtro do portal | SQL + `edicao` `Portal/DiarioController.php:160-204` | em memória, sem `edicao` `:152-178` | F2.5 |

> **Correção de auditoria:** o filtro de datas atual **não** está quebrado — `diario_portal.js:6-24` envia `data_inicial`/`data_final`, que é exatamente o que o controller lê. O `name="data_ini"` no blade é vestigial.

### 4.5 Frontend JS (A)

| Item | v5 | v6 atual | Issue |
|------|----|----------|-------|
| `generation-status.js` | polling recursivo, para no terminal, `data-status`, `title` de erro, `response.ok` (125 linhas) | `setInterval` cego, sem `data-status` (38 linhas) | F3.2 |
| `areas.js` | feedback Swal, `removeLoad` no erro, linha de 4 colunas, guard/revert Sortable `:192-226,321,352-368` | feedback/guards removidos | F3.3 |
| `diario_portal.js` | reset `select_page=0`, guard de ellipsis/disabled, guard de array `:20-123` | ausentes → `...` quebra | F2.5 / F3 (js) |
| `select-image.js` | `navMenu()` + guards `selectSize` `:47-58,107` | removidos | F3.4 |

### 4.6 Views (A)

| Item | v5 | v6 atual | Issue |
|------|----|----------|-------|
| `create.blade` | botão Processar PDF + polling 1s + progresso `(anexo x/y, página a/b)` + gating `:28-38,137-194` | só overlay genérico | F3.1 |
| `form.blade` | prefill autor/e-mail/data `:30,40,79` | `null` | F3.5 |
| `show-pdf` | og: meta + bloco de assinatura `:21-23,136-153` | ausentes | F3.6 |
| Portal index | filtro `Edição` `:47-53` | removido | F2.5 |

### 4.7 Models / schema / misc (A/C)

| Item | v5 | v6 atual | Class | Issue |
|------|----|----------|:-----:|-------|
| `FileTrait::compress` fallback extensão | `?? 'jpg'` | `?? ''` | A | F4.2 |
| `Diario` `$dates`/`$fillable` | inclui `data_publicacao`, `generation_*` | sem cast/`generation_*` no fillable | A | F4.2 |
| Enum `DiarioHasAreasTypesEnum` | existe | literais `'text'`/`'file'` | A | F4.2 |
| Nome de rota `diario-status-toggle` | presente | removido (sem referência) | C | F4.2 |
| Migrações re-datadas (`central_line_customs`, `cover_image`, `generation_*`, `file_in/path`) | — | net schema igual | C | — |
| Adendo (`diario_adendos`, `adendo_id`, `Requests/DiarioAdendo`) | morto no próprio v5 | órfão `#adendo_container`/`areas.js` | C | F4.1 |
| Fork `summernote-diario.js` (236 KB + interval) | existia | removido (otimização intencional) | C | — |
| Views duplicadas mortas (`preview/*/style|summernote`, `components/image`) | — | duplicadas | C | F4.1 |
| FK capa `onDelete('cascade')` (v5) vs `set null` (v6) | cascade (perigoso) | `set null` (mais seguro) | C | **não migrar** |

### 4.8 Features v6 a preservar (B)

- Workflow/4 olhos: `publish()` (`DiarioController.php:307-336`), `toogle_status()` (`:345-367`), `can_publish` (`DatatableController.php:165-177`).
- Auditoria pública: `Portal@auditoria` (`:138-150`) + `getPublicAuditTrail` (`DiarioOficialRepository.php:398-518`).
- Capa: `attachCoverImage`/`delete_cover_image`, `cover-image.blade.php`/`cover-image.js`, `cover_image_id`.
- Geração: `generationStatus` + badge na listagem; `?download=1` via `streamPdf`.
- Anexos: grid 4-up (`printAnexoMateria`/`gridBoxesForColumns`).
- Rich text: `richtext/summernote.js` (`clearFormatting`, `audioUpload`).
- Logs de auditoria em `store`/`update`/`sort`/matéria.

---

## 5. Especificação detalhada das mudanças (Fases 1–3)

### Fase 1

1. **`DiarioOficialRepository.php`** — `use Log;`; `@set_time_limit(0)`; QR em try/catch + `Log::warning`; callback de progresso em try/catch; escrita atômica (`$caminho.'.tmp.'.uniqid().'.pdf'` → delete destino → `Storage::move`); `$pdf->isPublished = (bool) data_get($data,'status',false)`.
2. **`FPdfService.php`** — `public $isPublished = false;`; `Header()` só chama `setSignature(...)` se `$this->isPublished`.
3. **`DiarioOficialComponent.php`** — `DB::table('diario_certificados')->where('deleted_at', null)->first()`; try/catch em `certReturn` com fallback `''`.
4. **`printAnexoMateria`** — no `catch`, cair para `convertPdfToImages` + posicionar no grid, em vez de descartar o anexo.

### Fase 2

5. **`mergePdfs(array $items, int $diarioId): string`** — FPDI concat → `_merged_<uniqid>.pdf` (log+skip em corrompido). Pré-processar após `groupBy('id_area')`: runs consecutivos de `type='file'` + `path` + mesmo `id_subarea`; setar `_merged_path` no primeiro; rejeitar excedentes; `values()`. `printAnexoMateria` resolve `data_get($materia,'_merged_path') ?: public_path($materia->getOriginal('path'))`. Limpeza `_merged_*` no sucesso e no catch.
6. **`attachCoverImage`** — `list($h)=getimagesize($file->getPathname()); compress($stored,null,null,min($h,1366),true)` (pular `webp/avif/gif/svg`); attach em transação.
7. **`refresh_PDF_preview`** — `generation_progress = null` ao iniciar; log no catch. **`dispatchGeneration`** — try/catch + `Log::warning`.
8. **`DiarioCertificadosController@store`** — validar CRT existe/não vazio e retorno de `openssl_pkcs12_read`; logar.
9. **Portal** — `filtroGET` em query SQL com `data_inicial`/`data_final`/`termo`/`edicao`; blade re-adiciona `#Search_edicao`; `diario_portal.js` envia `edicao` e guarda ellipsis/array.

### Fase 3

10. **`create.blade.php`** — botão "Processar PDF" (`?force=1`), polling 1s em `/cms/ajax/diario-generation-status/{id}`, mensagem de progresso, `#preview` desabilitado até `completed`; manter botão de workflow e `#pdfLoading`; guard `@isset($data)`/sem id.
11. **`generation-status.js` + `datatable.js`** — polling recursivo que para no terminal; `data-status` + `title` de erro; `response.ok`; `encodeURIComponent`.
12. **`areas.js`** — fechar/resetar modal fora do loop; restaurar linha de 4 colunas; feedback de sucesso/erro; `try/catch`+`response.ok`+`removeLoad`; guard no `Sortable.create` + revert em falha; manter payload `type`/`file_in`.
13. **`select-image.js`** — restaurar `navMenu()` e guards de `selectSize`.
14. **`form.blade.php`** — prefill `autor`, `email`, `data_publicacao`.
15. **`show-pdf.blade.php`** — og: meta + bloco de assinatura (responsável/e-mail/lei); manter link de auditoria e o layout.

---

## 6. Riscos

1. `mergePdfs` pode falhar em PDFs com CrossReference (FPDI gratuito) → logar e pular o merge, mantendo caminhos originais.
2. Restringir a assinatura a publicados muda o comportamento atual do preview (hoje sempre assinado) — validar com o negócio.
3. JS alterado exige bump de `?v=`; com `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1` no `.env`, recriar o container.
4. Merge pode regredir o grid 4-up — cobrir com testes de cenários de anexos.
5. Filtro do portal em SQL altera o contrato de `/diario-oficial/filtro` consumido por `diario_portal.js`.
6. Não regredir features v6 (workflow/revisor, auditoria, `cover_image_id`, polling, download).

---

## 7. Critérios de aceite

- [ ] Itens das Fases 1–3 implementados e verificados; Fase 4 registrada como backlog.
- [ ] 3 PDFs consecutivos do mesmo escopo **sem** quebra de página/coluna artificial entre eles.
- [ ] PDF com número ímpar de páginas em 2 colunas **não** perde a última página.
- [ ] Escrita atômica (sem PDF corrompido parcial), sem estouro de `max_execution_time`, com logging de erros.
- [ ] Assinatura digital aplicada **apenas** em edições publicadas.
- [ ] Falha de import de páginas cai para conversão em imagens (anexo preservado).
- [ ] Capa preserva qualidade (altura ≤ 1366px) e anexo é transacional.
- [ ] CMS: botão Processar PDF, polling 1s e progresso `(anexo x/y, página a/b)`; preview só ao concluir.
- [ ] Portal: filtro por Edição e por data funcionando com filtragem no banco.
- [ ] Features v6 preservadas: workflow/revisor, auditoria pública, `cover_image_id`, polling e download.
- [ ] `php -l` em todos os arquivos PHP alterados + testes/lint do projeto executados.

---

## 8. Verificação sugerida

1. Gerar PDF com (a) 3 anexos consecutivos mesmo escopo; (b) PDF ímpar ×2 em 2 colunas; (c) sem certificado/config; (d) edição extensa (N anexos) → sem timeout/corrupção e com anexos presentes.
2. Conferir assinatura: pública só quando `status` publica; rascunho sem assinatura.
3. Portal: filtro por Edição + intervalo de datas; busca por termo; paginação (incl. ellipsis).
4. CMS: fluxo rascunho → Processar PDF (progresso) → revisor → publicar (4 olhos) → auditoria.
5. `php -l` + `vendor/bin/phpunit` (se aplicável).

---

## 9. Referências de código (arquivos-chave)

- `app/Repositories/Eloquent/Prefecture/DiarioOficial/DiarioOficialRepository.php` (v6) ↔ `diario-oficial-reimplementacao/.../DiarioOficialRepository.php` (v5)
- `app/Services/FPdfService.php` (v6) ↔ `diario-oficial-reimplementacao/app/Services/FPdfService.php` + `app/Services/Pdf/{LayoutManager,HeaderRenderer,ImageProcessor,CoverHandler,PdfConverter}.php` (v5)
- `app/Http/Controllers/Prefecture/DiarioController.php` · `DiarioCertificadosController.php` (v6/v5)
- `app/Http/Controllers/Portal/DiarioController.php` (v6/v5)
- `app/Http/Components/Prefecture/Contents/Preview/DiarioOficialComponent.php` (v6/v5)
- JS: `public/assets/js/custom/contents/diario-oficial/{areas,generation-status,datatable,diario_portal}.js`, `public/assets/js/custom/portal/diario-oficial/viewer.js`, `public/assets/js/custom/utilities/helpers/{select-image,preview-diario}.js`
- Views: `resources/views/prefecture/contents/diario-oficial/{create,form,index}.blade.php`, `.../components/pdf-file-upload.blade.php`, `.../modal/areas/*`, `resources/views/portal/layouts/diario-oficial/{index,show-pdf}.blade.php`

---

## 10. Status de implementação (2026-09-11)

**Fases 1–3 implementadas e verificadas.** Issues correspondentes fechadas no tracker
(`v6_diario_emergencial-4862`, `-2177`, `-f26e`, `-f52f`, `-10cf`, `-7aac`, `-6207`,
`-ded5`, `-10f8`, `-76b0`, `-bef6`, `-1ec5`, `-83cc`, `-76df`, `-3f67`).

Evidências (a cada fase):

- **E2E Playwright:** 12/12 verdes (`tests-e2e`).
- **Gate visual sniffCSS:** 5 checkpoints, delta 0 (execuções repetidas estáveis).
- **`php -l`** em todos os arquivos PHP alterados.

Verificações específicas:

| Item | Evidência medida |
|------|------------------|
| F1.1 escrita atômica/log | sem `*.tmp.*`/`_merged_*` residuais; logs `[generatePdfFile]` |
| F1.1/F1.4 anexos | PDF do rascunho passou de 59 KB/1 pág → **2,6 MB/7 págs** (fallback FPDI) |
| F1.2 assinatura | rascunho `sig=no`; publicado `sig=yes` (estado restaurado) |
| F2.1 mergePdfs | 3 anexos mesmo escopo → **4 págs** vs 7 págs sem merge |
| F2.2 capa | 800×3000 → **364×1366** (altura ≤ 1366), transacional |
| F2.3 progresso | `generation_progress` termina **NULL** e status `completed` |
| F2.5 filtro portal | SQL com `edicao`/datas/termo; `edicao=999` → `[]` |
| F3.1 Processar PDF | botão → status "PDF gerado com sucesso.", preview habilitado |
| F3.2/F3.5/F3.6 | badge com `data-status`; prefill autor/email/data; `og:*` + bloco de assinatura |

> **Fase 4** (`4370`, `d436`, `0365`) permanece como backlog, conforme decisão de escopo.

Correções de robustez descobertas durante a verificação:

1. `PdfConverter` passou a retornar `success=false` quando **nenhuma** página converte
   (antes retornava `true` e descartava o anexo em silêncio).
2. `printAnexoMateria` agora tem fallback **simétrico** (import FPDI ⇄ imagens),
   preservando o anexo mesmo com a policy do ImageMagick bloqueando PDF.
3. Limpeza de `generation_progress` via query update (o Eloquent não marcava o campo
   como *dirty* após o repositório gravá-lo em outra instância).
4. JS de preview usava `window.location.hostname` (sem porta) → URLs quebradas em
   porta não-padrão; ajustado para `window.location.origin`.
5. DataTable do CMS com idioma via CDN (corrida de carregamento) → servido localmente.
6. Portas do `docker-compose` fixadas (`APP_HTTP_PORT`/`APP_HTTPS_PORT`, default
   32778/32779) para o harness E2E ser determinístico.

### 10.1 Atualização do preview ao adicionar conteúdo (2026-09-11)

Bug relatado: após adicionar conteúdo e clicar em “Gerar novamente”, o preview
aparentava não incluir o novo conteúdo. Investigação:

- A geração **está correta**: o PDF final contém todas as matérias/anexos
  (confirmado por extração de texto e renderização via `gs`).
- Causas reais corrigidas:
  1. **Páginas em branco**: `printImportedPagesGrid` terminava com `AddPage()`,
     gerando página vazia sempre que o conteúdo seguinte também abria página.
     Substituído por um sentinela de cursor (`SetY(280)`), deixando a quebra de
     página para o próximo conteúdo. `diário 19`: **12 págs (3 brancas) → 10 págs
     (0 brancas)**.
  2. **Preview podia servir PDF antigo**: `streamPdf` não enviava cabeçalhos de
     cache; agora envia `Cache-Control: no-store, no-cache, must-revalidate` e
     `Pragma: no-cache`.
  3. **Corrida no polling**: o botão “Gerar novamente” aceitava um
     `generation_status=completed` da execução anterior (antes do request forçado
     ser processado), encerrando o polling e recarregando o PDF antigo. Agora o
     front compara `generation_started_at` e só aceita uma conclusão nova
     (com timeout de segurança de 120s).
  4. **Preview de edição pendente**: `show()` agora força a regeração quando o
     `generation_status` não é `completed`/`processing`, em vez de servir o arquivo
     desatualizado.

### 10.2 Anexos inline — paridade de layout com a v5 (2026-09-11)

Bug relatado: após adicionar conteúdo, o anexo carregava, mas a página do
cabeçalho da área ficava em branco — o v6 sempre forçava uma página nova (grade
2×2), diferentemente do v5, que introduz o anexo logo abaixo do cabeçalho.

Alinhamento com a referência `diario-oficial-reimplementacao/`:

- `file_in = true` → **fluxo inline**: cada página do anexo é importada (via FPDI)
  dimensionada para a largura da coluna e flui verticalmente abaixo do
  cabeçalho, trocando de coluna e depois de página quando o espaço acaba
  (`printImportedPagesInline` + `inlineColumnLayout`). Antes: grade 2×2 com
  `AddPage()` imediato, orfanando o cabeçalho.
- `file_in = false` → **páginas inteiras**: uma página de origem por página de
  saída (`printImportedPagesFullPage`), sem cabeçalho/rodapé, como no v5.
- Fallbacks mantidos: grade e imagens (`printImagesFlow`).

Efeito no diário 19 (3 anexos, `file_in=true`): página 1 (cabeçalho "ATOS DO CHEFE
DO PODER EXECUTIVO") e página 6 ("ATOS DO PODER LEGISLATIVO") agora iniciam o
conteúdo do anexo logo abaixo do cabeçalho; **14 páginas** com todos os anexos
presentes. O caminho `file_in=false` foi validado alternando a matéria 22
(24 páginas, páginas inteiras) e revertido.

Como o import vetorial via FPDI passou a ser o caminho primário, a conversão por
Imagick (bloqueada pela policy neste ambiente) deixou de ser acionada no fluxo
normal — sem mais `Erro na página ... security policy`.

> ⚠️ **Superado por §10.4**: o merge por subárea e o FPDI como caminho primário
> foram substituídos (a subárea não pode causar quebra e o parser livre do FPDI
> não lê PDFs modernos).

### 10.3 Matriz de validação de layout (2026-09-11)

Validação **de fato**, dirigindo a UI real com Playwright (criação do diário,
inserção de matérias pelo modal, botão “Processar PDF”) em 14 cenários.

Harness: `tests-e2e/scenarios.cjs` (+ `scenarios-refetch.cjs`) e fixtures em
`tests-e2e/fixtures/scenario/` (PDFs de 1/2/3/5/8/18 págs, 3 págs landscape e
`inline.png`). Análise com `pdfinfo`/`pdftotext` e cobertura de pixels via
Ghostscript (detecção de página em branco excluindo cabeçalho/rodapé).

Cenários e resultado (nenhuma página em branco em todos):

| Cenário | Páginas | Verificação |
|---|---|---|
| 01 texto puro, várias páginas | 8 | sem brancos; 1 seção |
| 02 texto + imagens inline | 6 | **imagem renderiza** |
| 03 texto + anexo no início | 5 | anexo inline já na pág. 1 |
| 04 texto + anexo no meio | 7 | anexo full-column após o texto |
| 05 texto + anexo no fim | 6 | anexo em tamanho cheio (antes encolhia) |
| 06 anexo sozinho (5 págs) | 5 | 1 página de saída por página |
| 07 múltiplos anexos (1/2/3/5/8/18, subáreas distintas) | 37 | 37 = 1+2+3+5+8+18 |
| 08 múltiplos anexos mesma subárea (merge) | 11 | merge confirmado (4 anexos → 1 PDF) |
| 09 mesma área + mesma subárea | 4 | 1 seção |
| 10 mesma área + subárea diferente | 3 | 1 seção (subárea não gera quebra) |
| 11 área diferente + mesma subárea | 4 | **área nova em página nova** |
| 12 área diferente + subárea diferente | 4 | **área nova em página nova** |
| 13 duas colunas + anexo | 3 | anexo na coluna |
| 14 anexo `file_in=false` | 4 | páginas inteiras |

Defeitos encontrados e corrigidos nesta validação:

1. **Anexo encolhido a ~20mm**: quando sobrava pouco espaço, a página do anexo era
   escalada para caber num restinho (ilegível). Agora há um mínimo de área
   (`minPlaceHeight` ≈ 45% da coluna) e o tamanho é recalculado após avançar
   coluna/página. (`printImportedPagesInline` / `printImagesFlow`)
2. **Área diferente não iniciava em nova página**: agora cada área força quebra de
   seção (só adiciona página se a atual já tiver conteúdo, para não criar página
   em branco). (`generatePdfFile`)
3. **Imagem inline do texto não renderizava**: o v6 havia perdido o
   `processImageContent` do v5; a URL do storage não é acessível pelo `writeHTML`
   do TCPDF. Portado para `FPdfService`: converte `<img src>` local em data URI
   base64 e limita a 190mm × 200mm.

### 10.4 Correção do escopo por área e de anexos que o FPDI não lê (2026-09-11)

Bug relatado (diário 39): ao colocar **2+ PDFs na mesma área** (subáreas iguais ou
diferentes) o sistema forçava quebra de página; e o **3º PDF da mesma área não era
computado**.

Causas-raiz:

1. O merge de anexos usava o escopo `id_area + id_subarea`. Subáreas diferentes
   geravam chamadas separadas e, como cada chamada terminava com o cursor no fim
   da página, a seguinte forçava nova página (e deixava coluna/página em branco).
2. O `mergePdfs` (FPDI) **descartava silenciosamente** PDFs modernos que o parser
   livre do FPDI não lê (object streams / compressão PDF 1.5+): logava warning,
   não importava — mas ainda assim removia o item da renderização. Era exatamente
   o 3º anexo do diário 39.

Correções:

- **Escopo = área.** A subárea não tem representação visual e não pode causar
  quebra de página nem de coluna. O conteúdo de uma área flui continuamente; a
  troca de área é a única quebra de seção. O `mergePdfs` por subárea foi removido.
- **`file_in = true` rasterizado.** Cada anexo é convertido em JPEG via
  `PdfConverter` (Imagick + **Ghostscript**) e posicionado por
  `writeImagesInline` em um **fluxo de colunas contínuo com estado persistente**
  (coluna, `y` e topo da página), compartilhado entre anexos consecutivos da mesma
  área. Não depende do parser do FPDI (funciona com qualquer PDF), sem quebra nem
  encolhimento por anexo. Correção extra: ao trocar de coluna na 1ª página, usa o
  topo do conteúdo (pós-cabeçalho) e não o topo fixo da página.
- **`file_in = false`** segue vetorial (FPDI full-page), com fallback raster
  (`printImagesFullPage`).
- **QR Code** usa `config('app.url')` (APP_URL) em vez de
  `$_SERVER['SERVER_NAME']` — quebrava em CLI e ignorava esquema/porta.
- **Infra:** `docker/apache/Dockerfile` instala `ghostscript` e libera o coder PDF
  no policy do ImageMagick (`rights="read|write"`). **Requer rebuild da imagem**
  (`docker compose build app`), pois sem Ghostscript o container não rasteriza PDF.
- **Select de complemento** com `orderBy('id','desc')` no controller (era não
  determinístico: a ordem física da tabela mudava com updates e fazia o gate
  visual oscilar).

Revalidação: matriz de 14 cenários sem páginas em branco e com quebra só entre
áreas; diário 39 (área 10 com 3 anexos em subáreas distintas + área 12) com todos
os anexos presentes; E2E 12/12; gate visual OK.

### 10.5 Tamanho dos anexos (ImageProcessor) — 2 por coluna (2026-09-11)

Sintoma relatado: os anexos ficavam **sempre 1 por coluna**. Cada página A4 era
escalada para a largura cheia da coluna (`colWidth` ≈ 93mm → ~131mm de altura),
então só uma caberia por coluna.

Implementação clássica trazida: `app/Services/Pdf/ImageProcessor.php`
(`calculateOptimalDimensions` + `buildOptimizedImageTag`), que define o tamanho
máximo do elemento de forma proporcional e independente da resolução.

Detalhe importante: o **TCPDF 6.3 não implementa `max-width`/`max-height`** (o
parser HTML não reconhece essas propriedades — 0 ocorrências no `tcpdf.php`).
Por isso as dimensões calculadas são emitidas como `width`/`height` em **mm**,
aplicando o percentual à largura real da coluna (`buildTagForColumn`), o que
produz o mesmo efeito pretendido da implementação clássica.

Resultado no layout de **2 colunas**: página A4 (700×989px) → **~87 × 123mm**
(praticamente toda a largura útil da coluna de 89mm) → **2 anexos por coluna,
4 anexos por página**. No layout de 1 coluna: 141×200mm → 1 por página
(inalterado). Para garantir 2 por coluna, a altura é limitada a 123mm em layouts
multi-coluna (`maxHeight = columnsNumber > 1 ? 123 : 200`).

- `FPdfService` delega `processImageContent()` ao `ImageProcessor` (também para
  imagens do texto corrido) e expõe `buildAnexoImageTag($path, $columnWidthMM,
  $maxHeightMM)`, que usa `buildTagForColumn` (largura = coluna, proporcional).
- `writeImagesInline()` usa `writeHTML` + `selectColumn()` (o TCPDF decide a
  troca de coluna/página), mantendo o mesmo fluxo entre anexos da mesma área.

**Atenção (dev):** o PHP-FPM mantém o policy do ImageMagick em cache. Depois de
instalar o Ghostscript / editar o policy é preciso **reiniciar o container**
(`docker stop` + `docker start`) — caso contrário o web cai no fallback FPDI
(1 anexo por coluna, vetorial) e o CLI funciona enquanto o web não.

Revalidação: diário 39 com **4 anexos por página** (2 por coluna) nas páginas
de continuação; matriz de 14 cenários sem páginas em branco; E2E 12/12; gate
visual OK.




