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:
@@ -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 <jwt>`
|
||||
| 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 <jwt>`
|
||||
| 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 <jwt>`
|
||||
| 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: <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
|
||||
|
||||
**Welcome Banner:**
|
||||
|
||||
Reference in New Issue
Block a user