From 9dfb8626490df0b07521cdad20c9fb52cf079aaa Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Fri, 23 Jan 2026 11:40:34 +0000 Subject: [PATCH] ready panel now works with external resume json instead of local --- orchestrator/src/client/api/client.ts | 20 +++- .../src/client/components/ReadyPanel.tsx | 2 +- .../src/client/components/TailoringEditor.tsx | 2 +- .../discovered-panel/TailorMode.tsx | 2 +- .../src/server/api/routes/onboarding.ts | 64 ++++++---- orchestrator/src/server/api/routes/profile.ts | 110 ++++++++---------- .../src/server/api/routes/settings.ts | 31 +---- .../src/server/pipeline/orchestrator.ts | 12 +- orchestrator/src/server/services/pdf.ts | 8 +- .../src/server/services/profile.test.ts | 104 ++++++++++++++--- orchestrator/src/server/services/profile.ts | 58 ++++++--- orchestrator/src/shared/types.ts | 1 - 12 files changed, 250 insertions(+), 164 deletions(-) diff --git a/orchestrator/src/client/api/client.ts b/orchestrator/src/client/api/client.ts index f1dafcf..c8300b2 100644 --- a/orchestrator/src/client/api/client.ts +++ b/orchestrator/src/client/api/client.ts @@ -176,6 +176,19 @@ export async function getProfileProjects(): Promise return fetchApi('/profile/projects'); } +export async function getResumeProjectsCatalog(): Promise { + 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'); } @@ -184,10 +197,9 @@ export async function getProfileStatus(): Promise { return fetchApi('/profile/status'); } -export async function uploadProfile(profile: ResumeProfile): Promise { - return fetchApi('/profile/upload', { +export async function refreshProfile(): Promise { + return fetchApi('/profile/refresh', { method: 'POST', - body: JSON.stringify({ profile }), }); } @@ -205,7 +217,7 @@ export async function validateRxresume(email?: string, password?: string): Promi }); } -export async function validateResumeJson(): Promise { +export async function validateResumeConfig(): Promise { return fetchApi('/onboarding/validate/resume'); } diff --git a/orchestrator/src/client/components/ReadyPanel.tsx b/orchestrator/src/client/components/ReadyPanel.tsx index f2a5483..644c986 100644 --- a/orchestrator/src/client/components/ReadyPanel.tsx +++ b/orchestrator/src/client/components/ReadyPanel.tsx @@ -79,7 +79,7 @@ export const ReadyPanel: React.FC = ({ // Load project catalog once useEffect(() => { - api.getProfileProjects().then(setCatalog).catch(console.error); + api.getResumeProjectsCatalog().then(setCatalog).catch(console.error); }, []); // Reset mode when job changes diff --git a/orchestrator/src/client/components/TailoringEditor.tsx b/orchestrator/src/client/components/TailoringEditor.tsx index 6d37fc5..8a4cf09 100644 --- a/orchestrator/src/client/components/TailoringEditor.tsx +++ b/orchestrator/src/client/components/TailoringEditor.tsx @@ -55,7 +55,7 @@ export const TailoringEditor: React.FC = ({ useEffect(() => { // Load project catalog - api.getProfileProjects().then(setCatalog).catch(console.error); + api.getResumeProjectsCatalog().then(setCatalog).catch(console.error); // Set initial selection if (job.selectedProjectIds) { diff --git a/orchestrator/src/client/components/discovered-panel/TailorMode.tsx b/orchestrator/src/client/components/discovered-panel/TailorMode.tsx index e25e07e..eeb0a23 100644 --- a/orchestrator/src/client/components/discovered-panel/TailorMode.tsx +++ b/orchestrator/src/client/components/discovered-panel/TailorMode.tsx @@ -41,7 +41,7 @@ export const TailorMode: React.FC = ({ const [showDescription, setShowDescription] = useState(false); useEffect(() => { - api.getProfileProjects().then(setCatalog).catch(console.error); + api.getResumeProjectsCatalog().then(setCatalog).catch(console.error); }, []); useEffect(() => { diff --git a/orchestrator/src/server/api/routes/onboarding.ts b/orchestrator/src/server/api/routes/onboarding.ts index 723c620..d7608e2 100644 --- a/orchestrator/src/server/api/routes/onboarding.ts +++ b/orchestrator/src/server/api/routes/onboarding.ts @@ -1,9 +1,9 @@ import { Router, Request, Response } from 'express'; -import { readFile, stat } from 'fs/promises'; import { resumeDataSchema } from '@shared/rxresume-schema.js'; -import { DEFAULT_PROFILE_PATH } from '@server/services/profile.js'; import { RxResumeClient } from '@server/services/rxresume-client.js'; +import { getSetting } from '@server/repositories/settings.js'; +import { getResume, RxResumeCredentialsError } from '@server/services/rxresume-v4.js'; export const onboardingRouter = Router(); @@ -55,29 +55,51 @@ async function validateOpenrouter(apiKey?: string | null): Promise { +/** + * Validate that a base resume is configured and accessible via RxResume v4 API. + */ +async function validateResumeConfig(): Promise { try { - const fileInfo = await stat(DEFAULT_PROFILE_PATH); - if (!fileInfo.isFile() || fileInfo.size === 0) { - return { valid: false, message: 'Resume JSON is missing.' }; + // Check if rxresumeBaseResumeId is configured + const rxresumeBaseResumeId = await getSetting('rxresumeBaseResumeId'); + + if (!rxresumeBaseResumeId) { + return { + valid: false, + message: 'No base resume selected. Please select a resume from your RxResume account in Settings.' + }; } - const raw = await readFile(DEFAULT_PROFILE_PATH, 'utf-8'); - const parsed = JSON.parse(raw); - const result = resumeDataSchema.safeParse(parsed); - if (!result.success) { - const issue = result.error.issues[0]; - const path = issue?.path?.join('.') || ''; - const baseMessage = issue?.message ?? 'Resume JSON does not match the expected schema.'; - const details = path - ? `Field "${path}": ${baseMessage}` - : baseMessage; - return { valid: false, message: details }; - } + // Verify the resume is accessible and valid + try { + const resume = await getResume(rxresumeBaseResumeId); - return { valid: true, message: null }; + if (!resume.data || typeof resume.data !== 'object') { + return { valid: false, message: 'Selected resume is empty or invalid.' }; + } + + // Validate against schema + const result = resumeDataSchema.safeParse(resume.data); + if (!result.success) { + const issue = result.error.issues[0]; + const path = issue?.path?.join('.') || ''; + const baseMessage = issue?.message ?? 'Resume does not match the expected schema.'; + const details = path + ? `Field "${path}": ${baseMessage}` + : baseMessage; + return { valid: false, message: details }; + } + + return { valid: true, message: null }; + } catch (error) { + if (error instanceof RxResumeCredentialsError) { + return { valid: false, message: 'RxResume credentials not configured.' }; + } + const message = error instanceof Error ? error.message : 'Failed to fetch resume from RxResume.'; + return { valid: false, message }; + } } catch (error) { - const message = error instanceof Error ? error.message : 'Unable to read resume JSON.'; + const message = error instanceof Error ? error.message : 'Resume validation failed.'; return { valid: false, message }; } } @@ -119,6 +141,6 @@ onboardingRouter.post('/validate/rxresume', async (req: Request, res: Response) }); onboardingRouter.get('/validate/resume', async (_req: Request, res: Response) => { - const result = await validateResumeJson(); + const result = await validateResumeConfig(); res.json({ success: true, data: result }); }); diff --git a/orchestrator/src/server/api/routes/profile.ts b/orchestrator/src/server/api/routes/profile.ts index c75bd31..e129482 100644 --- a/orchestrator/src/server/api/routes/profile.ts +++ b/orchestrator/src/server/api/routes/profile.ts @@ -1,30 +1,16 @@ import { Router, Request, Response } from 'express'; -import { mkdir, stat, writeFile } from 'fs/promises'; -import { dirname } from 'path'; import { extractProjectsFromProfile } from '../../services/resumeProjects.js'; -import { clearProfileCache, DEFAULT_PROFILE_PATH, getProfile } from '../../services/profile.js'; -import { resumeDataSchema } from '@shared/rxresume-schema.js'; +import { getProfile, clearProfileCache } from '../../services/profile.js'; +import { getSetting } from '../../repositories/settings.js'; +import { getResume, RxResumeCredentialsError } from '../../services/rxresume-v4.js'; export const profileRouter = Router(); -async function profileExists(): Promise { - try { - const fileInfo = await stat(DEFAULT_PROFILE_PATH); - return fileInfo.isFile() && fileInfo.size > 0; - } catch { - return false; - } -} - /** * GET /api/profile/projects - Get all projects available in the base resume */ profileRouter.get('/projects', async (req: Request, res: Response) => { try { - if (!(await profileExists())) { - res.json({ success: true, data: [] }); - return; - } const profile = await getProfile(); const { catalog } = extractProjectsFromProfile(profile); res.json({ success: true, data: catalog }); @@ -39,10 +25,6 @@ profileRouter.get('/projects', async (req: Request, res: Response) => { */ profileRouter.get('/', async (req: Request, res: Response) => { try { - if (!(await profileExists())) { - res.json({ success: true, data: null }); - return; - } const profile = await getProfile(); res.json({ success: true, data: profile }); } catch (error) { @@ -52,13 +34,51 @@ profileRouter.get('/', async (req: Request, res: Response) => { }); /** - * GET /api/profile/status - Check if base resume exists + * GET /api/profile/status - Check if base resume is configured and accessible */ profileRouter.get('/status', async (_req: Request, res: Response) => { try { - const fileInfo = await stat(DEFAULT_PROFILE_PATH); - const exists = fileInfo.isFile() && fileInfo.size > 0; - res.json({ success: true, data: { exists, error: exists ? null : 'Resume file is empty' } }); + const rxresumeBaseResumeId = await getSetting('rxresumeBaseResumeId'); + + if (!rxresumeBaseResumeId) { + res.json({ + success: true, + data: { + exists: false, + error: 'No base resume selected. Please select a resume from your RxResume account in Settings.' + } + }); + return; + } + + // Verify the resume is accessible + try { + const resume = await getResume(rxresumeBaseResumeId); + if (!resume.data || typeof resume.data !== 'object') { + res.json({ + success: true, + data: { + exists: false, + error: 'Selected resume is empty or invalid.' + } + }); + return; + } + + res.json({ success: true, data: { exists: true, error: null } }); + } catch (error) { + if (error instanceof RxResumeCredentialsError) { + res.json({ + success: true, + data: { + exists: false, + error: 'RxResume credentials not configured.' + } + }); + return; + } + throw error; + } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; res.json({ success: true, data: { exists: false, error: message } }); @@ -66,43 +86,15 @@ profileRouter.get('/status', async (_req: Request, res: Response) => { }); /** - * POST /api/profile/upload - Upload base resume JSON + * POST /api/profile/refresh - Clear profile cache and refetch from RxResume v4 API */ -profileRouter.post('/upload', async (req: Request, res: Response) => { +profileRouter.post('/refresh', async (_req: Request, res: Response) => { try { - const profile = (req.body && typeof req.body === 'object' ? (req.body as Record).profile : null) as unknown; - - if (!profile || typeof profile !== 'object' || Array.isArray(profile)) { - throw new Error('Invalid profile payload. Expected a JSON object.'); - } - - const parsed = resumeDataSchema.safeParse(profile); - if (!parsed.success) { - const issue = parsed.error.issues[0]; - const path = issue?.path?.join('.') || ''; - const baseMessage = issue?.message ?? 'Resume JSON does not match the RxResume schema.'; - const details = path ? `Field "${path}": ${baseMessage}` : baseMessage; - throw new Error(`Invalid resume JSON: ${details}`); - } - - const existing = await stat(DEFAULT_PROFILE_PATH).catch(() => null); - if (existing && existing.isDirectory()) { - throw new Error('Resume path is a directory. Remove it and upload again.'); - } - - await mkdir(dirname(DEFAULT_PROFILE_PATH), { recursive: true }); - await writeFile(DEFAULT_PROFILE_PATH, JSON.stringify(parsed.data, null, 2), 'utf-8'); clearProfileCache(); - - res.json({ success: true, data: { exists: true, error: null } }); + const profile = await getProfile(true); + res.json({ success: true, data: profile }); } catch (error) { - let message = error instanceof Error ? error.message : 'Unknown error'; - if (error && typeof error === 'object' && 'code' in error) { - const code = (error as { code?: string }).code; - if (code === 'EROFS') { - message = 'Resume path is read-only. Remove the bind mount and restart the container.'; - } - } - res.status(400).json({ success: false, error: message }); + const message = error instanceof Error ? error.message : 'Unknown error'; + res.status(500).json({ success: false, error: message }); } }); diff --git a/orchestrator/src/server/api/routes/settings.ts b/orchestrator/src/server/api/routes/settings.ts index ddfef40..04956f0 100644 --- a/orchestrator/src/server/api/routes/settings.ts +++ b/orchestrator/src/server/api/routes/settings.ts @@ -69,35 +69,8 @@ settingsRouter.patch('/', async (req: Request, res: Response) => { promises.push(settingsRepo.setSetting('resumeProjects', null)); } else { promises.push((async () => { - const baseResumeId = 'rxresumeBaseResumeId' in input - ? normalizeEnvInput(input.rxresumeBaseResumeId) - : await settingsRepo.getSetting('rxresumeBaseResumeId'); - - let profile: Record = {}; - - if (baseResumeId) { - try { - const resume = await getResume(baseResumeId); - if (resume.data && typeof resume.data === 'object') { - profile = resume.data as Record; - } - } catch (error) { - if (error instanceof RxResumeCredentialsError) { - throw new Error('RxResume credentials missing while validating resume projects.'); - } - } - } - - if (Object.keys(profile).length === 0) { - const rawProfile = await getProfile(); - - if (rawProfile === null || typeof rawProfile !== 'object' || Array.isArray(rawProfile)) { - throw new Error('Invalid resume profile format: expected a non-null object'); - } - - profile = rawProfile as Record; - } - + // getProfile() will fetch from RxResume v4 API using rxresumeBaseResumeId + const profile = await getProfile(); const { catalog } = extractProjectsFromProfile(profile); const allowed = new Set(catalog.map((p) => p.id)); const normalized = normalizeResumeProjectsSettings(resumeProjects, allowed); diff --git a/orchestrator/src/server/pipeline/orchestrator.ts b/orchestrator/src/server/pipeline/orchestrator.ts index 4715bed..4bcac39 100644 --- a/orchestrator/src/server/pipeline/orchestrator.ts +++ b/orchestrator/src/server/pipeline/orchestrator.ts @@ -14,7 +14,7 @@ import { runUkVisaJobs } from '../services/ukvisajobs.js'; import { scoreJobSuitability } from '../services/scorer.js'; import { generateTailoring } from '../services/summary.js'; import { generatePdf } from '../services/pdf.js'; -import { DEFAULT_PROFILE_PATH, getProfile } from '../services/profile.js'; +import { getProfile } from '../services/profile.js'; import { getSetting } from '../repositories/settings.js'; import { pickProjectIdsForJob } from '../services/projectSelection.js'; import { extractProjectsFromProfile, resolveResumeProjectsSettings } from '../services/resumeProjects.js'; @@ -30,7 +30,6 @@ const DEFAULT_CONFIG: PipelineConfig = { topN: 10, minSuitabilityScore: 50, sources: ['gradcracker', 'indeed', 'linkedin', 'ukvisajobs'], - profilePath: DEFAULT_PROFILE_PATH, outputDir: join(getDataDir(), 'pdfs'), enableCrawling: true, enableScoring: true, @@ -108,7 +107,7 @@ export async function runPipeline(config: Partial = {}): Promise try { // Step 1: Load profile console.log('\nšŸ“‹ Loading profile...'); - const profile = await getProfile(mergedConfig.profilePath).catch((error) => { + const profile = await getProfile().catch((error) => { console.warn('āš ļø Failed to load profile for scoring, using empty profile:', error); return {} as Record; }); @@ -348,7 +347,7 @@ export async function runPipeline(config: Partial = {}): Promise // Process job (Generate Summary + PDF) // We catch errors here to ensure one failure doesn't stop the whole batch - const result = await processJob(job.id, { profilePath: mergedConfig.profilePath }); + const result = await processJob(job.id, { force: false }); if (result.success) { processedCount++; @@ -417,7 +416,6 @@ export async function runPipeline(config: Partial = {}): Promise export type ProcessJobOptions = { force?: boolean; - profilePath?: string; }; /** @@ -436,7 +434,7 @@ export async function summarizeJob( const job = await jobsRepo.getJobById(jobId); if (!job) return { success: false, error: 'Job not found' }; - const profile = await getProfile(options?.profilePath); + const profile = await getProfile(); // 1. Generate Summary & Tailoring let tailoredSummary = job.tailoredSummary; @@ -520,7 +518,7 @@ export async function generateFinalPdf( skills: job.tailoredSkills ? JSON.parse(job.tailoredSkills) : [] }, job.jobDescription || '', - options?.profilePath || DEFAULT_PROFILE_PATH, + undefined, // deprecated baseResumePath parameter job.selectedProjectIds ); diff --git a/orchestrator/src/server/services/pdf.ts b/orchestrator/src/server/services/pdf.ts index 29c7796..55b4378 100644 --- a/orchestrator/src/server/services/pdf.ts +++ b/orchestrator/src/server/services/pdf.ts @@ -93,7 +93,7 @@ export async function generatePdf( jobId: string, tailoredContent: TailoredPdfContent, jobDescription: string, - baseResumePath?: string, + _baseResumePath?: string, // Deprecated: now always uses getProfile() which fetches from v4 API selectedProjectIds?: string | null ): Promise { console.log(`šŸ“„ Generating PDF for job ${jobId} using RxResume v4 API...`); @@ -108,10 +108,8 @@ export async function generatePdf( const { email, password, baseUrl } = await getCredentials(); const client = new RxResumeClient(baseUrl); - // Read base resume - const baseResume = baseResumePath - ? JSON.parse(await import('fs/promises').then(fs => fs.readFile(baseResumePath, 'utf-8'))) - : JSON.parse(JSON.stringify(await getProfile())); + // Read base resume from profile (fetches from v4 API if configured) + const baseResume = JSON.parse(JSON.stringify(await getProfile())); // Sanitize skills: Ensure all skills have required schema fields (visible, description, id, level, keywords) // This fixes issues where the base JSON uses a shorthand format (missing required fields) diff --git a/orchestrator/src/server/services/profile.test.ts b/orchestrator/src/server/services/profile.test.ts index 7d17a42..1cf02d6 100644 --- a/orchestrator/src/server/services/profile.test.ts +++ b/orchestrator/src/server/services/profile.test.ts @@ -1,32 +1,100 @@ - import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { readFile } from 'fs/promises'; -import { getProfile } from './profile.js'; +import { getProfile, clearProfileCache } from './profile.js'; -vi.mock('fs/promises', async () => { - const fn = vi.fn(); - return { - readFile: fn, - default: { - readFile: fn +// Mock the dependencies +vi.mock('../repositories/settings.js', () => ({ + getSetting: vi.fn(), +})); + +vi.mock('./rxresume-v4.js', () => ({ + getResume: vi.fn(), + RxResumeCredentialsError: class RxResumeCredentialsError extends Error { + constructor() { + super('RxResume credentials not configured.'); + this.name = 'RxResumeCredentialsError'; } - }; -}); + }, +})); -describe('getProfile failure', () => { +import { getSetting } from '../repositories/settings.js'; +import { getResume, RxResumeCredentialsError } from './rxresume-v4.js'; + +describe('getProfile', () => { beforeEach(() => { vi.resetAllMocks(); + clearProfileCache(); }); - it('should throw an error if the profile file does not exist', async () => { - vi.mocked(readFile).mockRejectedValue(new Error('ENOENT: no such file or directory')); + it('should throw an error if rxresumeBaseResumeId is not configured', async () => { + vi.mocked(getSetting).mockResolvedValue(null); - await expect(getProfile('/non/existent/path.json', true)).rejects.toThrow('ENOENT: no such file or directory'); + await expect(getProfile()).rejects.toThrow( + 'Base resume not configured. Please select a base resume from your RxResume account in Settings.' + ); }); - it('should throw an error if the profile file is invalid JSON', async () => { - vi.mocked(readFile).mockResolvedValue('invalid json'); + it('should fetch profile from RxResume v4 API when configured', async () => { + const mockResumeData = { basics: { name: 'Test User' } }; + vi.mocked(getSetting).mockResolvedValue('test-resume-id'); + vi.mocked(getResume).mockResolvedValue({ + id: 'test-resume-id', + data: mockResumeData + } as any); - await expect(getProfile('/invalid/json.json', true)).rejects.toThrow(); + const profile = await getProfile(); + + expect(getSetting).toHaveBeenCalledWith('rxresumeBaseResumeId'); + expect(getResume).toHaveBeenCalledWith('test-resume-id'); + expect(profile).toEqual(mockResumeData); + }); + + it('should cache the profile and not refetch on subsequent calls', async () => { + const mockResumeData = { basics: { name: 'Test User' } }; + vi.mocked(getSetting).mockResolvedValue('test-resume-id'); + vi.mocked(getResume).mockResolvedValue({ + id: 'test-resume-id', + data: mockResumeData + } as any); + + await getProfile(); + await getProfile(); + + // getSetting is called each time to check resumeId + expect(getSetting).toHaveBeenCalledTimes(2); + // But getResume should only be called once due to caching + expect(getResume).toHaveBeenCalledTimes(1); + }); + + it('should refetch when forceRefresh is true', async () => { + const mockResumeData = { basics: { name: 'Test User' } }; + vi.mocked(getSetting).mockResolvedValue('test-resume-id'); + vi.mocked(getResume).mockResolvedValue({ + id: 'test-resume-id', + data: mockResumeData + } as any); + + await getProfile(); + await getProfile(true); + + expect(getResume).toHaveBeenCalledTimes(2); + }); + + it('should throw user-friendly error on credential issues', async () => { + vi.mocked(getSetting).mockResolvedValue('test-resume-id'); + vi.mocked(getResume).mockRejectedValue(new RxResumeCredentialsError()); + + await expect(getProfile()).rejects.toThrow( + 'RxResume credentials not configured. Set RXRESUME_EMAIL and RXRESUME_PASSWORD in settings.' + ); + }); + + it('should throw error if resume data is empty', async () => { + vi.mocked(getSetting).mockResolvedValue('test-resume-id'); + vi.mocked(getResume).mockResolvedValue({ + id: 'test-resume-id', + data: null + } as any); + + await expect(getProfile()).rejects.toThrow('Resume data is empty or invalid'); }); }); diff --git a/orchestrator/src/server/services/profile.ts b/orchestrator/src/server/services/profile.ts index dd935dd..6470f4c 100644 --- a/orchestrator/src/server/services/profile.ts +++ b/orchestrator/src/server/services/profile.ts @@ -1,33 +1,56 @@ -import { readFile } from 'fs/promises'; -import { join } from 'path'; +/** + * Profile service - fetches resume data from RxResume v4 API. + * + * The rxresumeBaseResumeId setting is REQUIRED for the app to function. + * There is no local file fallback. + */ -import { getDataDir } from '../config/dataDir.js'; - -export const DEFAULT_PROFILE_PATH = process.env.RESUME_PROFILE_PATH || join(getDataDir(), 'resume.json'); +import { getSetting } from '../repositories/settings.js'; +import { getResume, RxResumeCredentialsError } from './rxresume-v4.js'; let cachedProfile: any = null; -let cachedProfilePath: string | null = null; +let cachedResumeId: string | null = null; /** - * Get the base resume profile from resume.json. - * Caches the result since it doesn't change often. - * @param profilePath Optional absolute path to profile JSON. Defaults to base.json. - * @param forceRefresh Force reload from disk. + * Get the base resume profile from RxResume v4 API. + * + * Requires rxresumeBaseResumeId to be configured in settings. + * Results are cached until clearProfileCache() is called. + * + * @param forceRefresh Force reload from API. + * @throws Error if rxresumeBaseResumeId is not configured or API call fails. */ -export async function getProfile(profilePath?: string, forceRefresh = false): Promise { - const targetPath = profilePath || DEFAULT_PROFILE_PATH; +export async function getProfile(forceRefresh = false): Promise { + const rxresumeBaseResumeId = await getSetting('rxresumeBaseResumeId'); - if (cachedProfile && cachedProfilePath === targetPath && !forceRefresh) { + if (!rxresumeBaseResumeId) { + throw new Error( + 'Base resume not configured. Please select a base resume from your RxResume account in Settings.' + ); + } + + // Return cached profile if valid + if (cachedProfile && cachedResumeId === rxresumeBaseResumeId && !forceRefresh) { return cachedProfile; } try { - const content = await readFile(targetPath, 'utf-8'); - cachedProfile = JSON.parse(content); - cachedProfilePath = targetPath; + console.log(`šŸ“‹ Fetching profile from RxResume v4 API (resume: ${rxresumeBaseResumeId})...`); + const resume = await getResume(rxresumeBaseResumeId); + + if (!resume.data || typeof resume.data !== 'object') { + throw new Error('Resume data is empty or invalid'); + } + + cachedProfile = resume.data; + cachedResumeId = rxresumeBaseResumeId; + console.log(`āœ… Profile loaded from RxResume v4 API`); return cachedProfile; } catch (error) { - console.error(`āŒ Failed to load profile from ${targetPath}:`, error); + if (error instanceof RxResumeCredentialsError) { + throw new Error('RxResume credentials not configured. Set RXRESUME_EMAIL and RXRESUME_PASSWORD in settings.'); + } + console.error(`āŒ Failed to load profile from RxResume v4 API:`, error); throw error; } } @@ -45,4 +68,5 @@ export async function getPersonName(): Promise { */ export function clearProfileCache(): void { cachedProfile = null; + cachedResumeId = null; } diff --git a/orchestrator/src/shared/types.ts b/orchestrator/src/shared/types.ts index 049a787..a9f4d22 100644 --- a/orchestrator/src/shared/types.ts +++ b/orchestrator/src/shared/types.ts @@ -174,7 +174,6 @@ export interface PipelineConfig { topN: number; // Number of top jobs to process minSuitabilityScore: number; // Minimum score to auto-process sources: JobSource[]; // Job sources to crawl - profilePath: string; // Path to profile JSON outputDir: string; // Directory for generated PDFs enableCrawling?: boolean; enableScoring?: boolean;