From 3c9d83161efd9f71a445fe3f696ea42a2b4c5380 Mon Sep 17 00:00:00 2001 From: Sascha Wolf Date: Wed, 9 Sep 2026 17:45:42 +0200 Subject: [PATCH] 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) --- README.md | 167 ++++++-------- deploy/scanner-setup.sh | 57 +---- docs/01-architektur.md | 8 - docs/03-scanner.md | 140 +++++------- docs/06-betrieb.md | 38 ++-- docs/08-anleitung-entwickler.md | 1 - docs/09-quellcode.md | 1 - docs/README.md | 13 +- server/.env.example | 39 ++-- server/package.json | 1 - server/src/scanner/importLegacy.js | 339 ----------------------------- 11 files changed, 158 insertions(+), 646 deletions(-) delete mode 100644 server/src/scanner/importLegacy.js diff --git a/README.md b/README.md index 832ea72..ccde65a 100644 --- a/README.md +++ b/README.md @@ -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 = `/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 = `/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 diff --git a/deploy/scanner-setup.sh b/deploy/scanner-setup.sh index d4e42da..e1de459 100644 --- a/deploy/scanner-setup.sh +++ b/deploy/scanner-setup.sh @@ -2,23 +2,22 @@ # --------------------------------------------------------------------------- # Einmalige Einrichtung des portierten Projekt Scanners im CRM-Backend. # -# Ergaenzt /var/www/perinco_CRM/server/.env um die SCANNER_*- und LEGACY_*- -# Variablen. Idempotent: vorhandene Werte werden nicht ueberschrieben, der +# Ergaenzt /var/www/perinco_CRM/server/.env um die SCANNER_*-Variablen. +# Idempotent: vorhandene Werte werden nicht ueberschrieben, der # Verschluesselungsschluessel wird nur erzeugt, wenn noch keiner drinsteht. # # Geheimnisse werden nie ausgegeben - nur, ob ein Wert gesetzt ist. # +# Die eigenstaendige Vorgaenger-App (scanner.perinco.de, Port 5005, Datenbank +# project_scanner) wurde am 09.09.2026 zurueckgebaut. Die frueher hier +# gesetzten LEGACY_*-Variablen und das Importskript sind damit entfallen. +# # Aufruf auf dem Server: # bash /tmp/scanner-setup.sh # --------------------------------------------------------------------------- set -euo pipefail CRM_ENV=/var/www/perinco_CRM/server/.env -# Achtung: der Pfad der laufenden Scanner-App enthaelt ein Leerzeichen, deshalb -# ueberall in Anfuehrungszeichen. Die Unit heisst project-scanner-backend und -# hoert auf Port 5005. -LEGACY_DIR="/var/www/Projekt Scanner/backend" -LEGACY_ENV="$LEGACY_DIR/.env" if [ ! -f "$CRM_ENV" ]; then echo "FEHLER: $CRM_ENV fehlt." @@ -30,9 +29,8 @@ cp -a "$CRM_ENV" "$CRM_ENV.bak.$(date +%Y%m%d%H%M%S)" # Wert nur setzen, wenn der Schluessel fehlt oder leer ist. # # Werte mit Leerzeichen werden gequotet: systemd liest diese Datei als -# EnvironmentFile und trennt dort an Leerzeichen - ein ungequoteter Pfad wie -# "/var/www/Projekt Scanner/backend/uploads" kaeme abgeschnitten an. dotenv -# entfernt die Quotes beim Einlesen wieder. +# EnvironmentFile und trennt dort an Leerzeichen - ein ungequoteter Pfad kaeme +# abgeschnitten an. dotenv entfernt die Quotes beim Einlesen wieder. upsert() { local key="$1" value="$2" if grep -qE "^${key}=.+" "$CRM_ENV"; then @@ -48,13 +46,6 @@ upsert() { echo " $key: gesetzt" } -# Aus der alten .env lesen, ohne sie in die eigene Umgebung zu uebernehmen. -legacy_value() { - local key="$1" - [ -f "$LEGACY_ENV" ] || return 0 - sed -n "s/^${key}=//p" "$LEGACY_ENV" | tail -n1 | tr -d '\r' -} - echo "== Scanner-Konfiguration in $CRM_ENV ==" # Kopfzeile nur beim ersten Lauf, sonst sammelt sich bei jedem Aufruf eine an. @@ -65,7 +56,7 @@ fi upsert SCANNER_DB_NAME perinco_scanner upsert SCANNER_UPLOAD_DIR /var/www/perinco_CRM/server/uploads upsert SCANNER_SCHEDULER true -upsert SCANNER_SCHEDULER_OFFSET 30 +upsert SCANNER_SCHEDULER_OFFSET 0 if grep -qE '^SCANNER_ENCRYPTION_KEY=.+' "$CRM_ENV"; then echo " SCANNER_ENCRYPTION_KEY: bleibt unveraendert" @@ -75,35 +66,9 @@ else echo " SCANNER_ENCRYPTION_KEY: neu erzeugt (64 Hex-Zeichen)" fi -echo "== Uebernahme aus der alten App ==" - -if [ ! -f "$LEGACY_ENV" ]; then - echo " $LEGACY_ENV nicht gefunden - LEGACY_* bleibt leer, Import faellt aus." -else - LEGACY_DB_NAME_VALUE="$(legacy_value DB_NAME)" - LEGACY_DB_USER_VALUE="$(legacy_value DB_USER)" - LEGACY_DB_PASSWORD_VALUE="$(legacy_value DB_PASSWORD)" - LEGACY_DB_HOST_VALUE="$(legacy_value DB_HOST)" - LEGACY_DB_PORT_VALUE="$(legacy_value DB_PORT)" - LEGACY_KEY_VALUE="$(legacy_value ENCRYPTION_KEY)" - - upsert LEGACY_DB_NAME "${LEGACY_DB_NAME_VALUE:-project_scanner}" - upsert LEGACY_DB_HOST "${LEGACY_DB_HOST_VALUE:-localhost}" - upsert LEGACY_DB_PORT "${LEGACY_DB_PORT_VALUE:-3306}" - upsert LEGACY_DB_USER "${LEGACY_DB_USER_VALUE:-root}" - upsert LEGACY_DB_PASSWORD "${LEGACY_DB_PASSWORD_VALUE:-}" - upsert LEGACY_UPLOAD_DIR "$LEGACY_DIR/uploads" - - if [ -n "$LEGACY_KEY_VALUE" ]; then - upsert LEGACY_ENCRYPTION_KEY "$LEGACY_KEY_VALUE" - else - echo " LEGACY_ENCRYPTION_KEY: in der alten .env nicht gefunden - Portal-Passwoerter werden nicht uebernommen" - fi -fi - chmod 600 "$CRM_ENV" echo "== Ergebnis (Werte maskiert) ==" -grep -E '^(SCANNER|LEGACY)_' "$CRM_ENV" | sed -E 's/=(.+)/= /' | sort -u +grep -E '^SCANNER_' "$CRM_ENV" | sed -E 's/=(.+)/= /' | sort -u -echo "Fertig." +echo "Fertig. Danach: systemctl restart perinco-crm-backend" diff --git a/docs/01-architektur.md b/docs/01-architektur.md index 9684537..5e6b028 100644 --- a/docs/01-architektur.md +++ b/docs/01-architektur.md @@ -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
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 | diff --git a/docs/03-scanner.md b/docs/03-scanner.md index 3f7e82a..8251c0b 100644 --- a/docs/03-scanner.md +++ b/docs/03-scanner.md @@ -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
/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
/api/scanner"] --> N2[("perinco_scanner")] - N1 --> N3["uploads/ in
/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
inkl. OpenAI-Key"] - E --> P["Portale über den Namen zuordnen:
Benutzername, Session-Cookies, Status"] - P --> PW{"LEGACY_ENCRYPTION_KEY gesetzt?"} - 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 - A --> M{"--with-matches?"} - M -->|ja| M2["Projekte und Treffer
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 diff --git a/docs/06-betrieb.md b/docs/06-betrieb.md index 1187f4f..34d0223 100644 --- a/docs/06-betrieb.md +++ b/docs/06-betrieb.md @@ -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 = `/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 diff --git a/docs/08-anleitung-entwickler.md b/docs/08-anleitung-entwickler.md index b078946..7cc66db 100644 --- a/docs/08-anleitung-entwickler.md +++ b/docs/08-anleitung-entwickler.md @@ -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 `` 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. | diff --git a/docs/09-quellcode.md b/docs/09-quellcode.md index 81896b8..576c795 100644 --- a/docs/09-quellcode.md +++ b/docs/09-quellcode.md @@ -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: diff --git a/docs/README.md b/docs/README.md index 69f04ac..ad0bc3c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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
Chromium"] DB1[("MariaDB
perinco_CRM")] DB2[("MariaDB
perinco_scanner")] - ALT["alte Scanner-App
:5005"] - DB3[("MariaDB
project_scanner")] end OAI["OpenAI
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. diff --git a/server/.env.example b/server/.env.example index c68f8f4..be461b6 100644 --- a/server/.env.example +++ b/server/.env.example @@ -62,48 +62,35 @@ MAIL_SYNC=true # MAIL_MAX_BODY_CHARS=400000 # --------------------------------------------------------------------------- -# Projekt Scanner (portiert, laeuft parallel zur alten App) +# Projekt Scanner (portiert) # --------------------------------------------------------------------------- -# Diese Instanz ist vollstaendig eigenstaendig: eigene Datenbank, eigener -# Schluessel, eigenes Upload-Verzeichnis, eigener Scheduler. Sie fasst die Daten -# des alten Scanners nicht an. +# Eigene Datenbank, eigener Schluessel, eigenes Upload-Verzeichnis, eigener +# Scheduler. Die eigenstaendige Vorgaenger-App wurde am 09.09.2026 +# zurueckgebaut, es gibt keine zweite Instanz mehr. -# Eigene Datenbank - wird beim Start automatisch angelegt, falls sie fehlt. -# NICHT auf project_scanner zeigen lassen, das ist die Datenbank der alten App. +# Wird beim Start automatisch angelegt, falls sie fehlt. SCANNER_DB_NAME=perinco_scanner # SCANNER_DB_HOST=localhost # SCANNER_DB_PORT=3306 # SCANNER_DB_USER=root # SCANNER_DB_PASSWORD= -# Eigener Schluessel fuer die Portal-Passwoerter, unabhaengig von der alten App. +# Schluessel fuer die Portal-Passwoerter (AES). # Langer Zufallswert, z. B.: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" SCANNER_ENCRYPTION_KEY= -# Eigenes Verzeichnis fuer die CV-PDFs. Leer = /uploads. +# Verzeichnis fuer die CV-PDFs. Leer = /uploads. SCANNER_UPLOAD_DIR= # Hintergrund-Scraper (Playwright + OpenAI). Standard ist AN; nur "false" -# schaltet ihn ab. Achtung: solange die alte App laeuft, bewerten beide Instanzen -# dieselben Projekte unabhaengig voneinander - die OpenAI-Kosten fallen doppelt an. +# schaltet ihn ab. SCANNER_SCHEDULER=true -# Minuten Versatz fuer den allerersten Lauf dieser Instanz. Verhindert, dass -# beide Scanner gleichzeitig mit denselben Portal-Sessions losziehen (CAPTCHA). -# Greift nur, solange noch kein Lauf stattgefunden hat - danach traegt der in der -# Datenbank gespeicherte Zeitpunkt (last_scan_at) den Zeitplan ueber Neustarts -# hinweg. 0 = sofort loslegen. -SCANNER_SCHEDULER_OFFSET=30 +# Minuten Versatz fuer den allerersten Lauf. Stammt aus dem Parallelbetrieb mit +# der alten App und ist seit deren Rueckbau ohne Wirkung, solange in der +# Datenbank ein last_scan_at steht - der traegt den Zeitplan ueber Neustarts. +# 0 = sofort loslegen. +SCANNER_SCHEDULER_OFFSET=0 # Fallback fuer den OpenAI-Key; bevorzugt wird der Wert aus den Einstellungen. # OPENAI_API_KEY= - -# --- Einmalige Uebernahme aus der alten Scanner-Datenbank --- -# npm run scanner:import [-- --with-matches] [-- --force] [-- --dry-run] -LEGACY_DB_NAME=project_scanner -# LEGACY_DB_HOST / _PORT / _USER / _PASSWORD (leer = CRM-Werte von oben) -# Verzeichnis der CV-PDFs der alten App - von dort werden die Dateien kopiert. -# LEGACY_UPLOAD_DIR=/opt/project-scanner/backend/uploads -# ENCRYPTION_KEY der alten App - nur damit koennen die Portal-Passwoerter -# uebernommen (und hier neu verschluesselt) werden. -# LEGACY_ENCRYPTION_KEY= diff --git a/server/package.json b/server/package.json index 7e9c18f..8d357a2 100644 --- a/server/package.json +++ b/server/package.json @@ -12,7 +12,6 @@ "start": "node src/server.js", "dev": "node --watch src/server.js", "migrate": "node src/migrate.js", - "scanner:import": "node src/scanner/importLegacy.js", "test": "node test/mail.test.mjs && node test/acquisition.test.mjs && node test/userFields.test.mjs" }, "dependencies": { diff --git a/server/src/scanner/importLegacy.js b/server/src/scanner/importLegacy.js deleted file mode 100644 index 6250631..0000000 --- a/server/src/scanner/importLegacy.js +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Einmalige Uebernahme aus der alten Projekt-Scanner-Datenbank in die eigene - * Datenbank dieser Instanz. Beide Scanner laufen danach parallel und getrennt - - * dieses Skript kopiert nur, es veraendert die Quelle nicht. - * - * Aufruf (aus server/): - * npm run scanner:import - * npm run scanner:import -- --with-matches auch Projekte und Treffer - * npm run scanner:import -- --force vorhandene Zieldaten ersetzen - * npm run scanner:import -- --dry-run nur anzeigen, nichts schreiben - * npm run scanner:import -- --agents-only nur Suchagenten, sonst nichts - * npm run scanner:import -- --skip=Bilal,Nico Agenten nach Namen auslassen - * - * Quelle konfigurieren (server/.env): - * LEGACY_DB_NAME=project_scanner - * LEGACY_DB_HOST / _PORT / _USER / _PASSWORD (leer = CRM-Werte) - * LEGACY_UPLOAD_DIR=/opt/project-scanner/backend/uploads - * LEGACY_ENCRYPTION_KEY=... Portal-Passwoerter mit uebernehmen - * - * Was uebernommen wird: - * - Einstellungen (inkl. OpenAI-Key, SMTP-Zugang, Schwellenwerte) - * - Portale: Benutzername, Session-Cookies, Status; Passwoerter nur mit - * LEGACY_ENCRYPTION_KEY, weil sie mit dem alten Schluessel verschluesselt - * sind und hier neu verschluesselt werden muessen - * - Suchagenten inkl. CV-Text und CV-PDF (Datei wird kopiert) - * - mit --with-matches zusaetzlich Projekte und Treffer - * - * --agents-only laesst Einstellungen und Portale unberuehrt und laeuft - * nachtraeglich: vorhandene Agenten blockieren nicht mehr, sondern werden am - * Namen erkannt und uebersprungen. Die Ids der Quelle werden nur uebernommen, - * solange sie hier frei sind - sonst vergibt die Datenbank eine neue. - */ -import 'dotenv/config'; -import fs from 'node:fs'; -import path from 'node:path'; -import mysql from 'mysql2/promise'; -import { - Agent, - Match, - Portal, - Project, - Setting, - initScannerDatabase, -} from './models/index.js'; -import { encrypt, decryptWith } from './utils/crypto.js'; -import { ensureUploadDir } from './uploadDir.js'; - -const args = process.argv.slice(2); -const withMatches = args.includes('--with-matches'); -const force = args.includes('--force'); -const dryRun = args.includes('--dry-run'); -const agentsOnly = args.includes('--agents-only'); - -// --skip=Bilal oder --skip=Bilal,Nico - Vergleich ohne Gross-/Kleinschreibung. -const skipNames = args - .filter((arg) => arg.startsWith('--skip=')) - .flatMap((arg) => arg.slice('--skip='.length).split(',')) - .map((name) => name.trim().toLowerCase()) - .filter(Boolean); - -const LEGACY_UPLOAD_DIR = process.env.LEGACY_UPLOAD_DIR || ''; -const LEGACY_KEY = process.env.LEGACY_ENCRYPTION_KEY || ''; - -const log = (message) => console.log(`[Import] ${message}`); - -async function openLegacy() { - const database = process.env.LEGACY_DB_NAME || 'project_scanner'; - const connection = await mysql.createConnection({ - host: process.env.LEGACY_DB_HOST || process.env.DB_HOST || 'localhost', - port: Number(process.env.LEGACY_DB_PORT || process.env.DB_PORT || 3306), - user: process.env.LEGACY_DB_USER || process.env.DB_USER || 'root', - password: process.env.LEGACY_DB_PASSWORD || process.env.DB_PASSWORD || '', - database, - dateStrings: false, - }); - log(`Quelle: ${database}`); - return connection; -} - -/** Kopiert das CV-PDF in das eigene Upload-Verzeichnis. */ -function copyCv(storedPath, uploadDir) { - if (!storedPath) return ''; - - const fileName = path.basename(storedPath); - const candidates = [ - path.isAbsolute(storedPath) ? storedPath : null, - LEGACY_UPLOAD_DIR ? path.join(LEGACY_UPLOAD_DIR, fileName) : null, - path.resolve(storedPath), - ].filter(Boolean); - - const source = candidates.find((candidate) => fs.existsSync(candidate)); - if (!source) { - log(` CV-Datei nicht gefunden: ${storedPath} (LEGACY_UPLOAD_DIR setzen?)`); - return ''; - } - - const target = path.join(uploadDir, fileName); - if (!dryRun) fs.copyFileSync(source, target); - return target; -} - -async function importSettings(legacy) { - const [rows] = await legacy.query('SELECT key_name, value FROM settings'); - for (const row of rows) { - if (!dryRun) await Setting.setVal(row.key_name, row.value ?? ''); - } - log(`Einstellungen: ${rows.length} Schluessel uebernommen.`); -} - -async function importPortals(legacy) { - const [rows] = await legacy.query( - 'SELECT name, username, password_encrypted, session_state, connection_status, last_login_attempt FROM portals' - ); - - let withSession = 0; - let withPassword = 0; - - for (const row of rows) { - // Portale sind bei der Initialisierung schon angelegt - hier wird nur der - // Inhalt nachgezogen, ueber den Namen zugeordnet. - const portal = await Portal.findOne({ where: { name: row.name } }); - if (!portal) { - log(` Portal ${row.name} gibt es hier nicht - uebersprungen.`); - continue; - } - - portal.username = row.username || portal.username; - if (row.session_state) { - portal.sessionState = row.session_state; - portal.connectionStatus = row.connection_status || 'connected'; - portal.lastLoginAttempt = row.last_login_attempt || new Date(); - withSession++; - } - - if (row.password_encrypted && LEGACY_KEY) { - const plain = decryptWith(row.password_encrypted, LEGACY_KEY); - if (plain) { - // Neu verschluesseln - diese Instanz hat ihren eigenen Schluessel. - portal.passwordEncrypted = encrypt(plain); - withPassword++; - } else { - log(` Passwort von ${row.name} liess sich mit LEGACY_ENCRYPTION_KEY nicht lesen.`); - } - } - - if (!dryRun) await portal.save(); - } - - log(`Portale: ${rows.length} geprueft, ${withSession} mit Session, ${withPassword} mit Passwort.`); - if (!LEGACY_KEY) { - log(' Hinweis: ohne LEGACY_ENCRYPTION_KEY bleiben die Portal-Passwoerter leer.'); - } -} - -async function importAgents(legacy, uploadDir) { - const [rows] = await legacy.query( - `SELECT id, name, email, cv_text, cv_file_path, keywords, exclude_keywords, is_active, - created_at, updated_at - FROM agents ORDER BY id` - ); - - let created = 0; - let skipped = 0; - - for (const row of rows) { - if (skipNames.includes((row.name || '').trim().toLowerCase())) { - log(` Agent "${row.name}" per --skip ausgelassen.`); - skipped++; - continue; - } - - // Nachtraegliche Uebernahme: was hier schon unter dem Namen steht, bleibt - // unangetastet - der Agent koennte hier inzwischen bearbeitet worden sein. - const existing = await Agent.findOne({ where: { name: row.name } }); - if (existing) { - log(` Agent "${row.name}" gibt es hier schon (Id ${existing.id}) - uebersprungen.`); - skipped++; - continue; - } - - if (dryRun) { - log(` wuerde Agent ${row.id} "${row.name}" anlegen`); - created++; - continue; - } - - const cvFilePath = copyCv(row.cv_file_path, uploadDir); - - // Die Id der Quelle bleibt erhalten, damit uebernommene Treffer weiter - // passen - aber nur, wenn sie hier noch frei ist. - const idTaken = await Agent.findByPk(row.id); - await Agent.create( - { - ...(idTaken ? {} : { id: row.id }), - name: row.name, - email: row.email, - cvText: row.cv_text, - cvFilePath, - keywords: row.keywords, - excludeKeywords: row.exclude_keywords, - isActive: Boolean(row.is_active), - createdAt: row.created_at, - updatedAt: row.updated_at, - }, - { silent: true } - ); - created++; - } - - log(`Suchagenten: ${created} uebernommen, ${skipped} uebersprungen (${rows.length} in der Quelle).`); -} - -async function importProjectsAndMatches(legacy) { - const [projects] = await legacy.query( - `SELECT id, portal, portal_project_id, title, description, company, location, rate, url, - published_at, created_at - FROM projects ORDER BY id` - ); - - if (!dryRun) { - for (const row of projects) { - await Project.create( - { - id: row.id, - portal: row.portal, - portalProjectId: row.portal_project_id, - title: row.title, - description: row.description, - company: row.company, - location: row.location, - rate: row.rate, - url: row.url, - publishedAt: row.published_at, - createdAt: row.created_at, - }, - { silent: true } - ); - } - } - log(`Projekte: ${projects.length} uebernommen.`); - - const [matches] = await legacy.query( - `SELECT id, agent_id, project_id, match_percentage, pros, cons, status, notification_sent, - created_at, updated_at - FROM matches ORDER BY id` - ); - - const parseList = (value) => { - if (!value) return []; - try { - const parsed = JSON.parse(value); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } - }; - - if (!dryRun) { - for (const row of matches) { - await Match.create( - { - id: row.id, - agentId: row.agent_id, - projectId: row.project_id, - matchPercentage: row.match_percentage, - pros: parseList(row.pros), - cons: parseList(row.cons), - status: row.status, - notificationSent: Boolean(row.notification_sent), - createdAt: row.created_at, - updatedAt: row.updated_at, - }, - { silent: true } - ); - } - } - log(`Treffer: ${matches.length} uebernommen.`); -} - -async function main() { - if (dryRun) log('Probelauf - es wird nichts geschrieben.'); - - await initScannerDatabase(); - const uploadDir = ensureUploadDir(); - log(`Ziel-Upload-Verzeichnis: ${uploadDir}`); - - // Bei --agents-only ist der Lauf nachtraeglich gedacht und laeuft additiv: - // vorhandene Agenten sind kein Grund zum Abbruch, sie werden in - // importAgents am Namen erkannt. - const existingAgents = await Agent.count(); - if (existingAgents > 0 && !force && !agentsOnly) { - log( - `Abbruch: es gibt hier schon ${existingAgents} Suchagenten. Mit --force werden Agenten, ` + - 'Projekte und Treffer vorher geloescht, mit --agents-only kommen nur die fehlenden dazu.' - ); - process.exit(1); - } - - const legacy = await openLegacy(); - try { - if (force && agentsOnly) { - log('Abbruch: --force und --agents-only zusammen ergeben keinen Sinn.'); - process.exit(1); - } - - if (force && !dryRun) { - // Reihenfolge wegen der Fremdschluessel: Treffer, dann Projekte, dann Agenten. - await Match.destroy({ where: {}, truncate: false }); - await Project.destroy({ where: {}, truncate: false }); - await Agent.destroy({ where: {}, truncate: false }); - log('Vorhandene Agenten, Projekte und Treffer geloescht.'); - } - - if (agentsOnly) { - log('Nur Suchagenten - Einstellungen und Portale bleiben unberuehrt.'); - } else { - await importSettings(legacy); - await importPortals(legacy); - } - - await importAgents(legacy, uploadDir); - - if (withMatches && !agentsOnly) { - await importProjectsAndMatches(legacy); - } else { - log('Projekte und Treffer uebersprungen (--with-matches aktiviert sie).'); - } - } finally { - await legacy.end(); - } - - log('Fertig.'); - process.exit(0); -} - -main().catch((err) => { - console.error('[Import] Fehlgeschlagen:', err.message); - process.exit(1); -});