feat(scanner): Schwellen im Dashboard pro Benutzer
"Anzeige ab %" und "Hinweis ab %" liegen jetzt am CRM-Benutzer (crm_users.scanner_display_threshold/scanner_match_threshold) statt global in den Scanner-Einstellungen. NULL = globaler Standard. Die Mail-Schwelle bleibt global, die KI-Suche nimmt die Anzeigeschwelle des Suchenden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e1b69fffbd
commit
69001e9687
@@ -38,6 +38,8 @@ erDiagram
|
|||||||
longtext avatar "Data-URL"
|
longtext avatar "Data-URL"
|
||||||
varchar theme "Farbwelt, NULL = perinco"
|
varchar theme "Farbwelt, NULL = perinco"
|
||||||
varchar theme_mode "light | dark, NULL = folgt dem System"
|
varchar theme_mode "light | dark, NULL = folgt dem System"
|
||||||
|
tinyint scanner_display_threshold "Anzeige ab %, NULL = globaler Standard"
|
||||||
|
tinyint scanner_match_threshold "Hinweis ab %, NULL = globaler Standard"
|
||||||
varchar password_hash "bcrypt"
|
varchar password_hash "bcrypt"
|
||||||
tinyint is_active
|
tinyint is_active
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,6 +335,14 @@ aktiv, nur langsamer (Chrome bremst Timer versteckter Tabs auf etwa einmal pro
|
|||||||
Minute) und nur, wenn zuvor irgendwo auf der Seite geklickt wurde
|
Minute) und nur, wenn zuvor irgendwo auf der Seite geklickt wurde
|
||||||
(Autoplay-Regel). Browserunabhängig meldet nur die E-Mail.
|
(Autoplay-Regel). Browserunabhängig meldet nur die E-Mail.
|
||||||
|
|
||||||
|
**Schwellen pro Benutzer:** *Anzeige ab %* und *Hinweis ab %* im Dashboard
|
||||||
|
gehören dem angemeldeten Benutzer (`crm_users.scanner_display_threshold`,
|
||||||
|
`scanner_match_threshold`, gelesen und geschrieben über `/my-thresholds`).
|
||||||
|
`NULL` heißt: der globale Wert aus den Scanner-Einstellungen gilt. Die Schwelle
|
||||||
|
für die E-Mail bleibt global (`match_threshold`), weil die Pipeline ohne
|
||||||
|
Benutzer läuft. Die KI-Suche filtert Scanner-Treffer mit der Anzeigeschwelle
|
||||||
|
des Suchenden.
|
||||||
|
|
||||||
## Endpunkte
|
## Endpunkte
|
||||||
|
|
||||||
Alle unter `/api/scanner`, alle hinter `requireAuth`:
|
Alle unter `/api/scanner`, alle hinter `requireAuth`:
|
||||||
@@ -353,5 +361,6 @@ Alle unter `/api/scanner`, alle hinter `requireAuth`:
|
|||||||
| PUT/DELETE | `/matches/:id` | Status setzen, löschen |
|
| PUT/DELETE | `/matches/:id` | Status setzen, löschen |
|
||||||
| POST | `/projects/:id/applied-check` | Abgleich „bereits beworben!" von Hand wiederholen |
|
| POST | `/projects/:id/applied-check` | Abgleich „bereits beworben!" von Hand wiederholen |
|
||||||
| GET/POST | `/settings` | Geheimnisse maskiert |
|
| GET/POST | `/settings` | Geheimnisse maskiert |
|
||||||
|
| GET/PUT | `/my-thresholds` | eigene Schwellen des angemeldeten Benutzers, `null` = Standard |
|
||||||
| POST | `/scan` | Lauf starten, `409` wenn schon einer läuft |
|
| POST | `/scan` | Lauf starten, `409` wenn schon einer läuft |
|
||||||
| GET | `/status` | läuft ein Lauf, Scheduler an, Datenbankname |
|
| GET | `/status` | läuft ein Lauf, Scheduler an, Datenbankname |
|
||||||
|
|||||||
@@ -857,10 +857,14 @@ Die Filterzeile:
|
|||||||
| **Zeitraum** | wie alt darf das Projekt sein (Standard: letzte 24 Stunden) |
|
| **Zeitraum** | wie alt darf das Projekt sein (Standard: letzte 24 Stunden) |
|
||||||
| **Agent** | nur Treffer eines Beraters |
|
| **Agent** | nur Treffer eines Beraters |
|
||||||
| **Anzeige ab %** | schwächere Treffer ausblenden |
|
| **Anzeige ab %** | schwächere Treffer ausblenden |
|
||||||
| **Hinweis ab %** | ab wann Signalton und E-Mail |
|
| **Hinweis ab %** | ab wann Signalton und Hinweisbanner |
|
||||||
|
|
||||||
Die letzten zwei Werte werden **gespeichert** und gelten auch beim nächsten Mal
|
Die letzten zwei Werte werden **pro Benutzer gespeichert**: Jeder sieht seine
|
||||||
und in den Einstellungen. Rechts daneben steht `sichtbar / gesamt`.
|
eigene Einstellung, auch am nächsten Tag und an einem anderen Rechner, und
|
||||||
|
ändert damit nichts für die Kollegen. Wer noch nichts eingestellt hat, bekommt
|
||||||
|
die Standardwerte aus *Einstellungen → Scanner*. Die E-Mail bei neuen Treffern
|
||||||
|
richtet sich immer nach dem Standardwert dort, nicht nach dem eigenen Wert.
|
||||||
|
Rechts daneben steht `sichtbar / gesamt`.
|
||||||
|
|
||||||
Die Liste zeigt standardmäßig die **neuesten Treffer oben** (Spalte *Gefunden am*,
|
Die Liste zeigt standardmäßig die **neuesten Treffer oben** (Spalte *Gefunden am*,
|
||||||
absteigend). Ein Klick auf einen Spaltenkopf sortiert um, ein zweiter dreht die
|
absteigend). Ein Klick auf einen Spaltenkopf sortiert um, ein zweiter dreht die
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ Quelltext.
|
|||||||
| **Session** (Portal) | Gespeicherte Cookies einer angemeldeten Börsen-Sitzung, im Playwright-Format `storageState`. |
|
| **Session** (Portal) | Gespeicherte Cookies einer angemeldeten Börsen-Sitzung, im Playwright-Format `storageState`. |
|
||||||
| **Sitzung** (CRM) | Die eigene Anmeldung, als JWT im `localStorage`, 12 Stunden gültig. |
|
| **Sitzung** (CRM) | Die eigene Anmeldung, als JWT im `localStorage`, 12 Stunden gültig. |
|
||||||
| **Suchintervall** | Abstand zwischen zwei automatischen Suchläufen, in Minuten. Einstellung `search_interval`. |
|
| **Suchintervall** | Abstand zwischen zwei automatischen Suchläufen, in Minuten. Einstellung `search_interval`. |
|
||||||
| **Schwellenwert Anzeige** | `display_threshold` - Treffer darunter bleiben im Dashboard ausgeblendet. |
|
| **Schwellenwert Anzeige** | `display_threshold` - Treffer darunter bleiben im Dashboard ausgeblendet. Globaler Standard; jeder Benutzer kann ihn mit `crm_users.scanner_display_threshold` überschreiben. |
|
||||||
| **Schwellenwert Benachrichtigung** | `match_threshold` - ab hier gibt es Signalton und E-Mail. |
|
| **Schwellenwert Benachrichtigung** | `match_threshold` - ab hier gibt es Signalton und E-Mail. |
|
||||||
| **Verbindung** | Kante zwischen zwei Kontakten im Netzwerkdiagramm, mit Art und Stärke. Tabelle `crm_lead_connections`. |
|
| **Verbindung** | Kante zwischen zwei Kontakten im Netzwerkdiagramm, mit Art und Stärke. Tabelle `crm_lead_connections`. |
|
||||||
| **Volumen** | Tagessatz x Personentage - oder der **Projektpreis**, wenn einer eingetragen ist. Ein mit der Abschlusswahrscheinlichkeit gewichtetes Volumen gibt es nicht (seit 18.08.2026 entfernt); so wird bei perinco nicht gerechnet. |
|
| **Volumen** | Tagessatz x Personentage - oder der **Projektpreis**, wenn einer eingetragen ist. Ein mit der Abschlusswahrscheinlichkeit gewichtetes Volumen gibt es nicht (seit 18.08.2026 entfernt); so wird bei perinco nicht gerechnet. |
|
||||||
|
|||||||
+2
-1
@@ -103,7 +103,8 @@ Treffer dazu und gibt sie als `scanner_matches` mit:
|
|||||||
| `project_title`, `company`, `portal`, `location`, `rate`, `published_at`, `already_applied`, `url` | `projects` |
|
| `project_title`, `company`, `portal`, `location`, `rate`, `published_at`, `already_applied`, `url` | `projects` |
|
||||||
|
|
||||||
Mitgegeben wird nur, was auch die Scanner-Ansicht zeigt: Treffer ab der
|
Mitgegeben wird nur, was auch die Scanner-Ansicht zeigt: Treffer ab der
|
||||||
Anzeigeschwelle (`display_threshold`), die neuesten zuerst, höchstens 400. Die
|
Anzeigeschwelle des suchenden Benutzers (`crm_users.scanner_display_threshold`,
|
||||||
|
sonst der globale `display_threshold`), die neuesten zuerst, höchstens 400. Die
|
||||||
Projektbeschreibung bleibt draußen – sie ist lang und würde den Kontext
|
Projektbeschreibung bleibt draußen – sie ist lang und würde den Kontext
|
||||||
sprengen. Ist die Scanner-Datenbank nicht erreichbar, fehlt nur dieser Teil.
|
sprengen. Ist die Scanner-Datenbank nicht erreichbar, fehlt nur dieser Teil.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import { Agent, Portal, Project, Match, Setting } from '../scanner/models/index.js';
|
import { Agent, Portal, Project, Match, Setting } from '../scanner/models/index.js';
|
||||||
|
import { pool } from '../db.js';
|
||||||
import { parsePdfCv } from '../scanner/services/pdfParser.js';
|
import { parsePdfCv } from '../scanner/services/pdfParser.js';
|
||||||
import { encrypt } from '../scanner/utils/crypto.js';
|
import { encrypt } from '../scanner/utils/crypto.js';
|
||||||
import { ensureUploadDir, resolveCvPath } from '../scanner/uploadDir.js';
|
import { ensureUploadDir, resolveCvPath } from '../scanner/uploadDir.js';
|
||||||
@@ -518,6 +519,51 @@ router.post('/settings', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Persoenliche Schwellen fuers Dashboard. Sie liegen am CRM-Benutzer und nicht
|
||||||
|
* in den Scanner-Einstellungen, damit jeder seine eigene Ansicht hat. null =
|
||||||
|
* nichts eigenes gesetzt, dann gilt der globale Standard.
|
||||||
|
*/
|
||||||
|
function parseThreshold(value) {
|
||||||
|
if (value === null || value === undefined || value === '') return null;
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
if (Number.isNaN(parsed)) return undefined;
|
||||||
|
return Math.max(0, Math.min(100, parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/my-thresholds', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
'SELECT scanner_display_threshold, scanner_match_threshold FROM crm_users WHERE id = ?',
|
||||||
|
[req.user.id]
|
||||||
|
);
|
||||||
|
const row = rows[0] || {};
|
||||||
|
res.json({
|
||||||
|
display_threshold: row.scanner_display_threshold ?? null,
|
||||||
|
match_threshold: row.scanner_match_threshold ?? null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/my-thresholds', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const display = parseThreshold(req.body?.display_threshold);
|
||||||
|
const match = parseThreshold(req.body?.match_threshold);
|
||||||
|
if (display === undefined || match === undefined) {
|
||||||
|
return res.status(400).json({ error: 'Schwellen müssen Zahlen zwischen 0 und 100 sein.' });
|
||||||
|
}
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE crm_users SET scanner_display_threshold = ?, scanner_match_threshold = ? WHERE id = ?',
|
||||||
|
[display, match, req.user.id]
|
||||||
|
);
|
||||||
|
res.json({ display_threshold: display, match_threshold: match });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// MANUELLER SCAN
|
// MANUELLER SCAN
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ router.post('/ai', async (req, res, next) => {
|
|||||||
// "dazu steht nichts im Bestand", obwohl die Person in der Liste steht.
|
// "dazu steht nichts im Bestand", obwohl die Person in der Liste steht.
|
||||||
const icon = await fetchIconContacts();
|
const icon = await fetchIconContacts();
|
||||||
|
|
||||||
const scannerMatches = await loadScannerMatches();
|
const scannerMatches = await loadScannerMatches(req.user.id);
|
||||||
|
|
||||||
const truncated = [leads, opportunities, acquisitions, events, scannerMatches].some(
|
const truncated = [leads, opportunities, acquisitions, events, scannerMatches].some(
|
||||||
(list) => list.length >= AI_LIMIT
|
(list) => list.length >= AI_LIMIT
|
||||||
@@ -248,10 +248,18 @@ router.post('/ai', async (req, res, next) => {
|
|||||||
* Anzeigeschwelle, ohne Projektbeschreibung - die ist lang und wuerde den
|
* Anzeigeschwelle, ohne Projektbeschreibung - die ist lang und wuerde den
|
||||||
* Kontext sprengen. Faellt der Scanner aus, fehlt nur dieser Teil.
|
* Kontext sprengen. Faellt der Scanner aus, fehlt nur dieser Teil.
|
||||||
*/
|
*/
|
||||||
async function loadScannerMatches() {
|
async function loadScannerMatches(userId) {
|
||||||
try {
|
try {
|
||||||
|
// Die eigene Schwelle des Suchenden geht vor, sonst der globale Standard.
|
||||||
|
const [users] = await pool.query(
|
||||||
|
'SELECT scanner_display_threshold FROM crm_users WHERE id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
let threshold = users[0]?.scanner_display_threshold;
|
||||||
|
if (threshold === null || threshold === undefined) {
|
||||||
const setting = await ScannerSetting.findOne({ where: { keyName: 'display_threshold' } });
|
const setting = await ScannerSetting.findOne({ where: { keyName: 'display_threshold' } });
|
||||||
const threshold = Number(setting?.value) || 0;
|
threshold = Number(setting?.value) || 0;
|
||||||
|
}
|
||||||
const [rows] = await scannerDb.query(
|
const [rows] = await scannerDb.query(
|
||||||
`SELECT m.id, m.match_percentage, m.status, m.created_at AS found_at,
|
`SELECT m.id, m.match_percentage, m.status, m.created_at AS found_at,
|
||||||
a.name AS agent, p.title AS project_title, p.company, p.portal, p.location,
|
a.name AS agent, p.title AS project_title, p.company, p.portal, p.location,
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS avatar LONGTEXT NULL AFTER color;
|
|||||||
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS theme VARCHAR(32) NULL AFTER avatar;
|
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS theme VARCHAR(32) NULL AFTER avatar;
|
||||||
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS theme_mode VARCHAR(16) NULL AFTER theme;
|
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS theme_mode VARCHAR(16) NULL AFTER theme;
|
||||||
|
|
||||||
|
-- Eigene Schwellen im Scanner-Dashboard ("Anzeige ab %", "Hinweis ab %").
|
||||||
|
-- NULL = der Standard aus den Scanner-Einstellungen gilt. Die Schwelle fuer
|
||||||
|
-- Mail-Benachrichtigungen bleibt global, sie laeuft serverseitig ohne Benutzer.
|
||||||
|
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS scanner_display_threshold TINYINT UNSIGNED NULL AFTER theme_mode;
|
||||||
|
ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS scanner_match_threshold TINYINT UNSIGNED NULL AFTER scanner_display_threshold;
|
||||||
|
|
||||||
-- Objekt-ID des Microsoft-Kontos (Entra), gesetzt bei der ersten Anmeldung per
|
-- Objekt-ID des Microsoft-Kontos (Entra), gesetzt bei der ersten Anmeldung per
|
||||||
-- SSO. Sie ist die stabile Zuordnung: die E-Mail-Adresse eines Menschen kann
|
-- SSO. Sie ist die stabile Zuordnung: die E-Mail-Adresse eines Menschen kann
|
||||||
-- sich aendern, die Objekt-ID nicht. NULL = noch nie per Microsoft angemeldet;
|
-- sich aendern, die Objekt-ID nicht. NULL = noch nie per Microsoft angemeldet;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
type ScannerAgent,
|
type ScannerAgent,
|
||||||
type ScannerMatch,
|
type ScannerMatch,
|
||||||
type ScannerSettings,
|
type ScannerSettings,
|
||||||
|
type ScannerThresholds,
|
||||||
type ScannerStatus,
|
type ScannerStatus,
|
||||||
} from '../../lib/scannerApi';
|
} from '../../lib/scannerApi';
|
||||||
|
|
||||||
@@ -165,13 +166,14 @@ export const ScannerDashboard: React.FC = () => {
|
|||||||
async (silent = false) => {
|
async (silent = false) => {
|
||||||
if (!silent) setIsLoading(true);
|
if (!silent) setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const [agentRows, portalRows, matchRows, settings, statusRow] = await Promise.all([
|
const [agentRows, portalRows, matchRows, settings, statusRow, mine] = await Promise.all([
|
||||||
scannerApi.agents(),
|
scannerApi.agents(),
|
||||||
scannerApi.portals(),
|
scannerApi.portals(),
|
||||||
scannerApi.matches({ limit: 2000 }),
|
scannerApi.matches({ limit: 2000 }),
|
||||||
// Fehlende Einstellungen sind kein Grund, das Dashboard leer zu lassen.
|
// Fehlende Einstellungen sind kein Grund, das Dashboard leer zu lassen.
|
||||||
scannerApi.settings().catch((): Partial<ScannerSettings> => ({})),
|
scannerApi.settings().catch((): Partial<ScannerSettings> => ({})),
|
||||||
scannerApi.status().catch((): ScannerStatus | null => null),
|
scannerApi.status().catch((): ScannerStatus | null => null),
|
||||||
|
scannerApi.myThresholds().catch((): ScannerThresholds | null => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setAgents(agentRows);
|
setAgents(agentRows);
|
||||||
@@ -181,16 +183,15 @@ export const ScannerDashboard: React.FC = () => {
|
|||||||
// den Knopf hier genauso sperren wie ein selbst gestarteter.
|
// den Knopf hier genauso sperren wie ein selbst gestarteter.
|
||||||
if (statusRow) setIsScanning(statusRow.scanRunning);
|
if (statusRow) setIsScanning(statusRow.scanRunning);
|
||||||
|
|
||||||
const parsedDisplay = parseInt(
|
// Eigene Schwellen des Benutzers gehen vor, sonst der globale Standard.
|
||||||
settings.display_threshold ?? SCANNER_SETTINGS_DEFAULTS.display_threshold,
|
const parsedDisplay =
|
||||||
10
|
mine?.display_threshold ??
|
||||||
);
|
parseInt(settings.display_threshold ?? SCANNER_SETTINGS_DEFAULTS.display_threshold, 10);
|
||||||
if (!Number.isNaN(parsedDisplay)) setDisplayThreshold(parsedDisplay);
|
if (!Number.isNaN(parsedDisplay)) setDisplayThreshold(parsedDisplay);
|
||||||
|
|
||||||
const parsedNotify = parseInt(
|
const parsedNotify =
|
||||||
settings.match_threshold ?? SCANNER_SETTINGS_DEFAULTS.match_threshold,
|
mine?.match_threshold ??
|
||||||
10
|
parseInt(settings.match_threshold ?? SCANNER_SETTINGS_DEFAULTS.match_threshold, 10);
|
||||||
);
|
|
||||||
if (!Number.isNaN(parsedNotify)) {
|
if (!Number.isNaN(parsedNotify)) {
|
||||||
notifyThreshold.current = parsedNotify;
|
notifyThreshold.current = parsedNotify;
|
||||||
setMatchThreshold(parsedNotify);
|
setMatchThreshold(parsedNotify);
|
||||||
@@ -282,15 +283,15 @@ export const ScannerDashboard: React.FC = () => {
|
|||||||
if (saveTimer.current) window.clearTimeout(saveTimer.current);
|
if (saveTimer.current) window.clearTimeout(saveTimer.current);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/** Beide Schwellenwerte liegen in den Einstellungen - gebuendelt speichern. */
|
/** Beide Schwellenwerte gehoeren dem Benutzer - gebuendelt speichern. */
|
||||||
const persistThresholds = (display: number, notify: number) => {
|
const persistThresholds = (display: number, notify: number) => {
|
||||||
if (saveTimer.current) window.clearTimeout(saveTimer.current);
|
if (saveTimer.current) window.clearTimeout(saveTimer.current);
|
||||||
setSaveState('pending');
|
setSaveState('pending');
|
||||||
saveTimer.current = window.setTimeout(async () => {
|
saveTimer.current = window.setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await scannerApi.saveSettings({
|
await scannerApi.saveMyThresholds({
|
||||||
display_threshold: String(display),
|
display_threshold: display,
|
||||||
match_threshold: String(notify),
|
match_threshold: notify,
|
||||||
});
|
});
|
||||||
setSaveState('saved');
|
setSaveState('saved');
|
||||||
window.setTimeout(() => setSaveState('idle'), 1500);
|
window.setTimeout(() => setSaveState('idle'), 1500);
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export const ScannerSettings: React.FC = () => {
|
|||||||
className={inputClass}
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-ink-400">
|
<p className="mt-1 text-xs text-ink-400">
|
||||||
Treffer darunter bleiben im Dashboard ausgeblendet.
|
Standard: Treffer darunter bleiben im Dashboard ausgeblendet. Jeder Benutzer kann im Dashboard einen eigenen Wert setzen.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -181,7 +181,7 @@ export const ScannerSettings: React.FC = () => {
|
|||||||
className={inputClass}
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-ink-400">
|
<p className="mt-1 text-xs text-ink-400">
|
||||||
Ab diesem Wert gibt es Signalton und E-Mail.
|
Ab diesem Wert gibt es eine E-Mail. Für Signalton und Hinweis ist es der Standard, jeder Benutzer kann im Dashboard einen eigenen Wert setzen.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -104,6 +104,11 @@ export interface ScannerSettings {
|
|||||||
applied_check_days: string;
|
applied_check_days: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScannerThresholds {
|
||||||
|
display_threshold: number | null;
|
||||||
|
match_threshold: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const SCANNER_SETTINGS_DEFAULTS: ScannerSettings = {
|
export const SCANNER_SETTINGS_DEFAULTS: ScannerSettings = {
|
||||||
scanning_active: 'true',
|
scanning_active: 'true',
|
||||||
search_interval: '60',
|
search_interval: '60',
|
||||||
@@ -214,6 +219,14 @@ export const scannerApi = {
|
|||||||
body: JSON.stringify(input),
|
body: JSON.stringify(input),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Eigene Schwellen des angemeldeten Benutzers; null = globaler Standard.
|
||||||
|
myThresholds: () => apiRequest<ScannerThresholds>('/scanner/my-thresholds'),
|
||||||
|
saveMyThresholds: (input: ScannerThresholds) =>
|
||||||
|
apiRequest<ScannerThresholds>('/scanner/my-thresholds', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
}),
|
||||||
|
|
||||||
// ---------- Scan ----------
|
// ---------- Scan ----------
|
||||||
scan: () => apiRequest<{ message: string }>('/scanner/scan', { method: 'POST' }),
|
scan: () => apiRequest<{ message: string }>('/scanner/scan', { method: 'POST' }),
|
||||||
status: () => apiRequest<ScannerStatus>('/scanner/status'),
|
status: () => apiRequest<ScannerStatus>('/scanner/status'),
|
||||||
|
|||||||
Reference in New Issue
Block a user