passing max ukvisajobs

This commit is contained in:
DaKheera47 2025-12-26 20:47:28 +00:00
parent 878a2f0f54
commit bd7baafbec
9 changed files with 100 additions and 13 deletions

View File

@ -47,4 +47,3 @@ UKVISAJOBS_TOKEN=
UKVISAJOBS_AUTH_TOKEN= UKVISAJOBS_AUTH_TOKEN=
UKVISAJOBS_CSRF_TOKEN= UKVISAJOBS_CSRF_TOKEN=
UKVISAJOBS_CI_SESSION= UKVISAJOBS_CI_SESSION=
UKVISAJOBS_MAX_JOBS=50

View File

@ -55,7 +55,6 @@ services:
- UKVISAJOBS_AUTH_TOKEN=${UKVISAJOBS_AUTH_TOKEN:-} - UKVISAJOBS_AUTH_TOKEN=${UKVISAJOBS_AUTH_TOKEN:-}
- UKVISAJOBS_CSRF_TOKEN=${UKVISAJOBS_CSRF_TOKEN:-} - UKVISAJOBS_CSRF_TOKEN=${UKVISAJOBS_CSRF_TOKEN:-}
- UKVISAJOBS_CI_SESSION=${UKVISAJOBS_CI_SESSION:-} - UKVISAJOBS_CI_SESSION=${UKVISAJOBS_CI_SESSION:-}
- UKVISAJOBS_MAX_JOBS=${UKVISAJOBS_MAX_JOBS:-50}
- UKVISAJOBS_SEARCH_KEYWORD=${UKVISAJOBS_SEARCH_KEYWORD:-} - UKVISAJOBS_SEARCH_KEYWORD=${UKVISAJOBS_SEARCH_KEYWORD:-}
# Python path (uses system python in container) # Python path (uses system python in container)

View File

@ -9,7 +9,7 @@
* UKVISAJOBS_AUTH_TOKEN - Auth cookie token (defaults to UKVISAJOBS_TOKEN) * UKVISAJOBS_AUTH_TOKEN - Auth cookie token (defaults to UKVISAJOBS_TOKEN)
* UKVISAJOBS_CSRF_TOKEN - CSRF token cookie * UKVISAJOBS_CSRF_TOKEN - CSRF token cookie
* UKVISAJOBS_CI_SESSION - CI session cookie * UKVISAJOBS_CI_SESSION - CI session cookie
* UKVISAJOBS_MAX_JOBS - Maximum jobs to fetch (default: 50, max: 200) * UKVISAJOBS_MAX_JOBS - Maximum jobs to fetch (default: 50, max: 200) - Set via UI Settings
* UKVISAJOBS_SEARCH_KEYWORD - Optional search filter * UKVISAJOBS_SEARCH_KEYWORD - Optional search filter
*/ */

View File

@ -2,10 +2,10 @@
* API client for the orchestrator backend. * API client for the orchestrator backend.
*/ */
import type { import type {
Job, Job,
ApiResponse, ApiResponse,
JobsListResponse, JobsListResponse,
PipelineStatusResponse, PipelineStatusResponse,
JobSource, JobSource,
PipelineRun, PipelineRun,
@ -26,13 +26,13 @@ async function fetchApi<T>(
...options?.headers, ...options?.headers,
}, },
}); });
const data: ApiResponse<T> = await response.json(); const data: ApiResponse<T> = await response.json();
if (!data.success) { if (!data.success) {
throw new Error(data.error || 'API request failed'); throw new Error(data.error || 'API request failed');
} }
return data.data as T; return data.data as T;
} }
@ -47,7 +47,7 @@ export async function getJob(id: string): Promise<Job> {
} }
export async function updateJob( export async function updateJob(
id: string, id: string,
update: Partial<Job> update: Partial<Job>
): Promise<Job> { ): Promise<Job> {
return fetchApi<Job>(`/jobs/${id}`, { return fetchApi<Job>(`/jobs/${id}`, {
@ -105,6 +105,7 @@ export async function updateSettings(update: {
pipelineWebhookUrl?: string | null pipelineWebhookUrl?: string | null
jobCompleteWebhookUrl?: string | null jobCompleteWebhookUrl?: string | null
resumeProjects?: ResumeProjectsSettings | null resumeProjects?: ResumeProjectsSettings | null
ukvisajobsMaxJobs?: number | null
}): Promise<AppSettings> { }): Promise<AppSettings> {
return fetchApi<AppSettings>('/settings', { return fetchApi<AppSettings>('/settings', {
method: 'PATCH', method: 'PATCH',

View File

@ -42,6 +42,7 @@ export const SettingsPage: React.FC = () => {
const [pipelineWebhookUrlDraft, setPipelineWebhookUrlDraft] = useState("") const [pipelineWebhookUrlDraft, setPipelineWebhookUrlDraft] = useState("")
const [jobCompleteWebhookUrlDraft, setJobCompleteWebhookUrlDraft] = useState("") const [jobCompleteWebhookUrlDraft, setJobCompleteWebhookUrlDraft] = useState("")
const [resumeProjectsDraft, setResumeProjectsDraft] = useState<ResumeProjectsSettings | null>(null) const [resumeProjectsDraft, setResumeProjectsDraft] = useState<ResumeProjectsSettings | null>(null)
const [ukvisajobsMaxJobsDraft, setUkvisajobsMaxJobsDraft] = useState<number | null>(null)
const [isSaving, setIsSaving] = useState(false) const [isSaving, setIsSaving] = useState(false)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -57,6 +58,7 @@ export const SettingsPage: React.FC = () => {
setPipelineWebhookUrlDraft(data.overridePipelineWebhookUrl ?? "") setPipelineWebhookUrlDraft(data.overridePipelineWebhookUrl ?? "")
setJobCompleteWebhookUrlDraft(data.overrideJobCompleteWebhookUrl ?? "") setJobCompleteWebhookUrlDraft(data.overrideJobCompleteWebhookUrl ?? "")
setResumeProjectsDraft(data.resumeProjects) setResumeProjectsDraft(data.resumeProjects)
setUkvisajobsMaxJobsDraft(data.overrideUkvisajobsMaxJobs)
}) })
.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"
@ -81,6 +83,9 @@ export const SettingsPage: React.FC = () => {
const effectiveJobCompleteWebhookUrl = settings?.jobCompleteWebhookUrl ?? "" const effectiveJobCompleteWebhookUrl = settings?.jobCompleteWebhookUrl ?? ""
const defaultJobCompleteWebhookUrl = settings?.defaultJobCompleteWebhookUrl ?? "" const defaultJobCompleteWebhookUrl = settings?.defaultJobCompleteWebhookUrl ?? ""
const overrideJobCompleteWebhookUrl = settings?.overrideJobCompleteWebhookUrl const overrideJobCompleteWebhookUrl = settings?.overrideJobCompleteWebhookUrl
const effectiveUkvisajobsMaxJobs = settings?.ukvisajobsMaxJobs ?? 50
const defaultUkvisajobsMaxJobs = settings?.defaultUkvisajobsMaxJobs ?? 50
const overrideUkvisajobsMaxJobs = settings?.overrideUkvisajobsMaxJobs
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
@ -93,11 +98,13 @@ export const SettingsPage: React.FC = () => {
const currentWebhook = (overridePipelineWebhookUrl ?? "").trim() const currentWebhook = (overridePipelineWebhookUrl ?? "").trim()
const nextJobCompleteWebhook = jobCompleteWebhookUrlDraft.trim() const nextJobCompleteWebhook = jobCompleteWebhookUrlDraft.trim()
const currentJobCompleteWebhook = (overrideJobCompleteWebhookUrl ?? "").trim() const currentJobCompleteWebhook = (overrideJobCompleteWebhookUrl ?? "").trim()
const ukvisajobsChanged = ukvisajobsMaxJobsDraft !== (overrideUkvisajobsMaxJobs ?? null)
return ( return (
next !== current || next !== current ||
nextWebhook !== currentWebhook || nextWebhook !== currentWebhook ||
nextJobCompleteWebhook !== currentJobCompleteWebhook || nextJobCompleteWebhook !== currentJobCompleteWebhook ||
!resumeProjectsEqual(resumeProjectsDraft, settings.resumeProjects) !resumeProjectsEqual(resumeProjectsDraft, settings.resumeProjects) ||
ukvisajobsChanged
) )
}, [ }, [
settings, settings,
@ -108,6 +115,8 @@ export const SettingsPage: React.FC = () => {
overridePipelineWebhookUrl, overridePipelineWebhookUrl,
overrideJobCompleteWebhookUrl, overrideJobCompleteWebhookUrl,
resumeProjectsDraft, resumeProjectsDraft,
ukvisajobsMaxJobsDraft,
overrideUkvisajobsMaxJobs,
]) ])
const handleSave = async () => { const handleSave = async () => {
@ -120,17 +129,20 @@ export const SettingsPage: React.FC = () => {
const resumeProjectsOverride = resumeProjectsEqual(resumeProjectsDraft, settings.defaultResumeProjects) const resumeProjectsOverride = resumeProjectsEqual(resumeProjectsDraft, settings.defaultResumeProjects)
? null ? null
: resumeProjectsDraft : resumeProjectsDraft
const ukvisajobsMaxJobsOverride = ukvisajobsMaxJobsDraft === defaultUkvisajobsMaxJobs ? null : ukvisajobsMaxJobsDraft
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,
jobCompleteWebhookUrl: jobCompleteTrimmed.length > 0 ? jobCompleteTrimmed : null, jobCompleteWebhookUrl: jobCompleteTrimmed.length > 0 ? jobCompleteTrimmed : null,
resumeProjects: resumeProjectsOverride, resumeProjects: resumeProjectsOverride,
ukvisajobsMaxJobs: ukvisajobsMaxJobsOverride,
}) })
setSettings(updated) setSettings(updated)
setModelDraft(updated.overrideModel ?? "") setModelDraft(updated.overrideModel ?? "")
setPipelineWebhookUrlDraft(updated.overridePipelineWebhookUrl ?? "") setPipelineWebhookUrlDraft(updated.overridePipelineWebhookUrl ?? "")
setJobCompleteWebhookUrlDraft(updated.overrideJobCompleteWebhookUrl ?? "") setJobCompleteWebhookUrlDraft(updated.overrideJobCompleteWebhookUrl ?? "")
setResumeProjectsDraft(updated.resumeProjects) setResumeProjectsDraft(updated.resumeProjects)
setUkvisajobsMaxJobsDraft(updated.overrideUkvisajobsMaxJobs)
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"
@ -148,12 +160,14 @@ export const SettingsPage: React.FC = () => {
pipelineWebhookUrl: null, pipelineWebhookUrl: null,
jobCompleteWebhookUrl: null, jobCompleteWebhookUrl: null,
resumeProjects: null, resumeProjects: null,
ukvisajobsMaxJobs: null,
}) })
setSettings(updated) setSettings(updated)
setModelDraft("") setModelDraft("")
setPipelineWebhookUrlDraft("") setPipelineWebhookUrlDraft("")
setJobCompleteWebhookUrlDraft("") setJobCompleteWebhookUrlDraft("")
setResumeProjectsDraft(updated.resumeProjects) setResumeProjectsDraft(updated.resumeProjects)
setUkvisajobsMaxJobsDraft(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"
@ -272,6 +286,50 @@ export const SettingsPage: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle className="text-base">UKVisaJobs Extractor</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="text-sm font-medium">Max jobs to fetch</div>
<Input
type="number"
inputMode="numeric"
min={1}
max={200}
value={ukvisajobsMaxJobsDraft ?? defaultUkvisajobsMaxJobs}
onChange={(event) => {
const value = parseInt(event.target.value, 10)
if (Number.isNaN(value)) {
setUkvisajobsMaxJobsDraft(null)
} else {
setUkvisajobsMaxJobsDraft(Math.min(200, Math.max(1, value)))
}
}}
disabled={isLoading || isSaving}
/>
<div className="text-xs text-muted-foreground">
Maximum number of jobs to fetch from UKVisaJobs per pipeline run. Range: 1-200.
</div>
</div>
<Separator />
<div className="grid gap-2 text-sm sm:grid-cols-2">
<div>
<div className="text-xs text-muted-foreground">Effective</div>
<div className="break-words font-mono text-xs">{effectiveUkvisajobsMaxJobs}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Default</div>
<div className="break-words font-mono text-xs">{defaultUkvisajobsMaxJobs}</div>
</div>
</div>
</CardContent>
</Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base">Resume Projects</CardTitle> <CardTitle className="text-base">Resume Projects</CardTitle>

View File

@ -238,6 +238,11 @@ apiRouter.get('/settings', async (_req: Request, res: Response) => {
const overrideResumeProjectsRaw = await settingsRepo.getSetting('resumeProjects'); const overrideResumeProjectsRaw = await settingsRepo.getSetting('resumeProjects');
const resumeProjectsData = resolveResumeProjectsSettings({ catalog, overrideRaw: overrideResumeProjectsRaw }); 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;
res.json({ res.json({
success: true, success: true,
data: { data: {
@ -251,6 +256,9 @@ apiRouter.get('/settings', async (_req: Request, res: Response) => {
defaultJobCompleteWebhookUrl, defaultJobCompleteWebhookUrl,
overrideJobCompleteWebhookUrl, overrideJobCompleteWebhookUrl,
...resumeProjectsData, ...resumeProjectsData,
ukvisajobsMaxJobs,
defaultUkvisajobsMaxJobs,
overrideUkvisajobsMaxJobs,
}, },
}); });
} catch (error) { } catch (error) {
@ -268,6 +276,7 @@ const updateSettingsSchema = z.object({
lockedProjectIds: z.array(z.string().trim().min(1)).max(200), lockedProjectIds: z.array(z.string().trim().min(1)).max(200),
aiSelectableProjectIds: z.array(z.string().trim().min(1)).max(200), aiSelectableProjectIds: z.array(z.string().trim().min(1)).max(200),
}).nullable().optional(), }).nullable().optional(),
ukvisajobsMaxJobs: z.number().int().min(1).max(200).nullable().optional(),
}); });
/** /**
@ -306,6 +315,11 @@ apiRouter.patch('/settings', async (req: Request, res: Response) => {
} }
} }
if ('ukvisajobsMaxJobs' in input) {
const ukvisajobsMaxJobs = input.ukvisajobsMaxJobs ?? null;
await settingsRepo.setSetting('ukvisajobsMaxJobs', ukvisajobsMaxJobs !== null ? String(ukvisajobsMaxJobs) : 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;
@ -323,6 +337,11 @@ apiRouter.patch('/settings', async (req: Request, res: Response) => {
const overrideResumeProjectsRaw = await settingsRepo.getSetting('resumeProjects'); const overrideResumeProjectsRaw = await settingsRepo.getSetting('resumeProjects');
const resumeProjectsData = resolveResumeProjectsSettings({ catalog, overrideRaw: overrideResumeProjectsRaw }); 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;
res.json({ res.json({
success: true, success: true,
data: { data: {
@ -336,6 +355,9 @@ apiRouter.patch('/settings', async (req: Request, res: Response) => {
defaultJobCompleteWebhookUrl, defaultJobCompleteWebhookUrl,
overrideJobCompleteWebhookUrl, overrideJobCompleteWebhookUrl,
...resumeProjectsData, ...resumeProjectsData,
ukvisajobsMaxJobs,
defaultUkvisajobsMaxJobs,
overrideUkvisajobsMaxJobs,
}, },
}); });
} catch (error) { } catch (error) {

View File

@ -162,7 +162,11 @@ export async function runPipeline(config: Partial<PipelineConfig> = {}): Promise
detail: 'UKVisaJobs: scraping visa-sponsoring jobs...', detail: 'UKVisaJobs: scraping visa-sponsoring jobs...',
}); });
const ukVisaResult = await runUkVisaJobs({ maxJobs: 50 }); // Read max jobs setting from database (default to 50 if not set)
const ukvisajobsMaxJobsSetting = await settingsRepo.getSetting('ukvisajobsMaxJobs');
const ukvisajobsMaxJobs = ukvisajobsMaxJobsSetting ? parseInt(ukvisajobsMaxJobsSetting, 10) : 50;
const ukVisaResult = await runUkVisaJobs({ maxJobs: ukvisajobsMaxJobs });
if (!ukVisaResult.success) { if (!ukVisaResult.success) {
sourceErrors.push(`ukvisajobs: ${ukVisaResult.error ?? 'unknown error'}`); sourceErrors.push(`ukvisajobs: ${ukVisaResult.error ?? 'unknown error'}`);
} else { } else {

View File

@ -11,6 +11,7 @@ export type SettingKey = 'model'
| 'pipelineWebhookUrl' | 'pipelineWebhookUrl'
| 'jobCompleteWebhookUrl' | 'jobCompleteWebhookUrl'
| 'resumeProjects' | 'resumeProjects'
| 'ukvisajobsMaxJobs'
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))

View File

@ -201,4 +201,7 @@ export interface AppSettings {
resumeProjects: ResumeProjectsSettings; resumeProjects: ResumeProjectsSettings;
defaultResumeProjects: ResumeProjectsSettings; defaultResumeProjects: ResumeProjectsSettings;
overrideResumeProjects: ResumeProjectsSettings | null; overrideResumeProjects: ResumeProjectsSettings | null;
ukvisajobsMaxJobs: number;
defaultUkvisajobsMaxJobs: number;
overrideUkvisajobsMaxJobs: number | null;
} }