- HTML 56.3%
- Python 43.1%
- Jinja 0.6%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| docs | ||
| scripts | ||
| templates | ||
| tests | ||
| .gitignore | ||
| config.py | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
| server.py | ||
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,@licenseannotations en todo JS local. - Candidate-agnostic: CSS index-based (
--cand-0,.v-0,.w-0), templates iteran sobreCANDIDATESdeconfig.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/resolvedy 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.py — CANDIDATES 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(yseries-run2/projectionya normalizados endocs/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.