Scanner: neue Anzeigen zuerst abfragen, Uhrzeiten korrekt anzeigen

Seit dem Wegfall der Altersgrenze lieferten BA und StepStone nur die
relevantesten Anzeigen aller Jahre - fünf Läufe lang 0 neue Treffer.
- BA: erst veroeffentlichtseit=7 (bis 200), dann ohne Grenze (bis 200)
- StepStone: sort=2 Seiten 1-2 plus Relevanz Seite 1
  (action=sort_publish hatte keine Wirkung)
- Dashboard liest DB-Zeitstempel als UTC (vorher 2 h zu früh)

Erster Lauf danach: 481 statt 298 Anzeigen, 40 neue Treffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-29 17:28:04 +02:00
co-authored by Claude Opus 5.5
parent 0aa8f17b28
commit 651ad2b954
3 changed files with 69 additions and 10 deletions
+46 -6
View File
@@ -129,12 +129,33 @@ const slug = (keyword) =>
* Bundesagentur fuer Arbeit - offizielle Jobsuche-API (JSON, ohne Anmeldung;
* der Schluessel "jobboerse-jobsuche" ist der oeffentliche der Jobsuche-App).
* ---------------------------------------------------------------------- */
/*
* Zwei Abfragen je Begriff (29.09.2026): ohne Altersgrenze liefert die API
* 100 Anzeigen quer durch alle Jahre - neue waren darin Zufall, der Scanner
* fand stundenlang nichts Neues. Deshalb zuerst alles der letzten 7 Tage
* (bis 200), danach die ersten 200 ohne Grenze fuer den Altbestand.
*/
const BA_PAGE = 100;
async function baPage(keyword, page, since) {
const params = { was: keyword, size: String(BA_PAGE), page: String(page) };
if (since) params.veroeffentlichtseit = String(since);
const data = await request(
`https://rest.arbeitsagentur.de/jobboerse/jobsuche-service/pc/v6/jobs?${new URLSearchParams(params)}`,
{ json: true, headers: { 'X-API-Key': 'jobboerse-jobsuche' } }
);
return data?.ergebnisliste ?? data?.stellenangebote ?? [];
}
async function fetchBa(keyword) {
const url =
'https://rest.arbeitsagentur.de/jobboerse/jobsuche-service/pc/v6/jobs?' +
new URLSearchParams({ was: keyword, size: '100', page: '1' });
const data = await request(url, { json: true, headers: { 'X-API-Key': 'jobboerse-jobsuche' } });
const list = data?.ergebnisliste ?? data?.stellenangebote ?? [];
const list = [];
for (const since of [7, null]) {
for (let page = 1; page <= 2; page += 1) {
const chunk = await baPage(keyword, page, since);
list.push(...chunk);
if (chunk.length < BA_PAGE) break;
}
}
return list
.filter((job) => !/AUSBILDUNG|PRAKTIKUM/i.test(job.stellenangebotsart ?? job.angebotsart ?? ''))
@@ -178,9 +199,28 @@ async function fetchBa(keyword) {
/* ------------------------------------------------------------------------
* StepStone.de - Trefferliste als HTML, jede Karte traegt data-at-Attribute.
* ---------------------------------------------------------------------- */
/*
* Drei Seiten je Begriff: sort=2 ("neueste zuerst", Seiten 1 und 2) fuer
* frische Anzeigen, dazu Seite 1 nach Relevanz fuer den Altbestand. Der
* frueher benutzte Parameter action=sort_publish wirkt nicht - StepStone
* lieferte damit immer dieselben 25 "relevantesten" Anzeigen.
*/
const STEPSTONE_PAGES = ['?sort=2', '?sort=2&page=2', ''];
async function fetchStepstone(keyword) {
const base = 'https://www.stepstone.de';
const html = await request(`${base}/jobs/${encodeURIComponent(slug(keyword))}?action=sort_publish`);
const byId = new Map();
for (const [index, query] of STEPSTONE_PAGES.entries()) {
if (index > 0) await new Promise((resolve) => setTimeout(resolve, 800));
const html = await request(`${base}/jobs/${encodeURIComponent(slug(keyword))}${query}`);
for (const job of parseStepstone(html, base)) {
if (!byId.has(job.externalId)) byId.set(job.externalId, job);
}
}
return [...byId.values()];
}
function parseStepstone(html, base) {
const $ = cheerio.load(html);
// StepStone rendert CSS-in-JS: <style> steckt mitten in den Karten und
// landete sonst in Titel und Firmenname.