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:
+182
@@ -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"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user