#!/usr/bin/env node // Fügt Orte aus einem KI-Import-JSON (Format wie in anforderung.md // "Vorgabe für das KI-JSON" beschrieben, z.B. Export aus dem // Prompt-Generator) dauerhaft zu data/seed.js hinzu. // // Nutzung: // node scripts/add-seed-places.js [--dry-run] // // Lädt die echten App-Module (normalizePlace, Kategorie-Auflösung per // Label, generateId) per vm-Sandbox aus js/, statt deren Logik hier // erneut nachzubauen - so bleibt das Skript automatisch konsistent mit // der App, auch wenn sich Kategorien oder Default-Felder dort ändern. // Neue Einträge werden als Text vor dem abschließenden "];" eingefügt; // bestehende Einträge in seed.js bleiben dabei byteidentisch erhalten. const fs = require('fs'); const path = require('path'); const vm = require('vm'); const crypto = require('crypto'); const ROOT = path.join(__dirname, '..'); const SEED_PATH = path.join(ROOT, 'data', 'seed.js'); function loadAppSandbox() { // window muss der Kontext-Global selbst sein (nicht nur eine Property // darauf), damit die App-Dateien - die im Browser als Sloppy-Mode-Scripts // laufen und "EventMap" bar (ohne "window.") referenzieren - dieselbe // globale Bindung sehen wie "window.EventMap". const sandbox = { crypto: { randomUUID: crypto.randomUUID } }; sandbox.window = sandbox; vm.createContext(sandbox); // Reihenfolge muss der Abhängigkeitskette aus index.html entsprechen. const files = [ 'js/utils/dom.js', 'js/utils/id.js', 'js/config/categories.js', 'js/models/schema.js', 'js/models/place.js', ]; for (const file of files) { const code = fs.readFileSync(path.join(ROOT, file), 'utf8'); vm.runInContext(code, sandbox, { filename: file }); } const seedCode = fs.readFileSync(SEED_PATH, 'utf8'); vm.runInContext(seedCode, sandbox, { filename: 'data/seed.js' }); return sandbox.EventMap; } function inlineTags(tags) { return '[' + tags.map((t) => JSON.stringify(t)).join(', ') + ']'; } /** Formatiert einen normalisierten Place exakt im Stil der bestehenden seed.js-Einträge. */ function formatPlace(p) { const lines = [ ` "id": ${JSON.stringify(p.id)},`, ` "name": ${JSON.stringify(p.name)},`, ` "category": ${JSON.stringify(p.category)},`, ` "shortDescription": ${JSON.stringify(p.shortDescription)},`, ` "description": ${JSON.stringify(p.description)},`, ` "latitude": ${JSON.stringify(p.latitude)},`, ` "longitude": ${JSON.stringify(p.longitude)},`, ` "address": ${JSON.stringify(p.address)},`, ` "city": ${JSON.stringify(p.city)},`, ` "region": ${JSON.stringify(p.region)},`, ` "ageRecommendation": { "min": ${JSON.stringify(p.ageRecommendation.min)}, "max": ${JSON.stringify(p.ageRecommendation.max)} },`, ` "cost": { "type": ${JSON.stringify(p.cost.type)}, "description": ${JSON.stringify(p.cost.description)} },`, ` "openingHours": ${JSON.stringify(p.openingHours)},`, ` "durationMinutes": ${JSON.stringify(p.durationMinutes)},`, ` "facilities": { "strollerAccessible": ${p.facilities.strollerAccessible}, "wheelchairAccessible": ${p.facilities.wheelchairAccessible}, "toilets": ${p.facilities.toilets}, "parking": ${p.facilities.parking}, "restaurant": ${p.facilities.restaurant} },`, ` "environment": ${JSON.stringify(p.environment)},`, ` "website": ${JSON.stringify(p.website)},`, ` "image": ${JSON.stringify(p.image)},`, ` "rating": ${JSON.stringify(p.rating)},`, ` "tags": ${inlineTags(p.tags)},`, ` "source": ${JSON.stringify(p.source)},`, ` "sourceVerified": ${p.sourceVerified},`, ` "createdAt": ${JSON.stringify(p.createdAt)},`, ` "updatedAt": ${JSON.stringify(p.updatedAt)}`, ]; return ' {\n' + lines.join('\n') + '\n }'; } function main() { const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const inputPath = args.find((a) => !a.startsWith('--')); if (!inputPath) { console.error('Nutzung: node scripts/add-seed-places.js [--dry-run]'); process.exit(1); } const EventMap = loadAppSandbox(); const existingPlaces = EventMap.data.SEED_PLACES; const existingNames = new Set(existingPlaces.map((p) => p.name.trim().toLowerCase())); const inputRaw = fs.readFileSync(path.resolve(inputPath), 'utf8'); const input = JSON.parse(inputRaw); const candidates = Array.isArray(input.places) ? input.places : []; if (candidates.length === 0) { console.error('Keine "places" im angegebenen JSON gefunden.'); process.exit(1); } const toAdd = []; const skipped = []; for (const raw of candidates) { const key = String(raw.name || '').trim().toLowerCase(); if (!key || existingNames.has(key)) { skipped.push(raw.name || '(ohne Namen)'); continue; } const normalized = EventMap.models.normalizePlace(raw); toAdd.push(normalized); existingNames.add(key); // schützt auch vor Duplikaten innerhalb derselben Eingabedatei } console.log(`Gefunden: ${candidates.length}, neu: ${toAdd.length}, übersprungen (Duplikat/Name fehlt): ${skipped.length}`); if (skipped.length > 0) { console.log('Übersprungen:', skipped.join(', ')); } if (toAdd.length === 0) { console.log('Nichts zu tun.'); return; } const seedText = fs.readFileSync(SEED_PATH, 'utf8'); const closingIndex = seedText.lastIndexOf('\n];'); if (closingIndex === -1) { console.error('Konnte das Ende von EventMap.data.SEED_PLACES in seed.js nicht finden.'); process.exit(1); } const before = seedText.slice(0, closingIndex); const after = seedText.slice(closingIndex); // beginnt mit "\n];" const newBlocks = toAdd.map(formatPlace).join(',\n'); const updated = `${before},\n${newBlocks}${after}`; if (dryRun) { console.log('--dry-run: seed.js wird nicht verändert. Neue Blöcke:\n'); console.log(newBlocks); return; } fs.writeFileSync(SEED_PATH, updated, 'utf8'); console.log(`data/seed.js aktualisiert: ${toAdd.length} neue Einträge hinzugefügt.`); console.log(toAdd.map((p) => ` - ${p.name}`).join('\n')); } main();