Update ARCHITECTURE.md and roadmap to reflect v0.4.0

Architecture doc: add notifications table, v0.4.0 feature history, updated
beat schedule (fetch-actions-active-bills, dispatch-notifications), expanded
admin API table (17 endpoints), new /api/notifications section, BriefPanel
and ActionTimeline component docs, chamberBadgeColor utility, migrations
0010-0011, live LLM model picker endpoint, queue routing corrections.

Roadmap: mark Phase 1 notifications complete, check off LLM model picker,
admin health panel, chamber badges, action history fallback, backfill all
actions, brief regeneration UI; remove completed Phase 1 section.

Authored-By: Jack Levy
This commit is contained in:
Jack Levy
2026-03-01 11:45:21 -05:00
parent f3a8c1218a
commit 91790fd798
2 changed files with 217 additions and 35 deletions

View File

@@ -140,7 +140,7 @@ OPENAI_MODEL=gpt-4o
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-opus-4-6
GEMINI_API_KEY=
GEMINI_MODEL=gemini-1.5-pro
GEMINI_MODEL=gemini-2.0-flash
OLLAMA_BASE_URL=http://host.docker.internal:11434
OLLAMA_MODEL=llama3.1
@@ -247,17 +247,58 @@ Indexes: `bill_id`, `topic_tags` (GIN for JSONB containment queries)
### `members`
Primary key: `bioguide_id` (Congress.gov canonical identifier).
| Column | Type |
|---|---|
| bioguide_id | varchar (PK) |
| name | varchar |
| first_name / last_name | varchar |
| party | varchar |
| state | varchar |
| chamber | varchar |
| district | varchar (nullable, House only) |
| photo_url | varchar (nullable) |
| created_at / updated_at | timestamptz |
| Column | Type | Notes |
|---|---|---|
| bioguide_id | varchar (PK) | |
| name | varchar | Stored as "Last, First" |
| first_name / last_name | varchar | |
| party | varchar | |
| state | varchar | |
| chamber | varchar | |
| district | varchar (nullable) | House only |
| photo_url | varchar (nullable) | |
| official_url | varchar (nullable) | Member's official website |
| congress_url | varchar (nullable) | congress.gov profile link |
| birth_year | varchar(10) (nullable) | |
| address | varchar (nullable) | DC office address |
| phone | varchar(50) (nullable) | DC office phone |
| terms_json | json (nullable) | Array of `{congress, startYear, endYear, chamber, partyName, stateName, district}` |
| leadership_json | json (nullable) | Array of `{type, congress, current}` |
| sponsored_count | int (nullable) | Total bills sponsored (lifetime) |
| cosponsored_count | int (nullable) | Total bills cosponsored (lifetime) |
| detail_fetched | timestamptz (nullable) | Set when bio detail was enriched from Congress.gov |
| created_at / updated_at | timestamptz | |
Member detail fields (`congress_url` through `detail_fetched`) are populated lazily on first profile view via a Congress.gov detail API call. The `detail_fetched` timestamp is the gate for scheduling member interest scoring.
### `member_trend_scores`
One record per member per day (mirrors `trend_scores` for bills).
| Column | Type | Notes |
|---|---|---|
| id | int (PK) | |
| member_id | varchar (FK → members, CASCADE) | bioguide_id |
| score_date | date | |
| newsapi_count | int | Articles from NewsAPI (30-day window) |
| gnews_count | int | Articles from Google News RSS |
| gtrends_score | float | Google Trends interest 0100 |
| composite_score | float | Weighted combination 0100 (same formula as bill trend scores) |
Unique constraint: `(member_id, score_date)`. Indexes: `member_id`, `score_date`, `composite_score`.
### `member_news_articles`
News articles correlated to a specific member of Congress.
| Column | Type | Notes |
|---|---|---|
| id | int (PK) | |
| member_id | varchar (FK → members, CASCADE) | bioguide_id |
| source | varchar | News outlet |
| headline | text | |
| url | varchar | Unique per `(member_id, url)` |
| published_at | timestamptz | |
| relevance_score | float | Default 1.0 |
| created_at | timestamptz | |
---
@@ -269,7 +310,8 @@ Primary key: `bioguide_id` (Congress.gov canonical identifier).
| email | varchar (unique) | |
| hashed_password | varchar | bcrypt |
| is_admin | bool | First registered user = true |
| notification_prefs | jsonb | Future: ntfy, Telegram, RSS config |
| notification_prefs | jsonb | ntfy topic URL, ntfy auth token, ntfy enabled, RSS token |
| rss_token | varchar (nullable) | Unique token for personal RSS feed URL |
| created_at | timestamptz | |
---
@@ -296,7 +338,7 @@ Unique constraint: `(user_id, follow_type, follow_value)`
| bill_id | varchar (FK → bills, CASCADE) | |
| source | varchar | News outlet |
| headline | varchar | |
| url | varchar (unique) | Deduplication key |
| url | varchar | Unique per `(bill_id, url)` — same article can appear across multiple bills |
| published_at | timestamptz | |
| relevance_score | float | Default 1.0 |
| created_at | timestamptz | |
@@ -347,6 +389,22 @@ Key-value store for runtime-configurable settings.
---
### `notifications`
Stores notification events for dispatching to user channels (ntfy, RSS).
| Column | Type | Notes |
|---|---|---|
| id | int (PK) | |
| user_id | int (FK → users, CASCADE) | |
| bill_id | varchar (FK → bills, SET NULL) | nullable |
| event_type | varchar | e.g. `new_brief`, `bill_updated`, `new_action` |
| headline | text | Short description for ntfy title |
| body | text | Longer description for ntfy message / RSS content |
| dispatched_at | timestamptz (nullable) | NULL = not yet sent |
| created_at | timestamptz | |
---
## Alembic Migrations
| File | Description |
@@ -357,6 +415,11 @@ Key-value store for runtime-configurable settings.
| `0004_add_brief_type.py` | BillBrief.brief_type column (`full`/`amendment`) |
| `0005_add_users_and_user_follows.py` | users table + user_id FK on follows; drops global follows |
| `0006_add_brief_govinfo_url.py` | BillBrief.govinfo_url for frontend source links |
| `0007_add_member_bio_fields.py` | Member extended bio: `congress_url`, `birth_year`, `address`, `phone`, `terms_json`, `leadership_json`, `sponsored_count`, `cosponsored_count`, `detail_fetched` |
| `0008_add_member_interest_tables.py` | New tables: `member_trend_scores`, `member_news_articles` |
| `0009_fix_news_articles_url_uniqueness.py` | Changed `news_articles.url` from globally unique to per-bill unique `(bill_id, url)` |
| `0010_backfill_bill_congress_urls.py` | Backfill congress_url on existing bill records |
| `0011_add_notifications.py` | `notifications` table + `rss_token` column on users |
Migrations run automatically on API startup: `alembic upgrade head`.
@@ -390,8 +453,10 @@ Auth header: `Authorization: Bearer <jwt>`
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | `/` | — | Paginated members. Query: `chamber`, `party`, `state`, `q`, `page`, `per_page`. |
| GET | `/{bioguide_id}` | — | Member detail. |
| GET | `/{bioguide_id}` | — | Member detail. On first view, lazily enriches bio from Congress.gov and queues member interest scoring. Returns `latest_trend` if scored. |
| GET | `/{bioguide_id}/bills` | — | Member's sponsored bills, paginated. |
| GET | `/{bioguide_id}/trend` | — | Member trend score history. Query: `days` (7365, default 30). |
| GET | `/{bioguide_id}/news` | — | Member's recent news articles, limit 20. |
### `/api/follows`
@@ -419,7 +484,17 @@ Auth header: `Authorization: Bearer <jwt>`
|---|---|---|---|
| GET | `/` | Required | Current settings (DB overrides env). |
| PUT | `/` | Admin | Update `{key, value}`. Allowed keys: `llm_provider`, `llm_model`, `congress_poll_interval_minutes`. |
| POST | `/test-llm` | Admin | Test LLM connection. Returns `{status, provider, model, summary_preview}`. |
| POST | `/test-llm` | Admin | Test LLM connection with a lightweight ping (max_tokens=20). Returns `{status, provider, model, reply}`. |
| GET | `/llm-models?provider=X` | Admin | Fetch available models from the live provider API. Supports openai, anthropic, gemini, ollama. |
### `/api/notifications`
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | `/settings` | Required | User's notification preferences (ntfy URL/token, ntfy enabled, RSS token). |
| PUT | `/settings` | Required | Update notification preferences. |
| POST | `/settings/rss-reset` | Required | Regenerate RSS token (invalidates old URL). |
| GET | `/feed/{rss_token}.xml` | — | Personal RSS feed of notification events for this user. |
### `/api/admin`
@@ -428,11 +503,18 @@ Auth header: `Authorization: Bearer <jwt>`
| GET | `/users` | Admin | All users with follow counts. |
| DELETE | `/users/{id}` | Admin | Delete user (cannot delete self). Cascades follows. |
| PATCH | `/users/{id}/toggle-admin` | Admin | Promote/demote admin status (cannot change self). |
| GET | `/stats` | Admin | Pipeline progress: total bills, docs fetched, briefs generated, remaining. |
| GET | `/stats` | Admin | Pipeline counters: total bills, docs fetched, briefs generated, pending LLM, missing metadata/sponsors/actions, uncited briefs. |
| GET | `/api-health` | Admin | Test each external API in parallel; returns status + latency for Congress.gov, GovInfo, NewsAPI, Google News. |
| POST | `/trigger-poll` | Admin | Queue immediate Congress.gov poll. |
| POST | `/trigger-member-sync` | Admin | Queue member sync. |
| POST | `/trigger-trend-scores` | Admin | Queue trend score calculation. |
| POST | `/backfill-sponsors` | Admin | Queue one-off task to populate `sponsor_id` on all bills where it is NULL. |
| POST | `/trigger-fetch-actions` | Admin | Queue action fetch for recently active bills (last 30 days). |
| POST | `/backfill-all-actions` | Admin | Queue action fetch for ALL bills with no action history (one-time catch-up). |
| POST | `/backfill-sponsors` | Admin | Queue one-off task to populate `sponsor_id` on bills where it is NULL. |
| POST | `/backfill-metadata` | Admin | Fill null `introduced_date`, `chamber`, `congress_url` by re-fetching bill detail. |
| POST | `/backfill-citations` | Admin | Delete pre-citation briefs and re-queue LLM using stored document text. |
| POST | `/resume-analysis` | Admin | Re-queue LLM for docs with no brief; re-queue doc fetch for bills with no doc. |
| POST | `/bills/{bill_id}/reprocess` | Admin | Queue document + action fetches for a specific bill (debugging). |
| GET | `/task-status/{task_id}` | Admin | Celery task status and result. |
### `/api/health`
@@ -453,10 +535,10 @@ Auth header: `Authorization: Bearer <jwt>`
| Queue | Workers | Tasks |
|---|---|---|
| `polling` | worker | `poll_congress_bills`, `sync_members` |
| `polling` | worker | `app.workers.congress_poller.*`, `app.workers.notification_dispatcher.*` |
| `documents` | worker | `fetch_bill_documents` |
| `llm` | worker | `process_document_with_llm` |
| `news` | worker | `fetch_news_for_bill`, `fetch_news_for_active_bills`, `calculate_all_trend_scores` |
| `news` | worker | `app.workers.news_fetcher.*`, `app.workers.trend_scorer.*`, `app.workers.member_interest.*` |
**Worker settings:**
- `task_acks_late = True` — task removed from queue only after completion, not on pickup
@@ -470,6 +552,10 @@ Auth header: `Authorization: Bearer <jwt>`
| Configurable (default 30 min) | `poll_congress_bills` | Continuous |
| Every 6 hours | `fetch_news_for_active_bills` | Ongoing |
| Daily 2 AM UTC | `calculate_all_trend_scores` | Nightly |
| Every 12 hours (at :30) | `fetch_news_for_active_members` | Ongoing |
| Daily 3 AM UTC | `calculate_all_member_trend_scores` | Nightly |
| Daily 4 AM UTC | `fetch_actions_for_active_bills` | Nightly |
| Every 5 minutes | `dispatch_notifications` | Continuous |
---
@@ -503,8 +589,8 @@ Auth header: `Authorization: Bearer <jwt>`
↳ → fetch_news_for_bill.delay(bill_id)
4. news_fetcher.fetch_news_for_bill(bill_id)
↳ Queries NewsAPI using bill title + topic_tags
↳ Deduplicates by URL
↳ Queries NewsAPI + Google News RSS using bill title/number
↳ Deduplicates by (bill_id, url) — same article can appear for multiple bills
↳ Stores NewsArticle records
5. trend_scorer.calculate_all_trend_scores() [nightly]
@@ -513,6 +599,21 @@ Auth header: `Authorization: Bearer <jwt>`
↳ Fetches: NewsAPI count + Google News RSS count + Google Trends score
↳ Calculates composite_score (0100)
↳ Stores TrendScore record
Member interest pipeline (independent of bill pipeline):
6. member_interest.fetch_member_news(bioguide_id) [on first profile view + every 12h]
↳ Triggered on first member profile view (non-blocking via .delay())
↳ Queries NewsAPI + Google News RSS using member name + title
↳ Deduplicates by (member_id, url)
↳ Stores MemberNewsArticle records
7. member_interest.calculate_member_trend_score(bioguide_id) [on first profile view + nightly]
↳ Triggered on first member profile view (non-blocking via .delay())
↳ Only runs if member detail has been fetched (gate: detail_fetched IS NOT NULL)
↳ Fetches: NewsAPI count + Google News RSS count + Google Trends score
↳ Uses the same composite formula as bills
↳ Stores MemberTrendScore record
```
---
@@ -597,7 +698,7 @@ class ReverseBrief:
| `/bills` | Browse all bills with search, chamber/topic filters, pagination |
| `/bills/[id]` | Bill detail — brief with § citations, action timeline, news, trend chart |
| `/members` | Browse members of Congress, filter by chamber/party/state |
| `/members/[id]` | Member profile + sponsored bills |
| `/members/[id]` | Member profile — bio, contact info, leadership roles, service history, sponsored bills, public interest trend chart, recent news |
| `/following` | User's followed bills, members, and topics |
| `/topics` | Browse and follow policy topics |
| `/settings` | Admin panel (admin only) |
@@ -606,6 +707,9 @@ class ReverseBrief:
### Key Components
**`BriefPanel.tsx`**
Orchestrates AI brief display. If the latest brief is type `amendment`, shows an amber "What Changed" badge. Renders the latest brief via `AIBriefCard`. Below it, a collapsible "Version History" lists all older briefs; clicking one expands an inline `AIBriefCard`.
**`AIBriefCard.tsx`**
Renders the LLM brief. For cited items (new format), shows a `§ Section X(y)` chip next to each bullet. Clicking the chip expands an inline panel with:
- Blockquoted verbatim excerpt from the bill
@@ -613,11 +717,17 @@ Renders the LLM brief. For cited items (new format), shows a `§ Section X(y)` c
- One chip open at a time per card
- Old plain-string briefs render without chips (graceful backward compat)
**`ActionTimeline.tsx`**
Renders the legislative action history as a vertical timeline. Accepts optional `latestActionDate`/`latestActionText` fallback props — when `actions` is empty but a latest action exists (actions not yet fetched from Congress.gov), shows a single "latest known action" entry with a note that full history loads in the background.
**`MobileHeader.tsx`**
Top bar shown only on mobile (`md:hidden`). Displays the PocketVeto logo and a hamburger button that opens the slide-in drawer.
**`AuthGuard.tsx`**
Client component wrapping the entire app. Waits for Zustand hydration, then redirects unauthenticated users to `/login`. Public paths (`/login`, `/register`) bypass the guard.
Client component wrapping the entire app. Waits for Zustand hydration, then redirects unauthenticated users to `/login`. Public paths (`/login`, `/register`) bypass the guard. Implements the responsive shell: desktop sidebar always-visible (`hidden md:flex`), mobile drawer with backdrop overlay controlled by `drawerOpen` state.
**`Sidebar.tsx`**
Navigation with: Home, Bills, Members, Following, Topics, Settings (admin only). Shows current user email + logout button at the bottom.
Navigation with: Home, Bills, Members, Following, Topics, Settings (admin only). Shows current user email + logout button at the bottom. Accepts optional `onClose` prop — when provided (mobile drawer context), renders an X close button in the header and calls `onClose` on every nav link click.
**`BillCard.tsx`**
Compact bill preview showing bill ID, title, sponsor with party badge, latest action date, and status.
@@ -633,6 +743,10 @@ partyBadgeColor(party) → Tailwind classes
"Democrat" "bg-blue-600 text-white"
other "bg-slate-500 text-white"
chamberBadgeColor(chamber) Tailwind badge classes
"Senate" amber/gold (bg-amber-100 text-amber-700 )
"House" slate/silver (bg-slate-100 text-slate-600 )
partyColor(party) text color class (used inline)
trendColor(score) color class based on score thresholds
billLabel(type, number) "H.R. 1234", "S. 567", etc.
@@ -730,6 +844,77 @@ Nginx uses `resolver 127.0.0.11 valid=10s` (Docker's internal DNS) so upstream c
- AuthGuard with login/register pages
- Analysis status dashboard (auto-refresh every 30s)
### v0.3.0 — Member Profiles & Mobile UI
**Member Interest Tracking:**
- `member_trend_scores` and `member_news_articles` tables (migration 0008)
- `member_interest` Celery worker: `fetch_member_news`, `calculate_member_trend_score`, `fetch_news_for_active_members`, `calculate_all_member_trend_scores`
- Member interest scoring uses the identical composite formula as bills (NewsAPI + GNews + pytrends)
- New beat schedules: member news every 12h, member trend scores nightly at 3 AM UTC
- Lazy enrichment: on first profile view, bio is fetched from Congress.gov detail API and interest scoring is queued non-blocking
- Member detail fields added: `congress_url`, `birth_year`, `address`, `phone`, `terms_json`, `leadership_json`, `sponsored_count`, `cosponsored_count`, `detail_fetched` (migration 0007)
- New API endpoints: `GET /api/members/{id}/trend` and `GET /api/members/{id}/news`
- Member detail page redesigned: photo, bio header with party/state/district/birth year, contact info (address, phone, website, congress.gov), current leadership badges, trend chart ("Public Interest"), news panel, legislation stats (sponsored/cosponsored counts), full service history timeline, all leadership roles history
**News Deduplication Fix:**
- `news_articles.url` changed from globally unique to per-bill unique `(bill_id, url)` (migration 0009)
- The same article can now appear in multiple bills' news panels
- `fetch_news_for_bill` now fetches from both NewsAPI and Google News RSS (previously GNews was volume-signal only)
**Mobile UI:**
- `MobileHeader.tsx` — hamburger + logo top bar, hidden on desktop (`md:hidden`)
- `AuthGuard.tsx` — responsive shell: desktop sidebar always-on, mobile slide-in drawer with backdrop
- `Sidebar.tsx``onClose` prop for drawer mode (X button + close on nav click)
- Dashboard grid: `grid-cols-1 md:grid-cols-3` (single column on mobile)
- Members page: `grid-cols-1 sm:grid-cols-2` (single column on mobile, two on tablet+)
- Topics page: `grid-cols-1 sm:grid-cols-2`
### v0.4.0 — Notifications, Admin Health Panel, Bill Action Pipeline
**Notifications (Phase 1 complete):**
- `notifications` table — stores events per user (new_brief, bill_updated, new_action)
- ntfy dispatch — Celery task POSTs to user's ntfy topic URL (self-hosted or ntfy.sh); optional auth token
- RSS feed — tokenized per-user XML feed at `/api/notifications/feed/{token}.xml`
- `dispatch_notifications` beat task — runs every 5 minutes, fans out unsent events to enabled channels
- Notification settings UI — ntfy topic URL, auth token, enable/disable, RSS URL with copy button
**Bill Action Pipeline:**
- `fetch_bill_actions` Celery task — fetches full legislative history from Congress.gov, idempotent on `(bill_id, action_date, action_text)`, updates `Bill.actions_fetched_at`
- `fetch_actions_for_active_bills` nightly batch — queues action fetches for bills active in last 30 days
- `backfill_all_bill_actions` — one-time task to fetch actions for all bills with `actions_fetched_at IS NULL`
- Beat schedule entry at 4 AM UTC
- `ActionTimeline` updated: shows full history when fetched; falls back to `latest_action_date`/`latest_action_text` with "latest known action" label when history not yet loaded
**"What Changed" — BriefPanel:**
- New `BriefPanel.tsx` component wrapping `AIBriefCard`
- When latest brief is type `amendment`: shows amber "What Changed" badge row + date
- Collapsible "Version History" section listing older briefs (date, type badge, truncated summary)
- Clicking a history row expands an inline `AIBriefCard` for that version
**LLM Provider Improvements:**
- Live model picker — `GET /api/settings/llm-models?provider=X` fetches available models from each provider's API (OpenAI SDK, Anthropic REST, Gemini SDK, Ollama tags endpoint)
- DB overrides now fully propagated: `get_llm_provider(provider, model)` accepts explicit params; all call sites read from `app_settings`
- Default Gemini model updated: `gemini-1.5-pro` (deprecated) → `gemini-2.0-flash`
- Test connection replaced with lightweight ping (max_tokens=20, 3-word prompt) instead of full brief generation
**Admin Panel Overhaul:**
- Bill Pipeline section: progress bar + breakdown table (total, text published, no text yet, AI briefs, pending LLM, uncited)
- External API Health: Run Tests button, parallel health checks for Congress.gov / GovInfo / NewsAPI / Google News RSS with latency display
- Manual Controls redesigned as health panel: each action has a status dot (green/red/gray), description, contextual count badge (e.g. "⚠ 12 bills missing metadata"), and Run button
- Task status polling: after triggering a task, button shows spinning icon; polls `/api/admin/task-status/{id}` every 5s; shows task ID prefix + completion/failure state
- New stat fields: `bills_missing_sponsor`, `bills_missing_metadata`, `bills_missing_actions`, `pending_llm`, `no_text_bills`
- New admin tasks: Backfill Dates & Links, Backfill All Action Histories, Resume Analysis
**Chamber Color Badges:**
- `chamberBadgeColor(chamber)` utility: amber/gold for Senate, slate/silver for House
- Applied everywhere chamber is displayed: BillCard, bill detail header
**Bill Detail Page:**
- "No bill text published" state — shown when `has_document=false` and no briefs; includes bill label, date, and congress.gov link
- `has_document` field added to `BillDetailSchema` and `BillDetail` TypeScript type
- `introduced_date` shown conditionally (not rendered when null, preventing "Introduced: —")
- Admin reprocess endpoint: `POST /api/admin/bills/{bill_id}/reprocess`
### v0.2.2 — Sponsor Linking & Search Fixes
- **Root cause fixed:** Congress.gov list API does not return sponsor data — only the detail endpoint does. Poller now calls the detail endpoint for each new bill to get the sponsor and populate `bill.sponsor_id`
- **Backfill task:** `backfill_sponsor_ids` Celery task + `/api/admin/backfill-sponsors` endpoint + "Backfill Sponsors" button in Admin UI — fixes existing bills with `NULL` sponsor_id (~10 req/sec, ~3 min for 1,600 bills)

View File

@@ -24,6 +24,13 @@
- [x] What Changed (Amendment Briefs) — BriefPanel surfaces amendment briefs with "What Changed" badge and collapsible version history
- [x] Source Viewer — "View source" link in § citation popover opens GovInfo document in new tab (Option A; Option B = in-app highlighted viewer deferred pending UX review)
- [x] Admin Reprocess — POST /api/admin/bills/{bill_id}/reprocess queues document + action fetches for a specific bill
- [x] LLM Model Picker — live model list fetched from each provider's API; custom model name fallback
- [x] Admin Health Panel — bill pipeline breakdown table, external API health tests with latency, manual controls with status dots + task polling
- [x] Chamber Badges — amber/gold for Senate, slate/silver for House; applied on bill cards and detail pages
- [x] Action History Fallback — shows latest_action_date/text while full history loads; full timeline once fetched
- [x] Backfill All Actions — admin task to fetch action history for all pre-existing bills
- [x] Notifications (Phase 1) — ntfy dispatch, RSS feed, per-user settings UI, 5-min dispatcher beat task
- [x] Brief Regeneration UI — admin button to delete existing briefs for a bill and re-queue LLM processing. Useful for improving citation/diff logic without a full re-poll. (Backend reprocess endpoint already exists.)
---
@@ -31,15 +38,6 @@
---
### Phase 1 — Notifications Plumbing *(prerequisite for Alerts and Weekly Digest)*
- [ ] `notification_events` table — `(user_id, bill_id, event_type, payload, dispatched_at)`
- [ ] ntfy dispatch — Celery task POSTs to user's ntfy topic URL; user supplies their own topic URL (public ntfy.sh or self-hosted ntfy server with optional auth token)
- [ ] RSS feed — tokenized per-user feed at `/api/feed/{token}.xml`; token stored on user row
- [ ] User settings UI — ntfy topic URL field + optional ntfy auth token + RSS feed link/copy button
---
### Phase 2 — High Impact *(can run in parallel after Phase 1)*
- [ ] **Change-driven Alerts** — emit `notification_event` from poller/document fetcher on material changes: new doc version, substitute text, committee report, vote scheduled/result. Filter out procedural-only action text. Fan out to ntfy + RSS.
@@ -68,7 +66,6 @@
- [ ] **Search Improvements** — filters on global search (bill type, status, chamber, date range); search within a member's sponsored bills; topic-scoped search.
- [ ] **Desktop View** — wider multi-column layout optimized for large screens (sticky sidebar, expanded grid, richer bill detail layout).
- [ ] **Brief Regeneration UI** — admin button to delete existing briefs for a bill and re-queue LLM processing. Useful for improving citation/diff logic without a full re-poll. (Backend reprocess endpoint already exists.)
- [ ] **first_name / last_name Backfill** — Celery task to populate empty first/last from stored "Last, First" `name` field via split.
---