world-2026-hub/.agents/stats-screen-plan.md

265 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Plano de Implementação — Tela de Estatísticas Finais (World Cup 2026 Hub)
> Documento de planejamento (NÃO é implementação). Gerado em 2026-06-14 após workflow multi-agente
> (5 perspectivas) + aprovação da lista de dados pelo usuário. Escopo aprovado: **as 4 camadas**
> (✅ existentes · 🟡🧩 acréscimos baratos · 🔴 dados de jogadores · 📝 editorial).
>
> **PRINCÍPIO REITOR (requisito explícito do usuário):** *degradação graciosa*. Quando um dado não
> puder ser obtido, o tratamento deve ser elegante — **não pode quebrar a UI nem deixar visível para o
> usuário final que algo está faltando**. Sem `—`, sem cards vazios, sem "dados em breve". A regra é:
> **um dado/seção só aparece quando está completo o bastante para ser apresentado como autoritativo;
> caso contrário é removido do DOM** (não escondido com placeholder, *removido*). Ver §0 e §6.
---
## 0 · Camada de degradação graciosa (a espinha dorsal de todo o resto)
Esta é a peça arquitetural mais importante do plano. Tudo abaixo se apoia nela.
### 0.1 Contrato de disponibilidade
Cada estatística, card, linha de tabela e sub-seção declara um predicado `isAvailable(model)`:
- **Sub-seção inteira sem dados** → a seção **não é renderizada** e seu chip no sub-nav **é removido**
(navegação nunca aponta para o vazio).
- **Card/recorde individual sem dados** → o card não é inserido; o grid reflui naturalmente.
- **Célula opcional numa linha** (ex.: xG de um time sem feed) → a coluna some para todos OU a célula
cai para um valor neutro só se isso não denunciar ausência. Preferência: **a coluna inteira só existe
se todos os times tiverem o dado.**
- **Agregado sobre `stats` esparso** (posse/chutes/cartões) → só renderiza com **cobertura completa**
(todos os jogos da amostra relevante preenchidos). Sem cobertura → **escondido, sem disclaimer**.
(Decisão: o usuário NÃO quer "baseado em N jogos" visível. Ou faz backfill total, ou não mostra.)
### 0.2 Carga de dados tolerante a falha
`loadData()` em `app.js` passa a buscar os novos JSON; cada fetch novo é **opcional**:
arquivo ausente / 404 / JSON inválido → `console.warn` (dev) + **default vazio** (`[]`/`{}`),
**nunca** exceção que derrube o app. Os 6 arquivos atuais continuam obrigatórios.
### 0.3 Mídia com fallback
Fotos de jogadores / bandeiras quebradas → `onerror` cai para iniciais (monograma) ou silhueta
genérica. Nunca ícone de imagem quebrada.
### 0.4 Camadas se auto-desligam
- Sem `players.json`/eventos → **toda** a seção Jogadores + Prêmios + comparador de jogadores +
recordes de tempo de gol somem (chips inclusive). A tela continua completa só com dados de time.
- Sem `curiosities.json` → Recordes mostra só os auto-deriváveis; nada de "em breve".
- Sem `attendance` → recordes de público somem; o resto do Overview fica intacto.
Resultado: a mesma base de código entrega uma tela coerente e "cheia" com **só os dados existentes
hoje**, e vai "acendendo" seções conforme os dados das camadas 2/3/4 forem entrando — sem nenhuma
data de corte nem buraco visível.
---
## 1 · Arquitetura da tela
### 1.1 Estrutura de componentes (segue o padrão por-view existente)
```
index.html
├─ nav.tabs + <button data-tab="stats" ...> (6º tab, após Stadiums)
└─ <section id="panel-stats" class="panel" role="tabpanel" hidden>
<h2 class="section-title" data-i18n="nav.stats">…</h2>
<div id="stats-root"><p class="placeholder glass" data-i18n="app.comingSoon"></p></div>
assets/js/stats.js (NOVO módulo — espelha schedule.js/groups.js/bracket.js)
- initStats() chamado por app.js (mesmo padrão de import circular intencional)
- buildStatsModel(data) deriva TODO o modelo computável 1x, memoizado
- renderStats() / renderSection(id) render preguiçoso por seção (IntersectionObserver)
- escuta langchange (re-render só de labels) / favchange / timemodechange
assets/css/stats.css (NOVO — como bracket.css; componentes da tela de stats)
```
### 1.2 Rotas
- Hash route `#stats` + persistência `wc2026_prefs.lastTab`**de graça** (roteamento por hash já existe).
- Sub-navegação interna **não** é rota nem segundo `tablist`: é uma `<nav>` de âncoras (`#stats-teams`…)
com *scrollspy* via IntersectionObserver (evita conflito de setas com o `tablist` do topo — ver §3).
- Opcional (nice-to-have): deep-link `#stats=players` que faz scroll até a seção ao carregar.
### 1.3 Estado global
- **Nenhum estado persistente novo é obrigatório.** O modelo de stats é derivado de `getData()` e
**memoizado em memória** no módulo (`let statsModel`); recalcula só se os dados mudarem (não mudam
em runtime). `langchange` re-renderiza labels, **não** recomputa valores (nomes não se traduzem).
- Reusa estado existente: `wc2026_favorites` (destaque dourado em linhas/cards do time favorito),
`wc2026_prefs.timeMode` (arquivo de resultados + modal).
- Opcional: `wc2026_prefs.statsSort` p/ lembrar a última ordenação de tabela (baixa prioridade).
### 1.4 Integração com o existente (reuso, não reinvenção)
- `openMatchModal(matchId)` → cards de recorde, linhas do arquivo e qualquer linha ligada a um jogo.
- `resolveBracketTeams`, `computeStandings`, `getBracketTree`, `calculateChallengeScore` → reusados
para ranking, caminho do campeão, fase alcançada.
- `t(key)`, eventos `langchange`/`favchange`/`timemodechange`, `.glass`, `.slide-up`, `.container`.
- Cache-busting é automático: `loadData()` já anexa `?t=Date.now()` a cada fetch de `data/*.json`,
então novos arquivos de dados não exigem nenhum passo de versão (`DATA_VERSION` foi removido em 2026-06-18).
---
## 2 · Fontes de dados
### 2.1 Já existe (Camada 1 — ~70% da tela, zero coleta)
`teams.json`, `groups.json`, `matches.json`, `results.json` (placar/status/penalties + `stats`
opcional), `stadiums.json` (capacidade, timezone), `bracket-config.json`. Computados: standings,
bracket, challenge.
### 2.2 Precisa coletar/calcular
| Camada | Arquivo / campo novo | Schema | Destrava | Esforço |
|---|---|---|---|---|
| 🟡 2 | `results.json``attendance` | inteiro por jogo | público total/médio/ocupação, maior público | dado público FIFA |
| 🟡 2 | `results.json``cards: {home:{y,r}, away:{y,r}}` | split amarelo/vermelho | disciplina completa, fair-play | reformatar campo atual |
| 🟡 2 | `results.json``decidedIn` | `"regulation"\|"ET"\|"penalties"` | separar prorrogação de pênaltis | trivial |
| 🟡 2 | `results.json` → backfill `stats` nos 104 | posse/chutes existentes | posse/chutes/conversão como agregado confiável | médio (104 jogos) |
| 🟡 2 | `results.json``shotsOnTarget`, `passes`, `passAccuracy` (opc.) | nível time | chutes no alvo, passes | opcional |
| 🧩 2 | `teams.json``ranking`/`seed`, `wcDebut`, `confederation` | por time | zebras, Cinderela, estreantes, desempenho por confederação | trivial 1x |
| 🧩 2 | `stadiums.json``lat`,`lng` | por estádio | distância total percorrida | trivial 1x |
| 🔴 3 | `players.json` | roster mínimo `{id,name,team,position,birthDate,shirt?}` — só envolvidos | base de todos os individuais | médio |
| 🔴 3 | `player-events.json` | log append-only `{type:goal\|card\|ownGoal, player, team, matchId, minute, goalType?, assist?, card?}` | artilharia, hat-tricks, tempo de gol, disciplina | **alto, ~400+ linhas** |
| 🔴 3 | `awards.json` | `{goldenBall, goldenBoot, goldenGlove, bestYoung, squadOfTournament[], goalOfTournament}` | prêmios FIFA + Seleção do Torneio | trivial (1x pós-final) |
| 🔴 3 | (opc.) `keeper-stats.json` | defesas/clean-sheets por goleiro | Luva de Ouro detalhada | opcional |
| 📝 4 | `curiosities.json` | array de cards `{id,type,priority,titleEN/PT,bodyEN/PT,matchId?,teamId?,mediaRef?,statRef?}` | gol mais bonito, VAR, histórias | redação bilíngue |
| 📝 4 | `all-time-baselines.json` (ou embutido) | recordes históricos fixos p/ comparação | painel "esta Copa vs história" | trivial 1x |
> **Decisão de modelagem (jogadores):** usar **roster + log de eventos** (não agregados pré-somados).
> O log deriva tudo (artilharia, assistências, hat-tricks, gol mais rápido/jovem/tardio) com o mesmo
> padrão do projeto (results.json é por jogo; o código computa o resto). Minutos jogados e defesas de
> goleiro — caros por evento — ficam como **agregado opcional** (`keeper-stats.json`) ou são omitidos.
> Entrada **incremental por rodada** (encaixa no fluxo diário `/update-worldcup`), não num lote único.
### 2.3 Fora de escopo v1
- **xG**: não existe feed; exige provedor externo. Slot de UI previsto, **escondido** até haver dado.
- Distância percorrida por jogador, dribles, sprints: custo de manutenção alto demais para site estático.
---
## 3 · UI/UX — Proposta de navegação (wireframe textual)
**Colocação:** novo 6º tab `Stats` / `Estatísticas`, último (é o epílogo do torneio). Reusa o
`tablist` WAI-ARIA existente (roving tabindex, Setas/Home/End) — zero paradigma novo.
**Sub-navegação:** barra de chips *sticky* (scrollspy) abaixo do hero — **`<nav>` de âncoras, não um
segundo tablist** (evita conflito de Setas com o tab do topo; é também o gatilho do render preguiçoso).
Chips: **Overview · Times · Jogadores · Recordes · Comparador · Arquivo** (chips de seções vazias somem).
```
┌─ #panel-stats ───────────────────────────────────────────────────────────┐
│ ╔═ HERO "o veredito" (glass, slide-up) ════════════════════════════════╗ │
│ ║ 🏆 CAMPEÃO [bandeira] vice · 3º · Chuteira de Ouro · Seleção ║ │
│ ║ [ 172 ] [ 2.68 ] [ 31 ] [ 3.1M ] ← tiles count-up ║ │
│ ║ Gols Gols/jogo Pênaltis Público ║ │
│ ╚═══════════════════════════════════════════════════════════════════════╝ │
│ ┌─ SUB-NAV sticky (scrollspy) ● Overview Times Jogadores … Arquivo ───┐ │
│ ═══ #stats-overview ═══ cards (Partidas/Média/Cartões/Clean sheets) + │
│ CHART "gols por fase" (barras SVG, reveal) + "gols por rodada" │
│ ═══ #stats-teams ═══ filtros[confederação▾ fase▾ ⌕] + │
│ LEADERBOARD ordenável (#, time, P W E D, GF GA GD, Pts, [xG]) + │
│ cards: maior goleada, caminho do campeão, forma V/E/D │
│ ═══ #stats-players ═══ PÓDIO top-3 (Chuteira de Ouro, count-up) + │
│ chips[Artilheiros·Assist·Cartões·Defesas] → troca corpo da tabela +│
│ bloco PRÊMIOS + SELEÇÃO DO TORNEIO (gráfico de formação) │
│ ═══ #stats-records ═══ grid de record-cards (auto) + faixa "ESTREIAS DO │
│ FORMATO 48" (destaque) + cards editoriais (curiosities.json) │
│ ═══ #stats-comparator ═══ [A▾] vs [B▾] toggle Times/Jogadores + │
│ barras divergentes espelhadas (anima na escolha) │
│ ═══ #stats-archive ═══ 104 resultados, accordion por fase, filtros/sort, │
│ linha → openMatchModal() (preguiçoso, por último) │
└───────────────────────────────────────────────────────────────────────────┘
```
Acima da dobra (desktop): hero veredito + 4 tiles + sub-nav. O resto é recompensa de scroll.
**Componentes reutilizáveis:** `stat-card` (tile count-up), `leaderboard-table` (ordenável, a11y,
linha favorita dourada), `podium` (top-3), `record-card` (clica → modal), `chart-panel` (SVG/CSS +
`<details>` tabela alternativa), `comparator`, `filter-bar`, `section-nav` (scrollspy),
`results-archive`, `chip-tabs` (toggle intra-seção via `aria-pressed`, não tablist).
**Interações/animações** (todas atrás de `prefers-reduced-motion`):
count-up dos números (IntersectionObserver, ~900ms ease-out), barras crescendo da base,
linha com `stroke-dashoffset`, FLIP no re-sort, duelo de barras no comparador, reveal por seção.
**Responsividade:** ≤767 hero empilha + tiles 2×2, sub-nav scroll horizontal com snap, tabelas com
rank+nome congelados e scroll-x numa região focável; 7681439 grids 2-col; 1440+ `.container`
(`min(1200px,100%-2rem)`), grid 4-col, hero numa linha.
**Acessibilidade:** `<table>` real com `<caption>`/`scope`/`aria-sort` (header ordenável = `<button>`);
sub-nav é `<nav>` (não tablist); charts com alternativa textual (`<details>` tabela + `aria-label`);
count-up: valor final é o DOM real, tween só visual (`aria-live="off"`); foco visível via
`:focus-visible` existente.
---
## 4 · Stack e dependências
**Princípio:** zero dependência nova de runtime. Mantém o mandato do projeto (vanilla ES Modules, sem
framework/bundler/CDN) e o orçamento de **JS < 300KB (hoje ~74KB)**.
- **Gráficos: SEM biblioteca.** Barras/linhas/donut feitos à mão em **SVG inline + CSS** (casa com a
estética e com o orçamento). Se um gráfico realmente exigir mais, `import()` dinâmico de micro-lib
(<poucos KB) quando a seção entra na viewport **postura padrão: peso zero de chart-lib.**
- **Animação:** reusa `animations.css` + IntersectionObserver; helper `countUp()` em vanilla
(`requestAnimationFrame`). Tudo desligado em `prefers-reduced-motion`.
- **i18n:** novo namespace `stats.*` em `i18n.js` (dicts EN **e** PT). Nomes de dados não se traduzem.
- **CSS:** novo `assets/css/stats.css` (linkado no `<head>` como `bracket.css`), usando os tokens
existentes (`--accent-gold`, `--glass-bg`, `--radius`, etc.).
- **Tooling opcional (não-shippado):** script Node sem deps para gerar/validar agregados de jogadores
a partir do log de eventos facilita a entrada manual. Fora do bundle do site.
---
## 5 · Roadmap de implementação (incremental, com portões de aprovação)
> Convenção do projeto: 1 etapa por vez, resumo + aprovação antes da próxima. Esforço: **S** ≈ meia
> sessão · **M** ≈ 1 sessão · **L** ≈ ~2 sessões. Etapas AF entregam uma tela completa só com dados
> existentes; GI acendem as camadas 2/3/4; J é polimento.
| # | Etapa | Entrega | Camada | Esforço |
|---|---|---|---|---|
| **A** | **Scaffolding + motor de degradação** | tab, `#panel-stats`, `stats.js`, `stats.css`, namespace `stats.*`, render preguiçoso, scrollspy, `loadData()` tolerante a falha, contrato `isAvailable` 0) | infra | **M** |
| **B** | **Overview + Hero** | veredito (campeão/vice//), 4 tiles count-up, gráfico gols-por-fase e gols-por-rodada | 1 | **M** |
| **C** | **Estatísticas de times** | `leaderboard-table` ordenável+a11y, **ranking final 148 (cadeia de desempate)**, cards (maior goleada, caminho do campeão, forma, splits, clean sheets, sequências) | 1 | **L** |
| **D** | **Recordes + Estreias do formato 48** | record-cards auto-deriváveis + faixa de destaque "104 jogos / maior caminho à final / Round of 32 / melhor / campeão da nova era" | 1 | **M** |
| **E** | **Arquivo de resultados** | 104 jogos navegáveis, filtros/sort, linha modal (reusa padrões de `schedule.js`) | 1 | **M** |
| **F** | **Comparador de times** | seletor A vs B + barras divergentes animadas | 1 | **M** |
| **G** | **Camada 2 — dados baratos** | estende `results.json` (attendance, cards y/r, decidedIn), `teams.json` (ranking/wcDebut/confederation), `stadiums.json` (coords); backfill `stats`; liga recordes de público, disciplina, zebras, distância | 🟡🧩2 | **M** + entrada de dados |
| **H** | **Camada 3 — jogadores** | `players.json`+`player-events.json`+`awards.json`; pódio artilharia, chips (assist/cartões/defesas), bloco de prêmios, **Seleção do Torneio** (formação), comparador de jogadores, recordes de tempo de gol | 🔴3 | **L** (maior) + entrada contínua |
| **I** | **Camada 4 — editorial** | `curiosities.json` + `all-time-baselines.json`; render de cards editoriais + painel "esta Copa vs história" | 📝4 | **M** + redação |
| **J** | **Polimento** | auditoria responsiva/a11y, performance (lazy, sem blur em cards repetidos), Lighthouse, README + i18n review | todas | **M** |
Caminho mínimo para uma tela publicável e bonita: **A→B→C→D→E→F** (tudo com dados de hoje).
G/H/I são aditivos e podem entrar em qualquer ordem conforme os dados aparecem (graças a §0).
---
## 6 · Pontos de atenção (edge cases, dados incompletos, fallbacks)
1. **Torneio ainda não acabou (hoje 2026-06-14).** A tela é pós-Copa. Campeão/ranking existem após
a final o **hero veredito não renderiza** enquanto `FINAL` não estiver `finished`. Opção: manter o
tab **oculto** até a final terminar, ou deixá-lo "acender" progressivamente. Decidir antes da Etapa B.
2. **`stats` esparso (hoje ~9/104).** Agregados de posse/chutes/cartões aparecem com **cobertura
completa**; senão ficam escondidos (sem disclaimer requisito do usuário). Backfill é a Etapa G.
3. **`cards` é amarelo hoje.** Índice de disciplina/fair-play e "time mais faltoso" ficam
incompletos até o split y/r (Etapa G). Antes disso: rotular como "amarelos" ou esconder.
4. **Convenção de V/E/D no mata-mata.** Empate decidido nos pênaltis conta como empate (para gols) mas
vitória/derrota (para avanço). Definir e documentar na camada de dados antes de somar retrospectos.
5. **Desempate do ranking 148.** Cadeia determinística explícita: fase alcançada pontos SG GP
(fallback id). Documentar; sem isso o ranking não é reproduzível.
6. **Burden e confiabilidade dos dados de jogador.** ~400+ linhas manuais; risco de typo/evento
perdido. Usar **uma fonte autoritativa** (site oficial FIFA), entrada incremental por rodada. UI
esconde seções/linhas sem dado 0) nunca mostra lacuna.
7. **Editorial bilíngue.** Todo card de `curiosities.json` precisa EN **e** PT; se faltar um idioma,
*fallback* para o outro (não renderiza em branco).
8. **Performance.** Render preguiçoso por seção; **sem `backdrop-filter` em cards repetidos** (custo de
paint regra vigente no projeto); charts em SVG/CSS sem lib; memoizar o modelo.
9. **Cache-busting & deploy.** Automático via `?t=Date.now()` em cada fetch (sem `DATA_VERSION` a
bumpar removido em 2026-06-18); novos
arquivos em `data/` **são** deployados (bom); `.agents/` (este plano) é excluído (bom). Paths
relativos para fotos de jogador (gotcha #7 subpath Hostinger/Pages).
10. **Não quebrar o existente.** Manter o padrão de import circular com `app.js`; novo tab não pode
alterar o contrato do `tablist`/roteamento atual.
11. **Mídia ausente.** Fotos de jogador/bandeiras `onerror` para monograma/silhueta; nunca imagem
quebrada.
12. **xG sem feed.** Coluna/seção xG existe se todos os times tiverem o dado; senão, removida (não
mostrar coluna pela metade).
---
## Apêndice — proveniência
Consolidado de 5 sub-agentes (Times, Individuais, Partidas, Curiosidades, UX). Lista completa de
métricas com tags de disponibilidade está no histórico da sessão de 2026-06-14 (Fase 1). Escopo
aprovado: 4 camadas + degradação graciosa como requisito de primeira ordem.