From a866cd9a52c07917795f361ea1981354bb5e1bc9 Mon Sep 17 00:00:00 2001 From: Sascha Wolf Date: Wed, 9 Sep 2026 08:57:21 +0200 Subject: [PATCH] feat(scanner): Agenten nachtraeglich uebernehmen mit --agents-only und --skip Der Legacy-Import war ein Erststart-Werkzeug: er brach bei vorhandenen Agenten ab und schrieb immer auch Einstellungen und Portale mit. Um spaeter einzelne Suchagenten aus project_scanner nachzuziehen, gab es keinen Weg. --agents-only laesst Einstellungen, Portale, Projekte und Treffer unberuehrt und laeuft additiv: vorhandene Agenten blockieren nicht mehr, sondern werden am Namen erkannt und uebersprungen, damit im CRM bearbeitete Agenten nicht ueberschrieben werden. Die Id der Quelle wird nur uebernommen, solange sie im Ziel frei ist. --skip=Name[,Name] laesst Agenten nach Namen aus, ohne Ruecksicht auf Gross- und Kleinschreibung. --force ist mit --agents-only gesperrt: es wuerde Projekte und Treffer loeschen, die der Lauf gar nicht anfasst. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 5 ++ docs/03-scanner.md | 20 ++++++++ server/src/scanner/importLegacy.js | 74 +++++++++++++++++++++++++----- 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1de1848..832ea72 100644 --- a/README.md +++ b/README.md @@ -321,8 +321,13 @@ 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 ``` +`--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 | diff --git a/docs/03-scanner.md b/docs/03-scanner.md index 0598795..3f7e82a 100644 --- a/docs/03-scanner.md +++ b/docs/03-scanner.md @@ -220,6 +220,26 @@ 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 diff --git a/server/src/scanner/importLegacy.js b/server/src/scanner/importLegacy.js index 58af906..6250631 100644 --- a/server/src/scanner/importLegacy.js +++ b/server/src/scanner/importLegacy.js @@ -8,6 +8,8 @@ * 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 @@ -22,6 +24,11 @@ * 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'; @@ -42,6 +49,14 @@ 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 || ''; @@ -144,17 +159,39 @@ async function importAgents(legacy, uploadDir) { FROM agents ORDER BY id` ); + let created = 0; + let skipped = 0; + for (const row of rows) { - const cvFilePath = copyCv(row.cv_file_path, uploadDir); - if (dryRun) { - log(` wuerde Agent ${row.id} "${row.name}" anlegen`); + if (skipNames.includes((row.name || '').trim().toLowerCase())) { + log(` Agent "${row.name}" per --skip ausgelassen.`); + skipped++; continue; } - // Ids bleiben erhalten, damit uebernommene Treffer weiter passen. + // 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( { - id: row.id, + ...(idTaken ? {} : { id: row.id }), name: row.name, email: row.email, cvText: row.cv_text, @@ -167,9 +204,10 @@ async function importAgents(legacy, uploadDir) { }, { silent: true } ); + created++; } - log(`Suchagenten: ${rows.length} uebernommen.`); + log(`Suchagenten: ${created} uebernommen, ${skipped} uebersprungen (${rows.length} in der Quelle).`); } async function importProjectsAndMatches(legacy) { @@ -246,17 +284,25 @@ async function main() { 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) { + if (existingAgents > 0 && !force && !agentsOnly) { log( `Abbruch: es gibt hier schon ${existingAgents} Suchagenten. Mit --force werden Agenten, ` + - 'Projekte und Treffer vorher geloescht.' + '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 }); @@ -265,10 +311,16 @@ async function main() { log('Vorhandene Agenten, Projekte und Treffer geloescht.'); } - await importSettings(legacy); - await importPortals(legacy); + if (agentsOnly) { + log('Nur Suchagenten - Einstellungen und Portale bleiben unberuehrt.'); + } else { + await importSettings(legacy); + await importPortals(legacy); + } + await importAgents(legacy, uploadDir); - if (withMatches) { + + if (withMatches && !agentsOnly) { await importProjectsAndMatches(legacy); } else { log('Projekte und Treffer uebersprungen (--with-matches aktiviert sie).');