# Legislative Dashboard This is the anonymous + logged-in dashboard for `cali_scraper`, implementing spec section 5 (design documents: `design/API_CONTRACT.md`, `design/DESIGN_DASHBOARD.md`). It is served by a stdlib `http.server`, backed by the SQLite relational DB and the tag-cloud JSON, and produces the frozen API-contract response shapes. ## Architecture ``` scraper/dashboard/ __init__.py # package docstring + version ranking.py # pure I/O-free float-to-top scoring (tested in isolation) context.py # RelevanceContext from user prefs data.py # read-only SQLite access, tag enrichment, committee lookup prefs.py # JSON prefs store (watch/topic toggles, dev auth) resolve.py # ZIP -> district two-tier resolution api.py # API_CONTRACT handlers (no sockets) + 6 empty states server.py # http.server ThreadingHTTPServer + static file routing dashboard_cli.py # guarded CLI entry via cli.py subcommand registration static/ index.html # frontend shell style.css # visual system app.js # fetch client, anon/personal rendering, ZIP resolve, watch/opt-in ``` ### How it fits the CLI `scraper/cli.py` wires in the dashboard with the same guarded pattern as `analyze_cli` and `relate_cli`: ```python try: from .dashboard import dashboard_cli as _dcli _dcli.register(sub) except Exception: pass ``` A broken or missing dashboard module never breaks `parse`, `correlate`, or `analyze`. ### Run it ```bash python3 -m scraper dashboard --port 8733 # browse http://127.0.0.1:8733/ ``` ## API | Method | Path | Auth | Purpose | |--------|------------------------|-------|--------------------------------------| | GET | `/api/v1/bills` | any | Bill list, personal / anonymous | | GET | `/api/v1/tags` | any | Tag cloud | | GET | `/api/v1/legislators` | any | Legislators | | GET | `/api/v1/districts` | any | Districts by chamber/zip | | GET | `/api/v1/resolve` | any | ZIP -> district resolution | | POST | `/api/v1/prefs` | auth | Update user prefs | | POST | `/api/v1/auth/register`| none | Dev auth placeholder | | POST | `/api/v1/prefs/watch` | auth | Add/remove watched legislator | | POST | `/api/v1/prefs/topics` | auth | Add/remove opted-in topic | ### Float-to-top ranking Personal view applies a pure scoring function (`ranking.py`) over the bill list: - **Watched-legislator match (author)** = +3 - **Watched-legislator's committee match** = +2 - **Opted-in tag match** = +2 × (number of matched tags) - **Home-chamber match** = +1 (optional) - Bills with score > 0 are moved to the front with a "Relevant to you" marker and a "Floated: ..." reason line - Zero-scored bills remain in their date-grouped order below ## Tests ```bash python3 -m pytest tests/test_dashboard_ranking.py tests/test_dashboard_api.py ``` 32 tests (12 ranking + 20 API), all passing. Pure ranking logic is tested in isolation; API handlers are tested with no sockets via the `handler(req) -> (status, body)` pattern; a live `ThreadingHTTPServer` smoke test confirms static + API endpoints serve. ## DASHBOARD.md This document. The full UX specification is `design/DESIGN_DASHBOARD.md` and the frozen API contract is `design/API_CONTRACT.md`. ## Renders - `out/dashboard_render_anon.png` — anonymous dashboard - `out/dashboard_render_personal.png` — personalized dashboard (ZIP 90210, watch Schiavo, opt-in budget topic) Both are data-faithful snapshots from the live API, rendered via macOS `qlmanage` (no headless browser available in this environment; the browser remote-debugging permission popup is a user-level security dialog that cannot be auto-approved). ## Known limitations / next steps - `out/tag_cloud.json` must exist for tags endpoint (118 tags as of 2026-09-06). - `0 committee_members` roster — committee lookup degrades gracefully when no roster data is supplied. - Dev auth is a JSON-file local account, not production auth. - ZIP -> district uses a documented demo dataset until roster ETL lands.