docs: echte Umlaute in der Dokumentation

893 Stellen in Fliesstext, Tabellen und Mermaid-Beschriftungen. Dazu die
sichtbaren ss-Fälle (ausschließlich, groß, außen, schließt, heißt,
draußen, weiße, anstoßen, Straße).

Nicht angetastet, damit die Doku weiter beschreibt, was wirklich in der
Datenbank steht:

- Codeblöcke und Code-Spans - dort stehen Bezeichner, SQL, Enum-Werte
  und Dateinamen: `mail_pool`, `'uebernommen'`, `extract.js`
- in Mermaid nur der Text in Anführungszeichen; Knotennamen, Pfeile und
  Werte wie source = 'Mail' bleiben, wie sie sind
- `empfaenger` im ER-Diagramm von crm_mail_contact_candidates: die
  Aufzählung dort nennt die Datenbankwerte, nicht die Anzeige

Die Menü-Skizze in der Benutzeranleitung zeigt jetzt die aufgeklappten
Kontakte mit "Aus Mails" samt Zähler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-11 09:45:58 +02:00
co-authored by Claude Opus 5
parent 8b7ce448a6
commit 7973dc07b1
13 changed files with 688 additions and 684 deletions
+39 -39
View File
@@ -1,8 +1,8 @@
# 03 Projekt Scanner
Der Scanner durchsucht Projektboersen nach Auftraegen und bewertet jeden Fund mit
OpenAI gegen die Lebenslaeufe der Berater. Er ist eine vollstaendige Portierung
der eigenstaendigen App und laeuft parallel zu ihr.
Der Scanner durchsucht Projektbörsen nach Aufträgen und bewertet jeden Fund mit
OpenAI gegen die Lebensläufe der Berater. Er ist eine vollständige Portierung
der eigenständigen App und läuft parallel zu ihr.
## Ein Suchlauf
@@ -43,15 +43,15 @@ sequenceDiagram
Zwei Sparmassnahmen stecken bewusst **vor** dem OpenAI-Aufruf: der vorhandene
Treffer (Unique-Key `agent_id, project_id`) und die Ausschlussbegriffe. Ein
Projekt, das „Arbeitnehmerueberlassung" enthaelt, kostet damit keinen Cent.
Projekt, das „Arbeitnehmerüberlassung" enthält, kostet damit keinen Cent.
## Wann laeuft er
## Wann läuft er
```mermaid
flowchart TD
T["Cron-Tick, jede Minute"] --> A{"scanning_active?"}
A -->|nein| STOP1["nichts tun"]
A -->|ja| B{"Intervall geaendert?"}
A -->|ja| B{"Intervall geändert?"}
B -->|ja| RE["neu planen"]
B -->|nein| C{"Sperrfrist<br/>nur beim allerersten Start"}
C -->|noch aktiv| STOP2["nichts tun"]
@@ -59,7 +59,7 @@ flowchart TD
D -->|nein| STOP3["nichts tun"]
D -->|ja| E{"07:00-21:59?"}
E -->|nein| STOP4["nichts tun"]
E -->|ja| S["last_scan_at aus der DB lesen<br/>(faengt manuelle Laeufe mit ein)"]
E -->|ja| S["last_scan_at aus der DB lesen<br/>(fängt manuelle Läufe mit ein)"]
S --> F{"Intervall seit<br/>letztem Lauf um?"}
F -->|nein| STOP5["nichts tun"]
F -->|ja| G["Pipeline starten"]
@@ -72,23 +72,23 @@ gleichzeitig los, was Rate-Limits und CAPTCHAs provoziert - und zwei Chromium
gleichzeitig belasten den Speicher doppelt.
Der Zeitpunkt des letzten Laufs steht in `settings.last_scan_at`. Das ist kein
Luxus, sondern nach zwei beobachteten Fehlern noetig:
Luxus, sondern nach zwei beobachteten Fehlern nötig:
> **Erster Fehler:** Der Versatz war ueber einen vorgealterten `lastRunTime`
> gebaut. Ein Zeitstempel kann einen Lauf aber nur **vorziehen**, nie verzoegern -
> **Erster Fehler:** Der Versatz war über einen vorgealterten `lastRunTime`
> gebaut. Ein Zeitstempel kann einen Lauf aber nur **vorziehen**, nie verzögern -
> bei Intervall 15 und Versatz 30 lief er sofort. Seither eine eigene Frist
> (`notBefore`).
>
> **Zweiter Fehler:** Diese Frist begann bei **jedem** Neustart von vorn. Zwei
> Deployments im Abstand von zehn Minuten haben den faelligen Lauf zweimal um eine
> halbe Stunde verschoben - er fand nie statt. Wer oefter als alle 30 Minuten
> deployt, haette einen Scanner, der nie automatisch sucht. Deshalb liegt der
> Deployments im Abstand von zehn Minuten haben den fälligen Lauf zweimal um eine
> halbe Stunde verschoben - er fand nie statt. Wer öfter als alle 30 Minuten
> deployt, hätte einen Scanner, der nie automatisch sucht. Deshalb liegt der
> Zeitpunkt jetzt in der Datenbank, und der Versatz greift nur, solange dort noch
> nichts steht.
Der gespeicherte Zeitpunkt wird auch **im Tick** gelesen: ein manueller Scan aus
der Oberflaeche schreibt ihn, weiss aber nichts von der Variablen im Scheduler.
Ohne den Abgleich koennte der Scheduler direkt hinter einem manuellen Lauf noch
der Oberfläche schreibt ihn, weiß aber nichts von der Variablen im Scheduler.
Ohne den Abgleich könnte der Scheduler direkt hinter einem manuellen Lauf noch
einmal losziehen.
## Parallelbetrieb - alles verdoppelt
@@ -107,7 +107,7 @@ flowchart LR
N4["SCANNER_ENCRYPTION_KEY"] --- N1
end
B["Projektboersen"]
B["Projektbörsen"]
O["OpenAI"]
A1 --> B
N1 --> B
@@ -122,16 +122,16 @@ flowchart LR
| | alte App | CRM |
| --- | --- | --- |
| Datenbank | `project_scanner` | `perinco_scanner` |
| Schluessel | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` |
| Schlüssel | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` |
| CV-Dateien | eigener Ordner | eigener Ordner |
| Aufraeumen | 03:00 | 03:30 |
| Aufräumen | 03:00 | 03:30 |
| Anmeldung | HTTP Basic | CRM-Sitzung (JWT) |
**Der Preis:** Beide bewerten dieselben Projekte unabhaengig voneinander, die
**Der Preis:** Beide bewerten dieselben Projekte unabhängig voneinander, die
OpenAI-Kosten fallen also doppelt an. Wer das senken will, stellt im CRM ein
groesseres Suchintervall ein - die Einstellung gilt nur fuer diese Instanz.
größeres Suchintervall ein - die Einstellung gilt nur für diese Instanz.
## Uebernahme aus der alten Datenbank
## Übernahme aus der alten Datenbank
`server/src/scanner/importLegacy.js`, aufrufbar als `npm run scanner:import`:
@@ -140,9 +140,9 @@ flowchart TD
S["Start"] --> C{"Zieltabelle agents leer?"}
C -->|nein, ohne --force| AB["Abbruch"]
C -->|ja oder --force| E["Einstellungen kopieren<br/>inkl. OpenAI-Key"]
E --> P["Portale ueber den Namen zuordnen:<br/>Benutzername, Session-Cookies, Status"]
E --> P["Portale über den Namen zuordnen:<br/>Benutzername, Session-Cookies, Status"]
P --> PW{"LEGACY_ENCRYPTION_KEY gesetzt?"}
PW -->|ja| PW2["Passwort entschluesseln<br/>und neu verschluesseln"]
PW -->|ja| PW2["Passwort entschlüsseln<br/>und neu verschlüsseln"]
PW -->|nein| PW3["Passwort auslassen"]
PW2 --> A["Agenten anlegen, Ids behalten<br/>CV-PDF kopieren"]
PW3 --> A
@@ -152,33 +152,33 @@ flowchart TD
M2 --> F
```
Die Ids bleiben erhalten, damit uebernommene Treffer weiter auf die richtigen
Die Ids bleiben erhalten, damit übernommene Treffer weiter auf die richtigen
Agenten und Projekte zeigen. Deshalb bricht das Skript bei bereits vorhandenen
Agenten ab.
> **Beim Rollout am 06.08.2026 festgestellt:** Die Portal-Passwoerter der alten
> **Beim Rollout am 06.08.2026 festgestellt:** Die Portal-Passwörter der alten
> Datenbank lassen sich mit dem dortigen `ENCRYPTION_KEY` nicht mehr
> entschluesseln - der Schluessel wurde nach dem Speichern getauscht. Die alte App
> entschlüsseln - der Schlüssel wurde nach dem Speichern getauscht. Die alte App
> kann sie damit ebenso wenig lesen, es ging also nichts verloren. Die
> Session-Cookies liegen im Klartext und wurden uebernommen; damit scrapt die
> Session-Cookies liegen im Klartext und wurden übernommen; damit scrapt die
> neue Instanz sofort.
## Oberflaeche
## Oberfläche
| Reiter | Datei | Inhalt |
| --- | --- | --- |
| Dashboard | `ScannerDashboard.tsx` | Kennzahlen, Filter, Trefferliste, *Jetzt scannen*, Ton bei neuen Treffern |
| Unpassend | `ScannerRejected.tsx` | Treffer mit Status `rejected` |
| Suchagenten | `ScannerAgents.tsx` | Beraterprofile, CV-Upload |
| Boersen-Konten | `ScannerPortals.tsx` | Verbindungsstatus, Zugangsdaten, Cookie-Import |
| Börsen-Konten | `ScannerPortals.tsx` | Verbindungsstatus, Zugangsdaten, Cookie-Import |
| Einstellungen | `settings/ScannerSettings.tsx` | Intervall, Schwellenwerte, OpenAI, Ton, E-Mail |
**Wichtig zum Ton:** Er haengt an der Dashboard-Komponente. Wer auf einen anderen
Menuepunkt wechselt, hoert nichts - die Komponente wird ausgebaut und die
**Wichtig zum Ton:** Er hängt an der Dashboard-Komponente. Wer auf einen anderen
Menüpunkt wechselt, hört nichts - die Komponente wird ausgebaut und die
30-Sekunden-Abfrage endet. In einem anderen Browsertab bleibt der Ton dagegen
aktiv, nur langsamer (Chrome bremst Timer versteckter Tabs auf etwa einmal pro
Minute) und nur, wenn zuvor irgendwo auf der Seite geklickt wurde
(Autoplay-Regel). Browserunabhaengig meldet nur die E-Mail.
(Autoplay-Regel). Browserunabhängig meldet nur die E-Mail.
## Endpunkte
@@ -187,15 +187,15 @@ Alle unter `/api/scanner`, alle hinter `requireAuth`:
| Methode | Pfad | Zweck |
| --- | --- | --- |
| GET | `/agents` | Liste ohne `cvText` (Nutzlast) |
| POST/PUT | `/agents[/:id]` | anlegen/aendern, `multipart` mit CV |
| POST/PUT | `/agents[/:id]` | anlegen/ändern, `multipart` mit CV |
| DELETE | `/agents/:id` | Agent samt CV-Datei |
| GET | `/agents/:id/cv` | PDF als Stream |
| GET | `/portals` | Status ohne Geheimnisse |
| POST | `/portals/credentials` | Zugangsdaten, AES-verschluesselt |
| POST | `/portals/sync-session` | Cookie-Import, normalisiert fuer Playwright |
| POST | `/portals/test-connection/:id` | Mini-Scrape bzw. Session-Pruefung |
| POST | `/portals/credentials` | Zugangsdaten, AES-verschlüsselt |
| POST | `/portals/sync-session` | Cookie-Import, normalisiert für Playwright |
| POST | `/portals/test-connection/:id` | Mini-Scrape bzw. Session-Prüfung |
| GET | `/matches` | Filter: `status`, `agentId`, `minPercentage`, `limit` |
| PUT/DELETE | `/matches/:id` | Status setzen, loeschen |
| PUT/DELETE | `/matches/:id` | Status setzen, löschen |
| GET/POST | `/settings` | Geheimnisse maskiert |
| POST | `/scan` | Lauf starten, `409` wenn schon einer laeuft |
| GET | `/status` | laeuft ein Lauf, Scheduler an, Datenbankname |
| POST | `/scan` | Lauf starten, `409` wenn schon einer läuft |
| GET | `/status` | läuft ein Lauf, Scheduler an, Datenbankname |