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