# 08 — Gaps, Riscos e Dívidas do Módulo Diário Oficial

> Consolida as anomalias catalogadas no legado
> (`90-LEGADO/22-ANOMALIAS_E_RISCOS.md`,
> `90-LEGADO/CadeiaDeErros_ColunasPDF.md`,
> `90-LEGADO/2026-09-11-diario-upgrade-backlog.md` e
> `90-LEGADO/23-GUIA_EXECUCAO.md`), reconferidas contra o código do `v6/`.

Severidade: **alta** · **média** · **baixa**.

---

## 1. Bugs e erros conhecidos

### 1.1 Geração `pending` sem consumidor — **alta**

`DiarioController@dispatchGeneration()` grava `generation_status = 'pending'`, e
`Portal\DiarioController@showPDF` faz o mesmo quando o PDF não existe em disco.
**Não existe job, comando nem agendamento que consuma esse estado**: não há
`v6/app/Jobs`, e em `v6/app/Console/Commands` há apenas `CompressImages.php`,
`FixGaleriaFiles.php`, `GenerateSwagger.php`, `MakeRepository.php`,
`ReloadEvacina.php` e `UpdateVersion.php`.

Consequência: um diário visualizado no portal sem PDF gerado fica exibindo
"Gerando PDF" indefinidamente, até que alguém abra a edição no CMS e clique em
"Processar PDF". O `generationStatus()` só marca `failed` depois de 10 minutos
em `processing` — mas o estado `pending` **nunca** expira.

### 1.2 Senha do certificado passada a `shell_exec` sem escape — **alta**

`v6/app/Http/Controllers/Prefecture/DiarioCertificadosController.php:67`:

```php
shell_exec("openssl pkcs12 -in $nomeCertPFX -out $nomeCertCRT -nodes -passin pass:$password");
```

- A senha vem do request e **não** é escapada: superfície de command injection.
- O `Log::info("Executando comando: $command")` que expunha a senha em texto
  puro **foi removido no v6** (ainda existe no
  `v5/app/Http/Controllers/Prefecture/DiarioCertificadosController.php:78`), mas
  o `shell_exec` inseguro permaneceu.

### 1.3 QR Code gravado em arquivo de nome fixo — **média**

`DiarioOficialRepository::generatePdfFile()` grava sempre em
`public/img/qrcode.png`. Gerações simultâneas de diários diferentes
sobrescrevem o mesmo arquivo, podendo inserir no PDF o QR de outro diário.

### 1.4 Permissão de subárea resolvida como área — **média**

`DatatableController::get_sub_areas_diario()` chama
`struct_views($data, 'diario-area')`. Editar, excluir, ativar e inativar uma
subárea valida `diario-area.*` em vez de `diario-subarea.*`.

### 1.5 `mergePdfs` implementado e nunca chamado — **média**

O método `mergePdfs(array $items, $diarioId)` existe no
repositório do v6 (definição na linha 377), e `printAnexoMateria` consome
`data_get($materia, '_merged_path')` (linha 908). Mas **nenhum ponto do código
chama `mergePdfs`**, e portanto `_merged_path` nunca é preenchido. O merge de
anexos consecutivos do mesmo escopo **não acontece**; `cleanupMergedPdfs()`
sobra como rede de segurança para arquivos `_merged_*` que, na prática, nunca
são criados.

### 1.6 `createButtonDelete` com assinatura incompatível — **média**

`v6/public/assets/js/custom/contents/diario-oficial/datatable.js` chama
`createButtonDelete` com 8 argumentos, mas
`v6/public/assets/js/custom/utilities/table.js` define a função com 4
parâmetros. Os argumentos extras são ignorados (a exclusão funciona, mas os
parâmetros de URL e texto são Lost).

### 1.7 `viewer.js` chama método inexistente — **média**

`v6/public/assets/js/custom/portal/diario-oficial/viewer.js` liga a tecla Enter
do `#search-input` a `searchInPdf()`, função **não definida** no arquivo. O
objeto `apiUrls` montado em `show-pdf.blade.php` também não é usado: as URLs
estão hardcoded no JS.

### 1.8 `config.js` referencia variável errada — **baixa**

No tratamento de erro do XHR de remoção da imagem de cabeçalho, o código usa
`if(error)` quando a variável do `catch` se chama `errorMsg`.

### 1.9 HTML malformado e texto literal no CMS — **baixa**

- `v6/resources/views/portal/layouts/diario-oficial/show-pdf.blade.php` abre a
  tag `<body` sem o `>` de fechamento.
- `v6/resources/views/prefecture/contents/diario-oficial/form.blade.php` contém
  um `--}}` sem `{{--` de abertura, que é renderizado como texto literal.

### 1.10 Botão de impressão inexistente — **baixa**

`create.blade.php` e o componente de preview tentam ligar o botão `#printPdf`,
mas o markup do preview não cria esse elemento: o binding é um no-op.

### 1.11 CSS inválido nos templates legados — **baixa**

`v6/resources/views/prefecture/contents/diario-oficial/pdf/style.blade.php` tem
seletores quebrados (`.pb-10, {`, `.pb-14, {`) e
`.text-left { text-align: center; }`. A cópia órfã em
`v6/resources/views/prefecture/contents/components/preview/diario-oficial/style.blade.php`
usa a unidade inválida `.title-image { width: 180x; }`.

### 1.12 Registro de view do portal prepara dados não usados — **baixa**

`Portal\DiarioController@index` envia `diarios` paginado, mas a view é
populada por JavaScript (`diario_portal.js`); a variável não é utilizada.

### 1.13 `adendo_id` gravado em coluna inexistente — **baixa**

`DiarioAreaController` tenta gravar `adendo_id` em `diario_has_areas`. A coluna
não existe no schema do v6 (foi criada e removida no v5). Como não está no
`$fillable` do model, o valor é **ignorado silenciosamente** — sem erro, mas
também sem efeito.

## 2. Funcionalidades incompletas ou mortas

| Item | Situação |
|------|----------|
| `generation_status = 'pending'` | Estado sem consumidor (ver 1.1) |
| `mergePdfs()` | Implementado, nunca chamado (ver 1.5) |
| `v6/public/assets/js/custom/contents/diario-oficial/index.js` | Depende de `.toogle-status`, elemento que a listagem atual (DataTable com `createActiveButton`) não gera. O toggle real é feito pelo handler genérico de `table.js`, que envia `contents_id` |
| `v6/public/assets/js/custom/utilities/helpers/preview-diario.js` | Helper **órfão**: foi removido do `create.blade.php` |
| `v6/resources/views/prefecture/contents/diario-oficial/pdf/index.blade.php` e `pdf/style.blade.php` | Template HTML/PDF legado; o PDF real vem do `FPdfService` |
| `v6/resources/views/prefecture/contents/diario-oficial/pdf/summernote.blade.php` | Não é o Summernote: bundle de CSS (normalize + Bootstrap 3 + Glyphicons) |
| `v6/resources/views/prefecture/contents/components/preview/diario-oficial/summernote.blade.php` e `style.blade.php` | Cópias órfãs do item anterior |
| `v6/app/Http/Components/Prefecture/Contents/Preview/DiarioOficialComponent.php` | Recebe `$certificado`, `$icon` e `$cidade` no `render()`, mas a view de preview não os usa |
| `generation-status.js` | Declara a constante `RETRY_INTERVAL_MS` sem uso |
| `adendo_id` no controller de áreas | Código sem efeito (ver 1.13) |
| Views `areas_do_diario_two` e `sub_areas_do_diario_two` | Exclusivas do v5, SQL idêntico ao original, sem ganho |
| Tabela `diario_adendos` | Existente só no v5, sem model e sem código que a leia |
| `v5/app/Enum/Prefecture/DiarioOficial/DiarioHasAreasTypesEnum.php` | Enum do v5; no v6 os literais `'text'`/`'file'` estão espalhados pelo código |
| `diario_oficial_pdf` (view SQL) | Criada e mantida, mas o `FPdfService` do v6 monta o cabeçalho a partir de `Configuration` e models, não da view |

## 3. Riscos de segurança

| # | Risco | Severidade | Situação no v6 |
|:-:|-------|:-----------:|----------------|
| S1 | Senha do certificado em `shell_exec` sem escape | alta | **Aberto** (o log da senha foi removido, o `shell_exec` inseguro ficou) |
| S2 | Chave privada do certificado (`diario_certificados.pkey`) gravada em texto claro e com arquivo sob `storage/app/public/` | alta | **Aberto** |
| S3 | QR Code em arquivo global sobrescrito por geração concorrente | média | **Aberto** |
| S4 | QR Code apontando para `config('app.url')` sem validação de host | baixa | **Aberto** |
| S5 | Endpoint `refresh_PDF_preview` acessível fora do grupo `/cms` (registrado em `v6/routes/web.php`), com a geração do PDF ocorrendo no request | média | **Aberto** |
| S6 | `assets/js/custom/RichText/summernote.js` referenciado por formulários do módulo, mas **inexistente** (o correto é `v6/public/assets/js/custom/utilities/richtext/summernote.js`) | baixa | **Aberto** |

## 4. Dívidas e riscos de dados

| # | Dívida | Impacto na v7 |
|:-:|-------|---------------|
| D1 | **Duplicação na tabela `modulos`** (ver seção 5) | Dois registros para o mesmo módulo; `resources.modulo_id = 37` e `menu_orders.modulo_id = 16` divergem |
| D2 | Rotas duplicadas em `v6/routes/basepro.php` (`diario/configuracoes`, `diario/configuracoes/update`, `diario-area/{id}`) | Manutenção confusa |
| D3 | Nome de arquivo com typo: `v6/app/Http/Model/Prefecture/DiarioCerticado.php` (classe `DiarioCertificado`) | Confusão e dependência do classmap do Composer |
| D4 | Um único controller (`DiarioAreaController`) para áreas e matérias, decidindo pelo request | Acoplamento; a v7 deve separar `AreaController` e `MateriaController` |
| D5 | `diario_has_areas` é uma pivot **com atributos** (`titulo`, `conteudo`, `type`, `file_in`, `path`, `position`) | Modelagem a normalizar para `diario_materias` |
| D6 | Coluna `adendo_id` gravada em coluna inexistente | Migrar o dado (perdido) ou descartar |
| D7 | `generation_progress` é `text` com JSON | Migrar para `jsonb` |
| D8 | `diario_configs` é um registro com `id = 1` fixo | Migrar para configuração por chave |
| D9 | Nome do arquivo do PDF contém acento (`diário-{edicao}.pdf`) | Problema em filesystem, URL e encoding |
| D10 | `diario_certificados.path` aponta para dentro de `storage/app/public/` | Migrar para `storage/app/private/` |
| D11 | Views SQL como contrato do JavaScript (`value`, `status_name`, `download_link`) | A v7 deve congelar o contrato ou migrar o front para JSON |
| D12 | `@set_time_limit(0)` e geração síncrona dentro do request | Timeout de worker; migrar para fila |
| D13 | `diario_oficial.status` (bool) e `status_workflow` (smallint) coexistem | Duas fontes de verdade de estado |
| D14 | `RolesTableSeeder` não concede `diario-oficial.*` | Depende do seeder de workflow para as permissões do diário |

## 5. A duplicação `diario-ofifical` / `diario-oficial`

### 5.1 Fato verificado

A migration `v6/database/migrations/2024_11_01_150206_create_modulos_table.php`
cria a tabela `modulos` (`id`, `name`, `slug`, `status`, `timestamps`) e insere
**58 registros**. Duas entradas se referem ao Diário Oficial:

| id | name | slug | status | Origem no arquivo |
|---:|------|------|:------:|--------------------|
| **16** | `Diario Oficial` | **`diario-ofifical`** | 0 | linha 121 (`// ID 16`) |
| **37** | `Diário Oficial` | **`diario-oficial`** | 0 | linha 247 (`// ID 37`) |

Confirmado no banco em execução:

```
 id |             name             |      slug
----+------------------------------+------------------
 16 | Diario Oficial               | diario-ofifical
 37 | Diário Oficial               | diario-oficial
```

O slug `diario-ofifical` **não aparece em nenhum outro arquivo do v6** — é um
typo isolado na migration. Todo o código real usa `diario-oficial`:

| Uso | Arquivo |
|-----|---------|
| Item de menu do CMS | `v6/app/Providers/MenuServiceProvider.php:215` (`case 'diario-oficial'`) |
| Recurso de autorização | `v6/database/seeds/ResourcesTableSeeder.php` (`Resource::updateOrCreate(['slug' => 'diario-oficial'], [ 'modulo_id' => 37 ])`) |
| Widget do dashboard | `v6/app/Services/Dashboard/WidgetRegistry.php` e `v6/app/Services/Dashboard/Widgets/DiarioWidget.php` |
| Categoria de conteúdo | `v6/database/seeds/CategoryContentTableSeeder.php` |
| Formulário de módulos do admin | `v6/resources/views/admin/modulos/form.blade.php:111` |

### 5.2 Consequências

1. **A tela de módulos do admin mostra duas entradas** para o diário:
   `v6/app/Http/Controllers/Admin/ModulosController.php:19` lista
   `Modulos::where('slug', '!=', 'dashboard')->orderBy('id','ASC')->get()` — sem
   filtro, portanto id 16 ("Diario Oficial") aparece ao lado de id 37
   ("Diário Oficial").
2. **Divergência de `modulo_id` entre `resources` e `menu_orders`:**

| Tabela | Slug | `modulo_id` | Módulo referenciado |
|--------|------|:-----------:|---------------------|
| `resources` | `diario-oficial` | 37 | "Diário Oficial" (correto) |
| `resources` | `diario-area` | 37 | "Diário Oficial" (correto) |
| `resources` | `diario-subarea` | 37 | "Diário Oficial" (correto) |
| `resources` | `certificado` | 37 | "Diário Oficial" (correto) |
| `menu_orders` | `diario-oficial` | **16** | **"Diario Oficial" (typo)** |
| `menu_orders` | `certificado` | **16** | **"Diario Oficial" (typo)** |

Ou seja: os **recursos** (autorização) apontam para o módulo correto (37), mas
os **itens de menu** apontam para o módulo com o slug digitado errado (16).
Em um painel que agrupe por módulo, o diário aparece em dois grupos.

### 5.3 Correção proposta para a v7

1. Manter **uma** entrada em `modulos`, com `slug = 'diario-oficial'` e
   `name = 'Diário Oficial'`.
2. Remover o registro id 16 (ou renomeá-lo para algo inequívoco caso ele
   esteja em uso por algum item de menu já implantado).
3. Alinhar `menu_orders.modulo_id` e `resources.modulo_id` para o mesmo id.
4. Adicionar uma **constraint de unicidade** em `modulos.slug` para impedir a
   recriação do problema.

## 6. Itens que existem na v5 e foram removidos na v6

| Item | Situação no v5 | Situação no v6 |
|------|---------------|---------------|
| Tabela `diario_adendos` | Migration `2025_09_20_010852_create_diario_adendos_table.php`, sem model e sem leitura/escrita | **Removida** (a migration não existe no v6) |
| Coluna `diario_has_areas.adendo_id` | Criada em `2025_09_22_022119` e removida em `2025_09_22_203749` | **Nunca criada** no v6 |
| Request `DiarioAdendo\SaveRequest` | `v5/app/Http/Requests/DiarioAdendo/SaveRequest.php`, sem controller/rota | **Removido** |
| Views `areas_do_diario_two` e `sub_areas_do_diario_two` | Migrations de `2025-10-23` | **Removidas** |
| `v5/app/Services/Pdf/LayoutManager.php`, `HeaderRenderer.php`, `CoverHandler.php` | Motor de colunas, cabeçalho e capa separados | **Removidos**; consolidados no `FPdfService` |
| `v5/public/assets/plugins/summernote/summernote-diario.js` | Fork do Summernote (236 KB) com customizações de imagem e plugin `clearFormatting` | **Removido** (otimização intencional) |
| `v5/app/Enum/Prefecture/DiarioOficial/DiarioHasAreasTypesEnum.php` | Enum de tipos de matéria | **Removido** |
| Migration `v5/database/migrations/2023_02_14_094019_add_resource_to_diario_areas_and_subareas.php` | Inseria `resources`/`menu_orders` de `diario-area` e `diario-subarea` | Substituída por seeds no v6 |
| Coluna `diario_has_areas.path` | Adicionada em `2025_09_22_203749` | Adicionada direto em `2026_09_04_150000` |

## 7. Itens que existem no v6 e não existiam na v5

| Item | Descrição |
|------|-----------|
| Workflow de 4 olhos | `status_workflow` + `created_by_user_id` / `updated_by_user_id` / `published_by_user_id` / `published_at` + papéis `redator-diario-oficial` e `revisor-diario-oficial` |
| Auditoria pública (LGPD) | `Portal\DiarioController@auditoria` + `getPublicAuditTrail` |
| Capa do diário | `cover_image_id` + `addCoverPage()` + `cover-image.blade.php` / `cover-image.js` |
| Status de geração e polling | `generation_status`, `generation_progress`, `generationStatus()` e badge na listagem |
| Download do PDF | `?download=1` em `streamPdf` |
| Filtro do portal por edição | `filtroGET` com o parâmetro `edicao` |
| Escrita atômica do PDF | `.tmp` → `Storage::move` |
| Assinatura só em publicados | `FPdfService::$isPublished` + gate em `Header()` |
| Fallback FPDI → Imagick | `PdfConverter::countPages` |
| Extração de texto do PDF para busca | `searchPdf` + cache JSON + Ghostscript `txtwrite` |
| Capacidade de colunas | `columns_number` no diário e na matéria, `line_style`, `line_color` |

## 8. Mapa v5 → v6 (arquivos)

| Função | v5 | v6 |
|--------|----|----|
| Controller principal | `v5/app/Http/Controllers/Prefecture/DiarioController.php` | `v6/app/Http/Controllers/Prefecture/DiarioController.php` |
| Controller do portal | `v5/app/Http/Controllers/Portal/DiarioController.php` | `v6/app/Http/Controllers/Portal/DiarioController.php` |
| Repositório | `v5/app/Repositories/Eloquent/Prefecture/DiarioOficial/DiarioOficialRepository.php` | `v6/app/Repositories/Eloquent/Prefecture/DiarioOficial/DiarioOficialRepository.php` |
| Serviço de PDF | `v5/app/Services/FPdfService.php` + `v5/app/Services/Pdf/` (5 arquivos) | `v6/app/Services/FPdfService.php` + `v6/app/Services/Pdf/ImageProcessor.php` + `v6/app/Services/Pdf/PdfConverter.php` |
| Models | `v5/app/Http/Model/Prefecture/Diario*.php` (6) | `v6/app/Http/Model/Prefecture/Diario*.php` (6) |
| Requests | `v5/app/Http/Requests/DiarioOficialRequest.php` + `v5/app/Http/Requests/DiarioOficial/` | `v6/app/Http/Requests/DiarioOficialRequest.php` + `v6/app/Http/Requests/DiarioOficial/` |
| Resources | `v5/app/Http/Resources/DiarioOficial/` (3) | `v6/app/Http/Resources/DiarioOficial/` (3) |
| Views do CMS | `v5/resources/views/prefecture/contents/diario-oficial/` | `v6/resources/views/prefecture/contents/diario-oficial/` |
| Views do portal | `v5/resources/views/portal/layouts/diario-oficial/` | `v6/resources/views/portal/layouts/diario-oficial/` |
| JS do CMS | `v5/public/assets/js/custom/contents/diario-oficial/` | `v6/public/assets/js/custom/contents/diario-oficial/` (+ `cover-image.js`) |
| JS do portal | `v5/public/assets/js/custom/portal/diario-oficial/viewer.js` | `v6/public/assets/js/custom/portal/diario-oficial/viewer.js` |
| Rotas do CMS | `v5/routes/basepro.php` | `v6/routes/basepro.php` |
| Rotas do portal | `v5/routes/web.php` | `v6/routes/web.php` |
| Migrations do módulo | 28 (em `v5/database/migrations/`) | 28 (em `v6/database/migrations/`), com 7 re-datadas em 2026-09 |

## 9. Perguntas em aberto para o negócio

1. **Edição publicada pode ser corrigida?** Hoje `publish()` define
   `permission_edit = false`, mas o `update()` do CMS não checa esse campo. O
   elaborador consegue alterar uma edição já publicada? Se não, o botão deve
   ser bloqueado na view.
2. **O que acontece com a edição quando a segregação de funções está ativa e
   o diário está em rascunho?** Hoje `toogle_status` devolve `false` e nada
   muda: a edição fica inacessível no portal até a publicação. É a intenção?
3. **Anúncios (adendos) devem voltar?** O v5 tinha a tabela `diario_adendos`
   (já morta) e o backlog do upgrade registrou a limpeza dos órfãos de adendo
   como Fase 4. Há demanda por "adendo" (publicação de retificação)? Se sim,
   é um novo requisito, não uma restauração.
4. **A subárea deve ter alguma representação visual?** Hoje ela é
   completamente invisível no PDF. Se o negócio espera ver "Portarias" /
   "Decretos" no diário, isso é uma mudança de requisito, não um bug.
5. **Quantas páginas o portal deve permitir navegar?** O viewer atual usa o
   `<embed>` nativo do navegador; não há navegação interna por página nem
   busca highlight no PDF nativo.
6. **A assinatura deve ser obrigatória para publicar?** Hoje a assinatura só
   aparece no PDF de edições publicadas, mas nada impede publicar uma edição
   cujo certificado foi removido depois.
7. **O histórico do diário deve ser público?** A trilha pública existe
   (`auditoria`), mas não expõe o autor da edição nem o conteúdo da alteração.

## 10. Ordem de attack recomendada para a v7

| Prioridade | Item | Motivo |
|:----------:|------|--------|
| 1 | S1 (senha no `shell_exec`) e S2 (chave privada em texto claro) | Segurança |
| 2 | 1.1 (`pending` sem consumidor) → Job de fila | Funcionalidade visível quebrada no portal |
| 3 | 1.5 (`mergePdfs` nunca chamado) | Decidir entre ativar e remover o código morto |
| 4 | 1.4 (permissão de subárea) | Autorização incorreta |
| 5 | D1 (duplicação `diario-ofifical` / `diario-oficial`) | Consistência de dados e de menu |
| 6 | S3 (QR Code global) e 1.6/1.7 (bugs de JS) | Robustez e correção de UI |
| 7 | Limpeza dos órfãos (seção 2) e D2 (rotas duplicadas) | Manutenibilidade |
