Files
Sascha WolfandClaude Opus 5.5 69001e9687 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>
2026-10-27 08:50:40 +01:00

6.5 KiB
Raw Permalink Blame History

12 Globale Suche

Ein Feld oben in der Kopfleiste, zwei Modi. Übernommen aus dem ICON CRM – gleiche Gesten, gleicher Aufbau, damit niemand zweimal lernen muss, wo er hinfassen soll.

Strg+K (bzw. Cmd+K) springt von überall dorthin, Esc leert das Feld.

Modus Symbol Verhalten
Volltext Lupe läuft beim Tippen mit (220 ms Verzögerung), Treffer nach Bereichen gruppiert
Erweitert Funke eine Frage in Worten, beantwortet vom Modell – erst auf Enter

Was durchsucht wird

flowchart LR
    Q["Suchfeld"] --> API["GET /api/search?q="]
    API --> L[("crm_leads")]
    API --> O[("crm_opportunities")]
    API --> A[("crm_acquisitions")]
    API --> E[("crm_events")]
    API --> IC["ICON CRM<br/>fetchIconContacts()"]
Gruppe Quelle Felder
Kontakte crm_leads Name, Firma, E-Mail, Telefon, Mobil, Position, Tags, Notizen, Ort
Kontakte (ICON CRM) Schnittstelle Name, Position, Firma, E-Mail, Telefon, Tags
Opportunities crm_opportunities Titel, Firma, Beschreibung, Notizen, Tags, nächster Schritt
Akquise crm_acquisitions Titel, Firma, Beschreibung, Notizen, Tags, Absender, Anbieter, Berater
Termine crm_events Titel, Beschreibung, Ort

Gelöschtes (deleted_at) bleibt außen vor. Kontakte aus dem Vorbereich „Aus Mails" (mail_pool = 1) erscheinen mit, aber hinter den gepflegten und mit dem Hinweis „aus dem Postfach" – sie sind oft unvollständig.

Bewusst LIKE statt Volltextindex: die Datenmengen sind klein, und ein FULLTEXT-Index hülfe bei Teilwortsuche („informat" soll „Finanz Informatik" finden) nicht.

Die Kontakte des ICON CRM liegen nicht in dieser Datenbank; sie kommen über die Schnittstelle (fetchIconContacts(), eine Minute Zwischenspeicher) und werden im Speicher gefiltert. Fällt das andere Haus aus, fehlt nur diese Gruppe – die übrige Suche bleibt benutzbar. Ihre Treffer sind gold umrandet und öffnen nichts: gepflegt werden sie drüben.

Was ein Treffer öffnet

Gruppe Klick führt zu
Kontakte die Kontaktkarte (goTo('Kontakte', { leadId }))
Opportunities die Karte der Opportunity (goTo('Opportunities', { opportunityId }))
Akquise die Akquise-Ansicht
Termine die Interviews-Ansicht
Kontakte (ICON CRM) nichts – nur Anzeige

Für Kontakt und Opportunity gibt es einen Direktaufruf: navigation.goTo() nimmt dafür leadId bzw. opportunityId entgegen. Akquise und Termine kennen keinen – dort wechselt der Treffer in die Ansicht.

Erweiterte Suche

sequenceDiagram
    actor B as Benutzer
    participant E as Express
    participant M as MariaDB
    participant S as perinco_scanner
    participant O as OpenAI

    B->>E: POST /api/search/ai { question }
    E->>M: Bestand lesen (Kontakte, Opportunities, Akquise, Termine)
    E->>S: Scanner-Treffer lesen (ab Anzeigeschwelle)
    E->>E: zusammenfassen, ab 400 Zeilen je Liste kuerzen
    E->>O: Systemprompt + Bestand + Frage
    O-->>E: { answer, refs }
    E-->>B: Antwort + anklickbare Chips

Sie läuft nicht beim Tippen mit, sondern erst auf Enter – jede Anfrage kostet. Und sie erzeugt kein SQL: der Bestand wird mitgegeben. Ein Modell, das Abfragen auf die Datenbank schreiben darf, wäre eine Angriffsfläche; der Bestand ist klein genug, um ihn vollständig mitzuliefern. Wird gekürzt, sagt die Antwort das dazu.

Die genannten Datensätze kommen als refs zurück und stehen unter der Antwort als anklickbare Schaltflächen. Ein Scanner-Treffer (type: "scanner") führt in die Scanner-Ansicht.

Scanner-Treffer

Der Scanner hat eine eigene Datenbank (perinco_scanner, siehe server/src/scanner/db/config.js). Bis Oktober 2026 las die erweiterte Suche nur die crm_*-Tabellen und antwortete auf Fragen zum Scanner deshalb mit „Dazu steht nichts im Bestand." Seitdem holt loadScannerMatches() die Treffer dazu und gibt sie als scanner_matches mit:

Feld Herkunft
id, match_percentage, status, found_at matches
agent agents.name
project_title, company, portal, location, rate, published_at, already_applied, url projects

Mitgegeben wird nur, was auch die Scanner-Ansicht zeigt: Treffer ab der 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 sprengen. Ist die Scanner-Datenbank nicht erreichbar, fehlt nur dieser Teil.

Vier Regeln stehen ausdrücklich im Systemprompt, weil die Antworten sonst falsch werden:

  • daily_rate ist ein Tagessatz; null heißt nicht hinterlegt, nicht null Euro. Verglichen wird nur, wo ein Wert steht.
  • Kontakte mit mail_pool sind noch nicht gepflegt – sie zählen mit, werden aber als aus dem Postfach stammend benannt.
  • scanner_matches sind Treffer des Projekt-Scanners; Bezug in refs mit type: "scanner".
  • icon_contacts sind Kontakte des ICON CRM. Ihre Kennung ist eine Zeichenkette (icon-31) und gehört nicht in refs.

Schlüssel

Die erweiterte Suche benutzt denselben OpenAI-Schlüssel wie der Scanner – openai_api_key in der Scanner-Tabelle settings, gepflegt unter Einstellungen → KI (siehe 14 KI), ersatzweise OPENAI_API_KEY aus der Umgebung. Ein zweiter Schlüssel an zweiter Stelle wäre eine Quelle für den Fall „die eine Funktion geht, die andere nicht, und keiner weiß warum".

Modell: einstellbar unter Einstellungen → KI, Vorgabe gpt-4o-mini. Fehlt der Schlüssel, antwortet der Endpunkt mit 503 und dem Hinweis auf die Einstellungen; die Volltextsuche bleibt davon unberührt.

Der Aufruf liegt in server/src/ai.js – bewusst ohne das openai-Paket: gebraucht wird ein einziger Aufruf, und fetch ist in Node eingebaut. Der Scanner behält sein SDK, dort hängt mehr daran.

Dateien

Datei Aufgabe
server/src/routes/search.js beide Endpunkte
server/src/ai.js OpenAI-Aufruf, Schlüssel, Modellwahl
server/src/routes/ai.js Einstellungen → KI (siehe 14 KI)
server/src/routes/iconCrm.js fetchIconContacts() samt Zwischenspeicher
src/components/GlobalSearch.tsx das Feld, beide Modi, die Trefferliste
src/components/Header.tsx bindet es mittig in die Kopfleiste ein

Auf schmalen Fenstern (< 768 px) wird das Feld ausgeblendet – dort ist der Titel der Ansicht wichtiger.