# P0 — Fundação do painel: configurações tipadas e primitivas de administração

| Campo | Valor |
|---|---|
| Programa | F7+ — operação, estrutura e conteúdo |
| Status | `implementado e revisado` (ficha `_to_revision/P0-fundacao-painel.md`) |
| Depende de | F6 fechada (deploy, backup, restore) |
| Habilita | P1–P13 |
| Tamanho | **G** (4–6 sessões) |
| ADRs | **ADR-046 (aceita)** — configurações tipadas; ADR-016/017/035/036 |

## 1. Objetivo

Criar a **fonte única de configuração administrável** do produto — tipada, validada, cacheada,
auditada e sem key/value livre — e as primitivas de tela que todas as fases seguintes reutilizam.
Sem esta fase, cada módulo inventa sua própria tabela de settings, e o painel repete o
`configurations` do legado (chave sem schema, N+1, `config.json` na raiz, valor inválido em
produção).

**Resultado observável:** uma tela `Configurações → Geral` (identidade da instituição) grava em
`core.core_settings`, invalida o cache na hora, audita antes/depois e sobrevive a um valor inválido
sem quebrar o portal.

## 2. Por que agora (evidência)

- Não existe tabela/camada genérica de settings: a única “configuração em banco” é
  `core_module_discovery_settings` (`src/core/Internal/ModuleDiscovery/`), específica do ADR-034.
- Identidade, tema e shell vêm de `config/design.php`, `config/cms.php` e `config/seo.php`; a
  instância não tem como sobrepor sem deploy.
- O CMS já tem o shell e os componentes (`src/layout/Cms/components/*`: card, table, form-group,
  alert, modal, empty-state, status-badge), mas não tem um padrão de formulário por seção,
  leitura/escrita de configuração nem resumo de erros reutilizável.
- O dossiê `configuracoes-da-instituicao` documenta exatamente o risco: chave sem índice único,
  regra dinâmica por campo, `config.json` posicional e cache por `pull`. Nada disso entra.

## 3. Escopo fechado

### S1 — ADR-046 + schema + tabela + contrato

- Escrever/registrar a **ADR-046 (aceita)**: configuração é dado tipado, schema em código, escopo de
  instância, cache por seção, segredos cifrados, auditoria com before/after, nenhuma chave fora
  do schema.
- Migração `core.core_settings` (um **documento JSON por seção**, decisão D1/ADR-046):

| Coluna | Tipo | Regra |
|---|---|---|
| `id` | `uuid` PK | `uuidv7()` |
| `section` | `text` | não vazio; regex `^[a-z][a-z0-9_]*$`; **UNIQUE** |
| `payload` | `jsonb` | `NOT NULL DEFAULT '{}'`; `CHECK (jsonb_typeof(payload) = 'object')` |
| `schema_version` | `integer` | default `1`; permite evolução sem quebrar leitura |
| `encrypted_payload` | `text` nullable | campos `secret`, cifrados; nunca no `payload` público |
| `updated_by` | `uuid` null | FK `cms_users` `nullOnDelete` |
| `created_at`/`updated_at` | `timestamptz` | — |

  Índice por `section` (o UNIQUE já cobre a leitura). `down()` não dropa: move para
  `backup_core_settings_<ts>` (ADR-017).
- Contrato `Core\Contracts\Settings` (ou `SettingsReader` + `SettingsWriter`, se a escrita precisar
  de assinatura distinta): `get(string $section, string $key, mixed $default = null)`,
  `section(string $section, bool $maskSecrets = false): array`, `put(string $section, array $values, ?User $actor = null)`,
  `flush(?string $section = null)`.
- `Core\Settings\SettingsSchema`: definição declarativa por seção, **sem trait/abstract**:
  `type` (`string|text|bool|int|enum|color|url|email|media_id|secret|json`), `default`, `required`,
  `max`, `values` (enum), `encrypted`, `label` (chave i18n), `help` (chave i18n).
  Seções iniciais: `institution`, `theme`, `cms`, `seo`, `system`, `support`. `theme` carrega
  `portal`, `cms` e `widgets`; `widgets` é um mapa por id/tipo de widget com tokens semânticos
  validados pelo `ThemeSchema`/schema do widget (decisão D1).
- Resolução: valor do banco → fallback declarado no `config/` → default do schema. Valor inválido no banco é **ignorado** (vence o default) e registra log estruturado;
  nunca derruba request. Chave fora do schema nunca é gravada.
- Segredos: cast `encrypted` no `encrypted_payload` + mascaramento na leitura para a tela (`••••`);
  auditoria grava `changed: true`/contagem de chaves, nunca o valor.
- Cache: `settings.{section}` com TTL curto + `flush` na escrita; teste prova que a
  escrita invalida no mesmo request.
- Auditoria: `AuditEntry::event('core.settings.updated', ...)` com `before`/`after` sem segredos;
  ator, `request_id`, `trace_id` vêm do subscriber/contexto.

### S2 — Módulo `Infraestrutura/Configuracoes` (esqueleto + Geral)

- `src/modules/Infraestrutura/Configuracoes/ModuleProvider.php` (token `configuracoes`; grupo
  `Infraestrutura`; `permissions` = `[VIEW, EDIT]`; rota `configuracoes.cms.index`).
- Rotas CMS sem closure: `GET /cms/configuracoes` (índice de seções), `GET /cms/configuracoes/{section}`
  (formulário), `PUT /cms/configuracoes/{section}` (salvar). Seção desconhecida → 404.
- Seção **Geral** (`institution`): nome, razão social, CNPJ (opcional), endereço, telefone,
  e-mail, horário de atendimento, site. Sem upload nesta fase: logo/favicon entram em P1 (Media).
- Formulário gerado do schema: label/help i18n, `required`, `maxlength`, `aria-invalid`,
  resumo de erros `role="alert"`, CSRF, `@method('put')`. Sem JS obrigatório.
- `FormRequest` por seção (`UpdateSettingsRequest`) valida contra o schema; mensagens i18n.
- Autorização `Permission::authorize('configuracoes', EDIT)`; leitura com `VIEW`. Menu do painel
  aparece só para quem tem `configuracoes.ver` (o `PanelMenu` já resolve).
- Audit `audit.access` na leitura sensível? A leitura de configuração não é segredo; a escrita é
  auditada. Se a seção tiver segredo, leitura também entra em `config('audit.sensitive')`.

### S3 — Primitivas de administração compartilhadas

- Revisar o catálogo existente e **não duplicar**: `card`, `table`, `form-group`, `alert`, `modal`,
  `empty-state`, `status-badge`, `pagination`, `filter-card`, `breadcrumb`.
- Adicionar somente o que falta e é usado por ≥2 telas das fases seguintes:
  - `<x-cms::fieldset>` (título + descrição + conteúdo, `legend` acessível);
  - `<x-cms::form-errors>` (resumo + foco no primeiro erro);
  - `<x-cms::confirm>` (nativo `<dialog>`/popover, sem JS obrigatório, com alternativa de teclado);
  - `<x-cms::data-table>` fino sobre `table` (cabeçalho ordenável opcional, `aria-sort`, empty state).
- Componentes sem regra de negócio, sem consulta a banco, sem `@php` de domínio.
- Testes de contrato de render (snapshot semântico) + axe nas telas de settings.

### S4 — Observabilidade, comando e documentação

- `core:settings:show {section?}` (somente leitura; mascara segredos) para operação/diagnóstico.
- `log_event('core.settings.updated', ['section' => ..., 'keys' => count(...)])` — sem valores.
- Span `core.settings.write` na ação de escrita; `RecordingTelemetry` no teste.
- Atualizar `wiki/specs/11-acesso-e-ativacao.md` (se a seção tocar permissões) e o inventário.
- `make check` 2×; `make check E2E=1` para as telas novas.

## 4. Fora de escopo

- Editor de tema, shell do CMS, menus e SEO de base (**P1**).
- Usuários/perfis (**P2**), ativação de módulos (**P3**), estatísticas (**P4**).
- Upload de mídia nas configurações (P1, via Media).
- Configuração de e-mail/SMTP em banco: segredo de infraestrutura continua em `.env`; a tela só
  expõe dados não secretos e, se necessário, um “testar envio” (P13).
- Qualquer chave arbitrária, “custom settings” ou JSON livre editável.

## 5. Fatias de sessão

| Sessão | Entrega | Prova | Depende |
|---|---|---|---|
| **P0.S1** | ADR-046 aceita + migração + `SettingsSchema` + contrato + testes unitários | schema rejeita chave desconhecida; valor inválido é ignorado; `UNIQUE` e `CHECK` no banco | — |
| **P0.S2** | cache + auditoria + segredos + comando `core:settings:show` | escrita invalida cache; auditoria sem segredo; comando mascara | S1 |
| **P0.S3** | módulo `Configuracoes` + seção Geral + rotas + FormRequest | CRUD ponta a ponta; 404 de seção; 403 sem permissão | S2 |
| **P0.S4** | primitivas `fieldset`/`form-errors`/`confirm`/`data-table` + axe | axe sem A/AA nas telas; teclado no `confirm` | S3 |
| **P0.S5** | telemetria/log + docs + `make check` 2× + E2E | evidência no `kd`; sem flaky | S4 |

## 6. Contratos e dados

- **Contrato:** `Core\Contracts\Settings` (leitura/escrita) + `SettingsSchema` (dados).
- **Tabela:** `core.core_settings`.
- **Rotas:** `configuracoes.cms.index`, `configuracoes.cms.edit`, `configuracoes.cms.update`.
- **Permissões:** `configuracoes.ver`, `configuracoes.editar` (declaradas no manifesto).
- **Eventos:** `core.settings.updated` (auditoria + log de domínio), span `core.settings.write`.
- **Cache:** `settings.{section}`; invalidação na escrita; nenhum cache negativo.

## 7. DoD

- [x] ADR-046 aceita; nenhuma configuração nova fora do schema.
- [x] `core_settings` com `UNIQUE`/`CHECK`, `down()` seguro, `migrate:fresh` idempotente.
- [x] Escrita auditada com before/after; segredo nunca em claro na tela, no log ou na auditoria.
- [x] Tela `Configurações → Geral` funcional sem JS, acessível e com 4 locales.
- [x] `php artisan optimize` verde; nenhum `env()` fora de `config/`.
- [x] `make check` 2× e `make check E2E=1` verdes.
- [x] Cobertura `src/` ≥90%; PHPStan max 0 erros.
- [x] `INVENTARIO.md` marca as capacidades de P0 como `FEITO` quando a fase fechar.

## Fechamento — ficha de revisão

> **Obrigatório antes de fechar:** criar `_to_revision/P0-fundacao-painel.md` e seguir
> `_to_revision/README.md` + `_to_revision/PROTOCOLO.md` (v2.0). A ficha não é gate de build; é a
> entrada da fila de revisão profunda.

- Cabeçalho: fase, `kd` épico/tasks/slices, âncoras (este arquivo, ADRs e código) e status
  `⬜ pendente` → `🟡 em revisão` → `✅ revisado` / `🔧 com correções`.
- Conteúdo: o que foi implementado, arquivos/superfícies a revisar, checklist D1–D14 aplicável,
  riscos e observações.
- Fechamento: preencher “Notas da revisão” com data/revisor/resultado e achados (`kd write
  --anchor`), atualizar a fila em `_to_revision/README.md` e `_to_revision/_inventario.md`.
- Achado com correção exige teste vermelho→verde; a fase só é `closed` no `kd` com a ficha
  registrada.

## 8. Riscos

| Risco | Mitigação |
|---|---|
| Virar key/value livre | schema em código, rota por seção, nenhum endpoint de chave avulsa |
| N+1 / leitura por campo | `section()` carrega a seção inteira em uma query; cache por seção |
| Segredo vazar em auditoria/log | mascaramento no DTO; teste que inspeciona payload de auditoria |
| Configuração inválida derrubar o portal | default vence; valor inválido é ignorado + log; teste de regressão |
| Componentes de UI virarem framework | só entra o usado por ≥2 telas; sem regra de negócio no Blade |
| Cache stale entre requests | invalidação síncrona na escrita + teste de dois requests |

## 9. Retomada

```bash
kd prime
kd ask "settings tipados ADR-046 core_settings"
kd task list --ready
kd task show --id <P0.S*>
```

Ler: este arquivo → `INVENTARIO.md` §2 → ADR-046 → `src/core/Internal/ModuleDiscovery/*` (padrão de
settings específico) → `src/layout/Cms/components/*` → `src/modules/Exemplos/Blueprint/ModuleProvider.php`.
