---
id: MOD-SUPPORT-STRUCTURALLANDINGEDITOR
tipo: modulo
onda: 3
status: concluido
unidades: [MOD-SUPPORT-STRUCTURALLANDINGEDITOR]
fontes:
  - V2/Modules/Support/StructuralLandingEditor/
depende_de: [CORE-BOOT, CORE-CMS, CORE-AUTH, SVC-GALLERY, SVC-STORAGE, SVC-ACTIVITYLOGGER, MOD-SUPPORT-SUPPORTCONFIGS, UI-PORTAL]
consumidores: [UI-PORTAL, MOD-LAYOUTS-BASICPAGE, MOD-LAYOUTS-HOMEPAGE, MOD-INSTITUCIONAL-SOBRENOS, MOD-INSTITUCIONAL-FALECONOSCO, MOD-SUPPORT-SUPPORTCONFIGS]
criticidade: alta
dono: PageSle
---

# Support/StructuralLandingEditor

> Editor Estrutural do portal (SLE): cinco submódulos CMS que gravam tema, metadados,
> dados estruturais, features de acessibilidade e blocos de código por área; um runtime
> (`BlockRenderer`) que injeta esses blocos em `v2-portal::*` e `v2-basicpage::*`.
> A visão de temas/payloads está em
> [`editores-temas-e-assets.md`](editores-temas-e-assets.md); o contrato de telas em
> [`ui.md`](ui.md).

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | O suporte técnico reescreve a estrutura do portal (header, barras, footer, CSS, JS) e a identidade visual (temas CMS/portal, logos, metadados, contato, links, LAI) sem deploy, e o portal público passa a renderizar esses blocos na próxima requisição. |
| Entrada | 17 declarações de rota (14 endpoints + 3 aliases) em `cms/infra/structural-editor/*` (`Routes/cms.php:17-62`); posts de formulário com `tab` + campos; uploads de CSS/JS/imagem (`file`, `type`); `gallery_media_id`. |
| Saída | HTML do CMS (`v2-cms::layouts.app`); blocos injetados no HTML do portal; JSON `{success, asset, snippet}` nos uploads AJAX; `302` de volta ao submódulo com flash. |
| Efeitos colaterais | Escrita em `v2_structural_portal_contents`, `v2_layout_base_configs`, `v2_sle_assets`; por delegação, em `v2_portal_configs`, `v2_cms_theme_configs`, `v2_system_mail_configs` e `v2_gallery_media`; trilha em `activity_log`; `DEL` de `cache.prefix:page:*` no Redis; arquivos em `assets/structuralEditor/{css,js}` e `structural-landing-editor/favicon/portal.ico`. |
| Particularidade central | Conteúdo executável: cada bloco salvo é compilado por `BladeCompiler::compileString()` e avaliado com `eval()` no request do portal (`BlockRenderer.php:144-155`). Não existe preview nem rollback de bloco; o "histórico" é a trilha de auditoria. |
| Como se integra | `v2-structurallandingeditor::*`; `BlockRenderer`/`ComponentResolver` como singletons; helper global `sle_component()`; `LayoutBaseComposer` nos namespaces `v2-portal::*` e `v2-basicpage::*`; chaves de permissão `LayoutBase` (views) e grupo `Support` (middleware). |
| Se removido, quebra | Header/footer/barras/CSS/JS customizados do portal; tema do portal e do CMS; metadados/SEO/e-mail; logos e favicon; banner global; painel LAI; contato exibido em Fale Conosco; `LayoutBaseConfig` usado por Homepage, Sobre Nós e BasicPage; `sle_component()` no manual e nos blocos. |
| Superfície pública | `SleCmsController@{meta,saveMeta,theme,saveTheme,structuralData,saveStructuralData,features,saveFeatures,editor,saveEditor,assets,assetsUpload,assetsFromGallery,assetsDelete}`; `LayoutBaseCmsController@update`; `UserConfigsCmsController@{update,resetCmsTheme}`; `BlockRenderer::{hasBlocks,getBlocks,getVars,renderBlock}`; `ComponentResolver::{render,availableComponents}`; `AssetManagerService`; `UserConfigsService`; modelos `LayoutBaseConfig`, `SleAsset`, `StructuralPortalContent`; `sle_component`. |

## 1. Efeito

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

Só o e-mail de `config('v2.support.email')` (default `suporte@maxima.inf.br`,
`config/v2.php:4-6`) atravessa as rotas; demais usuários com papel Sentinel recebem
`403`/redirect antes de qualquer action (ver §10).

| Submódulo | Rota | O que o operador faz | Resultado visível |
|---|---|---|---|
| Metaconfigurações | `GET /meta` | edita título, descrição, palavras-chave, CNPJ, categoria de governo, analytics, meta tokens e scripts de header/footer, além do SMTP | valores passam a alimentar `v2_portal_configs` e `v2_system_mail_configs`; flash `Metaconfigurações salvas com sucesso!` |
| Tema Visual Global | `GET /theme` | edita tema do CMS (marca, logo, favicon, 3 cores), tema do portal (cores, tipografia, raio, sombra), logos institucionais e banner global | `v2_cms_theme_configs`, `v2_portal_configs`, `v2_layout_base_configs` (`logos`, `global_banner`); flash `Tema visual salvo com sucesso!` |
| Dados Estruturais | `GET /structural-data` | monta árvore de links do header/footer, contato, redes sociais e itens LAI | `v2_layout_base_configs` (`links_header`, `links_footer`, `contato`, `social_media`, `lai_items`); flash `Dados estruturais salvos com sucesso!` |
| Features | `GET /features` | liga/desliga VLibras, barra eMAG, widget de acessibilidade e painel LAI | `v2_portal_configs` (`vlibras_enabled`, `accessibility_bar_enabled`, `accessibility_widget_enabled`) e `v2_layout_base_configs.lai_enabled`; flash `Features salvas com sucesso!` |
| Editor | `GET /editor` | escreve HTML/Blade por área, ativa/desativa área, copia snippets e envia CSS/JS/imagens para a File Library | `v2_structural_portal_contents` (8 áreas) e `v2_sle_assets`; flash `Conteúdo estrutural salvo com sucesso!` |

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| Uma linha por área em `v2_structural_portal_contents`; áreas vazias são desativadas, nunca apagadas | `saveEditor` em transação | `SleCmsController.php:339-380` |
| `v2_layout_base_configs` upsert por `key` com auditoria old/new | `LayoutBaseCmsController::saveConfig` | `LayoutBaseCmsController.php:250-262` |
| `v2_sle_assets` + arquivo físico em `assets/structuralEditor/{css,js}` (CSS/JS) ou mídia na galeria (imagem) | `AssetManagerService::uploadVendor/uploadImage` | `AssetManagerService.php:26-76` |
| `v2_cms_theme_configs` e `v2_portal_configs` (e `v2_system_mail_configs`) | `UserConfigsService::updateConfigs`, em uma transação | `UserConfigsService.php:59-99` |
| Senha SMTP criptografada com `APP_KEY`; em branco preserva a anterior | `persistMailConfig` | `UserConfigsService.php:101-170` |
| Trilha de auditoria (`activity_log`) por submódulo, com `type` e `old/new` | `ActivityLoggerService::log` nos controllers | `SleCmsController.php:93-104,162-173,211-223,278-296,392-402`; `LayoutBaseCmsController.php:258-261` |
| Invalidação do cache público de páginas | `clearPageCache` (`KEYS prefix:page:*` + `DEL`) | `SleCmsController.php:38-50` |
| Favicon do portal em caminho fixo, sobrescrevendo o anterior | `handleFaviconUpload` | `LayoutBaseCmsController.php:118-166` |
| HTML do portal recebe os blocos | `base.blade.php`/`portal-content.blade.php` chamam `BlockRenderer` | `V2/layouts/portal/base.blade.php:77-91,126-138`; `V2/layouts/portal/partials/portal-content.blade.php:14-19,61-64,79-82,110-114` |

### 1.3 Contrato observável

| Entrada | Resultado |
|---|---|
| `GET /cms/infra/structural-editor/editor` sem sessão | `302 v2.cms.login` (`CmsAuthenticate.php:19-25`) |
| `GET ...` com sessão, sem papel | sessão limpa + `302 v2.cms.login` com `Acesso negado. Seu usuário não possui permissões administrativas.` (`CmsAuthenticate.php:27-45`) |
| `GET ...` com papel, e-mail ≠ suporte | `403 Forbidden. Support area is restricted.` (AJAX) ou `302 v2.cms.dashboard` com `Acesso negado. Esta área é restrita ao suporte técnico autorizado.` (`V2PermissionMiddleware.php:53-70`) |
| `GET ...` como suporte | `200`; se `LayoutBaseConfig` não existir, a tela abre com coleções vazias (try/catch) |
| `POST /editor/save` com `areas[header][content]` vazio e registro existente | `302` com flash success; registro fica `content=''`, `active=false` |
| `POST /editor/save` sem nenhuma área | `302` success; nada gravado |
| `POST /theme/save` com `tab=logos` | despacha para `LayoutBaseCmsController`; `200/302` |
| `POST /theme/save` com `tab=qualquer_outro` | despacha para `UserConfigsCmsController`; chaves `cms_*` vão ao tema CMS, `mail_*` ao SMTP, o resto ao portal |
| `POST /structural-data/save` com `tab` desconhecido | `saveConfig` não roda (`default: return`), mas o controller ainda loga auditoria e flash success (`LayoutBaseCmsController.php:57-59`; `SleCmsController.php:196-230`) |
| `POST /editor/assets/upload` sem arquivo/tipo inválido | `422` com erro de validação `The file field is required.`/`The selected type is invalid.` |
| `POST /editor/assets/from-gallery` com `gallery_media_id` inexistente | `422 {"success":false,"message":"Midia nao encontrada na galeria (ID: N)"}` |
| `POST /editor/assets/from-gallery` repetindo a mesma mídia | `200` com o asset já existente (deduplicação por `gallery_media_id`) |
| `POST /editor/assets/delete/{id}` inválido | `302` com flash `Erro ao remover asset: ...` (não há 404: a exceção é capturada) |
| `POST /cms/infra/user-configs/cms-theme/reset` (rota de SupportConfigs) | qualquer usuário CMS autenticado restaura `v2_cms_theme_configs`; sem guarda de suporte (SEC-G1-04, §3.4) |

### 1.4 O que este módulo NÃO faz

- **Não renderiza página**: só injeta fragmentos; a montagem do documento é de `UI-PORTAL`.
- **Não edita seções de landing** (hero, CTA, etc.): isso é `SVC-LANDINGSECTIONSEDITOR`; o SLE apenas compartilha a tabela `v2_layout_base_configs`.
- **Não gerencia a galeria**: upload de imagem vai para `SVC-GALLERY`; o SLE guarda `gallery_media_id` e o caminho.
- **Não valida conteúdo de bloco**: HTML/Blade/CSS/JS são gravados como texto e executados no portal.
- **Não cria conta/permissão `LayoutBase`**: usa a chave em `v2_can`, mas não há seeder de permissão para ela neste módulo (`[L] LG-SLE-01`).
- **Não publica nem agenda**: banner global tem `start_date`/`end_date`, mas o filtro é aplicado na view do portal, não no módulo.
- **Não notifica ninguém**: nenhum e-mail é enviado ao salvar; “E-mail” é apenas configuração de SMTP.

## 2. Abordagem

### 2.1 Estratégia em uma página

O módulo separa **configuração chave-valor** (`v2_layout_base_configs`), **conteúdo por
área** (`v2_structural_portal_contents`) e **biblioteca de arquivos** (`v2_sle_assets`).
O CMS é um despachante fino: valida o mínimo, delega a serviços (`UserConfigsService`,
`AssetManagerService`) e audita. O portal não conhece o CMS: injeta `BlockRenderer` e
consulta as áreas.

```mermaid
flowchart LR
  subgraph CMS["cms/infra/structural-editor"]
    M[meta] --> UC[UserConfigsService]
    T[theme] --> LB[LayoutBaseCmsController]
    T --> UC
    D[structural-data] --> LB
    F[features] --> UC
    F --> LB
    E[editor] --> SPC[(v2_structural_portal_contents)]
    A[assets] --> AMS[AssetManagerService]
  end
  UC --> PC[(v2_portal_configs)]
  UC --> CT[(v2_cms_theme_configs)]
  UC --> MC[(v2_system_mail_configs)]
  LB --> LBC[(v2_layout_base_configs)]
  AMS --> SA[(v2_sle_assets)]
  AMS --> GM[(v2_gallery_media)]
  CMS --> AUD[(activity_log)]
  CMS --> RD[(Redis page:*)]
  LBC --> CMP[LayoutBaseComposer]
  CMP --> PORTAL[views v2-portal::* / v2-basicpage::*]
  SPC --> BR[BlockRenderer]
  BR --> PORTAL
```

### 2.2 Mecânica passo a passo

1. **Boot.** `V2ServiceProvider` instancia `ModuleProvider`, que registra
   `BlockRenderer` e `ComponentResolver` como singletons e associa `LayoutBaseComposer`
   a `v2-portal::*` e `v2-basicpage::*` (`ModuleProvider.php:12-26`). As views ficam no
   namespace `v2-structurallandingeditor` e as rotas em `web + module.active:StructuralLandingEditor`
   (`V2/Core/Providers/V2ServiceProvider.php:304-307`).
2. **Autenticação.** `CmsAuthenticate` exige sessão Sentinel e ao menos um papel; em
   seguida delega a `V2PermissionMiddleware` (`CmsAuthenticate.php:41-44`).
3. **Restrição de grupo.** O `active_pattern` `cms/infra/structural-editor/*` casa com
   todas as rotas; como o grupo é `Support`, o middleware exige o e-mail de suporte, e
   libera `super-admin` apenas para módulos normais (`V2PermissionMiddleware.php:41-75`).
4. **Guarda de controller.** Cada action começa por `authorizeSupportAccess()`
   (`SleCmsController.php:31-36`), que repete a checagem de e-mail e aborta com
   `403 Acesso restrito ao suporte técnico autorizado.`.
5. **Leitura.** `meta`/`theme`/`features` montam `$userConfigs` via `UserConfigsService::getAll()`
   e `$layoutConfigs` via `LayoutBaseConfig::all()->keyBy('key')`, ambos em try/catch
   (tabela ausente não derruba a tela). `structuralData` só carrega `layoutConfigs`;
   `editor` carrega as 8 áreas via `StructuralPortalContent::getForArea()`
   (`SleCmsController.php:308-320`).
6. **Escrita — metadados/SEO/e-mail.** `saveMeta` captura o estado antigo, chama
   `UserConfigsCmsController::update`, audita `meta_save` e invalida o cache.
   `update` remove `_token`, `_method` e `tab` e delega a `UserConfigsService::updateConfigs`.
7. **Escrita — tema.** `saveTheme` roteia por `tab`: `logos` e `comunicado-global` vão a
   `LayoutBaseCmsController@update`; todo o resto vai a `UserConfigsCmsController@update`
   (`SleCmsController.php:146-158`). Dentro de `UserConfigsService`, a chave decide o
   destino: prefixo `cms_` → tema CMS, chaves `mail_*` → SMTP, o resto → portal.
8. **Escrita — dados estruturais.** `saveStructuralData` chama `LayoutBaseCmsController@update`,
   que faz `switch ($tab)` para `logos`, `links-header`, `links-footer`, `contato`,
   `redes-sociais`, `comunicado-global`, `transparencia-lai` (`LayoutBaseCmsController.php:27-60`).
9. **Escrita — features.** `saveFeatures` chama `UserConfigsCmsController@update` para os
   toggles de acessibilidade e grava `lai_enabled` em `v2_layout_base_configs`
   (`SleCmsController.php:254-271`).
10. **Escrita — editor.** `saveEditor` lê `areas[]`, captura estado antigo, abre
    transação e faz update/create por área; conteúdo trimado; vazio + inexistente =
    *skip*; vazio + existente = desativa (`:339-380`). Depois audita `editor_save`.
11. **Escrita — assets.** `assetsUpload` valida `file` (≤ 10 MB) e `type ∈ {css,js,image}`;
    CSS/JS vão a `uploadVendor` (arquivo cru + snippet), imagem vai a `uploadImage`
    (galeria + snippet `<picture>`), ambos criam `SleAsset` e auditoria. `assetsFromGallery`
    reaproveita mídia existente; `assetsDelete` apaga arquivo físico e linha (soft delete).
12. **Runtime.** No portal, `base.blade.php` injeta `BlockRenderer` e renderiza
    `css_head`, `js_head`, `js_foot`, `css_foot`; `portal-content.blade.php` decide entre
    bloco SLE e fallback governamental para `header`, `main_left`, `main_right`, `footer`.
13. **Render de bloco.** `BlockRenderer` carrega as áreas ativas uma única vez, monta
    variáveis (`$siteTitle`, `$portalTheme`, `$layoutConfigs`, `$contato`, `$headerLinks`,
    `$footerLinks`, `$redes`, `$logos`, `$request`), compila o texto como Blade e o avalia
    em buffer; erro de compilação/execução vira comentário HTML e `Log::warning`
    (`BlockRenderer.php:30-41,75-113,136-167`).
14. **Componentes.** Dentro de um bloco, `{!! sle_component('header-logo') !!}` chama
    `ComponentResolver::render`, que resolve 11 nomes para views `v2-portal::*` e devolve
    comentário HTML em caso de nome desconhecido ou exceção (`ComponentResolver.php:17-61`;
    `helpers.php:20-30`).
15. **Layout base → `v2-portal::*`.** `LayoutBaseComposer` (registrado no boot) injeta
    `$layoutConfigs` em toda view do portal e da BasicPage; header/footer e os componentes
    resolvidos leem `contato`, `links_header`, `links_footer`, `redes`, `logos` e
    `global_banner` desse objeto. O `BlockRenderer` oferece os mesmos dados como variáveis
    soltas dentro dos blocos. O tema (`$portalTheme`) **não** passa por esse composer: vem
    do composer do SupportConfigs (§3.3).
16. **GallerySender não participa do render.** É o adaptador de galeria
    (`ownerModule=structural_landing_editor`, `image/*`, 10 MB, shared=true) usado apenas
    pelos handlers de logos/banner para resolver um ID de mídia em payload
    (`Services/GallerySender.php:9-16`; `LayoutBaseCmsController.php:73-95,232-243`).

### 2.3 Decisões de design e por quê

| Decisão | Motivo observável | Marca |
|---|---|---|
| Conteúdo único por área (`unique(area)`) em vez de múltiplos blocos | A UI fala em “um único editor por área” e o `getBlocks()` devolve coleção de 1 item “para compatibilidade” | `[C]` `Migrations/2026_07_03_000000_...:22`; `BlockRenderer.php:61-72` |
| Blocos compilados como Blade + `eval` | Permite `@if`, `@foreach`, `@include` e `sle_component()` sem arquivo em disco | `[C]` `BlockRenderer.php:144-155`; `[I]` é a razão de o manual prometer “HTML, Blade, PHP, CSS e JavaScript” |
| Configuração em JSONB chave-valor | Evita migrations por campo e permite payload heterogêneo (links em árvore, itens LAI) | `[C]` `Migrations/2026_07_03_000001_...:22-29` |
| Desativar em vez de apagar área vazia | Mantém o registro para o fallback gov e evita “sumir” o editor | `[C]` `SleCmsController.php:349-356` |
| Auditoria com `new StructuralPortalContent()` como subject | Um único subject “genérico” carrega `type` no payload, permitindo um rodapé de atividades comum | `[C]` `SleCmsController.php:93-106`; `Views/cms/submodules/*/index.blade.php` `activity-footer` |
| Cache público limpo com `KEYS`/`DEL` do Redis | O portal é cacheado por página (`page.cache`) e qualquer bloco pode mudar | `[C]` `SleCmsController.php:38-50` |
| `GallerySender` dedicado | Isola `ownerModule='structural_landing_editor'`, mime `image/*`, 10 MB, shared=true | `[C]` `Services/GallerySender.php:9-16` |

### 2.4 Trechos de código pivô

```php
// V2/Modules/Support/StructuralLandingEditor/Controllers/SleCmsController.php:31-46
private function authorizeSupportAccess()
{
    if (!Sentinel::check() || Sentinel::getUser()->email !== config('v2.support.email')) {
        abort(403, 'Acesso restrito ao suporte técnico autorizado.');
    }
}

private function clearPageCache()
{
    try {
        $redis = Redis::connection();
        $pattern = config('cache.prefix', 'laravel') . ':page:*';
        $keys = $redis->keys($pattern);
        if (!empty($keys)) {
            $redis->del($keys);
        }
    } catch (\Exception $e) {
        Log::warning('[SLE] Falha ao limpar cache de páginas: ' . $e->getMessage());
    }
}
```

A guarda compara e-mail, não papel: `super-admin` sem o e-mail configurado é barrado no
controller (embora o middleware o deixasse passar em módulos não-Support). O cache é
best-effort: Redis fora do ar gera `warning`, não erro para o usuário.

```php
// V2/Modules/Support/StructuralLandingEditor/Controllers/SleCmsController.php:308-320
public function editor()
{
    $this->authorizeSupportAccess();

    $contents = [];
    $areas = ['header', 'main_left', 'main_right', 'footer', 'css_head', 'css_foot', 'js_head', 'js_foot'];

    foreach ($areas as $area) {
        $contents[$area] = StructuralPortalContent::getForArea($area);
    }

    return view('v2-structurallandingeditor::cms.submodules.editor.index', compact('contents'));
}
```

A lista canônica de áreas está no controller, mas a UI só expõe 5 abas (Header, Barras
Laterais, Footer, CSS, JS); as 8 áreas são cobertas por editores empilhados.

```php
// V2/Modules/Support/StructuralLandingEditor/Controllers/SleCmsController.php:339-356
DB::transaction(function () use ($areas) {
    foreach ($areas as $area => $data) {
        $content = trim($data['content'] ?? '');

        $record = StructuralPortalContent::getForArea($area);

        if (empty($content) && !$record) {
            continue;
        }

        if (empty($content) && $record) {
            // Se área ficou vazia e já existia, desativa em vez de apagar
            $record->update([
                'content' => '',
                'active'  => false,
            ]);
            continue;
        }
// ... update/create seguem nas linhas 358-378
```

A transação garante que um erro em uma área não deixa as demais parcialmente salvas. O
`trim()` faz de “somente espaços” uma área vazia.

```php
// V2/Modules/Support/StructuralLandingEditor/Services/BlockRenderer.php:30-41
protected function loadContents()
{
    if ($this->contentsCache === null) {
        $rows = StructuralPortalContent::where('active', true)
            ->where('content', '!=', '')
            ->get();

        $this->contentsCache = $rows->keyBy('area');
    }

    return $this->contentsCache;
}
```

Uma query por request para todas as áreas; `keyBy('area')` usa o `unique(area)`. O filtro
`active=true AND content != ''` é o que faz o portal cair no fallback gov.

```php
// V2/Modules/Support/StructuralLandingEditor/Services/BlockRenderer.php:144-162
$compiler = app('blade.compiler');
$compiled = $compiler->compileString($template);
$data = $this->getVars();

// Laravel precisa de $__env para renderizar sub-views (ex: sle_component)
$data['__env'] = app('view');

ob_start();
extract($data, EXTR_SKIP);

try {
    eval('?>' . $compiled);
} catch (\Throwable $e) {
    ob_end_clean();
    Log::warning('[SLE] Erro ao avaliar bloco: ' . $e->getMessage() . ' em ' . $e->getFile() . ':' . $e->getLine());
    return '<!-- SLE eval error -->';
}

return ltrim(ob_get_clean());
```

O `EXTR_SKIP` impede que o bloco sobrescreva `$compiled`, `$data` ou `$__env`. Falha de
execução não propaga exceção: o portal continua com um comentário no lugar do bloco.

```php
// V2/Modules/Support/StructuralLandingEditor/Controllers/LayoutBaseCmsController.php:250-262
private function saveConfig($key, $payload)
{
    $config = LayoutBaseConfig::firstOrNew(['key' => $key]);
    $oldPayload = $config->exists ? $config->payload : [];

    $config->payload = $payload;
    $config->save();

    $this->activityLogger->log($config, 'updated', [
        'old' => $oldPayload,
        'new' => $payload
    ]);
}
```

Todo payload de layout base passa por aqui: upsert por `key`, snapshot antigo e auditoria
tendo o próprio model como subject (diferente dos logs do `SleCmsController`). Não há
`DB::transaction` — `save` + `log` são dois efeitos independentes.

```php
// V2/Modules/Support/StructuralLandingEditor/Controllers/UserConfigsCmsController.php:25-36
public function update(Request $request)
{
    $data = $request->except(['_token', '_method', 'tab']);
    $files = $request->allFiles();

    try {
        $this->userConfigsService->updateConfigs($data, $files);
    } catch (\Exception $e) {
        Log::error('[UserConfigsCmsController] Erro ao atualizar configuracoes: ' . $e->getMessage());
        throw $e;
    }
}
```

Não há `$this->authorizeSupportAccess()` nem `v2_can`: o controller confia inteiramente
no middleware. É por isso que ele é reutilizável (e perigoso) a partir de outra rota.

```php
// V2/Modules/Support/StructuralLandingEditor/Controllers/UserConfigsCmsController.php:38-45
public function resetCmsTheme()
{
    try {
        $this->supportConfigsService->resetCmsThemeToDefault();
    } catch (\Exception $e) {
        Log::error('[UserConfigsCmsController] Erro ao resetar tema CMS: ' . $e->getMessage());
        throw $e;
    }
}
```

`resetCmsTheme` é acionado por `POST /cms/infra/user-configs/cms-theme/reset`
(`V2/Modules/Support/SupportConfigs/Routes/cms.php:61-64`). A rota pertence ao módulo
SupportConfigs, cujo `active_pattern` usa `|` (`SupportConfigs/ModuleProvider.php:87`) e
não casa com `Request::is()`; sem guarda no controller, **qualquer usuário CMS com papel**
restaura `cms_sidebar_bg`, `cms_brand_primary`, `cms_body_bg`, `cms_brand_name`,
`cms_brand_logo` e `cms_favicon` (`SupportConfigs/Services/SystemMaintenanceService.php:292-301`).
Ver `SEC-G1-04` em `_qa/security-g1.md:79-96`.

### 2.5 Algoritmos, consultas e custos

| Ponto | Consulta/complexidade | Custo e índice |
|---|---|---|
| Leitura de área no editor | `StructuralPortalContent::getForArea` → `where area = ? limit 1` por área (8 queries) | `unique(area)` garante O(1) |
| Runtime | 1 query (`active && content != ''`) por instância de `BlockRenderer` | singleton por request; `area` é unique |
| Layout base no portal | `LayoutBaseConfig::all()->keyBy('key')` no `LayoutBaseComposer`, memoizado em `static` | tabela pequena (≈7 chaves), full scan aceitável; soft delete não filtra explicitamente, mas o trait adiciona `deleted_at is null` |
| `BlockRenderer::getVars` | tenta `view()->shared('layoutConfigs')`; como ninguém usa `View::share`, sempre cai no fallback `LayoutBaseConfig::all()` | 1 query extra por request de bloco |
| Salvamento de tema/portal | `updateBatch` → 1 `select` + 1 `update/insert` por chave, dentro de uma transação | N chaves, sem bulk upsert |
| Auditoria | 1 insert em `activity_log` por submit (+1 por `saveConfig`) | payloads com `old/new` completos podem ser grandes (logos/variantes) |
| Upload CSS/JS | arquivo inteiro em memória (`file_get_contents`) e `put` no storage | limite do request 10 MB (`max:10240`) |
| Invalidação de cache | `KEYS prefix:page:*` bloqueante no Redis | custo cresce com o número de páginas cacheadas |

## 3. Particularidades

### 3.1 O que só existe aqui

- **Blocos executáveis por área** com fallback gov: header/barras/footer vazios ⇒ include
  padrão (`portal-content.blade.php:14-29,61-82,110-121`); CSS/JS vazios ⇒ nada é injetado.
- **`sle_component()`**: único helper de módulo registrado em `composer.json` `autoload.files`
  (`composer.json:37-39`), existindo para funcionar dentro de `eval()`.
- **Favicon fora da galeria**: `.ico` validado por extensão/MIME/tamanho e gravado em
  caminho fixo (`LayoutBaseCmsController.php:118-166`); todos os outros logos passam por
  galeria ou `MediaInputer`.
- **Dois “temas” independentes**: CMS (`v2_cms_theme_configs`) e portal (`v2_portal_configs`),
  com o mesmo formulário e roteamento por prefixo de chave.
- **`lai_enabled` fora do portal configs**: o único toggle de feature que mora em
  `v2_layout_base_configs`, lido diretamente por views de footer
  (`V2/layouts/portal/footer/footer-transparencia-lai.blade.php:12-15`).

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-SLE-01 | Toda action do `SleCmsController` exige e-mail de suporte; falha ⇒ `403` com `Acesso restrito ao suporte técnico autorizado.` | Nenhum dado é lido/escrito antes da guarda | `SleCmsController.php:31-36` |
| RN-SLE-02 | `v2_structural_portal_contents.area` é única entre 8 valores | só existe um bloco por área | `Migrations/2026_07_03_000000_...:22` |
| RN-SLE-03 | Área ativa exige `active=true` **e** `content != ''` | bloco vazio nunca renderiza | `BlockRenderer.php:32-34` |
| RN-SLE-04 | Salvar área vazia em registro existente desativa, não apaga | fallback gov volta a valer | `SleCmsController.php:349-356` |
| RN-SLE-05 | `tab ∈ {logos, comunicado-global}` no `/theme/save` vai para `LayoutBaseCmsController` | demais abas vão para `UserConfigs` | `SleCmsController.php:146-158` |
| RN-SLE-06 | Chave `cms_*` ⇒ tema CMS; `mail_*` ⇒ SMTP; resto ⇒ portal | o valor de `tab` não decide o destino de metadados | `UserConfigsService.php:68-80` |
| RN-SLE-07 | Senha SMTP vazia preserva a anterior; não vazia é `Crypt::encrypt` | `mail_password` nunca é devolvida (`getAll` retorna `''`) | `UserConfigsService.php:28-57,101-133` |
| RN-SLE-08 | Favicon aceita apenas `.ico`, MIME de ícone (ou `application/octet-stream`) e ≤ 100 KB | erro `InvalidArgumentException` com texto exato | `LayoutBaseCmsController.php:127-143` |
| RN-SLE-09 | CSS/JS armazenados são copiados para `assets/structuralEditor/{css,js}` com nome `vendor_<uniqid>_<time>` | nome original só em `original_name` | `AssetManagerService.php:31-40` |
| RN-SLE-10 | Imagem da File Library é mídia da galeria; `createFromGallery` deduplica por `gallery_media_id` | repetir o mesmo ID devolve o asset existente | `AssetManagerService.php:102-135` |
| RN-SLE-11 | Falha de compilação/execução de bloco não derruba a página | comentário `<!-- SLE compile/eval error -->` + `Log::warning` | `BlockRenderer.php:154-166` |
| RN-SLE-12 | `sle_component` desconhecido devolve comentário, não exceção | bloco continua renderizando | `ComponentResolver.php:40-42` |
| RN-SLE-13 | `resetCmsTheme` não tem guarda de suporte e a rota escapa do `active_pattern` | qualquer usuário CMS com papel reseta o tema | `UserConfigsCmsController.php:38-45`; `SupportConfigs/Routes/cms.php:61-64`; `SupportConfigs/ModuleProvider.php:87` |
| RN-SLE-14 | Upload de CSS/JS valida apenas `file` e `type`, sem extensão/MIME | arquivo arbitrário é aceito e publicado no storage público | `SleCmsController.php:427-430`; `AssetManagerService.php:26-51` |
| RN-SLE-28 | O portal calcula `(bool) $config->payload` para o LAI; como o payload é `{"enabled":bool}`, o array é sempre não vazio | desligar `lai_enabled` **não** esconde o painel LAI no rodapé | `V2/layouts/portal/footer/footer-transparencia-lai.blade.php:12-15`; `V2/layouts/portal/footer/footer.blade.php:26-30` |
| RN-SLE-29 | A rota alias `GET /cms/infra/structural-editor` não casa com `cms/infra/structural-editor/*` no `Request::is()` (falta a barra final) | o middleware de grupo não a reconhece; a proteção efetiva é a guarda do controller | `Routes/cms.php:60`; `V2/Core/Middleware/V2PermissionMiddleware.php:41-44` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **`$siteTitle` em blocos é sempre `Portal`**: `BlockRenderer` lê
  `config('v2_portal_configs.titulo_site')`, mas não existe `config/v2_portal_configs.php`;
  o título real está em `v2_portal_configs.titulo_site` (`BlockRenderer.php:92`).
- **`$portalTheme` em blocos é `[]`**: o tema é injetado por `$view->with` no composer de
  `v2-portal::*`, não por `View::share`; `view()->shared('portalTheme', [])` devolve o
  default. Já as views chamadas por `sle_component()` recebem o tema, porque o composer
  roda para elas (`ComponentResolver.php:47-53`; `SupportConfigs/ModuleProvider.php:18-63`).
- **`$layoutConfigs` em blocos vem do banco**, não do composer: `shared('layoutConfigs')`
  é `null` e há fallback (`BlockRenderer.php:79-88`).
- **`tab` desconhecido em `/structural-data/save`**: nenhum
  `case` casa, `saveConfig` não roda, mas o flash é de sucesso e o cache é limpo — falso
  positivo para o operador (`LayoutBaseCmsController.php:57-59`).
- **`assets?type=...`** filtra só `css`, `js`, `image`; qualquer outro valor devolve todos
  (`AssetManagerService.php:78-87`).
- **Upload/remoção de asset não limpa o cache de páginas**: só as cinco actions de
  configuração chamam `clearPageCache` (`SleCmsController.php:104,172,222,298,403`);
  `assetsUpload`/`assetsFromGallery`/`assetsDelete` não o fazem — o HTML só muda depois que
  alguém cola o snippet e salva o editor.
- **`assetsDelete`** captura `ModelNotFoundException` e devolve flash de erro em `302`;
  não há `404` para ID inexistente (`SleCmsController.php:501-528`).
- **CSS/JS “puro”**: a ajuda das abas diz para não enviar `<style>`/`<script>`
  (`tabs/css-head.blade.php:14-17`; `tabs/js-foot.blade.php:47-50`), mas o conteúdo é
  injetado sem wrapper e os seeders gravam tags `<link>`/`<script>`
  (`V2/Seeds/SleConfigsSeeder.php:61,167-176,180-186`). Na prática, o operador **precisa**
  incluir as tags.
- **Banner global fora da janela**: `start_date`/`end_date` não são validados no
  controller; quem decide exibir é `global-banner.blade.php:7-20`.
- **LAI desligado continua aparecendo**: o formulário grava `{"enabled":false}`, mas o
  portal faz `(bool)` do array (sempre `true`). Para uma reimplementação fiel ao
  comportamento observado, reproduza o bug; para o comportamento pretendido, leia
  `payload['enabled']` (RN-SLE-28).
- **Múltiplos logos com o mesmo caminho**: a tela resolve o ID da galeria por `file_path`
  (consulta direta a `v2_gallery_media` em `logos.blade.php:8-13`), podendo escolher a
  primeira mídia com aquele caminho.

### 3.4 Armadilhas de reimplementação

1. **Repetir a guarda só no middleware.** SEC-G1-04 mostra que uma rota de outro módulo
   pode chamar `UserConfigsCmsController@resetCmsTheme` sem a guarda. A reimplementação
   deve pôr `authorizeSupportAccess()` (ou policy) em **toda** action pública, inclusive
   nas reutilizadas por outro módulo.
2. **Renderizar bloco com `Blade::compileString` sem `$__env`.** `@include` e
   `sle_component` falham com `Call to a member function make() on null`. O módulo injeta
   `$__env = app('view')` (`BlockRenderer.php:148-149`).
3. **Usar `eval()` como o legado.** O manual do próprio módulo diz “não use `eval()`”
   (`manual/content.blade.php:250`), mas o runtime usa. Na reimplementação, prefira compilar
   para arquivo em disco/cache de views ou um sandbox que rejeite `<?php`; reproduzir o
   `eval` mantém a superfície de RCE para quem tem acesso ao editor.
4. **Achar que `active_pattern` aceita `|`.** Em `Request::is()` o pipe é literal; repetir
   o padrão de SupportConfigs reintroduz SEC-G1-04.
5. **Tratar `tab` como discriminador de metadados.** O destino real é o prefixo da chave;
   `tab` só separa `logos`/`comunicado-global` no tema.
6. **Assumir um bloco por página.** `getBlocks()` devolve no máximo 1 item por área; um
   novo design multi-bloco exige mudar o schema (`area` unique) e o runtime.
7. **Confundir os dois módulos de asset.** `v2_sle_assets` (SLE) e `v2_basic_page_assets`
   (BasicPage) são independentes, com snippets e diretórios próprios.

### 3.5 Divergências documentação × código e dívidas

| Documento/comentário | Afirma | Código |
|---|---|---|
| `manual/content.blade.php:154` | `$portalTheme` contém cores/logo | sempre `[]` no bloco (`BlockRenderer.php:95`) |
| `manual/content.blade.php:150` | `$siteTitle` é o título configurado | `'Portal'` (config inexistente, `BlockRenderer.php:92`) |
| `manual/content.blade.php:250` | “Não use `eval()`” | `renderBlock` usa `eval` (`BlockRenderer.php:155`) |
| `manual/content.blade.php:173-178` e `tabs/css-head.blade.php:14-17` | CSS/JS sem tags | seeders gravam tags; render não envolve |
| `tabs/userconfigs/tema-portal.blade.php:155-159` | tema injetado por `LayoutBaseComposer` → `colors.blade.php` | quem injeta é o composer anônimo de `SupportConfigs/ModuleProvider.php:18-63`; `LayoutBaseComposer` só injeta `layoutConfigs` |
| `tabs/userconfigs/tema-cms.blade.php:168-172` | logo/favicon lidos por `SidebarComposer`/`app.blade.php` | fora do escopo desta unidade; o SLE apenas grava |
| Nome do arquivo `2026_07_03_000000_create_v2_structural_portal_blocks_table.php` | tabela `v2_structural_portal_blocks` | cria `v2_structural_portal_contents` (`:18`) |
| `helpers.php:4-6` | helpers registrados “via composer.json autoload.files” | confirmado: `composer.json:37-39` |
| Bloco `main` no manual (`content.blade.php:45-48`) | área “Main” | runtime usa `main_left` e `main_right`; não existe área `main` |

Dívidas e código morto:

- `StructuralPortalContent::hasActiveInArea` (`:52-58`) não tem chamador.
- `Views/cms/components/asset-picker-modal.blade.php` (modal `#sleAssetPickerModal`) não é
  incluído por nenhuma view; a sidebar usa o picker do Gallery.
- `Views/cms/tabs/userconfigs/temas.blade.php` e
  `Views/cms/tabs/userconfigs/components/config-history.blade.php` não são referenciados.
- `asset-card.blade.php:59-60` repete o atributo `class` (`role="button" class="cursor-pointer"`),
  o segundo é ignorado.
- `Assets` do editor: CSS/JS sem validação de extensão/MIME (RN-SLE-14).
- O sidebar remove o botão de excluir dos cards clonados (`asset-sidebar.blade.php:244-246`),
  então a exclusão só existe na resposta AJAX bruta/`asset-picker-modal`.

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura | Precondição | Consumidores |
|---|---|---|---|---|
| `SleCmsController` | controller CMS | 14 actions (`Request` nas de escrita) | sessão + papel + e-mail de suporte | `Routes/cms.php` |
| `LayoutBaseCmsController@update` | controller sem rota própria | `update(Request): void` (depende de `tab`) | chamado por `SleCmsController` ou `app()` | `saveTheme`, `saveStructuralData` |
| `UserConfigsCmsController@update` | controller reutilizável | `update(Request): void` | nenhuma guarda própria | `saveMeta`, `saveTheme`, `saveFeatures`; `SupportConfigs` para reset |
| `UserConfigsCmsController@resetCmsTheme` | controller reutilizável | `resetCmsTheme(): void` | **nenhuma** | `SupportConfigs/Routes/cms.php:61-64` |
| `BlockRenderer::hasBlocks` | runtime | `hasBlocks(string $area): bool` | áreas ativas e não vazias | `base.blade.php`, `portal-content.blade.php` |
| `BlockRenderer::getBlocks` | runtime | `getBlocks(string $area): Collection` (0..1 item) | idem | idem |
| `BlockRenderer::getVars` | runtime | `getVars(): array` | — | `renderBlock`, manual |
| `BlockRenderer::renderBlock` | runtime | `renderBlock(string $template): string` | — | views do portal |
| `ComponentResolver::render` | runtime | `render(string $name, array $params = []): string` | nome no mapa | `sle_component` |
| `ComponentResolver::availableComponents` | introspecção | `(): array<string,{view,description}>` | — | manual (não usa) |
| `AssetManagerService` | serviço | `uploadVendor`, `uploadImage`, `listAssets`, `deleteAsset`, `createFromGallery`, `seedFromPath`, `generateSnippetForGallery` | storage/galeria configurados | `SleCmsController`; `V2/Seeds/SleAssetSeeder.php` |
| `UserConfigsService` | serviço | `getAll(): array`, `updateConfigs(array, array): void` | tabelas de SupportConfigs/mail | `SleCmsController`; `UserConfigsCmsController` |
| `LayoutBaseConfig` | model | `$table`, cast `payload=array`, SoftDeletes, UUID no `creating` | — | SLE, Homepage, Sobre Nós, Fale Conosco, seeds |
| `SleAsset` | model | idem + soft delete | — | File Library |
| `StructuralPortalContent` | model | `getForArea(string)`, `hasActiveInArea(string)` | — | editor/runtime |
| `sle_component` | helper global | `sle_component(string $name, array $params = []): string` | autoload `files` | blocos SLE |
| `v2-structurallandingeditor::cms.*` | views | ver `ui.md` | `$contents`/`$configs`/`$userConfigs` | controllers |

### 4.2 Contrato de UI/JS

Detalhado em [`ui.md`](ui.md). Resumo vinculante: formulários por aba com `tab` oculto,
`areas[<area>][content|content_type|active]`, `header_links_json`/`footer_links_json`/
`social_media_json`/`lai_items_json` como textarea oculto sincronizado por JS, e
`advance` de abas via Bootstrap 5 (`data-bs-toggle="tab"`).

### 4.3 Ordem e ciclo de vida

1. `V2ServiceProvider` descobre o módulo, registra views/migrations e **constrói** o
   `ModuleProvider` (singletons + composers) antes das rotas.
2. Rotas CMS recebem `web` + `module.active:StructuralLandingEditor`; o `active_pattern`
   do provider é `cms/infra/structural-editor/*` (`ModuleProvider.php:34`).
3. O `LayoutBaseComposer` roda por view de `v2-portal::*`/`v2-basicpage::*`; o
   `BlockRenderer` é resolvido por `@inject` no `base.blade.php`/`app-layout.blade.php`
   quando a classe existe (convivência com instalações sem o módulo).
4. Alterações no CMS só aparecem no portal após a requisição seguinte; o cache de página
   é limpo, mas não há versionamento de bloco.
5. `page.cache` é registrado nas rotas públicas (`V2ServiceProvider.php:297-302`), por isso
   a limpeza Redis é necessária.

### 4.4 Receitas de uso

**Injetar um componente nativo dentro de um bloco** (área `header`):

```blade
<header class="site-header">
    <div class="container d-flex align-items-center justify-content-between py-3">
        {!! sle_component('header-logo') !!}
        {!! sle_component('header-search') !!}
    </div>
    {!! sle_component('header-nav') !!}
</header>
```

Resultado: o header do portal usa as views `v2-portal::header.*`, com tema e
`layoutConfigs` injetados pelo composer mesmo dentro de `eval()`.

**Ler os dados estruturais em um bloco**:

```blade
@if(!empty($contato['telefone_1']))
    <a href="tel:{{ preg_replace('/\D/', '', $contato['telefone_1']) }}">
        {{ $contato['telefone_1'] }}
    </a>
@endif
```

Resultado: telefone configurado em Dados Estruturais → `v2_layout_base_configs.contato.payload.telefone_1`.

**Gravar um payload de layout base de outro módulo** (padrão Homepage/Sobre Nós):

```php
use V2\Modules\Support\StructuralLandingEditor\Models\LayoutBaseConfig;

$config = LayoutBaseConfig::withTrashed()->firstOrNew(['key' => 'landing_sections']);
$config->payload = $sections;   // array; o cast serializa em jsonb
$config->save();
```

Resultado: `v2_layout_base_configs` atualizado; `LayoutBaseComposer` passa a entregar a
chave a `v2-portal::*` e `v2-basicpage::*` no próximo request.

**Consumir a área no portal (sem duplicar o runtime)**:

```blade
@if($sleBlockRenderer->hasBlocks('footer'))
    @foreach($sleBlockRenderer->getBlocks('footer') as $block)
        <div class="sle-block" data-area="footer">
            {!! $sleBlockRenderer->renderBlock($block->content) !!}
        </div>
    @endforeach
@endif
```

Resultado: bloco renderizado; ausência de bloco mantém o footer padrão.

### 4.5 Anti-uso

1. **`{{ $sleBlockRenderer->renderBlock(...) }}` (escapado).** O HTML compilado é
   escapado e aparece como texto na página. Use `{!! !!}` (`portal-content.blade.php:17`).
2. **Chamar `UserConfigsCmsController@update` de uma rota sem middleware de suporte.**
   Não há guarda interna; qualquer sessão CMS altera tema/portal/e-mail (SEC-G1-04 é a
   prova para `resetCmsTheme`).
3. **Escrever `v2_layout_base_configs` com `DB::table()->insert`.** Pula o `creating` do
   model (UUID v4) e o cast; o registro nasce sem `id` ou com payload string. Use o model
   (`LayoutBaseConfig.php:29-37`).
4. **Guardar HTML de bloco em `v2_portal_configs`.** Não é renderizado; `BlockRenderer` só
   lê `v2_structural_portal_contents`. Para markup, use o editor.

### 4.6 Custos e limites da integração

- Upload de asset: `max:10240` KB (10 MB) por arquivo (`SleCmsController.php:428`); o
  `GallerySender` usa o mesmo limite (`GallerySender.php:10-16`).
- Favicon: 100 KB (`LayoutBaseCmsController.php:139-143`).
- Nenhum throttle próprio: o limite é o do grupo CMS.
- Auditoria guarda `old`/`new` completos — payloads de logos incluem variantes e podem
  inflar `activity_log`.
- `KEYS` no Redis é O(nº de páginas); em bases grandes, prefira `SCAN`.
- O conteúdo não tem tamanho máximo: um bloco de vários MB aumenta o HTML de todas as
  páginas e o custo de `eval`.

## 5. Rotas e contratos HTTP

Prefixo do grupo: `cms/infra/structural-editor`; middleware declarado
`\V2\Core\Middleware\CmsAuthenticate::class`; middleware efetivo `web`,
`module.active:StructuralLandingEditor`, e a checagem de grupo/permissão dentro do
`CmsAuthenticate` (`Routes/cms.php:17-62`).

| Tipo | Método | URI | Nome | Middleware | Controller@ação | Permissão | Efeito | Fonte |
|---|---|---|---|---|---|---|---|---|
| grupo | — | `cms/infra/structural-editor` | — | `web; CmsAuthenticate; module.active:StructuralLandingEditor` | — | grupo Support (e-mail) | agrupa 17 declarações (14 endpoints + 3 aliases) | `Routes/cms.php:17` |
| endpoint | GET | `.../meta` | `v2.structural-editor.meta` | idem | `SleCmsController@meta` | suporte | HTML; carrega user/layout configs | `:22` |
| endpoint | POST | `.../meta/save` | `v2.structural-editor.meta.save` | idem + CSRF | `@saveMeta` | suporte | grava portal/mail/cms; audita; limpa cache | `:23` |
| endpoint | GET | `.../theme` | `v2.structural-editor.theme` | idem | `@theme` | suporte | HTML | `:28` |
| endpoint | POST | `.../theme/save` | `v2.structural-editor.theme.save` | idem + CSRF | `@saveTheme` | suporte | grava tema/logos/banner | `:29` |
| endpoint | GET | `.../structural-data` | `v2.structural-editor.structural-data` | idem | `@structuralData` | suporte | HTML | `:34` |
| endpoint | POST | `.../structural-data/save` | `v2.structural-editor.structural-data.save` | idem + CSRF | `@saveStructuralData` | suporte | grava links/contato/redes/LAI | `:35` |
| endpoint | GET | `.../features` | `v2.structural-editor.features` | idem | `@features` | suporte | HTML | `:40` |
| endpoint | POST | `.../features/save` | `v2.structural-editor.features.save` | idem + CSRF | `@saveFeatures` | suporte | grava toggles + LAI | `:41` |
| endpoint | GET | `.../editor` | `v2.structural-editor.editor` | idem | `@editor` | suporte | HTML com 8 áreas | `:46` |
| endpoint | POST | `.../editor/save` | `v2.structural-editor.editor.save` | idem + CSRF | `@saveEditor` | suporte | grava áreas em transação | `:47` |
| endpoint | GET | `.../editor/assets` | `v2.structural-editor.assets` | idem | `@assets` | suporte | fragmento HTML de cards (AJAX) | `:52` |
| endpoint | POST | `.../editor/assets/upload` | `v2.structural-editor.assets.upload` | idem + CSRF | `@assetsUpload` | suporte | cria `SleAsset`; JSON ou redirect | `:53` |
| endpoint | POST | `.../editor/assets/from-gallery` | `v2.structural-editor.assets.from-gallery` | idem + CSRF | `@assetsFromGallery` | suporte | cria/reaproveita `SleAsset`; JSON | `:54` |
| endpoint | POST | `.../editor/assets/delete/{id}` | `v2.structural-editor.assets.delete` | idem + CSRF | `@assetsDelete` | suporte | apaga arquivo + soft delete | `:55` |
| alias | GET | `.../` | `v2.structural-editor.index` | idem | `@editor` | suporte | mesmo HTML do editor | `:60` |
| endpoint | POST | `.../save` | — (sem nome) | idem + CSRF | `@saveEditor` | suporte | alias retrocompatível | `:61` |
| endpoint | POST | `.../save-configs` | — (sem nome) | idem + CSRF | `@saveMeta` | suporte | alias retrocompatível | `:62` |

Rota externa que aciona um controller desta unidade (declarada por SupportConfigs):
`POST /cms/infra/user-configs/cms-theme/reset` → `UserConfigsCmsController@resetCmsTheme`,
nome `v2.supportconfigs.cms_theme.reset` (`V2/Modules/Support/SupportConfigs/Routes/cms.php:61-64`).
Ela **não** passa pela guarda de suporte porque o `active_pattern` do módulo dono não casa
(§3.2, RN-SLE-13).

### 5.1 Payloads

| Rota | Campos | Regras |
|---|---|---|
| `meta/save` | `tab` (`metadados`, `seo`, `email`), `titulo_site`, `descricao` (≤160 no HTML), `categoria_governo`, `cnpj_orgao`, `palavras_chaves`, `google_analytics_id`, `global_meta_tokens`, `header_scripts`, `footer_scripts`, `mail_*` | sem validação server-side; `tab` é descartado; chaves desconhecidas viram `v2_portal_configs` com `config_group=OUTROS` |
| `theme/save` (`tab=cms_theme`) | `cms_brand_name`, `cms_brand_logo` (media-input), `cms_favicon` (.ico), `cms_sidebar_bg`, `cms_brand_primary`, `cms_body_bg` | sem validação de cor; arquivos tratados por `SupportConfigsService` |
| `theme/save` (`tab=portal_theme`) | `theme_primary_color`, `theme_secondary_color`, `theme_accent_color`, `theme_link_color`, `theme_bg_main`, `theme_bg_canvas`, `theme_font_family`, `theme_font_size_base`, `theme_border_radius`, `theme_shadow_intensity` | idem |
| `theme/save` (`tab=logos`) | `logo_principal`, `logo_header`, `logo_header_contraste`, `sefaz`, `governo`, `membro_1`, `membro_2` (ID de galeria ou arquivo), `*_alt`, `favicon` (.ico), `favicon_alt` | validação só do favicon |
| `theme/save` (`tab=comunicado-global`) | `banner_image` (ID/arquivo), `banner_image_alt`, `url_destino`, `start_date`, `end_date` | sem validação de URL/data |
| `structural-data/save` | `tab` + `header_links_json` \| `footer_links_json` \| `social_media_json` \| `lai_items_json` \| campos de contato | JSON inválido ⇒ `[]` (silencioso) |
| `features/save` | `vlibras_enabled`, `accessibility_bar_enabled`, `accessibility_widget_enabled`, `lai_enabled` (+ hidden `0`) | `(bool)` do input; ausência de `lai_enabled` ⇒ `false` |
| `editor/save` | `areas[<area>][content]`, `areas[<area>][content_type]`, `areas[<area>][active]` | áreas fora da lista também são gravadas (não há whitelist!) |
| `assets/upload` | `file`, `type` (`css`,`js`,`image`), `alt_text` | `required|file|max:10240`, `in:css,js,image` |
| `assets/from-gallery` | `gallery_media_id`, `alt_text` | `required|integer|min:1` |

> Observação de segurança: `editor/save` grava qualquer chave de `areas[]` em
> `v2_structural_portal_contents` (o modelo só exige `area`), mas o runtime só lê as 8
> áreas conhecidas — a linha extra é inerte.

## 6. Dados e modelo de domínio

```mermaid
erDiagram
  v2_structural_portal_contents {
    uuid id PK
    string area UK
    text content
    string content_type
    boolean active
  }
  v2_layout_base_configs {
    uuid id PK
    string key UK
    jsonb payload
    uuid transaction_snapshot_id
    timestamp deleted_at
  }
  v2_sle_assets {
    uuid id PK
    string type
    string original_name
    string storage_path
    string mime_type
    bigint file_size
    int gallery_media_id FK
    text snippet
    timestamp deleted_at
  }
  v2_gallery_media ||--o{ v2_sle_assets : "gallery_media_id (sem FK)"
```

### 6.1 `v2_structural_portal_contents`

Migration `2026_07_03_000000_create_v2_structural_portal_blocks_table.php` (o nome do
arquivo cita `blocks`, a tabela é `contents`).

| Coluna | Tipo | Null | Default | Cast/nota |
|---|---|---|---|---|
| `id` | uuid | não | `v2_gen_uuid()` | PK; model também gera v4 no `creating` |
| `area` | varchar(30) | não | — | **unique**; 8 valores |
| `content` | text | sim | — | HTML/Blade/CSS/JS |
| `content_type` | varchar(20) | não | `htmlmixed` | `css`/`javascript`/`htmlmixed` (só rótulo; não muda o parser) |
| `active` | boolean | não | `true` | cast `bool` |
| `created_at`/`updated_at` | timestamps | sim | — | `dateFormat Y-m-d H:i:s` |

Quem escreve: `SleCmsController@saveEditor` e os seeders `StructuralPortalSeeder`,
`SleHeaderSeeder`, `SleFooterSeeder`, `SleConfigsSeeder` (via `DB::table(...)->updateOrInsert`).

Model `StructuralPortalContent`: `$fillable` inclui `id`, `area`, `content`, `content_type`,
`active`; `getForArea()` = `where(area)->first()`; `hasActiveInArea()` sem chamador.
Não usa SoftDeletes: área vazia é desativada, não removida.

### 6.2 `v2_layout_base_configs`

| Coluna | Tipo | Null | Default | Nota |
|---|---|---|---|---|
| `id` | uuid | não | — | sem default no banco; gerado no `creating` do model (`Uuid::uuid4()`) |
| `key` | string | não | — | **unique** |
| `payload` | jsonb | sim | — | cast `array` |
| `transaction_snapshot_id` | uuid | sim | — | não usado por este módulo |
| `created_at`/`updated_at` | timestamps | sim | — | — |
| `deleted_at` | timestamp | sim | — | SoftDeletes |

Chaves conhecidas e produtores:

| `key` | Formato | Escrito por | Lido por |
|---|---|---|---|
| `logos` | `{logo_principal, logo_header, logo_header_contraste, sefaz, governo, membro_1, membro_2, favicon}` (cada um `{original_name,path,disk,mime_type,size,type,variants,metadata}`) + `*_alt` | `LayoutBaseCmsController@handleLogosUpdate` | header/footer do portal, `base.blade.php` (favicon), `logos.blade.php` |
| `contato` | `endereco,map_link,telefone_1,telefone_2,email,horario,zipcode,po_box` | `handleContatoUpdate` | `footer-public-contact`, Fale Conosco, `$contato` em blocos |
| `links_header` | árvore `[{title,url,children[]}]` até 3 níveis | `handleLinksHeaderUpdate` | `header-nav`, `$headerLinks` |
| `links_footer` | `[{title, children:[{title,url}]}]` (colunas) | `handleLinksFooterUpdate` | `footer-services-area`, `$footerLinks` |
| `social_media` | `[{network,icon,url}]` | `handleRedesSociaisUpdate` | `footer-social-network`, `$redes` |
| `lai_items` | `[{id,name,icon,url,active,order}]` | `handleTransparenciaLaiUpdate` | `footer-transparencia-lai` |
| `lai_enabled` | `{enabled: bool}` | `SleCmsController@saveFeatures` | footer LAI (query direta) |
| `global_banner` | `{banner_image, banner_image_alt, url_destino, start_date, end_date}` | `handleComunicadoGlobalUpdate` | `base-struct/global-banner` |
| `landing_sections` | seções LSE por widget | `HomepageCmsController` / `SucesuHomepageSeeder` | `HomepagePortalController`, `BasicPage/portal/homepage.blade.php` |
| `sobre_nos_sections` | seções LSE | `SobreNosCmsController` | `SobreNosPortalController` |

### 6.3 `v2_sle_assets`

| Coluna | Tipo | Null | Default | Nota |
|---|---|---|---|---|
| `id` | uuid | não | `v2_gen_uuid()` | PK |
| `type` | varchar(20) | não | — | `css`, `js`, `image` |
| `original_name` | varchar(255) | não | — | nome enviado |
| `storage_path` | varchar(500) | não | — | caminho no disco (`assets/structuralEditor/css/...` ou caminho da galeria) |
| `mime_type` | varchar(100) | não | — | `file->getMimeType()` |
| `file_size` | unsignedBigInteger | não | — | bytes |
| `gallery_media_id` | unsignedInteger | sim | — | referência lógica a `v2_gallery_media.id`, **sem constraint** |
| `snippet` | text | não | — | HTML pronto para colar no editor |
| `created_at`/`updated_at` | timestamps | sim | — | — |
| `deleted_at` | timestamp | sim | — | SoftDeletes |

Índices: `idx_sle_assets_type (type)`, `idx_sle_assets_gallery (gallery_media_id)`.
Snippet: CSS → `<link rel="stylesheet" href="{{ v2_storage_url('...') }}">`; JS →
`<script src="{{ v2_storage_url('...') }}"></script>`; imagem →
`ImageRedigestService::buildPictureSnippet($filePath, $variants, $alt)`.

### 6.4 Tabelas escritas por delegação (donas de outras unidades)

| Tabela | Escrita por | Observação |
|---|---|---|
| `v2_portal_configs` | `PortalConfigsService::save` via `UserConfigsService::updateConfigs` | `config_key`/`config_value`; chaves `theme_*`, `titulo_site`, `descricao`, `palavras_chaves`, `cnpj_orgao`, `categoria_governo`, `google_analytics_id`, `global_meta_tokens`, `header_scripts`, `footer_scripts`, *flags* |
| `v2_cms_theme_configs` | `SupportConfigsService::saveCmsThemeConfigs` | `cms_*`; reset sem guarda (RN-SLE-13) |
| `v2_system_mail_configs` | `UserConfigsService::persistMailConfig` | `profile` default; `password` criptografado |
| `v2_gallery_media` | `GalleryService` (upload) | origem de `gallery_media_id` e das variantes |

## 7. Fluxos e estados

### 7.1 Estado de uma área

```mermaid
stateDiagram-v2
  [*] --> Inexistente
  Inexistente --> Ativa: create com content e active=1
  Inexistente --> Inexistente: save vazio (skip)
  Ativa --> Inativa: save vazio
  Inativa --> Ativa: save com content e active=1
  Ativa --> Ativa: save com content (active on/off)
  Inativa --> Inativa: save vazio
```

`Ativa` = `active=true` e `content != ''` (RN-SLE-03). `Inativa` mantém o texto (se
desativada pelo switch) ou `''` (se esvaziada).

### 7.2 FL-SLE-01 — salvar o editor

```mermaid
sequenceDiagram
  actor S as Suporte
  participant C as SleCmsController
  participant M as StructuralPortalContent
  participant A as ActivityLogger
  participant R as Redis
  S->>C: POST /editor/save (areas[])
  C->>C: authorizeSupportAccess()
  C->>M: getForArea() por área (old)
  C->>C: DB::transaction
  loop cada área
    C->>M: update() | create() | skip
  end
  C->>A: log(type=editor_save, areas, old, new)
  C->>R: KEYS page:* / DEL
  C-->>S: 302 /editor + "Conteúdo estrutural salvo com sucesso!"
```

### 7.3 FL-SLE-02 — tema/identidade

```mermaid
sequenceDiagram
  actor S as Suporte
  participant C as SleCmsController
  participant L as LayoutBaseCmsController
  participant U as UserConfigsCmsController
  S->>C: POST /theme/save (tab)
  C->>C: captura old (UserConfigs + LayoutBase)
  alt tab = logos | comunicado-global
    C->>L: update(request)
    L->>L: switch(tab) → saveConfig(key,payload)
  else outras abas
    C->>U: update(request)
    U->>U: remove _token/_method/tab
  end
  C->>C: log(type=theme_save) + clearPageCache
  C-->>S: 302 /theme + "Tema visual salvo com sucesso!"
```

### 7.4 FL-SLE-03 — upload de asset

```mermaid
sequenceDiagram
  actor S as Suporte
  participant C as SleCmsController
  participant A as AssetManagerService
  participant G as GalleryService
  participant St as StorageService
  S->>C: POST /editor/assets/upload (file, type)
  C->>C: validate(file ≤10MB, type)
  alt type=image
    C->>A: uploadImage(file, alt)
    A->>G: upload(config 'sle')
    G-->>A: id, file_path, variants
  else css|js
    C->>A: uploadVendor(file, type)
    A->>St: put('assets/structuralEditor/{css|js}/…')
  end
  A->>A: SleAsset::create(snippet)
  C->>C: log(created) + JSON {success,asset,snippet} | redirect
```

## 8. UI CMS

Contrato completo em [`ui.md`](ui.md). Telas:

| Tela | Rota | Permissão | Layout | Estados |
|---|---|---|---|---|
| Metaconfigurações | `v2.structural-editor.meta` | suporte | `v2-cms::layouts.app` | abas Metadados/SEO/E-mail; configs ausentes ⇒ campos vazios/defaults |
| Tema Visual Global | `...theme` | suporte | idem | abas Tema CMS/Tema Portal/Banner; logos dentro de Tema Portal; botão “Restaurar Padrões do CMS” |
| Dados Estruturais | `...structural-data` | suporte | idem | 5 abas; construtores JS de árvore/colunas/listas |
| Features | `...features` | suporte | idem | 4 toggles independentes |
| Editor | `...editor` | suporte | idem | 5 abas, 8 áreas; sidebar File Library; modal de alterações não salvas |
| Assets (fragmento) | `...assets?type=` | suporte | — | cards ou “Nenhum asset X encontrado.” |

Campos, mensagens e JS por tela: `ui.md` §3–§5. Todas as telas exibem
`@include('v2-cms::partials.activity-footer')` com `new StructuralPortalContent()`.

## 9. UI Portal

O módulo não é dono do layout; o efeito no portal é:

| Área | Posição | Fallback quando inativa/vazia | Fonte |
|---|---|---|---|
| `css_head` | após SEO, antes de `</head>` | nada | `base.blade.php:77-83` |
| `js_head` | `<head>`, após CSS head | nada (gravado no seed como vazio) | `:85-90` |
| `js_foot` | antes de `</body>` | nada | `:126-132` |
| `css_foot` | após os scripts, antes de `</body>` | nada | `:134-138` |
| `header` | topo do conteúdo | `v2-portal::header.header` + `global-banner` | `portal-content.blade.php:14-29` |
| `main_left` / `main_right` | sidebars (quando `showMainLeftArea`/`showMainRightArea`) | `main-left-area`/`main-right-area` | `:53-82` |
| `footer` | fim do conteúdo | `v2-portal::footer.footer` | `:110-121` |

O conteúdo é inserido com `{!! !!}`; não há sanitização, `nonce` CSP nem escaping. O
bloco substitui o fallback (não convive com ele) e não ganha classes de layout além de
`sle-block`/`data-area` no header/footer.

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

### 10.1 Chaves de configuração

| Chave | Default | Efeito | Fonte |
|---|---|---|---|
| `v2.support.email` | `suporte@maxima.inf.br` (`V2_SUPPORT_EMAIL`) | identidade única autorizada; comparada no middleware e na guarda | `config/v2.php:4-6`; `SleCmsController.php:33` |
| `cache.prefix` | `laravel` | prefixo do padrão `page:*` da invalidação | `SleCmsController.php:41-42` |
| `filesystems.media_disk` / `filesystems.default` | `public` | `disk` registrado no payload do favicon | `LayoutBaseCmsController.php:160` |
| `v2_portal_configs.titulo_site` | `Portal` | `$siteTitle` em blocos (arquivo de config inexistente) | `BlockRenderer.php:92` |

### 10.2 Permissões

| Slug/chave | Ação | Middleware/helper | Papel padrão | Onde |
|---|---|---|---|---|
| grupo `Support` | acesso | `V2PermissionMiddleware` (e-mail) | `config('v2.support.email')` | todas as rotas do grupo |
| `StructuralLandingEditor` | read/write | `CmsAuthenticate` → `V2PermissionMiddleware` | suporte/super-admin; demais por `v2_role` | `V2PermissionMiddleware.php:75-101` |
| `LayoutBase` | write | `v2_can('LayoutBase','write')` / `@v2can` | suporte e super-admin; demais só com permissão explícita | abas de `layoutbase/*` |
| `module.active:StructuralLandingEditor` | verificação de módulo ativo | middleware de rota | — | `V2ServiceProvider.php:304-307` |
| — (reset CMS) | write | **nenhum** | qualquer usuário CMS com papel | `SupportConfigs/Routes/cms.php:61-64` |

`ModuleProvider::getConfig()` (`ModuleProvider.php:28-72`): `title=Editor Estrutural`,
`position=91`, `active_pattern=cms/infra/structural-editor/*`, `restricted=config('v2.support.email')`,
`route=v2.structural-editor.editor` e cinco `subModulesLevel1` (Metaconfigurações, Tema
Visual Global, Dados Estruturais, Features, Editor).

## 11. Observabilidade, erros e limites

| Log | Canal/nível | Mensagem | Fonte |
|---|---|---|---|
| cache | default/`warning` | `[SLE] Falha ao limpar cache de páginas: …` | `SleCmsController.php:47` |
| leitura | `debug` | `[SLE] UserConfigsService indisponivel: …` | `:64,124,242` |
| escrita | `error` | `[SLE] Erro ao salvar metaconfiguracoes/tema/dados estruturais/features: …` | `:84,158,206,273` |
| assets | `error` | `[SLE] Erro ao enviar asset/…criar asset da galeria/…remover asset: …` | `:462,489,520` |
| layout base | `error` | `Erro ao salvar LayoutBase: …` | `LayoutBaseCmsController.php:67` |
| favicon | `info` | `[LayoutBaseCmsController] Favicon portal salvo em: …` | `:152` |
| bloco | `warning` | `[SLE] Erro ao compilar/avaliar bloco: …` | `BlockRenderer.php:158,164` |
| user configs | `error`/`debug` | `[UserConfigsCmsController] …`; `[UserConfigsService] …` | `UserConfigsCmsController.php:33,43`; `UserConfigsService.php:53,94` |

- **Auditoria**: `activity_log` via `ActivityLoggerService`; subjects: `StructuralPortalContent`
  (genérico, com `type`) e `LayoutBaseConfig`/`SleAsset` (model real).
- **Fail-open**: erro de bloco não interrompe o portal; erro de Redis não impede salvar.
- **Fail-closed**: guardas de acesso (`403`/redirect).
- **Limites**: 10 MB por asset; 100 KB por favicon; sem paginação em `listAssets`
  (retorna tudo); `assets()` monta o HTML completo para AJAX.
- **Dados sensíveis**: a senha SMTP nunca retorna (`mail_password=''`) e é mascarada
  (`***`) na auditoria (`UserConfigsService.php:145-160`).

## 12. Seeds e dados iniciais

| Seeder | Tabelas | Ordem | Idempotência | Fonte |
|---|---|---|---|---|
| `StructuralPortalSeeder` | `v2_structural_portal_contents` (8 áreas vazias), orquestra os demais | 1 | `updateOrInsert(['area'])` | `V2/Seeds/StructuralPortalSeeder.php:22-58` |
| `SleAssetSeeder` | `v2_gallery_media`, `v2_sle_assets` | 2 | cria/reaproveita por `createFromGallery` (dedup por `gallery_media_id`) | `V2/Seeds/SleAssetSeeder.php:30-85` |
| `SleHeaderSeeder` | `v2_structural_portal_contents.area=header` | 3 | `updateOrInsert` (por área) | `V2/Seeds/SleHeaderSeeder.php` |
| `SleFooterSeeder` | `v2_structural_portal_contents.area=footer` | 4 | idem | `V2/Seeds/SleFooterSeeder.php` |
| `SleConfigsSeeder` | `css_head`, `js_foot` e áreas vazias | 5 | `updateOrInsert` | `V2/Seeds/SleConfigsSeeder.php:180-227` |
| `SucesuHomepageSeeder` | `v2_layout_base_configs.landing_sections` | — | `firstOrNew(['key'])` | `V2/Seeds/BasicPages/SucesuHomepageSeeder.php:387-394` |

Só o cenário SUCESU recebe conteúdo: o orquestrador compara
`v2_portal_configs.titulo_site` com `SUCESU Nacional` e, fora disso, deixa as 8 áreas
vazias/desativadas (fallback gov) (`StructuralPortalSeeder.php:57-67`).

## 13. Lacunas

| ID | Lacuna | Onde procurar | Impacto |
|---|---|---|---|
| LG-SLE-01 | Não há seed de permissão `LayoutBase`; não foi possível confirmar se algum perfil recebe `write` em produção | `v2_permissions`, `v2_user_permissions`, telas de Gestão de Permissões | apenas suporte/super-admin conseguem salvar as abas de layout base |
| LG-SLE-02 | Não há histórico/versionamento de blocos: `activity_log` guarda `old/new`, mas não há UI de restauração | `v2-service-activitylogger` | recuperação manual pelo log |
| LG-SLE-03 | Não existe validação/sanitização de HTML/JS de bloco nem CSP nonce no `eval` | `ContentRichEditor`, `09-cache-csp-minify-maintenance.md` | risco de XSS/RCE por conta de suporte |
| LG-SLE-04 | O `active_pattern` de SupportConfigs e a ausência de guarda em `resetCmsTheme` devem ser corrigidos na reimplementação; não há decisão registrada de onde | `_qa/security-g1.md:79-96`; `MOD-SUPPORT-SUPPORTCONFIGS` | SEC-G1-04 permanece até correção |
| LG-SLE-05 | Não foi possível confirmar o comportamento de `view()->shared('portalTheme')` em todas as versões de Laravel do alvo | `vendor/laravel/framework/src/Illuminate/View/Factory.php:549-552` | o manual promete `$portalTheme`, o runtime entrega `[]` |
| LG-SLE-06 | `Assets` CSS/JS aceitam qualquer extensão/MIME; não se sabe se há validação no proxy/web server | `AssetManagerService.php:26-51`; configuração de storage | upload de conteúdo ativo no domínio do portal |

## 14. Checklist de paridade

- [ ] `PC-SLE-01` — `GET /cms/infra/structural-editor/editor` como não-suporte com papel retorna `403` (AJAX) ou redirect com a mensagem de área restrita.
- [ ] `PC-SLE-02` — Salvar header com HTML e `active=1` faz `{{` render do bloco aparecer no portal na requisição seguinte.
- [ ] `PC-SLE-03` — Esvaziar o header desativa a linha (`content=''`, `active=false`) e o portal volta ao header governamental.
- [ ] `PC-SLE-04` — As 8 áreas existem e `area` rejeita duplicata.
- [ ] `PC-SLE-05` — `tab=logos` grava `v2_layout_base_configs.logos` com `path`, `disk`, `mime_type`, `size`, `variants`; `tab=portal_theme` grava `theme_*` em `v2_portal_configs`.
- [ ] `PC-SLE-06` — Toggle `lai_enabled=0` é persistido como `{"enabled":false}` mesmo com checkbox desmarcado.
- [ ] `PC-SLE-07` — `sle_component('header-logo')` dentro de um bloco devolve HTML do header; nome inválido devolve `<!-- SLE: componente 'x' desconhecido -->`.
- [ ] `PC-SLE-08` — Falha no `compileString` produz `<!-- SLE compile error -->`; falha de PHP/sintaxe durante o `eval` produz `<!-- SLE eval error -->`; ambos geram `warning` e nunca `500`.
- [ ] `PC-SLE-09` — Upload de CSS cria arquivo em `assets/structuralEditor/css` e linha em `v2_sle_assets` com snippet `<link ... v2_storage_url(...)>`.
- [ ] `PC-SLE-10` — Repetir `assets/from-gallery` com o mesmo `gallery_media_id` não duplica o asset.
- [ ] `PC-SLE-11` — Favicon `.png` renomeado para `.ico` com MIME inválido é rejeitado; `.ico` ≤100 KB sobrescreve `structural-landing-editor/favicon/portal.ico`.
- [ ] `PC-SLE-12` — Os cinco submits de configuração (`meta/save`, `theme/save`, `structural-data/save`, `features/save`, `editor/save`) limpam as chaves `cache.prefix:page:*`; uploads e remoções de asset **não** limpam (exceção documentada em §3.3).
- [ ] `PC-SLE-13` — `resetCmsTheme` acessível por usuário CMS não-suporte (comportamento legado a corrigir) — teste de regressão/segurança.
- [ ] `PC-SLE-14` — `LayoutBaseConfig` continua legível por Homepage/Sobre Nós/BasicPage/Fale Conosco com cast `array`.
- [ ] `PC-SLE-15` — `$siteTitle` e `$portalTheme` dentro de bloco: documentar o valor real (`Portal`, `[]`) ou corrigir na reimplementação.
- [ ] `PC-SLE-39` — Com `lai_enabled={"enabled":false}`, o portal legado **continua** exibindo o painel LAI; a reimplementação deve decidir entre paridade (reproduzir) ou correção (ler `payload['enabled']`).
- [ ] `PC-SLE-40` — `GET /cms/infra/structural-editor` (sem barra final) responde `403` para não-suporte pela guarda do controller, mesmo que o middleware de grupo não reconheça a rota.

## 15. Evidências consultadas

- `V2/Modules/Support/StructuralLandingEditor/Controllers/{SleCmsController.php:20-533,LayoutBaseCmsController.php:20-263,UserConfigsCmsController.php:12-47}`
- `V2/Modules/Support/StructuralLandingEditor/{ModuleProvider.php:1-72,helpers.php:1-30,Routes/cms.php:1-63}`
- `V2/Modules/Support/StructuralLandingEditor/Services/{BlockRenderer.php:1-168,ComponentResolver.php:1-100,AssetManagerService.php:1-198,UserConfigsService.php:1-170,GallerySender.php:1-18}`
- `V2/Modules/Support/StructuralLandingEditor/{Models,ViewComposers,Migrations}/**`
- `V2/Modules/Support/StructuralLandingEditor/Views/cms/**`, `Views/manual/content.blade.php:1-316`
- `V2/layouts/portal/{base.blade.php:1-161,app-layout.blade.php:1-32,partials/portal-content.blade.php:1-121,base-struct/global-banner.blade.php:7-20,footer/footer-transparencia-lai.blade.php:12-15}`
- `V2/Modules/Support/SupportConfigs/{ModuleProvider.php:18-87,Routes/cms.php:61-64,Services/SystemMaintenanceService.php:292-308,Services/PortalConfigsService.php:79-188}`
- `V2/Modules/Layouts/{Homepage/Controllers/HomepageCmsController.php:32-101,Homepage/Controllers/HomepagePortalController.php:44-50,BasicPage/Views/portal/homepage.blade.php:37-40}`
- `V2/Modules/Institucional/{SobreNos/Controllers/SobreNosCmsController.php:33-68,FaleConosco/Controllers/FaleConoscoPortalController.php:41-44}`
- `V2/Seeds/{StructuralPortalSeeder.php,SleAssetSeeder.php,SleConfigsSeeder.php,SleHeaderSeeder.php,SleFooterSeeder.php,BasicPages/SucesuHomepageSeeder.php:387-394}`
- `V2/Core/{Providers/V2ServiceProvider.php:92-140,287-313,Middleware/CmsAuthenticate.php:1-45,Middleware/V2PermissionMiddleware.php:1-101,Services/PermissionService.php:38-75,Helpers/helpers.php:12-57}`
- `composer.json:27-40`; `config/v2.php:4-6`; `_qa/security-g1.md:79-96`
