---
id: SVC-LANDINGSECTIONSEDITOR
tipo: servico
onda: 2
status: concluido
unidades: [SVC-LANDINGSECTIONSEDITOR]
fontes:
  - V2/Services/LandingSectionsEditor/
depende_de: [SVC-GALLERY, SVC-CONTENTRICHEDITOR, SVC-STORAGE, CORE-RUNTIME]
consumidores: [MOD-LAYOUTS-HOMEPAGE, MOD-INSTITUCIONAL-SOBRENOS]
criticidade: alta
dono: SvcLse
---

# Serviço LandingSectionsEditor (LSE)

> **Alvo de reimplementação.** O contrato de registry/schema/ordem é preservado, mas os
> widgets viram componentes Blade com JS mínimo; jQuery, Bootstrap, Owl Carousel e
> `toastr` não são portados. O editor estrutural usa o shell `Layout\Cms` e os slots
> documentados. Ver [`pilha-alvo-public-v7.md`](../../09_reimplementacao/pilha-alvo-public-v7.md) §4.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Converte uma lista JSON de seções (`id`, `type`, `order`, `parameters`) em um editor CMS operável (adicionar/editar/reordenar/remover, com formulário por tipo, validação e preview) e no HTML do portal, despachando cada seção para o form/render do widget registrado. |
| Entrada | 10 rotas `cms/v2/landing-sections/*`; componente Blade `landing-sections-editor` (`name`, `value`, `label`); componente `landing-sections-viewer` (`sections`); API estática `WidgetRegistry`/`LandingSectionsEditorRendererService`. |
| Saída | Fragmentos HTML de formulário e de seção; JSON `{success,errors}`; HTML do iframe de preview; entrada de cache `landing_preview_<token>`; linhas em `v2_gallery_media` via `GallerySender`; registros de auditoria. |
| Efeitos colaterais | `Cache::put` de 1 h por token de preview (`V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:153`); `ActivityLoggerService::log` em mídia (`:286`, `:331`, `:364`, `:396`); `Log::debug` com flags de acessibilidade no preview (`:177-183`); upload/rotação/exclusão física de mídia delegada ao Gallery. |
| Particularidade central | É um registry de widgets + schemas + views: o serviço não persiste seções e não conhece os módulos consumidores; a chave de persistência (`landing_sections`, `sobre_nos_sections`) e o cache de página são responsabilidade de quem o consome. |
| Como se integra | Namespace PHP `V2\Services\LandingSectionsEditor\`; views `v2-service-landingsectionseditor::`; rotas com middleware `CmsAuthenticate`; registro de widgets por `WidgetRegistry::register($slug, $widget)` no construtor do `ModuleProvider` consumidor. |
| Se removido, quebra | Editor de seções da Homepage (`MOD-LAYOUTS-HOMEPAGE`), editor e portal de Sobre Nós (`MOD-INSTITUCIONAL-SOBRENOS`), preview e endpoints de mídia usados por ambos; qualquer widget `LseWidgets\Widgets\*` deixa de ter contrato/registro. |
| Superfície pública | `LandingSectionsEditorController` (10 ações); `LandingSectionsEditorRendererService::{registerLegacyWidgets,getWidgets,getSectionTypes,sanitizeContent,prepareSections}`; `WidgetRegistry::{register,get,all,has}`; `WidgetContract`; `BaseWidget`; `GenericWidget`; `ValidationService::{validate,getRulesFor}`; 22 classes `*Schema`; componentes Blade. |

## 1. Efeito

### 1.1 Transformação que o serviço realiza

Entrada típica (payload salvo pelo consumidor em `v2_layout_base_configs.payload`):

```json
[
  {
    "id": "3f2b6f0e-7c1a-4a4e-9d0f-6a1c2b3d4e5f",
    "type": "hero",
    "order": 1,
    "parameters": {
      "title": "SUCESU Nacional",
      "subtitle": "Tecnologia e representatividade",
      "text_alignment": "left",
      "title_color": "#ffffff",
      "subtitle_color": "#ffffff",
      "background_image_id": 42
    }
  }
]
```

- O editor renderiza a sidebar a partir desse array e, ao salvar uma seção, obtém o HTML do formulário em `GET /cms/v2/landing-sections/form` (`LandingSectionsEditorController::getForm`, `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:67-94`), valida por `POST /validate` (`:103-126`) e mantém o estado em memória + `<input type="hidden">`.
- O portal recebe o mesmo array e produz HTML: `prepareSections()` resolve mídias em lote e cada render view recebe `section` e `data` (`V2/Services/LandingSectionsEditor/Views/components/landing-sections-viewer.blade.php:33-48`).
- O preview ao vivo mantém uma cópia sanitizada em cache por 1 h, indexada pelo token aleatório de 40 caracteres gerado no componente (`Views/components/forms/landing-sections-editor.blade.php:2`, `Controllers/LandingSectionsEditorController.php:150-153`).

### 1.2 Efeitos sobre dados e sistema

| Efeito | Recurso afetado | Ponto no código | Fonte |
|---|---|---|---|
| Grava a lista de seções em cache por 1 h | cache `landing_preview_<token>` | `savePreview` | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:153` |
| Lê a lista do cache para montar o iframe | cache | `renderPreview` | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:172` |
| Lê mídias em uma única consulta `whereIn` | `v2_gallery_media` | `prepareSections` | `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:171-174` |
| Cria mídia no módulo `landing_page` | `v2_gallery_media` + arquivo | `storeMedia` → `GallerySender::upload` | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:280` |
| Atualiza `alt_text`/`description` | `v2_gallery_media` | `updateMedia` | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:325` |
| Rotaciona o arquivo 90° | arquivo + derivados | `rotateMedia` | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:359` |
| Exclui mídia e arquivo físico | `v2_gallery_media` + storage | `destroyMedia` | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:392` |
| Registra auditoria de mídia com modelo transiente | tabela do ActivityLogger | `new Model()->forceFill(['id' => $mediaId])` | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:284-289` |
| Loga flags de acessibilidade ao abrir preview | log | `Log::debug` | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:177-183` |

O LSE **não** grava seções: quem persiste é o consumidor. Ver `SobreNosCmsController::saveSections` (`V2/Modules/Institucional/SobreNos/Controllers/SobreNosCmsController.php:54-87`) e `HomepageCmsController::saveSections` (`V2/Modules/Layouts/Homepage/Controllers/HomepageCmsController.php:53-85`), ambos em `v2_layout_base_configs.payload` (casts `array`, `V2/Modules/Support/StructuralLandingEditor/Models/LayoutBaseConfig.php:25-27`).

### 1.3 Contrato observável

| Entrada | Saída |
|---|---|
| `GET /form` sem `type` | HTTP 400 `Section type is required.` (`Controllers/LandingSectionsEditorController.php:73-75`) |
| `GET /form?type=<t>` com view inexistente | HTTP 404 `Form view for section type '<t>' not found.` (`:89-91`) |
| `GET /form?type=hero` | HTML do form, variáveis `data`, `sectionId`, `type` (`:93`) |
| `POST /validate` válido | 200 `{"success":true}` (`:107`) |
| `POST /validate` inválido | 422 `{"success":false,"errors":{...}}` (`:108-112`) |
| `POST /validate` com exceção inesperada | 500 `{"success":false,"message":"An error occurred during validation."}` e log `[LandingSectionsEditor] Section validation failed: ...` (`:113-121`) |
| `POST /preview/save` sem token | 400 `{"success":false,"message":"Preview token is required."}` (`:133-135`) |
| `POST /preview/save` válido | 200 `{"success":true}`, cache de 3600 s (`:153-155`) |
| `GET /preview/render` sem token | 400 `Preview token is required.` (`:167-169`) |
| `GET /preview/render?token=<t>` | view `v2-service-landingsectionseditor::preview` com `sections` e `token` (`:186`) |
| `POST /preview/render-section` sem type | 400 `{"success":false,"message":"Section type is required."}` (`:200-202`) |
| `POST /preview/render-section` view inexistente | 404 `{"success":false,"message":"Section view not found."}` (`:228-230`) |
| `POST /preview/render-section` válido | 200 `{"success":true,"html":"<...>"}` (`:238-241`) |
| `GET /media` | 200 com a coleção serializada do Gallery; erro → 500 com corpo `[]` (`:249-263`) |
| `POST /media` sem arquivo | 422 (validação Laravel `required|file|max:10240`) (`:268-272`) |
| `POST /media` ok | 200 com o array de mídia do Gallery (`:294`) |
| `PUT /media/{id}` ok | 200 `{"success":true}`; `false` do Gallery → 404 `{"success":false,"message":"Media not found or unauthorized."}` (`:341`) |
| `POST /media/{id}/rotate` `false` | 400 `{"success":false,"message":"Rotation failed or mime not supported."}` (`:374`) |
| `DELETE /media/{id}` `false` | 400 `{"success":false,"message":"Failed to delete media."}` (`:405`) |

### 1.4 O que o serviço deliberadamente NÃO faz

- Não persiste a lista de seções nem conhece a chave de configuração; não invalida cache de página pública (isso está nos controllers dos módulos: `HomepageCmsController::invalidatePageCache`, `V2/Modules/Layouts/Homepage/Controllers/HomepageCmsController.php:126-142`; `SobreNosCmsController::clearPageCache`, `V2/Modules/Institucional/SobreNos/Controllers/SobreNosCmsController.php:94-106`).
- Não aplica permissão por módulo: as rotas só exigem `CmsAuthenticate` (`V2/Services/LandingSectionsEditor/Routes/cms.php:9`). A checagem `@v2can('SobreNos','write')` é feita apenas na view do consumidor (`V2/Modules/Institucional/SobreNos/Views/cms/editor.blade.php:9`).
- Não valida a lista inteira: `validate` valida apenas `type` corrente e ignora o restante do array. O payload final é aceito pelo consumidor com `json_decode` e `is_array`, sem validação de `id`/`type`/`order` (`HomepageCmsController.php:59-62`).
- Não ordena por `order`: a renderização percorre o array na ordem em que está salvo (`Views/components/landing-sections-viewer.blade.php:34`). O campo `order` é reescrito no cliente (`sections-manager.blade.php:78-79`) e, no caso de Sobre Nós, no servidor (`SobreNosSectionNormalizer::ensureIds`, `V2/Modules/Institucional/SobreNos/Services/SobreNosSectionNormalizer.php:18-36`).
- Não gera thumbnails/variantes: apenas lê `variants` já existentes em `v2_gallery_media` (`LandingSectionsEditorRendererService.php:192`).
- Não sanitiza o que é persistido: `sanitizeContent()` só é chamado em `savePreview` (`Controllers/LandingSectionsEditorController.php:150`) e `renderSection` (`:222`). O render público usa `{!! $renderText(...) !!}` para conteúdo rico (por exemplo `Views/render/text-only.blade.php:35`).

## 2. Abordagem

### 2.1 Estratégia e por quê

Padrão **registry + schema + view pair**:

- `WidgetRegistry` é um mapa estático `slug do tipo → WidgetContract` (`V2/Services/LandingSectionsEditor/Services/WidgetRegistry.php:10-26`).
- Cada widget declara ícone, título, classe de schema, view de form e view de render via `WidgetContract` (`V2/Services/LandingSectionsEditor/Widgets/WidgetContract.php:12-59`).
- Os 21 tipos padrão são registrados **preguiçosamente** por `registerLegacyWidgets()` usando `GenericWidget`, que apenas carrega as cinco propriedades (`V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:36-74`; `Widgets/GenericWidget.php:11-27`).
- Módulos registram widgets próprios no construtor do `ModuleProvider`, antes de qualquer render (`V2/Modules/Layouts/Homepage/ModuleProvider.php:20-32`; `V2/Modules/Institucional/SobreNos/ModuleProvider.php:18-25`).
- Mídia nunca é resolvida por consulta dentro da view de render: `prepareSections()` coleta IDs de todos os widgets, faz uma consulta e injeta caminho/caminho+variantes nos parâmetros (`LandingSectionsEditorRendererService.php:128-205`). A view do módulo pode transformar `file_path` em URL (via `StorageService`), mas não consulta o banco.

Motivação observável: o mesmo par de views serve CMS e portal; a única diferença entre tipos é o slug e o schema. `[I]` o registro preguiçoso evita que o serviço precise de um `ServiceProvider` próprio e permite que o módulo substitua um tipo padrão antes do primeiro `getWidgets()`.

### 2.2 Mecânica passo a passo

1. **Boot.** `V2ServiceProvider` instancia cada `ModuleProvider` (`V2/Core/Providers/V2ServiceProvider.php:98-111`), que chama `WidgetRegistry::register` para os tipos do módulo. Em seguida, mapeia as rotas de serviço sob `web` + namespace `V2\Services\<Servico>\Controllers` (`V2/Core/Providers/V2ServiceProvider.php:203-214`).
2. **Editor CMS.** O consumidor inclui `landing-sections-editor` com `name`, `value`, `label` (`V2/Modules/Layouts/Homepage/Views/cms/editor.blade.php:51-55`). O componente serializa `value` no hidden `#landing-sections-hidden-<name>` (`Views/components/forms/landing-sections-editor.blade.php:3-9,24`) e monta o dropdown a partir de `getSectionTypes()` (`:11`, `:35-70`).
3. **Adicionar/editar.** O clique chama `loadForm(type, sectionId, parameters)`; o modal é aberto por `showSectionModal()` e o HTML é buscado via `GET /form` (`Views/components/forms/logic/sections-manager.blade.php:161-210`). O HTML é cacheado em `formTemplatesCache` por `new_<type>`/`edit_<id>` (`:178`, `:194-206`).
4. **Salvar seção.** `saveSection()` serializa `input,textarea,select`, adiciona `type` e `section_id` e faz `POST /validate` (`sections-manager.blade.php:215-220`). Em 200, converte nomes `parameters[a][b]` em objeto aninhado (`:223-273`), gera UUID v4 no cliente quando é nova (`:276`, `:392-396`), atualiza o array, reescreve o hidden e dispara o preview com debounce de 800 ms (`preview-manager.blade.php:5-12`).
5. **Preview.** `triggerPreviewSave()` faz `POST /preview/save` e, em sucesso, troca o `src` do iframe para `GET /preview/render?token=...` (`preview-manager.blade.php:17-40`). O controller sanitiza e grava no cache (`Controllers/LandingSectionsEditorController.php:149-153`); a view de preview inclui o viewer (`Views/preview.blade.php:12-18`).
6. **Swap parcial.** Após salvar, `saveSection` faz `POST /preview/render-section` e envia `postMessage({type:'lotep:update-section-html', sectionId, html})` ao iframe (`sections-manager.blade.php:302-317`); o preview escuta e substitui o `.lse-render-section-block[data-section-id]` (`Views/preview.blade.php:22-33`).
7. **Persistência.** O submit do formulário externo envia `landing_sections_json`; o controller do consumidor faz `json_decode`, grava `LayoutBaseConfig.payload` e invalida `page:*` no Redis (`HomepageCmsController.php:59-80`; `SobreNosCmsController.php:60-82`).
8. **Render público.** O controller do módulo aplica dados dinâmicos, chama `prepareSections()` e devolve a view do portal (`HomepagePortalController.php:44-65`; `SobreNosPortalController.php:62-85`). O viewer resolve cada tipo via `WidgetRegistry::get` e inclui a render view (`Views/components/landing-sections-viewer.blade.php:35-46`).

### 2.3 Trechos de código pivô

```php
// V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:62-73
// O registro padrão é preguiçoso e NÃO sobrescreve um tipo já registrado por módulo.
foreach ($legacyConfigs as $type => $config) {
    if (!WidgetRegistry::has($type)) {
        $ns = 'v2-service-landingsectionseditor';
        WidgetRegistry::register($type, new GenericWidget(
            $config[0],
            $config[1],
            $config[2],
            $ns . '::forms.' . $type,
            $ns . '::render.' . $type
        ));
    }
}
```

**Efeito.** Um módulo que registre `hero`, `navigation` etc. no próprio `ModuleProvider` passa a controlar form/render/validação daquele slug; os 21 padrões só entram se o slug estiver livre. Como `register()` faz atribuição direta (`WidgetRegistry.php:19-21`), dois módulos com o mesmo slug têm resultado “último registrado vence”.

```php
// V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:85-93
// Resolve o form pelo widget; sem widget, cai no namespace padrão do serviço.
$widget = WidgetRegistry::get($type);
$viewName = $widget ? $widget->getFormView() : ('v2-service-landingsectionseditor::forms.' . $type);

if (!view()->exists($viewName)) {
    abort(404, "Form view for section type '{$type}' not found.");
}

return view($viewName, compact('data', 'sectionId', 'type'))->render();
```

**Efeito.** O endpoint é agnóstico ao tipo: basta registrar widget ou existir `Views/forms/<type>.blade.php`. `parameters` chega como string JSON (`:78-83`) e vira `$data` para a view; `sectionId` é repassado, mas os forms padrão não o utilizam.

```php
// V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:185-197
// Widget resolvido: prepare() enriquece e as variantes WebP são injetadas por caminho.
if (isset($widgets[$type])) {
    $section['parameters'] = $widgets[$type]->prepare($params, $anexos);

    // Injeta variantes WebP para cada anexo da seção
    $attachmentIds = $widgets[$type]->getAttachmentIds($params);
    foreach ($attachmentIds as $fieldPath => $mediaId) {
        if ($m = $anexos->get($mediaId)) {
            $variants = json_decode($m->variants ?? '{}', true) ?: [];
            if (!empty($variants)) {
                array_set($section['parameters'], $fieldPath . '_variants', $variants);
            }
        }
    }
}
```

**Efeito.** O contrato é `campo.caminho => id`; para `slides.0.image` a view recebe `slides[0][image]` e `slides[0][image_variants]`. `prepare()` de `BaseWidget` grava `file_path` cru (não URL), deixando a geração de URL para a view do módulo, que usa `StorageService`/`v2-service-media::components.picture` (`V2/Modules/Layouts/Homepage/Views/lse/render/hero.blade.php:2-3,30-37`).

```php
// V2/Services/LandingSectionsEditor/Widgets/BaseWidget.php:57-65
// Sanitização padrão: remove TODAS as tags de qualquer string recursivamente.
public function sanitize(array $parameters): array
{
    array_walk_recursive($parameters, function (&$value) {
        if (is_string($value)) {
            $value = strip_tags($value);
        }
    });
    return $parameters;
}
```

**Efeito.** `sanitizeContent()` aplica isso a todo widget registrado (`LandingSectionsEditorRendererService.php:108-124`). No preview, conteúdo rico serializado (JSON com `<p>`) e HTML livre perdem as tags; no portal o conteúdo persistido é renderizado sem essa passagem. O preview, portanto, não é fiel ao resultado final para `html-free` e para campos ricos.

```php
// V2/Services/LandingSectionsEditor/Services/WidgetRegistry.php:19-26
// Registro global estático: última escrita vence, sem checagem de colisão.
public static function register(string $type, WidgetContract $widget): void
{
    self::$widgets[$type] = $widget;
}
```

**Efeito.** A ordem de boot decide o dono do slug. `getSectionTypes()` devolve `all()` na ordem de inserção (`LandingSectionsEditorRendererService.php:92-105`), o que define a ordem no dropdown do editor.

```php
// V2/Services/LandingSectionsEditor/ValidationSchemas/ValidationService.php:37-52
// Resolve regras pelo registry; sem widget, tenta <StudlyType>Schema no namespace do LSE.
public function getRulesFor(string $type): array
{
    $widget = WidgetRegistry::get($type);
    if ($widget) {
        return $widget->getValidationRules();
    }

    $className = Str::studly($type) . 'Schema';
    $schemaClass = 'V2\Services\LandingSectionsEditor\ValidationSchemas\\' . $className;

    if (class_exists($schemaClass) && method_exists($schemaClass, 'rules')) {
        return $schemaClass::rules();
    }

    return [];
}
```

**Efeito.** `icons-text` → `IconsTextSchema`; `sucesu-hero` → `V2\...\ValidationSchemas\SucesuHeroSchema` (inexistente) ⇒ **sem regras**. Tipos de módulo só validam se registrados e com `schemaClass` válido. Sem regras, `validateSection` responde 200 sem inspecionar `parameters`.

```php
// V2/Services/LandingSectionsEditor/Views/components/forms/logic/preview-manager.blade.php:5-12
// O array é a fonte de verdade do editor; o hidden e o preview derivam dele.
const persistState = () => {
    $hiddenInput.val(JSON.stringify(sections));
    renderSidebar();

    // Debounce para evitar múltiplas chamadas AJAX em curto período ao reordenar/salvar
    if (previewDebounceTimer) clearTimeout(previewDebounceTimer);
    previewDebounceTimer = setTimeout(triggerPreviewSave, 800);
};
```

**Efeito.** Toda edição/reordenação/remoção converge para `persistState()`, que atualiza `#landing-sections-hidden-<name>`, redesenha a sidebar (`sections-manager.blade.php:356-388`) e agenda a sincronização do preview.

```php
// V2/Modules/Institucional/SobreNos/Controllers/SobreNosCmsController.php:60-74
// O consumidor é quem persiste; o LSE só fornece o JSON.
$payload = json_decode($request->input('landing_sections_json'), true);
if (!is_array($payload)) {
    $payload = [];
}

// Invariante do LandingSectionsEditor: toda seção precisa de id + order.
$payload = SobreNosSectionNormalizer::ensureIds($payload);

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

**Efeito.** Mostra o contrato de persistência completo: chave `sobre_nos_sections`, restauração de registro soft-deleted, normalização de `id`/`order` (ausente na Homepage) e payload JSON em `v2_layout_base_configs`.

### 2.4 Algoritmos e custos

- `prepareSections()` faz **2 passadas** sobre as seções e **1 consulta** `whereIn` em `v2_gallery_media` com IDs únicos (`LandingSectionsEditorRendererService.php:134-178`), portanto O(S + M) e sem N+1. Se a consulta falha, loga `[LandingSectionsEditor] Failed to fetch attachments: ...` e segue com coleção vazia (`:175-177`).
- `prepare()` de cada widget é O(parâmetros); `BaseWidget::prepare` percorre o mapa de IDs e usa `setNestedValue` com `explode('.')` (`Widgets/BaseWidget.php:106-150`).
- `sanitizeContent()` é O(nós de string) por seção e usa `strip_tags` (custo proporcional ao tamanho do texto).
- `getSectionTypes()` reexecuta o registro preguiçoso a cada chamada, mas só percorre 21 chaves e faz `has()` O(1).
- O editor acumula formulários em `formTemplatesCache` sem limite; cache é invalidado por seção editada/nova (`sections-manager.blade.php:275-278`).
- Preview: 1 `POST` por rajada de 800 ms e recarga total do iframe; cada salvamento também faz 1 `POST /render-section` (swap parcial). Não há throttle nas rotas.

## 3. Particularidades

### 3.1 O que é exclusivo deste serviço

- É a única fonte do **contrato de widget de seção de landing page**: `WidgetContract` + `BaseWidget` + `WidgetRegistry` + `ValidationService`. Gallery não conhece seções; ContentRichEditor só renderiza o campo rico; StructuralLandingEditor tem modelo/blocos próprios, mas não o registry.
- É o único lugar que define os **21 tipos padrão** e seus pares form/render (ver `widgets-e-schemas.md`).
- É o único componente que faz **live preview por iframe com swap por `postMessage`** (`lotep:update-section-html`) e cache de preview por token.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-LSE-01 | Toda seção precisa de `id` e `order` para ser editável/reordenável no cliente. | Sem `id`, `findIndex` falha e o form abre vazio; Sobre Nós normaliza no save, Homepage não. | `V2/Modules/Institucional/SobreNos/Services/SobreNosSectionNormalizer.php:5-36`; `V2/Services/LandingSectionsEditor/Views/components/forms/logic/sections-manager.blade.php:60-68,76` |
| RN-LSE-02 | O slug do tipo é a chave de tudo: registry, validação fallback, views padrão e persistência. | Renomear `type` no payload quebra resolução e render. | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:86-91,226-230` |
| RN-LSE-03 | Widget de módulo tem precedência sobre o padrão de mesmo slug. | Módulo pode substituir `hero`; padrão não sobrescreve. | `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:62-73` |
| RN-LSE-04 | Colisão entre módulos não é detectada; o último `register` vence. | Ordem de boot (ordem de `File::directories`) decide o render. | `V2/Services/LandingSectionsEditor/Services/WidgetRegistry.php:19-21`; `V2/Core/Providers/V2ServiceProvider.php:92-105` |
| RN-LSE-05 | `order` não é usado no render; a ordem visual é a ordem do array. | Reordenar o payload fora do editor muda o portal sem alterar `order`. | `V2/Services/LandingSectionsEditor/Views/components/landing-sections-viewer.blade.php:34-48` |
| RN-LSE-06 | Preview é sanitizado; persistência não. | Preview pode diferir do portal para rich text e HTML livre. | `V2/Services/LandingSectionsEditor/Widgets/BaseWidget.php:57-65`; `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:150` |
| RN-LSE-07 | Mídia resolvida em lote por `getAttachmentIds()`; a chave é um caminho com pontos. | Caminho errado grava `*_variants` em local inexistente. | `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:188-197` |
| RN-LSE-08 | Token de preview é aleatório de 40 caracteres, gerado por render do componente. | Cada instância do editor tem seu próprio espaço de cache; não há autorização por token. | `V2/Services/LandingSectionsEditor/Views/components/forms/landing-sections-editor.blade.php:2`; `Controllers/LandingSectionsEditorController.php:130-135` |

### 3.3 Casos-limite

| Caso | Comportamento | Fonte |
|---|---|---|
| `type` ausente em `/form` | 400, sem render. | `Controllers/LandingSectionsEditorController.php:73-75` |
| Tipo desconhecido em `/form` | Se não houver `Views/forms/<type>.blade.php`, 404 com o slug na mensagem. | `:86-91` |
| Tipo desconhecido em `/preview/render-section` | 404 `Section view not found.` | `:226-230` |
| Tipo desconhecido no portal | Seção é **silenciosamente omitida** (`view()->exists` falso). | `Views/components/landing-sections-viewer.blade.php:40-47` |
| Seção sem `type` no portal | Omitida (`!empty($type)`). | `Views/components/landing-sections-viewer.blade.php:40` |
| `parameters` inválido (JSON quebrado) | Vira `[]`; form abre com defaults. | `Controllers/LandingSectionsEditorController.php:78-83` |
| `sections` inválido no preview | Vira `[]`; cache grava lista vazia. | `:137-147` |
| Seção sem `id` no preview parcial | `renderSection` monta `['type','parameters']` sem `id`; views que usam `$section['id']` geram aviso/atributo vazio. | `:216-224`; `Views/render/text-only.blade.php:24` |
| Mídia ausente no banco | `$anexos->get()` nulo; nenhum `*_url`/`*_variants` é injetado. | `LandingSectionsEditorRendererService.php:190-196` |
| Falha ao consultar mídia | Log de erro e render sem imagens (fail-open). | `:175-177` |
| Upload acima de 10 MB | 422 do `validate` antes do Gallery. | `Controllers/LandingSectionsEditorController.php:268-272` |
| Gallery indisponível | `storeMedia` 500 `Upload failed: <mensagem>`; `listMedia` 500 com `[]`. | `:296-299`; `:255-257` |
| Mídia inexistente/não autorizada em update | 404 `Media not found or unauthorized.` | `:341` |
| Rotação de mime não suportado | 400 `Rotation failed or mime not supported.` | `:374` |
| Falha de auditoria | `Log::warning` e a operação de mídia continua (best-effort). | `:290-292` |

### 3.4 Armadilhas de reimplementação

1. **Registrar os padrões no boot** (em vez de preguiçosamente) muda a precedência: o padrão passaria a sobrescrever widgets de módulo se registrado depois. Preserve “registrar só se ausente”.
2. **Chamar `sanitizeContent()` no save do consumidor** altera o conteúdo persistido (remove HTML/JSON de rich text), divergindo do comportamento atual.
3. **Assumir que `prepare()` devolve URL**: widgets de módulo devolvem `file_path`. URL é responsabilidade da view (StorageService/picture). Já o fallback legado devolve URL pronta em `*_url` (`LandingSectionsEditorRendererService.php:214-270`).
4. **Confiar em `getSectionTypes()` para listar tudo**: ele lista apenas widgets registrados; `video` tem schema/form/render mas não está no registry.
5. **Mover o editor para um `<form>` interno**: o componente usa `<input type="hidden" name="<name>">` que precisa estar dentro do form do consumidor (`Views/components/forms/landing-sections-editor.blade.php:24`; `V2/Modules/Layouts/Homepage/Views/cms/editor.blade.php:46-55`).
6. **Duas instâncias do mesmo `name` na página**: os IDs são derivados de `$name` e o JS usa `$(document).on` com seletores por `name`, mas `formTemplatesCache`, `lseOverlayCount` e `window.reindexTiptapWrapper` são globais por IIFE (cada IIFE tem seu escopo, exceto `formTemplatesCache`/`reindexTiptapWrapper`) (`sections-manager.blade.php:3`; `landing-sections-editor.blade.php:97`).
7. **Dependência de jQuery/Bootstrap 5/Owl Carousel**: o componente usa `$`, `bootstrap.Modal` indiretamente via gallery-picker, `toastr` opcional e `owlCarousel` nos renders (`Views/render/carousel.blade.php:56`, `Views/render/icons-text.blade.php:65`, `Views/render/text-carousel.blade.php:98`). Sem essas libs o editor e alguns renders falham.

### 3.5 Dívidas e divergências

| Item | Evidência | Impacto |
|---|---|---|
| README diz “22 formulários/22 renderizadores” e “11 endpoints”; o registry registra **21** e `Routes/cms.php` declara **10**. | `V2/Services/LandingSectionsEditor/README.md:18-31`; `Routes/cms.php:12-23`; `LandingSectionsEditorRendererService.php:38-60` | `video` é órfão; contagem de rotas documentada errada. |
| `video` tem `VideoSchema`, `Views/forms/video.blade.php`, `Views/render/video.blade.php`, mas não é registrado nem aparece no dropdown. | `ValidationSchemas/VideoSchema.php:4-13`; `Views/forms/video.blade.php:3`; `Views/render/video.blade.php:10` | Seção legada não editável; validação funciona só por fallback de schema. |
| `render/video.blade.php` usa `$data['id']`, que nunca é preenchido (o id fica em `$section['id']`). | `Views/render/video.blade.php:10` | `id` vazio no HTML. |
| Widgets padrão registrados usam `BaseWidget::getAttachmentIds()` (vazio) e `BaseWidget::prepare()`; o fallback `legacyPrepare()` que produzia `*_url` deixa de rodar. | `Widgets/BaseWidget.php:98-100`; `LandingSectionsEditorRendererService.php:138-163,185-201` | `hero`, `carousel`, `text-only`, `only-image`, `text-image`, `cards-numbered` e `grid` não recebem `background_image_url`/`image_url` no portal; imagens somem ou caem no placeholder. |
| `image-picker-modal.blade.php` não é incluído por ninguém e escuta evento diferente (`lotep:open-global-media-picker`) do usado pelo editor (`lotep:open-gallery-picker-lse_global_media_picker`). | `Views/components/forms/logic/image-picker-modal.blade.php:190`; `Views/components/forms/logic/sections-manager.blade.php:125` | Código morto; a seleção real é feita pelo `gallery-picker` do Gallery. |
| `carousel` usa asset padrão de outra marca (`layouts/lotep/...`). | `Views/render/carousel.blade.php:4` | Fallback visual fora do design SUCESU. |
| `renderSection` não recebe `id`; o swap pode gerar HTML diferente do render principal. | `Controllers/LandingSectionsEditorController.php:216-219` | `aria-labelledby`/ids de seção vazios no swap. |
| `html-free` envia `postMessage({type:'resize-iframe'})`, mas nenhum listener trata essa mensagem. | `Views/render/html-free.blade.php:43-47`; `Views/preview.blade.php:22-34` | Iframe de altura automática não cresce no preview. |
| `order` só é normalizado em Sobre Nós. | `SobreNosCmsController.php:65-66` vs `HomepageCmsController.php:59-70` | Payload da Homepage pode ficar sem `order` consistente; render não usa, mas o editor reescreve. |

## 4. Integrabilidade

### 4.1 API pública

| Símbolo | Assinatura | Retorno | Exceções | Consumidores |
|---|---|---|---|---|
| `WidgetRegistry::register` | `(string $type, WidgetContract $widget): void` | — | — | `ModuleProvider` de Homepage/Sobre Nós; `registerLegacyWidgets` |
| `WidgetRegistry::get` | `(string $type): ?WidgetContract` | widget ou `null` | — | Controller, ValidationService, viewer |
| `WidgetRegistry::all` | `(): array<string,WidgetContract>` | mapa na ordem de inserção | — | `getWidgets`, `getSectionTypes` |
| `WidgetRegistry::has` | `(string $type): bool` | bool | — | `registerLegacyWidgets` |
| `LandingSectionsEditorRendererService::getSectionTypes` | `(): array<string,array{icon:string,title:string}>` | catálogo para o dropdown | — | `landing-sections-editor.blade.php:11` |
| `LandingSectionsEditorRendererService::getWidgets` | `(): array<string,WidgetContract>` | garante registro padrão antes de devolver | — | `sanitizeContent`, `prepareSections` |
| `LandingSectionsEditorRendererService::sanitizeContent` | `(array $content): array` | seções com strings sem tags | — | `savePreview`, `renderSection` |
| `LandingSectionsEditorRendererService::prepareSections` | `(array $sections): array` | seções com parâmetros enriquecidos | — | Controllers de portal e viewer |
| `ValidationService::validate` | `(Request $request): void` | — | `ValidationException` | `validateSection` |
| `ValidationService::getRulesFor` | `(string $type): array` | regras Laravel | — | `validate`; testes |
| `WidgetContract` | `getIcon`, `getTitle`, `getValidationRules`, `sanitize`, `getFormView`, `getRenderView`, `getAttachmentIds`, `prepare` | ver `widgets-e-schemas.md` | — | `GenericWidget`, widgets de módulo |

### 4.2 Componentes Blade e contrato de UI

- `v2-service-landingsectionseditor::components.forms.landing-sections-editor` — props obrigatórias `name` e `value`; opcional `label`. Ver `ui-contrato.md`.
- `v2-service-landingsectionseditor::components.landing-sections-viewer` — prop `sections` (array ou string JSON). Ver `ui-contrato.md`.
- `v2-service-landingsectionseditor::components.forms.image-picker` — props `name`, `value`, `inputId`, `label`. Usado por forms padrão e por forms de módulos (`V2/Modules/Layouts/Homepage/Views/lse/forms/hero.blade.php:72-77`).
- Partials `logic/*` são fragmentos de JS/HTML incluídos **dentro** do `<script>` do componente (`landing-sections-editor.blade.php:141-150`); não são componentes autônomos.
- `v2-service-landingsectionseditor::preview` é view de página inteira, estende `v2-portal::app-layout` (`Views/preview.blade.php:1-10`).

### 4.3 JS e ciclo de vida

- Init: `$(document).ready` → IIFE com `name`, `previewToken`, `csrfToken`, URLs de rota (`landing-sections-editor.blade.php:114-131`) → `state-loader` → `preview-manager` → `media-manager` → `sections-manager` → `init()` (`:141-153`).
- `init()` remove backdrops residuais, registra eventos, desenha a sidebar e faz o primeiro `POST /preview/save` (`sections-manager.blade.php:401-424`).
- Teardown: não existe. O componente não remove listeners (`$(document).on` global) nem limpa `formTemplatesCache`; recarregar a página é o único teardown.
- Múltiplas instâncias: cada IIFE tem escopo próprio, mas `formTemplatesCache` e `window.reindexTiptapWrapper` são globais e as chaves de cache não incluem `name` (`sections-manager.blade.php:3`; `landing-sections-editor.blade.php:97`).
- Sem JS: a sidebar fica vazia e o hidden mantém o JSON original; o formulário externo ainda salva o valor inicial sem edição.
- Dependências: jQuery, Bootstrap 5 (dropdown do "ADICIONAR SEÇÃO", gallery picker), `UIManager`/`initContentRichEditors` (opcionais, com guarda), `toastr` (opcional), `crypto.getRandomValues` (`sections-manager.blade.php:392-396`).

### 4.4 Ordem de inicialização e registro

1. `V2ServiceProvider` registra views/migrations/rotas do serviço sob `web` (`V2/Core/Providers/V2ServiceProvider.php:203-214`).
2. `ModuleProvider` de cada consumidor registra widgets no construtor (`Homepage/ModuleProvider.php:20-32`; `SobreNos/ModuleProvider.php:18-25`).
3. O primeiro `getWidgets()` (chamado por `getSectionTypes`, `sanitizeContent`, `prepareSections`) completa o registry com os 21 padrões (`LandingSectionsEditorRendererService.php:81-90`).
4. A página do CMS inclui o componente, que depende de `ContentRichEditor` (`assets_only`) e do `gallery-picker` do Gallery (`landing-sections-editor.blade.php:14,92`). Esses assets precisam existir antes do `@push('scripts')`.
5. O portal chama `prepareSections()` **depois** do integrador de dados do módulo (`HomepagePortalController.php:47-48`; `SobreNosPortalController.php:65-67`).

### 4.5 Receitas de uso

**1. Registrar um widget de módulo.**

```php
// V2/Modules/Layouts/Homepage/ModuleProvider.php:20-24
public function __construct()
{
    WidgetRegistry::register('sucesu-hero', new SucesuHeroWidget());
    WidgetRegistry::register('sucesu-services', new SucesuServicesWidget());
}
```

Resultado: `sucesu-hero` aparece em “Personalizados” no dropdown (prefixo `sucesu-`), usa o schema/view declarados no widget e tem precedência sobre um eventual `hero` padrão.

**2. Renderizar seções no portal.**

```blade
{{-- V2/Modules/Layouts/Homepage/Views/portal/index.blade.php:34-36 --}}
@include('v2-service-landingsectionseditor::components.landing-sections-viewer', [
    'sections' => $sections
])
```

Resultado: HTML de cada seção resolvida; tipos desconhecidos são omitidos. `$sections` deve vir de `prepareSections()` (o viewer chama de novo, de forma idempotente).

**3. Editar seções no CMS.**

```blade
{{-- V2/Modules/Layouts/Homepage/Views/cms/editor.blade.php:46-55 --}}
<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>
```

Resultado: o submit envia o JSON editado em `landing_sections_json`; o controller do módulo persiste.

**4. Consumir os endpoints diretamente.**

```http
GET /cms/v2/landing-sections/form?type=hero&parameters=%7B%7D
POST /cms/v2/landing-sections/validate
     type=hero&parameters[title]=Teste
```

Resultado: HTML do form; `{"success":true}` ou 422 com `errors` por campo (`parameters.title` etc.).

**5. Validar um tipo sem registrá-lo.**

```php
$rules = app(\V2\Services\LandingSectionsEditor\ValidationSchemas\ValidationService::class)
    ->getRulesFor('interactive-table');
// => regras de InteractiveTableSchema::rules()
```

Resultado: fallback por `Str::studly()` resolve schemas padrão ainda que o widget não esteja no registry (ex.: `video`).

### 4.6 Anti-uso

1. **Criar widget apenas com view, sem registrar.** `saveSection` chama `/validate`; sem widget e sem `<StudlyType>Schema`, `getRulesFor()` devolve `[]` e a validação passa vazia (`ValidationService.php:47-51`). O form até carrega por fallback de namespace, mas o tipo não aparece no dropdown e não tem regras.
2. **Registrar widget padrão com o mesmo slug de um módulo esperando o padrão vencer.** `registerLegacyWidgets` só registra se `has()` for falso (`LandingSectionsEditorRendererService.php:63`); o widget do módulo permanece e o form/render padrão nunca é usado.
3. **Usar o preview como conferência de conteúdo rico.** `sanitizeContent` remove tags antes do cache; o que aparece no iframe não é o HTML final do portal (`BaseWidget.php:57-65`; `Controllers/LandingSectionsEditorController.php:149-153`).

### 4.7 Custos e limites

- Upload: `max:10240` KB no controller (`Controllers/LandingSectionsEditorController.php:269`); `GallerySender('landing_page')` usa o default de 10240 KB e `allowShared = true` (`GallerySender.php:32`; `Controllers/LandingSectionsEditorController.php:56`).
- Preview: 3600 s por token; tokens não expiram de forma pró-ativa (chave só some por TTL) (`Controllers/LandingSectionsEditorController.php:153`).
- Sem paginação em `/media`: `listMedia()` sem argumentos usa defaults do Gallery (`GallerySender.php:58`), devolvendo toda a coleção visível — custo cresce com o acervo do módulo.
- Sem throttle/idempotência nas rotas; `POST /media` repetido cria novas mídias.

## 5. Rotas e endpoints

| Método | URI | Nome | Middleware | Controller@ação | Permissão | Efeito | Fonte |
|---|---|---|---|---|---|---|---|
| GET | `/cms/v2/landing-sections/form` | `v2.landing-sections.form` | `web`, `CmsAuthenticate` | `LandingSectionsEditorController@getForm` | nenhuma além do auth | Devolve HTML do formulário do tipo | `V2/Services/LandingSectionsEditor/Routes/cms.php:9,12` |
| POST | `/cms/v2/landing-sections/validate` | `v2.landing-sections.validate` | idem | `@validateSection` | — | Valida `parameters` do tipo; 200/422/500 | `Routes/cms.php:13` |
| POST | `/cms/v2/landing-sections/preview/save` | `v2.landing-sections.preview.save` | idem | `@savePreview` | — | Sanitiza e grava cache 1 h | `Routes/cms.php:14` |
| GET | `/cms/v2/landing-sections/preview/render` | `v2.landing-sections.preview.render` | idem | `@renderPreview` | — | Renderiza página de preview no iframe | `Routes/cms.php:15` |
| POST | `/cms/v2/landing-sections/preview/render-section` | `v2.landing-sections.render-section` | idem | `@renderSection` | — | Renderiza uma seção e devolve `html` | `Routes/cms.php:16` |
| GET | `/cms/v2/landing-sections/media` | `v2.landing-sections.media.index` | idem | `@listMedia` | — | Lista mídias do módulo `landing_page` + compartilhadas | `Routes/cms.php:19` |
| POST | `/cms/v2/landing-sections/media` | `v2.landing-sections.media.store` | idem | `@storeMedia` | — | Upload (`file`, `file_alt`, `description`) | `Routes/cms.php:20` |
| PUT | `/cms/v2/landing-sections/media/{id}` | `v2.landing-sections.media.update` | idem | `@updateMedia` | — | Atualiza `alt_text`/`description` | `Routes/cms.php:21` |
| POST | `/cms/v2/landing-sections/media/{id}/rotate` | `v2.landing-sections.media.rotate` | idem | `@rotateMedia` | — | Rotaciona 90° | `Routes/cms.php:22` |
| DELETE | `/cms/v2/landing-sections/media/{id}` | `v2.landing-sections.media.destroy` | idem | `@destroyMedia` | — | Exclui mídia e arquivo | `Routes/cms.php:23` |

O grupo aplica `prefix: cms/v2/landing-sections` e `middleware: CmsAuthenticate` (`Routes/cms.php:9`). O `V2ServiceProvider` adiciona `web` mas **não** adiciona `module.active` nem permissão a rotas de serviço (`V2/Core/Providers/V2ServiceProvider.php:210-214`).

## 6. Dados e migrations

O serviço **não possui migrations nem models**. Ele lê e escreve indiretamente:

| Tabela | Papel | Colunas usadas | Escrita | Fonte |
|---|---|---|---|---|
| `v2_layout_base_configs` | Persistência do payload por chave | `id` (uuid PK), `key` (unique), `payload` (jsonb, cast array), `deleted_at` | Controllers dos módulos consumidores | `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_03_000001_create_v2_layout_base_configs_table.php:22-29`; `V2/Modules/Support/StructuralLandingEditor/Models/LayoutBaseConfig.php:13-27` |
| `v2_gallery_media` | Resolução de imagens e variantes | `id`, `file_path`, `alt_text`, `variants`, `updated_at` | Gallery (via `GallerySender`) | `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:171-174,192` |

Chaves de configuração observadas: `landing_sections` (`V2/Modules/Layouts/Homepage/Controllers/HomepagePortalController.php:44`) e `sobre_nos_sections` (`V2/Modules/Institucional/SobreNos/Controllers/SobreNosPortalController.php:62`). O payload é uma lista ordenada de objetos `{id, type, order, parameters}`; exemplo semente em `V2/Seeds/BasicPages/SucesuHomepageSeeder.php:242-385` e em `V2/Modules/Institucional/SobreNos/Seeds/SobreNosDefaultSectionsSeeder.php:41-131`.

## 7. Configuração e permissões

| Chave/parâmetro | Default | Efeito | Fonte |
|---|---|---|---|
| Token de preview | `Str::random(40)` | Isola o cache de preview por instância | `Views/components/forms/landing-sections-editor.blade.php:2` |
| TTL do preview | `3600` s | Expiração do cache de preview | `Controllers/LandingSectionsEditorController.php:153` |
| `ownerModule` do Gallery | `landing_page` | Escopo de listagem/upload; `allowShared = true` | `Controllers/LandingSectionsEditorController.php:56`; `V2/Services/Gallery/Consumer/GallerySender.php:32-38` |
| Tamanho máximo de upload | `10240` KB | Validação + config do Gallery | `Controllers/LandingSectionsEditorController.php:269` |
| Namespace de views | `v2-service-landingsectionseditor` | Resolução padrão de forms/renders | `LandingSectionsEditorRendererService.php:64` |
| Prefixos “Personalizados” | `sucesu-`, `sobre-nos-` | Particiona o dropdown do editor | `Views/components/forms/landing-sections-editor.blade.php:36` |

Permissões: as rotas exigem apenas autenticação CMS. O botão “Salvar” de Sobre Nós é protegido por `@v2can('SobreNos','write')` na view (`V2/Modules/Institucional/SobreNos/Views/cms/editor.blade.php:9-14`); a Homepage não aplica checagem equivalente na view (`V2/Modules/Layouts/Homepage/Views/cms/editor.blade.php:30-43`). O endpoint de save também não checa permissão (`HomepageCmsController.php:53-57`).

## 8. Observabilidade e erros

| Situação | Nível/canal | Mensagem | Fonte |
|---|---|---|---|
| Falha inesperada de validação | `Log::error` | `[LandingSectionsEditor] Section validation failed: <msg>` | `Controllers/LandingSectionsEditorController.php:114` |
| Falha ao listar mídia | `Log::error` | `[LandingSectionsEditor] Failed to list media: <msg>` | `:255` |
| Upload falhou | `Log::error` | `[LandingSectionsEditor] Media upload failed: <msg>` | `:297` |
| Update falhou | `Log::error` | `[LandingSectionsEditor] Media update failed: <msg>` | `:344` |
| Rotação falhou | `Log::error` | `[LandingSectionsEditor] Media rotation failed: <msg>` | `:377` |
| Exclusão falhou | `Log::error` | `[LandingSectionsEditor] Media deletion failed: <msg>` | `:407` |
| Auditoria de mídia falhou | `Log::warning` | `[LandingSectionsEditor] Falha ao registrar auditoria de <ação>: <msg>` | `:291`, `:335`, `:368`, `:400` |
| Consulta de anexos falhou | `Log::error` | `[LandingSectionsEditor] Failed to fetch attachments: <msg>` | `LandingSectionsEditorRendererService.php:176` |
| Preview aberto | `Log::debug` | `[LandingSectionsEditor][Preview] accessibility flags` | `Controllers/LandingSectionsEditorController.php:177-181` |
| Falha ao carregar `portalConfigs` no preview | `Log::debug` | `[LandingSectionsEditor][Preview] Falha ao carregar portalConfigs: <msg>` | `:183` |

Auditoria: `ActivityLoggerService::log($model, 'created'|'updated'|'deleted', $properties)` é chamada com um `Illuminate\Database\Eloquent\Model` transiente com apenas `id` (`:284-289`). `[I]` o logger grava o `subject_type` como `Illuminate\Database\Eloquent\Model` e o `subject_id` como o id da mídia; o LSE não escreve diretamente em `v2_gallery_media` (delega ao Gallery).

Fail-open: falha de auditoria não interrompe; falha de mídia no `prepareSections` degrada para render sem imagens; falha de cache/Redis afeta apenas o preview e o cache de página do consumidor.

## 9. Consumidores e dependências

| Relação | Ator | O que usa | Como usa | Risco de quebra |
|---|---|---|---|---|
| Consumidor | `MOD-LAYOUTS-HOMEPAGE` | Registry (10 widgets `sucesu-*`), componente editor, viewer, `prepareSections`, endpoints de preview | `ModuleProvider` registra widgets; `HomepageCmsController` persiste `landing_sections`; `HomepagePortalController` integra dados e renderiza | Alto: renomear slugs `sucesu-*` invalida payloads salvos; mudar contrato de `prepare` quebra as views `v2-homepage::lse.render.*` |
| Consumidor | `MOD-INSTITUCIONAL-SOBRENOS` | Registry (5 widgets `sobre-nos-*`), editor, viewer, `prepareSections`, normalizer | `ModuleProvider` registra; `SobreNosCmsController` persiste `sobre_nos_sections` com `ensureIds`; `SobreNosPortalController` filtra `sobre-nos-testimonial` e integra diretoria | Alto: filtro de tipo desabilitado depende do slug exato; forms usam `image-picker` do LSE |
| Dependência (LSE → Gallery) | `SVC-GALLERY` | `GallerySender` | Listagem/upload/update/rotate/delete no escopo `landing_page` | Médio: mudança de assinatura/retorno altera respostas do LSE |
| Dependência (LSE → CRE) | `SVC-CONTENTRICHEDITOR` | Assets e forms ricos | `assets_only` no editor; `content-rich-editor` nos forms; `ContentRichEditorRendererService::toHtml` nos renders | Médio: campos ricos são JSON; render depende do serviço |
| Dependência (LSE → Storage) | `SVC-STORAGE` | `StorageService::url` | `image-picker` (thumbnail) e views de módulo | Baixo |
| Indireto (modelo dos consumidores) | `MOD-SUPPORT-STRUCTURALLANDINGEDITOR` | `LayoutBaseConfig` | Modelo usado pelos controllers dos módulos consumidores para persistir o payload; o LSE não o referencia | Médio: mudança de cast/chave afeta a persistência feita pelos consumidores |

## 10. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| LG-LSE-01 | Não há teste cobrindo upload/update/delete de mídia; só rotação inválida (400). | Comportamento de autorização/erros do Gallery não verificado por teste. | `tests/Feature/V2/Services/LandingSectionsEditor/LandingSectionsEditorTest.php:216-222` |
| LG-LSE-02 | Não está claro se `video` deveria ser registrado (schema/form/render existem). | Decisão de paridade: registrar ou remover. | `ValidationSchemas/VideoSchema.php`; `Views/forms/video.blade.php`; `Views/render/video.blade.php` |
| LG-LSE-03 | Contrato de `order` não é aplicado no render; apenas o cliente e Sobre Nós o mantêm. | Reordenação fora do editor pode divergir do esperado. | `Views/components/landing-sections-viewer.blade.php:34`; `SobreNosSectionNormalizer.php:31` |
| LG-LSE-04 | Não há endpoint de persistência no serviço; cada consumidor implementa o seu (chave e normalização divergem). | Reimplementações podem esquecer `ensureIds`/cache. | `HomepageCmsController.php:53-85`; `SobreNosCmsController.php:54-87` |
| LG-LSE-05 | Precedência de registro entre módulos depende da ordem de `File::directories` (não ordenada explicitamente). | Colisão de slug pode variar entre instalações. | `V2/Core/Providers/V2ServiceProvider.php:92-105` |
| LG-LSE-06 | Sem inventário de uso real dos 21 tipos padrão; nenhum está semeado na Homepage/Sobre Nós. | Pode haver tipos nunca exercitados em produção. | `V2/Seeds/BasicPages/SucesuHomepageSeeder.php:242-385`; `SobreNosDefaultSectionsSeeder.php:41-118` |
| LG-LSE-07 | `image-picker-modal.blade.php` permanece no código sem referência; não se sabe se há uso externo ao V2. | Custo de manutenção/limpeza. | `Views/components/forms/logic/image-picker-modal.blade.php` |

## 11. Checklist de paridade

- [ ] `PC-LSE-01` — `GET /cms/v2/landing-sections/form?type=hero` devolve HTML contendo `parameters[title]`. Fonte: `Controllers/LandingSectionsEditorController.php:85-93`.
- [ ] `PC-LSE-02` — `GET /form` sem `type` responde 400 `Section type is required.`. Fonte: `:73-75`.
- [ ] `PC-LSE-03` — `GET /form?type=inexistente` responde 404 com o slug na mensagem. Fonte: `:89-91`.
- [ ] `PC-LSE-04` — `POST /validate` com `type=hero&parameters[title]=` responde 422 com `errors.parameters.title`. Fonte: `ValidationSchemas/HeroSchema.php:9`; teste `LandingSectionsEditorTest.php:127-134`.
- [ ] `PC-LSE-05` — `POST /validate` com `title=Jogos Lotep&title_color=#ffffff` responde 200 `{success:true}`. Fonte: `LandingSectionsEditorTest.php:137-145`.
- [ ] `PC-LSE-06` — `POST /preview/save` grava `Cache::get('landing_preview_<token>')` e `GET /preview/render?token=` contém o título. Fonte: `Controllers/LandingSectionsEditorController.php:150-187`; teste `:151-183`.
- [ ] `PC-LSE-07` — `POST /preview/render-section` devolve `{success:true,html}` com o texto informado. Fonte: `:195-241`; teste `:196-211`.
- [ ] `PC-LSE-08` — `POST /media/{id}/rotate` com id inexistente responde 400. Fonte: `:355-386`; teste `:216-222`.
- [ ] `PC-LSE-09` — `getSectionTypes()` devolve 21 chaves e não inclui `video`. Fonte: `LandingSectionsEditorRendererService.php:38-60`.
- [ ] `PC-LSE-10` — Registrar `hero` antes do primeiro `getWidgets()` mantém o widget do módulo (título diferente de “Hero (Destaque Principal)”). Fonte: `:62-73`.
- [ ] `PC-LSE-11` — `prepareSections()` executa uma única consulta para todos os `image_id` de todos os widgets. Fonte: `:167-178`.
- [ ] `PC-LSE-12` — `sucesu-hero` com `slides[0].image_id` válido recebe `slides[0].image` e `slides[0].image_variants`. Fonte: `V2/Modules/Layouts/Homepage/LseWidgets/Widgets/SucesuHeroWidget.php:29-58`; `LandingSectionsEditorRendererService.php:185-197`.
- [ ] `PC-LSE-13` — Tipo desconhecido no viewer não gera bloco (`data-section-id` ausente). Fonte: `Views/components/landing-sections-viewer.blade.php:40-47`.
- [ ] `PC-LSE-14` — Payload salvo por Sobre Nós sempre tem `id` e `order=1..n`. Fonte: `SobreNosSectionNormalizer.php:22-33`.
- [ ] `PC-LSE-15` — Salvar no editor atualiza `#landing-sections-hidden-<name>` com o JSON e dispara `/preview/save` após 800 ms. Fonte: `preview-manager.blade.php:5-12`.

## 12. Evidências consultadas

- `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:1-411`
- `V2/Services/LandingSectionsEditor/Routes/cms.php:1-25`
- `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:1-274`
- `V2/Services/LandingSectionsEditor/Services/WidgetRegistry.php:1-55`
- `V2/Services/LandingSectionsEditor/Widgets/{WidgetContract,BaseWidget,GenericWidget}.php`
- `V2/Services/LandingSectionsEditor/ValidationSchemas/*.php` (22 schemas + `ValidationService.php`)
- `V2/Services/LandingSectionsEditor/Views/components/**/*.blade.php`: `landing-sections-editor`, `logic/{state-loader,preview-manager,media-manager,sections-manager,section-modal,styles,image-picker-modal}`, `image-picker`, `landing-sections-viewer`, `preview.blade.php`
- `V2/Services/LandingSectionsEditor/Views/forms/*.blade.php` e `Views/render/*.blade.php`
- `V2/Modules/Layouts/Homepage/{ModuleProvider.php,Controllers/HomepageCmsController.php,Controllers/HomepagePortalController.php,LseWidgets/**,Services/HomepageDataIntegrator.php,Views/cms/editor.blade.php,Views/portal/index.blade.php}`
- `V2/Modules/Institucional/SobreNos/{ModuleProvider.php,Controllers/SobreNosCmsController.php,Controllers/SobreNosPortalController.php,LseWidgets/**,Services/SobreNosSectionNormalizer.php,Services/SobreNosDataIntegrator.php,Seeds/SobreNosDefaultSectionsSeeder.php,Views/cms/editor.blade.php,Views/portal/index.blade.php}`
- `V2/Core/Providers/V2ServiceProvider.php:88-118,190-228`; `V2/Core/Helpers/helpers.php:251-268`
- `V2/Modules/Support/StructuralLandingEditor/{Models/LayoutBaseConfig.php,Migrations/2026_07_03_000001_create_v2_layout_base_configs_table.php}`
- `V2/Services/Gallery/Consumer/GallerySender.php:12-140`
- `V2/Seeds/BasicPages/SucesuHomepageSeeder.php:241-394`
- `tests/Feature/V2/Services/LandingSectionsEditor/LandingSectionsEditorTest.php:109-222`; `tests/Unit/V2/Services/LandingSectionsEditor/LseModalStylesTest.php:18-59`

## 13. Checklist de extração

| Item | Resposta |
|---|---|
| A1 efeito primário | Seções JSON → editor CMS + HTML do portal por widget. §1.1. |
| A2 entradas | 10 rotas, 2 componentes, `WidgetRegistry`; precondição: widgets registrados e views existentes. §1.3, §4. |
| A3 saídas | HTML de form/render, JSON, cache de preview, mídia no Gallery, auditoria. §1.2. |
| A4 efeitos colaterais | Cache 1 h, auditoria de mídia, logs, arquivos via Gallery. §1.2, §8. |
| A5 contrato observável | Tabela de erros/sucessos com códigos e mensagens exatas. §1.3. |
| A6 não acontece | Não persiste, não autoriza por módulo, não ordena por `order`, não sanitiza no save. §1.4. |
| A7 por perfil | Público vê render; CMS autenticado vê editor e mídia; API não expõe LSE (sem `Routes/api.php`). §5. |
| B1 estratégia | Registry + schema + par de views; diagrama textual em §2.2. |
| B2 mecânica | 8 passos com classes/métodos reais. §2.2. |
| B3 decisões | Registro preguiçoso, precedência de módulo, mídia fora da view. §2.1. |
| B4 trechos | 8 trechos de 7–15 linhas. §2.3. |
| B5 algoritmos | Duas passadas + uma consulta; O(S+M); sem N+1. §2.4. |
| B6 estado | Array em memória → hidden → cache → payload persistido; reset por reload. §2.2, §4.3. |
| B7 falhas | Fail-open em mídia/auditoria; 500 em upload; 422 em validação. §1.3, §8. |
| C1 fonte única | Contrato de widget de seção e catálogo de tipos. §3.1. |
| C2 comparação | Gallery não conhece seções; StructuralLandingEditor usa blocos/tabela própria. §3.1. |
| C3 invariantes | `RN-LSE-01..08`. §3.2. |
| C4 casos-limite | Tipo desconhecido, `parameters` inválido, mídia ausente, storage off. §3.3. |
| C5 surpresas | Preview ≠ portal; imagens padrão não resolvidas após registry. §3.5. |
| C6 armadilhas | 7 itens (ordem de registro, sanitização, URL vs path, `<form>` externo). §3.4. |
| C7 código morto | `image-picker-modal`, `video`, README desatualizado. §3.5. |
| D1 API pública | Tabela de símbolos/assinaturas. §4.1. |
| D2 namespace/registro | `v2-service-landingsectionseditor`, rotas de serviço, sem migrations próprias. §4.4, §6. |
| D3 ordem de init | Provider → widgets de módulo → padrões no primeiro `getWidgets()`. §4.4. |
| D4 contrato de UI | Remete a `ui-contrato.md`; resumo de props/partials. §4.2. |
| D5 receitas | 5 receitas PHP/Blade/HTTP. §4.5. |
| D6 anti-uso | 3 itens. §4.6. |
| D7 custos/limites | 10 MB, cache 1 h, sem paginação/throttle. §4.7. |
| D8 consumidores | Homepage, Sobre Nós, Gallery, CRE, Storage. §9. |
| D9 extensão | Novo widget via `ModuleProvider` + `BaseWidget`; ver `widgets-e-schemas.md`. §4.4. |
| E1–E7 rotas/HTTP | Tabela completa, sem permissão específica, CSRF via `web`, sem throttle. §5. |
| F1–F6 dados | Nenhuma tabela própria; `v2_layout_base_configs` e `v2_gallery_media` em §6. |
| G1–G6 UI CMS | Tela = editor do consumidor; campos/validação/modal em `ui-contrato.md`. |
| H1–H6 UI Portal | Viewer + renders; SEO é do módulo; mídia em §2.3/§3.5. |
| I1–I8 contrato JS | `ui-contrato.md` §3–§8. |
| J1–J5 config | `config.csv`; sem flag de módulo (serviço sempre ativo). §7. |
| K1–K5 observabilidade | Tabela de logs e auditoria. §8. |
| L1–L3 seeds | Não se aplica ao serviço; seeds pertencem aos módulos (`SucesuHomepageSeeder`, `SobreNosDefaultSectionsSeeder`). |
| M1 paridade | 15 itens `PC-LSE-*`. §11. |
| M2 lacunas | 7 itens `LG-LSE-*`. §10. |
| M3 evidências | §12. |
| M4 verificação | `lint_docs.py`, `coverage.py --strict`, `merge_indices.py --check`. |
