# EPIC.md — Plano de Performance (Real) · Portais v5/v6 · Laravel 5.5 / PHP 7.2 / Apache 2.4 / cPanel

> **Escopo deste documento:** Melhoria de performance do *projeto* (esta aplicação Laravel 5.5.50 /
> PHP 7.2.34 em bare-metal cPanel/AlmaLinux), organizada em **camadas** — da menos invasiva
> (configuração de servidor) à mais agressiva (compressão de HTML, digestor de imagens, refatoração
> de CSS). Cada camada é fechável por medição no canário `infra-update.devs.maxima.inf.br`.
>
> **Base de descoberta:** `_pesquisa/EPIC_BASE.md`, `_pesquisa/BASELINE.md`, `_pesquisa/DESCOBERTAS.md`.
> **Restrição central de stack:** PHP 7.2 / Laravel 5.5 / Apache 2.4 (EasyApache4) / WHM‑cPanel — ambos
> EOL; **não** atualizar framework/PHP. Respeitar: **nunca** usar `<IfModule mod_headers.c>` no
> `.htaccess` (causa HTTP 500 neste servidor — ver `_pesquisa/security/htaccess.txt`).

---

## 0. Ponto de partida (baseline `2026-09-08-12-10`, 62 domínios)

| Aspecto | Baseline | Alvo (tarefa #23410) | Camada que ataca |
|---|---|---|---|
| TTFB HTML | 1721 ms (3,2% < 800) | **< 800 ms** (A1) | L0, L1, L2, L3 |
| LCP | 3507 ms (17,7% ≤ 2500) | **< 2500 ms** (A1) | L4, L5, L6, L7 |
| FCP | 2948 ms | melhorar (A1) | L4, L6, L7 |
| Origem (processamento) | 1594 ms (**92% do TTFB**) | reduzir (A1) | L0, L2, L3 |
| `cache_policy` | 16/100 | ≥ 80 (A1/A2) | L0, L1 |
| `cache_hit` | **0,0** | **> 90%** (A2) | L0, L1, L2 |
| `revalidation` (ETag/304) | **0,0** | > 0 (A2) | L0 |
| `compression` | 0,34 (34% nem gzip) | 1,0 (A1) | L0 |
| `transfer` | 6,89 MB (62,9% > 5 MB) | < 5 MB (A1) | L0, L4, L5, L6 |
| `requests` | 77,7 / página | reduzir (A1) | L4, L5 |
| CLS / TBT | 0,018 / 12,7 ms | ✅ já ok | — |
| Disponibilidade / redirects | 100% / 0 | manter (A3, guardrail) | L0 (não regredir) |

**Exemplo de sucesso já observado:** `teixeira.pb.gov.br` (brotli + `max-age` estático) → TTFB 211 ms, LCP 787 ms.
**Prova de conceito já medida (DESCOBERTAS.md, estagio‑1):** ligar gzip no servidor → `transfer` **−62,1%**, estáticos **−10,5%**, página **−8,7%**.

---

## 1. Premissas e restrições de stack (valem para todas as camadas)

1. **PHP 7.2.34 / Laravel 5.5.50** — soluções devem funcionar sem upgrade. ⚠️ **Laravel 5.5 NÃO tem
   `artisan view:cache`** (comando adicionado só na 5.6). Usar `config:cache` + `route:cache` + `optimize`.
2. **Apache 2.4 / EasyApache4 + cPanel** — `.htaccess` da **raiz da conta é gerado pelo cPanel: não editar**.
   Toda diretiva de performance vai em **`public/.htaccess`** (a pasta pública do domínio).
3. **`<IfModule mod_headers.c>` causa 500** neste servidor → diretivas `Header` ficam "cruas". `mod_expires`
   e `mod_rewrite` podem continuar com seus wrappers (`<IfModule mod_expires.c>` é seguro).
4. **OPcache já está habilitado** no `php.ini` de produção (`opcache.enable=1`, 128M, max 16229 arquivos,
   `validate_timestamps=1`). Ajustes em L0.1.
5. **Veículo de medição:** canário `infra-update.devs.maxima.inf.br` (cópia fiel de produção). Aplicar a
   melhoria no canário e medir com o harness idempotente (auto‑cria `run-01`, `run-02`… e compara vs baseline):
   ```bash
   ETAPA=01-cache-servidor DESCRICAO="OPcache+deflate+cache-control estaticos" \
     bash runners/medir_estrutura.sh infra-update.devs.maxima.inf.br
   python3 ../comparar/comparar.py measuring/results/2026-09-08-12-10 measuring/results/<nova-data>
   ```
6. **Não regredir** rede/segurança (HTTP/2, TLS 1.3, redirects=0, disponibilidade=100%, headers de
   segurança do template `_pesquisa/security/htaccess.txt`).

---

## 2. Camadas (ordem de execução sugerida: L0 → L7)

```
L0  Servidor / Runtime .................. só config (.htaccess / php.ini / opcache)  [menor risco]
L1  Headers de resposta / Cache de borda  app (middleware) + Apache                  [resolve cache_hit=0]
L2  Cache de aplicação Laravel .......... deploy + runtime (config/route/optimize/FPC)
L3  Origem / Código ..................... profiling, N+1, middleware, autoload
L4  Assets front-end .................... build Mix, bundling, defer, fontes
L5  Mídia / "digestor de imagens" ....... WebP/AVIF, resize, responsivo, lazy        [agressiva]
L6  Compressão de HTML / agressiva ...... minify HTML, critical CSS, preload
L7  Refazer CSS ......................... consolidar, purge, unificar modelos        [mais invasiva]
```

---

### L0 — Servidor / Runtime (menos invasiva; só configuração)

**L0.1 · OPcache (ajuste fino em produção)** — *Critério A1 (origem/TTFB)* · *Esforço: baixo · Risco: baixo*
> ⚠️ **Correção de escopo (definida em `2026-08-09`):** as chaves `opcache.*` e `realpath_cache_*`
> são **`PHP_INI_SYSTEM`** → o `.user.ini`/MultiPHP INI da **conta** é **ignorado silenciosamente**.
> Ajuste deve ser feito no **WHM** (MultiPHP INI Editor › System, perfil `ea-php72`) ou direto no
> php.ini de sistema `/opt/cpanel/ea-php72/root/etc/php.ini`. Script pronto em
> `wiki/infra-update/WHM.md` (P0‑B). A conta NÃO consegue alterar essas chaves.
- No **WHM** (não na conta), definir para produção:
  ```ini
  opcache.enable=1
  opcache.memory_consumption=256
  opcache.interned_strings_buffer=32
  opcache.max_accelerated_files=10000   ; sobe p/ aguentar vendor+L5.5
  opcache.revalidate_freq=60
  opcache.validate_timestamps=0         ; em PRODUÇÃO: 0 (não checa mtime a cada req)
  opcache.save_comments=1
  realpath_cache_size=4096K
  realpath_cache_ttl=600
  ```
- `validate_timestamps=0` exige **reset de opcache no deploy** (`opcache_reset()` ou `touch` no
  `public/index.php`, ou `service php-fpm reload`). Sem isso, mudanças de código não refletem.
  ➜ isto continua controlável via app/deploy (ver L2.1).
- **Validação:** `php -i | grep opcache` (no servidor, como root) + `comparar.py` (espera queda de `origem`/`ttfb`).

**L0.2 · Compressão universal (gzip + brotli)** — *Critério A1 (transfer/TTFB) · estagio‑1 já provou −62% transfer*
> ⚠️ **Correção de escopo (definida em `2026-08-09`):** a compressão **NÃO** é configurável via
> `public/.htaccess` neste cPanel — o `AddOutputFilterByType DEFLATE` da conta é **sobreposto pelo
> vhost do EasyApache4** e comprovadamente ineficaz (baseline `compression=0,34`; ligar no servidor
> foi a única via que funcionou). Logo: **a compressão é SERVER‑SIDE**, via flag cPanel, e NÃO via
> `.htaccess`. O bloco `mod_deflate`/`mod_brotli` do `.htaccess` foi **removido** (veja `public/.htaccess`).
- **Mecanismo correto (WHM/cPanel):**
  1. **gzip (obrigatório):** cPanel › **Optimize Website** (ou WHM › Apache Configuration) → habilita
     `mod_deflate` global para todos os tipos de texto. É o caminho validado (−62% transfer no estagio‑1).
  2. **brotli (bônus, não bloqueia):** WHM › EasyApache4 › Customize › habilitar módulo **`mod_brotli`**;
     depois `AddOutputFilterByType BROTLI_COMPRESS ...` no include do vhost.
  - Passo a passo em `wiki/infra-update/WHM.md` (P0‑A) — feito manualmente no WHM/cPanel.
- **Validação:** `curl -I --compressed` mostra `Content-Encoding: gzip|brotli`; `comparar.py` → `compression` 1.0 e `transfer` cai.

**L0.3 · `Cache-Control` de estáticos + `immutable`** — *Critérios A1/A2 (cache_policy, cache_hit)*
- O template de segurança já tem `[H9]` `mod_expires` (1 ano), mas **não emite `Cache-Control:
  public, max-age=31536000, immutable`** (exigido pela tarefa). Complementar com header explícito
  para os assets versionados pelo Mix (`mix().version()` garante hash no nome → `immutable` é seguro):
  ```apache
  <IfModule mod_expires.c>
      ExpiresActive On
      ExpiresByType text/css "access plus 1 year"
      ExpiresByType application/javascript "access plus 1 year"
      ExpiresByType text/javascript "access plus 1 year"
      ExpiresByType image/jpeg "access plus 1 year"
      ExpiresByType image/png "access plus 1 year"
      ExpiresByType image/webp "access plus 1 year"
      ExpiresByType image/avif "access plus 1 year"
      ExpiresByType image/svg+xml "access plus 1 year"
      ExpiresByType image/gif "access plus 1 year"
      ExpiresByType font/woff2 "access plus 1 year"
      ExpiresByType font/woff "access plus 1 year"
      ExpiresByType font/ttf "access plus 1 year"
      ExpiresByType font/otf "access plus 1 year"
  </IfModule>
  # Cache-Control explícito + immutable para assets versionados
  <FilesMatch "\.(css|js|jpg|jpeg|png|webp|avif|gif|svg|woff2?|ttf|otf|ico)$">
      Header set Cache-Control "public, max-age=31536000, immutable"
  </FilesMatch>
  ```
- **Validação:** `curl -I` em `/assets/css/...` → `Cache-Control: public, max-age=31536000, immutable`;
  `comparar.py` → `cache_policy` ≥ 80, `cache_hit` sobe (estáticos servidos em cache pela borda).

**L0.4 · Revalidação condicional (ETag / 304)** — *Critério A2 (revalidation)*
- Apache manda `ETag` por padrão, mas `mod_deflate` costuma removê-lo. Garantir:
  ```apache
  FileETag MTime Size
  ```
- Para estáticos, basta não desabilitar `FileETag` e não usar `Header unset ETag`. Para respostas
  Laravel, ver L1.2. **Validação:** request com `If-None-Match` retorna `304`; `revalidation` > 0.

> **L0 como um todo** é o "estagio‑1 + cache de estáticos" e deve ser o **primeiro PR**. Atenção ao
> escopo de onde cada item roda (descoberto em `2026-08-09`):
> - **Conta / `public/.htaccess`** (baixo risco, versionável): L0.3 (`Cache-Control`+`immutable`) e L0.4 (`FileETag`). Funcionam.
> - **Servidor / WHM (root)** (precisa de acesso WHM): L0.2 (flag cPanel *Optimize Website* + `mod_brotli`) e L0.1 (OPcache — `opcache.*` é `PHP_INI_SYSTEM`, `.user.ini` da conta é no‑op). Passo a passo: `wiki/infra-update/WHM.md`.
> Alto impacto em `transfer`/`cache_policy`/`cache_hit`/`origem`.
>
> **Status de L0 (2026-09-09):** ✅ **Concluído no servidor** — OPcache (L0.1) e compressão
> gzip/Optimize Website (L0.2) aplicados via WHM; `public/.htaccess` com L0.3/L0.4 já implantado
> e sincronizado com o template de segurança. ⚠️ **APCu (P0‑C, extra/opcional): NÃO aplicado** —
> a extensão `ea-php72-php-apcu` não está disponível no repo EA4 deste servidor (`yum
> --enablerepo=EA4 install ea-php72-php-apcu` falhou). Como é opcional e o `apcu-autoloader` do
> `composer.json` degrada graciosamente (warning, sem quebra), foi **pulado**. Próximo passo:
> medir o canário (P0) e partir para **L1** (middleware `CacheControl` → resolve `cache_hit=0`).

---

### L1 — Headers de resposta / Cache de borda (app + Apache)  · *resolve `cache_hit = 0`*

**L1.1 · Middleware `CacheControl` para rotas públicas** — *Critérios A1/A2* · *Esforço: médio · Risco: médio*
- **Causa raiz confirmada:** o grupo `web` (`EncryptCookies`, `StartSession`, `ShareErrorsFromSession`,
  `VerifyCsrfToken` em `app/Http/Kernel.php`) injeta `Cache-Control: no-cache, private` em **toda**
  resposta → explica `cache_policy=16` e `cache_hit=0`.
- Criar `app/Http/Middleware/CacheControl.php` que **sobrepõe** esse header em rotas públicas
  (roda *depois* do grupo `web`, então vence o `no-cache`):
  ```php
  namespace App\Http\Middleware;
  use Closure;
  class CacheControl {
      public function handle($request, Closure $next, $seconds = 300) {
          $response = $next($request);
          // só GET público, sem conteúdo por-usuário e sem erro
          if ($request->method() !== 'GET' || $response->isRedirection()
              || $response->getStatusCode() >= 400) {
              return $response;
          }
          $response->headers->set(
              'Cache-Control',
              "public, max-age={$seconds}, stale-while-revalidate=86400, stale-if-error=604800"
          );
          $response->headers->remove('pragma');
          $response->headers->remove('expires');
          $response->headers->set('Vary', 'Accept-Encoding'); // compressão não varia por usuário
          return $response;
      }
  }
  ```
- Registrar em `Kernel.php` (`$routeMiddleware['cache.public']`) e aplicar **apenas** às rotas
  públicas do portal (landing, notícias, serviços, transparência) — **nunca** em rotas autenticadas/
  admin. Para HTML dinâmico de portal, `stale-while-revalidate` permite que a borda sirva conteúdo
  obsoleto enquanto revalida (LDL baixo). Para landing 100% estática, usar `max-age` maior (ex. 3600).
- **Validação:** `curl -I` na home do portal → `Cache-Control: public, max-age=..., stale-while-revalidate`;
  `comparar.py` → `cache_hit` sobe e `ttfb` cai (borda serve em cache).
- **Implementado (mínimo/seguro):** `wiki/infra-update/correcao-cache-control-borda-l1.md` — middleware
  `CacheControl` que remove `Set-Cookie` nas GET de conteúdo (sem refactor de grupo de rota, para não
  arriscar CSRF/admin). Conjunto com L3.3.
- **⚠️ GATE DE SEGURANÇA L1.0 (2026-09-09):** todo layout público do portal embute `<meta name="csrf-token">`
  + hidden `_token` (ex.: `resources/views/portal/layouts/modelo1/layout/head.blade.php:15`). Como o
  middleware remove o cookie de sessão, cada GET anônimo abre nova sessão → o HTML **varia por request**.
  Consequência: (1) `ETag = md5(corpo)` varia → `304` nunca dispara; (2) se a borda cacheasse esse HTML
  `public`, serviria o token de uma sessão a outra → **CSRF 419 no submit**. Por isso o `CacheControl`
  inspeciona o corpo: se há `csrf-token`/`_token`, responde `private, no-cache` e **mantém o cookie**;
  só páginas sem CSRF recebem `public` + ETag. Na prática, como TODAS as páginas do portal têm o meta,
  **nenhuma é cacheada hoje** — L1.1 entrega zero cache até o desacoplamento do CSRF. Pré-requisito
  registrado em `public_infra_update-c556` (L1.0 — injetar `_token` client-side no submit).

**L1.2 · ETag condicional em respostas Laravel (opcional)** — *Critério A2*
- Para páginas públicas cacheáveis, adicionar `Last-Modified`/`ETag` e responder `304` em revalidação.
  Pode ser feito no mesmo `CacheControl` ou num middleware dedicado; cuidado com conteúdo que muda
  por request. **Validação:** `revalidation` > 0.
- **Implementado (2026-09-09):** no próprio `app/Http/Middleware/CacheControl.php` (junto do L1.1).
  Após setar `Cache-Control`, calcula `ETag = md5(conteúdo)` e emite o header; se o request vier com
  `If-None-Match` batendo (ignora weak `W/`, aceita `*`), responde `304` sem corpo. Só incide no GET
  público já filtrado **e sem token CSRF** (corpo estável) → o `304` funciona de verdade. Em páginas com
  CSRF o ETag varia e o `304` não dispara (e o middleware nem emite ETag nessas — vê gate L1.0 acima).
  **Validação:** `curl -H 'If-None-Match: "<etag>"' -D - -o /dev/null` → `304` (só em páginas sem CSRF,
  após `public_infra_update-c556`).

**L1.3 · Purge / invalidação (conecta com A4)** — *deferido*
- O `mix().version()` já gera hash no nome dos assets → mudar CSS/JS = nova URL = cache da borda
  automaticamente invalidado (não precisa purge para estáticos). Para HTML cacheado em L1.1/L2.3,
  prever hook de purge (ver L2.3) que dispara o `purge` do Multi‑CDN na publicação de conteúdo.
- **Status (2026-09-09): DEFERIDO** — depende de A4 (API de purge do Multi‑CDN), fora do app. O HTML
  de L1.1 tem `s-maxage=300` + `stale-while-revalidate=86400`, então conteúdo publicado fica obsoleto
  ≤ ~300 s na borda (aceitável p/ portal). Quando o CDN expuser API, adiciona‑se um listener em evento
  de publicação que chama o purge. Registrado como dívida de A4.

---

### L2 — Cache de aplicação Laravel (deploy + runtime)  · *Critérios A1/A2*

**L2.1 · Ativar caches de bootstrap no deploy** — *A1 (origem/TTFB)* · *Esforço: baixo · Risco: baixo*
- **Achado:** `deploy.sh` **não** roda `config:cache`/`route:cache`/`optimize` → cada request reparsa
  config + resolves rotas + boota container. Para L5.5 isso é um miss grande.
- Adicionar ao `deploy.sh` (após `storage:link`, com `.env` já presente, **antes** de servir):
  ```bash
  php artisan config:cache
  php artisan route:cache
  php artisan optimize          # L5.5: cache de services/packages (não existe view:cache em 5.5)
  php artisan opcache:clear 2>/dev/null || true   # se ext opcache estiver presente
  ```
- ⚠️ `config:cache` exige que **nenhum** `env()` seja chamado fora de config/* (já validar no app).
  ⚠️ Após `config:cache`, mudar `.env` exige rodar `config:cache` de novo.
- **Validação:** `bootstrap/cache/config.php` e `routes.php` existem; `comparar.py` → queda de `origem`/`ttfb`.
- **Implementado:** `wiki/infra-update/correcao-bootstrap-cache-l2.md` — `routes/basepro.php` teve 6
  Closures removidas (habilita `route:cache`); `env()` em `app/` (InformationController, ActivationController)
  migrado para `config()` (novo `config/gemini.php`) desbloqueando `config:cache`; `deploy.sh` roda
  `config:cache`+`route:cache`+`optimize`+`opcache:clear`.

**L2.1.1 · Refatorar rotas para habilitar `route:cache`** — *A1 (origem/TTFB)* · *Esforço: baixo · Risco: baixo*
- **Por que é necessário:** `php artisan route:cache` **serializa todas as rotas e falha se qualquer
  rota usar Closure como ação**. No container o comando para com:
  `Unable to prepare route [cms] for serialization. Uses Closure.`
- **Inventário (todos em `routes/basepro.php`)** — 6 rotas bloqueadoras:

  | Linha | Tipo | Correção |
  |---|---|---|
  | 36 | ação inline `function(){ return redirect()->route('dashboard.index'); }` (grupo `cms`) | mover para controller (`Admin\MainController@cmsHome` retornando `redirect()->route('dashboard.index')`), ou `Route::redirect()` com URL literal |
  | 16 | 3º arg Closure `function(){ return abort(404); }` (after) | **remover o 3º argumento** — o `abort(404)` sobrescrevia o logout (era bug); ficar `Route::get('logout', 'Admin\LoginController@logout')` |
  | 19 | 3º arg Closure `{}` vazio + `->name('activate')` | remover 3º arg, **manter** `->name('activate')` |
  | 50 | 3º arg Closure `{}` vazio + `->name('usuario.addRoles')` | remover 3º arg, manter nome |
  | 52 | 3º arg Closure `{}` vazio + `->name('usuario.addRoles')` | remover 3º arg, manter nome |
  | 104 | 3º arg Closure `{}` vazio + `->name('acidade.getByState')` | remover 3º arg, manter nome |

- **Regra geral (cacheável em L5.5):** view simples → `Route::view($uri, 'view')`; redirect →
  controller ou `Route::redirect()`; **nunca** Closure como 2º arg nem como 3º arg "after". Lógica de
  pós‑processamento vai para o próprio controller (ele já pode retornar o que o closure fazia).
- ⚠️ `route:cache` é **all‑or‑nothing**: basta 1 Closure para falhar tudo. Após a refatoração, rodar
  `php artisan route:cache` no container/staging e repetir até `bootstrap/cache/routes.php` ser gerado;
  cada erro aponta a próxima rota a corrigir.
- ⚠️ **Não** manter `route:cache` em desenvolvimento (quebra alterações rápidas de rota); cachear só no
  deploy de produção (L2.1). Em dev, usar `php artisan route:clear`. Observação relacionada: os grupos
  usam `'before' => 'csrf'` (sintaxe L4, ignorada em 5.5) — CSRF já vem do grupo `web` do Kernel; migrar
  para `middleware(['csrf'])` só por clareza, não é bloqueador de cache.
- **Validação:** `bootstrap/cache/routes.php` existe; `artisan route:list` idêntico ao de pré‑cache;
  `comparar.py` → queda de `origem`/`ttfb` (resolução de rotas deixa de ocorrer por request).

**L2.2 · Driver de cache/route** — *A1*
- `CACHE_DRIVER=file` e `SESSION_DRIVER=file` (atuais). Em cPanel, Redis geralmente **não** disponível;
  manter `file` é aceitável. Se houver Redis, migrar `CACHE_DRIVER=redis` para cache de consultas/
  fragmentos. Sessão de visitante anônimo em página pública pode ser dispensada (ver L3.3).

**L2.3 · Full‑Page Cache (FPC) para rotas públicas** — *A1/A2/A4* · *Esforço: médio · Risco: médio*
- Para páginas públicas que hoje geram HTML por request (origem ~600 ms no canário), introduzir cache
  de página com TTL + invalidação por tag. Opções compatíveis com L5.5:
  - Middleware de resposta que grava o HTML em `Cache` (file/redis) por URL, servindo direto em hits
    subsequentes; ou
  - Pacote de FPC compatível com L5.5 (ex. `spatie/laravel-responsecache` — verificar compatibilidade
    L5.5/ PHP 7.2 antes).
- **Invalidação:** hook em eventos de publicação de conteúdo que limpa a URL (e dispara purge do CDN —
  conecta A4). **Validação:** `cache_hit` alto em HTML, `ttfb` < 800 em páginas públicas.
- ✅ **Implementado (2026-09-10)** — `app/Http/Middleware/PageCache.php` (1º do grupo `web`) grava o
  HTML renderizado por URL no store de Cache; invalidação por evento de modelo em `AppServiceProvider`
  + comando `cache:page:clear`; cobre **home E todas as rotas `Portal\*` (não-home)** igualmente.
  Doc: `wiki/infra-update/full-page-cache-l2-3.md`.

**L2.4 · Cache de consultas / eager loading (N+1)** — *A1* · *Esforço: médio*
- Investigar páginas públicas (PortalController, notícias, galerias) com profiling; aplicar
  `with()/load()` para eliminar N+1; cachear resultados de menu/categorias em `Cache::remember`.
  **Validação:** redução de tempo de DB reflete em `origem`/`ttfb`.

---

### L3 — Origem / Código (Laravel)  · *Critério A1*

**L3.1 · Autoload otimizado** — ✅ já feito (mover `app/Helpers/functions.php` para `autoload.files`).
**L3.2 · Debugbar fora de produção** — ✅ já feito (`class_exists` em `AppServiceProvider`).
**L3.3 · Reduzir middleware/sessão em rotas públicas** — *Esforço: médio · Risco: baixo*
- Páginas 100% públicas podem usar um grupo de rotas **sem** `StartSession`/`VerifyCsrfToken`
  (só `web` mínimo), eliminando a causa do `no-cache, private` na origem e o overhead de sessão.
  (Complementa L1.1.) **Validação:** `origem`/`ttfb` caem; `cache_policy` sobe.
  **Implementado junto com L1.1** (ver `wiki/infra-update/correcao-cache-control-borda-l1.md`): o
  `cache.public` remove o `Set-Cookie` nas GET públicas, dispensando o refactor de grupo `web.public`.
**L3.4 · N+1 / queries lentas / índices** — ver L2.4.
**L3.5 · `config.json` em cache** — o carregamento de `config.json` (app/mail/recaptcha) é feito em
  `config/*`; com `config:cache` (L2.1) ele é resolvido 1x. Garantir fallback `env()` (já corrigido).

---

### L4 — Assets front-end (build + entrega)  · *Critérios A1 (LCP/FCP/requests)*

**L4.1 · Build de produção no deploy** — *Esforço: baixo · Risco: baixo*
- **Achado:** `webpack.mix.js` compila `resources/assets/sass/app.scss` → `public/css/app.css` e copia
  JS, com `.version()` (hash + `mix-manifest.json`). Mas **`public/assets/css/app.css` está com 0 bytes**
  neste checkout → o build **não está rodando no deploy**.
- Adicionar ao `deploy.sh`:
  ```bash
  npm ci && npm run production     # gera app.css/app.js minificados + versionados
  ```
- Isso entrega CSS/JS **minificados** e com **hash no nome** (habilita `immutable` de L0.3 sem
  invalidação quebrada). **Validação:** `app.css` > 0 e minificado; `transfer`/`requests` caem.

**L4.2 · Vendorizar externos + `defer`/`async` (sem agrupar código)** — *Esforço: médio · Risco: baixo*
> **Diretivas do usuário (2026‑09‑10, re‑leitura do epic):** (1) **NÃO** editar/agrupar código em
> arquivos CSS/JS para formar bundles — instável; (2) priorizar `defer`/`async` nos parâmetros de
> importação **sem mexer nas posições**; (3) **vendorizar tudo** que é externo (JS, CSS, fontes) →
> **única fonte da verdade** no projeto (`public/assets/vendor/`).
- **Vendorizado (single source of truth) em `public/assets/vendor/`:** Google Fonts (Rubik, Nunito
  Sans, Source Sans Pro, Encode Sans Expanded, Lato — css + 73 woff2 em `fonts/google/`), popper
  (unificado p/ o copy local `js/modulos/popper.min.js`, igual ao usado em modelo1/2/6), Font
  Awesome 5.15.4 (`fontawesome/css/all.min.css` + `webfonts/`), Sortable 1.15.0, angular‑material
  1.0.0, Material Icons, Bootstrap 3.3.6 + glyphicons (admin forms), select2 4.0.3, Chart.js 2.7.0.
  Todos os layouts (default/master, modelo1‑6/head, newsbit/head, vendor/adminlte/master) e
  componentes (inputs/icon, inputs/archive, prefecture contents forms, changelog, acesso‑rapido)
  trocados de CDN → `asset('assets/vendor/...')`. Remove ~9 domínios externos de sub‑recurso.
- **`defer`/`async` (sem reposicionar):** `async` aplicado ao recaptcha (contact/ouvidoria). Os
  demais scripts **não** recebem `defer`/`async` porque têm inicialização inline **síncrona**
  dependente (bloco jQuery no master, `new Sortable(...)`, init de charts/select2/Chart.js, widget
  vlibras) — aplicar ali quebraria a página. Mantido como limitação consciente.
- **Externos mantidos (justificável):** `www.gstatic.com/charts/loader.js` (relatório‑estatístico +
  admin dashboard — lazy‑carrega o resto do gstatic, não vendorizável de forma limpa),
  `vlibras.gov.br` (widget de acessibilidade hospedado), `recaptcha` (precisa da site‑key),
  `pro.fontawesome.com` FA‑Pro (licenciado, em `banner-modal`). swagger/welcome são dev‑only.
- **Validação:** `requests` externos caem; sniffCSS no canário confirma `font-family` renderizada
  inalterada (Rubik em home) e ausência de requisições a `fonts.googleapis`/`cdnjs`/`maxcdn`.

**L4.3 · `defer`/`async` + mover JS para footer** — *A1 (FCP)*
- Só **69** de centenas de `<script>` usam `defer`. Adicionar `defer` a scripts não‑críticos e
  carregar JS no fim do body; scripts críticos via `preload`. **Validação:** `fcp` melhora.

**L4.4 · Fontes e 3rd‑party** — *A1*
- `font-display: swap` nos `@font-face` (ou no build); `<link rel="preconnect">` para
  `https://www.gstatic.com` (charts loader) e demais domínios externos. **Validação:** `fcp`/`lcp`.

---

### L5 — Mídia / "digestor de imagens"  · *mais agressiva · Critérios A1 (LCP/transfer)*

**L5.0 · Estado atual do digestor (já existe!)**
- `app/Http/Traits/FileTrait::compress()` roda **no upload** (`UploadFile.php:83`, `ContentController`
  em 10+ pontos) e gera variantes `_min`(200), `_med`(380), `_larg`(1480) + normal(500) a qualidade 90,
  depois `extractedCompress()` (resmush). `app/Console/Commands/CompressImages.php` (`make:compress`)
  faz o backlog.
- **Defeitos a corrigir:** (1) **não gera WebP/AVIF** — o `filter()` exclui `webp` mas o `compress()`
  só redimensiona; (2) qualidade 90 alta; (3) originais grandes passam (ex. `modelo6.jpg` = 3,7 MB);
  (4) `make:compress` é manual (não agendado).

**L5.1 · Gerar WebP/AVIF no digestor** — *Esforço: médio · Risco: baixo*
- Em `FileTrait::compress()`, após `->save()`, adicionar encode WebP (e AVIF se suportado) usando
  Intervention (`->encode('webp', 82)`) e/ou `rosell-dk/webp-convert` (já no composer):
  ```php
  Image::make($src_path)->resize($wWith, null, function ($c) { $c->aspectRatio(); })
       ->save($destinatio_path, 82);
  // variante webp
  $webp = preg_replace('/\.\w+$/', '.webp', $destinatio_path);
  Image::make($src_path)->resize($wWith, null, function ($c) { $c->aspectRatio(); })
       ->encode('webp', 82)->save($webp);
  ```
- Abrir `make:compress` para também produzir WebP no backlog. **Validação:** novas imagens têm `.webp`;
  `transfer` cai.

**L5.2 · Automatizar (upload + cron)** — *Esforço: baixo*
- Upload já chama `compress()` (ok). Agendar `php artisan make:compress` via cron (cPanel) para o
  backlog e para novos arquivos grandes. **Validação:** `storage/app/public` encolhe.

**L5.3 · Servir responsivo (`srcset` + `<picture>`)** — *A1 (LCP)* · *Esforço: médio*
- Usar as variantes `_min/_med/_larg` já geradas em `<img srcset>` e `<picture><source type="image/webp">`.
  Ajustar helpers `file_exist*`/`file_exist_portal` para devolver a URL webp quando aceito.
  **Validação:** `lcp` melhora (imagem hero menor/certa).

**L5.4 · `loading="lazy"`** — *A1* · *Esforço: baixo*
- Aplicar `loading="lazy"` (e `decoding="async"`) em imagens/iframes abaixo da dobra nas views do portal.
  **Validação:** `lcp`/`requests` caem.

**L5.5 · CDN/edge para mídia estática** — *A2* · *Esforço: baixo (conecta Multi‑CDN)*
- `storage/app/public` (7,3 MB, 134 imagens) com `Cache-Control` longo (L0.3) na borda do CDN.
  **Validação:** `cache_hit` sobe em imagens.

---

### L6 — Compressão de HTML / agressiva  · *Critérios A1 (TTFB/download/parse)*

**L6.1 · Minificação de HTML (middleware de resposta)** — *Esforço: baixo · Risco: baixo* · ✅ **Implementado (2026-09-10)**
  - `app/Http/Middleware/HtmlMinify.php` (registrado no grupo `web`, APÓS `CacheControl` e ANTES de
    `PictureTransform` → na volta outbound minifica o HTML final já com `<picture>`). Guards:
    `method()==='GET'`, `!config('app.debug')`, `Content-Type: text/html`. Colapsa whitespace entre
    tags (`explode \n` → trim → `> <`→`><` → `\s{2,}`→`' '`).
  - **Preserva blocos sensíveis** (`<pre>`,`<code>`,`<textarea>`,`<script>`,`<style>`,`<svg>`) via
    placeholder → o conteúdo inline (JS/CSS) NUNCA é reescrito pelo middleware. Comentários HTML
    `<!-- -->` **não** são removidos (decisão: zero risco).
  - **Correção de comentários `//` (bug conhecido de minificação):** NÃO via regex no middleware.
    Feito em nível de fonte por subagentes: todo `//` de linha em `<script>` inline (portal 49
    arquivos/235, prefecture 32/74, admin 4/5) convertido para `/* ... */` explícito. `://`, strings,
    regex, `@php` e `{{-- --}}` mantidos intactos. 85 blades alterados compilam sem erro (Blade compiler).
  - HTML médio = **135 KB** → ganho direto em download/parse. **Validação:** `pagina`/`ttfb` caem
    (pendente `comparar.py` no canário). Documentação: `wiki/infra-update/html-minify-l6-1.md`.
**L6.2 · Critical CSS inline** — *A1 (LCP)* · *Esforço: médio*
- Inline do CSS above‑the‑fold e `defer` do resto (conecta L4/L7). **Validação:** `lcp`/`fcp`.
**L6.3 · Preload de LCP/críticos** — *A1* · *Esforço: baixo*
- `<link rel="preload" as="image" href="...hero...">` e preload do JS crítico. **Validação:** `lcp`.
- ✅ **Implementado (2026-09-10)** — `app/Http/Middleware/PreloadCritical.php` (grupo `web`, após
  `HtmlMinify`/antes `PictureTransform`) injeta `<link rel="preload" as="style">` para
  `assets/css/app.css` + `assets/vendor/fonts/google/fonts.css` (CSS render-blocking do critical
  path). O preload de LCP (imagem herói) já é feito pelo `PictureTransform`. JS crítico fica de fora
  (risco de double-fetch; scripts do portal são vendor deferidos). Doc: `wiki/infra-update/html-minify-l6-1.md`.
**L6.4 · HTTP/2 Push / 103 Early Hints** — *opcional* · ❌ **Descartado (2026-09-10):** Apache 2.4/cPanel do stack não tem suporte nativo a `103 Early Hints`; `HTTP/2 Server Push` foi removido dos browsers (Chrome 2021) — inviável.

---

### L7 — Refazer CSS (mais agressiva / risco de design)  · *Critérios A1 (LCP/FCP)* · ❌ **Descartado (2026-09-10):** decisão do usuário — refatoração de CSS dos 6 modelos fora de escopo.

**L7.1 · Eliminar `<style>` inline** — *Esforço: alto · Risco: alto*
- Há **309** ocorrências de `<style>` em Blade. Migrar para o pipeline sass (`resources/assets/sass/app.scss`)
  e para arquivos CSS versionados (L4.1). **Validação:** `lcp`/`fcp`/`transfer`.
**L7.2 · Purge de CSS não usado** — *Esforço: médio*
- AdminLTE (`jeroennoten/laravel-adminlte`) + bootstrap‑sass são pesados. Usar `purgecss` no build Mix
  para remover seletores mortos. **Validação:** `transfer` cai.
**L7.3 · Unificar os 6 modelos de template** — *Esforço: alto*
- Baseline: Modelo 1 (LCP 4249), Modelo 2 (LCP 5662), Modelo 3 (LCP 2979 — melhor). Consolidar base
  CSS/JS comum; só o que difere por modelo. **Validação:** `lcp` converge para < 2500 em todos.

---

## 3. Roadmap recomendado (por ROI)

| Fase | Camadas | Por quê | Meta mensurável |
|---|---|---|---|
| **P0** | L0.1–L0.4 | Só config; estagio‑1 já provado (−62% transfer) | `compression`=1, `cache_policy`≥80, `cache_hit`>90% (estáticos), `revalidation`>0 |
| **P1** | L1.1, L2.1, L2.3 | Cache de borda + bootstrap = ataca `cache_hit=0` e `origem` | `cache_hit`>90%, `ttfb`<800, `origem` cai |
| **P2** | L3, L4.1–L4.4 | Origem/código + build de assets | `ttfb`<800, `fcp` melhora, `requests` cai |
| **P3** | L5.1–L5.5 | Digestor WebP + responsivo + lazy | `transfer`<5 MB, `lcp`<2500 |
| **P4** | L6, L7 | HTML minify + refactor CSS | `lcp`<2500, `fcp` bom, `transfer` mínimo |
| **PF** | Fase Final (§9) | Fontes/@import + servidor (hot paths/config) + **JS/render/monitoramento (rec.)** | `fcp`/`lcp`/`inp`/`ttfb`/CLS |

---

## 4. Rastreabilidade (EPIC_BASE → Critério → Métrica)

| Camada | Issue EPIC_BASE | Critério #23410 | Métrica de baseline |
|---|---|---|---|
| L0.2/L0.3 | 1.1, 3.1 | A1, A2 | `compression`, `cache_policy`, `cache_hit`, `transfer` |
| L0.4 | 1.3 | A2 | `revalidation` |
| L1.1/L1.2 | 1.1, 1.2, 2.2 | A1, A2, A4 | `cache_hit`, `cache_policy`, `ttfb` |
| L2.1/L2.3 | 2.1, 2.2, 7.1 | A1, A2 | `origem`, `ttfb`, `cache_hit` |
| L3/L4 | 2.1, 4.2 | A1 | `origem`, `ttfb`, `fcp`, `requests` |
| L5 | 4.1 | A1 | `transfer`, `lcp`, `requests` |
| L6/L7 | 4.1, 4.2 | A1 | `lcp`, `fcp`, `transfer`, `peso` |

---

## 5. Guardrails (não regredir)

- Manter HTTP/2, TLS 1.3, redirects=0, disponibilidade=100% (rede já saudável — Issue 5.1).
- Manter headers de segurança do template `_pesquisa/security/htaccess.txt` (não envolver `Header`
  em `<IfModule mod_headers.c>`).
- Cada PR fechável por `medir_estrutura.sh` + `comparar.py` contra `2026-09-08-12-10`.

---

## 6. Melhorias pendentes (segurança + performance) — *derivadas da revisão de skills (web_security / web_performance) pós‑L4.2*

> Não bloqueiam o deploy de L4.2 (são ortogonais), mas **devem ser feitas** para fechar os
> critérios de segurança/performance do EPIC. Registradas aqui para rastreabilidade.

### D. Content‑Security‑Policy strict (web_security non‑negotiable)
- **Estado atual:** nenhum `Content-Security-Policy` no app (grep vazio em `app/`, `bootstrap/`,
  `config/`). Ausência de defesa contra XSS / mixed content / injection de terceiros.
- **Ação:** adicionar CSP strict baseada em nonce (`script-src 'nonce-{RANDOM}' 'strict-dynamic'`),
  `object-src 'none'`, `base-uri 'none'`, `frame-ancestors 'none'`, `upgrade-insecure-requests`.
  Allowlist mínima de origins ainda externas: `www.gstatic.com` (charts), `www.google.com`
  (recaptcha), `vlibras.gov.br` (widget), `pro.fontawesome.com` (FA‑Pro, banner‑modal).
  NUNCA `unsafe-inline` em `script-src`; NUNCA allowlist baseada em host com `*`+credenciais.
- **Ganho:** elimina vetor de XSS e isola 3rd‑party (defense‑in‑depth, alinhado a "Zero Trust for
  Third Parties"). Vantagem extra: após o self‑hosting de L4.2, o `default-src 'self'` já cobre
  fontes/FA/Sortable/etc. sem SRI.
- **Validação:** `csp-evaluator.withgoogle.com` + teste com ETP Strict / 3rd‑party cookies bloqueados.

### E. Critical path de fontes + CLS (web_performance §HTML/CSS)
- **Estado atual:** `assets/vendor/fonts/google/fonts.css` (5 famílias: Rubik, Nunito Sans, Source
  Sans Pro, Encode Sans Expanded, Lato) é `<link>` render‑blocking no `<head>` de todos os layouts;
  `font-display: swap` já ok (sem FOIT). O CSS é pequeno e os `.woff2` só baixam sob demanda, mas o
  CSS em si ainda bloqueia o first paint.
- **Ações (opcionais, avançadas):**
  1. `preload` + `onload="this.rel='stylesheet'"` para o CSS de fontes (ou split do crítico: home
     usa só **Rubik** → `preload` apenas do `@font-face` Rubik inline no `<head>`).
  2. `font-size-adjust: from-font` nos seletores de texto para reduzir CLS no swap.
  3. (já coberto) `Cache-Control: immutable` em `assets/vendor/` via L0.3 (`public/.htaccess:136`).
- **Ganho:** FCP/LCP menores (CSS de fonte sai do critical path) e CLS estável no swap.
   - **Validação:** Lighthouse `font-display` + `CLS` ≤ 0.1; sniffCSS confirma `font-family` inalterada.
   - **Fase Final (2026-09-10):** item `public_infra_update-bbc2` registrado para fechar este critério
     (remover `@import` Raleway de `app.css` + `font-display: swap` nos `@font-face` MR/OR/MB +
     `size-adjust`/`ascent-override`). Ver §9.

---

## 7. Otimizações de flags async/defer/preconnect (investigação modelo1‑6 → correções)

> Subagentes investigaram (read-only) oportunidades de `async`/`defer`/`preconnect` em CSS/JS nos
> layouts `modelo1…6`. Abaixo o que foi **corrigido** nesta rodada e o que ficou de fora.

### Corrigido
- **preconnect** para origins de share/embed/vídeo ainda externas — adicionadas ao partial
  `portal/layouts/partials/preconnect.blade.php` (incluído nos 6 `modelo*/head`):
  `www.youtube.com`, `static.addtoany.com`, `www.addtoany.com`, `www.facebook.com`,
  `www.queimadas.pb.gov.br` (dns-prefetch). AddThis **NÃO** incluído: grep confirma `s7.addthis.com`
  ausente em todos os blades → divs inertes, sem fetch.
- **Regressão fechada — DataTables i18n:** `//cdn.datatables.net/.../Portuguese-Brasil.json` ainda
  vinha do CDN em 15 views (relatorio-estatistico*, -ouvidoria, licitações, full-report, default).
  Vendorizado para `public/assets/components/DataTables/i18n/Portuguese-Brasil.json` e reescrito p/
  `{{ asset("assets/components/DataTables/i18n/Portuguese-Brasil.json") }}`.
- **defer** em 14 `<script>` locais end-of-body de `modelo6/layout/scripts.blade.php`: fontawesome,
  popper, jquery, bootstrap, tinycolor, lozard, lozad, main, tamanho-fonte, functions, iframe,
  contrast, filtro, busca. Mantidos síncronos: html5shiv, respond, modernizr (polyfills de ordem/FOUC).
  Inline `search-toggle` e modais usam `DOMContentLoaded`/`ready` → seguros com defer.
- **CSS não-crítico não-bloqueante** (`media="print" onload="this.media='all'"`) em lightgallery,
  lightslider, daterangepicker, DataTables (modelo1/2/5/6) e lightgallery (modelo3/4). **ionicons**
  mantido render-blocking (usado no chrome do site → evita FOUC de ícones).
- **loading="lazy"** nos 6 iframes YouTube de `home/modalVideo.blade.php` (em modais).

### Não alterado (decisão)
- Nenhum `defer`/`async` novo em `<script>` externo: vlibras já `defer`; `gstatic charts/loader.js`
  tem init inline síncrono (`google.charts.load`) → `defer` quebraria os gráficos.
- iframes de sidebar (PHP-concatenados) e gerados via JS (`transparencia-fiscal/index`) — sem `lazy`
  (risco/complexidade); revisar se necessário.
- **D (CSP)** e **E (fontes/CLS)** da §6 seguem pendentes.

### Observações fora de escopo (flag-only não cobre — registradas)
- jQuery/bootstrap duplicados: modelo5 (bootstrap x2, tamanho-fonte x2); modelo6/ouvidoria
  (jQuery 3.3.1 **e** 3.2.1) → requests redundantes reais (refatoração, não flag).
- Origens em `incorporacao_head`/`incorporacao_rodape` (conteúdo do admin no DB) podem injetar
  3rd-party → auditar conteúdo.

---

## 8. L5 — WebP responsivo via middleware (sem toque nos módulos) · *Critérios A1 (transfer/LCP)*

> **Decisão (2026‑09‑10):** a otimização de mídia NÃO deve exigir edição nos Blades dos módulos.
> Implementada como (a) **middleware de resposta** que reescreve `<img>` → `<picture>` otimizado e
> (b) **geração de variantes em background** para imagens já cadastradas e futuras. Padrão V2
> (`MIDIA_OPTIMIZATION_EXAMPLE`) adaptado ao stack atual (Intervention/Imagick + storage local).

### Arquitetura
- **`app/Http/Middleware/PictureTransform.php`** (registrado no grupo `web`, APÓS `CacheControl`):
  - Em GET de portal público (`Portal\*` controller), `text/html`, status < 400, reescreve toda tag
    `<img>` de imagem local do storage em `<picture>` com `<source type="image/webp" srcset=...>`
    responsivo (360/720/1200/1920w) + `<img>` fallback original.
  - **Anti double-download** (espelha V2): `<img>` fallback recebe `loading="lazy"` quando há variantes;
    o primeiro `<source>` recebe `fetchpriority="high"` se a imagem original era `eager` (LCP).
  - Preserva atributos originais (`class/alt/style/data-*`), marca com `data-webp="optimized"` (idempotente),
    e protege `<picture>` já existentes. Se a imagem ainda não foi digerida (sem sidecar), **deixa o
    `<img>` original** — o background a otimiza na próxima corrida.
   - **Normaliza sufixos de resize legado** (`_min`/`_med`/`_larg`/`_big`) ao localizar o
     sidecar: notícias/secretarias/conteúdos usam `getMinFile`/`getMedFile`/`getLargFile`
     (`app/Helpers/functions.php`) que apontam para `xxx_larg.jpg` etc.; sem a normalização
     essas páginas escapavam da transformação. O `<img>` fallback mantém o arquivo legado
     (resize) p/ browsers sem WebP; os `<source>` WebP vêm do original.
- **`app/Http/Middleware/HtmlMinify.php`** (registro `web`, APÓS `CacheControl`/ANTES `PictureTransform`):
  minifica respostas `text/html` (GET, `!app.debug`), preservando `<pre>/<code>/<textarea>/<script>/<style>/<svg>`.
  Comentários `//` de JS foram convertidos para `/* */` em nível de fonte (ver L6.1) — o middleware não
  usa regex para isso.   Vide `wiki/infra-update/html-minify-l6-1.md` (documentação dedicada da minificação).
- **`app/Http/Traits/FileTrait::generateWebpVariants()`** (hook em `compress()`, topo do bloco
  `!$extras`): gera variantes WebP em `variants/` + sidecar `variants/{base}.variants.json`
  (mapa largura → caminho relativo). Engine Intervention/**Imagick** (gd também serve), quality 82,
  `stripImage()` (EXIF), **idempotente** (sidecar poblado → retorna). **Cobre uploads futuros**
  (todos os call sites de `compress()`/`verifyCompress()`/`saveUpload`).
  - Pula `svg` (vetorial) e **GIF animado** (preserva animação); imagens < 360px → variante `original`.
- **`app/Console/Commands/RedigestWebpImages.php`** (`images:redigest-webp --limit=N`): digestão
  em background do acervo. Chunked com offset em `.webp_redigest_offset`; dá a volta ao concluir
  (contínuo). Agendado em `app/Console/Kernel.php` → `everyTenMinutes()`.
- **`public/.htaccess`**: `AddType image/webp .webp` + `image/avif` (mod_mime) — sem isso o Apache
  servia `.webp` sem `Content-Type` e o navegador (nosniff) recusava o `<source>`.

### Validação (canário, container `public_infra_update-app-1`)
- Homepage renderiza **6 `<picture>`** com `type="image/webp"`, `<img>` fallback `loading="lazy"`
  `data-webp="optimized"`, atributos originais preservados (`class/alt`).
- Variantes em `storage/app/public/**/variants/*.webp` servidas como **`Content-Type: image/webp`** (200).
- `images:redigest-webp` gerou **156 sidecars / 309 variantes** do acervo (174 imagens); idempotente.
- Sem erro de Blade; página 200. `comparar.py` (transfer↓/lcp↓) pendente de medição no canário.

### Fora de escopo / ressalvas
- Imagens em `incorporacao_head`/`rodape` (DB) não passam pelo middleware (nem pelo storage local) →
  não recebem `<picture>` (auditadas em §7).
- GIF animado permanece JPG/PNG (não vira WebP).
  - §6 **D (CSP)** e **E (fontes/CLS)** seguem pendentes (segurança adiada por decisão do usuário).

---

## 9. Fase Final — melhorias estruturais restantes (sem borda CDN)  · *Critérios A1 (LCP/FCP/INP/TTFB/CLS)*

> **Contexto (2026-09-10):** após L0–L6 implementados (FPC L2.3, HtmlMinify L6.1, PreloadCritical L6.3,
> WebP L5, Cache-Control+ETag+immutable L1, compressão WHM L0), com L7 (refazer CSS) e L6.4 (Early
> Hints) **descartados**, restam melhorias **estruturais do lado da origem/app** — **fora da borda CDN**
> (decisão do usuário). Levantadas a partir dos guias `web_performance`, `css_guide`, `html_guide`,
> `php-72` e `laravel_guide` cruzados com o estado real do repo. Container de rastreabilidade:
> `public_infra_update-91a1`.
>
> **Escopo confirmado pelo usuário:**
> - **CSS** = mínimo (só fontes/`@import`); **não** consolidar os 14 CSS nem critical inline.
> - **Cache/sessão** = `file` (manter; Redis não adotado).
> - **Queries** = hot paths (home/notícias/conteúdo/secretarias) + `Cache::remember` de `Configuration`.
> - **JS delivery / render containment / monitoramento** = recomendação (não estabelecidos nas perguntas, mas no plano).

> **Status (2026-09-10):** ✅ todos os 5 itens implementados e validados no canário (template ativo
> `modelo1`). Verificação em produção: `Server-Timing: app;dur=3076.53` num MISS (TTFB de origem real);
> `@import` Raleway removido de `app.css`; `font-display: swap` ×3 em `style.css`; polyfills mortos
> (html5shiv/respond) + bootstrap duplicado removidos; `defer`/`async` nos scripts; `cv-auto` no
> `#footer-info`; RUM (`rum.js`) + Speculation Rules ativos.
>
> **Nota de cache de Configuration (servidor):** no `modelo1` o cabeçalho/rodapé já era servido por
> `ConfigHeaderServiceProvider`/`FooterServiceProvider` com `Cache::remember` (TTL 300s). Ganho extra:
> `default/master.blade.php` agora usa `Cache::rememberForever('prefecture.config')` (14→1 query) com
> invalidação automática via `boot()` do model `Configuration` (`Cache::forget` em `saved`/`deleted`);
> eager loading adicionado em `Content::ultimasNoticias()` (`archives`+`categoryContents`).

| Issue | Item | Camada / Guia | Decisão | Arquivo(s) |
|---|---|---|---|---|
| `public_infra_update-bbc2` | Fontes: remover `@import` Raleway + `font-display:swap` (MR/OR/MB) + `size-adjust` | §6 E / css_guide/03 | **Confirmado** | `public/assets/css/app.css:1`, `public/assets/vendor/portal/css/style.css:168-187` |
| `public_infra_update-7a7b` | Servidor: eager loading hot paths + `Cache::remember(Configuration)` + `Server-Timing` | L2.4 / laravel/05 / php/15 | **Confirmado** | `master.blade.php:9-24`, `PortalController`/`NewsController`/`Content*`/`Secretaria*`, novo middleware |
| `public_infra_update-ce7f` | JS delivery: `defer`/`async` + remover html5shiv/respond + dedup bootstrap + `async` 3rd-party | html_guide/06 / L4.3 (3177) | Recomendado | `master.blade.php:147-167` |
| `public_infra_update-5e6d` | Render containment: `content-visibility:auto` + `contain` | css_guide/03 | Recomendado | CSS dos modelos |
| `public_infra_update-df11` | Monitoramento: `PerformanceObserver` RUM + Speculation Rules + Lighthouse CI | web_performance/04,07 | Opcional | novo middleware/JS |

### 9.1 Detalhe dos itens
- **bbc2 (fontes, confirmado):** remover o `@import "https://fonts.googleapis.com/..."` de `app.css:1`
  (anti-pattern #1 — serializa CSS e trava o render) → `<link rel="stylesheet">` no `<head>` (o
  `preconnect` google/gstatic já existe em `preconnect.blade.php`) ou `preload`+`onload`. Adicionar
  `font-display: swap` aos `@font-face` MR/OR/MB (`style.css:168-187`, hoje sem → FOIT ≤ 3s) +
  `size-adjust`/`ascent-override` (reduz CLS no swap). Fecha §6 E. Self-host do Raleway = upgrade opcional.
- **7a7b (servidor, confirmado):** (1) colapsar as 9 queries `Configuration::where('key',…)->first()` de
  `master.blade.php:9-24` em **1** `Cache::rememberForever('prefecture.config', fn() =>
  Configuration::pluck('value','key')->toArray())` + invalidar no update do `Configuration`; (2)
  `with()`/`withCount()` nos hot paths para relações acessadas em loop (categorias, archives, secretaria,
  imagens); (3) `Cache::remember` em blocos compartilhados (menu/categorias/footer); (4) header
  `Server-Timing` num middleware. Cache/sessão **mantém `file`**. Ganho roda em toda página (no miss do
  FPC vira 1 query em vez de 9+; admin não é cacheado).
- **ce7f (JS, recomendado):** consolida `L4.3 (3177)` — `defer`/`async` em scripts; remover polyfills
  mortos html5shiv/respond; remover bootstrap carregado 2× (linhas 152-153); Modernizr `async`/remover;
  `async` em analytics/addthis/jivo. Reduz INP/TBT.
- **5e6d (render, recomendado):** `content-visibility:auto` + `contain-intrinsic-size` em seções abaixo
  da dobra; `contain: layout style` em widgets isolados (vlibras, modais).
- **df11 (monitoramento, opcional):** `PerformanceObserver` (LCP/CLS/INP) + `sendBeacon`; Speculation
  Rules API (prefetch próxima página); budget de Lighthouse no CI.

### 9.2 Guardrails
- Manter decisões: sem rewrite de CSS (L7 descartado), sem Redis (file), sem borda CDN.
- Validar cada item no canário com `medir_estrutura.sh` + `comparar.py`; não regredir CSP/segurança.
- Itens recomendados/opcionais (ce7f, 5e6d, df11) entram só após aprovação — não bloqueiam os confirmados.
