Files
dapelza 39321d680f Phase 7: KI-JSON-Import und Backup-Export/Import
Neues Modul js/import/ mit reiner Validierungslogik
(importValidator.js: parseAndValidate(), findDuplicate()). Jeder
Ausflugsziel-Eintrag im importierten KI-JSON wird einzeln bewertet statt
alles-oder-nichts: fehlender Name/Kategorie oder ungueltige Koordinaten
fuehren zur Ablehnung, weiche Probleme (unbekannte Kategorie, unsichere
website/image-URL, ungueltige Bewertung, falscher Feldtyp) erzeugen nur
eine Warnung. sanitizeEntry() setzt sicherheitskritische Felder vor der
Uebernahme zurueck (z.B. javascript:-URLs), da normalizePlace() selbst
keine Sicherheitspruefung vornimmt. findDuplicate() erkennt
wahrscheinliche Duplikate gegen den bestehenden Datenbestand.

UI (js/ui/import.js): Modal mit zwei Tabs - KI-Ausflugsziele importieren
(Text einfuegen oder Datei hochladen, Vorschau mit
gueltig/Warnung/abgelehnt, Duplikat-Toggle, Bestaetigen) und Backup
sichern/wiederherstellen (js/import/backup.js: Export als JSON-Datei,
Wiederherstellen mit Ersetzen/Ergaenzen-Wahl). Vorschau-Rendering
ausschliesslich ueber el()/textContent, da es sich um echte, ungepruefte
Fremddaten handelt.

Damit sind alle Kernfunktionen aus anforderung.md umgesetzt.
docs/plan.md entsprechend fortgeschrieben.
2026-07-24 12:50:05 +02:00

12 KiB
Raw Permalink Blame History

EventMap Gesamtplan

Dieses Dokument fasst den vom architect-Agenten erarbeiteten Plan zusammen und wird phasenweise fortgeschrieben. Aktuell umgesetzt: Phase 0 (Scaffold) und Phase 1 (Datenmodell + Karte + Liste, read-only mit Beispieldaten).

Tech-Stack

  • Vanilla HTML5/CSS3/JavaScript, klassische Scripts (kein type="module"); Module-Ersatz über einen globalen Namespace window.EventMap + IIFE-Kapselung pro Datei, siehe docs/adr/0001-vanilla-esm.md. Grund: die App wird per Doppelklick direkt aus file:// geöffnet, native ES-Module würden dort per CORS-Policy blockiert.
  • Kein Build-Tool, kein Bundler, kein Framework
  • Karte: Leaflet 1.9.4 + Leaflet.markercluster 1.5.3, lokal vendored unter vendor/
  • Kartengrundlage: OpenStreetMap-Daten, ausgeliefert über CARTO-Basemap-Tiles (basemaps.cartocdn.com, Stil "light_all") statt des offiziellen tile.openstreetmap.org. Grund: dessen Tile Usage Policy verlangt seit 2024 einen gültigen HTTP-Referer, den file://-Seiten (Doppelklick-Start) grundsätzlich nicht mitsenden können ("Access blocked: Referer is required"). CARTO liefert dieselben OSM-Daten kartographisch auf, ohne diese Einschränkung; Attribution zeigt weiterhin OpenStreetMap + CARTO.
  • Datenhaltung: LocalStorage (Offline-first), vorbereitet für spätere Migration auf Backend/API

Ordnerstruktur

index.html
css/            reset, variables (Design-Tokens), layout, components, responsive
js/config/      constants.js, categories.js
js/store/       store.js (Pub/Sub-State), storage.js (LocalStorage-IO), migrations.js
js/models/      place.js (Normalisierung), schema.js (Feldstruktur/Defaults)
js/map/         mapController.js, markers.js, geolocation.js
js/ui/          list.js (Ergebnisliste; weitere UI-Module folgen in späteren Phasen)
js/utils/       dom.js, geo.js, id.js
data/seed.js     Beispieldaten (10 Ausflugsziele, wird nur beim allerersten Start geladen)
vendor/         lokal vendorte Leaflet-Bibliotheken (JS/CSS/Bildassets)
docs/           dieser Plan, ADRs

Datenmodell (Ausflugsziel / Place)

Das interne Place-Objekt orientiert sich eng am verbindlichen KI-Import-JSON-Format aus anforderung.md (siehe dortiger Abschnitt "Vorgabe für das KI-JSON"):

  • Basisfelder: name, category, shortDescription, description, latitude, longitude, address, city, region
  • ageRecommendation: { min, max }
  • cost: { type: 'free'|'paid'|'mixed'|'unknown', description }
  • openingHours, durationMinutes
  • facilities: { strollerAccessible, wheelchairAccessible, toilets, parking, restaurant }
  • environment: 'indoor'|'outdoor'|'mixed'
  • website, image, rating, tags[], source, sourceVerified
  • Intern generiert: id (UUID), createdAt, updatedAt (ISO-Strings)

js/models/place.js normalisiert rohe Objekte (aus data/seed.js oder späteren Importen) zu diesem vollständigen internen Format; die Kategorie wird dabei immer auf eine gültige Kategorie-id (Slug, siehe js/config/categories.js) aufgelöst entweder direkt (falls schon ein Slug vorliegt) oder über das Label (z.B. "Spielplatz"spielplatz), wie es die KI liefern würde.

Kategorien

14 Kategorien aus der Anforderung + "Sonstige" als generischer Fallback (insgesamt 15), jeweils mit id (Slug), label, color (Hex) und icon (Emoji-Glyph) siehe js/config/categories.js.

Store-Architektur

js/store/store.js ist ein zentraler Pub/Sub-Store:

state = { places, filters: null, userLocation: null, selectedPlaceId: null }
  • subscribe(fn) registriert Listener, setState(patch) merged den State und benachrichtigt alle Subscriber.
  • initStore() lädt beim Start persistierte Daten aus LocalStorage (js/store/storage.js). Ist noch nichts gespeichert, wird data/seed.js geladen, normalisiert und einmalig persistiert.
  • Einstellungen (lastFilters, lastMapView, userConsentGeo, defaultCenter) werden getrennt über getSettings()/updateSettings() verwaltet und im Key eventmap.settings.v1 persistiert.

LocalStorage-Schema

  • eventmap.data.v1{ schemaVersion: 1, places: [...], updatedAt }
  • eventmap.settings.v1{ lastFilters, lastMapView, userConsentGeo, defaultCenter }

js/store/migrations.js stellt eine schlanke, sequenziell anwendbare Migrations-Infrastruktur bereit (migrate(data)). Aktuell existiert nur Schema-Version 1, das Migrationsarray ist entsprechend leer.

defaultCenter (frei wählbares Fallback-Kartenzentrum) ist als Datenfeld vorbereitet, aber in Phase 1 noch nicht über die UI einstellbar. Solange kein defaultCenter gesetzt ist, verwendet die Karte den hartcodierten Default aus js/config/constants.js (geografische Mitte Deutschlands, lat 51.1657, lng 10.4515, zoom 6).

Karte

  • js/map/mapController.js: initialisiert Leaflet-Karte + OSM-Tile-Layer (inkl. korrekter Attribution), verwaltet ein L.markerClusterGroup, reagiert per subscribe() auf Änderungen an state.places (Marker neu rendern) und state.selectedPlaceId (Karte zentrieren + Popup öffnen). fitToPlaces() passt die Ansicht auf alle vorhandenen Places an, mit Fallback auf das Default-Center falls keine Places mit gültigen Koordinaten vorhanden sind.
  • js/map/markers.js: erzeugt je Kategorie ein L.divIcon (Farbe + Emoji, kein PNG-Sprite nötig) und den Popup-Inhalt (Name, Kategorie, Kurzbeschreibung) als DOM-Node (nicht als HTML-String, siehe Sicherheitshinweis unten).
  • js/map/geolocation.js: fragt den Standort nur auf explizite Nutzeraktion (Button "Meinen Standort verwenden") ab, setzt bei Erfolg userLocation im Store und zentriert die Karte; bei Ablehnung/Fehler bleibt das Default-Center erhalten und ein Hinweis wird angezeigt.

Ergebnisliste

js/ui/list.js rendert state.places als Cards (Name, Kategorie mit Farbe/Icon, Kurzbeschreibung). Ein Klick auf eine Card setzt selectedPlaceId im Store; mapController.js reagiert darauf und zentriert die Karte auf den zugehörigen Marker. Filterung (Phase 4) ist hier noch nicht implementiert aktuell werden immer alle Places gezeigt.

Sicherheitshinweis: Da künftig Fremddaten per KI-Import in die App gelangen, wird an keiner Stelle innerHTML mit Place-Daten befüllt. js/utils/dom.js stellt dafür el() (DOM-Erzeugung über textContent/ Properties) sowie escapeHtml() als zusätzliches Sicherheitsnetz bereit.

Layout

  • Header mit Titel "EventMap".
  • Mobile/Tablet (< 1024px): Karte oben (fixe Höhe ca. 50vh), Liste darunter, normal scrollbar.
  • Desktop (≥ 1024px): Grid mit Karte (~65%) und Liste-Sidebar (~35%) nebeneinander, siehe css/responsive.css.

Kein Dev-Server nötig

Die App verwendet klassische <script>-Tags statt ES-Module (siehe docs/adr/0001-vanilla-esm.md) und lässt sich daher per Doppelklick auf index.html direkt aus dem lokal synchronisierten Ordner öffnen (file://...) ein lokaler HTTP-Server ist dafür nicht mehr nötig.

Die Beispieldaten liegen in data/seed.js als eingebettetes <script>-Objekt (EventMap.data.SEED_PLACES) statt als .json-Datei, da fetch() auf lokale Dateien über file:// in manchen Browsern (insbesondere Chrome/Chromium) blockiert wird. initStore() liest die Seed-Daten dadurch synchron direkt aus dem globalen Namespace, ganz ohne fetch().

Phasenübersicht

Phase Inhalt Status
0 Scaffold (Ordnerstruktur, vendored Leaflet, Grundgerüst) umgesetzt
1 Datenmodell, Karte, Liste (read-only, Seed-Daten) umgesetzt
2 Detailansicht (Titelbild, vollständige Beschreibung, Mini-Karte, Navigation-Button, Bearbeiten/Löschen) umgesetzt
3 Formular zum manuellen Anlegen/Bearbeiten (Validierung, Koordinatenprüfung) umgesetzt
4 Filter- und Suchfunktion (Kategorie, Entfernung, Alter, Kosten, Indoor/Outdoor, Ausstattung, Bewertung, Region) inkl. Anwendung auf Karte + Liste umgesetzt
5 Route/Navigation-Integration bereits in Phase 2 erledigt (OSM-Directions-Button in der Detailansicht)
6 KI-Prompt-Generator (Eingabeformular, Promptgenerierung, Copy/Download) umgesetzt
7 KI-JSON-Import (Validierung, Vorschau, Übernahme ins LocalStorage) + Backup-Export/Import umgesetzt

Damit sind alle in anforderung.md geforderten Kernfunktionen umgesetzt. Offen bleibt optionale Politur (Empty-States, A11y-Feinschliff, Adressgeocoding) — kein Muss laut Anforderung, bei Bedarf als eigene Folgephase.

KI-JSON-Import + Backup (Phase 7)

  • js/import/importValidator.js: reine Funktion parseAndValidate(jsonText) bewertet jeden Ausflugsziel-Eintrag im importierten KI-JSON EINZELN (kein Alles-oder-nichts). Pflicht für Übernahme: name, category, gültige Koordinaten — sonst Ablehnung mit Begründung. Weiche Probleme (unbekannte Kategorie, unbekannter Kosten-/Umgebungstyp, ungültige Bewertung, unsichere website/image-URL) führen zu einer Warnung, nicht zur Ablehnung; sanitizeEntry() setzt diese Felder vor der Übernahme sicherheitshalber zurück (z.B. javascript:-URLs → leerer String), BEVOR normalizePlace() sie verarbeitet, da dieses selbst keine Sicherheitsprüfung vornimmt. findDuplicate() erkennt wahrscheinliche Duplikate (Name + Koordinaten < 0.3 km) gegen den bestehenden Datenbestand.
  • js/ui/import.js: Modal mit zwei Tabs. Tab 1 "KI-Ausflugsziele importieren": Text einfügen ODER Datei hochladen (FileReader) → Prüfen → Vorschau (gültig/Warnung/abgelehnt, Duplikat-Hinweise, globaler "Duplikate überspringen"-Toggle) → Bestätigen. Tab 2 "Backup sichern/wiederherstellen" (js/import/backup.js): Export als JSON-Datei (downloadTextFile), Wiederherstellen mit Wahl "Ersetzen" (mit confirm()-Sicherheitsabfrage) oder "Ergänzen" (Upsert nach id).
  • Einstiegspunkt: Toolbar-Button "📥 KI-Ausflugsziele importieren".
  • Rendering der Vorschau ausschließlich über el()/textContent, da es sich um echte, ungeprüfte Fremddaten handelt.

KI-Prompt-Generator (Phase 6)

  • js/ai/promptBuilder.js: reine Funktion buildPrompt(formValues), deckt alle 10 Pflichtpunkte aus anforderung.md explizit nummeriert ab und bettet das exakte JSON-Zielschema aus anforderung.md ("Vorgabe für das KI-JSON") 1:1 als Konstante JSON_TEMPLATE ein (per Skript byte-genau abgeglichen).
  • Der Anweisungstext des Prompts ist immer Deutsch; das Formularfeld "Sprache der Ausgabe" steuert separat, in welcher Sprache die KI ihre name/ shortDescription/description-Texte verfassen soll.
  • js/ui/promptGenerator.js: Modal mit Live-Vorschau (<textarea readonly>, aktualisiert sich bei jeder Eingabe), Buttons "In Zwischenablage kopieren" (js/utils/clipboard.js, Clipboard API + execCommand-Fallback für file://-Einschränkungen) und "Als Textdatei speichern" (js/utils/download.js, clientseitiger Blob-Download).
  • Einstiegspunkt: Button in der Karten-Toolbar, öffnet EventMap.ui.openPromptGenerator().
  • Import der KI-Antwort (JSON zurück in die App) ist bewusst nicht Teil dieser Phase, sondern folgt in Phase 7.

Filter- und Suchfunktion (Phase 4)

  • js/filters/filterEngine.js: reine Funktion applyFilters(places, filters, userLocation) plus getDistanceKm(). Leerer/null/[]-Wert je Filterfeld = kein Filter aktiv. Places ohne Bewertung/Dauer werden bei aktivem Bewertungs-/Dauer-Filter nicht automatisch ausgeschlossen; Places ohne gültige Koordinaten werden bei aktivem Entfernungsfilter (mit vorhandenem Standort) ausgeschlossen, da ihre Entfernung nicht verifizierbar ist. Fehlt der Nutzerstandort komplett, wird der Entfernungsfilter ignoriert statt die Liste leerzuräumen.
  • js/filters/filterPanel.js: UI für alle Filter aus anforderung.md Abschnitt 3 (Kategorie, Entfernung inkl. "Standort verwenden"-Button, Alter, Kosten, Indoor/Outdoor, 5 Ausstattungsmerkmale, Aufenthaltsdauer, Mindestbewertung, Freitextsuche über Name/Ort/Region/Adresse), Trefferzähler "X von Y", "Filter zurücksetzen" und "Auf Ergebnisse zoomen".
  • Store: EventMap.store.updateFilters(patch) und EventMap.store.getFilteredPlaces() (Selector). js/ui/list.js und js/map/mapController.js rendern seitdem die gefilterte statt die volle Liste.
  • Layout: Mobile (< 1024px) als <details>/<summary> einklappbar oberhalb der Liste; Desktop (≥ 1024px) als eigener Grid-Bereich neben der Karte, über der Liste (css/responsive.css).
  • Nicht umgesetzt (bewusst kein Muss laut Plan): Persistierung der Filter in settings.lastFilters.