feat: granular per-mode alert filters (v0.9.3)

Replace coarse milestone/referral suppression with 8 named action
categories (vote, presidential, committee_report, calendar, procedural,
referral, new_document, new_amendment), each independently togglable
per follow mode (Follow / Pocket Veto / Pocket Boost).

- notification_utils: categorize_action() replaces is_milestone_action /
  is_referral_action; _build_payload stores action_category in payload
- congress_poller: use categorize_action() in _update_bill_if_changed
- notification_dispatcher: _should_dispatch() checks per-mode filter dict
  from notification_prefs; follow mode looked up before filter check
- schemas + api: alert_filters (nested dict) wired through settings
  GET/PUT endpoints; no DB migration required
- frontend: tabbed Alert Filters section (Follow / Pocket Veto /
  Pocket Boost), each with independent 8-toggle filter set, milestone
  parent checkbox (indeterminate-aware), Load defaults button, and
  per-tab Save button

Authored-By: Jack Levy
This commit is contained in:
Jack Levy
2026-03-02 19:05:24 -05:00
parent af821dad78
commit a39ae4ccba
7 changed files with 262 additions and 58 deletions

View File

@@ -50,6 +50,7 @@ def _prefs_to_response(prefs: dict, rss_token: str | None) -> NotificationSettin
quiet_hours_start=prefs.get("quiet_hours_start"),
quiet_hours_end=prefs.get("quiet_hours_end"),
timezone=prefs.get("timezone"),
alert_filters=prefs.get("alert_filters"),
)
@@ -100,6 +101,8 @@ async def update_notification_settings(
prefs["quiet_hours_end"] = body.quiet_hours_end
if body.timezone is not None:
prefs["timezone"] = body.timezone
if body.alert_filters is not None:
prefs["alert_filters"] = body.alert_filters
# Allow clearing quiet hours by passing -1
if body.quiet_hours_start == -1:
prefs.pop("quiet_hours_start", None)