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:
co-authored by
Claude Opus 5
parent
ab6b9f2372
commit
3c9d83161e
@@ -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
|
||||
|
||||
+11
-46
@@ -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/=(.+)/= <gesetzt>/' | sort -u
|
||||
grep -E '^SCANNER_' "$CRM_ENV" | sed -E 's/=(.+)/= <gesetzt>/' | sort -u
|
||||
|
||||
echo "Fertig."
|
||||
echo "Fertig. Danach: systemctl restart perinco-crm-backend"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
+13
-26
@@ -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 = <server>/uploads.
|
||||
# Verzeichnis fuer die CV-PDFs. Leer = <server>/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=
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user