423 lines
20 KiB
TypeScript
423 lines
20 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import { z } from 'zod';
|
|
import * as settingsRepo from '../../repositories/settings.js';
|
|
import {
|
|
extractProjectsFromProfile,
|
|
normalizeResumeProjectsSettings,
|
|
resolveResumeProjectsSettings,
|
|
} from '../../services/resumeProjects.js';
|
|
import { getProfile } from '../../services/profile.js';
|
|
|
|
export const settingsRouter = Router();
|
|
|
|
/**
|
|
* GET /api/settings - Get app settings (effective + defaults)
|
|
*/
|
|
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;
|
|
|
|
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,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
const updateSettingsSchema = z.object({
|
|
model: z.string().trim().min(1).max(200).nullable().optional(),
|
|
modelScorer: z.string().trim().min(1).max(200).nullable().optional(),
|
|
modelTailoring: z.string().trim().min(1).max(200).nullable().optional(),
|
|
modelProjectSelection: z.string().trim().min(1).max(200).nullable().optional(),
|
|
pipelineWebhookUrl: z.string().trim().min(1).max(2000).nullable().optional(),
|
|
jobCompleteWebhookUrl: z.string().trim().min(1).max(2000).nullable().optional(),
|
|
resumeProjects: z.object({
|
|
maxProjects: z.number().int().min(0).max(50),
|
|
lockedProjectIds: z.array(z.string().trim().min(1)).max(200),
|
|
aiSelectableProjectIds: z.array(z.string().trim().min(1)).max(200),
|
|
}).nullable().optional(),
|
|
ukvisajobsMaxJobs: z.number().int().min(1).max(200).nullable().optional(),
|
|
gradcrackerMaxJobsPerTerm: z.number().int().min(1).max(200).nullable().optional(),
|
|
searchTerms: z.array(z.string().trim().min(1).max(200)).max(50).nullable().optional(),
|
|
jobspyLocation: z.string().trim().min(1).max(100).nullable().optional(),
|
|
jobspyResultsWanted: z.number().int().min(1).max(500).nullable().optional(),
|
|
jobspyHoursOld: z.number().int().min(1).max(168).nullable().optional(),
|
|
jobspyCountryIndeed: z.string().trim().min(1).max(100).nullable().optional(),
|
|
jobspySites: z.array(z.string().trim().min(1).max(50)).max(10).nullable().optional(),
|
|
jobspyLinkedinFetchDescription: z.boolean().nullable().optional(),
|
|
showSponsorInfo: z.boolean().nullable().optional(),
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/settings - Update settings overrides
|
|
*/
|
|
settingsRouter.patch('/', async (req: Request, res: Response) => {
|
|
try {
|
|
const input = updateSettingsSchema.parse(req.body);
|
|
|
|
if ('model' in input) {
|
|
const model = input.model ?? null;
|
|
await settingsRepo.setSetting('model', model);
|
|
}
|
|
|
|
if ('modelScorer' in input) {
|
|
await settingsRepo.setSetting('modelScorer', input.modelScorer ?? null);
|
|
}
|
|
if ('modelTailoring' in input) {
|
|
await settingsRepo.setSetting('modelTailoring', input.modelTailoring ?? null);
|
|
}
|
|
if ('modelProjectSelection' in input) {
|
|
await settingsRepo.setSetting('modelProjectSelection', input.modelProjectSelection ?? null);
|
|
}
|
|
|
|
if ('pipelineWebhookUrl' in input) {
|
|
const pipelineWebhookUrl = input.pipelineWebhookUrl ?? null;
|
|
await settingsRepo.setSetting('pipelineWebhookUrl', pipelineWebhookUrl);
|
|
}
|
|
|
|
if ('jobCompleteWebhookUrl' in input) {
|
|
const webhookUrl = input.jobCompleteWebhookUrl ?? null;
|
|
await settingsRepo.setSetting('jobCompleteWebhookUrl', webhookUrl);
|
|
}
|
|
|
|
if ('resumeProjects' in input) {
|
|
const resumeProjects = input.resumeProjects ?? null;
|
|
|
|
if (resumeProjects === null) {
|
|
await settingsRepo.setSetting('resumeProjects', null);
|
|
} else {
|
|
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');
|
|
}
|
|
|
|
const profile = rawProfile as Record<string, unknown>;
|
|
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);
|
|
}
|
|
|
|
if ('gradcrackerMaxJobsPerTerm' in input) {
|
|
const gradcrackerMaxJobsPerTerm = input.gradcrackerMaxJobsPerTerm ?? null;
|
|
await settingsRepo.setSetting('gradcrackerMaxJobsPerTerm', gradcrackerMaxJobsPerTerm !== null ? String(gradcrackerMaxJobsPerTerm) : null);
|
|
}
|
|
|
|
if ('searchTerms' in input) {
|
|
const searchTerms = input.searchTerms ?? null;
|
|
await settingsRepo.setSetting('searchTerms', searchTerms !== null ? JSON.stringify(searchTerms) : null);
|
|
}
|
|
|
|
if ('jobspyLocation' in input) {
|
|
const value = input.jobspyLocation ?? null;
|
|
await settingsRepo.setSetting('jobspyLocation', value);
|
|
}
|
|
|
|
if ('jobspyResultsWanted' in input) {
|
|
const value = input.jobspyResultsWanted ?? null;
|
|
await settingsRepo.setSetting('jobspyResultsWanted', value !== null ? String(value) : null);
|
|
}
|
|
|
|
if ('jobspyHoursOld' in input) {
|
|
const value = input.jobspyHoursOld ?? null;
|
|
await settingsRepo.setSetting('jobspyHoursOld', value !== null ? String(value) : null);
|
|
}
|
|
|
|
if ('jobspyCountryIndeed' in input) {
|
|
const value = input.jobspyCountryIndeed ?? null;
|
|
await settingsRepo.setSetting('jobspyCountryIndeed', value);
|
|
}
|
|
|
|
if ('jobspySites' in input) {
|
|
const value = input.jobspySites ?? null;
|
|
await settingsRepo.setSetting('jobspySites', value !== null ? JSON.stringify(value) : null);
|
|
}
|
|
|
|
if ('jobspyLinkedinFetchDescription' in input) {
|
|
const value = input.jobspyLinkedinFetchDescription ?? null;
|
|
await settingsRepo.setSetting('jobspyLinkedinFetchDescription', value !== null ? (value ? '1' : '0') : null);
|
|
}
|
|
|
|
if ('showSponsorInfo' in input) {
|
|
const value = input.showSponsorInfo ?? null;
|
|
await settingsRepo.setSetting('showSponsorInfo', value !== null ? (value ? '1' : '0') : null);
|
|
}
|
|
|
|
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;
|
|
|
|
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,
|
|
},
|
|
});
|
|
} 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 });
|
|
}
|
|
});
|