diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..da0198a --- /dev/null +++ b/biome.json @@ -0,0 +1,7 @@ +{ + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + } +} \ No newline at end of file diff --git a/orchestrator/package.json b/orchestrator/package.json index d63c4cf..5af6628 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -90,4 +90,4 @@ "vite": "^6.0.3", "vitest": "^4.0.16" } -} \ No newline at end of file +} diff --git a/orchestrator/src/client/App.tsx b/orchestrator/src/client/App.tsx index c5df563..152e650 100644 --- a/orchestrator/src/client/App.tsx +++ b/orchestrator/src/client/App.tsx @@ -7,11 +7,11 @@ import { Navigate, Route, Routes, useLocation } from "react-router-dom"; import { CSSTransition, SwitchTransition } from "react-transition-group"; import { Toaster } from "@/components/ui/sonner"; +import { OnboardingGate } from "./components/OnboardingGate"; import { OrchestratorPage } from "./pages/OrchestratorPage"; import { SettingsPage } from "./pages/SettingsPage"; import { UkVisaJobsPage } from "./pages/UkVisaJobsPage"; import { VisaSponsorsPage } from "./pages/VisaSponsorsPage"; -import { OnboardingGate } from "./components/OnboardingGate"; export const App: React.FC = () => { const location = useLocation(); diff --git a/orchestrator/src/client/api/client.ts b/orchestrator/src/client/api/client.ts index b6223ef..875404d 100644 --- a/orchestrator/src/client/api/client.ts +++ b/orchestrator/src/client/api/client.ts @@ -2,40 +2,40 @@ * API client for the orchestrator backend. */ +import { trackEvent } from "@/lib/analytics"; import type { - Job, ApiResponse, - JobsListResponse, - PipelineStatusResponse, - JobSource, AppSettings, - ResumeProjectsSettings, - ResumeProjectCatalogItem, - UkVisaJobsSearchResponse, - UkVisaJobsImportResponse, CreateJobInput, + Job, + JobSource, + JobsListResponse, ManualJobDraft, - ManualJobInferenceResponse, ManualJobFetchResponse, + ManualJobInferenceResponse, + PipelineStatusResponse, + ProfileStatusResponse, + ResumeProfile, + ResumeProjectCatalogItem, + ResumeProjectsSettings, + UkVisaJobsImportResponse, + UkVisaJobsSearchResponse, + ValidationResult, + VisaSponsor, VisaSponsorSearchResponse, VisaSponsorStatusResponse, - VisaSponsor, - ResumeProfile, - ProfileStatusResponse, - ValidationResult, -} from '../../shared/types'; -import { trackEvent } from "@/lib/analytics"; +} from "../../shared/types"; -const API_BASE = '/api'; +const API_BASE = "/api"; async function fetchApi( endpoint: string, - options?: RequestInit + options?: RequestInit, ): Promise { const response = await fetch(`${API_BASE}${endpoint}`, { ...options, headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", ...options?.headers, }, }); @@ -47,12 +47,14 @@ async function fetchApi( 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?`); + 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'); + throw new Error(data.error || "API request failed"); } return data.data as T; @@ -60,7 +62,7 @@ async function fetchApi( // Jobs API export async function getJobs(statuses?: string[]): Promise { - const query = statuses?.length ? `?status=${statuses.join(',')}` : ''; + const query = statuses?.length ? `?status=${statuses.join(",")}` : ""; return fetchApi(`/jobs${query}`); } @@ -70,61 +72,67 @@ export async function getJob(id: string): Promise { export async function updateJob( id: string, - update: Partial + update: Partial, ): Promise { return fetchApi(`/jobs/${id}`, { - method: 'PATCH', + method: "PATCH", body: JSON.stringify(update), }); } -export async function processJob(id: string, options?: { force?: boolean }): Promise { - const query = options?.force ? '?force=1' : ''; +export async function processJob( + id: string, + options?: { force?: boolean }, +): Promise { + const query = options?.force ? "?force=1" : ""; return fetchApi(`/jobs/${id}/process${query}`, { - method: 'POST', + method: "POST", }); } export async function rescoreJob(id: string): Promise { return fetchApi(`/jobs/${id}/rescore`, { - method: 'POST', + method: "POST", }); } -export async function summarizeJob(id: string, options?: { force?: boolean }): Promise { - const query = options?.force ? '?force=1' : ''; +export async function summarizeJob( + id: string, + options?: { force?: boolean }, +): Promise { + const query = options?.force ? "?force=1" : ""; return fetchApi(`/jobs/${id}/summarize${query}`, { - method: 'POST', + method: "POST", }); } export async function generateJobPdf(id: string): Promise { return fetchApi(`/jobs/${id}/generate-pdf`, { - method: 'POST', + method: "POST", }); } export async function checkSponsor(id: string): Promise { return fetchApi(`/jobs/${id}/check-sponsor`, { - method: 'POST', + method: "POST", }); } export async function markAsApplied(id: string): Promise { return fetchApi(`/jobs/${id}/apply`, { - method: 'POST', + method: "POST", }); } export async function skipJob(id: string): Promise { return fetchApi(`/jobs/${id}/skip`, { - method: 'POST', + method: "POST", }); } // Pipeline API export async function getPipelineStatus(): Promise { - return fetchApi('/pipeline/status'); + return fetchApi("/pipeline/status"); } export async function runPipeline(config?: { @@ -132,8 +140,8 @@ export async function runPipeline(config?: { minSuitabilityScore?: number; sources?: JobSource[]; }): Promise<{ message: string }> { - return fetchApi<{ message: string }>('/pipeline/run', { - method: 'POST', + return fetchApi<{ message: string }>("/pipeline/run", { + method: "POST", body: JSON.stringify(config || {}), }); } @@ -144,13 +152,13 @@ export async function searchUkVisaJobs(input: { page?: number; }): Promise { if (input.searchTerm?.trim()) { - trackEvent('ukvisajobs_search', { + trackEvent("ukvisajobs_search", { searchTerm: input.searchTerm.trim(), page: input.page ?? 1, }); } - return fetchApi('/ukvisajobs/search', { - method: 'POST', + return fetchApi("/ukvisajobs/search", { + method: "POST", body: JSON.stringify(input), }); } @@ -158,8 +166,8 @@ export async function searchUkVisaJobs(input: { export async function importUkVisaJobs(input: { jobs: CreateJobInput[]; }): Promise { - return fetchApi('/ukvisajobs/import', { - method: 'POST', + return fetchApi("/ukvisajobs/import", { + method: "POST", body: JSON.stringify(input), }); } @@ -168,8 +176,8 @@ export async function importUkVisaJobs(input: { export async function fetchJobFromUrl(input: { url: string; }): Promise { - return fetchApi('/manual-jobs/fetch', { - method: 'POST', + return fetchApi("/manual-jobs/fetch", { + method: "POST", body: JSON.stringify(input), }); } @@ -177,8 +185,8 @@ export async function fetchJobFromUrl(input: { export async function inferManualJob(input: { jobDescription: string; }): Promise { - return fetchApi('/manual-jobs/infer', { - method: 'POST', + return fetchApi("/manual-jobs/infer", { + method: "POST", body: JSON.stringify(input), }); } @@ -186,8 +194,8 @@ export async function inferManualJob(input: { export async function importManualJob(input: { job: ManualJobDraft; }): Promise { - return fetchApi('/manual-jobs/import', { - method: 'POST', + return fetchApi("/manual-jobs/import", { + method: "POST", body: JSON.stringify(input), }); } @@ -197,23 +205,27 @@ let settingsPromise: Promise | null = null; export async function getSettings(): Promise { if (settingsPromise) return settingsPromise; - - settingsPromise = fetchApi('/settings').finally(() => { + + 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 { - return fetchApi('/profile/projects'); +export async function getProfileProjects(): Promise< + ResumeProjectCatalogItem[] +> { + return fetchApi("/profile/projects"); } -export async function getResumeProjectsCatalog(): Promise { +export async function getResumeProjectsCatalog(): Promise< + ResumeProjectCatalogItem[] +> { try { const settings = await getSettings(); if (settings.rxresumeBaseResumeId) { @@ -227,85 +239,94 @@ export async function getResumeProjectsCatalog(): Promise { - return fetchApi('/profile'); + return fetchApi("/profile"); } export async function getProfileStatus(): Promise { - return fetchApi('/profile/status'); + return fetchApi("/profile/status"); } export async function refreshProfile(): Promise { - return fetchApi('/profile/refresh', { - method: 'POST', + return fetchApi("/profile/refresh", { + method: "POST", }); } -export async function validateOpenrouter(apiKey?: string): Promise { - return fetchApi('/onboarding/validate/openrouter', { - method: 'POST', +export async function validateOpenrouter( + apiKey?: string, +): Promise { + return fetchApi("/onboarding/validate/openrouter", { + method: "POST", body: JSON.stringify({ apiKey }), }); } -export async function validateRxresume(email?: string, password?: string): Promise { - return fetchApi('/onboarding/validate/rxresume', { - method: 'POST', +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'); + return fetchApi("/onboarding/validate/resume"); } export async function updateSettings(update: { - model?: string | null - modelScorer?: string | null - modelTailoring?: string | null - modelProjectSelection?: 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 + model?: string | null; + modelScorer?: string | null; + modelTailoring?: string | null; + modelProjectSelection?: 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', + 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'); + const data = await fetchApi<{ resumes: { id: string; name: string }[] }>( + "/settings/rx-resumes", + ); return data.resumes; } -export async function getRxResumeProjects(resumeId: string, signal?: AbortSignal): Promise { +export async function getRxResumeProjects( + resumeId: string, + signal?: AbortSignal, +): Promise { const data = await fetchApi<{ projects: ResumeProjectCatalogItem[] }>( `/settings/rx-resumes/${encodeURIComponent(resumeId)}/projects`, - { signal } + { signal }, ); return data.projects; } - // Database API export async function clearDatabase(): Promise<{ message: string; @@ -316,8 +337,8 @@ export async function clearDatabase(): Promise<{ message: string; jobsDeleted: number; runsDeleted: number; - }>('/database', { - method: 'DELETE', + }>("/database", { + method: "DELETE", }); } @@ -329,13 +350,13 @@ export async function deleteJobsByStatus(status: string): Promise<{ message: string; count: number; }>(`/jobs/status/${status}`, { - method: 'DELETE', + method: "DELETE", }); } // Visa Sponsors API export async function getVisaSponsorStatus(): Promise { - return fetchApi('/visa-sponsors/status'); + return fetchApi("/visa-sponsors/status"); } export async function searchVisaSponsors(input: { @@ -344,20 +365,24 @@ export async function searchVisaSponsors(input: { minScore?: number; }): Promise { if (input.query?.trim()) { - trackEvent('visa_sponsor_search', { + trackEvent("visa_sponsor_search", { query: input.query.trim(), limit: input.limit, minScore: input.minScore, }); } - return fetchApi('/visa-sponsors/search', { - method: 'POST', + 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 getVisaSponsorOrganization( + name: string, +): Promise { + return fetchApi( + `/visa-sponsors/organization/${encodeURIComponent(name)}`, + ); } export async function updateVisaSponsorList(): Promise<{ @@ -367,8 +392,8 @@ export async function updateVisaSponsorList(): Promise<{ return fetchApi<{ message: string; status: VisaSponsorStatusResponse; - }>('/visa-sponsors/update', { - method: 'POST', + }>("/visa-sponsors/update", { + method: "POST", }); } diff --git a/orchestrator/src/client/api/index.ts b/orchestrator/src/client/api/index.ts index 4f1cce4..5ec7692 100644 --- a/orchestrator/src/client/api/index.ts +++ b/orchestrator/src/client/api/index.ts @@ -1 +1 @@ -export * from './client'; +export * from "./client"; diff --git a/orchestrator/src/client/components/FitAssessment.tsx b/orchestrator/src/client/components/FitAssessment.tsx index aacb42a..8330067 100644 --- a/orchestrator/src/client/components/FitAssessment.tsx +++ b/orchestrator/src/client/components/FitAssessment.tsx @@ -1,5 +1,5 @@ -import React from "react"; import { Sparkles } from "lucide-react"; +import type React from "react"; import { cn } from "@/lib/utils"; import type { Job } from "../../shared/types"; @@ -8,8 +8,8 @@ interface FitAssessmentProps { className?: string; } -export const FitAssessment: React.FC = ({ - job, +export const FitAssessment: React.FC = ({ + job, className, }) => { if (!job.suitabilityReason) return null; diff --git a/orchestrator/src/client/components/Header.tsx b/orchestrator/src/client/components/Header.tsx index feb70db..a67b0fe 100644 --- a/orchestrator/src/client/components/Header.tsx +++ b/orchestrator/src/client/components/Header.tsx @@ -2,7 +2,6 @@ * Header component with logo and pipeline trigger. */ -import React from "react"; import { Briefcase, ChevronDown, @@ -14,6 +13,7 @@ import { Settings, Shield, } from "lucide-react"; +import React from "react"; import { Link, useLocation } from "react-router-dom"; import { Button } from "@/components/ui/button"; @@ -56,7 +56,12 @@ export const Header: React.FC = ({ const location = useLocation(); const [sheetOpen, setSheetOpen] = React.useState(false); - const orderedSources: JobSource[] = ["gradcracker", "indeed", "linkedin", "ukvisajobs"]; + const orderedSources: JobSource[] = [ + "gradcracker", + "indeed", + "linkedin", + "ukvisajobs", + ]; const navLinks = [ { to: "/", label: "Dashboard", icon: Home }, @@ -75,23 +80,21 @@ export const Header: React.FC = ({ }; return ( -
-
-
+
+
+
- - + - - JobOps - + JobOps -