Backend:
- Add fetch_bill_actions task with pagination and idempotent upsert
- Add fetch_actions_for_active_bills nightly batch (4 AM UTC beat schedule)
- Wire fetch_bill_actions into new-bill creation and _update_bill_if_changed
- Add backfill_brief_citations task: detects pre-citation briefs by JSONB
type check, deletes them, re-queues LLM processing against stored text
(LLM calls only — zero Congress.gov or GovInfo calls)
- Add admin endpoints: POST /bills/{id}/reprocess, /backfill-citations,
/trigger-fetch-actions; add uncited_briefs count to /stats
Frontend:
- New BriefPanel component: wraps AIBriefCard, adds amber "What Changed"
badge for amendment briefs and collapsible version history with
inline brief expansion
- Swap AIBriefCard for BriefPanel on bill detail page
- Admin panel: Backfill Citations + Fetch Bill Actions buttons; amber
warning in stats when uncited briefs remain
- Add feature roadmap document with phased plan through Phase 5
Co-Authored-By: Jack Levy
170 lines
5.1 KiB
TypeScript
170 lines
5.1 KiB
TypeScript
import axios from "axios";
|
|
import type {
|
|
Bill,
|
|
BillAction,
|
|
BillDetail,
|
|
DashboardData,
|
|
Follow,
|
|
Member,
|
|
MemberTrendScore,
|
|
MemberNewsArticle,
|
|
NewsArticle,
|
|
PaginatedResponse,
|
|
SettingsData,
|
|
TrendScore,
|
|
} from "./types";
|
|
|
|
const apiClient = axios.create({
|
|
baseURL: process.env.NEXT_PUBLIC_API_URL || "",
|
|
timeout: 30000,
|
|
});
|
|
|
|
// Attach JWT from localStorage on every request
|
|
apiClient.interceptors.request.use((config) => {
|
|
if (typeof window !== "undefined") {
|
|
try {
|
|
const stored = localStorage.getItem("pocketveto-auth");
|
|
if (stored) {
|
|
const { state } = JSON.parse(stored);
|
|
if (state?.token) {
|
|
config.headers.Authorization = `Bearer ${state.token}`;
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore parse errors
|
|
}
|
|
}
|
|
return config;
|
|
});
|
|
|
|
interface AuthUser {
|
|
id: number;
|
|
email: string;
|
|
is_admin: boolean;
|
|
notification_prefs: Record<string, unknown>;
|
|
created_at: string;
|
|
}
|
|
|
|
interface TokenResponse {
|
|
access_token: string;
|
|
token_type: string;
|
|
user: AuthUser;
|
|
}
|
|
|
|
// Auth
|
|
export const authAPI = {
|
|
register: (email: string, password: string) =>
|
|
apiClient.post<TokenResponse>("/api/auth/register", { email, password }).then((r) => r.data),
|
|
login: (email: string, password: string) =>
|
|
apiClient.post<TokenResponse>("/api/auth/login", { email, password }).then((r) => r.data),
|
|
me: () =>
|
|
apiClient.get<AuthUser>("/api/auth/me").then((r) => r.data),
|
|
};
|
|
|
|
// Bills
|
|
export const billsAPI = {
|
|
list: (params?: Record<string, unknown>) =>
|
|
apiClient.get<PaginatedResponse<Bill>>("/api/bills", { params }).then((r) => r.data),
|
|
get: (id: string) =>
|
|
apiClient.get<BillDetail>(`/api/bills/${id}`).then((r) => r.data),
|
|
getActions: (id: string) =>
|
|
apiClient.get<BillAction[]>(`/api/bills/${id}/actions`).then((r) => r.data),
|
|
getNews: (id: string) =>
|
|
apiClient.get<NewsArticle[]>(`/api/bills/${id}/news`).then((r) => r.data),
|
|
getTrend: (id: string, days?: number) =>
|
|
apiClient.get<TrendScore[]>(`/api/bills/${id}/trend`, { params: { days } }).then((r) => r.data),
|
|
};
|
|
|
|
// Members
|
|
export const membersAPI = {
|
|
list: (params?: Record<string, unknown>) =>
|
|
apiClient.get<PaginatedResponse<Member>>("/api/members", { params }).then((r) => r.data),
|
|
get: (id: string) =>
|
|
apiClient.get<Member>(`/api/members/${id}`).then((r) => r.data),
|
|
getBills: (id: string, params?: Record<string, unknown>) =>
|
|
apiClient.get<PaginatedResponse<Bill>>(`/api/members/${id}/bills`, { params }).then((r) => r.data),
|
|
getTrend: (id: string, days?: number) =>
|
|
apiClient.get<MemberTrendScore[]>(`/api/members/${id}/trend`, { params: { days } }).then((r) => r.data),
|
|
getNews: (id: string) =>
|
|
apiClient.get<MemberNewsArticle[]>(`/api/members/${id}/news`).then((r) => r.data),
|
|
};
|
|
|
|
// Follows
|
|
export const followsAPI = {
|
|
list: () =>
|
|
apiClient.get<Follow[]>("/api/follows").then((r) => r.data),
|
|
add: (follow_type: string, follow_value: string) =>
|
|
apiClient.post<Follow>("/api/follows", { follow_type, follow_value }).then((r) => r.data),
|
|
remove: (id: number) =>
|
|
apiClient.delete(`/api/follows/${id}`),
|
|
};
|
|
|
|
// Dashboard
|
|
export const dashboardAPI = {
|
|
get: () =>
|
|
apiClient.get<DashboardData>("/api/dashboard").then((r) => r.data),
|
|
};
|
|
|
|
// Search
|
|
export const searchAPI = {
|
|
search: (q: string) =>
|
|
apiClient.get<{ bills: Bill[]; members: Member[] }>("/api/search", { params: { q } }).then((r) => r.data),
|
|
};
|
|
|
|
// Settings
|
|
export const settingsAPI = {
|
|
get: () =>
|
|
apiClient.get<SettingsData>("/api/settings").then((r) => r.data),
|
|
update: (key: string, value: string) =>
|
|
apiClient.put("/api/settings", { key, value }).then((r) => r.data),
|
|
testLLM: () =>
|
|
apiClient.post("/api/settings/test-llm").then((r) => r.data),
|
|
};
|
|
|
|
export interface AdminUser {
|
|
id: number;
|
|
email: string;
|
|
is_admin: boolean;
|
|
follow_count: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface AnalysisStats {
|
|
total_bills: number;
|
|
docs_fetched: number;
|
|
briefs_generated: number;
|
|
full_briefs: number;
|
|
amendment_briefs: number;
|
|
uncited_briefs: number;
|
|
remaining: number;
|
|
}
|
|
|
|
// Admin
|
|
export const adminAPI = {
|
|
// Stats
|
|
getStats: () =>
|
|
apiClient.get<AnalysisStats>("/api/admin/stats").then((r) => r.data),
|
|
// Users
|
|
listUsers: () =>
|
|
apiClient.get<AdminUser[]>("/api/admin/users").then((r) => r.data),
|
|
deleteUser: (id: number) =>
|
|
apiClient.delete(`/api/admin/users/${id}`),
|
|
toggleAdmin: (id: number) =>
|
|
apiClient.patch<AdminUser>(`/api/admin/users/${id}/toggle-admin`).then((r) => r.data),
|
|
// Tasks
|
|
triggerPoll: () =>
|
|
apiClient.post("/api/admin/trigger-poll").then((r) => r.data),
|
|
triggerMemberSync: () =>
|
|
apiClient.post("/api/admin/trigger-member-sync").then((r) => r.data),
|
|
triggerTrendScores: () =>
|
|
apiClient.post("/api/admin/trigger-trend-scores").then((r) => r.data),
|
|
backfillSponsors: () =>
|
|
apiClient.post("/api/admin/backfill-sponsors").then((r) => r.data),
|
|
backfillCitations: () =>
|
|
apiClient.post("/api/admin/backfill-citations").then((r) => r.data),
|
|
triggerFetchActions: () =>
|
|
apiClient.post("/api/admin/trigger-fetch-actions").then((r) => r.data),
|
|
getTaskStatus: (taskId: string) =>
|
|
apiClient.get(`/api/admin/task-status/${taskId}`).then((r) => r.data),
|
|
};
|