---
id: MOD-LAYOUTS-HOMEPAGE-DADOS
tipo: modulo
onda: 5
status: concluido
unidades: [MOD-LAYOUTS-HOMEPAGE]
fontes:
  - V2/Modules/Layouts/Homepage/Migrations/
  - V2/Modules/Layouts/Homepage/Models/
  - V2/Modules/Layouts/Homepage/LseWidgets/Schemas/
  - V2/Modules/Layouts/Homepage/Services/
  - V2/Modules/Layouts/Homepage/Controllers/
depende_de: [CORE-DATA, MOD-SUPPORT-STRUCTURALLANDINGEDITOR, SVC-LANDINGSECTIONSEDITOR]
consumidores: [MOD-LAYOUTS-HOMEPAGE, MOD-LAYOUTS-BASICPAGE, DATA-SEEDS-BASICPAGES]
criticidade: alta
dono: Homepage
---

# Homepage — dados, payload e modelo

> Existem **duas** estruturas de persistência com nomes parecidos e papéis opostos:
> `v2_layout_base_configs.payload` (chave `landing_sections`) é a fonte real, um array de seções;
> `v2_homepage_sections` é uma tabela criada pela migration do módulo, com model e escopos, mas
> **sem nenhum leitor ou escritor de produção**. Este documento fixa o schema do payload, os campos
> por widget, quem escreve/lê cada estrutura e as invariantes de dados.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Define o formato exato em que a home é persistida e lido pelo portal, incluindo os parâmetros de cada um dos 10 widgets e as chaves que só existem em tempo de renderização. |
| Entrada | `landing_sections_json` (string JSON) no POST do CMS; `payload` já gravado; seed `SucesuHomepageSeeder`. |
| Saída | Array de seções `{id,type,order,parameters}` hidratado; linhas em `v2_layout_base_configs`; tabela `v2_homepage_sections` (vazia). |
| Efeitos colaterais | Cast `array` do model converte jsonb↔array; `save()` dispara `updated_at`; nenhuma FK, trigger ou observer. |
| Particularidade central | `source_type`, `source_config` e `section_key` só existem no model morto; no payload real o "tipo de fonte" vive dentro de `parameters`, e **nenhum formulário do editor o grava**. |
| Como se integra | `LayoutBaseConfig` (SLE) para o payload; `HomepageSection` para a tabela morta; `HomepageDataIntegrator` e widgets leem o array. |
| Se removido, quebra | A home perde o conteúdo; o editor não tem o que carregar; os widgets não têm parâmetros. |
| Superfície pública | `LayoutBaseConfig::where('key','landing_sections')`; `HomepageSection` (sem uso); schemas `Sucesu*Schema::rules()`. |

## 1. Efeito

### 1.1 O que o usuário consegue fazer

- O operador salva a home; o sistema grava **uma linha** com todo o conteúdo, não uma linha por
  seção (`V2/Modules/Layouts/Homepage/Controllers/HomepageCmsController.php:64-70`).
- O visitante recebe o array na ordem gravada; nenhuma ordenação por coluna é aplicada
  (`V2/Modules/Layouts/Homepage/Controllers/HomepagePortalController.php:44-48`).
- O seed cria 8 seções de exemplo cobrindo os tipos principais
  (`V2/Seeds/BasicPages/SucesuHomepageSeeder.php:242-385`).

### 1.2 O que muda no sistema

| Efeito | Recurso | Ponto no código | Fonte |
|---|---|---|---|
| Cria/atualiza a linha `key='landing_sections'` | `v2_layout_base_configs` | `saveSections` | `HomepageCmsController.php:64-70` |
| Reativa linha soft-deletada (mesmo `id` e `created_at`) | `v2_layout_base_configs` | `withTrashed()->restore()` | `HomepageCmsController.php:64-67` |
| Substitui `payload` inteiro (sem merge) | `v2_layout_base_configs.payload` | `$config->payload = $payload` | `HomepageCmsController.php:68-70` |
| Popula a home de uma instalação nova | `v2_layout_base_configs` | `SucesuHomepageSeeder` | `SucesuHomepageSeeder.php:387-394` |
| Cria a tabela vazia `v2_homepage_sections` | schema | migration do módulo | `Migrations/2026_07_22_000001_create_v2_homepage_sections_table.php:16-30` |
| Renomeia a tabela no rollback (preserva dados) | schema | `down()` | migration `:33-38` |

### 1.3 Contrato observável

| Entrada | Resultado |
|---|---|
| `payload = []` | portal exibe o jumbotron; editor abre sem seções |
| `payload = [{id,type,parameters,order}]` | uma seção renderizada por widget registrado |
| `payload = null` (gravado por fora) | `integrate(null)` viola `array` → `TypeError`/500 |
| `payload = {"hero":[...]}` (formato BasicPage) | nenhuma seção renderizada (o viewer itera valores, mas cada item não tem `type`) |
| `parameters` ausente | widget recebe `[]`; `prepare` usa defaults; render geralmente esconde a seção |
| `image_id` inexistente | campo `image` não é injetado; render esconde a mídia (exceto hero, que gera `src=""`) |

### 1.4 O que este documento NÃO cobre

- Colunas de `v2_gallery_media`, `v2_news`, `v2_eventos` e `v2_parceiros_institucionais`: cada uma
  pertence ao seu módulo/serviço (ver docs de Notícias, Agenda, Parceiros e Gallery).
- Índices e migrations de `v2_layout_base_configs`: pertencem a `MOD-SUPPORT-STRUCTURALLANDINGEDITOR`.

## 2. Abordagem

### 2.1 Estratégia

Uma única chave, um `jsonb`, zero normalização. O LSE trabalha com uma **lista ordenada**; por isso
`payload` é um array JSON, e não um objeto por chave. A tabela `v2_homepage_sections` foi desenhada
para um modelo relacional (uma linha por seção, `sort_order`, `is_active`) que nunca foi adotado.

```mermaid
erDiagram
  v2_layout_base_configs {
    uuid id PK
    string key UK
    jsonb payload
    uuid transaction_snapshot_id
    timestamp created_at
    timestamp updated_at
    timestamp deleted_at
  }
  v2_homepage_sections {
    uuid id PK
    string section_key UK
    string type
    int sort_order
    string source_type
    jsonb source_config
    jsonb parameters
    bool is_active
    timestamp deleted_at
  }
  v2_layout_base_configs ||..o{ v2_homepage_sections : "modelo alternativo nunca usado"
```

### 2.2 Mecânica passo a passo

1. `saveSections` valida `landing_sections_json` como `required|string`
   (`HomepageCmsController.php:55-57`).
2. `json_decode(..., true)`; se o resultado não for array, vira `[]`
   (`HomepageCmsController.php:59-62`).
3. `LayoutBaseConfig::withTrashed()->firstOrNew(['key'=>'landing_sections'])`; se `trashed()`,
   `restore()` (`:64-67`).
4. Atribui o array e salva; o cast `array` serializa para jsonb
   (`V2/Modules/Support/StructuralLandingEditor/Models/LayoutBaseConfig.php:25-27`).
5. Invalida cache, audita `old`/`new` e redireciona (`HomepageCmsController.php:72-84`).
6. No portal, `payload` volta como array pelo mesmo cast e segue para o integrador
   (`HomepagePortalController.php:44-48`).
7. `v2_homepage_sections` permanece intocada: a migration só cria a tabela.

### 2.3 Decisões de design

| Decisão | Efeito | Marca |
|---|---|---|
| `payload` como lista, não mapa | preserva ordem e permite repetir o mesmo tipo | `[C]` `HomepageCmsController.php:59-70` |
| `order` gravado no item, mas ignorado na leitura | o cliente reindexa ao mover; o servidor não ordena | `[C]` `sections-manager.blade.php:71-97`; `landing-sections-viewer.blade.php:33-48` |
| Sem `schema_version` nem validação global | payload antigo continua renderizando; migração de formato é manual | `[I]` `HomepageCmsController.php:59-62` |
| `parameters` livre | cada widget interpreta o próprio subconjunto | `[C]` `BaseWidget::prepare` |

### 2.4 Trechos de código pivô

```php
// V2/Modules/Layouts/Homepage/Controllers/HomepageCmsController.php:59-70 — a única escrita
$payload = json_decode($request->input('landing_sections_json'), true);
if (!is_array($payload)) {
    $payload = [];
}

$config = LayoutBaseConfig::withTrashed()->firstOrNew(['key' => 'landing_sections']);
if ($config->trashed()) {
    $config->restore();
}
$oldPayload = $config->exists ? $config->payload : [];
$config->payload = $payload;
$config->save();
```

> Não há `validator` por seção, nem `DB::transaction`: a gravação é um único `UPDATE/INSERT`
> atômico por natureza, mas a auditoria e a invalidação de cache vêm depois e podem falhar
> isoladamente.

```php
// V2/Modules/Layouts/Homepage/Models/HomepageSection.php:31-57 — o modelo que ninguém usa
protected $table = 'v2_homepage_sections';
public $incrementing = false;
protected $keyType = 'string';

protected $fillable = [
    'id', 'section_key', 'type', 'sort_order', 'source_type',
    'source_config', 'parameters', 'is_active',
];

protected $casts = [
    'parameters'    => 'array',
    'source_config' => 'array',
    'is_active'     => 'boolean',
];
```

> Existe `scopeActive`/`scopeOrdered` e geração de UUID v4 em `creating`
> (`HomepageSection.php:71-102`), mas nenhuma consulta de produção usa o model.

```php
// V2/Modules/Layouts/Homepage/LseWidgets/Widgets/SucesuAssociadosWidget.php:43-61 — parâmetros que o widget grava em memória
$items = $parceiros->map(function ($parceiro) {
    return [
        'id'    => $parceiro->id,
        'name'  => $parceiro->name,
        'slug'  => $parceiro->slug,
        'image' => $parceiro->galleryLogo ? $parceiro->galleryLogo->file_path : null,
        'url'   => route('v2.parceiros-institucionais.portal.detalhe', ['id' => $parceiro->id]),
    ];
})->toArray();
// ...
$parameters['items'] = $items;
```

> `items` **não** é persistido em `sucesu-associados`: é recalculado a cada render e não deve ser
> gravado pelo editor (o formulário não o expõe).

### 2.5 Volume, cardinalidade e consultas

| Dado | Cardinalidade típica | Consulta |
|---|---|---|
| Linhas em `v2_layout_base_configs` com `key='landing_sections'` | 1 | `where('key','landing_sections')->first()` |
| Seções no payload | 8 no seed; sem limite no código | nenhuma query |
| Parâmetros por seção | de 3 (`sucesu-agenda`) a N itens (`hero`) | nenhuma query |
| `v2_homepage_sections` | 0 linhas | nenhuma |
| Mídias resolvidas | 1 query `whereIn` por request | `LandingSectionsEditorRendererService.php:171-174` |

## 3. Particularidades

### 3.1 O que só existe aqui

- O **formato canônico da seção** (`id`, `type`, `order`, `parameters`) é definido pelo JS do LSE e
  consumido por este módulo; nenhuma migration/model o descreve.
- A distinção entre **parâmetros persistidos** e **chaves de runtime** (`image`, `image_variants`,
  `icon`, `icon_variants`, `super_highlighted`, `months`, `upcoming_events`, `events_by_date`,
  `items` dinâmicos) só é observável lendo cada widget.

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-HOMEPAGE-21` | A identidade da seção é `id` (string gerada no cliente), não `section_key` | dois blocos do mesmo tipo têm ids diferentes; `section_key` não existe no payload | `sections-manager.blade.php:288-293` |
| `RN-HOMEPAGE-22` | `parameters` é gravado como veio do formulário, sem coerção de tipos | `limit`/`event_limit` chegam como string; widgets fazem `(int)` | `sections-manager.blade.php:223-273`; `SucesuNoticiasWidget.php:45` |
| `RN-HOMEPAGE-23` | Chaves de runtime não são removidas do array antes de persistir | se um cliente salvar o resultado de `prepareSections`, `image`, `months` etc. são gravados | `LandingSectionsEditorRendererService.php:181-203` |
| `RN-HOMEPAGE-24` | `source_config` só existe na tabela morta; no payload os limites ficam em `parameters` (`limit`, `event_limit`, `months_ahead`) | reimplementar com `source_config` quebra os widgets | `HomepageSection.php:41-57` vs `HomepageDataIntegrator.php:29-47` |
| `RN-HOMEPAGE-25` | `section_key` é `unique` no banco, mas não há chave natural no payload | o índice/unique nunca é exercido | migration `:22` |
| `RN-HOMEPAGE-26` | `payload` não tem default no banco (`nullable`) | linha criada por outro caminho pode ficar `NULL` e derrubar o portal | migration SLE `:25`; `HomepagePortalController.php:45-47` |
| `RN-HOMEPAGE-27` | UUID do model morto é v4 em PHP (`v2_uuid()`), não UUIDv7 | irrelevante para o payload (ids vêm do cliente) | `HomepageSection.php:71-80`; `V2/Core/Helpers/helpers.php:61-69` |

### 3.3 Catálogo de parâmetros por widget (persistidos)

Todos os campos abaixo são validados por `Sucesu*Schema::rules()`, que recebe o request do modal com
prefixo `parameters.`. "Obrigatório" é a regra **server-side**; o `required` do HTML pode divergir.

**`sucesu-hero`** — `SucesuHeroSchema::rules()` (`LseWidgets/Schemas/SucesuHeroSchema.php:7-17`)

| Campo | Regra | Observação |
|---|---|---|
| `slides` | `nullable|array` | lista de slides |
| `slides.*.title` | `required|string|max:255` | vira `<h1>` no 1º slide e `<h2>` nos demais |
| `slides.*.subtitle` | `nullable|string|max:500` | `alt` do picture usa o título |
| `slides.*.cta_text` | `nullable|string|max:255` | botão só aparece se preenchido |
| `slides.*.cta_url` | `nullable|string|max:500` | default `#` no render |
| `slides.*.image_id` | `nullable|string|max:255` | resolvido para `slides.{i}.image` |

**`sucesu-services`** — `SucesuServicesSchema.php:7-16`

| Campo | Regra | Observação |
|---|---|---|
| `items` | `nullable|array` | sem a lista, a seção não renderiza |
| `items.*.title` | `required|string|max:255` | link do card |
| `items.*.description` | `nullable|string|max:1000` | |
| `items.*.url` | `nullable|string|max:500` | default `#` |
| `items.*.icon_id` | `nullable|string|max:255` | resolvido para `items.{i}.icon` |

**`sucesu-about`** — `SucesuAboutSchema.php:7-22`

| Campo | Regra | Observação |
|---|---|---|
| `subtitle`, `title` | `nullable`; `title` `required|max:255` | `title` também é `alt` da imagem |
| `description` | `nullable|string|max:2000` | |
| `cta_text` / `cta_url` | `nullable|max:255` / `max:500` | |
| `image_id` | `nullable|string|max:255` | vira `image` |
| `counters` | `nullable|array` | |
| `counters.*.{value,suffix,label,icon_id}` | `value` 50, `suffix` 20, `label` 255, `icon_id` 255 | `icon` resolvido por `icon_id` |

**`sucesu-brands`** — `SucesuBrandsSchema.php:7-15`; **`sucesu-events`** — `SucesuEventsSchema.php:7-18`;
**`sucesu-news`** — `SucesuNewsSchema.php:7-20`; **`sucesu-regionais`** — `SucesuRegionaisSchema.php:7-19`.

| Widget | Campos | Regras/destaques |
|---|---|---|
| brands | `title`; `items[].name`, `items[].image_id` | `name` obrigatório; `title` opcional |
| events | `subtitle`; `title` (obrigatório); `items[].title/details/url/image_id` | `details` aceita HTML e é impresso cru |
| news | `subtitle`; `title` (obrigatório); `description`; `items[].title/excerpt/date/url/image_id` | `date` é string livre (`14 MAI`) |
| regionais | `subtitle`; `title` (obrigatório); `text1`; `states`; `text2`; `cta_text`; `cta_url`; `image_id` | `text1/text2` até 2000; `states` até 500 |

**`sucesu-noticias`** — `SucesuNoticiasSchema.php:12-20`

| Campo | Regra | Default efetivo |
|---|---|---|
| `title` | `nullable|string|max:255` | `Últimas Notícias` |
| `limit` | `nullable|integer|min:1|max:50` | 9 (widget) / 2 no input (form) |
| `categories` | `nullable|array` | todos os IDs quando vazio |
| `categories.*` | `nullable|string|max:255` | comparado a `category_id` |

**`sucesu-associados`** — `SucesuAssociadosSchema.php:7-16`

| Campo | Regra | Efeito real |
|---|---|---|
| `title` | `nullable|string|max:255` | cabeçalho do carrossel |
| `visible_slugs` | `nullable|array` | **ignorado** pelo widget |
| `hidden_slugs` | `nullable|array` | blacklist aplicada com `whereNotIn('slug', ...)` |

**`sucesu-agenda`** — `SucesuAgendaSchema.php:12-19`

| Campo | Regra | Default efetivo |
|---|---|---|
| `title` | `required|string|max:255` | form sugere `Agenda Executiva` |
| `event_limit` | `nullable|integer|min:1|max:12` | 4 |
| `months_ahead` | `nullable|integer|min:1|max:12` | 2 |

### 3.4 Casos-limite

- **Campo obrigatório ausente em payload antigo**: a validação só roda ao salvar a seção no modal;
  um payload editado à mão pode violar o schema e ainda assim renderizar.
- **`limit` como string**: `(int)` normaliza; `"abc"` vira `0` e a consulta usa `take(0)` → lista
  vazia (a seção some).
- **`categories` como string**: `whereIn` recebe string e o comportamento depende do driver; o
  schema impede apenas no modal.
- **`parameters` com `image` já preenchido**: `resolveImage` mantém a string e não consulta a
  galeria (`HomepageSectionResolver.php:270-284`) — mesma regra não existe no `prepare` dos widgets,
  que sempre sobrescreve quando há `image_id`.
- **Soft delete + save**: restaura e reusa a linha; o histórico antigo continua associado.
- **Concorrência**: duas abas → last-write-wins no payload inteiro.

### 3.5 Divergências e dívidas

| Item | Evidência |
|---|---|
| `v2_homepage_sections` sem uso: só migration, model, dashboard (rótulo) e testes | `Models/HomepageSection.php`; `V2/Services/Dashboard/DashboardStatsService.php:670` |
| `HomepageSectionResolver` pressupõe `source_type` por seção | `Services/HomepageSectionResolver.php:34 e :60-108` |
| `HomepageDataIntegrator` pressupõe `source_type` em `parameters` | `Services/HomepageDataIntegrator.php:29-40` |
| Formulários legados gravam `source_type`/`source_config` de outro modelo | `Views/cms/forms/{news,events,brands,regionais}.blade.php` |
| Seed grava `sucesu-brands` estático, embora exista caminho `dynamic-partners` | `SucesuHomepageSeeder.php:303-319` vs `HomepageDataIntegrator.php:38-40` |

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Assinatura | Precondição | Consumidores |
|---|---|---|---|
| `LayoutBaseConfig::where('key','landing_sections')->first()` | `?LayoutBaseConfig` | tabela migrada | Homepage (portal/CMS) |
| `LayoutBaseConfig::$payload` | `array|null` (cast) | — | Homepage, SLE |
| `HomepageSection` | model Eloquent | tabela migrada | nenhum (testes) |
| `Sucesu*Schema::rules()` | `array<string,string>` | — | `BaseWidget::getValidationRules` |

### 4.2 Contrato de dados (UI/JS)

- O hidden enviado pelo editor contém `JSON.stringify(sections)`; `sections` é o array em memória,
  com `order` reindexado de 1..N a cada movimento (`sections-manager.blade.php:71-97,288-293`).
- O JS de itens (`_item-list-js.blade.php:32-69`) reindexa nomes `parameters[items][N][...]`; um
  item removido do meio faz os seguintes “subirem” de índice.

### 4.3 Ordem e ciclo de vida

1. Migration do SLE (`v2_layout_base_configs`) deve existir antes do seed da home.
2. `SucesuHomepageSeeder` roda antes dos seeders de Notícias/Agenda/Parceiros no `DatabaseSeeder`
   (`database/seeds/DatabaseSeeder.php:92-98`); ainda assim, funciona porque as seções de banco
   consultam dados em tempo de render, não no seed.
3. A tabela `v2_homepage_sections` pode nem existir em instalações antigas: nada a consulta.

### 4.4 Receitas de uso

**1. Ler o payload com segurança, aceitando linha ausente:**

```php
$config = \V2\Modules\Support\StructuralLandingEditor\Models\LayoutBaseConfig::where('key', 'landing_sections')->first();
$sections = is_array($config->payload ?? null) ? $config->payload : [];
```

**2. Gravar uma seção nova preservando as existentes:**

```php
$sections = is_array($config->payload ?? null) ? $config->payload : [];
$sections[] = ['id' => v2_uuid(), 'type' => 'sucesu-news', 'order' => count($sections) + 1, 'parameters' => ['title' => 'Notícias']];
$config->payload = $sections;
$config->save();
```

**3. Validar uma seção antes de gravar (mesmas regras do editor):**

```php
$widget = \V2\Services\LandingSectionsEditor\Services\WidgetRegistry::get('sucesu-agenda');
$rules = $widget->getValidationRules();
\Validator::make(['parameters' => $params], $rules)->validate();
```

### 4.5 Anti-uso

- **Persistir o array depois de `prepareSections`**: grava `image`, `image_variants`, `months`,
  `upcoming_events` e outros campos calculados de runtime, engordando o payload e congelando dados.
- **Criar `HomepageSection` e esperar render**: a tabela é um beco sem saída; o portal só lê
  `landing_sections`.
- **Confiar em `source_config` no payload**: os widgets leem `parameters.limit`,
  `parameters.event_limit` e `parameters.months_ahead`.

### 4.6 Custos e limites

- Um `jsonb` por home, sem paginação; o custo é o tamanho do JSON e a decodificação por request.
- Sem versionamento: alterar o schema de um widget pode invalidar payloads antigos silenciosamente.
- Sem auditoria por seção: o log guarda `old`/`new` do payload inteiro
  (`HomepageCmsController.php:76-80`).

## 5. Rotas e contratos HTTP

Só duas rotas tocam dados diretamente; detalhes em [`spec.md`](spec.md) §5.

| Método | URI | Nome | Efeito sobre dados |
|---|---|---|---|
| POST | `/cms/homepage/save` | `v2.homepage.sections.save` | upsert/restore de `landing_sections` |
| POST | `/cms/homepage/refresh-cache` | `v2.homepage.cache.refresh` | não altera dados |

## 6. Dados e modelo de domínio

### 6.1 `v2_layout_base_configs` (fonte real)

| Coluna | Tipo | Null | Default | Notas |
|---|---|---|---|---|
| `id` | uuid | não | — | PK; gerado no model com `Uuid::uuid4()` |
| `key` | string | não | — | unique; aqui sempre `landing_sections` |
| `payload` | jsonb | sim | — | lista de seções |
| `transaction_snapshot_id` | uuid | sim | — | copiado para a auditoria |
| `created_at`/`updated_at` | timestamp | sim | — | |
| `deleted_at` | timestamp | sim | — | soft delete; save restaura |

Fonte: migration do SLE `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_03_000001_create_v2_layout_base_configs_table.php:22-29`;
model `LayoutBaseConfig.php:9-38`.

### 6.2 `v2_homepage_sections` (tabela morta)

| Coluna | Tipo | Null | Default | Cast/escopo |
|---|---|---|---|---|
| `id` | uuid | não | — | PK; `v2_uuid()` no `creating` |
| `section_key` | string(100) | não | — | unique |
| `type` | string(50) | não | — | — |
| `sort_order` | integer | não | 0 | `scopeOrdered` |
| `source_type` | string(50) | não | `static` | único lugar com esse conceito |
| `source_config` | jsonb | sim | — | cast `array` |
| `parameters` | jsonb | sim | — | cast `array` |
| `is_active` | boolean | não | true | cast `bool`; `scopeActive` |
| `created_at`/`updated_at` | timestamp | sim | — | |
| `deleted_at` | timestamp | sim | — | `SoftDeletes` |

Fonte: migration `:16-30`; model `HomepageSection.php:27-102`.

### 6.3 Forma do payload

```json
[
  {
    "id": "6f1c2d9e-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
    "type": "sucesu-hero",
    "order": 1,
    "parameters": {
      "slides": [
        { "title": "…", "subtitle": "…", "cta_text": "…", "cta_url": "/…", "image_id": "42" }
      ]
    }
  },
  {
    "id": "…",
    "type": "sucesu-agenda",
    "order": 2,
    "parameters": { "title": "Agenda Executiva", "event_limit": "4", "months_ahead": "2" }
  }
]
```

- `id`: string livre (o cliente gera UUID); usado só em `data-section-id` e no swap do preview.
- `type`: slug do widget; precisa estar registrado.
- `order`: 1..N; ignorado no render.
- `parameters`: objeto específico do widget (§3.3).

### 6.4 Quem escreve e quem lê

| Estrutura | Escritores | Leitores |
|---|---|---|
| `v2_layout_base_configs.key='landing_sections'` | `HomepageCmsController::saveSections`; `SucesuHomepageSeeder` | `HomepageCmsController::sections`; `HomepagePortalController::index`; `BasicPage/Views/portal/homepage.blade.php` (legado) |
| `v2_layout_base_configs.key='landing_sections'` (soft delete) | `saveSections` (restore) | idem |
| `v2_homepage_sections` | ninguém | ninguém |
| `activity_log` | `saveSections`/`refreshCache` | `sections()` (histórico) |

## 7. Fluxos e estados

```mermaid
stateDiagram-v2
  [*] --> Inexistente
  Inexistente --> Ativa: POST /save
  Ativa --> Ativa: POST /save (payload novo)
  Ativa --> Removida: DELETE externo (soft delete)
  Removida --> Ativa: POST /save (restore)
  Ativa --> CacheSujo: POST /save
  CacheSujo --> Ativa: DEL page:* + próximo GET
```

- `FL-HOMEPAGE-04` — **seed**: `SucesuHomepageSeeder` monta 8 seções com `v2_uuid()` e
  `firstOrNew(['key'=>'landing_sections'])`, salva e limpa o cache (`SucesuHomepageSeeder.php:387-421`).
- `FL-HOMEPAGE-05` — **hidratação no portal**: `payload` → `integrate` → `prepareSections` →
  `prepareSections` (viewer) → render por widget. Chaves de runtime aparecem e desaparecem nesse
  caminho, sem persistência.

## 8. UI CMS

- O editor não permite escolher `section_key`, `sort_order`, `source_type` nem `is_active`; esses
  campos só existem nas telas legadas (`Views/cms/forms/*`) e não têm rota. Detalhes em
  [`cms.md`](cms.md).

## 9. UI Portal

- Nenhuma seção é filtrada por `is_active` porque o payload não tem esse campo; todas as seções do
  array são renderizadas. Detalhes em [`portal.md`](portal.md).

## 10. Configuração, permissões e flags

- `cache.prefix` afeta a invalidação, não o armazenamento.
- Acesso ao payload é controlado pela permissão `Homepage` e pela flag `Homepage`; não há escopo
  regional (o conteúdo é nacional).

## 11. Observabilidade, erros e limites

- Escrita: auditoria `updated` com `old`/`new` completos do array
  (`HomepageCmsController.php:76-80`).
- Leitura: sem log de seções ignoradas ou parâmetros inválidos.
- Erros de banco no save não têm `try/catch`; a exceção sobe (500).
- Limite: `payload` jsonb sem tamanho explícito; o gargalo é o `post_max_size`.

## 12. Seeds e dados iniciais

`SucesuHomepageSeeder` grava 8 seções, nesta ordem de array (`SucesuHomepageSeeder.php:242-385`):

| `order`/índice | `type` | Dados persistidos |
|---|---|---|
| 0 | `sucesu-hero` | `slides[]` com `title/subtitle/cta_text/cta_url/image/image_id/image_variants` |
| 1 | `sucesu-services` | `items[]` com `title/description/icon/icon_id/icon_variants/url` |
| 2 | `sucesu-about` | textos, `image/image_id/image_variants`, `counters[]` |
| 3 | `sucesu-brands` | `title` + 6 `items[]` estáticos |
| 4 | `sucesu-associados` | apenas `title` (itens vêm do banco) |
| 5 | `sucesu-events` | `subtitle/title` + `items[]` |
| 6 | `sucesu-regionais` | textos, `states`, `cta_*`, `image*` |
| 7 | `sucesu-news` | `subtitle/title/description` + `items[]` |

O seed grava também `image`/`image_variants` **junto** com `image_id` — combinação que o editor não
produz e que o `prepare` sobrescreve quando a mídia existe.

## 13. Lacunas

| ID | Lacuna | Onde procurar | Impacto |
|---|---|---|---|
| `LG-HOMEPAGE-07` | Não há plano de migração de payloads antigos quando um schema muda | histórico/PRs futuros | risco de seção “sumir” após mudança de widget |
| `LG-HOMEPAGE-08` | `source_type`/`source_config` não aparecem em nenhum payload gerado pelo seed ou editor | busca global | os caminhos `dynamic-*` podem nunca ter sido usados |
| `LG-HOMEPAGE-09` | Não há validação de tamanho total do payload | `post_max_size`, jsonb | save pode falhar sem mensagem amigável |
| `LG-HOMEPAGE-10` | Não foi encontrado consumidor do rótulo `Seção Homepage` do dashboard | `DashboardStatsService.php:670` | funcionalidade de auditoria possivelmente incompleta |

## 14. Checklist de paridade

- [ ] `PC-HOMEPAGE-21` — `saveSections` grava o array exato e não cria linhas em `v2_homepage_sections`. Fonte: `HomepageCmsController.php:59-70`.
- [ ] `PC-HOMEPAGE-22` — Payload com chaves desconhecidas em `parameters` é preservado intacto. Fonte: `sections-manager.blade.php:223-273`.
- [ ] `PC-HOMEPAGE-23` — Linha soft-deletada é restaurada no save. Fonte: `HomepageCmsController.php:64-67`.
- [ ] `PC-HOMEPAGE-24` — `order` não influencia a ordem de renderização. Fonte: `HomepagePortalController.php:44-48`; viewer `:33-48`.
- [ ] `PC-HOMEPAGE-25` — `visible_slugs` é persistido mas não filtra o carrossel. Fonte: `SucesuAssociadosWidget.php:28-41`.
- [ ] `PC-HOMEPAGE-26` — `limit`/`event_limit` são strings no payload e convertidos com `(int)` no widget. Fonte: `SucesuNoticiasWidget.php:45`; `SucesuAgendaWidget.php:34-35`.
- [ ] `PC-HOMEPAGE-27` — `payload` nulo derruba `/` com `TypeError` (não há guarda). Fonte: `HomepagePortalController.php:45-47`; `HomepageDataIntegrator.php:23`.
- [ ] `PC-HOMEPAGE-28` — `down()` da migration renomeia a tabela para `backup_v2_homepage_sections_<Y_m_d_His>`. Fonte: migration `:33-38`.
- [ ] `PC-HOMEPAGE-29` — `HomepageSection::create([...])` gera UUID v4 e aceita `parameters` como array. Fonte: `HomepageSection.php:71-80`.
- [ ] `PC-HOMEPAGE-30` — `HomepageSection::active()->ordered()` ordena por `sort_order` asc. Fonte: `HomepageSection.php:88-102`.

## 15. Evidências consultadas

- `V2/Modules/Layouts/Homepage/Controllers/HomepageCmsController.php:53-85`
- `V2/Modules/Layouts/Homepage/Controllers/HomepagePortalController.php:42-48`
- `V2/Modules/Layouts/Homepage/Models/HomepageSection.php:1-103`
- `V2/Modules/Layouts/Homepage/Migrations/2026_07_22_000001_create_v2_homepage_sections_table.php:1-39`
- `V2/Modules/Layouts/Homepage/LseWidgets/Schemas/*.php` (10 arquivos)
- `V2/Modules/Layouts/Homepage/LseWidgets/Widgets/*.php` (10 arquivos)
- `V2/Modules/Layouts/Homepage/Services/HomepageDataIntegrator.php:23-51`
- `V2/Modules/Support/StructuralLandingEditor/Models/LayoutBaseConfig.php:1-39`
- `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_03_000001_create_v2_layout_base_configs_table.php:1-60`
- `V2/Seeds/BasicPages/SucesuHomepageSeeder.php:241-394`
- `V2/Services/LandingSectionsEditor/Views/components/forms/logic/sections-manager.blade.php:212-331`
