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).');