diff --git a/orchestrator/src/server/api/routes/settings.ts b/orchestrator/src/server/api/routes/settings.ts index e262a82..b8ab30d 100644 --- a/orchestrator/src/server/api/routes/settings.ts +++ b/orchestrator/src/server/api/routes/settings.ts @@ -33,150 +33,158 @@ settingsRouter.get('/', async (_req: Request, res: Response) => { settingsRouter.patch('/', async (req: Request, res: Response) => { try { const input = updateSettingsSchema.parse(req.body); + const promises: Promise[] = []; if ('model' in input) { - const model = input.model ?? null; - await settingsRepo.setSetting('model', model); + promises.push(settingsRepo.setSetting('model', input.model ?? null)); } if ('modelScorer' in input) { - await settingsRepo.setSetting('modelScorer', input.modelScorer ?? null); + promises.push(settingsRepo.setSetting('modelScorer', input.modelScorer ?? null)); } if ('modelTailoring' in input) { - await settingsRepo.setSetting('modelTailoring', input.modelTailoring ?? null); + promises.push(settingsRepo.setSetting('modelTailoring', input.modelTailoring ?? null)); } if ('modelProjectSelection' in input) { - await settingsRepo.setSetting('modelProjectSelection', input.modelProjectSelection ?? null); + promises.push(settingsRepo.setSetting('modelProjectSelection', input.modelProjectSelection ?? null)); } if ('pipelineWebhookUrl' in input) { - const pipelineWebhookUrl = input.pipelineWebhookUrl ?? null; - await settingsRepo.setSetting('pipelineWebhookUrl', pipelineWebhookUrl); + promises.push(settingsRepo.setSetting('pipelineWebhookUrl', input.pipelineWebhookUrl ?? null)); } if ('jobCompleteWebhookUrl' in input) { - const webhookUrl = input.jobCompleteWebhookUrl ?? null; - await settingsRepo.setSetting('jobCompleteWebhookUrl', webhookUrl); + promises.push(settingsRepo.setSetting('jobCompleteWebhookUrl', input.jobCompleteWebhookUrl ?? null)); } if ('resumeProjects' in input) { const resumeProjects = input.resumeProjects ?? null; if (resumeProjects === null) { - await settingsRepo.setSetting('resumeProjects', null); + promises.push(settingsRepo.setSetting('resumeProjects', null)); } else { - const rawProfile = await getProfile(); + promises.push((async () => { + 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'); - } + if (rawProfile === null || typeof rawProfile !== 'object' || Array.isArray(rawProfile)) { + throw new Error('Invalid resume profile format: expected a non-null object'); + } - const profile = rawProfile as Record; - const { catalog } = extractProjectsFromProfile(profile); - const allowed = new Set(catalog.map((p) => p.id)); - const normalized = normalizeResumeProjectsSettings(resumeProjects, allowed); - await settingsRepo.setSetting('resumeProjects', JSON.stringify(normalized)); + const profile = rawProfile as Record; + const { catalog } = extractProjectsFromProfile(profile); + const allowed = new Set(catalog.map((p) => p.id)); + const normalized = normalizeResumeProjectsSettings(resumeProjects, allowed); + await settingsRepo.setSetting('resumeProjects', JSON.stringify(normalized)); + })()); } } if ('ukvisajobsMaxJobs' in input) { - const ukvisajobsMaxJobs = input.ukvisajobsMaxJobs ?? null; - await settingsRepo.setSetting('ukvisajobsMaxJobs', ukvisajobsMaxJobs !== null ? String(ukvisajobsMaxJobs) : null); + const val = input.ukvisajobsMaxJobs ?? null; + promises.push(settingsRepo.setSetting('ukvisajobsMaxJobs', val !== null ? String(val) : null)); } if ('gradcrackerMaxJobsPerTerm' in input) { - const gradcrackerMaxJobsPerTerm = input.gradcrackerMaxJobsPerTerm ?? null; - await settingsRepo.setSetting('gradcrackerMaxJobsPerTerm', gradcrackerMaxJobsPerTerm !== null ? String(gradcrackerMaxJobsPerTerm) : null); + const val = input.gradcrackerMaxJobsPerTerm ?? null; + promises.push(settingsRepo.setSetting('gradcrackerMaxJobsPerTerm', val !== null ? String(val) : null)); } if ('searchTerms' in input) { - const searchTerms = input.searchTerms ?? null; - await settingsRepo.setSetting('searchTerms', searchTerms !== null ? JSON.stringify(searchTerms) : null); + const val = input.searchTerms ?? null; + promises.push(settingsRepo.setSetting('searchTerms', val !== null ? JSON.stringify(val) : null)); } if ('jobspyLocation' in input) { - const value = input.jobspyLocation ?? null; - await settingsRepo.setSetting('jobspyLocation', value); + promises.push(settingsRepo.setSetting('jobspyLocation', input.jobspyLocation ?? null)); } if ('jobspyResultsWanted' in input) { - const value = input.jobspyResultsWanted ?? null; - await settingsRepo.setSetting('jobspyResultsWanted', value !== null ? String(value) : null); + const val = input.jobspyResultsWanted ?? null; + promises.push(settingsRepo.setSetting('jobspyResultsWanted', val !== null ? String(val) : null)); } if ('jobspyHoursOld' in input) { - const value = input.jobspyHoursOld ?? null; - await settingsRepo.setSetting('jobspyHoursOld', value !== null ? String(value) : null); + const val = input.jobspyHoursOld ?? null; + promises.push(settingsRepo.setSetting('jobspyHoursOld', val !== null ? String(val) : null)); } if ('jobspyCountryIndeed' in input) { - const value = input.jobspyCountryIndeed ?? null; - await settingsRepo.setSetting('jobspyCountryIndeed', value); + promises.push(settingsRepo.setSetting('jobspyCountryIndeed', input.jobspyCountryIndeed ?? null)); } if ('jobspySites' in input) { - const value = input.jobspySites ?? null; - await settingsRepo.setSetting('jobspySites', value !== null ? JSON.stringify(value) : null); + const val = input.jobspySites ?? null; + promises.push(settingsRepo.setSetting('jobspySites', val !== null ? JSON.stringify(val) : null)); } if ('jobspyLinkedinFetchDescription' in input) { - const value = input.jobspyLinkedinFetchDescription ?? null; - await settingsRepo.setSetting('jobspyLinkedinFetchDescription', value !== null ? (value ? '1' : '0') : null); + const val = input.jobspyLinkedinFetchDescription ?? null; + promises.push(settingsRepo.setSetting('jobspyLinkedinFetchDescription', val !== null ? (val ? '1' : '0') : null)); } if ('showSponsorInfo' in input) { - const value = input.showSponsorInfo ?? null; - await settingsRepo.setSetting('showSponsorInfo', value !== null ? (value ? '1' : '0') : null); + const val = input.showSponsorInfo ?? null; + promises.push(settingsRepo.setSetting('showSponsorInfo', val !== null ? (val ? '1' : '0') : null)); } if ('openrouterApiKey' in input) { const value = normalizeEnvInput(input.openrouterApiKey); - await settingsRepo.setSetting('openrouterApiKey', value); - applyEnvValue('OPENROUTER_API_KEY', value); + promises.push(settingsRepo.setSetting('openrouterApiKey', value).then(() => { + applyEnvValue('OPENROUTER_API_KEY', value); + })); } if ('rxresumeEmail' in input) { const value = normalizeEnvInput(input.rxresumeEmail); - await settingsRepo.setSetting('rxresumeEmail', value); - applyEnvValue('RXRESUME_EMAIL', value); + promises.push(settingsRepo.setSetting('rxresumeEmail', value).then(() => { + applyEnvValue('RXRESUME_EMAIL', value); + })); } if ('rxresumePassword' in input) { const value = normalizeEnvInput(input.rxresumePassword); - await settingsRepo.setSetting('rxresumePassword', value); - applyEnvValue('RXRESUME_PASSWORD', value); + promises.push(settingsRepo.setSetting('rxresumePassword', value).then(() => { + applyEnvValue('RXRESUME_PASSWORD', value); + })); } if ('basicAuthUser' in input) { const value = normalizeEnvInput(input.basicAuthUser); - await settingsRepo.setSetting('basicAuthUser', value); - applyEnvValue('BASIC_AUTH_USER', value); + promises.push(settingsRepo.setSetting('basicAuthUser', value).then(() => { + applyEnvValue('BASIC_AUTH_USER', value); + })); } if ('basicAuthPassword' in input) { const value = normalizeEnvInput(input.basicAuthPassword); - await settingsRepo.setSetting('basicAuthPassword', value); - applyEnvValue('BASIC_AUTH_PASSWORD', value); + promises.push(settingsRepo.setSetting('basicAuthPassword', value).then(() => { + applyEnvValue('BASIC_AUTH_PASSWORD', value); + })); } if ('ukvisajobsEmail' in input) { const value = normalizeEnvInput(input.ukvisajobsEmail); - await settingsRepo.setSetting('ukvisajobsEmail', value); - applyEnvValue('UKVISAJOBS_EMAIL', value); + promises.push(settingsRepo.setSetting('ukvisajobsEmail', value).then(() => { + applyEnvValue('UKVISAJOBS_EMAIL', value); + })); } if ('ukvisajobsPassword' in input) { const value = normalizeEnvInput(input.ukvisajobsPassword); - await settingsRepo.setSetting('ukvisajobsPassword', value); - applyEnvValue('UKVISAJOBS_PASSWORD', value); + promises.push(settingsRepo.setSetting('ukvisajobsPassword', value).then(() => { + applyEnvValue('UKVISAJOBS_PASSWORD', value); + })); } if ('webhookSecret' in input) { const value = normalizeEnvInput(input.webhookSecret); - await settingsRepo.setSetting('webhookSecret', value); - applyEnvValue('WEBHOOK_SECRET', value); + promises.push(settingsRepo.setSetting('webhookSecret', value).then(() => { + applyEnvValue('WEBHOOK_SECRET', value); + })); } + await Promise.all(promises); + const data = await getEffectiveSettings(); res.json({ success: true, data }); } catch (error) { diff --git a/orchestrator/src/server/pipeline/orchestrator.ts b/orchestrator/src/server/pipeline/orchestrator.ts index c79448b..620e990 100644 --- a/orchestrator/src/server/pipeline/orchestrator.ts +++ b/orchestrator/src/server/pipeline/orchestrator.ts @@ -121,8 +121,11 @@ export async function runPipeline(config: Partial = {}): Promise const discoveredJobs: CreateJobInput[] = []; const sourceErrors: string[] = []; + // Read all settings at once to avoid sequential DB calls + const settings = await settingsRepo.getAllSettings(); + // Read search terms setting - const searchTermsSetting = await settingsRepo.getSetting('searchTerms'); + const searchTermsSetting = settings.searchTerms; let searchTerms: string[] = []; if (searchTermsSetting) { @@ -139,7 +142,7 @@ export async function runPipeline(config: Partial = {}): Promise ); // Apply setting override for JobSpy sites - const jobspySitesSettingRaw = await settingsRepo.getSetting('jobspySites'); + const jobspySitesSettingRaw = settings.jobspySites; if (jobspySitesSettingRaw) { try { const allowed = JSON.parse(jobspySitesSettingRaw); @@ -157,11 +160,11 @@ export async function runPipeline(config: Partial = {}): Promise detail: `JobSpy: scraping ${jobSpySites.join(', ')}...`, }); - const jobspyLocationSetting = await settingsRepo.getSetting('jobspyLocation'); - const jobspyResultsWantedSetting = await settingsRepo.getSetting('jobspyResultsWanted'); - const jobspyHoursOldSetting = await settingsRepo.getSetting('jobspyHoursOld'); - const jobspyCountryIndeedSetting = await settingsRepo.getSetting('jobspyCountryIndeed'); - const jobspyLinkedinFetchDescriptionSetting = await settingsRepo.getSetting('jobspyLinkedinFetchDescription'); + const jobspyLocationSetting = settings.jobspyLocation; + const jobspyResultsWantedSetting = settings.jobspyResultsWanted; + const jobspyHoursOldSetting = settings.jobspyHoursOld; + const jobspyCountryIndeedSetting = settings.jobspyCountryIndeed; + const jobspyLinkedinFetchDescriptionSetting = settings.jobspyLinkedinFetchDescription; const jobSpyResult = await runJobSpy({ sites: jobSpySites, @@ -170,7 +173,7 @@ export async function runPipeline(config: Partial = {}): Promise resultsWanted: jobspyResultsWantedSetting ? parseInt(jobspyResultsWantedSetting, 10) : undefined, hoursOld: jobspyHoursOldSetting ? parseInt(jobspyHoursOldSetting, 10) : undefined, countryIndeed: jobspyCountryIndeedSetting ?? undefined, - linkedinFetchDescription: jobspyLinkedinFetchDescriptionSetting !== null ? jobspyLinkedinFetchDescriptionSetting === '1' : undefined, + linkedinFetchDescription: jobspyLinkedinFetchDescriptionSetting !== null && jobspyLinkedinFetchDescriptionSetting !== undefined ? jobspyLinkedinFetchDescriptionSetting === '1' : undefined, }); if (!jobSpyResult.success) { sourceErrors.push(`jobspy: ${jobSpyResult.error ?? 'unknown error'}`); @@ -189,7 +192,7 @@ export async function runPipeline(config: Partial = {}): Promise // Pass existing URLs to avoid clicking "Apply" on jobs we already have const existingJobUrls = await jobsRepo.getAllJobUrls(); - const gradcrackerMaxJobsSetting = await settingsRepo.getSetting('gradcrackerMaxJobsPerTerm'); + const gradcrackerMaxJobsSetting = settings.gradcrackerMaxJobsPerTerm; const gradcrackerMaxJobs = gradcrackerMaxJobsSetting ? parseInt(gradcrackerMaxJobsSetting, 10) : 50; const crawlerResult = await runCrawler({ @@ -224,7 +227,7 @@ export async function runPipeline(config: Partial = {}): Promise }); // Read max jobs setting from database (default to 50 if not set) - const ukvisajobsMaxJobsSetting = await settingsRepo.getSetting('ukvisajobsMaxJobs'); + const ukvisajobsMaxJobsSetting = settings.ukvisajobsMaxJobs; const ukvisajobsMaxJobs = ukvisajobsMaxJobsSetting ? parseInt(ukvisajobsMaxJobsSetting, 10) : 50; const ukVisaResult = await runUkVisaJobs({ diff --git a/orchestrator/src/server/pipeline/sponsor-matching.test.ts b/orchestrator/src/server/pipeline/sponsor-matching.test.ts index 5ef0daa..1f2dd9e 100644 --- a/orchestrator/src/server/pipeline/sponsor-matching.test.ts +++ b/orchestrator/src/server/pipeline/sponsor-matching.test.ts @@ -36,6 +36,7 @@ vi.mock('../repositories/pipeline.js', () => ({ vi.mock('../repositories/settings.js', () => ({ getSetting: vi.fn().mockResolvedValue(null), + getAllSettings: vi.fn().mockResolvedValue({}), })); vi.mock('../services/crawler.js', () => ({ diff --git a/orchestrator/src/server/services/envSettings.ts b/orchestrator/src/server/services/envSettings.ts index 85eb338..d94fb78 100644 --- a/orchestrator/src/server/services/envSettings.ts +++ b/orchestrator/src/server/services/envSettings.ts @@ -73,34 +73,30 @@ export async function applyStoredEnvOverrides(): Promise { export async function getEnvSettingsData( overrides?: Partial> ): Promise> { + const activeOverrides = overrides || await settingsRepo.getAllSettings(); const readableValues: Record = {}; const privateValues: Record = {}; for (const { settingKey, envKey } of readableStringConfig) { - const override = overrides ? (overrides[settingKey] ?? null) : await settingsRepo.getSetting(settingKey); + const override = activeOverrides[settingKey] ?? null; const rawValue = override ?? process.env[envKey]; readableValues[settingKey] = normalizeEnvInput(rawValue); } for (const { settingKey, envKey, defaultValue } of readableBooleanConfig) { - const override = overrides ? (overrides[settingKey] ?? null) : await settingsRepo.getSetting(settingKey); + const override = activeOverrides[settingKey] ?? null; const rawValue = override ?? process.env[envKey]; readableValues[settingKey] = parseEnvBoolean(rawValue, defaultValue); } for (const { settingKey, envKey, hintKey } of privateStringConfig) { - const override = overrides ? (overrides[settingKey] ?? null) : await settingsRepo.getSetting(settingKey); + const override = activeOverrides[settingKey] ?? null; const rawValue = override ?? process.env[envKey]; privateValues[hintKey] = rawValue ? rawValue.slice(0, 4) : null; } - const basicAuthUser = overrides - ? (overrides['basicAuthUser'] ?? process.env.BASIC_AUTH_USER) - : (await settingsRepo.getSetting('basicAuthUser')) ?? process.env.BASIC_AUTH_USER; - - const basicAuthPassword = overrides - ? (overrides['basicAuthPassword'] ?? process.env.BASIC_AUTH_PASSWORD) - : (await settingsRepo.getSetting('basicAuthPassword')) ?? process.env.BASIC_AUTH_PASSWORD; + const basicAuthUser = activeOverrides['basicAuthUser'] ?? process.env.BASIC_AUTH_USER; + const basicAuthPassword = activeOverrides['basicAuthPassword'] ?? process.env.BASIC_AUTH_PASSWORD; return { ...readableValues, diff --git a/orchestrator/src/server/services/manualJob.test.ts b/orchestrator/src/server/services/manualJob.test.ts index 00321c4..dd6277f 100644 --- a/orchestrator/src/server/services/manualJob.test.ts +++ b/orchestrator/src/server/services/manualJob.test.ts @@ -4,6 +4,7 @@ import { inferManualJobDetails } from "./manualJob.js"; vi.mock("../repositories/settings.js", () => ({ getSetting: vi.fn(), + getAllSettings: vi.fn().mockResolvedValue({}), })); const originalEnv = process.env; diff --git a/orchestrator/src/server/services/pdf-skills-validation.test.ts b/orchestrator/src/server/services/pdf-skills-validation.test.ts index 785e69c..d106ea1 100644 --- a/orchestrator/src/server/services/pdf-skills-validation.test.ts +++ b/orchestrator/src/server/services/pdf-skills-validation.test.ts @@ -47,6 +47,7 @@ vi.mock('fs', () => ({ vi.mock('../repositories/settings.js', () => ({ getSetting: vi.fn().mockResolvedValue(null), + getAllSettings: vi.fn().mockResolvedValue({}), })); vi.mock('./projectSelection.js', () => ({ diff --git a/orchestrator/src/server/services/pdf-tailoring.test.ts b/orchestrator/src/server/services/pdf-tailoring.test.ts index ab8ae6e..df187fe 100644 --- a/orchestrator/src/server/services/pdf-tailoring.test.ts +++ b/orchestrator/src/server/services/pdf-tailoring.test.ts @@ -49,6 +49,7 @@ vi.mock('fs', () => ({ vi.mock('../repositories/settings.js', () => ({ getSetting: vi.fn().mockResolvedValue(null), + getAllSettings: vi.fn().mockResolvedValue({}), })); vi.mock('./projectSelection.js', () => ({ diff --git a/orchestrator/src/server/services/projectSelection.ts b/orchestrator/src/server/services/projectSelection.ts index a90530b..d78d56b 100644 --- a/orchestrator/src/server/services/projectSelection.ts +++ b/orchestrator/src/server/services/projectSelection.ts @@ -38,8 +38,10 @@ export async function pickProjectIdsForJob(args: { return fallbackPickProjectIds(args.jobDescription, args.eligibleProjects, desiredCount); } - const overrideModel = await getSetting('model'); - const overrideModelProjectSelection = await getSetting('modelProjectSelection'); + const [overrideModel, overrideModelProjectSelection] = await Promise.all([ + getSetting('model'), + getSetting('modelProjectSelection'), + ]); // Precedence: Project-specific override > Global override > Env var > Default const model = overrideModelProjectSelection || overrideModel || process.env.MODEL || 'openai/gpt-4o-mini'; diff --git a/orchestrator/src/server/services/scorer.ts b/orchestrator/src/server/services/scorer.ts index ea46c79..e6a09e8 100644 --- a/orchestrator/src/server/services/scorer.ts +++ b/orchestrator/src/server/services/scorer.ts @@ -44,8 +44,10 @@ export async function scoreJobSuitability( return mockScore(job); } - const overrideModel = await getSetting('model'); - const overrideModelScorer = await getSetting('modelScorer'); + const [overrideModel, overrideModelScorer] = await Promise.all([ + getSetting('model'), + getSetting('modelScorer'), + ]); // Precedence: Scorer-specific override > Global override > Env var > Default const model = overrideModelScorer || overrideModel || process.env.MODEL || 'openai/gpt-4o-mini'; diff --git a/orchestrator/src/server/services/summary.ts b/orchestrator/src/server/services/summary.ts index 03fde39..d339a78 100644 --- a/orchestrator/src/server/services/summary.ts +++ b/orchestrator/src/server/services/summary.ts @@ -69,8 +69,10 @@ export async function generateTailoring( return { success: false, error: 'API key not configured' }; } - const overrideModel = await getSetting('model'); - const overrideModelTailoring = await getSetting('modelTailoring'); + const [overrideModel, overrideModelTailoring] = await Promise.all([ + getSetting('model'), + getSetting('modelTailoring'), + ]); // Precedence: Tailoring-specific override > Global override > Env var > Default const model = overrideModelTailoring || overrideModel || process.env.MODEL || 'openai/gpt-4o-mini'; const prompt = buildTailoringPrompt(profile, jobDescription);