# Reconstrução cega — `MOD-INFRAESTRUTURA-FEEDRSS`

> Unidade-alvo: painel CMS de suporte do feed RSS (onda 4, grupo `Infraestrutura`,
> 4 arquivos, 3 rotas, 1 blade). O canal público `/feed.xml` e a tabela pertencem a
> `SVC-FEEDRSS` (onda 2) e **não** são recriados aqui; são tratados como contrato de
> dependência. Evidência usada: `05_modulos/Infraestrutura/FeedRss/spec.md` (citado como
> `spec`), `02_servicos-base/feed-rss.md` (citado como `svc`), `01_arquitetura/02-roteamento.md`
> (`roteamento`), `01_arquitetura/03-auth-rbac-sentinel.md` (`auth`),
> `01_arquitetura/04-middlewares-seguranca.md` (`middlewares`),
> `01_arquitetura/09-cache-csp-minify-maintenance.md` (`cache`),
> `08_matriz/*` e `_indices/*`.

## 0. Fronteira módulo × serviço (pré-requisito de leitura)

| Responsabilidade | `MOD-INFRAESTRUTURA-FEEDRSS` | `SVC-FEEDRSS` |
|---|---|---|
| Tabela `v2_feed_rss_items` (migration, model, unique, soft delete) | não | sim (`svc §6`) |
| Gerar e servir `/feed.xml` (RSS 2.0, cache HTTP) | não | sim (`svc §5`) |
| Receber publicação dos módulos de conteúdo (`FeedSender`/`FeedItemDTO`) | não | sim (`svc §4.1`) |
| `addItem` / `removeItem` (UPSERT e soft delete por entidade) | não | sim (`svc §4.1`) |
| `getStats` / `getAllItemsPaginated` (leitura do painel) | consome | fornece (`spec §4.1`) |
| `toggleItem` / `deleteItem` (escrita do painel) | consome | fornece (`spec §4.1`) |
| 3 rotas `v2.feedrss.support.*`, `FeedRssSupportController`, blade `v2-feedrss::cms.index` | sim | não |
| Autorização por identidade (`v2.support.email` ou Sentinel `super-admin`) | sim | não (a rota pública é anônima) |
| Auditoria `feed_rss_item_enabled/disabled/deleted` | dispara via serviço | executa (`svc §8`) |
| Invalidação de `page:*` após toggle/delete | **não existe** (`spec §1.2`, `svc §1.4`) | **não existe** |

Regra de ouro da unidade: o módulo **é somente UI de suporte**. Não cria item, não edita
conteúdo, não gera XML, não tem tabela, não tem migration, não tem seeder, não tem API
(`spec §1.4`, `spec §6`, `spec §12`).

---

## 1. Ficha de identidade

| Campo | Valor |
|---|---|
| **Efeito primário** | O suporte vê quantos itens existem no feed, a distribuição por módulo e a data do item mais recente; pode retirar/retornar um item do `/feed.xml` (`enabled`) ou removê-lo (`deleted_at`) sem tocar no conteúdo de origem (`spec §0`). |
| **Entrada** | `GET /cms/v2/feed-rss`; `POST /cms/v2/feed-rss/{id}/toggle` com `enabled=0\|1`; `POST /cms/v2/feed-rss/{id}` + `_method=DELETE` (method spoofing) (`spec §0`, `spec §5`). Não há query string, corpo JSON, upload ou formulário de criação. |
| **Saída** | `200` HTML (4 cartões + distribuição por módulo + tabela paginada em 20 + rodapé “Última atualização”); `302` para `v2.feedrss.support.index` ou `v2.cms.dashboard` com flash; `404` quando a flag do módulo está desligada; escrita em `v2_feed_rss_items` (`enabled`, `deleted_at`) e em `activity_log` (`spec §0`, `spec §1.3`). |
| **Efeitos colaterais** | (a) `UPDATE v2_feed_rss_items.enabled`; (b) `UPDATE v2_feed_rss_items.deleted_at` (soft delete); (c) `INSERT activity_log` por escrita (`feed_rss_support`); (d) alteração indireta do `/feed.xml`, sujeita ao cache de página de 5 min e ao `Cache-Control` de 60 s; (e) **nenhum** e-mail, índice de busca, arquivo, purge ou invalidação de cache (`spec §1.2`, `spec §1.4`). |
| **Particularidade central** | Autorização por **identidade** (`config('v2.support.email')` ou Sentinel `super-admin`), não por `v2_can`; o RBAC do middleware é condição necessária para perfis comuns, mas não suficiente; e o `enabled` não é definitivo porque o UPSERT do serviço força `true` (`spec §0`, `spec §2.3`, `svc §3.2 RN-FEEDRSS-02`). |
| **Dependências** | `CORE-AUTH` (Sentinel, RBAC, `CmsAuthenticate`), `CORE-CMS` (layout), `SVC-FEEDRSS` (`FeedRssService`, tabela, model, auditoria), `SVC-SEO` (declarado; ver BL-06), `SVC-ACTIVITYLOGGER` (`spec` frontmatter, `spec §2.2`). |
| **Consumidores** | Nenhum. É folha do grafo (`spec` frontmatter: `consumidores: []`). |
| **Como se integra** | `ModuleProvider` com `v2-feedrss`, `active_pattern=['cms/v2/feed-rss*']`, `restricted=config('v2.support.email')`; rotas nomeadas `v2.feedrss.support.*`; controller injeta `FeedRssService` (+ `SEOService`); `V2ServiceProvider` descobre o diretório e registra as rotas CMS (`spec §2.1`, `spec §10.4`). |
| **Se removido** | Só o painel; `/feed.xml`, a tabela e a publicação continuam via `SVC-FEEDRSS` (`spec §0`). |
| **Superfície pública** | `FeedRssSupportController` (`index`, `toggle`, `destroy`), `ModuleProvider::getConfig()`, 3 rotas, 1 blade (`spec §0`, `spec §4.1`). |
| **Volume/custo** | `index`: 5 consultas de estatística + 1 paginada + 1 `count` do paginador; `toggle`: 1 `SELECT` + 1 `UPDATE` + 1 `INSERT` de auditoria; `destroy`: 1 `SELECT` + 1 `UPDATE` + 1 `INSERT` (`spec §2.5`). |

### 1.1 O que o módulo deliberadamente não faz

- Não gera XML nem serve `/feed.xml` (`spec §1.4`).
- Não cria/edita itens: não há formulário de título, link, descrição, imagem (`spec §1.4`).
- Não consulta o conteúdo de origem para validar link/título (`spec §1.4`).
- Não executa purge de itens desabilitados nem remoção física (`spec §1.4`, `spec §3.2 RN-04`).
- Não invalida `page:*` após toggle/delete (`spec §3.2 RN-07`).
- Não usa `v2_can`/RBAC próprio; a barreira efetiva é o controller (`spec §2.3`, `spec §10.2`).
- Não envia e-mail/notificação ao remover (`spec §1.4`).
- Não registra leitura em `activity_log` (`spec §11`).

---

## 2. Schema (DDL conceitual)

### 2.1 O módulo não possui schema

Não há migrations, tabelas, models, colunas ou índices de propriedade do módulo
(`spec §6`: “O módulo não tem migrations nem model”; `spec §12`: seeder não se aplica;
`_indices/parcial-MOD-INFRAESTRUTURA-FEEDRSS/` só contém `rotas`, `componentes`, `config`,
`permissoes`). Qualquer DDL criado por engano nesta unidade é divergência S1
(`reconstrucoes-cegas.md §6 C2`).

### 2.2 Tabela consumida — `v2_feed_rss_items` (dona: `SVC-FEEDRSS`)

```sql
-- Criada por SVC-FEEDRSS. NÃO criar nesta unidade.
CREATE TABLE v2_feed_rss_items (
    id            uuid         NOT NULL,               -- gerado na aplicação (evento creating), não por DEFAULT
    module        varchar(50)  NOT NULL,               -- 'noticias' | 'agenda' | 'trabalhe-conosco' | ...
    entity_type   varchar(50)  NOT NULL,               -- 'news' | 'evento' | 'job-opening'
    entity_id     varchar(36)  NOT NULL,               -- UUID da entidade de origem
    title         varchar(255) NOT NULL,
    link          varchar(500) NOT NULL,
    description   text         NULL,
    content       text         NULL,                   -- HTML cru
    author        varchar(255) NULL,
    categories    jsonb        NULL,                   -- cast array
    image_url     varchar(500) NULL,
    enabled       boolean      NOT NULL DEFAULT true,
    published_at  timestamp    NULL,
    created_at    timestamp    NULL,
    updated_at    timestamp    NULL,
    deleted_at    timestamp    NULL,                   -- SoftDeletes
    PRIMARY KEY (id),
    CONSTRAINT uq_v2_feed_rss_items_entity UNIQUE (entity_type, entity_id)  -- SEM filtro de deleted_at
);

CREATE INDEX idx_v2_feed_rss_items_module       ON v2_feed_rss_items (module);
CREATE INDEX idx_v2_feed_rss_items_enabled      ON v2_feed_rss_items (enabled);
CREATE INDEX idx_v2_feed_rss_items_published_at ON v2_feed_rss_items (published_at);
```

Fatos que a reimplementação precisa preservar:

| Item | Contrato | Fonte |
|---|---|---|
| PK | `id uuid`, sem auto-incremento; UUID gerado no evento `creating`; `$incrementing=false`, `$keyType='string'` | `svc §6` |
| Sem FKs | Nenhuma FK; vínculo com a origem é polimórfico por `(entity_type, entity_id)` | `svc §6`, `08_matriz/entidades-e-propriedade.md` (linha 177) |
| Unique | `(entity_type, entity_id)` **sem** `WHERE deleted_at IS NULL` — a armadilha central | `svc §3.2 RN-03`, `svc §3.4 item 1` |
| Soft delete | `deleted_at`; `removeItem`/`deleteItem` nunca apagam a linha | `svc §1.1`, `spec §3.2 RN-04` |
| Casts | `categories => array`, `enabled => boolean`, `published_at => datetime` | `svc §6` |
| Escopos | `enabled()`, `recent()` (=`orderBy('published_at','desc')`), `byModule()`, `byEntityType()`, `byEntity()` | `svc §0`, `svc §2.3` |
| Migration | `up()` dropa previamente `backup_v2_feed_rss_items_%`, usa transação + `lock_timeout`; `down()` renomeia para `backup_v2_feed_rss_items_<Ymd_His>` | `svc §6` |
| Volume | 1 linha por entidade publicada + linhas soft-deleted sem purge | `svc §10 LG-04` |

### 2.3 `activity_log` (dona: `SVC-ACTIVITYLOGGER`)

Linha gravada por cada escrita do painel (`spec §11`, `svc §8`, `02_servicos-base/activity-logger.md §1.1`):

| Campo | Valor nesta unidade |
|---|---|
| `description` | `feed_rss_item_enabled` \| `feed_rss_item_disabled` \| `feed_rss_item_deleted` |
| `subject_type` / `subject_id` | morph do `FeedRssItem` e seu `id` (o item já soft-deleted no caso de `deleted`) |
| `causer_type` / `causer_id` | usuário Sentinel autenticado (se houver) |
| `properties` | `type='feed_rss_support'`, `v2_audit=true`, `ip_address`, `user_agent`, `transaction_snapshot_id` (quando houver) |
| `log_name` | `default` |

Escritas do serviço (fora do módulo) usam `type='feed_rss_service'` e descrições
`feed_rss_item_upserted` / `feed_rss_item_removed` (`svc §8`).

### 2.4 Consultas que o painel executa (contrato de leitura)

| Uso | Consulta conceitual | Observação |
|---|---|---|
| total | `SELECT count(*) FROM v2_feed_rss_items` | global scope exclui `deleted_at` |
| habilitados | `... WHERE enabled = true` | |
| desabilitados | `... WHERE enabled = false` | |
| por módulo | `SELECT module, count(*) AS total ... GROUP BY module` → `pluck('total','module')` | inclui desabilitados, exclui soft-deleted (`spec §2.4`) |
| último item | `ORDER BY published_at DESC LIMIT 1` | `NULLS FIRST` no PostgreSQL (`spec §2.4`) |
| tabela | `ORDER BY published_at DESC` + `LIMIT 20 OFFSET ...` | inclui habilitados e desabilitados |

---

## 3. Rotas

### 3.1 Rotas da unidade (3)

| # | Método | URI | Nome | Middleware (ordem efetiva) | Controller@ação | Permissão | Resposta / erros |
|---|---|---|---|---|---|---|---|
| R1 | `GET` (e `HEAD`, `[I]`) | `/cms/v2/feed-rss` | `v2.feedrss.support.index` | global → `web` → `module.active:FeedRss` → `CmsAuthenticate` → `V2PermissionMiddleware` → guarda `isSupportUser()` | `FeedRssSupportController@index` | RBAC `FeedRss` `read`\|`write` **e** identidade de suporte/super-admin | `200` HTML; `302` dashboard + `Acesso restrito ao suporte.`; `404 Modulo desativado.`; `401 Unauthorized.` / `302 /cms/login`; `403 Forbidden. No administrative roles assigned.`; `302` dashboard/back por RBAC |
| R2 | `POST` | `/cms/v2/feed-rss/{id}/toggle` | `v2.feedrss.support.toggle` | idem R1 + CSRF | `@toggle` | idem | `302` painel + `Item "<título>" habilitado com sucesso!` \| `Item "<título>" desabilitado com sucesso!`; `302` + `Item não encontrado.`; `302` dashboard + `Acesso restrito ao suporte.` |
| R3 | `DELETE` (`POST` + `_method=DELETE`) | `/cms/v2/feed-rss/{id}` | `v2.feedrss.support.destroy` | idem R1 + CSRF + method spoofing | `@destroy` | idem | `302` painel + `Item removido do feed com sucesso!`; `302` + `Item não encontrado.`; `302` dashboard + `Acesso restrito ao suporte.` |

Fonte: `spec §5`, `_indices/rotas.csv` (FEEDRSS-R01..R03), `_indices/parcial-MOD-INFRAESTRUTURA-FEEDRSS/rotas.csv`.
O arquivo de rotas declara `prefix => 'cms/v2/feed-rss'` e aplica `CmsAuthenticate`; o
prefixo **não** é derivado pelo provider (`roteamento §2.2`, `roteamento §3.4 item 4`).

### 3.2 Rota de serviço consumida, não criada (1)

| Método | URI | Nome | Middleware | Controller@ação | Permissão | Resposta |
|---|---|---|---|---|---|---|
| `GET` / `HEAD` | `/feed.xml` | `v2.feedrss.feed` | global → `web` → `MaintenanceMiddleware` → `page.cache` → `public.cache` | `V2\Services\FeedRSS\Controllers\FeedRssController@feed` | pública | `200` `application/rss+xml; charset=utf-8`, até `config('v2.feed_rss.limit')` (default 50) itens `enabled`; placeholder quando vazio; `503` em manutenção sem sessão; `X-Cache-Status: HIT\|MISS`; `Cache-Control: public, max-age=60, must-revalidate` |

Fonte: `svc §5`, `cache §1.1`, `_indices/rotas.csv` (SVC-FEEDRSS-R01). O prefixo de
serviço é `/` e a rota é registrada **depois** do fallback; ela não é capturada por
`GET /{slug}` porque o fallback restringe `slug` a `[a-z0-9-]+` e `/feed.xml` contém ponto
(`roteamento §2.2`, `roteamento §2.3`).

### 3.3 Cadeia de autorização (implementar exatamente nesta ordem)

```text
1. global: CheckForMaintenanceMode, CspMiddleware, ValidatePostSize, TrimStrings,
   ConvertEmptyStringsToNull, HtmlMinify, TrustProxies
2. web: EncryptCookies, AddQueuedCookiesToResponse, StartSession, CmsAiToken,
   ShareErrorsFromSession, VerifyCsrfToken (POST/DELETE), SubstituteBindings
3. module.active:FeedRss  ← flag; sem linha em v2_module_flags = ativo
4. CmsAuthenticate        ← sessão Sentinel + >= 1 role; delega ao RBAC
5. V2PermissionMiddleware ← casa cms/v2/feed-rss*; none/read/write; bypass suporte/super-admin
6. FeedRssSupportController::isSupportUser() ← e-mail config ou inRole('super-admin')
```

Fonte: `middlewares §1.2`, `auth §1.2`, `spec §2.2`. Consequências:

- **Anônimo** em módulo ativo: `401 Unauthorized.` (AJAX) ou `302 /cms/login` — antes
  do controller (`auth §1.3`).
- **Autenticado sem role**: `403 Forbidden. No administrative roles assigned.`
- **RBAC `none`/chave ausente**: JSON `403 Forbidden. Access denied for this module.`;
  navegação `302` dashboard + `Acesso negado. Seu perfil não possui permissão para acessar o módulo: <título>` (`auth §1.2`).
- **RBAC `read` em POST/DELETE**: JSON `403 Forbidden. Write actions are not allowed for your profile.`;
  navegação `302 back()` + `Operação negada. Seu perfil possui apenas permissão de leitura para o módulo: <título>` (`auth §1.2`).
- **E-mail de suporte** e **`super-admin`**: bypass do RBAC (`auth §2.5`); ainda assim o
  controller reavalia a identidade. `super-admin` precisa existir na tabela Sentinel
  `roles`; o seed padrão não o cria (`spec §13 LG-03`).
- **Módulo desligado**: como o prefixo real é composto (`cms/v2/feed-rss`), o
  `ModuleAccessMiddleware` cai no ramo `abort(404, 'Modulo desativado.')` (ou JSON
  `{"error":"Modulo desativado: FeedRss","code":404}` para AJAX/JSON). O redirect só
  ocorre quando `getPrefix() === 'cms'` exato (`middlewares §1.3`, `08_matriz/flags-e-navegacao.md §3.4`).
  Há **contradição documental** (ver BL-01).

### 3.4 Matriz de respostas por entrada

| Entrada | Resultado |
|---|---|
| `GET` como suporte/super-admin | `200`, 20 itens |
| `GET` como outro usuário autenticado e RBAC permitido | `302 v2.cms.dashboard` + `Acesso restrito ao suporte.` |
| `GET` com RBAC `none` | `302` dashboard + mensagem do RBAC (não chega ao controller) |
| `GET` anônimo | `401`/`302` login |
| `GET` com flag `FeedRss=false` | `404 Modulo desativado.` |
| `POST` `enabled=1` | `302` painel + habilitado |
| `POST` `enabled=0` | `302` + desabilitado |
| `POST` `enabled=true`, `on`, `yes` ou campo ausente | `302` + **desabilitado** (contrato literal `'1'`) |
| `POST` id inexistente ou soft-deleted | `302` + `Item não encontrado.` |
| `DELETE` existente | `302` + `Item removido do feed com sucesso!` |
| `DELETE` inexistente | `302` + `Item não encontrado.` |
| Qualquer ação de não-suporte | `302` dashboard + `Acesso restrito ao suporte.` |
| `POST`/`DELETE` sem `_token` | `419` (framework; ver BL-05) |
| `{id}` não-UUID | comportamento não documentado (ver BL-04) |
| `/feed.xml` com itens | `200` RSS 2.0 |
| `/feed.xml` sem itens | `200` + 1 `<item>` placeholder |
| `/feed.xml` segunda vez < 5 min | `200` + `X-Cache-Status: HIT` (corpo antigo) |
| `/feed.xml` com Redis fora | `200` normal, sem `X-Cache-Status`, `Log::warning` |

Fonte: `spec §1.3`, `svc §1.3`, `cache §1.3`.

---

## 4. Telas CMS

### 4.1 `v2-feedrss::cms.index` — “Feed RSS”

| Item | Valor |
|---|---|
| Rota | `GET /cms/v2/feed-rss` (`v2.feedrss.support.index`) |
| Controller | `FeedRssSupportController@index` |
| View | `v2-feedrss::cms.index` (`V2/Modules/Infraestrutura/FeedRss/Views/cms/index.blade.php`) |
| Layout | `v2-cms::layouts.app` |
| Breadcrumb | “Infraestrutura › Feed RSS” |
| Dados injetados | `$stats` = `total`, `enabled`, `disabled`, `by_module`, `last_item`, `last_date`, `updated_at`; `$items` = `LengthAwarePaginator` |
| Ações | Habilitar/Desabilitar (POST), Remover (POST + `_method=DELETE`) |
| JS | nenhum `@push('scripts')`, nenhum AJAX; só `confirm()` nativo no delete |
| Cache | nenhum: `/cms*` está fora do `page.cache` e do `public.cache` (`cache §1.2`) |

Fonte: `spec §8.1`, `_indices/componentes.csv` (FEEDRSS-C01).

### 4.2 Estrutura da página (ordem observável)

1. **Cabeçalho / breadcrumb** — “Infraestrutura › Feed RSS”.
2. **Linha de 4 cartões**:
   | Cartão | Conteúdo |
   |---|---|
   | “Total de Itens” | `$stats['total']` |
   | “Habilitados” | `$stats['enabled']` |
   | “Desabilitados” | `$stats['disabled']` |
   | “Feed Público” | link absoluto `url('/feed.xml')`, abre em nova aba |
3. **Distribuição por módulo** — para cada `by_module` (`module => total`), badge com
   `ucfirst($module)` e a contagem; inclui desabilitados; exclui soft-deleted.
4. **Cabeçalho da lista** — exibe a data do item mais recente (`$stats['last_date']`).
5. **Tabela paginada** (`aria-label="Feeds RSS"`, `scope="col"` nos cabeçalhos):
   | Coluna | Renderização |
   |---|---|
   | Título | link para `$item->link`, nova aba, texto `str_limit($item->title, 50)` |
   | Módulo | badge com `ucfirst($item->module)` |
   | Tipo | `$item->entity_type` cru |
   | Publicado em | data formatada; `—` quando `published_at` é nulo |
   | Status | indicador de `enabled` (texto exato não documentado — BL-03) |
   | Ações | dois formulários (toggle e delete) |
6. **Paginação** — `$items->links()` quando o paginador tem páginas; 20 por página.
7. **Rodapé** — “Última atualização: `d/m/Y H:i:s`” com `$stats['updated_at']`
   (hora do request, **não** a data de alteração do feed).

### 4.3 Formulários e contrato de UI

**Toggle** (`spec §2.3`, `spec §4.2`):

```html
<form action="{{ route('v2.feedrss.support.toggle', ['id' => $item->id]) }}" method="POST" class="d-inline">
    {{ csrf_field() }}
    <input type="hidden" name="enabled" value="{{ $item->enabled ? '0' : '1' }}">
    <button type="submit"
            class="btn btn-sm btn-outline-{{ $item->enabled ? 'warning' : 'success' }}"
            title="{{ $item->enabled ? 'Desabilitar' : 'Habilitar' }}">
        <i class="bi {{ $item->enabled ? 'bi-pause-circle' : 'bi-play-circle' }}"></i>
    </button>
</form>
```

- Sem `confirm()` no toggle.
- O valor enviado é sempre o **inverso** do estado atual.
- `name="enabled"` é o único campo; `_token` é CSRF.

**Delete** (`spec §2.3`, `spec §4.2`):

```html
<form action="{{ route('v2.feedrss.support.destroy', ['id' => $item->id]) }}" method="POST"
      class="d-inline" onsubmit="return confirm('Remover este item do feed RSS?');">
    {{ csrf_field() }}
    {{ method_field('DELETE') }}
    <button type="submit" class="btn btn-sm btn-outline-danger" title="Remover">
        <i class="bi bi-trash"></i>
    </button>
</form>
```

- Texto de confirmação exato: `Remover este item do feed RSS?`.
- `_method=DELETE` é obrigatório porque HTML não envia DELETE.

### 4.4 Estados da tela

| Estado | Gatilho | Renderização / mensagem exata |
|---|---|---|
| Inicial vazio | `total = 0` | “Nenhum item no feed RSS.” + “Os itens aparecerão automaticamente quando conteúdo for publicado.” (`spec §8.1`) |
| Normal | há itens | 4 cartões + distribuição + tabela |
| Acesso negado | não-suporte em qualquer action | `302` ao dashboard + flash `Acesso restrito ao suporte.` |
| Item inexistente | toggle/delete com id ausente ou soft-deleted | `302` ao painel + flash `Item não encontrado.` |
| Toggle sucesso | `enabled=1` | `302` + `Item "<título>" habilitado com sucesso!` |
| Toggle sucesso | qualquer outro valor | `302` + `Item "<título>" desabilitado com sucesso!` |
| Delete sucesso | item existente | `302` + `Item removido do feed com sucesso!` |
| Módulo desligado | flag `FeedRss=false` | `404 Modulo desativado.` (ver BL-01) |
| RBAC negado | perfil sem permissão | mensagens de `auth §1.2` (não as do módulo) |

Mensagens de sucesso interpolam `$item->title` **cru** (aspas aparecem literalmente); o
escape HTML ocorre só na renderização do flash pelo layout (`spec §3.3`).

### 4.5 Validações

- **Não existe `FormRequest`, `$request->validate()` nem validação de `enabled`** nesta
  unidade; o contrato é a comparação literal `$request->input('enabled') === '1'`
  (`spec §2.3`, `spec §3.2 RN-02`).
- `{id}` não tem `where()`/constraint na rota; qualquer string chega ao controller
  (`roteamento §3.3`, análogo ao caso `conexao-tags`).
- CSRF é obrigatório em POST/DELETE (grupo `web`); method spoofing é aceito.

### 4.6 Acessibilidade e DOM

- `aria-label="Feeds RSS"` na tabela; `scope="col"`; `title` nos botões de ícone.
- Não há `aria-live` próprio para o flash (o layout do CMS trata) (`spec §4.2`).
- `names`/DOM relevantes: `enabled`, `_method`, `_token`, links de paginação.
- Sem `data-*`, sem IDs próprios documentados (BL-03).

---

## 5. Regras de negócio e fluxos

### 5.1 Regras

| ID | Regra | Efeito observável | Fonte |
|---|---|---|---|
| R-01 | Só o e-mail de `config('v2.support.email')` ou um usuário Sentinel `inRole('super-admin')` usa o painel | demais recebem `302` + flash, não `403` | `spec §3.2 RN-01`, `spec §2.3` |
| R-02 | A guarda é reavaliada em **toda** action (`index`, `toggle`, `destroy`) | não há bypass por link direto | `spec §1.3` |
| R-03 | O RBAC (`FeedRss`, pattern `cms/v2/feed-rss*`) é condição necessária para perfis comuns, insuficiente para acesso | perfil `write` não-suporte continua bloqueado | `spec §10.2`, `_indices/permissoes.csv` |
| R-04 | O grupo/flag é avaliado antes da autenticação | anônimo em módulo desligado recebe 404, não 401 | `middlewares §1.2` |
| R-05 | `enabled` só vira `true` quando o POST traz exatamente a string `'1'` | `true`, `on`, `yes`, `2`, ausente → `false` | `spec §3.2 RN-02` |
| R-06 | O toggle localiza por PK com global scope de soft delete | item removido responde “não encontrado”, não é reabilitado | `spec §3.2 RN-03` |
| R-07 | Delete é **soft delete** (`deleted_at`), nunca remoção física | linha permanece; some do painel e do feed | `spec §3.2 RN-04` |
| R-08 | O painel lista habilitados **e** desabilitados, sem busca/filtro | localizar item exige paginar | `spec §3.2 RN-05` |
| R-09 | A distribuição por módulo inclui desabilitados | “Itens por Módulo” ≠ “itens no feed” | `spec §3.2 RN-06` |
| R-10 | Toggle/delete **não** invalidam `page:*` | `/feed.xml` pode ficar até 5 min desatualizado (Redis) + 60 s no navegador | `spec §3.2 RN-07`, `svc §1.4` |
| R-11 | O feed público é único, consolidado, sem filtro por módulo ou query string | `module` só serve ao painel/estatísticas | `spec §3.2 RN-08` |
| R-12 | O módulo não cria itens | tabela só é escrita por `FeedSender`/serviço | `spec §3.2 RN-09` |
| R-13 | O cartão “Feed Público” usa `url('/feed.xml')` absoluto, não `route()` | independe do nome da rota do serviço | `spec §3.2 RN-10` |
| R-14 | Toda escrita do painel é auditada com `properties.type='feed_rss_support'` | 1 `INSERT` por toggle/delete | `spec §11`, `svc §8` |
| R-15 | O UPSERT do serviço grava `enabled=true` | desabilitar pelo suporte não é permanente: o próximo `saved` do conteúdo reabilita | `svc §3.2 RN-02`, `spec §3.3` |
| R-16 | A identidade do item é `(entity_type, entity_id)`, sem `module` e sem filtro de `deleted_at` | item removido não pode ser reinserido (unique violation) | `svc §3.2 RN-01/RN-03` |
| R-17 | A ordem da tabela é `published_at DESC` sem desempate; no PostgreSQL `NULLS FIRST` | itens sem data aparecem no topo; ordem entre iguais é indefinida | `spec §2.4`, `svc §3.2 RN-06` |
| R-18 | `updated_at` exibido é `Carbon::now()` do request | rótulo “Última atualização” não representa alteração do feed | `spec §3.5`, `spec §8.1` |
| R-19 | Não há validação de `enabled` nem de `{id}` | payload arbitrário não gera erro de validação; só o efeito binário | `spec §2.3` |
| R-20 | A unidade não grava no conteúdo de origem | notícia/evento/vaga intactos | `spec §1.2` |

### 5.2 Fluxos

**FL-01 — Abrir o painel (`index`).**
1. Request passa por global → `web` → `module.active:FeedRss` → `CmsAuthenticate`/RBAC.
2. Controller executa `isSupportUser()`; falso → `flash('Acesso restrito ao suporte.')->error()` + `redirect()->route('v2.cms.dashboard')`.
3. Verdadeiro → `FeedRssService::getStats()` (5 consultas).
4. `FeedRssService::getAllItemsPaginated(20)` (`recent()->paginate(20)`).
5. Renderiza `v2-feedrss::cms.index` com `stats` + `items`.
6. Rodapé usa `stats['updated_at']` (agora).

**FL-02 — Habilitar/desabilitar (`toggle`).**
1. Mesma cadeia de R-01..R-04; CSRF validado.
2. `$enabled = ($request->input('enabled') === '1')`.
3. `toggleItem($id, $enabled)`: `FeedRssItem::find($id)` (exclui soft-deleted).
4. Não encontrado → `null`, flash `Item não encontrado.`, redirect ao painel.
5. Encontrado → `enabled = $enabled`, `save()`.
6. Audita `feed_rss_item_enabled` ou `feed_rss_item_disabled` com `type='feed_rss_support'`.
7. Flash `Item "<title>" habilitado|desabilitado com sucesso!` + redirect `v2.feedrss.support.index`.
8. Nenhum `Cache::forget`; o feed muda só após o TTL.

**FL-03 — Remover (`destroy`).**
1. Mesma cadeia; CSRF + `_method=DELETE`.
2. `deleteItem($id)`: localiza; ausente → `false`.
3. Presente → soft delete (`deleted_at = now()`).
4. Audita `feed_rss_item_deleted` (sujeito = item).
5. `true` → flash `Item removido do feed com sucesso!`; `false` → `Item não encontrado.`.
6. Redirect ao painel; sem purge, sem invalidação de cache.

**FL-04 — Publicação/despublicação pela origem (fora do módulo, mas afeta o painel).**
1. `News`/`Evento`/`JobOpening` no hook `saved` decidem publicar ou remover (`svc §2.2`).
2. Publicar → `FeedSender::forModule($m)->pushFromArray([...])` → `addItem` → `updateOrCreate(entity_type, entity_id)` com `enabled=true`.
3. Despublicar → `FeedSender::remove(...)` → `removeItem` → soft delete.
4. Ambos rodam em `try/catch` que apenas loga (`svc §3.2 RN-09`).
5. Efeito no painel: novo item no topo (se `published_at` recente) ou desaparece.

**FL-05 — Leitura pública (`/feed.xml`, serviço).**
1. `page.cache` tenta `page:` + `sha1(fullUrl)` no Redis (5 min); HIT devolve corpo antigo + `X-Cache-Status: HIT`.
2. MISS → `FeedRssController@feed` lê `config('v2.feed_rss.limit')` e chama `generateXml`.
3. `getFeedItems`: `enabled().recent().take($limit)`.
4. Resolve `titulo_site` via `PortalConfigsService` (catch silencioso → `config('app.name')`).
5. Renderiza XML; `public.cache` adiciona `Cache-Control` público.
6. Estado vazio → 1 `<item>` com título `Nenhum conteúdo disponível no momento`.

**FL-06 — Máquina de estados do item.**

```text
[*] --addItem (enabled=true)--> Habilitado
Habilitado --toggle enabled!=1--> Desabilitado
Desabilitado --toggle enabled=1--> Habilitado
Desabilitado --novo addItem--> Habilitado        (R-15)
Habilitado|Desabilitado --delete/remove--> Removido (soft)
Removido --addItem do mesmo par--> QueryException (R-16)
Removido --> [*]                                 (sem restore exposto)
```

**FL-07 — Negação de acesso.**
1. Sem sessão → `401`/`302` login (antes do controller).
2. Sessão sem role → `403 Forbidden. No administrative roles assigned.`
3. Role com RBAC `none` → dashboard/403 do RBAC.
4. Role com RBAC `read` em escrita → back/403 do RBAC.
5. RBAC ok mas identidade ≠ suporte/super-admin → dashboard + `Acesso restrito ao suporte.`
6. Flag desligada → `404` (ver BL-01).

### 5.3 Critérios de aceite derivados (PC-FEEDRSS-01..11 do `spec §14`)

| PC | Verificação objetiva |
|---|---|
| 01 | Não-suporte → `302` dashboard + flash literal |
| 02 | 4 cartões com total/habilitados/desabilitados + URL `/feed.xml` |
| 03 | Tabela mistura habilitados/desabilitados, 20 por página |
| 04 | `enabled=1` → habilita + auditoria `feed_rss_item_enabled` |
| 05 | valor ≠ `1` → desabilita |
| 06 | delete → `deleted_at` + auditoria `feed_rss_item_deleted` |
| 07 | item desabilitado some do `/feed.xml` após o cache |
| 08 | não existe rota de criação/edição |
| 09 | `/feed.xml` público, consolidado, limite 50 |
| 10 | flag desligada bloqueia as 3 rotas (404 conforme `middlewares`) |
| 11 | item removido não pode ser republicado (unique violation) |

---

## 6. Arquivos a criar

### 6.1 Unidade `MOD-INFRAESTRUTURA-FEEDRSS` (4 + testes)

| Path | Responsabilidade | Conteúdo mínimo |
|---|---|---|
| `V2/Modules/Infraestrutura/FeedRss/ModuleProvider.php` | Registro do módulo: namespace de views, grupo, config de sidebar/permissão | `title='Feed RSS'`, `description`, `icon='bi bi-rss'`, `position=91`, `active_pattern=['cms/v2/feed-rss*']`, `route='v2.feedrss.support.index'`, `restricted=config('v2.support.email')`, `subModulesLevel1=[['title'=>'Monitorar Feed','icon'=>'bi bi-rss','route'=>'v2.feedrss.support.index','position'=>1]]`; grupo `Infraestrutura`; registrar `v2-feedrss` (`spec §10.4`) |
| `.../Controllers/FeedRssSupportController.php` | Guarda de identidade + 3 actions; delega toda a dados ao serviço | construtor com `FeedRssService` (e `SEOService`, conforme `spec`/BL-06); `isSupportUser()`; `index`, `toggle`, `destroy`; nenhuma validação além do literal `'1'` |
| `.../Routes/cms.php` | 3 rotas nomeadas sob `prefix => 'cms/v2/feed-rss'` com `CmsAuthenticate` | `GET /` → `v2.feedrss.support.index`; `POST /{id}/toggle` → `...toggle`; `DELETE /{id}` → `...destroy` (`spec §5`) |
| `.../Views/cms/index.blade.php` | Tela única de monitoramento | layout `v2-cms::layouts.app`; breadcrumb; 4 cartões; distribuição; tabela; paginação; rodapé; 2 formulários (`spec §4.2`, `spec §8.1`) |
| `tests/Feature/FeedRss/SupportPanelTest.php` | Contrato de acesso e leitura | matriz de autorização (anônimo/role/RBAC/identidade/flag), `200`, 4 cartões, 20/página, estado vazio, ordem `published_at DESC` |
| `tests/Feature/FeedRss/ToggleItemTest.php` | Toggle e auditoria | `'1'` habilita; `'0'`/`'true'`/ausente desabilita; inexistente → mensagem; 1 linha `activity_log` com `type=feed_rss_support`; item volta ao feed após novo push |
| `tests/Feature/FeedRss/DeleteItemTest.php` | Soft delete e auditoria | `deleted_at` preenchido; linha permanece; some do painel; mensagem; auditoria; republicação falha (ou decisão de BL-11) |
| `tests/Feature/FeedRss/CacheStalenessTest.php` | Efeito de cache | após toggle, `/feed.xml` ainda serve HIT até o TTL; sem invalidar `page:*` |
| `tests/Unit/FeedRss/PanelStatsTest.php` | Estatísticas | `by_module` inclui desabilitados; exclui soft-deleted; `last_date` do item mais recente; `updated_at` = agora |

**Não criar nesta unidade:** migrations, seeds, `config/*`, rotas `web.php`/`api.php`,
models, DTOs, componentes JS, e-mail ou views de portal.

### 6.2 Contratos consumidos do `SVC-FEEDRSS` (não criar aqui)

| Símbolo | Uso pelo módulo |
|---|---|
| `FeedRssService::getStats(): array` | 7 chaves (`total`, `enabled`, `disabled`, `by_module`, `last_item`, `last_date`, `updated_at`) |
| `FeedRssService::getAllItemsPaginated($perPage = 20): LengthAwarePaginator` | tabela do painel |
| `FeedRssService::toggleItem($id, bool $enabled): ?FeedRssItem` | toggle + auditoria |
| `FeedRssService::deleteItem($id): bool` | soft delete + auditoria |
| `FeedRssItem` | model com `SoftDeletes`, UUID, casts, escopos `enabled/recent` |
| `v2_feed_rss_items` | tabela (DDL em §2.2) |
| `FeedRssController@feed` + rota `v2.feedrss.feed` | alvo do cartão “Feed Público” |

### 6.3 Esqueleto de implementação (contrato, não código final)

```php
class FeedRssSupportController extends Controller
{
    public function __construct(FeedRssService $feedRssService /*, SEOService $seo */) {}

    protected function isSupportUser(): bool
    {
        $user = Sentinel::getUser();
        if (!$user) return false;
        $email = config('v2.support.email');
        return $user->inRole('super-admin')
            || (!empty($email) && $user->email === $email);
    }

    public function index(Request $request)          // 200 view | 302 dashboard
    public function toggle(Request $request, $id)    // 302 index | 302 dashboard
    public function destroy($id)                     // 302 index | 302 dashboard
}
```

```php
// Routes/cms.php
Route::group(['prefix' => 'cms/v2/feed-rss',
              'middleware' => [\V2\Core\Middleware\CmsAuthenticate::class]], function () {
    Route::get('/', 'FeedRssSupportController@index')->name('v2.feedrss.support.index');
    Route::post('/{id}/toggle', 'FeedRssSupportController@toggle')->name('v2.feedrss.support.toggle');
    Route::delete('/{id}', 'FeedRssSupportController@destroy')->name('v2.feedrss.support.destroy');
});
```

Ordem de implementação: (1) dependências do serviço; (2) `ModuleProvider`; (3) rotas;
(4) controller; (5) blade; (6) testes de contrato; (7) conferência dos PCs de `spec §14`.

---

## 7. Lacunas (`BL-*`)

> Classes conforme `09_reimplementacao/reconstrucoes-cegas.md §P5`: **A** = dado ausente,
> **B** = ambiguidade, **D** = doc × código, **E** = índice inconsistente. Severidade
> S1 (schema/rota/status/segurança), S2 (tela/validação/mensagem/integração), S3 (cosmético).

| ID | Classe/Severidade | Lacuna | O que o corpus diz | Impacto na reimplementação |
|---|---|---|---|---|
| `BL-FEEDRSS-01` | D / S1 | Resposta quando a flag `FeedRss` está desligada | `spec §1.3` promete `302` dashboard + `Módulo "FeedRss" está desativado...`; `middlewares §1.3` e `08_matriz/flags-e-navegacao.md §3.4` dizem `abort(404, 'Modulo desativado.')` para prefixo composto; `_indices/rotas.csv` registra “404 módulo desativado” | Escolher 404 (duas fontes transversais + índice) e registrar a divergência; se o gabarito exigir 302, o contrato HTTP muda |
| `BL-FEEDRSS-02` | B / S1 | Interação RBAC × identidade de suporte | `spec §10.2` diz que `FeedRss=none` bloqueia no middleware; `spec §3.5` e `auth §2.5` dizem que o e-mail de suporte tem bypass total do RBAC; não está claro se o bypass ocorre antes do match de `active_pattern` | Define se um suporte com `FeedRss=none` vê o painel. Implementação proposta: bypass primeiro (acesso liberado), mas exige confirmação |
| `BL-FEEDRSS-03` | A / S2 | Textos/formatos da tela | Não há o texto do badge de status, o formato da data de `published_at`/`last_date`, subtítulos dos cartões, atributos `target`/`rel` do link do título, nem IDs DOM próprios | Tela pode divergir em S2; `str_limit(…,50)` e o texto de confirmação estão documentados, o resto não |
| `BL-FEEDRSS-04` | A / S1 | `{id}` que não é UUID | Rotas sem `where()`; serviço faz `find($id)`; nada descreve o erro do PostgreSQL (`invalid input syntax for type uuid`) | Pode gerar `500` em vez de “Item não encontrado.”; decidir validação/`whereUuid` |
| `BL-FEEDRSS-05` | A / S2 | Falha de CSRF (419) e renderização do flash | CSRF é obrigatório (`web`), mas status/corpo/mensagem de 419 não são documentados para esta unidade | Teste de contrato pode falhar por detalhe de framework |
| `BL-FEEDRSS-06` | A / S2 | Uso de `SEOService` no controller do módulo | `spec §0` diz que o controller injeta `FeedRssService` e `SEOService`; nenhum método do `SEOService` é citado no módulo (no serviço, `svc §3.5` afirma injeção não usada) | Pode ser dependência morta; não criar uso inventado |
| `BL-FEEDRSS-07` | B / S2 | Chave e alvo de invalidação do cache de página | `spec §3.4` sugere `page:sha1(url('/feed.xml'))`; `cache §1.1` diz `page:` + `sha1(fullUrl)`; `cache §0` fala `laravelpage:<sha1>` e `laravel:page:*` no comando; nenhum caminho canônico de invalidação existe | Se a reimplementação decidir invalidar no painel, o alvo é indeterminado a partir do corpus |
| `BL-FEEDRSS-08` | A / S2 | Campos fixos do RSS | Não há o texto de `channel/description`, `generator`, `language`, origem/tamanho do `logoUrl`, nem o comportamento de `published_at = null` na view (`toRssString()` em null) | Afeta o contrato do serviço (fora da unidade), mas o painel depende dele; `svc §3.3` admite `[I]` para o caso nulo |
| `BL-FEEDRSS-09` | B / S2 | Grafia da chave de flag | `module.active:FeedRss` (PascalCase) versus chaves lowercase em `v2_module_flags`; `flags §3.5` mostra duplicação de chaves e não define case-sensitivity | Um `UPDATE`/seed com grafia errada pode não desligar o módulo |
| `BL-FEEDRSS-10` | B / S2 | `deleteItem` em item já soft-deleted | `spec §3.2 RN-03` cobre o toggle (`find` exclui); para `deleteItem` só se afirma “localiza por id” e retorno `bool` | Pode retornar `false` (“não encontrado”) ou `true` idempotente; as mensagens mudam |
| `BL-FEEDRSS-11` | A / S1 | Política para republicação após remoção | `svc §3.4 item 1` e `LG-FEEDRSS-01` dizem que a decisão (reproduzir a unique violation ou corrigir com unique parcial/delete físico) **não está documentada** | Muda schema (unique parcial), comportamento do serviço e o PC-11; exige decisão explícita |
| `BL-FEEDRSS-12` | A / S3 | Ordem dos badges de módulo | `groupBy('module')->pluck()` sem `orderBy`; nenhum documento fixa a ordem de exibição | Saída visual não determinística entre bancos |
| `BL-FEEDRSS-13` | B / S3 | Vínculo da auditoria de exclusão | `spec §11` diz sujeito = item; `svc §8` diz que “a remoção usa sujeito vazio” (referindo-se a `removeItem`); não há exemplo para `deleteItem` | Timeline do item pode ou não mostrar a exclusão |
| `BL-FEEDRSS-14` | A / S2 | Atomicidade de escrita + auditoria | `svc §9` admite `[I]` para falha do logger; nenhum doc diz se toggle/delete usam transação | Falha de auditoria pode deixar item alterado sem log (ou lançar após persistir) |
| `BL-FEEDRSS-15` | A / S3 | Paginação fora de faixa | Nenhum documento descreve `?page=999` (tabela vazia? redirect?) | Estado de erro/limite não determinável |
| `BL-FEEDRSS-16` | A / S2 | Cobertura de testes | `spec §13 LG-01`: não há testes do painel/toggle/delete no legado | Não há gabarito de regressão; os testes de §6.1 são derivados do doc |
| `BL-FEEDRSS-17` | A / S3 | Cabeçalhos HTTP do painel | `cache §1.2` garante que `/cms` não é cacheado, mas não fixa `Cache-Control`/`X-Cache-Status` do painel | Divergência cosmética de headers |
| `BL-FEEDRSS-18` | A / S2 | `v2.support.email` vazio/nulo | `isSupportUser` exige `!empty`; `SidebarComposer` usa `isset($config['restricted'])`; não há contrato para string vazia | Pode esconder o módulo de todos ou expor com `restricted` vazio; ver BL-02 |

### 7.1 Itens que **não** são lacuna (documentados o suficiente)

- Mensagens exatas de sucesso/erro do módulo (`spec §1.3`).
- Efeito do toggle/delete em dados e auditoria (`spec §1.2`, `svc §8`).
- Literalidade do `enabled === '1'` (`spec §2.3`, `spec §3.2 RN-02`).
- Separação módulo × serviço e ausência de tabela própria (`spec §0`, `spec §6`).
- Efeito do cache de página e do `Cache-Control` no `/feed.xml` (`cache §1.1`).
- Middleware e ordem de avaliação das rotas (`middlewares §1.2`, `auth §1.2`).
