From 8e2beaebf715e37ea8a0a6b0dca4f8de1df0b887 Mon Sep 17 00:00:00 2001 From: Hermann Date: Tue, 28 Jul 2026 11:27:02 +0200 Subject: [PATCH] Weitere Ausflugsziele in Seed-Daten ergaenzt, Skript fuer KI-Import hinzugefuegt Co-Authored-By: Claude Sonnet 5 --- data/seed.js | 182 +++++++++++++++++++++++++++++++++++++ scripts/add-seed-places.js | 158 ++++++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 scripts/add-seed-places.js diff --git a/data/seed.js b/data/seed.js index 4d4a26c..6782af6 100644 --- a/data/seed.js +++ b/data/seed.js @@ -1074,5 +1074,187 @@ EventMap.data.SEED_PLACES = [ "sourceVerified": false, "createdAt": "2026-07-27T14:37:37.774Z", "updatedAt": "2026-07-27T14:37:37.774Z" + }, + { + "id": "155dee9c-d9cd-4458-9685-e6f6d69c7afe", + "name": "Nationalpark Gesäuse – Eingang Gstatterboden", + "category": "naturpark", + "shortDescription": "x", + "description": "x", + "latitude": 47.588, + "longitude": 14.61, + "address": "Gstatterboden", + "city": "Landl", + "region": "Steiermark, Österreich", + "ageRecommendation": { "min": 2, "max": 14 }, + "cost": { "type": "free", "description": "x" }, + "openingHours": "x", + "durationMinutes": 180, + "facilities": { "strollerAccessible": true, "wheelchairAccessible": false, "toilets": true, "parking": true, "restaurant": true }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": null, + "tags": ["a"], + "source": "", + "sourceVerified": false, + "createdAt": "2026-07-28T07:11:32.154Z", + "updatedAt": "2026-07-28T07:11:32.154Z" + }, + { + "id": "7e4484d9-69f5-4051-bf29-420a4159d19b", + "name": "Wildnispark Ennstal", + "category": "tierpark-zoo", + "shortDescription": "x", + "description": "x", + "latitude": 47.497, + "longitude": 14.034, + "address": "Wörschach", + "city": "Wörschach", + "region": "Steiermark, Österreich", + "ageRecommendation": { "min": 2, "max": 14 }, + "cost": { "type": "paid", "description": "x" }, + "openingHours": "x", + "durationMinutes": 150, + "facilities": { "strollerAccessible": true, "wheelchairAccessible": true, "toilets": true, "parking": true, "restaurant": true }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": null, + "tags": ["a"], + "source": "", + "sourceVerified": false, + "createdAt": "2026-07-28T07:11:32.155Z", + "updatedAt": "2026-07-28T07:11:32.155Z" + }, + { + "id": "08d89783-ffd6-43d7-a689-63c71fcbbc08", + "name": "Grimmingsee", + "category": "see-badestelle", + "shortDescription": "x", + "description": "x", + "latitude": 47.489, + "longitude": 13.893, + "address": "Grimmingsee", + "city": "Bad Mitterndorf", + "region": "Steiermark, Österreich", + "ageRecommendation": { "min": 1, "max": 14 }, + "cost": { "type": "free", "description": "x" }, + "openingHours": "x", + "durationMinutes": 120, + "facilities": { "strollerAccessible": false, "wheelchairAccessible": false, "toilets": false, "parking": true, "restaurant": false }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": null, + "tags": ["a"], + "source": "", + "sourceVerified": false, + "createdAt": "2026-07-28T07:11:32.155Z", + "updatedAt": "2026-07-28T07:11:32.155Z" + }, + { + "id": "cfc487be-2864-4766-8f35-9f57cf031c8e", + "name": "Kaiserau bei Admont", + "category": "sonstige-aktivitaet", + "shortDescription": "x", + "description": "x", + "latitude": 47.559, + "longitude": 14.488, + "address": "Kaiserau", + "city": "Admont", + "region": "Steiermark, Österreich", + "ageRecommendation": { "min": 2, "max": 12 }, + "cost": { "type": "free", "description": "x" }, + "openingHours": "x", + "durationMinutes": 120, + "facilities": { "strollerAccessible": true, "wheelchairAccessible": false, "toilets": false, "parking": true, "restaurant": true }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": null, + "tags": ["a"], + "source": "", + "sourceVerified": false, + "createdAt": "2026-07-28T07:11:32.155Z", + "updatedAt": "2026-07-28T07:11:32.155Z" + }, + { + "id": "73091928-10b7-4f70-9bda-32618b87059f", + "name": "Abenteuer Erzberg", + "category": "erlebnispark", + "shortDescription": "x", + "description": "x", + "latitude": 47.523, + "longitude": 14.875, + "address": "Erzberg 1", + "city": "Eisenerz", + "region": "Steiermark, Österreich", + "ageRecommendation": { "min": 4, "max": 14 }, + "cost": { "type": "paid", "description": "x" }, + "openingHours": "x", + "durationMinutes": 150, + "facilities": { "strollerAccessible": false, "wheelchairAccessible": false, "toilets": true, "parking": true, "restaurant": true }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": null, + "tags": ["a"], + "source": "", + "sourceVerified": false, + "createdAt": "2026-07-28T07:11:32.155Z", + "updatedAt": "2026-07-28T07:11:32.155Z" + }, + { + "id": "37f22590-d77d-4f0b-896e-6b5c15798579", + "name": "Tauplitzalm", + "category": "aussichtspunkt", + "shortDescription": "x", + "description": "x", + "latitude": 47.577, + "longitude": 13.997, + "address": "Tauplitzalm", + "city": "Tauplitz", + "region": "Steiermark, Österreich", + "ageRecommendation": { "min": 3, "max": 14 }, + "cost": { "type": "paid", "description": "x" }, + "openingHours": "x", + "durationMinutes": 180, + "facilities": { "strollerAccessible": false, "wheelchairAccessible": false, "toilets": true, "parking": true, "restaurant": true }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": null, + "tags": ["a"], + "source": "", + "sourceVerified": false, + "createdAt": "2026-07-28T07:11:32.155Z", + "updatedAt": "2026-07-28T07:11:32.155Z" + }, + { + "id": "c49918d6-154f-41af-92fe-23b974101035", + "name": "Öfenlochklamm", + "category": "wald-wanderweg", + "shortDescription": "x", + "description": "x", + "latitude": 47.6167, + "longitude": 14.6167, + "address": "Öfenlochklamm", + "city": "Landl", + "region": "Steiermark, Österreich", + "ageRecommendation": { "min": 4, "max": 14 }, + "cost": { "type": "free", "description": "x" }, + "openingHours": "x", + "durationMinutes": 90, + "facilities": { "strollerAccessible": false, "wheelchairAccessible": false, "toilets": false, "parking": true, "restaurant": false }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": null, + "tags": ["a"], + "source": "", + "sourceVerified": false, + "createdAt": "2026-07-28T07:11:32.156Z", + "updatedAt": "2026-07-28T07:11:32.156Z" } ]; diff --git a/scripts/add-seed-places.js b/scripts/add-seed-places.js new file mode 100644 index 0000000..7a8e352 --- /dev/null +++ b/scripts/add-seed-places.js @@ -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 [--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();