import axios from "axios"; import type { AlignmentData, Bill, BillAction, BillDetail, BillVote, BriefSchema, Collection, CollectionDetail, DashboardData, Follow, Member, MemberTrendScore, MemberNewsArticle, NewsArticle, NotificationEvent, NotificationSettings, NotificationSettingsUpdate, 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; created_at: string; } interface TokenResponse { access_token: string; token_type: string; user: AuthUser; } // Auth export const authAPI = { register: (email: string, password: string) => apiClient.post("/api/auth/register", { email, password }).then((r) => r.data), login: (email: string, password: string) => apiClient.post("/api/auth/login", { email, password }).then((r) => r.data), me: () => apiClient.get("/api/auth/me").then((r) => r.data), }; // Bills export const billsAPI = { list: (params?: Record) => apiClient.get>("/api/bills", { params }).then((r) => r.data), get: (id: string) => apiClient.get(`/api/bills/${id}`).then((r) => r.data), getActions: (id: string) => apiClient.get(`/api/bills/${id}/actions`).then((r) => r.data), getNews: (id: string) => apiClient.get(`/api/bills/${id}/news`).then((r) => r.data), getTrend: (id: string, days?: number) => apiClient.get(`/api/bills/${id}/trend`, { params: { days } }).then((r) => r.data), getVotes: (id: string) => apiClient.get(`/api/bills/${id}/votes`).then((r) => r.data), generateDraft: (id: string, body: { stance: string; recipient: string; tone: string; selected_points: string[]; include_citations: boolean; zip_code?: string; rep_name?: string; }) => apiClient.post<{ draft: string }>(`/api/bills/${id}/draft-letter`, body).then((r) => r.data), }; // Collections export const collectionsAPI = { list: () => apiClient.get("/api/collections").then((r) => r.data), create: (name: string, is_public: boolean) => apiClient.post("/api/collections", { name, is_public }).then((r) => r.data), get: (id: number) => apiClient.get(`/api/collections/${id}`).then((r) => r.data), update: (id: number, data: { name?: string; is_public?: boolean }) => apiClient.patch(`/api/collections/${id}`, data).then((r) => r.data), delete: (id: number) => apiClient.delete(`/api/collections/${id}`), addBill: (id: number, bill_id: string) => apiClient.post(`/api/collections/${id}/bills/${bill_id}`).then((r) => r.data), removeBill: (id: number, bill_id: string) => apiClient.delete(`/api/collections/${id}/bills/${bill_id}`), getByShareToken: (token: string) => apiClient.get(`/api/collections/share/${token}`).then((r) => r.data), }; // Share (public) export const shareAPI = { getBrief: (token: string) => apiClient.get<{ brief: BriefSchema; bill: Bill }>(`/api/share/brief/${token}`).then((r) => r.data), getCollection: (token: string) => apiClient.get(`/api/share/collection/${token}`).then((r) => r.data), }; // Notes export const notesAPI = { get: (billId: string) => apiClient.get(`/api/notes/${billId}`).then((r) => r.data), upsert: (billId: string, content: string, pinned: boolean) => apiClient.put(`/api/notes/${billId}`, { content, pinned }).then((r) => r.data), delete: (billId: string) => apiClient.delete(`/api/notes/${billId}`), }; // Members export const membersAPI = { list: (params?: Record) => apiClient.get>("/api/members", { params }).then((r) => r.data), get: (id: string) => apiClient.get(`/api/members/${id}`).then((r) => r.data), byZip: (zip: string) => apiClient.get(`/api/members/by-zip/${zip}`).then((r) => r.data), getBills: (id: string, params?: Record) => apiClient.get>(`/api/members/${id}/bills`, { params }).then((r) => r.data), getTrend: (id: string, days?: number) => apiClient.get(`/api/members/${id}/trend`, { params: { days } }).then((r) => r.data), getNews: (id: string) => apiClient.get(`/api/members/${id}/news`).then((r) => r.data), }; // Follows export const followsAPI = { list: () => apiClient.get("/api/follows").then((r) => r.data), add: (follow_type: string, follow_value: string) => apiClient.post("/api/follows", { follow_type, follow_value }).then((r) => r.data), remove: (id: number) => apiClient.delete(`/api/follows/${id}`), updateMode: (id: number, mode: string) => apiClient.patch(`/api/follows/${id}/mode`, { follow_mode: mode }).then((r) => r.data), }; // Dashboard export const dashboardAPI = { get: () => apiClient.get("/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), }; export interface LLMModel { id: string; name: string; } // Settings export const settingsAPI = { get: () => apiClient.get("/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), listModels: (provider: string) => apiClient.get<{ models: LLMModel[]; error?: string }>("/api/settings/llm-models", { params: { provider } }).then((r) => r.data), }; export interface AdminUser { id: number; email: string; is_admin: boolean; follow_count: number; created_at: string; } export interface ApiHealthResult { status: "ok" | "error" | "skipped"; detail: string; latency_ms?: number; } export interface ApiHealth { congress_gov: ApiHealthResult; govinfo: ApiHealthResult; newsapi: ApiHealthResult; google_news: ApiHealthResult; rep_lookup: ApiHealthResult; redis: ApiHealthResult; smtp: ApiHealthResult; pytrends: ApiHealthResult; } export interface AnalysisStats { total_bills: number; docs_fetched: number; briefs_generated: number; full_briefs: number; amendment_briefs: number; uncited_briefs: number; no_text_bills: number; pending_llm: number; bills_missing_sponsor: number; bills_missing_metadata: number; bills_missing_actions: number; unlabeled_briefs: number; remaining: number; total_members: number; house_count: number; senate_count: number; members_missing_chamber: number; total_votes: number; stanced_bills_total: number; stanced_bills_with_votes: number; } export interface NotificationTestResult { status: "ok" | "error"; detail: string; event_count?: number; } // Notifications export const notificationsAPI = { getSettings: () => apiClient.get("/api/notifications/settings").then((r) => r.data), updateSettings: (data: NotificationSettingsUpdate) => apiClient.put("/api/notifications/settings", data).then((r) => r.data), resetRssToken: () => apiClient.post("/api/notifications/settings/rss-reset").then((r) => r.data), testNtfy: (data: { ntfy_topic_url: string; ntfy_auth_method: string; ntfy_token: string; ntfy_username: string; ntfy_password: string; }) => apiClient.post("/api/notifications/test/ntfy", data).then((r) => r.data), testRss: () => apiClient.post("/api/notifications/test/rss").then((r) => r.data), testEmail: () => apiClient.post("/api/notifications/test/email").then((r) => r.data), testFollowMode: (mode: string, event_type: string) => apiClient.post("/api/notifications/test/follow-mode", { mode, event_type }).then((r) => r.data), getHistory: () => apiClient.get("/api/notifications/history").then((r) => r.data), }; // Admin export const adminAPI = { // Stats getStats: () => apiClient.get("/api/admin/stats").then((r) => r.data), // Users listUsers: () => apiClient.get("/api/admin/users").then((r) => r.data), deleteUser: (id: number) => apiClient.delete(`/api/admin/users/${id}`), toggleAdmin: (id: number) => apiClient.patch(`/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), triggerFetchNews: () => apiClient.post("/api/admin/trigger-fetch-news").then((r) => r.data), triggerFetchVotes: () => apiClient.post("/api/admin/trigger-fetch-votes").then((r) => r.data), triggerMemberTrendScores: () => apiClient.post("/api/admin/trigger-member-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), backfillAllActions: () => apiClient.post("/api/admin/backfill-all-actions").then((r) => r.data), backfillMetadata: () => apiClient.post("/api/admin/backfill-metadata").then((r) => r.data), backfillLabels: () => apiClient.post("/api/admin/backfill-labels").then((r) => r.data), resumeAnalysis: () => apiClient.post("/api/admin/resume-analysis").then((r) => r.data), triggerWeeklyDigest: () => apiClient.post("/api/admin/trigger-weekly-digest").then((r) => r.data), getApiHealth: () => apiClient.get("/api/admin/api-health").then((r) => r.data), getTaskStatus: (taskId: string) => apiClient.get(`/api/admin/task-status/${taskId}`).then((r) => r.data), getNewsApiQuota: () => apiClient.get<{ remaining: number; limit: number }>("/api/admin/newsapi-quota").then((r) => r.data), clearGnewsCache: () => apiClient.post<{ cleared: number }>("/api/admin/clear-gnews-cache").then((r) => r.data), submitLlmBatch: () => apiClient.post("/api/admin/submit-llm-batch").then((r) => r.data), getLlmBatchStatus: () => apiClient.get<{ status: string; batch_id?: string; doc_count?: number; submitted_at?: string }>( "/api/admin/llm-batch-status" ).then((r) => r.data), backfillCosponsors: () => apiClient.post("/api/admin/backfill-cosponsors").then((r) => r.data), backfillCategories: () => apiClient.post("/api/admin/backfill-categories").then((r) => r.data), calculateEffectiveness: () => apiClient.post("/api/admin/calculate-effectiveness").then((r) => r.data), }; // Alignment export const alignmentAPI = { get: () => apiClient.get("/api/alignment").then((r) => r.data), };