The daily-file bill queue is a backward snapshot, so "upcoming" could only resurface the latest past day (the 08-31 snapshot read as today's queue) and there was no "what is scheduled next." leginfo bill status/history are backward-only, so the genuine future-dated signal is the chambers' "upcoming events" calendars. - api.py: floor the "upcoming" view at *today* (not the DB max date) when no as_of is pinned; surface next_legislative_day + scheduled_events (next 7 days) on /bills. Add _today_iso / _next_legislative_day / _scheduled_events, all degrading to a recess marker / [] (never 5xx). - relational.py: schema v2 adds the scheduled_events table + store_scheduled_events (idempotent per-day refresh), load_scheduled_events, next_legislative_day. - parsers.py: parse_events_page + classify_event_kind for the chamber events calendar; skip empty-title rows (decompose the <time> before the fallback). - ingest.py + cli.py: --store-events lane (cache-first, recess-safe) that parses the calendar HTML into scheduled_events. - sources.py + config.yaml: register assembly_events / senate_events (kind: events, empty_is_ok: true). - run_ingest.sh: run the events lane after the daily-file lane as non-fatal (skippable via CALI_SCRAPER_SKIP_EVENTS=1); daily-file exit still gates cron. - frontend: "Next legislative day" line + "What's next · next 7 days" band with honest recess empty states. - DASHBOARD.md: document the forward signal + known limitations (leginfo backward-only gap, recess empty state, 08-31 snapshot is analysis-only). - tests: 5 new API tests + 18 parse-events tests; 170 pass, 92% coverage.
6.3 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
Forward signal: "what's next" (scheduled_events, schema v2)
The daily-file bill queue is a backward snapshot — leginfo's bill status and
bill history are past-action logs, so they cannot answer "what is scheduled
next." The only genuine future-dated signal is the chambers' "upcoming events"
calendars (assembly.ca.gov/events/upcoming, senate.ca.gov/calendar). These
are fetched (python3 -m scraper ingest --store-events), parsed into the
scheduled_events table (relational schema v2), and surfaced on /bills as two
new envelope keys:
next_legislative_day—{date, kind, chamber, title, source}(the earliest event on/afteras_of), or{date: null, recess: true}when nothing is scheduled.scheduled_events— the next 7 days of chamber events.
The "Upcoming" filter is a forward view: with upcoming=1 and no explicit
as_of, the bill queue floors at today, so a stale/older daily-file
snapshot can no longer present itself as "upcoming." Omitting upcoming
(the "All" / history view) still returns every row.
The dashboard renders a "Next legislative day" line in the freshness band and a "What's next · next 7 days" band. A recess (empty calendar) is an honest empty state, not an error.
Tests
python3 -m pytest tests/test_dashboard_ranking.py tests/test_dashboard_api.py \
tests/test_parse_events.py
55 tests (12 ranking + 25 API + 18 parse-events), 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. The event parser is tested against
inline fixtures plus the saved samples/assembly_events_*.html pages.
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.
- Forward signal gap: leginfo's bill status/history are backward-only
(past actions + status narrative, no future dates), so there is no per-bill
"next step" label. The dashboard's forward answer comes solely from the
chamber events calendars (
scheduled_events). - Recess empty state: while the Assembly (and at times the Senate) is in
final recess,
/events/upcominglegitimately returns 0 events. The "What's next" band andnext_legislative_daythen show an honest "recess / unknown" state rather than fabricating a date. This is expected, not a parsing failure. - The 2026-08-31 daily-file snapshot is analysis-only; it is not re-surfaced as "upcoming" because the upcoming view floors at today.