diff --git a/.env.example b/.env.example index e526eb7..dd793bd 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,15 @@ UKVISAJOBS_EMAIL= UKVISAJOBS_PASSWORD= UKVISAJOBS_HEADLESS=true +# ============================================================================= +# Adzuna (multi-country API source) - optional +# ============================================================================= +# App credentials from Adzuna developer account. +ADZUNA_APP_ID= +ADZUNA_APP_KEY= +# Optional default per-term cap (can be overridden by UI run budget logic). +# ADZUNA_MAX_JOBS_PER_TERM=50 + # ============================================================================= # JobSpy - Job search configuration # ============================================================================= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efe9418..97ca177 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - project: [orchestrator, gradcracker-extractor, ukvisajobs-extractor] + project: + - orchestrator + - adzuna-extractor + - gradcracker-extractor + - ukvisajobs-extractor steps: - uses: actions/checkout@v4 - name: Setup Node diff --git a/Dockerfile b/Dockerfile index 7b2e736..2cfe57b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,6 +35,7 @@ COPY package*.json ./ COPY docs-site/package*.json ./docs-site/ COPY shared/package*.json ./shared/ COPY orchestrator/package*.json ./orchestrator/ +COPY extractors/adzuna/package*.json ./extractors/adzuna/ COPY extractors/gradcracker/package*.json ./extractors/gradcracker/ COPY extractors/ukvisajobs/package*.json ./extractors/ukvisajobs/ @@ -52,6 +53,7 @@ WORKDIR /app COPY shared ./shared COPY docs-site ./docs-site COPY orchestrator ./orchestrator +COPY extractors/adzuna ./extractors/adzuna COPY extractors/gradcracker ./extractors/gradcracker COPY extractors/jobspy ./extractors/jobspy COPY extractors/ukvisajobs ./extractors/ukvisajobs @@ -97,6 +99,7 @@ COPY package*.json ./ COPY docs-site/package*.json ./docs-site/ COPY shared/package*.json ./shared/ COPY orchestrator/package*.json ./orchestrator/ +COPY extractors/adzuna/package*.json ./extractors/adzuna/ COPY extractors/gradcracker/package*.json ./extractors/gradcracker/ COPY extractors/ukvisajobs/package*.json ./extractors/ukvisajobs/ @@ -110,6 +113,7 @@ COPY --from=builder /app/orchestrator/dist ./orchestrator/dist COPY --from=builder /app/docs-site/build ./orchestrator/dist/docs COPY shared ./shared COPY orchestrator ./orchestrator +COPY extractors/adzuna ./extractors/adzuna COPY extractors/gradcracker ./extractors/gradcracker COPY extractors/jobspy ./extractors/jobspy COPY extractors/ukvisajobs ./extractors/ukvisajobs diff --git a/README.md b/README.md index 1d8978e..7bf1f96 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ docker compose up -d ## Why JobOps? -* **Universal Scraping**: Supports **LinkedIn, Indeed, Glassdoor** + specialized boards (Gradcracker, UK Visa Jobs). +* **Universal Scraping**: Supports **LinkedIn, Indeed, Glassdoor, Adzuna** + specialized boards (Gradcracker, UK Visa Jobs). * **AI Scoring**: Ranks jobs by fit against *your* profile using your preferred LLM (OpenRouter/OpenAI/Gemini). * **Auto-Tailoring**: Generates custom resumes (PDFs) for every application using RxResume v4. * **Email Tracking**: Connect Gmail to auto-detect interviews, offers, and rejections. @@ -81,6 +81,7 @@ docker compose up -d | **LinkedIn** | Global / General | | **Indeed** | Global / General | | **Glassdoor** | Global / General | +| **Adzuna** | Multi-country API source | | **Gradcracker** | STEM / Grads (UK) | | **UK Visa Jobs** | Sponsorship (UK) | diff --git a/docs-site/docs/extractors/adzuna.md b/docs-site/docs/extractors/adzuna.md new file mode 100644 index 0000000..f3ad6d0 --- /dev/null +++ b/docs-site/docs/extractors/adzuna.md @@ -0,0 +1,58 @@ +--- +id: adzuna +title: Adzuna Extractor +description: API-based Adzuna extraction with orchestrator ingestion and progress updates. +sidebar_position: 6 +--- + +## What it is + +Adzuna is an API-backed extractor implemented in two lean pieces: + +1. `extractors/adzuna/src/main.ts` fetches paginated Adzuna search results and writes `jobs.json`. +2. `orchestrator/src/server/services/adzuna.ts` runs the extractor, parses progress lines, and maps rows into `CreateJobInput`. + +It de-duplicates in the existing repository path using `sourceJobId` fallback to `jobUrl`. + +## Why it exists + +Adzuna provides stable API discovery for countries that are not covered by UK-only sources. It adds a lower-maintenance source without introducing new API routes or UI sections. + +## How to use it + +1. Create an Adzuna developer account. +2. Open [Adzuna Access Details](https://developer.adzuna.com/admin/access_details). +3. Copy your **App ID** and **App Key**. +4. In Job Ops, open **Settings** and paste them into `Adzuna App ID` and `Adzuna App Key` under **Environment & Accounts**. +5. In **Pipeline Run** (Automatic tab), select a compatible country and enable **Adzuna** in Sources. +6. Start the run; Adzuna progress appears in the existing crawl progress stream. + +Default controls: + +- `ADZUNA_APP_ID` +- `ADZUNA_APP_KEY` +- `ADZUNA_MAX_JOBS_PER_TERM` (default `50`) + +Supported countries in this integration: + +- United Kingdom, United States, Austria, Australia, Belgium, Brazil, Canada, Switzerland, Germany, Spain, France, India, Italy, Mexico, Netherlands, New Zealand, Poland, Singapore, South Africa. + +## Common problems + +### Adzuna is disabled in source selection + +- `Adzuna App ID` and `Adzuna App Key` are missing from Settings (or env). + +### Adzuna is skipped for my selected country + +- The selected country is not in the supported list above. + +### Adzuna fails with authorization errors + +- Verify `ADZUNA_APP_ID` and `ADZUNA_APP_KEY` are valid and active in your Adzuna account. + +## Related pages + +- [Extractors Overview](/docs/next/extractors/overview) +- [Pipeline Run](/docs/next/features/pipeline-run) +- [Settings](/docs/next/features/settings) diff --git a/docs-site/docs/extractors/overview.md b/docs-site/docs/extractors/overview.md index f6a2670..203602f 100644 --- a/docs-site/docs/extractors/overview.md +++ b/docs-site/docs/extractors/overview.md @@ -13,12 +13,14 @@ This page helps you choose the right extractor for your run, understand key cons | --- | --- | --- | --- | --- | | [Gradcracker](/docs/next/extractors/gradcracker) | UK graduate roles from Gradcracker | Crawling stability depends on page structure and anti-bot behavior; tuned for low concurrency | `GRADCRACKER_SEARCH_TERMS`, `GRADCRACKER_MAX_JOBS_PER_TERM`, `JOBOPS_SKIP_APPLY_FOR_EXISTING` | Scrapes listing metadata, then detail pages and apply URL resolution | | [JobSpy](/docs/next/extractors/jobspy) | Multi-source discovery (Indeed, LinkedIn, Glassdoor) | Requires Python wrapper execution per term; source availability and quality vary by site/location | `JOBSPY_SITES`, `JOBSPY_SEARCH_TERMS`, `JOBSPY_RESULTS_WANTED`, `JOBSPY_HOURS_OLD`, `JOBSPY_LINKEDIN_FETCH_DESCRIPTION` | Produces JSON per term, then orchestrator normalizes and de-duplicates by `jobUrl` | +| [Adzuna](/docs/next/extractors/adzuna) | API-based multi-country discovery with low scraping overhead | Requires valid App ID/App Key; country must be in Adzuna-supported list | `ADZUNA_APP_ID`, `ADZUNA_APP_KEY`, `ADZUNA_MAX_JOBS_PER_TERM` | API pagination to dataset output; orchestrator maps progress and de-duplicates by `sourceJobId`/`jobUrl` | | [UKVisaJobs](/docs/next/extractors/ukvisajobs) | UK visa sponsorship-focused roles | Requires authenticated session and periodic token/cookie refresh | `UKVISAJOBS_EMAIL`, `UKVISAJOBS_PASSWORD`, `UKVISAJOBS_MAX_JOBS`, `UKVISAJOBS_SEARCH_KEYWORD` | API pagination + dataset output; orchestrator de-dupes and may fetch missing descriptions | | [Manual Import](/docs/next/extractors/manual) | One-off jobs not covered by scrapers | Inference quality depends on model/provider and input quality; some URLs cannot be fetched reliably | App/API endpoints (`/api/manual-jobs/infer`, `/api/manual-jobs/import`) | Accepts text/HTML/URL, runs inference, then saves and scores job after review | ## Which extractor should I use? - Use **JobSpy** for broad first-pass sourcing across common boards. +- Use **Adzuna** when you want API-first discovery in supported non-UK markets. - Use **Gradcracker** when targeting graduate pipelines in the UK. - Use **UKVisaJobs** for sponsorship-specific UK searches. - Use **Manual Import** when you already have a specific posting and need direct import. @@ -29,5 +31,6 @@ Many runs combine sources: broad discovery first, then manual import for high-pr - [Gradcracker](/docs/next/extractors/gradcracker) - [JobSpy](/docs/next/extractors/jobspy) +- [Adzuna](/docs/next/extractors/adzuna) - [UKVisaJobs](/docs/next/extractors/ukvisajobs) - [Manual Import](/docs/next/extractors/manual) diff --git a/docs-site/docs/features/pipeline-run.md b/docs-site/docs/features/pipeline-run.md index 8be3860..2660906 100644 --- a/docs-site/docs/features/pipeline-run.md +++ b/docs-site/docs/features/pipeline-run.md @@ -47,6 +47,7 @@ If values are edited manually, the UI shows **Custom**. - Country selection affects which sources are available. - UK-only sources are disabled for non-UK countries. +- Adzuna is available only for its supported countries and when App ID/App Key are configured in Settings. - Glassdoor can be enabled only when: - selected country supports Glassdoor - a **Glassdoor city** is set in Advanced settings @@ -98,6 +99,11 @@ For accepted input formats, inference behavior, and limits, see [Manual Import E - Verify selected country supports Glassdoor. - Set a Glassdoor city in Advanced settings. +### Adzuna is not selectable + +- Set `Adzuna App ID` and `Adzuna App Key` in **Settings > Environment & Accounts**. +- Verify the selected country is one of Adzuna's supported markets. + ### Run takes longer than expected - Reduce term count. diff --git a/docs-site/docs/features/settings.md b/docs-site/docs/features/settings.md index 0d4723c..ad51acd 100644 --- a/docs-site/docs/features/settings.md +++ b/docs-site/docs/features/settings.md @@ -86,6 +86,7 @@ Settings gives you runtime overrides for the key parts of discovery, scoring, ta - Configure service accounts: - RxResume email/password - UKVisaJobs email/password + - Adzuna app ID/app key - Optional basic authentication for write operations ### Backup diff --git a/docs-site/sidebars.ts b/docs-site/sidebars.ts index e8ed354..25f615c 100644 --- a/docs-site/sidebars.ts +++ b/docs-site/sidebars.ts @@ -41,6 +41,7 @@ const sidebars: SidebarsConfig = { "extractors/overview", "extractors/gradcracker", "extractors/jobspy", + "extractors/adzuna", "extractors/manual", "extractors/ukvisajobs", ], diff --git a/extractors/adzuna/README.md b/extractors/adzuna/README.md new file mode 100644 index 0000000..3d74dd0 --- /dev/null +++ b/extractors/adzuna/README.md @@ -0,0 +1,15 @@ +# Adzuna Extractor + +Minimal extractor that pulls jobs from Adzuna's search API and writes a dataset +for orchestrator ingestion. + +## Environment + +- `ADZUNA_APP_ID` (required) +- `ADZUNA_APP_KEY` (required) +- `ADZUNA_COUNTRY` (default: `gb`) +- `ADZUNA_SEARCH_TERMS` (JSON array or `|` / comma / newline-delimited) +- `ADZUNA_MAX_JOBS_PER_TERM` (default: `50`) +- `ADZUNA_RESULTS_PER_PAGE` (default: `50`, max `50`) +- `ADZUNA_OUTPUT_JSON` (default: `storage/datasets/default/jobs.json`) +- `JOBOPS_EMIT_PROGRESS=1` to emit `JOBOPS_PROGRESS` events diff --git a/extractors/adzuna/package.json b/extractors/adzuna/package.json new file mode 100644 index 0000000..d25523d --- /dev/null +++ b/extractors/adzuna/package.json @@ -0,0 +1,20 @@ +{ + "name": "adzuna-extractor", + "version": "0.0.1", + "type": "module", + "description": "Adzuna extractor - fetches jobs from Adzuna search API", + "main": "src/main.ts", + "dependencies": { + "job-ops-shared": "^1.0.0", + "tsx": "^4.4.0" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "typescript": "~5.9.0" + }, + "scripts": { + "start": "tsx src/main.ts", + "start:dev": "tsx src/main.ts", + "check:types": "tsc --noEmit" + } +} diff --git a/extractors/adzuna/src/main.ts b/extractors/adzuna/src/main.ts new file mode 100644 index 0000000..b03d686 --- /dev/null +++ b/extractors/adzuna/src/main.ts @@ -0,0 +1,245 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { + toNumberOrNull, + toStringOrNull, +} from "job-ops-shared/utils/type-conversion"; + +const API_BASE = "https://api.adzuna.com/v1/api"; +const JOBOPS_PROGRESS_PREFIX = "JOBOPS_PROGRESS "; + +type AdzunaCompany = { display_name?: unknown }; +type AdzunaLocation = { display_name?: unknown }; +type AdzunaJob = { + id?: unknown; + title?: unknown; + description?: unknown; + created?: unknown; + redirect_url?: unknown; + company?: AdzunaCompany; + location?: AdzunaLocation; + salary_min?: unknown; + salary_max?: unknown; + contract_time?: unknown; + contract_type?: unknown; +}; + +type ExtractedJob = { + source: "adzuna"; + sourceJobId?: string; + title: string; + employer: string; + jobUrl: string; + applicationLink: string; + location?: string; + salary?: string; + datePosted?: string; + jobDescription?: string; + jobType?: string; +}; + +function parsePositiveInt(input: string | undefined, fallback: number): number { + const parsed = input ? Number.parseInt(input, 10) : Number.NaN; + if (!Number.isFinite(parsed) || parsed < 1) return fallback; + return parsed; +} + +function parseSearchTerms(raw: string | undefined): string[] { + if (!raw || raw.trim().length === 0) return ["web developer"]; + + const trimmed = raw.trim(); + if (trimmed.startsWith("[")) { + try { + const parsed = JSON.parse(trimmed) as unknown; + if (Array.isArray(parsed)) { + const terms = parsed + .map((value) => toStringOrNull(value)) + .filter((value): value is string => value !== null); + if (terms.length > 0) return terms; + } + } catch { + // Fall through to delimiter parsing. + } + } + + const delimiter = trimmed.includes("|") + ? "|" + : trimmed.includes("\n") + ? "\n" + : ","; + const terms = trimmed + .split(delimiter) + .map((value) => value.trim()) + .filter(Boolean); + return terms.length > 0 ? terms : ["web developer"]; +} + +function requireEnv(name: string): string { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`Missing required environment variable: ${name}`); + return value; +} + +function emitProgress(payload: Record): void { + if (process.env.JOBOPS_EMIT_PROGRESS !== "1") return; + console.log(`${JOBOPS_PROGRESS_PREFIX}${JSON.stringify(payload)}`); +} + +function formatSalary(job: AdzunaJob): string | null { + const min = toNumberOrNull(job.salary_min); + const max = toNumberOrNull(job.salary_max); + + if (min === null && max === null) return null; + + if (min !== null && max !== null) { + return `${Math.round(min)}-${Math.round(max)}`; + } + if (min !== null) return `${Math.round(min)}+`; + if (max !== null) return `${Math.round(max)}`; + return null; +} + +function mapJob(raw: AdzunaJob): ExtractedJob | null { + const id = toStringOrNull(raw.id); + const title = toStringOrNull(raw.title) ?? "Unknown Title"; + const employer = + toStringOrNull(raw.company?.display_name) ?? "Unknown Employer"; + const jobUrl = toStringOrNull(raw.redirect_url); + if (!jobUrl) return null; + + const contractType = toStringOrNull(raw.contract_type); + const contractTime = toStringOrNull(raw.contract_time); + const jobType = [contractType, contractTime].filter(Boolean).join(" / "); + + return { + source: "adzuna", + sourceJobId: id ?? undefined, + title, + employer, + jobUrl, + applicationLink: jobUrl, + location: toStringOrNull(raw.location?.display_name) ?? undefined, + salary: formatSalary(raw) ?? undefined, + datePosted: toStringOrNull(raw.created) ?? undefined, + jobDescription: toStringOrNull(raw.description) ?? undefined, + jobType: jobType || undefined, + }; +} + +async function fetchJobsPage(args: { + country: string; + page: number; + appId: string; + appKey: string; + what: string; + resultsPerPage: number; +}): Promise { + const url = new URL(`${API_BASE}/jobs/${args.country}/search/${args.page}`); + url.searchParams.set("app_id", args.appId); + url.searchParams.set("app_key", args.appKey); + if (args.what) { + url.searchParams.set("what", args.what); + } + url.searchParams.set("results_per_page", String(args.resultsPerPage)); + + const response = await fetch(url.toString(), { + headers: { Accept: "application/json" }, + }); + if (!response.ok) { + throw new Error(`Adzuna request failed with status ${response.status}`); + } + + const body = (await response.json()) as { results?: unknown }; + if (!Array.isArray(body.results)) return []; + return body.results as AdzunaJob[]; +} + +async function run(): Promise { + const appId = requireEnv("ADZUNA_APP_ID"); + const appKey = requireEnv("ADZUNA_APP_KEY"); + const country = (process.env.ADZUNA_COUNTRY || "gb").trim().toLowerCase(); + const maxJobsPerTerm = parsePositiveInt( + process.env.ADZUNA_MAX_JOBS_PER_TERM, + 50, + ); + const configuredResultsPerPage = parsePositiveInt( + process.env.ADZUNA_RESULTS_PER_PAGE, + 50, + ); + const resultsPerPage = Math.min(50, configuredResultsPerPage); + const searchTerms = parseSearchTerms(process.env.ADZUNA_SEARCH_TERMS); + const outputJson = + process.env.ADZUNA_OUTPUT_JSON || + join(process.cwd(), "storage/datasets/default/jobs.json"); + + const jobs: ExtractedJob[] = []; + + for (let i = 0; i < searchTerms.length; i += 1) { + const searchTerm = searchTerms[i]; + const termIndex = i + 1; + + emitProgress({ + event: "term_start", + termIndex, + termTotal: searchTerms.length, + searchTerm, + }); + + let page = 1; + let termCount = 0; + while (termCount < maxJobsPerTerm) { + const remaining = maxJobsPerTerm - termCount; + const take = Math.min(resultsPerPage, remaining); + const pageResults = await fetchJobsPage({ + country, + page, + appId, + appKey, + what: searchTerm, + resultsPerPage: take, + }); + + let mappedOnPage = 0; + for (const raw of pageResults) { + if (termCount >= maxJobsPerTerm) break; + const mapped = mapJob(raw); + if (!mapped) continue; + jobs.push(mapped); + termCount += 1; + mappedOnPage += 1; + } + + emitProgress({ + event: "page_fetched", + termIndex, + termTotal: searchTerms.length, + searchTerm, + pageNo: page, + resultsOnPage: mappedOnPage, + totalCollected: termCount, + }); + + if (pageResults.length < take) break; + page += 1; + if (page > 100) break; + } + + emitProgress({ + event: "term_complete", + termIndex, + termTotal: searchTerms.length, + searchTerm, + jobsFoundTerm: termCount, + }); + } + + await mkdir(dirname(outputJson), { recursive: true }); + await writeFile(outputJson, `${JSON.stringify(jobs, null, 2)}\n`, "utf-8"); + console.log(`Adzuna extractor wrote ${jobs.length} jobs`); +} + +run().catch((error: unknown) => { + const message = error instanceof Error ? error.message : "Unknown error"; + console.error(`Adzuna extractor failed: ${message}`); + process.exitCode = 1; +}); diff --git a/extractors/adzuna/tsconfig.json b/extractors/adzuna/tsconfig.json new file mode 100644 index 0000000..6ace792 --- /dev/null +++ b/extractors/adzuna/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "outDir": "dist", + "strict": true, + "noUnusedLocals": false, + "lib": ["ES2022", "DOM"], + "types": ["node"] + }, + "include": ["./src/**/*"] +} diff --git a/orchestrator/src/client/components/PipelineProgress.tsx b/orchestrator/src/client/components/PipelineProgress.tsx index 38defd0..1c1f40e 100644 --- a/orchestrator/src/client/components/PipelineProgress.tsx +++ b/orchestrator/src/client/components/PipelineProgress.tsx @@ -24,7 +24,7 @@ interface PipelineProgress { | "failed"; message: string; detail?: string; - crawlingSource: "gradcracker" | "jobspy" | "ukvisajobs" | null; + crawlingSource: "gradcracker" | "jobspy" | "ukvisajobs" | "adzuna" | null; crawlingSourcesCompleted: number; crawlingSourcesTotal: number; crawlingTermsProcessed: number; @@ -84,6 +84,7 @@ const sourceLabel: Record< gradcracker: "Gradcracker", jobspy: "JobSpy", ukvisajobs: "UKVisaJobs", + adzuna: "Adzuna", }; const clamp = (value: number, min: number, max: number) => diff --git a/orchestrator/src/client/pages/OrchestratorPage.test.tsx b/orchestrator/src/client/pages/OrchestratorPage.test.tsx index a441fe5..13f6892 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.test.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.test.tsx @@ -695,6 +695,7 @@ describe("OrchestratorPage", () => { jobspyResultsWanted: 150, gradcrackerMaxJobsPerTerm: 150, ukvisajobsMaxJobs: 150, + adzunaMaxJobsPerTerm: 150, jobspyCountryIndeed: "united kingdom", jobspyLocation: "United Kingdom", }); diff --git a/orchestrator/src/client/pages/OrchestratorPage.tsx b/orchestrator/src/client/pages/OrchestratorPage.tsx index de86dc3..4917220 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.tsx @@ -294,6 +294,7 @@ export const OrchestratorPage: React.FC = () => { jobspyResultsWanted: limits.jobspyResultsWanted, gradcrackerMaxJobsPerTerm: limits.gradcrackerMaxJobsPerTerm, ukvisajobsMaxJobs: limits.ukvisajobsMaxJobs, + adzunaMaxJobsPerTerm: limits.adzunaMaxJobsPerTerm, jobspyCountryIndeed: values.country, jobspyLocation, }); diff --git a/orchestrator/src/client/pages/SettingsPage.tsx b/orchestrator/src/client/pages/SettingsPage.tsx index 66f80c1..2ec7072 100644 --- a/orchestrator/src/client/pages/SettingsPage.tsx +++ b/orchestrator/src/client/pages/SettingsPage.tsx @@ -57,6 +57,8 @@ const DEFAULT_FORM_VALUES: UpdateSettingsInput = { basicAuthPassword: "", ukvisajobsEmail: "", ukvisajobsPassword: "", + adzunaAppId: "", + adzunaAppKey: "", webhookSecret: "", enableBasicAuth: false, backupEnabled: null, @@ -96,6 +98,9 @@ const NULL_SETTINGS_PAYLOAD: UpdateSettingsInput = { basicAuthPassword: null, ukvisajobsEmail: null, ukvisajobsPassword: null, + adzunaAppId: null, + adzunaAppKey: null, + adzunaMaxJobsPerTerm: null, webhookSecret: null, enableBasicAuth: undefined, backupEnabled: null, @@ -129,6 +134,8 @@ const mapSettingsToForm = (data: AppSettings): UpdateSettingsInput => ({ basicAuthPassword: "", ukvisajobsEmail: data.ukvisajobsEmail ?? "", ukvisajobsPassword: "", + adzunaAppId: data.adzunaAppId ?? "", + adzunaAppKey: "", webhookSecret: "", enableBasicAuth: data.basicAuthActive, backupEnabled: data.overrideBackupEnabled, @@ -235,11 +242,13 @@ const getDerivedSettings = (settings: AppSettings | null) => { readable: { rxresumeEmail: settings?.rxresumeEmail ?? "", ukvisajobsEmail: settings?.ukvisajobsEmail ?? "", + adzunaAppId: settings?.adzunaAppId ?? "", basicAuthUser: settings?.basicAuthUser ?? "", }, private: { rxresumePasswordHint: settings?.rxresumePasswordHint ?? null, ukvisajobsPasswordHint: settings?.ukvisajobsPasswordHint ?? null, + adzunaAppKeyHint: settings?.adzunaAppKeyHint ?? null, basicAuthPasswordHint: settings?.basicAuthPasswordHint ?? null, webhookSecretHint: settings?.webhookSecretHint ?? null, }, @@ -527,6 +536,10 @@ export const SettingsPage: React.FC = () => { envPayload.ukvisajobsEmail = normalizeString(data.ukvisajobsEmail); } + if (dirtyFields.adzunaAppId || dirtyFields.adzunaAppKey) { + envPayload.adzunaAppId = normalizeString(data.adzunaAppId); + } + if (data.enableBasicAuth === false) { envPayload.basicAuthUser = null; envPayload.basicAuthPassword = null; @@ -568,6 +581,11 @@ export const SettingsPage: React.FC = () => { if (value !== undefined) envPayload.ukvisajobsPassword = value; } + if (dirtyFields.adzunaAppKey) { + const value = normalizePrivateInput(data.adzunaAppKey); + if (value !== undefined) envPayload.adzunaAppKey = value; + } + if (dirtyFields.webhookSecret) { const value = normalizePrivateInput(data.webhookSecret); if (value !== undefined) envPayload.webhookSecret = value; diff --git a/orchestrator/src/client/pages/orchestrator/AutomaticRunTab.tsx b/orchestrator/src/client/pages/orchestrator/AutomaticRunTab.tsx index ea2d24a..f0779b2 100644 --- a/orchestrator/src/client/pages/orchestrator/AutomaticRunTab.tsx +++ b/orchestrator/src/client/pages/orchestrator/AutomaticRunTab.tsx @@ -153,6 +153,7 @@ export const AutomaticRunTab: React.FC = ({ const rememberedRunBudget = settings?.jobspyResultsWanted ?? + settings?.adzunaMaxJobsPerTerm ?? settings?.gradcrackerMaxJobsPerTerm ?? settings?.ukvisajobsMaxJobs ?? DEFAULT_VALUES.runBudget; @@ -533,7 +534,9 @@ export const AutomaticRunTab: React.FC = ({ ? countryAllowed ? GLASSDOOR_LOCATION_REASON : GLASSDOOR_COUNTRY_REASON - : `${sourceLabel[source]} is available only when country is United Kingdom.`; + : source === "gradcracker" || source === "ukvisajobs" + ? `${sourceLabel[source]} is available only when country is United Kingdom.` + : `${sourceLabel[source]} is not available for the selected country.`; const button = (