This commit is contained in:
DaKheera47 2026-01-22 16:12:18 +00:00
parent 6913607eef
commit 0844071b7a
5 changed files with 174 additions and 283 deletions

View File

@ -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) => {

View File

@ -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 });
}
});

View File

@ -38,6 +38,14 @@ export async function getSetting(key: SettingKey): Promise<string | null> {
return row?.value ?? null
}
export async function getAllSettings(): Promise<Partial<Record<SettingKey, string>>> {
const rows = await db.select().from(settings)
return rows.reduce((acc, row) => {
acc[row.key as SettingKey] = row.value
return acc
}, {} as Partial<Record<SettingKey, string>>)
}
export async function setSetting(key: SettingKey, value: string | null): Promise<void> {
const now = new Date().toISOString()

View File

@ -70,30 +70,37 @@ export async function applyStoredEnvOverrides(): Promise<void> {
]);
}
export async function getEnvSettingsData(): Promise<Record<string, string | boolean | number | null>> {
export async function getEnvSettingsData(
overrides?: Partial<Record<SettingKey, string>>
): Promise<Record<string, string | boolean | number | null>> {
const readableValues: Record<string, string | boolean | null> = {};
const privateValues: Record<string, string | null> = {};
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,

View File

@ -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<AppSettings> {
// 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;
}