From b34a1968df9cde6d791973a5697252c6d382e173 Mon Sep 17 00:00:00 2001 From: schakal00783 Date: Tue, 29 Sep 2026 13:19:16 +0200 Subject: [PATCH] =?UTF-8?q?Scanner:=200=20Treffer=20durch=20Anf=C3=BChrung?= =?UTF-8?q?szeichen=20in=20Schlagw=C3=B6rtern=20behoben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Anführungszeichen um Begriffe beim Eingeben, Speichern und Lesen entfernen; der Agent vom 29.09. suchte nach "Interim" samt Zeichen - Umlaute in Such-URLs umschreiben, karriere.at-404 = keine Treffer - Filterleiste mit festen Breiten, bricht um statt überzulaufen Co-Authored-By: Claude Opus 5.5 --- docs/12-scanner.md | 9 +++++++++ server/src/routes/scanner.js | 3 ++- server/src/scanner/matching.js | 11 +++++++++++ server/src/scanner/pipeline.js | 4 ++-- server/src/scanner/sources.js | 17 +++++++++++++++-- src/components/scanner/ScannerDashboard.tsx | 20 ++++++++++---------- src/components/scanner/scannerUi.tsx | 3 ++- 7 files changed, 51 insertions(+), 16 deletions(-) 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 = () => {

Stellen-Treffer

- + - + - + - + { step={5} value={minScore} onChange={(event) => setMinScore(Number(event.target.value) || 0)} - className={`${inputClass} w-20`} + className={inputClass} /> - + - + setSearch(event.target.value)} placeholder="Titel, Firma, Ort" - className={`${inputClass} w-44`} + className={inputClass} />
@@ -422,8 +422,8 @@ const Kpi: React.FC<{ value: React.ReactNode; label: string; icon: string }> = (
); -const Filter: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => ( -