feat(scanner): Alarm erst ab Mindest-Quote je Suchagent
Neues Feld alarm_min_score (0-100 %): Alarm nur, wenn ein Alarm-Schlagwort gefunden wurde und die Quote den Wert erreicht. 0 % = bei jedem Fund wie bisher. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1969d0b77a
commit
371512a47d
@@ -191,6 +191,12 @@ async function migrateColumns() {
|
||||
'alarm_keywords',
|
||||
'ALTER TABLE icon_scanner_agents ADD COLUMN alarm_keywords TEXT NULL AFTER excludes',
|
||||
],
|
||||
// Scanner: Alarm erst ab einer Mindest-Quote, 20.10.2026.
|
||||
[
|
||||
'icon_scanner_agents',
|
||||
'alarm_min_score',
|
||||
'ALTER TABLE icon_scanner_agents ADD COLUMN alarm_min_score TINYINT UNSIGNED NOT NULL DEFAULT 0 AFTER alarm_keywords',
|
||||
],
|
||||
// Scanner: Ausschluss-Unternehmen je Agent ("Hays"), 20.10.2026.
|
||||
[
|
||||
'icon_scanner_agents',
|
||||
|
||||
@@ -39,6 +39,7 @@ const agentOut = (row) => ({
|
||||
excludes: parseList(row.excludes),
|
||||
exclude_companies: parseList(row.exclude_companies),
|
||||
alarm_keywords: parseList(row.alarm_keywords),
|
||||
alarm_min_score: Number(row.alarm_min_score ?? 0),
|
||||
is_active: Boolean(row.is_active),
|
||||
matches: Number(row.matches ?? 0),
|
||||
created_at: row.created_at,
|
||||
@@ -124,22 +125,24 @@ function readAgent(body) {
|
||||
const excludes = cleanList(body?.excludes);
|
||||
const excludeCompanies = cleanList(body?.exclude_companies);
|
||||
const alarmKeywords = cleanList(body?.alarm_keywords);
|
||||
const alarmMinScore = Math.min(100, Math.max(0, Math.round(Number(body?.alarm_min_score) || 0)));
|
||||
if (!name) return { error: 'Bitte einen Namen angeben.' };
|
||||
if (keywords.length === 0 && alarmKeywords.length === 0) {
|
||||
return { error: 'Mindestens ein Schlagwort oder Alarm-Schlagwort angeben.' };
|
||||
}
|
||||
if (sources.length === 0) return { error: 'Mindestens eine Börse auswählen.' };
|
||||
return { name, keywords, alarmKeywords, sources, excludes, excludeCompanies, is_active: body?.is_active !== false };
|
||||
return { name, keywords, alarmKeywords, alarmMinScore, sources, excludes, excludeCompanies, is_active: body?.is_active !== false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Spaltenwerte in der Reihenfolge name, keywords, alarm_keywords, sources,
|
||||
* excludes, exclude_companies, is_active.
|
||||
* Spaltenwerte in der Reihenfolge name, keywords, alarm_keywords,
|
||||
* alarm_min_score, sources, excludes, exclude_companies, is_active.
|
||||
*/
|
||||
const agentValues = (agent) => [
|
||||
agent.name,
|
||||
JSON.stringify(agent.keywords),
|
||||
JSON.stringify(agent.alarmKeywords),
|
||||
agent.alarmMinScore,
|
||||
JSON.stringify(agent.sources),
|
||||
JSON.stringify(agent.excludes),
|
||||
JSON.stringify(agent.excludeCompanies),
|
||||
@@ -168,8 +171,9 @@ router.post('/agents', async (req, res, next) => {
|
||||
try {
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO icon_scanner_agents
|
||||
(name, keywords, alarm_keywords, sources, excludes, exclude_companies, is_active, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
(name, keywords, alarm_keywords, alarm_min_score, sources, excludes, exclude_companies, is_active,
|
||||
created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[...agentValues(agent), req.user.id]
|
||||
);
|
||||
await logHistory({
|
||||
@@ -192,13 +196,18 @@ router.put('/agents/:id', async (req, res, next) => {
|
||||
try {
|
||||
const [result] = await pool.query(
|
||||
`UPDATE icon_scanner_agents
|
||||
SET name = ?, keywords = ?, alarm_keywords = ?, sources = ?, excludes = ?, exclude_companies = ?,
|
||||
is_active = ?
|
||||
SET name = ?, keywords = ?, alarm_keywords = ?, alarm_min_score = ?, sources = ?, excludes = ?,
|
||||
exclude_companies = ?, is_active = ?
|
||||
WHERE id = ?`,
|
||||
[...agentValues(agent), Number(req.params.id)]
|
||||
);
|
||||
if (result.affectedRows === 0) return res.status(404).json({ error: 'Suchagent nicht gefunden.' });
|
||||
await dropExcludedMatches(Number(req.params.id), agent);
|
||||
// Offene Alarme unter der (neuen) Mindest-Quote nicht mehr melden.
|
||||
await pool.query(
|
||||
'UPDATE icon_scanner_matches SET alarm = 0 WHERE agent_id = ? AND alarm = 1 AND alarm_ack = 0 AND score < ?',
|
||||
[Number(req.params.id), agent.alarmMinScore]
|
||||
);
|
||||
const [[row]] = await pool.query('SELECT * FROM icon_scanner_agents WHERE id = ?', [Number(req.params.id)]);
|
||||
res.json({ agent: agentOut(row) });
|
||||
} catch (err) {
|
||||
|
||||
@@ -67,7 +67,8 @@ export function isExcludedCompany(agent, company) {
|
||||
/**
|
||||
* @returns {{ score: number, hits: string[], alarm: boolean } | null}
|
||||
* null = ausgeschlossen oder kein einziges Schlagwort gefunden.
|
||||
* `alarm` = mindestens ein Alarm-Schlagwort steht in der Anzeige.
|
||||
* `alarm` = mindestens ein Alarm-Schlagwort steht in der Anzeige und
|
||||
* die Quote erreicht `agent.alarmMinScore` (0 = immer).
|
||||
*/
|
||||
export function scoreJob(agent, job) {
|
||||
const title = job.title ?? '';
|
||||
@@ -93,10 +94,11 @@ export function scoreJob(agent, job) {
|
||||
if (hits.length === 0) return null;
|
||||
|
||||
const alarmSet = new Set((agent.alarmKeywords ?? []).map((term) => term.toLowerCase()));
|
||||
const score = Math.round((100 * points) / (2 * keywords.length));
|
||||
return {
|
||||
score: Math.round((100 * points) / (2 * keywords.length)),
|
||||
score,
|
||||
hits,
|
||||
alarm: hits.some((hit) => alarmSet.has(hit.toLowerCase())),
|
||||
alarm: score >= (agent.alarmMinScore ?? 0) && hits.some((hit) => alarmSet.has(hit.toLowerCase())),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ async function loadActiveAgents() {
|
||||
id: row.id,
|
||||
keywords: parseList(row.keywords),
|
||||
alarmKeywords: parseList(row.alarm_keywords),
|
||||
alarmMinScore: Number(row.alarm_min_score ?? 0),
|
||||
sources: parseList(row.sources),
|
||||
excludes: parseList(row.excludes),
|
||||
excludeCompanies: parseList(row.exclude_companies),
|
||||
|
||||
@@ -477,6 +477,8 @@ CREATE TABLE IF NOT EXISTS icon_scanner_agents (
|
||||
-- Schlagwoerter mit Alarm: werden wie `keywords` gesucht und gezaehlt,
|
||||
-- ein Fund meldet sich zusaetzlich mit Ton und Pop-up.
|
||||
alarm_keywords TEXT NULL,
|
||||
-- Alarm erst ab dieser Quote (0-100 %); 0 = bei jedem Fund.
|
||||
alarm_min_score TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
-- Unternehmen, deren Anzeigen dieser Agent nie zeigt (z. B. ["Hays"]).
|
||||
exclude_companies TEXT NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
|
||||
Reference in New Issue
Block a user