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
+61 -106
View File
@@ -211,22 +211,13 @@ starten absteigend, Text aufsteigend; leere Werte landen immer hinten.
## Projekt Scanner
Der eigenständige *Projekt Scanner* ist vollständig hier eingezogen und soll
die alte App ersetzen. Die läuft weiter unter `scanner.perinco.de`:
Der eigenständige *Projekt Scanner* ist vollständig hier eingezogen und hat die
alte App unter `scanner.perinco.de` am **09.09.2026** ersetzt. Seither läuft nur
noch diese Instanz; der Rückbau ist in
[docs/03-scanner.md](docs/03-scanner.md#rückbau-der-alten-app---09092026)
protokolliert.
```
/var/www/Projekt Scanner/backend Express auf 0.0.0.0:5005
/var/www/Projekt Scanner/frontend/dist DocumentRoot des Vhosts
systemd-Unit: project-scanner-backend (nicht "project-scanner")
```
Bis zur Ablosung laufen **beide parallel und beide voll funktional**: eigene
Datenbank, eigener Verschlüsselungsschlüssel, eigenes Upload-Verzeichnis,
eigener Scheduler. Die alte App bleibt unverändert - weder ihre Dateien noch
ihre Datenbank werden angefasst.
Menüpunkt **Scanner** links; ein Klick klappt die Reiter der alten App darunter
auf:
Menüpunkt **Scanner** links; ein Klick klappt die Reiter darunter auf:
| Reiter | Inhalt |
| --- | --- |
@@ -248,7 +239,6 @@ beiden Stellen ändern; es ist derselbe Wert in der Datenbank.
| 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` |
| 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:
@@ -259,19 +249,23 @@ Drei bewusste Unterschiede zum Original:
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
### Eigene Ressourcen, eigener Zeitplan
Beide Scanner laufen **gleichzeitig und funktional**, ohne sich zu berühren.
Dafür ist hier jede geteilte Ressource verdoppelt:
Der Scanner benutzt durchweg Eigenes und nichts aus der Vorgänger-App - das war
die Voraussetzung für den Parallelbetrieb und macht den Rückbau folgenlos:
| | alte App | diese Instanz |
| --- | --- | --- |
| Datenbank | `project_scanner` | `perinco_scanner` (eigene Tabellen, gleiche Namen) |
| Datenbank | `project_scanner` (gedroppt) | `perinco_scanner` |
| Verschlüsselung | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` - eigener Wert |
| CV-PDFs | `/var/www/Projekt Scanner/backend/uploads` | `SCANNER_UPLOAD_DIR` = `/var/www/perinco_CRM/server/uploads` |
| CV-PDFs | `/var/www/Projekt Scanner/backend/uploads` (gelöscht) | `SCANNER_UPLOAD_DIR` = `/var/www/perinco_CRM/server/uploads` |
| Scheduler | eigener Cron im alten Dienst | `SCANNER_SCHEDULER`, Standard `true` |
| Aufräumen | 03:00 | 03:30 |
Geteilt war genau eine Sache, und die liegt ausserhalb beider Verzeichnisse:
`/root/.cache/ms-playwright` mit dem Build `chromium-1223`. Der Cache ist beim
Rückbau bewusst stehen geblieben, siehe Abschnitt *Playwright*.
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 genügt also, das Backend
@@ -279,28 +273,23 @@ zu starten.
| Variable | Bedeutung |
| --- | --- |
| `SCANNER_DB_NAME` | eigene Datenbank, Standard `perinco_scanner` - **nicht** auf `project_scanner` zeigen lassen |
| `SCANNER_DB_NAME` | eigene Datenbank, Standard `perinco_scanner` |
| `SCANNER_DB_HOST/_PORT/_USER/_PASSWORD` | leer = CRM-Zugangsdaten |
| `SCANNER_ENCRYPTION_KEY` | eigener Schlüssel für die Portal-Passwörter; ohne ihn wird ein bekannter Ersatzschlüssel benutzt und beim Start gewarnt |
| `SCANNER_UPLOAD_DIR` | eigenes CV-Verzeichnis; leer = `<server>/uploads` |
| `SCANNER_ENCRYPTION_KEY` | Schlüssel für die Portal-Passwörter; ohne ihn wird ein bekannter Ersatzschlüssel benutzt und beim Start gewarnt |
| `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 **allerersten** Lauf, Standard `30`. Danach trägt `settings.last_scan_at` den Zeitplan über Neustarts hinweg |
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz für den **allerersten** Lauf, seit dem Rückbau `0`. Danach trägt `settings.last_scan_at` den Zeitplan über Neustarts hinweg |
> **Kosten:** Beide Instanzen bewerten dieselben Projekte unabhängig
> voneinander - die OpenAI-Kosten fallen also doppelt an, solange die alte App
> 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.
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 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 verschränkt.
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
fälliger Lauf konnte beliebig lange ausfallen (siehe
Der **Zeitversatz** war nicht Kosmetik: beide Instanzen benutzten dieselben
Portal-Sessions und wären beim ersten Start gleichzeitig losgezogen - das
provoziert Rate-Limits und CAPTCHAs bei Freelancermap und Freelance.de. Mit dem
Rückbau steht der Wert auf 0; die Mechanik bleibt im Code, weil sie bei jedem
Erststart einer weiteren Instanz wieder greift. Ab dem zweiten Start trägt
`settings.last_scan_at` den Zeitplan - sonst begann die Frist bei jedem
Deployment von vorn und ein 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
@@ -309,57 +298,41 @@ 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 übernehmen
### Was aus der alten App wurde
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`:
Beim Rollout am 06.08.2026 hat ein Skript (`importLegacy.js`,
`npm run scanner:import`) Agenten, Portale, Einstellungen und Session-Cookies aus
`project_scanner` herübergeholt; die CV-PDFs wurden in das eigene
Upload-Verzeichnis kopiert. Mit dem Rückbau am 09.09.2026 sind Quelle und Skript
weg.
```bash
cd /var/www/perinco_CRM/server
npm run scanner:import -- --dry-run # nur anzeigen
npm run scanner:import # Einstellungen, Portale, Agenten + CVs
npm run scanner:import -- --with-matches # zusätzlich Projekte und Treffer
npm run scanner:import -- --force # vorhandene Zieldaten vorher löschen
npm run scanner:import -- --agents-only --skip=Bilal # später: nur fehlende Agenten
```
> **Damals festgestellt:** Die in der alten Datenbank gespeicherten
> Portal-Passwörter liessen sich mit dem dortigen `ENCRYPTION_KEY` **nicht** mehr
> entschlüsseln - der Schlüssel war nach dem Speichern getauscht worden. Die alte
> App konnte sie damit ebenso wenig lesen, es ging also nichts verloren. Die
> Sessions (Cookies) lagen im Klartext und wurden übernommen.
`--agents-only` ist der nachträgliche Lauf: nur Suchagenten, additiv, vorhandene
werden über den Namen erkannt und übersprungen. `--skip=` lässt Agenten nach
Namen aus. Details: [03 Projekt Scanner](docs/03-scanner.md#agenten-nachträglich-holen).
Quelle kommt aus `LEGACY_*` in `server/.env`:
| 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 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 |
Ü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 übernommene Treffer
weiter auf die richtigen Agenten und Projekte zeigen. Deshalb bricht das Skript
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-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
> Passwörter unter *Börsen-Konten* neu ein. Die Sessions (Cookies) sind davon
> unberührt und wurden übernommen.
Nicht mitgekommen sind 13380 Treffer, die es nur in der alten Datenbank gab,
darunter 92 als *reviewed* und 5 als *applied* markierte. Ein Nachimport hätte
mit den vorhandenen Ids in `perinco_scanner` kollidiert, und der Bestand war
ohnehin flüchtig - der Aufräumjob hält nur die jüngsten Tage. Ein Dump liegt
unter `/root/backups/project_scanner_20260909-173447.sql.gz`.
### 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 fällt kein zweiter Browser-Download an und
gescrapt wird mit der Version, die dort produktiv funktioniert. Ein `^` in der
Version würde beides aufgeben, deshalb steht in `server/package.json` ein
`comments`-Feld mit dieser Begründung.
`playwright` ist auf **exakt 1.60.0** gepinnt. Der passende Chromium-Build
(`chromium-1223`) liegt auf dem Server 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 würde beides aufgeben,
deshalb steht in `server/package.json` ein `comments`-Feld mit dieser
Begründung.
> **Nicht mit aufräumen:** Dieser Cache (641 MB) stammt aus der Installation der
> alten Scanner-App, liegt aber ausserhalb beider Anwendungsverzeichnisse und
> wurde von beiden benutzt - `PLAYWRIGHT_BROWSERS_PATH` ist nirgends gesetzt.
> Beim Rückbau am 09.09.2026 ist er deshalb bewusst stehen geblieben. Wer ihn
> löscht, nimmt dem Scanner den Browser; Reparatur:
> `cd /var/www/perinco_CRM/server && npx playwright install chromium`.
## Diktat (Whisper im Browser)
@@ -601,16 +574,10 @@ Ein Rollout mit neuen Backend-Paketen braucht `-Deps`.
### Einmalige Scanner-Einrichtung (am 06.08.2026 erledigt)
```bash
# 1. .env ergänzen: erzeugt SCANNER_ENCRYPTION_KEY, holt die LEGACY_*-Werte
# aus "/var/www/Projekt Scanner/backend/.env", legt vorher ein .env.bak an
# 1. .env ergänzen: erzeugt SCANNER_ENCRYPTION_KEY, legt vorher ein .env.bak an
bash /tmp/scanner-setup.sh # aus deploy/scanner-setup.sh
# 2. Bestand übernehmen
cd /var/www/perinco_CRM/server
npm run scanner:import -- --dry-run
npm run scanner:import -- --with-matches
# 3. MemoryMax anheben und neu starten
# 2. MemoryMax anheben und neu starten
cp /var/www/perinco_CRM/deploy/perinco-crm-backend.service /etc/systemd/system/
systemctl daemon-reload && systemctl restart perinco-crm-backend
journalctl -u perinco-crm-backend -n 40
@@ -618,26 +585,14 @@ journalctl -u perinco-crm-backend -n 40
Die Datenbank `perinco_scanner` muss **nicht** von Hand angelegt werden, das
erledigt das Backend beim Start. Playwright-Browser ebenfalls nicht, siehe
Abschnitt *Playwright* oben.
> **Reihenfolge beachten:** Der Import schreibt `search_interval` aus der alten
> App (15 Minuten) in die Einstellungen. Der laufende Scheduler bemerkt die
> Ä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.
Abschnitt *Playwright* oben. Der damalige Schritt *Bestand übernehmen*
(`npm run scanner:import`) ist mit dem Rückbau der alten App entfallen.
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 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.
Manuell entspricht das:
```powershell