Changelog
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 8 de 12 · 136 versões no total.
-
v4.7.0 2026-05-16 Ficha por matéria + Joaquim Barbosa (DC) Duas adições estruturais. Cada link de matéria nas listas do dashboard agora abre uma ficha analítica interna que mostra tudo que o banco tem sobre ela; e Joaquim Barbosa entra como pré-candidato do Democracia Cristã, em movimento que disputa internamente com a pré-candidatura de Aldo Rebelo no mesmo partido.
Added
- 📰
/cobertura/{id}/{slug}— ficha analítica por matéria. Cada link de matéria nas listas (Cobertura mais recente, Artigos da semana/mês, listagem na página de pré-candidato) passa a abrir uma página interna com alinhamento editorial do veículo, gênero jornalístico, cluster Zeitgeist, tópicos identificados, sentimento por pré-candidato (tabela) e até 5 matérias relacionadas (janela ±3 dias, score por mesmo cluster + co-ocorrência de candidatos). CTA proeminente "Ler matéria completa em [veículo]" no topo e<link rel="canonical" href="{url_original}">no head pra respeitar a fonte. Open Graph completo pra compartilhamento. Backend:data.article_detail(id)+ slugify NFKD ASCII (kebab, max 60 chars cortado na palavra). - 🗳️ Novo pré-candidato: Joaquim Barbosa (DC) — Ex-ministro do STF (2003–2014), primeiro negro a presidir o Supremo. Relator da AP 470 (Caso Mensalão). Aposentou-se voluntariamente em 2014. Filiou-se ao Democracia Cristã em maio/2026; cotado pelo partido como nome para disputar a Presidência em 2026, em movimento que se sobrepõe à pré-candidatura de Aldo Rebelo lançada pelo mesmo DC em 07/05/2026 (disputa interna documentada por Carta Capital, Poder360, Revista Fórum e DCM). Coleta dedicada começou na rodada das 21:00 BRT de 16/05.
- 🔍 10 artigos retroativos sobre JB que já estavam no banco (coletados por menção cruzada em queries do Aldo) foram associados ao novo candidato e classificados pelo
sentimento.py(10/10positive— tom favorável da cobertura sobre a filiação ao DC). Tópicos e Zeitgeist já estavam preenchidos.
Changed
- 🔄 Listas de artigos refatoradas pra usar partial reutilizável (
partials/article_list_item.html) — 9 blocos duplicados em 5 templates consolidados em 1 só. Listas passam a linkar pra ficha interna (internal_url) em vez de direto pro veículo; link original preservado dentro da ficha. - 📊 5 funções de listagem em
data.pyretornamid+internal_url—recent_articles,_articles_for_window(semanal/mensal),candidate_detail.articles,emerging_topics.examples. Esquema consistente: toda lista de matérias do banco entra na navegação interna. - 📝 Metodologia ganhou seção dedicada à ficha por matéria.
Notes
- Lacuna nas queries dos coletores corrigida no mesmo dia — descobriu-se que adicionar um pré-candidato exige 5 mudanças (INSERT em
candidates+ patterns emCANDIDATE_PATTERNS+ queries emBRAVE_QUERIES/TAVILY_QUERIES/GNEWS_QUERIES), não só o INSERT. Comentário inline emsrc/candidate_utils.pyjá documentava isso desde sempre. Procedimento agora rodado integralmente pro JB; checklist registrado em memória curada. rel=canonicaldas fichas aponta pra fonte original — decisão editorial de respeito ao trabalho dos veículos cobertos. O painel não disputa SEO da palavra-chave; a ficha agrega valor analítico próprio.
- 📰
-
v4.6.1 2026-05-12 Migração Gemini 3.1 Flash Lite para GA Changed
gemini_client.py: model ID atualizado degemini-3.1-flash-lite-previewparagemini-3.1-flash-lite(GA). Mesmo modelo, só o identificador mudou — sem alteração de comportamento nem de custos. Preview descontinuado pelo Google em 25/05/2026.
-
v4.6.0 2026-05-10 Edmilson Costa (PCB) entra no monitoramento Adição tardia de pré-candidato presidencial: Edmilson Costa, lançado pelo Comitê Central do PCB em 24/02/2026 com slogan "Por um governo do Poder Popular". A imprensa já vinha tratando-o como pré-candidato em listas de pesquisa eleitoral (Meio/Ideia, Real Time Big Data) — a inclusão regulariza o registro no painel.
Added
- 🗳️ Novo pré-candidato: Edmilson Costa (PCB) — Doutor em Economia pela Unicamp, com pós-doutorado no IFCH/Unicamp. Militante histórico do PCB, perseguido na ditadura militar como líder estudantil.
- 🔍 Busca retroativa: 11 artigos existentes vinculados ao novo candidato na tabela
article_candidates(todos como menção em listas de pesquisa eleitoral; nenhum dedicado). - ⚙️ Coleta futura configurada: 3 patterns regex, 5 queries Brave e 3 Tavily em
candidate_utils.py. Patterns conservadores (sem\bcosta\bsolto — sobrenome muito comum).
Changed
- 📝 Metodologia: 12 pré-candidatos ativos (era 11) + 5 dropped (Ciro Gomes, Eduardo Leite, Leonardo Péricles, Michelle Bolsonaro, Tarcísio de Freitas).
-
v4.5.0 2026-05-10 Bundle Plotly custom (1.2MB vs 4.6MB) Bundle Plotly trocado do full distribution (4.6MB) para bundle custom (1.2MB, -73%) com apenas os 5 trace types que o dashboard de fato usa: Bar, Heatmap, Scatter, Scatterpolar, Treemap. Constructed via esbuild sob
frontend/.Added
frontend/— toolchain JS isolada do projeto Python:package.json— declaraplotly.js@3.0.1+esbuild@^0.24.0como devDepsplotly-bundle.js— entry point que importaplotly.js/lib/core+ traces individuais viaPlotly.register([Bar, Heatmap, Scatter, Scatterpolar, Treemap])global-shim.js— shim ESM injetado no bundle pra polyfillarglobalThis.global(fallback se runtime shim falhar)npm run build→ esbuild geraapp/static/js/plotly-custom-3.0.1.min.js
- Runtime shim em
base.html—<script>window.global = window;</script>ANTES do bundle. Plotly v3 tem código que referencia bareglobaldentro do IIFE, fora do alcance do--define/--banner/--injectdo esbuild. O shim no escopowindowresolve qualquerglobalvia scope chain. 1-linha, robusto, dispensa re-build a cada mudança.
Changed
base.html— referência muda deplotly-3.0.1.min.js(4.6MB) praplotly-custom-3.0.1.min.js(1.2MB).app/static/js/plotly-3.0.1.min.jsremovido do repo (não é mais usado).
Notes
- Validação no Chrome: 6 traces em
/comparativo(bar + scatter), 1 em/radar(scatterpolar), 4 em/cenario-geral(scatter + heatmap + treemap). Zero erros console. - Manutenção pós-upgrade Plotly: bumpar versão em
frontend/package.json,cd frontend && npm install && npm run build, atualizar nome do arquivo embase.html(carrega versão no nome pra cache-bust). Documentado emfrontend/plotly-bundle.js. - Tentativas falhadas antes do shim runtime:
--define:global=globalThis(substituição textual não pegou todos os usos),--banner:js="var global=..."(sombreado pela IIFE),--inject:./global-shim.js(módulo importado mas escopo isolado). Conselho do advisor: shim nowindowantes do script é mais simples e robusto.
Performance
- Tamanho bruto: 4.6MB → 1.2MB (-73%)
- First-visit em 4G médio (5Mbps efetivo): ~7.5s → ~2s (economia ~5.5s)
- First-visit em 100Mbps: ~370ms → ~100ms (economia ~270ms)
- Subsequent visits: zero (browser cache + CF cache 4h em ambos os casos)
-
v4.4.0 2026-05-10 HTMX em filter changes (4 rotas) Filter changes em
/comparativo,/radar,/espectroe/cenario-geraldeixam de fazer full page reload e passam a fazer fragment swap via HTMX. Form ganha auto-trigger nochange(debounce 300ms) — não precisa mais clicar "Atualizar". Classe ativa das pills do/cenario-geralé re-renderizada junto com o partial.Added
- HTMX 2.0.4 self-host em
app/static/js/htmx-2.0.4.min.js(51KB, mesmo motivo do Plotly: evita DNS/TLS pra CDN, reaproveita HTTP/2 do mesmo domínio). - Listener global em
base.htmlque re-executa<script>inline injetado por swap. Browsers não executam scripts que entram viainnerHTML— o listener detectahtmx:afterSwape re-cria cada<script>como nó novo (browser executa nós recém-injetados). Chave para fazer Plotly funcionar pós-swap (pio.to_html()injeta<div>+<script>Plotly.newPlot()</script>inline). - Partials por rota em
app/templates/partials/:comparativo_results.html— tabela resumo + 7 charts + top tópicosradar_results.html— spider chart + tabela de valores brutosespectro_results.html— KPIs por espectro + 4 chartscenario_geral_results.html— pills + KPIs + 5 charts + lista de artigos
- Detecção de
HX-Requestheader nas 4 rotas — se presente, retornapartials/<rota>_results.html; se ausente, retornapages/<rota>.htmlque{% include %}o partial. Sem duplicação de lógica de contexto. Compatível com no-JS (forms ainda submetem normalmente).
Changed
- Forms ganham
hx-trigger="change delay:300ms"— qualquer toggle de checkbox dispara filter change após 300ms de inatividade (debounce). Botão "Atualizar" mantido como fallback. - Pills do
/cenario-geralganhamhx-get+hx-target+hx-push-url— funcionam tanto como links normais (no-JS) quanto como swap HTMX. hx-push-url="true"em todas as rotas — URL no browser atualiza junto com o swap, mantém comportamento de navegação esperado (back button, share URL, etc.).
Notes
- Ganho de tempo no filter change varia conforme cache do
db_cached:- Cache hit (rota leve, ex.:
/cenario-geral?period=30dsegunda vez): ~700-1000ms full reload → ~150-300ms swap. Redução ~80%. - Cache miss (rota cara, ex.:
/comparativocom seleção nova): ~8-15s full reload → ~8-15s swap (mesma carga server-side; ganho marginal de render+transfer). Mas a sensação de "responsividade" do click é melhor — UI muda instantaneamente quando a resposta chega, sem flash de white reload.
- Cache hit (rota leve, ex.:
- Todas as outras rotas (
/,/zeitgeist,/cenario-mensal,/cenario-semanal,/pre-candidatos,/pre-candidatos/{slug},/analises,/metodologia,/changelog,/privacidade) ficam intocadas — não têm filtros interativos e portanto não precisam de HTMX.
- HTMX 2.0.4 self-host em
-
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 (