Commit Graph

71 Commits

Author SHA1 Message Date
Jack Levy
1a8061539f fix: member sync — robust chamber parsing and district int conversion
- 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
2026-03-15 11:09:55 -04:00
Jack Levy
db0d841ef0 fix: API health check — correct TIGERweb CD field detection in admin test
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
2026-03-15 11:01:24 -04:00
Jack Levy
bb10ff6dac fix: ZIP rep lookup — correct TIGERweb field names for 119th Congress
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
2026-03-15 10:58:50 -04:00
Jack Levy
4cada298ab fix: ZIP rep lookup — handle GEOID20 for 119th Congress TIGERweb layers
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
2026-03-15 10:29:09 -04:00
Jack Levy
3d19cd571a chore: bind-mount postgres/redis data dirs, update docs for pocketveto.org
- 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
2026-03-15 02:17:37 -04:00
Jack Levy
d378f35cc5 chore: add README, LICENSE (GPL 3.0), and update .env.example
- 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
2026-03-15 01:34:44 -04:00
Jack Levy
a96bd024e9 docs: v1.0.0 — full documentation update
- 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
2026-03-15 01:10:52 -04:00
Jack Levy
9633b4dcb8 feat: v1.0.0 — UX polish, security hardening, code quality
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
2026-03-15 01:10:31 -04:00
Jack Levy
4308404cca fix: use SMTP_SSL for port 465, STARTTLS for 587
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
2026-03-14 23:18:32 -04:00
Jack Levy
f6770b16be fix: proactively fetch votes for stanced bills + register vote_fetcher with Celery
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
2026-03-14 19:38:06 -04:00
Jack Levy
5e52cf5903 fix: backfill_brief_labels bulk SQL runs before ORM load to prevent session flush race
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
2026-03-14 19:28:33 -04:00
Jack Levy
41f6f96077 fix: trending section blank when scores are stale + trend scorer error isolation
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
2026-03-14 19:04:22 -04:00
Jack Levy
380ff4addb feat: email unsubscribe tokens with one-click opt-out
- 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>
2026-03-14 18:56:59 -04:00
Jack Levy
49bda16ad5 feat: email notifications with tabbed channel UI (v0.9.10)
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>
2026-03-14 18:46:26 -04:00
Jack Levy
8625c850a0 fix: handle within-page cosponsor duplicates from Congress.gov API
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>
2026-03-14 18:33:12 -04:00
Jack Levy
d0da0b8dce feat: Member Effectiveness Score + Representation Alignment View (v0.9.9)
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>
2026-03-14 18:05:09 -04:00
Jack Levy
cba19c7bb3 feat: LLM Batch API — OpenAI + Anthropic 50% cost reduction (v0.9.8)
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>
2026-03-14 17:35:15 -04:00
Jack Levy
7e5c5b473e feat: API optimizations — quota batching, ETags, caching, async sponsor (v0.9.7)
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>
2026-03-14 16:50:51 -04:00
Jack Levy
247a874c8d feat: Discovery alert filters + notification reasons (v0.9.6)
- 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
2026-03-14 13:21:48 -04:00
Jack Levy
91473e6464 feat: roll-call votes + granular alert filter fix (v0.9.5)
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
2026-03-02 20:33:32 -05:00
Jack Levy
676bf1b78d docs: overhaul documentation + add deployment guides
- ARCHITECTURE.md: update for v0.9.0/v0.9.3 — collections schema,
  collection_bills schema, alert_filters in notification_prefs,
  action_category in notification payload, migrations 0015/0016,
  /api/collections + /api/share endpoints, updated pages table,
  pipeline flow reflects categorize_action(), v0.9.0 and v0.9.3
  feature history entries
- ROADMAP.md: new file merging "MVP threshold" and "Feature Roadmap"
  docs into one clean shipped/upcoming/backlog structure with v1.0
  definition; removes stale design notes and duplicate entries
- DEPLOYING.md: new — prerequisites, .env setup, first run, admin
  account, domain/SSL with Caddy, useful commands
- UPDATING.md: new — SSH setup, manual deploy, deploy script, Gitea
  webhook + webhook listener, rollback procedure, env-only updates
- Delete: "MVP threshold this make v1 complete.md" and
  "PocketVeto — Feature Roadmap.md" (superseded by ROADMAP.md)
- how-it-works/page.tsx: accurate per-mode default alert sets,
  Alert Filters callout linking to Notifications settings
- notifications/page.tsx: Follow mode default includes amendment filed;
  Pocket Veto default excludes calendar placement

Authored-By: Jack Levy
2026-03-02 19:22:02 -05:00
Jack Levy
a39ae4ccba 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
2026-03-02 19:05:24 -05:00
Jack Levy
af821dad78 fix: show dispatch indicator (✓) for topic-follow events too
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
2026-03-02 16:09:13 -05:00
Jack Levy
d8c1c99b9c fix: send referral notifications to direct bill/member followers
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
2026-03-02 16:00:03 -05:00
Jack Levy
48771287d3 feat: ZIP → rep lookup, member page redesign, letter improvements
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
2026-03-02 15:47:46 -05:00
Jack Levy
5bb0c2b8ec feat: accordion sections, search, and filters on following page
- 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
2026-03-02 12:48:49 -05:00
Jack Levy
73b1480028 fix: seed bills page filters from URL search params
Topic links (and chamber/search links) now correctly pre-filter
the bills list when navigating to /bills?topic=healthcare etc.

Authored-By: Jack Levy
2026-03-02 12:41:51 -05:00
Jack Levy
1485d72065 docs: explain JWT_SECRET_KEY in .env.example
Authored-By: Jack Levy
2026-03-02 11:26:35 -05:00
Jack Levy
483c068ab5 chore: add production deploy script
Authored-By: Jack Levy
2026-03-02 00:07:20 -05:00
Jack Levy
76ad1a06cc chore: annotate prod compose with dev flags for reference
Authored-By: Jack Levy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 00:05:12 -05:00
Jack Levy
22743935c4 chore: add docker-compose.prod.yml production override
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>
2026-03-02 00:04:15 -05:00
Jack Levy
10aced78b0 fix: remove Poll Now button from dashboard
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>
2026-03-01 23:57:35 -05:00
Jack Levy
8a51c41766 Merge feat/phase3-complete: collections, watchlists & shareable links (v0.9.0)
Authored-By: Jack Levy
2026-03-01 23:25:06 -05:00
Jack Levy
9e5ac9b33d feat: collections, watchlists, and shareable links (v0.9.0)
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>
2026-03-01 23:23:45 -05:00
Jack Levy
22b68f9502 docs: update ARCHITECTURE.md for v0.7.0 and v0.8.0
- 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
2026-03-01 22:33:04 -05:00
Jack Levy
197300703e Merge feat/personal-notes: private bill notes
Co-Authored-By: Jack Levy
2026-03-01 22:15:09 -05:00
Jack Levy
62a217cb22 feat: personal notes on bill detail pages
- 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
2026-03-01 22:14:52 -05:00
Jack Levy
128c8e9257 Merge feat/weekly-digest: weekly digest + local-time quiet hours
Co-Authored-By: Jack Levy
2026-03-01 22:05:17 -05:00
Jack Levy
0de8c83987 feat: weekly digest + local-time quiet hours
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
2026-03-01 22:04:54 -05:00
Jack Levy
a0e7ab4cd3 feat(ux): welcome banner, dashboard auth fix, docs update
- WelcomeBanner.tsx: guest-only dismissible onboarding card on dashboard
  (localStorage pv_seen_welcome, Browse Bills CTA, X dismiss)
- useDashboard: add !!token to query key so login/logout triggers
  a fresh fetch without manual refresh
- ARCHITECTURE.md: WelcomeBanner component, auth-aware query keys,
  v0.6.1 feature history entry
- Roadmap: mark welcome banner items complete
- Add MVP planning notes (Phase 3-6 roadmap draft)

Co-Authored-By: Jack Levy
2026-03-01 21:22:16 -05:00
Jack Levy
1e37c99599 feat(phase2): fact/inference labeling, change-driven alerts, admin cleanup
- Add label: cited_fact | inference to LLM brief schema (all 4 providers)
- Inferred badge in AIBriefCard for inference-labeled points
- backfill_brief_labels Celery task: classifies existing cited points in-place
- POST /api/admin/backfill-labels + unlabeled_briefs stat counter
- Expand milestone keywords: markup, conference
- Add is_referral_action() for committee referrals (referred to)
- Two-tier milestone notifications: progress tier (all follow modes) and
  referral tier (pocket_veto/boost only, neutral suppressed)
- Topic followers now receive bill_updated milestone notifications via
  latest brief topic_tags lookup in _update_bill_if_changed()
- Admin Manual Controls: collapsible Maintenance section for backfill tasks
- Update ARCHITECTURE.md and roadmap for Phase 2 completion

Co-Authored-By: Jack Levy
2026-03-01 17:34:45 -05:00
Jack Levy
dc5e756749 feat(email_gen): draft constituent letter generator + bill text indicators
- 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
2026-03-01 16:37:52 -05:00
Jack Levy
7106c9a63c Merge branch 'public_page'
Authored-By: Jack Levy
2026-03-01 15:55:02 -05:00
Jack Levy
ddd74a02d5 feat(public_page): allow unauthenticated browsing with auth-gated interactivity
- 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
2026-03-01 15:54:54 -05:00
Jack Levy
73881b2404 feat(notifications): follow modes, milestone alerts, notification enhancements
Follow Modes (neutral / pocket_veto / pocket_boost):
- Alembic migration 0013 adds follow_mode column to follows table
- FollowButton rewritten as mode-aware dropdown for bills; simple toggle for members/topics
- PATCH /api/follows/{id}/mode endpoint with validation
- Dispatcher filters pocket_veto follows (suppress new_document/new_amendment events)
- Dispatcher adds ntfy Actions header for pocket_boost follows

Change-driven (milestone) Alerts:
- New notification_utils.py with shared emit helpers and 30-min dedup
- congress_poller emits bill_updated events on milestone action text
- llm_processor replaced with shared emit util (also notifies member/topic followers)

Notification Enhancements:
- ntfy priority levels (high for bill_updated, default for others)
- Quiet hours (UTC): dispatcher holds events outside allowed window
- Digest mode (daily/weekly): send_notification_digest Celery beat task
- Notification history endpoint + Recent Alerts UI section
- Enriched following page (bill titles, member photos/details via sub-components)
- Follow mode test buttons in admin settings panel

Infrastructure:
- nginx: switch upstream blocks to set $variable proxy_pass so Docker DNS
  re-resolves upstream IPs after container rebuilds (valid=10s)
- TROUBLESHOOTING.md documenting common Docker/nginx/postgres gotchas

Authored-By: Jack Levy
2026-03-01 15:09:13 -05:00
Jack Levy
22b205ff39 feat(notifications): add Click link to all ntfy alerts (test + real)
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
2026-03-01 12:19:05 -05:00
Jack Levy
ea52381199 fix(notifications): replace em dash in ntfy Title header (ASCII-only), improve error surfacing
HTTP headers are ASCII-only; the em dash in "PocketVeto — Test Notification"
caused a UnicodeEncodeError on every test attempt. Replaced with colon.

Frontend catch blocks now extract the real server error detail from the
axios response body instead of showing a generic fallback message.

Authored-By: Jack Levy
2026-03-01 12:14:37 -05:00
Jack Levy
50399adf44 feat(notifications): add Test button for ntfy and RSS with inline result
- POST /api/notifications/test/ntfy — sends a real push using current form
  values (not saved settings) so auth can be verified before saving; returns
  status + HTTP detail on success or error message on failure
- POST /api/notifications/test/rss — confirms the feed token exists and
  returns event count; no bill FK required
- NtfyTestRequest + NotificationTestResult schemas added
- Frontend: Test button next to Save on both ntfy and RSS sections; result
  shown inline as a green/red pill; uses current form state for ntfy so
  the user can test before committing

All future notification types should follow the same test-before-save pattern.

Authored-By: Jack Levy
2026-03-01 12:10:10 -05:00
Jack Levy
2e2fefb795 feat: per-user notifications (ntfy + RSS), deduplicated actions, backfill task
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
2026-03-01 12:04:13 -05:00
Jack Levy
91790fd798 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
2026-03-01 11:45:21 -05:00