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
- 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>
- 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
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
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
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
Notifications:
- New /notifications page accessible to all users (ntfy + RSS config)
- ntfy now supports no-auth, Bearer token, and HTTP Basic auth (for ACL-protected self-hosted servers)
- RSS enabled/disabled independently of ntfy; token auto-generated on first GET
- Notification settings removed from admin-only Settings page; replaced with link card
- Sidebar adds Notifications nav link for all users
- notification_dispatcher.py: fan-out now marks RSS events dispatched independently
Action history:
- Migration 0012: deduplicates existing bill_actions rows and adds UNIQUE(bill_id, action_date, action_text)
- congress_poller.py: replaces existence-check inserts with ON CONFLICT DO NOTHING (race-condition safe)
- Added backfill_all_bill_actions task (no date filter) + admin endpoint POST /backfill-all-actions
Authored-By: Jack Levy