feat(stats): add tournament-to-date stats tab

This commit is contained in:
Lucas Kalil 2026-06-14 21:38:01 -03:00
parent ba81e49eac
commit d5a9dadc5d
9 changed files with 1263 additions and 4 deletions

View file

@ -272,6 +272,27 @@ Follow `how-refresh-data.md` (project root). In short:
- **`assets/js/app.js` `loadData()` appends `?v=${DATA_VERSION}`** to every `data/*.json` fetch (`DATA_VERSION` constant near the top of the file, currently `'2026-06-13-rev1'`). Fixes production browsers/Hostinger caching stale `results.json` after a daily refresh — `cache: 'reload'` only helps the developer's own browser, not real visitors.
- **Must be bumped on every data refresh** — added as step 4 of the daily routine in `how-refresh-data.md`. Format `YYYY-MM-DD-revN`; increment `revN` for same-day re-edits.
### Stats screen — planning (2026-06-14)
- **Plano completo em `.agents/stats-screen-plan.md`** (pós-Copa "tela de estatísticas finais"). Gerado via workflow de 5 sub-agentes (Times/Individuais/Partidas/Curiosidades/UX). **Nada implementado ainda** — só planejamento, aprovado pelo usuário.
- **Escopo aprovado: as 4 camadas** — ✅ dados existentes · 🟡🧩 acréscimos baratos (attendance, cartões y/r, ranking/wcDebut/confederation em teams.json, coords em stadiums.json, backfill do `stats`) · 🔴 dados de jogadores (`players.json` + log de eventos + `awards.json`) · 📝 editorial (`curiosities.json`).
- **REQUISITO DE 1ª ORDEM — degradação graciosa:** quando um dado não existir, a UI **não pode quebrar nem deixar visível ao usuário final que falta algo**. Regra: dado/seção só renderiza com cobertura completa; senão é **removido do DOM** (não placeholder/"—"/"em breve"); chips de sub-nav de seções vazias também somem; `loadData()` deve tolerar fetch de arquivo novo ausente (default vazio, não exceção).
- **Decisões técnicas planejadas:** novo 6º tab `stats``#panel-stats` + `assets/js/stats.js` + `assets/css/stats.css` (segue padrão por-view + import circular com app.js); sub-nav = `<nav>` de âncoras com scrollspy (NÃO um 2º tablist); **sem chart-lib** (SVG/CSS inline, respeita budget <300KB); render preguiçoso por seção; modelo de stats memoizado, re-render de labels no `langchange`. Roadmap AJ com portões de aprovação (AF entregam a tela com dados de hoje; G/H/I acendem camadas 2/3/4).
### Stats tab — PARCIAL durante a copa (2026-06-14, Etapa A+B)
- **Nova 6ª aba `stats`** implementada de forma incremental (plano em `C:\Users\Lucas\.claude\plans\contexto-o-planejamento-foamy-brook.md`). É a **fundação evolutiva** do plano pós-copa (`.agents/stats-screen-plan.md`) — mesmo tab/módulo; seções pós-copa "acendem" depois.
- **Arquivos novos:** `assets/js/stats.js` + `assets/css/stats.css`. Integração: `stats.css` linkado no `<head>`; botão+painel `panel-stats`/`#stats-root` no `index.html`; `'stats'` adicionado a `TABS` em `app.js`; `initStats()` chamado no `try` de `init()`; chaves `nav.stats` + `stats.*` (EN/PT) em `i18n.js`.
- **Filosofia (decidida via /grill-me):** agregados correntes "até agora", **só `status==='finished'`** (consistente com `computeStandings`); "X de 104" é moldura, não lacuna. `stats` opcional (posse/chutes/cartões) entra com **gating por-jogo** (`aggregateTeams` ignora jogo sem o campo). Sem polling — recomputa no load + re-render de labels no `langchange` (modelo memoizado em `let model`).
- **Implementado nesta etapa (A+B):** `aggregateTeams()` (agregação torneio-wide, grupo+mata-mata, própria — `computeStandings` é só por-grupo); hero "pulso" com tiles count-up (IntersectionObserver dispara a animação quando o painel abre, reduced-motion-safe); Overview (jogos/decididas/empates) + chart "gols por fase" (só fases com ≥1 jogo — sem barras zeradas de R32+); link "ver partidas" → `navigateTo('matches')`. Verificado: 27 gols/3.00 média/margem 6/3 clean sheets, EN↔PT, mobile 2×2, console limpo.
- **Seção "Estatísticas por time" (2026-06-14, +/grill-me):** tabela paginada das **48 seleções, 6 páginas fixas de 8** (`PAGE_SIZE`/`COLUMNS` no topo de `stats.js`). 10 colunas ordenáveis (J/V/E/D/GP/GC/SG/Pts/G·J/CS; rótulos curtos reusam `standings.*`, os 2 novos têm `title`). Clique no header faz **toggle desc↔asc** (seta + `aria-sort`), default **GP desc** ao abrir; desempate fixo SG→GP→nome; trocar coluna ou re-ordenar volta à pág. 1. Coluna # = **rank global** 148. Times sem jogo = zeros reais (não lacuna), caem ao fim. Faixa **top-3 leaders** (melhor ataque=GP / defesa=GC entre quem jogou / mais clean sheets) acima — `computeLeaders` só considera `played>0`. Estado `sortKey`/`sortDir`/`teamPage` é módulo-level e **sobrevive ao langchange** (como o zoom do bracket); ordenação/paginação re-renderizam só `#stats-teams-table` (não re-disparam count-ups). Mobile: `.stats-table-wrap` scroll-x com `#`+`Seleção` `position:sticky` (left 0 / 2.5rem). Verificado: sort/toggle/troca-de-coluna/paginação/EN↔PT/mobile-sticky, console limpo.
- **Falta (etapas DG, aguardando aprovação):** líderes posse/chutes/disciplina; recordes auto (maior goleada→modal); comparador time-vs-time; polimento. **Sem** arquivo de resultados (linka p/ Matches). `penalties` ainda não existe em results.json → card de pênaltis só renderiza quando ≥1 disputa ocorrer.
### Tooltips de header + legenda (2026-06-14, +/grill-me)
- **Tooltips nas siglas das tabelas** (Stats "Estatísticas por time" **e** as 12 tabelas de Grupos). Decidido via /grill-me: tooltip custom glass (não `title` nativo), **nome + definição onde ajuda**, e **legenda compacta só no mobile**.
- **`initTooltips()` em `app.js`** (chamado no `init()`): um único balão `.app-tooltip` `position:fixed`, via **delegação de eventos** em `document` (`mouseover`/`focusin` mostram, `mouseout`/`focusout`/`scroll` escondem). Sobrevive a re-renders das tabelas e **nunca é cortado** por overflow/stacking (motivo de não usar `::after` dentro do `.stats-table-wrap` que tem `overflow-x:auto`). Centraliza sobre o elemento, faz clamp na viewport e **flip pra baixo** se não couber acima.
- **Como dar tooltip a um header:** adicionar classe `has-tip` + `data-tip="<texto>"` + `aria-label="<sigla> — <texto>"` (o aria-label cobre leitor de tela, já que o balão é visual). Textos em `i18n.js` no namespace **`tip.*`** (EN/PT), reusados pelas duas tabelas (`tip.played/won/drawn/lost/gf/ga/gd/pts/gpg/cs`).
- **Legenda mobile:** `<p class="stats-legend">` com pares `<b>sigla</b> = texto``display:none` no desktop, `flex` em `≤600px` (estilo em `stats.css`). Uma por tabela: `legendHTML()` em `stats.js` (usa `COLUMNS`) e em `groups.js` (lista fixa das 8 colunas). Cobre o touch, onde o hover não dispara.
- CSS do tooltip/legenda vive em `stats.css` (carregado global, então `.has-tip`/`.app-tooltip`/`.stats-legend` valem também na aba Grupos).
### How to add a UI label
1. Add the key to both `en` and `pt` dicts in `assets/js/i18n.js`.
2. Use `t("key")` at the render site — never hardcode the string.