feat(scanner): Ausschluss-Unternehmen je Suchagent
Neues Feld exclude_companies: steht ein Eintrag im Firmennamen der Anzeige (z. B. "Hays"), verwirft der Agent sie. Beim Speichern werden vorhandene Treffer dieser Firmen gelöscht (außer "in Bearbeitung"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a24fd938a2
commit
1969d0b77a
@@ -217,8 +217,10 @@ freelancermap) nach offenen Stellen in DACH – jedes Alter; im Dashboard grenze
|
|||||||
siehe [12 Scanner](12-scanner.md).
|
siehe [12 Scanner](12-scanner.md).
|
||||||
|
|
||||||
1. **Scanner → Suchagenten → + Neuer Suchagent.** Name, Schlagwörter (Enter
|
1. **Scanner → Suchagenten → + Neuer Suchagent.** Name, Schlagwörter (Enter
|
||||||
oder Komma trennt), Ausschlussbegriffe und Börsen wählen. Jedes Schlagwort
|
oder Komma trennt), Ausschlussbegriffe, Ausschluss-Unternehmen und Börsen
|
||||||
ist eine eigene Suche.
|
wählen. Jedes Schlagwort ist eine eigene Suche. Unter
|
||||||
|
**Ausschluss Unternehmen** z. B. „Hays" eintragen – dann zeigt dieser Agent
|
||||||
|
keine Anzeigen von Hays mehr; schon vorhandene verschwinden beim Speichern.
|
||||||
2. **Scanner (Dashboard).** Die Treffer erscheinen nach dem nächsten Lauf –
|
2. **Scanner (Dashboard).** Die Treffer erscheinen nach dem nächsten Lauf –
|
||||||
automatisch nach Zeitplan oder sofort über **Jetzt scannen**.
|
automatisch nach Zeitplan oder sofort über **Jetzt scannen**.
|
||||||
3. Die **Quote** zeigt, wie viele Schlagwörter in der Anzeige stehen; ein
|
3. Die **Quote** zeigt, wie viele Schlagwörter in der Anzeige stehen; ein
|
||||||
|
|||||||
+10
-3
@@ -79,7 +79,7 @@ sequenceDiagram
|
|||||||
loop je Anzeige
|
loop je Anzeige
|
||||||
P->>DB: speichern (eindeutig je Börse + Kennung), Firma abgleichen
|
P->>DB: speichern (eindeutig je Börse + Kennung), Firma abgleichen
|
||||||
loop je Agent, der diese Börse durchsucht
|
loop je Agent, der diese Börse durchsucht
|
||||||
P->>P: Ausschlussbegriff? weiter
|
P->>P: Ausschlussbegriff oder Ausschluss-Unternehmen? weiter
|
||||||
P->>P: Schlagwörter zählen → Quote
|
P->>P: Schlagwörter zählen → Quote
|
||||||
P->>DB: Treffer (eindeutig je Agent + Anzeige)
|
P->>DB: Treffer (eindeutig je Agent + Anzeige)
|
||||||
end
|
end
|
||||||
@@ -111,6 +111,12 @@ mit „SAP FI" im Text → (2 + 1 + 0) / 6 = **50 %**.
|
|||||||
(„Projektleiter" in „IT-Projektleiterin").
|
(„Projektleiter" in „IT-Projektleiterin").
|
||||||
- Ein Ausschlussbegriff in Titel, Firma oder Text verwirft die Anzeige für
|
- Ein Ausschlussbegriff in Titel, Firma oder Text verwirft die Anzeige für
|
||||||
diesen Agenten.
|
diesen Agenten.
|
||||||
|
- **Ausschluss-Unternehmen** (seit 20.10.2026, Spalte `exclude_companies`):
|
||||||
|
steht ein Eintrag im **Firmennamen** der Anzeige, verwirft der Agent sie –
|
||||||
|
„Hays" blendet „Hays Professional Solutions GmbH" aus, nicht aber eine
|
||||||
|
Anzeige, die Hays nur im Text erwähnt. Gleiche Wortregel wie oben (≤ 3
|
||||||
|
Zeichen nur als ganzes Wort). Beim Speichern des Agenten werden schon
|
||||||
|
vorhandene Treffer dieser Firmen gelöscht, außer Status „in Bearbeitung".
|
||||||
- Kein Schlagwort gefunden → kein Treffer.
|
- Kein Schlagwort gefunden → kein Treffer.
|
||||||
|
|
||||||
Das kostet nichts und ist nachvollziehbar; OpenAI wird vom Scanner nicht
|
Das kostet nichts und ist nachvollziehbar; OpenAI wird vom Scanner nicht
|
||||||
@@ -219,6 +225,7 @@ erDiagram
|
|||||||
text keywords "JSON-Array"
|
text keywords "JSON-Array"
|
||||||
text sources "JSON-Array der Quellen-Ids"
|
text sources "JSON-Array der Quellen-Ids"
|
||||||
text excludes "JSON-Array"
|
text excludes "JSON-Array"
|
||||||
|
text exclude_companies "JSON-Array, Abgleich mit Firmenname"
|
||||||
text alarm_keywords "JSON-Array, Ton + Pop-up"
|
text alarm_keywords "JSON-Array, Ton + Pop-up"
|
||||||
tinyint is_active
|
tinyint is_active
|
||||||
}
|
}
|
||||||
@@ -274,8 +281,8 @@ Alle unter `/api/scanner`, alle hinter `requireAuth`.
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| GET | `/overview` | alle | Kennzahlen, Quellen samt Ergebnis des letzten Laufs, Zeitplan, läuft gerade ein Lauf |
|
| GET | `/overview` | alle | Kennzahlen, Quellen samt Ergebnis des letzten Laufs, Zeitplan, läuft gerade ein Lauf |
|
||||||
| GET | `/agents` | alle | Suchagenten mit Trefferzahl |
|
| GET | `/agents` | alle | Suchagenten mit Trefferzahl |
|
||||||
| POST | `/agents` | alle | anlegen `{ name, keywords[], alarm_keywords[], sources[], excludes[], is_active }` – Schlagwort oder Alarm-Schlagwort Pflicht |
|
| POST | `/agents` | alle | anlegen `{ name, keywords[], alarm_keywords[], sources[], excludes[], exclude_companies[], is_active }` – Schlagwort oder Alarm-Schlagwort Pflicht |
|
||||||
| PUT | `/agents/:id` | alle | ändern (gleicher Körper) |
|
| PUT | `/agents/:id` | alle | ändern (gleicher Körper); löscht vorhandene Treffer von Ausschluss-Unternehmen (außer „in Bearbeitung“) |
|
||||||
| DELETE | `/agents/:id` | alle | löschen, Treffer gehen mit |
|
| DELETE | `/agents/:id` | alle | löschen, Treffer gehen mit |
|
||||||
| GET | `/matches` | alle | Filter `hours` (gefunden), `olderThan` / `youngerThan` (Tage ab Veröffentlichung, einzeln oder zusammen; `age=recent|old` = feste 21-Tage-Grenze, aus der ersten Fassung), `agentId`, `source`, `known=1`, `alarm=1`, `status` (`alle` zeigt auch Verworfene), `minScore`, `q`, `sort` (`known`/`score`/`agent`/`title`/`company`/`source`/`published`/`status`/`created`; `oldest` = `published` aufsteigend) mit `dir` (`asc`/`desc`), `limit` |
|
| GET | `/matches` | alle | Filter `hours` (gefunden), `olderThan` / `youngerThan` (Tage ab Veröffentlichung, einzeln oder zusammen; `age=recent|old` = feste 21-Tage-Grenze, aus der ersten Fassung), `agentId`, `source`, `known=1`, `alarm=1`, `status` (`alle` zeigt auch Verworfene), `minScore`, `q`, `sort` (`known`/`score`/`agent`/`title`/`company`/`source`/`published`/`status`/`created`; `oldest` = `published` aufsteigend) mit `dir` (`asc`/`desc`), `limit` |
|
||||||
| GET | `/alarms` | alle | unquittierte Alarm-Treffer (höchstens 50) |
|
| GET | `/alarms` | alle | unquittierte Alarm-Treffer (höchstens 50) |
|
||||||
|
|||||||
@@ -191,6 +191,12 @@ async function migrateColumns() {
|
|||||||
'alarm_keywords',
|
'alarm_keywords',
|
||||||
'ALTER TABLE icon_scanner_agents ADD COLUMN alarm_keywords TEXT NULL AFTER excludes',
|
'ALTER TABLE icon_scanner_agents ADD COLUMN alarm_keywords TEXT NULL AFTER excludes',
|
||||||
],
|
],
|
||||||
|
// Scanner: Ausschluss-Unternehmen je Agent ("Hays"), 20.10.2026.
|
||||||
|
[
|
||||||
|
'icon_scanner_agents',
|
||||||
|
'exclude_companies',
|
||||||
|
'ALTER TABLE icon_scanner_agents ADD COLUMN exclude_companies TEXT NULL AFTER alarm_keywords',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'icon_scanner_matches',
|
'icon_scanner_matches',
|
||||||
'alarm',
|
'alarm',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { requireAdmin, requireAuth } from '../middleware/auth.js';
|
|||||||
import { logHistory } from '../history.js';
|
import { logHistory } from '../history.js';
|
||||||
import { SOURCES, SOURCE_IDS, sourceReady } from '../scanner/sources.js';
|
import { SOURCES, SOURCE_IDS, sourceReady } from '../scanner/sources.js';
|
||||||
import { parseList, runScan, scanStatus } from '../scanner/pipeline.js';
|
import { parseList, runScan, scanStatus } from '../scanner/pipeline.js';
|
||||||
import { stripQuotes } from '../scanner/matching.js';
|
import { isExcludedCompany, stripQuotes } from '../scanner/matching.js';
|
||||||
import {
|
import {
|
||||||
getLastScanAt,
|
getLastScanAt,
|
||||||
loadScannerSecrets,
|
loadScannerSecrets,
|
||||||
@@ -37,6 +37,7 @@ const agentOut = (row) => ({
|
|||||||
// Nur Quellen, die es noch gibt (Jooble ist am 29.09.2026 entfallen).
|
// Nur Quellen, die es noch gibt (Jooble ist am 29.09.2026 entfallen).
|
||||||
sources: parseList(row.sources).filter((id) => SOURCE_IDS.includes(id)),
|
sources: parseList(row.sources).filter((id) => SOURCE_IDS.includes(id)),
|
||||||
excludes: parseList(row.excludes),
|
excludes: parseList(row.excludes),
|
||||||
|
exclude_companies: parseList(row.exclude_companies),
|
||||||
alarm_keywords: parseList(row.alarm_keywords),
|
alarm_keywords: parseList(row.alarm_keywords),
|
||||||
is_active: Boolean(row.is_active),
|
is_active: Boolean(row.is_active),
|
||||||
matches: Number(row.matches ?? 0),
|
matches: Number(row.matches ?? 0),
|
||||||
@@ -121,32 +122,54 @@ function readAgent(body) {
|
|||||||
const keywords = cleanList(body?.keywords);
|
const keywords = cleanList(body?.keywords);
|
||||||
const sources = cleanList(body?.sources).filter((id) => SOURCE_IDS.includes(id));
|
const sources = cleanList(body?.sources).filter((id) => SOURCE_IDS.includes(id));
|
||||||
const excludes = cleanList(body?.excludes);
|
const excludes = cleanList(body?.excludes);
|
||||||
|
const excludeCompanies = cleanList(body?.exclude_companies);
|
||||||
const alarmKeywords = cleanList(body?.alarm_keywords);
|
const alarmKeywords = cleanList(body?.alarm_keywords);
|
||||||
if (!name) return { error: 'Bitte einen Namen angeben.' };
|
if (!name) return { error: 'Bitte einen Namen angeben.' };
|
||||||
if (keywords.length === 0 && alarmKeywords.length === 0) {
|
if (keywords.length === 0 && alarmKeywords.length === 0) {
|
||||||
return { error: 'Mindestens ein Schlagwort oder Alarm-Schlagwort angeben.' };
|
return { error: 'Mindestens ein Schlagwort oder Alarm-Schlagwort angeben.' };
|
||||||
}
|
}
|
||||||
if (sources.length === 0) return { error: 'Mindestens eine Börse auswählen.' };
|
if (sources.length === 0) return { error: 'Mindestens eine Börse auswählen.' };
|
||||||
return { name, keywords, alarmKeywords, sources, excludes, is_active: body?.is_active !== false };
|
return { name, keywords, alarmKeywords, sources, excludes, excludeCompanies, is_active: body?.is_active !== false };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Spaltenwerte in der Reihenfolge name, keywords, alarm_keywords, sources, excludes, is_active. */
|
/**
|
||||||
|
* Spaltenwerte in der Reihenfolge name, keywords, alarm_keywords, sources,
|
||||||
|
* excludes, exclude_companies, is_active.
|
||||||
|
*/
|
||||||
const agentValues = (agent) => [
|
const agentValues = (agent) => [
|
||||||
agent.name,
|
agent.name,
|
||||||
JSON.stringify(agent.keywords),
|
JSON.stringify(agent.keywords),
|
||||||
JSON.stringify(agent.alarmKeywords),
|
JSON.stringify(agent.alarmKeywords),
|
||||||
JSON.stringify(agent.sources),
|
JSON.stringify(agent.sources),
|
||||||
JSON.stringify(agent.excludes),
|
JSON.stringify(agent.excludes),
|
||||||
|
JSON.stringify(agent.excludeCompanies),
|
||||||
agent.is_active ? 1 : 0,
|
agent.is_active ? 1 : 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bereits gespeicherte Treffer von Ausschluss-Unternehmen entfernen - sonst
|
||||||
|
* blieben sie bis zum Aufraeumen sichtbar. Treffer "in Bearbeitung" bleiben,
|
||||||
|
* an denen arbeitet schon jemand.
|
||||||
|
*/
|
||||||
|
async function dropExcludedMatches(agentId, agent) {
|
||||||
|
if (agent.excludeCompanies.length === 0) return;
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT m.id, j.company FROM icon_scanner_matches m JOIN icon_scanner_jobs j ON j.id = m.job_id
|
||||||
|
WHERE m.agent_id = ? AND m.status <> 'in_bearbeitung'`,
|
||||||
|
[agentId]
|
||||||
|
);
|
||||||
|
const ids = rows.filter((row) => isExcludedCompany(agent, row.company)).map((row) => row.id);
|
||||||
|
if (ids.length > 0) await pool.query('DELETE FROM icon_scanner_matches WHERE id IN (?)', [ids]);
|
||||||
|
}
|
||||||
|
|
||||||
router.post('/agents', async (req, res, next) => {
|
router.post('/agents', async (req, res, next) => {
|
||||||
const agent = readAgent(req.body);
|
const agent = readAgent(req.body);
|
||||||
if (agent.error) return res.status(400).json({ error: agent.error });
|
if (agent.error) return res.status(400).json({ error: agent.error });
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`INSERT INTO icon_scanner_agents (name, keywords, alarm_keywords, sources, excludes, is_active, created_by)
|
`INSERT INTO icon_scanner_agents
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
(name, keywords, alarm_keywords, sources, excludes, exclude_companies, is_active, created_by)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[...agentValues(agent), req.user.id]
|
[...agentValues(agent), req.user.id]
|
||||||
);
|
);
|
||||||
await logHistory({
|
await logHistory({
|
||||||
@@ -169,11 +192,13 @@ router.put('/agents/:id', async (req, res, next) => {
|
|||||||
try {
|
try {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`UPDATE icon_scanner_agents
|
`UPDATE icon_scanner_agents
|
||||||
SET name = ?, keywords = ?, alarm_keywords = ?, sources = ?, excludes = ?, is_active = ?
|
SET name = ?, keywords = ?, alarm_keywords = ?, sources = ?, excludes = ?, exclude_companies = ?,
|
||||||
|
is_active = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[...agentValues(agent), Number(req.params.id)]
|
[...agentValues(agent), Number(req.params.id)]
|
||||||
);
|
);
|
||||||
if (result.affectedRows === 0) return res.status(404).json({ error: 'Suchagent nicht gefunden.' });
|
if (result.affectedRows === 0) return res.status(404).json({ error: 'Suchagent nicht gefunden.' });
|
||||||
|
await dropExcludedMatches(Number(req.params.id), agent);
|
||||||
const [[row]] = await pool.query('SELECT * FROM icon_scanner_agents WHERE id = ?', [Number(req.params.id)]);
|
const [[row]] = await pool.query('SELECT * FROM icon_scanner_agents WHERE id = ?', [Number(req.params.id)]);
|
||||||
res.json({ agent: agentOut(row) });
|
res.json({ agent: agentOut(row) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ export function agentTerms(agent) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ausschluss-Unternehmen: geprueft wird nur der Firmenname der Anzeige, nicht
|
||||||
|
* der Text - "Hays" soll Anzeigen von Hays ausblenden, nicht jede, die Hays
|
||||||
|
* irgendwo erwaehnt.
|
||||||
|
*/
|
||||||
|
export function isExcludedCompany(agent, company) {
|
||||||
|
if (!company) return false;
|
||||||
|
return (agent.excludeCompanies ?? []).some((term) => containsTerm(company, term));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {{ score: number, hits: string[], alarm: boolean } | null}
|
* @returns {{ score: number, hits: string[], alarm: boolean } | null}
|
||||||
* null = ausgeschlossen oder kein einziges Schlagwort gefunden.
|
* null = ausgeschlossen oder kein einziges Schlagwort gefunden.
|
||||||
@@ -64,6 +74,7 @@ export function scoreJob(agent, job) {
|
|||||||
const all = `${title}\n${job.company ?? ''}\n${job.description ?? ''}`;
|
const all = `${title}\n${job.company ?? ''}\n${job.description ?? ''}`;
|
||||||
|
|
||||||
if ((agent.excludes ?? []).some((term) => containsTerm(all, term))) return null;
|
if ((agent.excludes ?? []).some((term) => containsTerm(all, term))) return null;
|
||||||
|
if (isExcludedCompany(agent, job.company)) return null;
|
||||||
|
|
||||||
const keywords = agentTerms(agent);
|
const keywords = agentTerms(agent);
|
||||||
if (keywords.length === 0) return null;
|
if (keywords.length === 0) return null;
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ async function loadActiveAgents() {
|
|||||||
alarmKeywords: parseList(row.alarm_keywords),
|
alarmKeywords: parseList(row.alarm_keywords),
|
||||||
sources: parseList(row.sources),
|
sources: parseList(row.sources),
|
||||||
excludes: parseList(row.excludes),
|
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,
|
-- Schlagwoerter mit Alarm: werden wie `keywords` gesucht und gezaehlt,
|
||||||
-- ein Fund meldet sich zusaetzlich mit Ton und Pop-up.
|
-- ein Fund meldet sich zusaetzlich mit Ton und Pop-up.
|
||||||
alarm_keywords TEXT NULL,
|
alarm_keywords TEXT NULL,
|
||||||
|
-- Unternehmen, deren Anzeigen dieser Agent nie zeigt (z. B. ["Hays"]).
|
||||||
|
exclude_companies TEXT NULL,
|
||||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
created_by INT UNSIGNED NULL,
|
created_by INT UNSIGNED NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import { AlarmBell, SOURCE_LABELS, SOURCE_ORDER, TermInput } from './scannerUi';
|
|||||||
*
|
*
|
||||||
* Ein Agent = Schlagwörter + Börsen + Ausschlussbegriffe. Jedes Schlagwort
|
* Ein Agent = Schlagwörter + Börsen + Ausschlussbegriffe. Jedes Schlagwort
|
||||||
* wird in jeder gewählten Börse einzeln gesucht; eine Anzeige, die einen
|
* wird in jeder gewählten Börse einzeln gesucht; eine Anzeige, die einen
|
||||||
* Ausschlussbegriff enthält, wird gar nicht erst gespeichert.
|
* Ausschlussbegriff enthält, wird gar nicht erst gespeichert. Dasselbe gilt
|
||||||
|
* für Anzeigen eines Ausschluss-Unternehmens (Abgleich nur mit dem Firmennamen).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const EMPTY: ScannerAgentInput = {
|
const EMPTY: ScannerAgentInput = {
|
||||||
@@ -26,6 +27,7 @@ const EMPTY: ScannerAgentInput = {
|
|||||||
keywords: [],
|
keywords: [],
|
||||||
sources: ['ba', 'stepstone', 'karriere'],
|
sources: ['ba', 'stepstone', 'karriere'],
|
||||||
excludes: [],
|
excludes: [],
|
||||||
|
exclude_companies: [],
|
||||||
alarm_keywords: [],
|
alarm_keywords: [],
|
||||||
is_active: true,
|
is_active: true,
|
||||||
};
|
};
|
||||||
@@ -104,7 +106,7 @@ export const ScannerAgents: React.FC = () => {
|
|||||||
<p className="max-w-3xl text-sm text-ink-500">
|
<p className="max-w-3xl text-sm text-ink-500">
|
||||||
Jeder Agent sucht seine Schlagwörter in den gewählten Börsen. Je mehr Schlagwörter in einer Anzeige
|
Jeder Agent sucht seine Schlagwörter in den gewählten Börsen. Je mehr Schlagwörter in einer Anzeige
|
||||||
vorkommen, desto höher die Quote – ein Treffer im Titel zählt doppelt. Anzeigen mit einem
|
vorkommen, desto höher die Quote – ein Treffer im Titel zählt doppelt. Anzeigen mit einem
|
||||||
Ausschlussbegriff fallen raus.
|
Ausschlussbegriff oder von einem Ausschluss-Unternehmen fallen raus.
|
||||||
</p>
|
</p>
|
||||||
<PrimaryButton type="button" onClick={() => setEditing({ id: null, value: EMPTY })}>
|
<PrimaryButton type="button" onClick={() => setEditing({ id: null, value: EMPTY })}>
|
||||||
+ Neuer Suchagent
|
+ Neuer Suchagent
|
||||||
@@ -146,6 +148,7 @@ export const ScannerAgents: React.FC = () => {
|
|||||||
keywords: agent.keywords,
|
keywords: agent.keywords,
|
||||||
sources: agent.sources,
|
sources: agent.sources,
|
||||||
excludes: agent.excludes,
|
excludes: agent.excludes,
|
||||||
|
exclude_companies: agent.exclude_companies,
|
||||||
alarm_keywords: agent.alarm_keywords,
|
alarm_keywords: agent.alarm_keywords,
|
||||||
is_active: agent.is_active,
|
is_active: agent.is_active,
|
||||||
},
|
},
|
||||||
@@ -207,6 +210,15 @@ export const ScannerAgents: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
{agent.exclude_companies.length > 0 && (
|
||||||
|
<Row label="Ausschluss Unternehmen">
|
||||||
|
{agent.exclude_companies.map((term) => (
|
||||||
|
<span key={term} className="rounded-full border border-red-200 bg-red-50 px-2 py-0.5 text-xs text-red-700">
|
||||||
|
{term}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -272,6 +284,18 @@ export const ScannerAgents: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Ausschluss Unternehmen"
|
||||||
|
hint="Anzeigen dieser Unternehmen zeigt der Agent nicht – geprüft wird nur der Firmenname. Vorhandene Treffer verschwinden beim Speichern."
|
||||||
|
>
|
||||||
|
<TermInput
|
||||||
|
value={value.exclude_companies}
|
||||||
|
onChange={(exclude_companies) => patch({ exclude_companies })}
|
||||||
|
placeholder="z. B. Hays, Randstad"
|
||||||
|
tone="red"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="field-label mb-1 block text-xs font-semibold uppercase tracking-wide text-ink-500">Börsen</p>
|
<p className="field-label mb-1 block text-xs font-semibold uppercase tracking-wide text-ink-500">Börsen</p>
|
||||||
<div className="grid gap-2 sm:grid-cols-2">
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
|
|||||||
+3
-1
@@ -479,6 +479,8 @@ export interface ScannerAgent {
|
|||||||
keywords: string[];
|
keywords: string[];
|
||||||
sources: ScannerSourceId[];
|
sources: ScannerSourceId[];
|
||||||
excludes: string[];
|
excludes: string[];
|
||||||
|
/** Unternehmen, deren Anzeigen dieser Agent nie zeigt (Abgleich mit dem Firmennamen). */
|
||||||
|
exclude_companies: string[];
|
||||||
/** Schlagwörter, deren Fund mit Ton und Pop-up gemeldet wird. */
|
/** Schlagwörter, deren Fund mit Ton und Pop-up gemeldet wird. */
|
||||||
alarm_keywords: string[];
|
alarm_keywords: string[];
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
@@ -568,7 +570,7 @@ export interface ScannerSecrets {
|
|||||||
|
|
||||||
export type ScannerAgentInput = Pick<
|
export type ScannerAgentInput = Pick<
|
||||||
ScannerAgent,
|
ScannerAgent,
|
||||||
'name' | 'keywords' | 'alarm_keywords' | 'sources' | 'excludes' | 'is_active'
|
'name' | 'keywords' | 'alarm_keywords' | 'sources' | 'excludes' | 'exclude_companies' | 'is_active'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const put = <T>(path: string, body: unknown) =>
|
const put = <T>(path: string, body: unknown) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user