No ar · coleta até 01/11 dados de 30/10/2026, 21:02
PROJETO · HISTÓRICO DE VERSÕES

Versões

Todas as mudanças importantes do projeto, em ordem reversa cronológica. Formato segue Keep a Changelog; versões em SemVer.

Histórico de versões

Registro cronológico de tudo que mudou no painel — dados, metodologia e interface —, da versão mais recente para a mais antiga.

Página 11 de 14 · 167 versões no total.

  1. v4.3.0 2026-05-10 Filtro estrutural de URLs duplicadas + dedup retroativo

    Fecha o gap de duplicação histórica do banco. normalize_url em db_utils.py ganhou 4 regras novas, todas determinísticas (sem custo LLM). Padrões mapeados a partir das duplicatas reais no banco antes do fix.

    Changed

    • normalize_url agora trata 4 padrões adicionais de URL duplicada:
      • www. strip do hostname (www.estadao.com.br → estadao.com.br)
      • /amp/ strip quando primeiro segmento do path (/amp/politica/x → /politica/x, padrão Google AMP)
      • srsltid query string adicionado a _STRIP_PARAMS (Google Search redirect, comum no Estadão)
      • Force https no scheme (cobre matérias servidas em http+https da mesma fonte; ex.: em.com.br)
    • brave.py agora aplica normalize_url antes de fazer SELECT/INSERT de articles.url. Era o único coletor sem normalização — bug histórico que era responsável por boa parte das duplicatas.

    Added

    • scripts/dedupe_articles_retroactive.py — varre o banco, agrupa por URL canônica, mantém o de menor id (mais antigo no banco), reassocia article_candidates/topics/article_zeitgeist/article_zeitgeist_processed dos duplicados pro mantido (via INSERT OR IGNORE), atualiza URL canônica no mantido, deleta os duplicados. Snapshot defensivo do DB antes de qualquer escrita. --apply pra efetivar; default é dry-run.
    • tests/test_normalize_url.py — 16 testes unittest cobrindo todos os padrões novos + casos de canto (URL vazia, hostname com subdomínio real preservado, /foo/amp/x que NÃO é o padrão Google AMP).

    Notes

    • Varredura retroativa de 10/05/2026: 1.615 grupos de duplicatas → 1.801 artigos removidos (de 18.139 → 16.338, redução de 9,9%). 1.139 URLs atualizadas pro canônico. 290 vínculos article_candidates movidos. Snapshot defensivo: eleicoes2026.db.dedupe-20260510-134653.
    • 175 pares residuais sobraram (mesmo título no mesmo veículo) — casos de paths estruturalmente diferentes (ex.: InfoMoney serve a mesma matéria em /live/ e /politica/). Fora do escopo de URL normalization; exigiria match por título+data+corpo.

  2. v4.2.0 2026-05-09 Página de pré-candidato ganha 4 dimensões novas

    Added

    • Delta semana atual vs anterior na grade de KPIs do candidato — novo card com volume dos últimos 7 dias e variação (↑/↓ %) em relação aos 7 dias anteriores. Janela termina na última data com dado no banco (em vez de today calendarial), evita falsos zeros quando o pipeline está atrasado. Caso ilustrativo: Lula em 09/05 mostra 2.351 ↓ 1.9% vs 2.396 (semana anterior).
    • Espectro editorial da cobertura em barras horizontais — distribui os artigos sobre o candidato por nível de editorial_lean_detail (extrema-esquerda → esquerda → centro → direita → extrema-direita), com cores fixas pra cada posição do espectro. Resposta a "de onde vem a cobertura sobre X". Dado já estava no banco, faltava render na página individual.
    • Aderência a agendas globais (Zeitgeist) em barras — volume do candidato em cada um dos 6 clusters globais (Clima, Energia, IA, Geopolítica, Bioeconomia, Democracia). Conecta com a seção fixa Zeitgeist que passou a ser obrigatória nos resumos editoriais semanal e mensal a partir da mesma data. Reaproveita schema de article_zeitgeist.
    • Mais comparado(a) na cobertura com — top 5 outros pré-candidatos que aparecem no mesmo artigo que X. Filtra status='dropped', mínimo 3 artigos compartilhados. Cada item linka pra página do co-citado, criando navegação cruzada entre candidatos. Versão simplificada da rede de co-citação que existe em /comparativo (lista, não grafo).

    Notes

    • Todas as 4 dimensões respeitam o recorte oficial (MIN_PUBLISHED_DATE='2026-03-25') e filtram verified=1.
    • Páginas de candidatos de baixo volume (Samara Martins, Rui Costa Pimenta) renderizam em <200ms — seções vazias são suprimidas via {% if %} no template, não quebram visualmente.
    • CSS novo confinado a app/static/css/app.css (delta classes + lean bars). Cache-busting via STATIC_VERSION cobre o deploy.

  3. v4.1.2 2026-05-03 Marca SapiensLabs no rodapé + cache-busting

    Changed

    • Marca SapiensLabs reposicionada para o rodapé da sidebar — antes ocupava o topo em wordmark vertical gigante (SAPIENS / LABS empilhados, ~280px). Agora vai como assinatura discreta no fim da navegação: wordmark horizontal SAPIENSLABS em até 180px, eyebrow micro "SOBRE ARTES E TÉCNICAS", separado do menu por border-top e padding generoso (margin-top: 56px, padding: 24px 28px 28px). Bloco inteiro vira link clicável para https://sapienslabs.com.br/ (target=_blank), com hover suave (opacity: 0.6).
    • Estrutura semântica: <aside> agora termina em <div class="sl-sidebar__footer"><a class="sl-sidebar__brand">…</a></div> — wrapper dedicado separa spacing (footer) do link (brand), evita conflitos com <a> inline default.

    Added

    • Cache-busting nas folhas de estilo — base.html anexa ?v={{ static_version }} em design-system.css e app.css. STATIC_VERSION é calculado uma vez no startup do uvicorn como int(mtime(app.css)). Razão: Cloudflare cacheia CSS por 4h (max-age=14400); hard reload do browser não bate na CF edge. Sem cache-busting, mudanças visuais ficavam invisíveis até o TTL expirar. Daqui pra frente, qualquer alteração em app.css muda o stamp e invalida cache automaticamente.

    Notes

    • Marco de maturidade: dashboard considerado produto estável. Pós-cutover Streamlit→FastAPI rodando há 1 dia sem incidentes; UI tunada; LGPD coberto; GA4 + CWA ativos; cache-busting fecha o gap de iteração visual rápida.

  4. v4.1.1 2026-05-02 GA4 + Política de Privacidade

    Added

    • Google Analytics 4 (G-DJ3D4RT0CL) — mesmo measurement ID do sapienslabs.com.br, para visão consolidada de audiência. Snippet em base.html com anonymize_ip: true e respeito ao cabeçalho Do Not Track (DNT=1 não dispara o tag). Cloudflare Web Analytics permanece ativo em paralelo (cookieless, via beacon do edge).
    • Página /privacidade — política de privacidade dedicada ao painel, aderente à LGPD, em pt-BR. 13 seções: responsável, dados tratados, finalidades, base legal, compartilhamento (Cloudflare + Google), transferência internacional (Art. 33-36), direitos do titular (Art. 18), retenção, segurança (Art. 48), crianças e adolescentes (Art. 14), link cruzado para a política do site principal, atualizações, contato (ANPD).
    • NAV grupo "Sobre" ganhou link "Privacidade" (numeral 12). Sitemap.xml passa a incluir /privacidade.

    Reescrita completa do dashboard, abandonando Streamlit em favor de FastAPI + Jinja2 + Plotly. Streamlit (até v3.11.1) cobrava preço alto em CSS-fight: markup proprietário, classes geradas tipo st-emotion-cache-*, componentes que ignoravam regras do tema. A nova stack consome o Design System SapiensLabs canônico de raiz, com URLs limpas, SEO real e sidebar custom.

    Added

    • FastAPI 0.115.6 + Jinja2 3.1.4 + Plotly 6.7.0 + uvicorn 0.32.1 — nova stack do dashboard. Roda em :18504 na LXC-121, expõe https://eleicoes2026.sapienslabs.com.br via Cloudflare tunnel.
    • 14 rotas server-rendered: /, /cenario-geral, /cenario-mensal, /cenario-semanal, /pre-candidatos, /pre-candidatos/{slug}, /comparativo, /radar, /espectro, /zeitgeist, /analises, /metodologia, /changelog, e handler 404 com lista de rotas válidas.
    • Home editorial (capa de jornal): hero + KPIs + week+featured + análise + recents + CTAs.
    • db_cached decorator em app/core/data.py — cache em memória process-local que invalida quando os.stat(DB_PATH).st_mtime muda. Snapshot atômico do pipeline (3x/dia: 07h/14h/21h BRT) refresca o cache naturalmente sem coordenação. Aplicado em 14 funções (helpers de janela + funções pesadas como comparativo_*, espectro_data, zeitgeist_data, KPIs).
    • /robots.txt e /sitemap.xml dinâmicos — sitemap inclui 12 rotas estáticas + 1 entry por candidato ativo (filtra droppeds via candidate_index()). Submetido ao Google Search Console.
    • Plotly 3.0.1 self-host em app/static/js/plotly-3.0.1.min.js. Elimina dependência de cdn.plot.ly, reaproveita conexão HTTP/2 do mesmo domínio, Cloudflare cacheia no edge GRU (cf-cache-status: HIT consistente).
    • Cascata de tema: localStorage > prefers-color-scheme > horário (7-19h = light), inline no <head> de base.html. Toggle no canto inferior persiste em localStorage.
    • WOFF2 self-host das 4 famílias do DS (Antonio + Archivo Narrow + Inter + Fira Mono) em app/static/fonts/.
    • DS canônico self-host em app/static/css/design-system.css (v1.5.24) com override LOCAL em app/static/css/app.css para --fg1/--fg2/--fg3 em [data-theme="light"] (canônico só tem dark; TODO: promover ao canônico).
    • infra/systemd/eleicoes-dashboard-fastapi.service — systemd unit canônica versionada no repo.
    • uvicorn --workers 2 — paralelismo entre workers (LXC-121 tem 2 cores). Validado: 2 requests /comparativo simultâneos servidos em 7s + 7s (era 7s + 14s sequencial).

    Changed

    • Dashboard agora roda em /opt/eleicoes2026-fastapi/ com venv dedicado (Python 3.11.2) — separado do /opt/eleicoes2026/ que mantém pipeline + Streamlit-LEGACY.
    • Cloudflare tunnel ingress apontando agora pra :18504 (era :18503). Configurado no Zero Trust dashboard (config.yml local do cloudflared continua morto).
    • Tailscale Serve :8504 ativo (aditivo, sem reset para evitar incidente do 01/05). :8503 desligado depois do cutover.
    • home_kpis() filtra status != 'dropped' — Pré-candidatos antes contava 16 (incluindo droppeds), agora 12.
    • _kpis_for_window filtra droppeds via JOIN candidates c ON c.slug = ac.candidate WHERE c.status != 'dropped' — Cenário Mensal/Semanal coerentes com Home.
    • home_kpis() respeita MIN_PUBLISHED_DATE='2026-03-25' — KPIs antes contavam todos artigos do banco (14.289), agora respeitam o recorte oficial (14.171, mesmo que Cenário Geral period=all).
    • Lançamento de versão atualiza changelog em CHANGELOG.md + app/templates/pages/changelog.html (era CHANGELOG.md + page_changelog() no dashboard.py).
    • Tag de versão: v4.1.0-fastapi (sufixo -fastapi para distinguir da v4.0.0 já existente — migração Streamlit multipage de 24/04/2026, agora arquivada conceptualmente).

    Removed

    • Streamlit em produção. Serviço eleicoes-dashboard.service está inactive mas enabled (rede de segurança para rollback rápido). Cleanup definitivo (disable + arquivamento de src/dashboard.py / src/app.py / src/pages/) previsto após 1-2 semanas estáveis.
    • Tailscale Serve :8503 — backend Streamlit estava parado, gerava 502 fantasma na tailnet.

    Fixed

    • HTTP 502 imediato após cutover Cloudflare — uvicorn estava em --host 127.0.0.1, cloudflared chega pelo IP da LXC (não loopback). Fix: --host 0.0.0.0 na unit + restart. Lição embutida no CLAUDE.md.
    • COUNT(DISTINCT ac.article_id) com LEFT JOIN articles + filtro de data no JOIN ignora o filtro — trocado por COUNT(DISTINCT a.id) que respeita NULL do JOIN não-batido. Diferença observada de ~1000 artigos no candidates_grid (Cenário Geral) e cenario_mensal_candidates.
    • Plotly via defer quebra com inline Plotly.newPlot() — defer carrega o script externo depois do parse do body, então as chamadas inline executam antes de window.Plotly existir. Solução: Plotly síncrono no <head> de base.html, todos os charts geram com include_plotlyjs=False.
    • grid-template-columns: 280px 1fr causa grid-blowout quando filhos têm width fixo (Plotly SVG). Trocado para minmax(0, 1fr) com box-sizing: border-box global e overflow-x: clip no html/body como salva-vidas.
    • update_layout(_base_layout(), showlegend=False) dava TypeError: got multiple values for showlegend** — _base_layout() já retornava showlegend=True. showlegend=False movido pro layout dict antes do update_layout.
    • {% for item in group.items %} em Jinja resolvia o método builtin .items do dict — chave renomeada para links.

    Performance

    RotaAntes (Streamlit)Depois (FastAPI cache miss)Cache hit
    /comparativo~15-20s (Streamlit re-roda tudo)7s0.7s (10×)
    /espectro~6-8s2.6s0.65s (4×)
    /zeitgeist~3s0.9s0.62s
    /radar~2s0.77s0.57s
    /~1.5s0.83s0.55s

    Cache invalida 3x/dia ao final de cada coleta (mtime do snapshot público muda).

    Notes

    • Cutover em produção 02/05/2026 ~01h00 BRT. Big-bang: Cloudflare ingress trocado de :18503 → :18504, Streamlit parado em seguida, tag v4.1.0-fastapi pushed. Streamlit fica enabled mas inactive por 1-2 semanas como rede de segurança.
    • Custo realizado: ~14h efetivas distribuídas em duas grandes janelas (01-02/05). Estimativa original de ~50h em 6 fases foi superada graças ao trabalho prévio de DS alignment (etapas A-H aplicadas no Streamlit em 01/05) que mapeou o canônico antes da migração.
    • Cloudflare Managed Content injection detectado em /robots.txt no público — CF injeta header com AI Content Signals + bloqueio de GPTBot/ClaudeBot/Amazonbot/Google-Extended/etc. antes do nosso conteúdo. Nosso Sitemap: permanece visível. Proteção bem-vinda contra training de LLMs sem ação explícita.

  5. v3.11.0 2026-04-30 Snapshot atômico do DB público

    Added

    • Snapshot atômico do DB público no run_coleta.sh. Pipeline escreve em data/eleicoes2026.db (privado/trabalho); ao final do zeitgeist, faz cp + mv atômico para data/eleicoes2026-public.db. Dashboard, weekly_summary.py e monthly_summary.py leem do snapshot público. Elimina a janela de 30–85 min em que o dashboard mostrava dados parciais durante a coleta (artigos sem sentimento/tópicos/zeitgeist e FPs ainda não removidos pelo verificador). Tradeoff aceito: dashboard pode ficar até ~85 min defasado entre o início da coleta e o snapshot final, mas sempre coerente.

    Changed

    • DB_PATH no .env aponta pro eleicoes2026-public.db (lido por dashboard via db_utils.py, weekly_summary, monthly_summary). Pipeline mantém escrita no privado via override defensivo (export DB_PATH=/opt/eleicoes2026/data/eleicoes2026.db) logo após o source .env no run_coleta.sh.
    • weekly_summary.py e monthly_summary.py (em blog-automation/produtores/) passaram a usar os.environ.get("DB_PATH", "/opt/eleicoes2026/data/eleicoes2026-public.db") no lugar do antigo Path.home() / "nazare/projetos/eleicoes2026/data/eleicoes2026.db". Services systemd weekly-summary.service e monthly-summary.service agora carregam EnvironmentFile=/opt/eleicoes2026/.env.

    Fixed

    • Symlink gambiarra removido — /root/nazare/projetos/eleicoes2026/data/eleicoes2026.db (e diretório fake /root/nazare/) na LXC-121 servia só pra fazer o Path.home() / "nazare/..." dos summaries achar o DB. Agora não é mais necessário; código aponta direto pra /opt/... ou usa env var.

    Notes

    • Incidente em produção 21:00 BRT 30/04 — primeira coleta após o deploy original do snapshot rodou com bug: run_coleta.sh fazia source .env, que setava DB_PATH=...public.db; pipeline (collect + 5 produtores) leu esse env e escreveu direto no PÚBLICO. Bloco snapshot final iria fazer cp PRIVADO→PÚBLICO, sobrescrevendo dados novos com versão velha de 14h. Detectado por inconsistência visível no dashboard (mostrando dados de 21:10 quando deveria estar congelado em 20:36), recovery em 18 min: systemctl stop, backups defensivos do estado parcial (eleicoes2026.db.recovery-pre21h-13588 e recovery-21h-parcial-13955 em /opt/eleicoes2026/data/), swap dos arquivos para preservar os 367 artigos novos no privado, fix do override no run_coleta.sh, deploy, coleta manual disparada 21:18:52 e concluída 22:19:40 com snapshot funcionando corretamente. Lição embutida: pipeline e reader compartilhando .env precisa de override defensivo das vars que o reader usa, OU arquivos de ambiente separados.
    • Pitfall registrado em CLAUDE.md: nunca remover o export DB_PATH=...eleicoes2026.db do run_coleta.sh — sem ele o pipeline regride pro modo público-write, e o snapshot final destrói os dados novos.

  6. v3.10.1 2026-04-30 Reclassificação retroativa + fix visual em cards

    Changed

    • Banco inteiro reclassificado de Gemini 2.5 → 3.1 Flash Lite Preview (~11h overnight, 22:36 BRT 29/04 → 09:43 BRT 30/04). Motivado por casos invertidos detectados ao validar o card "Candidato divisivo" da Cenário Semanal: matérias em que o candidato era ALVO de acusação apareciam scoreadas como POSITIVO (viés do 2.5: "destaque no título = positivo", sem distinguir autor vs alvo da fala). Smoke test confirmou que o 3.1 corrige todos os 4 casos invertidos sem precisar mudar o prompt.
    • Stats da reclassificação: 17.630 pares sentimento + 13.088 artigos com tópicos + 13.086 com gênero + 11.265 com zeitgeist (2.427 vínculos). Verificador removeu 4.319 pares FP de sentimento + 918 artigos completamente FP (todos os candidatos cobertos eram menção tangencial).
    • Snapshot pré-backfill preservado em /opt/eleicoes2026/backups/manual/pre-rebackfill-2026-04-29.db.gz (24 MB).

    Fixed

    • Cards de pré-candidato sem cobertura mostravam </div> literal numa caixa cinza (Hertz Dias na Cenário Semanal era o caso visível, mas o bug atingia qualquer slot vazio nos 3 painéis). Causa: templates f''' ... ''' multiline com indentação interna passando por st.markdown(unsafe_allow_html=True). O markdown do Streamlit processa antes do HTML render — quando uma variável injetada (sent_html/notes_html) é vazia, gera linha em branco seguida de linha com 12 espaços de indentação, que markdown interpreta como <pre><code>. Solução: re-escrever os templates como concatenação de strings f-prefixed em uma única linha lógica. Aplicado em:
      • pages/1_Cenario_Geral.py (notes_html vazio quando candidato sem nota)
      • pages/2_Cenario_Mensal.py (sent_html vazio quando 0 menções)
      • pages/3_Cenario_Semanal.py (sent_html vazio quando 0 menções)

    Notes

    • Pitfall do Gemini API: spending cap mensal por projeto (configurável em https://ai.studio/spend) — backfills grandes podem estourar e travar com HTTP 429 RESOURCE_EXHAUSTED. Não é rate limit transitório; só destrava aumentando o cap. Aconteceu durante este backfill às ~06:30 BRT, retomado às 07:23 após o cap subir. backfill_run.sh é idempotente (cada produtor com WHERE X IS NULL ou LEFT JOIN), então retomada do ponto de parada foi automática.
    • Após o backfill, eleicoes-coleta.timer foi reativada às 09:54 BRT. Como a unit tem Persistent=true, ela detectou que perdeu o disparo das 07:00 (mascarado durante a janela do backfill) e disparou retroativamente — coleta cobriu o gap automaticamente sem intervenção manual.

  7. v3.10.0 2026-04-29 Cenário Mensal: novo painel do mês corrente

    Added

    • Nova página: Cenário Mensal (pages/2_Cenario_Mensal.py). Janela fixa do mês civil corrente — dia 1º 00:00 ao primeiro dia do mês seguinte 00:00 (exclusivo) — comparada ao mês anterior. Posicionada entre Cenário Geral e Cenário Semanal no menu (:material/calendar_month:), espelhando a estrutura da Semanal: 1. KPIs do mês (artigos, candidatos cobertos, fontes únicas, sentimento médio — todos com Δ vs anterior) 2. Grid de pré-candidatos com volume e sentimento do mês + delta vs mês passado 3. Candidato destaque (compósito growth × log1p volume) + Candidato divisivo (dispersão de sentimento entre veículos, threshold 30 art.) + Sentimento por candidato (grid horizontal) 4. Dumbbell de volume — mês atual × mês anterior 5. Tópicos emergentes do mês 6. Artigos publicados no mês (top 50)
    • boot() carrega 70 dias para cobrir mês atual + anterior com folga (mesmo nos primeiros dias do mês ou em fevereiro).

    Changed

    • Threshold do Candidato divisivo ajustado para 30 artigos no Cenário Mensal (3× os 10 da Semanal). Em base mensal a dispersão precisa de mais cobertura por candidato para não capturar ruído estatístico.
    • Renumeração em cascata de pages/N_*.py para abrir o slot 2 da Cenário Mensal: Cenário Semanal 2 → 3, Pré-candidatos 3 → 4, Comparativo 4 → 5, Radar 5 → 6, Espectro Editorial 6 → 7, Zeitgeist 7 → 8, Análises 8 → 9, Metodologia 9 → 10, Changelog 10 → 11.
    • app.py: novo st.Page para Cenário Mensal e mapa _REDIR ampliado com 'Cenário Mensal' / 'Cenario Mensal'. Todas as URLs antigas (?page=) seguem funcionando.

    Notes

    • Três janelas editoriais agora coexistem:
      • Cenário Geral — visão histórica, janela ajustável via pills (25/03/2026 em diante)
      • Cenário Mensal — mês civil corrente, balanço mensal contra mês anterior
      • Cenário Semanal — segunda-domingo da semana corrente, leitura ao vivo
    • A Cenário Mensal não é o substrato do resumo mensal editorial (que cobre dia 26 do mês anterior ao dia 25 do mês alvo, publicado em blog-automation/produtores/monthly_summary.py) — janelas diferentes para usos diferentes, do mesmo jeito que Cenário Semanal e weekly_summary.

  8. v3.9.2 2026-04-29 Candidato divisivo (clivagem editorial da semana)

    Added

    • Bloco "Candidato divisivo" na coluna direita do bloco 2.3 (que estava vazia desde a remoção de "Evolução do sentimento" na v3.9.1). Aponta o pré-candidato com maior dispersão de sentimento entre veículos na semana corrente — sinal de polarização editorial. Métrica: desvio-padrão de sentiment_score por candidato, threshold mínimo de 10 artigos classificados.
    • Citação de matérias extremas: o card mostra qual veículo + qual matéria ancoraram o lado mais crítico e o mais elogioso da cobertura, com link clicável (target=_blank, rel=noopener) e título truncado em 90 caracteres. Concretiza pra o leitor a clivagem editorial.

    Changed

    • Label "amplitude" → "dispersão": o número exibido (ex.: 0.55) é desvio-padrão, não range max-min. Termo anterior induzia a leitura errada — quem visse "amplitude 0.55" e o range "-1.00 ↔ +1.00" estranhava a aritmética.
    • Range concreto adicionado logo abaixo do label (range -1.00 ↔ +1.00 em monospace) — leitor casual aproveita a leitura intuitiva; leitor técnico aproveita o std.

    Notes

    • Critério estatístico (std) é mais robusto a outliers que range simples. Um único artigo extremo de cada lado já fixa range = 2.00, mesmo com 99% da cobertura neutra; std reflete a distribuição real.
    • Card complementa o "Candidato destaque" da coluna esquerda: destaque captura movimento de volume; divisivo captura clivagem editorial. Duas leituras independentes da semana.

  9. v3.9.1 2026-04-29 Refinamento da Cenário Semanal

    Added

    • Dumbbell de volume por candidato (seção 2.4): visualização semana corrente × semana anterior. Bolinha cinza no valor da semana passada, bolinha colorida (cor do candidato) na semana atual; linha verde aponta crescimento, vermelha aponta queda. Resolve a comparação cruzada entre candidatos sem o ruído de uma timeline (descartada por excesso de séries).

    Changed

    • Reordenação de blocos da Cenário Semanal: Sentimento (destaque + por candidato) agora vem antes de Tópicos emergentes — leitura imediata do clima da semana, com aprofundamento em pauta logo depois.
    • Critério do Candidato destaque migrado para compósito growth × log1p(volume) (mesma fórmula dos tópicos emergentes). Antes era |Δ| absoluto — capturava também colapsos. Novo critério filtra score > 0: destaque é sempre editorial positivo. Quando ninguém está em alta (caso comum nas primeiras horas de uma semana), o bloco mostra mensagem honesta.
    • KPI "Sentimento médio" ganhou detail com delta vs semana passada (↑/→/↓ vs +X.XX). Card "Evolução do sentimento" do bloco 2.3 removido — era informação duplicada.

    Fixed

    • app.py registrava paths antigos após a renumeração pages/N_*.py da v3.9.0. st.navigation quebrava ao tentar montar páginas — HTTP 200 do Streamlit não detectava (a app sobe; o erro só dispara ao trocar de página). Mapa _REDIR e lista de st.Page(...) atualizados.
    • KPI "Candidatos cobertos" (Cenário Semanal) contava nunique() em article_candidates da semana sem filtrar dropped. Resultado: 14 vs 12 mostrados no grid (4 dropped tinham vínculos históricos contando dobrado). Agora segue o mesmo critério do grid (pre-candidate + candidate por padrão; só candidate quando only_confirmed=True).

    Notes

    • Janela do Cenário Semanal continua segunda-domingo. No meio da semana, o dumbbell e o destaque mostram comparação parcial (3 dias da semana corrente × 7 da anterior) — esperado.
    • Removido um falso positivo manualmente do banco em 29/04: artigo "Datafolha divulga 1ª pesquisa de opinião após registro de candidaturas" (Agência Brasil), vinculado erroneamente a Cabo Daciolo. Conteúdo era de eleição passada com published_at falsamente atualizado pela republicação. Verificador critério 6 não pegou — vale revisar regras de período eleitoral consumado se virar recorrente.

  10. v3.9.0 2026-04-29 Cenário Geral × Cenário Semanal: split editorial

    Added

    • Nova página: Cenário Semanal. Janela fixa segunda-domingo da semana corrente, sem filtro de período. Conteúdo: KPIs da semana (volume + Δ vs anterior, candidatos cobertos, fontes únicas, sentimento médio), grid de pré-candidatos com volume e sentimento da semana + delta, tópicos emergentes, candidato destaque, evolução do sentimento, sentimento por candidato (grid horizontal), lista de artigos publicados na semana.

    Changed

    • Cenário Geral refocada para visão histórica: removidas seções de "Esta Semana", "Tópicos emergentes desta semana", "Candidato destaque", "Sentimento da semana" e "Sentimento por candidato" (semanais) — todas migradas para Cenário Semanal.
    • Pills de período saíram da sidebar. Agora vivem inline no body de Cenário Geral (única página com janela ajustável). Default: Tudo (toda a série desde 25/03/2026). Outras opções: 90 dias, 30 dias, 7 dias, Personalizado. Cenário Semanal e demais páginas usam janela fixa por desenho.
    • Ordem do menu: Início → Cenário Geral → Cenário Semanal → Pré-candidatos → Comparativo → Radar → Espectro Editorial → Zeitgeist → Análises → Metodologia → Changelog. Arquivos pages/N_*.py renumerados em cascata.
    • Grid de pré-candidatos imediatamente após os KPIs em ambas as páginas (geral com volume total no período; semanal com volume da semana + delta vs anterior).

    Notes

    • Janela do Cenário Semanal é segunda-domingo (semana civil corrente). Não confundir com a janela do resumo semanal editorial (sábado-sexta, publicado no sábado seguinte) — janelas diferentes para usos diferentes.
    • boot() ganhou parâmetros days_back e date_range para que cada página controle sua própria janela; sidebar ficou só com brand + toggle "apenas confirmados" + tema.

  11. v3.8.2 2026-04-29 Recorte do dashboard agora é editorial, não de coleta

    Fixed

    • Timeline de cobertura escondia silenciosamente artigos válidos do início da série. load_data() filtrava por fetched_at >= now() - 30d. Como o coletor entrou em produção em 25/03 capturando publicações retroativas, 871 artigos publicados entre 25-30/03 ficavam fora do dashboard — e o problema piorava 1 dia por dia (cada amanhã, mais 1 dia da janela editorial sumia do gráfico).
    • Filtro passa a ser por published_at com cutoff sempre limitado por MIN_PUBLISHED_DATE = 2026-03-25 (recorte oficial). Janela "30 dias" agora significa "publicado nos últimos 30 dias", não "coletado nos últimos 30 dias".
    • Período "Personalizado" migrado de fetched_at para published_at (com parse_published_dates para lidar com formato misto com/sem timezone — pd.to_datetime direto gera NaT silenciosamente, conforme gotcha já documentado).

    Notes

    • Mudança afeta arquivos: src/core/data.py (queries SQL) e src/core/bootstrap.py (recorte personalizado).
    • Cache @st.cache_data(ttl=300) é invalidado no restart do eleicoes-dashboard.service.

  12. v3.8.1 2026-04-28 Leonardo Péricles dropped

    Changed

    • Leonardo Péricles (UP) marcado como status='dropped' no banco — substituído por Samara Martins como pré-candidato da Unidade Popular em 08/04/2026. A descoberta veio da análise do resumo mensal de abril (apenas 24 artigos sobre Péricles vs 108 sobre Samara). Status alterado retroativamente em 28/04/2026 para refletir a transição partidária.
    • Coletores Brave/Google News/Tavily passam a ignorar Péricles automaticamente (registros históricos preservados no banco).
    • README atualizado: Péricles movido para "Descontinuados".