diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5f730d3..3f0aef6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -312,7 +312,7 @@ News articles correlated to a specific member of Congress. | email | varchar (unique) | | | hashed_password | varchar | bcrypt | | is_admin | bool | First registered user = true | -| notification_prefs | jsonb | ntfy topic URL, ntfy auth token, ntfy enabled, RSS token | +| notification_prefs | jsonb | ntfy topic URL, ntfy auth method/token/credentials, ntfy enabled, RSS token, quiet_hours_start/end (0–23 local), timezone (IANA name, e.g. `America/New_York`) | | rss_token | varchar (nullable) | Unique token for personal RSS feed URL | | created_at | timestamptz | | @@ -333,6 +333,23 @@ Unique constraint: `(user_id, follow_type, follow_value)` --- +### `bill_notes` +One private note per user per bill. Stored in the app; never shared. + +| Column | Type | Notes | +|---|---|---| +| id | int (PK) | | +| user_id | int (FK → users, CASCADE) | | +| bill_id | varchar (FK → bills, CASCADE) | | +| content | text | User's note text | +| pinned | bool | If true, note floats above `BriefPanel` on the detail page | +| created_at | timestamptz | | +| updated_at | timestamptz | | + +Unique constraint: `(user_id, bill_id)` — one note per user per bill. PUT endpoint upserts (create or update). + +--- + ### `news_articles` | Column | Type | Notes | @@ -400,7 +417,7 @@ Stores notification events for dispatching to user channels (ntfy, RSS). | id | int (PK) | | | user_id | int (FK → users, CASCADE) | | | bill_id | varchar (FK → bills, SET NULL) | nullable | -| event_type | varchar | `new_document`, `new_amendment`, `bill_updated` | +| event_type | varchar | `new_document`, `new_amendment`, `bill_updated`, `weekly_digest` | | payload | jsonb | `{bill_title, bill_label, brief_summary, bill_url, milestone_tier}` | | dispatched_at | timestamptz (nullable) | NULL = pending dispatch | | created_at | timestamptz | | @@ -424,6 +441,9 @@ Stores notification events for dispatching to user channels (ntfy, RSS). | `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 | +| `0012_dedupe_bill_actions_unique.py` | Unique constraint on `(bill_id, action_date, action_text)` for idempotent action ingestion | +| `0013_add_follow_mode.py` | `follow_mode` column on `follows` (`neutral` / `pocket_veto` / `pocket_boost`) | +| `0014_add_bill_notes.py` | `bill_notes` table with unique constraint `(user_id, bill_id)` | Migrations run automatically on API startup: `alembic upgrade head`. @@ -492,14 +512,26 @@ Auth header: `Authorization: Bearer ` | 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/notes` + +| Method | Path | Auth | Description | +|---|---|---|---| +| GET | `/{bill_id}` | Required | Get user's note for a bill. Returns 404 if no note exists. | +| PUT | `/{bill_id}` | Required | Upsert note `{content, pinned}`. Creates or updates (one note per user per bill). | +| DELETE | `/{bill_id}` | Required | Delete the user's note. Returns 404 if none exists. | + ### `/api/notifications` | Method | Path | Auth | Description | |---|---|---|---| -| GET | `/settings` | Required | User's notification preferences (ntfy URL/token, ntfy enabled, RSS token). | +| GET | `/settings` | Required | User's notification preferences (ntfy URL/auth, quiet hours, timezone, RSS token, digest settings). | | 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. | +| POST | `/test/ntfy` | Required | Send a test ntfy push using the provided credentials (not saved). | +| POST | `/test/rss` | Required | Generate a test RSS entry and return event count. | +| POST | `/test/follow-mode` | Required | Simulate a follow-mode notification to preview delivery behavior. | +| GET | `/history` | Required | Recent notification events (dispatched + pending). | ### `/api/admin` @@ -520,6 +552,7 @@ Auth header: `Authorization: Bearer ` | POST | `/backfill-citations` | Admin | Delete pre-citation briefs and re-queue LLM using stored document text. | | POST | `/backfill-labels` | Admin | Classify existing cited brief points as `cited_fact` or `inference` in-place — one compact LLM call per brief, no re-generation. | | POST | `/resume-analysis` | Admin | Re-queue LLM for docs with no brief; re-queue doc fetch for bills with no doc. | +| POST | `/trigger-weekly-digest` | Admin | Manually trigger the weekly digest task for all users now. | | 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. | @@ -562,6 +595,7 @@ Auth header: `Authorization: Bearer ` | 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 | +| Mondays 8:30 AM UTC | `send_weekly_digest` | Weekly | --- @@ -758,6 +792,9 @@ Navigation with: Home, Bills, Members, Following, Topics, Settings (admin only). **`DraftLetterPanel.tsx`** Collapsible panel rendered below `BriefPanel` on the bill detail page (only when a brief exists). Lets users select up to 3 cited points from the brief, choose stance (YES/NO), tone (short/polite/firm), and optionally enter a ZIP code (not stored). Stance auto-populates from the user's follow mode (`pocket_boost` → YES, `pocket_veto` → NO); clears if they unfollow. Recipient (house/senate) is derived from the bill's chamber. Calls `POST /{bill_id}/draft-letter` and renders the plain-text draft in a readonly textarea with a copy-to-clipboard button. +**`NotesPanel.tsx`** +Collapsible private-note panel on the bill detail page (auth-gated; returns null for guests). Header shows "My Note" with last-saved date and a pin icon when a note exists. Expanded: auto-resize textarea, Pin/Unpin toggle, Save button (disabled when clean), and Trash icon (only when note exists). Pinned notes render above `BriefPanel`; unpinned notes render below `DraftLetterPanel`. Uses `retry: false, throwOnError: false` on the TanStack Query so a 404 (no note yet) is treated as empty state rather than an error. Mutations show a "Saved!" flash for 2 seconds. + **`BillCard.tsx`** Compact bill preview showing bill ID, title, sponsor with party badge, latest action date, status, and a text availability indicator: `Brief` (green, analysis done) / `Pending` (amber, text retrieved but not yet analysed) / `No text` (muted, nothing published on Congress.gov). @@ -998,6 +1035,40 @@ Nginx uses `resolver 127.0.0.11 valid=10s` (Docker's internal DNS) so upstream c - Manual Controls split into two sections: always-visible recurring controls (Poll, Members, Trends, Actions, Resume) and a collapsible **Maintenance** section for one-time backfill tasks - Maintenance section header shows "⚠ action needed" when any backfill has a non-zero count +### v0.8.0 — Personal Notes + +**Private Per-Bill Notes:** +- `bill_notes` table (migration 0014) — `(user_id, bill_id)` unique constraint enforces one note per user per bill +- `BillNote` SQLAlchemy model with CASCADE relationships on both `User` and `Bill` +- `PUT /api/notes/{bill_id}` upsert — single endpoint handles create and update; no separate create route +- `GET /api/notes/{bill_id}` returns 404 when no note exists (not an error — treated as empty state by the frontend) +- `DELETE /api/notes/{bill_id}` removes the note; returns 404 if none +- All notes endpoints require authentication +- `NotesPanel.tsx` — collapsible UI; auth-gated (returns null for guests) + - Auto-resize textarea via `scrollHeight` + - Pin/Unpin toggle: pinned notes float above `BriefPanel`; unpinned render below `DraftLetterPanel` + - `isDirty` check prevents saving unchanged content; "Saved!" flash for 2 seconds on success + - `retry: false, throwOnError: false` so 404 = no note, not an error state +- Pin state fetched on page load via a shared `["note", billId]` query key (reused by both the page and `NotesPanel`) + +### v0.7.0 — Weekly Digest & Local-Time Quiet Hours + +**Weekly Digest:** +- `send_weekly_digest` Celery task — runs Mondays at 8:30 AM UTC +- Per user: queries bills updated in the past 7 days among followed items; sends a low-priority ntfy push listing up to 5 bills with brief summaries +- Creates a `NotificationEvent` with `event_type="weekly_digest"` and `dispatched_at=now` to prevent double-dispatch by the regular `dispatch_notifications` task; digest events still appear in the user's RSS feed +- Uses the most recently updated followed bill as `bill_id` anchor (FK is NOT NULL) +- `POST /api/admin/trigger-weekly-digest` endpoint + "Send Weekly Digest" button in Admin Manual Controls +- ntfy push: `Priority: low`, `Tags: newspaper,calendar` + +**Local-Time Quiet Hours:** +- `quiet_hours_start` / `quiet_hours_end` are now interpreted in the user's **local time** rather than UTC +- Browser auto-detects timezone via `Intl.DateTimeFormat().resolvedOptions().timeZone` (IANA name) on the Notifications settings page +- Timezone saved in `notification_prefs` JSONB as `timezone` (no migration needed — JSONB is schema-flexible) +- Backend `_in_quiet_hours()` converts the dispatch UTC timestamp to local time using `zoneinfo.ZoneInfo` (Python 3.9+ stdlib); falls back to UTC if no timezone stored +- `NotificationSettingsResponse` / `NotificationSettingsUpdate` schemas now include `timezone: Optional[str]` +- Notifications UI: quiet hours displayed in 12-hour AM/PM format; shows `"Times are in your local timezone: "` hint; amber warning if saved timezone differs from currently detected one (e.g., after travel) + ### v0.6.1 — Welcome Banner & Dashboard Auth Fix **Welcome Banner:**