From e0f6ef06cdb93029df535a23cad7fc2d70382064 Mon Sep 17 00:00:00 2001 From: schakal00783 Date: Tue, 29 Sep 2026 11:24:05 +0200 Subject: [PATCH] Scanner: Jooble entfernt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quelle, Schlüsselfeld in Einstellungen -> Scanner und Doku raus. Agenten, die jooble noch gespeichert haben, bekommen es nicht mehr angezeigt; die Pipeline überspringt unbekannte Quellen ohnehin. Co-Authored-By: Claude Opus 5.5 --- docs/08-anleitung-benutzer.md | 6 ++-- docs/12-scanner.md | 5 +-- server/src/routes/scanner.js | 5 +-- server/src/scanner/settings.js | 1 - server/src/scanner/sources.js | 34 --------------------- src/components/scanner/ScannerAgents.tsx | 4 +-- src/components/scanner/scannerUi.tsx | 3 +- src/components/settings/ScannerSettings.tsx | 19 ++++-------- src/lib/api.ts | 5 ++- 9 files changed, 20 insertions(+), 62 deletions(-) diff --git a/docs/08-anleitung-benutzer.md b/docs/08-anleitung-benutzer.md index c928292..907e3b7 100644 --- a/docs/08-anleitung-benutzer.md +++ b/docs/08-anleitung-benutzer.md @@ -189,7 +189,7 @@ Kontakt (*Kontakt → Stundensatz vor Ort / remote*). ## Scanner -Sucht in Jobbörsen (Agentur für Arbeit, StepStone, karriere.at, Adzuna, Jooble, +Sucht in Jobbörsen (Agentur für Arbeit, StepStone, karriere.at, Adzuna, freelancermap) nach offenen Stellen in DACH, höchstens 21 Tage alt. Technik siehe [12 Scanner](12-scanner.md). @@ -206,8 +206,8 @@ siehe [12 Scanner](12-scanner.md). zur Börse. Status setzen: *In Bearbeitung*, wenn sich jemand kümmert, *Verworfen*, wenn es nicht passt. -Zeitplan (Wochentage, Uhrzeit, Intervall, Höchstalter) und die kostenlosen -Schlüssel für Adzuna und Jooble pflegt ein Administrator unter +Zeitplan (Wochentage, Uhrzeit, Intervall, Höchstalter) und den kostenlosen +Schlüssel für Adzuna pflegt ein Administrator unter *Einstellungen → Scanner*. ## Historie diff --git a/docs/12-scanner.md b/docs/12-scanner.md index df43374..4e790fd 100644 --- a/docs/12-scanner.md +++ b/docs/12-scanner.md @@ -20,10 +20,11 @@ einfacher HTTP-Abruf (`server/src/scanner/sources.js`). | `stepstone` | StepStone.de | DE | HTML der Trefferliste, `data-at`-Attribute | keiner | | `karriere` | karriere.at | AT | HTML der Trefferliste, `.m-jobsListItem` | keiner | | `adzuna` | Adzuna | DE · AT · CH | REST-API, `max_days_old` | kostenlos, App ID + Key | -| `jooble` | Jooble | DE · AT · CH | REST-API je Land, `datecreatedfrom` | kostenlos, API-Key | | `freelancermap` | freelancermap | DE | HTML, Selektoren wie im perinco-Scanner | keiner | Adzuna ist die einzige Quelle für die Schweiz (jobs.ch blockt mit CAPTCHA). +Jooble war anfangs dabei und ist am 29.09.2026 auf Wunsch wieder entfallen; +Agenten, die es noch in ihrer Liste tragen, überspringen es stillschweigend. Eine Quelle ohne Schlüssel wird übersprungen, der Lauf geht mit den anderen weiter. Scheitert eine Quelle, steht das im Lauf-Protokoll und als Hinweis über der Trefferliste. @@ -142,7 +143,7 @@ Liste neu, sobald er fertig ist. | `scanner_from` / `scanner_to` | `06:00` / `20:00` | Fenster, deutsche Zeit | | `scanner_interval` | `60` | Minuten, 15 bis 1440 | | `scanner_max_age` | `21` | Tage, 1 bis 90 | -| `scanner_adzuna_id`, `scanner_adzuna_key`, `scanner_jooble_key` | leer | gehen nie im Klartext zum Browser | +| `scanner_adzuna_id`, `scanner_adzuna_key` | leer | gehen nie im Klartext zum Browser | | `scanner_last_scan_at` | – | Beginn des letzten Laufs (ISO) | ## Tabellen diff --git a/server/src/routes/scanner.js b/server/src/routes/scanner.js index e7c6c58..ea0afb6 100644 --- a/server/src/routes/scanner.js +++ b/server/src/routes/scanner.js @@ -31,7 +31,8 @@ const agentOut = (row) => ({ id: row.id, name: row.name, keywords: parseList(row.keywords), - sources: parseList(row.sources), + // Nur Quellen, die es noch gibt (Jooble ist am 29.09.2026 entfallen). + sources: parseList(row.sources).filter((id) => SOURCE_IDS.includes(id)), excludes: parseList(row.excludes), is_active: Boolean(row.is_active), matches: Number(row.matches ?? 0), @@ -323,7 +324,7 @@ router.get('/settings', requireAdmin, async (req, res, next) => { router.patch('/settings', requireAdmin, async (req, res, next) => { try { const settings = await saveScannerSettings(req.body ?? {}); - const touchedKeys = ['adzunaId', 'adzunaKey', 'joobleKey'].filter((key) => req.body?.[key] !== undefined); + const touchedKeys = ['adzunaId', 'adzunaKey'].filter((key) => req.body?.[key] !== undefined); await logHistory({ entityType: 'system', entityLabel: 'Scanner', diff --git a/server/src/scanner/settings.js b/server/src/scanner/settings.js index 1a457ea..22683a3 100644 --- a/server/src/scanner/settings.js +++ b/server/src/scanner/settings.js @@ -22,7 +22,6 @@ export const DEFAULTS = { const SECRET_KEYS = { adzunaId: 'scanner_adzuna_id', adzunaKey: 'scanner_adzuna_key', - joobleKey: 'scanner_jooble_key', }; const TIME = /^([01]\d|2[0-3]):[0-5]\d$/; diff --git a/server/src/scanner/sources.js b/server/src/scanner/sources.js index 0313457..c7d5455 100644 --- a/server/src/scanner/sources.js +++ b/server/src/scanner/sources.js @@ -26,7 +26,6 @@ export const SOURCES = { stepstone: { label: 'StepStone', countries: 'DE', needsKey: null, fetch: fetchStepstone }, karriere: { label: 'karriere.at', countries: 'AT', needsKey: null, fetch: fetchKarriere }, adzuna: { label: 'Adzuna', countries: 'DE · AT · CH', needsKey: 'adzuna', fetch: fetchAdzuna }, - jooble: { label: 'Jooble', countries: 'DE · AT · CH', needsKey: 'jooble', fetch: fetchJooble }, freelancermap: { label: 'freelancermap', countries: 'DE', needsKey: null, fetch: fetchFreelancermap }, }; @@ -37,7 +36,6 @@ export function sourceReady(id, secrets) { const needs = SOURCES[id]?.needsKey; if (!needs) return true; if (needs === 'adzuna') return Boolean(secrets.adzunaId && secrets.adzunaKey); - if (needs === 'jooble') return Boolean(secrets.joobleKey); return false; } @@ -291,38 +289,6 @@ async function fetchAdzuna(keyword, { maxAge, secrets }) { return jobs.filter((job) => job.title && job.url); } -/* ------------------------------------------------------------------------ - * Jooble - kostenloser API-Schluessel (jooble.org/api/about), Sammler fuer DACH. - * Je Land ein eigener Host. - * ---------------------------------------------------------------------- */ -async function fetchJooble(keyword, { maxAge, secrets }) { - const since = new Date(Date.now() - maxAge * 24 * 3_600_000).toISOString().slice(0, 10); - const jobs = []; - for (const country of ['de', 'at', 'ch']) { - const data = await request(`https://${country}.jooble.org/api/${encodeURIComponent(secrets.joobleKey)}`, { - json: true, - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ keywords: keyword, datecreatedfrom: since, page: '1' }), - }); - for (const job of data?.jobs ?? []) { - jobs.push({ - source: 'jooble', - externalId: String(job.id ?? job.link), - title: clean(job.title).replace(/<[^>]+>/g, ''), - company: clean(job.company) || null, - location: clean(job.location) || null, - country: country.toUpperCase(), - employmentType: clean(job.type) || null, - url: job.link, - description: clean(job.snippet).replace(/<[^>]+>/g, '').replace(/ /g, ' ') || null, - publishedAt: toDate(job.updated), - }); - } - } - return jobs.filter((job) => job.title && job.url); -} - /* ------------------------------------------------------------------------ * freelancermap - Projekte fuer Freiberufler und Interim, neueste zuerst. * Selektoren wie im Scanner des perinco CRM. diff --git a/src/components/scanner/ScannerAgents.tsx b/src/components/scanner/ScannerAgents.tsx index c97dfd1..b4f9628 100644 --- a/src/components/scanner/ScannerAgents.tsx +++ b/src/components/scanner/ScannerAgents.tsx @@ -281,7 +281,7 @@ export const ScannerAgents: React.FC = () => { {sources.some((item) => !item.ready) && (

- Adzuna und Jooble brauchen einen kostenlosen API-Schlüssel + Adzuna braucht einen kostenlosen API-Schlüssel {isAdmin ? ( <> {' '} @@ -300,7 +300,7 @@ export const ScannerAgents: React.FC = () => { ) : ( ' (Administrator: Einstellungen → Scanner)' )} - . Ohne Schlüssel werden sie übersprungen. + . Ohne Schlüssel wird es übersprungen.

)} diff --git a/src/components/scanner/scannerUi.tsx b/src/components/scanner/scannerUi.tsx index 60237e4..6ef68ce 100644 --- a/src/components/scanner/scannerUi.tsx +++ b/src/components/scanner/scannerUi.tsx @@ -8,11 +8,10 @@ export const SOURCE_LABELS: Record = { stepstone: 'StepStone', karriere: 'karriere.at', adzuna: 'Adzuna', - jooble: 'Jooble', freelancermap: 'freelancermap', }; -export const SOURCE_ORDER: ScannerSourceId[] = ['ba', 'stepstone', 'karriere', 'adzuna', 'jooble', 'freelancermap']; +export const SOURCE_ORDER: ScannerSourceId[] = ['ba', 'stepstone', 'karriere', 'adzuna', 'freelancermap']; export const STATUS_LABELS: Record = { neu: 'Neu', diff --git a/src/components/settings/ScannerSettings.tsx b/src/components/settings/ScannerSettings.tsx index 7feb897..f310ae0 100644 --- a/src/components/settings/ScannerSettings.tsx +++ b/src/components/settings/ScannerSettings.tsx @@ -8,7 +8,7 @@ import { WEEKDAY_LABELS } from '../scanner/scannerUi'; * Reiter "Scanner" in den Einstellungen (nur Administratoren). * * Wann und wie oft der Scanner selbst sucht, wie alt Anzeigen höchstens sein - * dürfen, und die kostenlosen API-Schlüssel für Adzuna und Jooble. Die + * dürfen, und der kostenlose API-Schlüssel für Adzuna. Die * Schlüssel kommen - wie der OpenAI-Schlüssel - nie zurück zum Browser. */ @@ -18,7 +18,7 @@ export const ScannerSettings: React.FC = () => { const { toast } = useToast(); const [schedule, setSchedule] = useState(null); const [secrets, setSecrets] = useState(null); - const [keys, setKeys] = useState({ adzunaId: '', adzunaKey: '', joobleKey: '' }); + const [keys, setKeys] = useState({ adzunaId: '', adzunaKey: '' }); const [busy, setBusy] = useState(false); useEffect(() => { @@ -37,7 +37,7 @@ export const ScannerSettings: React.FC = () => { const data = await scannerApi.saveSettings(changes); setSchedule(data.settings); setSecrets(data.secrets); - setKeys({ adzunaId: '', adzunaKey: '', joobleKey: '' }); + setKeys({ adzunaId: '', adzunaKey: '' }); toast('Gespeichert.', 'success'); } catch (err) { toast(err instanceof Error ? err.message : 'Speichern fehlgeschlagen.', 'error'); @@ -134,11 +134,11 @@ export const ScannerSettings: React.FC = () => {

API-Schlüssel

Agentur für Arbeit, StepStone, karriere.at und freelancermap brauchen nichts. Adzuna (einzige Quelle für - die Schweiz) und Jooble geben nach kostenloser Registrierung einen Schlüssel aus. Ohne Schlüssel werden - sie übersprungen. + die Schweiz) gibt nach kostenloser Registrierung einen Schlüssel aus. Ohne Schlüssel wird es + übersprungen.

-
+
{ value={keys.adzunaKey} onChange={(adzunaKey) => setKeys({ ...keys, adzunaKey })} /> - setKeys({ ...keys, joobleKey })} - hint="jooble.org/api/about → Schlüssel anfordern" - />
;