# Event Discovery — Implementierungsplan ## Status (2026-04-17) Planung, noch kein Code. ## Ziel Eine KI im Events-Modul, die automatisch öffentliche Veranstaltungen in den Regionen des Nutzers findet, strukturiert und als kuratierten Feed vorschlägt. Der Nutzer konfiguriert Städte/Gebiete + Interessen; das System scannt Event-Kalender, Venue-Websites und Vereinsseiten, dedupliziert und rankt nach Relevanz. ## Abgrenzung - **Eigene Events** (socialEvents, RSVP, Bring-Liste) bleiben unberührt — Discovery ist ein paralleler Read-only-Feed - **mana-research** wird als Provider-Schicht genutzt (Web-Suche, Extraktion), aber Discovery-Logik lebt in mana-events - **mana-crawler** wird NICHT direkt genutzt — Firecrawl/Jina über mana-research reichen für Event-Extraktion - **mana-ai Missions** kommen erst in Phase 3 — Phase 1-2 läuft als dedizierter Cron/API ohne Mission-Runner --- ## Architektur ``` Nutzer (Events-Modul, Tab "Entdecken") │ ▼ apps/mana web ──→ mana-events (3065) │ ┌────────────┼────────────────┐ ▼ ▼ ▼ Discovery API Source Manager Crawl Scheduler │ │ │ ▼ ▼ ▼ PostgreSQL mana-research mana-geocoding (event_discovery (Web-Suche, (Region → Schema) Extraktion) BoundingBox) ``` ### Neue DB-Tabellen (PostgreSQL, Schema `event_discovery` in `mana_platform`) ```sql -- Quellen, die regelmäßig gescannt werden discovery_sources id uuid PK user_id text NOT NULL -- Besitzer type text NOT NULL -- 'ical' | 'website' | 'api' | 'search_query' url text -- Feed-URL oder Website-URL name text NOT NULL -- "Jazzhaus Freiburg", "VHS Konstanz" region_id uuid FK → discovery_regions crawl_interval_hours int DEFAULT 24 last_crawled_at timestamptz last_success_at timestamptz error_count int DEFAULT 0 last_error text is_active boolean DEFAULT true created_at timestamptz DEFAULT now() updated_at timestamptz DEFAULT now() -- Regionen des Nutzers discovery_regions id uuid PK user_id text NOT NULL label text NOT NULL -- "Freiburg", "Basel" lat double precision lon double precision radius_km int DEFAULT 25 is_active boolean DEFAULT true created_at timestamptz DEFAULT now() -- Nutzer-Interessen für Relevanz-Scoring discovery_interests id uuid PK user_id text NOT NULL category text NOT NULL -- 'music' | 'tech' | 'sport' | 'art' | ... freetext text -- "Impro-Theater", "Rust Meetups" weight real DEFAULT 1.0 -- Nutzer kann priorisieren created_at timestamptz DEFAULT now() -- Gefundene Events (dedupliziert, normalisiert) discovered_events id uuid PK source_id uuid FK → discovery_sources (CASCADE) external_id text -- Dedupe-Anker (URL oder Hash) dedupe_hash text NOT NULL -- sha256(lower(title) + date + location) title text NOT NULL description text location text lat double precision lon double precision start_at timestamptz NOT NULL end_at timestamptz all_day boolean DEFAULT false image_url text source_url text NOT NULL -- Link zur Original-Seite source_name text -- "Jazzhaus Freiburg" category text -- LLM-klassifiziert price_info text -- "Frei", "12 €", "VVK 15 / AK 18" raw_extracted jsonb -- Rohdaten der LLM-Extraktion crawled_at timestamptz DEFAULT now() expires_at timestamptz -- start_at + 1 Tag (für Cleanup) UNIQUE(dedupe_hash) -- Idempotenz -- Nutzer-Interaktion mit entdeckten Events discovery_user_actions id uuid PK user_id text NOT NULL event_id uuid FK → discovered_events (CASCADE) action text NOT NULL -- 'save' | 'dismiss' | 'hide_source' acted_at timestamptz DEFAULT now() UNIQUE(user_id, event_id) -- Indizes CREATE INDEX idx_discovered_events_start ON discovered_events(start_at); CREATE INDEX idx_discovered_events_source ON discovered_events(source_id); CREATE INDEX idx_discovery_sources_user ON discovery_sources(user_id, is_active); CREATE INDEX idx_discovery_regions_user ON discovery_regions(user_id); CREATE INDEX idx_discovery_actions_user ON discovery_user_actions(user_id); ``` ### Lokale Tabellen (Dexie) — nur Cache + Offline Discovery-Daten sind **nicht** local-first (sie entstehen auf dem Server). Dexie dient nur als Offline-Cache: ``` discoveryRegions — id, label, lat, lon, radiusKm, isActive discoveryInterests — id, category, freetext, weight discoveredEvents — id, title, description, location, lat, lon, startAt, endAt, sourceUrl, sourceName, imageUrl, category, priceInfo, relevanceScore, userAction (null|save|dismiss), crawledAt ``` Keine Verschlüsselung nötig — das sind öffentliche Event-Daten, keine User-Inhalte. Kein Sync über mana-sync — der Server ist die Source of Truth, Client pollt/cached. --- ## Phase 1 — Regionen, iCal-Feeds, Discovery-Tab **Ziel:** Nutzer kann Regionen + iCal-Feeds manuell konfigurieren. Events werden geparst und im "Entdecken"-Tab angezeigt. ### 1.1 Backend: DB-Schema + CRUD-Routen (mana-events) **Dateien:** ``` services/mana-events/src/db/schema/ discovery.ts ← NEU: Drizzle-Schema für alle 5 Tabellen services/mana-events/src/routes/ discovery.ts ← NEU: CRUD für regions, interests, sources discovery-feed.ts ← NEU: Feed-Endpoint (paginiert, gefiltert) services/mana-events/src/app.ts → Neue Routen registrieren unter /api/v1/discovery/* ``` **API-Endpunkte (alle JWT-authentifiziert):** ``` # Regionen GET /api/v1/discovery/regions → [{id, label, lat, lon, radiusKm}] POST /api/v1/discovery/regions ← {label, lat, lon, radiusKm} PUT /api/v1/discovery/regions/:id ← {label?, radiusKm?, isActive?} DELETE /api/v1/discovery/regions/:id # Interessen GET /api/v1/discovery/interests → [{id, category, freetext, weight}] POST /api/v1/discovery/interests ← {category, freetext?, weight?} DELETE /api/v1/discovery/interests/:id # Quellen (Phase 1: nur iCal) GET /api/v1/discovery/sources → [{id, type, url, name, region, status}] POST /api/v1/discovery/sources ← {type: 'ical', url, name, regionId} DELETE /api/v1/discovery/sources/:id POST /api/v1/discovery/sources/:id/crawl → Sofort-Crawl auslösen # Feed GET /api/v1/discovery/feed → {events: [...], total, hasMore} ?from=ISO&to=ISO&category=music&limit=20&offset=0 POST /api/v1/discovery/feed/:eventId/action ← {action: 'save' | 'dismiss'} ``` ### 1.2 Backend: iCal-Parser + Crawl-Loop **Dateien:** ``` services/mana-events/src/discovery/ ical-parser.ts ← iCal → discovered_events (ical.js oder node-ical) crawl-scheduler.ts ← Interval-basierter Crawl-Loop (wie rateBucketSweeper) deduplicator.ts ← sha256(lower(title) + startAt.toISODate() + lower(location)) types.ts ← NormalizedEvent, CrawlResult, SourceStatus ``` **Ablauf eines Crawl-Zyklus:** ``` crawl-scheduler.ts (runs every 15 min) │ ├─ SELECT sources WHERE is_active AND last_crawled_at < now() - interval_hours │ ├─ Für jede fällige Source: │ ├─ fetch(source.url) mit 10s Timeout │ ├─ ical-parser.ts: VEVENT → NormalizedEvent[] │ ├─ deduplicator.ts: dedupe_hash berechnen │ ├─ UPSERT INTO discovered_events ON CONFLICT(dedupe_hash) │ │ → Bestehende: title/description/location updaten falls geändert │ │ → Neue: INSERT │ ├─ UPDATE source SET last_crawled_at, last_success_at, error_count=0 │ └─ Bei Fehler: error_count++, last_error setzen │ → Nach 5 Fehlern: is_active = false (Nutzer wird informiert) │ └─ DELETE FROM discovered_events WHERE expires_at < now() (Cleanup abgelaufener Events) ``` **Dependency:** `node-ical` (Bun-kompatibel, ~50KB, parst VEVENT/VTODO/VFREEBUSY) ### 1.3 Frontend: Discovery-Tab + Regionen-Setup **Dateien:** ``` apps/mana/apps/web/src/lib/modules/events/ discovery/ api.ts ← HTTP-Client (fetchWithAuth, analog events/api.ts) types.ts ← DiscoveredEvent, DiscoveryRegion, DiscoveryInterest, etc. queries.svelte.ts ← Reactive state ($state) für Feed, Regionen, Interessen stores.svelte.ts ← Mutationen (addRegion, addSource, saveEvent, dismissEvent) components/ DiscoveryTab.svelte ← Der neue Tab-Inhalt DiscoverySetup.svelte ← Onboarding: Regionen + Interessen konfigurieren DiscoveredEventCard.svelte ← Karte mit Titel, Datum, Ort, Quelle, Aktionen SourceManager.svelte ← iCal-Feed-URLs verwalten (hinzufügen, Status, löschen) RegionPicker.svelte ← Stadt-Suche via mana-geocoding + Radius-Slider ListView.svelte ← Tab-Navigation ergänzen: "Meine Events" | "Entdecken" ``` **ListView.svelte — Tab-Erweiterung:** ```svelte
{#if activeTab === 'mine'} {:else} {/if} ``` **DiscoveryTab.svelte — Aufbau:** ```svelte {#if !hasRegions} {:else}
{#if feed.value.length === 0}

Noch keine Events gefunden. Füge iCal-Feeds hinzu oder warte auf den nächsten Scan.

{:else} {#each feed.value as event (event.id)} actions.save(event.id)} onDismiss={() => actions.dismiss(event.id)} onOpen={() => window.open(event.sourceUrl, '_blank')} /> {/each} {/if} {/if} ``` **DiscoveredEventCard.svelte — Felder:** - Titel (fett) - Datum + Uhrzeit (formatiert, relativ: "Morgen, 19:00" / "Sa 26. Apr, 20:00") - Ort + Entfernung zum nächsten Region-Zentrum - Quelle (Link zur Original-Seite) - Kategorie-Badge (Musik, Tech, Sport, ...) - Preis-Info falls vorhanden - Aktionen: "Merken" (→ eigenes socialEvent anlegen), "Nicht interessant", "Zur Quelle" **"Merken"-Flow:** ``` Nutzer klickt "Merken" → discoveryStore.saveEvent(discoveredEvent) → eventsStore.createEvent({ title: event.title, startTime: event.startAt, endTime: event.endAt, location: event.location, description: `${event.description}\n\nQuelle: ${event.sourceUrl}`, locationLat: event.lat, locationLon: event.lon, }) → POST /api/v1/discovery/feed/:eventId/action {action: 'save'} → Karte zeigt "Gespeichert ✓" ``` ### 1.4 Module-Integration **module.config.ts** — Neue Dexie-Tabellen registrieren (Cache-only, kein Sync): ```typescript // events/module.config.ts — erweitert tables: [ { name: 'socialEvents', syncName: 'events' }, { name: 'eventGuests' }, { name: 'eventInvitations' }, { name: 'eventItems' }, // NEU: Discovery-Cache (nicht gesynct, rein lokal) { name: 'discoveryRegions' }, { name: 'discoveryInterests' }, { name: 'discoveredEvents' }, ], ``` **database.ts** — Indizes: ```typescript discoveryRegions: 'id, isActive', discoveryInterests: 'id, category', discoveredEvents: 'id, startAt, category, userAction, [startAt+category]', ``` **Keine Encryption-Registry** — öffentliche Daten. ### 1.5 Deliverables Phase 1 - [ ] Drizzle-Schema `event_discovery` + `bun run db:push` - [ ] CRUD-Routen für Regionen, Interessen, Quellen - [ ] iCal-Parser mit Dedup + Cleanup - [ ] Crawl-Scheduler (15-Min-Intervall) - [ ] Feed-Endpoint (paginiert, nach Datum gefiltert) - [ ] Frontend: Tab-Navigation in ListView - [ ] Frontend: DiscoverySetup (Regionen + Interessen) - [ ] Frontend: RegionPicker mit mana-geocoding Autocomplete - [ ] Frontend: SourceManager (iCal-URLs CRUD) - [ ] Frontend: DiscoveredEventCard + Feed-Ansicht - [ ] Frontend: "Merken" → socialEvent anlegen - [ ] Tests: iCal-Parser Unit-Tests, Feed-Route Integration-Tests --- ## Phase 2 — Automatische Quellen-Entdeckung + LLM-Extraktion **Ziel:** Die KI findet selbst Event-Quellen für die Regionen des Nutzers und extrahiert Events von unstrukturierten Websites. ### 2.1 Quellen-Entdeckung (Meta-Crawl) **Neue Datei:** `services/mana-events/src/discovery/source-discoverer.ts` **Ablauf:** ``` Nutzer fügt Region "Freiburg" hinzu │ ├─ Trigger: source-discoverer.discoverForRegion(region) │ ├─ Schritt 1: Web-Suche via mana-research │ Queries (parallel, via mana-research POST /api/v1/search): │ "Veranstaltungskalender Freiburg ical" │ "Events Freiburg 2026" │ "Kulturzentren Freiburg Programm" │ "Vereine Freiburg Veranstaltungen" │ "Konzerte Theater Freiburg Termine" │ ├─ Schritt 2: Ergebnisse filtern │ → URLs die auf .ics enden → Typ 'ical' │ → URLs mit /kalender, /programm, /events, /veranstaltungen → Typ 'website' │ → Bekannte Plattformen (eventbrite.*/freiburg, meetup.com/*freiburg) → Typ 'api' │ ├─ Schritt 3: LLM-Klassifikation (optional, via mana-llm) │ Prompt: "Ist diese URL eine Event-Quelle? Wenn ja: Name, Typ, Region." │ → Filtert Noise (Nachrichtenartikel über Events, generische Stadtseiten) │ ├─ Schritt 4: Vorschläge speichern │ → INSERT INTO discovery_sources (status: 'suggested') │ → Nutzer sieht Vorschläge im SourceManager und kann aktivieren/ablehnen │ └─ Schritt 5: Sofort-Crawl für aktivierte Quellen ``` **API-Erweiterung:** ``` POST /api/v1/discovery/regions/:id/discover-sources → Triggert Meta-Crawl, returns {suggestedCount} GET /api/v1/discovery/sources?status=suggested → Vorgeschlagene Quellen die der Nutzer noch bestätigen muss PUT /api/v1/discovery/sources/:id/activate PUT /api/v1/discovery/sources/:id/reject ``` ### 2.2 Website-Extraktion (LLM-basiert) **Neue Datei:** `services/mana-events/src/discovery/website-extractor.ts` **Ablauf für Typ `website`:** ``` Source: { type: 'website', url: 'https://jazzhaus.de/programm' } │ ├─ Schritt 1: Seite crawlen via mana-research │ POST mana-research /api/v1/extract │ { url: source.url, provider: 'jina' } (oder 'firecrawl') │ → Markdown-Text der Seite │ ├─ Schritt 2: LLM-Extraktion via mana-llm │ System-Prompt: │ "Du bist ein Event-Extractor. Extrahiere ALLE kommenden │ Veranstaltungen von dieser Seite. Pro Event: │ - title (string, required) │ - date (ISO 8601, required) │ - endDate (ISO 8601, optional) │ - location (string, optional — Venue-Name + Adresse) │ - description (string, max 300 Zeichen) │ - category (music|theater|art|tech|sport|food|family|other) │ - priceInfo (string, optional — z.B. 'VVK 15€ / AK 18€') │ - imageUrl (string, optional) │ Antwort als JSON-Array. Ignoriere vergangene Events. │ Heutiges Datum: {today}" │ User-Prompt: │ → JSON-Array von NormalizedEvents │ ├─ Schritt 3: Validierung + Normalisierung │ → Datum parsen (LLMs machen manchmal "25. April 2026" statt ISO) │ → Geocoding via mana-geocoding falls location vorhanden │ → dedupe_hash berechnen │ └─ Schritt 4: UPSERT INTO discovered_events ``` **LLM-Kosten:** ~500-2000 Input-Tokens pro Seite + ~200-500 Output-Tokens. Bei Haiku-Klasse: ~0.001-0.003 $ pro Seite. Bei täglichem Crawl von 50 Quellen: ~$0.05-0.15/Tag. ### 2.3 Relevanz-Scoring **Neue Datei:** `services/mana-events/src/discovery/scorer.ts` ```typescript function scoreEvent( event: DiscoveredEvent, interests: DiscoveryInterest[], regions: DiscoveryRegion[], userActions: Map ): number { let score = 50; // Basis // Kategorie-Match: +20 pro Match mit Nutzer-Interesse (gewichtet) for (const interest of interests) { if (event.category === interest.category) score += 20 * interest.weight; if (interest.freetext && event.title.toLowerCase().includes(interest.freetext.toLowerCase())) score += 15 * interest.weight; } // Entfernung: -1 pro km über 5km (nah = besser) const nearestRegion = findNearestRegion(event, regions); if (nearestRegion) { const distKm = haversine(event.lat, event.lon, nearestRegion.lat, nearestRegion.lon); score -= Math.max(0, distKm - 5); } // Zeitnähe: +10 wenn innerhalb 7 Tagen, +5 wenn innerhalb 14 Tagen const daysUntil = (new Date(event.startAt).getTime() - Date.now()) / 86400000; if (daysUntil <= 7) score += 10; else if (daysUntil <= 14) score += 5; // Wochenende-Bonus: +5 wenn Sa/So (die meisten Nutzer sind freier) const dow = new Date(event.startAt).getDay(); if (dow === 0 || dow === 6) score += 5; // Source-Qualität: +5 wenn Source hohe Erfolgsquote hat // (Phase 3: implizites Feedback aus save/dismiss-Ratio) return Math.max(0, Math.min(100, score)); } ``` **Feed-Endpoint erweitert:** `ORDER BY relevance_score DESC, start_at ASC` ### 2.4 Frontend-Erweiterungen **SourceManager.svelte — erweitert:** ```svelte {#if suggestedSources.length > 0}

Vorgeschlagene Quellen

{#each suggestedSources as source} activateSource(source.id)} onReject={() => rejectSource(source.id)} /> {/each} {/if} ``` **DiscoveredEventCard.svelte — erweitert:** - Relevanz-Indikator (farbiger Dot: grün >70, gelb >40, grau <40) - "Warum vorgeschlagen?"-Tooltip (Kategorie-Match, Nähe, Zeitnähe) - Kategorie-Badge prominenter ### 2.5 Deliverables Phase 2 - [ ] Source-Discoverer: Web-Suche → iCal/Website-URLs → Vorschläge - [ ] Website-Extractor: Crawl → LLM-Extraktion → normalisierte Events - [ ] Relevanz-Scorer mit Kategorie/Distanz/Zeit-Gewichtung - [ ] API: `/discover-sources`, `/activate`, `/reject` - [ ] Frontend: "Quellen automatisch finden" + Vorschlags-UI - [ ] Frontend: Relevanz-Indikator + "Warum vorgeschlagen?" - [ ] Crawl-Scheduler erweitert: Website-Typ + Fehlerhandling - [ ] Tests: Website-Extractor mit Mock-HTML, Scorer Unit-Tests --- ## Phase 3 — mana-ai Integration + Proaktive Vorschläge **Ziel:** Discovery wird zu einem AI-Tool. Mana-AI-Missions können proaktiv Events finden und vorschlagen. ### 3.1 AI-Tool: `discover_events` **In `@mana/shared-ai`:** ```typescript { name: 'discover_events', description: 'Suche öffentliche Veranstaltungen in den konfigurierten Regionen des Nutzers', parameters: { query: { type: 'string', description: 'Optionaler Suchtext (z.B. "Jazz Konzerte")' }, category: { type: 'string', description: 'Kategorie-Filter' }, days_ahead: { type: 'number', description: 'Wie viele Tage voraus (default 14)' }, }, defaultPolicy: 'auto', // Read-only, kann im Reasoning-Loop laufen } ``` **Server-side (mana-ai):** Ruft `mana-events /api/v1/discovery/feed` auf, injiziert Ergebnisse als ResolvedInput. ### 3.2 AI-Tool: `suggest_event` ```typescript { name: 'suggest_event', description: 'Schlage dem Nutzer ein entdecktes Event vor (erscheint als Proposal)', parameters: { discovered_event_id: { type: 'string', required: true }, reason: { type: 'string', description: 'Warum dieses Event relevant ist' }, }, defaultPolicy: 'propose', // Nutzer muss bestätigen } ``` **Approve-Handler:** Führt den "Merken"-Flow aus (discoveredEvent → socialEvent). ### 3.3 Proaktive Mission: "Event-Scout" Als **Agent-Template** (analog Recherche-Agent): ```typescript { name: 'Event-Scout', description: 'Findet regelmäßig Events in deinen Regionen und schlägt passende vor', defaultMissions: [ { objective: 'Prüfe neue Events in meinen Regionen. Schlage die 3-5 relevantesten vor, die ich noch nicht gesehen habe.', cadence: 'daily', isPaused: false, } ], policy: { discover_events: 'auto', suggest_event: 'propose', } } ``` ### 3.4 Feedback-Loop **Implizites Profil aus Nutzer-Aktionen:** ``` save_count(category=music) / total_music_shown → music_affinity dismiss_count(source=X) / total_from_X → source_quality → Gewichtung in scorer.ts anpassen: - Kategorien mit hoher Affinity: weight * 1.5 - Quellen mit niedriger Qualität: weight * 0.5 - Quellen mit >80% dismiss: deaktivieren + Nutzer informieren ``` ### 3.5 Notifications Via `mana-notify`: - **Täglicher Digest** (optional): "5 neue Events in Freiburg diese Woche" - **Highlight-Alert** (optional): Push bei Events mit Score >90 - **Source-Status**: "iCal-Feed von Jazzhaus ist seit 3 Tagen nicht erreichbar" ### 3.6 Deliverables Phase 3 - [ ] AI-Tools `discover_events` + `suggest_event` in shared-ai + mana-ai - [ ] Agent-Template "Event-Scout" - [ ] Feedback-Loop: implizites Profil → Scorer-Gewichtung - [ ] Notification-Integration (täglicher Digest, Highlight-Alert) - [ ] Tests: AI-Tool Unit-Tests, Feedback-Aggregation --- ## Phase 4 — Event-Plattform-APIs + Social **Ziel:** Strukturierte APIs von Event-Plattformen anbinden für höhere Datenqualität. ### 4.1 Provider-Adapter ``` services/mana-events/src/discovery/providers/ base.ts ← Interface: fetchEvents(region, dateRange) → NormalizedEvent[] ical.ts ← Bestehender iCal-Parser (refactored) website.ts ← Bestehender Website-Extractor (refactored) eventbrite.ts ← Eventbrite API (OAuth, kostenlos für Reads) meetup.ts ← Meetup GraphQL API facebook-events.ts ← Meta Graph API (eingeschränkt, braucht App Review) ``` ### 4.2 Stadt-Portale Viele Städte haben halbstrukturierte Event-Kalender: - freiburg.de/veranstaltungen → RSS/Atom wo vorhanden, sonst Website-Extractor - basel.ch/events → Ähnlich - Tourismus-Seiten (Schwarzwald-Tourismus, Basel-Tourismus) → Diese werden als `type: 'website'` Quellen mit spezifischen Crawl-Hinweisen angelegt. ### 4.3 Deliverables Phase 4 - [ ] Provider-Adapter-Interface + Refactoring bestehender Parser - [ ] Eventbrite-Provider - [ ] Meetup-Provider - [ ] Stadt-Portal-Unterstützung (optimierte Extraktion) --- ## Abhängigkeiten | Service | Rolle | Schon vorhanden? | |---------|-------|-------------------| | mana-events (3065) | Hosting der Discovery-Logik + DB | Ja, wird erweitert | | mana-research (3068) | Web-Suche + Extraktion | Ja | | mana-geocoding (3018) | Region-Geocoding + Distanzberechnung | Ja | | mana-llm | LLM-Aufrufe für Extraktion + Klassifikation | Ja | | mana-credits | Kosten-Tracking für LLM + Research-Calls | Ja | | mana-notify (3024) | Push-Notifications für Digests | Ja | | mana-ai (3067) | Mission-Runner für proaktive Vorschläge | Ja, Phase 3 | **Neue npm-Dependencies:** - `node-ical` — iCal-Parsing (Phase 1) - Keine weiteren — alles andere ist über bestehende Services abgedeckt --- ## Risiken + Mitigationen | Risiko | Mitigation | |--------|------------| | iCal-Feeds kaputt / nicht-standard | Robuster Parser + error_count + Auto-Deaktivierung nach 5 Fehlern | | LLM-Extraktion unzuverlässig | Structured Output (JSON-Mode), Validierung, Fallback auf Regex-Extraktion für bekannte Formate | | Zu viele irrelevante Events | Relevanz-Scoring + Dismiss-Feedback + Source-Qualitäts-Tracking | | Hohe LLM-Kosten bei vielen Quellen | Haiku-Klasse nutzen, Caching (gleiche Seite → kein Re-Extract wenn unverändert), Rate-Limits pro User | | Geocoding-Ungenauigkeit | Fallback: Events ohne Koordinaten bekommen Region-Zentrum + maximalen Radius | | DSGVO: öffentliche Events speichern | Events sind öffentlich publiziert, kein personenbezogener Inhalt. User-Actions (save/dismiss) sind personal data → Löschung bei Account-Delete | --- ## Empfehlung **Phase 1 zuerst bauen.** Das allein ist schon wertvoll — ein Nutzer, der 10 iCal-Feeds seiner Lieblings-Venues einträgt, bekommt einen aggregierten Event-Feed ohne dass je eine KI laufen muss. Phase 2 macht es dann intelligent (automatische Quellen-Entdeckung + unstrukturierte Seiten). Phase 3 macht es proaktiv (KI schlägt Events vor). Phase 4 ist nice-to-have. Geschätzter Aufwand Phase 1: Backend ~1.5 Tage, Frontend ~1.5 Tage, Tests ~0.5 Tage = **~3.5 Tage**.