diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bcfddc0..961b5f3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 0–100 | +| composite_score | float | Weighted combination 0–100 (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 ` | 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` (7–365, default 30). | +| GET | `/{bioguide_id}/news` | — | Member's recent news articles, limit 20. | ### `/api/follows` @@ -419,7 +484,17 @@ Auth header: `Authorization: Bearer ` |---|---|---|---| | 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 ` | 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 ` | 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 ` | 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 ` ↳ → 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 ` ↳ Fetches: NewsAPI count + Google News RSS count + Google Trends score ↳ Calculates composite_score (0–100) ↳ 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) diff --git a/PocketVeto — Feature Roadmap.md b/PocketVeto — Feature Roadmap.md index 155f009..faf07ed 100644 --- a/PocketVeto — Feature Roadmap.md +++ b/PocketVeto — Feature Roadmap.md @@ -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. ---