chore(scanner): alte Scanner-App zurueckgebaut

Die eigenstaendige Vorgaenger-App unter scanner.perinco.de ist auf dem Server
entfernt: Unit project-scanner-backend (Port 5005), beide Apache-Vhosts, das
Let's-Encrypt-Zertifikat samt Renewal-Config, /var/www/Projekt Scanner (486 MB)
und die Datenbank project_scanner (Dump unter /root/backups). Damit entfallen
die LEGACY_*-Variablen und das Importskript, dessen Quelle es nicht mehr gibt.

/root/.cache/ms-playwright bleibt bewusst stehen: der Cache stammt aus der
Installation der alten App, liegt aber ausserhalb beider Verzeichnisse und wird
vom CRM-Scanner benutzt - playwright ist auf 1.60.0 gepinnt, damit es genau
dieser Build (chromium-1223) bleibt. Der Hinweis steht jetzt in README und
docs/06-betrieb.md, damit ihn beim naechsten Aufraeumen niemand mitnimmt.

SCANNER_SCHEDULER_OFFSET geht auf 0: der Versatz sollte verhindern, dass beide
Instanzen mit denselben Portal-Cookies gleichzeitig losziehen. Ohne zweite
Instanz hat er keinen Zweck mehr, die Mechanik bleibt fuer kuenftige Erststarts
im Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-09 17:45:42 +02:00
co-authored by Claude Opus 5
parent ab6b9f2372
commit 3c9d83161e
11 changed files with 158 additions and 646 deletions
-8
View File
@@ -24,13 +24,6 @@ flowchart LR
A -->|"/api"| E["Express :5006"]
E --> C[("perinco_CRM")]
E --> S[("perinco_scanner")]
U2(("Browser")) -->|"HTTPS"| A2["Apache :443<br/>scanner.perinco.de"]
A2 -->|"/api, /uploads, /health"| E2["alte Scanner-App :5005"]
E2 --> S2[("project_scanner")]
classDef alt fill:#f7f7f7,stroke:#9a9a9a,stroke-dasharray: 4 3
class U2,A2,E2,S2 alt
```
Der Scanner im CRM hängt unter `/api/scanner` und braucht damit **keine eigene
@@ -115,6 +108,5 @@ Portbelegung `ss -tlnp` prüfen. Relevant hier:
| Port | Dienst |
| --- | --- |
| 5006 | perinco CRM Backend (nur `127.0.0.1`) |
| 5005 | alte Scanner-App (`0.0.0.0`) |
| 3000 | weitere Vite-App |
| 5678 | n8n |
+50 -90
View File
@@ -2,7 +2,8 @@
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.
der eigenständigen App, die er seit dem 09.09.2026 ersetzt
([Rückbau](#rückbau-der-alten-app---09092026)).
## Ein Suchlauf
@@ -71,10 +72,13 @@ Takt: mit dem Endzeitpunkt kam die Laufzeit von drei bis vier Minuten jedes Mal
obendrauf, aus 15 Minuten wurden 19 - rund ein Fuenftel weniger Durchgaenge pro
Tag, und die Trefferliste der Boersen zeigt nur die neuesten Projekte.
Die **Sperrfrist** ist die Antwort auf ein konkretes Problem: beide Instanzen
benutzen dieselben Portal-Cookies. Beim ersten Start ziehen sonst beide
gleichzeitig los, was Rate-Limits und CAPTCHAs provoziert - und zwei Chromium
gleichzeitig belasten den Speicher doppelt.
Die **Sperrfrist** war die Antwort auf ein konkretes Problem des
Parallelbetriebs: beide Instanzen benutzten dieselben Portal-Cookies. Beim
ersten Start zogen sonst beide gleichzeitig los, was Rate-Limits und CAPTCHAs
provozierte - und zwei Chromium gleichzeitig belasten den Speicher doppelt. Seit
dem Rückbau der alten App steht `SCANNER_SCHEDULER_OFFSET` auf 0; die Mechanik
bleibt im Code, weil sie bei jedem Erststart einer weiteren Instanz wieder
greift.
Der Zeitpunkt des letzten Laufs steht in `settings.last_scan_at`. Das ist kein
Luxus, sondern nach zwei beobachteten Fehlern nötig:
@@ -155,97 +159,53 @@ nie. Dagegen das **Notventil** `PIPELINE_MAX_RUNTIME_MS`: läuft die Sperre
länger als eine Stunde, gibt der nächste Aufruf sie frei und startet neu. So
erholt sich der Scanner selbst, statt auf ein Deployment zu warten.
## Parallelbetrieb - alles verdoppelt
## Rückbau der alten App - 09.09.2026
```mermaid
flowchart LR
subgraph alt["alte App (scanner.perinco.de)"]
A1["Express :5005"] --> A2[("project_scanner")]
A1 --> A3["uploads/ in<br/>/var/www/Projekt Scanner/backend"]
A4["ENCRYPTION_KEY"] --- A1
end
Der Scanner lief seit dem Rollout am 06.08.2026 **parallel** zur eigenständigen
Vorgänger-App unter `scanner.perinco.de`. Diese ist am 09.09.2026 vollständig
zurückgebaut. Verschwunden sind:
subgraph neu["CRM (pcrm.perinco.de)"]
N1["Express :5006<br/>/api/scanner"] --> N2[("perinco_scanner")]
N1 --> N3["uploads/ in<br/>/var/www/perinco_CRM/server"]
N4["SCANNER_ENCRYPTION_KEY"] --- N1
end
| Weg | War |
| --- | --- |
| Unit `project-scanner-backend` | Express auf `0.0.0.0:5005` |
| Vhosts `scanner.perinco.de(.conf/-le-ssl.conf)` | eigener Apache-Vhost, Basic Auth |
| Zertifikat `scanner.perinco.de` | eigenes Let's-Encrypt-Zert samt Renewal-Config |
| `/var/www/Projekt Scanner` | 486 MB Code, `node_modules`, 19 CV-PDFs |
| Datenbank `project_scanner` | 16,2 MB; Dump liegt unter `/root/backups/project_scanner_20260909-173447.sql.gz` |
| `LEGACY_*` in `server/.env` | Quelle des Importskripts |
| `server/src/scanner/importLegacy.js` | `npm run scanner:import` |
B["Projektbörsen"]
O["OpenAI"]
A1 --> B
N1 --> B
A1 --> O
N1 --> O
**Was bewusst stehen geblieben ist:** `/root/.cache/ms-playwright` (641 MB, Build
`chromium-1223`). Der Cache stammt aus der Installation der alten App, aber
`PLAYWRIGHT_BROWSERS_PATH` ist nirgends gesetzt - **beide** Apps haben ihn
benutzt, und `playwright` ist im CRM auf exakt `1.60.0` gepinnt, damit es genau
dieser Build bleibt. Wer den Cache mit dem alten Verzeichnis löscht, nimmt dem
CRM-Scanner den Browser. Der Ordner `/var/www/Projekt Scanner/backend/.playwright`
war dagegen toter Ballast einer Installation mit gesetztem
`PLAYWRIGHT_BROWSERS_PATH` und ist mit dem Verzeichnis mitgegangen.
IMP["npm run scanner:import"] -.->|"liest"| A2
IMP -.->|"schreibt"| N2
IMP -.->|"kopiert"| N3
```
Was der Rückbau ändert:
| | alte App | CRM |
| --- | --- | --- |
| Datenbank | `project_scanner` | `perinco_scanner` |
| Schlüssel | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` |
| CV-Dateien | eigener Ordner | eigener Ordner |
| Aufräumen | 03:00 | 03:30 |
| Anmeldung | HTTP Basic | CRM-Sitzung (JWT) |
- **Kosten:** Beide Instanzen haben dieselben Projekte unabhängig voneinander
bewertet, die OpenAI-Kosten fielen doppelt an. Faktisch war das schon am
09.09.2026 um 08:49 vorbei - da wurde `scanning_active` in der alten
Oberfläche auf `false` gesetzt, letzter Lauf dort 09:02.
- **Portal-Sessions:** Beide Instanzen benutzten dieselben Cookies der
Projektbörsen. Der Grund für `SCANNER_SCHEDULER_OFFSET` fällt damit weg, der
Wert steht jetzt auf 0. Rate-Limits und CAPTCHAs durch zwei gleichzeitig
scrapende Chromium-Sätze sind Geschichte.
- **Speicher:** rund 200 MB RSS und ein zweiter Playwright-Satz weniger auf
einer Box mit 7,7 GiB.
- **Daten:** Nicht übernommen wurden 13380 Treffer, die es nur in der alten
Datenbank gab, darunter 92 als *reviewed* und 5 als *applied* markierte. Ein
nachträglicher Import war nicht sinnvoll: das Importskript vergibt die
Original-Ids und wäre mit den vorhandenen Ids in `perinco_scanner`
kollidiert. Der Bestand war ohnehin flüchtig - der Aufräumjob hält nur die
jüngsten Tage. Wer doch etwas sucht, greift auf den Dump zurück.
**Der Preis:** Beide bewerten dieselben Projekte unabhängig voneinander, die
OpenAI-Kosten fallen also doppelt an. Wer das senken will, stellt im CRM ein
größeres Suchintervall ein - die Einstellung gilt nur für diese Instanz.
## Übernahme aus der alten Datenbank
`server/src/scanner/importLegacy.js`, aufrufbar als `npm run scanner:import`:
```mermaid
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 über den Namen zuordnen:<br/>Benutzername, Session-Cookies, Status"]
P --> PW{"LEGACY_ENCRYPTION_KEY gesetzt?"}
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
A --> M{"--with-matches?"}
M -->|ja| M2["Projekte und Treffer<br/>mit Original-Ids"]
M -->|nein| F["fertig"]
M2 --> F
```
Die Ids bleiben erhalten, damit übernommene Treffer weiter auf die richtigen
Agenten und Projekte zeigen. Deshalb bricht das Skript bei bereits vorhandenen
Agenten ab.
### Agenten nachträglich holen
Der Vollimport ist ein Erststart-Werkzeug. Um später nur die Suchagenten
nachzuziehen, ohne Einstellungen, Portale und Treffer anzufassen:
```bash
cd /var/www/perinco_CRM/server
npm run scanner:import -- --agents-only --skip=Bilal --dry-run
npm run scanner:import -- --agents-only --skip=Bilal
```
`--agents-only` läuft **additiv**: vorhandene Agenten blockieren nicht, sondern
werden über den Namen erkannt und übersprungen - was hier im CRM schon
bearbeitet wurde, bleibt also unangetastet. Die Id der Quelle wird nur
übernommen, solange sie in `perinco_scanner` frei ist; sonst vergibt die
Datenbank eine neue. `--skip=` nimmt einen Namen oder eine Liste
(`--skip=Bilal,Nico`), Groß- und Kleinschreibung spielt keine Rolle.
`--force` ist in diesem Modus gesperrt: es würde Projekte und Treffer löschen,
die der Lauf gar nicht anrührt.
> **Beim Rollout am 06.08.2026 festgestellt:** Die Portal-Passwörter der alten
> Datenbank 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. Die
> Session-Cookies liegen im Klartext und wurden übernommen; damit scrapt die
> neue Instanz sofort.
Die Domain `scanner.perinco.de` zeigt weiter auf den Server, hat aber keinen
eigenen Vhost mehr: HTTP landet beim ersten aktivierten Vhost, HTTPS gibt eine
Zertifikatswarnung. Der DNS-Eintrag muss beim Anbieter gelöscht werden.
## Oberfläche
+20 -18
View File
@@ -34,11 +34,10 @@ 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, läuft bis zur Ablosung |
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.
Die zweite Unit `project-scanner-backend` (Port 5005, `/var/www/Projekt Scanner/backend`)
ist mit dem [Rückbau der alten App](03-scanner.md#rückbau-der-alten-app---09092026)
am 09.09.2026 entfallen. Port 5005 ist seither frei.
`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
@@ -58,8 +57,7 @@ mitten im Scrapen beenden.
| `SCANNER_ENCRYPTION_KEY` | AES-Schlüssel für Portal-Passwörter |
| `SCANNER_UPLOAD_DIR` | CV-Verzeichnis, leer = `<server>/uploads` |
| `SCANNER_SCHEDULER` | Hintergrund-Scraper, Standard an, nur `false` schaltet ab |
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz für den ersten Lauf, Standard 30 |
| `LEGACY_*` | Quelle für `npm run scanner:import` |
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz für den ersten Lauf, seit dem Rückbau der alten App 0 |
| `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` |
@@ -81,10 +79,17 @@ Exchange Online: [11 Postfachanbindung](11-postfach.md).
## 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
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`.
`playwright` ist auf **exakt 1.60.0** gepinnt. Der passende Chromium-Build
(`chromium-1223`) liegt in `/root/.cache/ms-playwright` - dieser Cache stammt aus
der Installation der alten App und hat deren Rückbau überlebt, weil beide Apps
ihn geteilt haben und `PLAYWRIGHT_BROWSERS_PATH` nirgends gesetzt ist. Ein `^` in
der Version würde einen zweiten Browser-Download ziehen; die Begründung steht als
`comments`-Feld in `server/package.json`.
> **Beim Aufräumen nicht anfassen:** `/root/.cache/ms-playwright` (641 MB) liegt
> ausserhalb beider Anwendungsverzeichnisse. Wer ihn mit löscht, nimmt dem
> Scanner den Browser - Oberfläche und Treffer bleiben, Scan-Lauf und
> Verbindungstest fallen aus.
Fehlt der Browser, funktioniert alles ausser Scan-Lauf und Verbindungstest:
@@ -96,17 +101,14 @@ cd /var/www/perinco_CRM/server && npx playwright install chromium
```bash
bash /var/www/perinco_CRM/deploy/scanner-setup.sh # .env ergänzen, Schlüssel erzeugen
cd /var/www/perinco_CRM/server
npm run scanner:import -- --dry-run
npm run scanner:import -- --with-matches
systemctl restart perinco-crm-backend
```
> **Reihenfolge:** Der Import schreibt `search_interval` aus der alten App in die
> 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.
Datenbank und Tabellen legt der Start selbst an, Portale und
Standardeinstellungen werden geseedet. Agenten, Zugangsdaten und Cookies kommen
danach über die Oberfläche. Das frühere `npm run scanner:import` holte diese
Daten aus der Datenbank der alten App - beides ist mit dem Rückbau am 09.09.2026
entfallen.
## Störungssuche
-1
View File
@@ -187,7 +187,6 @@ Diese Punkte haben real Zeit gekostet:
| **Reihenfolge in `server.js`** | Der 404-Fallback `app.use('/api', ...)` fängt alles, was danach registriert wird. Neue Router davor einhängen. |
| **`Content-Type` bei FormData** | `apiRequest` setzt JSON nur, wenn der Body **keine** `FormData` ist - sonst fehlt die multipart-Grenze und der Upload scheitert. |
| **Dateien in neuen Tabs** | Ein `<a href>` schickt kein Bearer-Token. Deshalb holt `openAgentCv()` das PDF per `fetch` und ö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-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. |
-1
View File
@@ -188,7 +188,6 @@ Die portierte Scanner-API unter `/api/scanner`, vollständig in
| `services/pdfParser.js` | `pdf-parse`, doppelte Leerzeilen raus |
| `utils/crypto.js` | AES über `crypto-js`; `decryptWith()` für fremde Schlüssel |
| `uploadDir.js` | `UPLOAD_DIR`, `ensureUploadDir()`, `resolveCvPath()` |
| `importLegacy.js` | einmalige Übernahme aus der alten Datenbank |
Zusagen, auf die sich der Rest verlässt:
+3 -10
View File
@@ -12,7 +12,7 @@ keine Vorkenntnisse voraus.
| --- | --- |
| [01 Architektur](01-architektur.md) | Topologie, Prozesse, Ports, Anfrageweg, Rollen |
| [02 Datenmodell](02-datenmodell.md) | Tabellen beider Datenbanken als ER-Diagramme |
| [03 Projekt Scanner](03-scanner.md) | Suchlauf von der Börse bis zum Treffer, Parallelbetrieb |
| [03 Projekt Scanner](03-scanner.md) | Suchlauf von der Börse bis zum Treffer, Rückbau der alten App |
| [04 Diktat](04-diktat.md) | Spracherkennung im Browser, Modell-Lebenszyklus |
| [05 Frontend](05-frontend.md) | Ansichten, Menü, Komponentenkarte, Zustand |
| [06 Betrieb](06-betrieb.md) | Deployment, Umgebungsvariablen, Dienste, Störungssuche |
@@ -42,8 +42,6 @@ flowchart TB
PW["Playwright<br/>Chromium"]
DB1[("MariaDB<br/>perinco_CRM")]
DB2[("MariaDB<br/>perinco_scanner")]
ALT["alte Scanner-App<br/>:5005"]
DB3[("MariaDB<br/>project_scanner")]
end
OAI["OpenAI<br/>gpt-4o-mini"]
@@ -62,13 +60,8 @@ flowchart TB
API -.-> MSCH
MSCH -->|"Delta Query, nur lesend"| GRAPH
MSCH --> DB1
ALT --> DB3
ALT --> BOERSE
SPA -.->|"Audio bleibt im Browser"| WSP
classDef alt fill:#f7f7f7,stroke:#9a9a9a,stroke-dasharray: 4 3
class ALT,DB3 alt
```
Gestrichelt: die ursprüngliche Scanner-App, die bis zur Ablosung parallel
weiterläuft. Sie hat ihre eigene Datenbank und wird vom CRM nicht angefasst.
Die ursprüngliche Scanner-App unter `scanner.perinco.de` ist am 09.09.2026
zurückgebaut worden; das CRM ist seither die einzige Instanz.