ntdservices commited on
Commit
d3db06b
·
verified ·
1 Parent(s): bbe312d

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +66 -38
  2. index.html +98 -13
app.py CHANGED
@@ -6,8 +6,52 @@ import unicodedata, os
6
  app = Flask(__name__, static_folder='.', static_url_path='')
7
 
8
  API_KEY = "4uwfiazjez9koo7aju9ig4zxhr"
9
- ELECTION_DATE = "2024-11-05"
10
  BASE_URL = "https://api2-app2.onrender.com/v2/elections"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  @app.after_request
13
  def add_no_store(resp):
@@ -223,77 +267,61 @@ def per_county():
223
  # --- add this bulk endpoint next to /results_state ---
224
  @app.route("/results_districts")
225
  def per_districts_bulk():
226
- state = (request.args.get("state") or "").upper()
227
- if not state:
228
- return jsonify({"ok": False, "error": "missing-params"}), 200
229
-
230
- # hit simulator's districts endpoint (same host/date as BASE_URL/ELECTION_DATE)
231
- url_base = f"{BASE_URL}/{ELECTION_DATE}".replace("/v2/elections/", "/v2/districts/")
232
- headers = {"x-api-key": API_KEY}
233
- try:
234
- resp = requests.get(f"{url_base}?statepostal={state}&level=ru", headers=headers, timeout=6)
235
- if resp.status_code != 200:
236
- return jsonify({"ok": False, "error": f"upstream-{resp.status_code}"}), 200
237
- except Exception as e:
238
- return jsonify({"ok": False, "error": "api-unavailable", "detail": str(e)}), 200
239
 
240
- # parse XML into the bulk shape the frontend expects
241
  try:
242
- root = ET.fromstring(resp.text)
243
  except ET.ParseError:
244
  return jsonify({"ok": False, "error": "bad-xml"}), 200
245
 
246
- by_geo = {} # GEOID -> { candidates:[{name,party,votes}], total }
247
  for ru in root.iter("ReportingUnit"):
248
  geoid = (ru.attrib.get("DistrictId") or "").strip()
249
- if not geoid:
250
- continue
251
- cands = []
252
- total = 0
253
  for c in ru.findall("Candidate"):
254
  votes = _safe_int(c.attrib.get("VoteCount"))
255
- first = (c.attrib.get("First","") or "").strip()
256
- last = (c.attrib.get("Last","") or "").strip()
257
- full = f"{first} {last}".strip()
258
  party = (c.attrib.get("Party") or "").upper()
259
  cands.append({"name": full, "party": party, "votes": votes})
260
  total += votes
261
- by_geo[geoid] = {"candidates": cands, "total": total}
262
 
263
  return jsonify({
264
  "ok": True,
265
  "state": state,
266
  "office": "H",
267
- "districts": by_geo
268
  }), 200
269
 
270
 
 
271
  @app.route("/results_state")
272
  def per_state_bulk():
273
- """
274
- OPTIONAL bulk endpoint: return all counties for a state in one call.
275
- Front-end can switch to this to reduce thousands of requests → ~50.
276
- """
277
  state = (request.args.get("state") or "").upper()
278
  office = (request.args.get("office", "P") or "P").upper()
279
- if not state:
280
- return jsonify({"ok": False, "error": "missing-params"}), 200
 
 
281
 
282
  try:
283
- snap = _get_parsed_state(state, office)
284
  except Exception as e:
285
- return jsonify({"ok": False, "error": "api-unavailable", "detail": str(e)}), 200
286
-
287
- if not snap:
288
- return jsonify({"ok": False, "error": "no-snapshot"}), 200
289
 
290
  return jsonify({
291
  "ok": True,
292
  "state": state,
293
  "office": office,
294
- "counties": snap # { base_name: { candidates:[...], total:int }, ... }
295
  }), 200
296
 
 
297
  # (District endpoint left as-is; can be upgraded similarly later.)
298
  @app.route("/results_cd")
299
  def per_district():
 
6
  app = Flask(__name__, static_folder='.', static_url_path='')
7
 
8
  API_KEY = "4uwfiazjez9koo7aju9ig4zxhr"
 
9
  BASE_URL = "https://api2-app2.onrender.com/v2/elections"
10
+ ELECTION_DATE = "2024-11-05"
11
+ POLL_INTERVAL = 15 # seconds
12
+
13
+ _cache = {"states": {}, "districts": {}}
14
+ _cache_lock = threading.Lock()
15
+
16
+ import concurrent.futures
17
+
18
+ ALL_STATES = [
19
+ "AL","AK","AZ","AR","CA","CO","CT","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY",
20
+ "LA","MA","MD","ME","MI","MN","MO","MS","MT","NC","ND","NE","NH","NJ","NM","NV","NY",
21
+ "OH","OK","OR","PA","RI","SC","SD","TN","TX","UT","VA","VT","WA","WI","WV","WY"
22
+ ]
23
+
24
+ def fetch_one(url, cache_key, cache_bucket):
25
+ try:
26
+ r = requests.get(url, headers={"x-api-key": API_KEY}, timeout=10)
27
+ if r.ok:
28
+ with _cache_lock:
29
+ _cache[cache_bucket][cache_key] = {"payload": r.text, "ts": time.time()}
30
+ print("Fetched upstream:", cache_key, "->", len(r.text), "bytes")
31
+ except Exception as e:
32
+ print("Poll error:", e)
33
+
34
+ def poll_api():
35
+ while True:
36
+ start = time.time()
37
+ tasks = []
38
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
39
+ for state in ALL_STATES:
40
+ # P, S, G
41
+ for office in ["P","S","G"]:
42
+ url = f"{BASE_URL}/{ELECTION_DATE}?statepostal={state}&officeId={office}&level=ru"
43
+ tasks.append(pool.submit(fetch_one, url, (state, office), "states"))
44
+ # House
45
+ url = f"{BASE_URL}/{ELECTION_DATE}".replace("/v2/elections", "/v2/districts")
46
+ url = f"{url}?statepostal={state}&officeId=H&level=ru"
47
+ tasks.append(pool.submit(fetch_one, url, (state,), "districts"))
48
+ concurrent.futures.wait(tasks)
49
+ elapsed = time.time() - start
50
+ time.sleep(max(0, POLL_INTERVAL - elapsed))
51
+
52
+ threading.Thread(target=poll_api, daemon=True).start()
53
+
54
+
55
 
56
  @app.after_request
57
  def add_no_store(resp):
 
267
  # --- add this bulk endpoint next to /results_state ---
268
  @app.route("/results_districts")
269
  def per_districts_bulk():
270
+ state = (request.args.get("state") or "").upper()
271
+ with _cache_lock:
272
+ snap = _cache["districts"].get((state,))
273
+ if not snap:
274
+ return jsonify({"ok": False, "error": "no-snapshot"}), 200
 
 
 
 
 
 
 
 
275
 
 
276
  try:
277
+ root = ET.fromstring(snap["payload"])
278
  except ET.ParseError:
279
  return jsonify({"ok": False, "error": "bad-xml"}), 200
280
 
281
+ districts = {}
282
  for ru in root.iter("ReportingUnit"):
283
  geoid = (ru.attrib.get("DistrictId") or "").strip()
284
+ if not geoid: continue
285
+ cands, total = [], 0
 
 
286
  for c in ru.findall("Candidate"):
287
  votes = _safe_int(c.attrib.get("VoteCount"))
288
+ full = f"{(c.attrib.get('First','') or '').strip()} {(c.attrib.get('Last','') or '').strip()}".strip()
 
 
289
  party = (c.attrib.get("Party") or "").upper()
290
  cands.append({"name": full, "party": party, "votes": votes})
291
  total += votes
292
+ districts[geoid] = {"candidates": cands, "total": total}
293
 
294
  return jsonify({
295
  "ok": True,
296
  "state": state,
297
  "office": "H",
298
+ "districts": districts
299
  }), 200
300
 
301
 
302
+
303
  @app.route("/results_state")
304
  def per_state_bulk():
 
 
 
 
305
  state = (request.args.get("state") or "").upper()
306
  office = (request.args.get("office", "P") or "P").upper()
307
+ with _cache_lock:
308
+ snap = _cache["states"].get((state, office))
309
+ if not snap:
310
+ return jsonify({"ok": False, "error": "no-snapshot"}), 200
311
 
312
  try:
313
+ parsed = _parse_election_xml(snap["payload"])
314
  except Exception as e:
315
+ return jsonify({"ok": False, "error": "parse-failed", "detail": str(e)}), 200
 
 
 
316
 
317
  return jsonify({
318
  "ok": True,
319
  "state": state,
320
  "office": office,
321
+ "counties": parsed
322
  }), 200
323
 
324
+
325
  # (District endpoint left as-is; can be upgraded similarly later.)
326
  @app.route("/results_cd")
327
  def per_district():
index.html CHANGED
@@ -7,6 +7,24 @@
7
  <script src="https://d3js.org/d3.v6.min.js"></script>
8
  <script src="https://d3js.org/topojson.v3.min.js"></script>
9
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  :root{ --gop:#ec1d19; --dem:#0067cb; --panel:#f6f8fc; --line:#d9e1ef; }
11
  html,body{height:100%;margin:0;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#122}
12
  header{padding:.6rem 1rem;border-bottom:3px solid var(--dem);display:flex;align-items:center;justify-content:center}
@@ -54,6 +72,15 @@
54
  <g id="counties" style="display:none"></g>
55
  <g id="cds" style="display:none"></g>
56
  </svg>
 
 
 
 
 
 
 
 
 
57
  </div>
58
 
59
  <aside id="right">
@@ -128,6 +155,12 @@ raceBtnEls.forEach(btn=>{
128
  // Otherwise leave blank until user hits Refresh (or Auto).
129
  paintAll();
130
  rebuildWinners();
 
 
 
 
 
 
131
  setStatus(fetchedStatesByRace[currentRace].size ? 'Showing cached data.' : 'Blank. Click Refresh to fetch.');
132
  });
133
  });
@@ -138,13 +171,18 @@ autoBtn.onclick = ()=>{
138
  clearInterval(autoTimer); autoTimer=null;
139
  autoBtn.textContent='Start Auto';
140
  setStatus('Auto refresh off.');
 
 
141
  }else{
142
  autoTimer = setInterval(refreshAllForCurrentRace, 15000);
143
  autoBtn.textContent='Stop Auto';
144
  setStatus('Auto refresh on (15s).');
 
 
145
  }
146
  };
147
 
 
148
  /* ------------------------------- load maps -------------------------------- */
149
  Promise.all([
150
  d3.json("https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"),
@@ -234,21 +272,42 @@ Promise.all([
234
  });
235
 
236
  /* -------------------------- fetch (server snapshot) ------------------------ */
237
- async function refreshAllForCurrentRace(){
238
- if (!currentRace){ setStatus('Choose a race first.'); return; }
 
 
 
239
 
240
- setStatus('Refreshing… (server snapshot)');
241
- const states = Object.values(fipsToPostal);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- if (currentRace === 'H'){
244
- for (const st of states){ await fetchDistrictBulk(st); }
245
- } else {
246
- for (const st of states){ await fetchStateBulk(st, currentRace); }
247
- }
248
- setStatus('Last update: ' + new Date().toLocaleTimeString());
249
- paintAll();
250
- rebuildWinners();
251
- }
252
 
253
  async function fetchStateBulk(stateCode, race){
254
  const url = `/results_state?state=${stateCode}&office=${race}`;
@@ -494,6 +553,32 @@ function showCellDetails(title, cell, raceLabel){
494
  /* --------------------------------- helpers -------------------------------- */
495
  function labelFor(r){ return r==='P'?'President':r==='G'?'Governor':r==='S'?'Senate':'House'; }
496
  function setStatus(msg){ statusEl.textContent = msg; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  </script>
498
  </body>
499
  </html>
 
7
  <script src="https://d3js.org/d3.v6.min.js"></script>
8
  <script src="https://d3js.org/topojson.v3.min.js"></script>
9
  <style>
10
+ /* map HUD */
11
+ #hud{
12
+ position:absolute; left:10px; bottom:10px; z-index:99999;
13
+ background:rgba(10,25,45,.85); color:#e9f2ff;
14
+ border:1px solid rgba(255,255,255,.18); border-radius:10px;
15
+ padding:10px 12px; max-width:320px; font-size:12px; line-height:1.35;
16
+ box-shadow:0 6px 24px rgba(0,0,0,.25), inset 0 1px 0 rgba(255,255,255,.06);
17
+ backdrop-filter: blur(4px);
18
+ }
19
+ #hud b{ color:#fff }
20
+ #hud .row{ margin:2px 0 }
21
+ #hud .dot{ display:inline-block; width:6px; height:6px; border-radius:50%; background:#8fd;
22
+ margin-right:6px; vertical-align:middle; }
23
+ #hud .muted{ opacity:.8 }
24
+ #hud .bar{ height:6px; background:rgba(255,255,255,.15); border-radius:999px; overflow:hidden; margin-top:6px }
25
+ #hud .bar > i{ display:block; height:100%; width:0%; background:#7fd;
26
+ transition:width .2s ease }
27
+
28
  :root{ --gop:#ec1d19; --dem:#0067cb; --panel:#f6f8fc; --line:#d9e1ef; }
29
  html,body{height:100%;margin:0;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#122}
30
  header{padding:.6rem 1rem;border-bottom:3px solid var(--dem);display:flex;align-items:center;justify-content:center}
 
72
  <g id="counties" style="display:none"></g>
73
  <g id="cds" style="display:none"></g>
74
  </svg>
75
+ <div id="hud" aria-live="polite">
76
+ <div class="row"><span class="dot"></span><b>Status:</b> Idle</div>
77
+ <div class="row"><b>Race:</b> — <span class="muted">(counties/districts)</span></div>
78
+ <div class="row"><b>Source:</b> —</div>
79
+ <div class="row"><b>Progress:</b> <span id="hud-progress">0 / 0</span></div>
80
+ <div class="bar"><i id="hud-bar"></i></div>
81
+ <div class="row muted" id="hud-sub">—</div>
82
+ </div>
83
+
84
  </div>
85
 
86
  <aside id="right">
 
155
  // Otherwise leave blank until user hits Refresh (or Auto).
156
  paintAll();
157
  rebuildWinners();
158
+ hud.set('race', `${labelFor(currentRace)} — ${describeLayer()}`);
159
+ hud.set('source', 'Server snapshot (backend caches upstream ~15s)');
160
+ hud.set('status', 'Ready to fetch / paint');
161
+ hud.set('progress', { text:'0 / 0', pct:0 });
162
+ hud.set('sub', 'Switch races freely; cached data is reused.');
163
+
164
  setStatus(fetchedStatesByRace[currentRace].size ? 'Showing cached data.' : 'Blank. Click Refresh to fetch.');
165
  });
166
  });
 
171
  clearInterval(autoTimer); autoTimer=null;
172
  autoBtn.textContent='Start Auto';
173
  setStatus('Auto refresh off.');
174
+ hud.set('status','Auto refresh off');
175
+ hud.set('sub','—');
176
  }else{
177
  autoTimer = setInterval(refreshAllForCurrentRace, 15000);
178
  autoBtn.textContent='Stop Auto';
179
  setStatus('Auto refresh on (15s).');
180
+ hud.set('status','Auto refresh on (every 15s)');
181
+ hud.set('sub','Will refresh all states on schedule.');
182
  }
183
  };
184
 
185
+
186
  /* ------------------------------- load maps -------------------------------- */
187
  Promise.all([
188
  d3.json("https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"),
 
272
  });
273
 
274
  /* -------------------------- fetch (server snapshot) ------------------------ */
275
+ async function refreshAllForCurrentRace(){
276
+ if (!currentRace){
277
+ setStatus('Choose a race first.');
278
+ return;
279
+ }
280
 
281
+ hud.set('status', 'Refreshing…');
282
+ hud.set('source', 'Server snapshot (backend caches upstream ~15s)');
283
+ hud.set('race', `${labelFor(currentRace)} — ${describeLayer()}`);
284
+ hud.set('sub', 'Fetching all races, one state at a time; repainting after each.');
285
+
286
+ const states = Object.values(fipsToPostal);
287
+
288
+ let done = 0;
289
+ hud.set('progress', { text: `0 / ${states.length}`, pct: 0 });
290
+
291
+ // Always fetch all four races
292
+ for (const st of states){
293
+ await fetchStateBulk(st, 'P');
294
+ await fetchStateBulk(st, 'G');
295
+ await fetchStateBulk(st, 'S');
296
+ await fetchDistrictBulk(st);
297
+ done++;
298
+ hud.set('progress', {
299
+ text: `${done} / ${states.length}`,
300
+ pct: Math.round(done/states.length*100)
301
+ });
302
+ hud.set('sub', `Last: ${st} (${new Date().toLocaleTimeString()})`);
303
+ }
304
 
305
+ hud.set('status', 'Up to date');
306
+ hud.set('sub', 'Last paint: ' + new Date().toLocaleTimeString());
307
+ setStatus('Last update: ' + new Date().toLocaleTimeString());
308
+ paintAll();
309
+ rebuildWinners();
310
+ }
 
 
 
311
 
312
  async function fetchStateBulk(stateCode, race){
313
  const url = `/results_state?state=${stateCode}&office=${race}`;
 
553
  /* --------------------------------- helpers -------------------------------- */
554
  function labelFor(r){ return r==='P'?'President':r==='G'?'Governor':r==='S'?'Senate':'House'; }
555
  function setStatus(msg){ statusEl.textContent = msg; }
556
+ /* ------------------------------- HUD helpers ------------------------------- */
557
+ const hud = {
558
+ el: document.getElementById('hud'),
559
+ progEl: document.getElementById('hud-progress'),
560
+ barEl: document.getElementById('hud-bar'),
561
+ subEl: document.getElementById('hud-sub'),
562
+ set(k, v){
563
+ const map = {
564
+ status: 0, race: 1, source: 2, progress: 3, sub: 5
565
+ };
566
+ const rows = this.el.querySelectorAll('.row');
567
+ if (k === 'status') rows[0].innerHTML = `<span class="dot"></span><b>Status:</b> ${v}`;
568
+ if (k === 'race') rows[1].innerHTML = `<b>Race:</b> ${v}`;
569
+ if (k === 'source') rows[2].innerHTML = `<b>Source:</b> ${v}`;
570
+ if (k === 'progress'){
571
+ this.progEl.textContent = v.text;
572
+ this.barEl.style.width = v.pct + '%';
573
+ }
574
+ if (k === 'sub') this.subEl.textContent = v;
575
+ }
576
+ };
577
+
578
+ function describeLayer(){
579
+ return (currentRace === 'H') ? 'districts (House)' : 'counties (' + labelFor(currentRace) + ')';
580
+ }
581
+
582
  </script>
583
  </body>
584
  </html>