# F4.I3.T3 — Admin surface só em /cms (nunca rota pública)

| Campo | Valor |
|---|---|
| **Fase** | F4 — Produto & CMS |
| **Issue** | F4.I3 — Widgets, banners e builder (plano 16) |
| **kd task** | `task_017w7gyn` (`closed`) |
| **Âncora** | `wiki/implementation/04-produto-cms.md` |
| **Status da revisão** | 🔧 com correções |

## O que foi implementado

- Service `Layout` expõe `Routes/cms.php` (`layout.cms.*`) com `Permission::authorize('Layout', …)` (deny-by-default) e views no shell `<x-layout::cms>`.
- Services **declaram** permissões no `config()`; `AccessGate`/`CmsUserSeeder` registram (`layout.*`). E2E `10-cms-layout`.

## Arquivos para revisar

- `src/services/Layout/Routes/cms.php`
- `src/services/Layout/Http/Controllers/Cms/*`
- `src/services/Layout/Http/Requests/SaveLayoutProfileRequest.php`
- `src/services/Layout/View/cms/*.blade.php`
- `src/services/Layout/Lang/{pt_BR,en,es,fr}/cms.php`
- `src/core/Access/AccessGate.php`
- `src/core/Area/Cms/Database/Seeders/CmsUserSeeder.php`
- `src/services/Layout/Test/LayoutAdminTest.php`
- `src/tests-e2e/tests/10-cms-layout.spec.ts`

## Pontos de revisão

### PHP 8.4
- [ ] `declare(strict_types=1)`; tipos nativos (union/enum/`readonly`); sem `mixed` desnecessário.
- [ ] Dependências **injetadas** (nenhum `new`/`app()`/`resolve()` no corpo); sem traits/abstract/generics.
- [ ] `match` em vez de cadeias de `if`; `?->`/`??`; sem código morto; PHPStan `max` limpo.

### Laravel 12
- [ ] Nativo primeiro (Eloquent casts/scopes, `FormRequest`, `Gate`, scheduler, events) — sem reimplementar.
- [ ] Sem closure em rota; resources/rotas nomeadas; `config()` só lê, `env()` só em `config/`.
- [ ] Migração **idempotente**; `down()` → `backup_*` (nunca `DROP`); sem `orderBy` cru quando `latest/oldest` serve.

### HTML semântico + a11y (eMAG/WCAG 2.2 AA)
- [ ] Um `<main>`, headings em nível correto, landmarks; só ARIA quando necessário.
- [ ] Alvos ≥ 24 px (≥ 44 px em ponteiro grosso); foco visível; rótulos em todos os campos; contraste (axe).
- [ ] Funciona **sem JS**; `sniffCSS` sem `fail`/`warn` fora do ledger.

### CSS moderno
- [ ] `oklch`, `clamp`, `rem`/`fr`/`dvh`/`%`; `@layer` (a11y por último); `@property`; subgrid com fallback.
- [ ] Sem `!important`, sem `style="…"` inline, sem hex/rgb (exceto justificado); sem CDN.

### Decisões do projeto (ADRs)
- [ ] Respeita a ADR do escopo (ver cabeçalho) e não escapa das demais (001, 002, 004, 013, 015…).
- [ ] Determinismo: relógio/id injetados, ordem explícita, dados idempotentes (ADR-001).

### i18n (ADR-012)
- [ ] Nenhuma string visível fixa; 4 locales com paridade de chaves; `route()`/`localized_url()` (nunca path literal).

### Observabilidade/segurança/testes
- [ ] Span + `log_event('{module}.{action}')` com atributos `scalar|null`, **sem PII** (ADR-024).
- [ ] Teste que falha sem a mudança (vermelho→verde) + evidência no `kd`; `make quality` 2×.

### Específicos desta task

- [ ] Rota existe **só** no domínio do CMS; `403` sem permissão; service não importa `layout`.
- [ ] Lang `layout-service::` (sem colisão com a camada `layout`).

## Comandos úteis

```bash
make quality            # lint + PHPStan max + gates + testes
make e2e                # Playwright + axe (se a task toca UI)
make check-boundaries   # fronteiras core/services/layout/modules
```

---

> Revisar contra `AGENTS.md`, `wiki/adr/` e o plano citado. Registrar achados no `kd` (`kd write --anchor`) e, se houver correção, teste de regressão.

## Notas da revisão

| Data | Revisor | Resultado | Observações |
|---|---|---|---|
| 2026-10-18 | agente | 🔧 | **A** (**alto**) o formulário de layout **não mostrava erro de validação nenhum**: os erros viviam só na sessão (`assertSessionHasErrors`, o único gate) e a view não tinha resumo nem mensagem por campo — o usuário via o formulário recarregado sem explicação (WCAG 3.3.1 A) → resumo em `<x-cms::alert variant="error" id="form-errors">` (`role="alert"`), o `scope` migrado para o `<x-cms::form-group>` da casa (`aria-invalid` + `aria-describedby` + mensagem `role="alert"`), `aria-invalid`/`aria-describedby` nas linhas de placement, `attributes()` no `FormRequest` (nomes legíveis) e a chave `error_summary` nos 4 locales — teste PHPUnit e e2e **vermelhos antes** (`fact_01dcdocr`); **B** (**alto**) o **estado de erro reprovava contraste** (`#ef4444` sobre `#f9fafb` = **3.6:1**, WCAG 1.4.3 AA) — descoberto porque o e2e novo é o **primeiro** a renderizar um estado de erro sob axe (nenhum spec de CMS/portal submetia formulário inválido) → a raiz era `--status-danger` (`oklch(63.7% …)`, red-500) usado como **texto** por `text-error` (`form-group`, `editor.css`, `content.css`), escurecido para `oklch(50.6% 0.1927 27.7)` (≈5.5:1; o mesmo token pinta o fundo do alerta) — e2e **vermelho → verde** (`fact_01uyxbdt`); **C** (**alto, transversal → task** `task_00inehom`) **o app não tem `lang/`**: `__(validation.required)` devolve a **chave crua** (medido; `app()->getLocale()` = pt_BR e `fallback_locale` = pt_BR) → todo formulário do produto (o DSAR do cidadão inclusive) mostra `validation.required` na tela (`risk_01shfi4j`); **D** (médio → task `task_00lzwcrb`) convidado numa admin surface de **service** recebe **403** em vez do login (`cms.localhost/layout` = 403 × `cms.localhost/noticias` = 302, medido) porque `requireModuleRoutes()` só envolve rotas de **módulo** — e as raízes do Seo no mesmo `Routes/cms.php` precisam seguir públicas (`risk_01oagqo4`); **E** (médio) a rota de **edição** (`PUT /layout/{profile}`) não tinha **nenhum** teste e o ramo *ignore-self* do escopo único estava descoberto → teste novo (atualiza mantendo o próprio escopo); cobertura de `src/` **90,84 → 91,13%** (`fact_00r782qf`); **F** (baixo) `SaveLayoutProfileRequest` reimplementa o escopo único com um closure e `DB::table('layout.layout_profiles')` em vez do nativo `Rule::unique(...)->ignore($profile)` (já usado no projeto); **G–J** (obs) a admin surface de service **não aparece na navegação** (`PanelMenu` monta do manifesto de módulos; `/layout` só por URL digitada), a agenda `datetime-local` fica **sem fuso** no `jsonb` (não é `timestamptz`), sem `throttle` nas escritas do CMS (já anotado em #26) e o alvo mínimo do **CMS** não é medido por e2e (o axe não cobre `target-size` em A/AA) — `risk_009tjwbb`. **Verificado OK:** `declare(strict_types=1)`; sem closure em rota; rotas nomeadas; admin surface **só** no domínio do CMS (testado: host do portal → 404); deny-by-default por ação (`layout.ver`/`layout.editar`, 403 sem permissão); o service **declara** `permissions` no `config()` e o `AccessGate` registra abilities de **services** (ADR-015/033); namespace `layout-service::` sem colisão com a camada `layout` (4 locales com paridade); leituras sensíveis passam pelo middleware `audit.access` por `config('audit.sensitive')` — a tela de configuração de layout **não** é dado pessoal e corretamente não entra na trilha; migrações idempotentes com `backup_*` (T2). `make check E2E=1`: 20 OK · 0 FAIL · 1 SKIP, e2e **91 passed**, cobertura **91,13%**. |

> Resultado: `✅ revisado` · `🔧 com correções` · `⏭️ delegado` (com justificativa).
