---
id: MOD-LAYOUTS-HOMEPAGE-CMS
tipo: modulo
onda: 5
status: concluido
unidades: [MOD-LAYOUTS-HOMEPAGE]
fontes:
  - V2/Modules/Layouts/Homepage/Views/cms/
  - V2/Modules/Layouts/Homepage/Views/lse/forms/
  - V2/Modules/Layouts/Homepage/Controllers/HomepageCmsController.php
  - V2/Modules/Layouts/Homepage/Routes/cms.php
  - V2/Modules/Layouts/Homepage/ModuleProvider.php
depende_de: [CORE-CMS, CORE-AUTH, SVC-LANDINGSECTIONSEDITOR, SVC-GALLERY, SVC-CONTENTRICHEDITOR, SVC-ACTIVITYLOGGER, MOD-SUPPORT-STRUCTURALLANDINGEDITOR]
consumidores: [UI-CMS]
criticidade: media
dono: Homepage
---

# Homepage — telas do CMS

> Há **uma** tela viva (`GET /cms/homepage`), que é um invólucro do editor de seções do LSE, e um
> conjunto de telas mortas (`cms/index.blade.php` e `cms/forms/*`) que apontam para rotas que não
> existem. Este documento descreve a tela viva campo a campo, o contrato JS que ela usa e as telas
> legadas que **não** devem ser reimplementadas.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Permite a um operador autorizado compor a home adicionando, editando, reordenando e removendo seções, e salvar o conjunto em um único POST. |
| Entrada | `GET /cms/homepage`; interações no componente LSE (dropdown de tipos, modal por seção, setas, lixeira); `POST /cms/homepage/save` com `landing_sections_json`; `POST /cms/homepage/refresh-cache`. |
| Saída | HTML `v2-homepage::cms.editor`; HTML de formulário por tipo; HTML do iframe de preview; `302` + flash; JSON `{success}`/422. |
| Efeitos colaterais | Navegação do menu CMS (item "Homepage"); preview em cache por 1 h; mídia enviada ao Gallery; auditoria ao salvar; `DEL page:*`. |
| Particularidade central | O estado da tela inteira é um único `<input type="hidden">`; não há drag-and-drop (as setas ↑/↓ são o reordenamento) e o teste de validação é por seção, não no salvar. |
| Como se integra | Layout `v2-cms::layouts.app`; componente `v2-service-landingsectionseditor::components.forms.landing-sections-editor`; rotas LSE `v2.landing-sections.*`; `v2-cms::partials.activity-footer`. |
| Se removido, quebra | O operador perde a única forma de editar a home; o portal continua servindo o último payload. |
| Superfície pública | Views `cms.editor`; 10 forms `lse.forms.*`; IDs/classes usados pelo JS do LSE. |

## 1. Efeito

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

- **Abrir** o editor: título `Homepage — Seções`, cabeçalho `Editor de Seções da Homepage`, botão
  `Salvar` e botão `Atualizar cache` (`V2/Modules/Layouts/Homepage/Views/cms/editor.blade.php:1-42`).
- **Adicionar seção**: dropdown `ADICIONAR SEÇÃO` com os tipos padrão do LSE e os 10 `sucesu-*` em
  "Personalizados" (`V2/Services/LandingSectionsEditor/Views/components/forms/landing-sections-editor.blade.php:31-72`).
- **Editar seção**: abre um painel modal customizado (`role="dialog"`, `aria-modal="true"`) que
  carrega o form do tipo por AJAX (`V2/Services/LandingSectionsEditor/Views/components/forms/logic/section-modal.blade.php:1-18`).
- **Salvar seção**: valida no servidor, grava no array em memória, atualiza o hidden, dispara o
  preview e mostra `Secao atualizada localmente!` via toastr
  (`V2/Services/LandingSectionsEditor/Views/components/forms/logic/sections-manager.blade.php:215-300`).
- **Reordenar**: setas ↑/↓ movem uma posição, recalculam `order` de 1..N e persistem o estado
  (`sections-manager.blade.php:71-97`).
- **Remover**: confirmação nativa `Deseja realmente remover esta secao?` e toast
  `Secao removida localmente!` (`sections-manager.blade.php:336-351`).
- **Salvar a home**: submete o formulário externo e recebe
  `Seções da homepage salvas com sucesso!` (`V2/Modules/Layouts/Homepage/Controllers/HomepageCmsController.php:76-84`).
- **Atualizar cache**: form separado, `POST /cms/homepage/refresh-cache`, flash
  `Cache da homepage atualizado com sucesso!` (`HomepageCmsController.php:96-113`).
- **Preview**: iframe à direita é recarregado a cada mudança (debounce 800 ms) e recebe troca
  pontual de seção por `postMessage` (`sections-manager.blade.php:302-317`).

### 1.2 O que muda no sistema

| Efeito | Onde | Fonte |
|---|---|---|
| Um request AJAX por tipo de formulário, depois cacheado no cliente | `formTemplatesCache['new_'+type]` | `sections-manager.blade.php:2 e :177-209` |
| Uma entrada de cache de preview por token (1 h) | `landing_preview_<token>` | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:150-153` |
| Mídia escolhida no modal global da galeria vira `image_id` no form | evento `lotep:open-gallery-picker-lse_global_media_picker` | `sections-manager.blade.php:122-132` |
| Salvar a home grava `payload`, audita e apaga `page:*` | `saveSections` | `HomepageCmsController.php:64-80`; `:126-142` |
| Histórico de auditoria exibido no rodapé | `v2-cms::partials.activity-footer` | `cms/editor.blade.php:62-65` |

### 1.3 Contrato observável

| Entrada | Saída |
|---|---|
| Abrir o editor sem permissão | `302 v2.cms.dashboard` + `Acesso negado. Seu perfil não possui permissão para acessar o módulo: Homepage` |
| Salvar seção sem `title` obrigatório | alerta JS `Por favor, corrija os seguintes erros:` + lista do 422 |
| Falha de rede na validação | alerta `Erro na validacao dos dados da secao.` |
| Erro ao buscar o form | painel mostra `Erro ao carregar o formulario da secao.` |
| Nenhuma seção | sidebar mostra `Nenhuma secao adicionada.` |
| Salvar sem `landing_sections_json` | `302` com `The landing sections json field is required.` |
| Salvar JSON inválido | `302` + flash success; payload vira `[]` |
| Salvar sem permissão de escrita | `302 back` + `Operação negada. Seu perfil possui apenas permissão de leitura para o módulo: Homepage` |

### 1.4 O que esta tela NÃO faz

- **Não há drag-and-drop**: `initSortable()` é um no-op (`sections-manager.blade.php:147-149`).
- **Não há rascunho/publicação**: salvar já é público (o cache é invalidado).
- **Não há exclusão individual persistida**: remover só altera o array; só o `Salvar` grava.
- **Não há preview server-side da home inteira no CMS**: o iframe renderiza o endpoint público de
  preview do LSE (`/cms/v2/landing-sections/preview/render`), não `/`.
- **Não exibe `old/new` por seção**: o histórico é do payload inteiro.
- **Não valida o JSON no cliente** antes de enviar; o hidden é gerado pelo próprio componente.

## 2. Abordagem

### 2.1 Estratégia

```mermaid
flowchart LR
  A[GET /cms/homepage] --> B[cms.editor]
  B --> C[componente LSE]
  C --> D[hidden landing_sections_json]
  C --> E[sidebar em memória]
  C --> F[modal por seção]
  F -->|GET /form| G[form do widget]
  F -->|POST /validate| H[schema do widget]
  F -->|POST /preview/render-section| I[HTML da seção]
  C -->|POST /preview/save| J[cache 1h]
  J --> K[iframe preview]
  D -->|POST /cms/homepage/save| L[(landing_sections)]
```

### 2.2 Mecânica passo a passo

1. `sections()` lê a config e o histórico; passa `$sections` como `value` do componente
   (`cms/editor.blade.php:51-55`).
2. O componente gera `token = Str::random(40)` e serializa `value` para o hidden
   (`landing-sections-editor.blade.php:1-12,23-24`).
3. `state-loader` faz `JSON.parse` do hidden; erro → `sections = []`
   (`logic/state-loader.blade.php:2-9`).
4. `init()` limpa backdrops, registra eventos, renderiza a sidebar e salva o preview inicial
   (`sections-manager.blade.php:401-424`).
5. Adicionar/editar → `loadForm(type, id, parameters)` → modal com spinner
   `Carregando formulario...` → `GET /form` → HTML injetado → `UIManager.initSelect2`,
   `initIconPickers`, `initContentRichEditors` (`sections-manager.blade.php:161-210`).
6. Salvar seção → `serialize()` + `type`/`section_id` → `POST /validate`; sucesso → reconstrói
   `parameters` a partir de `serializeArray()`; cria `{id: generateUuid(), type, parameters, order}`
   ou atualiza `parameters` (`sections-manager.blade.php:215-294`).
7. `persistState()` grava o hidden, re-renderiza a sidebar e agenda `triggerPreviewSave` em 800 ms
   (`logic/preview-manager.blade.php:14-40`).
8. `triggerPreviewSave` → `POST /preview/save` (cache 1 h) → recarrega o iframe
   (`preview-manager.blade.php:26-50`).
9. `POST /cms/homepage/save` → decodifica o hidden → grava payload → audita → `DEL page:*`.
10. `POST /cms/homepage/refresh-cache` → só invalida cache e audita.

### 2.3 Decisões de design

| Decisão | Motivo | Marca |
|---|---|---|
| Um único hidden em vez de um form por seção | simplifica o salvamento atômico do payload | `[C]` `landing-sections-editor.blade.php:24` |
| Modal custom (overlay + painel), não Bootstrap Modal | evita backdrops acumulados e permite submodais (galeria) | `[C]` `section-modal.blade.php:1-18`; `sections-manager.blade.php:5-33` |
| Setas em vez de Sortable | o reordenamento precisava funcionar sem dependência extra | `[C]` `sections-manager.blade.php:147-156` |
| Cache de formulário no cliente por `new_<type>`/`edit_<id>` | evita requests repetidos ao abrir o mesmo tipo | `[C]` `sections-manager.blade.php:2 e :177-205` |
| Preview em cache de servidor por token | iframe precisa de uma fonte estável entre reloads | `[C]` `LandingSectionsEditorController.php:150-153` |
| Validação só da seção no modal | evita bloquear o salvamento por uma seção incompleta | `[I]` `HomepageCmsController.php:55-62` |

### 2.4 Trechos de código pivô

```php
{{-- V2/Modules/Layouts/Homepage/Views/cms/editor.blade.php:46-56 — o form externo só carrega o componente --}}
<form id="homepage-sections-form" action="{{ route('v2.homepage.sections.save') }}" method="POST">
    {{ csrf_field() }}

    @include('v2-service-landingsectionseditor::components.forms.landing-sections-editor', [
        'name' => 'landing_sections_json',
        'value' => $sections,
        'label' => 'Seções da Homepage'
    ])
</form>
```

> Todo o estado é serializado pelo componente no input `landing_sections_json`; o form externo não
> tem outros campos. `Salvar` está fora do form e usa o atributo `form="homepage-sections-form"`.

```javascript
// V2/Services/LandingSectionsEditor/Views/components/forms/logic/preview-manager.blade.php:14-40 — persistência do estado
const persistState = () => {
    $hiddenInput.val(JSON.stringify(sections));
    renderSidebar();

    if (previewDebounceTimer) clearTimeout(previewDebounceTimer);
    previewDebounceTimer = setTimeout(triggerPreviewSave, 800);
};

const triggerPreviewSave = () => {
    $.ajax({
        url: urls.savePreview,
        type: 'POST',
        data: { token: previewToken, sections: JSON.stringify(sections) },
        success: (response) => { if (response.success) { refreshPreview(); } }
    });
};
```

> O hidden e o cache de preview são atualizados **antes** do salvamento definitivo. Fechar a aba
> sem clicar em Salvar perde as mudanças do payload, mas deixa um preview em cache por 1 h.

```javascript
// V2/Services/LandingSectionsEditor/Views/components/forms/logic/sections-manager.blade.php:215-294 (resumo) — validação e criação da seção
$.post(urls.validate, formData, (response) => {
    if (response.success) {
        const serialized = $form.find('input, textarea, select').serializeArray();
        const parameters = {};
        serialized.forEach(item => {
            const nameAttr = item.name;
            if (nameAttr.startsWith('parameters[')) {
                // reconstrói objetos/arrays a partir de parameters[a][0][b]
            }
        });
        const savedSectionId = currentSectionId || generateUuid();
        if (currentSectionId) {
            const found = sections.find(s => s.id === currentSectionId);
            if (found) { found.parameters = parameters; }
        } else {
            sections.push({ id: savedSectionId, type: currentType, parameters: parameters, order: sections.length + 1 });
        }
        hideSectionModal();
        persistState();
        // ...
    }
});
```

> A validação acontece **antes** de qualquer alteração em memória; o `parameters` reconstruído é
> exatamente o que irá para o JSON. `serializeArray` ignora inputs `disabled`.

```php
// V2/Modules/Layouts/Homepage/Controllers/HomepageCmsController.php:96-113 — refresh de cache sem conteúdo
public function refreshCache()
{
    $this->invalidatePageCache();

    try {
        $config = LayoutBaseConfig::withTrashed()->where('key', 'landing_sections')->first();
        if ($config) {
            $this->activityLogger->log($config, 'updated', ['type' => 'homepage_cache_refresh']);
        }
    } catch (\Exception $e) {
        Log::warning('[HomepageCms] Falha ao registrar auditoria do refresh de cache: ' . $e->getMessage());
    }

    flash('Cache da homepage atualizado com sucesso!')->success();
    return redirect()->route('v2.homepage.sections');
}
```

> O refresh não falha se não existir config: apenas não audita. A invalidação em si é best-effort.

### 2.5 Custos e limites

- Um `GET /form` por tipo (cacheado no cliente) e um `POST /validate` por salvamento de seção.
- Um `POST /preview/save` + recarga de iframe a cada 800 ms de inatividade após uma mudança.
- `Cache::put` de 1 h por token; tokens nunca são limpos explicitamente.
- O salvamento definitivo é um único `POST` com o payload completo (sem limite de tamanho no
  controller).

## 3. Particularidades

### 3.1 O que só existe aqui

- **Wrapper de 68 linhas** para um editor de 424 linhas de lógica JS do LSE: o módulo não tem
  lógica de UI própria.
- **Dois botões no mesmo cabeçalho** com semânticas diferentes: `Salvar` (submete o form externo) e
  `Atualizar cache` (form separado) (`cms/editor.blade.php:30-43`).
- **Histórico do payload inteiro** no rodapé, não por seção.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-HOMEPAGE-41` | O id da seção é gerado no cliente (`crypto.getRandomValues`) | salvar a mesma seção duas vezes mantém o id; duplicar gera novo | `sections-manager.blade.php:389-396` |
| `RN-HOMEPAGE-42` | `parameters` só inclui campos `name="parameters[...]"` | campos sem esse prefixo (ex.: `type`, `section_id`) não são persistidos | `sections-manager.blade.php:226-273` |
| `RN-HOMEPAGE-43` | Inputs `disabled` não são serializados | o hidden de `hidden_slugs` só entra quando o parceiro está oculto | `lse/forms/associados.blade.php:25-31,48-57` |
| `RN-HOMEPAGE-44` | O preview não reflete o payload salvo, e sim o estado em memória | recarregar a página antes de salvar volta ao conteúdo antigo | `preview-manager.blade.php:14-50` |
| `RN-HOMEPAGE-45` | O editor não exibe erros do `POST /save` na própria tela | validação de `landing_sections_json` aparece como erro de sessão do Laravel | `HomepageCmsController.php:55-57` |
| `RN-HOMEPAGE-46` | A ordem das seções é a ordem de `sections` no JS; `order` é reescrito | mover uma seção emite novo `order` e novo array | `sections-manager.blade.php:71-97` |
| `RN-HOMEPAGE-47` | O modal não prende foco nem fecha com ESC por código próprio | acessibilidade depende do overlay/click; ver §3.4 | `section-modal.blade.php:1-18`; `sections-manager.blade.php:106-117` |

### 3.3 Catálogo de formulários dos 10 widgets

Todos recebem `$data` (parâmetros atuais), `$sectionId` e `$type` e usam `parameters[...]`.

| Tipo | View | Campos e controles | Observações |
|---|---|---|---|
| `sucesu-hero` | `lse/forms/hero.blade.php` | slides: `title` (req), `subtitle`, `cta_text`, `cta_url`, `image_id` | `<template>` + reindex de `parameters[slides][N]` e `data-input-id` (`:84-169`) |
| `sucesu-services` | `lse/forms/services.blade.php` | `items[]`: `title` (req), `description`, `url`, `icon_id` | item-card genérico (`:12-24`) |
| `sucesu-about` | `lse/forms/about.blade.php` | `subtitle`, `title` (req), `description`, `cta_text`, `cta_url`, `image_id`, `counters[].{value,suffix,label,icon_id}` | dois `_item-list-js` separados (`:93-99`) |
| `sucesu-brands` | `lse/forms/brands.blade.php` | `title`, `items[].{name (req), image_id}` | |
| `sucesu-events` | `lse/forms/events.blade.php` | `subtitle`, `title` (req), `items[].{title (req), details, url, image_id}` | `details` é input text, não textarea (`:98-119`) |
| `sucesu-news` | `lse/forms/news.blade.php` | `subtitle`, `title` (req), `description`, `items[].{title (req), excerpt, date, url, image_id}` | `date` placeholder `14 MAI` (`:43-49`) |
| `sucesu-regionais` | `lse/forms/regionais.blade.php` | `subtitle`, `title` (req), `text1`, `states`, `text2`, `cta_text`, `cta_url`, `image_id` | `title` com `required` |
| `sucesu-noticias` | `lse/forms/noticias.blade.php` | `title`, `limit` (`min=2 max=50`), `categories[]` | consulta `NewsCategory::ordered()` (`:1-6`); default = todas |
| `sucesu-associados` | `lse/forms/associados.blade.php` | `title`, `visible_slugs[]`, `hidden_slugs[]` | consulta `Parceiro::active()->ordered()` (`:1-7`); checkbox sincroniza hidden por JS (`:46-68`) |
| `sucesu-agenda` | `lse/forms/agenda.blade.php` | `title` (req), `event_limit` (`1..12`), `months_ahead` (`1..12`) | textos de ajuda `Quantos eventos futuros…`, `Quantos meses à frente…` (`:57-74`) |

Placeholders exatos também fazem parte do contrato visual: `Ex: O Poder da Big Data`,
`Ex: 14 e 15 de maio — Centro de Eventos`, `Ex: /agenda/big-data`
(`lse/forms/events.blade.php:94-110`).

### 3.4 Casos-limite, estados e comportamentos surpreendentes

| Estado | Comportamento |
|---|---|
| Sidebar vazia | `Nenhuma secao adicionada.` (`sections-manager.blade.php:359-362`) |
| Botão ↑ na primeira seção / ↓ na última | `disabled` (`sections-manager.blade.php:378-379`) |
| Form já carregado | vem do `formTemplatesCache` e não faz request (`:194-198`) |
| Erro 422 | `alert` com a lista de erros por campo; nada é alterado (`:319-329`) |
| Erro 500/outro | `alert('Erro na validacao dos dados da secao.')` (`:327-329`) |
| Remoção | `confirm` nativo; sem chamada ao servidor até Salvar (`:336-350`) |
| Trocas de tipo no dropdown | os forms são recarregados por tipo; `interactive-table` ganha `modal-xl` (`:168-172`) |
| Fechar modal | `X`, `Fechar` ou clique no overlay; sem ESC (`:111-117`) |
| Sem JS | o hidden existe, mas não há como adicionar/editar seções; o form envia o JSON inicial |
| Toastr ausente | as mensagens de sucesso são silenciosas (`typeof toastr !== 'undefined'`) |

**Acessibilidade observada:** `label for` em todos os campos, `aria-required` nos obrigatórios,
`aria-label` nos botões de ação, `role="dialog"`/`aria-modal="true"`/`aria-labelledby` no modal,
`title` no iframe (`landing-sections-editor.blade.php:85`). **Lacunas:** não há `aria-live` para
toasts/erros, o modal não prende foco e não fecha com ESC, e o overlay não é anunciado
(`section-modal.blade.php:1-18`).

### 3.5 Telas legadas (não reimplementar)

| Arquivo | O que tentava fazer | Por que está morto |
|---|---|---|
| `Views/cms/index.blade.php` | listar seções em tabela, arrastar para reordenar (SortableJS), alternar `is_active` | nenhuma rota o renderiza; referencia `v2.homepage.edit`, `v2.homepage.reorder` e `cms/homepage/{id}/toggle` (`:64,103,123`) |
| `Views/cms/forms/hero.blade.php` | editar seção hero com `source_type` | form aponta para `v2.homepage.update` e `v2.homepage.index` (`:20,26,107`), inexistentes |
| `Views/cms/forms/{about,brands,events,news,regionais,services,agenda}.blade.php` | CRUD por seção em `v2_homepage_sections` | mesmos problemas; `agenda` tem `source_type` hidden e `source_config` (`:3-4,31`) |
| `Views/cms/index.blade.php` (estado vazio) | `Nenhuma seção encontrada. Execute o seeder…` | só aparece se houvesse rota |

> Há uma divergência adicional: os forms legados enviam `source_type` no **nível do form**, mas o
> integrador lê `parameters.source_type` (`HomepageDataIntegrator.php:29-40`).

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura/props | Precondição | Consumidores |
|---|---|---|---|---|
| `v2-homepage::cms.editor` | view | `$sections` (array), `$history` (Collection), `$config` (model/null) | rota autenticada | `HomepageCmsController@sections` |
| `landing-sections-editor` | componente | `name`, `value`, `label` | dentro de um form com CSRF | editor da home e de Sobre Nós |
| `_item-card` | partial | `item`, `index`, `groupKey`, `inputIdBase`, `fields[]` | dentro de um form de seção | forms `services/brands/events/news/about` |
| `_item-list-js` | partial | `containerId`, `templateId`, `addBtnId`, `noItemsId`, `inputIdBase`, `groupKey`, `labelSingular` | IDs existentes no DOM | idem |
| `_hero-slide-card` | partial | `slide`, `index` | — | `lse/forms/hero` |

### 4.2 Contrato de UI/JS

| Elemento | ID/classe | Papel |
|---|---|---|
| Hidden do payload | `#landing-sections-hidden-landing_sections_json` | estado serializado |
| Lista de seções | `#section-list-landing_sections_json` | renderizada por `renderSidebar()` |
| Iframe | `#preview-iframe-landing_sections_json` | preview |
| Overlay/painel | `#lse-overlay-landing_sections_json`, `#section-modal-landing_sections_json` | modal |
| Corpo do modal | `#modal-body-content-landing_sections_json` | form carregado |
| Botões | `#btn-modal-save-…`, `#btn-modal-close-…`, `#btn-modal-close2-…` | ações |
| Item de seção | `.lse-section-item-landing_sections_json` + `.btn-move-up-section`, `.btn-move-down-section`, `.btn-edit-section`, `.btn-remove-section` | ações por seção |
| Eventos globais | `lotep:open-gallery-picker-lse_global_media_picker`, `lotep:update-section-html` | galeria e swap de preview |

Fonte: `sections-manager.blade.php:38-142,370-384`; `preview-manager.blade.php:45-50`.

### 4.3 Ordem e ciclo de vida

1. O componente depende de jQuery, Bootstrap (modal/backdrop classes), SortableJS **não** (desativado)
   e do gallery picker global (`landing-sections-editor.blade.php:92`).
2. `init()` roda no `$(document).ready`; se o componente for inserido depois, é preciso chamar
   `init()` manualmente.
3. Cada seção carrega o form sob demanda; o cache de formulário é limpo em `edit_<id>`/`new_<type>`
   ao salvar e `edit_<id>` ao remover (`:275-278,339-340`).
4. O iframe é recriado por `src` (reload completo) e atualizado por `postMessage` para troca pontual.

### 4.4 Receitas de uso

**1. Abrir a tela em um teste de feature:**

```php
$this->actingAs($user)->get(route('v2.homepage.sections'))
    ->assertStatus(200)
    ->assertSee('Editor de Seções da Homepage');
```

**2. Salvar um payload mínimo:**

```http
POST /cms/homepage/save HTTP/1.1
Content-Type: application/x-www-form-urlencoded
X-CSRF-TOKEN: ...

landing_sections_json=[{"id":"sec-1","type":"sucesu-hero","order":1,"parameters":{"slides":[]}}]
```

Resposta: `302` para `v2.homepage.sections` com `Seções da homepage salvas com sucesso!`.

**3. Forçar a atualização do cache pela tela:** clicar em **Atualizar cache** (envia o form
separado com CSRF) e conferir o flash `Cache da homepage atualizado com sucesso!`.

### 4.5 Anti-uso

- **Criar rotas para `cms/index.blade.php`/`cms/forms/*`**: elas pressupõem `HomepageSection` e um
  controller de CRUD que não existe; reimplementá-las criaria uma segunda fonte de verdade.
- **Mover o botão Salvar para dentro do form do componente**: o componente já usa o atributo
  `form`; duplicar o submit pode enviar o hidden duas vezes ou perder o `csrf_field()` externo.
- **Revalidar o payload inteiro no save** (mudança de contrato): hoje salvar aceita qualquer JSON;
  bloquear mudaria o comportamento observável de payloads antigos.
- **Confiar em `toastr`**: ele é opcional; o fluxo funciona sem feedback visual.

### 4.6 Custos e limites

- Requisições por sessão de edição: 1 abertura + N forms + M validações + P previews (debounce) +
  1 save.
- Preview limitado a 1 h de cache por token; sem GC.
- Upload de mídia no form: até 10 MB por arquivo (`LandingSectionsEditorController.php:268-272`).
- Sem indicador de “alterações não salvas” ao sair da página.

## 5. Rotas e contratos HTTP

| Método | URI | Nome | Permissão | Efeito |
|---|---|---|---|---|
| GET | `/cms/homepage` | `v2.homepage.sections` | `Homepage:read` | HTML do editor + histórico |
| POST | `/cms/homepage/save` | `v2.homepage.sections.save` | `Homepage:write` | grava payload + flash |
| POST | `/cms/homepage/refresh-cache` | `v2.homepage.cache.refresh` | `Homepage:write` | limpa `page:*` + flash |

Dependências HTTP do editor (serviço LSE, `CmsAuthenticate`): `GET /cms/v2/landing-sections/form`,
`POST /validate`, `POST /preview/save`, `GET /preview/render`, `POST /preview/render-section`,
`GET|POST|PUT|DELETE /media*` (`V2/Services/LandingSectionsEditor/Routes/cms.php:9-25`).

## 6. Dados e modelo de domínio

- A tela não acessa `v2_homepage_sections`; lê/escreve `v2_layout_base_configs.payload` via
  `LayoutBaseConfig` (`cms/editor.blade.php:51-55`; `HomepageCmsController.php:32 e :64-70`).
- Histórico: `activity_log` filtrado por `subject` = a config (`HomepageCmsController.php:35-42`).

## 7. Fluxos e estados

```mermaid
stateDiagram-v2
  [*] --> Carregando
  Carregando --> Editando: config carregada
  Editando --> ModalAberto: adicionar/editar
  ModalAberto --> Validando: Salvar Seção
  Validando --> Editando: 422 (alert)
  Validando --> Editando: success (memória + hidden)
  Editando --> PreviewAtualizando: qualquer mudança (debounce 800ms)
  PreviewAtualizando --> Editando
  Editando --> Salvando: Salvar (form externo)
  Salvando --> Editando: 302 + flash
  Editando --> SemAlteracoes: recarregar sem salvar
```

## 8. UI CMS

### 8.1 Tela `GET /cms/homepage`

| Aspecto | Valor |
|---|---|
| Rota/permissão | `v2.homepage.sections` / `Homepage:read` (write para os POSTs) |
| Layout | `v2-cms::layouts.app` com `title='Homepage — Seções'` |
| Objetivo | compor a home por seções |
| Estrutura | header (`h1` + texto de ajuda) → card "Seções" com Salvar/Atualizar cache → componente LSE (sidebar + iframe) → rodapé de auditoria |
| Estados | carregando form (spinner), vazio (`Nenhuma secao adicionada.`), erro de form, 422 (alert) |
| Mensagens exatas | ver §1.3 |
| JS | inline no componente LSE; jQuery; CSRF via `$.ajaxSetup` (`sections-manager.blade.php:38-42`) |
| Ordenação | botões ↑/↓; sem drag |
| Paginação/bulk actions | não se aplica |

### 8.2 Formulários de seção

Tabela em §3.3. Todos são parciais (sem layout) porque são injetados no modal.

### 8.3 Acessibilidade

- Positivo: `label for`, `aria-label` nos botões de ícone, `aria-required`, `role=dialog`,
  `aria-modal`, `aria-labelledby`, `title` do iframe, `alt` nos previews de imagem.
- Negativo: sem `aria-live` para toast/alert, sem foco preso, sem ESC, contraste de `text-muted`
  pequeno em várias legendas, botões de ação com apenas ícone + `title` (não `aria-label`).

## 9. UI Portal

Não se aplica: esta unidade de tela é exclusiva do CMS. O portal é descrito em
[`portal.md`](portal.md).

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

- Permissão `Homepage` (`read|write`) via `active_pattern='cms/homepage*'`
  (`ModuleProvider.php:44`; `V2PermissionMiddleware.php:40-48`).
- `super-admin` e `v2.support.email` ignoram o RBAC (`V2PermissionMiddleware.php:69-72`).
- Flag `module.active:Homepage`: desativada → POST AJAX recebe
  `{"error":"Modulo desativado: Homepage","code":404}` e a navegação normal recebe
  `404 Modulo desativado.` (o redirect ao dashboard só ocorre quando o prefixo da rota é
  exatamente `cms`; aqui é `cms/homepage`).
- Menu: item "Homepage" (`position=0`) com submódulo "Seções da Homepage" (`bi bi-layers`,
  `position=1`) (`ModuleProvider.php:39-54`).

## 11. Observabilidade, erros e limites

| Evento | Registro | Fonte |
|---|---|---|
| Salvar | `activity_log` `updated`, `type=homepage_sections_save`, `old`, `new` | `HomepageCmsController.php:76-80` |
| Refresh | `activity_log` `updated`, `type=homepage_cache_refresh`; falha → warning | `HomepageCmsController.php:100-109` |
| Falha no Redis | `warning [HomepageCms] Falha ao invalidar cache de pagina: …` | `HomepageCmsController.php:137-141` |
| Preview | `Log::debug` das flags de acessibilidade | `LandingSectionsEditorController.php:174-184` |
| Upload de mídia | `activity_log` `created`/`updated`/`deleted` no LSE | `LandingSectionsEditorController.php:282-292,327-337,360-370,392-402` |

Limites: nenhum throttle próprio; upload de 10 MB; `Cache::put` 1 h; `KEYS` no Redis ao salvar.

## 12. Seeds e dados iniciais

- Nenhum seeder desta tela; o conteúdo inicial vem de `SucesuHomepageSeeder`.
- Para uma instalação sem payload, a tela abre vazia (não há botão “criar seções padrão”).

## 13. Lacunas

| ID | Lacuna | Onde procurar | Impacto |
|---|---|---|---|
| `LG-HOMEPAGE-16` | Não há confirmação ao sair com alterações não salvas | `sections-manager.blade.php` | perda silenciosa de edição |
| `LG-HOMEPAGE-17` | O modal não implementa focus trap/ESC | `section-modal.blade.php`; `sections-manager.blade.php:106-117` | acessibilidade WCAG 2.1.2/2.4.3 |
| `LG-HOMEPAGE-18` | Não há indicação visual de qual seção tem erro de schema | fluxo 422 | operador precisa abrir seção por seção |
| `LG-HOMEPAGE-19` | Não há limite de seções/itens na UI | forms | payloads muito grandes |
| `LG-HOMEPAGE-20` | Telas legadas não têm decisão registrada de remoção | `Views/cms/index.blade.php`; `Views/cms/forms/` | risco de alguém “consertá-las” |

## 14. Checklist de paridade

- [ ] `PC-HOMEPAGE-45` — `GET /cms/homepage` renderiza o componente LSE com `name=landing_sections_json` e o payload atual no hidden. Fonte: `cms/editor.blade.php:51-55`; `landing-sections-editor.blade.php:23-24`.
- [ ] `PC-HOMEPAGE-46` — O dropdown separa `sucesu-*` em "Personalizados". Fonte: `landing-sections-editor.blade.php:35-70`.
- [ ] `PC-HOMEPAGE-47` — Salvar seção valida no servidor e só então altera o array em memória. Fonte: `sections-manager.blade.php:215-294`.
- [ ] `PC-HOMEPAGE-48` — Reordenar recalcula `order` de 1..N e persiste o hidden. Fonte: `sections-manager.blade.php:71-97`; `preview-manager.blade.php:14-21`.
- [ ] `PC-HOMEPAGE-49` — Remover pede confirmação com o texto exato e não chama o servidor. Fonte: `sections-manager.blade.php:336-350`.
- [ ] `PC-HOMEPAGE-50` — Preview é salvo no servidor 800 ms após a última mudança. Fonte: `preview-manager.blade.php:14-40`.
- [ ] `PC-HOMEPAGE-51` — Formulário já aberto é servido do cache do cliente (`new_<type>`/`edit_<id>`). Fonte: `sections-manager.blade.php:177-198`.
- [ ] `PC-HOMEPAGE-52` — `sucesu-associados` só envia `hidden_slugs` para parceiros desmarcados. Fonte: `lse/forms/associados.blade.php:25-31,48-67`.
- [ ] `PC-HOMEPAGE-53` — `sucesu-noticias` lista todas as categorias selecionadas por default. Fonte: `lse/forms/noticias.blade.php:1-6,26-40`.
- [ ] `PC-HOMEPAGE-54` — Falha de validação mostra `Por favor, corrija os seguintes erros:` com os campos. Fonte: `sections-manager.blade.php:319-329`.
- [ ] `PC-HOMEPAGE-55` — O botão "Atualizar cache" é um form separado com CSRF e não envia o payload. Fonte: `cms/editor.blade.php:35-42`.
- [ ] `PC-HOMEPAGE-56` — O rodapé mostra o histórico do `LayoutBaseConfig` e tolera ausência de config. Fonte: `cms/editor.blade.php:62-65`; `HomepageCmsController.php:35-42`.
- [ ] `PC-HOMEPAGE-57` — As telas legadas não são acessíveis por rota alguma. Fonte: `Routes/cms.php:5-9`; grep por `v2-homepage::cms.index`.

## 15. Evidências consultadas

- `V2/Modules/Layouts/Homepage/Views/cms/editor.blade.php:1-68`
- `V2/Modules/Layouts/Homepage/Views/cms/index.blade.php:1-139`
- `V2/Modules/Layouts/Homepage/Views/cms/forms/*.blade.php` (8 arquivos)
- `V2/Modules/Layouts/Homepage/Views/lse/forms/*.blade.php` (13 arquivos)
- `V2/Modules/Layouts/Homepage/Controllers/HomepageCmsController.php:1-143`
- `V2/Modules/Layouts/Homepage/Routes/cms.php:1-9`
- `V2/Modules/Layouts/Homepage/ModuleProvider.php:37-55`
- `V2/Services/LandingSectionsEditor/Views/components/forms/landing-sections-editor.blade.php:1-158`
- `V2/Services/LandingSectionsEditor/Views/components/forms/logic/{state-loader,preview-manager,section-modal,sections-manager,media-manager}.blade.php`
- `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:67-242`
- `V2/Services/LandingSectionsEditor/Routes/cms.php:9-25`
