File size: 5,516 Bytes
4aa353b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# HTTP API

All backend routes live under `/api/` and are served by FastAPI. In
production, the browser hits them same-origin via a Next.js rewrite; in
dev they're at `http://localhost:8000/api/...`. Full auto-generated docs
are always at `http://localhost:8000/docs` (Swagger UI).

## Public endpoints (no auth)

### Health / readiness

| Method | Path | Response |
|---|---|---|
| GET | `/api/health` | `{status, data_dir, data_dir_exists}` β€” always 200 if process is up |
| GET | `/api/ready` | `{ready, checks, bills_count}` β€” 200 if data is loaded, 503 otherwise |

### Bills

| Method | Path | Notes |
|---|---|---|
| GET | `/api/bills` | List bills. Query: `state`, `status`, `session_year`, `category`, `search`, `limit`, `offset` |
| GET | `/api/bills/stats` | Totals: `{total_bills, federal_bills, state_bills, most_active_state, last_updated_at}` |
| GET | `/api/bills/filters` | Dropdown data: `{states, statuses, years, categories}` |
| GET | `/api/bills/{bill_id}` | Full bill detail (used by the row-detail modal) |
| GET | `/api/bills/by-state` | `{STATE: count}` for the choropleth. Optional `?year=YYYY` |
| GET | `/api/bills/export.csv` | Filtered CSV download (same filters as `/api/bills`) |

### Insights

| Method | Path |
|---|---|
| GET | `/api/insights/by-state?limit=N` |
| GET | `/api/insights/by-category?limit=N` |
| GET | `/api/insights/over-time` |

### Calendar

| Method | Path | Notes |
|---|---|---|
| GET | `/api/calendar` | Month view. Query: `year`, `month`, `types[]` |
| GET | `/api/calendar/upcoming` | Next N days. Query: `days` (default 7, max 30) |

### Newsletter

| Method | Path | Notes |
|---|---|---|
| GET | `/api/newsletters` | List available dates |
| GET | `/api/newsletters/{date}` | Markdown of one newsletter |
| GET | `/api/newsletters/{date}/changes` | Weekly changes CSV rows for that date |
| GET | `/api/newsletters/{date}/digest` | Stats + in-motion bills for the hero + Bills in Motion sections |
| GET | `/api/newsletters/{date}/featured-bill` | "Bill of the Week" β€” scored pick from the week's changes |
| GET | `/api/newsletters/{date}/category-trends` | Top IAPP categories affected this week |

### States

| Method | Path |
|---|---|
| GET | `/api/states/featured` |
| GET | `/api/states/{state}/spotlight?key_bills=N` |

### Feedback / subscribe

| Method | Path | Body |
|---|---|---|
| POST | `/api/feedback` | `{name?, email?, type, description}` |
| POST | `/api/newsletter/subscribe` | `{email}` |
| POST | `/api/newsletter/unsubscribe` | `{email}` |

### AI tools

| Method | Path | Body |
|---|---|---|
| GET | `/api/ai/bills/list` | Bills available in the AI tools dropdown |
| GET | `/api/ai/bill/{state}/{bill_number}` | Bill + summary + suggested questions + has_report flag |
| GET | `/api/ai/report/{state}/{bill_number}` | Pre-generated report markdown |
| POST | `/api/ai/qa` | `{state, bill_number, question}` |
| POST | `/api/ai/compare` | `{focus_state, focus_bill_number, compare[], question}` |
| POST | `/api/ai/eu-compare` | `{state, bill_number, question}` |

## Auth

| Method | Path | Notes |
|---|---|---|
| POST | `/api/auth/login` | `{username, password}` β†’ `{token, username, name}` |
| GET | `/api/auth/me` | Current user; requires `Authorization: Bearer <token>` |
| POST | `/api/auth/logout` | Client discards the token |

## Admin endpoints

All under `/api/admin/*`. Require `Authorization: Bearer <jwt>` where the
JWT was issued by `/api/auth/login`. Enforced by the `require_admin`
dependency in every admin route.

### Overview

| Method | Path | Notes |
|---|---|---|
| GET | `/api/admin/overview` | Card counts, including `new_feedback_count` (fed to the nav badge) |

### Users

| Method | Path | Body |
|---|---|---|
| GET | `/api/admin/users` | List |
| POST | `/api/admin/users` | `{username, name, email, password}` |
| PATCH | `/api/admin/users/{username}` | `{name?, email?, password?}` (any subset) |
| DELETE | `/api/admin/users/{username}` | |

### Subscribers

| Method | Path |
|---|---|
| GET | `/api/admin/subscribers` |
| DELETE | `/api/admin/subscribers/{email}` |

### Feedback

| Method | Path | Body |
|---|---|---|
| GET | `/api/admin/feedback` | |
| PATCH | `/api/admin/feedback/{id}` | `{status}` β€” one of `new`, `read`, `resolved` |
| DELETE | `/api/admin/feedback/{id}` | |

### Pipeline

| Method | Path | Body / Notes |
|---|---|---|
| GET | `/api/admin/pipeline/status` | Current or last run |
| GET | `/api/admin/pipeline/history?limit=N` | Last N runs |
| GET | `/api/admin/pipeline/scripts` | The 14 step list with names + indices |
| GET | `/api/admin/pipeline/log?tail=N` | Last N lines of the pipeline log |
| POST | `/api/admin/pipeline/run` | `{pull?, test?, test_count?, start_from?, stop_after?, reset_vectorstore?, upload_to_hf?}` (all optional) |
| POST | `/api/admin/pipeline/cancel` | Kills the running subprocess group |
| POST | `/api/admin/pipeline/rebuild-manifest` | Standalone `rebuild_vectorstore_manifest.py` run |

### Weekly changes

| Method | Path |
|---|---|
| GET | `/api/admin/weekly-changes` |
| GET | `/api/admin/weekly-changes/{week_of}` |

## Error format

Errors follow FastAPI's default `{"detail": "..."}` shape. Server-side
5xx errors also include a `request_id` β€” a short hex string echoed in
the `X-Request-ID` response header. When a user reports an error, ask
for the request ID and grep the backend logs for `rid=<id>`.

## Rate limiting

None currently. Add one at the ingress layer (HF's edge) if traffic ever
warrants it.