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
This commit is contained in:
Jack Levy
2026-03-01 22:33:04 -05:00
parent 197300703e
commit 22b68f9502

View File

@@ -312,7 +312,7 @@ News articles correlated to a specific member of Congress.
| email | varchar (unique) | | | email | varchar (unique) | |
| hashed_password | varchar | bcrypt | | hashed_password | varchar | bcrypt |
| is_admin | bool | First registered user = true | | 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 (023 local), timezone (IANA name, e.g. `America/New_York`) |
| rss_token | varchar (nullable) | Unique token for personal RSS feed URL | | rss_token | varchar (nullable) | Unique token for personal RSS feed URL |
| created_at | timestamptz | | | 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` ### `news_articles`
| Column | Type | Notes | | Column | Type | Notes |
@@ -400,7 +417,7 @@ Stores notification events for dispatching to user channels (ntfy, RSS).
| id | int (PK) | | | id | int (PK) | |
| user_id | int (FK → users, CASCADE) | | | user_id | int (FK → users, CASCADE) | |
| bill_id | varchar (FK → bills, SET NULL) | nullable | | 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}` | | payload | jsonb | `{bill_title, bill_label, brief_summary, bill_url, milestone_tier}` |
| dispatched_at | timestamptz (nullable) | NULL = pending dispatch | | dispatched_at | timestamptz (nullable) | NULL = pending dispatch |
| created_at | timestamptz | | | 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)` | | `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 | | `0010_backfill_bill_congress_urls.py` | Backfill congress_url on existing bill records |
| `0011_add_notifications.py` | `notifications` table + `rss_token` column on users | | `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`. Migrations run automatically on API startup: `alembic upgrade head`.
@@ -492,14 +512,26 @@ Auth header: `Authorization: Bearer <jwt>`
| POST | `/test-llm` | Admin | Test LLM connection with a lightweight ping (max_tokens=20). Returns `{status, provider, model, reply}`. | | 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. | | 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` ### `/api/notifications`
| Method | Path | Auth | Description | | 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. | | PUT | `/settings` | Required | Update notification preferences. |
| POST | `/settings/rss-reset` | Required | Regenerate RSS token (invalidates old URL). | | 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. | | 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` ### `/api/admin`
@@ -520,6 +552,7 @@ Auth header: `Authorization: Bearer <jwt>`
| POST | `/backfill-citations` | Admin | Delete pre-citation briefs and re-queue LLM using stored document text. | | 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 | `/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 | `/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). | | 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. | | GET | `/task-status/{task_id}` | Admin | Celery task status and result. |
@@ -562,6 +595,7 @@ Auth header: `Authorization: Bearer <jwt>`
| Daily 3 AM UTC | `calculate_all_member_trend_scores` | Nightly | | Daily 3 AM UTC | `calculate_all_member_trend_scores` | Nightly |
| Daily 4 AM UTC | `fetch_actions_for_active_bills` | Nightly | | Daily 4 AM UTC | `fetch_actions_for_active_bills` | Nightly |
| Every 5 minutes | `dispatch_notifications` | Continuous | | 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`** **`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. 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`** **`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). 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 - 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 - 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: <IANA name>"` hint; amber warning if saved timezone differs from currently detected one (e.g., after travel)
### v0.6.1 — Welcome Banner & Dashboard Auth Fix ### v0.6.1 — Welcome Banner & Dashboard Auth Fix
**Welcome Banner:** **Welcome Banner:**