feat(pwa): add Tier 1 support — installable app with manifest and icons

This commit is contained in:
Lucas Kalil 2026-06-16 15:09:34 -03:00
parent 2ad27084d5
commit 715ecedbcb
17 changed files with 156 additions and 2 deletions

View file

@ -66,6 +66,10 @@ Use checkboxes to track progress. Items marked **🔴 BLOCKER** prevent release;
- [ ] Real stadium photos + team flag SVGs in `assets/images/` (10 new-team flags created 2026-06-12 in placeholder style) - [ ] Real stadium photos + team flag SVGs in `assets/images/` (10 new-team flags created 2026-06-12 in placeholder style)
- [ ] **Pós-Copa: estado final da home.** Quando a Final encerrar, o hero fica vazio (por design atual). Criar um estado pós-torneio (campeão/epílogo) na home — ver entrada "Hero cronômetro inteligente (2026-06-15)" em project-memory; provavelmente converge com a aba Stats (`stats-screen-plan.md`). - [ ] **Pós-Copa: estado final da home.** Quando a Final encerrar, o hero fica vazio (por design atual). Criar um estado pós-torneio (campeão/epílogo) na home — ver entrada "Hero cronômetro inteligente (2026-06-15)" em project-memory; provavelmente converge com a aba Stats (`stats-screen-plan.md`).
### 🟢 OPTIONAL
- [x] ~~PWA Tier 1 — instalável (manifest + ícones + meta tags; 2026-06-16). Atende todos os critérios de aceitação da issue.~~
- [ ] PWA Tier 2 — service worker + offline (deferido; ver `.agents/issues.md` → "PWA Tier 2". DEVE excluir `data/*.json` do cache p/ não quebrar o live-refresh + `DATA_VERSION`).
--- ---
## Quick final checklist ## Quick final checklist

View file

@ -60,6 +60,41 @@ Only if:
--- ---
## PWA Tier 2 — Service Worker + Offline (2026-06-16)
**Status:** Analyzed, deferred (Tier 1 shipped 2026-06-16 — see project-memory "PWA — installable app").
### Context
The PWA install issue was delivered as **Tier 1** (manifest + icons + meta tags), which already meets
every acceptance criterion (installable, correct name/icon, standalone launch from the OS shortcut, no
app-pipeline risk). Tier 2 — a service worker for offline launch and the strongest cross-browser
"app feel" — was intentionally left out. It is **not** required for the install prompt in modern
Chrome/Edge.
### Why deferred (the real risk)
A naïve precaching SW would cache `data/*.json` and **silently defeat the 2026-06-16 live-refresh
system** (the 90s `results.json` poll with `cache:'no-store'` + the `DATA_VERSION` cache-buster) —
open tabs would stop seeing new scores, and `DATA_VERSION` bumps would do nothing. It would also make
the "stale JS module" gotcha (#5) *permanent* (cached assets live until the cache name changes).
### How to implement (if revisited) — constraints, not optional
1. **Never cache `data/*.json`.** Use network-only, or network-first with the cache only as an
offline fallback (so an offline launch shows the last-seen results). The 90s poll must stay the
owner of freshness.
2. **Version the SW cache** with a constant mirroring/derived from `DATA_VERSION`; clean up old caches
on `activate` — otherwise every code deploy risks serving stale JS forever (gotcha #5).
3. **Register at the subpath** (`worldcup2026/sw.js`) so the SW scope matches the deploy (gotcha #7);
keep `start_url`/`scope` relative as they already are.
4. App-shell strategy: cache-first (versioned) for `index.html` + `assets/css` + `assets/js` +
`assets/icons`; precache on `install`.
5. Verify the poll still updates an open tab **with the SW active** (the easy thing to regress).
### When to implement
Only if offline launch / a fuller install experience is actually wanted, and only with the data-cache
exclusion + cache-versioning above. Otherwise Tier 1 is sufficient.
---
## Live Data Refresh — Stale Results Until Page Reload (2026-06-15) ## Live Data Refresh — Stale Results Until Page Reload (2026-06-15)
**Status:** ✅ **Implemented 2026-06-16** (Option A⁺ — "Fixed polling done right"). The analysis below **Status:** ✅ **Implemented 2026-06-16** (Option A⁺ — "Fixed polling done right"). The analysis below

View file

@ -24,7 +24,14 @@ worldcup2026/
├── index.html ★ SPA shell — header, nav tabs (Home, Matches, ├── index.html ★ SPA shell — header, nav tabs (Home, Matches,
│ Groups, Knockout, Stadiums, Stats), hero, dashboard, │ Groups, Knockout, Stadiums, Stats), hero, dashboard,
│ modal container; loads app.js as ES module │ modal container; loads app.js as ES module.
<head> has the PWA block (manifest link, theme-color,
│ favicons, apple-mobile-web-app-* meta)
├── manifest.json PWA web app manifest (name/short_name, standalone,
│ theme/background #081421, icons[]) — relative paths
│ (start_url ".", scope "./") for the subpath deploy
│ favicon.ico Root favicon (16+32, from the trophy logo)
├── assets/ ├── assets/
│ ├── css/ │ ├── css/
@ -59,7 +66,11 @@ worldcup2026/
│ │ │ matches only), hero pulse + overview + goals-by-stage. │ │ │ matches only), hero pulse + overview + goals-by-stage.
│ │ │ PARTIAL (during-cup) — grows into the post-cup plan. │ │ │ PARTIAL (during-cup) — grows into the post-cup plan.
│ │ └── calendar.js .ics export (RFC 5545, CRLF, Blob download) │ │ └── calendar.js .ics export (RFC 5545, CRLF, Blob download)
│ └── images/ Team flag SVGs, stadium placeholders │ ├── images/ Team flag SVGs, stadium placeholders
│ └── icons/ PWA app icons (from the header trophy logo): icon.svg
│ (master + manifest SVG), icon-192/512.png (purpose any),
│ icon-maskable-192/512.png (safe-zone padded),
│ apple-touch-icon.png (180), favicon-16/32.png, favicon.ico
├── data/ All content — REAL WC2026 data since 2026-06-12 ├── data/ All content — REAL WC2026 data since 2026-06-12
│ ├── teams.json 48 real qualifiers: { id, name, flag } (FIFA codes) │ ├── teams.json 48 real qualifiers: { id, name, flag } (FIFA codes)
@ -149,6 +160,7 @@ matches.json time (UTC) ── formatMatchTime(match, stadium, mode)
| Where is simulation state stored/cleared? | `localStorage` key `wc2026_simulation`, via `assets/js/storage.js` | | Where is simulation state stored/cleared? | `localStorage` key `wc2026_simulation`, via `assets/js/storage.js` |
| Where do I change colors/theme? | CSS variables at the top of `assets/css/style.css` | | Where do I change colors/theme? | CSS variables at the top of `assets/css/style.css` |
| Where do I add a stadium? | `data/stadiums.json` + image in `assets/images/` | | Where do I add a stadium? | `data/stadiums.json` + image in `assets/images/` |
| Where do I change the app name / install icon / theme color? | `manifest.json` (name/short_name/theme) + `assets/icons/` (regenerate PNGs from `icon.svg`) + PWA `<meta>` in `index.html` `<head>` |
| How do I replace mock data with real WC2026 data? | `how-update.md` (root) — done 2026-06-12; kept as schema reference | | How do I replace mock data with real WC2026 data? | `how-update.md` (root) — done 2026-06-12; kept as schema reference |
| How do I update scores during the tournament? | `how-refresh-data.md` (root) — daily results.json routine + thirdPlaceAssignment how-to | | How do I update scores during the tournament? | `how-refresh-data.md` (root) — daily results.json routine + thirdPlaceAssignment how-to |

View file

@ -231,6 +231,15 @@ Static web app showing the FIFA World Cup 2026 (Mexico/USA/Canada, 48 teams) —
- **Não tratado (aceito, baixo risco — mudanças raras, poucas/dia):** modal aberto não auto-atualiza (relê no próximo open); re-render durante interação (drag do bracket / digitação no filtro) — filtros sobrevivem (state módulo-level), scroll pode pular. - **Não tratado (aceito, baixo risco — mudanças raras, poucas/dia):** modal aberto não auto-atualiza (relê no próximo open); re-render durante interação (drag do bracket / digitação no filtro) — filtros sobrevivem (state módulo-level), scroll pode pular.
- **Verificado (preview, sem tocar no disco — `window.fetch` interceptado pra simular jogo 16 IRN×NZL finished 30, `visibilitychange` disparando `pollResults`):** dashboard Encerradas 15→16 / Próximas 89→88; hero trocou IRN×NZL→FRA×SEN (jogo 16 virou `over`); Group G recomputou (Irã `1 1 0 0 3 0 +3 3`); bracket(32)/stats(4 tiles)/matches(104) re-renderizaram; **console limpo**. Restaurado o `fetch` real → poll seguinte **auto-revertou** pra 15/89 (prova a assinatura nos dois sentidos). `DATA_VERSION` **não** bumpado (nenhum dado mudou no disco — só código). - **Verificado (preview, sem tocar no disco — `window.fetch` interceptado pra simular jogo 16 IRN×NZL finished 30, `visibilitychange` disparando `pollResults`):** dashboard Encerradas 15→16 / Próximas 89→88; hero trocou IRN×NZL→FRA×SEN (jogo 16 virou `over`); Group G recomputou (Irã `1 1 0 0 3 0 +3 3`); bracket(32)/stats(4 tiles)/matches(104) re-renderizaram; **console limpo**. Restaurado o `fetch` real → poll seguinte **auto-revertou** pra 15/89 (prova a assinatura nos dois sentidos). `DATA_VERSION` **não** bumpado (nenhum dado mudou no disco — só código).
### PWA — installable app (Tier 1, 2026-06-16)
- **O site virou um PWA instalável** (issue "Adicionar suporte a instalação como aplicativo"). Escopo entregue = **Tier 1** (manifest + ícones + meta tags) — atende a TODOS os critérios de aceitação (instalável, nome/ícone corretos, abre standalone pelo atalho do SO). **Service worker / offline ficou de fora de propósito** (Tier 2, registrado em `.agents/issues.md`).
- **Decisão-chave (por quê só Tier 1):** um SW que cacheasse `data/*.json` **quebraria** o poll de 90s + `DATA_VERSION` (live-refresh de 2026-06-16) — abas abertas parariam de ver placares novos. Tier 1 não toca em nada do pipeline de dados/JS → risco zero pro live-refresh. Se Tier 2 entrar, o SW **precisa excluir `data/*.json`** do cache (network-only/network-first) e versionar junto com `DATA_VERSION` (senão piora a gotcha #5 de módulo velho).
- **Arquivos novos:** `manifest.json` (raiz), `favicon.ico` (raiz), `assets/icons/` (icon.svg master + icon-192/512.png `purpose:any` + icon-maskable-192/512.png + apple-touch-icon.png 180 + favicon-16/32.png + favicon.ico). **Nenhum JS mudou.** `index.html` ganhou um bloco PWA no `<head>` (link manifest, `<meta theme-color #081421>`, favicons, `apple-mobile-web-app-capable/-status-bar-style black-translucent/-title "WC 2026 Hub"`).
- **Ícones derivados do logo do header** (o troféu SVG inline) sobre o gradiente escuro `#10243b→#081421` com o dourado `#d4af37`. Fontes em `assets/icons/icon.svg` (any, troféu a ~60%) e `icon-maskable.svg` (troféu a ~46%, dentro da safe-zone do maskable). **Rasterizados com ImageMagick** (`magick -background none icon.svg -resize NxN ...`); `favicon.ico` = 16+32 combinados. **Para trocar o ícone:** editar o(s) SVG e re-rodar os mesmos comandos `magick` (não há build step; é geração de asset 1x).
- **Manifest:** `name "World Cup 2026 Hub"` / `short_name "WC 2026 Hub"` (nome estático, EN — manifest não faz i18n runtime); `display:standalone`; `background_color`+`theme_color` = `#081421` (`--bg-primary`, evita flash branco no splash); `start_url:"."` + `scope:"./"` **relativos** (gotcha #7 — site mora em `…/worldcup2026/`; absoluto quebraria). Nomeado `manifest.json` (não `.webmanifest`) p/ MIME seguro na Hostinger.
- **Deploy:** os arquivos novos (manifest.json, favicon.ico, assets/icons/) **não** estão no `exclude` do `deploy.yml` → sobem normalmente. HTTPS da Hostinger já satisfaz o requisito de PWA.
- **Verificado (preview localhost:8126, contexto seguro):** manifest 200 e parseado (name/short/start `.`/scope `./`/display standalone/theme `#081421`); todos os ícones 200 `image/png`; `favicon.ico` 200; `<meta theme-color>` + apple tags presentes; **console limpo**; app intacto (hero FRA×SEN, 4 cards do dashboard, 16 encerradas/88 próximas — sem regressão visual). **Não testável pelo preview:** o prompt de instalação real / "Add to Home Screen" + ícone no SO — confirmar no Chrome/Edge devtools (aba Application) ou num celular após o deploy.
### How to update real-world data (scores, schedule) ### How to update real-world data (scores, schedule)
Follow `how-refresh-data.md` (project root). In short: Follow `how-refresh-data.md` (project root). In short:
1. Edit `data/results.json` (scores/status) or `data/matches.json` (schedule, rare). 1. Edit `data/results.json` (scores/status) or `data/matches.json` (schedule, rare).

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/icons/favicon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/icons/favicon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
assets/icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
assets/icons/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/icons/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<linearGradient id="bgm" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#10243b"/>
<stop offset="1" stop-color="#081421"/>
</linearGradient>
</defs>
<rect width="512" height="512" fill="url(#bgm)"/>
<!-- Same trophy, scaled smaller so it stays inside the maskable safe zone (inner ~80%) -->
<g transform="translate(88,94) scale(14)" fill="none" stroke="#d4af37" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="5" r="1.8" fill="#d4af37" stroke="none"/>
<path d="M7 8h10v2.3a5 5 0 0 1-10 0V8z"/>
<path d="M7 8.4H4.6a2.6 2.6 0 0 0 2.4 3.4"/>
<path d="M17 8.4h2.4a2.6 2.6 0 0 1-2.4 3.4"/>
<path d="M12 15.3v2.2"/>
<rect x="9" y="18.3" width="6" height="1.6" rx="0.6" fill="#d4af37" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 907 B

18
assets/icons/icon.svg Normal file
View file

@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#10243b"/>
<stop offset="1" stop-color="#081421"/>
</linearGradient>
</defs>
<rect width="512" height="512" fill="url(#bg)"/>
<!-- Header trophy logo (0 0 24 24 art), scaled to fill ~60% of the icon -->
<g transform="translate(40,48) scale(18)" fill="none" stroke="#d4af37" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="5" r="1.8" fill="#d4af37" stroke="none"/>
<path d="M7 8h10v2.3a5 5 0 0 1-10 0V8z"/>
<path d="M7 8.4H4.6a2.6 2.6 0 0 0 2.4 3.4"/>
<path d="M17 8.4h2.4a2.6 2.6 0 0 1-2.4 3.4"/>
<path d="M12 15.3v2.2"/>
<rect x="9" y="18.3" width="6" height="1.6" rx="0.6" fill="#d4af37" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 890 B

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -5,6 +5,18 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="World Cup 2026 Hub — schedule, groups, interactive knockout bracket and stadiums."> <meta name="description" content="World Cup 2026 Hub — schedule, groups, interactive knockout bracket and stadiums.">
<title>World Cup 2026 Hub</title> <title>World Cup 2026 Hub</title>
<!-- PWA -->
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#081421">
<link rel="icon" href="favicon.ico" sizes="32x32 16x16">
<link rel="icon" type="image/svg+xml" href="assets/icons/icon.svg">
<link rel="apple-touch-icon" href="assets/icons/apple-touch-icon.png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="WC 2026 Hub">
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="assets/css/bracket.css"> <link rel="stylesheet" href="assets/css/bracket.css">
<link rel="stylesheet" href="assets/css/stats.css"> <link rel="stylesheet" href="assets/css/stats.css">

46
manifest.json Normal file
View file

@ -0,0 +1,46 @@
{
"name": "World Cup 2026 Hub",
"short_name": "WC 2026 Hub",
"description": "World Cup 2026 Hub — schedule, groups, interactive knockout bracket and stadiums.",
"lang": "en",
"dir": "ltr",
"start_url": ".",
"scope": "./",
"display": "standalone",
"orientation": "any",
"background_color": "#081421",
"theme_color": "#081421",
"categories": ["sports"],
"icons": [
{
"src": "assets/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "assets/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "assets/icons/icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "assets/icons/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "assets/icons/icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
}
]
}