# P3 — Área de suporte, ativação de módulos e operação

| Campo | Valor |
|---|---|
| **Fase** | P3 — Área de suporte, ativação de módulos e operação (F7+) |
| **`kd` épico** | `epic_00hpfnpa` |
| **`kd` fatias** | `task_00wfox2k` (S1) · `task_01tfanhh` (S2) · `task_01xi38oe` (S3) · `task_015xs35p` (S4) · `task_017h4cu4` (S5) · `task_01pkdhbr` (S6) · `task_01ged08f` (S7 — fechamento) |
| **Âncoras** | `wiki/implementation-phases/P3-suporte-ativacao.md` §1–§3, §6 · `wiki/implementation-phases/TECNICO.md` §2–§4, §7–§11, §14 · ADR-033, ADR-034, ADR-035, ADR-037, ADR-043, ADR-024, ADR-004, ADR-016, ADR-046 · `wiki/specs/11-acesso-e-ativacao.md` §3, §4.1–§4.2 · `wiki/specs/04-acessibilidade-ux.md` |
| **Status da revisão** | ✅ revisado (S1–S7 + P3.FIX `task_00p702f7` + P3.FIX2 `task_01ky0cum`; revisão paralela e verificação independente final aprovadas em 2026-10-25) |

## S1 — módulo `Suporte/Painel`, menu e 404 (`task_00wfox2k`)

### O que foi implementado

- **Módulo `src/modules/Suporte/Painel`** (grupo `Suporte` vindo da pasta, token `painel`):
  `ModuleProvider` com `permissions [VIEW, EDIT]`, ícone Lucide `lucide-shield-keyhole`,
  `position` 5 e rota `painel.cms.index`.
- **Rota CMS** `GET /suporte` → `painel.cms.index` (sem closure; `/suporte` no domínio do painel,
  como o contrato da fase pede). Nenhuma rota de S2–S6 foi criada, nenhum link morto.
- **Controller/Action**: `PainelIndexController` (final, invocável) autoriza
  `Permission::authorize('Painel', Permission::VIEW)` e delega a `PainelOverviewAction`
  (`final readonly`, DI de `Telemetry` + `ModuleDiscoveryService`) que emite span e
  `log_event('painel.overview', …)` com escalares, sem PII (ADR-024) e devolve o DTO
  `PainelOverview` (total/ativos/inativos do manifesto).
- **View `Layout/Views/cms/index.blade.php`**: exatamente um `<x-layout::cms>`, só o conteúdo
  (introdução + cartões “Módulos” e “Regras desta área”); sem `<h1>`, `<style>`, `style=` ou
  `<script>`.
- **`Lang/{pt_BR,en,es,fr}/index.php`** com paridade de chaves; nenhuma string visível fixa.
- **`PanelMenu`**: `fromManifest()` passa `$user?->isSupport()` a `grouped($includeSupport)` e
  `build()` repete a negação para o grupo `Suporte` (defesa em profundidade, mesma fonte
  `SupportAccess`). `EnsureSupportGroupAccess` não mudou: continua 404 para não-equipe, inclusive
  perfil máximo; o manifesto não vaza o grupo.
- **Testes**: `PainelCmsTest` (200 para a equipe; 404 para perfil máximo; redirecionamento do
  anônimo ao login; nenhuma rota `painel.*` alcançável fora da equipe; 4 locales; contrato de
  layout; contagem do manifesto com span `painel.overview`) e dois casos novos em
  `PanelMenuTest` (grupo só para a equipe; `build()` descarta o grupo para não-suporte).
- **E2E**: `23-suporte-painel.spec.ts` + história `23-suporte-painel.md` (projeto `cms-desktop`),
  índice do `src/tests-e2e/README.md`, da `wiki/specs/04-acessibilidade-ux.md` e a contagem de
  histórias do `AGENTS.md` atualizados.

### Decisão de nomenclatura (importante para S2–S6)

O plano P3 §3/§6 nomeava rota `suporte.cms.*` e permissões `suporte.ver|editar`. Isso **não fecha**
com a Core: `AccessGate::register()` define a ability com `Permission::for(moduleKey, ação)` e o
`moduleKey` é a **pasta** no manifesto; `Adr004GrepFriendlyTest` exige o token da pasta em nomes de
artefato **e** de rota. Renomear a pasta para `Suporte` quebraria o contrato
`src/modules/Suporte/Painel` da fase.

Implementado, então: pasta `Painel`, token `painel` (`painel.cms.*`, `painel.ver|painel.editar`,
i18n `painel::`, span/log `painel.overview`) e **URI `/suporte`** no domínio do painel. Nenhum gate
foi afrouxado. O `SupportGroupAccessTest` da P2 já usava `Painel`/`painel.ver` como fixture.
Registro: `kd decision_0072xd0v`. **Corrigido na S7:** o texto §3/§6 da fase agora diz `painel`,
`painel.cms.*`, `painel.ver|editar` e `painel.module_*` (sem alterar código).

### Arquivos para revisar

- `src/modules/Suporte/Painel/{ModuleProvider.php,MODULE.md}`
- `src/modules/Suporte/Painel/Routes/cms.php`
- `src/modules/Suporte/Painel/Logic/{Actions/PainelOverviewAction.php,Controllers/Cms/PainelIndexController.php,DTOs/PainelOverview.php}`
- `src/modules/Suporte/Painel/Layout/Views/cms/index.blade.php`
- `src/modules/Suporte/Painel/Lang/{pt_BR,en,es,fr}/index.php`
- `src/modules/Suporte/Painel/Test/PainelCmsTest.php`
- `src/core/Internal/Navigation/PanelMenu.php` e `src/core/Internal/Navigation/Test/PanelMenuTest.php`
- `src/tests-e2e/tests/{23-suporte-painel.spec.ts,support/cms.ts}`, `src/tests-e2e/histories/23-suporte-painel.md`,
  `src/tests-e2e/playwright.config.ts`
- `src/tests-e2e/README.md`, `wiki/specs/04-acessibilidade-ux.md`, `AGENTS.md` (contagem de histórias)

### Pontos de revisão (D1–D14 aplicáveis)

- **D1/D3** Action única e determinística; nenhum `now()`/`rand()`; a contagem vem do mesmo
  resolvedor do Core (`ModuleDiscoveryService`), sem DB direto.
- **D2** Rota sem closure; controller fino; `Permission::authorize` antes de renderizar; nenhuma
  mutação por `GET`.
- **D4** Módulo só depende de `Core`/`Layout`; nenhum import de outro módulo.
- **D7** Um shell, `<h1>` do shell, cartões semânticos (`<dl>`, `<ul>`), funcionamento sem JS.
- **D8** 4 locales com paridade (`ModuleI18nTest`) e nenhuma string fixa (`__()`).
- **D9** Token `painel` em classes, views, rotas e chaves; doc do módulo sincronizada.
- **D10/D11** Nenhuma tabela nova; nenhum PII no span/log; área restrita coberta por
  `support.group` (404) e `audit.access` já no grupo da área CMS.
- **D12** Span + `log_event` com o mesmo nome; sem PII.
- **D13** Testes de comportamento (não wiring); E2E com axe.
- **D14** `MODULE.md` do módulo; esta ficha e a fila de revisão.

### Evidência

- **Vermelho → verde (comportamento novo):** com `PanelMenu::fromManifest()` voltando a chamar
  `grouped()` (sem o `isSupport()`), `PanelMenuTest::the_support_group_is_listed_only_for_the_support_team`
  **falha** (`Tests: 7, Failures: 1`); restaurando a correção, `PanelMenuTest` fica **OK (7 testes,
  26 asserções)**. A rota do módulo também falhava antes do manifesto novo (`RouteNotFoundException:
  Route [painel.cms.index] not defined`) — prova de que o teste do menu/tela não passa por wiring.
- PHPUnit: `PainelCmsTest` **9 testes / 61 asserções** verdes; `PanelMenuTest` **7/26**;
  `SupportGroupAccessTest`, `ModuleDiscoveryTest`, `ModuleI18nTest`, `LayoutContractTest`,
  `Adr004GrepFriendlyTest`, `DocsReferencesTest`, `E2eDocsTest`, `CmsNavigationTest`, `IconTest`
  verdes (85/85 na rodada agregada de estrutura).
- `tools/name-check.sh`: **OK** (10 módulos; aviso esperado de view com nome convencional).
- **`make check` 2×**: `24 OK · 0 FAIL · 9 SKIP` e `23 OK · 0 FAIL · 10 SKIP` (sem flaky; coverage
  ≥90% OK, PHPStan max 0, `a11y` obtido do CMS com sniff estrito).
- **`make check E2E=1`**: `24 OK · 0 FAIL`; `e2e` **99 s** com **124 passed** (1 skip condicional),
  incluindo `[cms-desktop] › 23-suporte-painel.spec.ts` ×2 (404 do não-equipe; equipe vê item e
  tela com axe) e `perf` OK; `tsc --noEmit` OK; `playwright test --list` = **125 testes / 23 arquivos**.

### Riscos/observações

- **Nomenclatura** (acima) é o achado mais provável da revisão: o plano e o código divergem por
  construção; a decisão está no kd.
- O anônimo em `/suporte` recebe **302 para o login** (a autenticação da área roda antes do guard)
  e, autenticado sem ser da equipe, **404**; não há vazamento de conteúdo. Documentado no teste.
- As telas de S2–S6 precisam nascer com o mesmo prefixo de rota `painel.cms.*` e URI
  `/suporte/...`, sem duplicar a regra do grupo.

## S2 — Ativação/desativação de módulos com proteção (`task_01tfanhh`)

### O que foi implementado

- **Action única do Core** `Core\Internal\ModuleDiscovery\Logic\Actions\SetModuleActivation`
  (`execute(module, active, reason, force = false)`), consumida pela UI e por
  `core:module:activate`. Valida no servidor: módulo no manifesto (com revarredura quando
  recém-gerado, restaurando o manifesto em cache antes de recusar), **grupo `Suporte` recusado
  sempre** (inclusive `force=true`), **essencial** (`config('core.essential_modules')`) só muda com
  `force` e **motivo** obrigatório (≤ 255). Grava via `ModuleActivation::setActive` (invalidação
  imediata) e emite **auditoria explícita** `painel.module_activated`/`painel.module_deactivated` com
  `{module, active, reason, force}` no `payload_after` (target `module`), span e `log_event` de
  mesmo nome — sem PII. Não usa `AuditEntry::model()`.
- **Config** `config/core.php` → `essential_modules` = `['Acesso', 'Configuracoes']` (nomes exatos
  do manifesto; lista única, sem duplicação em módulo/comando/view).
- **Comando** `core:module:activate` passou a só validar/formar a saída e delegar à Action; ganhou
  `--force` (só aqui, auditado) e `--reason` (default determinístico `cli`), preservando
  `--disable`. Sem segunda implementação (nem `setActive` nem a lista de essenciais no comando).
- **UI no `Suporte/Painel`**:
  - `GET /suporte/modulos` (`painel.cms.modules`) lista o manifesto com grupo (rótulo traduzido com
    fallback), título traduzido, rota declarada, permissões declaradas e estado, em ordem
    determinística **grupo → position → nome** (fonte única: manifesto).
  - `POST /suporte/modulos/{module}/ativar|desativar` (`painel.cms.modules.activate|deactivate`),
    CSRF, `Permission::authorize('Painel', EDIT)`, motivo `required|max:255`, confirmação nativa
    (`x-cms::confirm`/popover), nunca `GET` mutando.
  - Essenciais e grupo `Suporte` aparecem **sem toggle** (badges + dica); a recusa real também é do
    servidor (defesa em profundidade). Nenhum caminho de UI envia `force` (controllers chamam
    `force: false`; o request não aceita o campo).
  - Flash i18n (`painel::modules.activated|deactivated`) e erros de domínio convertidos em erro de
    formulário i18n (`PainelActivationFailure`), nunca 500; módulo inexistente → 404.
  - View `cms/painel-modules.blade.php`: um único `<x-layout::cms>`, só conteúdo, sem `<h1>`/
    style/script inline; 4 locales em `painel::modules.*`. A visão geral ganhou o botão para a lista.
- **E2E**: jornada nova em `23-suporte-painel.spec.ts` — a equipe desativa `Blueprint` com motivo,
  o portal responde **404** no request seguinte, reativa e volta a **200**; estado global restaurado
  no `finally` (e normalizado antes, se uma execução anterior caiu no meio). Axe A/AA na lista.
- **Incidental (gate)**: `tools/e2e.sh` passava um argumento vazio ao runner docker quando não havia
  filtro (`printf '%q '` sem args emite `''`), quebrando `tsc`/Playwright; o append agora só ocorre
  com argumentos.

### Arquivos para revisar

- `config/core.php`
- `src/core/Internal/ModuleDiscovery/Logic/Actions/SetModuleActivation.php`
- `src/core/Internal/ModuleDiscovery/Enums/ModuleActivationRefusal.php`
- `src/core/Internal/ModuleDiscovery/Exceptions/{ModuleActivationRefused,ModuleNotFound}.php`
- `src/core/Internal/ModuleDiscovery/Console/ActivateModuleCommand.php`
- `src/core/Internal/ModuleDiscovery/README.md`, `src/core/Internal/Generators/Console/README.md`
- `src/modules/Suporte/Painel/Logic/Actions/PainelModulesAction.php`
- `src/modules/Suporte/Painel/Logic/DTOs/PainelModule.php`
- `src/modules/Suporte/Painel/Logic/Requests/PainelModuleActivationRequest.php`
- `src/modules/Suporte/Painel/Logic/Support/PainelActivationFailure.php`
- `src/modules/Suporte/Painel/Logic/Controllers/Cms/PainelModules*Controller.php`
- `src/modules/Suporte/Painel/Routes/cms.php`
- `src/modules/Suporte/Painel/Layout/Views/cms/{index,painel-modules}.blade.php`
- `src/modules/Suporte/Painel/Lang/{pt_BR,en,es,fr}/{index,modules}.php`
- `src/modules/Suporte/Painel/MODULE.md`
- `src/core/Internal/ModuleDiscovery/Test/SetModuleActivationTest.php`
- `tests/Feature/ModuleActivationCommandTest.php`
- `src/modules/Suporte/Painel/Test/PainelModulesCmsTest.php`
- `src/tests-e2e/tests/23-suporte-painel.spec.ts`, `src/tests-e2e/histories/23-suporte-painel.md`,
  `src/tests-e2e/README.md`, `wiki/specs/04-acessibilidade-ux.md`, `tools/e2e.sh`

### Pontos de revisão (D1–D14 aplicáveis)

- **D1/D3** Uma Action (sem segunda regra), determinística (ordem explícita, sem `now()`/`rand()`).
- **D2** Rotas sem closure; controller fino; `POST` + CSRF; autorização antes de mutar; sem `GET`
  mutando; domínio recusado vira validação i18n, não 500.
- **D3/D10** Manifesto como fonte única; `config/core.php` como fonte única dos essenciais; sem
  duplicar lista.
- **D4** `Suporte/Painel` depende só de Core/Layout; a regra mora no Core.
- **D6/D7** daisyUI/Tailwind existentes; confirmação nativa (popover), labels únicos, semântica de
  tabela, `<h1>` do shell.
- **D8** 4 locales com paridade; nenhuma string visível fixa.
- **D9** Token `painel` nos artefatos/rotas/views; Action do Core em `ModuleDiscovery` (namespace do
  concern); `core.*` nos eventos, como `core.settings.updated`.
- **D11/D12** Sem PII no log/span; auditoria explícita com motivo e `force`; ator snapshot; target
  `module`.
- **D13** Testes de comportamento (dois requests reais, recusas, motivo, CSRF, ordem); E2E com axe e
  cleanup.
- **D14** `MODULE.md` (rotas/eventos) e esta ficha; README do E2E e spec de a11y atualizados.

### Evidência

- PHPUnit alvo: `SetModuleActivationTest` **7 testes/62 asserções**; `ModuleActivationCommandTest`
  **6/37**; `PainelModulesCmsTest` **10/119**; gates de estrutura (`ModuleI18nTest`,
  `DocsReferencesTest`, `ModulesTelemetryTest`, `Adr004GrepFriendlyTest`, `E2eDocsTest`,
  `LayoutContractTest`, `ModuleActivationTest`, `ConsoleCommandsCoverageTest`, `CmsNavigationTest`)
  **56/4652** na rodada agregada. PHPStan max **0**; `pint --test` OK; `tools/name-check.sh` OK.
- E2E alvo: `tools/e2e.sh test 23-suporte-painel` → **7 passed** (inclui o fluxo desativa 404 →
  reativa 200 e axe), typecheck OK.
- **Efeito imediato** provado no PHPUnit com **dois requests reais** (`GET /noticias` portal e
  `/noticias` CMS: 200 → 404 → 200), sem `optimize` e sem reload; o mapa de ativação ficou
  restaurado (`Blueprint: true`) após o E2E.
- **`make check` 2×** (rodada final, após todas as edições): `24 OK · 0 FAIL · 9 SKIP` (111 s) e
  `22 OK · 0 FAIL · 11 SKIP` (102 s — os SKIPs extras são `prep:*`/`optimize-verify` idempotentes).
- **`make check E2E=1`**: `24 OK · 0 FAIL · 9 SKIP`; `e2e` **1,6 min** com **125 passed** de
  **126 testes / 23 arquivos** (1 skip condicional `07-cms-shell`), incluindo o novo fluxo
  desativa → 404 → reativa → 200; `perf` **10 passed**; typecheck OK. O mapa de ativação terminou
  `{"Menus": true, "Blueprint": true}` (sem estado global sujo).

### Riscos/observações

- **Token dos eventos:** o plano §3/§6 dizia `suporte.module_activated|module_deactivated`; o token
  canônico do manifesto é **`painel`** (S1), então o evento é `painel.module_*` — uma única
  vocabulary (`painel.cms.*`, `painel.modules`, `painel.module_*`). Registrado no `kd`
  (`decision_01w2m428`, que substitui a leitura inicial `core.module_*` em `decision_01eflr8d`)
  junto da decisão de nomenclatura da S1 (`decision_0072xd0v`).
- **Essencial é estrito:** qualquer mudança (ativar **ou** desativar) exige `force` — leitura
  literal do contrato da fatia (“só pode mudar com force”). A UI não oferece toggle para essencial.
  A emenda da ADR-034 fala em “reativar o essencial”; com `force` o CLI reativa e audita o motivo.
- **Grupo `Suporte` também recusa módulos já inativos**: não há caminho para “reativar” o painel
  pela UI; o painel nunca é desativado e o próprio guard `module.activation` não se aplica a ele
  quando inativo (o guard responde 404 para o grupo inteiro). Mantido fail-closed.
- A lista mostra rota **declarada** (nome) e não link para o módulo: evita link morto/param
  obrigatório e domínio cruzado.
- O `--force` fica registrado na auditoria (`payload_after.force`) e no log (`forced`), mesmo quando
  usado em módulo não essencial (sem efeito extra).


## S3 — Diagnóstico do sistema e `core:diagnose` (`task_01xi38oe`)

### O que foi implementado

- **Core `src/core/Internal/Diagnostics`**: uma única implementação consumida pela tela e pelo
  comando.
  - `DiagnosticsService` executa as verificações com o timeout curto do config, agrega o
    `DiagnosticReport`, captura `Throwable` por check e emite span + `log_event` `core.diagnose`
    (contagens, sem PII). Carimbo vem do `Clock` injetado.
  - `DiagnosticRegistry` fixa a ordem; `DiagnosticPresenter` traduz rótulo/estado/valor (mesmas
    frases na tela e no CLI); `DiagnosticFailure` publica **só a classe** da exceção (nunca a
    mensagem); `DiagnosticStatus` fecha os quatro estados (`ok|warning|error|unavailable`).
  - 6 portas de sonda (`DatabaseProbe`, `CacheProbe`, `QueueProbe`, `StorageProbe`, `DiskProbe`,
    `BackupProbe`) com implementações concretas em `Probes/` e doubles nos testes. Sem
    trait/abstract/generic.
- **13 checks**: `version` (de `VERSION` via `SystemVersion`), `php`, `framework`, `database`
  (`select version()` em **conexão PDO dedicada** com `connect_timeout` + `statement_timeout`),
  `last_migration` (nome/lote; a tabela do Laravel **não** guarda data da execução), `cache_store`,
  `valkey` (`PING` apenas quando o store é Redis, cliente dedicado com connect/read timeout),
  `queue_driver`, `queue_health` (tamanho e atraso pela API nativa; não consome job e não chama
  `pop()`), `scheduler` (**sempre `unavailable`** com nota: o Laravel não expõe heartbeat de
  `schedule:run` — nada de carimbo inventado), `storage` (`HeadBucket` do bucket S3/RustFS com
  timeout HTTP; sonda em `src/core/Internal/System/Storage/S3BucketProbe.php`, a fronteira de
  storage do arch-check), `disk_free` e `backup` (nome/tamanho/data do dump local mais recente;
  conteúdo nunca é aberto).
- **Comando** `php artisan core:diagnose`: registrado no Core, só **formata** o relatório do mesmo
  serviço (sem lógica duplicada). Exit code: **1** quando qualquer check ficou `error`; **0**
  quando só há `ok`/`warning`/`unavailable` (estados explícitos, ex.: scheduler sem heartbeat, cache
  que não é Redis). Decisão documentada no README do Core/Diagnostics.
- **Tela** `GET /suporte/diagnostico` no domínio do painel (`painel.cms.diagnostico`, `painel.ver` + grupo
  `Suporte`): 13 checks em 5 cartões (`<h2>`), badge por estado, carimbo da coleta, alerta quando
  há falha; um único `<x-layout::cms>`, só o `<main>`, sem `<h1>`/estilo/JS inline. A Action
  `PainelDiagnosticAction` emite span + `log_event` `painel.diagnose` (contagens) e **reusa** o
  `DiagnosticsService`. Link “Diagnóstico” na visão geral (rota real — sem link morto).
- **i18n**: rótulos/estados dos checks no **Core**
  (`src/core/Lang/{pt_BR,en,es,fr}/diagnostics.php`) porque a tela e o comando compartilham o texto;
  o chrome da página fica em `painel::diagnostics.*` (4 locales). Paridade das 4 locales dos dois
  conjuntos coberta por teste.
- **Config** `config/diagnostics.php` (`DIAGNOSTICS_TIMEOUT` default 2 s, `backup_directory`,
  `disk_path`), documentada em `config/_OTHER_CONFIGS.md`.
- **Sem vazamento**: saída allowlist (nenhum log técnico/erros recentes, segredo, DSN, token,
  senha ou PII); falha vira classe sanitizada; a tela responde **200** mesmo com check em `error`;
  nenhum `exec`/shell — só PDO/Redis/Queue/S3/filesystem nativos com timeout configurado.
- **E2E**: caso novo no spec/história 23 — a equipe abre o diagnóstico pelo link, vê o `scheduler`
  explicitamente indisponível e a tela passa no axe. Nenhum dado é mutado (sem limpeza).

### Arquivos para revisar

- `config/diagnostics.php`, `config/_OTHER_CONFIGS.md`
- `bootstrap/providers.php`, `src/core/CoreServiceProvider.php`
- `src/core/Internal/Diagnostics/**` (Contracts, Probes, Checks, DTOs, Enums, Support,
  `DiagnosticsService`, `DiagnosticPresenter`, `DiagnosticFailure`, `DiagnosticRegistry`,
  `DiagnosticsServiceProvider`, `Console/DiagnoseCommand.php`, `README.md`, `Test/**`)
- `src/core/Internal/System/SystemVersion.php` (novo `read()`),
  `src/core/Internal/System/Storage/{S3BucketProbe,S3ClientFactory}.php`
- `src/core/Lang/{pt_BR,en,es,fr}/diagnostics.php`
- `src/modules/Suporte/Painel/Logic/Actions/PainelDiagnosticAction.php`
- `src/modules/Suporte/Painel/Logic/Controllers/Cms/PainelDiagnosticController.php`
- `src/modules/Suporte/Painel/Logic/DTOs/{PainelDiagnosis,PainelDiagnosisGroup}.php`
- `src/modules/Suporte/Painel/Routes/cms.php`
- `src/modules/Suporte/Painel/Layout/Views/cms/{index,painel-diagnostico}.blade.php`
- `src/modules/Suporte/Painel/Lang/{pt_BR,en,es,fr}/{index,diagnostics}.php`
- `src/modules/Suporte/Painel/MODULE.md`
- `src/modules/Suporte/Painel/Test/PainelDiagnosticsCmsTest.php`
- `tests/Feature/{DiagnoseCommandTest,DiagnosticsProbeTest}.php`
- `src/tests-e2e/tests/23-suporte-painel.spec.ts`, `src/tests-e2e/histories/23-suporte-painel.md`

### Pontos de revisão (D1–D14 aplicáveis)

- **D1/D3** Serviço único e determinístico (ordem fixa de checks/grupos; `Clock` injetado, sem
  `now()`); falha isolada por check; nenhuma abstração sem uso (portas usadas por 2 consumidores e
  por doubles).
- **D2** Rota `GET` sem closure, controller fino, autorização antes de renderizar; comando sem
  lógica de verificação.
- **D4** `Suporte/Painel` depende de Core/Layout; `Diagnostics` não importa módulo/service; a sonda
  S3 respeita a fronteira de storage (arch-check verde).
- **D6/D7** Cartões `<section>` com `<h2>`, listas semânticas, badge com texto (não só cor), um
  shell, `<h1>` do shell, funciona sem JS.
- **D8** 4 locales nos dois conjuntos (Core e módulo), paridade testada; nenhuma string visível
  fixa na view.
- **D11** Nada de segredo/DSN/token/senha/PII; mensagem de exceção descartada; teste prova que o
  HTML e a saída do comando não contêm a senha/segredo injetado.
- **D12** Span + `log_event` `core.diagnose` (Core) e `painel.diagnose` (tela), só com escalares.
- **D13** Checks com doubles cobrindo sucesso/exceção/indisponível; relatório agregado; comando com
  o mesmo serviço; feature 200/404/4 locales/layout; prova de falha → cartão/HTTP 200; E2E + axe.
- **D14** `MODULE.md`, README do Core/Diagnostics, config documentada, esta ficha e a fila.

### Evidência

- PHPUnit alvo: `DiagnosticChecksTest` **27**, `DiagnosticsServiceTest` **3**,
  `DiagnosticPresenterTest` **3**, `DiagnosticTargetsTest` **6**, `DiagnoseCommandTest` **4**,
  `DiagnosticsProbeTest` **8**, `PainelDiagnosticsCmsTest` **8** — **59 testes / 335 asserções**
  verdes. Estruturais (`DocsReferencesTest`, `ModuleI18nTest`, `LayoutContractTest`,
  `E2eDocsTest`, `IconTest`, `ModulesTelemetryTest`, `ConsoleCommandsCoverageTest`,
  `StorageBucketTest`, `SetModuleActivationTest`): **59/4789** verdes na rodada agregada.
- Gates: `phpstan` **0 erros** (max, sem baseline), `pint --test` OK, `tools/name-check.sh` OK,
  `tools/arch-check` OK, `tools/migration-check` OK, `tools/optimize-check` OK,
  `tools/docs-check.sh` OK.
- **`make check` 2×** (rodada final): `25 OK · 0 FAIL · 8 SKIP` (117 s) e `22 OK · 0 FAIL · 11 SKIP`
  (106 s) — sem flaky. `coverage-check` **93,29%** (14.676/15.731; piso 90%), PHPStan max **0** e
  `a11y` estrita OK; os SKIPs extras são `prep:*`/`optimize-verify` idempotentes.
- **`make check E2E=1`**: `24 OK · 0 FAIL · 9 SKIP` (235 s); `e2e` **99 s** com **126 passed**
  (1 skip condicional), incluindo `[cms-desktop] › 23-suporte-painel.spec.ts` (404 do não-equipe,
  visão geral, toggle 404→200 e o novo **diagnóstico + axe**); `perf` **10 passed**;
  `coverage-check` 93,29%.

### Riscos/observações

- **Última migração não tem data**: a tabela `migrations` do Laravel guarda apenas nome e lote; o
  card mostra esses dois campos e não inventa horário.
- **Scheduler `unavailable` por contrato**: não há fonte nativa de “última execução”; a nota aponta
  o cron do host. Se um heartbeat existir no futuro, ele entra no check (nunca na view).
- **Timeout da fila**: `Queue::size()/delayedSize()` usam a conexão configurada do Laravel. Para
  Redis o `connect_timeout` global continua 0 (o `BLPOP` do worker exige `read_timeout` 0); o
  check é leitura (`LLEN`/`ZCARD`) e a sonda de Valkey usa cliente dedicado com timeout. Um servidor
  “conectado e mudo” é a única janela residual — documentada, sem alterar o worker.
- **Ownership das frases**: os rótulos dos checks vivem no Core (não em `painel::`) porque a tela e
  o comando precisam da mesma frase; o `ModuleI18nTest` continua exigindo que a view use
  `painel::`/`layout::` — o apresentador entrega o texto pronto.
- **Backup**: o card lê só metadados do diretório local (`storage/backups`); sem dump, o estado é
  `unavailable` (não um “OK” falso). Cópia offsite é do `up_prod.sh` e não é medida aqui.

## S4 — Leitor da trilha de auditoria (`task_015xs35p`)

### O que foi implementado

- **Leitura no Core (fonte única)** `src/core/Internal/Audit`:
  - `DTOs/AuditFilters` normaliza a borda (evento/módulo/ator em `trim`, módulo em minúsculas,
    `from`/`to` como dia inteiro no fuso da aplicação, `per_page` em `1..100` com padrão **25** e
    `direction` em `next|prev`) e expõe `toQuery()` para os links preservarem a seleção.
  - `Services/AuditLogReader::page()` faz **uma** consulta por página: `ORDER BY created_at DESC,
    id DESC` (desempate único), `LIMIT per_page + 1` para saber se há próxima (sem `count(*)`) e
    **keyset** — nunca `OFFSET`. A condição é expandida
    (`created_at < ? OR (created_at = ? AND id < ?)`) para o planejador podar a partição e usar o
    índice composto; `direction=prev` busca a janela mais nova em ordem ascendente e a devolve na
    ordem de exibição. Filtros: `event` exato, `module` exato (minúsculas), `actor` por **id**
    exato, **e-mail** exato (sem caixa) ou trecho do **nome**, e intervalo inclusivo.
  - `DTOs/AuditCursor` é uma posição opaca (base64url de `{t: created_at com microssegundos e
    offset, i: uuid}`) validada na leitura; token adulterado volta à primeira página (nunca 500).
  - `DTOs/AuditPage` devolve `items` + `nextCursor`/`prevCursor` (`null` = lado inexistente);
    quando a página filtrada vem vazia no fim da trilha, o cursor de **voltar** é preservado.
  - A página lê só `id/event/module/area/origin/action/entity_type/actor/created_at` — os `jsonb`
    grandes (`payload_before/after/changes/metadata`) só no detalhe (binding por UUID).
- **Índices** (`Database/Migrations/2026_10_25_000000_add_reader_indexes_to_core_audit_logs.php`):
  `(created_at DESC, id DESC)`, `(module, created_at DESC, id DESC)` e
  `(event, created_at DESC, id DESC)`, idempotentes (`IF NOT EXISTS`); `down()` remove **só estes
  índices** (índice não é dado — ADR-017). O filtro por trecho de nome **não** tem índice de
  trigrama (extensão ausente): a varredura fica limitada pelo intervalo e pelo limite da página —
  necessidade documentada no README do Audit.
- **Rotas** (`Routes/cms.php`): `GET /suporte/auditoria` (`painel.cms.audit`) e
  `GET /suporte/auditoria/{log}` (`painel.cms.audit.show`, `whereUuid` — UUID inválido/inexistente é
  **404**). Nenhuma rota de edição/exclusão; a trilha continua imutável (trigger no banco).
- **Controller/Requests/Actions finos**:
  - `PainelAuditFilterRequest` valida **forma** (datas `Y-m-d`, `to >= from` via closure i18n,
    `per_page` `1..100`, `direction` `next|prev`, textos `max:191`, `cursor` `max:512`); erro de
    forma é 302 + sessão (nunca 500) e o cursor adulterado é tolerado (primeira página).
  - `PainelAuditIndexController`/`PainelAuditShowController`: `Permission::authorize('Painel', VIEW)`
    e view; nada de consulta Eloquent, nada de auditoria manual.
  - `PainelAuditAction` (span + `log_event` `painel.audit`, só escalares e **booleanos** de filtro —
    o valor do ator é PII) monta `PainelAuditPage` com `nextUrl`/`prevUrl` preservando os filtros;
    `PainelAuditShowAction` (`painel.audit_entry`) converte o registro em `PainelAuditEntry`
    (payloads → JSON **texto**, nunca HTML).
- **UI** `Layout/Views/cms/painel-auditoria.blade.php` e `painel-auditoria-show.blade.php`: um único
  `<x-layout::cms>`, só o conteúdo; filtros `GET` (`x-cms::filter-card`), tabela semântica
  (`x-cms::data-table` com `caption`/`scope`), estado vazio comum e o específico de filtro,
  navegação keyset com os mesmos rótulos/alvos da paginação do projeto (`layout::shell.pagination*`,
  `rel=prev|next`, ≥2,75 rem), `data-audit-id`/`data-event` para E2E/testes. Detalhe com `<dl>` e
  `<pre>` escapados (before/after/changes), ator snapshot, `request_id`, `trace_id`,
  `integrity_hash`, `created_at`; nota de imutabilidade. A visão geral ganhou o botão “Auditoria”
  (rota real — sem link morto).
- **i18n** `Lang/{pt_BR,en,es,fr}/audit.php` (paridade de chaves; nenhuma string fixa; datas via
  `LocalizedDate`).
- **Leitura auditada**: `config/audit.php` passa a listar **explicitamente** `painel.cms.audit` e
  `painel.cms.audit.show` além de `*audit*`. Cada request gera **um** evento pelo middleware
  `audit.access` da área CMS (via única), com `event=painel.view`, `area=cms`, `entity_type` = nome
  da rota e ator snapshot; o controller não escreve auditoria. **Sem recursão**: o evento é gravado
  depois da resposta e o `GlobalAuditSubscriber` ignora o próprio `AuditLog`.
  **Divergência preservada** (registrada no `kd`): a semântica global é `{module}.view`
  (`painel.view`), não o literal `audit.access` do texto da fase; a via única do middleware foi
  mantida em vez de duplicar um segundo evento.
- **Export CSV**: **fora do MVP** (decisão explícita) — não há botão, rota nem stub; a paginação
  keyset atende a leitura operacional.

### Arquivos para revisar

- `src/core/Internal/Audit/DTOs/{AuditFilters,AuditCursor,AuditPage}.php`
- `src/core/Internal/Audit/Services/AuditLogReader.php`
- `src/core/Internal/Audit/Database/Migrations/2026_10_25_000000_add_reader_indexes_to_core_audit_logs.php`
- `src/core/Internal/Audit/{README.md,Test/AuditLogReaderTest.php}`
- `config/audit.php`
- `src/modules/Suporte/Painel/Logic/{Actions/PainelAuditAction.php,Actions/PainelAuditShowAction.php}`
- `src/modules/Suporte/Painel/Logic/Controllers/Cms/{PainelAuditIndexController,PainelAuditShowController}.php`
- `src/modules/Suporte/Painel/Logic/DTOs/{PainelAuditPage,PainelAuditRow,PainelAuditEntry}.php`
- `src/modules/Suporte/Painel/Logic/Requests/PainelAuditFilterRequest.php`
- `src/modules/Suporte/Painel/Routes/cms.php`
- `src/modules/Suporte/Painel/Layout/Views/cms/{index,painel-auditoria,painel-auditoria-show}.blade.php`
- `src/modules/Suporte/Painel/Lang/{pt_BR,en,es,fr}/audit.php`
- `src/modules/Suporte/Painel/{MODULE.md,Test/PainelAuditCmsTest.php}`
- `src/tests-e2e/tests/23-suporte-painel.spec.ts`, `src/tests-e2e/histories/23-suporte-painel.md`,
  `wiki/specs/04-acessibilidade-ux.md`

### Pontos de revisão (D1–D14 aplicáveis)

- **D1/D3** Filtros/ordenação explícitos (sem `now()`/`rand()`); uma leitura por página; keyset com
  desempate único (nunca `OFFSET`/`latest()` sem tie-breaker); limite fechado (`1..100`, padrão 25).
- **D2** Rotas `GET` sem closure, controller fino, `whereUuid`, `Permission::authorize` antes do
  render; forma validada no `FormRequest`; nada de mutação por `GET`.
- **D4** O módulo consome a porta de leitura do Core (`AuditLogReader`) e não consulta a tabela
  diretamente; nenhum import de outro módulo.
- **D5/D7** Um shell, `<h1>` do shell, tabela com `caption`/`scope`, estado vazio explícito,
  navegação com rótulos do projeto e alvos ≥2,75 rem; sem `<style>`/JS inline; funciona sem JS.
- **D8** 4 locales com paridade (`ModuleI18nTest`); datas via `LocalizedDate`.
- **D10** Sem migração destrutiva; índices aditivos/idempotentes e `down()` sem drop de dado;
  payloads `jsonb` não são renderizados como HTML.
- **D11** PII só a que já existe na trilha (ator snapshot); valor do filtro de ator **não** vai para
  log/span; `metadata` (IP/UA/URL) não é exibido; nada de segredo.
- **D12** Span + `log_event` de mesmo nome em cada Action (`painel.audit`, `painel.audit_entry`);
  leitura auditada **uma vez** por request, sem recursão.
- **D13** Testes de keyset (sem repetir/perder linhas, inclusive com timestamps empatados),
  filtros combinados, limite, detalhe/404, imutabilidade (sem rota mutante), uma consulta por
  página, um evento por request, 4 locales e E2E com axe.
- **D14** `MODULE.md`, README do Audit, esta ficha, fila/inventário, história/spec de E2E.

### Evidência

- **Vermelho → verde (observado):** na primeira execução o filtro por ator devolvia **0 linhas**
  (`where('actor->>id')` compila para `"actor"->>'>id'` no grammar do Postgres) e os testes de
  keyset comparavam ids vazios (o default `uuidv7()` vem do banco e o Eloquent não o devolve no
  insert) — os dois falharam de forma explícita e ficaram verdes corrigindo a consulta
  (`actor->id`) e criando os registros de teste com UUID explícito. Depois disso, os guardas ficam
  vermelhos se a leitura passar a contar/consultar por linha (teste de **uma** consulta) ou se um
  request gerar 0 ou 2 eventos de acesso (`painel.view` por request).
- O teste de keyset cobre ainda **timestamps empatados** (desempate por `id`) e o caminho de
  `direction=prev`, além de cursor adulterado e página vazia no fim da trilha.
- PHPUnit alvo: `AuditLogReaderTest` **8 testes / 47 asserções**; `PainelAuditCmsTest`
  **13 testes / 176 asserções**; `AuditAccessTest` **2/9** (inalterado, cobre o middleware genérico).
  Estruturais (`ModuleI18nTest`, `LayoutContractTest`, `Adr004GrepFriendlyTest`, `DocsReferencesTest`,
  `E2eDocsTest`, `ModulesTelemetryTest`, `AuditTrailTest`, `PainelCmsTest`, `PainelModulesCmsTest`,
  `PainelDiagnosticsCmsTest`, `AuditPolicyTest`, `AuditRetentionTest`, `AuditAuthTest`,
  `AuditEntryTest`, `ModuleTokenTest`): **106 testes / 5.473 asserções** na rodada agregada.
- Gates: `phpstan` max **0 erros**; `pint --test` OK; `tools/name-check.sh --grep painel` OK
  (1 Model, 8 Actions, 20 Views, 13 Tests; aviso esperado de módulo sem migração);
  `check-migrations` OK (índices aditivos; `down()` só remove índice) e `check-boundaries` OK.
- Migração aplicada (`prep:migrate`): índices `core_audit_logs_created_at_id_idx`,
  `core_audit_logs_module_created_at_id_idx` e `core_audit_logs_event_created_at_id_idx` presentes em
  `core.core_audit_logs` no banco de dev.
- **`make check` 2×** (árvore final): `24 OK · 0 FAIL · 9 SKIP` (111 s) e `22 OK · 0 FAIL · 11 SKIP`
  (106 s) — sem flaky; `coverage-check` **93,42%** (15.133/16.199; piso 90%), PHPStan max 0 e `a11y`
  estrita OK nos dois. Os SKIPs extras são `prep:*`/`optimize-verify` idempotentes.
- **`make check E2E=1`**: `24 OK · 0 FAIL · 9 SKIP` (233 s); `e2e` **97 s** com **127 passed**
  (1 skip condicional), incluindo
  `[cms-desktop] › 23-suporte-painel.spec.ts:142 › a equipe filtra a trilha e abre o detalhe de um
  evento` (filtro por `painel.view`, tabela com caption, detalhe com hash, axe em ambos); `perf`
  **10 passed**; `coverage-check` 93,42%.

### Riscos/observações

- **Divergência de vocabulário do evento (registrada):** a fase chama o evento de leitura de
  `audit.access`, mas a via única já existente grava `{module}.view` (`painel.view`) com
  `entity_type` = nome da rota. Preservar a via única venceu duplicar: um segundo evento no
  controller seria trilha dupla e violaria ADR-035. Fica no `kd` para a revisão decidir se o texto
  da fase muda (recomendado) ou se o middleware ganha um alias — sem alterar o comportamento agora.
- **Índices do filtro por ator:** id/e-mail exatos casam via `jsonb` mas o `OR` com `ILIKE` no nome
  impede o uso de GIN; a varredura é limitada pelo intervalo e pelo limite da página. Se o volume
  crescer, o próximo passo medido é índice GIN em `actor` + busca de nome separada (sem mudar a UI).
- **Sem total de registros:** a tela não mostra “N eventos” de propósito (contagem em tabela
  particionada custa uma varredura e a trilha não é analytics — ADR-035). A navegação é
  anterior/próxima.
- **`direction=prev` é janela, não histórico:** voltar refaz a janela imediatamente mais nova (sem
  repetir/perder linhas ao caminhar em cada direção), como todo keyset — não há breadcrumb de
  páginas numeradas.
- **Export CSV fora do MVP** (decisão explícita): sem rota, botão ou stub. Reavaliar se a operação
  pedir extração; nesse caso entra como stream + auditoria própria (e neutralização anti-injection).
- **Evento de acesso e a própria lista:** o evento é gravado **depois** da resposta, então não
  aparece na página que o gerou; a página seguinte o mostra (documentado no E2E, que usa isso como
  prova). Nada de “duas linhas por leitura”.

## S5 — Modo de manutenção (`task_017h4cu4`)

### O que foi implementado

- **Settings `system` (ADR-046, evolução compatível do P0):** os campos `maintenance` (bool, default
  `false`) e `maintenance_message` (texto ≤ 500) foram **preservados**; entraram
  `maintenance_retry_after` (int, `0..86400`, default `3600`) e `maintenance_allow_cms` (bool,
  default `true`). O schema ganhou `min` para inteiros (limite fechado) e as 4 locales têm os
  rótulos. A escrita continua substituindo o documento: a Action lê a seção e mescla os quatro
  campos, preservando `timezone`.
  **Decisão de shape:** o plano descreve `system.maintenance` como objeto (`enabled`/`message`/…),
  mas o ADR-046 declara **campos tipados planos** por seção; a evolução ficou
  `maintenance*` (prefixo), sem chave livre nem documento aninhado. Registrada no `kd`.
- **Core `MaintenanceState`** (`src/core/Internal/Maintenance/MaintenanceState.php`): leitura
  defensiva/`final readonly` (booleano, mensagem vazia → `null`, `retry_after` no intervalo,
  `allow_cms` com default `true`) e `toArray()` só com os quatro campos (before/after da auditoria).
- **Action única `SetMaintenanceMode`** (Core): valida mensagem/`retry_after` também no servidor,
  mescla o restante da seção, grava por `Settings::put` (invalidação síncrona), emite span + 
  `log_event('painel.maintenance_changed')` e **auditoria explícita** `painel.maintenance_changed`
  (target `settings`/`system`, before/after sem PII). A porta ainda grava `core.settings.updated`,
  a trilha genérica da escrita.
- **Middleware `EnforceMaintenanceMode`** (alias `maintenance`, parâmetro de área no
  `AreaRegistrar`): roda por área depois do locale e dos guards (`support.group`/`module.activation`).
  - **Antes de ler settings**, allowlist explícita: `/up`, `robots.txt`, `sitemap.xml`,
    `sitemap-*.xml`, `.well-known/security.txt`, `manifest.webmanifest`, `favicon.ico`,
    `theme.css`, `cms-theme.css`, `build/*` e `midia/*` (prefixo de locale do portal é removido) —
    nenhum acesso ao banco no hot path de asset. SEO/health continuam 200.
  - **Portal:** 503 HTML acessível (view dedicada, sem JS), `Retry-After` do setting,
    `X-Robots-Tag: noindex, nofollow` e `Cache-Control: no-store`.
  - **API:** 503 `application/problem+json` (RFC 7807) com `title/status/detail/instance/trace_id`,
    `Retry-After`; o `X-Request-ID` vem do middleware global. Sem stack/mensagem técnica.
  - **CMS:** `allow_cms=true` mantém tudo acessível; com `false`, só a equipe de suporte
    (`SupportAccess`), `login`/`logout`, o próprio `GET/PUT /suporte/manutencao` e o health seguem
    respondendo — os demais recebem o 503. Nunca há auto-lockout.
  - **Fail-open:** falha ao ler settings retorna `$next` e loga `maintenance.settings_failed`
    (warning) com a **classe** da exceção, sem mensagem/segredo.
- **Página 503 dedicada** `resources/views/errors/maintenance.blade.php`: documento mínimo
  (sem shell de conteúdo público), `<main>`/`<h1>` semânticos, CSS do portal via `@vite`, **sem
  `<style>` inline e sem JS**, mensagem configurada ou texto traduzido padrão, aviso de retorno
  (segundos/minutos) e contato (`support.contact_email`) quando existir; nada de detalhe técnico.
- **UI `GET/PUT /suporte/manutencao`** (`painel.cms.maintenance[.update]`):
  `PainelMaintenanceIndexController` (leitura via porta `Settings`), `PainelMaintenanceRequest`
  (booleans normalizados, `retry_after` `integer|between:0,86400`, mensagem ≤ 500, `confirm`
  obrigatório), `PainelMaintenanceUpdateController` (`painel.editar`, recusa do schema → erro de
  formulário i18n), view com um único `<x-layout::cms>`, CSRF, `@method('PUT')`, confirmação
  explícita, flash i18n e link “Manutenção” na visão geral.
- **i18n:** `painel::maintenance.*` (4 locales, paridade) na UI; `maintenance.*` no Core para a
  página 503/API; rótulos novos em `settings.*` (4 locales).
- **E2E:** jornada acrescentada em `23-suporte-painel.spec.ts` (liga manutenção → portal 503 com
  `Retry-After`/`X-Robots-Tag`, CMS 200, API 503 RFC 7807 → axe na 503 → desliga e restaura 200),
  com normalização antes e `finally` de restauração. O spec passou para o projeto **`cms-serial`**
  (um worker, roda antes dos demais) porque módulo/manutenção são estado global; o projeto
  `cms-serial` agora roda 18 e 23 com `workers: 1`.

### Arquivos para revisar

- `src/core/Settings/SettingsSchema.php`, `src/core/Settings/Test/SettingsSchemaTest.php`
- `src/core/Internal/Settings/Test/DatabaseSettingsTest.php`
- `src/core/Internal/Maintenance/{MaintenanceState.php,Logic/Actions/SetMaintenanceMode.php,Test/SetMaintenanceModeTest.php}`
- `src/core/Internal/Http/Middleware/EnforceMaintenanceMode.php`
- `src/core/Internal/Routing/AreaRegistrar.php`, `src/core/CoreServiceProvider.php`
- `resources/views/errors/maintenance.blade.php`
- `src/core/Lang/{pt_BR,en,es,fr}/maintenance.php` e `settings.php`
- `src/modules/Suporte/Painel/Logic/{Controllers/Cms/PainelMaintenanceIndexController.php,Controllers/Cms/PainelMaintenanceUpdateController.php,Requests/PainelMaintenanceRequest.php}`
- `src/modules/Suporte/Painel/Routes/cms.php`
- `src/modules/Suporte/Painel/Layout/Views/cms/{index,painel-manutencao}.blade.php`
- `src/modules/Suporte/Painel/Lang/{pt_BR,en,es,fr}/maintenance.php`
- `src/modules/Suporte/Painel/{MODULE.md,Test/PainelMaintenanceCmsTest.php}`
- `tests/Feature/MaintenanceModeTest.php`
- `src/tests-e2e/{playwright.config.ts,tests/23-suporte-painel.spec.ts,histories/23-suporte-painel.md,README.md}`
- `wiki/specs/11-acesso-e-ativacao.md` (§4.1 e verificação P3.S5)

### Pontos de revisão (D1–D14 aplicáveis)

- **D1/D3** Uma Action de escrita (UI não valida sozinha); estado derivado do schema, sem
  `now()`/`rand()`; nenhuma chave fora do schema.
- **D2** Rotas sem closure; `GET` não muta; `PUT` + CSRF + `painel.editar`; validação de forma no
  request e de domínio na Action (422/erro de formulário, nunca 500).
- **D4** O módulo depende de `Core\Contracts\Settings` e da Action/DTO do Core; middleware e DTO
  moram no Core; nenhum import de outro módulo.
- **D5/D7** Página 503 sem shell público (documento mínimo), `<main>`/`<h1>`, sem JS/estilo inline;
  UI com um único `<x-layout::cms>`, `<h1>` do shell, labels/`fieldset`, alvos e foco visíveis,
  funciona sem JS (confirmação é checkbox, não dialog dependente de JS).
- **D8** 4 locales com paridade (UI e Core) e nenhuma string visível fixa.
- **D10/D11** Sem migração/tabela nova; nenhum segredo/PII em log/auditoria; mensagem de settings
  é conteúdo público; fail-open loga só a classe.
- **D12** Span + `log_event` de mesmo nome na escrita; auditoria explícita before/after; a leitura
  da tela não polui a trilha além do `audit.access` já existente da área CMS.
- **D13** Testes de comportamento: 503/200 em requests reais, headers, allowlist (inclusive paths
  sem rota Laravel), CMS com `allow_cms` true/false, login e toggle sobrevivendo, restauração,
  locale, fail-open, validação/recusa sem escrita, auditoria e 4 locales; E2E com axe e `finally`.
- **D14** `MODULE.md`, esta ficha, README/história E2E e config do Playwright sincronizados.

### Evidência

- PHPUnit alvo: `SetMaintenanceModeTest` **3 testes / 37 asserções**; `MaintenanceModeTest`
  **9 / 68**; `PainelMaintenanceCmsTest` **8 / 92**; `SettingsSchemaTest` **17 / 268**;
  `DatabaseSettingsTest` **13 / 57**. Rodada agregada com os gates estruturais (`ModuleI18nTest`,
  `LayoutContractTest`, `DocsReferencesTest`, `E2eDocsTest`, `Adr004GrepFriendlyTest`,
  `CheckModuleActivationTest`): **81 testes / 5.445 asserções** verdes.
- `phpstan` (max, sem baseline) **0 erros**; `pint` aplicado nos arquivos alterados.
- E2E alvo: `tools/e2e.sh test 23-suporte-painel` → **6 passed** em `cms-serial`, incluindo o fluxo
  liga → portal 503/`Retry-After: 120`/noindex → CMS 200 → API 503 problem+json → axe A/AA na 503 →
  desliga → portal 200. O estado ficou restaurado (`core:settings:show system` com
  `maintenance: false`) mesmo após a primeira execução falhar no locator do texto.
- **`make check` 2×** (rodadas da árvore): `24 OK · 0 FAIL · 9 SKIP` (117 s) e
  `22 OK · 0 FAIL · 11 SKIP` (109 s) — sem flaky; `coverage-check` **93,38%** (15.475/16.572; piso
  90%), PHPStan max **0** e `a11y` estrita OK nos dois. Após o último teste de contato da 503, a
  árvore final rodou novamente: `23 OK · 0 FAIL · 10 SKIP` (118 s) com coverage **93,39%**
  (15.476/16.572). Os SKIPs extras são `prep:*`/`optimize-verify` idempotentes.
- **`make check E2E=1`**: `24 OK · 0 FAIL · 9 SKIP` (278 s); `e2e` **117 s** com **129 testes**
  (**128 passed**, 1 skip condicional `07-cms-shell`), incluindo a jornada de manutenção em
  `cms-serial` (liga → portal 503/`Retry-After`/noindex + axe → CMS 200 → API 503 RFC 7807 →
  desliga → portal 200); `perf` **16 passed**; `coverage-check` 93,38%.

### Riscos/observações

- **Shape plano `maintenance*`** (acima): escolha explícita para não violar o ADR-046 e não perder
  os campos do P0; se a revisão preferir o objeto `system.maintenance`, será uma migração de schema
  com fallback — hoje não há consumidor do formato aninhado. Registro: `kd decision_00bgnfi9`.
  A mudança do spec 23 para `cms-serial` está em `kd decision_01wybl0e`.
- **Mensagem configurada é conteúdo público e não traduzida:** o campo guarda o texto exibido a
  todos; quando vazio, entra o texto padrão traduzido (`maintenance.default_message`). Traduzir a
  mensagem por locale exigiria documento i18n (novo shape, fora do ADR-046) — backlog.
- **CMS com `allow_cms=false`:** o bloqueio atinge qualquer usuário autenticado que não seja da
  equipe; a equipe mantém o painel inteiro para operar/desligar. `login`/`logout`, o toggle e o
  health continuam explícitos na allowlist. O 503 do CMS usa a mesma página dedicada.
- **Allowlist de assets:** `build/*` e `midia/*` são servidos pelo Laravel/edge; a checagem vem
  antes de qualquer leitura de settings. Arquivos físicos (`favicon.ico`, bundle) são atendidos pelo
  edge e nem passam pelo grupo da área, mas a allowlist é defesa em profundidade e está testada.
- **Middleware depois dos guards:** o grupo `Suporte` continua 404 e módulo inativo continua 404
  mesmo em manutenção (o 503 não revela nem esconde módulo). O `audit.access` do CMS roda antes;
  a leitura vira evento como qualquer request da área.
- **E2E em projeto serial:** a manutenção derruba o portal inteiro; o spec foi para `cms-serial`
  (workers 1) e restaura no `finally`. Nenhum outro projeto roda durante a janela.

## S6 — Contato do fornecedor, versão e novidades (`task_01pkdhbr`)

### O que foi implementado

- **Settings `support` (ADR-046, evolução compatível do P0):** os campos `contact_email` (email),
  `contact_phone` (telefone) e `hours` (texto ≤ 200) foram **preservados** com o mesmo nome; entraram
  `company` (≤ 160), `site` (URL `http(s)` ≤ 2048), `whatsapp` (telefone) e `address` (texto ≤ 500).
  A ordem declarada é `company, site, contact_email, contact_phone, whatsapp, address, hours`.
  Nenhuma chave livre, nenhum HTML/CSS no banco; e-mail/URL continuam validados pelo schema.
- **Novo tipo `phone` no schema:** `+` opcional no início, apenas dígitos/`()-. ` e espaço, 8–15
  dígitos (E.164). Recusa letras, `javascript:` e caracteres de controle — o valor vira `tel:`/
  `wa.me` na tela, nunca HTML. O normalizador faz `trim` como nos demais tipos textuais.
  **Compatibilidade:** um telefone P0 “de gente” (`(11) 4000-0000`) continua aceito; um valor sem
  forma de telefone passa a ser **ignorado** (vence a ausência/fallback), como qualquer valor fora
  do schema. Registrado no `kd` como evolução aditiva do ADR-046 (o texto do ADR lista os tipos
  iniciais; não houve mudança de contrato de armazenamento).
- **DTO `PainelContact`** (`final readonly`): normaliza a seção (`text()` corta vazio → `null`),
  `isConfigured()`/`fieldCount()` (estado vazio e log), `phoneHref()` (`tel:` com `+` preservado),
  `whatsappUrl()` (`https://wa.me/{dígitos}`) e `siteLabel()` (host para leitura, href real).
  A versão entra no construtor; nenhum valor é renderizado como HTML.
- **Action `PainelContactAction`** (`final readonly`): lê `Settings::section('support')` (porta
  cacheada, sem consulta direta) e `SystemVersion::get()`; span + `log_event('painel.contact')` com
  `configured` e `fields` — **nunca** e-mail/telefone/WhatsApp/endereço (sem PII, ADR-024).
- **Tela `GET /suporte/contato`** (`painel.cms.contact`, `painel.ver` + grupo `Suporte`):
  `PainelContactController` fino (autoriza e delega); a visão geral ganhou o botão “Contato do
  fornecedor” (rota real, sem link morto). A view `cms/painel-contato.blade.php` usa **um**
  `<x-layout::cms>`, só o `<main>`, sem `<h1>`/style/script inline e 4 locales (`painel::contact.*`).
  - **Preenchido:** cartões “Fornecedor” (empresa, endereço em texto com quebras, horário) e
    “Canais de atendimento” (site e WhatsApp com `target="_blank"` + `rel="noopener"` e aviso
    `sr-only` de nova aba; e-mail `mailto:`; telefone `tel:`). Nenhum `iframe`/mapa/CDN.
  - **Vazio:** `x-cms::empty-state` com título/corpo explicativos e link real para
    `/configuracoes/support` — nunca tela em branco.
  - **Versão/novidades:** mostra `VERSION` (`SystemVersion`) e diz, sem link morto, que esta
    instalação não publica notas de versão; nenhum caminho local do deploy é exposto. Manual só
    quando o pacote existir no deploy (hoje não existe) — nada de changelog inventado.
- **Configurações:** `support` entrou em `ConfiguracoesSections::EDITABLE` (formulário genérico do
  schema, sem JSON livre) com rótulos/descrição nas 4 locales. O formulário genérico passou a usar
  legenda/id por seção (`configuracoes-{section}`) e `type="tel"` para o tipo `phone`; nenhum campo
  novo é editado como JSON.
- **i18n:** `settings.support.*` (4 locales, rótulos do schema — cobertos pelo teste de paridade) e
  `painel::contact.*` (4 locales na UI; `menu` reutilizado pelo botão da visão geral).
- **E2E:** caso novo em `23-suporte-painel.spec.ts` (projeto `cms-serial`): normaliza a seção vazia,
  abre `/suporte/contato` pela visão geral, confere estado vazio + versão + link de configuração,
  preenche os sete campos em `/configuracoes/support`, confere os links seguros/endereço/ausência de
  `iframe` e axe; limpa a seção no `finally` (e confere o vazio restaurado). História 23, README do
  E2E e spec 04 atualizados.

### Arquivos para revisar

- `src/core/Settings/SettingsSchema.php` (`support` + tipo `phone`/`isPhone`)
- `src/core/Settings/Test/SettingsSchemaTest.php`,
  `src/core/Internal/Settings/Test/DatabaseSettingsTest.php`
- `src/core/Lang/{pt_BR,en,es,fr}/settings.php`
- `src/modules/Suporte/Painel/Logic/Actions/PainelContactAction.php`
- `src/modules/Suporte/Painel/Logic/Controllers/Cms/PainelContactController.php`
- `src/modules/Suporte/Painel/Logic/DTOs/PainelContact.php`
- `src/modules/Suporte/Painel/Routes/cms.php`
- `src/modules/Suporte/Painel/Layout/Views/cms/{index,painel-contato}.blade.php`
- `src/modules/Suporte/Painel/Lang/{pt_BR,en,es,fr}/contact.php`
- `src/modules/Suporte/Painel/{MODULE.md,Test/PainelContactCmsTest.php}`
- `src/modules/Infraestrutura/Configuracoes/Logic/ConfiguracoesSections.php`
- `src/modules/Infraestrutura/Configuracoes/Layout/Views/cms/form.blade.php`
- `src/modules/Infraestrutura/Configuracoes/Lang/{pt_BR,en,es,fr}/cms.php`,
  `src/modules/Infraestrutura/Configuracoes/MODULE.md`
- `src/tests-e2e/tests/23-suporte-painel.spec.ts`,
  `src/tests-e2e/histories/23-suporte-painel.md`, `src/tests-e2e/README.md`,
  `wiki/specs/{04-acessibilidade-ux,11-acesso-e-ativacao}.md`

### Pontos de revisão (D1–D14 aplicáveis)

- **D1/D3** Documento plano sem chave livre nem nested shape; DTO/Action pequenos e determinísticos;
  validação no schema (fonte única) e nenhum `now()`/`rand()`.
- **D2** Rota `GET` sem closure; controller fino; `Permission::authorize('Painel', VIEW)` antes de
  renderizar; configuração editada só pela rota existente de Configurações (`PUT` + CSRF).
- **D4** O módulo depende de `Core\Contracts\Settings`, `SystemVersion` e `Telemetry`; nenhum import
  de outro módulo (o link para Configurações é navegação por `route()`, não acoplamento).
- **D5** Zero CDN/iframe/mapa; site/WhatsApp são links de navegação explícitos; nenhum asset externo.
- **D6/D7** Um shell, `<h1>` do shell, `<dl>`/listas semânticas, estado vazio explícito, alvos do
  design system, funciona sem JS; sem style/script inline.
- **D8** 4 locales com paridade (`ModuleI18nTest`), nenhuma string visível fixa; 4 rótulos do schema.
- **D10/D11** Sem migração/tabela nova; valor inválido no banco é ignorado (teste com documento
  cru); nenhum caminho local do deploy, nenhum segredo; log sem os valores de contato.
- **D12** Span + `log_event('painel.contact')` com o mesmo nome; leitura não polui a trilha além do
  `audit.access` já existente da área CMS.
- **D13** Testes de comportamento: 200/404/302, estado vazio, dados preenchidos, links seguros,
  ausência de `iframe`, versão, 4 locales, contrato de layout, valor inválido ignorado; E2E com axe
  e normalização/limpeza no `finally`.
- **D14** `MODULE.md` (rotas/eventos/dependências), esta ficha, fila/inventário, specs 04/11 e
  história/README do E2E sincronizados.

### Evidência

- **Vermelho → verde (observado):** o E2E de contato, ao **limpar** a seção `support`, produziu
  **500** (`QueryException` / `core_settings_payload_object_check`) porque o documento vazio era
  gravado como `[]` em vez de `{}`. Corrigido em `DatabaseSettings::encodePayload` (`(object) []`
  quando o documento está vazio) com regressão
  `it_persists_an_empty_section_as_a_json_object`; o spec voltou a **7 passed**. Esse era um defeito
  real do caminho genérico de settings que a tela de contato (todos os campos opcionais) tornou
  alcançável.
- **PHPUnit alvo:** `SettingsSchemaTest` **18 testes** (novo caso de `support`, incluindo
  compatibilidade P0, URL/e-mail/telefone e tamanho), `DatabaseSettingsTest` **15** (documento P0
  preservado, round-trip dos canais e documento vazio como objeto), `PainelContactCmsTest` **9**
  (200/404/302, estado vazio, preenchido com links `mailto:`/`tel:`/`wa.me`, config link 200, rota na
  visão geral, valor inválido no banco ignorado, 4 locales, layout). Rodada agregada dos gates de
  estrutura (`ModuleI18nTest`, `LayoutContractTest`, `Adr004GrepFriendlyTest`, `DocsReferencesTest`,
  `E2eDocsTest`, `Configuracoes*`, `Painel*`): **120 testes / 6.191 asserções** verdes.
- Gates: `phpstan` max **0**; `pint --test` OK; `docs-check` OK; `check-boundaries`/`check-migrations`/
  `check-vendor`/`optimize-check` OK; `tools/name-check.sh` OK pelo `make check`.
- **`make check` 1ª rodada:** `24 OK · 0 FAIL · 9 SKIP` (120 s), `coverage-check` **93,43%**
  (15.650/16.751; piso 90%) e suíte PHPUnit **1.162 testes / 19.475 asserções**; `a11y` estrita OK.
- **`make check` 2ª rodada (árvore final):** `24 OK · 0 FAIL · 9 SKIP` (116 s), `coverage-check`
  **93,43%** (15.653/16.754) e **1.163 testes / 19.478 asserções**; `a11y` estrita OK.
- **Rodada extra após as atualizações de documentação:** `22 OK · 0 FAIL · 11 SKIP` (110 s) — sem
  flaky; os SKIPs extras são `prep:*`/`optimize-verify` idempotentes.
- **`make check E2E=1`:** `24 OK · 0 FAIL · 9 SKIP` (293 s); `e2e` **123 s** com **130 testes**
  (**129 passed**, 1 skip condicional `07-cms-shell`), incluindo
  `[cms-serial] › 23-suporte-painel.spec.ts:302 › a equipe consulta o contato do fornecedor e a
  versão` (vazio → preenchido → links seguros → axe → limpeza no `finally`); `perf` **17 passed**;
  `coverage-check` 93,43%. O E2E alvo isolado (`tools/e2e.sh test 23-suporte-painel`) deu **7 passed**
  em `cms-serial`, e a seção `support` terminou vazia (`{}`).

### Riscos/observações

- **Tipo `phone` é evolução aditiva do ADR-046:** o ADR lista os tipos iniciais; o novo tipo valida
  melhor o dado que vira link `tel:`/`wa.me` sem criar chave livre. O valor fora do formato é
  ignorado (mesma política de qualquer campo inválido), e o teste registra explicitamente que
  `(11) 4000-0000` do P0 continua válido. Registrar no `kd` (decisão) para o revisor confirmar.
- **`support` virou seção editável:** sem isso o link “Configurar contato do fornecedor” seria um
  link morto (o contrato da fatia exige link sem dead link). O formulário é o genérico do schema
  (todos os campos escalares), com `PUT` + CSRF + `configuracoes.editar`; a equipe de suporte tem o
  passe livre do `Gate::before`, então o link é acionável para ela.
- **“Novidades” sem fonte:** o pacote não publica notas de versão/manual no deploy; a tela mostra
  apenas a versão e uma frase explícita — nenhum changelog, nenhum link para `/wiki`, nenhum caminho
  local. Se um dia existir superfície de notas, é um link novo com rota real (fora desta fatia).
- **E2E muta a seção `support`:** o spec já roda em `cms-serial` (estado global) e normaliza antes
  e no `finally`; a limpeza também evita que o e-mail opcional apareça na página 503 de outra
  execução. Nenhum outro spec depende da seção.
- **WhatsApp é link de navegação, não embed:** `wa.me` só é montado a partir dos dígitos validados;
  sem JavaScript, sem widget/asset de terceiro (ADR-002). O aviso de nova aba é `sr-only`.



## S7 — Fechamento: E2E, gates e docs (`task_01ged08f`)

### O que foi feito (sem feature nova)

- **Revisão ponta a ponta** das superfícies S1–S6 contra o escopo da fase: nenhum comportamento
  novo; as correções foram de **cobertura** (E2E), **higiene de estado** (E2E serial), **texto da
  fase/ADR** (nomenclatura `painel`, shape plano de manutenção, evento de leitura `painel.view`) e
  **documentação** (INVENTARIO, README da fase, fila de revisão).
- **E2E ampliado** em `23-suporte-painel.spec.ts` (projeto `cms-serial`, 1 worker):
  - 404 do grupo para o perfil máximo em `/suporte`, `/suporte/modulos`, `/suporte/auditoria` e
    `/suporte/manutencao` (antes só `/suporte`);
  - na lista de módulos, `Acesso`/`Configuracoes` aparecem como **Essencial** e `Painel` como
    **Protegido**, **sem** botão de ativar/desativar (a recusa server-side segue coberta pelo
    `PainelModulesCmsTest`, inclusive com `force` forjado);
  - o caso de manutenção passou a **fotografar** mensagem/`retry_after`/`allow_cms` antes da janela
    e a **restaurar** o formulário inteiro no `finally` (antes ficava o texto `… (e2e)` e
    `retry_after=120` no banco de dev); o caso de módulos desliga a manutenção antes de medir
    404/200, então uma execução interrompida não contamina a seguinte.
- **Docs sincronizados:** `INVENTARIO.md` (suporte/ativação/logs/diagnóstico/manutenção/contato →
  FEITO), emenda da ADR-034 (verificação da regra e recuperação só por CLI auditado), emenda da
  ADR-033 (superfície do grupo `Suporte`, token `painel`, 404 do grupo), `P3-suporte-ativacao.md`
  (status `em revisão`, contratos e DoD), `wiki/implementation-phases/README.md` (P3 `em revisão`),
  `_to_revision/README.md` (S7 + fase 🟡) e `_to_revision/_inventario.md`.

### Inventário de arquivos/superfícies (revisão)

- **Superfície/rotas:** `src/modules/Suporte/Painel/{ModuleProvider.php,Routes/cms.php,MODULE.md}`;
  `painel.cms.{index,modules,modules.activate,modules.deactivate,diagnostico,audit,audit.show,maintenance,maintenance.update,contact}`.
- **Ações/DTOs/requests:** `Logic/Actions/*`, `Logic/DTOs/*`, `Logic/Requests/*`,
  `Logic/Support/PainelActivationFailure.php`, `Logic/Controllers/Cms/*`.
- **Views/i18n:** `Layout/Views/cms/*` (7 telas), `Lang/{pt_BR,en,es,fr}/{index,modules,diagnostics,audit,maintenance,contact}.php`.
- **Core:** `Internal/ModuleDiscovery/{Logic/Actions/SetModuleActivation.php,Enums,Exceptions,Console/ActivateModuleCommand.php}`;
  `Internal/Diagnostics/**`; `Internal/Audit/{DTOs,Services/AuditLogReader.php,Migrations/2026_10_25_…}`;
  `Internal/Maintenance/**`; `Internal/Http/Middleware/EnforceMaintenanceMode.php`;
  `Internal/Routing/AreaRegistrar.php`; `Internal/Navigation/PanelMenu.php`;
  `Settings/SettingsSchema.php`; `Lang/{pt_BR,en,es,fr}/{diagnostics,maintenance,settings}.php`.
- **Config/views raiz:** `config/core.php`, `config/diagnostics.php`, `config/audit.php`,
  `resources/views/errors/maintenance.blade.php`.
- **Testes:** `src/modules/Suporte/Painel/Test/*` (6 classes), `src/core/Internal/**/Test/*` (Core),
  `tests/Feature/{ModuleActivationCommandTest,DiagnoseCommandTest,DiagnosticsProbeTest,MaintenanceModeTest}.php`,
  `src/tests-e2e/tests/23-suporte-painel.spec.ts` + história 23.
- **Docs:** `wiki/implementation-phases/{P3-suporte-ativacao.md,README.md,INVENTARIO.md}`,
  `wiki/adr/{ADR-033,ADR-034,ADR-035,ADR-043}`, `wiki/specs/{04-acessibilidade-ux,11-acesso-e-ativacao}.md`,
  `_to_revision/{README.md,_inventario.md,P3-suporte-ativacao.md}`.

### Decisões/riscos que o revisor deve confirmar

- **Token `painel` em vez de `suporte`** (ADR-004/ADR-014): a pasta é `Painel`; a URI continua
  `/suporte` no domínio do painel. O texto da fase foi alinhado (S7) e o registro está no `kd`
  (`decision_0072xd0v`). Eventos: `painel.module_*` (`decision_01w2m428`).
- **Shape plano de manutenção**: `system.maintenance`, `maintenance_message`,
  `maintenance_retry_after`, `maintenance_allow_cms` — campos tipados por seção (ADR-046), sem
  objeto aninhado (`decision_00bgnfi9`).
- **Evento de leitura da trilha**: a via única `audit.access` grava `{module}.view`
  (`painel.view`), não o literal `audit.access` do texto original — uma leitura, um evento.
- **Scheduler `unavailable` por contrato**: o Laravel não expõe heartbeat de `schedule:run`; o
  card mostra a nota e nunca um carimbo inventado.
- **Essencial é estrito (ativar e desativar exigem `--force` no CLI)**: a UI não oferece toggle e
  a recuperação de um essencial desativado pelo CLI é só por
  `core:module:activate {Nome} --force --reason=…` (auditado). Risco operacional aceito; se o
  dono quiser reativação pela UI, é mudança de contrato (nova decisão), não bug.
- **Filtro de ator por trecho do nome** usa `ILIKE` (sem índice de trigrama): `%`/`_` do usuário
  funcionam como curinga; a varredura é limitada pelo intervalo/keyset. Documentado no README do
  Audit; um índice GIN/trigram só com volume medido (ADR-035 não é analytics).
- **Sem export CSV da trilha** (fora do MVP, sem stub/rota) e **sem changelog/manual** (não há
  fonte no deploy): a tela de contato mostra a versão e uma frase explícita.

### Evidência de gates (árvore final, 2026-10-25)

- **`make check` #1:** `22 OK · 0 FAIL · 11 SKIP` — **110,14 s**; cobertura `src/` **93,43 %**
  (15.653/16.754; piso 90 %); PHPUnit **1.163 testes / 19.478 asserções**, `OK`; PHPStan max 0;
  `a11y` estrita OK (sniff CMS; 28 avisos de “contraste não mensurável” já na ledger → 0 `fail`).
- **`make check` #2 (mesma árvore):** `22 OK · 0 FAIL · 11 SKIP` — **110,66 s**; cobertura e
  PHPUnit idênticos (`93,43 %`, `1.163/19.478`) — **sem flaky**. Os 11 `SKIP` são `prep:*`/
  `optimize-verify` gated por staleness (idempotentes), nunca verificação desligada.
- **`make check` #3 (árvore final, já com as edições de documentação):** `22 OK · 0 FAIL · 11 SKIP`
  — **111,46 s**; mesma cobertura (`93,43 %`) e mesmo PHPUnit (`1.163/19.478`) — sem flaky.
- **`make check E2E=1`:** `24 OK · 0 FAIL · 9 SKIP` — **294,29 s**; `e2e` **125 s** com
  **130 testes / 129 passed / 1 skipped** (skip condicional `07-cms-shell`), incluindo os **7** de
  `[cms-serial] › 23-suporte-painel.spec.ts`; `perf` **17 passed** (59 s) e `a11y` OK (11 s);
  cobertura `93,43 %`.
- **Alvo isolado:** `tools/e2e.sh test 23-suporte-painel` → **7 passed** (30,5 s) antes da rodada
  final; estado global conferido depois: `modules_activation {"Menus": true, "Blueprint": true}`,
  `support {}` e `system.maintenance = false` (mensagem/`retry_after` restaurados ao valor
  anterior; o resíduo `… (e2e)`/`120` de execuções antigas foi limpo).
- **`bash tools/docs-check.sh`:** `OK — 13 spec(s) com dono/status e link no índice.`
- **`tools/name-check.sh --grep painel`:** OK (pelo `make check`/`check-gates`).

### Checklist D1–D14 (aplicável)

- **D1/D2** PHP 8.4/Laravel 12: `strict_types`, DI, `readonly`, `match`, sem trait/abstract; rotas
  sem closure, controllers finos, `FormRequest`/`validated()`, `POST`/`PUT` com CSRF, `GET` não
  muta. **OK** (PHPStan max 0/Pint no `make check`).
- **D3** Determinismo: ordem explícita (manifesto grupo→position→nome; checks fixos no registry),
  `Clock` no carimbo do diagnóstico, sem `now()`/`rand()`; E2E serial normaliza/restaura estado.
  **OK**.
- **D4** Fronteiras: módulo só consome Core/Layout; `Diagnostics` não importa módulo/service; a
  sonda S3 usa a fronteira de storage. **OK** (`arch-check`).
- **D5** Zero CDN/iframe no contato; links externos explícitos com `rel=noopener`. **OK**.
- **D6/D7** Um shell por tela, `<h1>` do shell, tabelas com `caption`/`scope`, badges com texto,
  estados vazios, 503 sem JS, foco/alvos do design system; axe A/AA no E2E. **OK**.
- **D8** 4 locales com paridade (módulo + Core `diagnostics`/`maintenance`/`settings`), nenhuma
  string visível fixa; datas via `LocalizedDate`. **OK** (`ModuleI18nTest`).
- **D9** Token `painel` em classes/rotas/views/chaves; Action do Core em `ModuleDiscovery`; eventos
  `painel.*` documentados. **OK** (`name-check --grep painel`).
- **D10** Só migração aditiva de índices do leitor, `down()` remove apenas índices; nenhum HTML/CSS
  no banco; settings como JSON tipado. **OK** (`migration-check`).
- **D11** 404 (não 403) para o grupo, mesmo perfil máximo; `POST`/`PUT` com CSRF e autorização;
  nenhum segredo/PII em log/span/HTML (diagnóstico só classe de exceção; settings mascarados).
  **OK**.
- **D12** Span + `log_event` de mesmo nome por Action (`painel.*`, `core.diagnose`); auditoria
  explícita nas mutações (`painel.module_*`, `painel.maintenance_changed`); leitura da trilha =
  um `painel.view` por request, sem recursão. **OK**.
- **D13** Testes de comportamento (dois requests reais, recusas, motivo, CSRF, keyset sem
  repetir/perder, 503/200, falha de check → 200, imutabilidade); E2E com axe e restauração de
  estado; `make check` 2×. **OK**.
- **D14** Docs/ADR/fase/INVENTARIO/fila/ficha sincronizados; `docs-check` verde; sem stub/link
  morto. **OK**.


## P3.FIX — correções da revisão profunda (`task_00p702f7`, 2026-10-25)

Revisão profunda (8 revisores) sobre S1–S7. Abaixo só o que foi **corrigido agora**, com teste
vermelho→verde próprio; nada de commit/stage.

### Ativação (P2)

1. **`ModuleActivation::setActive` atômico.** O `map()` + `updateOrCreate` era read-modify-write:
   duas escritas concorrentes perdiam atualização e os dois primeiros toques podiam colidir com
   `23505`. Agora: `DB::transaction(..., attempts: 3)` com `insertOrIgnore` da chave, linha lida sob
   `lockForUpdate()`, merge sobre a versão **committed** e invalidação do cache agendada com
   `DB::afterCommit` (`map()` preservado). Testes em `tests/Feature/ModuleActivationTest.php`: merge
   sequencial (`ksort` — `jsonb` não preserva ordem), semente idempotente (`insert ... on conflict do
   nothing`), leitura sob `for update` e rollback com transação externa. **Vermelho observado** ao
   restaurar o `updateOrCreate` (asserção de `for update`: `actual size 0`). A primeira versão usava
   `Cache::forget` logo após o `DB::transaction` interno — com transação externa o `forget` corria
   antes do commit raiz; corrigido em **P3.FIX2**.
2. **`SetModuleActivation` com estado + auditoria na mesma transação** (`attempts: 3`);
   `Artisan::call('core:cache-modules')` e `log_event` só depois do commit. Teste
   `a_failing_audit_writer_rolls_back_the_activation` usa um `AuditWriter` que lança **apenas** no
   evento explícito `painel.module_*` (o evento ambiente do model passa pelo
   `GlobalAuditSubscriber`). **Vermelho observado** sem a transação (`map` continha `Demo`).
3. **`LOCK_EX`** em `SetModuleActivationTest` e `ModuleActivationCommandTest` (mesmo padrão de
   `PainelModulesCmsTest`): o teste reescreve o manifesto compartilhado.
4. **`PainelModulesCmsTest::moduleEvents()`** passou de `core.module.%` para **`painel.module_%`** —
   as duas asserções de “recusa não gera evento” deixaram de ser vacuamente verdes.
5. **Manifesto intocado no módulo vazio**: o teste garante o manifesto antes (`ensureManifest()`),
   fotografa **conteúdo e mtime** e prova que ambos não mudam (antes só checava existência).

### Diagnóstico (P2)

6. **`queue_health` com timeout.** `QueueProbe::summary(string, int)` recebe o timeout do check; no
   driver `redis` a sonda abre cliente **dedicado** (`RedisTarget`, connect/read timeout,
   `OPT_PREFIX`) e lê `lLen {prefixo}{fila}` / `zCard {prefixo}{fila}:delayed` — sem herdar o
   `read_timeout=0` do worker e **sem consumir job**; demais drivers seguem na API nativa. Testes:
   double (timeout repassado), leitura real numa fila exclusiva (`diag-probe-*`, limpa no
   `finally`) e falha rápida com servidor inacessível (<3 s).
7. **S3 sem retries no diagnóstico.** `S3ClientFactory::options()` (função pura) aplica
   `retries => 0` quando há `http` com timeouts curtos; o provisionamento mantém os defaults do
   SDK (`fromDisk` delega). Teste em `StorageBucketTest` (vermelho sem `retries`).

### Auditoria

8. **`PanelAuditFilterRequest`** usa `__('painel::audit.filter_range_invalid')`; teste em locale
   `en` prova a mensagem traduzida (vermelho com a chave crua).
9. **Fim do dia com microssegundos** em `AuditLogReader`: bind `Y-m-d H:i:s.uP` (o grammar do
   Eloquent cortava o `.999999` e perdia o último segundo). Teste com linha `23:59:59.500000`
   (inserida via query builder, como o `AuditService` grava). **Vermelho observado**
   (`actual size 1` vs `2`).
10. **`%`/`_`/`\` literais** no filtro de nome (`likeLiteral()`), documentado no README do Audit
    (sem índice/trigram inventado). Teste com `100%` e `Equipe_` — **vermelho observado** sem
    escape.

### Ficha/E2E/docs

11. **E2E**: `expectNoA11yViolations` em `/suporte/manutencao`; o diagnóstico assere
    `data-check-status` + `data-check-value` (versão `\d+.\d+.\d+`, `PostgreSQL \d+`, scheduler
    `unavailable` + nota) em vez de casar rótulo.
12. **Carimbo do diagnóstico localizado**: `<time datetime="ISO">` com texto `LocalizedDate`;
    `DiagnosticReport`/`PainelDiagnosis` carregam `DateTimeImmutable` (testes de DTO/apresentador
    atualizados e teste novo do `<time>`).
13. **`ModuleDiscoveryService::essentialModules()`** é a fonte única do parsing; usada por
    `SetModuleActivation` e `PainelModulesAction`; teste de guarda impede o retorno do parsing
    duplicado.
14. **Rota documentada** do diagnóstico corrigida para `/suporte/diagnostico` (README do
    Diagnostics, `config/_OTHER_CONFIGS.md`, docblock do `DiagnosticsService`, esta ficha).
15. **“Sem formulário” no leitor da trilha** virou asserção sobre o HTML renderizado: 1 form `GET`
    (filtro) e 1 `POST` (logout do shell), nenhum com ação de mutação.
16. **Ponte pública `/cms[/...]`** do portal incluída na allowlist de manutenção (é só redirect
    para o domínio do painel): teste 301 vs 503 (**vermelho observado**) e spec 11 §4.1 atualizada.
    A ponte **não** é bypass: o conteúdo público segue 503.
17. **ADR-046** ganhou a emenda normativa do tipo **`phone`** (allowlist de tipos +
    `^\+?[0-9 ().-]+$`, 8–15 dígitos, `+` opcional, valor inválido ignorado); o cartão “Canais”
    só renderiza com `PainelContact::hasChannels()` (sem canal não há cartão vazio) — teste novo.
18. **ADR-033**: “404 para qualquer não-equipe” → “usuário **autenticado** não-equipe (anônimo →
    302 login)”, nos dois pontos do texto.

### Riscos aceitos (não corrigidos nesta rodada)

- **WhatsApp sem DDI**: `wa.me` usa os dígitos validados; número local continua aceito pelo schema
  (8–15 dígitos). Não se inventa DDI na apresentação.
- **`scheduler` always `unavailable`**: o Laravel não expõe heartbeat de `schedule:run`; o card
  aponta o cron do host em vez de fabricar carimbo.
- **PII de contato na trilha genérica**: a escrita de settings grava `core.settings.updated` com
  chaves alteradas, não valores; a seção `support` em si não é logada. O e-mail de contato aparece
  na página 503 pública por design (dado institucional).
- **`direction=prev` é janela de keyset** (não histórico numerado) e **sem export CSV** — decisões
  do S4 mantidas.
- **Concorrência do `setActive`**: a prova automatizada é a forma da escrita (transação + `for
  update`); não há teste com dois processos reais (custo/flakiness). O rollback e o merge
  sequencial estão cobertos.

### Evidência (2026-10-25)

- Alvos (PHPUnit): `src/modules/Suporte/Painel/Test` + `ModuleActivationTest` +
  `ModuleActivationCommandTest` + `MaintenanceModeTest` + `DiagnosticsProbeTest` +
  `StorageBucketTest` → **104 testes / 878 asserções**; Core `Diagnostics` **39/178**, `Audit`
  **35/167**, `ModuleDiscovery` **8/31**; `PainelModulesCmsTest` 10/119, `PainelAuditCmsTest`
  14/187, `PainelDiagnosticsCmsTest` 9/116, `PainelContactCmsTest` 10/98, `SetModuleActivationTest`
  8/68, `ModuleActivationTest` 11/28.
- **PHPStan max 0 erros**; **Pint `--test` PASS**; todos os vermelhos acima observados revertendo
  a correção e restaurados em seguida.
- **`make check` #1:** `25 OK · 0 FAIL · 8 SKIP` — **112,71 s** (`docs-check`, `static`/PHPStan max,
  `check-boundaries`, `check-migrations`, `check-gates`, `coverage-check`, `a11y` estrita todos OK).
- **`make check` #2 (mesma árvore):** `22 OK · 0 FAIL · 11 SKIP` — **112,30 s** (os SKIP extras são
  `prep:*`/`optimize-verify`/`upload-chain` gated por staleness) — **sem flaky**.
- **`make check E2E=1`:** `24 OK · 0 FAIL · 9 SKIP` — **300,01 s**; `e2e` **131 s** com **129 passed
  / 1 skipped**, incluindo `[cms-serial] › 23-suporte-painel.spec.ts` (diagnóstico com
  `data-check-status`/`data-check-value` e axe na tela de manutenção) e o fluxo de manutenção;
  `perf` **60 s / 17 passed**.
- **Cobertura:** `src/` **93,42 %** (15.699/16.805; piso 90 %) e PHPUnit **1.179 testes /
  19.560 asserções**; `phpstan` max **0**; `pint --test` PASS; `docs-check` OK.

## P3.FIX2 — correção da verificação independente (`task_01ky0cum`, 2026-10-25)

Verificação independente do P3.FIX: **17 pontos conferidos**, 1 achado **P2** (regressão introduzida
pelo próprio P3.FIX) e 4 resíduos baratos. Corrigido agora, com teste vermelho→verde próprio; sem
commit/stage.

### P2 — invalidação do cache antes do commit da transação externa

1. **`ModuleActivation::setActive`** trocou `Cache::forget(…)` por
   `DB::afterCommit(static fn () => Cache::forget(self::CACHE_PREFIX.$module))`. O `DB::transaction`
   interno é **savepoint** quando há transação externa: o `forget` antigo corria na liberação do
   savepoint, antes do commit raiz, e outra request podia recachear o valor *committed* antigo por
   até 60 s (TTL). Com `afterCommit`, sob transação externa a chave só é esquecida no commit raiz,
   o rollback não dispara callback e o uso avulso (sem transação pendente) invalida na hora.
   Testes em `tests/Feature/ModuleActivationTest.php`:
   `the_cache_key_survives_until_the_enclosing_transaction_commits`,
   `the_cache_key_is_never_invalidated_when_the_enclosing_transaction_rolls_back` e a asserção de
   `Cache::has()` imediato em `test_the_activation_cache_is_invalidated_on_toggle`.
   **Vermelho observado** ao restaurar `Cache::forget` (2 falhas: chave ausente dentro da transação
   e ausente após o rollback).
2. **`before` da auditoria sob lock** (risco residual da verificação, corrigido): `setActive()`
   passou a devolver o valor anterior **lido da linha travada** (`?bool`; `null` = ausente no mapa)
   e `SetModuleActivation` aplica o default do manifesto ao `before` — antes o valor vinha de
   `isActive()` (cache, possivelmente defasado) **fora** da transação. Contrato preservado: `void`
   → `?bool` é compatível com os chamadores existentes (o retorno era ignorado). Teste novo
   `the_audit_before_is_the_locked_row_and_not_a_stale_cache_value` (cache “quente” diz ativo,
   banco diz inativo). **Vermelho observado** com a leitura anterior por `isActive()`.

### Resíduos baratos

3. **`PainelModulesCmsTest::moduleEvents()`** usa
   `whereIn('event', ['painel.module_activated','painel.module_deactivated'])` em vez de
   `like 'painel.module_%'` — o `_` do `LIKE` é curinga e casaria também `painel.modules`
   (hoje só `log_event`, não auditado; a asserção passa a ser exata independentemente disso).
4. **`AuditLogReader`** formata os **dois** limites com `Y-m-d H:i:s.uP`: antes `from` era `Carbon`
   (o grammar formata `Y-m-d H:i:s`, sem offset/µs, dependendo da TZ da sessão do PostgreSQL) e
   `to` era string com offset. Teste novo
   `both_interval_bounds_carry_the_same_offset_and_fraction` (limites `-03:00` com fração,
   linhas UTC). **Vermelho observado** com o bind antigo (`from` sem offset incluía uma linha a
   mais).

### Riscos aceitos (registro, sem código)

- **`SetMaintenanceMode` não é atômico com o evento explícito**: `Settings::put()` commita (e
  audita `core.settings.updated` na própria transação) antes de `audit->record('painel.maintenance_changed')`;
  se a trilha explícita falhar, o estado fica gravado e a resposta é 500 (a trilha genérica
  permanece). Diferente do invariante de ativação (estado + evidência na mesma transação); corrigir
  exigiria mudar a porta `Settings` ou abrir transação em volta dela — custo desproporcional ao
  risco (a escrita é uma tela única de operação e a perda é só do evento explícito).
- **`before` residual**: agora vem da linha travada na **mesma** transação da escrita; a
  serialização entre dois toggles continua provada pela forma (`FOR UPDATE`), sem teste com dois
  processos reais (mesmo risco aceito do P3.FIX).

### Evidência (2026-10-25)

- Alvos: `ModuleActivationTest` + `SetModuleActivationTest` + `AuditLogReaderTest` +
  `PainelModulesCmsTest` + `ModuleActivationCommandTest` → **49 testes / 329 asserções**, verdes.
- **PHPStan max 0 erros**; **Pint `--test` PASS** nos arquivos tocados.
- **`make check` #1:** `24 OK · 0 FAIL · 9 SKIP` — **116,62 s**; PHPUnit **1.183 testes /
  19.582 asserções**; cobertura `src/` **93,42 %** (15.703/16.809; piso 90 %).
- **`make check` #2 (mesma árvore):** `22 OK · 0 FAIL · 11 SKIP` — **109,55 s** (os SKIP extras são
  `prep:*`/`optimize-verify`/`upload-chain` gated por staleness) — **sem flaky**.
- **`make check` #3 (árvore final, já com estas edições de documentação):** `22 OK · 0 FAIL ·
  11 SKIP` — **109,26 s**; mesma cobertura e mesmo PHPUnit — sem flaky.
- **`make check E2E=1`:** `24 OK · 0 FAIL · 9 SKIP` — **304,79 s**; `e2e` **131 s** com
  **129 passed / 1 skipped** (inclui `[cms-serial] › 23-suporte-painel.spec.ts`), `a11y` OK e
  `perf` **17 passed** (56,8 s); cobertura `93,42 %`; `docs-check` OK já com as edições desta seção.
- **Vermelhos observados** (revertendo cada correção e restaurando em seguida): `Cache::forget`
  antes do commit (2 falhas), `before` via `isActive` fora da transação (1), bind `from` sem
  offset/µs (1).
- `bash tools/docs-check.sh` → `OK — 13 spec(s) com dono/status e link no índice`.
- **Sem commit/stage/sync**; `notas/`/`vendor/` intocados; nenhum gate desligado.

## Notas da revisão

> Revisão profunda concluída; verificação independente final aprovou o P3.FIX2 em 2026-10-25.

| Data | Revisor | Resultado | Observações |
|---|---|---|---|
| 2026-10-25 | revisão profunda P3 (8 revisores) | 🟡 achados corrigidos | 18 achados aplicados em `task_00p702f7` (seção P3.FIX); falhas de concorrência/atomicidade, timeout de fila/S3, microssegundos e curingas, a11y/E2E, rotas e docs. Verificação independente pendente. |
| 2026-10-25 | verificação independente → correção (`task_01ky0cum`) | ✅ aprovado | 1 achado **P2** (invalidação de cache antes do commit raiz) e 4 resíduos baratos: `DB::afterCommit`, `before` lido sob lock, `whereIn` exato no teste, bounds do leitor com o mesmo `Y-m-d H:i:s.uP`. `SetMaintenanceMode` (atomicidade) registrado como risco aceito. Seção P3.FIX2. Verificação final: nenhum achado P0–P3; suíte 1.183/19.582, cobertura 93,42%, `make check` 2× e E2E 129/1. |
