# EPIC — Continuidade Infra-Update: base para Borda Multi-CDN (pré-A4 → A2/A3/A4 → origem)

> **Escopo deste documento:** continuidade do EPIC de performance (`plan/2026-08-09-infra-update.epic.md`,
> seed `public_infra_update-3127`, plano `pl-cc49`) rumo aos critérios **A2/A3/A4** da tarefa #23410,
> a partir da **aferição ao vivo** do canário `infra-update.devs.maxima.inf.br` (2026-09-11).
>
> **Stack (restrição central):** PHP 7.2.34 / Laravel 5.5.50 / Apache 2.4 (EasyApache4) / cPanel-AlmaLinux.
> **NÃO** atualizar framework/PHP. **Nunca** `<IfModule mod_headers.c>` no `.htaccess` (HTTP 500).
> **Sem** `git commit`/`sd sync` automático — seeds apenas locais.
>
> **Rastreabilidade sd:** épico `public_infra_update-3662` · plano `pl-d27c` (9 steps).
> **Baseline de comparação:** `2026-09-08-12-10` (`medir_estrutura.sh` + `comparar.py`).

---

## 1. Ponto de partida (o que a aferição ao vivo confirmou)

**Já implementado e verificado no ar** (camada origem/aplicação):

- FPC de origem (`x-page-cache: HIT`), `Cache-Control: public, s-maxage=300, stale-while-revalidate`
- ETag fraco + `304` em `If-None-Match`; gzip ativo; estáticos `immutable`; WebP (`image/webp`)
- HTML minificado, `<picture>`/WebP, `rum.js`, Speculation Rules, sem `googleapis`
- Headers de segurança (CORP/COOP/HSTS/permissions/referrer/x-frame/nosniff)

**Aberto / crítico:**

| Achado | Evidência | Critério |
|---|---|---|
| `sw.js` servido `immutable` (quebra update do SW) | `curl -I /sw.js` → `max-age=31536000, immutable` | regressão PWA |
| `Vary: Accept-Encoding,User-Agent` — **RESOLVIDO 2026-09-11** | vinha de `/home/devmaxima/.htaccess:14` (`Header append Vary User-Agent`, trecho HTML5 Boilerplate herdado) | ~~fragmenta A2~~ |
| Nenhum CDN no ar (`cache_hit = 0`, sem `age`/`via`/`cf-*`) | 18 etapas + aferição | **A2** |
| Sem failover geográfico | — | **A3** |
| Purge síncrono deferido (seeds `650c`, `d9e8` abertas) | — | **A4** |
| MISS de origem ~3 s em `getContentNews` | `Server-Timing app;dur≈3076` | A1 (origem) |
| `robots.txt` → 302 `/error/404`; soft-404 (200) | curl | SEO |
| `/sitemap.xml` serve estático legado; rota Laravel é dead code | `public/sitemap.xml` (git) + `routes/web.php:120` | SEO |

### 1.1 Problemática do sitemap (achado de origem, independente da borda)

- **Sintoma:** `GET /sitemap.xml` devolve o **arquivo estático** `public/sitemap.xml`
  (git-tracked, 5.132 B, legado de homologação): `lastmod` fixo em `2023-08-03`, `<loc>` de
  outros domínios (`guarabira.online`, `guarabira.pb.gov.br`, `fgts.gov.br`, `falabr.cgu.gov.br`,
  `iapm.pb.gov.br`) e uma URL interna vazada (`http://prefeituras_v5/publicacoes`). A raiz XML é
  inválida para o schema: usa `xsi="…"`/`schemaLocation="…"` em vez de `xmlns:xsi` +
  `xsi:schemaLocation`, e traz o typo `hhttp://`.
- **Por que a rota não roda:** o front controller só trata caminhos inexistentes (`-f`); o
  `.htaccess:69` reescreve apenas `robots.txt`. Logo `Route::any('/sitemap.xml', …)`
  (`routes/web.php:120`) é **dead code** em produção.
- **Regressão do F3.2:** o novo `robots.txt` publica `Sitemap: {host}/sitemap.xml`, ou seja,
  **direciona crawlers para o arquivo legado**.
- **Controller também quebrado** (`Portal\SitemapController@sitemap`): esquema `http://` hardcoded
  via `$_SERVER`; `asXML('sitemap.xml')` grava arquivo em disco num `GET` e **não retorna
  response**; `echo` (sem `application/xml`); `strpos(...) == false` (`0 == false`) monta URL
  errada para links iniciados em `//`; `foreach ($url as $url)` sombreado e `$url->links->link`
  potencialmente null; rota `any` dentro de `check.site.manutention` (sai do ar em manutenção, ao
  contrário do `robots.txt`).
- **Correção proposta (app-side, sem borda):** rewrite `^sitemap\.xml$` → front controller; rota
  `GET` **fora** do grupo de manutenção; `SitemapController` retorna `application/xml` com esquema
  de `url()`/`Request::secure()`, `xmlns:xsi` + `xsi:schemaLocation` corretos, `lastmod` real e
  **sem** escrita em disco. Aceite: `200`, `Content-Type: application/xml`, somente `<loc>` do host
  atual, válido no validador de sitemap do Google.
- **Rastro:** candidato de origem independente de CDN (não é step do `pl-d27c`); detalhado em
  `wiki/infra-update/melhorias-pre-borda-f1-f3-f2.1.md §7`.

---

## 2. Frentes e sequência correta de execução

Ordem por dependência e risco. **Itens externos/comerciais ficam por último.**

### Frente 1 — Bugs pré-A4 (curto; desimpedem a borda)

| Step | Issue `sd` | Conteúdo | Bloqueia |
|---|---|---|---|
| F1.1 | `public_infra_update-a8e0` | `sw.js` → `max-age=0, must-revalidate`. Mover a exceção do `public/.htaccess` para **depois** do bloco L0.3 (o último `Header set` vence; o comentário atual está errado). | F2.2 |
| F1.2 | `public_infra_update-0a60` ✅ | Remover `Vary: User-Agent` **na camada que o adiciona**. Causa real: `/home/devmaxima/.htaccess:14` (`Header append Vary User-Agent env=!dont-vary`, H5BP, herdado por todos os sites da conta) — não era WHM nem pagespeed. Remover/comentar a linha; `[L0.3c]` do app fica como defesa. | ~~F2.2~~ |

> O app (`CacheControl.php:103-110`) já remove `User-Agent`; o header era re-inserido pelo
> `.htaccess` da **conta** (`/home/devmaxima/.htaccess`), herdado por todos os sites (aparecia até
> em estático servido direto pelo Apache). O `Header edit` do subdiretório não vencia porque o
> `append` da raiz é aplicado depois — o fix é remover o `append` na origem.

### Frente 3 — Origem (MISS/N+1) e limpeza (independentes; podem correr em paralelo à F1)

| Step | Issue `sd` | Conteúdo |
|---|---|---|
| F3.1 | `public_infra_update-8c27` | Profiling do MISS (`getContentNews`/`getCategoryNews`), índices portáveis e `Cache::remember` das listas; `PerformanceIndexTest`. |
| F3.2 | `public_infra_update-da8f` | `robots.txt` 200 `text/plain` (com `Sitemap:`) e soft-404 → status `404` (validar impacto no admin). |

### Frente 2 — Borda / app (app-side antes; externo por último)

| Step | Issue `sd` | Conteúdo | Bloqueado por |
|---|---|---|---|
| F2.1 | `public_infra_update-03e7` | **App-side**: normalizar chave de cache (ignorar `utm_*`/`gclid`/`fbclid`) em `PageCache::keyFor()`; criar `CdnPurgeService` + `config/cdn.php`; hooks em `PageCache::flush()`/observers. **Não é externo.** | F1.1/F1.2 (indireto) |
| F2.2 | `public_infra_update-b629` | **[EXTERNO]** Contratar 2–3 provedores + geo-balancer; cache de HTML (A2) + origin shield; WAF/TLS idênticos. | a8e0, 0a60, 03e7 |
| F2.3 | `public_infra_update-9845` | **[EXTERNO]** Failover geográfico (A3): health checks, DNS/anycast steering, secundário. | b629 |
| F2.4 | `public_infra_update-979f` | **[EXTERNO]** Purge síncrono multi-provedor (A4) com chave normalizada. | 03e7, b629 |
| F2.5 | `public_infra_update-3c35` | Monitoramento (RUM/Catchpoint) + medição final A1–A4. | b629, 9845, 979f |

**Grafo resumido:**

```
F1.1 ─┐
F1.2 ─┼─► F2.2(ext) ─┬─► F2.3(ext) ─┐
F2.1 ─┘              └─► F2.4(ext) ─┼─► F2.5
                                    ┘
F3.1 (indep.)   F3.2 (indep.)
```

---

## 3. Detalhe por issue (o que será feito)

### F1.1 — `public_infra_update-a8e0` · corrigir cache do `sw.js`
- **Arquivo:** `public/.htaccess`.
- **Bug:** `<FilesMatch "(sw\.js|manifest\.webmanifest)$">` (~L157) é sobrescrito pelo L0.3 (~L164) — ambos `Header set`, o último vence.
- **Ação:** reordenar (exceção após L0.3) ou fundir num único `FilesMatch`.
- **Aceite:** `curl -sI .../sw.js` → `public, max-age=0, must-revalidate`.

### F1.2 — `public_infra_update-0a60` · remover `Vary: User-Agent` — ✅ fechado (2026-09-11)
- **Causa raiz:** `/home/devmaxima/.htaccess:14` — `Header append Vary User-Agent env=!dont-vary`
  (trecho do HTML5 Boilerplate, na raiz da conta, herdado por todos os sites sob `/home/devmaxima`).
  **Não** era include do WHM (`pre_main_global.conf`) nem `mod_pagespeed` (ambos checados e limpos).
- **Ação:** removida/comentada a linha no `.htaccess` da conta (sem restart do Apache). O `[L0.3c]`
  (`Header edit Vary`) do `public/.htaccess` permanece como defesa em profundidade.
- **Aceite (verificado no canário):** `Vary: Accept-Encoding` apenas em `/`, `/sw.js`, `/robots.txt`,
  `/sitemap.xml`, `/noticias` e estático, com `Accept-Encoding` = `gzip`/`br`/`identity`/ausente;
  compressão intacta (`content-encoding: gzip`).

### F3.1 — `public_infra_update-8c27` · MISS ~3 s
- **Arquivos:** `app/Http/Traits/NewsTrait.php:28-49`, scopes `byCategoryContentNews`/`byOrder`/`byStatus`, `paginate()`.
- **Ação:** profiling; índices (`contents(publication_date,status,category_contents_id)`); `Cache::remember` por categoria+página.
- **Aceite:** `server-timing` do MISS cai; `EXPLAIN` sem `Seq Scan` (`PerformanceIndexTest`).

### F3.2 — `public_infra_update-da8f` · limpeza SEO
- **Arquivos:** `routes/web.php`, `app/Exceptions/Handler.php:54-65`, `Admin/ErrorsController`.
- **Ação:** rota `robots.txt`; 404 com status 404 (não redirect p/ 200).
- **Aceite:** `robots.txt` 200 `text/plain`; URL inexistente → HTTP `404`.

### F2.1 — `public_infra_update-03e7` · chave de cache + hooks de purge (app)
- **Arquivos:** `app/Http/Middleware/PageCache.php:176-179`, `app/Services/Cdn/CdnPurgeService.php` (novo), `config/cdn.php` (novo).
- **Ação:** normalizar query na chave (Issue 6.2); serviço de purge desacoplado + idempotente; gatilho em `created`/`deleted`.
- **Aceite:** `?utm_source=x`/`?gclid=y` reaproveitam a mesma entrada de FPC.

### F2.2–F2.4 — externos (contratação e config de borda)
- **A2:** cache de HTML + shield + WAF/TLS/ACME idênticos; chave normalizada.
- **A3:** failover por latência + health checks + secundário; teste de derrubada.
- **A4:** purge síncrono via `CdnPurgeService` em todos os provedores.
- **Aceite:** `cache_hit > 90%`, TTFB < 800 ms / LCP < 2500 ms (da borda), failover sem impacto, purge imediato.

### F2.5 — `public_infra_update-3c35` · monitoramento + medição final
- RUM (`rum.js`) → endpoint real; Catchpoint; SLOs A1–A4; `comparar.py` pós-borda.

---

## 4. Critérios de aceite do épico (rastreio à tarefa #23410)

| Critério | Fecha com |
|---|---|
| **A1** TTFB/LCP (regiões distantes) | F1.1, F1.2, F3.1, F2.2 |
| **A2** Cache Hit estáticos > 90% | F2.1, F2.2, F2.5 |
| **A3** Disponibilidade/failover | F2.3, F2.5 |
| **A4** Purge síncrono | F2.1, F2.4, F2.5 |

Guardrails (não regredir): HTTP/2, TLS 1.3, redirects, disponibilidade, headers de segurança,
`php -l` + `phpunit` verdes (exceto o stub `Tests\Feature\ExampleTest` por design).

---

## 5. Riscos

1. **OPcache `validate_timestamps=0`** — requer reset no deploy (`opcache:clear`/`touch`/reload php-fpm).
2. **`Vary: User-Agent` (resolvido)** — vinha de `/home/devmaxima/.htaccess:14` (herdado por todos os sites da conta); linha removida. Se outro domínio tiver `.htaccess` próprio com o mesmo boilerplate, replicar a remoção.
3. **Soft-404** afeta o `Handler` global — validar admin com `phpunit` + navegação.
4. **Índices em tabela grande** — `CREATE INDEX CONCURRENTLY` (pgsql) / janela.
5. **Borda / contratação** — dependência comercial; ordem das externas pode variar com acesso aos provedores.
6. **Purge** — evitar thrash (só `created`/`deleted`) e garantir idempotência/retry.

---

## 6. Fora de escopo

- Migração de framework/PHP/Laravel ou reescrita de aplicação.
- Refatoração de CSS dos 6 modelos (L7 descartado) e HTTP/2 Push / 103 Early Hints.
- CSP enforce enquanto a borda cachear HTML com nonce (resolver por `hash` — ver
  `plan/2026-09-10-csp-enforce-edge-coexistence.md`); o deploy atual mantém CSP desligado.

---

## 7. Onde achar

- Plano sd: `sd plan show pl-d27c` · Épico sd: `public_infra_update-3662`
- Base anterior: `plan/2026-08-09-infra-update.epic.md`, `plan/2026-09-10-security-epic.md`
- Wikis: `wiki/infra-update/correcao-cache-control-borda-l1.md`, `full-page-cache-origem-l2-3.md`,
  `fase-final.md`, `csp-epico-seguranca.md`
