Weitere Ausflugsziele in Seed-Daten ergaenzt, Skript fuer KI-Import hinzugefuegt

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 11:27:02 +02:00
co-authored by Claude Sonnet 5
parent 40e488452e
commit 8e2beaebf7
2 changed files with 340 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
#!/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 <pfad-zur-ki-datei.json> [--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 <pfad-zur-ki-datei.json> [--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();