- Add secrets/db_password file support to docker-compose.yml (Docker secrets mount)
- config.py reads POSTGRES_PASSWORD_FILE if set, builds DATABASE_URL with proper URL encoding
- Remove inline DATABASE_URL construction from docker-compose.yml (was subject to $VAR interpolation)
- Any password with any characters now works — no escaping needed
Authored by: Jack Levy
Previously skipped bills that had any stored votes. Now also re-queues
bills where latest_action_date > MAX(vote_date), catching new votes on
already-tracked bills.
Authored by: Jack Levy
Docker Compose interprets $ as variable substitution in unquoted values.
Passwords containing $, &, # etc. must be wrapped in single quotes.
Authored by: Jack Levy
- Handle terms as list or dict (Congress.gov API inconsistency)
- Infer 'House of Representatives' from district presence as fallback
- Convert district to int before str() to avoid leading zeros / 'None'
Authored by: Jack Levy
Same bug as members.py: health check used old CD\d+FP$ regex (no match
for CD119) and skipped GEOID. Now mirrors members.py logic: GEOID primary,
STATE+CD\d+ fallback, Congressional layer filter.
Authored by: Jack Levy
Actual API fields are STATE (not STATEFP) and CD119 (not CD119FP).
GEOID primary path works for regular districts; fallback now uses
STATE + CD\d+ pattern confirmed against live TIGERweb responses.
Authored by: Jack Levy
The 119th Congressional Districts layer uses 2020 Census vintage field
names (GEOID20, STATEFP20, CD119FP) instead of GEOID. The GEOID check
was silently falling through; added GEOID20 fallback, isdigit() guard,
try/except on CD field parsing, and debug logging of unparseable layers.
Authored by: Jack Levy
- docker-compose.yml: replace named volumes with ./postgres/data and ./redis/data bind mounts
- .gitignore: exclude postgres/ and redis/ data directories
- DEPLOYING.md: update clone URL to public PocketVeto repo
- UPDATING.md: fix paths (~/pocketveto), clone URL, webhook IDs
Authored by: Jack Levy
- README.md: feature overview, tech stack, quick-start guide
- LICENSE: GNU General Public License v3.0
- .env.example: add ENCRYPTION_SECRET_KEY, LLM_RATE_LIMIT_RPM, correct model defaults
Authored by: Jack Levy
- ROADMAP.md: mark all v0.9.8–v0.9.10 items shipped; Phase 4
accountability features complete; v1.0 criteria all met; update to
reflect current state as of v0.9.10
- DEPLOYING.md: add SMTP/email section, ENCRYPTION_SECRET_KEY entry,
fix OPENAI_MODEL default (gpt-4o → gpt-4o-mini), add pocketveto.org
reference
- UPDATING.md: replace personal git remote with YOUR_GIT_REMOTE
placeholder for public deployability
- ARCHITECTURE.md: add member_scores table, alignment API, LLM Batch
API, email unsubscribe, bill tab UI, topic tags constant, Fernet
encryption pattern, feature history through v0.9.10
Authored by: Jack Levy
UI/UX:
- Bill detail page tab UI (Analysis / Timeline / Votes / Notes)
- Topic tag pills on bill detail and listing pages — filtered to known
topics, clickable, properly labelled via shared lib/topics.ts
- Notes panel always-open in Notes tab; sign-in prompt for guests
- Collapsible sidebar with icon-only mode and localStorage persistence
- Bills page defaults to has-text filter enabled
- Follow mode dropdown transparency fix
- Favicon (Landmark icon, blue background)
Security:
- Fernet encryption for ntfy passwords at rest (app/core/crypto.py)
- Separate ENCRYPTION_SECRET_KEY env var; falls back to JWT derivation
- ntfy_password no longer returned in GET response — replaced with
ntfy_password_set: bool; NotificationSettingsUpdate type for writes
- JWT_SECRET_KEY fail-fast on startup if using default placeholder
- get_optional_user catches (JWTError, ValueError) only, not Exception
Bug fixes & code quality:
- Dashboard N+1 topic query replaced with single OR query
- notification_utils.py topic follower N+1 replaced with batch query
- Note query in bill detail page gated on token (enabled: !!token)
- search.py max_length=500 guard against oversized queries
- CollectionCreate.validate_name wired up with @field_validator
- LLM_RATE_LIMIT_RPM default raised from 10 to 50
Authored by: Jack Levy
Auto-detect SSL vs STARTTLS based on port number instead of always
using SMTP + starttls(), which times out on port 465 (implicit SSL).
Authored by: Jack Levy
vote_fetcher was missing from Celery's include list (task not registered with
workers) and had no beat schedule — votes only fetched on-demand when a user
visited a bill's votes page. Stanced bills (pocket_veto/pocket_boost) never had
votes fetched, leaving the alignment page blank.
Add fetch_votes_for_stanced_bills nightly task (4:30 AM UTC) that queues
fetch_bill_votes for every bill any user has stanced but has no stored votes.
Register vote_fetcher in the include list and add it to the polling queue route.
Authored by: Jack Levy
Quoteless unlabeled points (old-format briefs with no citation system) were
being auto-labeled via raw SQL after db.get() loaded them into the session
identity map. SQLAlchemy's commit-time flush could re-emit the ORM object's
cached state, silently overwriting the raw UPDATE.
Fix: run a single bulk SQL UPDATE for all matching rows before any ORM objects
are loaded into the session. The commit is then a clean single-statement
transaction with nothing to interfere. LLM classification of quoted points
continues in a separate pass with normal flag_modified + commit.
Authored by: Jack Levy
Dashboard _get_trending() was querying scores within 1 day only — if the
nightly trend task hadn't run (e.g. worker restarted mid-run), the trending
section returned empty. Now falls back through 1→3→7→30 day windows so
stale scores always surface something.
Trend scorer now wraps per-bill scoring in try/except so a single bad
newsapi/gnews call can't abort the entire 1600-bill run.
Authored by: Jack Levy
- Migration 0019: email_unsubscribe_token column on users (unique, indexed)
- Token auto-generated on first email address save (same pattern as RSS token)
- GET /api/notifications/unsubscribe/{token} — no auth required, sets
email_enabled=False and returns a branded HTML confirmation page
- List-Unsubscribe + List-Unsubscribe-Post headers on every email
(improves deliverability; enables one-click unsubscribe in Gmail/Outlook)
- Unsubscribe link appended to email body plain text
Authored by: Jack Levy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add email as a second notification channel alongside ntfy:
- Tabbed channel selector: ntfy | Email | Telegram (coming soon) | Discord (coming soon)
- Active channel shown with green status dot on tab
- Email tab: address input, Save & Enable, Test, Disable — same UX pattern as ntfy
- Backend: SMTP config in settings (SMTP_HOST/PORT/USER/PASSWORD/FROM/STARTTLS)
- Dispatcher: _send_email() helper wired into dispatch_notifications
- POST /api/notifications/test/email endpoint with descriptive error messages
- Email fires in same window as ntfy (respects quiet hours / digest hold)
- Telegram and Discord tabs show coming-soon banners with planned feature description
- .env.example documents all SMTP settings with provider examples
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Congress.gov occasionally returns the same member twice on a single page
with different sponsorship dates (observed: Sen. Warnock on 119-s-1383).
The DB uniqueness check didn't catch this because the first insert hadn't
been committed yet when processing the duplicate row, causing a
UniqueViolation. Fix adds an `inserted_this_run` set to skip bioguide_ids
already added in the current fetch loop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Member Effectiveness Score
- New BillCosponsor table (migration 0018) with per-bill co-sponsor
party data required for the bipartisan multiplier
- bill_category column on Bill (substantive | commemorative | administrative)
set by a cheap one-shot LLM call after each brief is generated
- effectiveness_score / percentile / tier columns on Member
- New bill_classifier.py worker with 5 tasks:
classify_bill_category — triggered from llm_processor after brief
fetch_bill_cosponsors — triggered from congress_poller on new bill
calculate_effectiveness_scores — nightly at 5 AM UTC
backfill_bill_categories / backfill_all_bill_cosponsors — one-time
- Scoring: distance-traveled pts × bipartisan (1.5×) × substance (0.1×
for commemorative) × leadership (1.2× for committee chairs)
- Percentile normalised within (seniority tier × party) buckets
- Effectiveness card on member detail page with colour-coded bar
- Admin panel: 3 new backfill/calculate controls in Maintenance section
Representation Alignment View
- New GET /api/alignment endpoint: cross-references user's stanced bill
follows (pocket_veto/pocket_boost) with followed members' vote positions
- Efficient bulk queries — no N+1 loops
- New /alignment page with ranked member list and alignment bars
- Alignment added to sidebar nav (auth-required)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Submit up to 1000 unbriefed documents to the provider Batch API in one
shot instead of individual synchronous LLM calls. Results are polled
every 30 minutes via a new Celery beat task and imported automatically.
- New worker: llm_batch_processor.py
- submit_llm_batch: guards against duplicate batches, builds JSONL
(OpenAI) or request list (Anthropic), stores state in AppSetting
- poll_llm_batch_results: checks batch status, imports completed
results with idempotency, emits notifications + triggers news fetch
- celery_app: register worker, route to llm queue, beat every 30 min
- admin API: POST /submit-llm-batch + GET /llm-batch-status endpoints
- Frontend: submitLlmBatch + getLlmBatchStatus in adminAPI; settings
page shows batch control row (openai/anthropic only) with live
progress line while batch is processing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nine efficiency improvements across the data pipeline:
1. NewsAPI OR batching (news_service.py + news_fetcher.py)
- Combine up to 4 bills per NewsAPI call using OR query syntax
- NEWSAPI_BATCH_SIZE=4 means ~4× effective daily quota (100→400 bill-fetches)
- fetch_news_for_bill_batch task; fetch_news_for_active_bills queues batches
2. Google News RSS cache (news_service.py)
- 2-hour Redis cache shared between news_fetcher and trend_scorer
- Eliminates duplicate RSS hits when both workers run against same bill
- clear_gnews_cache() admin helper + admin endpoint
3. pytrends keyword batching (trends_service.py + trend_scorer.py)
- Compare up to 5 bills per pytrends call instead of 1
- get_trends_scores_batch() returns scores in original order
- Reduces pytrends calls by ~5× and associated rate-limit risk
4. GovInfo ETags (govinfo_api.py + document_fetcher.py)
- If-None-Match conditional GET; DocumentUnchangedError on HTTP 304
- ETags stored in Redis (30-day TTL) keyed by MD5(url)
- document_fetcher catches DocumentUnchangedError → {"status": "unchanged"}
5. Anthropic prompt caching (llm_service.py)
- cache_control: {type: ephemeral} on system messages in AnthropicProvider
- Caches the ~700-token system prompt server-side; ~50% cost reduction on
repeated calls within the 5-minute cache window
6. Async sponsor fetch (congress_poller.py)
- New fetch_sponsor_for_bill Celery task replaces blocking get_bill_detail()
inline in poll loop
- Bills saved immediately with sponsor_id=None; sponsor linked async
- Removes 0.25s sleep per new bill from poll hot path
7. Skip doc fetch for procedural actions (congress_poller.py)
- _DOC_PRODUCING_CATEGORIES = {vote, committee_report, presidential, ...}
- fetch_bill_documents only enqueued when action is likely to produce
new GovInfo text (saves ~60–70% of unnecessary document fetch attempts)
8. Adaptive poll frequency (congress_poller.py)
- _is_congress_off_hours(): weekends + before 9AM / after 9PM EST
- Skips poll if off-hours AND last poll < 1 hour ago
- Prevents wasteful polling when Congress is not in session
9. Admin panel additions (admin.py + settings/page.tsx + api.ts)
- GET /api/admin/newsapi-quota → remaining calls today
- POST /api/admin/clear-gnews-cache → flush RSS cache
- Settings page shows NewsAPI quota remaining (amber if < 10)
- "Clear Google News Cache" button in Manual Controls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 4th "Discovery" tab in Alert Filters for member/topic follow notifications,
with per-source enable toggle, independent event-type filters, and per-entity
mute chips (mute specific members/topics without unfollowing)
- Enrich notification event payloads with follow_mode, matched_member_name,
matched_member_id, and matched_topic so each event knows why it was created
- Dispatcher branches on payload.source for member_follow/topic_follow events,
checking source-level enabled toggle, per-event-type filters, and muted_ids/muted_tags
- Add _build_reason helper; ntfy messages append a "why" line (📌/👤/🏷)
- EventRow in notification history shows a small italic reason line
- Update How It Works: fix stale member/topic paragraph, add Discovery alerts item
Authored-by: Jack Levy
Roll-call votes:
- Migration 0017: bill_votes + member_vote_positions tables
- Fetch vote XML directly from House Clerk / Senate LIS URLs
embedded in bill actions recordedVotes objects
- GET /api/bills/{id}/votes triggers background fetch on first view
- VotePanel on bill detail: yea/nay bar, result badge, followed
member positions with Sen./Rep. title, party badge, and state
Alert filter fix:
- _should_dispatch returns True when alert_filters is None so users
who haven't saved filters still receive all notifications
Authored-By: Jack Levy
Topic events that fire a push notification (milestones like
calendar placement, passed, new text) now show ✓ in the
"Based on your topic follows" section, consistent with the
Recent Alerts section. Also clarifies the section description
to explain which events are pushed vs suppressed.
Authored-By: Jack Levy
The dispatcher was suppressing all referral-tier events (committee
referrals) for neutral-mode users, regardless of whether they
directly followed a bill or just followed a topic. This meant
directly-followed bills like HR 7711 and S 3853 showed ✓ in
Recent Alerts but no ntfy notification was ever fired.
Now only topic-follow referral events are suppressed for neutral
users (topic follows are loose and noisy). Direct bill follows and
member follows always receive referral events.
Authored-By: Jack Levy
ZIP lookup (GET /api/members/by-zip/{zip}):
- Two-step geocoding: Nominatim (ZIP → lat/lng) then Census TIGERweb
Legislative identify (lat/lng → congressional district via GEOID)
- Handles at-large states (AK, DE, MT, ND, SD, VT, WY)
- Added rep_lookup health check to admin External API Health panel
congress_api.py fixes:
- parse_member_from_api: normalize state full name → 2-letter code
(Congress.gov returns "Florida", DB expects "FL")
- parse_member_from_api: read district from top-level data field,
not current_term (district is not inside the term object)
Celery beat: schedule sync_members daily at 1 AM UTC so chamber,
district, and contact info stay current without manual triggering
Members page redesign: photo avatars, party/state/chamber chips,
phone + website links, ZIP lookup form to find your reps
Draft letter improvements: pass rep_name from ZIP lookup so letter
opens with "Dear Representative Franklin," instead of generic salutation;
add has_document filter to bills list endpoint
UX additions: HelpTip component, How It Works page, "How it works"
sidebar nav link, collections page description copy
Authored-By: Jack Levy
- Each section (Bills, Members, Topics) collapses/expands independently,
open by default
- Search input per section filters by bill label/title, member name,
or topic string
- Chamber filter for bills, party filter for members — dropdowns only
appear when more than one value is present in the loaded data
- useQueries batch-fetches bill/member data at page level for filtering;
shares React Query cache with individual rows so no extra API calls
Authored-By: Jack Levy
Removes --reload from uvicorn, adds restart: unless-stopped to all
services, bumps uvicorn to 2 workers.
Authored-By: Jack Levy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin-only actions belong in the admin panel, not the dashboard.
Cleaned up dead imports and "Run a poll to populate" copy.
Authored-By: Jack Levy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 3 completion — Personal Workflow feature set is now complete.
Collections / Watchlists:
- New tables: collections (UUID share_token, slug, public/private) and
collection_bills (unique bill-per-collection constraint)
- Full CRUD API at /api/collections with bill add/remove endpoints
- Public share endpoint /api/collections/share/{token} (no auth)
- /collections list page with inline create form and delete
- /collections/[id] detail page: inline rename, public toggle,
copy-share-link, bill search/add/remove
- CollectionPicker bookmark-icon popover on bill detail pages
- Collections nav link in sidebar (auth-required)
Shareable Brief Links:
- share_token UUID column on bill_briefs (backfilled on migration)
- Unified public share router at /api/share (brief + collection)
- /share/brief/[token] — minimal layout, full AIBriefCard, CTAs
- /share/collection/[token] — minimal layout, bill list, CTA
- Share2 button in BriefPanel header row, "Link copied!" flash
AuthGuard: /collections → AUTH_REQUIRED; /share prefix → NO_SHELL_PATHS
Authored-By: Jack Levy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add bill_notes table schema (migration 0014)
- Add missing migrations 0012 and 0013 to the migrations table
- Add /api/notes endpoints section
- Add ntfy test, RSS test, follow-mode test, and history endpoints to /api/notifications
- Add POST /trigger-weekly-digest to admin API table
- Add weekly digest Monday beat schedule entry
- Update users.notification_prefs to document timezone field
- Update notifications.event_type to include weekly_digest
- Add NotesPanel.tsx to Frontend Key Components
- Add v0.7.0 (weekly digest + local-time quiet hours) to Feature History
- Add v0.8.0 (personal notes) to Feature History
Authored-By: Jack Levy
- bill_notes table (migration 0014): user_id, bill_id, content, pinned,
created_at, updated_at; unique constraint (user_id, bill_id)
- BillNote SQLAlchemy model with back-refs on User and Bill
- GET/PUT/DELETE /api/notes/{bill_id} — auth-required, one note per (user, bill)
- NotesPanel component: collapsible, auto-resize textarea, pin toggle,
save + delete; shows last-saved date and pin indicator in collapsed header
- Pinned notes render above BriefPanel; unpinned render below DraftLetterPanel
- Guests see nothing (token guard in component + query disabled)
Co-Authored-By: Jack Levy
Weekly Digest (send_weekly_digest Celery task):
- Runs every Monday 8:30 AM UTC via beat schedule
- Queries all followed bills updated in the past 7 days per user
- Sends low-priority ntfy push (Priority: low, Tags: newspaper,calendar)
- Creates a NotificationEvent (weekly_digest type) for RSS feed visibility
- Admin can trigger immediately via POST /api/admin/trigger-weekly-digest
- Manual Controls panel now includes "Send Weekly Digest" button
Local-time quiet hours:
- Browser auto-detects IANA timezone via Intl.DateTimeFormat().resolvedOptions().timeZone
- Timezone saved to notification_prefs alongside quiet_hours_start/end on Save
- Dispatcher converts UTC → user's local time (zoneinfo stdlib) before hour comparison
- Falls back to UTC if timezone absent or unrecognised
- Quiet hours UI: 12-hour AM/PM selectors, shows detected timezone as hint
- Clearing quiet hours also clears stored timezone
Co-Authored-By: Jack Levy
- Add DraftLetterPanel: collapsible UI below BriefPanel for bills with a
brief; lets users select up to 3 cited points, pick stance/tone, and
generate a plain-text letter via the configured LLM provider
- Stance pre-fills from follow mode (pocket_boost → YES, pocket_veto → NO)
and clears when the user unfollows; recipient derived from bill chamber
- Add POST /api/bills/{bill_id}/draft-letter endpoint with proper LLM
provider/model resolution from AppSetting (respects Settings page choice)
- Add generate_text() to LLMProvider ABC and all four providers
- Expose has_document on BillSchema (list endpoint) via a single batch
query; BillCard shows Brief / Pending / No text indicator per bill
Authored-By: Jack Levy
- Add get_optional_user dependency; dashboard returns guest-safe payload
- AuthGuard only redirects /following and /notifications for guests
- Sidebar hides auth-required nav items and shows Sign In/Register for guests
- Dashboard shows trending bills as "Most Popular" for unauthenticated visitors
- FollowButton opens AuthModal instead of acting when not signed in
- Members page pins followed members at the top for quick unfollowing
- useFollows skips API call and invalidates dashboard on follow/unfollow
Authored-By: Jack Levy
Test notification:
- Click header -> {PUBLIC_URL}/notifications so tapping the test opens the app
Real bill alerts (dispatcher):
- Title reformatted: "New Bill Text: HR 1234" (event type + bill identifier)
- Body: bill full name on first line, AI summary below (300 chars)
- Tags updated per event type (page, memo, siren) instead of generic scroll
- Click header was already set from bill_url; no change needed there
Authored-By: Jack Levy