Add per-profile keyword sets, job source settings, sponsorship signal pills, and several ATS extractors. Fix Hiring Cafe discovery via Next.js SSR search, profile activate for comma-separated basicAuthUser aliases, and resume path backfill migrations. Update settings and hiring-cafe docs; localhost compose overlay for loopback-only deploys.
90 lines
2.6 KiB
TypeScript
90 lines
2.6 KiB
TypeScript
/**
|
|
* Direct company career pages — auto-detect ATS backends and pull public job feeds.
|
|
*/
|
|
|
|
import type {
|
|
ExtractorManifest,
|
|
ExtractorRunResult,
|
|
} from "@shared/types/extractors";
|
|
import type { CreateJobInput } from "@shared/types/jobs";
|
|
import {
|
|
detectAtsTargets,
|
|
fetchCareersPage,
|
|
fetchJobsForTarget,
|
|
readUrlList,
|
|
} from "./src/resolve-ats.js";
|
|
|
|
function matchesTerm(job: CreateJobInput, term: string): boolean {
|
|
const lower = term.toLowerCase();
|
|
if (job.title.toLowerCase().includes(lower)) return true;
|
|
if (job.employer.toLowerCase().includes(lower)) return true;
|
|
if (job.jobDescription?.toLowerCase().includes(lower)) return true;
|
|
return false;
|
|
}
|
|
|
|
export const manifest: ExtractorManifest = {
|
|
id: "careerspages",
|
|
displayName: "Career Pages",
|
|
providesSources: ["careerspages"],
|
|
async run(context): Promise<ExtractorRunResult> {
|
|
if (context.shouldCancel?.()) return { success: true, jobs: [] };
|
|
|
|
const pageUrls = readUrlList(context.settings.careersPageUrls);
|
|
if (pageUrls.length === 0) {
|
|
return {
|
|
success: true,
|
|
jobs: [],
|
|
error:
|
|
"No career page URLs configured. Set CAREERS_PAGE_URLS or careersPageUrls (JSON array or comma/newline-separated URLs).",
|
|
};
|
|
}
|
|
|
|
const terms = context.searchTerms.length > 0 ? context.searchTerms : [];
|
|
const seen = new Set<string>();
|
|
const out: CreateJobInput[] = [];
|
|
|
|
try {
|
|
for (let i = 0; i < pageUrls.length; i += 1) {
|
|
if (context.shouldCancel?.()) break;
|
|
const pageUrl = pageUrls[i];
|
|
|
|
context.onProgress?.({
|
|
phase: "list",
|
|
termsProcessed: i,
|
|
termsTotal: pageUrls.length,
|
|
currentUrl: pageUrl,
|
|
detail: `Career pages: resolving ATS for ${pageUrl}`,
|
|
});
|
|
|
|
const html = await fetchCareersPage(pageUrl);
|
|
const targets = detectAtsTargets(html, pageUrl);
|
|
if (targets.length === 0) continue;
|
|
|
|
for (const target of targets) {
|
|
if (context.shouldCancel?.()) break;
|
|
const jobs = await fetchJobsForTarget(target);
|
|
for (const job of jobs) {
|
|
if (
|
|
terms.length > 0 &&
|
|
!terms.some((term) => matchesTerm(job, term))
|
|
) {
|
|
continue;
|
|
}
|
|
const key = job.jobUrl;
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
out.push(job);
|
|
}
|
|
}
|
|
}
|
|
|
|
return { success: true, jobs: out };
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
return { success: false, jobs: out, error: message };
|
|
}
|
|
},
|
|
};
|
|
|
|
export default manifest;
|