# Plano independente — PWA sólido (fundação: manifest dinâmico + Service Worker otimizado)

**Data:** 2026-09-10
**Escopo:** Fundação de um PWA instalável e offline-first para o portal (foco em Service Worker otimizado).
**Status:** Planejado (issues criadas no `sd`; sem commit/sync automático).
**Relação com o EPIC de performance (`pl-cc49`):** independente — este plano NÃO é adotado naquele EPIC.

---

## 1. Rastreabilidade (sd)

- **Epic (pai):** `public_infra_update-c3bb` — *PWA: fundação (manifest dinâmico + Service Worker otimizado)*
- **Plan:** `pl-12a6` — *PWA Fundacao (manifest dinamico + Service Worker otimizado)* (status `approved`, adopt-only; 7 steps)
- **Issues (steps):**

| ID | Título | Prior. | Bloqueado por |
|---|---|---|---|
| `public_infra_update-18bc` | manifest.webmanifest dinâmico via Configuration | 2 | — |
| `public_infra_update-faed` | gerar ícones 192/512 (maskable+any) do logo | 3 | — |
| `public_infra_update-a93f` | offline.html fallback brandeado | 3 | — |
| `public_infra_update-853d` | Service Worker otimizado (`public/sw.js`) | 2 | `a93f`, `18bc`, `faed` |
| `public_infra_update-5378` | exceção `.htaccess` para `sw.js`/manifest | 3 | — |
| `public_infra_update-3518` | partial `pwa.blade.php` em todos os heads | 3 | `853d` |
| `public_infra_update-7078` | validação em canário | 3 | `853d`, `3518`, `5378`, `18bc`, `faed` |

**Labels aplicadas:** `pwa` (todas), `manifest`, `service-worker`, `offline`, `frontend`, `performance`, `cache`, `laravel-55`, `config`, `apache`, `js`.

---

## 2. Estado atual (auditoria)

- **`public/manifest.webmanifest` está CORROMPO**: contém entradas do Mix (`"/js/icheck.js": …`, `"/css/app.css": …`) e não possui `icons` (192/512), `start_url`, `scope`, `display`, `theme_color`, `background_color`, `prefer_related_applications` → PWA **não instalável** hoje.
- Todos os heads (`modelo1…6`, `default`, `newsbit`) já linkam `/manifest.webmanifest`, mas **sem** `<meta name="theme-color">` e **sem** registro de Service Worker.
- `public/.htaccess` L0.3 aplica `Cache-Control: …immutable` a todo `.js` → SW não atualizaria (precisa de exceção + `updateViaCache:'none'`).
- Sem `offline.html` e sem SW registrado em lugar algum.
- Cor de marca é configurável: `Configuration` (chaves `cor_1…cor_4`), exposta por `HelperController::getColors`. `public/mix-manifest.json` versiona `app.css`.
- Imagem fonte para ícones: `public/assets/images/prefeitura-logo.png` (349×124 RGBA). ImageMagick (`convert`) disponível.

---

## 3. Decisões (confirmadas com o usuário)

- **Escopo:** Fundação apenas — manifest + SW + offline + `.htaccess`. Sem botão de instalação customizado e sem Background Sync/Push nesta rodada.
- **`theme_color`/`background_color`:** **dinâmicos** via `Configuration` (`cor_1`, fallback `#00a65a`).
- **Ícones:** gerados a partir de `prefeitura-logo.png` via ImageMagick.
- **SW:** vanilla (sem Workbox) para evitar etapa de build extra.

---

## 4. Arquitetura do Service Worker (matriz de cache)

Segue a tabela de estratégias do `pwa_guide` §3. Caches nomeados e versionados: `SHELL`, `PAGES`, `ASSETS`, `RUNTIME`.

| Recurso | Estratégia | Motivo |
|---|---|---|
| Navegações (`request.mode === 'navigate'`, portal público) | **Network-first** + cache `PAGES`; fallback `offline.html` | Frescor (bate no PageCache/edge rápido) + offline para páginas já visitadas |
| Assets estáticos (`/assets/...`; ext `css\|js\|woff2?\|png\|jpg\|webp\|svg\|ico`) | **Cache-first** (`ASSETS`) | Já `immutable`; safe |
| GET variável do portal (`/noticias/...`, `/transparencia/...`, etc.) | **Stale-while-revalidate** | Frescor moderado |
| `/csrf-token`, `/getColors`, `/getPrivacyBar`, ajax | **Network-only** | Tokens/dados dinâmicos não devem ser cacheados |
| `/admin/*`, `/prefecture/*`, `/api-dados-abertos`, `/sitemap.xml` | **Network-only** (fallthrough, sem `respondWith`) | Área administrativa nunca em cache |
| Cross-origin (gstatic/vlibras/addtoany/youtube) | imagens: **SWR**; demais: rede | Evita opacidade problemática; espelha Speculation Rules |

**Ciclo de vida (`public/sw.js`):**
- `install`: `cache.addAll([offline.html, manifest, app.css versionado, ícones])` envolto em `Promise.allSettled` (não falha o install se 1 recurso 404).
- `activate`: apaga caches `!==` versão atual + `clients.claim()` (sem `skipWaiting` agressivo).
- `fetch`: filtra `method !== 'GET'` → retorna (não intercepta POST); aplica a matriz acima; **sempre** `response.clone()` antes de `cache.put`.
- Registro (no partial): `navigator.serviceWorker.register('/sw.js', { scope:'/', updateViaCache:'none' })`.

---

## 5. Arquivos a criar/editar

**Novos**
- `app/Http/Controllers/Portal/ManifestController.php` (`show()`)
- `public/sw.js`
- `public/offline.html`
- `resources/views/portal/layouts/partials/pwa.blade.php`
- `public/assets/images/icons/icon-{192,512}.png`, `icon-{192,512}-maskable.png`

**Editar**
- `routes/web.php`: rota `GET /manifest.webmanifest` (fora do grupo manutenção) → `ManifestController@show`.
- `public/.htaccess`: exceção `<FilesMatch "(sw\.js|manifest\.webmanifest)$">` com `Cache-Control "public, max-age=0, must-revalidate"` antes da regra L0.3.
- 7 heads (`modelo1…6`, `default`, `newsbit/layout/head.blade.php`): `@include('portal.layouts.partials.pwa')` após o preconnect.
- `public/manifest.webmanifest`: substituído pela resposta da rota (o arquivo estático fica órfão/ignorado).

---

## 6. Passos de implementação (mapeados às issues)

1. **`18bc`** — `ManifestController@show`: monta JSON com `id` fixo `/`, `name`, `short_name`, `description`, `start_url "/"`, `scope "/"`, `display "standalone"`, `display_override ["standalone","browser"]`, `background_color`+`theme_color`=`cor_1` (fallback `#00a65a`), `prefer_related_applications:false`, `icons` (192/512 maskable+any), `shortcuts` (home, notícias, transparência, ouvidoria). Responde `application/manifest+json`. Rota em `web.php`.
2. **`faed`** — ImageMagick: compor `prefeitura-logo.png` em canvas quadrado 192/512, logo em 80% central, fundo=cor de marca (maskable) e branco (any).
3. **`a93f`** — `offline.html` estático (logo + mensagem + link home).
4. **`853d`** — `public/sw.js` conforme §4.
5. **`5378`** — exceção `.htaccess`.
6. **`3518`** — partial `pwa.blade.php` (`<meta name="theme-color" content="{{ cor_1 }}">` + registro SW com feature detection) e `@include` nos 7 heads.
7. **`7078`** — validação (ver §7).

---

## 7. Validação (canário, sem commit)

- `curl -I /sw.js` → `Cache-Control` **sem** `immutable` (max-age=0, must-revalidate).
- `curl /manifest.webmanifest` → JSON válido, `content-type: application/manifest+json`, `theme_color`=cor configurada, `icons` presentes.
- DevTools → Application: SW ativo; modo **Offline** → páginas visitadas vêm do cache, demais caem em `offline.html`.
- `npx pwa-validator manifest.webmanifest` / Lighthouse (PWA) → instalável ✅.
- `grep` de `googleapis`/assets inalterado; `/admin` e `/prefecture` continuam funcionais (network-only).

---

## 8. Riscos / notas

- SW abrange `/admin` e `/prefecture` por escopo `/`, mas tratados como network-only → sem quebra do backend.
- `updateViaCache:'none'` + exceção `.htaccess` garantem atualização do SW sem `skipWaiting` agressivo.
- Fora de escopo nesta rodada: botão de instalação (`beforeinstallprompt`), Background Sync (envio offline de formulários), Push (requer VAPID no servidor).
- **Pendente de decisão:** `sd sync` (commit de `.seeds`) — não executado por diretriz anterior de "sem commit"; rodar sob demanda.
