# P1.S3 — editor do shell do CMS (Configurações → Painel)

| Campo | Valor |
|---|---|
| **Fase** | P1 — Estrutura geral do site e do CMS |
| **Issue** | P1.S3 — editor do shell (`Configurações → Painel`) com `CmsShellResolver` lendo settings, 422/403 e DOM/foco por posição |
| **`kd` task** | `task_01lp1zcw` (épico `epic_01vhakc0`) |
| **Âncoras** | `wiki/implementation-phases/P1-estrutura-site-cms.md` §3/S3, §5–§8 · `wiki/implementation-phases/TECNICO.md` §14 · ADR-036, ADR-016, ADR-046, ADR-012, ADR-025 |
| **Status da revisão** | ✅ revisado (2026-10-24) |

## O que foi implementado

- **`Configurações → Painel`** (`/configuracoes/cms`): `cms` entrou em
  `ConfiguracoesSections::EDITABLE`; o índice lista a seção com rótulo/descrição nos 4 locales
  (“Painel”). Nenhuma seção nova de rota, nenhum endpoint chave/valor.
- **Formulário dedicado, sem JSON**: `ConfiguracoesShellEditor` expõe um controle por campo —
  `title`, `logo`, `color_primary`, `color_accent` (texto), checkbox por região
  (`regions.{header,footer,sidebar}.enabled`), `sidebar_position` (`left|right|orbital`) e
  `sidebar_width` (escala fechada de `config/cms.php`). O documento `cms` não é editado como JSON
  (sem `<textarea>`/campo `name="regions"`).
- **Uma única verdade de leitura × escrita**: `Layout\Cms\Shell\CmsShellSchema` centraliza a forma
  fechada (cor `oklch()`, logo same-origin/`https://`, título ≤120, posições e escala de largura);
  `CmsShellResolver` passou a delegar a ela e o editor valida a mesma forma. Valor aceito na
  escrita é exatamente o que a leitura entende.
- **Validação com rótulo e sem escrita parcial**: qualquer campo inválido responde **422**
  reapresentando o formulário (`configuracoes::validation.invalid` + `:attribute` traduzido) e nada
  é gravado; campo de identidade vazio é **omitido** (= herda `config/cms.php`/tema do tenant).
  Região ausente ou posição/largura ausentes também são 422 (não se “adivinha” estrutura).
- **Autorização antes de validar**: `UpdateConfiguracoesRequest::authorize()` e o controller
  chamam `Permission::authorize('Configuracoes', EDIT)` antes de qualquer validação/merge; `ver`
  recebe 403 (nunca 422/302).
- **Gravação pela porta**: `Settings::put('cms', ...)`; a seção inteira é gravada (todos os campos do
  schema `cms` estão no formulário), o Core valida de novo, audita (`core.settings.updated`) e
  invalida `settings.cms` — o próximo request que renderiza o shell já reflete a mudança.
- **Precedência explícita do `CmsShellResolver`**: instância → settings (banco → `config/cms.php` →
  default do schema). `null`/ausente herda; valor inválido no banco **não** sombreia a configuração
  (schema normaliza e o resolver valida de novo); valor inválido de settings é logado
  (`core.settings.invalid`, só `section`/contagem/`fields`).
- **Estrutura preservada**: nenhuma mudança de markup do shell; região desligada remove o landmark;
  `left` mantém a sidebar no DOM **antes** do `<main>`, `right` **depois** e `orbital` substitui a
  coluna por popover nativo. Sem `<style>`/`style=`/JS inline; CLS 0 (posição é CSS, não medição).

## Fora desta fatia (não implementado de propósito)

- **`cms.logo` como `media_id`**: o campo aceita caminho same-origin ou `https://`, como o schema da
  S1 e o resolver; um UUID do catálogo **não** é resolvido em `<x-service-media::picture>` nesta
  fatia (o resolver o ignora e o editor o recusa — não vira `src` inválido). Fica para a fatia que
  ligar o catálogo ao shell.
- **Seletor de cor com conversão para `oklch()`** (ADR-036, “custos”): sem conversor de cor no PHP e
  sem JS inline, o campo é texto `oklch()`; hex/rgb são recusados no painel (422), em vez de
  convertidos em silêncio.
- Menus (P1.S4), ação de layout por área (P1.S4b) e SEO (P1.S5).
- Axe completo da tela: gate do P1.S6; aqui há as checagens estruturais (labels, `fieldset`,
  `aria-describedby`, ausência de inline).

## Arquivos para revisar

- `src/layout/Cms/Shell/CmsShellSchema.php` (novo)
- `src/layout/Cms/Shell/CmsShellResolver.php`
- `config/cms.php` (comentário da camada de settings)
- `src/modules/Infraestrutura/Configuracoes/Logic/ConfiguracoesSections.php`
- `src/modules/Infraestrutura/Configuracoes/Logic/ConfiguracoesShellEditor.php` (novo)
- `src/modules/Infraestrutura/Configuracoes/Logic/Controllers/Cms/ConfiguracoesEditController.php`
- `src/modules/Infraestrutura/Configuracoes/Logic/Controllers/Cms/ConfiguracoesUpdateController.php`
- `src/modules/Infraestrutura/Configuracoes/Logic/Requests/UpdateConfiguracoesRequest.php`
- `src/modules/Infraestrutura/Configuracoes/Layout/Views/cms/configuracoes-shell.blade.php` (novo)
- `src/modules/Infraestrutura/Configuracoes/Lang/{pt_BR,en,es,fr}/cms.php`
- `src/modules/Infraestrutura/Configuracoes/MODULE.md`
- `src/modules/Infraestrutura/Configuracoes/Test/ConfiguracoesShellTest.php` (novo)
- `tests/Feature/CmsShellTest.php`

## Pontos de revisão

### D1 — Simplicidade / PHP 8.4
- [ ] `declare(strict_types=1)`, tipos em parâmetros/retorno; DI por construtor no editor
      (`CmsShellResolver`, `Settings`), sem `new`/`resolve`/`app()` no corpo.
- [ ] Sem trait/abstract/generic/`Result`; `CmsShellSchema` é função pura de configuração
      (`config('cms.sidebar_widths')`) — sem estado.
- [ ] `CmsShellResolver` perdeu validadores duplicados (`text`/`url`/`isOklch`/`position`/`width`)
      e delega à forma fechada; nenhuma abstração além do schema com dois consumidores
      (leitura × escrita).

### D2 — Laravel 12
- [ ] Nenhum `env()` novo (config só em `config/cms.php`); `config()` apenas para a escala.
- [ ] `FormRequest` normaliza checkbox (`"1"`/`"0"` → bool) e expõe `shellPayload()` em lista
      explícita (nunca `all()`); `rules()` do request não duplica o editor.
- [ ] Controller: autorização → validação (mesma verdade da leitura) → porta → 422/redirect; sem
      regra de negócio duplicada.

### D3 — Determinismo
- [ ] Ordem fixa: campos de identidade → regiões (`CmsShellSchema::REGIONS`) → posição → largura;
      regiões normalizadas na ordem declarada; nenhuma chave de `jsonb` dependente de ordem física.
- [ ] Sem `now()`/`rand()` no domínio; testes sem relógio/rede.
- [ ] Mesma entrada ⇒ mesmo `CmsShell`; sem settings = shell anterior (teste de paridade HTML).

### D4 — Fronteiras
- [ ] `core` não importa `layout` (intocado); o módulo importa `Layout\Cms\Shell\{CmsShellSchema,
      CmsShellResolver}` — permitido pela matriz (`modules → core, layout, próprio módulo`).
- [ ] `layout` não importa módulo; `arch-check` OK.

### D5 — Dependências / licenças / CDN
- [ ] Nenhuma dependência nova, nenhum asset/CDN; a tela usa só componentes existentes do shell.

### D6 — CSS / Tailwind / daisyUI
- [ ] Nenhum CSS novo; o editor reusa `form-control`/`select`/`checkbox`/`label-text` do design
      system; `shell.css` não foi tocado nesta fatia.
- [ ] Sem `!important`, `px`, `style=`, `<style>`; posição/largura continuam em atributos `data-`
      consumidos pela folha existente (CLS 0).

### D7 — HTML semântico & acessibilidade
- [ ] Um único shell `<x-layout::cms>`; só conteúdo no slot; um `<h1>` (shell); `fieldset/legend`
      para identidade, estrutura e regiões.
- [ ] Cada controle tem `<label for>`; ajuda/erro em `aria-describedby`; erro com `aria-invalid` e
      `role="alert"` no campo + resumo; `autofocus` no primeiro erro; sem JS obrigatório.
- [ ] Ordem de foco = ordem visual: `left` (sidebar antes do `<main>`), `right` (depois),
      `orbital` (popover nativo, menu acionável); região off remove o landmark, sem heading órfão.
- [ ] Checkbox de região sempre envia `true`/`false` (hidden + checkbox), então o estado não
      desaparece silenciosamente.

### D8 — i18n
- [ ] 4 locales com as mesmas chaves (seção, identidade, regiões, posição/largura, erros);
      nenhuma string visível fixa; `route()` para navegação interna; `<html lang>` é do shell.
- [ ] Rótulo do campo usado como `:attribute` na mensagem de validação.

### D9 — Grep-friendly
- [ ] Arquivos novos com o token `configuracoes` no nome (`ConfiguracoesShellEditor`,
      `configuracoes-shell.blade.php`, `ConfiguracoesShellTest`); `name-check.sh` OK.

### D10 — Dados / banco
- [ ] Nenhuma migração; `cms.regions` continua `jsonb` de `{região: {enabled: bool}}` (sem
      HTML/CSS/classe); posição/largura são enums da escala.
- [ ] Salvar a seção não perde campos: o formulário cobre **todos** os campos do schema `cms`.

### D11 — Segurança / LGPD
- [ ] `Permission::authorize` antes de validar/gravar; `ver` → 403; inválido → 422; sem 500.
- [ ] Logo só caminho same-origin (`/…`, sem `//`) ou `https://`; `javascript:`/`data:` recusados;
      cor só `oklch()` sem `;{}<>url()@`.
- [ ] Nenhum valor de campo em log/auditoria (o Core registra `section`/contagem; o resolver loga
      `fields` de um conjunto fechado, sem valor); sem PII.

### D12 — Observabilidade / auditoria
- [ ] Nenhum span novo no módulo; escrita usa `core.settings.write`/`core.settings.updated` da
      porta (auditoria before/after redigida pelo Core); `core.settings.invalid` só com escalares.

### D13 — Testes
- [ ] Shell: 3 posições **via settings** com ordem de DOM, cada região desligada, identidade do
      banco, instância vencendo settings, valor inválido no banco não sombreando `config/`,
      paridade sem linha de settings.
- [ ] Editor: render por campo (valores salvos/efetivos), save válido refletido no próximo shell,
      iguais vazios = herda, 422 com rótulo (posição/largura/logo/cor/região ausente), 403 antes da
      validação, 422 preservando a camada anterior, 4 locales.
- [ ] Checagens estruturais: `<label for>`/`id`, `aria-describedby`, ausência de JSON/`<textarea>`/
      `<style>`/`style=`/`<script>` inline.
- [ ] Suíte direcionada + `make check` (evidência abaixo).

### D14 — Documentação
- [ ] `MODULE.md` atualizado (seção editável, `CmsShellSchema`, formulário, 422/403, sem media-id).
- [ ] Esta ficha + linha 63 do `README.md`.

## Evidência

- `docker compose exec -T php-fpm ./vendor/bin/phpunit --filter='CmsShellTest'` → **25 testes,
  141 asserções, OK** (inclui as 6 provas novas via settings).
- `docker compose exec -T php-fpm ./vendor/bin/phpunit --filter='ConfiguracoesShellTest'` →
  **12 testes, 219 asserções, OK**.
- Regressão das áreas tocadas (`CmsShellTest|Configuracoes*Test|SettingsSchemaTest|ModuleI18nTest|
  LayoutContractTest|SettingsDrivenSiteTest|DocsReferencesTest|I18nTest|Adr004GrepFriendlyTest`)
  → **121 testes, 3.912 asserções, OK**.
- `docker compose exec -T php-fpm ./vendor/bin/phpstan analyse --memory-limit=1G` → **0 erros**
  (nível max).
- `pint` nos arquivos alterados → PASS.
- `docker compose exec -T php-fpm php tools/arch-check` → OK; `tools/name-check.sh` → OK;
  `tools/docs-check.sh` → OK.
- `git diff --check` → sem erros de whitespace.
- `make check` (2×, na árvore final) → **24 OK · 0 FAIL · 9 SKIP** (e2e/perf opt-in),
  **105,36 s** e **106,30 s**; `coverage` + `coverage-check` OK; `a11y` (sniff, strict) OK com os
  mesmos `warn` de “transparent background” do `/cms/login` já da ledger.

## Riscos e observações

- **Salvar congela a camada de `config/` para a seção**: o formulário mostra os valores **efetivos**
  (config + settings), então salvar grava posição/largura/regiões explícitas — igual ao editor de
  tema (S2). Campos de identidade vazios continuam herdando; posição/largura/regiões são escolhas
  explícitas e passam a morar no banco (mudar `config/cms.php` não as sobrescreve mais).
- **Painel não aceita hex/rgb para a identidade**: o resolver só publica `oklch()` (ADR-036). O
  formulário recusa (422) em vez de converter; a ajuda do campo explica o formato.
- **Logo não resolve `media_id`**: caminho same-origin ou `https://`, como na S1. A variante por
  catálogo depende da fatia de Media → shell.
- **Largura é atributo `data-*` consumido pelo CSS existente**: um valor fora da escala nem chega a
  ser salvo (422), então não há classe/estilo derivado de entrada livre.
- **Regiões exigem envio explícito**: request manipulada sem uma região responde 422 (não desliga a
  região por omissão). O formulário sempre envia (hidden + checkbox).
- **`section('cms')` resolve fallback de `config/` antes do resolver**: um valor inválido de
  `config/cms.php` também aparece como “settings inválido” no log `core.settings.invalid` (o
  resolver não distingue a origem). É ruído de diagnóstico, sem efeito no shell.
- **Teste de paridade compara HTML completo** (sem `render` de request): o token CSRF vem da mesma
  sessão de teste; se o gate passar a variar o token por chamada, trocar por comparação do
  `CmsShell` resolvido.
- **Axe/sniff completos** ficam para o P1.S6; aqui a prova é estrutural (labels, landmarks,
  ordem de DOM, ausência de inline).

## Notas da revisão

| Data | Revisor | Resultado | Observações |
|---|---|---|---|
| 2026-10-24 | omp (S3) | ✅ | 0 bloqueador/alto/médio. Verificado no código: precedência instância → settings → `config/` com `null` herdando e valor inválido nunca sombreando a camada anterior (CmsShellResolver.php:30-88, `enabled()` 215-224; settings inválidas caem no default/fallback do `SettingsSchema`); leitura × escrita delegam **100%** a `CmsShellSchema` — o resolver não tem mais regex própria (CmsShellSchema.php:20-120) e o editor valida com `rules()`/`save()` chamando os mesmos métodos (ConfiguracoesShellEditor.php:103-164/166-221), então não há duplicação divergente; `cms.regions` é `{região:{enabled:bool}}`, normalizado em ordem fixa e com chave desconhecida descartada (SettingsSchema.php:180-186/557-586), compatível com `config/cms.php`; cor só `oklch()` e logo só same-origin/`https://`, sem `javascript:`/`data:`/`//`, com media-id recusado (CmsShellSchema.php:79-120 + MODULE.md); posição decide o DOM (`left` antes, `right` depois do `<main>`, `orbital` sem coluna) e região desligada remove o landmark (Components/cms.blade.php:109-170; CmsShellTest); largura só da escala de `config/`, com o CSS coberto por teste; 422 reapresenta o formulário com rótulo traduzido e sem escrita parcial, e 403 vem antes da validação (ConfiguracoesUpdateController.php:112-147; UpdateConfiguracoesRequest.php:31); 4 locales e ausência de `<style>`/`style=`/`<script>` inline provadas em ConfiguracoesShellTest; paridade de HTML e “próximo request” cobertas. Baixo/Observação (sem bloqueio): (a) `CmsSidebarPosition::fromValue()` perdeu o único chamador quando o resolver passou a usar `CmsShellSchema::position()` (CmsSidebarPosition.php:18) — remover para não manter duas semânticas de fallback; (b) a escala de largura e as posições existem em três lugares (enum, `CmsShellSchema::POSITIONS`/`widths()` lendo `config/`, enum fixo em SettingsSchema.php:189-204): acrescentar valor só no `config/` faria o editor oferecê-lo e a porta recusar com **302** em vez de 422 — hoje as listas são idênticas, mas registrar; (c) logo `https://` externo é aceito e prometido pela ajuda, mas o CSP do painel (`docker/web/Caddyfile:118`, `img-src 'self' data:`) bloqueia origem externa: a capacidade é anterior à fatia (teste do F4 a fixa) e aqui só ficou visível ao operador — restringir a same-origin (ou ampliar a CSP) e ajustar a ajuda; (d) ADR-036 (“custos”) fala em converter a cor para `oklch()` e a tela entrega texto com 422 para hex — desvio já intencional/documentado na ficha; propor nota/emenda na ADR. |
