diff --git a/lib/api.js b/lib/api.js index 8f83da9..802cd69 100644 --- a/lib/api.js +++ b/lib/api.js @@ -633,7 +633,12 @@ function createExternalApi(deps) { const urlNorm = blacklist.normalizeUrl(b.quelle_url || '') || null; const anzeigeDatum = sanitizeInput(b.anzeige_datum || ''); const kontaktEmail = sanitizeInput(b.kontakt_email || ''); - // Column order matches the UPDATE/INSERT statements below (url_norm last). + // Company slug: stored as supplied by the client; derived from firma only + // when omitted. This is the key the blacklist matches on (see below). + const firmaSlug = b.firma_slug != null && String(b.firma_slug).trim() !== '' + ? sanitizeInput(String(b.firma_slug)) + : (blacklist.firmaSlug(b.firma) || null); + // Column order matches the UPDATE/INSERT statements below (firma_slug last). const fields = [ sanitizeInput(b.firma), sanitizeInput(b.stelle), @@ -649,6 +654,7 @@ function createExternalApi(deps) { sanitizeInput(b.status || 'offen'), serializeLabels(b.labels), urlNorm, + firmaSlug, ]; // De-dup: prefer a (quelle, external_id) match, else the same normalized @@ -671,7 +677,7 @@ function createExternalApi(deps) { await dbRun( `UPDATE jobangebote SET firma = ?, stelle = ?, ort = ?, adresse = ?, ansprechpartner = ?, gehalt = ?, beschreibung = ?, quelle_url = ?, art = ?, anzeige_datum = ?, kontakt_email = ?, status = ?, - labels = ?, url_norm = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, + labels = ?, url_norm = ?, firma_slug = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [...fields, existing.id] ); const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [existing.id]); @@ -680,8 +686,8 @@ function createExternalApi(deps) { const result = await dbRun( `INSERT INTO jobangebote - (external_id, quelle, firma, stelle, ort, adresse, ansprechpartner, gehalt, beschreibung, quelle_url, art, anzeige_datum, kontakt_email, status, labels, url_norm) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + (external_id, quelle, firma, stelle, ort, adresse, ansprechpartner, gehalt, beschreibung, quelle_url, art, anzeige_datum, kontakt_email, status, labels, url_norm, firma_slug) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [externalId, quelle, ...fields] ); const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [result.lastID]); diff --git a/lib/blacklist.js b/lib/blacklist.js index 1bf56ac..7b127bd 100644 --- a/lib/blacklist.js +++ b/lib/blacklist.js @@ -130,7 +130,9 @@ function offerSignature(offer) { external_id: o.external_id != null && String(o.external_id).trim() !== '' ? String(o.external_id).trim() : null, firma_norm: normText(o.firma), - firma_slug: firmaSlug(o.firma), + // Prefer a client-supplied slug (offers carry firma_slug); else derive it. + firma_slug: o.firma_slug != null && String(o.firma_slug).trim() !== '' + ? String(o.firma_slug).trim() : firmaSlug(o.firma), stelle_norm: normText(o.stelle), ort_norm: normText(o.ort), }; diff --git a/lib/openapi.js b/lib/openapi.js index c1ca45e..28e39bd 100644 --- a/lib/openapi.js +++ b/lib/openapi.js @@ -887,6 +887,7 @@ function buildOpenApiSpec(baseUrl = '') { labels: { ...labelsSchema, nullable: true }, verknuepfte_bewerbung_id: { type: 'integer', nullable: true }, url_norm: { type: 'string', nullable: true, description: 'Normalisierte URL für die De-Duplizierung (serverseitig gesetzt)' }, + firma_slug: { type: 'string', nullable: true, description: 'Firmen-Slug für Blacklist-Abgleich; wie übergeben gespeichert, bei Fehlen aus firma abgeleitet.' }, created_at: { type: 'string', format: 'date-time' }, updated_at: { type: 'string', format: 'date-time' }, }, @@ -905,6 +906,7 @@ function buildOpenApiSpec(baseUrl = '') { gehalt: { type: 'string' }, beschreibung: { type: 'string', description: 'Stellenbeschreibungstext' }, quelle_url: { type: 'string' }, + firma_slug: { type: 'string', description: 'Optionaler Firmen-Slug für den Blacklist-Abgleich (z. B. "bosch"). Wird wie übergeben gespeichert; fehlt er, leitet der Server ihn aus firma ab.' }, art: { type: 'string', enum: ART_OPTIONS }, anzeige_datum: { type: 'string', format: 'date', description: 'Datum der eigentlichen Stellenanzeige (ISO YYYY-MM-DD)' }, kontakt_email: { type: 'string', format: 'email', description: 'Kontakt-E-Mail-Adresse der Stelle' }, diff --git a/server.js b/server.js index 43c2353..5aa7805 100644 --- a/server.js +++ b/server.js @@ -803,6 +803,19 @@ function initializeDatabase() { }); }); db.run('CREATE INDEX IF NOT EXISTS idx_jobangebote_url_norm ON jobangebote(url_norm)', () => {}); + // Company slug carried on the offer itself (supplied by the indexing + // client, else derived from firma). Used for blacklist matching and + // "same company" grouping. Backfill derives it from firma. + db.run('ALTER TABLE jobangebote ADD COLUMN firma_slug TEXT', () => { + db.all('SELECT id, firma FROM jobangebote WHERE firma_slug IS NULL AND firma IS NOT NULL AND firma != ""', (err, rows) => { + if (err || !rows) return; + rows.forEach((r) => { + const slug = blacklist.firmaSlug(r.firma); + if (slug) db.run('UPDATE jobangebote SET firma_slug = ? WHERE id = ?', [slug, r.id], () => {}); + }); + }); + }); + db.run('CREATE INDEX IF NOT EXISTS idx_jobangebote_firma_slug ON jobangebote(firma_slug)', () => {}); // Blacklist of job offers that must never (re)appear in the list. A // deleted offer is auto-blacklisted; manual entries can block a URL, // a whole domain, a company, or a specific company+title posting.