No description
  • HTML 56.3%
  • Python 43.1%
  • Jinja 0.6%
Find a file
2026-06-18 17:52:39 -05:00
.forgejo/workflows initial 2026-06-15 21:33:28 -05:00
docs update .gitignore 2026-06-15 21:40:16 -05:00
scripts docs(readme): document static build/archive workflow and recent features 2026-06-18 17:52:39 -05:00
templates feat(resumen): add Avance and Pendientes columns to the abroad-vote table 2026-06-18 17:25:55 -05:00
tests feat(anomalies): link anomalies to the actual affected ballots (actas) 2026-06-16 20:45:53 -05:00
.gitignore update .gitignore 2026-06-15 21:40:16 -05:00
config.py initial 2026-06-15 21:33:28 -05:00
LICENSE initial 2026-06-15 21:33:28 -05:00
pyproject.toml initial 2026-06-15 21:33:28 -05:00
README.md docs(readme): document static build/archive workflow and recent features 2026-06-18 17:52:39 -05:00
requirements-dev.txt initial 2026-06-15 21:33:28 -05:00
requirements.txt initial 2026-06-15 21:33:28 -05:00
server.py feat(resumen): add Avance and Pendientes columns to the abroad-vote table 2026-06-18 17:25:55 -05:00

Segunda Vuelta 2026 — Proyeccion y monitoreo por region (datos ONPE)

Monitoreo en vivo y proyeccion al 100 % de la segunda vuelta presidencial del Peru (8 de junio de 2026), region por region, a partir de los datos oficiales de la ONPE. Candidatos y colores se configuran exclusivamente via config.py — toda la presentacion (CSS, templates, JS) es agnostica al numero de candidatos.

Note

Este proyecto reproduce datos oficiales de la ONPE y agrega una proyeccion estadistica propia. No afirma irregularidades ni fraude; los hallazgos se presentan como observaciones. El resultado oficial y definitivo lo proclama el Jurado Nacional de Elecciones (JNE) a mediados de julio.


Arquitectura

flowchart LR
    CFG[config.py] -.-> scrape
    CFG -.-> proy
    CFG -.-> server
    ONPE[ONPE API] --> scrape[scrape_run2.py]
    ONPE --> actas[scrape_ballots.py]
    scrape --> serie[series-run2.json]
    scrape --> anom[anomalies.json]
    actas --> enriched[ballots_observed_enriched.json]
    serie --> proy[projection.py]
    proy --> proy_json[projection.json]
    proy_json --> server[server.py / Flask]
    enriched --> server
    serie --> server
    anom --> server
    server --> FE[Dashboard HTML + JS]
    server --> pdf_reports[pdf_reports.py]
    pdf_reports --> report[Report PDF]
    pdf_reports --> summary[Summary PDF]
    build_report_pdf[build_report_pdf.py CLI] --> pdf_reports
    build_summary_pdf[build_summary_pdf.py CLI] --> pdf_reports

Features

  • Dashboard (/): mapa de predominancia por departamento, tabla ordenable, panel de proyeccion con banda de incertidumbre, grafico de convergencia, banner de anomalias activas.
  • Participacion (/participacion): compara la participacion por region entre la 2. vuelta 2026, la 1. vuelta 2026 y la 2. vuelta 2021.
  • Resumen ejecutivo (/resumen): hero cards, waffle de actas pendientes por zona, desglose por region con pestañas Departamentos / Voto Extranjero. La tabla de extranjero (por pais) incluye columnas Avance y Pendientes calculadas sobre sus actas observadas (contabilizadas vs en el JEE).
  • Anomalias (/anomalias): deteccion automatica con 7 reglas de monotonicidad, tracking de resolucion, badges de escalacion por tiempo. Cada anomalia abierta muestra las mesas reales afectadas (estado "Para envio al JEE") como chips; al hacer clic se abre un modal con ubicacion, estado y votos del acta. Si la ONPE aun no publica el acta, cae al perfil inferido. Auto-resuelve cuando el valor se recupera >= 50 % del anterior.
  • Actas observadas (/actas): explorador con tabs Nacional / Voto Extranjero, KPIs, filtros, tabla ordenable, paginacion, modal de detalle completo via API de la ONPE.
  • Showcase editorial (/showcase): narrativa visual "La Cuenta Larga".
  • LibreJS compliant (/librejs): pagina Web Labels con enlaces directos a cada script, @license annotations en todo JS local.
  • Candidate-agnostic: CSS index-based (--cand-0, .v-0, .w-0), templates iteran sobre CANDIDATES de config.py. Agregar un candidato = editar solo el dict. Sin nombres ni slugs en CSS/clases.
  • Proyeccion estratificada por region: en vez de extrapolar el % nacional crudo (sesgado porque Lima/Callao entran primero), proyecta cada region por separado y la pondera por su propio volumen; estima las actas faltantes con el comportamiento de esa region. Reporta banda 95 % Monte Carlo y escenarios de sensibilidad.
  • Anomaly detection: 7 reglas de monotonicidad sobre los cortes historicos (retrocesos en votos, divergencias anormales, saltos de liderazgo, etc.). Persiste a JSON con estado open/resolved y escalacion temporal por dias (Abierta < 2 d, Alerta < 7 d, Critica >= 7 d). Auto-resuelve con umbral de recuperacion >= 50 %.
  • Auto-refresh configurable: 10 s / 30 s / 1 min / 5 min, persistido en localStorage. Auto-scrape loop en backend cada 60 s.
  • Dark PRO UI: paleta oscura, glassmorphism, mobile-first, touch targets

    = 44 px, Lucide icons locales.


Stack

Capa Tecnologia
Config config.pyCANDIDATES dict (slug, nombre, color, partido), TIMEZONE
Backend Python 3.11 + Flask
Scraping requests + Session con WAF evasion
Proyeccion Python puro (Monte Carlo, 4000 sims)
PDF generation ReportLab + matplotlib (mapas)
Frontend HTML + CSS + JS vanilla
Charts Chart.js v4.4.1 (local, ~201 KB)
Icons Lucide (local, ~402 KB)
Fuentes Inter, Fraunces, JetBrains Mono (Google Fonts)

Uso local

# dependencias
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# servidor (inicia scraper-loop automatico en background)
python server.py
# -> http://localhost:5000

# o ejecutar manualmente los scripts
python scripts/scrape_run2.py
python scripts/projection.py
python scripts/scrape_ballots.py

Warning

La ONPE tiene rate-limit. El scraper usa pausas de 0.3-0.45 s entre llamadas con backoff. Extranjero (145 actas) puede tomar ~2 min.


Archivar: snapshot estatico

Cuando el conteo termine, el dashboard se puede congelar en un sitio estatico autocontenido (sin backend) para archivarlo o publicarlo en Pages.

# 1. (opcional) refrescar los datos a su estado final
python scripts/scrape_run2.py
python scripts/projection.py
python scripts/scrape_ballots.py

# 2. construir el snapshot dentro de docs/
python scripts/build_static.py
# flags: --skip-actas  --acta-limit N  --skip-pdf  --out <dir>

# 3. archivar
git add docs/ && git commit -m "chore: archive final static snapshot"

El generador (scripts/build_static.py) usa el test_client de Flask para, con un unico mecanismo "GET la ruta y escribe el resultado":

  • renderiza cada pagina a docs/*.html;
  • congela los endpoints computados a docs/api/*.json (y series-run2 / projection ya normalizados en docs/api/data/);
  • genera los PDFs a docs/api/*.pdf;
  • pre-descarga el detalle de cada acta referenciada a docs/api/acta/<id>.json (para que el modal funcione offline);
  • reescribe en el HTML los fetch('/api/...') a esos archivos estaticos y el nav a *.html (rutas relativas, sirven en cualquier host o subcarpeta).

Endurecimiento (OWASP): el artefacto estatico elimina todo el codigo servidor (proxy en vivo de actas, refresh por subprocess, endpoints dinamicos), inyecta una Content-Security-Policy restrictiva (default-src 'self', object-src 'none', base-uri 'self', form-action 'none') y referrer: no-referrer en cada pagina. Los ids de acta se fuerzan a int (sin path traversal). Nota: la CSP usa 'unsafe-inline' porque las paginas tienen scripts y handlers inline; un hardening total requeriria externalizar el JS + nonces.

El resultado en docs/ queda autocontenido junto a assets/ y data/.


API endpoints

Ruta Descripcion
/ Dashboard principal
/resumen Resumen ejecutivo
/anomalias Panel de anomalias
/actas Explorador de actas observadas
/participacion Comparativa de participacion
/showcase Narrativa visual "La Cuenta Larga"
/librejs Pagina Web Labels (LibreJS compliance)
/api/resumen Datos agregados: actas, crudo, proyeccion, pendientes por zona, regiones
/api/anomalias Anomalias detectadas; las abiertas incluyen sus actas afectadas
/api/actas-observadas Todas las actas observadas (nacional + extranjero)
/api/extranjero Voto extranjero por pais, con avance y pendientes sobre actas observadas
/api/acta/<id> Detalle completo de un acta (proxy a ONPE)
/api/data/<filename> Sirve archivos JSON estaticos de docs/data/
/api/refresh Ejecuta scrape_run2.py + projection.py + scrape_ballots.py
/api/informe-pdf Descarga informe tecnico completo (PDF, ~110 KB)
/api/resumen-pdf Descarga resumen ejecutivo de 2 paginas (PDF, ~105 KB)

Estructura del proyecto

.
├── config.py                           # CANDIDATES dict + TIMEZONE, unica fuente de verdad
├── server.py                          # Flask app, 17 rutas, auto-scrape loop
├── requirements.txt
├── LICENSE
├── scripts/
│   ├── onpe_client.py                 # HTTP client con rate-limit y WAF evasion
│   ├── scrape_run2.py                 # ONPE scraper + anomaly detection + auto-resolve
│   ├── scrape_ballots.py              # Acta explorer data + enrichment
│   ├── projection.py                  # Motor de proyeccion estratificada
│   ├── parse_flex.py                  # Candidate matching
│   ├── pdf_reports.py                 # PDF generation (core, imported by server.py + CLI)
│   ├── build_report_pdf.py            # CLI wrapper for technical report
│   ├── build_summary_pdf.py           # CLI wrapper for executive summary
│   ├── build_static.py                # Congela el dashboard en un sitio estatico (docs/)
│   └── regions.py                     # Region canonical names + codigo ONPE -> nombre
├── templates/
│   ├── nav.jinja                      # Navbar DRY, genera --cand-N, .v-N, .w-N, .sc-val-N
│   ├── index.html                     # Dashboard principal
│   ├── summary.html                   # Resumen ejecutivo
│   ├── anomalies.html                 # Panel de anomalias
│   ├── ballots.html                   # Explorador de actas
│   ├── participation.html             # Comparativa de participacion
│   ├── showcase.html                  # Narrativa visual
│   └── librejs.html                   # Web Labels (LibreJS compliance)
└── docs/
    ├── assets/
    │   ├── main.css                   # ~980 lineas, dark theme completo, index-based CSS
    │   ├── dashboard.js               # Refresh/config logic
    │   ├── chart.umd.min.js           # Chart.js v4.4.1 UMD (local, ~201 KB)
    │   ├── chart.umd.js.map           # Source map stub para Chart.js
    │   ├── lucide.min.js              # Lucide icons (local, ~402 KB)
    │   └── peru-departamentos.geojson # Mapa departamental (TopoJSON simplificado)
    └── data/
        ├── series-run2.json            # Cortes historicos
        ├── projection.json             # Proyeccion al 100 %
        ├── anomalies.json              # Anomalias detectadas
        ├── heartbeat.json              # Timestamp del ultimo scrape exitoso
        ├── ballots_observed_enriched.json  # Acta explorer data
        ├── ballots_observed_domestic.json  # Cache nacional
        ├── ballots_observed_abroad.json    # Cache extranjero
        ├── ubigeo_prefix_map.json       # Ubigeo 5-dig → departamento
        ├── participation_1v_2026.json
        └── participation_2v_2021.json

Fuente de datos

API oficial: https://resultadosegundavuelta.onpe.gob.pe (idEleccion=10). El backend solo devuelve JSON ante llamadas tipo XHR; ver headers en scripts/onpe_client.py. No requiere navegador.

Ubigeo interno ONPE: llega como entero (pierde el cero inicial). El departamento se deriva rellenando a 6 digitos y leyendo el codigo de 2 digitos contra regions.ONPE_COD (01-25, + 92 = EXTRANJERO); esto evita la colision 01/10 (AMAZONAS/ICA) y 02/20 (ANCASH/PUNO).

Datos historicos (locales, no versionados):

  • Vuelta 2026: auditor.sqlite (proyecto de Fase 1)
  • Vuelta 2021: CSV oficial de la PCM

Licencia

Codigo fuente bajo GNU Affero General Public License v3.0. Datos oficiales (c) ONPE -- uso permitido para fines informativos.


Sitio no oficial. Datos de 2026 preliminares y sujetos a cambio.