4.2 KiB
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:
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
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
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 dashboardout/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.jsonmust exist for tags endpoint (118 tags as of 2026-09-06).0 committee_membersroster — 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.