# 119th Congress Voting Dashboard — Interactive SPA Rewrite (FINAL v1) ## Context The user has iteratively built a working voting-dashboard pipeline in this session: `fetch.py`, `parse.py`, and `analyze.py` are written and produce a unified schema (1,342 cached XML rollcalls — 553 House + 789 Senate) for the 119th Congress. Eight standalone dashboard HTML files were previously generated (Massie, Khanna, AOC, Omar, MTG, Jordan, Donalds, Graham) and now live in `legacy/`. The user now wants: 1. A dashboard for **every member** of the 119th Congress (~535 House + Senate). 2. A **single interactive web page** with a member picker that re-renders without a page reload. 3. A **comparison page** for overlaying multiple representatives on shared charts. 4. **Framework-free** so it can be embedded into a third-party site (no React/Vue/Next). 5. **Static-file data hosting** — per-member JSON served alongside the page. 6. Member selector: **searchable typeahead dropdown PLUS sidebar filters** (chamber, party, state). 7. Structure must generalize to future Congresses via a `--congress N` CLI arg. 8. Cheap re-analysis: change `analyze.py`, re-render all member JSON in seconds; no re-fetch. This plan incorporates feedback from PM + programmer (Consult) + security (Consult) + compliance (Consult) agents. Decisions locked by the user: - **Labels:** keep current "Helped Republicans / Helped Democrats / Blocked Dem-Backed / Blocked GOP-Backed" wording. Editorial-bias concern flagged by compliance will be captured in `NOTES.md` (not on dashboards). - **Comparison page:** ship all 5 overlay charts in MVP. Programmer/PM concern about derivative charts will be captured in `NOTES.md`. - **Congress.gov API key:** redact from `DOCUMENTATION.md`, move to `.env` (gitignored) with a rotation note. The key has never been used by the pipeline. --- ## Target File Layout ``` polisci/ ├── DOCUMENTATION.md # existing — UPDATE §2 (redact key), §8 (file layout), §9 (regeneration) ├── NOTES.md # NEW — captures known concerns we chose not to fix in MVP ├── PROJECT_SCOPE.md # NEW — written by PM agent (only it may edit) ├── .env # NEW — gitignored — holds CONGRESS_GOV_API_KEY ├── .gitignore # NEW — at least: .env, __pycache__/, *.pyc, data/*/cache/ ├── fetch.py # existing — unchanged ├── parse.py # existing — minor: validate upstream strings (reject <, >, NUL) ├── analyze.py # existing — unchanged (output shape stable) ├── enrich_roster.py # NEW — Congress.gov API → data//members_directory.json (complete roster) ├── build_members.py # NEW — write data//members/.json + manifest.json (parallelized) ├── build_app.py # NEW — copy template/ → results//; embed manifest version; copy data/ ├── build_all.py # NEW — orchestrator: fetch → parse → build_members → build_app ├── tests/ # NEW — pytest unit tests for analyze.py with frozen fixtures │ ├── fixtures/ │ │ ├── partisan_house.xml │ │ ├── bipartisan_house.xml │ │ ├── partisan_senate.xml │ │ └── failed_blocking_senate.xml │ └── test_analyze.py ├── template/ # NEW — input templates copied at build time │ ├── app.html # single-member dashboard shell │ ├── compare.html # comparison shell │ ├── app.js # shared frontend logic │ ├── app.css # shared styles, all selectors namespaced under #polisci-root │ └── vendor/ # NEW — pinned local copies (no CDN) │ ├── chart.umd.min.js # Chart.js 4.4.0 │ └── sortable.min.js # SortableJS 1.15.2 ├── data/119/ │ ├── house/{cache/, votes.jsonl, roster.json} │ ├── senate/{cache/, votes.jsonl, roster.json} │ ├── members/.json # NEW — per-member metrics (~80 KB each) │ ├── manifest.json # NEW — array of {id,n,p,s,c,district,served_partial} for picker │ ├── members_directory.json # NEW — complete 119th roster from Congress.gov │ ├── lis_to_bioguide.json # NEW — Senate ID crosswalk │ ├── api_cache/ # NEW — cached Congress.gov responses (idempotent) │ └── build_report.json # NEW — per-build success/failure log ├── results/119/ # output — entire dir is the embeddable artifact │ ├── app.html │ ├── compare.html │ ├── app.js │ ├── app.css │ ├── vendor/{chart…, sortable…} │ ├── data/ │ │ ├── manifest.json │ │ └── members/.json │ └── README.md # NEW — embedding instructions + recommended CSP/sandbox snippet └── legacy/ # existing — archived after Milestone 0 validation passes ``` --- ## Frontend Architecture ### `app.html` — single-member view - URL: `app.html?id=M001184` (no `?c=` — Congress is implicit in the deploy path; programmer recommendation) - **Sidebar** (collapsible on mobile): Chamber checkboxes (House / Senate), Party checkboxes (R / D / I), State multi-select populated from manifest. - **Searchable typeahead**: case-insensitive substring + initials match ("AOC" → Ocasio-Cortez). Iterates `manifest` as an **array** (not object) for sort/filter speed. Filtered live by sidebar. - **On selection**: fetch `data/members/.json?v={manifest.version}` (cache-busted on classifier changes), then **mutate existing Chart.js datasets in place** and call `chart.update('none')` — no teardown/rebuild per switch. All 5 charts created once at page init. - **State persistence**: URL via `history.replaceState` for filter typing, `history.pushState` only on member selection. localStorage as Could-Have (per PM), namespaced as `polisci:v119:lastMember`, validated against manifest on read. ### `compare.html` — overlay view - URL: `compare.html?ids=M001184,K000389,O000172` (shareable; cap parse at 6 IDs; validate each against manifest) - Same sidebar + typeahead. Multi-select pills (color-coded by member-assigned color). - **All 5 overlay charts** per user decision: 1. Alignment-over-time (line, one per member; switcher for which alignment class) 2. Voted-against-own-party rate over time (line) 3. Side-by-side KPI grouped bar (% against GOP, % against Dem, Lone Wolf %, Participation %, Blocked counts) 4. Defection scatter (X: % against GOP, Y: % against Dem; one dot per member, party color) 5. Vote-distribution grouped bar (Yea/Nay/Present/Not Voting per member) - Pill click opens member's `app.html?id=` in a new tab. ### Embedding contract Three modes, all supported by the same artifact in `results/119/`: 1. **Standalone**: open `app.html` directly. 2. **Iframe**: `