diff --git a/docs/12-scanner.md b/docs/12-scanner.md index 4e790fd..3195cd1 100644 --- a/docs/12-scanner.md +++ b/docs/12-scanner.md @@ -29,6 +29,15 @@ 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. +> **Anführungszeichen um Schlagwörter** (`"Interim", "Vakanz"`) werden beim +> Speichern, in der Eingabe und beim Lesen alter Agenten entfernt +> (`stripQuotes` in `matching.js`). Vorher suchte die Bewertung nach den Zeichen +> mit und fand am 29.09.2026 in 178 Anzeigen keinen Treffer. +> +> **Umlaute in Such-URLs** werden umgeschrieben (`ö` → `oe`). karriere.at +> antwortet auf Begriffe ohne eigene Suchseite mit 404 – das gilt als „keine +> Treffer", nicht als Fehler. + > **HTML-Quellen brechen, wenn die Börse ihr Layout ändert.** Liefert StepStone, > karriere.at oder freelancermap plötzlich 0 Treffer, zuerst die Selektoren in > `sources.js` gegen die aktuelle Seite prüfen. diff --git a/server/src/routes/scanner.js b/server/src/routes/scanner.js index ea0afb6..6e4b49e 100644 --- a/server/src/routes/scanner.js +++ b/server/src/routes/scanner.js @@ -4,6 +4,7 @@ import { requireAdmin, requireAuth } from '../middleware/auth.js'; import { logHistory } from '../history.js'; import { SOURCES, SOURCE_IDS, sourceReady } from '../scanner/sources.js'; import { parseList, runScan, scanStatus } from '../scanner/pipeline.js'; +import { stripQuotes } from '../scanner/matching.js'; import { getLastScanAt, loadScannerSecrets, @@ -25,7 +26,7 @@ router.use(requireAuth); const STATUSES = ['neu', 'gesehen', 'in_bearbeitung', 'verworfen']; const cleanList = (value) => - [...new Set((Array.isArray(value) ? value : []).map((item) => String(item).trim()).filter(Boolean))].slice(0, 50); + [...new Set((Array.isArray(value) ? value : []).map(stripQuotes).filter(Boolean))].slice(0, 50); const agentOut = (row) => ({ id: row.id, diff --git a/server/src/scanner/matching.js b/server/src/scanner/matching.js index 8bf8220..a59cc28 100644 --- a/server/src/scanner/matching.js +++ b/server/src/scanner/matching.js @@ -10,6 +10,17 @@ * Gewicht 2 = im Titel, 1 = nur im Text */ +/* + * Anfuehrungszeichen um Begriffe entfernen: wer "Interim", "Vakanz" eingibt, + * meint die Woerter, nicht die Zeichen. Mit Zeichen fand die Bewertung am + * 29.09.2026 in 178 Anzeigen keinen einzigen Treffer. + */ +export const stripQuotes = (text) => + String(text ?? '') + .trim() + .replace(/^["'„“”‚‘’«»]+|["'„“”‚‘’«»]+$/g, '') + .trim(); + const normalize = (text) => String(text ?? '').normalize('NFC').toLowerCase(); const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); diff --git a/server/src/scanner/pipeline.js b/server/src/scanner/pipeline.js index 9c297d8..6d18f33 100644 --- a/server/src/scanner/pipeline.js +++ b/server/src/scanner/pipeline.js @@ -1,6 +1,6 @@ import { pool } from '../db.js'; import { SOURCES, sourceReady } from './sources.js'; -import { findCompany, normalizeCompany, scoreJob } from './matching.js'; +import { findCompany, normalizeCompany, scoreJob, stripQuotes } from './matching.js'; import { loadScannerSecrets, loadScannerSettings, setLastScanAt } from './settings.js'; /* @@ -142,7 +142,7 @@ async function execute(trigger) { export function parseList(value) { try { const list = JSON.parse(value ?? '[]'); - return Array.isArray(list) ? list.map((item) => String(item).trim()).filter(Boolean) : []; + return Array.isArray(list) ? list.map(stripQuotes).filter(Boolean) : []; } catch { return []; } diff --git a/server/src/scanner/sources.js b/server/src/scanner/sources.js index c7d5455..fb777f4 100644 --- a/server/src/scanner/sources.js +++ b/server/src/scanner/sources.js @@ -114,10 +114,15 @@ export function parseRelativeDate(text, now = new Date()) { return null; } +// Umlaute umschreiben: karriere.at antwortet auf /jobs/interimslösung mit 404. const slug = (keyword) => clean(keyword) .toLowerCase() - .replace(/[^a-z0-9äöüß]+/g, '-') + .replace(/ä/g, 'ae') + .replace(/ö/g, 'oe') + .replace(/ü/g, 'ue') + .replace(/ß/g, 'ss') + .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); /* ------------------------------------------------------------------------ @@ -214,7 +219,15 @@ async function fetchStepstone(keyword) { * ---------------------------------------------------------------------- */ async function fetchKarriere(keyword) { const base = 'https://www.karriere.at'; - const html = await request(`${base}/jobs/${encodeURIComponent(slug(keyword))}`); + let html; + try { + html = await request(`${base}/jobs/${encodeURIComponent(slug(keyword))}`); + } catch (err) { + // Fuer Begriffe ohne eigene Suchseite antwortet karriere.at mit 404 - + // das heisst "nichts gefunden", nicht "Quelle kaputt". + if (err.message === 'HTTP 404') return []; + throw err; + } const $ = cheerio.load(html); const jobs = []; diff --git a/src/components/scanner/ScannerDashboard.tsx b/src/components/scanner/ScannerDashboard.tsx index 127fc1f..671764a 100644 --- a/src/components/scanner/ScannerDashboard.tsx +++ b/src/components/scanner/ScannerDashboard.tsx @@ -236,7 +236,7 @@ export const ScannerDashboard: React.FC = () => {