PROJETO · HISTÓRICO DE VERSÕES

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.

  1. 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/10 positive — 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.py retornam id + internal_urlrecent_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 em CANDIDATE_PATTERNS + queries em BRAVE_QUERIES/TAVILY_QUERIES/GNEWS_QUERIES), não só o INSERT. Comentário inline em src/candidate_utils.py já documentava isso desde sempre. Procedimento agora rodado integralmente pro JB; checklist registrado em memória curada.
    • rel=canonical das 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.

  2. v4.6.1 2026-05-12 Migração Gemini 3.1 Flash Lite para GA

    Changed

    • gemini_client.py: model ID atualizado de gemini-3.1-flash-lite-preview para gemini-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.

  3. 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\b solto — 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).

  4. 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 — declara plotly.js@3.0.1 + esbuild@^0.24.0 como devDeps
      • plotly-bundle.js — entry point que importa plotly.js/lib/core + traces individuais via Plotly.register([Bar, Heatmap, Scatter, Scatterpolar, Treemap])
      • global-shim.js — shim ESM injetado no bundle pra polyfillar globalThis.global (fallback se runtime shim falhar)
      • npm run build → esbuild gera app/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 bare global dentro do IIFE, fora do alcance do --define/--banner/--inject do esbuild. O shim no escopo window resolve qualquer global via scope chain. 1-linha, robusto, dispensa re-build a cada mudança.

    Changed

    • base.html — referência muda de plotly-3.0.1.min.js (4.6MB) pra plotly-custom-3.0.1.min.js (1.2MB).
    • app/static/js/plotly-3.0.1.min.js removido 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 em base.html (carrega versão no nome pra cache-bust). Documentado em frontend/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 no window antes 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)

  5. v4.4.0 2026-05-10 HTMX em filter changes (4 rotas)

    Filter changes em /comparativo, /radar, /espectro e /cenario-geral deixam de fazer full page reload e passam a fazer fragment swap via HTMX. Form ganha auto-trigger no change (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.html que re-executa <script> inline injetado por swap. Browsers não executam scripts que entram via innerHTML — o listener detecta htmx:afterSwap e 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ópicos
      • radar_results.html — spider chart + tabela de valores brutos
      • espectro_results.html — KPIs por espectro + 4 charts
      • cenario_geral_results.html — pills + KPIs + 5 charts + lista de artigos
    • Detecção de HX-Request header nas 4 rotas — se presente, retorna partials/<rota>_results.html; se ausente, retorna pages/<rota>.html que {% 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-geral ganham hx-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=30d segunda vez): ~700-1000ms full reload → ~150-300ms swap. Redução ~80%.
      • Cache miss (rota cara, ex.: /comparativo com 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.
    • 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.

  6. 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.brestadao.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.

  7. 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.

  8. 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 estilobase.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.

  9. 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.

  10. 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.

  11. 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.

  12. 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.