/** * API client for the orchestrator backend. */ import type { ApiResponse, ApplicationStage, ApplicationTask, AppSettings, BackupInfo, CreateJobInput, Job, JobOutcome, JobSource, JobsListResponse, ManualJobDraft, ManualJobFetchResponse, ManualJobInferenceResponse, PipelineStatusResponse, ProfileStatusResponse, ResumeProfile, ResumeProjectCatalogItem, ResumeProjectsSettings, StageEvent, StageEventMetadata, StageTransitionTarget, UkVisaJobsImportResponse, UkVisaJobsSearchResponse, ValidationResult, VisaSponsor, VisaSponsorSearchResponse, VisaSponsorStatusResponse, } from "@shared/types"; import { trackEvent } from "@/lib/analytics"; const API_BASE = "/api"; async function fetchApi( endpoint: string, options?: RequestInit, ): Promise { const response = await fetch(`${API_BASE}${endpoint}`, { ...options, headers: { "Content-Type": "application/json", ...options?.headers, }, }); const text = await response.text(); let data: ApiResponse; try { data = JSON.parse(text); } catch { // If the response is not JSON, it's likely an HTML error page console.error("API returned non-JSON response:", text.substring(0, 500)); throw new Error( `Server error (${response.status}): Expected JSON but received HTML. Is the backend server running?`, ); } if (!data.success) { throw new Error(data.error || "API request failed"); } return data.data as T; } // Jobs API export async function getJobs(statuses?: string[]): Promise { const query = statuses?.length ? `?status=${statuses.join(",")}` : ""; return fetchApi(`/jobs${query}`); } export async function getJob(id: string): Promise { return fetchApi(`/jobs/${id}?t=${Date.now()}`); } export async function updateJob( id: string, update: Partial, ): Promise { return fetchApi(`/jobs/${id}`, { method: "PATCH", body: JSON.stringify(update), }); } export async function processJob( id: string, options?: { force?: boolean }, ): Promise { const query = options?.force ? "?force=1" : ""; return fetchApi(`/jobs/${id}/process${query}`, { method: "POST", }); } export async function rescoreJob(id: string): Promise { return fetchApi(`/jobs/${id}/rescore`, { method: "POST", }); } export async function summarizeJob( id: string, options?: { force?: boolean }, ): Promise { const query = options?.force ? "?force=1" : ""; return fetchApi(`/jobs/${id}/summarize${query}`, { method: "POST", }); } export async function generateJobPdf(id: string): Promise { return fetchApi(`/jobs/${id}/generate-pdf`, { method: "POST", }); } export async function checkSponsor(id: string): Promise { return fetchApi(`/jobs/${id}/check-sponsor`, { method: "POST", }); } export async function markAsApplied(id: string): Promise { return fetchApi(`/jobs/${id}/apply`, { method: "POST", }); } export async function skipJob(id: string): Promise { return fetchApi(`/jobs/${id}/skip`, { method: "POST", }); } export async function getJobStageEvents(id: string): Promise { return fetchApi(`/jobs/${id}/events?t=${Date.now()}`); } export async function getJobTasks( id: string, options?: { includeCompleted?: boolean }, ): Promise { const params = new URLSearchParams(); if (options?.includeCompleted) params.set("includeCompleted", "1"); params.set("t", Date.now().toString()); const query = params.toString(); return fetchApi(`/jobs/${id}/tasks?${query}`); } export async function transitionJobStage( id: string, input: { toStage: StageTransitionTarget; occurredAt?: number | null; metadata?: StageEventMetadata | null; outcome?: JobOutcome | null; }, ): Promise { return fetchApi(`/jobs/${id}/stages`, { method: "POST", body: JSON.stringify(input), }); } export async function updateJobStageEvent( id: string, eventId: string, input: { toStage?: ApplicationStage; occurredAt?: number | null; metadata?: StageEventMetadata | null; outcome?: JobOutcome | null; }, ): Promise { return fetchApi(`/jobs/${id}/events/${eventId}`, { method: "PATCH", body: JSON.stringify(input), }); } export async function deleteJobStageEvent( id: string, eventId: string, ): Promise { return fetchApi(`/jobs/${id}/events/${eventId}`, { method: "DELETE", }); } export async function updateJobOutcome( id: string, input: { outcome: JobOutcome | null; closedAt?: number | null }, ): Promise { return fetchApi(`/jobs/${id}/outcome`, { method: "PATCH", body: JSON.stringify(input), }); } // Pipeline API export async function getPipelineStatus(): Promise { return fetchApi("/pipeline/status"); } export async function runPipeline(config?: { topN?: number; minSuitabilityScore?: number; sources?: JobSource[]; }): Promise<{ message: string }> { return fetchApi<{ message: string }>("/pipeline/run", { method: "POST", body: JSON.stringify(config || {}), }); } // UK Visa Jobs API export async function searchUkVisaJobs(input: { searchTerm?: string; page?: number; }): Promise { if (input.searchTerm?.trim()) { trackEvent("ukvisajobs_search", { searchTerm: input.searchTerm.trim(), page: input.page ?? 1, }); } return fetchApi("/ukvisajobs/search", { method: "POST", body: JSON.stringify(input), }); } export async function importUkVisaJobs(input: { jobs: CreateJobInput[]; }): Promise { return fetchApi("/ukvisajobs/import", { method: "POST", body: JSON.stringify(input), }); } // Manual Job Import API export async function fetchJobFromUrl(input: { url: string; }): Promise { return fetchApi("/manual-jobs/fetch", { method: "POST", body: JSON.stringify(input), }); } export async function inferManualJob(input: { jobDescription: string; }): Promise { return fetchApi("/manual-jobs/infer", { method: "POST", body: JSON.stringify(input), }); } export async function importManualJob(input: { job: ManualJobDraft; }): Promise { return fetchApi("/manual-jobs/import", { method: "POST", body: JSON.stringify(input), }); } // Settings & Profile API let settingsPromise: Promise | null = null; export async function getSettings(): Promise { if (settingsPromise) return settingsPromise; settingsPromise = fetchApi("/settings").finally(() => { // Clear the promise after a short delay to allow subsequent fresh fetches // but coalesce simultaneous requests. setTimeout(() => { settingsPromise = null; }, 100); }); return settingsPromise; } export async function getProfileProjects(): Promise< ResumeProjectCatalogItem[] > { return fetchApi("/profile/projects"); } export async function getResumeProjectsCatalog(): Promise< ResumeProjectCatalogItem[] > { try { const settings = await getSettings(); if (settings.rxresumeBaseResumeId) { return await getRxResumeProjects(settings.rxresumeBaseResumeId); } } catch { // fall through to profile-based projects } return getProfileProjects(); } export async function getProfile(): Promise { return fetchApi("/profile"); } export async function getProfileStatus(): Promise { return fetchApi("/profile/status"); } export async function refreshProfile(): Promise { return fetchApi("/profile/refresh", { method: "POST", }); } export async function validateLlm(input: { provider?: string; baseUrl?: string; apiKey?: string; }): Promise { return fetchApi("/onboarding/validate/llm", { method: "POST", body: JSON.stringify(input), }); } export async function validateRxresume( email?: string, password?: string, ): Promise { return fetchApi("/onboarding/validate/rxresume", { method: "POST", body: JSON.stringify({ email, password }), }); } export async function validateResumeConfig(): Promise { return fetchApi("/onboarding/validate/resume"); } export async function updateSettings(update: { model?: string | null; modelScorer?: string | null; modelTailoring?: string | null; modelProjectSelection?: string | null; llmProvider?: string | null; llmBaseUrl?: string | null; llmApiKey?: string | null; pipelineWebhookUrl?: string | null; jobCompleteWebhookUrl?: string | null; resumeProjects?: ResumeProjectsSettings | null; ukvisajobsMaxJobs?: number | null; gradcrackerMaxJobsPerTerm?: number | null; searchTerms?: string[] | null; jobspyLocation?: string | null; jobspyResultsWanted?: number | null; jobspyHoursOld?: number | null; jobspyCountryIndeed?: string | null; jobspySites?: string[] | null; jobspyLinkedinFetchDescription?: boolean | null; showSponsorInfo?: boolean | null; openrouterApiKey?: string | null; rxresumeEmail?: string | null; rxresumePassword?: string | null; basicAuthUser?: string | null; basicAuthPassword?: string | null; ukvisajobsEmail?: string | null; ukvisajobsPassword?: string | null; webhookSecret?: string | null; rxresumeBaseResumeId?: string | null; }): Promise { return fetchApi("/settings", { method: "PATCH", body: JSON.stringify(update), }); } export async function getRxResumes(): Promise<{ id: string; name: string }[]> { const data = await fetchApi<{ resumes: { id: string; name: string }[] }>( "/settings/rx-resumes", ); return data.resumes; } export async function getRxResumeProjects( resumeId: string, signal?: AbortSignal, ): Promise { const data = await fetchApi<{ projects: ResumeProjectCatalogItem[] }>( `/settings/rx-resumes/${encodeURIComponent(resumeId)}/projects`, { signal }, ); return data.projects; } // Database API export async function clearDatabase(): Promise<{ message: string; jobsDeleted: number; runsDeleted: number; }> { return fetchApi<{ message: string; jobsDeleted: number; runsDeleted: number; }>("/database", { method: "DELETE", }); } export async function deleteJobsByStatus(status: string): Promise<{ message: string; count: number; }> { return fetchApi<{ message: string; count: number; }>(`/jobs/status/${status}`, { method: "DELETE", }); } // Visa Sponsors API export async function getVisaSponsorStatus(): Promise { return fetchApi("/visa-sponsors/status"); } export async function searchVisaSponsors(input: { query: string; limit?: number; minScore?: number; }): Promise { if (input.query?.trim()) { trackEvent("visa_sponsor_search", { query: input.query.trim(), limit: input.limit, minScore: input.minScore, }); } return fetchApi("/visa-sponsors/search", { method: "POST", body: JSON.stringify(input), }); } export async function getVisaSponsorOrganization( name: string, ): Promise { return fetchApi( `/visa-sponsors/organization/${encodeURIComponent(name)}`, ); } export async function updateVisaSponsorList(): Promise<{ message: string; status: VisaSponsorStatusResponse; }> { return fetchApi<{ message: string; status: VisaSponsorStatusResponse; }>("/visa-sponsors/update", { method: "POST", }); } // Bulk operations (intentionally none - processing is manual) // Backup API export interface BackupListResponse { backups: BackupInfo[]; nextScheduled: string | null; } export async function getBackups(): Promise { return fetchApi("/backups"); } export async function createManualBackup(): Promise { return fetchApi("/backups", { method: "POST", }); } export async function deleteBackup(filename: string): Promise { await fetchApi(`/backups/${encodeURIComponent(filename)}`, { method: "DELETE", }); }