From 0844071b7a02866d61914c3bace1f5b25bf8bac8 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Thu, 22 Jan 2026 16:12:18 +0000 Subject: [PATCH] dedupe --- .../src/client/pages/SettingsPage.tsx | 3 +- .../src/server/api/routes/settings.ts | 281 +----------------- .../src/server/repositories/settings.ts | 8 + .../src/server/services/envSettings.ts | 19 +- orchestrator/src/server/services/settings.ts | 146 +++++++++ 5 files changed, 174 insertions(+), 283 deletions(-) create mode 100644 orchestrator/src/server/services/settings.ts diff --git a/orchestrator/src/client/pages/SettingsPage.tsx b/orchestrator/src/client/pages/SettingsPage.tsx index 5bf70bd..1702ba9 100644 --- a/orchestrator/src/client/pages/SettingsPage.tsx +++ b/orchestrator/src/client/pages/SettingsPage.tsx @@ -116,7 +116,8 @@ const normalizeString = (value: string | null | undefined) => { const normalizePrivateInput = (value: string | null | undefined) => { const trimmed = value?.trim() - return trimmed ? trimmed : undefined + if (trimmed === "") return null + return trimmed || undefined } const isSameStringList = (left: string[] | null | undefined, right: string[] | null | undefined) => { diff --git a/orchestrator/src/server/api/routes/settings.ts b/orchestrator/src/server/api/routes/settings.ts index 039cca9..e262a82 100644 --- a/orchestrator/src/server/api/routes/settings.ts +++ b/orchestrator/src/server/api/routes/settings.ts @@ -3,15 +3,14 @@ import { updateSettingsSchema } from '@shared/settings-schema.js'; import * as settingsRepo from '@server/repositories/settings.js'; import { applyEnvValue, - getEnvSettingsData, normalizeEnvInput, } from '@server/services/envSettings.js'; import { extractProjectsFromProfile, normalizeResumeProjectsSettings, - resolveResumeProjectsSettings, } from '@server/services/resumeProjects.js'; import { getProfile } from '@server/services/profile.js'; +import { getEffectiveSettings } from '@server/services/settings.js'; export const settingsRouter = Router(); @@ -20,142 +19,8 @@ export const settingsRouter = Router(); */ settingsRouter.get('/', async (_req: Request, res: Response) => { try { - const overrideModel = await settingsRepo.getSetting('model'); - const defaultModel = process.env.MODEL || 'openai/gpt-4o-mini'; - const model = overrideModel || defaultModel; - - // Specific AI models - const overrideModelScorer = await settingsRepo.getSetting('modelScorer'); - const modelScorer = overrideModelScorer || model; - - const overrideModelTailoring = await settingsRepo.getSetting('modelTailoring'); - const modelTailoring = overrideModelTailoring || model; - - const overrideModelProjectSelection = await settingsRepo.getSetting('modelProjectSelection'); - const modelProjectSelection = overrideModelProjectSelection || model; - - const overridePipelineWebhookUrl = await settingsRepo.getSetting('pipelineWebhookUrl'); - const defaultPipelineWebhookUrl = process.env.PIPELINE_WEBHOOK_URL || process.env.WEBHOOK_URL || ''; - const pipelineWebhookUrl = overridePipelineWebhookUrl || defaultPipelineWebhookUrl; - - const overrideJobCompleteWebhookUrl = await settingsRepo.getSetting('jobCompleteWebhookUrl'); - const defaultJobCompleteWebhookUrl = process.env.JOB_COMPLETE_WEBHOOK_URL || ''; - const jobCompleteWebhookUrl = overrideJobCompleteWebhookUrl || defaultJobCompleteWebhookUrl; - - const profile = await getProfile(); - const { catalog } = extractProjectsFromProfile(profile); - const overrideResumeProjectsRaw = await settingsRepo.getSetting('resumeProjects'); - const resumeProjectsData = resolveResumeProjectsSettings({ catalog, overrideRaw: overrideResumeProjectsRaw }); - - const overrideUkvisajobsMaxJobsRaw = await settingsRepo.getSetting('ukvisajobsMaxJobs'); - const defaultUkvisajobsMaxJobs = 50; - const overrideUkvisajobsMaxJobs = overrideUkvisajobsMaxJobsRaw ? parseInt(overrideUkvisajobsMaxJobsRaw, 10) : null; - const ukvisajobsMaxJobs = overrideUkvisajobsMaxJobs ?? defaultUkvisajobsMaxJobs; - - const overrideGradcrackerMaxJobsPerTermRaw = await settingsRepo.getSetting('gradcrackerMaxJobsPerTerm'); - const defaultGradcrackerMaxJobsPerTerm = 50; - const overrideGradcrackerMaxJobsPerTerm = overrideGradcrackerMaxJobsPerTermRaw ? parseInt(overrideGradcrackerMaxJobsPerTermRaw, 10) : null; - const gradcrackerMaxJobsPerTerm = overrideGradcrackerMaxJobsPerTerm ?? defaultGradcrackerMaxJobsPerTerm; - - const overrideSearchTermsRaw = await settingsRepo.getSetting('searchTerms'); - const defaultSearchTermsEnv = process.env.JOBSPY_SEARCH_TERMS || 'web developer'; - const defaultSearchTerms = defaultSearchTermsEnv.split('|').map(s => s.trim()).filter(Boolean); - const overrideSearchTerms = overrideSearchTermsRaw ? JSON.parse(overrideSearchTermsRaw) as string[] : null; - const searchTerms = overrideSearchTerms ?? defaultSearchTerms; - - // JobSpy settings (GET) - const overrideJobspyLocation = await settingsRepo.getSetting('jobspyLocation'); - const defaultJobspyLocation = process.env.JOBSPY_LOCATION || 'UK'; - const jobspyLocation = overrideJobspyLocation || defaultJobspyLocation; - - const overrideJobspyResultsWantedRaw = await settingsRepo.getSetting('jobspyResultsWanted'); - const defaultJobspyResultsWanted = parseInt(process.env.JOBSPY_RESULTS_WANTED || '200', 10); - const overrideJobspyResultsWanted = overrideJobspyResultsWantedRaw ? parseInt(overrideJobspyResultsWantedRaw, 10) : null; - const jobspyResultsWanted = overrideJobspyResultsWanted ?? defaultJobspyResultsWanted; - - const overrideJobspyHoursOldRaw = await settingsRepo.getSetting('jobspyHoursOld'); - const defaultJobspyHoursOld = parseInt(process.env.JOBSPY_HOURS_OLD || '72', 10); - const overrideJobspyHoursOld = overrideJobspyHoursOldRaw ? parseInt(overrideJobspyHoursOldRaw, 10) : null; - const jobspyHoursOld = overrideJobspyHoursOld ?? defaultJobspyHoursOld; - - const overrideJobspyCountryIndeed = await settingsRepo.getSetting('jobspyCountryIndeed'); - const defaultJobspyCountryIndeed = process.env.JOBSPY_COUNTRY_INDEED || 'UK'; - const jobspyCountryIndeed = overrideJobspyCountryIndeed || defaultJobspyCountryIndeed; - - const overrideJobspySitesRaw = await settingsRepo.getSetting('jobspySites'); - const defaultJobspySites = (process.env.JOBSPY_SITES || 'indeed,linkedin').split(',').map(s => s.trim()).filter(Boolean); - const overrideJobspySites = overrideJobspySitesRaw ? JSON.parse(overrideJobspySitesRaw) as string[] : null; - const jobspySites = overrideJobspySites ?? defaultJobspySites; - - const overrideJobspyLinkedinFetchDescriptionRaw = await settingsRepo.getSetting('jobspyLinkedinFetchDescription'); - const defaultJobspyLinkedinFetchDescription = (process.env.JOBSPY_LINKEDIN_FETCH_DESCRIPTION || '1') === '1'; - const overrideJobspyLinkedinFetchDescription = overrideJobspyLinkedinFetchDescriptionRaw - ? overrideJobspyLinkedinFetchDescriptionRaw === 'true' || overrideJobspyLinkedinFetchDescriptionRaw === '1' - : null; - const jobspyLinkedinFetchDescription = overrideJobspyLinkedinFetchDescription ?? defaultJobspyLinkedinFetchDescription; - - // Show Sponsor Info setting (on by default) - const overrideShowSponsorInfoRaw = await settingsRepo.getSetting('showSponsorInfo'); - const defaultShowSponsorInfo = true; - const overrideShowSponsorInfo = overrideShowSponsorInfoRaw - ? overrideShowSponsorInfoRaw === 'true' || overrideShowSponsorInfoRaw === '1' - : null; - const showSponsorInfo = overrideShowSponsorInfo ?? defaultShowSponsorInfo; - - const envSettings = await getEnvSettingsData(); - - res.json({ - success: true, - data: { - model, - defaultModel, - overrideModel, - modelScorer, - overrideModelScorer, - modelTailoring, - overrideModelTailoring, - modelProjectSelection, - overrideModelProjectSelection, - pipelineWebhookUrl, - defaultPipelineWebhookUrl, - overridePipelineWebhookUrl, - jobCompleteWebhookUrl, - defaultJobCompleteWebhookUrl, - overrideJobCompleteWebhookUrl, - ...resumeProjectsData, - ukvisajobsMaxJobs, - defaultUkvisajobsMaxJobs, - overrideUkvisajobsMaxJobs, - gradcrackerMaxJobsPerTerm, - defaultGradcrackerMaxJobsPerTerm, - overrideGradcrackerMaxJobsPerTerm, - searchTerms, - defaultSearchTerms, - overrideSearchTerms, - jobspyLocation, - defaultJobspyLocation, - overrideJobspyLocation, - jobspyResultsWanted, - defaultJobspyResultsWanted, - overrideJobspyResultsWanted, - jobspyHoursOld, - defaultJobspyHoursOld, - overrideJobspyHoursOld, - jobspyCountryIndeed, - defaultJobspyCountryIndeed, - overrideJobspyCountryIndeed, - jobspySites, - defaultJobspySites, - overrideJobspySites, - jobspyLinkedinFetchDescription, - defaultJobspyLinkedinFetchDescription, - overrideJobspyLinkedinFetchDescription, - showSponsorInfo, - defaultShowSponsorInfo, - overrideShowSponsorInfo, - ...envSettings, - }, - }); + const data = await getEffectiveSettings(); + res.json({ success: true, data }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ success: false, error: message }); @@ -312,146 +177,10 @@ settingsRouter.patch('/', async (req: Request, res: Response) => { applyEnvValue('WEBHOOK_SECRET', value); } - const overrideModel = await settingsRepo.getSetting('model'); - const defaultModel = process.env.MODEL || 'openai/gpt-4o-mini'; - const model = overrideModel || defaultModel; - - const overrideModelScorer = await settingsRepo.getSetting('modelScorer'); - const modelScorer = overrideModelScorer || model; - - const overrideModelTailoring = await settingsRepo.getSetting('modelTailoring'); - const modelTailoring = overrideModelTailoring || model; - - const overrideModelProjectSelection = await settingsRepo.getSetting('modelProjectSelection'); - const modelProjectSelection = overrideModelProjectSelection || model; - - const overridePipelineWebhookUrl = await settingsRepo.getSetting('pipelineWebhookUrl'); - const defaultPipelineWebhookUrl = process.env.PIPELINE_WEBHOOK_URL || process.env.WEBHOOK_URL || ''; - const pipelineWebhookUrl = overridePipelineWebhookUrl || defaultPipelineWebhookUrl; - - const overrideJobCompleteWebhookUrl = await settingsRepo.getSetting('jobCompleteWebhookUrl'); - const defaultJobCompleteWebhookUrl = process.env.JOB_COMPLETE_WEBHOOK_URL || ''; - const jobCompleteWebhookUrl = overrideJobCompleteWebhookUrl || defaultJobCompleteWebhookUrl; - - const profile = await getProfile(); - const { catalog } = extractProjectsFromProfile(profile); - const overrideResumeProjectsRaw = await settingsRepo.getSetting('resumeProjects'); - const resumeProjectsData = resolveResumeProjectsSettings({ catalog, overrideRaw: overrideResumeProjectsRaw }); - - const overrideUkvisajobsMaxJobsRaw = await settingsRepo.getSetting('ukvisajobsMaxJobs'); - const defaultUkvisajobsMaxJobs = 50; - const overrideUkvisajobsMaxJobs = overrideUkvisajobsMaxJobsRaw ? parseInt(overrideUkvisajobsMaxJobsRaw, 10) : null; - const ukvisajobsMaxJobs = overrideUkvisajobsMaxJobs ?? defaultUkvisajobsMaxJobs; - - const overrideGradcrackerMaxJobsPerTermRaw = await settingsRepo.getSetting('gradcrackerMaxJobsPerTerm'); - const defaultGradcrackerMaxJobsPerTerm = 50; - const overrideGradcrackerMaxJobsPerTerm = overrideGradcrackerMaxJobsPerTermRaw ? parseInt(overrideGradcrackerMaxJobsPerTermRaw, 10) : null; - const gradcrackerMaxJobsPerTerm = overrideGradcrackerMaxJobsPerTerm ?? defaultGradcrackerMaxJobsPerTerm; - - // Search terms - stored as JSON array, default from env var (pipe-separated) - const overrideSearchTermsRaw = await settingsRepo.getSetting('searchTerms'); - const defaultSearchTermsEnv = process.env.JOBSPY_SEARCH_TERMS || 'web developer'; - const defaultSearchTerms = defaultSearchTermsEnv.split('|').map(s => s.trim()).filter(Boolean); - const overrideSearchTerms = overrideSearchTermsRaw ? JSON.parse(overrideSearchTermsRaw) as string[] : null; - const searchTerms = overrideSearchTerms ?? defaultSearchTerms; - - // JobSpy settings (re-fetch to update response) - const overrideJobspyLocation = await settingsRepo.getSetting('jobspyLocation'); - const defaultJobspyLocation = process.env.JOBSPY_LOCATION || 'UK'; - const jobspyLocation = overrideJobspyLocation || defaultJobspyLocation; - - const overrideJobspyResultsWantedRaw = await settingsRepo.getSetting('jobspyResultsWanted'); - const defaultJobspyResultsWanted = parseInt(process.env.JOBSPY_RESULTS_WANTED || '200', 10); - const overrideJobspyResultsWanted = overrideJobspyResultsWantedRaw ? parseInt(overrideJobspyResultsWantedRaw, 10) : null; - const jobspyResultsWanted = overrideJobspyResultsWanted ?? defaultJobspyResultsWanted; - - const overrideJobspyHoursOldRaw = await settingsRepo.getSetting('jobspyHoursOld'); - const defaultJobspyHoursOld = parseInt(process.env.JOBSPY_HOURS_OLD || '72', 10); - const overrideJobspyHoursOld = overrideJobspyHoursOldRaw ? parseInt(overrideJobspyHoursOldRaw, 10) : null; - const jobspyHoursOld = overrideJobspyHoursOld ?? defaultJobspyHoursOld; - - const overrideJobspyCountryIndeed = await settingsRepo.getSetting('jobspyCountryIndeed'); - const defaultJobspyCountryIndeed = process.env.JOBSPY_COUNTRY_INDEED || 'UK'; - const jobspyCountryIndeed = overrideJobspyCountryIndeed || defaultJobspyCountryIndeed; - - const overrideJobspySitesRaw = await settingsRepo.getSetting('jobspySites'); - const defaultJobspySites = (process.env.JOBSPY_SITES || 'indeed,linkedin').split(',').map(s => s.trim()).filter(Boolean); - const overrideJobspySites = overrideJobspySitesRaw ? JSON.parse(overrideJobspySitesRaw) as string[] : null; - const jobspySites = overrideJobspySites ?? defaultJobspySites; - - const overrideJobspyLinkedinFetchDescriptionRaw = await settingsRepo.getSetting('jobspyLinkedinFetchDescription'); - const defaultJobspyLinkedinFetchDescription = (process.env.JOBSPY_LINKEDIN_FETCH_DESCRIPTION || '1') === '1'; - const overrideJobspyLinkedinFetchDescription = overrideJobspyLinkedinFetchDescriptionRaw - ? overrideJobspyLinkedinFetchDescriptionRaw === 'true' || overrideJobspyLinkedinFetchDescriptionRaw === '1' - : null; - const jobspyLinkedinFetchDescription = overrideJobspyLinkedinFetchDescription ?? defaultJobspyLinkedinFetchDescription; - - // Show Sponsor Info setting - const overrideShowSponsorInfoRaw = await settingsRepo.getSetting('showSponsorInfo'); - const defaultShowSponsorInfo = true; - const overrideShowSponsorInfo = overrideShowSponsorInfoRaw - ? overrideShowSponsorInfoRaw === 'true' || overrideShowSponsorInfoRaw === '1' - : null; - const showSponsorInfo = overrideShowSponsorInfo ?? defaultShowSponsorInfo; - - const envSettings = await getEnvSettingsData(); - - res.json({ - success: true, - data: { - model, - defaultModel, - overrideModel, - modelScorer, - overrideModelScorer, - modelTailoring, - overrideModelTailoring, - modelProjectSelection, - overrideModelProjectSelection, - pipelineWebhookUrl, - defaultPipelineWebhookUrl, - overridePipelineWebhookUrl, - jobCompleteWebhookUrl, - defaultJobCompleteWebhookUrl, - overrideJobCompleteWebhookUrl, - ...resumeProjectsData, - ukvisajobsMaxJobs, - defaultUkvisajobsMaxJobs, - overrideUkvisajobsMaxJobs, - gradcrackerMaxJobsPerTerm, - defaultGradcrackerMaxJobsPerTerm, - overrideGradcrackerMaxJobsPerTerm, - searchTerms, - defaultSearchTerms, - overrideSearchTerms, - jobspyLocation, - defaultJobspyLocation, - overrideJobspyLocation, - jobspyResultsWanted, - defaultJobspyResultsWanted, - overrideJobspyResultsWanted, - jobspyHoursOld, - defaultJobspyHoursOld, - overrideJobspyHoursOld, - jobspyCountryIndeed, - defaultJobspyCountryIndeed, - overrideJobspyCountryIndeed, - jobspySites, - defaultJobspySites, - overrideJobspySites, - jobspyLinkedinFetchDescription, - defaultJobspyLinkedinFetchDescription, - overrideJobspyLinkedinFetchDescription, - showSponsorInfo, - defaultShowSponsorInfo, - overrideShowSponsorInfo, - ...envSettings, - }, - }); + const data = await getEffectiveSettings(); + res.json({ success: true, data }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; - // PATCH usually returns 500 for unknown, but let's stick to what was there (400?) - // Wait, the file said 400? Let's verify line 608. res.status(400).json({ success: false, error: message }); } }); diff --git a/orchestrator/src/server/repositories/settings.ts b/orchestrator/src/server/repositories/settings.ts index 310c144..6f98678 100644 --- a/orchestrator/src/server/repositories/settings.ts +++ b/orchestrator/src/server/repositories/settings.ts @@ -38,6 +38,14 @@ export async function getSetting(key: SettingKey): Promise { return row?.value ?? null } +export async function getAllSettings(): Promise>> { + const rows = await db.select().from(settings) + return rows.reduce((acc, row) => { + acc[row.key as SettingKey] = row.value + return acc + }, {} as Partial>) +} + export async function setSetting(key: SettingKey, value: string | null): Promise { const now = new Date().toISOString() diff --git a/orchestrator/src/server/services/envSettings.ts b/orchestrator/src/server/services/envSettings.ts index 45eb77d..85eb338 100644 --- a/orchestrator/src/server/services/envSettings.ts +++ b/orchestrator/src/server/services/envSettings.ts @@ -70,30 +70,37 @@ export async function applyStoredEnvOverrides(): Promise { ]); } -export async function getEnvSettingsData(): Promise> { +export async function getEnvSettingsData( + overrides?: Partial> +): Promise> { const readableValues: Record = {}; const privateValues: Record = {}; for (const { settingKey, envKey } of readableStringConfig) { - const override = await settingsRepo.getSetting(settingKey); + const override = overrides ? (overrides[settingKey] ?? null) : await settingsRepo.getSetting(settingKey); const rawValue = override ?? process.env[envKey]; readableValues[settingKey] = normalizeEnvInput(rawValue); } for (const { settingKey, envKey, defaultValue } of readableBooleanConfig) { - const override = await settingsRepo.getSetting(settingKey); + const override = overrides ? (overrides[settingKey] ?? null) : await settingsRepo.getSetting(settingKey); const rawValue = override ?? process.env[envKey]; readableValues[settingKey] = parseEnvBoolean(rawValue, defaultValue); } for (const { settingKey, envKey, hintKey } of privateStringConfig) { - const override = await settingsRepo.getSetting(settingKey); + const override = overrides ? (overrides[settingKey] ?? null) : await settingsRepo.getSetting(settingKey); const rawValue = override ?? process.env[envKey]; privateValues[hintKey] = rawValue ? rawValue.slice(0, 4) : null; } - const basicAuthUser = (await settingsRepo.getSetting('basicAuthUser')) ?? process.env.BASIC_AUTH_USER; - const basicAuthPassword = (await settingsRepo.getSetting('basicAuthPassword')) ?? process.env.BASIC_AUTH_PASSWORD; + 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; return { ...readableValues, diff --git a/orchestrator/src/server/services/settings.ts b/orchestrator/src/server/services/settings.ts new file mode 100644 index 0000000..fd82c7e --- /dev/null +++ b/orchestrator/src/server/services/settings.ts @@ -0,0 +1,146 @@ +import { AppSettings } from '@shared/types.js'; +import * as settingsRepo from '@server/repositories/settings.js'; +import { getEnvSettingsData } from './envSettings.js'; +import { extractProjectsFromProfile, resolveResumeProjectsSettings } from './resumeProjects.js'; +import { getProfile } from './profile.js'; + +/** + * Get the effective app settings, combining environment variables and database overrides. + */ +export async function getEffectiveSettings(): Promise { + // Parallelize slow operations + const [overrides, profile] = await Promise.all([ + settingsRepo.getAllSettings(), + getProfile(), + ]); + + const envSettings = await getEnvSettingsData(overrides); + + const defaultModel = process.env.MODEL || 'openai/gpt-4o-mini'; + const overrideModel = overrides.model ?? null; + const model = overrideModel || defaultModel; + + const overrideModelScorer = overrides.modelScorer ?? null; + const modelScorer = overrideModelScorer || model; + + const overrideModelTailoring = overrides.modelTailoring ?? null; + const modelTailoring = overrideModelTailoring || model; + + const overrideModelProjectSelection = overrides.modelProjectSelection ?? null; + const modelProjectSelection = overrideModelProjectSelection || model; + + const defaultPipelineWebhookUrl = process.env.PIPELINE_WEBHOOK_URL || process.env.WEBHOOK_URL || ''; + const overridePipelineWebhookUrl = overrides.pipelineWebhookUrl ?? null; + const pipelineWebhookUrl = overridePipelineWebhookUrl || defaultPipelineWebhookUrl; + + const defaultJobCompleteWebhookUrl = process.env.JOB_COMPLETE_WEBHOOK_URL || ''; + const overrideJobCompleteWebhookUrl = overrides.jobCompleteWebhookUrl ?? null; + const jobCompleteWebhookUrl = overrideJobCompleteWebhookUrl || defaultJobCompleteWebhookUrl; + + const { catalog } = extractProjectsFromProfile(profile); + const overrideResumeProjectsRaw = overrides.resumeProjects ?? null; + const resumeProjectsData = resolveResumeProjectsSettings({ catalog, overrideRaw: overrideResumeProjectsRaw }); + + const defaultUkvisajobsMaxJobs = 50; + const overrideUkvisajobsMaxJobsRaw = overrides.ukvisajobsMaxJobs; + const overrideUkvisajobsMaxJobs = overrideUkvisajobsMaxJobsRaw ? parseInt(overrideUkvisajobsMaxJobsRaw, 10) : null; + const ukvisajobsMaxJobs = overrideUkvisajobsMaxJobs ?? defaultUkvisajobsMaxJobs; + + const defaultGradcrackerMaxJobsPerTerm = 50; + const overrideGradcrackerMaxJobsPerTermRaw = overrides.gradcrackerMaxJobsPerTerm; + const overrideGradcrackerMaxJobsPerTerm = overrideGradcrackerMaxJobsPerTermRaw ? parseInt(overrideGradcrackerMaxJobsPerTermRaw, 10) : null; + const gradcrackerMaxJobsPerTerm = overrideGradcrackerMaxJobsPerTerm ?? defaultGradcrackerMaxJobsPerTerm; + + const defaultSearchTermsEnv = process.env.JOBSPY_SEARCH_TERMS || 'web developer'; + const defaultSearchTerms = defaultSearchTermsEnv.split('|').map(s => s.trim()).filter(Boolean); + const overrideSearchTermsRaw = overrides.searchTerms; + const overrideSearchTerms = overrideSearchTermsRaw ? JSON.parse(overrideSearchTermsRaw) as string[] : null; + const searchTerms = overrideSearchTerms ?? defaultSearchTerms; + + const defaultJobspyLocation = process.env.JOBSPY_LOCATION || 'UK'; + const overrideJobspyLocation = overrides.jobspyLocation ?? null; + const jobspyLocation = overrideJobspyLocation || defaultJobspyLocation; + + const defaultJobspyResultsWanted = parseInt(process.env.JOBSPY_RESULTS_WANTED || '200', 10); + const overrideJobspyResultsWantedRaw = overrides.jobspyResultsWanted; + const overrideJobspyResultsWanted = overrideJobspyResultsWantedRaw ? parseInt(overrideJobspyResultsWantedRaw, 10) : null; + const jobspyResultsWanted = overrideJobspyResultsWanted ?? defaultJobspyResultsWanted; + + const defaultJobspyHoursOld = parseInt(process.env.JOBSPY_HOURS_OLD || '72', 10); + const overrideJobspyHoursOldRaw = overrides.jobspyHoursOld; + const overrideJobspyHoursOld = overrideJobspyHoursOldRaw ? parseInt(overrideJobspyHoursOldRaw, 10) : null; + const jobspyHoursOld = overrideJobspyHoursOld ?? defaultJobspyHoursOld; + + const defaultJobspyCountryIndeed = process.env.JOBSPY_COUNTRY_INDEED || 'UK'; + const overrideJobspyCountryIndeed = overrides.jobspyCountryIndeed ?? null; + const jobspyCountryIndeed = overrideJobspyCountryIndeed || defaultJobspyCountryIndeed; + + const defaultJobspySites = (process.env.JOBSPY_SITES || 'indeed,linkedin').split(',').map(s => s.trim()).filter(Boolean); + const overrideJobspySitesRaw = overrides.jobspySites; + const overrideJobspySites = overrideJobspySitesRaw ? JSON.parse(overrideJobspySitesRaw) as string[] : null; + const jobspySites = overrideJobspySites ?? defaultJobspySites; + + const defaultJobspyLinkedinFetchDescription = (process.env.JOBSPY_LINKEDIN_FETCH_DESCRIPTION || '1') === '1'; + const overrideJobspyLinkedinFetchDescriptionRaw = overrides.jobspyLinkedinFetchDescription; + const overrideJobspyLinkedinFetchDescription = overrideJobspyLinkedinFetchDescriptionRaw + ? overrideJobspyLinkedinFetchDescriptionRaw === 'true' || overrideJobspyLinkedinFetchDescriptionRaw === '1' + : null; + const jobspyLinkedinFetchDescription = overrideJobspyLinkedinFetchDescription ?? defaultJobspyLinkedinFetchDescription; + + const defaultShowSponsorInfo = true; + const overrideShowSponsorInfoRaw = overrides.showSponsorInfo; + const overrideShowSponsorInfo = overrideShowSponsorInfoRaw + ? overrideShowSponsorInfoRaw === 'true' || overrideShowSponsorInfoRaw === '1' + : null; + const showSponsorInfo = overrideShowSponsorInfo ?? defaultShowSponsorInfo; + + return { + model, + defaultModel, + overrideModel, + modelScorer, + overrideModelScorer, + modelTailoring, + overrideModelTailoring, + modelProjectSelection, + overrideModelProjectSelection, + pipelineWebhookUrl, + defaultPipelineWebhookUrl, + overridePipelineWebhookUrl, + jobCompleteWebhookUrl, + defaultJobCompleteWebhookUrl, + overrideJobCompleteWebhookUrl, + ...resumeProjectsData, + ukvisajobsMaxJobs, + defaultUkvisajobsMaxJobs, + overrideUkvisajobsMaxJobs, + gradcrackerMaxJobsPerTerm, + defaultGradcrackerMaxJobsPerTerm, + overrideGradcrackerMaxJobsPerTerm, + searchTerms, + defaultSearchTerms, + overrideSearchTerms, + jobspyLocation, + defaultJobspyLocation, + overrideJobspyLocation, + jobspyResultsWanted, + defaultJobspyResultsWanted, + overrideJobspyResultsWanted, + jobspyHoursOld, + defaultJobspyHoursOld, + overrideJobspyHoursOld, + jobspyCountryIndeed, + defaultJobspyCountryIndeed, + overrideJobspyCountryIndeed, + jobspySites, + defaultJobspySites, + overrideJobspySites, + jobspyLinkedinFetchDescription, + defaultJobspyLinkedinFetchDescription, + overrideJobspyLinkedinFetchDescription, + showSponsorInfo, + defaultShowSponsorInfo, + overrideShowSponsorInfo, + ...envSettings, + } as AppSettings; +}