π³οΈ
election-tracker
Georgia 2026 primary winner predictor (Flask + weighted scoring model)
- Primary: https://github.com/klill6506/election-tracker
- GitHub: https://github.com/klill6506/election-tracker
- Local:
D:\Personal\election-tracker
README
# sherpa-dispatch
2026 general-election predictor β November 3, 2026.
Weighted prediction model for ~65 competitive U.S. Senate and U.S. House races. Per-race inputs come from Cook Political Report (rating + PVI), Kalshi (prediction-market prices), and structural signals (incumbency). Weights are slider-tweakable. iPad-first.
## Stack
- **Backend:** Flask 3 + PyYAML (production), `ruamel.yaml` + `requests` (data refresh)
- **Frontend:** Vanilla HTML/CSS/JS (no build step)
- **Hosting:** Render free tier, Ohio region (`render.yaml` Blueprint)
- **Data:** YAML files in `data/`; auto-refreshed by GitHub Actions crons
## Local run (Windows 11)
```powershell
cd D:\Personal\election-tracker
.\venv\Scripts\Activate.ps1
python app.py
# http://localhost:5000
```
## Deploy
Push to `main` β Render auto-redeploys via Blueprint.
## Data flow
```
Cook Political Report API βββΊ scripts/refresh_cook.py ββ
ββββΊ data/general_races.yaml
Kalshi public REST API βββΊ scripts/refresh_kalshi.py ββ β
βΌ
data/general_overrides.yaml (pin / exclude / kalshi_markets) app.py + scoring/general_engine.py
β
βΌ
templates/dashboard.html
```
Both refresh scripts run daily on GitHub Actions:
- `refresh-cook.yml` β 13:30 UTC (requires `COOK_API_EMAIL` + `COOK_API_PASSWORD` secrets)
- `refresh-kalshi.yml` β 14:30 UTC (no auth needed)
To pin or exclude races, or to set challenger/open-seat nominee names, edit [`data/general_overrides.yaml`](data/general_overrides.yaml). Kalshi (Senate) needs no mapping β it's auto-discovered from each race's state.
## Model β two whole-seat forecasts
Each chamber shows two forecasts, both built from Cook ratings:
- **Cook forecast
β¦(truncated for upload size)
STATUS
# STATUS.md β election-tracker
Updated **2026-06-11** β migrated to Coolify (election.kenlill.com); Render running in parallel.
## β
2026-06-11 β Render β Coolify migration (live + verified)
- **Live at https://election.kenlill.com** (Coolify app `d1095pemrsimply3wy08wbdi` on kenlill.com; TLS via Traefik/Let's Encrypt, wildcard DNS β zero Cloudflare changes needed).
- **What was added to the repo:** `Dockerfile` (`python:3.12-slim` + curl + gunicorn:8000) and `.dockerignore` (commit `e35c726`). `render.yaml` untouched β Render ignores the Dockerfile and keeps deploying in parallel.
- **No volume, no runtime env vars** β the server is stateless (YAML data baked in from git; Cook/Kalshi/approval refreshes run in GH Actions, credentials live only in GH secrets).
- **Auto-deploy on push:** GitHub webhook (id `639943814`) β Coolify, secret PATCHed in; ping 200. Repo is private β Coolify pulls via read-only deploy key (GitHub id `154227674`).
- **Verified:** `/health` 200; dashboard byte-identical to Render; `POST /api/recalculate {"wave":1.5}` identical to Render's output; 400 on out-of-range wave. **End-to-end freshness proof:** `workflow_dispatch` of refresh-kalshi β auto-commit (`8115281`) β webhook β Coolify auto-redeploy β live `kalshi_refreshed_at` advanced 17:51β22:42 UTC. (Coolify served the new data before Render finished its own redeploy.)
- **β³ Ken's call (no action pending from Claude):** suspend the Render service (https://sherpa-dispatch.onrender.com) after a few days of election.kenlill.com behaving. Until then both hosts track `main`.
## β
Resolved β Cook API auth (2026-06-02 β 2026-06-08)
- **Root cause:** the GitHub Actions secret `COOK_API_PASSWORD` no longer matched the working Cook credentials (the password contains a `$`, which gets silently truncated if the secret was ever set from a `bash`/`zsh` shell with double quotes β `"β¦$4"` expands `$4` to nothing). Cook's API + Basic auth were fine the whole time: a fresh `curl.exe -u 'email:pass'` retur
β¦(truncated for upload size)
DECISIONS
# DECISIONS.md β election-tracker Architectural choices, recorded once. Don't re-litigate without new information. ## 2026-05-05 β Flask + vanilla HTML/CSS/JS over a build step **Decision:** Server-render Jinja2 templates; vanilla JS for slider interactivity. No React, Vite, or bundler. **Rationale:** Single-evening build. Scope is small (one page, one form). A build pipeline would add weight without delivering value. Vanilla JS is enough for the sliderβfetchβrender loop. **Reconsider only if:** the UI grows to multiple distinct pages with shared client state. ## 2026-05-05 β YAML files over a database **Decision:** Store all race/candidate data and default weights in `data/*.yaml`, version-controlled in git. **Rationale:** Data is small (~20 KB), changes infrequently (weekly), and is curated by one person. A database adds operational complexity (provisioning, migrations, backups) for zero gain. Git history doubles as an audit log of data edits. Render redeploys on push, so YAML changes flow live with no extra plumbing. **Reconsider only if:** multiple curators need to edit concurrently, or update frequency exceeds daily. ## 2026-05-06 β Render free tier, Ohio region, Blueprint deploy **Decision:** Use Render's `render.yaml` Blueprint to deploy the Flask app on the free web-service tier in the Ohio region. **Rationale:** Free, push-to-deploy, gunicorn-friendly, no Docker. Ohio is fine for an iPad-viewer in Georgia. The 15-min idle spin-down is acceptable for personal/low-traffic use. **Reconsider only if:** cold-start latency becomes annoying ($7/mo Starter eliminates it) or owner wants always-on monitoring. **Superseded 2026-06-11** β see "Migrate hosting to Coolify on kenlill.com" below. Render stays running in parallel until Ken suspends it. ## 2026-06-11 β Migrate hosting to Coolify on kenlill.com (Render runs in parallel) **Decision:** Deploy the app to Ken's self-hosted Coolify (https://coolify.kenlill.com, Hetzner) at **https://election.kenlill.com**, as β¦(truncated for upload size)
MEMORY
# MEMORY.md β election-tracker Accumulated context for future sessions. ## Hosting (2026-06-11) β Coolify deploy live, Render in parallel - **New URL: https://election.kenlill.com** β Coolify on kenlill.com (Hetzner `5.161.245.237`), app uuid `d1095pemrsimply3wy08wbdi`, project Apps / env production. Render (https://sherpa-dispatch.onrender.com) left RUNNING; Ken suspends it himself when satisfied. - **The app is stateless on the server**: no volume, no runtime env vars. All data is YAML in git; the GH Actions crons (Cook 13:30 / Kalshi 14:30 / approval 15:00 UTC) commit refreshed data and BOTH hosts auto-redeploy from `main`. - **The push webhook is load-bearing** (GitHub hook id `639943814` β `coolify.kenlill.com/webhooks/source/github/events/manual`): if it breaks, election.kenlill.com serves stale data while Render keeps updating β the Cook staleness banner would eventually surface it. - Private repo β Coolify pulls via read-only deploy key (GitHub key id `154227674`, Coolify key uuid `kb1bnn67rn64qmyvkvutzb6r`). - Dockerfile = `python:3.12-slim` + curl (Coolify healthchecks curl *inside* the container) + gunicorn on 8000. `render.yaml` (`runtime: python`) ignores it, so Render is unaffected. - Playbook + cross-app recipe: `V:\dev\sherpa-server\MIGRATION-RENDER-TO-COOLIFY.md` (`D:\dev\...` on the main machine). ## Origin - Built by a coworker in a single evening on **2026-05-05**. - Owner (Ken) reviewed it on **2026-05-06**, fixed three deploy blockers (debug=True, missing input validation, no YAML error handling), and pushed to GitHub + Render the same day. - Public-facing internal name: **sherpa-dispatch**. Folder/repo name: **election-tracker**. ## Scope (v1) - **Modeled (10):** GA Senate, Governor, Lt Gov, SoS, AG (5 statewide) + GA-01, 10, 11, 13, 14 (5 competitive House districts). - **Presumptive (9):** GA-02, 03, 04, 05, 06, 07, 08, 09, 12 β incumbents shown collapsed. - **Primary date:** 2026-05-19. App was originally built ~2 weeks before this. ## β¦(truncated for upload size)
CLAUDE.md
# CLAUDE.md β election-tracker
Project-specific rules for Claude. See `~/.claude/CLAUDE.md` for global standards.
## What this is
Internal name **sherpa-dispatch**. A Flask app that forecasts the **November 3, 2026 general election** for ~65 competitive U.S. Senate + U.S. House races. Two whole-seat forecasts per chamber:
- **Cook forecast** β pure Cook Political Report ratings (Lean/Likely/Solid β a called seat; Toss Up β unassigned).
- **Your forecast** β the same, shifted by one **National Environment ("wave") slider** (R ββββΊ D) the user drags. The wave moves Lean/Toss-up seats between columns.
Cook PVI and Kalshi market prices are carried as on-card context, not model inputs. The original GA-primary model AND the later sigmoid-of-weights model were both retired on 2026-06-03. See DECISIONS.md.
## Stack
- **Backend:** Flask 3.0 + PyYAML 6.0 + gunicorn 21.2
- **Frontend:** Vanilla HTML/CSS/JS β no build step, no framework
- **Hosting:** Coolify on kenlill.com (**https://election.kenlill.com**, deployed via `Dockerfile`) β primary since 2026-06-11. Render (free tier, `render.yaml` Blueprint, https://sherpa-dispatch.onrender.com) still runs in parallel until Ken suspends it.
- **Data:** YAML files (`data/general_races.yaml` auto-generated, `data/general_overrides.yaml` hand-maintained, `data/weights.yaml` defaults). No database.
- **Refresh:** GitHub Actions crons run `scripts/refresh_cook.py` (Cook API), `scripts/refresh_kalshi.py` (Kalshi public API), and `scripts/refresh_approval.py` (Wikipedia approval table) daily.
- **Approval anchor:** the wave slider auto-starts at the approval-implied prediction (`data/approval.yaml` β `scoring/forecast_context.py`). House feels the full wave; the **Senate feels half** (`SENATE_WAVE_FACTOR = 0.5`) β Senate is map/candidate-driven. Approval prediction is House-calibrated, central-not-a-floor.
- **Python:** 3.12. Scrapers use stdlib `html.parser` (no lxml).
## Conventions
- Data lives in YAML, never inline in Python. Co
β¦(truncated for upload size)
Diary mentions
No recent diary mentions.