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.
12 KiB
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 Namespacewindow.EventMap+ IIFE-Kapselung pro Datei, siehedocs/adr/0001-vanilla-esm.md. Grund: die App wird per Doppelklick direkt ausfile://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 offiziellentile.openstreetmap.org. Grund: dessen Tile Usage Policy verlangt seit 2024 einen gültigen HTTP-Referer, denfile://-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,durationMinutesfacilities: { 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, wirddata/seed.jsgeladen, normalisiert und einmalig persistiert.- Einstellungen (
lastFilters,lastMapView,userConsentGeo,defaultCenter) werden getrennt übergetSettings()/updateSettings()verwaltet und im Keyeventmap.settings.v1persistiert.
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 einL.markerClusterGroup, reagiert persubscribe()auf Änderungen anstate.places(Marker neu rendern) undstate.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 einL.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 ErfolguserLocationim 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 FunktionparseAndValidate(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, unsicherewebsite/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), BEVORnormalizePlace()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" (mitconfirm()-Sicherheitsabfrage) oder "Ergänzen" (Upsert nachid).- 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 FunktionbuildPrompt(formValues), deckt alle 10 Pflichtpunkte ausanforderung.mdexplizit nummeriert ab und bettet das exakte JSON-Zielschema ausanforderung.md("Vorgabe für das KI-JSON") 1:1 als KonstanteJSON_TEMPLATEein (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ürfile://-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 FunktionapplyFilters(places, filters, userLocation)plusgetDistanceKm(). 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 ausanforderung.mdAbschnitt 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)undEventMap.store.getFilteredPlaces()(Selector).js/ui/list.jsundjs/map/mapController.jsrendern 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.