From 45662c386acec9dc4a3192eb9dc87f3e24ee66d9 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Sat, 27 Dec 2025 11:28:23 +0000 Subject: [PATCH] jobspy specific settings --- orchestrator/src/client/api/client.ts | 5 + .../src/client/pages/SettingsPage.tsx | 191 +++++++++++++++++- orchestrator/src/server/api/routes.ts | 113 ++++++++++- .../src/server/pipeline/orchestrator.ts | 11 + .../src/server/repositories/settings.ts | 5 + orchestrator/src/shared/types.ts | 15 ++ 6 files changed, 338 insertions(+), 2 deletions(-) diff --git a/orchestrator/src/client/api/client.ts b/orchestrator/src/client/api/client.ts index a98c74c..ffbb459 100644 --- a/orchestrator/src/client/api/client.ts +++ b/orchestrator/src/client/api/client.ts @@ -107,6 +107,11 @@ export async function updateSettings(update: { resumeProjects?: ResumeProjectsSettings | null ukvisajobsMaxJobs?: number | null searchTerms?: string[] | null + jobspyLocation?: string | null + jobspyResultsWanted?: number | null + jobspyHoursOld?: number | null + jobspyCountryIndeed?: string | null + jobspyLinkedinFetchDescription?: boolean | null }): Promise { return fetchApi('/settings', { method: 'PATCH', diff --git a/orchestrator/src/client/pages/SettingsPage.tsx b/orchestrator/src/client/pages/SettingsPage.tsx index 7eb207d..3981819 100644 --- a/orchestrator/src/client/pages/SettingsPage.tsx +++ b/orchestrator/src/client/pages/SettingsPage.tsx @@ -44,6 +44,11 @@ export const SettingsPage: React.FC = () => { const [resumeProjectsDraft, setResumeProjectsDraft] = useState(null) const [ukvisajobsMaxJobsDraft, setUkvisajobsMaxJobsDraft] = useState(null) const [searchTermsDraft, setSearchTermsDraft] = useState(null) + const [jobspyLocationDraft, setJobspyLocationDraft] = useState(null) + const [jobspyResultsWantedDraft, setJobspyResultsWantedDraft] = useState(null) + const [jobspyHoursOldDraft, setJobspyHoursOldDraft] = useState(null) + const [jobspyCountryIndeedDraft, setJobspyCountryIndeedDraft] = useState(null) + const [jobspyLinkedinFetchDescriptionDraft, setJobspyLinkedinFetchDescriptionDraft] = useState(null) const [isSaving, setIsSaving] = useState(false) const [isLoading, setIsLoading] = useState(true) @@ -61,6 +66,11 @@ export const SettingsPage: React.FC = () => { setResumeProjectsDraft(data.resumeProjects) setUkvisajobsMaxJobsDraft(data.overrideUkvisajobsMaxJobs) setSearchTermsDraft(data.overrideSearchTerms) + setJobspyLocationDraft(data.overrideJobspyLocation) + setJobspyResultsWantedDraft(data.overrideJobspyResultsWanted) + setJobspyHoursOldDraft(data.overrideJobspyHoursOld) + setJobspyCountryIndeedDraft(data.overrideJobspyCountryIndeed) + setJobspyLinkedinFetchDescriptionDraft(data.overrideJobspyLinkedinFetchDescription) }) .catch((error) => { const message = error instanceof Error ? error.message : "Failed to load settings" @@ -91,6 +101,21 @@ export const SettingsPage: React.FC = () => { const effectiveSearchTerms = settings?.searchTerms ?? [] const defaultSearchTerms = settings?.defaultSearchTerms ?? [] const overrideSearchTerms = settings?.overrideSearchTerms + const effectiveJobspyLocation = settings?.jobspyLocation ?? "" + const defaultJobspyLocation = settings?.defaultJobspyLocation ?? "" + const overrideJobspyLocation = settings?.overrideJobspyLocation + const effectiveJobspyResultsWanted = settings?.jobspyResultsWanted ?? 200 + const defaultJobspyResultsWanted = settings?.defaultJobspyResultsWanted ?? 200 + const overrideJobspyResultsWanted = settings?.overrideJobspyResultsWanted + const effectiveJobspyHoursOld = settings?.jobspyHoursOld ?? 72 + const defaultJobspyHoursOld = settings?.defaultJobspyHoursOld ?? 72 + const overrideJobspyHoursOld = settings?.overrideJobspyHoursOld + const effectiveJobspyCountryIndeed = settings?.jobspyCountryIndeed ?? "" + const defaultJobspyCountryIndeed = settings?.defaultJobspyCountryIndeed ?? "" + const overrideJobspyCountryIndeed = settings?.overrideJobspyCountryIndeed + const effectiveJobspyLinkedinFetchDescription = settings?.jobspyLinkedinFetchDescription ?? true + const defaultJobspyLinkedinFetchDescription = settings?.defaultJobspyLinkedinFetchDescription ?? true + const overrideJobspyLinkedinFetchDescription = settings?.overrideJobspyLinkedinFetchDescription const profileProjects = settings?.profileProjects ?? [] const maxProjectsTotal = profileProjects.length const lockedCount = resumeProjectsDraft?.lockedProjectIds.length ?? 0 @@ -111,7 +136,12 @@ export const SettingsPage: React.FC = () => { nextJobCompleteWebhook !== currentJobCompleteWebhook || !resumeProjectsEqual(resumeProjectsDraft, settings.resumeProjects) || ukvisajobsChanged || - searchTermsChanged + searchTermsChanged || + jobspyLocationDraft !== (overrideJobspyLocation ?? null) || + jobspyResultsWantedDraft !== (overrideJobspyResultsWanted ?? null) || + jobspyHoursOldDraft !== (overrideJobspyHoursOld ?? null) || + jobspyCountryIndeedDraft !== (overrideJobspyCountryIndeed ?? null) || + jobspyLinkedinFetchDescriptionDraft !== (overrideJobspyLinkedinFetchDescription ?? null) ) }, [ settings, @@ -126,6 +156,16 @@ export const SettingsPage: React.FC = () => { overrideUkvisajobsMaxJobs, searchTermsDraft, overrideSearchTerms, + jobspyLocationDraft, + jobspyResultsWantedDraft, + jobspyHoursOldDraft, + jobspyCountryIndeedDraft, + jobspyLinkedinFetchDescriptionDraft, + overrideJobspyLocation, + overrideJobspyResultsWanted, + overrideJobspyHoursOld, + overrideJobspyCountryIndeed, + overrideJobspyLinkedinFetchDescription, ]) const handleSave = async () => { @@ -140,6 +180,11 @@ export const SettingsPage: React.FC = () => { : resumeProjectsDraft const ukvisajobsMaxJobsOverride = ukvisajobsMaxJobsDraft === defaultUkvisajobsMaxJobs ? null : ukvisajobsMaxJobsDraft const searchTermsOverride = arraysEqual(searchTermsDraft ?? [], defaultSearchTerms) ? null : searchTermsDraft + const jobspyLocationOverride = jobspyLocationDraft === defaultJobspyLocation ? null : jobspyLocationDraft + const jobspyResultsWantedOverride = jobspyResultsWantedDraft === defaultJobspyResultsWanted ? null : jobspyResultsWantedDraft + const jobspyHoursOldOverride = jobspyHoursOldDraft === defaultJobspyHoursOld ? null : jobspyHoursOldDraft + const jobspyCountryIndeedOverride = jobspyCountryIndeedDraft === defaultJobspyCountryIndeed ? null : jobspyCountryIndeedDraft + const jobspyLinkedinFetchDescriptionOverride = jobspyLinkedinFetchDescriptionDraft === defaultJobspyLinkedinFetchDescription ? null : jobspyLinkedinFetchDescriptionDraft const updated = await api.updateSettings({ model: trimmed.length > 0 ? trimmed : null, pipelineWebhookUrl: webhookTrimmed.length > 0 ? webhookTrimmed : null, @@ -147,6 +192,11 @@ export const SettingsPage: React.FC = () => { resumeProjects: resumeProjectsOverride, ukvisajobsMaxJobs: ukvisajobsMaxJobsOverride, searchTerms: searchTermsOverride, + jobspyLocation: jobspyLocationOverride, + jobspyResultsWanted: jobspyResultsWantedOverride, + jobspyHoursOld: jobspyHoursOldOverride, + jobspyCountryIndeed: jobspyCountryIndeedOverride, + jobspyLinkedinFetchDescription: jobspyLinkedinFetchDescriptionOverride, }) setSettings(updated) setModelDraft(updated.overrideModel ?? "") @@ -155,6 +205,11 @@ export const SettingsPage: React.FC = () => { setResumeProjectsDraft(updated.resumeProjects) setUkvisajobsMaxJobsDraft(updated.overrideUkvisajobsMaxJobs) setSearchTermsDraft(updated.overrideSearchTerms) + setJobspyLocationDraft(updated.overrideJobspyLocation) + setJobspyResultsWantedDraft(updated.overrideJobspyResultsWanted) + setJobspyHoursOldDraft(updated.overrideJobspyHoursOld) + setJobspyCountryIndeedDraft(updated.overrideJobspyCountryIndeed) + setJobspyLinkedinFetchDescriptionDraft(updated.overrideJobspyLinkedinFetchDescription) toast.success("Settings saved") } catch (error) { const message = error instanceof Error ? error.message : "Failed to save settings" @@ -174,6 +229,11 @@ export const SettingsPage: React.FC = () => { resumeProjects: null, ukvisajobsMaxJobs: null, searchTerms: null, + jobspyLocation: null, + jobspyResultsWanted: null, + jobspyHoursOld: null, + jobspyCountryIndeed: null, + jobspyLinkedinFetchDescription: null, }) setSettings(updated) setModelDraft("") @@ -182,6 +242,11 @@ export const SettingsPage: React.FC = () => { setResumeProjectsDraft(updated.resumeProjects) setUkvisajobsMaxJobsDraft(null) setSearchTermsDraft(null) + setJobspyLocationDraft(null) + setJobspyResultsWantedDraft(null) + setJobspyHoursOldDraft(null) + setJobspyCountryIndeedDraft(null) + setJobspyLinkedinFetchDescriptionDraft(null) toast.success("Reset to default") } catch (error) { const message = error instanceof Error ? error.message : "Failed to reset settings" @@ -390,6 +455,130 @@ export const SettingsPage: React.FC = () => { + + + JobSpy Scraper + + + +
+
+
Location
+ setJobspyLocationDraft(event.target.value)} + placeholder={defaultJobspyLocation || "UK"} + disabled={isLoading || isSaving} + /> +
+ Location to search for jobs (e.g. "UK", "London", "Remote"). +
+
+ Effective: {effectiveJobspyLocation || "—"} + Default: {defaultJobspyLocation || "—"} +
+
+ +
+
Results Wanted
+ { + const value = parseInt(event.target.value, 10) + if (Number.isNaN(value)) { + setJobspyResultsWantedDraft(null) + } else { + setJobspyResultsWantedDraft(Math.min(500, Math.max(1, value))) + } + }} + disabled={isLoading || isSaving} + /> +
+ Number of results to fetch per term per site. Max 500. +
+
+ Effective: {effectiveJobspyResultsWanted} + Default: {defaultJobspyResultsWanted} +
+
+ +
+
Hours Old
+ { + const value = parseInt(event.target.value, 10) + if (Number.isNaN(value)) { + setJobspyHoursOldDraft(null) + } else { + setJobspyHoursOldDraft(Math.min(168, Math.max(1, value))) + } + }} + disabled={isLoading || isSaving} + /> +
+ Max age of jobs in hours (e.g. 72 for 3 days). +
+
+ Effective: {effectiveJobspyHoursOld}h + Default: {defaultJobspyHoursOld}h +
+
+ +
+
Indeed Country
+ setJobspyCountryIndeedDraft(event.target.value)} + placeholder={defaultJobspyCountryIndeed || "UK"} + disabled={isLoading || isSaving} + /> +
+ Country domain for Indeed (e.g. "UK" for indeed.co.uk). +
+
+ Effective: {effectiveJobspyCountryIndeed || "—"} + Default: {defaultJobspyCountryIndeed || "—"} +
+
+
+ + + +
+ setJobspyLinkedinFetchDescriptionDraft(!!checked)} + disabled={isLoading || isSaving} + /> +
+ +

+ If enabled, JobSpy will make extra requests to fetch full descriptions. Slower but better data. +

+
+ Effective: {effectiveJobspyLinkedinFetchDescription ? "Yes" : "No"} + Default: {defaultJobspyLinkedinFetchDescription ? "Yes" : "No"} +
+
+
+
+
+ Resume Projects diff --git a/orchestrator/src/server/api/routes.ts b/orchestrator/src/server/api/routes.ts index a8b0f4f..d245d29 100644 --- a/orchestrator/src/server/api/routes.ts +++ b/orchestrator/src/server/api/routes.ts @@ -243,13 +243,38 @@ apiRouter.get('/settings', async (_req: Request, res: Response) => { const overrideUkvisajobsMaxJobs = overrideUkvisajobsMaxJobsRaw ? parseInt(overrideUkvisajobsMaxJobsRaw, 10) : null; const ukvisajobsMaxJobs = overrideUkvisajobsMaxJobs ?? defaultUkvisajobsMaxJobs; - // 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 + 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 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; + res.json({ success: true, data: { @@ -269,6 +294,21 @@ apiRouter.get('/settings', async (_req: Request, res: Response) => { searchTerms, defaultSearchTerms, overrideSearchTerms, + jobspyLocation, + defaultJobspyLocation, + overrideJobspyLocation, + jobspyResultsWanted, + defaultJobspyResultsWanted, + overrideJobspyResultsWanted, + jobspyHoursOld, + defaultJobspyHoursOld, + overrideJobspyHoursOld, + jobspyCountryIndeed, + defaultJobspyCountryIndeed, + overrideJobspyCountryIndeed, + jobspyLinkedinFetchDescription, + defaultJobspyLinkedinFetchDescription, + overrideJobspyLinkedinFetchDescription, }, }); } catch (error) { @@ -288,6 +328,11 @@ const updateSettingsSchema = z.object({ }).nullable().optional(), ukvisajobsMaxJobs: 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(), + jobspyLinkedinFetchDescription: z.boolean().nullable().optional(), }); /** @@ -336,6 +381,31 @@ apiRouter.patch('/settings', async (req: Request, res: Response) => { 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 ('jobspyLinkedinFetchDescription' in input) { + const value = input.jobspyLinkedinFetchDescription ?? null; + await settingsRepo.setSetting('jobspyLinkedinFetchDescription', 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; @@ -365,6 +435,32 @@ apiRouter.patch('/settings', async (req: Request, res: Response) => { 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 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; + res.json({ success: true, data: { @@ -384,6 +480,21 @@ apiRouter.patch('/settings', async (req: Request, res: Response) => { searchTerms, defaultSearchTerms, overrideSearchTerms, + jobspyLocation, + defaultJobspyLocation, + overrideJobspyLocation, + jobspyResultsWanted, + defaultJobspyResultsWanted, + overrideJobspyResultsWanted, + jobspyHoursOld, + defaultJobspyHoursOld, + overrideJobspyHoursOld, + jobspyCountryIndeed, + defaultJobspyCountryIndeed, + overrideJobspyCountryIndeed, + jobspyLinkedinFetchDescription, + defaultJobspyLinkedinFetchDescription, + overrideJobspyLinkedinFetchDescription, }, }); } catch (error) { diff --git a/orchestrator/src/server/pipeline/orchestrator.ts b/orchestrator/src/server/pipeline/orchestrator.ts index 2fd02c3..9e144e3 100644 --- a/orchestrator/src/server/pipeline/orchestrator.ts +++ b/orchestrator/src/server/pipeline/orchestrator.ts @@ -134,9 +134,20 @@ 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 jobSpyResult = await runJobSpy({ sites: jobSpySites, searchTerms, + location: jobspyLocationSetting ?? undefined, + resultsWanted: jobspyResultsWantedSetting ? parseInt(jobspyResultsWantedSetting, 10) : undefined, + hoursOld: jobspyHoursOldSetting ? parseInt(jobspyHoursOldSetting, 10) : undefined, + countryIndeed: jobspyCountryIndeedSetting ?? undefined, + linkedinFetchDescription: jobspyLinkedinFetchDescriptionSetting !== null ? jobspyLinkedinFetchDescriptionSetting === '1' : undefined, }); if (!jobSpyResult.success) { sourceErrors.push(`jobspy: ${jobSpyResult.error ?? 'unknown error'}`); diff --git a/orchestrator/src/server/repositories/settings.ts b/orchestrator/src/server/repositories/settings.ts index 7844e31..2d260b1 100644 --- a/orchestrator/src/server/repositories/settings.ts +++ b/orchestrator/src/server/repositories/settings.ts @@ -13,6 +13,11 @@ export type SettingKey = 'model' | 'resumeProjects' | 'ukvisajobsMaxJobs' | 'searchTerms' + | 'jobspyLocation' + | 'jobspyResultsWanted' + | 'jobspyHoursOld' + | 'jobspyCountryIndeed' + | 'jobspyLinkedinFetchDescription' export async function getSetting(key: SettingKey): Promise { const [row] = await db.select().from(settings).where(eq(settings.key, key)) diff --git a/orchestrator/src/shared/types.ts b/orchestrator/src/shared/types.ts index f8153a1..a8429c5 100644 --- a/orchestrator/src/shared/types.ts +++ b/orchestrator/src/shared/types.ts @@ -207,4 +207,19 @@ export interface AppSettings { searchTerms: string[]; defaultSearchTerms: string[]; overrideSearchTerms: string[] | null; + jobspyLocation: string; + defaultJobspyLocation: string; + overrideJobspyLocation: string | null; + jobspyResultsWanted: number; + defaultJobspyResultsWanted: number; + overrideJobspyResultsWanted: number | null; + jobspyHoursOld: number; + defaultJobspyHoursOld: number; + overrideJobspyHoursOld: number | null; + jobspyCountryIndeed: string; + defaultJobspyCountryIndeed: string; + overrideJobspyCountryIndeed: string | null; + jobspyLinkedinFetchDescription: boolean; + defaultJobspyLinkedinFetchDescription: boolean; + overrideJobspyLinkedinFetchDescription: boolean | null; }