docs: echte Umlaute in der Dokumentation
893 Stellen in Fliesstext, Tabellen und Mermaid-Beschriftungen. Dazu die sichtbaren ss-Fälle (ausschließlich, groß, außen, schließt, heißt, draußen, weiße, anstoßen, Straße). Nicht angetastet, damit die Doku weiter beschreibt, was wirklich in der Datenbank steht: - Codeblöcke und Code-Spans - dort stehen Bezeichner, SQL, Enum-Werte und Dateinamen: `mail_pool`, `'uebernommen'`, `extract.js` - in Mermaid nur der Text in Anführungszeichen; Knotennamen, Pfeile und Werte wie source = 'Mail' bleiben, wie sie sind - `empfaenger` im ER-Diagramm von crm_mail_contact_candidates: die Aufzählung dort nennt die Datenbankwerte, nicht die Anzeige Die Menü-Skizze in der Benutzeranleitung zeigt jetzt die aufgeklappten Kontakte mit "Aus Mails" samt Zähler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8b7ce448a6
commit
7973dc07b1
@@ -1,14 +1,14 @@
|
||||
# perinco CRM
|
||||
|
||||
CRM im perinco Corporate Design. Vite/React-Frontend, Express-Backend, MariaDB.
|
||||
Enthaelt den portierten *Projekt Scanner* und die lesende Anbindung des
|
||||
Vertriebspostfachs `crm@perinco.de` ueber Microsoft Graph.
|
||||
Enthält den portierten *Projekt Scanner* und die lesende Anbindung des
|
||||
Vertriebspostfachs `crm@perinco.de` über Microsoft Graph.
|
||||
|
||||
> **Dokumentation mit Funktionszeichnungen:** [`docs/`](docs/README.md)
|
||||
> — Architektur, Datenmodell, Scanner-Ablauf, Diktat, Frontend, Betrieb,
|
||||
> [Postfachanbindung](docs/11-postfach.md).
|
||||
> Zum Einstieg: [Anleitung fuer Benutzer](docs/07-anleitung-benutzer.md) ·
|
||||
> [Anleitung fuer Entwickler](docs/08-anleitung-entwickler.md) ·
|
||||
> Zum Einstieg: [Anleitung für Benutzer](docs/07-anleitung-benutzer.md) ·
|
||||
> [Anleitung für Entwickler](docs/08-anleitung-entwickler.md) ·
|
||||
> [Quellcode-Referenz](docs/09-quellcode.md)
|
||||
|
||||
## Corporate Identity
|
||||
@@ -17,12 +17,12 @@ Direkt aus dem perinco Logo gesampelt:
|
||||
|
||||
| Rolle | Wert |
|
||||
| --- | --- |
|
||||
| Primaerfarbe (Gruen) | `#6BA04A`, Akzent `#68A33F` |
|
||||
| Sekundaerfarbe (Grau) | `#6E6E6E` |
|
||||
| Primärfarbe (Grün) | `#6BA04A`, Akzent `#68A33F` |
|
||||
| Sekundärfarbe (Grau) | `#6E6E6E` |
|
||||
| Schrift | Avenir LT (wie perinco.de); Fallback Nunito Sans |
|
||||
|
||||
Die Farbskalen liegen als Tailwind-Theme in `src/index.css` (`brand-*`, `ink-*`)
|
||||
sowie die Flaechen `canvas` (Seite) und `surface` (Karten, Leisten, Dialoge).
|
||||
sowie die Flächen `canvas` (Seite) und `surface` (Karten, Leisten, Dialoge).
|
||||
|
||||
## Dark Mode
|
||||
|
||||
@@ -32,21 +32,21 @@ Doppelklick auf den Umschalter stellt das wieder her.
|
||||
|
||||
Umgesetzt ist der Dark Mode **ohne `dark:`-Varianten im Markup**: die Klasse
|
||||
`dark` auf `<html>` tauscht in `src/index.css` nur die Werte der Variablen
|
||||
`canvas`, `surface`, `ink-*`, `brand-*` und der getoenten Statusfarben. Die
|
||||
Komponenten benutzen durchgaengig diese Skalen und folgen damit automatisch.
|
||||
`canvas`, `surface`, `ink-*`, `brand-*` und der getönten Statusfarben. Die
|
||||
Komponenten benutzen durchgängig diese Skalen und folgen damit automatisch.
|
||||
|
||||
Drei Dinge bleiben in beiden Themes gleich und sind deshalb bewusst so gewaehlt:
|
||||
Drei Dinge bleiben in beiden Themes gleich und sind deshalb bewusst so gewählt:
|
||||
|
||||
| Was | Warum |
|
||||
| --- | --- |
|
||||
| `ink-950` | liegt nur unter Dialog-Overlays, muss dunkel bleiben |
|
||||
| `text-white` | steht immer auf farbigen Flaechen (Buttons, Termin-Chips) |
|
||||
| `brand-500` | das CI-Gruen der Buttons; nur die Tints darum kippen |
|
||||
| `text-white` | steht immer auf farbigen Flächen (Buttons, Termin-Chips) |
|
||||
| `brand-500` | das CI-Grün der Buttons; nur die Tints darum kippen |
|
||||
|
||||
Das Netzwerkdiagramm kann im SVG keine Tailwind-Klassen stapeln und benutzt
|
||||
deshalb `var(--color-*)` direkt. Das Logo ist fuer helle Hintergruende gemacht -
|
||||
deshalb `var(--color-*)` direkt. Das Logo ist für helle Hintergründe gemacht -
|
||||
die Utility `.logo-adaptive` dreht im Dark Mode die Helligkeit und holt per
|
||||
`hue-rotate` das Gruen zurueck.
|
||||
`hue-rotate` das Grün zurück.
|
||||
|
||||
Ein Inline-Skript in `index.html` setzt die Klasse vor dem ersten Frame, sonst
|
||||
blitzt beim Laden die helle Variante auf.
|
||||
@@ -62,11 +62,11 @@ Browser ──► Apache (:443/:80, pcrm.perinco.de)
|
||||
└──► Microsoft Graph (crm@perinco.de, nur lesend)
|
||||
```
|
||||
|
||||
Der Scanner haengt unter `/api/scanner`, die Postfachanbindung unter `/api/mail` -
|
||||
Der Scanner hängt unter `/api/scanner`, die Postfachanbindung unter `/api/mail` -
|
||||
beide brauchen damit **keine** eigene Apache-Regel.
|
||||
|
||||
Der Browser spricht **nie** direkt mit MariaDB - deshalb das Express-Backend.
|
||||
Das Backend bindet ausschliesslich auf `127.0.0.1` und ist von aussen nur ueber
|
||||
Das Backend bindet ausschließlich auf `127.0.0.1` und ist von außen nur über
|
||||
den Apache-Proxy erreichbar.
|
||||
|
||||
### Datenmodell
|
||||
@@ -74,25 +74,25 @@ den Apache-Proxy erreichbar.
|
||||
| Tabelle | Inhalt |
|
||||
| --- | --- |
|
||||
| `crm_users` | Benutzer, Rolle `admin` oder `user`, bcrypt-Hash, `color` (Kalenderfarbe), `avatar` (Foto) |
|
||||
| `crm_leads` | Kontakte inkl. `created_by` (Ersteller) und `owner_id` (zustaendig); `mail_pool = 1` ist der Vorbereich „Aus Mails" aus dem Postfachabgleich |
|
||||
| `crm_leads` | Kontakte inkl. `created_by` (Ersteller) und `owner_id` (zuständig); `mail_pool = 1` ist der Vorbereich „Aus Mails" aus dem Postfachabgleich |
|
||||
| `crm_opportunities` | Vertriebschancen: Phase, Tagessatz, Laufzeit, letzte Interaktion, Wahrscheinlichkeit |
|
||||
| `crm_opportunity_contacts` | Ansprechpartner je Opportunity (n:m auf `crm_leads`) mit Rolle |
|
||||
| `crm_lead_connections` | Wer steht mit wem in Kontakt; Paar aufsteigend gespeichert, Unique-Key verhindert Duplikate |
|
||||
| `crm_events` | Termine des gemeinsamen Teamkalenders; `description` kann Mention-Tokens enthalten |
|
||||
| `crm_event_participants` | Teilnehmer je Termin (n:m), Primaerschluessel verhindert Doppeleintraege |
|
||||
| `crm_event_participants` | Teilnehmer je Termin (n:m), Primärschlüssel verhindert Doppeleinträge |
|
||||
|
||||
### Rollen
|
||||
|
||||
* `admin` - sieht den Menuepunkt Einstellungen inkl. Nutzerverwaltung
|
||||
* `user` - sieht Einstellungen nicht; die API weist Zugriffe zusaetzlich mit 403 ab
|
||||
* `admin` - sieht den Menüpunkt Einstellungen inkl. Nutzerverwaltung
|
||||
* `user` - sieht Einstellungen nicht; die API weist Zugriffe zusätzlich mit 403 ab
|
||||
|
||||
Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator
|
||||
degradiert, deaktiviert oder geloescht wird.
|
||||
degradiert, deaktiviert oder gelöscht wird.
|
||||
|
||||
Ausnahme von der Admin-Regel ist `GET /api/users/directory`: die Benutzerliste
|
||||
brauchen alle Rollen fuer Netzwerkdiagramm, Zustaendigkeiten, @-Mentions,
|
||||
brauchen alle Rollen für Netzwerkdiagramm, Zuständigkeiten, @-Mentions,
|
||||
Teilnehmerauswahl und Farben. Passwort-Hashes liefert der Endpunkt nie.
|
||||
Termine darf jeder anlegen; aendern und loeschen nur der Ersteller oder ein
|
||||
Termine darf jeder anlegen; ändern und löschen nur der Ersteller oder ein
|
||||
Administrator.
|
||||
|
||||
## Struktur
|
||||
@@ -173,40 +173,40 @@ deploy/
|
||||
|
||||
## Opportunities
|
||||
|
||||
Erster Reiter im Menue und Startansicht. Drei Ansichten wie bei den Kontakten,
|
||||
Erster Reiter im Menü und Startansicht. Drei Ansichten wie bei den Kontakten,
|
||||
umgeschaltet oben rechts:
|
||||
|
||||
| Ansicht | Inhalt |
|
||||
| --- | --- |
|
||||
| **Kachel** | Karten mit Volumen, Tagessatz, Laufzeit, Zeitraum, letzter Interaktion und naechstem Schritt |
|
||||
| **Kachel** | Karten mit Volumen, Tagessatz, Laufzeit, Zeitraum, letzter Interaktion und nächstem Schritt |
|
||||
| **Liste** | sortierbare Tabelle, Spaltenkopf klicken kehrt die Richtung um |
|
||||
| **Pipeline** | Kanban nach Phase, Karten per Drag & Drop verschiebbar |
|
||||
|
||||
Die dritte Ansicht ist bewusst eine Pipeline statt eines Netzwerks: bei einer
|
||||
Vertriebschance interessiert der Fortschritt durch die Phasen. Wer nicht ziehen
|
||||
mag oder auf dem Tablet arbeitet, aendert die Phase ueber das Auswahlfeld auf der
|
||||
mag oder auf dem Tablet arbeitet, ändert die Phase über das Auswahlfeld auf der
|
||||
Karte.
|
||||
|
||||
**Felder:** Titel, Firma, Projektbeschreibung (in Kachel und Liste
|
||||
aufklappbar), Phase, Abschlusswahrscheinlichkeit, Tagessatz, Laufzeit in
|
||||
Personentagen (halbe Tage moeglich), Projektstart und -ende, letzte Interaktion
|
||||
mit Notiz, naechster Schritt, Tags, interne Notizen, Zustaendigkeit sowie
|
||||
Personentagen (halbe Tage möglich), Projektstart und -ende, letzte Interaktion
|
||||
mit Notiz, nächster Schritt, Tags, interne Notizen, Zuständigkeit sowie
|
||||
beliebig viele Ansprechpartner aus den Kontakten - jeder mit eigener Rolle.
|
||||
|
||||
**Volumen** rechnet die App aus Tagessatz x Personentagen, **gewichtet** zusaetzlich
|
||||
**Volumen** rechnet die App aus Tagessatz x Personentagen, **gewichtet** zusätzlich
|
||||
mit der Wahrscheinlichkeit. Beides steht live im Formular, pro Karte, als Summe
|
||||
ueber der Liste, je Pipeline-Spalte und in der Kopfzeile.
|
||||
über der Liste, je Pipeline-Spalte und in der Kopfzeile.
|
||||
|
||||
**Sortieren** geht doppelt: ueber das Auswahlfeld in der Steuerleiste (wirkt in
|
||||
allen Ansichten) und in der Liste ueber die Spaltenkoepfe. Geld- und Datumsfelder
|
||||
**Sortieren** geht doppelt: über das Auswahlfeld in der Steuerleiste (wirkt in
|
||||
allen Ansichten) und in der Liste über die Spaltenköpfe. Geld- und Datumsfelder
|
||||
starten absteigend, Text aufsteigend; leere Werte landen immer hinten.
|
||||
|
||||
Aendern und loeschen darf der Ersteller, der Zustaendige und jeder Administrator.
|
||||
Ändern und löschen darf der Ersteller, der Zuständige und jeder Administrator.
|
||||
|
||||
## Projekt Scanner
|
||||
|
||||
Der eigenstaendige *Projekt Scanner* ist vollstaendig hier eingezogen und soll
|
||||
die alte App ersetzen. Die laeuft weiter unter `scanner.perinco.de`:
|
||||
Der eigenständige *Projekt Scanner* ist vollständig hier eingezogen und soll
|
||||
die alte App ersetzen. Die läuft weiter unter `scanner.perinco.de`:
|
||||
|
||||
```
|
||||
/var/www/Projekt Scanner/backend Express auf 0.0.0.0:5005
|
||||
@@ -215,24 +215,24 @@ systemd-Unit: project-scanner-backend (nicht "project-scanner")
|
||||
```
|
||||
|
||||
Bis zur Ablosung laufen **beide parallel und beide voll funktional**: eigene
|
||||
Datenbank, eigener Verschluesselungsschluessel, eigenes Upload-Verzeichnis,
|
||||
eigener Scheduler. Die alte App bleibt unveraendert - weder ihre Dateien noch
|
||||
Datenbank, eigener Verschlüsselungsschlüssel, eigenes Upload-Verzeichnis,
|
||||
eigener Scheduler. Die alte App bleibt unverändert - weder ihre Dateien noch
|
||||
ihre Datenbank werden angefasst.
|
||||
|
||||
Menuepunkt **Scanner** links; ein Klick klappt die Reiter der alten App darunter
|
||||
Menüpunkt **Scanner** links; ein Klick klappt die Reiter der alten App darunter
|
||||
auf:
|
||||
|
||||
| Reiter | Inhalt |
|
||||
| --- | --- |
|
||||
| **Dashboard** | Kennzahlen, Filter (Zeitraum, Agent, Schwellenwerte), sortierbare Match-Tabelle, *Jetzt scannen* |
|
||||
| **Unpassend** | als „passt nicht" markierte Treffer; zurueckholen oder loeschen |
|
||||
| **Unpassend** | als „passt nicht" markierte Treffer; zurückholen oder löschen |
|
||||
| **Suchagenten** | Beraterprofile: Name, E-Mail, Keywords, Ausschlussbegriffe, CV-PDF, aktiv/inaktiv |
|
||||
| **Boersen-Konten** | Verbindungsstatus je Portal, Zugangsdaten, Cookie-Import, Verbindungstest |
|
||||
| **Börsen-Konten** | Verbindungsstatus je Portal, Zugangsdaten, Cookie-Import, Verbindungstest |
|
||||
|
||||
Die **Einstellungen** des Scanners stehen nicht im Untermenue, sondern als Reiter
|
||||
*Scanner* unter *Einstellungen* - also nur fuer Rolle `admin`. Die Reiter selbst
|
||||
Die **Einstellungen** des Scanners stehen nicht im Untermenü, sondern als Reiter
|
||||
*Scanner* unter *Einstellungen* - also nur für Rolle `admin`. Die Reiter selbst
|
||||
sieht jede Rolle. Anzeige- und Benachrichtigungs-Schwellenwert lassen sich an
|
||||
beiden Stellen aendern; es ist derselbe Wert in der Datenbank.
|
||||
beiden Stellen ändern; es ist derselbe Wert in der Datenbank.
|
||||
|
||||
### Was portiert wurde
|
||||
|
||||
@@ -241,60 +241,60 @@ beiden Stellen aendern; es ist derselbe Wert in der Datenbank.
|
||||
| Sequelize-Modelle (`agents`, `portals`, `projects`, `matches`, `settings`) | `server/src/scanner/models/` |
|
||||
| Scraper (Playwright), Matching (OpenAI `gpt-4o-mini`), Pipeline, Scheduler, Cleanup, PDF-Parser | `server/src/scanner/services/` |
|
||||
| REST-API | `server/src/routes/scanner.js`, gemountet als `/api/scanner` |
|
||||
| Oberflaeche | `src/components/scanner/`, in Tailwind und CI-Farben neu gebaut |
|
||||
| Uebernahme aus der alten DB | `server/src/scanner/importLegacy.js` (`npm run scanner:import`) |
|
||||
| Oberfläche | `src/components/scanner/`, in Tailwind und CI-Farben neu gebaut |
|
||||
| Übernahme aus der alten DB | `server/src/scanner/importLegacy.js` (`npm run scanner:import`) |
|
||||
|
||||
Drei bewusste Unterschiede zum Original:
|
||||
|
||||
1. **Anmeldung** Die HTTP-Basic-Auth des Scanners entfaellt - alle Endpunkte
|
||||
1. **Anmeldung** Die HTTP-Basic-Auth des Scanners entfällt - alle Endpunkte
|
||||
liegen hinter `requireAuth`, also der CRM-Sitzung.
|
||||
2. **CV-PDFs** kommen ueber `GET /api/scanner/agents/:id/cv` statt aus einem
|
||||
2. **CV-PDFs** kommen über `GET /api/scanner/agents/:id/cv` statt aus einem
|
||||
statischen Verzeichnis: ein Link im neuen Tab kann kein Bearer-Token
|
||||
mitschicken, deshalb holt das Frontend die Datei und oeffnet sie als Blob.
|
||||
3. **Meldungen** laufen als Toast statt `alert()`, Loeschabfragen als Dialog.
|
||||
mitschicken, deshalb holt das Frontend die Datei und öffnet sie als Blob.
|
||||
3. **Meldungen** laufen als Toast statt `alert()`, Löschabfragen als Dialog.
|
||||
|
||||
### Parallelbetrieb - alles doppelt
|
||||
|
||||
Beide Scanner laufen **gleichzeitig und funktional**, ohne sich zu beruehren.
|
||||
Dafuer ist hier jede geteilte Ressource verdoppelt:
|
||||
Beide Scanner laufen **gleichzeitig und funktional**, ohne sich zu berühren.
|
||||
Dafür ist hier jede geteilte Ressource verdoppelt:
|
||||
|
||||
| | alte App | diese Instanz |
|
||||
| --- | --- | --- |
|
||||
| Datenbank | `project_scanner` | `perinco_scanner` (eigene Tabellen, gleiche Namen) |
|
||||
| Verschluesselung | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` - eigener Wert |
|
||||
| Verschlüsselung | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` - eigener Wert |
|
||||
| CV-PDFs | `/var/www/Projekt Scanner/backend/uploads` | `SCANNER_UPLOAD_DIR` = `/var/www/perinco_CRM/server/uploads` |
|
||||
| Scheduler | eigener Cron im alten Dienst | `SCANNER_SCHEDULER`, Standard `true` |
|
||||
| Aufraeumen | 03:00 | 03:30 |
|
||||
| Aufräumen | 03:00 | 03:30 |
|
||||
|
||||
Die Scanner-Datenbank legt das Backend beim Start **selbst an**
|
||||
(`CREATE DATABASE IF NOT EXISTS`), die Tabellen kommen aus `sequelize.sync()`,
|
||||
Portale und Standardeinstellungen werden geseedet. Es genuegt also, das Backend
|
||||
Portale und Standardeinstellungen werden geseedet. Es genügt also, das Backend
|
||||
zu starten.
|
||||
|
||||
| Variable | Bedeutung |
|
||||
| --- | --- |
|
||||
| `SCANNER_DB_NAME` | eigene Datenbank, Standard `perinco_scanner` - **nicht** auf `project_scanner` zeigen lassen |
|
||||
| `SCANNER_DB_HOST/_PORT/_USER/_PASSWORD` | leer = CRM-Zugangsdaten |
|
||||
| `SCANNER_ENCRYPTION_KEY` | eigener Schluessel fuer die Portal-Passwoerter; ohne ihn wird ein bekannter Ersatzschluessel benutzt und beim Start gewarnt |
|
||||
| `SCANNER_ENCRYPTION_KEY` | eigener Schlüssel für die Portal-Passwörter; ohne ihn wird ein bekannter Ersatzschlüssel benutzt und beim Start gewarnt |
|
||||
| `SCANNER_UPLOAD_DIR` | eigenes CV-Verzeichnis; leer = `<server>/uploads` |
|
||||
| `SCANNER_SCHEDULER` | Hintergrund-Scraper; Standard an, nur `false` schaltet ab |
|
||||
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz fuer den **allerersten** Lauf, Standard `30`. Danach traegt `settings.last_scan_at` den Zeitplan ueber Neustarts hinweg |
|
||||
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz für den **allerersten** Lauf, Standard `30`. Danach trägt `settings.last_scan_at` den Zeitplan über Neustarts hinweg |
|
||||
|
||||
> **Kosten:** Beide Instanzen bewerten dieselben Projekte unabhaengig
|
||||
> **Kosten:** Beide Instanzen bewerten dieselben Projekte unabhängig
|
||||
> voneinander - die OpenAI-Kosten fallen also doppelt an, solange die alte App
|
||||
> laeuft. Das ist der Preis des Parallelbetriebs, kein Fehler. Wer nur zuschauen
|
||||
> läuft. Das ist der Preis des Parallelbetriebs, kein Fehler. Wer nur zuschauen
|
||||
> will, setzt `SCANNER_SCHEDULER=false`; der Knopf *Jetzt scannen* funktioniert
|
||||
> dann weiter.
|
||||
|
||||
Der **Zeitversatz** ist nicht Kosmetik: beide Instanzen benutzen dieselben
|
||||
Portal-Sessions (aus derselben Cookie-Quelle) und wuerden beim ersten Start
|
||||
Portal-Sessions (aus derselben Cookie-Quelle) und würden beim ersten Start
|
||||
gleichzeitig losziehen - das provoziert Rate-Limits und CAPTCHAs bei
|
||||
Freelancermap und Freelance.de. `SCANNER_SCHEDULER_OFFSET=30` verschiebt den
|
||||
ersten Lauf um 30 Minuten, danach laufen beide im eigenen Intervall verschraenkt.
|
||||
ersten Lauf um 30 Minuten, danach laufen beide im eigenen Intervall verschränkt.
|
||||
|
||||
Ab dem zweiten Start traegt `settings.last_scan_at` den Zeitplan - die Frist
|
||||
Ab dem zweiten Start trägt `settings.last_scan_at` den Zeitplan - die Frist
|
||||
greift dann nicht mehr. Sonst begann sie bei jedem Deployment von vorn und ein
|
||||
faelliger Lauf konnte beliebig lange ausfallen (siehe
|
||||
fälliger Lauf konnte beliebig lange ausfallen (siehe
|
||||
[docs/03-scanner.md](docs/03-scanner.md#wann-laeuft-er)).
|
||||
|
||||
Innerhalb dieser Instanz kann **nur ein Lauf** gleichzeitig arbeiten: Scheduler
|
||||
@@ -303,11 +303,11 @@ Aufruf bekommt 409 und den Hinweis *„Ein Suchlauf läuft bereits"*. Das Dashbo
|
||||
zeigt oben links den Zustand - automatische Suche aktiv, nur manuelle Suche,
|
||||
pausiert, Suchlauf läuft - samt Namen der Datenbank.
|
||||
|
||||
### Daten aus der alten App uebernehmen
|
||||
### Daten aus der alten App übernehmen
|
||||
|
||||
Damit die neue Instanz nicht bei Null anfaengt, holt ein Skript Agenten, Portale
|
||||
und Einstellungen herueber. Es liest die alte Datenbank nur, geschrieben wird
|
||||
ausschliesslich in `perinco_scanner`:
|
||||
Damit die neue Instanz nicht bei Null anfängt, holt ein Skript Agenten, Portale
|
||||
und Einstellungen herüber. Es liest die alte Datenbank nur, geschrieben wird
|
||||
ausschließlich in `perinco_scanner`:
|
||||
|
||||
```bash
|
||||
cd /var/www/perinco_CRM/server
|
||||
@@ -319,42 +319,42 @@ npm run scanner:import -- --force # vorhandene Zieldaten vorher loesche
|
||||
|
||||
Quelle kommt aus `LEGACY_*` in `server/.env`:
|
||||
|
||||
| Variable | Wofuer |
|
||||
| Variable | Wofür |
|
||||
| --- | --- |
|
||||
| `LEGACY_DB_NAME` | Standard `project_scanner` |
|
||||
| `LEGACY_DB_HOST/_PORT/_USER/_PASSWORD` | leer = CRM-Zugangsdaten |
|
||||
| `LEGACY_UPLOAD_DIR` | Verzeichnis der alten CV-PDFs (`/var/www/Projekt Scanner/backend/uploads`); die Dateien werden **kopiert**. Wegen des Leerzeichens im Pfad in der `.env` in Anfuehrungszeichen setzen - systemd liest die Datei als `EnvironmentFile` und trennt sonst am Leerzeichen |
|
||||
| `LEGACY_ENCRYPTION_KEY` | nur damit lassen sich die Portal-Passwoerter uebernehmen: entschluesseln mit dem alten, neu verschluesseln mit dem eigenen Schluessel |
|
||||
| `LEGACY_UPLOAD_DIR` | Verzeichnis der alten CV-PDFs (`/var/www/Projekt Scanner/backend/uploads`); die Dateien werden **kopiert**. Wegen des Leerzeichens im Pfad in der `.env` in Anführungszeichen setzen - systemd liest die Datei als `EnvironmentFile` und trennt sonst am Leerzeichen |
|
||||
| `LEGACY_ENCRYPTION_KEY` | nur damit lassen sich die Portal-Passwörter übernehmen: entschlüsseln mit dem alten, neu verschlüsseln mit dem eigenen Schlüssel |
|
||||
|
||||
Uebernommen werden auch die Session-Cookies der Portale (Klartext-JSON in der
|
||||
Übernommen werden auch die Session-Cookies der Portale (Klartext-JSON in der
|
||||
DB) - der neue Scanner ist damit sofort scharf.
|
||||
|
||||
Agenten, Projekte und Treffer behalten ihre Ids, damit uebernommene Treffer
|
||||
Agenten, Projekte und Treffer behalten ihre Ids, damit übernommene Treffer
|
||||
weiter auf die richtigen Agenten und Projekte zeigen. Deshalb bricht das Skript
|
||||
ab, wenn im Ziel schon Agenten stehen - `--force` raeumt vorher auf.
|
||||
ab, wenn im Ziel schon Agenten stehen - `--force` räumt vorher auf.
|
||||
|
||||
> **Beim Rollout am 06.08.2026 festgestellt:** Die in der alten Datenbank
|
||||
> gespeicherten Portal-Passwoerter lassen sich mit dem dortigen
|
||||
> `ENCRYPTION_KEY` **nicht** mehr entschluesseln - der Schluessel wurde nach dem
|
||||
> gespeicherten Portal-Passwörter lassen sich mit dem dortigen
|
||||
> `ENCRYPTION_KEY` **nicht** mehr entschlüsseln - der Schlüssel wurde nach dem
|
||||
> Speichern getauscht. Die alte App kann sie damit ebenso wenig lesen; es ging
|
||||
> also nichts verloren. Wer den Notfall-Login des Scrapers will, gibt die
|
||||
> Passwoerter unter *Boersen-Konten* neu ein. Die Sessions (Cookies) sind davon
|
||||
> unberuehrt und wurden uebernommen.
|
||||
> Passwörter unter *Börsen-Konten* neu ein. Die Sessions (Cookies) sind davon
|
||||
> unberührt und wurden übernommen.
|
||||
|
||||
### Playwright
|
||||
|
||||
`playwright` ist auf **exakt 1.60.0** gepinnt - dieselbe Version wie die laufende
|
||||
Scanner-App. Deren Chromium-Build (`chromium-1223`) liegt auf dem Server bereits
|
||||
in `/root/.cache/ms-playwright`, also faellt kein zweiter Browser-Download an und
|
||||
in `/root/.cache/ms-playwright`, also fällt kein zweiter Browser-Download an und
|
||||
gescrapt wird mit der Version, die dort produktiv funktioniert. Ein `^` in der
|
||||
Version wuerde beides aufgeben, deshalb steht in `server/package.json` ein
|
||||
`comments`-Feld mit dieser Begruendung.
|
||||
Version würde beides aufgeben, deshalb steht in `server/package.json` ein
|
||||
`comments`-Feld mit dieser Begründung.
|
||||
|
||||
## Diktat (Whisper im Browser)
|
||||
|
||||
Ueberall, wo laengerer Text entsteht - Projektbeschreibung, Interaktionsnotiz,
|
||||
naechster Schritt, interne Notizen - sitzt ein Mikrofon-Button. Die Erkennung
|
||||
laeuft **vollstaendig im Browser des Benutzers**: Whisper als ONNX-Modell ueber
|
||||
Überall, wo längerer Text entsteht - Projektbeschreibung, Interaktionsnotiz,
|
||||
nächster Schritt, interne Notizen - sitzt ein Mikrofon-Button. Die Erkennung
|
||||
läuft **vollständig im Browser des Benutzers**: Whisper als ONNX-Modell über
|
||||
`@huggingface/transformers`, gerechnet auf der Grafikkarte (WebGPU) oder
|
||||
ersatzweise auf der CPU (WASM).
|
||||
|
||||
@@ -366,8 +366,8 @@ Das Backend ist daran **nicht beteiligt** und sieht keine Audiodaten - es gibt
|
||||
keinen API-Key, keine Kosten pro Minute und keinen Datenabfluss. Aufnahmen
|
||||
werden nirgends gespeichert.
|
||||
|
||||
* Die Erkennung laeuft in einem **Web Worker** (`src/lib/whisper.worker.ts`),
|
||||
damit die Oberflaeche waehrenddessen nicht einfriert.
|
||||
* Die Erkennung läuft in einem **Web Worker** (`src/lib/whisper.worker.ts`),
|
||||
damit die Oberfläche währenddessen nicht einfriert.
|
||||
* Die **ONNX-Laufzeit** wird mit ausgeliefert (Vite bundelt die `.wasm`), die
|
||||
**Modelldateien** kommen beim ersten Laden vom Hugging-Face-CDN und bleiben
|
||||
danach im Browser-Cache. Wer auch das vermeiden will, kann die Modelle auf den
|
||||
@@ -375,42 +375,42 @@ werden nirgends gespeichert.
|
||||
|
||||
### Reiter "Whisper"
|
||||
|
||||
Unten im Menue, ueber den Einstellungen, fuer **alle Rollen** sichtbar. Es gibt
|
||||
Unten im Menü, über den Einstellungen, für **alle Rollen** sichtbar. Es gibt
|
||||
dort **nichts zu konfigurieren** - genau ein Knopf:
|
||||
|
||||
1. Reiter oeffnen, dort steht *"Einmalig werden 299 MB Sprachmodell geladen."*
|
||||
1. Reiter öffnen, dort steht *"Einmalig werden 299 MB Sprachmodell geladen."*
|
||||
2. **Sprachmodell laden (299 MB)** klicken, Fortschrittsbalken abwarten
|
||||
3. Fertig - ab jetzt sind die Mikrofon-Buttons in den Formularen aktiv, auch
|
||||
nach jeder weiteren Anmeldung
|
||||
|
||||
Dazu gibt es eine Probeaufnahme und **Sprachmodell loeschen**, falls man den
|
||||
Platz zurueckhaben will. Sonst nichts: keine Modellauswahl, keine Genauigkeit,
|
||||
kein Rechenweg, keine Geraetetabelle.
|
||||
Dazu gibt es eine Probeaufnahme und **Sprachmodell löschen**, falls man den
|
||||
Platz zurückhaben will. Sonst nichts: keine Modellauswahl, keine Genauigkeit,
|
||||
kein Rechenweg, keine Gerätetabelle.
|
||||
|
||||
Das ist Absicht. Modell, Genauigkeit, Rechenweg und Sprache stehen als
|
||||
Konstanten in `src/lib/whisper.tsx` - `small`, `q4`, Grafikkarte wenn vorhanden,
|
||||
Deutsch. Jede andere Kombination war in den Messungen entweder schlechter oder
|
||||
kaputt (siehe unten); eine Auswahl haette also nur Gelegenheit geboten, etwas
|
||||
kaputt (siehe unten); eine Auswahl hätte also nur Gelegenheit geboten, etwas
|
||||
falsch zu machen.
|
||||
|
||||
Beim Anmelden wird das Modell automatisch bereitgestellt, **wenn** es schon in
|
||||
diesem Browser liegt - ein Download von 759 MB startet nie von allein. Der
|
||||
Automatismus greift bewusst nur einmal pro Seitenaufruf: wer das Modell loescht,
|
||||
Automatismus greift bewusst nur einmal pro Seitenaufruf: wer das Modell löscht,
|
||||
bekommt es nicht sofort wieder heruntergeladen.
|
||||
|
||||
### Wo das Modell liegt und wie lange
|
||||
|
||||
Nicht in Cookies - dort passen nur wenige Kilobyte. Die Dateien liegen im
|
||||
**Cache-Speicher der Seite** (`caches`, Name `transformers-cache`), der
|
||||
Sitzungen und Browserneustarts ueberlebt. Beim Laden bittet die App zusaetzlich
|
||||
Sitzungen und Browserneustarts überlebt. Beim Laden bittet die App zusätzlich
|
||||
per `navigator.storage.persist()` um dauerhaften Speicher; damit darf der Browser
|
||||
die Dateien bei Platznot nicht mehr selbst wegraeumen. Chrome entscheidet darueber
|
||||
nach eigenen Kriterien und lehnt haeufig ab - dann funktioniert alles genauso, nur
|
||||
die Dateien bei Platznot nicht mehr selbst wegräumen. Chrome entscheidet darüber
|
||||
nach eigenen Kriterien und lehnt häufig ab - dann funktioniert alles genauso, nur
|
||||
ohne diese Zusicherung.
|
||||
|
||||
Geraet oder Browserprofil gewechselt? Dann faellt der Download erneut an; der
|
||||
Speicher gehoert zum Profil, nicht zum Benutzerkonto. Findet die App das Modell
|
||||
nach dem Laden nicht im Cache, sagt sie es ausdruecklich.
|
||||
Gerät oder Browserprofil gewechselt? Dann fällt der Download erneut an; der
|
||||
Speicher gehört zum Profil, nicht zum Benutzerkonto. Findet die App das Modell
|
||||
nach dem Laden nicht im Cache, sagt sie es ausdrücklich.
|
||||
|
||||
### Warum small und q4 - die Messungen
|
||||
|
||||
@@ -424,17 +424,17 @@ eine echte Stimme) auf einem Rechner mit NVIDIA-GPU, jeweils in q4:
|
||||
| small | "auf Esfihana", "Der Tagessatz liegt bei 1100 Euro" | 299 MB | 10 s |
|
||||
| **large-v3-turbo** | **"auf S4 HANA"**, "Der Tagessatz liegt bei 1100 Euro" | 759 MB | 13 s |
|
||||
|
||||
Alle drei erkennen etwa 4x schneller als Echtzeit; auf Geraeten ohne WebGPU
|
||||
Alle drei erkennen etwa 4x schneller als Echtzeit; auf Geräten ohne WebGPU
|
||||
verschiebt sich das Tempo erheblich.
|
||||
|
||||
Festgelegt ist **large-v3-turbo**: nur dieses Modell bekommt Fachbegriffe wie
|
||||
"S4 HANA" richtig, und genau daran scheitern die kleineren im Alltag. Die 759 MB
|
||||
fallen einmal pro Browser an - danach liegt das Modell im Cache-Speicher der
|
||||
Seite und ueberlebt Browserneustarts (gemessen: 762 MB, zweite Sitzung ohne
|
||||
Seite und überlebt Browserneustarts (gemessen: 762 MB, zweite Sitzung ohne
|
||||
jeden Download bereit).
|
||||
|
||||
`small` ist der Kompromiss: turbo erkennt besser, wird vom Browser aber nicht
|
||||
zwischengespeichert (siehe Groessengrenzen unten) und laedt darum bei jedem Start
|
||||
zwischengespeichert (siehe Größengrenzen unten) und lädt darum bei jedem Start
|
||||
erneut 759 MB.
|
||||
|
||||
### Welche Genauigkeit auf welchem Rechenweg
|
||||
@@ -443,41 +443,41 @@ Deshalb ist `q4` festgelegt:
|
||||
|
||||
| | q4 | q8 | fp16 | fp32 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **WebGPU** | tiny 1,4 s · small 1,9 s · turbo 3,8 s | Kauderwelsch oder keine Antwort | tiny laeuft, small blockiert den Tab | Einzeldatei zu gross |
|
||||
| **WASM** | laeuft | small 10,3 s | ungetestet | Einzeldatei zu gross |
|
||||
| **WebGPU** | tiny 1,4 s · small 1,9 s · turbo 3,8 s | Kauderwelsch oder keine Antwort | tiny läuft, small blockiert den Tab | Einzeldatei zu groß |
|
||||
| **WASM** | läuft | small 10,3 s | ungetestet | Einzeldatei zu groß |
|
||||
|
||||
Der wichtigste Grund fuer diese Beschraenkung: bei `small` in fp16 auf WebGPU
|
||||
Der wichtigste Grund für diese Beschränkung: bei `small` in fp16 auf WebGPU
|
||||
reagiert der ganze Browser-Tab nicht mehr - **kein Timeout in der Seite kann das
|
||||
abfangen**, weil das Skript selbst nicht mehr laeuft.
|
||||
abfangen**, weil das Skript selbst nicht mehr läuft.
|
||||
|
||||
Genau deshalb ist die Auswahl verschwunden: `q4` steht fest, und damit kann diese
|
||||
Klasse von Fehlern nicht mehr auftreten. Als Netz bleibt trotzdem eine Frist pro
|
||||
Erkennung und eine Haenger-Erkennung beim Laden - falls ein Geraet trotzdem etwas
|
||||
Erkennung und eine Hänger-Erkennung beim Laden - falls ein Gerät trotzdem etwas
|
||||
Unerwartetes tut.
|
||||
|
||||
Zusaetzlich hat jede Erkennung eine Frist (15x Aufnahmedauer, mindestens 45 s,
|
||||
hoechstens 5 min). Laeuft sie ab, wird der Worker verworfen und der Fehler
|
||||
Zusätzlich hat jede Erkennung eine Frist (15x Aufnahmedauer, mindestens 45 s,
|
||||
höchstens 5 min). Läuft sie ab, wird der Worker verworfen und der Fehler
|
||||
gemeldet, statt dauerhaft "Erkenne..." zu zeigen.
|
||||
|
||||
### Die Groessengrenze des Browsers
|
||||
### Die Größengrenze des Browsers
|
||||
|
||||
Nicht die Gesamtgroesse entscheidet, sondern die **groesste Einzeldatei** - der
|
||||
Browser muss sie in einem zusammenhaengenden Speicherblock halten. Ueber etwa
|
||||
Nicht die Gesamtgröße entscheidet, sondern die **größte Einzeldatei** - der
|
||||
Browser muss sie in einem zusammenhängenden Speicherblock halten. Über etwa
|
||||
500 MB bricht das Laden mit "Array buffer allocation failed" ab. Belege: 425 MB
|
||||
(turbo in q4) laden und laufen, 688 MB (turbo in fp32) scheitern.
|
||||
|
||||
Genau daran scheitern fuer turbo alle Genauigkeiten ausser q4 - ein weiterer
|
||||
Genau daran scheitern für turbo alle Genauigkeiten ausser q4 - ein weiterer
|
||||
Grund, warum es keine Auswahl gibt.
|
||||
|
||||
Die Downloadgroesse im Reiter kommt live von Hugging Face
|
||||
Die Downloadgröße im Reiter kommt live von Hugging Face
|
||||
(`src/lib/whisperModels.ts`), damit dort eine echte Zahl steht.
|
||||
|
||||
> **Korrektur:** In einer frueheren Fassung stand hier eine zweite Grenze von
|
||||
> **Korrektur:** In einer früheren Fassung stand hier eine zweite Grenze von
|
||||
> etwa 300 MB pro Cache-Eintrag, oberhalb derer das Modell angeblich nicht
|
||||
> zwischengespeichert wird. Das war falsch - ein Artefakt der Testumgebung.
|
||||
> Von Playwright gestartete Chrome-Instanzen schreiben den Cache-Speicher nicht,
|
||||
> ein normal gestartetes Chrome dagegen sehr wohl: turbo/q4 liegt dort mit
|
||||
> 762 MB im Cache und ueberlebt einen Browserneustart. Deshalb prueft
|
||||
> 762 MB im Cache und überlebt einen Browserneustart. Deshalb prüft
|
||||
> `scratchpad/realchrome.mjs` die Persistenz gegen ein normal gestartetes
|
||||
> Chrome und nicht gegen den Automatisierungskontext.
|
||||
|
||||
@@ -490,59 +490,59 @@ Die Downloadgroesse im Reiter kommt live von Hugging Face
|
||||
|
||||
Solange nichts geladen ist, erscheint im Opportunity-Formular ein Hinweis mit
|
||||
Knopf **Zu Whisper**; die einzelnen Mikrofon-Buttons werden zu *Diktat
|
||||
einrichten*. Beides schliesst den Dialog und springt in den Reiter. Auf Geraeten
|
||||
einrichten*. Beides schließt den Dialog und springt in den Reiter. Auf Geräten
|
||||
ohne Mikrofon oder ohne HTTPS erscheint gar kein Diktat.
|
||||
|
||||
## Kalender
|
||||
|
||||
Ein gemeinsamer Teamkalender: alle Benutzer sehen dieselben Termine, jeder darf
|
||||
Eintraege anlegen.
|
||||
Einträge anlegen.
|
||||
|
||||
* **Ansichten** Monat, Woche, Tag; `Heute` und die Pfeile blaettern. Klick auf
|
||||
* **Ansichten** Monat, Woche, Tag; `Heute` und die Pfeile blättern. Klick auf
|
||||
einen Monatstag oder eine Stundenzelle legt direkt einen Termin an, Klick auf
|
||||
einen Wochentagskopf springt in die Tagesansicht.
|
||||
* **Farben** Jeder Termin hat die Farbe seines Erstellers. Die Farbe kommt aus
|
||||
`crm_users.color`; ist sie leer, wird sie stabil aus der Benutzer-ID
|
||||
abgeleitet (Palette in `src/types.ts`). Aenderbar unter *Einstellungen → Mein
|
||||
Konto* und - fuer Admins fuer alle - in der Nutzerverwaltung. Die Legende
|
||||
unter dem Kalender zeigt, welche Farbe zu wem gehoert.
|
||||
abgeleitet (Palette in `src/types.ts`). Änderbar unter *Einstellungen → Mein
|
||||
Konto* und - für Admins für alle - in der Nutzerverwaltung. Die Legende
|
||||
unter dem Kalender zeigt, welche Farbe zu wem gehört.
|
||||
* **Teilnehmer** Ein Termin kann beliebig viele Benutzer haben; sie erscheinen
|
||||
mit Foto - oder, wenn keines hinterlegt ist, als Initiale auf ihrer
|
||||
Kalenderfarbe - und sind anklickbar.
|
||||
* **@-Mentions** In der Beschreibung `@` tippen, um Kontakte *und* Benutzer zu
|
||||
markieren (Pfeiltasten, Enter/Tab zum Uebernehmen). Gespeichert wird ein Token
|
||||
markieren (Pfeiltasten, Enter/Tab zum Übernehmen). Gespeichert wird ein Token
|
||||
`@[Anzeigename](lead:12)` bzw. `@[Anzeigename](user:3)`. In der Leseansicht
|
||||
sind Mentions klickbar: Kontakte oeffnen das Kontaktfenster, Benutzer ein
|
||||
Detailfenster mit Farbe, naechsten Terminen und zugeordneten Kontakten.
|
||||
sind Mentions klickbar: Kontakte öffnen das Kontaktfenster, Benutzer ein
|
||||
Detailfenster mit Farbe, nächsten Terminen und zugeordneten Kontakten.
|
||||
Existiert die Person nicht mehr, bleibt der Name als grauer Text stehen.
|
||||
* **Naechste Termine** Rechts neben dem Kalender, nach Tagen gruppiert und
|
||||
zeitlich sortiert - der naechste anstehende Termin steht oben. Vergangene
|
||||
* **Nächste Termine** Rechts neben dem Kalender, nach Tagen gruppiert und
|
||||
zeitlich sortiert - der nächste anstehende Termin steht oben. Vergangene
|
||||
Termine fallen aus der Liste, bleiben im Kalender aber erhalten.
|
||||
|
||||
## Fotos
|
||||
|
||||
Fotos von Kontakten und Benutzern liegen als Data-URL in der Datenbank
|
||||
(`crm_leads.avatar`, `crm_users.avatar`). Damit das die Nutzlast nicht aufblaeht,
|
||||
(`crm_leads.avatar`, `crm_users.avatar`). Damit das die Nutzlast nicht aufbläht,
|
||||
verkleinert der Browser jedes Bild **vor** dem Upload
|
||||
(`src/lib/imageCompress.ts`):
|
||||
|
||||
* mittiger Ausschnitt auf ein Quadrat von 256 px - mehr braucht keine Ansicht,
|
||||
die groesste ist 80 px
|
||||
* WebP mit Qualitaet 0,82, Fallback JPEG, dann PNG
|
||||
die größte ist 80 px
|
||||
* WebP mit Qualität 0,82, Fallback JPEG, dann PNG
|
||||
* aus einem 2,9-MB-Foto werden so einige Kilobyte
|
||||
|
||||
Das Backend akzeptiert nur Bild-Data-URLs und http(s)-Adressen und begrenzt die
|
||||
Laenge; die Quelldatei darf bis 25 MB gross sein, weil sie ohnehin
|
||||
Länge; die Quelldatei darf bis 25 MB groß sein, weil sie ohnehin
|
||||
komprimiert wird.
|
||||
|
||||
Benutzerfotos pflegt jeder fuer sich unter *Einstellungen → Mein
|
||||
Konto*, Admins fuer alle in der Nutzerverwaltung. Angezeigt werden sie in der
|
||||
Benutzerfotos pflegt jeder für sich unter *Einstellungen → Mein
|
||||
Konto*, Admins für alle in der Nutzerverwaltung. Angezeigt werden sie in der
|
||||
Sidebar, bei Terminteilnehmern und Erstellern, im Benutzer-Detailfenster, in den
|
||||
@-Vorschlaegen, in der Nutzerverwaltung und als Benutzerknoten im
|
||||
Netzwerkdiagramm. Ohne Foto erscheint ueberall die Initiale auf der
|
||||
@-Vorschlägen, in der Nutzerverwaltung und als Benutzerknoten im
|
||||
Netzwerkdiagramm. Ohne Foto erscheint überall die Initiale auf der
|
||||
Kalenderfarbe (`UserAvatar`).
|
||||
|
||||
Ueberlappende Termine teilen sich im Zeitraster die Spaltenbreite, mehrtaegige
|
||||
Überlappende Termine teilen sich im Zeitraster die Spaltenbreite, mehrtägige
|
||||
Termine erscheinen an jedem betroffenen Tag, und eine rote Linie markiert die
|
||||
aktuelle Uhrzeit.
|
||||
|
||||
@@ -550,14 +550,14 @@ aktuelle Uhrzeit.
|
||||
|
||||
Kraftgerichtetes Layout (`d3-force`), gerendert als SVG.
|
||||
|
||||
* **Kreise** = Kontakte, Farbe nach Status, Groesse nach Anzahl Verbindungen
|
||||
* **Kreise** = Kontakte, Farbe nach Status, Größe nach Anzahl Verbindungen
|
||||
* **Quadrate** = Benutzer, die Kontakte angelegt haben
|
||||
* **Gruene Linie** = Kontakt ↔ Kontakt (Dicke nach Verbindungsstaerke)
|
||||
* **Grüne Linie** = Kontakt ↔ Kontakt (Dicke nach Verbindungsstärke)
|
||||
* **Graue gestrichelte Linie** = Ersteller → Kontakt
|
||||
|
||||
Bedienung: Hintergrund ziehen verschiebt, Mausrad zoomt auf den Cursor,
|
||||
Knoten ziehen fixiert ihn (gruener Punkt), Doppelklick loest die Fixierung.
|
||||
Ueber *Verbindung erstellen* zwei Kontakte anklicken, um eine Kante anzulegen;
|
||||
Knoten ziehen fixiert ihn (grüner Punkt), Doppelklick löst die Fixierung.
|
||||
Über *Verbindung erstellen* zwei Kontakte anklicken, um eine Kante anzulegen;
|
||||
Esc bricht ab. Die Ansicht respektiert Suche und Statusfilter.
|
||||
|
||||
## Lokale Entwicklung
|
||||
@@ -569,7 +569,7 @@ npm start # Backend :5006
|
||||
cd .. && npm run dev # Frontend :3100, /api wird geproxied
|
||||
```
|
||||
|
||||
Fuer lokale Entwicklung braucht es eine erreichbare MariaDB mit der Datenbank
|
||||
Für lokale Entwicklung braucht es eine erreichbare MariaDB mit der Datenbank
|
||||
`perinco_CRM`; das Schema legt das Backend beim Start selbst an. Die
|
||||
Scanner-Datenbank `perinco_scanner` legt es komplett selbst an, inklusive
|
||||
Tabellen, Portalen und Standardeinstellungen.
|
||||
@@ -611,19 +611,19 @@ Abschnitt *Playwright* oben.
|
||||
|
||||
> **Reihenfolge beachten:** Der Import schreibt `search_interval` aus der alten
|
||||
> App (15 Minuten) in die Einstellungen. Der laufende Scheduler bemerkt die
|
||||
> Aenderung binnen einer Minute und plant sich neu - beim Rollout hat er deshalb
|
||||
> Änderung binnen einer Minute und plant sich neu - beim Rollout hat er deshalb
|
||||
> direkt nach dem Import einen Scan gestartet, den der folgende Restart mitten im
|
||||
> Scrapen abgeschossen hat (`Target page ... has been closed` im Log, harmlos).
|
||||
> Wer das vermeiden will, macht Schritt 3 vor Schritt 2.
|
||||
|
||||
Das hoehere `MemoryMax` (1536M statt 512M) ist kein Luxus: ein Scan-Lauf startet
|
||||
Chromium als Kindprozess in derselben cgroup, mit 512M wuerde der OOM-Killer das
|
||||
Backend mitten im Scrapen beenden. `deploy.ps1` laedt die Unit nicht mit hoch -
|
||||
Das höhere `MemoryMax` (1536M statt 512M) ist kein Luxus: ein Scan-Lauf startet
|
||||
Chromium als Kindprozess in derselben cgroup, mit 512M würde der OOM-Killer das
|
||||
Backend mitten im Scrapen beenden. `deploy.ps1` lädt die Unit nicht mit hoch -
|
||||
daher der `cp` oben.
|
||||
|
||||
> **RAM im Parallelbetrieb:** Im schlechtesten Fall scrapen beide Scanner
|
||||
> gleichzeitig, also laufen zwei Chromium-Instanzen. `SCANNER_SCHEDULER_OFFSET`
|
||||
> verschiebt die Laeufe gegeneinander, garantiert aber nichts - der alte Dienst
|
||||
> verschiebt die Läufe gegeneinander, garantiert aber nichts - der alte Dienst
|
||||
> hat seinen eigenen Zeitplan. Auf einem Server mit knappem RAM und vollem Swap
|
||||
> ist das der Punkt, den man nach dem ersten Tag im Journal nachsieht.
|
||||
|
||||
@@ -665,4 +665,4 @@ in `deploy/pcrm.perinco.de.conf` und den certbot-Aufruf anpassen.
|
||||
|
||||
Beim ersten Backend-Start wird ein Administrator aus den `ADMIN_*`-Variablen in
|
||||
`server/.env` angelegt - aber nur, wenn `crm_users` leer ist. Das Passwort ist
|
||||
danach ausschliesslich ueber die Nutzerverwaltung bzw. *Mein Konto* aenderbar.
|
||||
danach ausschließlich über die Nutzerverwaltung bzw. *Mein Konto* änderbar.
|
||||
|
||||
+16
-16
@@ -2,7 +2,7 @@
|
||||
|
||||
## Bausteine
|
||||
|
||||
Drei Teile, keiner davon spricht direkt mit einem anderen ausser ueber HTTP bzw.
|
||||
Drei Teile, keiner davon spricht direkt mit einem anderen ausser über HTTP bzw.
|
||||
SQL:
|
||||
|
||||
| Teil | Technik | Wo |
|
||||
@@ -11,8 +11,8 @@ SQL:
|
||||
| Backend | Express 4, mysql2 (CRM), Sequelize (Scanner) | `/var/www/perinco_CRM/server`, `127.0.0.1:5006` |
|
||||
| Datenbank | MariaDB 10.11 | `perinco_CRM` und `perinco_scanner` |
|
||||
|
||||
Der Browser spricht **nie** direkt mit MariaDB - deshalb ueberhaupt ein Backend.
|
||||
Das Backend bindet ausschliesslich auf `127.0.0.1` und ist von aussen nur ueber
|
||||
Der Browser spricht **nie** direkt mit MariaDB - deshalb überhaupt ein Backend.
|
||||
Das Backend bindet ausschließlich auf `127.0.0.1` und ist von außen nur über
|
||||
den Apache-Reverse-Proxy erreichbar.
|
||||
|
||||
## Topologie
|
||||
@@ -33,20 +33,20 @@ flowchart LR
|
||||
class U2,A2,E2,S2 alt
|
||||
```
|
||||
|
||||
Der Scanner im CRM haengt unter `/api/scanner` und braucht damit **keine eigene
|
||||
Apache-Regel**: alles laeuft ueber den bestehenden `/api`-Proxy. Dasselbe gilt fuer
|
||||
Der Scanner im CRM hängt unter `/api/scanner` und braucht damit **keine eigene
|
||||
Apache-Regel**: alles läuft über den bestehenden `/api`-Proxy. Dasselbe gilt für
|
||||
die Postfachanbindung unter `/api/mail` (siehe
|
||||
[11 Postfachanbindung](11-postfach.md)).
|
||||
|
||||
## Ausgehende Verbindungen
|
||||
|
||||
Das Backend spricht von sich aus mit drei fremden Diensten. Alle drei sind
|
||||
optional - fehlen die Zugangsdaten, laeuft die Anwendung ohne den jeweiligen
|
||||
optional - fehlen die Zugangsdaten, läuft die Anwendung ohne den jeweiligen
|
||||
Teil weiter.
|
||||
|
||||
| Ziel | Wofuer | Ausgeloest von |
|
||||
| Ziel | Wofür | Ausgelöst von |
|
||||
| --- | --- | --- |
|
||||
| Projektboersen | Scraping der Projektangebote | Scanner-Scheduler |
|
||||
| Projektbörsen | Scraping der Projektangebote | Scanner-Scheduler |
|
||||
| `api.openai.com` | Bewertung Lebenslauf gegen Projekt | Scanner-Pipeline |
|
||||
| `login.microsoftonline.com`, `graph.microsoft.com` | Postfach `crm@perinco.de` lesen | Mail-Scheduler |
|
||||
|
||||
@@ -80,10 +80,10 @@ sequenceDiagram
|
||||
E-->>B: JSON
|
||||
```
|
||||
|
||||
Bei `401` loescht das Frontend das Token und springt zurueck auf den
|
||||
Bei `401` löscht das Frontend das Token und springt zurück auf den
|
||||
Anmeldeschirm (`setUnauthorizedHandler` in `src/lib/api.ts`). Das Token wird bei
|
||||
jeder Anfrage neu gegen die Datenbank geprueft - ein deaktivierter Benutzer ist
|
||||
sofort draussen, ohne auf den Ablauf des Tokens zu warten.
|
||||
jeder Anfrage neu gegen die Datenbank geprüft - ein deaktivierter Benutzer ist
|
||||
sofort draußen, ohne auf den Ablauf des Tokens zu warten.
|
||||
|
||||
## Rollen
|
||||
|
||||
@@ -98,19 +98,19 @@ Bei den Mails darf jede Rolle lesen und zuordnen - `crm@perinco.de` ist ein
|
||||
gemeinsames Vertriebspostfach. Einrichten, Verbindungstest und manueller Abgleich
|
||||
sind Administratoren vorbehalten.
|
||||
|
||||
Die Rollenpruefung sitzt **doppelt**: das Menue blendet aus, was nicht erlaubt
|
||||
ist, und das Backend weist Zugriffe zusaetzlich mit `403` ab (`requireAdmin`).
|
||||
Die Rollenprüfung sitzt **doppelt**: das Menü blendet aus, was nicht erlaubt
|
||||
ist, und das Backend weist Zugriffe zusätzlich mit `403` ab (`requireAdmin`).
|
||||
Ausnahme ist `GET /api/users/directory` - die Benutzerliste brauchen alle Rollen
|
||||
fuer Netzwerkdiagramm, Zustaendigkeiten, @-Mentions, Teilnehmerauswahl und
|
||||
für Netzwerkdiagramm, Zuständigkeiten, @-Mentions, Teilnehmerauswahl und
|
||||
Kalenderfarben. Passwort-Hashes liefert der Endpunkt nie.
|
||||
|
||||
Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator
|
||||
degradiert, deaktiviert oder geloescht wird.
|
||||
degradiert, deaktiviert oder gelöscht wird.
|
||||
|
||||
## Belegte Ports auf dem Server
|
||||
|
||||
Der Rechner hostet weitere Dienste (n8n, Docker, ~15 Apache-Vhosts). Vor jeder
|
||||
Portbelegung `ss -tlnp` pruefen. Relevant hier:
|
||||
Portbelegung `ss -tlnp` prüfen. Relevant hier:
|
||||
|
||||
| Port | Dienst |
|
||||
| --- | --- |
|
||||
|
||||
+34
-34
@@ -7,9 +7,9 @@ Zwei Datenbanken, absichtlich getrennt:
|
||||
| `perinco_CRM` | Benutzer, Kontakte, Opportunities, Termine, Postfach, Einstellungen | `mysql2` direkt, Schema aus `server/src/schema.sql` |
|
||||
| `perinco_scanner` | Suchagenten, Portale, Projekte, Treffer, Scanner-Einstellungen | Sequelize, Schema aus den Modellen (`sequelize.sync()`) |
|
||||
|
||||
Die Trennung ist kein Zufall: die Scanner-Tabellen sind eine 1:1-Uebernahme aus
|
||||
der eigenstaendigen App. Gleiche Tabellennamen, gleiche Spalten - nur in einer
|
||||
eigenen Datenbank, damit beide Anwendungen parallel laufen koennen.
|
||||
Die Trennung ist kein Zufall: die Scanner-Tabellen sind eine 1:1-Übernahme aus
|
||||
der eigenständigen App. Gleiche Tabellennamen, gleiche Spalten - nur in einer
|
||||
eigenen Datenbank, damit beide Anwendungen parallel laufen können.
|
||||
|
||||
## perinco_CRM
|
||||
|
||||
@@ -61,7 +61,7 @@ erDiagram
|
||||
enum stage "Neu .. Verloren"
|
||||
tinyint probability "0-100"
|
||||
decimal daily_rate
|
||||
decimal duration_days "Personentage, halbe moeglich"
|
||||
decimal duration_days "Personentage, halbe möglich"
|
||||
date last_interaction_at
|
||||
varchar next_step
|
||||
int owner_id FK
|
||||
@@ -88,15 +88,15 @@ erDiagram
|
||||
|
||||
Vier Eigenheiten, die man kennen muss:
|
||||
|
||||
- **`crm_leads` fuehrt zwei Bestaende.** `mail_pool = 0` ist der gepflegte
|
||||
- **`crm_leads` führt zwei Bestände.** `mail_pool = 0` ist der gepflegte
|
||||
Kontaktbestand, `mail_pool = 1` der Vorbereich „Aus Mails" mit den Funden des
|
||||
Postfachabgleichs. Nur der Kontaktbestand erscheint in Kontaktliste,
|
||||
Opportunities, Kalender und Netzwerk; der Vorbereich hat seine eigene Ansicht.
|
||||
Eine Spalte statt einer zweiten Tabelle, damit `crm_mail_links` und
|
||||
`crm_mail_contact_candidates` fuer beide Bestaende gelten
|
||||
`crm_mail_contact_candidates` für beide Bestände gelten
|
||||
([11 Postfachanbindung](11-postfach.md)).
|
||||
- **`tags` ist Text, kein JSON-Typ.** Bewusst, damit es auf aelteren
|
||||
MariaDB-Versionen ohne JSON-Funktionen laeuft. Das Backend serialisiert.
|
||||
- **`tags` ist Text, kein JSON-Typ.** Bewusst, damit es auf älteren
|
||||
MariaDB-Versionen ohne JSON-Funktionen läuft. Das Backend serialisiert.
|
||||
- **`crm_lead_connections` speichert das Paar sortiert** (kleinere Id in
|
||||
`from_lead_id`). Der Unique-Key verhindert damit auch die Umkehrung als
|
||||
Duplikat, obwohl die Verbindung ungerichtet gedacht ist.
|
||||
@@ -106,7 +106,7 @@ Vier Eigenheiten, die man kennen muss:
|
||||
|
||||
### Postfach crm@perinco.de
|
||||
|
||||
Diese Tabellen liegen in derselben Datenbank, haengen aber nur ueber
|
||||
Diese Tabellen liegen in derselben Datenbank, hängen aber nur über
|
||||
`crm_mail_links` und `crm_mail_contact_candidates.lead_id` am Rest. Fachliche
|
||||
Beschreibung in [11 Postfachanbindung](11-postfach.md).
|
||||
|
||||
@@ -116,21 +116,21 @@ erDiagram
|
||||
crm_mail_messages ||--o{ crm_mail_contact_candidates : liefert
|
||||
crm_leads ||--o{ crm_mail_links : "lead_id"
|
||||
crm_opportunities ||--o{ crm_mail_links : "opportunity_id"
|
||||
crm_leads ||--o{ crm_mail_contact_candidates : "lead_id, nach Uebernahme"
|
||||
crm_leads ||--o{ crm_mail_contact_candidates : "lead_id, nach Übernahme"
|
||||
|
||||
crm_mail_messages {
|
||||
int id PK
|
||||
varchar graph_id "Id aus Microsoft Graph"
|
||||
char graph_id_hash UK "SHA-256 hex, traegt die Eindeutigkeit"
|
||||
char graph_id_hash UK "SHA-256 hex, trägt die Eindeutigkeit"
|
||||
varchar conversation_id "Unterhaltung"
|
||||
varchar subject
|
||||
varchar from_email
|
||||
longtext to_json "[{name,email}] als Text"
|
||||
datetime received_at "UTC"
|
||||
longtext body_text "aus HTML gewandelt, Zeilen erhalten"
|
||||
longtext body_html "entschaerft fuer die Anzeige"
|
||||
longtext body_html "entschärft für die Anzeige"
|
||||
tinyint has_attachments
|
||||
tinyint is_deleted "im Postfach geloescht"
|
||||
tinyint is_deleted "im Postfach gelöscht"
|
||||
}
|
||||
crm_mail_links {
|
||||
int id PK
|
||||
@@ -151,13 +151,13 @@ erDiagram
|
||||
varchar company
|
||||
enum origin "absender | empfaenger | signatur | text"
|
||||
tinyint confidence "0-100"
|
||||
enum status "offen | uebernommen | verworfen | dublette"
|
||||
int lead_id FK "gesetzt ab uebernommen"
|
||||
enum status "offen | übernommen | verworfen | dublette"
|
||||
int lead_id FK "gesetzt ab übernommen"
|
||||
}
|
||||
crm_mail_sync_state {
|
||||
tinyint id PK "immer 1"
|
||||
varchar mailbox
|
||||
text delta_link "naechster Graph-Aufruf"
|
||||
text delta_link "nächster Graph-Aufruf"
|
||||
tinyint has_more "nextLink offen"
|
||||
datetime last_run_at
|
||||
datetime last_success_at
|
||||
@@ -171,20 +171,20 @@ erDiagram
|
||||
|
||||
Vier Eigenheiten:
|
||||
|
||||
- **Die Graph-ID hat einen Hash daneben.** Sie ist mit ueber 150 Zeichen zu lang
|
||||
fuer einen Unique-Index unter `utf8mb4`; die Eindeutigkeit traegt deshalb
|
||||
- **Die Graph-ID hat einen Hash daneben.** Sie ist mit über 150 Zeichen zu lang
|
||||
für einen Unique-Index unter `utf8mb4`; die Eindeutigkeit trägt deshalb
|
||||
`graph_id_hash` (SHA-256 als Hex, `CHAR(64)`).
|
||||
- **`received_at` steht in UTC**, anders als `crm_events.starts_at` (lokale
|
||||
Wandzeit). Graph liefert UTC, und eine stille Umrechnung beim Speichern waere
|
||||
Wandzeit). Graph liefert UTC, und eine stille Umrechnung beim Speichern wäre
|
||||
eine Fehlerquelle. Das Frontend rechnet um (`parseMailDate` in
|
||||
`src/lib/mailApi.ts`).
|
||||
- **`crm_mail_links` erlaubt NULL in beiden Fremdschluesseln.** MariaDB laesst in
|
||||
einem Unique-Index mehrere NULL zu - die beiden Schluessel sperren damit genau
|
||||
- **`crm_mail_links` erlaubt NULL in beiden Fremdschlüsseln.** MariaDB lässt in
|
||||
einem Unique-Index mehrere NULL zu - die beiden Schlüssel sperren damit genau
|
||||
die Doppelung desselben Paares, und eine Mail kann an mehreren Kontakten und
|
||||
mehreren Opportunities haengen.
|
||||
- **`crm_settings` enthaelt keine Geheimnisse.** Zugangsdaten fuer Microsoft 365
|
||||
stehen ausschliesslich in `server/.env`, damit ein Datenbank-Dump keine Secrets
|
||||
enthaelt.
|
||||
mehreren Opportunities hängen.
|
||||
- **`crm_settings` enthält keine Geheimnisse.** Zugangsdaten für Microsoft 365
|
||||
stehen ausschließlich in `server/.env`, damit ein Datenbank-Dump keine Secrets
|
||||
enthält.
|
||||
|
||||
## perinco_scanner
|
||||
|
||||
@@ -197,7 +197,7 @@ erDiagram
|
||||
agents {
|
||||
int id PK
|
||||
varchar name
|
||||
varchar email "fuer Benachrichtigungen"
|
||||
varchar email "für Benachrichtigungen"
|
||||
longtext cv_text "aus dem PDF extrahiert"
|
||||
varchar cv_file_path "Pfad im Upload-Verzeichnis"
|
||||
varchar keywords "kommagetrennt, Suchbegriffe"
|
||||
@@ -207,7 +207,7 @@ erDiagram
|
||||
projects {
|
||||
int id PK
|
||||
varchar portal "freelancermap.de, hays.de, ..."
|
||||
varchar portal_project_id "Id der Boerse"
|
||||
varchar portal_project_id "Id der Börse"
|
||||
varchar title
|
||||
longtext description
|
||||
varchar location
|
||||
@@ -244,12 +244,12 @@ erDiagram
|
||||
|
||||
- **`matches` hat einen Unique-Key auf `(agent_id, project_id)`.** Dadurch wird
|
||||
jedes Projekt pro Agent genau einmal von OpenAI bewertet - das ist der Grund,
|
||||
warum ein zweiter Scan-Lauf ueber dieselben Projekte nichts kostet.
|
||||
- **`projects` wird nach 7 Tagen aufgeraeumt** (`cleanupOldProjects`), die
|
||||
warum ein zweiter Scan-Lauf über dieselben Projekte nichts kostet.
|
||||
- **`projects` wird nach 7 Tagen aufgeräumt** (`cleanupOldProjects`), die
|
||||
Treffer verschwinden per `ON DELETE CASCADE` mit.
|
||||
- **`settings` ist ein Schluessel-Wert-Speicher.** Geheimnisse (`openai_api_key`,
|
||||
- **`settings` ist ein Schlüssel-Wert-Speicher.** Geheimnisse (`openai_api_key`,
|
||||
`smtp_pass`) liefert die API nur maskiert als `********` aus; ein leeres oder
|
||||
maskiertes Feld beim Speichern laesst den gespeicherten Wert unberuehrt.
|
||||
maskiertes Feld beim Speichern lässt den gespeicherten Wert unberührt.
|
||||
|
||||
## Statuswege
|
||||
|
||||
@@ -259,16 +259,16 @@ stateDiagram-v2
|
||||
new --> reviewed : "Als Gesehen markieren"
|
||||
new --> applied : "Als Beworben markieren"
|
||||
reviewed --> applied
|
||||
applied --> new : "Zurueck auf Neu"
|
||||
applied --> new : "Zurück auf Neu"
|
||||
reviewed --> new
|
||||
new --> rejected : "Passt nicht"
|
||||
reviewed --> rejected
|
||||
applied --> rejected
|
||||
rejected --> new : "Zurueck ins Dashboard"
|
||||
rejected --> new : "Zurück ins Dashboard"
|
||||
rejected --> [*] : loeschen
|
||||
new --> [*] : loeschen
|
||||
```
|
||||
|
||||
`rejected` ist der einzige Status, der aus dem Dashboard verschwindet und
|
||||
stattdessen im Reiter *Unpassend* auftaucht. Solche Treffer zaehlen auch in den
|
||||
stattdessen im Reiter *Unpassend* auftaucht. Solche Treffer zählen auch in den
|
||||
Kennzahlen nicht mit.
|
||||
|
||||
+39
-39
@@ -1,8 +1,8 @@
|
||||
# 03 Projekt Scanner
|
||||
|
||||
Der Scanner durchsucht Projektboersen nach Auftraegen und bewertet jeden Fund mit
|
||||
OpenAI gegen die Lebenslaeufe der Berater. Er ist eine vollstaendige Portierung
|
||||
der eigenstaendigen App und laeuft parallel zu ihr.
|
||||
Der Scanner durchsucht Projektbörsen nach Aufträgen und bewertet jeden Fund mit
|
||||
OpenAI gegen die Lebensläufe der Berater. Er ist eine vollständige Portierung
|
||||
der eigenständigen App und läuft parallel zu ihr.
|
||||
|
||||
## Ein Suchlauf
|
||||
|
||||
@@ -43,15 +43,15 @@ sequenceDiagram
|
||||
|
||||
Zwei Sparmassnahmen stecken bewusst **vor** dem OpenAI-Aufruf: der vorhandene
|
||||
Treffer (Unique-Key `agent_id, project_id`) und die Ausschlussbegriffe. Ein
|
||||
Projekt, das „Arbeitnehmerueberlassung" enthaelt, kostet damit keinen Cent.
|
||||
Projekt, das „Arbeitnehmerüberlassung" enthält, kostet damit keinen Cent.
|
||||
|
||||
## Wann laeuft er
|
||||
## Wann läuft er
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
T["Cron-Tick, jede Minute"] --> A{"scanning_active?"}
|
||||
A -->|nein| STOP1["nichts tun"]
|
||||
A -->|ja| B{"Intervall geaendert?"}
|
||||
A -->|ja| B{"Intervall geändert?"}
|
||||
B -->|ja| RE["neu planen"]
|
||||
B -->|nein| C{"Sperrfrist<br/>nur beim allerersten Start"}
|
||||
C -->|noch aktiv| STOP2["nichts tun"]
|
||||
@@ -59,7 +59,7 @@ flowchart TD
|
||||
D -->|nein| STOP3["nichts tun"]
|
||||
D -->|ja| E{"07:00-21:59?"}
|
||||
E -->|nein| STOP4["nichts tun"]
|
||||
E -->|ja| S["last_scan_at aus der DB lesen<br/>(faengt manuelle Laeufe mit ein)"]
|
||||
E -->|ja| S["last_scan_at aus der DB lesen<br/>(fängt manuelle Läufe mit ein)"]
|
||||
S --> F{"Intervall seit<br/>letztem Lauf um?"}
|
||||
F -->|nein| STOP5["nichts tun"]
|
||||
F -->|ja| G["Pipeline starten"]
|
||||
@@ -72,23 +72,23 @@ gleichzeitig los, was Rate-Limits und CAPTCHAs provoziert - und zwei Chromium
|
||||
gleichzeitig belasten den Speicher doppelt.
|
||||
|
||||
Der Zeitpunkt des letzten Laufs steht in `settings.last_scan_at`. Das ist kein
|
||||
Luxus, sondern nach zwei beobachteten Fehlern noetig:
|
||||
Luxus, sondern nach zwei beobachteten Fehlern nötig:
|
||||
|
||||
> **Erster Fehler:** Der Versatz war ueber einen vorgealterten `lastRunTime`
|
||||
> gebaut. Ein Zeitstempel kann einen Lauf aber nur **vorziehen**, nie verzoegern -
|
||||
> **Erster Fehler:** Der Versatz war über einen vorgealterten `lastRunTime`
|
||||
> gebaut. Ein Zeitstempel kann einen Lauf aber nur **vorziehen**, nie verzögern -
|
||||
> bei Intervall 15 und Versatz 30 lief er sofort. Seither eine eigene Frist
|
||||
> (`notBefore`).
|
||||
>
|
||||
> **Zweiter Fehler:** Diese Frist begann bei **jedem** Neustart von vorn. Zwei
|
||||
> Deployments im Abstand von zehn Minuten haben den faelligen Lauf zweimal um eine
|
||||
> halbe Stunde verschoben - er fand nie statt. Wer oefter als alle 30 Minuten
|
||||
> deployt, haette einen Scanner, der nie automatisch sucht. Deshalb liegt der
|
||||
> Deployments im Abstand von zehn Minuten haben den fälligen Lauf zweimal um eine
|
||||
> halbe Stunde verschoben - er fand nie statt. Wer öfter als alle 30 Minuten
|
||||
> deployt, hätte einen Scanner, der nie automatisch sucht. Deshalb liegt der
|
||||
> Zeitpunkt jetzt in der Datenbank, und der Versatz greift nur, solange dort noch
|
||||
> nichts steht.
|
||||
|
||||
Der gespeicherte Zeitpunkt wird auch **im Tick** gelesen: ein manueller Scan aus
|
||||
der Oberflaeche schreibt ihn, weiss aber nichts von der Variablen im Scheduler.
|
||||
Ohne den Abgleich koennte der Scheduler direkt hinter einem manuellen Lauf noch
|
||||
der Oberfläche schreibt ihn, weiß aber nichts von der Variablen im Scheduler.
|
||||
Ohne den Abgleich könnte der Scheduler direkt hinter einem manuellen Lauf noch
|
||||
einmal losziehen.
|
||||
|
||||
## Parallelbetrieb - alles verdoppelt
|
||||
@@ -107,7 +107,7 @@ flowchart LR
|
||||
N4["SCANNER_ENCRYPTION_KEY"] --- N1
|
||||
end
|
||||
|
||||
B["Projektboersen"]
|
||||
B["Projektbörsen"]
|
||||
O["OpenAI"]
|
||||
A1 --> B
|
||||
N1 --> B
|
||||
@@ -122,16 +122,16 @@ flowchart LR
|
||||
| | alte App | CRM |
|
||||
| --- | --- | --- |
|
||||
| Datenbank | `project_scanner` | `perinco_scanner` |
|
||||
| Schluessel | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` |
|
||||
| Schlüssel | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` |
|
||||
| CV-Dateien | eigener Ordner | eigener Ordner |
|
||||
| Aufraeumen | 03:00 | 03:30 |
|
||||
| Aufräumen | 03:00 | 03:30 |
|
||||
| Anmeldung | HTTP Basic | CRM-Sitzung (JWT) |
|
||||
|
||||
**Der Preis:** Beide bewerten dieselben Projekte unabhaengig voneinander, die
|
||||
**Der Preis:** Beide bewerten dieselben Projekte unabhängig voneinander, die
|
||||
OpenAI-Kosten fallen also doppelt an. Wer das senken will, stellt im CRM ein
|
||||
groesseres Suchintervall ein - die Einstellung gilt nur fuer diese Instanz.
|
||||
größeres Suchintervall ein - die Einstellung gilt nur für diese Instanz.
|
||||
|
||||
## Uebernahme aus der alten Datenbank
|
||||
## Übernahme aus der alten Datenbank
|
||||
|
||||
`server/src/scanner/importLegacy.js`, aufrufbar als `npm run scanner:import`:
|
||||
|
||||
@@ -140,9 +140,9 @@ flowchart TD
|
||||
S["Start"] --> C{"Zieltabelle agents leer?"}
|
||||
C -->|nein, ohne --force| AB["Abbruch"]
|
||||
C -->|ja oder --force| E["Einstellungen kopieren<br/>inkl. OpenAI-Key"]
|
||||
E --> P["Portale ueber den Namen zuordnen:<br/>Benutzername, Session-Cookies, Status"]
|
||||
E --> P["Portale über den Namen zuordnen:<br/>Benutzername, Session-Cookies, Status"]
|
||||
P --> PW{"LEGACY_ENCRYPTION_KEY gesetzt?"}
|
||||
PW -->|ja| PW2["Passwort entschluesseln<br/>und neu verschluesseln"]
|
||||
PW -->|ja| PW2["Passwort entschlüsseln<br/>und neu verschlüsseln"]
|
||||
PW -->|nein| PW3["Passwort auslassen"]
|
||||
PW2 --> A["Agenten anlegen, Ids behalten<br/>CV-PDF kopieren"]
|
||||
PW3 --> A
|
||||
@@ -152,33 +152,33 @@ flowchart TD
|
||||
M2 --> F
|
||||
```
|
||||
|
||||
Die Ids bleiben erhalten, damit uebernommene Treffer weiter auf die richtigen
|
||||
Die Ids bleiben erhalten, damit übernommene Treffer weiter auf die richtigen
|
||||
Agenten und Projekte zeigen. Deshalb bricht das Skript bei bereits vorhandenen
|
||||
Agenten ab.
|
||||
|
||||
> **Beim Rollout am 06.08.2026 festgestellt:** Die Portal-Passwoerter der alten
|
||||
> **Beim Rollout am 06.08.2026 festgestellt:** Die Portal-Passwörter der alten
|
||||
> Datenbank lassen sich mit dem dortigen `ENCRYPTION_KEY` nicht mehr
|
||||
> entschluesseln - der Schluessel wurde nach dem Speichern getauscht. Die alte App
|
||||
> entschlüsseln - der Schlüssel wurde nach dem Speichern getauscht. Die alte App
|
||||
> kann sie damit ebenso wenig lesen, es ging also nichts verloren. Die
|
||||
> Session-Cookies liegen im Klartext und wurden uebernommen; damit scrapt die
|
||||
> Session-Cookies liegen im Klartext und wurden übernommen; damit scrapt die
|
||||
> neue Instanz sofort.
|
||||
|
||||
## Oberflaeche
|
||||
## Oberfläche
|
||||
|
||||
| Reiter | Datei | Inhalt |
|
||||
| --- | --- | --- |
|
||||
| Dashboard | `ScannerDashboard.tsx` | Kennzahlen, Filter, Trefferliste, *Jetzt scannen*, Ton bei neuen Treffern |
|
||||
| Unpassend | `ScannerRejected.tsx` | Treffer mit Status `rejected` |
|
||||
| Suchagenten | `ScannerAgents.tsx` | Beraterprofile, CV-Upload |
|
||||
| Boersen-Konten | `ScannerPortals.tsx` | Verbindungsstatus, Zugangsdaten, Cookie-Import |
|
||||
| Börsen-Konten | `ScannerPortals.tsx` | Verbindungsstatus, Zugangsdaten, Cookie-Import |
|
||||
| Einstellungen | `settings/ScannerSettings.tsx` | Intervall, Schwellenwerte, OpenAI, Ton, E-Mail |
|
||||
|
||||
**Wichtig zum Ton:** Er haengt an der Dashboard-Komponente. Wer auf einen anderen
|
||||
Menuepunkt wechselt, hoert nichts - die Komponente wird ausgebaut und die
|
||||
**Wichtig zum Ton:** Er hängt an der Dashboard-Komponente. Wer auf einen anderen
|
||||
Menüpunkt wechselt, hört nichts - die Komponente wird ausgebaut und die
|
||||
30-Sekunden-Abfrage endet. In einem anderen Browsertab bleibt der Ton dagegen
|
||||
aktiv, nur langsamer (Chrome bremst Timer versteckter Tabs auf etwa einmal pro
|
||||
Minute) und nur, wenn zuvor irgendwo auf der Seite geklickt wurde
|
||||
(Autoplay-Regel). Browserunabhaengig meldet nur die E-Mail.
|
||||
(Autoplay-Regel). Browserunabhängig meldet nur die E-Mail.
|
||||
|
||||
## Endpunkte
|
||||
|
||||
@@ -187,15 +187,15 @@ Alle unter `/api/scanner`, alle hinter `requireAuth`:
|
||||
| Methode | Pfad | Zweck |
|
||||
| --- | --- | --- |
|
||||
| GET | `/agents` | Liste ohne `cvText` (Nutzlast) |
|
||||
| POST/PUT | `/agents[/:id]` | anlegen/aendern, `multipart` mit CV |
|
||||
| POST/PUT | `/agents[/:id]` | anlegen/ändern, `multipart` mit CV |
|
||||
| DELETE | `/agents/:id` | Agent samt CV-Datei |
|
||||
| GET | `/agents/:id/cv` | PDF als Stream |
|
||||
| GET | `/portals` | Status ohne Geheimnisse |
|
||||
| POST | `/portals/credentials` | Zugangsdaten, AES-verschluesselt |
|
||||
| POST | `/portals/sync-session` | Cookie-Import, normalisiert fuer Playwright |
|
||||
| POST | `/portals/test-connection/:id` | Mini-Scrape bzw. Session-Pruefung |
|
||||
| POST | `/portals/credentials` | Zugangsdaten, AES-verschlüsselt |
|
||||
| POST | `/portals/sync-session` | Cookie-Import, normalisiert für Playwright |
|
||||
| POST | `/portals/test-connection/:id` | Mini-Scrape bzw. Session-Prüfung |
|
||||
| GET | `/matches` | Filter: `status`, `agentId`, `minPercentage`, `limit` |
|
||||
| PUT/DELETE | `/matches/:id` | Status setzen, loeschen |
|
||||
| PUT/DELETE | `/matches/:id` | Status setzen, löschen |
|
||||
| GET/POST | `/settings` | Geheimnisse maskiert |
|
||||
| POST | `/scan` | Lauf starten, `409` wenn schon einer laeuft |
|
||||
| GET | `/status` | laeuft ein Lauf, Scheduler an, Datenbankname |
|
||||
| POST | `/scan` | Lauf starten, `409` wenn schon einer läuft |
|
||||
| GET | `/status` | läuft ein Lauf, Scheduler an, Datenbankname |
|
||||
|
||||
+12
-12
@@ -1,8 +1,8 @@
|
||||
# 04 Diktat (Whisper im Browser)
|
||||
|
||||
Ueberall, wo laengerer Text entsteht - Projektbeschreibung, Interaktionsnotiz,
|
||||
naechster Schritt, interne Notizen - sitzt ein Mikrofon-Button. Die Erkennung
|
||||
laeuft **vollstaendig im Browser des Benutzers**.
|
||||
Überall, wo längerer Text entsteht - Projektbeschreibung, Interaktionsnotiz,
|
||||
nächster Schritt, interne Notizen - sitzt ein Mikrofon-Button. Die Erkennung
|
||||
läuft **vollständig im Browser des Benutzers**.
|
||||
|
||||
Das Backend ist daran nicht beteiligt und sieht keine Audiodaten: kein API-Key,
|
||||
keine Kosten pro Minute, kein Datenabfluss. Aufnahmen werden nirgends gespeichert.
|
||||
@@ -23,7 +23,7 @@ flowchart LR
|
||||
```
|
||||
|
||||
Die Erkennung liegt im Web Worker (`src/lib/whisper.worker.ts`), damit die
|
||||
Oberflaeche waehrenddessen nicht einfriert. Die ONNX-Laufzeit wird mit
|
||||
Oberfläche währenddessen nicht einfriert. Die ONNX-Laufzeit wird mit
|
||||
ausgeliefert (Vite bundelt die `.wasm`), nur die Modelldateien kommen beim ersten
|
||||
Laden vom CDN und bleiben danach im Cache-Speicher der Seite.
|
||||
|
||||
@@ -36,7 +36,7 @@ stateDiagram-v2
|
||||
loading --> ready : im Worker initialisiert
|
||||
loading --> error : Abbruch, Haenger, kein Speicher
|
||||
error --> loading : erneut versuchen
|
||||
ready --> idle : "Sprachmodell loeschen"
|
||||
ready --> idle : "Sprachmodell löschen"
|
||||
ready --> ready : Diktat benutzen
|
||||
|
||||
note right of loading
|
||||
@@ -47,14 +47,14 @@ stateDiagram-v2
|
||||
|
||||
Beim Anmelden wird das Modell automatisch bereitgestellt, **wenn** es schon in
|
||||
diesem Browser liegt. Ein Download von 759 MB startet nie von allein, und der
|
||||
Automatismus greift bewusst nur einmal pro Seitenaufruf: wer das Modell loescht,
|
||||
Automatismus greift bewusst nur einmal pro Seitenaufruf: wer das Modell löscht,
|
||||
bekommt es nicht sofort wieder heruntergeladen.
|
||||
|
||||
## Wo es sitzt
|
||||
|
||||
Reiter **Whisper** in den Einstellungen - fuer **jede Rolle** sichtbar, denn jeder
|
||||
richtet sein Diktat auf seinem eigenen Geraet ein. Genau deshalb sind die
|
||||
Einstellungen ueberhaupt fuer alle Rollen zugaenglich; welche Reiter erscheinen,
|
||||
Reiter **Whisper** in den Einstellungen - für **jede Rolle** sichtbar, denn jeder
|
||||
richtet sein Diktat auf seinem eigenen Gerät ein. Genau deshalb sind die
|
||||
Einstellungen überhaupt für alle Rollen zugänglich; welche Reiter erscheinen,
|
||||
entscheidet die Rolle:
|
||||
|
||||
```mermaid
|
||||
@@ -76,10 +76,10 @@ Modell, Genauigkeit, Rechenweg und Sprache stehen als Konstanten in
|
||||
Deutsch. Jede andere Kombination war in den Messungen entweder schlechter oder
|
||||
kaputt - bei `small` in fp16 auf WebGPU reagiert der ganze Browser-Tab nicht mehr,
|
||||
und **kein Timeout in der Seite kann das abfangen**, weil das Skript selbst nicht
|
||||
mehr laeuft.
|
||||
mehr läuft.
|
||||
|
||||
Eine Auswahl haette also nur Gelegenheit geboten, etwas falsch zu machen. Die
|
||||
Eine Auswahl hätte also nur Gelegenheit geboten, etwas falsch zu machen. Die
|
||||
Messwerte stehen im [README](../README.md#warum-small-und-q4---die-messungen).
|
||||
|
||||
Als Netz bleibt eine Frist pro Erkennung (15x Aufnahmedauer, mindestens 45 s,
|
||||
hoechstens 5 min) und eine Haenger-Erkennung beim Laden.
|
||||
höchstens 5 min) und eine Hänger-Erkennung beim Laden.
|
||||
|
||||
+24
-24
@@ -1,10 +1,10 @@
|
||||
# 05 Frontend
|
||||
|
||||
Eine einzige React-Anwendung ohne Router: `App.tsx` haelt die aktuelle Ansicht in
|
||||
einem Zustand und rendert sie. Das genuegt, weil die App hinter einer Anmeldung
|
||||
Eine einzige React-Anwendung ohne Router: `App.tsx` hält die aktuelle Ansicht in
|
||||
einem Zustand und rendert sie. Das genügt, weil die App hinter einer Anmeldung
|
||||
sitzt und keine tiefen Links braucht.
|
||||
|
||||
## Menue und Ansichten
|
||||
## Menü und Ansichten
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -13,15 +13,15 @@ flowchart TD
|
||||
K --> C["Kalender"]
|
||||
K --> L["Kontakte"]
|
||||
L --> L1["Alle Kontakte"]
|
||||
L --> L2["Aus Mails<br/>Vorbereich, Zaehler im Menue"]
|
||||
L --> L2["Aus Mails<br/>Vorbereich, Zähler im Menü"]
|
||||
K --> M["Mails"]
|
||||
M --> M1["Postfach"]
|
||||
M --> M2["Kontakt-Vorschlaege"]
|
||||
M --> M2["Kontakt-Vorschläge"]
|
||||
K --> SC["Scanner"]
|
||||
SC --> SC1["Dashboard"]
|
||||
SC --> SC2["Unpassend"]
|
||||
SC --> SC3["Suchagenten"]
|
||||
SC --> SC4["Boersen-Konten"]
|
||||
SC --> SC4["Börsen-Konten"]
|
||||
SB --> E["Einstellungen"]
|
||||
E --> E1["Nutzerverwaltung (admin)"]
|
||||
E --> E2["Mein Konto"]
|
||||
@@ -30,16 +30,16 @@ flowchart TD
|
||||
E --> E5["Whisper"]
|
||||
```
|
||||
|
||||
*Postfach* und *Kontakt-Vorschlaege* sind Reiter **innerhalb** der Ansicht
|
||||
*Mails*, keine Menuepunkte - sie gehoeren zusammen und wechseln oft hin und her.
|
||||
*Postfach* und *Kontakt-Vorschläge* sind Reiter **innerhalb** der Ansicht
|
||||
*Mails*, keine Menüpunkte - sie gehören zusammen und wechseln oft hin und her.
|
||||
|
||||
Die Menuepunkte *Kontakte* und *Scanner* klappen ihre Unterpunkte auf; der
|
||||
Die Menüpunkte *Kontakte* und *Scanner* klappen ihre Unterpunkte auf; der
|
||||
Kopfeintrag ist dabei selbst eine Ansicht (Kontaktliste bzw. Scanner-Dashboard).
|
||||
Bei eingeklappter Leiste (Symbolbreite) erscheinen nur die Symbole, die
|
||||
Unterpunkte kommen mit den Beschriftungen zurueck. Beide Gruppen entstehen aus
|
||||
Unterpunkte kommen mit den Beschriftungen zurück. Beide Gruppen entstehen aus
|
||||
derselben Struktur `MenuGroup` in `Sidebar.tsx`.
|
||||
|
||||
*Aus Mails* traegt einen Zaehler mit der Anzahl der offenen Funde - er kommt aus
|
||||
*Aus Mails* trägt einen Zähler mit der Anzahl der offenen Funde - er kommt aus
|
||||
`App.tsx`, das die Kontakte ohnehin geladen hat.
|
||||
|
||||
## Zustand und Datenfluss
|
||||
@@ -64,34 +64,34 @@ flowchart TB
|
||||
DATA --> V2["Kalender"]
|
||||
DATA --> V3["Kontakte<br/>mail_pool = false"]
|
||||
DATA --> V5["Aus Mails<br/>mail_pool = true + Kontakte zum Abgleich"]
|
||||
DATA --> V4["Mails<br/>leads + opportunities fuer den Zuordnungsdialog"]
|
||||
DATA --> V4["Mails<br/>leads + opportunities für den Zuordnungsdialog"]
|
||||
NAV --> DF["DictationField<br/>springt nach Whisper"]
|
||||
NAV --> ML["Mails<br/>springt zum Kontakt einer Zuordnung"]
|
||||
|
||||
SCV["Scanner-Ansichten"] -->|"laden selbst"| API["/api/scanner"]
|
||||
MV["Mailansicht"] -->|"laedt selbst"| MAPI["/api/mail"]
|
||||
MV["Mailansicht"] -->|"lädt selbst"| MAPI["/api/mail"]
|
||||
```
|
||||
|
||||
Die CRM-Daten werden **einmal zentral** geladen und als Props durchgereicht; die
|
||||
Scanner- und Mailansichten laden ihre eigenen Daten selbst - der Scanner, weil er
|
||||
eine andere Datenbank und eine Auffrischung im 30-Sekunden-Takt hat; die Mails,
|
||||
weil sie blaettern, suchen und filtern und dabei nicht die ganze Liste im Speicher
|
||||
weil sie blättern, suchen und filtern und dabei nicht die ganze Liste im Speicher
|
||||
halten sollen. Kontakte und Opportunities bekommt die Mailansicht dagegen als
|
||||
Props: der Zuordnungsdialog braucht sie beide zum Vorschlagen.
|
||||
|
||||
`/api/leads` liefert beide Bestaende in einem Aufruf; `App.tsx` teilt sie in
|
||||
`/api/leads` liefert beide Bestände in einem Aufruf; `App.tsx` teilt sie in
|
||||
`leads` (Kontakte) und `mailPoolLeads` (Vorbereich „Aus Mails") auf. Alle
|
||||
anderen Ansichten bekommen ausschliesslich `leads` - ein Fund aus einer Mail soll
|
||||
anderen Ansichten bekommen ausschließlich `leads` - ein Fund aus einer Mail soll
|
||||
in keiner Auswahlliste auftauchen. Die Ansicht *Aus Mails* braucht beides: sie
|
||||
vergleicht jeden Fund gegen den Kontaktbestand und markiert Dubletten.
|
||||
|
||||
`goTo('Kontakte', { leadId })` ist der Sprung aus einer Mailzuordnung zum
|
||||
Kontakt. `App.tsx` haelt die Id in `focusLeadId`, die Kontaktansicht oeffnet den
|
||||
Kontakt und meldet mit `onFocusHandled()` zurueck - der Effekt haengt an `leads`
|
||||
Kontakt. `App.tsx` hält die Id in `focusLeadId`, die Kontaktansicht öffnet den
|
||||
Kontakt und meldet mit `onFocusHandled()` zurück - der Effekt hängt an `leads`
|
||||
und wartet damit auf einen gerade erst angelegten Kontakt.
|
||||
|
||||
`reload={() => loadData(true)}` laedt still nach - ohne Ladeanzeige, damit ein
|
||||
Speichern im Dialog nicht die ganze Liste flackern laesst.
|
||||
`reload={() => loadData(true)}` lädt still nach - ohne Ladeanzeige, damit ein
|
||||
Speichern im Dialog nicht die ganze Liste flackern lässt.
|
||||
|
||||
## Komponentenkarte
|
||||
|
||||
@@ -127,8 +127,8 @@ src/
|
||||
## Dark Mode ohne `dark:`-Varianten
|
||||
|
||||
Die Klasse `dark` auf `<html>` tauscht in `src/index.css` nur die Werte der
|
||||
Variablen `canvas`, `surface`, `ink-*`, `brand-*` und der getoenten Statusfarben.
|
||||
Die Komponenten benutzen durchgaengig diese Skalen und folgen damit automatisch -
|
||||
Variablen `canvas`, `surface`, `ink-*`, `brand-*` und der getönten Statusfarben.
|
||||
Die Komponenten benutzen durchgängig diese Skalen und folgen damit automatisch -
|
||||
kein `dark:` im Markup.
|
||||
|
||||
```mermaid
|
||||
@@ -143,9 +143,9 @@ flowchart LR
|
||||
Ein Inline-Skript in `index.html` setzt die Klasse vor dem ersten Frame, sonst
|
||||
blitzt beim Laden die helle Variante auf. Drei Werte bleiben in beiden Modi
|
||||
gleich: `ink-950` (liegt nur unter Dialog-Overlays), `text-white` (steht immer auf
|
||||
farbigen Flaechen) und `brand-500` (das CI-Gruen der Buttons).
|
||||
farbigen Flächen) und `brand-500` (das CI-Grün der Buttons).
|
||||
|
||||
Eine bewusste Ausnahme: der **Mailinhalt** bleibt auch im Dunkelmodus auf weissem
|
||||
Grund. Fremdes Mail-HTML bringt seine eigenen Farben mit; ein dunkler Hintergrund
|
||||
darunter macht schwarze Schrift auf schwarzer Flaeche. Der Rahmen liest sich damit
|
||||
darunter macht schwarze Schrift auf schwarzer Fläche. Der Rahmen liest sich damit
|
||||
wie ein Blatt Papier im dunklen Fenster - gewollt.
|
||||
|
||||
+22
-22
@@ -26,7 +26,7 @@ powershell -File deploy/deploy.ps1 -SkipBuild # ohne neuen Build
|
||||
|
||||
Das Passwort kommt aus `$env:PERINCO_CRM_SSH_PASSWORD` oder per `-Password`.
|
||||
|
||||
`deploy.ps1` laedt die systemd-Unit **nicht** mit hoch. Nach Aenderungen an
|
||||
`deploy.ps1` lädt die systemd-Unit **nicht** mit hoch. Nach Änderungen an
|
||||
`deploy/perinco-crm-backend.service` von Hand kopieren und `daemon-reload`.
|
||||
|
||||
## Dienste auf dem Server
|
||||
@@ -34,14 +34,14 @@ Das Passwort kommt aus `$env:PERINCO_CRM_SSH_PASSWORD` oder per `-Password`.
|
||||
| Unit | Verzeichnis | Port | Anmerkung |
|
||||
| --- | --- | --- | --- |
|
||||
| `perinco-crm-backend` | `/var/www/perinco_CRM/server` | 5006 | `MemoryMax=1536M` |
|
||||
| `project-scanner-backend` | `/var/www/Projekt Scanner/backend` | 5005 | alte App, laeuft bis zur Ablosung |
|
||||
| `project-scanner-backend` | `/var/www/Projekt Scanner/backend` | 5005 | alte App, läuft bis zur Ablosung |
|
||||
|
||||
Der Verzeichnisname der alten App enthaelt ein **Leerzeichen**. In Skripten und in
|
||||
der `.env` immer in Anfuehrungszeichen setzen - systemd liest `.env` als
|
||||
Der Verzeichnisname der alten App enthält ein **Leerzeichen**. In Skripten und in
|
||||
der `.env` immer in Anführungszeichen setzen - systemd liest `.env` als
|
||||
`EnvironmentFile` und trennt sonst am Leerzeichen.
|
||||
|
||||
`MemoryMax=1536M` statt der urspruenglichen 512M: ein Scan-Lauf startet Chromium
|
||||
als Kindprozess in derselben cgroup. Mit 512M wuerde der OOM-Killer das Backend
|
||||
`MemoryMax=1536M` statt der ursprünglichen 512M: ein Scan-Lauf startet Chromium
|
||||
als Kindprozess in derselben cgroup. Mit 512M würde der OOM-Killer das Backend
|
||||
mitten im Scrapen beenden.
|
||||
|
||||
## Umgebungsvariablen
|
||||
@@ -50,21 +50,21 @@ mitten im Scrapen beenden.
|
||||
|
||||
| Variable | Bedeutung |
|
||||
| --- | --- |
|
||||
| `PORT`, `HOST` | `5006`, `127.0.0.1` - von aussen nur ueber Apache |
|
||||
| `PORT`, `HOST` | `5006`, `127.0.0.1` - von außen nur über Apache |
|
||||
| `DB_*` | Verbindung zu `perinco_CRM` |
|
||||
| `JWT_SECRET`, `JWT_EXPIRES_IN` | Signatur der Sitzungen, Standard 12 h |
|
||||
| `ADMIN_*` | wird beim ersten Start angelegt, nur wenn `crm_users` leer ist |
|
||||
| `SCANNER_DB_NAME` | eigene Scanner-Datenbank, Standard `perinco_scanner` |
|
||||
| `SCANNER_ENCRYPTION_KEY` | AES-Schluessel fuer Portal-Passwoerter |
|
||||
| `SCANNER_ENCRYPTION_KEY` | AES-Schlüssel für Portal-Passwörter |
|
||||
| `SCANNER_UPLOAD_DIR` | CV-Verzeichnis, leer = `<server>/uploads` |
|
||||
| `SCANNER_SCHEDULER` | Hintergrund-Scraper, Standard an, nur `false` schaltet ab |
|
||||
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz fuer den ersten Lauf, Standard 30 |
|
||||
| `LEGACY_*` | Quelle fuer `npm run scanner:import` |
|
||||
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz für den ersten Lauf, Standard 30 |
|
||||
| `LEGACY_*` | Quelle für `npm run scanner:import` |
|
||||
| `MS_TENANT_ID`, `MS_CLIENT_ID`, `MS_CLIENT_SECRET` | App-only-Zugriff auf Microsoft Graph |
|
||||
| `MS_MAILBOX` | abzugleichendes Postfach, Standard `crm@perinco.de` |
|
||||
| `MS_MAIL_FOLDER` | Ordner, Standard `inbox` |
|
||||
| `MAIL_SYNC` | Postfachabgleich, Standard an, nur `false` schaltet ab |
|
||||
| `MAIL_MAX_PAGES`, `MAIL_MAX_BODY_CHARS` | Bremsen fuer Erstabgleich und Textgroesse |
|
||||
| `MAIL_MAX_PAGES`, `MAIL_MAX_BODY_CHARS` | Bremsen für Erstabgleich und Textgröße |
|
||||
|
||||
Die Scanner-Datenbank wird beim Start automatisch angelegt
|
||||
(`CREATE DATABASE IF NOT EXISTS`), Tabellen kommen aus `sequelize.sync()`, Portale
|
||||
@@ -75,16 +75,16 @@ Fehlen die `MS_*`-Werte, startet das Backend normal und protokolliert
|
||||
*Postfach* nennen die fehlende Variable beim Namen. Einrichtung in Entra und
|
||||
Exchange Online: [11 Postfachanbindung](11-postfach.md).
|
||||
|
||||
> Die `MS_*`-Werte werden **beim Start** gelesen. Nach einer Aenderung an der
|
||||
> `.env` muss der Dienst neu starten, ein Speichern in der Oberflaeche genuegt
|
||||
> Die `MS_*`-Werte werden **beim Start** gelesen. Nach einer Änderung an der
|
||||
> `.env` muss der Dienst neu starten, ein Speichern in der Oberfläche genügt
|
||||
> nicht - dort liegen nur Intervall, Schalter und Zeitraum (`crm_settings`).
|
||||
|
||||
## Playwright
|
||||
|
||||
`playwright` ist auf **exakt 1.60.0** gepinnt - dieselbe Version wie die alte App.
|
||||
Deren Chromium-Build (`chromium-1223`) liegt in `/root/.cache/ms-playwright`, also
|
||||
faellt kein zweiter Browser-Download an. Ein `^` in der Version wuerde das
|
||||
aufgeben; die Begruendung steht als `comments`-Feld in `server/package.json`.
|
||||
fällt kein zweiter Browser-Download an. Ein `^` in der Version würde das
|
||||
aufgeben; die Begründung steht als `comments`-Feld in `server/package.json`.
|
||||
|
||||
Fehlt der Browser, funktioniert alles ausser Scan-Lauf und Verbindungstest:
|
||||
|
||||
@@ -103,16 +103,16 @@ systemctl restart perinco-crm-backend
|
||||
```
|
||||
|
||||
> **Reihenfolge:** Der Import schreibt `search_interval` aus der alten App in die
|
||||
> Einstellungen. Der laufende Scheduler merkt die Aenderung binnen einer Minute
|
||||
> Einstellungen. Der laufende Scheduler merkt die Änderung binnen einer Minute
|
||||
> und plant sich neu - beim Rollout hat er direkt nach dem Import einen Scan
|
||||
> gestartet, den der folgende Restart mitten im Scrapen beendet hat. Harmlos, aber
|
||||
> vermeidbar: erst neu starten, dann importieren.
|
||||
|
||||
## Stoerungssuche
|
||||
## Störungssuche
|
||||
|
||||
| Symptom | Blick darauf |
|
||||
| --- | --- |
|
||||
| Anmeldung schlaegt fehl | `journalctl -u perinco-crm-backend -n 50`, `JWT_SECRET` gesetzt? |
|
||||
| Anmeldung schlägt fehl | `journalctl -u perinco-crm-backend -n 50`, `JWT_SECRET` gesetzt? |
|
||||
| Scanner-Reiter leer, 500er | `[Scanner] Datenbank nicht erreichbar` im Journal |
|
||||
| Kein Treffer trotz Scan | `scanning_active`, aktive Agenten mit CV, OpenAI-Key gesetzt |
|
||||
| Portal `expired` | Cookies neu importieren (Cookie-Editor) |
|
||||
@@ -120,9 +120,9 @@ systemctl restart perinco-crm-backend
|
||||
| Backend stirbt beim Scrapen | `MemoryMax` zu klein, `systemctl show ... -p MemoryCurrent` |
|
||||
| Ton bleibt aus | Schwellenwert, Dashboard-Ansicht offen, einmal auf der Seite geklickt? |
|
||||
| Keine Mails im Postfachreiter | `journalctl -u perinco-crm-backend \| grep '\[Mail\]'`, dann Verbindungstest in den Einstellungen |
|
||||
| Mail-Abgleich meldet `ErrorAccessDenied` | Administratorzustimmung fuer `Mail.Read` fehlt, oder die ApplicationAccessPolicy erlaubt das Postfach nicht |
|
||||
| Mail-Abgleich meldet `ErrorAccessDenied` | Administratorzustimmung für `Mail.Read` fehlt, oder die ApplicationAccessPolicy erlaubt das Postfach nicht |
|
||||
|
||||
Nuetzliche Abfragen:
|
||||
Nützliche Abfragen:
|
||||
|
||||
```bash
|
||||
systemctl status perinco-crm-backend
|
||||
@@ -151,6 +151,6 @@ mysqldump -u"$DB_USER" -p"$DB_PASSWORD" "$SCANNER_DB_NAME" > scanner_$(date +%F)
|
||||
tar czf uploads_$(date +%F).tar.gz -C /var/www/perinco_CRM/server uploads
|
||||
```
|
||||
|
||||
Die `.env` gehoert **nicht** ins Repository, aber in die Sicherung - ohne
|
||||
`SCANNER_ENCRYPTION_KEY` sind die Portal-Passwoerter verloren. Genau das ist der
|
||||
Die `.env` gehört **nicht** ins Repository, aber in die Sicherung - ohne
|
||||
`SCANNER_ENCRYPTION_KEY` sind die Portal-Passwörter verloren. Genau das ist der
|
||||
alten App passiert.
|
||||
|
||||
+140
-136
@@ -1,30 +1,30 @@
|
||||
# 07 Anleitung fuer Benutzer
|
||||
# 07 Anleitung für Benutzer
|
||||
|
||||
Diese Anleitung setzt keine Vorkenntnisse voraus. Sie beschreibt, was die
|
||||
Anwendung tut und wie man sie bedient.
|
||||
|
||||
## Was die Anwendung ist
|
||||
|
||||
Ein CRM fuer die Projektvermittlung. Es beantwortet vier Fragen:
|
||||
Ein CRM für die Projektvermittlung. Es beantwortet vier Fragen:
|
||||
|
||||
| Frage | Bereich |
|
||||
| --- | --- |
|
||||
| Welche Geschaefte sind in Arbeit und was sind sie wert? | **Opportunities** |
|
||||
| Welche Geschäfte sind in Arbeit und was sind sie wert? | **Opportunities** |
|
||||
| Wer ist wann wo, und wer ist beteiligt? | **Kalender** |
|
||||
| Wen kennen wir, und wer kennt wen? | **Kontakte** |
|
||||
| Welche Projekte auf den Boersen passen zu unseren Beratern? | **Scanner** |
|
||||
| Welche Projekte auf den Börsen passen zu unseren Beratern? | **Scanner** |
|
||||
|
||||
## Anmelden
|
||||
|
||||
`https://pcrm.perinco.de`. Anmelden geht mit **Benutzername oder E-Mail** plus
|
||||
Passwort. Die Sitzung haelt 12 Stunden, danach fragt die Anwendung erneut.
|
||||
Passwort. Die Sitzung hält 12 Stunden, danach fragt die Anwendung erneut.
|
||||
|
||||
Sagt die Meldung „Benutzername oder Passwort ist falsch", bleibt bewusst offen,
|
||||
welches von beidem nicht stimmte - so kann ueber die Anmeldemaske niemand
|
||||
welches von beidem nicht stimmte - so kann über die Anmeldemaske niemand
|
||||
Benutzernamen durchprobieren.
|
||||
|
||||
Es gibt zwei Rollen: **Administrator** sieht zusaetzlich die Nutzerverwaltung und
|
||||
die Scanner-Einstellungen. Alles andere ist fuer beide Rollen gleich.
|
||||
Es gibt zwei Rollen: **Administrator** sieht zusätzlich die Nutzerverwaltung und
|
||||
die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
|
||||
|
||||
## Sich zurechtfinden
|
||||
|
||||
@@ -33,13 +33,15 @@ die Scanner-Einstellungen. Alles andere ist fuer beide Rollen gleich.
|
||||
│ Kategorien │ Titel der Ansicht [Hell/Dunkel] │
|
||||
│ Opportunities├──────────────────────────────────────────┤
|
||||
│ Kalender │ │
|
||||
│ Kontakte │ Inhalt │
|
||||
│ Kontakte ▾ │ Inhalt │
|
||||
│ Alle Kont. │ │
|
||||
│ Aus Mails 5│ │
|
||||
│ Mails │ │
|
||||
│ Scanner ▾ │ │
|
||||
│ Dashboard │ │
|
||||
│ Unpassend │ │
|
||||
│ Suchagenten│ │
|
||||
│ Boersen-… │ │
|
||||
│ Börsen-… │ │
|
||||
├──────────────┤ │
|
||||
│ Einstellungen│ │
|
||||
├──────────────┤ │
|
||||
@@ -47,44 +49,46 @@ die Scanner-Einstellungen. Alles andere ist fuer beide Rollen gleich.
|
||||
└──────────────┴───────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
* Der Knopf mit den drei Strichen oben links klappt das Menue auf Symbolbreite
|
||||
* Der Knopf mit den drei Strichen oben links klappt das Menü auf Symbolbreite
|
||||
zusammen - hilfreich auf kleinen Bildschirmen.
|
||||
* Rechts oben schaltet zwischen hellem und dunklem Design. Ein **Doppelklick**
|
||||
auf den Schalter folgt wieder der Systemeinstellung.
|
||||
* Der Menuepunkt **Scanner** klappt beim Anklicken seine Unterpunkte auf.
|
||||
* Die Menüpunkte **Kontakte** und **Scanner** klappen beim Anklicken ihre
|
||||
Unterpunkte auf. Die Zahl neben *Aus Mails* sagt, wie viele Funde aus dem
|
||||
Postfach noch auf eine Entscheidung warten.
|
||||
|
||||
## Opportunities
|
||||
|
||||
Der Startbereich: eine Opportunity ist ein Geschaeft in Arbeit. Oben rechts
|
||||
Der Startbereich: eine Opportunity ist ein Geschäft in Arbeit. Oben rechts
|
||||
wechselt man die Darstellung:
|
||||
|
||||
| Ansicht | wofuer |
|
||||
| Ansicht | wofür |
|
||||
| --- | --- |
|
||||
| **Kachel** | Ueberblick mit Volumen, Tagessatz, Laufzeit, letzter Interaktion |
|
||||
| **Liste** | viele Eintraege vergleichen, Spaltenkoepfe sortieren |
|
||||
| **Kachel** | Überblick mit Volumen, Tagessatz, Laufzeit, letzter Interaktion |
|
||||
| **Liste** | viele Einträge vergleichen, Spaltenköpfe sortieren |
|
||||
| **Pipeline** | Fortschritt durch die Phasen, Karten per Maus verschieben |
|
||||
|
||||
**Volumen** rechnet die Anwendung selbst: Tagessatz x Personentage. Das
|
||||
**gewichtete** Volumen multipliziert das noch mit der Abschlusswahrscheinlichkeit.
|
||||
Beide Zahlen stehen live im Formular, auf jeder Karte, als Summe ueber der Liste,
|
||||
Beide Zahlen stehen live im Formular, auf jeder Karte, als Summe über der Liste,
|
||||
je Pipeline-Spalte und in der Kopfzeile.
|
||||
|
||||
Zu jeder Opportunity gehoeren beliebig viele **Ansprechpartner** aus den
|
||||
Zu jeder Opportunity gehören beliebig viele **Ansprechpartner** aus den
|
||||
Kontakten, jeder mit eigener Rolle („Entscheider", „Fachbereich", …).
|
||||
|
||||
Aendern und loeschen darf der **Ersteller**, der **Zustaendige** und jeder
|
||||
**Administrator**. Wer keines davon ist, bekommt eine Fehlermeldung - das prueft
|
||||
der Server, nicht nur die Oberflaeche.
|
||||
Ändern und löschen darf der **Ersteller**, der **Zuständige** und jeder
|
||||
**Administrator**. Wer keines davon ist, bekommt eine Fehlermeldung - das prüft
|
||||
der Server, nicht nur die Oberfläche.
|
||||
|
||||
Auf dem Tablet oder ohne Lust auf Ziehen: die Phase laesst sich auch ueber das
|
||||
Auswahlfeld direkt auf der Karte aendern.
|
||||
Auf dem Tablet oder ohne Lust auf Ziehen: die Phase lässt sich auch über das
|
||||
Auswahlfeld direkt auf der Karte ändern.
|
||||
|
||||
## Kalender
|
||||
|
||||
Ein **gemeinsamer** Teamkalender - alle sehen dieselben Termine, jeder darf
|
||||
eintragen.
|
||||
|
||||
* **Monat, Woche, Tag** oben umschalten, `Heute` und die Pfeile blaettern.
|
||||
* **Monat, Woche, Tag** oben umschalten, `Heute` und die Pfeile blättern.
|
||||
* Klick auf einen Monatstag oder eine Stundenzelle legt sofort einen Termin an.
|
||||
* Klick auf einen Wochentagskopf springt in die Tagesansicht.
|
||||
* **Verschieben per Ziehen**: Termin anfassen und auf einen anderen Tag oder eine
|
||||
@@ -92,134 +96,134 @@ eintragen.
|
||||
stehen; in Woche und Tag rastet der Termin auf **15 Minuten** ein, und eine
|
||||
gestrichelte Linie zeigt vorab die neue Startzeit. Die **Dauer bleibt** immer
|
||||
gleich, und der Punkt, an dem man den Termin angefasst hat, wandert mit. Ziehen
|
||||
darf nur, wer den Termin auch aendern darf.
|
||||
* **Farben** gehoeren zum Ersteller. Die eigene Farbe aendert man unter
|
||||
darf nur, wer den Termin auch ändern darf.
|
||||
* **Farben** gehören zum Ersteller. Die eigene Farbe ändert man unter
|
||||
*Einstellungen → Mein Konto*; die Legende unter dem Kalender zeigt, welche
|
||||
Farbe zu wem gehoert.
|
||||
Farbe zu wem gehört.
|
||||
* **Teilnehmer** erscheinen mit Foto oder Initiale und sind anklickbar.
|
||||
* **@-Erwaehnungen**: In der Beschreibung `@` tippen, dann Kontakte *und*
|
||||
Benutzer aus der Vorschlagsliste waehlen (Pfeiltasten, Enter). In der
|
||||
Leseansicht sind sie klickbar - Kontakte oeffnen die Kontaktkarte, Benutzer ein
|
||||
Detailfenster mit naechsten Terminen.
|
||||
* Rechts stehen die **naechsten Termine**, nach Tagen gruppiert. Vergangenes
|
||||
* **@-Erwähnungen**: In der Beschreibung `@` tippen, dann Kontakte *und*
|
||||
Benutzer aus der Vorschlagsliste wählen (Pfeiltasten, Enter). In der
|
||||
Leseansicht sind sie klickbar - Kontakte öffnen die Kontaktkarte, Benutzer ein
|
||||
Detailfenster mit nächsten Terminen.
|
||||
* Rechts stehen die **nächsten Termine**, nach Tagen gruppiert. Vergangenes
|
||||
fliegt aus der Liste, bleibt im Kalender aber erhalten.
|
||||
|
||||
Aendern und loeschen darf nur der **Ersteller** oder ein **Administrator**.
|
||||
Ändern und löschen darf nur der **Ersteller** oder ein **Administrator**.
|
||||
Anlegen darf jeder.
|
||||
|
||||
## Kontakte
|
||||
|
||||
Drei Ansichten, davon eine besondere:
|
||||
|
||||
| Ansicht | wofuer |
|
||||
| Ansicht | wofür |
|
||||
| --- | --- |
|
||||
| **Kachel** | Ueberblick mit Foto, Firma, Status |
|
||||
| **Liste** | sortierbar, viele Eintraege |
|
||||
| **Kachel** | Überblick mit Foto, Firma, Status |
|
||||
| **Liste** | sortierbar, viele Einträge |
|
||||
| **Netzwerk** | wer steht mit wem in Kontakt |
|
||||
|
||||
Im **Netzwerk** sind Kreise Kontakte (Farbe = Status, Groesse = Anzahl
|
||||
Verbindungen) und Quadrate Benutzer, die Kontakte angelegt haben. Gruene Linien
|
||||
Im **Netzwerk** sind Kreise Kontakte (Farbe = Status, Größe = Anzahl
|
||||
Verbindungen) und Quadrate Benutzer, die Kontakte angelegt haben. Grüne Linien
|
||||
verbinden Kontakte untereinander, graue gestrichelte den Ersteller mit seinem
|
||||
Kontakt.
|
||||
|
||||
Bedienung: Hintergrund ziehen verschiebt, Mausrad zoomt, einen Knoten ziehen
|
||||
setzt ihn fest (gruener Punkt), Doppelklick loest ihn wieder. Ueber *Verbindung
|
||||
setzt ihn fest (grüner Punkt), Doppelklick löst ihn wieder. Über *Verbindung
|
||||
erstellen* zwei Kontakte anklicken, um eine Verbindung anzulegen; `Esc` bricht ab.
|
||||
Suche und Statusfilter wirken auch hier.
|
||||
|
||||
Sobald Kontakte aus dem Postfach uebernommen wurden, erscheint neben dem
|
||||
Statusfilter ein **Herkunftsfilter**: *Aus Mails uebernommen* zeigt nur die aus
|
||||
Sobald Kontakte aus dem Postfach übernommen wurden, erscheint neben dem
|
||||
Statusfilter ein **Herkunftsfilter**: *Aus Mails übernommen* zeigt nur die aus
|
||||
dem Postfach stammenden, *Selbst angelegt* nur die von Hand erfassten.
|
||||
|
||||
### Aus Mails
|
||||
|
||||
Der Menuepunkt *Kontakte* klappt zwei Unterpunkte auf: *Alle Kontakte* und
|
||||
*Aus Mails*. Der Zaehler am Menuepunkt sagt, wie viele Funde warten.
|
||||
Der Menüpunkt *Kontakte* klappt zwei Unterpunkte auf: *Alle Kontakte* und
|
||||
*Aus Mails*. Der Zähler am Menüpunkt sagt, wie viele Funde warten.
|
||||
|
||||
In *Aus Mails* stehen die Personen, die der Postfachabgleich in Mails gefunden
|
||||
hat. **Sie sind noch keine Kontakte**: sie erscheinen nicht in der Kontaktliste,
|
||||
nicht in Opportunities und nicht im Kalender. Jede Karte zeigt alle gefundenen
|
||||
Angaben und darunter, aus welcher Mail sie stammen.
|
||||
|
||||
Zwei Faelle:
|
||||
Zwei Fälle:
|
||||
|
||||
* **Weisse Karte** - die Person ist neu. *In Kontakte übernehmen* legt sie als
|
||||
* **Weiße Karte** - die Person ist neu. *In Kontakte übernehmen* legt sie als
|
||||
Kontakt an, mit Herkunft „Mail".
|
||||
* **Gold markierte Karte** - es gibt sie in den Kontakten schon, erkannt an der
|
||||
E-Mail-Adresse oder am Namen. Gold hinterlegte Felder weichen ab; darunter steht
|
||||
durchgestrichen, was bisher im Kontakt stand. *Kontakt aktualisieren* oeffnet
|
||||
die Feldauswahl: angehakt sind zunaechst nur die Luecken, also Felder, die im
|
||||
Kontakt leer sind. Ein Wert, der etwas Gepflegtes ersetzen wuerde, ist als
|
||||
*ueberschreibt* gekennzeichnet und muss bewusst angehakt werden - die Angabe
|
||||
durchgestrichen, was bisher im Kontakt stand. *Kontakt aktualisieren* öffnet
|
||||
die Feldauswahl: angehakt sind zunächst nur die Lücken, also Felder, die im
|
||||
Kontakt leer sind. Ein Wert, der etwas Gepflegtes ersetzen würde, ist als
|
||||
*überschreibt* gekennzeichnet und muss bewusst angehakt werden - die Angabe
|
||||
kann aus einer alten Signatur stammen. *Neu anlegen* erzeugt trotz Dublette
|
||||
einen zweiten Kontakt, etwa wenn es zwei Personen mit demselben Namen gibt.
|
||||
|
||||
Weitere Bedienelemente:
|
||||
|
||||
* **Alle neuen übernehmen** legt alle weissen Karten auf einmal als Kontakte an;
|
||||
gold markierte bleiben unberuehrt.
|
||||
* **Alle neuen übernehmen** legt alle weißen Karten auf einmal als Kontakte an;
|
||||
gold markierte bleiben unberührt.
|
||||
* **Eintrag hier behalten** - normalerweise verschwindet ein Eintrag aus *Aus
|
||||
Mails*, sobald er als Kontakt gefuehrt wird. Mit dem Haken bleibt er stehen.
|
||||
Mails*, sobald er als Kontakt geführt wird. Mit dem Haken bleibt er stehen.
|
||||
* Das **Papierkorbsymbol** entfernt einen Fund. Die Mails bleiben erhalten; taucht
|
||||
die Person in einer neuen Mail auf, erscheint sie hier wieder.
|
||||
* Der Filter waehlt zwischen allen Eintraegen, nur neuen Personen und nur
|
||||
* Der Filter wählt zwischen allen Einträgen, nur neuen Personen und nur
|
||||
Dubletten.
|
||||
|
||||
## Mails
|
||||
|
||||
Das gemeinsame Postfach `crm@perinco.de` liegt hier im CRM. Der Zugriff ist
|
||||
**nur lesend** - im Postfach selbst wird nichts veraendert, keine Mail als gelesen
|
||||
markiert, nichts geloescht. Antworten geht weiter in Outlook; der Link *In Outlook
|
||||
oeffnen* springt direkt zur Mail.
|
||||
**nur lesend** - im Postfach selbst wird nichts verändert, keine Mail als gelesen
|
||||
markiert, nichts gelöscht. Antworten geht weiter in Outlook; der Link *In Outlook
|
||||
öffnen* springt direkt zur Mail.
|
||||
|
||||
Zwei Reiter oben:
|
||||
|
||||
### Postfach
|
||||
|
||||
Links die Mailliste, rechts die gewaehlte Mail. Die Kennzeichen an einem
|
||||
Links die Mailliste, rechts die gewählte Mail. Die Kennzeichen an einem
|
||||
Listeneintrag sagen, was noch zu tun ist:
|
||||
|
||||
| Kennzeichen | Bedeutung |
|
||||
| --- | --- |
|
||||
| **1x zugeordnet** | haengt an einer Opportunity oder einem Kontakt |
|
||||
| **2 Kontakte** | so viele Kontakt-Vorschlaege warten noch auf eine Entscheidung |
|
||||
| 📎 | Anhaenge vorhanden (im Postfach, nicht im CRM) |
|
||||
| **1x zugeordnet** | hängt an einer Opportunity oder einem Kontakt |
|
||||
| **2 Kontakte** | so viele Kontakt-Vorschläge warten noch auf eine Entscheidung |
|
||||
| 📎 | Anhänge vorhanden (im Postfach, nicht im CRM) |
|
||||
|
||||
Das Auswahlfeld filtert die Liste. **Nicht zugeordnet** ist die eigentliche
|
||||
Arbeitsliste: alles, was noch nirgends haengt. Die Suche geht ueber Betreff,
|
||||
Absender und den kompletten Mailtext; mehrere Woerter wirken als ein Suchbegriff.
|
||||
Arbeitsliste: alles, was noch nirgends hängt. Die Suche geht über Betreff,
|
||||
Absender und den kompletten Mailtext; mehrere Wörter wirken als ein Suchbegriff.
|
||||
|
||||
**Zuordnen** haengt die Mail an eine Opportunity und/oder einen Kontakt. Der
|
||||
Dialog schlaegt oben vor, was zu den Mailadressen passt - ein Klick auf den
|
||||
Vorschlag genuegt. Beides gleichzeitig ist moeglich, jede Zuordnung laesst sich
|
||||
einzeln wieder mit dem **x** loesen.
|
||||
**Zuordnen** hängt die Mail an eine Opportunity und/oder einen Kontakt. Der
|
||||
Dialog schlägt oben vor, was zu den Mailadressen passt - ein Klick auf den
|
||||
Vorschlag genügt. Beides gleichzeitig ist möglich, jede Zuordnung lässt sich
|
||||
einzeln wieder mit dem **x** lösen.
|
||||
|
||||
Beim Mailinhalt sind externe Bilder zunaechst gesperrt. Das ist Absicht: geladene
|
||||
Bilder melden dem Absender, dass die Mail geoeffnet wurde. Ueber *Externe Bilder
|
||||
laden* geht es pro Mail. *Groesser* klappt den Inhalt auf, *Nur Text anzeigen*
|
||||
Beim Mailinhalt sind externe Bilder zunächst gesperrt. Das ist Absicht: geladene
|
||||
Bilder melden dem Absender, dass die Mail geöffnet wurde. Über *Externe Bilder
|
||||
laden* geht es pro Mail. *Größer* klappt den Inhalt auf, *Nur Text anzeigen*
|
||||
zeigt die Rohfassung ohne Formatierung.
|
||||
|
||||
Steht an einer Mail **Im Postfach geloescht**, wurde sie in Outlook entfernt. Im
|
||||
Steht an einer Mail **Im Postfach gelöscht**, wurde sie in Outlook entfernt. Im
|
||||
CRM bleibt sie erhalten, damit Zuordnungen nicht verloren gehen.
|
||||
|
||||
### Kontakt-Vorschlaege
|
||||
### Kontakt-Vorschläge
|
||||
|
||||
Aus Absender, Empfaengerliste und Signatur jeder Mail liest die Anwendung
|
||||
Aus Absender, Empfängerliste und Signatur jeder Mail liest die Anwendung
|
||||
Kontaktdaten. Wer eine echte Mailadresse hat und aus Absender oder
|
||||
Empfaengerliste stammt, landet **automatisch** im Vorbereich
|
||||
Empfängerliste stammt, landet **automatisch** im Vorbereich
|
||||
[Aus Mails](#aus-mails) - nicht direkt in den Kontakten. Alles Unsichere landet
|
||||
hier - typischerweise Personen, die nur mit Telefonnummer im Text genannt werden.
|
||||
|
||||
Der Balken rechts ist die Zuversicht: je voller und gruener, je verlaesslicher die
|
||||
Der Balken rechts ist die Zuversicht: je voller und grüner, je verlässlicher die
|
||||
Angaben. Das Kennzeichen darunter sagt, woher sie stammen (*Absender*,
|
||||
*Empfaenger*, *Signatur*, *Im Text genannt*).
|
||||
*Empfänger*, *Signatur*, *Im Text genannt*).
|
||||
|
||||
Zwei Knoepfe:
|
||||
Zwei Knöpfe:
|
||||
|
||||
* **Als Kontakt anlegen** - es entsteht sofort ein richtiger Kontakt mit Herkunft
|
||||
„Mail", nicht nur ein Eintrag in *Aus Mails*: hier hat ja schon jemand
|
||||
hingesehen und entschieden.
|
||||
* **Ergaenzen** (statt „anlegen", wenn **Bereits vorhanden** dransteht) - der
|
||||
* **Ergänzen** (statt „anlegen", wenn **Bereits vorhanden** dransteht) - der
|
||||
vorhandene Kontakt wird um die fehlenden Angaben erweitert. Was schon gepflegt
|
||||
ist, bleibt unangetastet.
|
||||
* **Verwerfen** - der Vorschlag verschwindet aus der Liste.
|
||||
@@ -227,18 +231,18 @@ Zwei Knoepfe:
|
||||
Eigene Kollegen (`@perinco.de`) und Funktionsadressen wie `info@` oder
|
||||
`no-reply@` werden nie vorgeschlagen.
|
||||
|
||||
Ist die Liste leer und die automatische Uebernahme an, ist das der Normalzustand:
|
||||
Ist die Liste leer und die automatische Übernahme an, ist das der Normalzustand:
|
||||
es gab nichts zu entscheiden.
|
||||
|
||||
## Scanner
|
||||
|
||||
Durchsucht Projektboersen und bewertet jeden Fund mit KI gegen die Lebenslaeufe
|
||||
Durchsucht Projektbörsen und bewertet jeden Fund mit KI gegen die Lebensläufe
|
||||
der Berater.
|
||||
|
||||
### Dashboard
|
||||
|
||||
Oben vier Kennzahlen, darunter die Trefferliste. Der Knopf **Jetzt scannen**
|
||||
startet sofort einen Suchlauf; laeuft schon einer, sagt die Anwendung das.
|
||||
startet sofort einen Suchlauf; läuft schon einer, sagt die Anwendung das.
|
||||
|
||||
Die Filterzeile:
|
||||
|
||||
@@ -246,123 +250,123 @@ Die Filterzeile:
|
||||
| --- | --- |
|
||||
| **Zeitraum** | wie alt darf das Projekt sein (Standard: letzte 24 Stunden) |
|
||||
| **Agent** | nur Treffer eines Beraters |
|
||||
| **Anzeige ab %** | schwaechere Treffer ausblenden |
|
||||
| **Anzeige ab %** | schwächere Treffer ausblenden |
|
||||
| **Hinweis ab %** | ab wann Signalton und E-Mail |
|
||||
|
||||
Die letzten zwei Werte werden **gespeichert** und gelten auch beim naechsten Mal
|
||||
Die letzten zwei Werte werden **gespeichert** und gelten auch beim nächsten Mal
|
||||
und in den Einstellungen. Rechts daneben steht `sichtbar / gesamt`.
|
||||
|
||||
Ein Klick auf eine Zeile oeffnet die Details: Projektbeschreibung, die
|
||||
KI-Auswertung mit Pro und Contra, dazu die Knoepfe *Gesehen*, *Beworben*,
|
||||
*Zurueck auf Neu*, *Boerse oeffnen*, *Passt nicht* und *Treffer loeschen*.
|
||||
Ein Klick auf eine Zeile öffnet die Details: Projektbeschreibung, die
|
||||
KI-Auswertung mit Pro und Contra, dazu die Knöpfe *Gesehen*, *Beworben*,
|
||||
*Zurück auf Neu*, *Börse öffnen*, *Passt nicht* und *Treffer löschen*.
|
||||
|
||||
Ueber der Liste steht der Betriebszustand: *automatische Suche aktiv*, *nur
|
||||
manuelle Suche*, *pausiert* oder *Suchlauf laeuft*.
|
||||
Über der Liste steht der Betriebszustand: *automatische Suche aktiv*, *nur
|
||||
manuelle Suche*, *pausiert* oder *Suchlauf läuft*.
|
||||
|
||||
### Ton bei neuen Treffern
|
||||
|
||||
Findet der Scanner einen Treffer ueber dem Wert *Hinweis ab %*, spielt das
|
||||
Findet der Scanner einen Treffer über dem Wert *Hinweis ab %*, spielt das
|
||||
Dashboard einen Signalton und zeigt ein Hinweisbanner. Drei Dinge muss man dazu
|
||||
wissen:
|
||||
|
||||
1. **Nur im Dashboard.** Wer auf Opportunities oder Kontakte wechselt, hoert
|
||||
nichts - und bekommt es auch spaeter nicht nachgemeldet.
|
||||
2. **In einem anderen Browsertab** klingelt es weiter, aber verzoegert: der
|
||||
Browser bremst versteckte Tabs auf etwa eine Pruefung pro Minute. Ausserdem
|
||||
muss man vorher einmal irgendwo auf der Seite geklickt haben, sonst laesst der
|
||||
1. **Nur im Dashboard.** Wer auf Opportunities oder Kontakte wechselt, hört
|
||||
nichts - und bekommt es auch später nicht nachgemeldet.
|
||||
2. **In einem anderen Browsertab** klingelt es weiter, aber verzögert: der
|
||||
Browser bremst versteckte Tabs auf etwa eine Prüfung pro Minute. Ausserdem
|
||||
muss man vorher einmal irgendwo auf der Seite geklickt haben, sonst lässt der
|
||||
Browser keinen Ton zu.
|
||||
3. **Ist der Tab geschlossen, passiert nichts.** Wer unabhaengig vom Browser
|
||||
3. **Ist der Tab geschlossen, passiert nichts.** Wer unabhängig vom Browser
|
||||
benachrichtigt werden will, schaltet die **E-Mail** in den Einstellungen ein.
|
||||
|
||||
### Unpassend
|
||||
|
||||
Alles, was ueber *Passt nicht* aussortiert wurde. Von hier bringt *Zurueck ins
|
||||
Dashboard* einen Treffer wieder zurueck, oder er wird endgueltig geloescht.
|
||||
Alles, was über *Passt nicht* aussortiert wurde. Von hier bringt *Zurück ins
|
||||
Dashboard* einen Treffer wieder zurück, oder er wird endgültig gelöscht.
|
||||
|
||||
### Suchagenten
|
||||
|
||||
Ein Suchagent ist ein Berater:
|
||||
|
||||
| Feld | wofuer |
|
||||
| Feld | wofür |
|
||||
| --- | --- |
|
||||
| **Name** | erscheint an jedem Treffer |
|
||||
| **E-Mail** | Ziel der Benachrichtigung fuer diesen Berater |
|
||||
| **Suchbegriffe** | kommagetrennt, damit wird auf den Boersen gesucht |
|
||||
| **Ausschluss-Begriffe** | kommagetrennt; Projekte mit diesen Woertern werden uebersprungen |
|
||||
| **E-Mail** | Ziel der Benachrichtigung für diesen Berater |
|
||||
| **Suchbegriffe** | kommagetrennt, damit wird auf den Börsen gesucht |
|
||||
| **Ausschluss-Begriffe** | kommagetrennt; Projekte mit diesen Wörtern werden übersprungen |
|
||||
| **Lebenslauf (PDF)** | wird als Text ausgelesen und gegen jedes Projekt bewertet |
|
||||
| **aktiv** | nur aktive Agenten suchen im Hintergrund |
|
||||
|
||||
Ohne Lebenslauf kann ein Agent nichts bewerten. Die Ausschluss-Begriffe sind
|
||||
nicht nur Bequemlichkeit: sie greifen **vor** der KI-Bewertung und sparen damit
|
||||
echte Kosten. „Arbeitnehmerueberlassung" oder „Festanstellung" sind typische
|
||||
echte Kosten. „Arbeitnehmerüberlassung" oder „Festanstellung" sind typische
|
||||
Kandidaten.
|
||||
|
||||
> **Loeschen nimmt die Treffer mit.** Wird ein Suchagent geloescht, verschwinden
|
||||
> alle seine bisherigen Treffer und sein Lebenslauf mit ihm - die Datenbank raeumt
|
||||
> das automatisch auf. Die gefundenen **Projekte** bleiben erhalten, sie gehoeren
|
||||
> **Löschen nimmt die Treffer mit.** Wird ein Suchagent gelöscht, verschwinden
|
||||
> alle seine bisherigen Treffer und sein Lebenslauf mit ihm - die Datenbank räumt
|
||||
> das automatisch auf. Die gefundenen **Projekte** bleiben erhalten, sie gehören
|
||||
> nicht zu einem einzelnen Agenten. Wer einen Berater nur vorübergehend aus der
|
||||
> Suche nehmen will, setzt ihn besser auf **inaktiv** statt ihn zu loeschen: dann
|
||||
> Suche nehmen will, setzt ihn besser auf **inaktiv** statt ihn zu löschen: dann
|
||||
> sucht er nicht mehr, die Treffer bleiben aber sichtbar.
|
||||
|
||||
### Boersen-Konten
|
||||
### Börsen-Konten
|
||||
|
||||
Zeigt fuer jede Boerse, ob die Verbindung steht. Zwei Arten:
|
||||
Zeigt für jede Börse, ob die Verbindung steht. Zwei Arten:
|
||||
|
||||
* **Oeffentliche Portale** (Hays, Etengo, …) brauchen keinen Login.
|
||||
* **Geschuetzte Portale** (Freelance.de, Freelancermap.de) brauchen eine
|
||||
angemeldete Sitzung. Dafuer exportiert man die Cookies mit der
|
||||
Browser-Erweiterung *Cookie-Editor* als JSON und fuegt sie unter
|
||||
* **Öffentliche Portale** (Hays, Etengo, …) brauchen keinen Login.
|
||||
* **Geschützte Portale** (Freelance.de, Freelancermap.de) brauchen eine
|
||||
angemeldete Sitzung. Dafür exportiert man die Cookies mit der
|
||||
Browser-Erweiterung *Cookie-Editor* als JSON und fügt sie unter
|
||||
**Cookie-Import** ein.
|
||||
|
||||
*Jetzt pruefen* testet eine Verbindung sofort. Steht dort **Abgelaufen /
|
||||
*Jetzt prüfen* testet eine Verbindung sofort. Steht dort **Abgelaufen /
|
||||
CAPTCHA**, hilft nur ein neuer Cookie-Import.
|
||||
|
||||
Die Zugangsdaten (Benutzername, Passwort) sind optional - der Scraper versucht
|
||||
damit im Notfall eine eigene Anmeldung. Passwoerter liegen verschluesselt in der
|
||||
damit im Notfall eine eigene Anmeldung. Passwörter liegen verschlüsselt in der
|
||||
Datenbank.
|
||||
|
||||
## Einstellungen
|
||||
|
||||
| Reiter | Wer | Inhalt |
|
||||
| --- | --- | --- |
|
||||
| **Nutzerverwaltung** | Admin | Benutzer anlegen, bearbeiten, deaktivieren, loeschen |
|
||||
| **Mein Konto** | alle | Foto, Kalenderfarbe, Passwort aendern |
|
||||
| **Postfach** | Admin | Verbindung zu Microsoft 365, Abgleich, Kontaktuebernahme |
|
||||
| **Scanner** | Admin | Suchintervall, Schwellenwerte, KI-Schluessel, Ton, E-Mail |
|
||||
| **Whisper** | alle | Spracherkennung fuer das Diktat einrichten |
|
||||
| **Nutzerverwaltung** | Admin | Benutzer anlegen, bearbeiten, deaktivieren, löschen |
|
||||
| **Mein Konto** | alle | Foto, Kalenderfarbe, Passwort ändern |
|
||||
| **Postfach** | Admin | Verbindung zu Microsoft 365, Abgleich, Kontaktübernahme |
|
||||
| **Scanner** | Admin | Suchintervall, Schwellenwerte, KI-Schlüssel, Ton, E-Mail |
|
||||
| **Whisper** | alle | Spracherkennung für das Diktat einrichten |
|
||||
|
||||
Im Reiter **Scanner** steht das **Suchintervall (Minuten)** - also wie oft der
|
||||
Hintergrund-Scanner die Boersen durchsucht.
|
||||
Hintergrund-Scanner die Börsen durchsucht.
|
||||
|
||||
Darueber der Hauptschalter **Scanner aktiviert**. Ist er aus, sucht die Anwendung
|
||||
Darüber der Hauptschalter **Scanner aktiviert**. Ist er aus, sucht die Anwendung
|
||||
nicht mehr: kein Lauf im Hintergrund, und *Jetzt scannen* antwortet mit einem
|
||||
Hinweis statt zu starten. Der Zustand steht auch oben im Scanner-Dashboard, damit
|
||||
man nicht raetselt, warum keine neuen Treffer kommen.
|
||||
man nicht rätselt, warum keine neuen Treffer kommen.
|
||||
|
||||
Im Reiter **Postfach** steht oben, ob die Verbindung zu Microsoft 365 steht.
|
||||
**Verbindung testen** prueft beides: die Anmeldung *und* den tatsaechlichen
|
||||
Zugriff auf `crm@perinco.de`. Schlaegt etwas fehl, nennt die Meldung den
|
||||
**Verbindung testen** prüft beides: die Anmeldung *und* den tatsächlichen
|
||||
Zugriff auf `crm@perinco.de`. Schlägt etwas fehl, nennt die Meldung den
|
||||
wahrscheinlichen Grund. Darunter lassen sich Intervall, Zeitraum des ersten
|
||||
Durchlaufs und die automatische Kontaktuebernahme einstellen.
|
||||
Durchlaufs und die automatische Kontaktübernahme einstellen.
|
||||
|
||||
Die Zugangsdaten selbst stehen **nicht** in der Oberflaeche, sondern in der
|
||||
Die Zugangsdaten selbst stehen **nicht** in der Oberfläche, sondern in der
|
||||
Serverkonfiguration - sie werden beim Start gelesen. Fehlt einer, sagt die Seite
|
||||
welcher.
|
||||
|
||||
Foto und Kalenderfarbe werden **sofort** gespeichert, ohne Speichern-Knopf. Der
|
||||
letzte aktive Administrator kann sich nicht selbst degradieren oder loeschen -
|
||||
letzte aktive Administrator kann sich nicht selbst degradieren oder löschen -
|
||||
das verhindert der Server.
|
||||
|
||||
## Diktat
|
||||
|
||||
Ueberall, wo laengerer Text entsteht, sitzt ein Mikrofon-Knopf. Beim ersten Mal
|
||||
steht dort *Diktat einrichten* und fuehrt in die Einstellungen: dort einmal
|
||||
Überall, wo längerer Text entsteht, sitzt ein Mikrofon-Knopf. Beim ersten Mal
|
||||
steht dort *Diktat einrichten* und führt in die Einstellungen: dort einmal
|
||||
**Sprachmodell laden** klicken, rund 759 MB, danach ist es dauerhaft in diesem
|
||||
Browser.
|
||||
|
||||
Die Erkennung laeuft **komplett auf dem eigenen Rechner**. Aufnahmen verlassen
|
||||
das Geraet nicht und werden nirgends gespeichert. Auf einem anderen Rechner oder
|
||||
in einem anderen Browser faellt der Download erneut an - er gehoert zum Browser,
|
||||
Die Erkennung läuft **komplett auf dem eigenen Rechner**. Aufnahmen verlassen
|
||||
das Gerät nicht und werden nirgends gespeichert. Auf einem anderen Rechner oder
|
||||
in einem anderen Browser fällt der Download erneut an - er gehört zum Browser,
|
||||
nicht zum Benutzerkonto.
|
||||
|
||||
## Wenn etwas nicht klappt
|
||||
@@ -373,10 +377,10 @@ nicht zum Benutzerkonto.
|
||||
| Trefferliste ist leer | *Anzeige ab %* zu hoch oder *Zeitraum* zu kurz |
|
||||
| Kein Ton bei Treffern | Dashboard offen? Einmal auf der Seite geklickt? Wert *Hinweis ab %* erreicht? |
|
||||
| Mikrofon-Knopf fehlt | kein Mikrofon oder keine HTTPS-Verbindung |
|
||||
| Boerse zeigt „Abgelaufen" | Cookies neu importieren |
|
||||
| „Ein Suchlauf laeuft bereits" | kurz warten, es laeuft nur einer zur Zeit |
|
||||
| Aendern nicht erlaubt | bei Opportunities darf nur Ersteller, Zustaendiger oder Admin |
|
||||
| Börse zeigt „Abgelaufen" | Cookies neu importieren |
|
||||
| „Ein Suchlauf läuft bereits" | kurz warten, es läuft nur einer zur Zeit |
|
||||
| Ändern nicht erlaubt | bei Opportunities darf nur Ersteller, Zuständiger oder Admin |
|
||||
| Keine Mails sichtbar | steht oben ein Hinweisbalken? Sonst Admin bitten, in *Einstellungen → Postfach* die Verbindung zu testen |
|
||||
| „Erstabgleich laeuft noch" | aeltere Mails werden in mehreren Durchlaeufen nachgeholt, die Liste waechst noch |
|
||||
| „Erstabgleich läuft noch" | ältere Mails werden in mehreren Durchläufen nachgeholt, die Liste wächst noch |
|
||||
| Bilder in einer Mail fehlen | *Externe Bilder laden* klicken - sie sind absichtlich gesperrt |
|
||||
| Kontakt-Vorschlaege bleiben leer | Normalfall: sichere Vorschlaege wandern automatisch nach *Kontakte › Aus Mails* und tauchen hier nicht auf |
|
||||
| Kontakt-Vorschläge bleiben leer | Normalfall: sichere Vorschläge wandern automatisch nach *Kontakte › Aus Mails* und tauchen hier nicht auf |
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# 08 Anleitung fuer Entwickler
|
||||
# 08 Anleitung für Entwickler
|
||||
|
||||
Fuer jemanden, der das Projekt zum ersten Mal oeffnet. Was man braucht, wie man es
|
||||
startet, wo man was aendert - und welche Fallen es gibt.
|
||||
Für jemanden, der das Projekt zum ersten Mal öffnet. Was man braucht, wie man es
|
||||
startet, wo man was ändert - und welche Fallen es gibt.
|
||||
|
||||
## In zwei Minuten zum Bild
|
||||
|
||||
* **Ein** Vite-Frontend (React 19, TypeScript, Tailwind 4), **ein** Express-Backend,
|
||||
**zwei** MariaDB-Datenbanken.
|
||||
* Kein Router im Frontend: `App.tsx` haelt die Ansicht in einem `useState`. Die
|
||||
* Kein Router im Frontend: `App.tsx` hält die Ansicht in einem `useState`. Die
|
||||
App liegt hinter einer Anmeldung, tiefe Links braucht sie nicht.
|
||||
* Kein ORM im CRM-Teil - dort schreibt das Backend SQL mit `mysql2`. Der
|
||||
Scanner-Teil benutzt Sequelize, weil er 1:1 aus einer anderen Anwendung
|
||||
uebernommen wurde.
|
||||
* Alles auf Deutsch: Bezeichner, Kommentare, Meldungen. Kommentare erklaeren das
|
||||
übernommen wurde.
|
||||
* Alles auf Deutsch: Bezeichner, Kommentare, Meldungen. Kommentare erklären das
|
||||
**Warum**, nicht das Was.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
| | Version |
|
||||
| --- | --- |
|
||||
| Node | 20 oder neuer (Server laeuft 20.20) |
|
||||
| Node | 20 oder neuer (Server läuft 20.20) |
|
||||
| MariaDB | 10.11 oder neuer, erreichbar mit Benutzer und Passwort |
|
||||
| Browser | Chrome/Edge fuer WebGPU beim Diktat (sonst laeuft es auf der CPU) |
|
||||
| Browser | Chrome/Edge für WebGPU beim Diktat (sonst läuft es auf der CPU) |
|
||||
|
||||
## Lokal starten
|
||||
|
||||
@@ -45,10 +45,10 @@ Was dabei von selbst passiert:
|
||||
* Fehlt `ADMIN_PASSWORD`, wird beim ersten Start ein Zufallspasswort erzeugt und
|
||||
**einmalig ins Log geschrieben**. Dort nachsehen, sonst kommt man nicht rein.
|
||||
|
||||
Ohne MariaDB startet das Backend nicht. Das Frontend laesst sich trotzdem
|
||||
ansehen - siehe *Oberflaeche pruefen ohne Datenbank* weiter unten.
|
||||
Ohne MariaDB startet das Backend nicht. Das Frontend lässt sich trotzdem
|
||||
ansehen - siehe *Oberfläche prüfen ohne Datenbank* weiter unten.
|
||||
|
||||
## Pruefen
|
||||
## Prüfen
|
||||
|
||||
```bash
|
||||
npm run lint # = tsc --noEmit, das ist die Wahrheit
|
||||
@@ -56,15 +56,15 @@ npm run build # muss ohne Fehler durchlaufen
|
||||
cd server && npm test # Pruefungen der Postfachanbindung
|
||||
```
|
||||
|
||||
Automatische Tests gibt es nur fuer **einen** Bereich: `server/test/mail.test.mjs`
|
||||
prueft die Kontaktextraktion aus Mails und den Graph-Client. Beides laeuft
|
||||
unbeaufsichtigt im Hintergrund und laesst sich von Hand kaum nachstellen - ein
|
||||
Automatische Tests gibt es nur für **einen** Bereich: `server/test/mail.test.mjs`
|
||||
prüft die Kontaktextraktion aus Mails und den Graph-Client. Beides läuft
|
||||
unbeaufsichtigt im Hintergrund und lässt sich von Hand kaum nachstellen - ein
|
||||
falsch erkannter Name landet still in der Kontaktliste, und ein 429 vom echten
|
||||
Dienst kommt genau dann, wenn niemand hinsieht. Kein Test-Framework, reines Node,
|
||||
keine Datenbank.
|
||||
|
||||
Fuer alles andere gilt: `tsc`, ein Build und ein Blick in die Oberflaeche - notfalls
|
||||
mit dem Stub aus [Oberflaeche pruefen ohne Datenbank](#oberflaeche-pruefen-ohne-datenbank).
|
||||
Für alles andere gilt: `tsc`, ein Build und ein Blick in die Oberfläche - notfalls
|
||||
mit dem Stub aus [Oberfläche prüfen ohne Datenbank](#oberflaeche-pruefen-ohne-datenbank).
|
||||
|
||||
## Aufbau
|
||||
|
||||
@@ -86,7 +86,7 @@ docs/ diese Dokumentation
|
||||
deploy/ Apache-Vhosts, systemd-Unit, Deployskript
|
||||
```
|
||||
|
||||
## Wie eine Anfrage laeuft
|
||||
## Wie eine Anfrage läuft
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -109,25 +109,25 @@ sequenceDiagram
|
||||
A-->>K: typisiertes Objekt
|
||||
```
|
||||
|
||||
Jede Route liefert den **vollstaendigen, angereicherten Datensatz** zurueck (mit
|
||||
Jede Route liefert den **vollständigen, angereicherten Datensatz** zurück (mit
|
||||
`created_by_name`, `owner_name`, Ansprechpartnern, Teilnehmern). Das Frontend muss
|
||||
danach nichts nachladen.
|
||||
|
||||
## Etwas hinzufuegen
|
||||
## Etwas hinzufügen
|
||||
|
||||
### Neues Feld an einem Kontakt
|
||||
|
||||
1. `server/src/schema.sql`: Spalte mit `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`
|
||||
ergaenzen - das Statement muss beim naechsten Start wiederholbar sein.
|
||||
ergänzen - das Statement muss beim nächsten Start wiederholbar sein.
|
||||
2. `server/src/routes/leads.js`: Feld in die Listen `TEXT_FIELDS` bzw. in
|
||||
`INSERT`/`UPDATE` und in den `SELECT` aufnehmen.
|
||||
3. `src/types.ts`: Feld ins `Lead`-Interface.
|
||||
4. `src/components/leads/LeadModal.tsx`: Eingabefeld, `inputClass` benutzen.
|
||||
5. Wenn es in Kachel oder Liste sichtbar sein soll: `LeadGrid.tsx`, `LeadList.tsx`.
|
||||
|
||||
### Neuer Menuepunkt
|
||||
### Neuer Menüpunkt
|
||||
|
||||
1. `src/types.ts`: Wert in `ViewState` ergaenzen.
|
||||
1. `src/types.ts`: Wert in `ViewState` ergänzen.
|
||||
2. `src/components/Sidebar.tsx`: Eintrag in `CATEGORY_ITEMS` (Name + SVG-Pfad).
|
||||
3. `src/App.tsx`: Komponente rendern und einen Untertitel setzen.
|
||||
|
||||
@@ -137,7 +137,7 @@ Kopfzeile. Wer eine abweichende Beschriftung braucht, setzt `label` am `MenuItem
|
||||
|
||||
### Neuer Reiter in den Einstellungen
|
||||
|
||||
1. `src/types.ts`: Wert in `SettingsTab`; nur fuer Admins zusaetzlich in
|
||||
1. `src/types.ts`: Wert in `SettingsTab`; nur für Admins zusätzlich in
|
||||
`ADMIN_SETTINGS_TABS`.
|
||||
2. `src/components/settings/Settings.tsx`: Eintrag in `TABS` und Rendering.
|
||||
|
||||
@@ -147,24 +147,24 @@ Ein Sprung dorthin aus einer beliebigen Tiefe:
|
||||
### Neuer Endpunkt
|
||||
|
||||
1. Router in `server/src/routes/` anlegen, `router.use(requireAuth)` nicht
|
||||
vergessen, `requireAdmin` wo noetig.
|
||||
2. In `server/src/server.js` einhaengen - **vor** dem `/api`-404-Fallback.
|
||||
3. In `src/lib/api.ts` eine typisierte Funktion ergaenzen.
|
||||
vergessen, `requireAdmin` wo nötig.
|
||||
2. In `server/src/server.js` einhängen - **vor** dem `/api`-404-Fallback.
|
||||
3. In `src/lib/api.ts` eine typisierte Funktion ergänzen.
|
||||
|
||||
### Neue Einstellung im CRM
|
||||
|
||||
Nur fuer Werte, die im Betrieb verstellbar sein sollen. **Zugangsdaten gehoeren
|
||||
Nur für Werte, die im Betrieb verstellbar sein sollen. **Zugangsdaten gehören
|
||||
nicht dazu** - die stehen in `server/.env` und werden beim Start gelesen, damit ein
|
||||
Datenbank-Dump keine Geheimnisse enthaelt.
|
||||
Datenbank-Dump keine Geheimnisse enthält.
|
||||
|
||||
1. Schluessel samt Vorgabe in das Vorgabenobjekt des Bereichs eintragen (Muster:
|
||||
1. Schlüssel samt Vorgabe in das Vorgabenobjekt des Bereichs eintragen (Muster:
|
||||
`MAIL_SETTING_DEFAULTS` in `server/src/mail/config.js`). Es ist gleichzeitig die
|
||||
Positivliste des Speicher-Endpunkts.
|
||||
2. Lesen mit `getSettings(defaults)` aus `server/src/settings.js`, schreiben mit
|
||||
`setSetting()`. Zahlen und Schalter mit `asInt`/`asBool` wandeln.
|
||||
3. Feld im passenden Einstellungsreiter ergaenzen.
|
||||
3. Feld im passenden Einstellungsreiter ergänzen.
|
||||
|
||||
Die Tabelle `crm_settings` legt `schema.sql` an; ein neuer Schluessel braucht keine
|
||||
Die Tabelle `crm_settings` legt `schema.sql` an; ein neuer Schlüssel braucht keine
|
||||
Migration.
|
||||
|
||||
## Fallen
|
||||
@@ -173,26 +173,26 @@ Diese Punkte haben real Zeit gekostet:
|
||||
|
||||
| Falle | Was passiert |
|
||||
| --- | --- |
|
||||
| **Reihenfolge in `server.js`** | Der 404-Fallback `app.use('/api', ...)` faengt alles, was danach registriert wird. Neue Router davor einhaengen. |
|
||||
| **Reihenfolge in `server.js`** | Der 404-Fallback `app.use('/api', ...)` fängt alles, was danach registriert wird. Neue Router davor einhängen. |
|
||||
| **`Content-Type` bei FormData** | `apiRequest` setzt JSON nur, wenn der Body **keine** `FormData` ist - sonst fehlt die multipart-Grenze und der Upload scheitert. |
|
||||
| **Dateien in neuen Tabs** | Ein `<a href>` schickt kein Bearer-Token. Deshalb holt `openAgentCv()` das PDF per `fetch` und oeffnet einen Blob. |
|
||||
| **Dateien in neuen Tabs** | Ein `<a href>` schickt kein Bearer-Token. Deshalb holt `openAgentCv()` das PDF per `fetch` und öffnet einen Blob. |
|
||||
| **Leerzeichen in Pfaden** | Der alte Scanner liegt unter `/var/www/Projekt Scanner/…`. In der `.env` muss der Wert gequotet sein, sonst schneidet systemd ihn am Leerzeichen ab. |
|
||||
| **`.env` ist gleichzeitig `EnvironmentFile`** | Also kein `export`, keine Shell-Ausdruecke, Werte mit Leerzeichen in Anfuehrungszeichen. |
|
||||
| **Zeitstempel koennen nur vorziehen** | Ein „vorgealterter" `lastRunTime` verzoegert keinen Cronlauf. Wer verzoegern will, braucht eine eigene Frist. |
|
||||
| **Sperrfristen bei jedem Start neu** | Frueher schob jedes Deployment den faelligen Scan um 30 Minuten - er fand nie statt. Deshalb liegt `last_scan_at` in der Datenbank. |
|
||||
| **`.env` ist gleichzeitig `EnvironmentFile`** | Also kein `export`, keine Shell-Ausdrücke, Werte mit Leerzeichen in Anführungszeichen. |
|
||||
| **Zeitstempel können nur vorziehen** | Ein „vorgealterter" `lastRunTime` verzögert keinen Cronlauf. Wer verzögern will, braucht eine eigene Frist. |
|
||||
| **Sperrfristen bei jedem Start neu** | Früher schob jedes Deployment den fälligen Scan um 30 Minuten - er fand nie statt. Deshalb liegt `last_scan_at` in der Datenbank. |
|
||||
| **`sequelize.sync({ alter: true })`** | Legt bei jedem Start Unique-Indizes neu an und sprengt irgendwann MySQLs Grenze von 64 Keys. Nur `sync()` benutzen. |
|
||||
| **Kaskaden beim Loeschen** | Einen Suchagenten loeschen loescht alle seine Treffer mit. Inaktiv setzen ist meist gemeint. |
|
||||
| **Doppelte Scanner-Laeufe** | Scheduler und *Jetzt scannen* teilen eine Sperre. Ohne sie zahlt man jede KI-Bewertung zweimal. |
|
||||
| **`allow-same-origin` bei `srcDoc`** | Ein so eingebettetes Dokument erbt die Origin der Anwendung und kann `localStorage` lesen - also das Anmeldetoken. Fremdes HTML gehoert in `sandbox=""` ohne jedes Flag. Der Preis: die Inhaltshoehe ist nicht auslesbar, deshalb hat der Mailrahmen eine feste Hoehe. |
|
||||
| **`/me` unter App-only** | Es gibt keinen Benutzer im Token, also auch kein `/me` - jeder Graph-Aufruf muss `/users/{postfach}/…` lauten. Genau daran ist das Altsystem gescheitert, still, weil die Antwort nicht geprueft wurde. |
|
||||
| **Kaskaden beim Löschen** | Einen Suchagenten löschen löscht alle seine Treffer mit. Inaktiv setzen ist meist gemeint. |
|
||||
| **Doppelte Scanner-Läufe** | Scheduler und *Jetzt scannen* teilen eine Sperre. Ohne sie zahlt man jede KI-Bewertung zweimal. |
|
||||
| **`allow-same-origin` bei `srcDoc`** | Ein so eingebettetes Dokument erbt die Origin der Anwendung und kann `localStorage` lesen - also das Anmeldetoken. Fremdes HTML gehört in `sandbox=""` ohne jedes Flag. Der Preis: die Inhaltshöhe ist nicht auslesbar, deshalb hat der Mailrahmen eine feste Höhe. |
|
||||
| **`/me` unter App-only** | Es gibt keinen Benutzer im Token, also auch kein `/me` - jeder Graph-Aufruf muss `/users/{postfach}/…` lauten. Genau daran ist das Altsystem gescheitert, still, weil die Antwort nicht geprüft wurde. |
|
||||
| **UTC neben Wandzeit** | `crm_mail_messages.received_at` steht in UTC (so liefert Graph es), `crm_events.starts_at` in lokaler Wandzeit. Beim Lesen im Frontend `parseMailDate()` benutzen, sonst ist die Anzeige um Stunden verschoben. |
|
||||
| **Aufzaehlungen in der Datenbank sind deutsch** | `crm_mail_links.origin` (`manuell`/`automatisch`), Vorschlagsstatus (`offen`, `uebernommen`, `verworfen`, `dublette`). Wer englische Werte schickt, bekommt einen Datenbankfehler statt einer Validierungsmeldung. |
|
||||
| **Aufzählungen in der Datenbank sind deutsch** | `crm_mail_links.origin` (`manuell`/`automatisch`), Vorschlagsstatus (`offen`, `uebernommen`, `verworfen`, `dublette`). Wer englische Werte schickt, bekommt einen Datenbankfehler statt einer Validierungsmeldung. |
|
||||
|
||||
## Oberflaeche pruefen ohne Datenbank
|
||||
## Oberfläche prüfen ohne Datenbank
|
||||
|
||||
Fuer Bildschirmfotos und Klickwege genuegt ein Stub: ein kleiner `node:http`-Server,
|
||||
der `dist/` ausliefert und auf `/api/...` erfundene Daten zurueckgibt. Token vorab
|
||||
setzen, dann ist die Anmeldung uebersprungen:
|
||||
Für Bildschirmfotos und Klickwege genügt ein Stub: ein kleiner `node:http`-Server,
|
||||
der `dist/` ausliefert und auf `/api/...` erfundene Daten zurückgibt. Token vorab
|
||||
setzen, dann ist die Anmeldung übersprungen:
|
||||
|
||||
```js
|
||||
await page.goto(BASE);
|
||||
@@ -208,18 +208,18 @@ Browser-Download.
|
||||
|
||||
* **Deutsch** in Bezeichnern, Kommentaren, Meldungen. Fachbegriffe bleiben
|
||||
englisch (`Match`, `Opportunity`, `Lead`).
|
||||
* **Kommentare erklaeren das Warum.** Was der Code tut, steht im Code.
|
||||
* **Kommentare erklären das Warum.** Was der Code tut, steht im Code.
|
||||
* **Kein `dark:` im Markup.** Nur die Skalen `ink-*`, `brand-*`, `canvas`,
|
||||
`surface` benutzen, den Rest macht `index.css`.
|
||||
* **Gemeinsame Klassen** statt kopierter Utility-Ketten: `inputClass`,
|
||||
`labelClass` aus `components/leads/leadUi.tsx`, dazu `cardClass`,
|
||||
`primaryButtonClass` aus `components/scanner/scannerUi.tsx`.
|
||||
* **Meldungen als Toast**, Loeschabfragen ueber `ConfirmDialog` - nie `alert()`
|
||||
* **Meldungen als Toast**, Löschabfragen über `ConfirmDialog` - nie `alert()`
|
||||
oder `confirm()`.
|
||||
* **Rechte immer doppelt**: Oberflaeche blendet aus, Backend antwortet mit 403.
|
||||
* **Rechte immer doppelt**: Oberfläche blendet aus, Backend antwortet mit 403.
|
||||
|
||||
## Deployment
|
||||
|
||||
Siehe [06 Betrieb](06-betrieb.md). Kurzform: lokal bauen, `deploy/deploy.ps1`
|
||||
laedt `dist/` und `server/src` hoch und startet den Dienst neu. Auf dem Server
|
||||
lädt `dist/` und `server/src` hoch und startet den Dienst neu. Auf dem Server
|
||||
wird **nicht** gebaut - dort sind RAM und Swap knapp.
|
||||
|
||||
+68
-68
@@ -1,6 +1,6 @@
|
||||
# 09 Quellcode-Referenz
|
||||
|
||||
Was in welcher Datei steckt und welche Zusagen sie einhaelt. Gedacht zum
|
||||
Was in welcher Datei steckt und welche Zusagen sie einhält. Gedacht zum
|
||||
Nachschlagen, nicht zum Durchlesen.
|
||||
|
||||
## Backend
|
||||
@@ -11,16 +11,16 @@ Baut die Express-App. Die **Reihenfolge** ist bedeutsam:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
J["express.json({ limit: '6mb' })<br/>Avatare kommen als Data-URL"] --> C["cors (nur fuer den Dev-Server)"]
|
||||
J["express.json({ limit: '6mb' })<br/>Avatare kommen als Data-URL"] --> C["cors (nur für den Dev-Server)"]
|
||||
C --> H["/api/health"]
|
||||
H --> R["/api/auth, /users, /leads,<br/>/connections, /events, /opportunities"]
|
||||
R --> S["/api/scanner (requireAuth + Router)"]
|
||||
S --> F["/api → 404 JSON<br/>faengt alles Unbekannte"]
|
||||
F --> E["Fehlerbehandlung:<br/>400 kaputtes JSON, 413 zu gross, 500 Rest"]
|
||||
S --> F["/api → 404 JSON<br/>fängt alles Unbekannte"]
|
||||
F --> E["Fehlerbehandlung:<br/>400 kaputtes JSON, 413 zu groß, 500 Rest"]
|
||||
```
|
||||
|
||||
Beim Start: `initDatabase()` (CRM-Schema, Admin-Seed) → `initScannerDatabase()`
|
||||
(darf fehlschlagen, dann laeuft das CRM ohne Scanner) → Scheduler, sofern
|
||||
(darf fehlschlagen, dann läuft das CRM ohne Scanner) → Scheduler, sofern
|
||||
`SCANNER_SCHEDULER !== 'false'`.
|
||||
|
||||
### `server/src/db.js`
|
||||
@@ -28,11 +28,11 @@ Beim Start: `initDatabase()` (CRM-Schema, Admin-Seed) → `initScannerDatabase()
|
||||
| Export | Zweck |
|
||||
| --- | --- |
|
||||
| `pool` | mysql2-Pool, `connectionLimit: 8`, `dateStrings: true` |
|
||||
| `initDatabase()` | fuehrt `schema.sql` Statement fuer Statement aus, dann `ensureAdminUser()` |
|
||||
| `initDatabase()` | führt `schema.sql` Statement für Statement aus, dann `ensureAdminUser()` |
|
||||
|
||||
`dateStrings: true` ist eine bewusste Entscheidung: Termine kommen als
|
||||
`YYYY-MM-DD HH:MM:SS` **ohne** Zeitzonenumrechnung an. Der Kalender rechnet mit
|
||||
Wandzeit; ein `Date`-Objekt aus dem Treiber wuerde beim Serialisieren nach UTC
|
||||
Wandzeit; ein `Date`-Objekt aus dem Treiber würde beim Serialisieren nach UTC
|
||||
verschieben.
|
||||
|
||||
`ensureAdminUser()` legt nur an, wenn `crm_users` **leer** ist. Ohne
|
||||
@@ -44,19 +44,19 @@ im Quelltext steht kein Passwort.
|
||||
| Export | Zweck |
|
||||
| --- | --- |
|
||||
| `signToken(user)` | JWT mit `sub` und `role`, Laufzeit aus `JWT_EXPIRES_IN` (12 h) |
|
||||
| `requireAuth` | Token pruefen **und** Benutzer frisch aus der DB laden |
|
||||
| `requireAuth` | Token prüfen **und** Benutzer frisch aus der DB laden |
|
||||
| `requireAdmin` | 403, wenn `req.user.role !== 'admin'` |
|
||||
|
||||
Dass `requireAuth` bei jeder Anfrage die Datenbank liest, ist Absicht: ein
|
||||
deaktivierter oder geloeschter Benutzer ist sofort draussen und nicht erst, wenn
|
||||
sein Token ablaeuft. `secret()` wirft, wenn `JWT_SECRET` fehlt oder kuerzer als 16
|
||||
deaktivierter oder gelöschter Benutzer ist sofort draußen und nicht erst, wenn
|
||||
sein Token abläuft. `secret()` wirft, wenn `JWT_SECRET` fehlt oder kürzer als 16
|
||||
Zeichen ist - lieber ein harter Startfehler als eine schwache Signatur.
|
||||
|
||||
### `server/src/userFields.js`
|
||||
|
||||
`normalizeColor` und `normalizeAvatar`, benutzt von `routes/auth.js` und
|
||||
`routes/users.js`. Farben nur als `#rrggbb`, Avatare nur als Bild-Data-URL oder
|
||||
`http(s)`-Adresse, jeweils laengenbegrenzt.
|
||||
`http(s)`-Adresse, jeweils längenbegrenzt.
|
||||
|
||||
### Die CRM-Router
|
||||
|
||||
@@ -65,33 +65,33 @@ Alle unter `router.use(requireAuth)`.
|
||||
| Router | Endpunkte | Rechte |
|
||||
| --- | --- | --- |
|
||||
| `auth.js` | `POST /login`, `GET /me`, `PATCH /me`, `POST /change-password` | `/login` offen, Rest angemeldet |
|
||||
| `users.js` | `GET /directory`; danach `requireAdmin`: `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | `/directory` fuer alle, Rest Admin |
|
||||
| `users.js` | `GET /directory`; danach `requireAdmin`: `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | `/directory` für alle, Rest Admin |
|
||||
| `leads.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id`; Vorbereich: `GET /mail-pool/sources`, `POST /mail-pool/promote`, `POST /:id/promote` | jeder Angemeldete |
|
||||
| `connections.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | jeder Angemeldete |
|
||||
| `events.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | anlegen jeder; aendern/loeschen nur Ersteller oder Admin |
|
||||
| `opportunities.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | aendern/loeschen nur Ersteller, Zustaendiger oder Admin |
|
||||
| `events.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | anlegen jeder; ändern/löschen nur Ersteller oder Admin |
|
||||
| `opportunities.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | ändern/löschen nur Ersteller, Zuständiger oder Admin |
|
||||
| `mail.js` | `GET /status`, `/messages`, `/messages/:id`, `/candidates`; `POST /messages/:id/links`, `/candidates/:id/adopt`, `/candidates/:id/reject`; `DELETE /links/:id`; `requireAdmin`: `GET/POST /settings`, `POST /test`, `POST /sync` | lesen und zuordnen jeder; einrichten Admin |
|
||||
|
||||
Wiederkehrende Muster in diesen Routern:
|
||||
|
||||
* `emptyToNull()` - leere Zeichenketten werden `NULL`, damit „nicht gesetzt" und
|
||||
„leer" in der Datenbank nicht zwei Zustaende sind.
|
||||
* `normalizeDate()` prueft echte Kalendertage; der 31. Februar wird abgelehnt und
|
||||
„leer" in der Datenbank nicht zwei Zustände sind.
|
||||
* `normalizeDate()` prüft echte Kalendertage; der 31. Februar wird abgelehnt und
|
||||
nicht stillschweigend verschoben.
|
||||
* `normalizeNumber()` akzeptiert Komma als Dezimaltrennzeichen und begrenzt nach
|
||||
oben - ein Tagessatz von 10 Millionen ist ein Tippfehler.
|
||||
* `parseTags()` liest die als Text gespeicherten JSON-Arrays defensiv: kaputter
|
||||
Inhalt ergibt eine leere Liste, keinen Absturz.
|
||||
* `GET /login` antwortet bei unbekanntem Benutzer und falschem Passwort
|
||||
**identisch**, damit sich ueber die API keine Benutzernamen aufzaehlen lassen.
|
||||
**identisch**, damit sich über die API keine Benutzernamen aufzählen lassen.
|
||||
* Der letzte aktive Administrator kann nicht degradiert, deaktiviert oder
|
||||
geloescht werden.
|
||||
gelöscht werden.
|
||||
|
||||
### `server/src/settings.js`
|
||||
|
||||
`getSetting`, `getSettings`, `setSetting` auf `crm_settings`, dazu `asBool` und
|
||||
`asInt`. Alle Werte liegen als Text in der Datenbank. `getSettings(defaults)`
|
||||
nimmt die Vorgaben gleichzeitig als Positivliste der erlaubten Schluessel - ein
|
||||
nimmt die Vorgaben gleichzeitig als Positivliste der erlaubten Schlüssel - ein
|
||||
unbekannter Name kommt so nie in die Datenbank.
|
||||
|
||||
Bewusst getrennt vom Setting-Modell des Scanners (andere Datenbank) und bewusst
|
||||
@@ -108,36 +108,36 @@ Postfachanbindung, fachlich in [11 Postfachanbindung](11-postfach.md).
|
||||
| `html.js` | `htmlToText()` (Zeilen bleiben erhalten, Linkziele werden hinter den Linktext gesetzt), `sanitizeHtml()` |
|
||||
| `extract.js` | `extractCandidates()`, `parsePersonName()`, `parseSignature()`, `AUTO_ADOPT_MIN_CONFIDENCE` |
|
||||
| `store.js` | Zustandszeile, `upsertMessage()`, `insertCandidates()`, `linkMessage()`, `findLeadByEmail()` je Bestand |
|
||||
| `contacts.js` | `adoptCandidate()`: Eintrag in „Aus Mails" (`target: 'pool'`) oder richtiger Kontakt (`'contacts'`), sonst ergaenzen; `markCandidate()` |
|
||||
| `contacts.js` | `adoptCandidate()`: Eintrag in „Aus Mails" (`target: 'pool'`) oder richtiger Kontakt (`'contacts'`), sonst ergänzen; `markCandidate()` |
|
||||
| `sync.js` | ein Abgleichslauf, Delta-Seiten, Einzelsperre, Fehlerbehandlung |
|
||||
| `scheduler.js` | Minutentakt, Faelligkeit aus `last_run_at`, kein Zeitfenster |
|
||||
| `scheduler.js` | Minutentakt, Fälligkeit aus `last_run_at`, kein Zeitfenster |
|
||||
|
||||
Eigenheiten, die man kennen muss:
|
||||
|
||||
* `graphRequest()` nimmt Pfade **und** vollstaendige URLs - Delta-Links kommen
|
||||
absolut von Graph zurueck.
|
||||
* Ein `401` verwirft das Token genau einmal und wiederholt; so ueberlebt ein Lauf
|
||||
ein serverseitig zurueckgezogenes Token.
|
||||
* `upsertMessage()` laesst die Textspalten unberuehrt, wenn `bodyText` im
|
||||
uebergebenen Objekt `undefined` ist. Ein reines Metadaten-Update darf den schon
|
||||
geholten Mailtext nicht loeschen.
|
||||
* `graphRequest()` nimmt Pfade **und** vollständige URLs - Delta-Links kommen
|
||||
absolut von Graph zurück.
|
||||
* Ein `401` verwirft das Token genau einmal und wiederholt; so überlebt ein Lauf
|
||||
ein serverseitig zurückgezogenes Token.
|
||||
* `upsertMessage()` lässt die Textspalten unberührt, wenn `bodyText` im
|
||||
übergebenen Objekt `undefined` ist. Ein reines Metadaten-Update darf den schon
|
||||
geholten Mailtext nicht löschen.
|
||||
* `extractCandidates()` verlangt im Fliesstext eine Anrede vor dem Namen.
|
||||
Ohne diese Bedingung liest der Musterabgleich jede Folge grossgeschriebener
|
||||
Woerter als Namen.
|
||||
Ohne diese Bedingung liest der Musterabgleich jede Folge großgeschriebener
|
||||
Wörter als Namen.
|
||||
* Der Lauf ist **einfach gesperrt** (`running` in `sync.js`) - ein zweiter Aufruf
|
||||
kommt sofort mit `skipped` zurueck, statt parallel zu laufen.
|
||||
kommt sofort mit `skipped` zurück, statt parallel zu laufen.
|
||||
|
||||
### `server/src/routes/scanner.js`
|
||||
|
||||
Die portierte Scanner-API unter `/api/scanner`, vollstaendig in
|
||||
Die portierte Scanner-API unter `/api/scanner`, vollständig in
|
||||
[03 Projekt Scanner](03-scanner.md#endpunkte) tabelliert. Eigenheiten:
|
||||
|
||||
* `multer` schreibt CV-PDFs nach `UPLOAD_DIR`, Grenze 20 MB.
|
||||
* `resolveCvPath()` findet eine Datei auch dann, wenn in der Datenbank noch ein
|
||||
relativer Pfad aus der alten App steht - es zaehlt der Dateiname.
|
||||
* `GET /agents` laesst `cvText` weg (mehrere Hundert KB pro Agent).
|
||||
relativer Pfad aus der alten App steht - es zählt der Dateiname.
|
||||
* `GET /agents` lässt `cvText` weg (mehrere Hundert KB pro Agent).
|
||||
* `SECRET_KEYS` (`openai_api_key`, `smtp_pass`) werden als `********`
|
||||
ausgeliefert; kommt dieser Wert oder ein leeres Feld zurueck, bleibt der
|
||||
ausgeliefert; kommt dieser Wert oder ein leeres Feld zurück, bleibt der
|
||||
gespeicherte Wert stehen.
|
||||
* `normalizeCookie()` biegt Exporte von Cookie-Editor auf das zurecht, was
|
||||
Playwright akzeptiert (`sameSite`, `expirationDate` → `expires`).
|
||||
@@ -147,27 +147,27 @@ Die portierte Scanner-API unter `/api/scanner`, vollstaendig in
|
||||
| Datei | Inhalt |
|
||||
| --- | --- |
|
||||
| `db/config.js` | Sequelize-Instanz, eigene Datenbank, `underscored: true` |
|
||||
| `db/ensureDatabase.js` | `CREATE DATABASE IF NOT EXISTS`, Name gegen Zeichenklasse geprueft |
|
||||
| `db/ensureDatabase.js` | `CREATE DATABASE IF NOT EXISTS`, Name gegen Zeichenklasse geprüft |
|
||||
| `models/index.js` | `initScannerDatabase()`: verbinden, `sync()`, Portale und Standardeinstellungen seeden |
|
||||
| `models/Agent.js` | `cvText`, `cvFilePath`, `keywords`, `excludeKeywords`, `isActive` |
|
||||
| `models/Project.js` | Unique auf `(portal, portal_project_id)`, `updatedAt: false` |
|
||||
| `models/Match.js` | Unique auf `(agent_id, project_id)`, `pros`/`cons` als JSON-Text mit Getter/Setter |
|
||||
| `models/Portal.js` | Zugangsdaten, `sessionState`, `connectionStatus` |
|
||||
| `models/Setting.js` | Schluessel-Wert mit `getVal`/`setVal` |
|
||||
| `services/scraper.js` | Playwright je Portal; unveraendert aus der alten App |
|
||||
| `models/Setting.js` | Schlüssel-Wert mit `getVal`/`setVal` |
|
||||
| `services/scraper.js` | Playwright je Portal; unverändert aus der alten App |
|
||||
| `services/matchingEngine.js` | OpenAI `gpt-4o-mini`, `response_format: json_object`, `temperature: 0.1` |
|
||||
| `services/pipeline.js` | ein Lauf: scrapen, speichern, bewerten, benachrichtigen |
|
||||
| `services/scheduler.js` | node-cron jede Minute, Zeitfenster, Sperrfrist, `last_scan_at` |
|
||||
| `services/cleanup.js` | Projekte aelter als 7 Tage loeschen (Treffer kaskadieren) |
|
||||
| `services/cleanup.js` | Projekte älter als 7 Tage löschen (Treffer kaskadieren) |
|
||||
| `services/pdfParser.js` | `pdf-parse`, doppelte Leerzeilen raus |
|
||||
| `utils/crypto.js` | AES ueber `crypto-js`; `decryptWith()` fuer fremde Schluessel |
|
||||
| `utils/crypto.js` | AES über `crypto-js`; `decryptWith()` für fremde Schlüssel |
|
||||
| `uploadDir.js` | `UPLOAD_DIR`, `ensureUploadDir()`, `resolveCvPath()` |
|
||||
| `importLegacy.js` | einmalige Uebernahme aus der alten Datenbank |
|
||||
| `importLegacy.js` | einmalige Übernahme aus der alten Datenbank |
|
||||
|
||||
Zusagen, auf die sich der Rest verlaesst:
|
||||
Zusagen, auf die sich der Rest verlässt:
|
||||
|
||||
* `runMatchingPipeline()` laeuft **nie zweimal gleichzeitig** und gibt `false`
|
||||
zurueck, wenn schon einer laeuft. `isPipelineRunning()` fragt den Zustand ab.
|
||||
* `runMatchingPipeline()` läuft **nie zweimal gleichzeitig** und gibt `false`
|
||||
zurück, wenn schon einer läuft. `isPipelineRunning()` fragt den Zustand ab.
|
||||
* Der Zeitpunkt landet im `finally` in `settings.last_scan_at` - auch nach einem
|
||||
Abbruch, damit sich ein gescheiterter Lauf nicht sofort wiederholt.
|
||||
* `cleanup.js` benutzt `sequelize.col('created_at')` statt des Attributnamens:
|
||||
@@ -181,10 +181,10 @@ Zusagen, auf die sich der Rest verlaesst:
|
||||
| --- | --- |
|
||||
| `apiRequest<T>(path, options)` | zentraler Fetch: Token, JSON-Header, 401-Behandlung, Fehlermeldung aus dem Body |
|
||||
| `getToken` / `setToken` / `clearToken` | Token im `localStorage` (`perinco_crm_token`) |
|
||||
| `setUnauthorizedHandler` | wird bei 401 gerufen; `AuthProvider` haengt sich hier ein |
|
||||
| `setUnauthorizedHandler` | wird bei 401 gerufen; `AuthProvider` hängt sich hier ein |
|
||||
| `ApiError` | Fehler mit `status` |
|
||||
| `authApi`, `usersApi`, `leadsApi`, `opportunitiesApi`, `eventsApi`, `connectionsApi` | typisierte Endpunkte |
|
||||
| `leadsApi.mailPoolSources` / `.promote` / `.promoteMany` | Vorbereich „Aus Mails": Herkunft lesen, einzeln bzw. gesammelt uebernehmen |
|
||||
| `leadsApi.mailPoolSources` / `.promote` / `.promoteMany` | Vorbereich „Aus Mails": Herkunft lesen, einzeln bzw. gesammelt übernehmen |
|
||||
|
||||
Zwei Feinheiten: Der `Content-Type` wird **nicht** gesetzt, wenn der Body
|
||||
`FormData` ist. Und ein Netzwerkfehler wird zu `ApiError(status 0)` mit
|
||||
@@ -198,8 +198,8 @@ Typen (`ScannerAgent`, `ScannerPortal`, `ScannerProject`, `ScannerMatch`,
|
||||
|
||||
Die Feldnamen sind absichtlich die der Scanner-Datenbank (camelCase aus
|
||||
Sequelize), inklusive der Grossbuchstaben `Agent` und `Project` an den Joins -
|
||||
damit bleibt der Vergleich mit der alten App moeglich. Nur `ScannerProject`
|
||||
traegt beide Schreibweisen fuer `created_at`, weil das Modell die Spalte selbst
|
||||
damit bleibt der Vergleich mit der alten App möglich. Nur `ScannerProject`
|
||||
trägt beide Schreibweisen für `created_at`, weil das Modell die Spalte selbst
|
||||
umbenennt.
|
||||
|
||||
### `src/lib/mailApi.ts`
|
||||
@@ -208,28 +208,28 @@ Typen (`MailMessageSummary`, `MailMessageDetail`, `MailCandidate`, `MailLink`,
|
||||
`MailStatus`, `MailSyncResult`, `MailConnectionTest`, `MailSettingsValues`), das
|
||||
Objekt `mailApi`, `MAIL_SETTINGS_DEFAULTS` und die Datumshelfer.
|
||||
|
||||
`parseMailDate()` haengt ein `Z` an den MariaDB-Zeitstempel, bevor daraus ein
|
||||
`parseMailDate()` hängt ein `Z` an den MariaDB-Zeitstempel, bevor daraus ein
|
||||
`Date` wird. Ohne das liest der Browser den Wert als Ortszeit, und die Anzeige
|
||||
waere je Zeitzone um Stunden verschoben - die Mailzeiten stehen als UTC in der
|
||||
wäre je Zeitzone um Stunden verschoben - die Mailzeiten stehen als UTC in der
|
||||
Datenbank, anders als die Kalendertermine. `formatMailDate()` zeigt heute nur die
|
||||
Uhrzeit, dieses Jahr Tag und Monat, sonst mit Jahr.
|
||||
|
||||
### Kontexte
|
||||
|
||||
| Datei | Haelt | Eigenheit |
|
||||
| Datei | Hält | Eigenheit |
|
||||
| --- | --- | --- |
|
||||
| `auth.tsx` | Benutzer, `isAdmin`, `login`, `logout`, `updateUser` | prueft beim Start das gespeicherte Token gegen `/auth/me` |
|
||||
| `theme.tsx` | hell/dunkel/System | Wahl in `localStorage`, Doppelklick loest sie wieder |
|
||||
| `navigation.tsx` | `currentView`, `goTo(view, { tab, leadId })` | erspart es, Callbacks durch vier Ebenen zu reichen; `leadId` oeffnet den Kontakt direkt |
|
||||
| `whisper.tsx` | Modellstatus, Worker, Fortschritt, `transcribe` | ein Worker fuer die ganze App |
|
||||
| `auth.tsx` | Benutzer, `isAdmin`, `login`, `logout`, `updateUser` | prüft beim Start das gespeicherte Token gegen `/auth/me` |
|
||||
| `theme.tsx` | hell/dunkel/System | Wahl in `localStorage`, Doppelklick löst sie wieder |
|
||||
| `navigation.tsx` | `currentView`, `goTo(view, { tab, leadId })` | erspart es, Callbacks durch vier Ebenen zu reichen; `leadId` öffnet den Kontakt direkt |
|
||||
| `whisper.tsx` | Modellstatus, Worker, Fortschritt, `transcribe` | ein Worker für die ganze App |
|
||||
|
||||
### Whisper
|
||||
|
||||
| Datei | Inhalt |
|
||||
| --- | --- |
|
||||
| `whisper.tsx` | Konstanten (`WHISPER_MODEL`, `WHISPER_DTYPE`, `WHISPER_LANGUAGE`), Provider, Fristen, Haenger-Erkennung |
|
||||
| `whisper.worker.ts` | `WorkerRequest`/`WorkerResponse`, laedt und erkennt |
|
||||
| `whisperModels.ts` | `fetchModelSize()` holt die echte Groesse von Hugging Face |
|
||||
| `whisper.tsx` | Konstanten (`WHISPER_MODEL`, `WHISPER_DTYPE`, `WHISPER_LANGUAGE`), Provider, Fristen, Hänger-Erkennung |
|
||||
| `whisper.worker.ts` | `WorkerRequest`/`WorkerResponse`, lädt und erkennt |
|
||||
| `whisperModels.ts` | `fetchModelSize()` holt die echte Größe von Hugging Face |
|
||||
| `audio.ts` | `toWhisperAudio()` - Blob zu 16-kHz-Mono-PCM |
|
||||
| `dictation.tsx` | `useRecorder()` - Mikrofon, Dauer, Abbruch |
|
||||
|
||||
@@ -242,12 +242,12 @@ Warum genau diese Konstanten festgelegt sind, steht in
|
||||
| --- | --- |
|
||||
| `components/leads/leadUi.tsx` | `inputClass`, `labelClass`, `StatusBadge`, `Avatar`, Statusfarben |
|
||||
| `components/leads/mailPool.ts` | `findMailPoolMatch()`, `diffFields()`, `isConflict()`, `MAIL_POOL_FIELDS` - Dublettenabgleich des Vorbereichs |
|
||||
| `components/leads/MailPoolLeads.tsx` | Ansicht „Aus Mails": Goldmarkierung, Abweichungen, Uebernehmen, Sammeluebernahme |
|
||||
| `components/leads/MailPoolMergeDialog.tsx` | Feldauswahl beim Ergaenzen eines vorhandenen Kontakts |
|
||||
| `components/leads/MailPoolLeads.tsx` | Ansicht „Aus Mails": Goldmarkierung, Abweichungen, Übernehmen, Sammelübernahme |
|
||||
| `components/leads/MailPoolMergeDialog.tsx` | Feldauswahl beim Ergänzen eines vorhandenen Kontakts |
|
||||
| `components/scanner/scannerUi.tsx` | `cardClass`, Buttonklassen, `MatchBadge`, `MatchStatusBadge`, `PortalStatusPill`, `formatDateTime`, `projectTimestamp` |
|
||||
| `components/mail/mailUi.tsx` | `Badge`, `ConfidenceBar`, Herkunfts- und Statusbeschriftungen, `formatAddresses`, `avatarColor` |
|
||||
| `components/mail/MailBody.tsx` | Mailinhalt im `<iframe sandbox="">`, externe Bilder per CSP gesperrt |
|
||||
| `components/ui/Modal.tsx` | `Modal` (Escape schliesst) und `ConfirmDialog` |
|
||||
| `components/ui/Modal.tsx` | `Modal` (Escape schließt) und `ConfirmDialog` |
|
||||
| `components/Toast.tsx` | `useToast().addToast(text, kind)` mit `success`, `error` oder `info` |
|
||||
| `components/ui/DictationField.tsx` | Mikrofon-Knopf, Hinweisbox, Textfeld mit Diktat |
|
||||
| `lib/imageCompress.ts` | `compressImage()` auf 256 px, WebP → JPEG → PNG |
|
||||
@@ -263,7 +263,7 @@ nicht auseinanderlaufen:
|
||||
| `MAIL_LEAD_SOURCE`, `LeadOriginFilter` | Herkunft „Mail" in `Lead.source` und ihr Filter |
|
||||
| `USER_COLORS`, `userColor()` | Farbe gesetzt, sonst stabil aus der Benutzer-Id |
|
||||
| `opportunityVolume()`, `weightedVolume()`, `formatEuro()` | Tagessatz x Personentage, gewichtet, formatiert |
|
||||
| `OPPORTUNITY_STAGES`, `OPEN_STAGES`, `LEAD_STATUSES`, `CONNECTION_TYPES` | die Aufzaehlungen der Datenbank |
|
||||
| `OPPORTUNITY_STAGES`, `OPEN_STAGES`, `LEAD_STATUSES`, `CONNECTION_TYPES` | die Aufzählungen der Datenbank |
|
||||
| `eventColor()`, `leadFullName()`, `leadInitials()`, `contactFullName()` | Anzeigelogik |
|
||||
|
||||
## Datenfluss im Frontend
|
||||
@@ -278,16 +278,16 @@ flowchart TD
|
||||
F -->|"still, ohne Ladeanzeige"| C
|
||||
|
||||
G["Scanner-Ansichten"] -->|"eigener 30-s-Takt"| H["/api/scanner"]
|
||||
I["Mailansicht"] -->|"blaettert, sucht, filtert"| J["/api/mail"]
|
||||
I["Mailansicht"] -->|"blättert, sucht, filtert"| J["/api/mail"]
|
||||
D --> I
|
||||
I -->|"nach Uebernahme eines Vorschlags"| F
|
||||
I -->|"nach Übernahme eines Vorschlags"| F
|
||||
```
|
||||
|
||||
Die Mailansicht laedt ihre Mails selbst, bekommt Kontakte und Opportunities aber
|
||||
Die Mailansicht lädt ihre Mails selbst, bekommt Kontakte und Opportunities aber
|
||||
als Props - der Zuordnungsdialog braucht beide Listen zum Vorschlagen. Nach einer
|
||||
Kontaktuebernahme ruft sie `reload()`, damit der neue Kontakt in der Kontaktliste
|
||||
Kontaktübernahme ruft sie `reload()`, damit der neue Kontakt in der Kontaktliste
|
||||
der App auftaucht.
|
||||
|
||||
Die fuenf CRM-Endpunkte laufen **parallel**, nicht nacheinander - sonst addieren
|
||||
sich fuenf Rundreisen beim Anmelden. Die Scanner-Ansichten haengen bewusst nicht
|
||||
Die fünf CRM-Endpunkte laufen **parallel**, nicht nacheinander - sonst addieren
|
||||
sich fünf Rundreisen beim Anmelden. Die Scanner-Ansichten hängen bewusst nicht
|
||||
an `loadData`: andere Datenbank, andere Auffrischung.
|
||||
|
||||
+34
-34
@@ -1,64 +1,64 @@
|
||||
# 10 Glossar
|
||||
|
||||
Begriffe, die in der Oberflaeche, im Code und in den Tabellen vorkommen. Die
|
||||
Begriffe, die in der Oberfläche, im Code und in den Tabellen vorkommen. Die
|
||||
englischen Fachbegriffe sind bewusst nicht eingedeutscht - sie stehen so auch im
|
||||
Quelltext.
|
||||
|
||||
| Begriff | Bedeutung |
|
||||
| --- | --- |
|
||||
| **Agent** / Suchagent | Ein Beraterprofil im Scanner: Name, Lebenslauf als PDF, Suchbegriffe, Ausschluss-Begriffe. Tabelle `agents`. Nicht zu verwechseln mit einem Benutzerkonto. |
|
||||
| **Ausschluss-Begriff** | Wort, das ein Projekt fuer diesen Agenten sofort aussortiert - noch **vor** der KI-Bewertung, spart also Kosten. Typisch: „Arbeitnehmerueberlassung". |
|
||||
| **Benutzer** | Konto fuer die Anmeldung, Rolle `admin` oder `user`. Tabelle `crm_users`. |
|
||||
| **Boerse** / Portal | Projektplattform, die der Scanner durchsucht. Oeffentlich (Hays, Etengo) oder geschuetzt (Freelance.de, Freelancermap.de). Tabelle `portals`. |
|
||||
| **Ausschluss-Begriff** | Wort, das ein Projekt für diesen Agenten sofort aussortiert - noch **vor** der KI-Bewertung, spart also Kosten. Typisch: „Arbeitnehmerüberlassung". |
|
||||
| **Benutzer** | Konto für die Anmeldung, Rolle `admin` oder `user`. Tabelle `crm_users`. |
|
||||
| **Börse** / Portal | Projektplattform, die der Scanner durchsucht. Öffentlich (Hays, Etengo) oder geschützt (Freelance.de, Freelancermap.de). Tabelle `portals`. |
|
||||
| **App-only** | Zugriff auf Microsoft 365 im Namen der Anwendung, nicht eines Benutzers (OAuth-Ablauf *Client Credentials*). Braucht Anwendungsberechtigungen und Administratorzustimmung, aber keinen Login. |
|
||||
| **Cookie-Import** | Uebertragen einer angemeldeten Browsersitzung auf den Scraper, per JSON-Export der Erweiterung *Cookie-Editor*. Landet als `session_state` am Portal. |
|
||||
| **Delta Query** | Graph-Abfrage, die beim naechsten Aufruf nur noch die Aenderungen liefert. Der Fortschritt steckt in einem Verweis (`delta_link`), der in `crm_mail_sync_state` liegt. |
|
||||
| **Kontakt** / Lead | Person im Netzwerk: Firma, Position, Status, Verbindungen. Tabelle `crm_leads` mit `mail_pool = 0`. Im Code durchgaengig `lead`. |
|
||||
| **Aus Mails** / Vorbereich | Zeilen in `crm_leads` mit `mail_pool = 1`: Personen, die der Postfachabgleich gefunden hat. Eigener Menuepunkt unter *Kontakte*, taucht in keiner Auswahlliste auf. Erst das Uebernehmen macht daraus einen Kontakt. |
|
||||
| **Cookie-Import** | Übertragen einer angemeldeten Browsersitzung auf den Scraper, per JSON-Export der Erweiterung *Cookie-Editor*. Landet als `session_state` am Portal. |
|
||||
| **Delta Query** | Graph-Abfrage, die beim nächsten Aufruf nur noch die Änderungen liefert. Der Fortschritt steckt in einem Verweis (`delta_link`), der in `crm_mail_sync_state` liegt. |
|
||||
| **Kontakt** / Lead | Person im Netzwerk: Firma, Position, Status, Verbindungen. Tabelle `crm_leads` mit `mail_pool = 0`. Im Code durchgängig `lead`. |
|
||||
| **Aus Mails** / Vorbereich | Zeilen in `crm_leads` mit `mail_pool = 1`: Personen, die der Postfachabgleich gefunden hat. Eigener Menüpunkt unter *Kontakte*, taucht in keiner Auswahlliste auf. Erst das Übernehmen macht daraus einen Kontakt. |
|
||||
| **Dublette** (Aus Mails) | Ein Fund, zu dem es in den Kontakten schon eine Person gibt - erkannt an der E-Mail-Adresse, sonst am vollen Namen. In der Liste **gold** markiert, abweichende Felder hervorgehoben. |
|
||||
| **Kontakt-Vorschlag** | Aus einer Mail gelesene Person, die noch kein Kontakt ist. Tabelle `crm_mail_contact_candidates`. Wird uebernommen, ergaenzt oder verworfen. |
|
||||
| **Match** / Treffer | Die Bewertung **eines** Projekts fuer **einen** Agenten: Prozentwert, Pro-Liste, Contra-Liste, Status. Tabelle `matches`. Ein Projekt erzeugt so viele Treffer, wie es aktive Agenten gibt. |
|
||||
| **Opportunity** | Geschaeft in Arbeit: Phase, Tagessatz, Laufzeit, Wahrscheinlichkeit, Ansprechpartner. Tabelle `crm_opportunities`. |
|
||||
| **Kontakt-Vorschlag** | Aus einer Mail gelesene Person, die noch kein Kontakt ist. Tabelle `crm_mail_contact_candidates`. Wird übernommen, ergänzt oder verworfen. |
|
||||
| **Match** / Treffer | Die Bewertung **eines** Projekts für **einen** Agenten: Prozentwert, Pro-Liste, Contra-Liste, Status. Tabelle `matches`. Ein Projekt erzeugt so viele Treffer, wie es aktive Agenten gibt. |
|
||||
| **Opportunity** | Geschäft in Arbeit: Phase, Tagessatz, Laufzeit, Wahrscheinlichkeit, Ansprechpartner. Tabelle `crm_opportunities`. |
|
||||
| **Phase** / Stage | Fortschritt einer Opportunity: Neu, Qualifizierung, Angebot, Verhandlung, Gewonnen, Verloren. |
|
||||
| **Pipeline** | Zwei Bedeutungen, je nach Zusammenhang: die Kanban-Ansicht der Opportunities **oder** der Suchlauf des Scanners (`services/pipeline.js`). |
|
||||
| **Projekt** | Ein von einer Boerse gescrapter Auftrag. Tabelle `projects`, Aufbewahrung 7 Tage. |
|
||||
| **Scraper** | Der Teil, der mit einem echten Browser (Playwright/Chromium) die Suchseiten der Boersen abruft. |
|
||||
| **Session** (Portal) | Gespeicherte Cookies einer angemeldeten Boersen-Sitzung, im Playwright-Format `storageState`. |
|
||||
| **Sitzung** (CRM) | Die eigene Anmeldung, als JWT im `localStorage`, 12 Stunden gueltig. |
|
||||
| **Suchintervall** | Abstand zwischen zwei automatischen Suchlaeufen, in Minuten. Einstellung `search_interval`. |
|
||||
| **Projekt** | Ein von einer Börse gescrapter Auftrag. Tabelle `projects`, Aufbewahrung 7 Tage. |
|
||||
| **Scraper** | Der Teil, der mit einem echten Browser (Playwright/Chromium) die Suchseiten der Börsen abruft. |
|
||||
| **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. |
|
||||
| **Suchintervall** | Abstand zwischen zwei automatischen Suchläufen, in Minuten. Einstellung `search_interval`. |
|
||||
| **Schwellenwert Anzeige** | `display_threshold` - Treffer darunter bleiben im Dashboard ausgeblendet. |
|
||||
| **Schwellenwert Benachrichtigung** | `match_threshold` - ab hier gibt es Signalton und E-Mail. |
|
||||
| **Verbindung** | Kante zwischen zwei Kontakten im Netzwerkdiagramm, mit Art und Staerke. Tabelle `crm_lead_connections`. |
|
||||
| **Volumen** | Tagessatz x Personentage. **Gewichtet** heisst zusaetzlich mit der Abschlusswahrscheinlichkeit multipliziert. |
|
||||
| **Whisper** | Das Spracherkennungsmodell fuer das Diktat. Laeuft im Browser, nicht auf dem Server. |
|
||||
| **Zugriffsrichtlinie** | `ApplicationAccessPolicy` in Exchange Online. Begrenzt eine App-only-Berechtigung auf einzelne Postfaecher - ohne sie gilt sie tenant-weit. |
|
||||
| **Zustaendiger** / Owner | Benutzer, dem ein Kontakt oder eine Opportunity zugeordnet ist. Darf sie aendern, auch wenn er sie nicht angelegt hat. |
|
||||
| **Verbindung** | Kante zwischen zwei Kontakten im Netzwerkdiagramm, mit Art und Stärke. Tabelle `crm_lead_connections`. |
|
||||
| **Volumen** | Tagessatz x Personentage. **Gewichtet** heißt zusätzlich mit der Abschlusswahrscheinlichkeit multipliziert. |
|
||||
| **Whisper** | Das Spracherkennungsmodell für das Diktat. Läuft im Browser, nicht auf dem Server. |
|
||||
| **Zugriffsrichtlinie** | `ApplicationAccessPolicy` in Exchange Online. Begrenzt eine App-only-Berechtigung auf einzelne Postfächer - ohne sie gilt sie tenant-weit. |
|
||||
| **Zuständiger** / Owner | Benutzer, dem ein Kontakt oder eine Opportunity zugeordnet ist. Darf sie ändern, auch wenn er sie nicht angelegt hat. |
|
||||
| **Zuversicht** / Confidence | Wert von 0 bis 100 an einem Kontakt-Vorschlag. Ab 70 und mit echter Mailadresse landet er automatisch im Vorbereich *Aus Mails*. |
|
||||
|
||||
## Herkunft eines Kontakt-Vorschlags
|
||||
|
||||
| Wert in der DB | in der Oberflaeche | Zuversicht |
|
||||
| Wert in der DB | in der Oberfläche | Zuversicht |
|
||||
| --- | --- | --- |
|
||||
| `absender` | Absender | 90, mit bestaetigender Signatur 95 |
|
||||
| `empfaenger` | Empfaenger | 78 |
|
||||
| `absender` | Absender | 90, mit bestätigender Signatur 95 |
|
||||
| `empfaenger` | Empfänger | 78 |
|
||||
| `signatur` | Signatur | 55 - weitere Adresse in der Signatur, etwa eine Assistenz |
|
||||
| `text` | Im Text genannt | 35 - nur mit Anrede und Rufnummer, nie automatisch uebernommen |
|
||||
| `text` | Im Text genannt | 35 - nur mit Anrede und Rufnummer, nie automatisch übernommen |
|
||||
|
||||
## Status eines Kontakt-Vorschlags
|
||||
|
||||
| Wert | in der Oberflaeche | Bedeutung |
|
||||
| Wert | in der Oberfläche | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `offen` | Offen | wartet auf eine Entscheidung |
|
||||
| `uebernommen` | Uebernommen | es entstand ein Eintrag - im Automatiklauf in *Aus Mails*, von Hand ein Kontakt |
|
||||
| `dublette` | Ergaenzt | die Adresse war im Zielbestand vorhanden, der Eintrag wurde erweitert |
|
||||
| `uebernommen` | Übernommen | es entstand ein Eintrag - im Automatiklauf in *Aus Mails*, von Hand ein Kontakt |
|
||||
| `dublette` | Ergänzt | die Adresse war im Zielbestand vorhanden, der Eintrag wurde erweitert |
|
||||
| `verworfen` | Verworfen | von Hand abgelehnt |
|
||||
|
||||
## Statuswerte im Scanner
|
||||
|
||||
| Wert in der DB | in der Oberflaeche | Bedeutung |
|
||||
| Wert in der DB | in der Oberfläche | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `new` | Neu | noch nicht angesehen |
|
||||
| `reviewed` | Gesehen | geprueft, noch keine Bewerbung |
|
||||
| `reviewed` | Gesehen | geprüft, noch keine Bewerbung |
|
||||
| `applied` | Beworben | Bewerbung ist raus |
|
||||
| `rejected` | Abgelehnt | passt nicht; erscheint nur unter *Unpassend* |
|
||||
|
||||
@@ -66,7 +66,7 @@ Quelltext.
|
||||
|
||||
| Wert | Bedeutung |
|
||||
| --- | --- |
|
||||
| `connected` | Verbindung steht, letzte Pruefung erfolgreich |
|
||||
| `expired` | Session abgelaufen oder CAPTCHA - neuer Cookie-Import faellig |
|
||||
| `error` | Portal nicht erreichbar oder Seitenaufbau geaendert |
|
||||
| `disconnected` | nie verbunden; bei oeffentlichen Portalen heisst das nur „bereit" |
|
||||
| `connected` | Verbindung steht, letzte Prüfung erfolgreich |
|
||||
| `expired` | Session abgelaufen oder CAPTCHA - neuer Cookie-Import fällig |
|
||||
| `error` | Portal nicht erreichbar oder Seitenaufbau geändert |
|
||||
| `disconnected` | nie verbunden; bei öffentlichen Portalen heißt das nur „bereit" |
|
||||
|
||||
+93
-93
@@ -1,15 +1,15 @@
|
||||
# 11 Postfachanbindung (Microsoft 365)
|
||||
|
||||
Das gemeinsame Vertriebspostfach `crm@perinco.de` wird **lesend** in das CRM
|
||||
gespiegelt. Aus jeder Mail entstehen Kontakt-Vorschlaege, und jede Mail laesst
|
||||
sich an eine Opportunity und an Kontakte haengen.
|
||||
gespiegelt. Aus jeder Mail entstehen Kontakt-Vorschläge, und jede Mail lässt
|
||||
sich an eine Opportunity und an Kontakte hängen.
|
||||
|
||||
Vorbild ist die gleiche Funktion im Altsystem KeepTrack. Uebernommen wurde die
|
||||
Vorbild ist die gleiche Funktion im Altsystem KeepTrack. Übernommen wurde die
|
||||
Idee, nicht die Umsetzung - die Unterschiede stehen unten unter
|
||||
[Was anders ist als im Altsystem](#was-anders-ist-als-im-altsystem) und sind der
|
||||
Grund fuer mehrere Entwurfsentscheidungen.
|
||||
Grund für mehrere Entwurfsentscheidungen.
|
||||
|
||||
## Ueberblick
|
||||
## Überblick
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -25,7 +25,7 @@ flowchart LR
|
||||
SY["mail/sync.js<br/>Delta Query"]
|
||||
EX["mail/extract.js<br/>Kontakte lesen"]
|
||||
ST["mail/store.js"]
|
||||
CO["mail/contacts.js<br/>Uebernahme"]
|
||||
CO["mail/contacts.js<br/>Übernahme"]
|
||||
API["routes/mail.js<br/>/api/mail"]
|
||||
end
|
||||
|
||||
@@ -57,7 +57,7 @@ Login.
|
||||
2. **Client-Geheimnis** anlegen und den Wert sofort kopieren.
|
||||
3. **API-Berechtigung** Microsoft Graph → *Anwendungsberechtigungen* →
|
||||
`Mail.Read`, danach Administratorzustimmung erteilen.
|
||||
`Mail.ReadBasic` genuegt nicht, es liefert keinen Mailtext.
|
||||
`Mail.ReadBasic` genügt nicht, es liefert keinen Mailtext.
|
||||
4. **Zugriff auf dieses eine Postfach begrenzen.** Anwendungsberechtigungen
|
||||
gelten tenant-weit; ohne Richtlinie darf die App jedes Postfach lesen:
|
||||
|
||||
@@ -73,8 +73,8 @@ Login.
|
||||
Test-ApplicationAccessPolicy -Identity sascha.wolf@perinco.de -AppId <APP-ID> # Denied
|
||||
```
|
||||
|
||||
`Add-MailboxPermission` ist hier **wirkungslos** - das ist der Weg fuer
|
||||
delegierten Zugriff bzw. EWS-Impersonation. Bei App-only zaehlt allein die
|
||||
`Add-MailboxPermission` ist hier **wirkungslos** - das ist der Weg für
|
||||
delegierten Zugriff bzw. EWS-Impersonation. Bei App-only zählt allein die
|
||||
Zugriffsrichtlinie.
|
||||
|
||||
Der Server muss ausgehend `login.microsoftonline.com` und `graph.microsoft.com`
|
||||
@@ -82,8 +82,8 @@ auf 443 erreichen.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
Zugangsdaten stehen ausschliesslich in `server/.env` - nie in der Datenbank und
|
||||
nie in einer API-Antwort. Ein Datenbank-Dump enthaelt damit keine Geheimnisse.
|
||||
Zugangsdaten stehen ausschließlich in `server/.env` - nie in der Datenbank und
|
||||
nie in einer API-Antwort. Ein Datenbank-Dump enthält damit keine Geheimnisse.
|
||||
|
||||
| Variable | Pflicht | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
@@ -143,32 +143,32 @@ sequenceDiagram
|
||||
Wesentliche Eigenschaften:
|
||||
|
||||
* **Kein Schreibzugriff.** Der Fortschritt steht im `delta_link` in der
|
||||
Datenbank, nicht im `isRead`-Kennzeichen des Postfachs. Deshalb genuegt
|
||||
Datenbank, nicht im `isRead`-Kennzeichen des Postfachs. Deshalb genügt
|
||||
`Mail.Read`, und deshalb geht keine Mail verloren, wenn ein Mensch sie vorher
|
||||
in Outlook oeffnet.
|
||||
in Outlook öffnet.
|
||||
* **Fortsetzbar.** Nach jeder Seite wird der Verweis gesichert. Ein Absturz oder
|
||||
ein Deployment mitten im Erstabgleich kostet hoechstens eine Seite.
|
||||
ein Deployment mitten im Erstabgleich kostet höchstens eine Seite.
|
||||
* **Gedeckelt.** `MAIL_MAX_PAGES` begrenzt einen Lauf. Ist danach noch etwas
|
||||
offen, setzt der naechste Lauf nach 15 Sekunden fort (`has_more`), statt das
|
||||
offen, setzt der nächste Lauf nach 15 Sekunden fort (`has_more`), statt das
|
||||
normale Intervall abzuwarten.
|
||||
* **Selbstheilend.** Antwortet Graph mit `410 Gone` bzw. `resyncRequired`, wird
|
||||
der Verweis verworfen und beim naechsten Lauf neu aufgebaut. Vorhandene Mails
|
||||
der Verweis verworfen und beim nächsten Lauf neu aufgebaut. Vorhandene Mails
|
||||
werden am SHA-256 der Graph-ID wiedererkannt, es entstehen keine Doppelten.
|
||||
* **Nur Metadaten in der Delta-Abfrage.** Der Mailtext wird einzeln nachgeholt
|
||||
und nur, wenn er fehlt. Ein reines Metadaten-Update (etwa ein geaendertes
|
||||
Gelesen-Kennzeichen) loescht den gespeicherten Text nicht.
|
||||
und nur, wenn er fehlt. Ein reines Metadaten-Update (etwa ein geändertes
|
||||
Gelesen-Kennzeichen) löscht den gespeicherten Text nicht.
|
||||
* **Zeitgrenze doppelt.** Der Erstabgleich versucht
|
||||
`$filter=receivedDateTime ge ...`; die Filterunterstuetzung in Delta-Abfragen
|
||||
ist eng begrenzt, bei einem 400 laeuft er ohne Filter weiter. Der Filter ist
|
||||
deshalb nur eine Ersparnis - geprueft wird die Grenze **immer** beim Speichern,
|
||||
und zwar nur fuer noch unbekannte Mails. Ein an den ersten Lauf gebundener
|
||||
Merker waere hier falsch: ein ungefilterter Erstabgleich zieht sich ueber
|
||||
mehrere Laeufe, und in den folgenden kaeme der Rest des Postfachs trotz Grenze
|
||||
`$filter=receivedDateTime ge ...`; die Filterunterstützung in Delta-Abfragen
|
||||
ist eng begrenzt, bei einem 400 läuft er ohne Filter weiter. Der Filter ist
|
||||
deshalb nur eine Ersparnis - geprüft wird die Grenze **immer** beim Speichern,
|
||||
und zwar nur für noch unbekannte Mails. Ein an den ersten Lauf gebundener
|
||||
Merker wäre hier falsch: ein ungefilterter Erstabgleich zieht sich über
|
||||
mehrere Läufe, und in den folgenden käme der Rest des Postfachs trotz Grenze
|
||||
herein.
|
||||
|
||||
Im Postfach geloeschte Mails werden mit `is_deleted = 1` gekennzeichnet, nicht
|
||||
entfernt: eine an eine Opportunity gehaengte Mail soll nicht aus dem CRM
|
||||
verschwinden, weil jemand sein Postfach aufraeumt.
|
||||
Im Postfach gelöschte Mails werden mit `is_deleted = 1` gekennzeichnet, nicht
|
||||
entfernt: eine an eine Opportunity gehängte Mail soll nicht aus dem CRM
|
||||
verschwinden, weil jemand sein Postfach aufräumt.
|
||||
|
||||
## Kontakte aus Mails
|
||||
|
||||
@@ -178,7 +178,7 @@ nicht.
|
||||
```mermaid
|
||||
flowchart TD
|
||||
M["Mail"] --> A["Absender<br/>from (Graph)"]
|
||||
M --> E["Empfaenger<br/>to, cc, bcc (Graph)"]
|
||||
M --> E["Empfänger<br/>to, cc, bcc (Graph)"]
|
||||
M --> S["Signaturblock<br/>Text nach der Grussformel"]
|
||||
M --> T["Fliesstext<br/>Anrede + Name + Rufnummer"]
|
||||
|
||||
@@ -193,26 +193,26 @@ flowchart TD
|
||||
|
||||
C --> Z{"E-Mail vorhanden<br/>und Zuversicht ≥ 70?"}
|
||||
Z -->|ja, Automatik an| N["Eintrag in 'Aus Mails'<br/>mail_pool = 1"]
|
||||
Z -->|"Adresse dort schon"| G["Eintrag ergaenzen"]
|
||||
Z -->|"Adresse dort schon"| G["Eintrag ergänzen"]
|
||||
Z -->|nein| R["bleibt zur Durchsicht"]
|
||||
```
|
||||
|
||||
Zuversicht je Herkunft: Absender 90 (95, wenn die Signatur die Adresse
|
||||
bestaetigt), Empfaenger 78, weitere Adresse aus der Signatur 55, im Text genannte
|
||||
bestätigt), Empfänger 78, weitere Adresse aus der Signatur 55, im Text genannte
|
||||
Person 35.
|
||||
|
||||
Aus dem Signaturblock kommen Telefon und Mobil (nach Beschriftung
|
||||
unterschieden), Firma (an der Rechtsform erkannt), Position, Strasse, PLZ, Ort,
|
||||
unterschieden), Firma (an der Rechtsform erkannt), Position, Straße, PLZ, Ort,
|
||||
Website und LinkedIn-Profil.
|
||||
|
||||
Zwei Entscheidungen, die die Trefferqualitaet tragen:
|
||||
Zwei Entscheidungen, die die Trefferqualität tragen:
|
||||
|
||||
* **Im Fliesstext ist eine Anrede Pflicht** („Herrn Max Mustermann Tel. …").
|
||||
Ohne diese Bedingung liest der Musterabgleich jede Folge grossgeschriebener
|
||||
Woerter als Namen; „Zweiter Ansprechpartner" landete so als Kontakt. Ein
|
||||
Ohne diese Bedingung liest der Musterabgleich jede Folge großgeschriebener
|
||||
Wörter als Namen; „Zweiter Ansprechpartner" landete so als Kontakt. Ein
|
||||
verpasster Treffer ist harmloser als ein falscher - eine Durchsichtsliste voll
|
||||
Unsinn schaut niemand an.
|
||||
* **Der Anzeigename schlaegt die Adresse.** Bei „Mueller" plus
|
||||
* **Der Anzeigename schlägt die Adresse.** Bei „Müller" plus
|
||||
`k.mueller@…` wird der Vorname aus der Adresse genommen, der Nachname aber aus
|
||||
dem Anzeigenamen - dort stehen die Umlaute richtig.
|
||||
|
||||
@@ -221,64 +221,64 @@ Zwei Entscheidungen, die die Trefferqualitaet tragen:
|
||||
Der Hintergrundlauf schreibt **nicht** in den Kontaktbestand. Was er findet,
|
||||
landet in einem Vorbereich: `crm_leads.mail_pool = 1`. Die Kontaktansicht zeigt
|
||||
nur `mail_pool = 0`, der Vorbereich steht als Unterpunkt *Aus Mails* unter dem
|
||||
Menuepunkt *Kontakte*.
|
||||
Menüpunkt *Kontakte*.
|
||||
|
||||
Der Grund ist Pflegbarkeit: die Kontaktliste ist eine gepflegte Liste, an der
|
||||
Opportunities, Kalender und Netzwerk haengen. Ein Abgleich, der ungefragt jede
|
||||
Opportunities, Kalender und Netzwerk hängen. Ein Abgleich, der ungefragt jede
|
||||
Adresse aus jeder Mail hineinschreibt, macht sie unbrauchbar - genau daran ist
|
||||
das Altsystem gescheitert. Deshalb sind Funde aus Mails Rohmaterial, bis jemand
|
||||
entscheidet.
|
||||
|
||||
Eine Spalte statt einer zweiten Tabelle, weil `crm_mail_links` und
|
||||
`crm_mail_contact_candidates` per Fremdschluessel auf `crm_leads` zeigen: der
|
||||
Vorbereich behaelt damit seine Mailbezuege, ohne dass es zwei Wege gibt, eine
|
||||
`crm_mail_contact_candidates` per Fremdschlüssel auf `crm_leads` zeigen: der
|
||||
Vorbereich behält damit seine Mailbezüge, ohne dass es zwei Wege gibt, eine
|
||||
Person mit einer Mail zu verbinden.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P["Aus Mails<br/>mail_pool = 1"] --> D{"Person schon<br/>in den Kontakten?"}
|
||||
D -->|nein| NEU["In Kontakte uebernehmen<br/>Kopie mit mail_pool = 0"]
|
||||
D -->|nein| NEU["In Kontakte übernehmen<br/>Kopie mit mail_pool = 0"]
|
||||
D -->|"ja - Adresse oder Name"| GOLD["gold markiert<br/>Abweichungen hervorgehoben"]
|
||||
GOLD --> M["Felder einzeln in den<br/>vorhandenen Kontakt uebernehmen"]
|
||||
GOLD --> M["Felder einzeln in den<br/>vorhandenen Kontakt übernehmen"]
|
||||
GOLD --> N2["trotzdem neu anlegen"]
|
||||
P --> W["entfernen"]
|
||||
```
|
||||
|
||||
Der Abgleich gegen die Kontakte laeuft im Browser (`mailPool.ts`): erst ueber die
|
||||
E-Mail-Adresse, sonst ueber den vollen Namen. Telefonnummern werden dabei ueber
|
||||
Der Abgleich gegen die Kontakte läuft im Browser (`mailPool.ts`): erst über die
|
||||
E-Mail-Adresse, sonst über den vollen Namen. Telefonnummern werden dabei über
|
||||
ihre Ziffern verglichen, damit „0221 123-4" und „02211234" nicht als Abweichung
|
||||
erscheinen.
|
||||
|
||||
Beim Uebernehmen sind nur die **Luecken** vorausgewaehlt - Felder, in denen der
|
||||
Kontakt noch nichts hat. Werte, die einen gepflegten Eintrag ersetzen wuerden,
|
||||
sind als *ueberschreibt* gekennzeichnet und muessen bewusst angehakt werden: der
|
||||
Beim Übernehmen sind nur die **Lücken** vorausgewählt - Felder, in denen der
|
||||
Kontakt noch nichts hat. Werte, die einen gepflegten Eintrag ersetzen würden,
|
||||
sind als *überschreibt* gekennzeichnet und müssen bewusst angehakt werden: der
|
||||
Mailfund kann aus einer alten Signatur stammen.
|
||||
|
||||
Uebernommene Kontakte tragen weiterhin `crm_leads.source = 'Mail'` - die
|
||||
Kontaktliste hat dafuer den Herkunftsfilter *Aus Mails uebernommen*.
|
||||
Übernommene Kontakte tragen weiterhin `crm_leads.source = 'Mail'` - die
|
||||
Kontaktliste hat dafür den Herkunftsfilter *Aus Mails übernommen*.
|
||||
|
||||
Bestandsumstellung: `db.js` verschiebt beim ersten Start alle vorhandenen
|
||||
Kontakte mit `source = 'Mail'` in den Vorbereich. Das laeuft genau einmal,
|
||||
Kontakte mit `source = 'Mail'` in den Vorbereich. Das läuft genau einmal,
|
||||
festgehalten im Merker `leads_mail_pool_migrated` in `crm_settings` - sonst
|
||||
wanderten uebernommene Kontakte bei jedem Neustart zurueck.
|
||||
wanderten übernommene Kontakte bei jedem Neustart zurück.
|
||||
|
||||
## Anzeige des Mailtexts
|
||||
|
||||
Fremdes HTML laeuft in einem `<iframe sandbox="">` **ohne jedes allow-Flag**. Das
|
||||
Fremdes HTML läuft in einem `<iframe sandbox="">` **ohne jedes allow-Flag**. Das
|
||||
Dokument bekommt eine eigene, undurchsichtige Origin und kann weder Skripte
|
||||
ausfuehren noch auf die Anwendung zugreifen.
|
||||
ausführen noch auf die Anwendung zugreifen.
|
||||
|
||||
`allow-same-origin` waere hier ein Fehler: ein `srcDoc`-Dokument erbt damit die
|
||||
Origin der Anwendung und koennte `localStorage` lesen - also das Anmeldetoken.
|
||||
Weil die Hoehe des Inhalts ohne dieses Flag nicht auslesbar ist, hat der Rahmen
|
||||
eine feste Hoehe mit eigenem Rollbalken und einen Schalter „Groesser".
|
||||
`allow-same-origin` wäre hier ein Fehler: ein `srcDoc`-Dokument erbt damit die
|
||||
Origin der Anwendung und könnte `localStorage` lesen - also das Anmeldetoken.
|
||||
Weil die Höhe des Inhalts ohne dieses Flag nicht auslesbar ist, hat der Rahmen
|
||||
eine feste Höhe mit eigenem Rollbalken und einen Schalter „Größer".
|
||||
|
||||
Zusaetzlich entfernt das Backend beim Speichern Skripte, eingebettete Rahmen,
|
||||
Formulare, Ereignis-Attribute und gefaehrliche URL-Schemata
|
||||
Zusätzlich entfernt das Backend beim Speichern Skripte, eingebettete Rahmen,
|
||||
Formulare, Ereignis-Attribute und gefährliche URL-Schemata
|
||||
(`mail/html.js`, `sanitizeHtml`).
|
||||
|
||||
Externe Bilder sind per Content-Security-Policy im eingebetteten Dokument
|
||||
gesperrt und lassen sich pro Mail freigeben - andernfalls meldet jedes Oeffnen
|
||||
gesperrt und lassen sich pro Mail freigeben - andernfalls meldet jedes Öffnen
|
||||
dem Absender, dass die Mail gelesen wurde.
|
||||
|
||||
## Endpunkte
|
||||
@@ -287,79 +287,79 @@ Alle unter `/api/mail`, alle hinter `requireAuth`.
|
||||
|
||||
| Methode | Pfad | Rolle | Zweck |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/status` | alle | Betriebszustand, Zaehler, letzter Fehler |
|
||||
| GET | `/status` | alle | Betriebszustand, Zähler, letzter Fehler |
|
||||
| GET | `/messages` | alle | Liste; `search`, `filter`, `limit`, `offset` |
|
||||
| GET | `/messages/:id` | alle | Mail mit Text, Zuordnungen, Vorschlaegen |
|
||||
| POST | `/messages/:id/links` | alle | an Kontakt und/oder Opportunity haengen |
|
||||
| DELETE | `/links/:id` | alle | Zuordnung loesen |
|
||||
| GET | `/candidates` | alle | Kontakt-Vorschlaege, `status` waehlbar |
|
||||
| POST | `/candidates/:id/adopt` | alle | Kontakt anlegen bzw. ergaenzen; `target: 'pool'` legt in „Aus Mails" ab |
|
||||
| GET | `/messages/:id` | alle | Mail mit Text, Zuordnungen, Vorschlägen |
|
||||
| POST | `/messages/:id/links` | alle | an Kontakt und/oder Opportunity hängen |
|
||||
| DELETE | `/links/:id` | alle | Zuordnung lösen |
|
||||
| GET | `/candidates` | alle | Kontakt-Vorschläge, `status` wählbar |
|
||||
| POST | `/candidates/:id/adopt` | alle | Kontakt anlegen bzw. ergänzen; `target: 'pool'` legt in „Aus Mails" ab |
|
||||
| POST | `/candidates/:id/reject` | alle | Vorschlag verwerfen |
|
||||
|
||||
Der Vorbereich haengt an den Kontakten und hat seine Endpunkte deshalb unter
|
||||
Der Vorbereich hängt an den Kontakten und hat seine Endpunkte deshalb unter
|
||||
`/api/leads`:
|
||||
|
||||
| Methode | Pfad | Zweck |
|
||||
| --- | --- | --- |
|
||||
| GET | `/leads/mail-pool/sources` | Herkunft je Eintrag: Mailanzahl, letzter Betreff, Zuversicht |
|
||||
| POST | `/leads/mail-pool/promote` | mehrere Eintraege als neue Kontakte anlegen |
|
||||
| POST | `/leads/:id/promote` | einen Eintrag uebernehmen; mit `target_lead_id` und `fields` in einen vorhandenen Kontakt |
|
||||
| POST | `/leads/mail-pool/promote` | mehrere Einträge als neue Kontakte anlegen |
|
||||
| POST | `/leads/:id/promote` | einen Eintrag übernehmen; mit `target_lead_id` und `fields` in einen vorhandenen Kontakt |
|
||||
|
||||
`keep_pool: true` behaelt den Eintrag in „Aus Mails"; ohne das verschwindet er
|
||||
dort, sobald er als Kontakt gefuehrt wird. In beiden Faellen wandern die
|
||||
Mailverknuepfungen mit an den Zielkontakt.
|
||||
`keep_pool: true` behält den Eintrag in „Aus Mails"; ohne das verschwindet er
|
||||
dort, sobald er als Kontakt geführt wird. In beiden Fällen wandern die
|
||||
Mailverknüpfungen mit an den Zielkontakt.
|
||||
| GET | `/settings` | admin | verstellbare Einstellungen |
|
||||
| POST | `/settings` | admin | Einstellungen speichern |
|
||||
| POST | `/test` | admin | Anmeldung **und** Postfachzugriff pruefen |
|
||||
| POST | `/sync` | admin | Abgleich anstossen, `{"reset":true}` von vorn |
|
||||
| POST | `/test` | admin | Anmeldung **und** Postfachzugriff prüfen |
|
||||
| POST | `/sync` | admin | Abgleich anstoßen, `{"reset":true}` von vorn |
|
||||
|
||||
Lesen ist fuer jeden Angemeldeten frei - `crm@perinco.de` ist ein gemeinsames
|
||||
Lesen ist für jeden Angemeldeten frei - `crm@perinco.de` ist ein gemeinsames
|
||||
Vertriebspostfach. Konfigurieren, Testen und Anstossen sind Administratoren
|
||||
vorbehalten.
|
||||
|
||||
`filter` in der Liste: `unlinked` (noch nirgends zugeordnet - die Arbeitsliste),
|
||||
`candidates` (offene Kontakt-Vorschlaege), `linked` (bereits zugeordnet).
|
||||
`candidates` (offene Kontakt-Vorschläge), `linked` (bereits zugeordnet).
|
||||
|
||||
## Was anders ist als im Altsystem
|
||||
|
||||
| Thema | KeepTrack | perinco CRM |
|
||||
| --- | --- | --- |
|
||||
| Fortschritt | `$filter=isRead eq false`, danach `PATCH isRead` | Delta Query mit gespeichertem `delta_link` |
|
||||
| Berechtigung | Schreibzugriff noetig | `Mail.Read` genuegt |
|
||||
| Der Haken daran | `PATCH` ging auf `/me/…` - unter App-only gibt es kein `/me`. Der Aufruf lief in einen 400, die Antwort wurde nicht geprueft. Mails blieben ungelesen und wurden bei **jedem** Lauf erneut durch das Sprachmodell geschickt. | entfaellt |
|
||||
| Berechtigung | Schreibzugriff nötig | `Mail.Read` genügt |
|
||||
| Der Haken daran | `PATCH` ging auf `/me/…` - unter App-only gibt es kein `/me`. Der Aufruf lief in einen 400, die Antwort wurde nicht geprüft. Mails blieben ungelesen und wurden bei **jedem** Lauf erneut durch das Sprachmodell geschickt. | entfällt |
|
||||
| Tenant-/Client-ID | kamen vom Browser mit; jeder Angemeldete konnte das Server-Secret gegen einen fremden Tenant verwenden | serverseitig aus der `.env`, nie im Browser |
|
||||
| Kontakte ohne Adresse | Platzhalter `name@kontakt-aus-email.local` erfunden, um eine Datenbankbedingung zu erfuellen | Kontakt ohne Adresse; Vorschlag bleibt zur Durchsicht liegen |
|
||||
| Vorhandener Kontakt | wurde uebersprungen | leere Felder werden ergaenzt, gepflegte bleiben |
|
||||
| Kontaktdaten lesen | kompletter Mailtext an OpenAI, auch fuer Daten, die Graph strukturiert liefert | Graph-Felder plus Signaturauswertung, kein externer Dienst |
|
||||
| Mail am Deal | Text in das Freitextfeld `notizen` vorangestellt | eigene Tabelle `crm_mail_links`, loesbar, mehrfach moeglich |
|
||||
| Verbindungstest | prueft nur, ob ein Token kommt | prueft Token **und** Postfachzugriff, mit Hinweis je Fehlercode |
|
||||
| Kontakte ohne Adresse | Platzhalter `name@kontakt-aus-email.local` erfunden, um eine Datenbankbedingung zu erfüllen | Kontakt ohne Adresse; Vorschlag bleibt zur Durchsicht liegen |
|
||||
| Vorhandener Kontakt | wurde übersprungen | leere Felder werden ergänzt, gepflegte bleiben |
|
||||
| Kontaktdaten lesen | kompletter Mailtext an OpenAI, auch für Daten, die Graph strukturiert liefert | Graph-Felder plus Signaturauswertung, kein externer Dienst |
|
||||
| Mail am Deal | Text in das Freitextfeld `notizen` vorangestellt | eigene Tabelle `crm_mail_links`, lösbar, mehrfach möglich |
|
||||
| Verbindungstest | prüft nur, ob ein Token kommt | prüft Token **und** Postfachzugriff, mit Hinweis je Fehlercode |
|
||||
| Paging | `@odata.nextLink` ignoriert, `$top=50` fest | Seiten werden gefolgt, je Lauf gedeckelt, fortsetzbar |
|
||||
| HTML-Anzeige | ungefiltert | serverseitig entschaerft, `sandbox=""`, externe Bilder gesperrt |
|
||||
| HTML-Anzeige | ungefiltert | serverseitig entschärft, `sandbox=""`, externe Bilder gesperrt |
|
||||
| Zeitangaben | gemischt | durchgehend UTC in der Datenbank, Umrechnung im Frontend |
|
||||
|
||||
## Was noch nicht drin ist
|
||||
|
||||
* **Senden, Antworten, Weiterleiten.** Braucht `Mail.ReadWrite` und `Mail.Send`
|
||||
und damit eine erneute Administratorzustimmung.
|
||||
* **Anhaenge.** `has_attachments` wird gefuehrt und angezeigt, die Dateien
|
||||
* **Anhänge.** `has_attachments` wird geführt und angezeigt, die Dateien
|
||||
selbst werden nicht geholt.
|
||||
* **Automatische Zuordnung zu Opportunities.** Die Zuordnung erfolgt per Hand;
|
||||
der Dialog schlaegt anhand der Mailadressen Kontakt und Opportunity vor. Eine
|
||||
Bewertung durch ein Sprachmodell waere der naechste Schritt - dann bitte mit
|
||||
der Dialog schlägt anhand der Mailadressen Kontakt und Opportunity vor. Eine
|
||||
Bewertung durch ein Sprachmodell wäre der nächste Schritt - dann bitte mit
|
||||
dem Schutz gegen Prompt-Injection, der im Altsystem in `mail-sync` fehlte.
|
||||
* **Echtzeit statt Intervall.** Graph Change Notifications brauchen einen
|
||||
oeffentlich erreichbaren Endpunkt (haben wir) und eine Erneuerung der
|
||||
öffentlich erreichbaren Endpunkt (haben wir) und eine Erneuerung der
|
||||
Subscription etwa alle drei Tage.
|
||||
|
||||
## Stoerungssuche
|
||||
## Störungssuche
|
||||
|
||||
| Beobachtung | Ursache und Abhilfe |
|
||||
| --- | --- |
|
||||
| Einstellungen melden fehlende Variablen | `server/.env` ergaenzen, Dienst neu starten - die Werte werden beim Start gelesen |
|
||||
| Einstellungen melden fehlende Variablen | `server/.env` ergänzen, Dienst neu starten - die Werte werden beim Start gelesen |
|
||||
| Test scheitert „bei der Anmeldung" | Tenant-ID, Client-ID oder Geheimnis falsch, oder das Geheimnis ist abgelaufen |
|
||||
| Test scheitert „beim Postfachzugriff", `ErrorAccessDenied` | Administratorzustimmung fehlt, oder die ApplicationAccessPolicy erlaubt dieses Postfach nicht |
|
||||
| Test scheitert mit `ResourceNotFound` | `MS_MAILBOX` zeigt auf ein Postfach, das es nicht gibt |
|
||||
| „Erstabgleich laeuft noch" bleibt stehen | Log auf `[Mail]` pruefen; `MAIL_MAX_PAGES` erhoehen, wenn es zu langsam vorangeht |
|
||||
| Keine Kontakt-Vorschlaege | stehen alle Absender auf einer eigenen Domain? `mail_internal_domains` pruefen |
|
||||
| Doppelte Kontakte | tritt nicht auf: Abgleich erfolgt ueber die Adresse in Kleinschreibung; bei Treffer wird ergaenzt statt angelegt |
|
||||
| Zaehler und Liste weichen ab | die Liste blendet im Postfach geloeschte Mails aus, der Zaehler in den Einstellungen ebenfalls - Zuordnungen bleiben trotzdem erhalten |
|
||||
| „Erstabgleich läuft noch" bleibt stehen | Log auf `[Mail]` prüfen; `MAIL_MAX_PAGES` erhöhen, wenn es zu langsam vorangeht |
|
||||
| Keine Kontakt-Vorschläge | stehen alle Absender auf einer eigenen Domain? `mail_internal_domains` prüfen |
|
||||
| Doppelte Kontakte | tritt nicht auf: Abgleich erfolgt über die Adresse in Kleinschreibung; bei Treffer wird ergänzt statt angelegt |
|
||||
| Zähler und Liste weichen ab | die Liste blendet im Postfach gelöschte Mails aus, der Zähler in den Einstellungen ebenfalls - Zuordnungen bleiben trotzdem erhalten |
|
||||
|
||||
+12
-12
@@ -3,24 +3,24 @@
|
||||
Technische Dokumentation mit Funktionszeichnungen. Die Diagramme sind Mermaid und
|
||||
werden von Gitea direkt gerendert.
|
||||
|
||||
**Neu hier?** Wer die Anwendung *benutzen* will, faengt bei
|
||||
[07 Anleitung fuer Benutzer](07-anleitung-benutzer.md) an. Wer daran *entwickeln*
|
||||
will, bei [08 Anleitung fuer Entwickler](08-anleitung-entwickler.md). Beide setzen
|
||||
**Neu hier?** Wer die Anwendung *benutzen* will, fängt bei
|
||||
[07 Anleitung für Benutzer](07-anleitung-benutzer.md) an. Wer daran *entwickeln*
|
||||
will, bei [08 Anleitung für Entwickler](08-anleitung-entwickler.md). Beide setzen
|
||||
keine Vorkenntnisse voraus.
|
||||
|
||||
| Kapitel | Inhalt |
|
||||
| --- | --- |
|
||||
| [01 Architektur](01-architektur.md) | Topologie, Prozesse, Ports, Anfrageweg, Rollen |
|
||||
| [02 Datenmodell](02-datenmodell.md) | Tabellen beider Datenbanken als ER-Diagramme |
|
||||
| [03 Projekt Scanner](03-scanner.md) | Suchlauf von der Boerse bis zum Treffer, Parallelbetrieb |
|
||||
| [03 Projekt Scanner](03-scanner.md) | Suchlauf von der Börse bis zum Treffer, Parallelbetrieb |
|
||||
| [04 Diktat](04-diktat.md) | Spracherkennung im Browser, Modell-Lebenszyklus |
|
||||
| [05 Frontend](05-frontend.md) | Ansichten, Menue, Komponentenkarte, Zustand |
|
||||
| [06 Betrieb](06-betrieb.md) | Deployment, Umgebungsvariablen, Dienste, Stoerungssuche |
|
||||
| [07 Anleitung fuer Benutzer](07-anleitung-benutzer.md) | Bedienung aller Bereiche, Schritt fuer Schritt |
|
||||
| [08 Anleitung fuer Entwickler](08-anleitung-entwickler.md) | Einrichten, erweitern, bekannte Fallen |
|
||||
| [05 Frontend](05-frontend.md) | Ansichten, Menü, Komponentenkarte, Zustand |
|
||||
| [06 Betrieb](06-betrieb.md) | Deployment, Umgebungsvariablen, Dienste, Störungssuche |
|
||||
| [07 Anleitung für Benutzer](07-anleitung-benutzer.md) | Bedienung aller Bereiche, Schritt für Schritt |
|
||||
| [08 Anleitung für Entwickler](08-anleitung-entwickler.md) | Einrichten, erweitern, bekannte Fallen |
|
||||
| [09 Quellcode-Referenz](09-quellcode.md) | jede Datei, ihre Exporte und Zusagen |
|
||||
| [10 Glossar](10-glossar.md) | Agent, Match, Lead, Opportunity, Status- und Portalwerte |
|
||||
| [11 Postfachanbindung](11-postfach.md) | crm@perinco.de ueber Microsoft Graph, Kontakte aus Mails |
|
||||
| [11 Postfachanbindung](11-postfach.md) | crm@perinco.de über Microsoft Graph, Kontakte aus Mails |
|
||||
|
||||
Die Designentscheidungen und die Messwerte zur Spracherkennung stehen im
|
||||
[README des Projekts](../README.md).
|
||||
@@ -47,7 +47,7 @@ flowchart TB
|
||||
end
|
||||
|
||||
OAI["OpenAI<br/>gpt-4o-mini"]
|
||||
BOERSE["Projektboersen<br/>freelancermap, hays, ..."]
|
||||
BOERSE["Projektbörsen<br/>freelancermap, hays, ..."]
|
||||
GRAPH["Microsoft Graph<br/>crm@perinco.de"]
|
||||
|
||||
SPA -->|"HTTPS /api"| AP
|
||||
@@ -70,5 +70,5 @@ flowchart TB
|
||||
class ALT,DB3 alt
|
||||
```
|
||||
|
||||
Gestrichelt: die urspruengliche Scanner-App, die bis zur Ablosung parallel
|
||||
weiterlaeuft. Sie hat ihre eigene Datenbank und wird vom CRM nicht angefasst.
|
||||
Gestrichelt: die ursprüngliche Scanner-App, die bis zur Ablosung parallel
|
||||
weiterläuft. Sie hat ihre eigene Datenbank und wird vom CRM nicht angefasst.
|
||||
|
||||
Reference in New Issue
Block a user