Files
eventmap/docs/plan.md
T
dapelzaandClaude Sonnet 5 2aaff8f680 Seed-Daten per Fetch von externem Server laden, UI-Sprachumschalter (DE/EN) und KI-Übersetzungs-Prompt-Generator ergänzen
- Beispieldaten werden nicht mehr eingebettet (data/seed.js entfernt),
  sondern beim ersten Start per fetch(EventMap.config.SEED_DATA_URL) von
  einem externen, selbst gehosteten Server geladen (siehe ADR 0001,
  Revision 2026-07-28).
- Neuer UI-Sprachumschalter (Deutsch/Englisch) für die Bedienoberfläche:
  js/i18n/strings.js + js/i18n/i18n.js, Sprachwahl im Menü, Persistenz über
  settings.language, Retranslation ohne Reload.
- Neuer Menüpunkt "Ausflugsziel übersetzen (KI)": erzeugt einen Prompt, der
  eine externe KI bittet, Name/Beschreibungen eines bestehenden Ortes ins
  Englische, Spanische oder Französische zu übersetzen (js/ai/translatePromptBuilder.js,
  js/ui/translatePromptGenerator.js).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 15:16:31 +02:00

241 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](https://leafletjs.com/) 1.9.4 + [Leaflet.markercluster](https://github.com/Leaflet/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
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 den per `EventMap.config.SEED_DATA_URL`
nachgeladenen Beispieldaten 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:
```js
state = { places, filters: null, userLocation: null, selectedPlaceId: null }
```
- `subscribe(fn)` registriert Listener, `setState(patch)` merged den State und
benachrichtigt alle Subscriber.
- `initStore()` (async) lädt beim Start persistierte Daten aus LocalStorage
(`js/store/storage.js`). Ist noch nichts gespeichert, werden die Beispieldaten
per `fetch(EventMap.config.SEED_DATA_URL)` nachgeladen, normalisiert und
einmalig persistiert. Schlägt der Download fehl (offline, CORS, Server nicht
erreichbar), startet die App mit einer leeren Places-Liste und zeigt einen
Hinweis im Status-Banner an, statt die Initialisierung abzubrechen.
- 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 nicht im Repo, sondern werden beim allerersten Start
(leerer LocalStorage) per `fetch(EventMap.config.SEED_DATA_URL)` von einem
externen Server geladen (siehe `docs/adr/0001-vanilla-esm.md`). Das
funktioniert auch unter `file://`, da die Same-Origin-Blockade in Chrome/Chromium
nur `fetch()` auf *lokale* Dateien betrifft, nicht auf entfernte `https://`-URLs
(vorausgesetzt der Server sendet passende CORS-Header).
## 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`.