jobspy specific settings
This commit is contained in:
parent
55494a4803
commit
45662c386a
@ -107,6 +107,11 @@ export async function updateSettings(update: {
|
|||||||
resumeProjects?: ResumeProjectsSettings | null
|
resumeProjects?: ResumeProjectsSettings | null
|
||||||
ukvisajobsMaxJobs?: number | null
|
ukvisajobsMaxJobs?: number | null
|
||||||
searchTerms?: string[] | null
|
searchTerms?: string[] | null
|
||||||
|
jobspyLocation?: string | null
|
||||||
|
jobspyResultsWanted?: number | null
|
||||||
|
jobspyHoursOld?: number | null
|
||||||
|
jobspyCountryIndeed?: string | null
|
||||||
|
jobspyLinkedinFetchDescription?: boolean | null
|
||||||
}): Promise<AppSettings> {
|
}): Promise<AppSettings> {
|
||||||
return fetchApi<AppSettings>('/settings', {
|
return fetchApi<AppSettings>('/settings', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|||||||
@ -44,6 +44,11 @@ export const SettingsPage: React.FC = () => {
|
|||||||
const [resumeProjectsDraft, setResumeProjectsDraft] = useState<ResumeProjectsSettings | null>(null)
|
const [resumeProjectsDraft, setResumeProjectsDraft] = useState<ResumeProjectsSettings | null>(null)
|
||||||
const [ukvisajobsMaxJobsDraft, setUkvisajobsMaxJobsDraft] = useState<number | null>(null)
|
const [ukvisajobsMaxJobsDraft, setUkvisajobsMaxJobsDraft] = useState<number | null>(null)
|
||||||
const [searchTermsDraft, setSearchTermsDraft] = useState<string[] | null>(null)
|
const [searchTermsDraft, setSearchTermsDraft] = useState<string[] | null>(null)
|
||||||
|
const [jobspyLocationDraft, setJobspyLocationDraft] = useState<string | null>(null)
|
||||||
|
const [jobspyResultsWantedDraft, setJobspyResultsWantedDraft] = useState<number | null>(null)
|
||||||
|
const [jobspyHoursOldDraft, setJobspyHoursOldDraft] = useState<number | null>(null)
|
||||||
|
const [jobspyCountryIndeedDraft, setJobspyCountryIndeedDraft] = useState<string | null>(null)
|
||||||
|
const [jobspyLinkedinFetchDescriptionDraft, setJobspyLinkedinFetchDescriptionDraft] = useState<boolean | null>(null)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
@ -61,6 +66,11 @@ export const SettingsPage: React.FC = () => {
|
|||||||
setResumeProjectsDraft(data.resumeProjects)
|
setResumeProjectsDraft(data.resumeProjects)
|
||||||
setUkvisajobsMaxJobsDraft(data.overrideUkvisajobsMaxJobs)
|
setUkvisajobsMaxJobsDraft(data.overrideUkvisajobsMaxJobs)
|
||||||
setSearchTermsDraft(data.overrideSearchTerms)
|
setSearchTermsDraft(data.overrideSearchTerms)
|
||||||
|
setJobspyLocationDraft(data.overrideJobspyLocation)
|
||||||
|
setJobspyResultsWantedDraft(data.overrideJobspyResultsWanted)
|
||||||
|
setJobspyHoursOldDraft(data.overrideJobspyHoursOld)
|
||||||
|
setJobspyCountryIndeedDraft(data.overrideJobspyCountryIndeed)
|
||||||
|
setJobspyLinkedinFetchDescriptionDraft(data.overrideJobspyLinkedinFetchDescription)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const message = error instanceof Error ? error.message : "Failed to load settings"
|
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 effectiveSearchTerms = settings?.searchTerms ?? []
|
||||||
const defaultSearchTerms = settings?.defaultSearchTerms ?? []
|
const defaultSearchTerms = settings?.defaultSearchTerms ?? []
|
||||||
const overrideSearchTerms = settings?.overrideSearchTerms
|
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 profileProjects = settings?.profileProjects ?? []
|
||||||
const maxProjectsTotal = profileProjects.length
|
const maxProjectsTotal = profileProjects.length
|
||||||
const lockedCount = resumeProjectsDraft?.lockedProjectIds.length ?? 0
|
const lockedCount = resumeProjectsDraft?.lockedProjectIds.length ?? 0
|
||||||
@ -111,7 +136,12 @@ export const SettingsPage: React.FC = () => {
|
|||||||
nextJobCompleteWebhook !== currentJobCompleteWebhook ||
|
nextJobCompleteWebhook !== currentJobCompleteWebhook ||
|
||||||
!resumeProjectsEqual(resumeProjectsDraft, settings.resumeProjects) ||
|
!resumeProjectsEqual(resumeProjectsDraft, settings.resumeProjects) ||
|
||||||
ukvisajobsChanged ||
|
ukvisajobsChanged ||
|
||||||
searchTermsChanged
|
searchTermsChanged ||
|
||||||
|
jobspyLocationDraft !== (overrideJobspyLocation ?? null) ||
|
||||||
|
jobspyResultsWantedDraft !== (overrideJobspyResultsWanted ?? null) ||
|
||||||
|
jobspyHoursOldDraft !== (overrideJobspyHoursOld ?? null) ||
|
||||||
|
jobspyCountryIndeedDraft !== (overrideJobspyCountryIndeed ?? null) ||
|
||||||
|
jobspyLinkedinFetchDescriptionDraft !== (overrideJobspyLinkedinFetchDescription ?? null)
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
settings,
|
settings,
|
||||||
@ -126,6 +156,16 @@ export const SettingsPage: React.FC = () => {
|
|||||||
overrideUkvisajobsMaxJobs,
|
overrideUkvisajobsMaxJobs,
|
||||||
searchTermsDraft,
|
searchTermsDraft,
|
||||||
overrideSearchTerms,
|
overrideSearchTerms,
|
||||||
|
jobspyLocationDraft,
|
||||||
|
jobspyResultsWantedDraft,
|
||||||
|
jobspyHoursOldDraft,
|
||||||
|
jobspyCountryIndeedDraft,
|
||||||
|
jobspyLinkedinFetchDescriptionDraft,
|
||||||
|
overrideJobspyLocation,
|
||||||
|
overrideJobspyResultsWanted,
|
||||||
|
overrideJobspyHoursOld,
|
||||||
|
overrideJobspyCountryIndeed,
|
||||||
|
overrideJobspyLinkedinFetchDescription,
|
||||||
])
|
])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@ -140,6 +180,11 @@ export const SettingsPage: React.FC = () => {
|
|||||||
: resumeProjectsDraft
|
: resumeProjectsDraft
|
||||||
const ukvisajobsMaxJobsOverride = ukvisajobsMaxJobsDraft === defaultUkvisajobsMaxJobs ? null : ukvisajobsMaxJobsDraft
|
const ukvisajobsMaxJobsOverride = ukvisajobsMaxJobsDraft === defaultUkvisajobsMaxJobs ? null : ukvisajobsMaxJobsDraft
|
||||||
const searchTermsOverride = arraysEqual(searchTermsDraft ?? [], defaultSearchTerms) ? null : searchTermsDraft
|
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({
|
const updated = await api.updateSettings({
|
||||||
model: trimmed.length > 0 ? trimmed : null,
|
model: trimmed.length > 0 ? trimmed : null,
|
||||||
pipelineWebhookUrl: webhookTrimmed.length > 0 ? webhookTrimmed : null,
|
pipelineWebhookUrl: webhookTrimmed.length > 0 ? webhookTrimmed : null,
|
||||||
@ -147,6 +192,11 @@ export const SettingsPage: React.FC = () => {
|
|||||||
resumeProjects: resumeProjectsOverride,
|
resumeProjects: resumeProjectsOverride,
|
||||||
ukvisajobsMaxJobs: ukvisajobsMaxJobsOverride,
|
ukvisajobsMaxJobs: ukvisajobsMaxJobsOverride,
|
||||||
searchTerms: searchTermsOverride,
|
searchTerms: searchTermsOverride,
|
||||||
|
jobspyLocation: jobspyLocationOverride,
|
||||||
|
jobspyResultsWanted: jobspyResultsWantedOverride,
|
||||||
|
jobspyHoursOld: jobspyHoursOldOverride,
|
||||||
|
jobspyCountryIndeed: jobspyCountryIndeedOverride,
|
||||||
|
jobspyLinkedinFetchDescription: jobspyLinkedinFetchDescriptionOverride,
|
||||||
})
|
})
|
||||||
setSettings(updated)
|
setSettings(updated)
|
||||||
setModelDraft(updated.overrideModel ?? "")
|
setModelDraft(updated.overrideModel ?? "")
|
||||||
@ -155,6 +205,11 @@ export const SettingsPage: React.FC = () => {
|
|||||||
setResumeProjectsDraft(updated.resumeProjects)
|
setResumeProjectsDraft(updated.resumeProjects)
|
||||||
setUkvisajobsMaxJobsDraft(updated.overrideUkvisajobsMaxJobs)
|
setUkvisajobsMaxJobsDraft(updated.overrideUkvisajobsMaxJobs)
|
||||||
setSearchTermsDraft(updated.overrideSearchTerms)
|
setSearchTermsDraft(updated.overrideSearchTerms)
|
||||||
|
setJobspyLocationDraft(updated.overrideJobspyLocation)
|
||||||
|
setJobspyResultsWantedDraft(updated.overrideJobspyResultsWanted)
|
||||||
|
setJobspyHoursOldDraft(updated.overrideJobspyHoursOld)
|
||||||
|
setJobspyCountryIndeedDraft(updated.overrideJobspyCountryIndeed)
|
||||||
|
setJobspyLinkedinFetchDescriptionDraft(updated.overrideJobspyLinkedinFetchDescription)
|
||||||
toast.success("Settings saved")
|
toast.success("Settings saved")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to save settings"
|
const message = error instanceof Error ? error.message : "Failed to save settings"
|
||||||
@ -174,6 +229,11 @@ export const SettingsPage: React.FC = () => {
|
|||||||
resumeProjects: null,
|
resumeProjects: null,
|
||||||
ukvisajobsMaxJobs: null,
|
ukvisajobsMaxJobs: null,
|
||||||
searchTerms: null,
|
searchTerms: null,
|
||||||
|
jobspyLocation: null,
|
||||||
|
jobspyResultsWanted: null,
|
||||||
|
jobspyHoursOld: null,
|
||||||
|
jobspyCountryIndeed: null,
|
||||||
|
jobspyLinkedinFetchDescription: null,
|
||||||
})
|
})
|
||||||
setSettings(updated)
|
setSettings(updated)
|
||||||
setModelDraft("")
|
setModelDraft("")
|
||||||
@ -182,6 +242,11 @@ export const SettingsPage: React.FC = () => {
|
|||||||
setResumeProjectsDraft(updated.resumeProjects)
|
setResumeProjectsDraft(updated.resumeProjects)
|
||||||
setUkvisajobsMaxJobsDraft(null)
|
setUkvisajobsMaxJobsDraft(null)
|
||||||
setSearchTermsDraft(null)
|
setSearchTermsDraft(null)
|
||||||
|
setJobspyLocationDraft(null)
|
||||||
|
setJobspyResultsWantedDraft(null)
|
||||||
|
setJobspyHoursOldDraft(null)
|
||||||
|
setJobspyCountryIndeedDraft(null)
|
||||||
|
setJobspyLinkedinFetchDescriptionDraft(null)
|
||||||
toast.success("Reset to default")
|
toast.success("Reset to default")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to reset settings"
|
const message = error instanceof Error ? error.message : "Failed to reset settings"
|
||||||
@ -390,6 +455,130 @@ export const SettingsPage: React.FC = () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">JobSpy Scraper</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Location</div>
|
||||||
|
<Input
|
||||||
|
value={jobspyLocationDraft ?? defaultJobspyLocation}
|
||||||
|
onChange={(event) => setJobspyLocationDraft(event.target.value)}
|
||||||
|
placeholder={defaultJobspyLocation || "UK"}
|
||||||
|
disabled={isLoading || isSaving}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Location to search for jobs (e.g. "UK", "London", "Remote").
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>Effective: {effectiveJobspyLocation || "—"}</span>
|
||||||
|
<span>Default: {defaultJobspyLocation || "—"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Results Wanted</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
inputMode="numeric"
|
||||||
|
min={1}
|
||||||
|
max={500}
|
||||||
|
value={jobspyResultsWantedDraft ?? defaultJobspyResultsWanted}
|
||||||
|
onChange={(event) => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Number of results to fetch per term per site. Max 500.
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>Effective: {effectiveJobspyResultsWanted}</span>
|
||||||
|
<span>Default: {defaultJobspyResultsWanted}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Hours Old</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
inputMode="numeric"
|
||||||
|
min={1}
|
||||||
|
max={168}
|
||||||
|
value={jobspyHoursOldDraft ?? defaultJobspyHoursOld}
|
||||||
|
onChange={(event) => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Max age of jobs in hours (e.g. 72 for 3 days).
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>Effective: {effectiveJobspyHoursOld}h</span>
|
||||||
|
<span>Default: {defaultJobspyHoursOld}h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Indeed Country</div>
|
||||||
|
<Input
|
||||||
|
value={jobspyCountryIndeedDraft ?? defaultJobspyCountryIndeed}
|
||||||
|
onChange={(event) => setJobspyCountryIndeedDraft(event.target.value)}
|
||||||
|
placeholder={defaultJobspyCountryIndeed || "UK"}
|
||||||
|
disabled={isLoading || isSaving}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Country domain for Indeed (e.g. "UK" for indeed.co.uk).
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>Effective: {effectiveJobspyCountryIndeed || "—"}</span>
|
||||||
|
<span>Default: {defaultJobspyCountryIndeed || "—"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="linkedin-desc"
|
||||||
|
checked={jobspyLinkedinFetchDescriptionDraft ?? defaultJobspyLinkedinFetchDescription}
|
||||||
|
onCheckedChange={(checked) => setJobspyLinkedinFetchDescriptionDraft(!!checked)}
|
||||||
|
disabled={isLoading || isSaving}
|
||||||
|
/>
|
||||||
|
<div className="grid gap-1.5 leading-none">
|
||||||
|
<label
|
||||||
|
htmlFor="linkedin-desc"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Fetch LinkedIn Description
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
If enabled, JobSpy will make extra requests to fetch full descriptions. Slower but better data.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>Effective: {effectiveJobspyLinkedinFetchDescription ? "Yes" : "No"}</span>
|
||||||
|
<span>Default: {defaultJobspyLinkedinFetchDescription ? "Yes" : "No"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Resume Projects</CardTitle>
|
<CardTitle className="text-base">Resume Projects</CardTitle>
|
||||||
|
|||||||
@ -243,13 +243,38 @@ apiRouter.get('/settings', async (_req: Request, res: Response) => {
|
|||||||
const overrideUkvisajobsMaxJobs = overrideUkvisajobsMaxJobsRaw ? parseInt(overrideUkvisajobsMaxJobsRaw, 10) : null;
|
const overrideUkvisajobsMaxJobs = overrideUkvisajobsMaxJobsRaw ? parseInt(overrideUkvisajobsMaxJobsRaw, 10) : null;
|
||||||
const ukvisajobsMaxJobs = overrideUkvisajobsMaxJobs ?? defaultUkvisajobsMaxJobs;
|
const ukvisajobsMaxJobs = overrideUkvisajobsMaxJobs ?? defaultUkvisajobsMaxJobs;
|
||||||
|
|
||||||
// Search terms - stored as JSON array, default from env var (pipe-separated)
|
|
||||||
const overrideSearchTermsRaw = await settingsRepo.getSetting('searchTerms');
|
const overrideSearchTermsRaw = await settingsRepo.getSetting('searchTerms');
|
||||||
const defaultSearchTermsEnv = process.env.JOBSPY_SEARCH_TERMS || 'web developer';
|
const defaultSearchTermsEnv = process.env.JOBSPY_SEARCH_TERMS || 'web developer';
|
||||||
const defaultSearchTerms = defaultSearchTermsEnv.split('|').map(s => s.trim()).filter(Boolean);
|
const defaultSearchTerms = defaultSearchTermsEnv.split('|').map(s => s.trim()).filter(Boolean);
|
||||||
const overrideSearchTerms = overrideSearchTermsRaw ? JSON.parse(overrideSearchTermsRaw) as string[] : null;
|
const overrideSearchTerms = overrideSearchTermsRaw ? JSON.parse(overrideSearchTermsRaw) as string[] : null;
|
||||||
const searchTerms = overrideSearchTerms ?? defaultSearchTerms;
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@ -269,6 +294,21 @@ apiRouter.get('/settings', async (_req: Request, res: Response) => {
|
|||||||
searchTerms,
|
searchTerms,
|
||||||
defaultSearchTerms,
|
defaultSearchTerms,
|
||||||
overrideSearchTerms,
|
overrideSearchTerms,
|
||||||
|
jobspyLocation,
|
||||||
|
defaultJobspyLocation,
|
||||||
|
overrideJobspyLocation,
|
||||||
|
jobspyResultsWanted,
|
||||||
|
defaultJobspyResultsWanted,
|
||||||
|
overrideJobspyResultsWanted,
|
||||||
|
jobspyHoursOld,
|
||||||
|
defaultJobspyHoursOld,
|
||||||
|
overrideJobspyHoursOld,
|
||||||
|
jobspyCountryIndeed,
|
||||||
|
defaultJobspyCountryIndeed,
|
||||||
|
overrideJobspyCountryIndeed,
|
||||||
|
jobspyLinkedinFetchDescription,
|
||||||
|
defaultJobspyLinkedinFetchDescription,
|
||||||
|
overrideJobspyLinkedinFetchDescription,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -288,6 +328,11 @@ const updateSettingsSchema = z.object({
|
|||||||
}).nullable().optional(),
|
}).nullable().optional(),
|
||||||
ukvisajobsMaxJobs: z.number().int().min(1).max(200).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(),
|
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);
|
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 overrideModel = await settingsRepo.getSetting('model');
|
||||||
const defaultModel = process.env.MODEL || 'openai/gpt-4o-mini';
|
const defaultModel = process.env.MODEL || 'openai/gpt-4o-mini';
|
||||||
const model = overrideModel || defaultModel;
|
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 overrideSearchTerms = overrideSearchTermsRaw ? JSON.parse(overrideSearchTermsRaw) as string[] : null;
|
||||||
const searchTerms = overrideSearchTerms ?? defaultSearchTerms;
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@ -384,6 +480,21 @@ apiRouter.patch('/settings', async (req: Request, res: Response) => {
|
|||||||
searchTerms,
|
searchTerms,
|
||||||
defaultSearchTerms,
|
defaultSearchTerms,
|
||||||
overrideSearchTerms,
|
overrideSearchTerms,
|
||||||
|
jobspyLocation,
|
||||||
|
defaultJobspyLocation,
|
||||||
|
overrideJobspyLocation,
|
||||||
|
jobspyResultsWanted,
|
||||||
|
defaultJobspyResultsWanted,
|
||||||
|
overrideJobspyResultsWanted,
|
||||||
|
jobspyHoursOld,
|
||||||
|
defaultJobspyHoursOld,
|
||||||
|
overrideJobspyHoursOld,
|
||||||
|
jobspyCountryIndeed,
|
||||||
|
defaultJobspyCountryIndeed,
|
||||||
|
overrideJobspyCountryIndeed,
|
||||||
|
jobspyLinkedinFetchDescription,
|
||||||
|
defaultJobspyLinkedinFetchDescription,
|
||||||
|
overrideJobspyLinkedinFetchDescription,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -134,9 +134,20 @@ export async function runPipeline(config: Partial<PipelineConfig> = {}): Promise
|
|||||||
detail: `JobSpy: scraping ${jobSpySites.join(', ')}...`,
|
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({
|
const jobSpyResult = await runJobSpy({
|
||||||
sites: jobSpySites,
|
sites: jobSpySites,
|
||||||
searchTerms,
|
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) {
|
if (!jobSpyResult.success) {
|
||||||
sourceErrors.push(`jobspy: ${jobSpyResult.error ?? 'unknown error'}`);
|
sourceErrors.push(`jobspy: ${jobSpyResult.error ?? 'unknown error'}`);
|
||||||
|
|||||||
@ -13,6 +13,11 @@ export type SettingKey = 'model'
|
|||||||
| 'resumeProjects'
|
| 'resumeProjects'
|
||||||
| 'ukvisajobsMaxJobs'
|
| 'ukvisajobsMaxJobs'
|
||||||
| 'searchTerms'
|
| 'searchTerms'
|
||||||
|
| 'jobspyLocation'
|
||||||
|
| 'jobspyResultsWanted'
|
||||||
|
| 'jobspyHoursOld'
|
||||||
|
| 'jobspyCountryIndeed'
|
||||||
|
| 'jobspyLinkedinFetchDescription'
|
||||||
|
|
||||||
export async function getSetting(key: SettingKey): Promise<string | null> {
|
export async function getSetting(key: SettingKey): Promise<string | null> {
|
||||||
const [row] = await db.select().from(settings).where(eq(settings.key, key))
|
const [row] = await db.select().from(settings).where(eq(settings.key, key))
|
||||||
|
|||||||
@ -207,4 +207,19 @@ export interface AppSettings {
|
|||||||
searchTerms: string[];
|
searchTerms: string[];
|
||||||
defaultSearchTerms: string[];
|
defaultSearchTerms: string[];
|
||||||
overrideSearchTerms: string[] | null;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user