Scanner: ohne Altersgrenze suchen, Filter Anzeigenalter, Alarm-Schlagwörter

- Suche ohne 21-Tage-Grenze (BA ohne veroeffentlichtseit, Adzuna ohne
  max_days_old); Einstellung Höchstalter entfällt
- Dashboard: Filter Anzeigenalter (bis/älter als 21 Tage), Sortierung
  älteste Anzeige, Filter nur Alarm, Zeitraum heißt jetzt Gefunden
- Suchagent: Schlagwörter mit Alarm; Treffer mit alarm = 1 melden sich
  über den AlarmWatcher (App-weit, jede Minute) mit Ton und Pop-up,
  Quittieren gilt für alle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-29 13:28:29 +02:00
co-authored by Claude Opus 5.5
parent b34a1968df
commit 1ad774fdd2
16 changed files with 978 additions and 625 deletions
+11 -13
View File
@@ -1,7 +1,7 @@
import { pool } from '../db.js';
import { SOURCES, sourceReady } from './sources.js';
import { findCompany, normalizeCompany, scoreJob, stripQuotes } from './matching.js';
import { loadScannerSecrets, loadScannerSettings, setLastScanAt } from './settings.js';
import { agentTerms, findCompany, normalizeCompany, scoreJob, stripQuotes } from './matching.js';
import { loadScannerSecrets, setLastScanAt } from './settings.js';
/*
* Ein Suchlauf.
@@ -9,7 +9,7 @@ import { loadScannerSecrets, loadScannerSettings, setLastScanAt } from './settin
* 1. aktive Agenten laden
* 2. je Boerse die Schlagwoerter aller Agenten zusammenfuehren - dasselbe
* Wort wird nur einmal abgefragt, egal wie viele Agenten es tragen
* 3. Anzeigen abrufen, alles ueber der Altersgrenze verwerfen
* 3. Anzeigen abrufen - jeden Alters (das Dashboard filtert nach Alter)
* 4. Anzeigen speichern, Firma gegen das CRM abgleichen
* 5. jede Anzeige gegen jeden Agenten bewerten, der diese Boerse durchsucht
*
@@ -56,24 +56,22 @@ async function execute(trigger) {
const [runResult] = await pool.query('INSERT INTO icon_scanner_runs (trigger_kind) VALUES (?)', [trigger]);
const runId = runResult.insertId;
const settings = await loadScannerSettings();
const secrets = await loadScannerSecrets();
const agents = await loadActiveAgents();
// Boerse -> Schlagwoerter (ohne Doppelte, Gross/klein egal)
// Boerse -> Schlagwoerter inkl. Alarm-Schlagwoerter (ohne Doppelte)
const plan = new Map();
for (const agent of agents) {
for (const source of agent.sources) {
if (!SOURCES[source] || !sourceReady(source, secrets)) continue;
const words = plan.get(source) ?? new Map();
for (const keyword of agent.keywords) words.set(keyword.toLowerCase(), keyword);
for (const keyword of agentTerms(agent)) words.set(keyword.toLowerCase(), keyword);
plan.set(source, words);
}
}
const perSource = {};
const jobs = new Map();
const oldest = Date.now() - settings.maxAge * 24 * 3_600_000;
for (const [source, words] of plan) {
const stats = { ok: true, count: 0, errors: [] };
@@ -82,11 +80,10 @@ async function execute(trigger) {
if (!first) await sleep(REQUEST_GAP_MS);
first = false;
try {
const found = await SOURCES[source].fetch(keyword, { maxAge: settings.maxAge, secrets });
// Seit 29.09.2026 ohne Altersgrenze: auch aeltere Anzeigen kommen
// herein, gefiltert wird im Dashboard ("Anzeigenalter").
const found = await SOURCES[source].fetch(keyword, { secrets });
for (const job of found) {
// Ohne Datum bleibt die Anzeige drin: die Boersen sortieren bzw.
// filtern selbst nach Alter, das fehlende Feld ist ein Parserluecke.
if (job.publishedAt && job.publishedAt.getTime() < oldest) continue;
jobs.set(`${job.source}:${job.externalId}`, job);
stats.count += 1;
}
@@ -111,8 +108,8 @@ async function execute(trigger) {
const result = scoreJob(agent, job);
if (!result) continue;
const [insert] = await pool.query(
`INSERT IGNORE INTO icon_scanner_matches (agent_id, job_id, score, hits) VALUES (?, ?, ?, ?)`,
[agent.id, jobId, result.score, JSON.stringify(result.hits)]
`INSERT IGNORE INTO icon_scanner_matches (agent_id, job_id, score, hits, alarm) VALUES (?, ?, ?, ?, ?)`,
[agent.id, jobId, result.score, JSON.stringify(result.hits), result.alarm ? 1 : 0]
);
matchesNew += insert.affectedRows;
}
@@ -153,6 +150,7 @@ async function loadActiveAgents() {
return rows.map((row) => ({
id: row.id,
keywords: parseList(row.keywords),
alarmKeywords: parseList(row.alarm_keywords),
sources: parseList(row.sources),
excludes: parseList(row.excludes),
}));