# Módulo Home (`Portal/Home`)

> Contrato de leitura da **home como módulo** (ADR-004/ADR-014/ADR-016). A estrutura, a
> acessibilidade e o SEO de base vêm do shell; a Home declara apenas o conteúdo central, a raiz no
> sitemap, o **editor do documento de seções** (P5.S2.1), a **publicação versionada** e a **prévia
> autenticada** (P5.S2.2).

## Propósito

- **Grupo:** `Portal` (organizacional — não entra no token).
- **Áreas:** portal (renderização do **publicado**) e CMS (editor do rascunho, publicação e prévia
  autenticada).
- **Token:** `home`.
- **Ativação:** dado em `core_module_discovery_settings` (ADR-034):
  `php artisan core:module:activate Home [--disable]`.

## Estrutura (contrato físico)

```
src/modules/Portal/Home/
├── ModuleProvider.php              # permissions home.{ver,editar,aprovar}; rota do painel
├── Routes/portal.php               # GET / → HomepageController (publicado)
├── Routes/cms.php                  # GET/PUT /home, POST /publicar, GET /preview
├── Logic/Controllers/HomepageController.php      # PublishedHomepage → view
├── Logic/Controllers/Cms/HomeEditorController.php    # lista + diálogos + prévia + publicar/prévia
├── Logic/Controllers/Cms/HomeDraftSaveController.php # 403 → validação → Action → redirect
├── Logic/Controllers/Cms/HomePublishController.php   # 403 → Action → redirect (409 no conflito)
├── Logic/Controllers/Cms/HomePreviewController.php   # 403 → rascunho no shell do portal
├── Logic/Actions/SaveHomeDraft.php  # schema por bloco + transação + 409
├── Logic/Actions/PublishHome.php    # rascunho → published + cache + audit home.published
├── Logic/Requests/SaveHomeDraftRequest.php   # forma do formulário (não o schema de bloco)
├── Logic/Support/HomeDraftReader.php         # rascunho salvo / default
├── Logic/Support/HomeDraftSections.php       # normalização do formulário + ações de estrutura
├── Logic/Support/PublishedHomepage.php       # fonte pública versionada + fallback default
├── Logic/Support/HomePreviewDocument.php     # rascunho no preview com cache curto escopado
├── Logic/Support/HomeQuickLinks.php          # atalhos do menu via porta `Navigation` (S5.1)
├── Logic/Exceptions/HomeDraftConflict.php    # conflito de versão/publicação (409)
├── Widgets/HomeQuickLinksWidget.php          # fonte `home.quick-links` (P5.S5.1)
├── Database/Migrations/2026_10_25_000000_create_home_documents_table.php
├── Database/Models/HomeDocument.php          # `home.home_documents` (`draft` e `published`)
├── Seo/HomeSitemapSource.php                 # contribui a raiz ao SitemapRegistry
├── Layout/Views/index.blade.php              # home pública (shell + <x-layout::blocks>)
├── Layout/Views/home-preview.blade.php       # prévia autenticada (shell real do portal)
├── Layout/Views/cms/index.blade.php          # editor (shell + seções + diálogos + prévia)
├── Layout/Views/cms/partials/home-block-fields.blade.php
├── Layout/Views/cms/partials/home-block-field.blade.php
├── Layout/Views/widgets/home-quick-links.blade.php   # fragmento do widget (S5.1)
├── Lang/{pt_BR,en,es,fr}/index.php, cms.php, widget.php
└── Assets/js/home-editor.js                  # `<dialog>` (melhoria progressiva)
```

## Rotas

| Nome | Método | URI | Controller | Acesso |
|---|---|---|---|---|
| `portal.home` | GET | `/` | `HomepageController` (invocável) | pública |
| `home.cms.index` | GET | `/cms/home` | `HomeEditorController` (invocável) | `home.ver` |
| `home.cms.update` | PUT | `/cms/home` | `HomeDraftSaveController` (invocável) | `home.editar` (freio `throttle:60,1`) |
| `home.cms.publish` | POST | `/cms/home/publicar` | `HomePublishController` (invocável) | `home.aprovar` (freio `throttle:30,1`) |
| `home.preview` | GET | `/cms/home/preview?locale=` | `HomePreviewController` (invocável) | `home.ver` (sessão do CMS) |

As rotas CMS vivem no domínio do painel e nascem autenticadas pelo `AreaRegistrar` (ADR-037); a
autorização é do controller (`Permission::authorize`, ADR-033) e o request de gravação também
responde **403 antes de 422**. Não existe rota pública de rascunho: a prévia é autenticada (sem token
público) e a home pública lê **somente** a variante publicada.

## Permissões (ADR-033)

O manifesto declara `home.ver`, `home.editar` e `home.aprovar` (`Permission::ACTIONS` menos
`arquivar`). Ler o editor e a prévia autenticada exige `home.ver`; gravar o rascunho exige
`home.editar`; publicar exige `home.aprovar`. Tudo é negado por padrão e o botão “Publicar” só
aparece para quem tem a permissão (o servidor valida de novo).

## Dados (ADR-007/ADR-017)

- Schema `home`, tabela `home_documents`: `id` (uuidv7), `variant` (**único**; `draft` e
  `published`), `document` (`jsonb`, `CHECK jsonb_typeof = 'object'`), `lock_version`,
  `created_at`/`updated_at`.
- `draft` é o que o editor grava; `published` é a **cópia canônica validada** do rascunho no momento
  de publicar. A home pública (`PublishedHomepage`) lê só `published`; sem linha publicada (ou com
  documento inválido) cai no default (hero + quick-links). O rascunho nunca é público.
- Publicar um documento **sem seção visível** é recusado (422): o default é o fallback de “não
  publicado”, não de “publicado vazio”. Rascunho ausente/inválido também é recusado.
- `published.lock_version` é o **número da publicação** (1, 2, …) e a chave do cache público
  (`home.published.{versão}`, TTL `layout.home.published_ttl`); publicar esquece a chave substituída
  de forma síncrona. `draft.lock_version` continua sendo a edição otimista (409).
- Documento = `{version, sections:[{id,type,props,hidden}]}` — a **ordem do array é a ordem
  visual** e `hidden` preserva a posição. `version` é `BlocksSchema::VERSION`; tipo desconhecido é
  ignorado; `props` é fechado por tipo e chave proibida (`html`, `template`, `iframe`, `style`,
  posição) invalida o bloco. Nada de HTML/CSS/Blade no `jsonb`.
- **Uma linha por variante** (índice único) e `lock_version` sob `lockForUpdate`: página velha
  recebe **409** e nada é sobrescrito. `down()` renomeia para `backup_*`.

## Eventos e observabilidade (ADR-024/ADR-035)

- **Emite:** `home.save_draft` (span `home.save_draft` + `log_event`, com `operation`, `sections` e
  `lock_version`) e `home.publish` (span `home.publish` + `log_event`, com `version`; sem PII).
- **Auditoria:** a mutação de `HomeDocument` entra na trilha pelo `GlobalAuditSubscriber` (CMS =
  full) e a publicação grava o evento explícito **`home.published`** pelo `AuditWriter`
  (`before`/`after` com variante, versão e nº de seções visíveis; ator snapshot — ADR-035). O 409 e
  as validações não gravam nada.
- **SEO:** registra `HomeSitemapSource` no `SitemapRegistry` (ADR-027); o XML é do service Seo.

## Layout (ADR-016)

- A home pública usa **um** `<x-layout::portal>` e preenche só o `<main>` com
  `<x-layout::blocks>` — o renderizador do documento de seções.
- O editor usa **um** `<x-layout::cms>`: o shell é dono do `<h1>`; o módulo entrega `<h2>`
  (seções/prévia), `<h3>` (seção) e `<h4>` (diálogo). Cada seção tem um `<dialog>` **nativo** com os
  campos — **sem `<form>` aninhado** (os campos pertencem ao formulário `PUT`). Sem JS o CSS
  (`@media (scripting: none)`) mantém os campos no fluxo e os botões continuam valendo; o caminho
  principal é por botões (subir/descer/ocultar/remover/salvar), com teclado (WCAG 2.2 SC 2.5.7).
- O editor tem a **prévia estrutural** do documento salvo (lista), a **prévia autenticada** no shell
  real do portal (`home.preview`, um link por locale) e a ação **Publicar** (formulário próprio,
  sem `<form>` aninhado; publica o rascunho **salvo**).
- A prévia autenticada usa `<x-layout::portal>` + o **mesmo** `<x-layout::blocks>`; o `<h1>` é o do
  `hero` (o shell do portal não tem `<h1>`), o SEO sai `noindex, nofollow` (meta) e a área restrita
  responde `X-Robots-Tag: noindex, nofollow, noarchive` com cache privado. Nenhum anônimo recebe
  rascunho.
- **Locales do conteúdo:** campos traduzíveis do catálogo aceitam `{pt_BR: …, en: …}` (ou valor
  simples = default). O renderizador resolve pelo locale da URL (`BlockLocalizer`: locale vigente →
  default → primeiro preenchido). O editor mostra os 4 locales por campo; gravar só o default mantém
  a forma simples e os documentos da S2.1 continuam válidos.
- `props` de texto rico são AST JSON (`textarea` nesta fatia; o editor TipTap do service RichText
  entra com a edição rica, ADR-028). Mídia é **id Media** (ADR-026) — sem seletor nesta fatia.

## Widget no layout (P5.S5.1)

`ModuleProvider::register()` registra a fonte **`home.quick-links`** no `WidgetRegistry` do Core.

- `title` = `home::widget.title` (4 locales), ícone `lucide-link`, `embeddable = true`, slots
  `main.left`, `main.right`, `main.after`.
- **Leitura:** `HomeQuickLinks` consome a porta `Core\Contracts\Navigation` (`tree('principal')`),
  **nunca** o módulo `Menus` (import proibido — ADR-013). Só o primeiro nível vira atalho (a árvore
  completa já é o header); item de grupo sem link é descartado. Rótulo/URL/ícone vêm resolvidos
  pela porta (locale vigente, sem link morto — ADR-012) e a árvore é **cacheada** pelo dono
  (`navigation.principal.{locale}`) — um widget em N slots não gera N consultas.
- **Limite:** `config('home.quick_links_limit')` (default 6, teto 12; valor inválido cai no
  default).
- **Degradação:** `Menus` inativo → a porta responde vazio → o slot fica vazio, sem 500 (ADR-034).
- **HTML:** `<nav>` com rótulo, cartões como links (alvo ≥ 2.75rem), `rel="external noopener"` para
  destino externo; sem `<script>`, `style=` ou `id` fixo no fragmento.

## Comandos

```bash
php artisan core:module:activate Home
php artisan core:module:activate Home --disable
php artisan route:list --name=home.
```

## Verificação

```bash
make check
make check E2E=1
bash tools/name-check.sh --grep home
vendor/bin/phpunit --filter=HomeEditorCmsTest
vendor/bin/phpunit --filter=HomePublishPreviewCmsTest
vendor/bin/phpunit --filter=HomeQuickLinksWidgetTest      # fonte de widget (P5.S5.1)
```
