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
+182
View File
@@ -1074,5 +1074,187 @@ EventMap.data.SEED_PLACES = [
"sourceVerified": false, "sourceVerified": false,
"createdAt": "2026-07-27T14:37:37.774Z", "createdAt": "2026-07-27T14:37:37.774Z",
"updatedAt": "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"
} }
]; ];
+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();