diff --git a/server/src/scanner/matching.js b/server/src/scanner/matching.js index 1578d38..8bf8220 100644 --- a/server/src/scanner/matching.js +++ b/server/src/scanner/matching.js @@ -63,7 +63,7 @@ export function scoreJob(agent, job) { * "Müller Holding GmbH & Co. KG" -> "müller holding". */ const LEGAL_FORMS = - /\b(gmbh|mbh|ag|se|kg|kgaa|ohg|gbr|ug|e\.?\s?v|e\.?\s?k|co|inc|ltd|llc|plc|sa|sarl|bv|nv|haftungsbeschränkt|gesellschaft mit beschränkter haftung|aktiengesellschaft)\b\.?/g; + /\b(gmbh|gesmbh|mbh|ag|se|kg|kgaa|ohg|gbr|ug|e\.?\s?v|e\.?\s?k|co|inc|ltd|llc|plc|sa|sarl|bv|nv|haftungsbeschränkt|gesellschaft mit beschränkter haftung|aktiengesellschaft)\b\.?/g; export function normalizeCompany(name) { return normalize(name) diff --git a/server/src/scanner/sources.js b/server/src/scanner/sources.js index 7143d8d..0313457 100644 --- a/server/src/scanner/sources.js +++ b/server/src/scanner/sources.js @@ -136,21 +136,37 @@ async function fetchBa(keyword, { maxAge }) { return list .filter((job) => !/AUSBILDUNG|PRAKTIKUM/i.test(job.stellenangebotsart ?? job.angebotsart ?? '')) .map((job) => { - const refnr = job.refnr ?? job.referenznummer ?? job.hashId; - const place = job.arbeitsort ?? job.arbeitsorte?.[0] ?? {}; + // v6 (Stand 29.09.2026): firma, stellenlokationen[].adresse, + // datumErsteVeroeffentlichung, externeURL, vertragsdauer. Die v4-Namen + // bleiben als Rueckfall stehen. + const refnr = job.referenznummer ?? job.refnr ?? job.hashId; + const place = job.stellenlokationen?.[0]?.adresse ?? job.arbeitsort ?? job.arbeitsorte?.[0] ?? {}; const land = clean(place.land); return { source: 'ba', externalId: String(refnr ?? job.stellenangebotsTitel ?? job.titel), title: clean(job.stellenangebotsTitel ?? job.titel ?? job.beruf), - company: clean(job.arbeitgeber ?? job.arbeitgeberName) || null, + company: clean(job.firma ?? job.arbeitgeber ?? job.arbeitgeberName) || null, location: clean([place.plz, place.ort].filter(Boolean).join(' ')) || clean(place.region) || null, // Die BA fuehrt auch Stellen ausserhalb DACH - die fallen unten raus. - country: !land || /deutschland/i.test(land) ? 'DE' : /österreich/i.test(land) ? 'AT' : /schweiz/i.test(land) ? 'CH' : null, - employmentType: /SELBST/i.test(job.stellenangebotsart ?? '') ? 'Freiberuflich' : 'Anstellung', - url: job.externeUrl || `https://www.arbeitsagentur.de/jobsuche/jobdetail/${encodeURIComponent(refnr)}`, - description: clean(job.beruf) || null, - publishedAt: toDate(job.aktuelleVeroeffentlichungsdatum ?? job.veroeffentlichungsdatum ?? job.modifikationsTimestamp), + // Die API schreibt ohne Umlaut: DEUTSCHLAND, OESTERREICH, SCHWEIZ. + country: !land || /deutschland/i.test(land) ? 'DE' : /(ö|oe)sterreich/i.test(land) ? 'AT' : /schweiz/i.test(land) ? 'CH' : null, + employmentType: /SELBST/i.test(job.stellenangebotsart ?? '') + ? 'Freiberuflich' + : job.vertragsdauer === 'UNBEFRISTET' + ? 'Festanstellung' + : job.vertragsdauer === 'BEFRISTET' + ? 'Befristet' + : 'Anstellung', + // Die Detailseite der BA ist stabiler als der externe Bewerbungslink. + url: `https://www.arbeitsagentur.de/jobsuche/jobdetail/${encodeURIComponent(refnr)}`, + description: clean((job.alleBerufe ?? [job.hauptberuf ?? job.beruf]).filter(Boolean).join(', ')) || null, + publishedAt: toDate( + job.datumErsteVeroeffentlichung ?? + job.veroeffentlichungszeitraum?.von ?? + job.aktuelleVeroeffentlichungsdatum ?? + job.veroeffentlichungsdatum + ), }; }) .filter((job) => job.title && job.externalId && job.country); @@ -163,6 +179,9 @@ async function fetchStepstone(keyword) { const base = 'https://www.stepstone.de'; const html = await request(`${base}/jobs/${encodeURIComponent(slug(keyword))}?action=sort_publish`); const $ = cheerio.load(html); + // StepStone rendert CSS-in-JS: