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.
-
v4.3.0 2026-05-10 Filtro estrutural de URLs duplicadas + dedup retroativo Fecha o gap de duplicação histórica do banco.
normalize_urlemdb_utils.pyganhou 4 regras novas, todas determinísticas (sem custo LLM). Padrões mapeados a partir das duplicatas reais no banco antes do fix.Changed
normalize_urlagora 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)srsltidquery string adicionado a_STRIP_PARAMS(Google Search redirect, comum no Estadão)- Force
httpsno scheme (cobre matérias servidas emhttp+httpsda mesma fonte; ex.:em.com.br)
brave.pyagora aplicanormalize_urlantes de fazer SELECT/INSERT dearticles.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 menorid(mais antigo no banco), reassociaarticle_candidates/topics/article_zeitgeist/article_zeitgeist_processeddos duplicados pro mantido (viaINSERT OR IGNORE), atualiza URL canônica no mantido, deleta os duplicados. Snapshot defensivo do DB antes de qualquer escrita.--applypra 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/xque 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_candidatesmovidos. 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.
-
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
todaycalendarial), evita falsos zeros quando o pipeline está atrasado. Caso ilustrativo: Lula em 09/05 mostra2.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 filtramverified=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 viaSTATIC_VERSIONcobre o deploy.
- 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
-
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
SAPIENSLABSem até 180px, eyebrow micro "SOBRE ARTES E TÉCNICAS", separado do menu porborder-tope padding generoso (margin-top: 56px,padding: 24px 28px 28px). Bloco inteiro vira link clicável parahttps://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.htmlanexa?v={{ static_version }}emdesign-system.csseapp.css.STATIC_VERSIONé calculado uma vez no startup do uvicorn comoint(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 emapp.cssmuda 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.
- 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
-
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 embase.htmlcomanonymize_ip: truee 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
:18504na LXC-121, expõehttps://eleicoes2026.sapienslabs.com.brvia 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_cacheddecorator emapp/core/data.py— cache em memória process-local que invalida quandoos.stat(DB_PATH).st_mtimemuda. 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 comocomparativo_*,espectro_data,zeitgeist_data, KPIs)./robots.txte/sitemap.xmldinâmicos — sitemap inclui 12 rotas estáticas + 1 entry por candidato ativo (filtra droppeds viacandidate_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 decdn.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>debase.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 emapp/static/css/app.csspara--fg1/--fg2/--fg3em[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/comparativosimultâ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
:8504ativo (aditivo, semresetpara evitar incidente do 01/05).:8503desligado depois do cutover. home_kpis()filtrastatus != 'dropped'— Pré-candidatos antes contava 16 (incluindo droppeds), agora 12._kpis_for_windowfiltra droppeds viaJOIN candidates c ON c.slug = ac.candidate WHERE c.status != 'dropped'— Cenário Mensal/Semanal coerentes com Home.home_kpis()respeitaMIN_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(eraCHANGELOG.md+page_changelog()nodashboard.py). - Tag de versão:
v4.1.0-fastapi(sufixo-fastapipara 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.serviceestáinactivemasenabled(rede de segurança para rollback rápido). Cleanup definitivo (disable + arquivamento desrc/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.0na 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 porCOUNT(DISTINCT a.id)que respeita NULL do JOIN não-batido. Diferença observada de ~1000 artigos nocandidates_grid(Cenário Geral) ecenario_mensal_candidates.- Plotly via
deferquebra com inlinePlotly.newPlot()— defer carrega o script externo depois do parse do body, então as chamadas inline executam antes dewindow.Plotlyexistir. Solução: Plotly síncrono no<head>debase.html, todos os charts geram cominclude_plotlyjs=False. grid-template-columns: 280px 1frcausa grid-blowout quando filhos têm width fixo (Plotly SVG). Trocado paraminmax(0, 1fr)combox-sizing: border-boxglobal eoverflow-x: clipno html/body como salva-vidas.update_layout(_base_layout(), showlegend=False) davaTypeError: got multiple values for showlegend** —_base_layout()já retornavashowlegend=True.showlegend=Falsemovido pro layout dict antes doupdate_layout.{% for item in group.items %}em Jinja resolvia o método builtin.itemsdo dict — chave renomeada paralinks.
Performance
Rota Antes (Streamlit) Depois (FastAPI cache miss) Cache hit /comparativo~15-20s (Streamlit re-roda tudo) 7s 0.7s (10×) /espectro~6-8s 2.6s 0.65s (4×) /zeitgeist~3s 0.9s 0.62s /radar~2s 0.77s 0.57s /~1.5s 0.83s 0.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, tagv4.1.0-fastapipushed. Streamlit ficaenabledmasinactivepor 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.txtno público — CF injeta header com AI Content Signals + bloqueio de GPTBot/ClaudeBot/Amazonbot/Google-Extended/etc. antes do nosso conteúdo. NossoSitemap:permanece visível. Proteção bem-vinda contra training de LLMs sem ação explícita.
- Google Analytics 4 (G-DJ3D4RT0CL) — mesmo measurement ID do
-
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 emdata/eleicoes2026.db(privado/trabalho); ao final do zeitgeist, fazcp+mvatômico paradata/eleicoes2026-public.db. Dashboard,weekly_summary.pyemonthly_summary.pyleem 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_PATHno.envaponta proeleicoes2026-public.db(lido por dashboard viadb_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 osource .envnorun_coleta.sh.weekly_summary.pyemonthly_summary.py(emblog-automation/produtores/) passaram a usaros.environ.get("DB_PATH", "/opt/eleicoes2026/data/eleicoes2026-public.db")no lugar do antigoPath.home() / "nazare/projetos/eleicoes2026/data/eleicoes2026.db". Services systemdweekly-summary.serviceemonthly-summary.serviceagora carregamEnvironmentFile=/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 oPath.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.shfaziasource .env, que setavaDB_PATH=...public.db; pipeline (collect + 5 produtores) leu esse env e escreveu direto no PÚBLICO. Bloco snapshot final iria fazercp 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-13588erecovery-21h-parcial-13955em/opt/eleicoes2026/data/), swap dos arquivos para preservar os 367 artigos novos no privado, fix do override norun_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.envprecisa de override defensivo das vars que o reader usa, OU arquivos de ambiente separados. - Pitfall registrado em
CLAUDE.md: nunca remover oexport DB_PATH=...eleicoes2026.dbdorun_coleta.sh— sem ele o pipeline regride pro modo público-write, e o snapshot final destrói os dados novos.
- Snapshot atômico do DB público no
-
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: templatesf''' ... '''multiline com indentação interna passando porst.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_htmlvazio quando candidato sem nota)pages/2_Cenario_Mensal.py(sent_htmlvazio quando 0 menções)pages/3_Cenario_Semanal.py(sent_htmlvazio 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 comWHERE X IS NULLouLEFT JOIN), então retomada do ponto de parada foi automática. - Após o backfill,
eleicoes-coleta.timerfoi reativada às 09:54 BRT. Como a unit temPersistent=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.
-
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_*.pypara 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: novost.Pagepara Cenário Mensal e mapa_REDIRampliado 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.
- Nova página: Cenário Mensal (
-
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_scorepor 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.00em 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.
- 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
-
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 filtrascore > 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.pyregistrava paths antigos após a renumeraçãopages/N_*.pyda v3.9.0.st.navigationquebrava 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_REDIRe lista dest.Page(...)atualizados.- KPI "Candidatos cobertos" (Cenário Semanal) contava
nunique()emarticle_candidatesda semana sem filtrardropped. 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 + candidatepor padrão; sócandidatequandoonly_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_atfalsamente atualizado pela republicação. Verificador critério 6 não pegou — vale revisar regras de período eleitoral consumado se virar recorrente.
-
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_*.pyrenumerados 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âmetrosdays_backedate_rangepara que cada página controle sua própria janela; sidebar ficou só com brand + toggle "apenas confirmados" + tema.
-
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 porfetched_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_atcom cutoff sempre limitado porMIN_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_atparapublished_at(comparse_published_datespara lidar com formato misto com/sem timezone —pd.to_datetimedireto gera NaT silenciosamente, conforme gotcha já documentado).
Notes
- Mudança afeta arquivos:
src/core/data.py(queries SQL) esrc/core/bootstrap.py(recorte personalizado). - Cache
@st.cache_data(ttl=300)é invalidado no restart doeleicoes-dashboard.service.
- Timeline de cobertura escondia silenciosamente artigos válidos do início da série.
-
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".
- Leonardo Péricles (UP) marcado como