# Notícias — Gaps, Bugs e Dívidas

## 1. Bugs e erros conhecidos

| # | Severidade | Descrição | Evidência |
|---|---|---|---|
| B01 | **Alta** | O escopo `scopeByCategoryContentNews` grava `categorySlugSession` na sessão e só a rota AJAX limpa (`session()->pull`). As demais rotas podem herdar o filtro da requisição anterior do mesmo visitante. | `v6/app/Http/Model/Prefecture/Content.php:212-224`; `v6/app/Http/Controllers/Portal/NewsController.php:113` |
| B02 | **Alta** | A listagem do portal **e** a view SQL `noticias` exigem `category_contents.category_contents_id = 1`. No banco `prefeituras_v5` atual **nenhuma** categoria tem pai 1 e a view retorna 0 linhas -> o módulo está inoperante sem configuração manual. | `select count(*) from noticias;` -> `0`; `select id, category_contents_id from category_contents;` -> nenhum `category_contents_id = 1` |
| B03 | **Alta** | `scopeByStatusNoticias` retorna dentro do `foreach`: filtra **apenas o primeiro registro** do lote ou não filtra nada. | `v6/app/Http/Model/Prefecture/Content.php:145-160` |
| B04 | Média | `scopeByLimit` aplica `limit()` no sub-select de `whereHas` (categorias), não nos conteúdos. | `v6/app/Http/Model/Prefecture/Content.php:227-243` |
| B05 | Média | `limit($limit)` antes de `paginate($paginate)` é anulado pelo `paginate()`. | `v6/app/Http/Traits/NewsTrait.php:28-40` |
| B06 | Média | `scopeByStatusNoticias` executa `$query->get()` **dentro** do scope: a query é disparada duas vezes (custo e risco de inconsistência entre as duas execuções). | `v6/app/Http/Model/Prefecture/Content.php:147` |
| B07 | Média | `updateClicks()` faz read-modify-write (`$content->clicks + 1`) — Lost update sob concorrência; também faz 2 queries por visita. | `v6/app/Http/Controllers/Portal/NewsController.php:137-142` |
| B08 | Média | `ContentRequest::rules()` decide obrigatoriedade da `slug` lendo `Route::getCurrentRoute()->getName()` — acopla validação ao nome da rota. | `v6/app/Http/Requests/ContentRequest.php:76` e `:104` |
| B09 | Média | `ContentController::genericIndex`/`genericGenerate` são cadeias de `if` por slug; slug não mapeado redireciona para o dashboard silenciosamente. | `v6/app/Http/Controllers/Prefecture/ContentController.php:91-119` e `:163-273` |
| B10 | Baixa | `ContentController::pathUpdate` grava com `file_put_contents()` + `@mkdir()`, ignorando o disco configurado do Laravel. | `v6/app/Http/Controllers/Prefecture/ContentController.php:373-435` |
| B11 | Baixa | `NewsController::indexContentsAjax` devolve `null` (200 vazio) quando a requisição não é Ajax, em vez de 404/JSON de erro. | `v6/app/Http/Controllers/Portal/NewsController.php:111-135` |
| B12 | Baixa | `ContentController::store` desambigua a slug com `Content::max('id') + 1` — código de corrida sob gravações simultâneas. | `v6/app/Http/Controllers/Prefecture/ContentController.php:665-668` |
| B13 | Baixa | A ordenação da galeria difere entre o portal (v6: `order asc`) e o restante do sistema (v5: `created_at desc`) — divergência silenciosa de UX. | diff `v5/app/Http/Controllers/Portal/NewsController.php` x `v6/...` |
| B14 | Baixa | `Content::secretarias()` é `hasMany(Content::class, 'secretaria', 'slug')` — auto-relação por **slug**, não por id: frágil. | `v6/app/Http/Model/Prefecture/Content.php:88-91` |

## 2. Funcionalidades incompletas ou mortas

| Item | Situação |
|---|---|
| `Content::ultimasNoticias($limit = 3)` | Método estático **não usado** pelo portal (o portal usa `NewsTrait`); útil apenas como API de compatibilidade |
| `scopeByLimit` | Não é chamado em lugar nenhum do portal (grep) — só `scopeByCategoryContentNews` é usado |
| `scopeByStatusNoticias` | Sem consumidor identificado |
| `scopeByOrderClicks` | Existente, mas o portal usa `getMorePopularNews()` com query própria |
| `Content::archiveAcidade()` | `hasOne` com `byHighLight(0)` — reservado ao módulo "A Cidade" |
| `Configuration::key = 'noticias'` | Chave usada como booleano (`0` = agrupado). Não há validação nem documentação do contrato |
| Chave `aviso_noticias` | Lida em `NewsController@index`, mas o valor não é repassado para as views (`imageNewsDisabled` écompactado e raramente usado) |
| `modulos.status` | O módulo `noticias` está com `status = 1`; `publicacoes` está com `status = 0` — o status do módulo **não** é lido pelo portal para exibir/ocultar as páginas |
| Galeria no CMS | O botão de galeria navega por âncora (`#galeria`); a edição de galeria usa `ajaxUploadFiles` (POST) e `ajaxDeleteFilesOfGallery` — sem endpoint dedicado de reordenação da galeria |

## 3. Itens da v5 que mudaram na v6

| Item | v5 | v6 | Efeito |
|---|---|---|---|
| Galeria da notícia | `orderBy('created_at', 'desc')` | `orderBy('order', 'asc')` | A ordem manual passa a valer |
| Modelo `Content` | sem `linha_fina` e `councilmen_id` | com ambos (`+` relação `councilman()`) | Novas abilities editoriais |
| Slug do conteúdo | `str_slug($title)` | `slugfy($title)` + desambiguação `-<id>` / `pub<id>-` | URLs mais estáveis |
| Link de vídeo | salvo cru | `sanitizeLink()` | Evita `https:///rota-interna` |
| Formulário do CMS | com botão "Preencher com IA" (modal Gemini) e campo de vídeo aceitando YouTube/Vimeo/Facebook/Instagram | **sem** o botão de IA; vídeo aceito apenas por YouTube; **com** o campo `linha_fina`; secretaria/órgãos condicionados a `$modulos` | **Regressão funcional na v6**: o preenchimento por IA e o suporte a vídeo de outras plataformas foram perdidos |
| `ContentController` (CMS) | — | + ~1400 linhas | `pathUpdate`, `view_pdf`, `linha_fina`, auditoria detalhada, `isBlank`, `referencia` |
| `PortalController` | — | + ~280 linhas | Novas rotas e controllers de portal |
| `DatatableController` | — | + ~266 linhas | Novos datatables (destaques, enquetes, etc.) |
| `og:description` da notícia | `Str::limit(strip_tags($text), 50, '...')` | `strip_tags($text)` sem limite | Meta tag mais longa na v6 |
| API `publicacoes.export` | inexistente | `GET /publicacoes/export` | Nova capacidade (módulo Publicações) |

**Perdas identificadas na v6 (módulo Notícias):** botão "Preencher com IA"
(`prefecture.contents.components.ia-modal.index` +
`assets/js/custom/utilities/helpers/ia-modal.js`, presentes na v5 e
ausentes na v6) e o suporte a vídeo de Facebook/Instagram/Vimeo no rótulo do
campo de link do formulário.

## 4. Mapa v5 -> v6

| Camada | v5 | v6 | Observação |
|---|---|---|---|
| Modelo | `v5/app/Http/Model/Prefecture/Content.php` | `v6/app/Http/Model/Prefecture/Content.php` | 10 linhas de diferença (acréscimos) |
| Modelo categoria | `v5/app/Http/Model/Prefecture/CategoryContent.php` | `v6/app/Http/Model/Prefecture/CategoryContent.php` | idem |
| Controller CMS | `v5/app/Http/Controllers/Prefecture/ContentController.php` | `v6/app/Http/Controllers/Prefecture/ContentController.php` | ~1400 linhas de diferença |
| Controller CMS categorias | `v5/app/Http/Controllers/Prefecture/CategoryContentController.php` | `v6/app/Http/Controllers/Prefecture/CategoryContentController.php` | — |
| Controller CMS datatable | `v5/app/Http/Controllers/Prefecture/DatatableController.php` | `v6/app/Http/Controllers/Prefecture/DatatableController.php` | ~266 linhas de diferença |
| Controller portal | `v5/app/Http/Controllers/Portal/NewsController.php` | `v6/app/Http/Controllers/Portal/NewsController.php` | 1 linha de diferença (ordem da galeria) |
| Trait portal | `v5/app/Http/Traits/NewsTrait.php` | `v6/app/Http/Traits/NewsTrait.php` | praticamente idêntico |
| Rotas portal | `v5/routes/web.php:95-101` | `v6/routes/web.php:105-111` | mesmas URIs e nomes |
| Rotas CMS | `v5/routes/basepro.php:159-162`, `:207-209` | `v6/routes/basepro.php:173-176`, `:235-237` | mesmas URIs e nomes |
| Views CMS | `v5/resources/views/prefecture/contents/noticias/` | `v6/resources/views/prefecture/contents/noticias/` | mesma estrutura (`index`, `create`, `edit`, `form`, `show`); só `form.blade.php` difere (`linha_fina`, modal de IA removido, vídeo restrito a YouTube) |
| Views portal | `v5/resources/views/portal/layouts/*/noticias/` | `v6/resources/views/portal/layouts/*/noticias/` | mesmos layouts nos dois repositórios (`default`, `modelo1..6`, `newsbit`); 8 arquivos diferem no `modelo1`, 4 no `modelo2`, 3 no `modelo4`/`modelo6`, 2 no `modelo3`, 1 no `modelo5`, 0 no `newsbit` e no `default` |
| Views SQL | idem | idem | a view `noticias` evoluiu por 8 migrations |
| Seeds | `v5/database/seeds/CategoryContentTableSeeder.php` | `v6/database/seeds/CategoryContentTableSeeder.php` | mesma estrutura; a v6 acrescenta `display_type`, `custom_icon`, `description` |

### Rotas do módulo: comparação

O conjunto de rotas do CMS de notícias é **idêntico** nas duas versões:

| Rota | v5 | v6 |
|---|---|---|
| `POST /cms/marcar-destaque` | `v5/routes/basepro.php:318` | `v6/routes/basepro.php:346` |
| `POST /cms/set-status` | `v5/routes/basepro.php:319` | `v6/routes/basepro.php:347` |
| `POST /cms/conteudo/deleteAll` | `v5/routes/basepro.php:131` | `v6/routes/basepro.php:141` |
| `GET /cms/conteudos/{slug}` | `v5/routes/basepro.php:207` | `v6/routes/basepro.php:235` |
| `GET /cms/conteudos/cadastrar/{slug}` | `v5/routes/basepro.php:208` | `v6/routes/basepro.php:236` |
| `GET /cms/conteudos/editar/{slug}/{id}` | `v5/routes/basepro.php:209` | `v6/routes/basepro.php:237` |
| `Route::resource('conteudo')` | `v5/routes/basepro.php:160` | `v6/routes/basepro.php:174` |
| `Route::resource('categoria-conteudo')` | `v5/routes/basepro.php:159` | `v6/routes/basepro.php:173` |

## 5. Riscos de dados (migração de schema)

| Risco | Detalhe | Mitigação |
|---|---|---|
| Subcategorias de Notícias ausentes | A view `noticias` e o escopo do portal dependem de `category_contents.category_contents_id = 1` | Criar as subcategorias via `categoria-conteudo` antes da migração, ou recriar o filtro por `slug` da raiz |
| `quantidade_itens` e `layout` nulos | Todos os valores são `NULL` no banco atual -> `getCategoriesNews` (que exige `> 0`) não retorna faixa nenhuma | Migrar para `0`/`2` com default e criar as faixas no CMS |
| Slugs antigos | A v6 mudou a geração da slug | Congelar um mapa `id -> slug` antes da migração e recriar redirecionamentos |
| `linha_fina` e `referencia` | Colunas novas (2026) | `nullable` — sem backfill obrigatório |
| `contents.slug` sem índice único | A unicidade é garantida só na aplicação | Criar índice único parcial (`where deleted_at is null`) |
| `archives.highlight` tri-estado | `1` = destaque, `NULL` = galeria, `0` = galeria "a-cidade" | Modelar como `ArchiveRole` enum na reconstrução |
| `clicks` sem atomicidade | Contagem por read-modify-write | `increment()` atômico |

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

1. **Quais são as subcategorias oficiais de Notícias?** Hoje o seed não
   define nenhuma; sem elas o módulo não exibe nada.
2. `quantidade_itens` define o número exibido na home — quem define esse
   número e com que critério?
3. A chave `configurations.noticias = 0` significa "modo agrupado". Existe um
   contrato documentado para essa chave?
4. Deve uma notícia em rascunho ser visível via URL direta
   (`/noticia/{slug}`)? Hoje não é (filtro `status = true`).
5. O contador de cliques deve contar apenas o portal ou também o CMS?
6. Qual o limite deNews por página? Hoje é 12 (`$paginate = 12`).
7. O `layout` (1..3) da categoria "Notícias" é global ou por subcategoria?
   O código lê apenas o da raiz.
8. A view `noticias` deveria filtrar `publication_date <= now()` como o
   portal? Hoje o CMS mostra matérias futuras como se fossem publicadas.

## 7. Checklist de pendências

- [ ] Criar subcategorias de Notícias (id 1) no ambiente de produção.
- [ ] Definir `quantidade_itens` e `layout` das categorias.
- [ ] Remover `session()` dos scopes `byCategoryContentNews` e `byLimit`.
- [ ] Corrigir `scopeByStatusNoticias` (ou removê-lo).
- [ ] Trocar `updateClicks()` por `increment()`.
- [ ] Trocar `Route::getCurrentRoute()->getName()` por `->isMethod('PUT')` ou
      pelo tipo da operação.
- [ ] Extrair a cadeia de `if` do `ContentController` para uma tabela de
      configuração por slug.
- [ ] Criar índice único em `contents.slug` (parcial, soft delete).
- [ ] Documentar/decidir o comportamento da view `noticias` quanto a
      rascunhos e datas futuras.
