# Especificação técnica — OmniCo (projeto primo)

| Campo | Valor |
|---|---|
| **Projeto de origem** | OmniCo-Site (`/home/ljarillo/projetos/omnico-site`) |
| **Repositório** | `https://github.com/neotix/OmniCo-Site.git` |
| **Branch analisada** | `homolog-updated` |
| **Data do levantamento** | 03/09/2026 |
| **Propósito deste documento** | Fonte de verdade do legado para o omni-site: páginas, CMS, contratos Inertia, entidades e gaps |

> Levantamento feito no código, não no `CLAUDE.md` do primo. Esse arquivo está desatualizado em pontos importantes (Laravel 11 vs 12, Inertia 2 vs 3, “3 notícias na Home” vs 2, shadcn/ui declarado mas não instalado).

---

## 1. O que é o OmniCo

Site institucional do **holding Omni&Co** (grupo: Omni Banco, Finsol, Instituto Omni, Omni1). Não é o site de produto Omni (financiamento, SuperApp, etc.) — esse é o omni-site.

Público: institucional, RI, governança, sustentabilidade, carreiras e imprensa.

Não há API REST pública. Não há formulário de contato (tabela `contact_messages` foi dropada). CTAs de e-mail usam `mailto:`. Vagas apontam para Gupy.

---

## 2. Stack real

| Camada | Tecnologia | Versão no código |
|---|---|---|
| Backend | Laravel | `^12.0` |
| PHP | 8.2+ (CI 8.2; Sail local PHP 8.5) | |
| Bridge Laravel ↔ React | Inertia.js | `inertiajs/inertia-laravel ^3.0` + `@inertiajs/react ^3.0.3` |
| Front | React + TypeScript | `^19.2` / `^5` (TS 6 no package) |
| Build | Vite `^8` + `@vitejs/plugin-react` | |
| CSS | Tailwind `^4` + SCSS por página | |
| CMS / admin | Filament `^3.3` (Livewire) | |
| Banco | MySQL 8.x (Sail: `mysql:8.4`) | |
| Slugs | `spatie/laravel-sluggable ^3.8` | |
| Sitemap | `spatie/laravel-sitemap ^8.0` | |
| Testes | Pest `^3.8` + Vitest `^4` | |
| Local | Laravel Sail / Docker Compose | |
| Animações | Framer Motion `^12` | |
| Gráficos (RI) | Recharts `^3` | |
| Lightbox | `@fancyapps/ui` | |

**Não está no `package.json`:** shadcn/ui, Radix. O CLAUDE.md do primo cita shadcn; o código não usa.

**Não há** `routes/api.php`. Axios só aparece em `resources/js/bootstrap.js` (header `X-Requested-With`) — o site público não consome JSON próprio.

---

## 3. Arquitetura em uma frase

```
Browser  →  rotas web.php  →  Controller  →  Inertia::render('Page', $props)
                                              ↓
                                    Pages/*.tsx (wrapper fino)
                                              ↓
                                    features/<feature>/<Feature>Page.tsx
                                              ↓
                                    seções + settings/entidades

Admin    →  /admin (Filament)  →  Manage*Page / Resource
                                              ↓
                                    settings (key/value)  +  tabelas de entidade
                                              ↓
                                    ActivityLogger
```

Dois mundos:

1. **Site público** — Inertia. GET only. Props serializadas no HTML da primeira carga; navegações seguintes são XHR Inertia.
2. **CMS** — Filament/Livewire em rota isolada (`FILAMENT_PATH`, default `admin`). Autenticado. Não compartilha componentes React.

---

## 4. Páginas públicas

### 4.1 Mapa rota → controller → Inertia → feature

| URL | Nome da rota | Controller | Página Inertia | Feature | Props específicas |
|---|---|---|---|---|---|
| `/` | `home` | `HomeController` | `Home` | `features/home` | `latestClippings`, `homeSettings`, `brands` |
| `/quem-somos` | `about` | `StaticPageController@about` | `About` | `features/about` | `pageSettings`, `brands` |
| `/governanca` | `governance` | `StaticPageController@governance` | `Governance` | `features/governance` | `pageSettings`, `directors`, `policies` |
| `/investidores` | `investors` | `StaticPageController@investors` | `Investors` | `features/investors` | `pageSettings`, `portfolioRows`, `ratingRows` |
| `/sustentabilidade` | `sustainability` | `StaticPageController@sustainability` | `Sustainability` | `features/sustainability` | `pageSettings`, `projects`, `documents` |
| `/carreiras` | `careers` | `StaticPageController@careers` | `Careers` | `features/careers` | `pageSettings` |
| `/imprensa` | `press.index` | `PressController@index` | `Press/Index` | `features/press` | `clippings` (paginado), `pageSettings` |
| `/robots.txt` | `robots` | `RobotsController` | — (texto puro) | — | — |
| *(qualquer 404)* | — | handler em `bootstrap/app.php` | `NotFound` | `features/notfound` | — |
| 403 / 500 / 503 (não-local) | — | idem | `Error` | `features/error` | `status` |

Não existem rotas para `/politica-de-privacidade` nem `/canal-de-denuncias`, embora o CMS tenha campos para isso.

### 4.2 Seções de cada página (front)

| Página | Seções (ordem de render) |
|---|---|
| **Home** | Hero → Diferenciais (“Sobre a Omni&Co”) → Nossas Marcas → Manifesto / O Grupo → Imprensa (2 clippings) → Tagline |
| **Quem Somos** | Hero → Sobre → Nossas Marcas → Nossos Pilares → Nossa História → Premiações → Depoimento CEO |
| **Governança** | Hero → Diretrizes/Princípios → Liderança (diretoria) → Estrutura colegiada (comitês) → Composição / societária → Políticas e documentos |
| **Investidores** | Hero → Panorama → Indicadores (tabela CMS + gráfico hardcoded) → Rating → Programa Elas → Fale com RI |
| **Sustentabilidade** | Hero → Propósito → Pilares → **Trajetória (hardcoded)** → Projetos/Iniciativas (tabela) → Transparência (docs) |
| **Carreiras** | Hero → Cultura (valores + vídeo + quote CEO) → Diferenciais → Benefícios → Pessoas (depoimentos em settings) → Vagas (CTA Gupy) |
| **Imprensa** | Hero → Grid de clippings (12/página, link externo) → Assessoria |

Navbar e footer vêm do `AppLayout` (compartilhado). Páginas com hero overlay (`/`, `/quem-somos`, `/investidores`, `/governanca`, `/sustentabilidade`, `/carreiras`) começam com navbar transparente; `/imprensa` tem `padding-top`.

### 4.3 Âncoras usadas no menu

O dropdown da navbar é **estático** (labels e hashes), exceto “Nossas marcas”, que vem de `layoutBrands`.

| Menu | Itens |
|---|---|
| A Omni&Co | `/quem-somos`, `#posicionamento`, `#historia`, `#premiacoes` |
| Nossas marcas | `/quem-somos#marca-{slug}` por marca com `show_in_nav` |
| Governança | `/governanca`, `#lideranca`, `#estrutura`, `#composicao`, `#politicas` |
| RI | `/investidores`, `#indicadores`, `#rating`, `#programa-elas`, `#fale-com-ri` |
| Sustentabilidade | `/sustentabilidade`, `#iniciativas`, `#politicas` |
| Carreiras | `#proposito`, `#beneficios`, `#vagas` |
| Imprensa | `/imprensa`, `#assessoria` |

---

## 5. Comunicação front ↔ back (Inertia)

### 5.1 Contrato

Não há endpoints JSON de negócio. O contrato é:

1. Laravel monta um array PHP.
2. `Inertia::render('NomeDaPagina', $props)` serializa para JSON embutido na página (ou na resposta XHR).
3. React recebe as props no componente default de `resources/js/Pages/...`.
4. `Pages/*.tsx` só despacha para `features/<feature>/<FeaturePage>`.

Navegação interna usa `<Link>` do `@inertiajs/react` (visita Inertia, não full reload). Links externos (clippings, marcas, Gupy, Canal de Denúncias) são `<a href>` nativo.

Única paginação server-side: **Imprensa** (`paginate(12)`). O front usa `clippings.links` do LengthAwarePaginator do Laravel.

### 5.2 Props compartilhadas (todas as páginas)

Definidas em `app/Http/Middleware/HandleInertiaRequests.php`:

| Prop | Tipo | Origem | Uso no front |
|---|---|---|---|
| `flash.success` / `flash.message` | session | flash Laravel | (pouco usado no site público) |
| `appUrl` | `config('app.url')` | — | SEO (canonical, OG absoluto) |
| `currentPath` | `$request->getPathInfo()` | — | SEO canonical |
| `siteSettings` | `Record<string, string>` **lazy** | `settings` group `global` | footer, cookies, SEO fallback |
| `layoutBrands` | array **lazy** | `BrandPresenter::forLayout()` | navbar (dropdown marcas) + footer (marquee de logos) |

`siteSettings` e `layoutBrands` são closures — Inertia só avalia se a página precisa. `global_seo_og_image` já sai como URL pública (`Storage::disk('public')->url`).

**Não** se chama a prop compartilhada de `brands`: Home e About passam `brands` próprio (payload maior). O layout usa `layoutBrands` de propósito.

### 5.3 Resolução de imagens no controller

Controllers leem `settings.value` (path relativo no disco `public`, ex. `home/xyz.jpg`) e **só na saída** convertem chaves de imagem para URL (`/storage/...`). O front nunca recebe o path cru das keys listadas em `IMAGE_KEYS` / `resolveImages()`.

Entidades (`Brand`, `PressClipping`, `Director`, `SustainabilityProject`) expõem accessors `*_url` com a mesma lógica: HTTP absoluto passa; path começando em `/` é asset público (`/images/...`); resto vira `/storage/...`.

### 5.4 HTML no conteúdo

Vários campos são `RichEditor` no Filament. O front renderiza com `dangerouslySetInnerHTML`. `BrandPresenter::ensureHtml()` envolve texto puro em `<p>` se não houver tags.

### 5.5 Fallback de imprensa

`App\Support\PressClippingFallback` substitui o banco quando `APP_ENV=homolog` **ou** a extensão PHP `intl` não está carregada (fora de `testing`). Devolve 8 notícias fake e uma “paginação” de 1 página. É paliativo de servidor, não regra de negócio.

### 5.6 Bootstrap Inertia

`resources/js/app.tsx`:

- `import.meta.glob('./Pages/**/*.tsx', { eager: true })`
- Layout default = `AppLayout` se a página não definir `layout`
- Título: `{title} — OmniCo`
- Progress bar cinza

Root Blade: `resources/views/app.blade.php` (`@inertia` + `@inertiaHead` + Vite + GTM `GTM-NP3T99VX`).

---

## 6. Como o CMS funciona

### 6.1 Painel

- Provider: `App\Providers\Filament\AdminPanelProvider`
- URL: `/{FILAMENT_PATH}` (env `FILAMENT_PATH`, default `admin`)
- Login Filament nativo + reset de senha custom (`RequestPasswordReset`)
- Branding NeoCMS (`public/images/neocms_omnieco.png`)
- Dark mode off; cor primária violeta
- Grupos de navegação: **Páginas** e **Sistema**
- Menu do usuário: Alterar senha + Sair

Não há `routes/admin.php`. Filament registra as próprias rotas.

### 6.2 Dois padrões de edição

**A. Custom Page (padrão principal das páginas institucionais)**

Classes `Manage*Page` em `app/Filament/Pages/`. Cada uma:

1. `mount()` lê `Setting::where('group', X)` + defaults hardcoded.
2. Form Filament em abas = seções visuais do site.
3. `save()` faz `Setting::set($key, $value, $group)` e, quando há entidade (diretoria, políticas, projetos), sincroniza a tabela (create/update/delete pelo repeater).
4. Upload de substituição: campo FileUpload vazio **não** apaga o arquivo atual (`ClearsReplacementUploads` + `shouldSkipEmptyReplacementUpload`). Preview via `Placeholder` com a URL do storage.
5. Toggle “página publicada” grava `{group}_is_published` (`'1'`/`'0'`).
6. `ActivityLogger::log('page.{x}.updated', ...)`.

**B. Resource CRUD (entidades reutilizáveis)**

- `BrandResource` — visível no menu.
- `PressClippingResource` — **escondido** do menu; a UI real é a aba “Notícias” de `ManagePressPage` (página + table Livewire no mesmo screen).
- `UserResource`, `PerfilResource`, `ActivityLogResource` — Sistema.
- Resources **ocultos** (`shouldRegisterNavigation = false`) que ainda existem como leftover ou suporte: `PageResource`, `SettingResource` (form vazio), `TestimonialResource`, `DirectorResource`, `PolicyResource`, `SustainabilityProjectResource`, `SustainabilityDocumentResource`. Diretoria/políticas/projetos/docs são editados **dentro** das Manage Pages, não pelo CRUD clássico.

### 6.3 Modelo de settings (key/value)

Tabela `settings`:

| Coluna | Uso |
|---|---|
| `key` | único, ex. `hero_headline_1`, `about_ceo_name` |
| `value` | texto / HTML / path de arquivo / JSON (repeaters de RI) |
| `group` | `home`, `about`, `governance`, `investors`, `sustainability`, `careers`, `press`, `global` |

API: `Setting::get($key, $default)` e `Setting::set($key, $value, $group)`.

Isso **não** é um CMS de páginas genéricas. Cada key é contrato com o React.

### 6.4 Flag `{page}_is_published`

Existe no admin (toggle + widget `PagesStatusWidget`). **Não é lida pelos controllers públicos.** Desativar a página no CMS não tira o ar o site. É status operacional / dashboard.

O que realmente some do ar:

- `Brand.is_published = false` → some de Home, About, nav e footer.
- `PressClipping.is_published = false` → some da listagem e não entra no select de destaque da Home.

### 6.5 Dashboard do admin

- Saudação (`Dashboard`)
- `QuickActionsWidget`: atalhos para nova notícia, destaque Home, docs de governança
- `PagesStatusWidget`: semáforo das 7 páginas
- `UltimasNoticiasWidget`: últimas 6 notícias com edição modal

### 6.6 Uploads

- Disco `public` (`storage/app/public` → `public/storage` via `storage:link`)
- Imagens: jpeg/png/webp; tamanhos típicos 2–5 MB; notícia 665×380; OG 1200×630
- PDF: `PdfFileUpload` (até 20 MB, limitado pelo servidor)
- Vídeo (manifesto Home, cultura Carreiras, propósito Sustentabilidade): URL (YouTube/Vimeo) **ou** arquivo; Livewire temp upload elevado a 200 MB em `AppServiceProvider`
- `UploadLimit` alinha hint do campo com `upload_max_filesize` / `post_max_size` / regra Livewire

### 6.7 Auth, perfis e auditoria

**Usuário**

- `role`: `root` | `usuario`
- Root: acesso total, **não aparece** na listagem de usuários, não pode ser deletado pela UI
- Usuário comum só entra no painel se tiver `perfil_id`
- Sem perfil = não acessa (`canAccessPanel`)

**Permissões** (`permissoes.chave` ↔ `perfis` N:N)

| Módulo | Chaves |
|---|---|
| Imprensa | `imprensa.noticias.{criar,editar,excluir,publicar}`, `imprensa.pagina.editar` |
| Páginas | `paginas.{home,sobre,governanca,investidores,sustentabilidade,carreiras}.editar` |
| Marcas | `marcas.{criar,editar,excluir,publicar}` |
| Documentos | `governanca.documentos.gerenciar` |
| Usuários | `usuarios.{criar,editar,excluir}` |
| Sistema | `sistema.configuracoes.editar`, `sistema.logs.visualizar` |

`PerfilResource` é **somente root**. Seed cria perfis “Administrador” (tudo) e “Operador” (sem Usuários/Sistema).

Convite: criar usuário dispara e-mail para definir senha (`Notifications\ResetPassword` em modo invitation).

**Activity log** (`activity_logs`): login, logout, saves das Manage Pages, etc. Resource só leitura. Prune diário: `activity-logs:prune --days=90`.

---

## 7. O que o CMS controla — por superfície

Legenda: **CMS** = editável no Filament e lido no front. **Parcial** = copy no CMS, estrutura/dados fixos no React. **Hardcoded** = só no front (ou defaults se setting vazio). **Morto** = existe no admin e **não** chega no site.

### 7.1 Global (`ManageSettingsPage`, group `global`) → prop `siteSettings`

| Campo | CMS? | Front |
|---|---|---|
| `global_footer_tagline` | sim | footer |
| `global_footer_email` | sim | footer (`mailto`) |
| `global_footer_copyright` | sim | footer; token `{year}` |
| `global_social_{linkedin,instagram,facebook,youtube,spotify}` | sim | ícones; URL vazia = esconde |
| `global_cookie_banner_text` | sim | `CookieBanner` (HTML) |
| `global_seo_site_name` | sim | `<Seo>` fallback de título |
| `global_seo_description` | sim | meta description fallback |
| `global_seo_og_image` | sim | OG fallback |
| `global_company_{name,email,phone,address}` | **morto** | nenhum componente lê |
| `global_footer_complaints_url` | **morto** | footer usa URL fixa `contatoseguro.com.br/pt/omni/` |
| `global_privacy_policy_content` | **morto** | sem rota `/politica-de-privacidade` |

Consentimento de cookies: só `localStorage` (`omnico_cookie_consent`). Não dispara GTM consent mode no código.

### 7.2 Home (`ManageHomePage`, group `home`)

Abas: Banner, Sobre a Omni&Co, Nossas Marcas, O Grupo, Imprensa, Tagline, SEO.

| Bloco | CMS | Entidade extra |
|---|---|---|
| Hero (bg, 2 linhas de título, subtítulo HTML, CTA label) | sim | — |
| Diferenciais: textos, 3 stats, 3 imagens | sim | `hero_cta` não tem URL própria no form (label só) |
| Cabeçalho + CTA da seção Marcas | sim | **cards = `brands`** (`BrandPresenter::forHome`) |
| Manifesto: eyebrow/títulos, vídeo URL ou arquivo, thumb | sim | — |
| Imprensa: títulos + CTA + **2 selects** `home_press_1/2` | sim | `PressClipping`; se os 2 IDs não estão preenchidos, pega os 2 mais recentes |
| Tagline 2 linhas | sim | — |
| SEO title/description/og | sim | — |

Props: `homeSettings` (mapa flat), `brands[]`, `latestClippings[]`.

### 7.3 Quem Somos (`ManageAboutPage`, group `about`)

Abas: Banner, Sobre, Nossas Marcas, Nossos Pilares, Nossa História, Premiações, Depoimento CEO, SEO.

| Bloco | CMS | Notas |
|---|---|---|
| Hero, Sobre (textos + imagem) | sim | |
| Cabeçalho da seção Marcas | sim | cards = `BrandPresenter::forAbout()` |
| 4 pilares (acesso, movimento, segurança, ecossistema): imagem, título, body | sim | keys fixas, não lista |
| História: 14 slots (`about_historia_{year,footnote,text}_{1..14}`) | sim | slots vazios o front ignora |
| Premiações: 9 slots (label, name, text, image) | sim | |
| CEO: foto, quote HTML, nome, cargo | sim | |
| SEO | sim | |

### 7.4 Governança (`ManageGovernancePage`, group `governance`)

| Bloco | CMS | Persistência |
|---|---|---|
| Hero, diretrizes (eyebrow/títulos/body) | sim | settings |
| 4 cards de princípio (imagem, título, body) | sim | settings `governance_principio_*_{1..4}` |
| Diretoria (repeater: nome, cargo, foto, posição da imagem) | sim | tabela `directors` |
| Comitês: textos + imagem do organograma | sim | settings |
| Estrutura societária: textos + chart | sim | settings |
| Documentos: 4 empresas × 4 categorias, repeater de PDF | sim | tabela `policies` |
| SEO | sim | settings |

Categorias de política: Estatutos, Compliance e Ética, Demonstrações Financeiras, Relatório Pilar 3.  
Empresas: Omni, Finsol, Instituto Omni, Omni 1.

No front, `view_url` e `download_url` saem iguais (mesmo arquivo).

Permissão extra: `governanca.documentos.gerenciar` no Resource oculto; a edição real na Manage Page exige `paginas.governanca.editar`.

### 7.5 Investidores (`ManageInvestorsPage`, group `investors`)

| Bloco | CMS | Persistência |
|---|---|---|
| Hero | sim | settings |
| Panorama: textos, 4 cards, 2 fotos, botão + **PDF** | sim | settings |
| Indicadores: títulos, valor da carteira, tabela (repeater JSON), meta, caption do gráfico | sim | `investors_indicadores_rows` e `investors_rating_rows` são **JSON em settings** (não tabela) |
| Rating: textos + tabela JSON + nota | sim | settings |
| Programa Elas: textos, 4 stats, foto, logo | sim | settings |
| Fale com RI: textos + 2 contatos (nome/e-mail) | sim | settings |
| **Gráfico de área (2021–2028)** | **hardcoded** | `features/investors/constants.ts` → `PORTFOLIO_CHART` |
| SEO | sim | settings |

O controller faz `json_decode` das rows; se vazio, usa fallback igual ao constant do front.

### 7.6 Sustentabilidade (`ManageSustainabilityPage`, group `sustainability`)

| Bloco | CMS | Persistência |
|---|---|---|
| Hero | sim | settings |
| Propósito: textos + vídeo URL/arquivo + thumb | sim | settings |
| 4 pilares (título, descrição, foto) | sim | settings `sustainability_pilar_{1..4}_*` |
| Iniciativas: cabeçalho + repeater | sim | tabela `sustainability_projects` |
| Transparência: textos + repeater de docs (title, view_url, download_url) | sim | tabela `sustainability_documents` |
| **Trajetória (timeline 2021–2025)** | **hardcoded** | `TrajetoriaSection.tsx` — a feature **não recebe** settings |
| SEO | sim | settings |

O front ainda tem `constants.ts` com PILARES/PROJECTS/DOCS/TIMELINE de fallback visual; o que prevalece na página são props do controller (exceto trajetória).

### 7.7 Carreiras (`ManageCareersPage`, group `careers`)

| Bloco | CMS | Notas |
|---|---|---|
| Hero + CTA (label/URL Gupy) | sim | default admin `https://omni.gupy.io`; fallback front `https://vemseromni.gupy.io/` (**divergência**) |
| Cultura: textos, 6 valores (foto/título/desc), vídeo, quote CEO | sim | |
| 6 diferenciais | sim | |
| Benefícios: 3 textareas (um item por linha) | sim | |
| 3 depoimentos (foto, nome, cargo, quote) | sim | **não** usa tabela `testimonials` |
| Vagas: textos, body, CTA, URL, imagem | sim | |
| SEO | sim | |

Não há integração Gupy além do link.

### 7.8 Imprensa (`ManagePressPage`, group `press` + table)

Duas camadas no mesmo screen (tabs: banner / noticias / assessoria / seo):

1. Settings da página (hero, assessoria, 2 e-mails, SEO).
2. CRUD de `press_clippings` (título, veículo, data, URL externa, imagem, `is_published`).

Listagem pública: só `is_published = true`, 12 por página, ordem `published_at desc`. Clique = site do veículo (`target=_blank`). Sem página de matéria interna.

Home: 2 clippings escolhidos ou os 2 mais recentes.

### 7.9 Marcas (`BrandResource`) — entidade transversal

Alimenta **quatro superfícies** sem settings por marca:

| Superfície | Campos usados |
|---|---|
| Home carrossel | `name`, `home_image`, `home_description` |
| Quem Somos cards | `name`, `slug`, `about_logo`, `about_photo`, `about_body`, `about_cta_label`, `about_cta_url` |
| Navbar | `name`, `slug` → `/quem-somos#marca-{slug}` se `show_in_nav` |
| Footer marquee | `footer_logo`, `about_cta_url` se `show_in_footer` |

Seed atual (4 marcas): Omni, Finsol, Instituto Omni, Omni1. Reordenação por `sort_order`. Slug não regenera no update (âncoras estáveis).

Copy da **seção** (eyebrow/títulos/CTA da Home e About) continua em settings.

---

## 8. Banco de dados

### 8.1 Tabelas que o site usa de verdade

| Tabela | Função no site público |
|---|---|
| `settings` | Quase todo copy/imagem/SEO das páginas + globais |
| `brands` | Marcas dinâmicas |
| `press_clippings` | Imprensa + destaques Home |
| `directors` | Cards da diretoria |
| `policies` | PDFs de governança |
| `sustainability_projects` | Iniciativas |
| `sustainability_documents` | Relatórios / políticas ESG |
| `users` / `perfis` / `permissoes` / `perfil_permissao` | só admin |
| `activity_logs` | só admin |
| `sessions` / `cache` / `jobs` | infra Laravel |

### 8.2 Tabelas leftover (não alimentam o React)

| Tabela | Situação |
|---|---|
| `pages` | Seed das slugs institucionais; `PageResource` oculto; **nenhum controller lê `Page`** |
| `testimonials` | Resource oculto; Carreiras usa settings `careers_testimonial_*` |
| `contact_messages` | **dropada** (`2026_07_14_000001`) |

### 8.3 Schema resumido das entidades de conteúdo

**brands:** `name`, `slug` unique, `sort_order`, `is_published`, `home_image`, `home_description`, `about_logo`, `about_photo`, `about_body`, `about_cta_label`, `about_cta_url`, `footer_logo`, `show_in_nav`, `show_in_footer`.

**press_clippings:** `title`, `vehicle_name`, `url`, `published_at`, `is_published`, `image`.

**directors:** `name`, `role`, `photo`, `img_position`, `sort_order`.

**policies:** `category`, `company`, `title`, `description`, `date`, `download_url`, `view_url`, `sort_order`.

**sustainability_projects:** `tag`, `body_1`, `photo`, `image_on_left`, `sort_order`.

**sustainability_documents:** `title`, `download_url`, `view_url`, `sort_order`.

---

## 9. Front-end — estrutura e estilo

Espelha a convenção que o omni-site também adotou:

```
resources/js/
  Pages/                 # wrappers Inertia
  features/<feature>/    # Page + sections + data/assets + types/constants/hooks
  Components/            # Seo, CookieBanner, SiteNavbar, ui/*
  Layouts/AppLayout.tsx
  lib/motion/variants.ts
  types/
resources/css/
  app.css                # Tailwind / tokens
  pages/<page>/*.scss    # BEM por seção
  layouts/*.scss
```

Estilo híbrido: tokens Tailwind + SCSS com classes tipo `home-page`, `inv-indicadores__heading`, `sus-trajetoria`. Animações via Framer (`fadeUp`, `stagger`, `VP`).

Assets estáticos em `public/images/`. Uploads CMS em `storage/app/public/{home,about,governance,brands,...}`.

---

## 10. SEO, sitemap, robots, analytics

- Componente `Seo`: title, description, canonical, OG, Twitter Card. Title da página + ` — {siteName}`.
- Cada Manage Page tem aba SEO (`{prefix}_seo_title/description/og_image`).
- `robots.txt` dinâmico: `Disallow: /admin` e `/admin/*`; aponta sitemap.
- Sitemap gerado por `GenerateSitemapJob` (schedule 03:00) em `public/sitemap.xml`. Só as 7 URLs estáticas. Clippings **não** entram (são URLs externas).
- GTM hardcoded no Blade (`GTM-NP3T99VX`). CSP libera Google Tag + YouTube/Vimeo.
- Meta `robots: index, follow` no Blade (não varia por env).

---

## 11. Segurança (o que está no código)

Middleware `SecurityHeaders` em todas as rotas `web`:

- `X-Frame-Options: SAMEORIGIN`
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy` sem camera/mic/geo
- CSP com `'unsafe-inline'`/`'unsafe-eval'` (necessário Vite/Inertia/GTM); Vite/HMR só se `APP_ENV=local` **e** host localhost/`.test`/`.local`

Sessão: driver `database`, lifetime 120. Produção deveria ter `SESSION_ENCRYPT=true` e `SESSION_SECURE_COOKIE=true` (documentado em `docs/tasks` do primo; `.env.example` ainda false).

Filament: CSRF, AuthenticateSession, rate limit nativo do login.

Não há formulário público → sem honeypot/rate-limit de contato.

---

## 12. Jobs, queue, mail

| Job / comando | Quando |
|---|---|
| `GenerateSitemapJob` | diário 03:00 |
| `activity-logs:prune --days=90` | diário |
| `brands:prune-legacy-settings` | comando avulso (migração marcas) |

`.env.example`: `QUEUE_CONNECTION=sync`. Tabela `jobs` existe. O job de sitemap usa `Dispatchable` e o scheduler dispara direto — funciona mesmo em sync.

Mail: SMTP. Usado para convite / reset de senha do admin, não para o site público.

---

## 13. CI/CD e ambientes

| Branch | Workflow | Destino |
|---|---|---|
| `homolog-updated` | lint Pint → migrate/seed → Pest → Vitest → build → **SSH** | homolog |
| PR merge em `master` | lint → Pest → Vitest → build → **FTP** | produção |

PHP 8.2 e Node 20 no GitHub Actions. Secrets FTP (`FTP_HOST/USER/PASSWORD`) e equivalentes SSH no homolog.

Acessos locais (README): site `http://localhost`, admin `http://localhost/admin`. Seed cria `admin@omnico.com.br` / `password` como **root**.

---

## 14. Inventário de settings (contrato CMS ↔ React)

Grupos e prefixos. Keys interpoladas indicam faixa.

### `home`

`hero_bg_image`, `hero_headline_1`, `hero_headline_2`, `hero_subtitle`, `hero_cta_label`  
`diff_eyebrow`, `diff_heading_1`, `diff_heading_2`, `diff_body_1`, `diff_cta_label`, `diff_cta_url`  
`diff_stat1_{number,unit,label,footnote}`, `diff_graph_image`  
`diff_stat2_{number,unit,label}`, `diff_stat2_image`  
`diff_stat3_{number,unit,label}`, `diff_stat3_image`  
`marcas_eyebrow`, `marcas_heading_{pre,bold,normal}`, `marcas_cta_{label,url}`  
`grupo_eyebrow`, `grupo_heading_{pre,bold}`  
`manifesto_video_url`, `manifesto_video_file`, `manifesto_thumbnail`  
`imprensa_eyebrow`, `imprensa_heading_{pre,bold}`, `imprensa_cta_{label,url}`, `home_press_1`, `home_press_2`  
`tagline_1`, `tagline_2`  
`home_seo_{title,description,og_image}`, `home_is_published`

### `about`

`about_hero_{bg_image,headline_warm,headline_white,subtitle}`  
`about_sobre_{eyebrow,heading_normal,heading_gray,heading_bold,body,image}`  
`about_marcas_{eyebrow,heading_pre,heading_bold,heading_pos}`  
`about_pilares_{eyebrow,heading_pre,heading_bold,heading_pos}`  
`about_pilar_{acesso,movimento,seguranca,ecossistema}_{image,title,body}`  
`about_historia_{eyebrow,heading_bold,heading_pos}` + `about_historia_{year,footnote,text}_{1..14}`  
`about_premiacoes_{eyebrow,heading_pre,heading_bold}` + `about_premiacao_{label,name,text,image}_{1..9}`  
`about_ceo_{photo,quote,name,role}`  
`about_seo_{title,description,og_image}`, `about_is_published`

### `governance`

`governance_hero_{bg_image,headline_bold,headline_line1,subtitle}`  
`governance_diretrizes_{eyebrow,heading_pre,heading_bold,body}`  
`governance_principio_{image,title,body}_{1..4}`  
`governance_lideranca_{eyebrow,heading_pre,heading_bold}`  
`governance_comites_{eyebrow,heading_pre,heading_bold}`  
`governance_estrutura_body_col_{1,2}`, `governance_estrutura_chart_image`  
`governance_societaria_{eyebrow,heading_pre,heading_bold,body,chart_image}`  
`governance_documentos_{eyebrow,heading_pre,heading_bold}`  
`governance_seo_{title,description,og_image}`, `governance_is_published`

### `investors`

Prefixo `investors_*`: hero, panorama (cards 1–4, fotos, PDF), indicadores (incl. JSON `investors_indicadores_rows`), rating (JSON `investors_rating_rows`), elas (4 stats + foto/logo), RI (2 contatos), SEO, `investors_is_published`.

### `sustainability`

Prefixo `sustainability_*`: hero, proposito (vídeo), pilares 1–4, iniciativas (cabeçalho; itens na tabela), transparencia, SEO, `sustainability_is_published`.

### `careers`

Prefixo `careers_*`: hero (CTA Gupy), cultura (vídeo + valores 1–6 + quote), diferenciais 1–6, benefícios 3 colunas, testimonials 1–3, vagas, SEO, `careers_is_published`.

### `press`

`press_hero_{bg_image,eyebrow,heading_pre,heading_bold,cta_label}`  
`press_assessoria_{eyebrow,heading_pre,heading_bold,body}`  
`press_contact_{1,2}_{label,email}`  
`press_seo_{title,description,og_image}`, `press_is_published`

### `global`

Ver §7.1.

---

## 15. Fluxo de dados (resumo visual)

```
                    ┌─────────────────────────────────────┐
                    │           Filament /admin           │
                    │  ManageHomePage, About, ...         │
                    │  BrandResource  /  ManagePressPage  │
                    └──────────────┬──────────────────────┘
                                   │ save()
              ┌────────────────────┼────────────────────┐
              ▼                    ▼                    ▼
        settings            brands / press /      activity_logs
        (key/value)         directors / policies
                            projects / documents
              │                    │
              └─────────┬──────────┘
                        ▼
              Controllers (GET)
              + HandleInertiaRequests (share)
                        │
                        ▼  Inertia::render
              Pages/*.tsx → features/*Page.tsx
                        │
                        ▼
              AppLayout (siteSettings, layoutBrands)
              + seções da página
```

Nenhuma mutation Inertia (`useForm` / `router.post`) no site público.

---

## 16. O que está hardcoded ou incompleto (importante para o omni-site)

1. **Trajetória ESG** — não passa pelo CMS.
2. **Gráfico de carteira (RI)** — `PORTFOLIO_CHART` no TS.
3. **`{page}_is_published`** — não tira página do ar.
4. **Política de privacidade** — conteúdo no CMS, rota inexistente; banner aponta para `/politica-de-privacidade` (404).
5. **Canal de denúncias** — URL no CMS ignorada; footer hardcode Contato Seguro.
6. **Dados cadastrais da empresa** (`global_company_*`) — não aparecem no site.
7. **Tabela `pages` / `testimonials`** — dead code de template.
8. **PressClippingFallback** — mascara falha de `intl` em homolog com notícias fake.
9. **URLs Gupy divergentes** — admin default `omni.gupy.io` vs front `vemseromni.gupy.io`.
10. **Navbar hashes** — estáticos; se o CMS mudar a ordem das seções, o menu não muda.
11. **Footer logos fallback** — se a marca não tiver `footer_logo`, ainda há mapa slug → PNG antigo no `AppLayout`.
12. **CLAUDE.md do primo** — desatualizado (stack, 3 clippings na Home, shadcn, queue `database`).

---

## 17. Testes (cobertura do primo)

Pest (feature): rotas 200, 404, robots, marcas na Home/About, BrandResource, permissões, usuários, upload de banner, activity log, senha, settings legados.  
Unit: `UploadLimit`, mensagens de FileUpload.  
Vitest configurado (`passWithNoTests`).

Não há testes E2E das seções visuais.

---

## 18. Implicações para o omni-site

O OmniCo é o **irmão institucional**: mesmo stack (Laravel + Inertia + React + Filament + settings key/value + features/), outro produto e outro CMS.

Padrões reaproveitáveis:

- `Manage*Page` por página (não `Page` genérica com blob HTML)
- `settings.group` = página
- Entidade só quando o conteúdo é lista reutilizada (marcas, clippings, docs)
- Props Inertia tipadas na feature; `Pages/` fino
- Shared props para layout (`siteSettings`, `layoutBrands`)
- Uploads com preview + substituição sem apagar no save vazio
- Permissões granulares por página + root

O que **não** copiar cegamente:

- Flag de publicação que não publica
- Settings mortos
- Fallback de clippings
- Conteúdo hardcoded ao lado de CMS (gráfico, trajetória)
- GTM e URLs de terceiros no Blade sem env

---

## 19. Arquivos-chave para consulta rápida

| Assunto | Path no OmniCo |
|---|---|
| Rotas públicas | `routes/web.php` |
| Props Inertia shared | `app/Http/Middleware/HandleInertiaRequests.php` |
| Home / estáticas / imprensa | `app/Http/Controllers/{Home,StaticPage,Press}Controller.php` |
| Painel | `app/Providers/Filament/AdminPanelProvider.php` |
| CMS páginas | `app/Filament/Pages/Manage*Page.php` |
| Marcas | `app/Filament/Resources/BrandResource.php`, `app/Support/BrandPresenter.php` |
| Permissões | `app/Models/Permissao.php` |
| Layout + nav | `resources/js/Layouts/AppLayout.tsx`, `resources/js/Components/SiteNavbar.tsx` |
| Features | `resources/js/features/{home,about,governance,investors,sustainability,careers,press}/` |
| Spec marcas (já existia) | `docs/spec-marcas-dinamicas.md` |
| Segurança | `docs/revisao_seguranca_owasp.md`, `docs/tasks/` |

---

*Fim do levantamento. Código de origem: OmniCo-Site, branch `homolog-updated`.*
