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