# Módulo Painel (`Suporte/Painel`)

> Contrato de leitura do módulo (ADR-004/ADR-014) — mantenha sincronizado com o código.

## Propósito

Entregar a **área de suporte** no painel: visão geral (P3.S1), **ativação/desativação de módulos**
(P3.S2) com motivo/auditoria/invalidação imediata, **diagnóstico somente leitura** (P3.S3), que reusa
a verificação do `core:diagnose`, o **leitor da trilha de auditoria** (P3.S4), imutável, filtrado e
paginado por keyset, o **modo de manutenção** (P3.S5) com settings tipados, middleware por área
(portal/API/CMS) e página 503 dedicada, e o **contato do fornecedor + versão** (P3.S6), com estado
vazio explicativo e links de navegação (sem iframe/mapa de terceiro).

- **Grupo:** `Suporte` — grupo reservado: **invisível** na navegação e **404** nas rotas para quem
  não é a equipe (`suporte@maxima.inf.br`), inclusive com o perfil de maior privilégio (ADR-033).
- **Área:** CMS (domínio próprio, ADR-037), URI `/suporte`.
- **Ativação:** dado em `core_module_discovery_settings`, gravado pela Action **única** do Core
  `SetModuleActivation` (mesma usada por `core:module:activate`). O grupo `Suporte` nunca é
  desativável (nem com `--force`) e essenciais (`config('core.essential_modules')`) só mudam pelo
  CLI com `--force` (emenda ADR-034).

## Estrutura

```
src/modules/Suporte/Painel/
├── ModuleProvider.php
├── MODULE.md
├── Logic/
│   ├── Actions/{PainelOverviewAction,PainelModulesAction,PainelDiagnosticAction,
│   │              PainelAuditAction,PainelAuditShowAction,PainelContactAction}.php
│   ├── Controllers/Cms/{PainelIndexController,PainelDiagnosticController,
│   │                    PainelModulesIndexController,PainelModulesActivateController,
│   │                    PainelModulesDeactivateController,PainelAuditIndexController,
│   │                    PainelAuditShowController,PainelMaintenanceIndexController,
│   │                    PainelMaintenanceUpdateController,PainelContactController}.php
│   ├── DTOs/{PainelOverview,PainelModule,PainelDiagnosis,PainelDiagnosisGroup,
│   │          PainelAuditPage,PainelAuditRow,PainelAuditEntry,PainelContact}.php
│   ├── Requests/{PainelModuleActivationRequest,PainelAuditFilterRequest,
│   │              PainelMaintenanceRequest}.php
│   └── Support/PainelActivationFailure.php
├── Routes/cms.php
├── Layout/Views/cms/{index,painel-modules,painel-diagnostico,
│                    painel-auditoria,painel-auditoria-show,painel-manutencao,
│                    painel-contato}.blade.php
├── Lang/{pt_BR,en,es,fr}/{index,modules,diagnostics,audit,maintenance,contact}.php
└── Test/{PainelCmsTest,PainelModulesCmsTest,PainelDiagnosticsCmsTest,PainelAuditCmsTest,
          PainelMaintenanceCmsTest,PainelContactCmsTest}.php
```

## Token canônico

**`painel`** — classes `Painel*`, view `painel::cms.index`, chaves `painel::index.*`, rotas
`painel.cms.*` e log/span `painel.{ação}`. Verificação: `tools/name-check.sh --grep painel` e
`Adr004GrepFriendlyTest` na cadeia do `make check`.

## Dependências

- **Core:** `Core\Access\Permission`, `Core\Contracts\Telemetry`,
  `Core\Internal\ModuleDiscovery\ModuleDiscoveryService` (manifesto + ativação — ADR-014/ADR-034),
  `Core\Internal\Diagnostics\{DiagnosticsService,DiagnosticPresenter}` (P3.S3 — mesma verificação
  do `core:diagnose`) e `Core\Internal\Audit\{Services\AuditLogReader,DTOs\AuditFilters}`
  (P3.S4 — leitura keyset da trilha; o módulo não consulta a tabela direto).
  P3.S5 usa `Core\Contracts\Settings` (seção `system`, ADR-046),
  `Core\Internal\Maintenance\MaintenanceState` (leitura tipada) e
  `Core\Internal\Maintenance\Logic\Actions\SetMaintenanceMode` (escrita única, auditada); o
  middleware `maintenance` do Core aplica o estado por área.
  P3.S6 lê a seção `support` pela porta `Core\Contracts\Settings` (ADR-046) e a versão de
  `VERSION` via `Core\Internal\System\SystemVersion` — nenhum caminho local do deploy é exposto.
- **Layout:** shell `<x-layout::cms>` e a primitiva `<x-cms::card>`.
- **Services:** nenhum. **Outros módulos:** nenhum import.
- **Tabelas:** nenhuma própria. O leitor da trilha lê `core.core_audit_logs` **através** do
  `AuditLogReader` do Core (somente leitura; a escrita continua sendo `AuditWriter`).

## Rotas

| Nome | Método | URI | Controller | Acesso |
|---|---|---|---|---|
| `painel.cms.index` | GET | `/suporte` | `Logic\Controllers\Cms\PainelIndexController` | `painel.ver` + equipe de suporte |
| `painel.cms.contact` | GET | `/suporte/contato` | `Logic\Controllers\Cms\PainelContactController` | `painel.ver` + equipe de suporte |
| `painel.cms.diagnostico` | GET | `/suporte/diagnostico` | `Logic\Controllers\Cms\PainelDiagnosticController` | `painel.ver` + equipe de suporte |
| `painel.cms.modules` | GET | `/suporte/modulos` | `Logic\Controllers\Cms\PainelModulesIndexController` | `painel.ver` + equipe de suporte |
| `painel.cms.modules.activate` | POST | `/suporte/modulos/{module}/ativar` | `Logic\Controllers\Cms\PainelModulesActivateController` | `painel.editar` + equipe de suporte |
| `painel.cms.modules.deactivate` | POST | `/suporte/modulos/{module}/desativar` | `Logic\Controllers\Cms\PainelModulesDeactivateController` | `painel.editar` + equipe de suporte |
| `painel.cms.audit` | GET | `/suporte/auditoria` | `Logic\Controllers\Cms\PainelAuditIndexController` | `painel.ver` + equipe de suporte |
| `painel.cms.audit.show` | GET | `/suporte/auditoria/{log}` | `Logic\Controllers\Cms\PainelAuditShowController` | `painel.ver` + equipe de suporte |
| `painel.cms.maintenance` | GET | `/suporte/manutencao` | `Logic\Controllers\Cms\PainelMaintenanceIndexController` | `painel.ver` + equipe de suporte |
| `painel.cms.maintenance.update` | PUT | `/suporte/manutencao` | `Logic\Controllers\Cms\PainelMaintenanceUpdateController` | `painel.editar` + equipe de suporte |

O caminho é relativo ao domínio do painel (`cms.<base>`); o guard `support.group` responde **404**
antes do controller a quem não é da equipe. Sem closure (ADR-008) e nenhuma mutação por `GET`.

O leitor da trilha (`painel.cms.audit*`) é **somente leitura**: não existe rota de edição, exclusão
ou export (export CSV está fora do MVP — ver ficha P3.S4). `{log}` usa `whereUuid`, então UUID
inválido ou inexistente responde **404**. As duas rotas casam `config('audit.sensitive')` e cada
request grava **um** evento de acesso (`painel.view`, `entity_type` = nome da rota) pelo middleware
`audit.access` da área CMS; o controller nunca audita (sem duplicação e sem recursão).

## Permissões (ADR-033)

`painel.{ver|editar}` declaradas em `ModuleProvider::config()` e aplicadas com
`Permission::authorize()` (deny-by-default). O `AccessGate` só resolve essas abilities para a equipe
de suporte — a permissão de perfil não abre o grupo.

## Eventos e observabilidade (ADR-024)

- **Emite:** `painel.overview` (`log_event('painel.overview', …)` + span de mesmo nome),
  `painel.modules` (`log_event('painel.modules', …)` + span), `painel.diagnose`
  (`log_event('painel.diagnose', …)` + span, com as contagens do relatório), `painel.audit`
  (lista da trilha: linhas, eventos distintos e **booleanos** de filtro — o valor do ator é PII e
  não vai para o log) e `painel.audit_entry` (detalhe: só booleanos de payload) — apenas escalares,
  sem PII. O diagnóstico reusa `core.diagnose` do Core; a tela nunca executa verificação própria.
  O contato (P3.S6) emite `painel.contact` com `configured` e a contagem de campos — **nunca** o
  e-mail, telefone, WhatsApp ou endereço.
- **Auditoria da mutação:** a Action do Core grava, de forma explícita,
  `painel.module_activated`/`painel.module_deactivated` com `{module, active, reason, force}` no
  `payload_after`, target `module`, ator snapshot e hash (ADR-035). Não usa `AuditEntry::model()`
  (a tabela de settings não é model auditado). Ler a lista/visão não altera a trilha.
- **Recusa:** essencial/Suporte vira erro de formulário i18n (`422` JSON / volta com erro no HTML),
  nunca 500; módulo inexistente é 404.
- **Diagnóstico:** somente leitura; check com falha vira cartão de erro com **HTTP 200** (o detalhe
  é só a classe sanitizada da exceção) e nunca expõe segredo/DSN/PII. O scheduler aparece como
  **indisponível** de forma explícita (não há heartbeat nativo).
- **Leitor da trilha (P3.S4):** `GET /suporte/auditoria` (`painel.cms.audit`) lista com filtros por
  `event` (exato), `module` (exato, minúsculas), `actor` (id/e-mail exatos ou trecho do nome),
  `from`/`to` (`Y-m-d`, inclusivos) e `per_page` (`1..100`, padrão `25`); paginação por **cursor**
  (`cursor` + `direction=next|prev`) — sem `OFFSET` e sem contagem total. `GET
  /suporte/auditoria/{log}` (`painel.cms.audit.show`) mostra before/after/changes, ator snapshot,
  `request_id`, `trace_id`, `integrity_hash` e `created_at`. Payloads são exibidos como JSON
  **escapado**; não há rota de edição/exclusão (a trilha é imutável por trigger no banco).
- **Manutenção (P3.S5):** `GET/PUT /suporte/manutencao` leem/gravam a seção `system` pela porta
  `Settings` (chaves `maintenance`, `maintenance_message`, `maintenance_retry_after`,
  `maintenance_allow_cms`; compatível com os campos do P0). A escrita é a Action única
  `SetMaintenanceMode`, que valida limites (`0..86400` s, mensagem ≤ 500), preserva `timezone` e
  grava a auditoria explícita `painel.maintenance_changed` (before/after) + span/log de mesmo nome.
  O middleware `maintenance` aplica o estado por área: portal 503 HTML com `Retry-After`/
  `X-Robots-Tag: noindex`; API 503 RFC 7807; CMS acessível quando `allow_cms`, e sem ele só a
  equipe de suporte, login/logout, o próprio toggle e o health-check. Assets/SEO/`/up` passam sem
  tocar o banco; falha ao ler settings é **fail-open** e logada sem segredo.
- **Contato do fornecedor (P3.S6):** `GET /suporte/contato` (somente leitura) lê a seção `support`
  (campos `company`, `site`, `contact_email`, `contact_phone`, `whatsapp`, `address`, `hours`;
  os três últimos existiam desde o P0) e a versão de `VERSION`. Sem fornecedor configurado, mostra
  estado vazio explicativo com o link para `/configuracoes/support` (sem link morto). O endereço é
  texto; site/WhatsApp abrem em nova aba com `rel="noopener"`; telefone/e-mail são `tel:`/`mailto:`.
  Zero `iframe`/mapa/CDN, nenhuma nota de versão inventada e nenhum caminho local do deploy.
- **Leitura auditada:** as duas rotas casam `config('audit.sensitive')` (`painel.cms.audit`,
  `painel.cms.audit.show` + `*audit*`); o middleware `audit.access` grava **um** evento por request
  (`painel.view`, área `cms`, `entity_type` = nome da rota). O evento de acesso é escrito **depois**
  da resposta e o `GlobalAuditSubscriber` ignora `AuditLog`, então não há recursão nem duplicação.
  Divergência registrada: a semântica global do evento é `{module}.view` (`painel.view`), não
  `audit.access` — a via única (middleware) foi preservada.

## Layout (ADR-016)

- `cms/index.blade.php`, `cms/painel-modules.blade.php`, `cms/painel-diagnostico.blade.php`,
  `cms/painel-auditoria.blade.php`, `cms/painel-auditoria-show.blade.php`,
  `cms/painel-manutencao.blade.php` e `cms/painel-contato.blade.php` usam **um único**
  `<x-layout::cms>` e preenchem apenas o
  conteúdo; o título/H1 e a estrutura são do shell. Sem estilo/JS inline; tabelas/listas semânticas
  (`x-cms::data-table` com `caption`/`scope`), confirmação nativa/checkbox explícito e 4 locales no
  namespace `painel`. Os rótulos/estados dos checks vêm do apresentador do Core
  (`src/core/Lang/{locale}/diagnostics.php`), porque a tela e o comando compartilham o texto; a
  página 503 usa `resources/views/errors/maintenance.blade.php` e o texto do Core
  (`src/core/Lang/{locale}/maintenance.php`).
- A paginação por keyset usa o mesmo `<nav>`/rótulos da paginação do projeto
  (`layout::shell.pagination*`, alvos ≥2,75 rem) com `rel=prev|next`; `x-cms::pagination` recebe
  `LengthAwarePaginator` e por isso não se aplica a cursor.
- O motivo é `required max:255` (validado no request **e** na Action) e todo toggle é `POST` +
  CSRF; a UI nunca envia `force`.

## Comandos

```bash
php artisan core:module:activate Noticias --disable --reason="Janela de manutenção"  # delega à Action
php artisan core:module:activate Acesso --disable --force   # essencial só com --force (auditado)
php artisan core:module:activate Painel --disable --force   # RECUSA: grupo Suporte é intocável
php artisan core:diagnose                                   # MESMO serviço da tela; exit 1 se algum check falhar
```

## Convenções

- [x] `declare(strict_types=1)`; DI; sem traits/abstract/generics/`Result`.
- [x] Action única com `Telemetry`; controller fino (autoriza e delega).
- [x] Nenhum segredo/PII em log, auditoria, URL ou fixture.

## Verificação

```bash
make exec cmd='bash tools/name-check.sh --grep painel'
make check
make check E2E=1
```
