# A Cidade — Gaps, Dívidas e Riscos

> Versão analisada: `v6` (Laravel 5.5.50 / PHP 7.2.34) com comparação `v5`.
> Todas as afirmações foram verificadas no repositório e nas bases de
> desenvolvimento (`v6_6f22e1bd-postgres-1` e `v5_3f273858-postgres-1`).

---

O **mapa v5 → v6** completo, as **perguntas em aberto** e o registro de
**verificações realizadas e não realizadas** estão em `09-MAPA-V5-V6.md`.

## 1. Bugs e erros conhecidos

### BUG-1 — Conteúdo de outra categoria é servido em `/a-cidade/{slug}` (severidade: **alta**)

**Evidência:**

```php
// v6/app/Http/Traits/GetInformationsPrefectureTrait.php:23-26
public function getContentDataWith($param)
{
    return  Content::with('archiveHighlight', 'archiveGallery', 'categoryContents:id,name,slug')->where('slug', $param)->first();
}
```

A query filtra **apenas** por `contents.slug`. Não há `whereHas('categoryContents', ...)`
nem verificação de `category_contents_id = 4`.

**Efeito:** qualquer conteúdo do portal — notícia, legislação, publicação — é
servido na URL `/a-cidade/{slug}`, dentro do template de A Cidade, com o
breadcrumb da **categoria dele** (exceto no modelo3, que força "A Cidade"), e
**contando clique** em `contents.clicks`. É também uma **superfície de
enumeração**: o slug é previsível (`historia`, `mapa`).

**Correção:** `whereHas('categoryContents', fn ($q) => $q->where('slug', 'a-cidade'))`
ou, na v7, route model binding + Policy (ver `06-MIGRACAO-MODERNA.md` §6).

### BUG-2 — Rascunho publicado na URL pública (severidade: **alta**)

**Evidência:** `contents.status` não aparece em nenhuma query do módulo —
nem em `getContentDataWith()`, nem na view SQL `a_cidade`
(`v6/database/migrations/2024_11_29_094836_update_view_a_cidade.php:17-27`).

**Efeito:** um conteúdo com `status = false` (não publicado) é servido na URL
pública e aparece na listagem do CMS. Na base de desenvolvimento, os 4
conteúdos de A Cidade têm `status = NULL` — que também é servido.

**Correção:** adicionar `->where('status', true)` (ou `published_at <= now()`).

### BUG-3 — Layout `default` derruba o portal e liga a manutenção automática (severidade: **alta**)

**Evidência:**

```php
// v6/app/Http/Traits/GetInformationsPrefectureTrait.php:92-102
public function getTemplateActived()
{
    $templateActived = Configuration::where('key', 'layout')->first()->value;

    if (!View::exists('portal.layouts.' . $templateActived . '.home.index')) {
        DB::table('configurations')->where('key', 'pagina_em_manutencao')->update(['value' => 1]);
        abort(404, 'view_inexistente');
    }

    return $templateActived;
}
```

O gatilho é a view `home/index` do layout — **não** a view do módulo.

**Verificação de filesystem (v6):** `modelo1`..`modelo6` têm **as duas** views
(`home/index.blade.php` e `informacoes-instituicao/acidade/show.blade.php`);
`default` e `newsbit` **não têm nenhuma das duas**.

**Efeito:** se `configurations.key = 'layout'` tiver o valor `default` ou
`newsbit`, **qualquer** página do portal (não só A Cidade) responde 404 e o
sistema **liga a manutenção automática** (`pagina_em_manutencao = 1`). Como o
`abort(404)` acontece antes de resolver a view do módulo, o erro real
("falta `home/index`") **não tem relação** com A Cidade — mas o operador não tem
como saber disso. O mesmo defeito existe na v5 (`default` e `newsbit` também
não têm `home/index.blade.php`) — **não é uma regressão da v6**.

**Correção:** (a) criar `home/index.blade.php` nos layouts `default`/`newsbit`,
ou (b) decidir que esses layouts não são válidos e validando a configuração
no CMS, ou (c) remover a escrita em `pagina_em_manutencao` do caminho de
resolução de layout.

### BUG-4 — O `include` da galeria do modelo3 aponta para um diretório inexistente (severidade: **alta**)

**Evidência:**

```blade
{{-- v6/resources/views/portal/layouts/modelo3/informacoes-instituicao/acidade/show.blade.php:24 --}}
@include("portal.layouts.modelo3.informacoes-prefeitura.galeria.index")
```

**Efeito:** o diretório `informacoes-prefeitura` foi renomeado para
`informacoes-instituicao` na v6, mas o include do modelo3 **não** foi
atualizado. O include só é alcançado quando o conteúdo tem `galery = 1` **e**
`archiveGallery` não vazia — nesse caso a página responde
**"View [portal.layouts.modelo3.informacoes-prefeitura.galeria.index] not
found"** (HTTP 500). Os outros cinco layouts usam o caminho correto
(`informacoes-instituicao.galeria.index`).

**Regressão da v6:** na v5 o include era coerente
(`v5/resources/views/portal/layouts/modelo3/informacoes-prefeitura/acidade/show.blade.php:24`).

**Correção:** trocar por
`@include("portal.layouts.modelo3.informacoes-instituicao.galeria.index")`.

### BUG-5 — A view do layout `default` é órfã e incompatível (severidade: média)

`v6/resources/views/portal/layouts/default/pages/contents/a-cidade/show.blade.php`
existe (55 linhas, **byte a byte idêntica** à da v5) e **não é renderizada por
nenhum controller**. Além disso, ela espera `$content` (o controller entrega
`$contentNew`), usa `$null` (que não existe), resolve a imagem por
`$content->archives->where('highlight', 1)` (o controller entrega
`$contentNew->archiveHighlight`) e inclui
`portal.layouts.default.pages.elements.gallery` (o controller usa
`...informacoes-instituicao.galeria.index`).

Mesmo se a rota fosse alterada para o layout `default`, a view quebraria.

### BUG-6 — O botão "Acessar" da grade não protege `slug` nulo (severidade: média)

**Evidência (v6):**

```js
// v6/public/assets/js/custom/contents/acidade/datatable.js:14
+ createSingleButton(data.id + 'Link',`/a-cidade/${data.slug}`, true, 'glyphicon glyphicon-link text-primary form_link', 'Acessar A cidade', '_blank')
```

**Evidência (v5, que protegia):**

```js
// v5/public/assets/js/custom/contents/acidade/datatable.js:13-16
let urlPortal = data.slug ? `/a-cidade/${data.slug}` : '#';
let btnLink = '';
if(data.slug){ btnLink = createSingleButton('iLink', urlPortal, true, '...', 'Acessar A Cidade', '_blank'); }
```

**Efeito:** conteúdo sem `slug` (coluna é nullable) gera um link
`/a-cidade/undefined`. **Regressão introduzida na v6.**

**Correção:** restaurar a guarda `data.slug ? ... : '#'`.

### BUG-7 — A imagem de destaque nunca aparece (severidade: média)

**Evidência:**

```blade
{{-- v6/resources/views/portal/layouts/modelo{1..6}/informacoes-instituicao/acidade/show.blade.php --}}
@if(file_exist($contentNew->archiveHighlight->path))
```

```php
// v6/app/Helpers/functions.php:267-274
function file_exist(string $path)
{
    return file_exists($path) ? $path : false;
}
```

`archives.path` é gravado **relativo** a `public/`
(`storage/content/a-cidade/<id>/destaque/<nome>` — veja
`ContentController@store`), mas `file_exists()` resolve o caminho relativo ao
*cwd* do processo PHP, que não é garantido ser `public/`. O resultado é que o
slot `imageHighlight` quase nunca é preenchido.

O mesmo defeito existe na v5.

**Correção:** `Storage::exists(public_path($path))` ou usar o helper
`file_exist_portal()` (`v6/app/Helpers/functions.php:281`), que já existe e
devolve a imagem padrão quando o arquivo não existe.

### BUG-8 — O botão de exclusão em lote exige a permissão do módulo errado (severidade: média)

```blade
{{-- v6/resources/views/prefecture/contents/a-cidade/index.blade.php:15 --}}
@if (has_access(['noticias.delete']))
    <div class="float-right pl-3">
        @component('prefecture.components.deleteByLoteSubmitButton', ['route' => 'ajax.generic.content.deleteAll'])
```

**Efeito:** o botão "Excluir Registro(s)" da tela de A Cidade depende de
`noticias.delete`, e não de `a-cidade.delete`. Um operador com acesso a A
Cidade e sem acesso a Notícias não consegue excluir em lote; um operador com
acesso a Notícias vê o botão indevidamente. A coluna de seleção múltipla, no
mesmo arquivo (`:29`), usa corretamente `a-cidade.delete`.

### BUG-9 — Slug inexistente devolve HTTP 200 (severidade: média)

```php
// v6/app/Http/Controllers/Portal/AboutController.php:32
if (!$contentNew) return view("admin.errors.conteudo-nao-encontrado");
```

A resposta é **200**. Para SEO, métricas e monitoramento, o correto é 404. Além
disso, a view de erro estende `portal.layouts.modelo2.layout.layout`
**fixo** — um visitante no modelo1/modelo5 vê um layout diferente do resto do
portal.

### BUG-10 — O contador de cliques não é atômico (severidade: média)

```php
// v6/app/Http/Traits/GetInformationsPrefectureTrait.php:104-108
public function updateClicks($idContent)
{
    $content = Content::where('id', $idContent)->first();
    Content::where('id', $content->id)->update(['clicks' => $content->clicks + 1]);
}
```

Duas queries em read-modify-write, sem `WHERE` nem lock: visitas simultâneas
**perdem** incrementos. Além disso, `getTemplateActived()` é chamado **na
montagem do `view()`**, ou seja, **depois** de `updateClicks()` — logo, **um 404
por layout inválido já incrementa o contador** do conteúdo.

### BUG-11 — O módulo não aparece no menu do CMS (severidade: média)

```php
// v6/app/Providers/MenuServiceProvider.php:46
if (has_access("$menu->slug.index") && isset($menu->modulo->status) && $menu->modulo->status) {
```

`modulos.id = 13` (`a-cidade`) tem `status = false` na base atual (confirmado
por consulta), enquanto `a-instituicao` (id 12), `orgaos` (17) e `secretarias`
(18) também têm `status = false`. O papel `administrador` **tem** todas as
permissões `a-cidade.*` (verificado em `roles.permissions`), mas a entrada do
menu é suprimida pela segunda condição — o operador só chega a
`/cms/conteudos/a-cidade` digitando a URL.

**Correção:** decisão de negócio — ativar `modulos.status` ou remover a segunda
condição do menu.

### BUG-12 — O label do botão de salvar está invertido (severidade: baixa)

```js
// v6/resources/views/prefecture/contents/a-cidade/create.blade.php:54-63
if ( $('#btn-galeria').is(':checked') == false) {
    $('#btn-galeria').on('click', function () {
        if ( $('#btn-galeria').is(':checked')) {
            $('#btn-submit').empty();
            $('#btn-submit').append('Salvar e ir para galeria')
        } else { ...
```

A condição externa (`galeria desmarcada`) e a interna (`galeria marcada`) são
invertidas: o rótulo "Salvar e ir para galeria" é exibido quando a galeria
está **desligada**, e some quando é ligada.

### BUG-13 — PDF na galeria vira imagem quebrada (severidade: baixa)

`archiveGallery()` (`v6/app/Http/Model/Prefecture/Content.php:192-195`) não
filtra `extension`. A tela de edição filtra (`'extension', '<>', 'pdf'` em
`ContentController.php:290`), mas a página pública não.

### BUG-14 — Slug pode colidir com um id futuro (severidade: baixa)

`contents.slug` **não tem** índice único. O sufixo de colisão é
`slug-<max(id)+1>`, calculado no momento da criação — duas criações
simultâneas com o mesmo título geram o mesmo slug.

### BUG-15 — `row_number()` usado como identificador (severidade: baixa)

A coluna `value` da view `a_cidade` é recalculada a cada consulta; excluir um
conteúdo renumera todos os seguintes. A grade do CMS a exibe como se fosse o
número do registro.

### BUG-16 — Docblock e imports copiados do controller de notícias (severidade: baixa)

`Portal\AboutController` tem o docblock *"Classe criada com objetivo de
disponibilizar a tela de notícias."* e importa `NewsTrait` e `Route` sem usar
nenhum dos dois. Idêntico na v5.

### BUG-17 — Breadcrumb com link morto (severidade: baixa)

`v6/routes/breadcrumbs.php:268` — `$breadcrumbs->push('A Cidade', url('/#'))`.
O primeiro nível não navega para a listagem do módulo.

### BUG-18 — `menu_orders.clicks` mede uso do CMS, não do portal (severidade: baixa)

`menuClicks('a-cidade')` é chamado em `returnIndex()`/`returnData()`
(`ContentController.php:121-126`, `:275-284`) — quando o **operador** abre a
tela. O número exibido no painel como "acessos ao módulo" é, na verdade,
acessos ao CMS.

## 2. Funcionalidades incompletas ou mortas

| Item | Situação |
|---|---|
| **API de dados abertos** | **Inexistente** para A Cidade. `v6/routes/api.php` expõe apenas `v1/secretarias`, `v1/orgaos`, `v1/legislacoes`, `v1/noticias` e `v1/publicacoes`. |
| **Listagem pública do módulo** | Não existe. Não há rota `/a-cidade` (sem slug): o visitante só chega a uma página pelo menu ou pela busca. |
| **Layout `default`** | Inalcançável (BUG-3). A view `default/pages/contents/a-cidade/show.blade.php` é órfã em **ambas** as versões (v5 e v6), byte a byte idêntica. |
| **Layout `newsbit`** | Inalcançável pelo mesmo motivo; só existe o **componente** `newsbit/.../components-acidade/index.blade.php`, sem o `show.blade.php`. |
| **Diretórios `default/pages/contents/a-prefeitura/` e `components-a-prefeitura/`** | Órfãos em ambas as versões — remanescente da renomeação do módulo A Instituição. |
| **Coluna `links.isBlank`** | Preenchida no `store()` (`'isBlank' => $blank`), mas **não é lida** pela view do portal — o vídeo sempre abre no mesmo local. |
| **`contents.status`** | Nunca exibido, nunca filtrado. |
| **Campo `galery`** | Só funciona se o operador marcar o checkbox **e** o `archiveGallery` tiver itens; não há verificação de conteúdo. |
| **Ordenação, busca e exportação da grade** | Ordenação fixa (`created_at DESC`); não há ordenação por título, filtro por status nem campo de busca. Exportação só em Excel, PDF, CSV e TXT, todos gerados **no cliente**. |
| **Log de auditoria e contador por sessão** | `generateLog()` registra "Listou conteúdo de a cidade", sem trilha por conteúdo individual. Não há contador de cliques por sessão — cada requisição conta. |

## 3. Riscos de dados

| # | Risco | Evidência | Mitigação na v7 |
|---|---|---|---|
| D-1 | **Renomeação por seed, não por migration**: `category_contents.id = 6` só vira `a-instituicao` se o seed rodar. Uma base que rode as migrations da v6 sem os seeds fica com `a-prefeitura` e o menu do CMS não bate. | `v6/database/seeds/CategoryContentTableSeeder.php:41-53`, `v6/database/seeds/MenuOrderSeeder.php:72-87`, `v6/database/seeds/ResourcesTableSeeder.php:89-105` | Migration idempotente com `UPDATE`. |
| D-2 | **Views SQL dentro de migrations**: o `down()` de `2024_11_29_094836_update_view_a_cidade.php:37-47` recria a view **sem** a coluna `slug`. Reverter a migration quebra o botão "Acessar" da grade. | `v6/database/migrations/2024_11_29_094836_update_view_a_cidade.php:35-48` | `down()` coerente com o estado-alvo. |
| D-3 | **Sem unicidade de slug**: hoje a colisão é resolvida em runtime. Ao criar o índice único na v7, é preciso backfill de duplicatas. | `v6/app/Http/Controllers/Prefecture/ContentController.php:665-669` | Backfill + índice único parcial (`WHERE deleted_at IS NULL`). |
| D-4 | **`contents.status` NULL**: os 4 conteúdos semeados têm `status = NULL`. Um `WHERE status = true` na v7 **os esconderia de imediato**. | consulta na base: `status` = `(null)` nos 4 | Backfill `status = true` antes de ativar o filtro. |
| D-5 | **Chave semântica repetida em 4 tabelas**: `contents.slug`, `category_contents.slug`, `menu_orders.slug` e `resources.slug` — qualquer um pode divergir. | 4 tabelas | Restrição de unicidade + seeder de reconciliação. |
| D-6 | **Soft delete de categoria não cascateia**: apagar (soft) `category_contents.id = 4` deixa os conteúdos órfãos, e a view SQL passa a listá-los sem categoria válida no menu. O **caminho de arquivos** também é contrato: `archives.path` = `storage/content/a-cidade/<id>/destaque/<nome>` é referenciado por `getLargFile()`/`getMinFile()` e por URLs já publicadas. | FK `contents_category_contents_id_foreign` só protege o hard delete; `ContentController@store`, `v6/app/Helpers/functions.php:977-1006` | Política de cascata explícita e preservação da chave de caminho na migração para storage. |
| D-7 | **Links apontando para `/a-cidade/{slug}`** já publicados em redes sociais e documentos; e **rascunho já exposto**: corrigir o filtro **remove** páginas já indexadas. | — | Manter o prefixo `/a-cidade` (decisão R-1 em `06-MIGRACAO-MODERNA.md`) e corrigir o filtro gerando 404 (não redirect) para o slug. |

---

As **perguntas em aberto** e o registro de **verificações
realizadas e não realizadas** estão em `09-MAPA-V5-V6.md`.

