# Geofence Feature - Implementierungsplan ## Übersicht Dieses Dokument beschreibt die schrittweise Implementierung eines Geofence-Features für den Location Tracker. Das Feature ermöglicht Device-Ownern, kreisförmige Geofence-Zonen zu definieren und benachrichtigt sie via Email und Dashboard, wenn ihre Geräte diese Zonen betreten oder verlassen. ## Feature-Anforderungen ### Funktionale Anforderungen - ✅ **Enter/Exit Events**: Benachrichtigung beim Betreten und Verlassen von Zonen - ✅ **Kreisförmige Zonen**: Mittelpunkt (Lat/Lon) + Radius in Metern - ✅ **Pro Device Owner**: Jeder User kann Zonen für seine eigenen Geräte erstellen - ✅ **Multi-Zone Support**: Ein Gerät kann gleichzeitig in mehreren Zonen sein - ✅ **Email Benachrichtigungen**: Via bestehendes SMTP-System - ✅ **Dashboard Integration**: Event-History im Admin-Panel anzeigen - ✅ **Zonenlimit**: Admins unbegrenzt, normale User maximal 5 Zonen - ✅ **30-Tage History**: Automatische Cleanup-Funktion ### Nicht-Funktionale Anforderungen - Performance: Geofence-Check darf Location-Ingestion nicht verzögern - Skalierbarkeit: Design sollte später Polygon-Zonen ermöglichen - User Experience: Einfache visuelle Zone-Erstellung auf Karte --- ## Datenbank-Schema ### 1. Geofence-Zonen Tabelle ```sql CREATE TABLE IF NOT EXISTS Geofence ( id TEXT PRIMARY KEY, -- UUID name TEXT NOT NULL, -- z.B. "Zuhause", "Büro" description TEXT, -- Optionale Beschreibung -- Geometrie (vorerst nur Kreis) shape_type TEXT NOT NULL DEFAULT 'circle', -- 'circle' (später: 'polygon') center_latitude REAL NOT NULL, -- Mittelpunkt Breitengrad center_longitude REAL NOT NULL, -- Mittelpunkt Längengrad radius_meters INTEGER NOT NULL, -- Radius in Metern -- Zuordnung owner_id TEXT NOT NULL, -- FK zu User.id device_id TEXT, -- Optional: FK zu Device.id (NULL = alle User-Devices) -- Status & Metadaten is_active INTEGER DEFAULT 1, -- 0 = deaktiviert, 1 = aktiv color TEXT DEFAULT '#3b82f6', -- Farbe für Karten-Visualisierung -- Benachrichtigungen notify_on_enter INTEGER DEFAULT 1, -- 0 oder 1 notify_on_exit INTEGER DEFAULT 1, -- 0 oder 1 email_notifications INTEGER DEFAULT 1, -- 0 oder 1 -- Timestamps created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (owner_id) REFERENCES User(id) ON DELETE CASCADE, FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE, CHECK (shape_type IN ('circle', 'polygon')), CHECK (radius_meters > 0 AND radius_meters <= 50000), -- Max 50km CHECK (center_latitude BETWEEN -90 AND 90), CHECK (center_longitude BETWEEN -180 AND 180) ); CREATE INDEX idx_geofence_owner ON Geofence(owner_id); CREATE INDEX idx_geofence_device ON Geofence(device_id); CREATE INDEX idx_geofence_active ON Geofence(is_active); ``` ### 2. Geofence-Events Tabelle ```sql CREATE TABLE IF NOT EXISTS GeofenceEvent ( id INTEGER PRIMARY KEY AUTOINCREMENT, geofence_id TEXT NOT NULL, -- FK zu Geofence.id device_id TEXT NOT NULL, -- FK zu Device.id location_id INTEGER NOT NULL, -- FK zu Location.id -- Event-Details event_type TEXT NOT NULL, -- 'enter' oder 'exit' latitude REAL NOT NULL, -- Position beim Event longitude REAL NOT NULL, -- Metadaten distance_from_center REAL, -- Distanz vom Zonenmittelpunkt in Metern notification_sent INTEGER DEFAULT 0, -- 0 = pending, 1 = sent, 2 = failed notification_error TEXT, -- Error Message falls fehlgeschlagen -- Timestamps timestamp TEXT NOT NULL, -- Event-Zeitstempel (von Location) created_at TEXT DEFAULT (datetime('now')), -- Wann Event erfasst wurde FOREIGN KEY (geofence_id) REFERENCES Geofence(id) ON DELETE CASCADE, FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE, CHECK (event_type IN ('enter', 'exit')) ); CREATE INDEX idx_geofence_event_geofence ON GeofenceEvent(geofence_id); CREATE INDEX idx_geofence_event_device ON GeofenceEvent(device_id); CREATE INDEX idx_geofence_event_timestamp ON GeofenceEvent(timestamp DESC); CREATE INDEX idx_geofence_event_notification ON GeofenceEvent(notification_sent); CREATE INDEX idx_geofence_event_composite ON GeofenceEvent(device_id, geofence_id, timestamp DESC); ``` ### 3. Geofence-Status Tabelle (für State Tracking) ```sql CREATE TABLE IF NOT EXISTS GeofenceStatus ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, -- FK zu Device.id geofence_id TEXT NOT NULL, -- FK zu Geofence.id -- Aktueller Status is_inside INTEGER NOT NULL DEFAULT 0, -- 0 = outside, 1 = inside last_enter_time TEXT, -- Wann zuletzt betreten last_exit_time TEXT, -- Wann zuletzt verlassen last_checked_at TEXT, -- Letzte Prüfung -- Timestamps created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE, FOREIGN KEY (geofence_id) REFERENCES Geofence(id) ON DELETE CASCADE, UNIQUE(device_id, geofence_id) -- Ein Status-Eintrag pro Device/Geofence-Paar ); CREATE INDEX idx_geofence_status_device ON GeofenceStatus(device_id); CREATE INDEX idx_geofence_status_geofence ON GeofenceStatus(geofence_id); CREATE INDEX idx_geofence_status_inside ON GeofenceStatus(is_inside); ``` --- ## Backend-Architektur ### 1. Geofence-Engine (`lib/geofence-engine.ts`) Die zentrale Logik für Geofence-Berechnungen: ```typescript // Haversine-Distanz zwischen zwei Punkten berechnen function calculateDistance( lat1: number, lon1: number, lat2: number, lon2: number ): number { // Gibt Distanz in Metern zurück } // Prüft ob Position innerhalb einer Zone ist function isInsideGeofence( latitude: number, longitude: number, geofence: Geofence ): boolean { if (geofence.shape_type === 'circle') { const distance = calculateDistance( latitude, longitude, geofence.center_latitude, geofence.center_longitude ); return distance <= geofence.radius_meters; } // Später: Polygon-Support return false; } // Hauptfunktion: Prüft Location gegen alle aktiven Geofences async function checkGeofences( location: Location, deviceId: string ): Promise { // 1. Alle aktiven Geofences für Device/Owner laden // 2. Für jede Zone prüfen ob drinnen // 3. Mit GeofenceStatus vergleichen (war vorher drinnen?) // 4. Enter/Exit Events generieren // 5. GeofenceStatus aktualisieren // 6. Events zurückgeben } ``` ### 2. Datenbank-Layer (`lib/geofence-db.ts`) CRUD-Operationen für Geofences: ```typescript export const geofenceDb = { // Geofence CRUD create(data: CreateGeofenceInput): Geofence, update(id: string, data: UpdateGeofenceInput): Geofence | null, delete(id: string): boolean, findById(id: string): Geofence | null, findByOwner(ownerId: string): Geofence[], findByDevice(deviceId: string): Geofence[], findActiveForDevice(deviceId: string, ownerId: string): Geofence[], // Zonenlimit prüfen countByOwner(ownerId: string): number, canCreateGeofence(ownerId: string, userRole: string): boolean, // GeofenceStatus Operations getStatus(deviceId: string, geofenceId: string): GeofenceStatus | null, updateStatus(deviceId: string, geofenceId: string, isInside: boolean): void, // GeofenceEvent Operations createEvent(event: CreateEventInput): GeofenceEvent, findEvents(filters: EventFilters): GeofenceEvent[], markNotificationSent(eventId: number, success: boolean, error?: string): void, // Cleanup cleanupOldEvents(olderThanDays: number): number, }; ``` ### 3. Integration in MQTT-Subscriber (`lib/mqtt-subscriber.ts`) Geofence-Check bei jeder neuen Location: ```typescript // In handleLocationUpdate(): async function handleLocationUpdate(data: any) { // ... bestehende Location-Speicherung ... // Geofence-Check asynchron ausführen (nicht blockieren) setImmediate(async () => { try { const events = await checkGeofences(savedLocation, deviceId); // Events in Datenbank speichern for (const event of events) { await geofenceDb.createEvent(event); } // Benachrichtigungen versenden (asynchron) if (events.length > 0) { await sendGeofenceNotifications(events); } } catch (error) { console.error('Geofence check failed:', error); } }); } ``` ### 4. Notification-Service (`lib/geofence-notifications.ts`) ```typescript async function sendGeofenceNotifications(events: GeofenceEvent[]) { for (const event of events) { try { const geofence = await geofenceDb.findById(event.geofence_id); const device = await deviceDb.findById(event.device_id); const owner = await userDb.findById(geofence.owner_id); if (!geofence.email_notifications || !owner.email) { continue; } // Email versenden via bestehendes SMTP-System await emailService.sendGeofenceAlert({ to: owner.email, deviceName: device.name, geofenceName: geofence.name, eventType: event.event_type, timestamp: event.timestamp, latitude: event.latitude, longitude: event.longitude, }); // Als erfolgreich markieren await geofenceDb.markNotificationSent(event.id, true); } catch (error) { console.error('Failed to send notification:', error); await geofenceDb.markNotificationSent( event.id, false, error.message ); } } } ``` --- ## API-Endpunkte ### 1. Geofence CRUD (`/api/geofences`) **GET /api/geofences** - Alle Geofences des eingeloggten Users - Query-Parameter: `deviceId` (optional) - Response: `{ geofences: Geofence[] }` **POST /api/geofences** - Neue Geofence erstellen - Prüft Zonenlimit (5 für User, unbegrenzt für Admin) - Body: `{ name, description?, center_latitude, center_longitude, radius_meters, device_id?, color?, notify_on_enter?, notify_on_exit? }` - Response: `{ geofence: Geofence }` **PATCH /api/geofences/:id** - Geofence aktualisieren - Prüft Ownership - Body: `{ name?, description?, radius_meters?, color?, notify_on_enter?, notify_on_exit?, is_active? }` - Response: `{ geofence: Geofence }` **DELETE /api/geofences/:id** - Geofence löschen (CASCADE: Status & Events werden auch gelöscht) - Prüft Ownership - Response: `{ success: boolean }` ### 2. Geofence-Events (`/api/geofences/events`) **GET /api/geofences/events** - Event-History des Users - Query-Parameter: - `geofenceId` (optional) - `deviceId` (optional) - `eventType` (optional: 'enter' | 'exit') - `startTime` (optional) - `endTime` (optional) - `limit` (default: 100, max: 1000) - Response: `{ events: GeofenceEvent[], total: number }` **GET /api/geofences/events/stats** - Statistiken für Dashboard - Response: ```json { "total_events": 1234, "events_last_24h": 45, "most_active_geofence": { "id": "...", "name": "Zuhause", "event_count": 89 }, "most_active_device": { "id": "12", "name": "iPhone", "event_count": 67 } } ``` ### 3. Geofence-Status (`/api/geofences/status`) **GET /api/geofences/status** - Aktueller Status aller Devices in allen Zonen - Response: ```json { "status": [ { "device_id": "12", "device_name": "iPhone", "geofence_id": "uuid-1", "geofence_name": "Zuhause", "is_inside": true, "last_enter_time": "2025-11-20T14:30:00.000Z" } ] } ``` ### 4. Geofence-Cleanup (`/api/geofences/cleanup`) **POST /api/geofences/cleanup** - Alte Events löschen (älter als 30 Tage) - Admin-only - Response: `{ deleted_events: number }` --- ## Frontend-Komponenten ### 1. Geofence-Management-Seite (`/app/admin/geofences/page.tsx`) **Features:** - Liste aller Geofences mit Status (aktiv/inaktiv) - "Add Geofence" Button öffnet Modal - Edit/Delete Actions pro Zone - Zeigt Zonenlimit-Counter: "3 / 5 Zonen" (für User) oder "12 Zonen" (für Admin) **Tabellen-Spalten:** - Name - Device (falls zugeordnet) - Status (Aktiv/Inaktiv Badge) - Radius - Created - Actions (Edit, Delete) ### 2. Geofence-Editor Modal (`/components/geofence/GeofenceEditor.tsx`) **Mode: Create** - Name-Input (required) - Description-Textarea (optional) - Device-Dropdown (optional: "Alle meine Devices" oder spezifisches Device) - Karte mit Click-To-Place Marker - Radius-Slider (50m - 5000m mit Live-Preview) - Color-Picker - Checkboxen: "Email bei Enter", "Email bei Exit" **Mode: Edit** - Gleiche Felder wie Create - Zusätzlich: "Aktiv/Inaktiv" Toggle **Validierung:** - Name: 1-100 Zeichen - Radius: 50-50000 Meter - Bei Create: Prüfe Zonenlimit ### 3. Geofence-Layer für Karte (`/components/map/GeofenceLayer.tsx`) **Funktionalität:** - Zeigt alle Geofence-Zonen als Kreise auf der Karte - Farbe gemäß `geofence.color` - Opacity: 0.3 für Fill, 0.8 für Border - Popup beim Hover/Click: - Name - Radius - Status (z.B. "Gerät X ist drinnen") **Integration:** - In MapView.tsx integrieren - Optional: Toggle-Button "Geofences anzeigen/verstecken" ### 4. Geofence-Events Dashboard (`/app/admin/geofences/events/page.tsx`) **Features:** - Tabelle mit neuesten Events (Enter/Exit) - Filter: Device, Geofence, Event-Type, Datum - Auto-Refresh alle 30 Sekunden - Export als CSV **Tabellen-Spalten:** - Timestamp - Device - Geofence - Event (Badge: "Enter" grün, "Exit" rot) - Position (Lat/Lon) - Notification Status (Badge: "Sent" grün, "Failed" rot, "Pending" gelb) ### 5. Dashboard-Widget (`/components/admin/GeofenceWidget.tsx`) **Zeigt auf Admin-Startseite:** - Anzahl aktiver Geofences - Events in letzten 24h - Aktuelle Devices in Zonen (mit Namen) - Link zu /admin/geofences --- ## Email-Templates ### 1. Geofence-Enter Email (`emails/geofence-enter.tsx`) ``` Betreff: [Device] hat [Zone] betreten Hallo [Username], Ihr Gerät "[Device Name]" hat die Zone "[Geofence Name]" betreten. Details: - Zeit: [Timestamp] - Position: [Lat, Lon] - Distanz vom Zentrum: [X Meter] [Link zur Karte anzeigen] --- Gesendet von Location Tracker ``` ### 2. Geofence-Exit Email (`emails/geofence-exit.tsx`) ``` Betreff: [Device] hat [Zone] verlassen Hallo [Username], Ihr Gerät "[Device Name]" hat die Zone "[Geofence Name]" verlassen. Details: - Zeit: [Timestamp] - Position: [Lat, Lon] - Aufenthaltsdauer: [X Stunden Y Minuten] (falls getrackt) [Link zur Karte anzeigen] --- Gesendet von Location Tracker ``` --- ## Schrittweise Implementierung ### Phase 1: Datenbank & Core-Logik (Backend) **Priorität: HOCH | Geschätzter Aufwand: Mittel** 1. **Datenbank-Tabellen erstellen** - Neues Script `scripts/init-geofence-db.js` - Tabellen: `Geofence`, `GeofenceEvent`, `GeofenceStatus` - Indexes für Performance - Zu bearbeitende Datei: `scripts/init-geofence-db.js` (neu) 2. **Geofence-Engine implementieren** - Haversine-Distanzberechnung - `isInsideGeofence()` Funktion - `checkGeofences()` Hauptlogik mit State-Tracking - Zu bearbeitende Datei: `lib/geofence-engine.ts` (neu) 3. **Datenbank-Layer implementieren** - CRUD-Operationen für `Geofence` - Status-Management für `GeofenceStatus` - Event-Logging für `GeofenceEvent` - Zonenlimit-Checks - Zu bearbeitende Datei: `lib/geofence-db.ts` (neu) 4. **TypeScript-Typen definieren** - Interfaces für `Geofence`, `GeofenceEvent`, `GeofenceStatus` - Zu bearbeitende Datei: `lib/types.ts` (erweitern) **Testen:** ```bash # Datenbank initialisieren node scripts/init-geofence-db.js # Unit-Test für Distanzberechnung node scripts/test-geofence-engine.js ``` --- ### Phase 2: MQTT-Integration & Event-Processing **Priorität: HOCH | Geschätzter Aufwand: Gering-Mittel** 1. **Geofence-Check in MQTT-Subscriber integrieren** - Nach Location-Speicherung Geofence-Check aufrufen - Asynchrone Ausführung (setImmediate) - Error-Handling & Logging - Zu bearbeitende Datei: `lib/mqtt-subscriber.ts` 2. **Notification-Service implementieren** - Email-Versand für Enter/Exit Events - Integration mit bestehendem SMTP-Service - Retry-Logik bei Fehlern - Zu bearbeitende Datei: `lib/geofence-notifications.ts` (neu) 3. **Email-Templates erstellen** - `emails/geofence-enter.tsx` - `emails/geofence-exit.tsx` - React-Email-Komponenten mit Styling - Zu bearbeitende Dateien: - `emails/geofence-enter.tsx` (neu) - `emails/geofence-exit.tsx` (neu) **Testen:** ```bash # Test-Location einfügen die Geofence triggert node scripts/test-geofence-trigger.js # SMTP-Email-Versand testen curl -X POST http://localhost:3000/api/geofences/test-notification ``` --- ### Phase 3: API-Endpunkte **Priorität: HOCH | Geschätzter Aufwand: Mittel** 1. **CRUD-Endpunkte implementieren** - `POST /api/geofences` - Create - `GET /api/geofences` - List - `PATCH /api/geofences/:id` - Update - `DELETE /api/geofences/:id` - Delete - Authentifizierung & Authorization prüfen - Zu bearbeitende Datei: `app/api/geofences/route.ts` (neu) - Zu bearbeitende Datei: `app/api/geofences/[id]/route.ts` (neu) 2. **Event-Endpunkte implementieren** - `GET /api/geofences/events` - History mit Filtern - `GET /api/geofences/events/stats` - Dashboard-Statistiken - Zu bearbeitende Datei: `app/api/geofences/events/route.ts` (neu) - Zu bearbeitende Datei: `app/api/geofences/events/stats/route.ts` (neu) 3. **Status-Endpunkt implementieren** - `GET /api/geofences/status` - Aktueller Status aller Devices - Zu bearbeitende Datei: `app/api/geofences/status/route.ts` (neu) 4. **Cleanup-Endpunkt implementieren** - `POST /api/geofences/cleanup` - Alte Events löschen (Admin-only) - Zu bearbeitende Datei: `app/api/geofences/cleanup/route.ts` (neu) **Testen:** ```bash # API-Endpunkte testen curl -X POST http://localhost:3000/api/geofences \ -H "Content-Type: application/json" \ -d '{"name":"Test Zone","center_latitude":50.0,"center_longitude":8.0,"radius_meters":500}' curl http://localhost:3000/api/geofences curl http://localhost:3000/api/geofences/events ``` --- ### Phase 4: Frontend - Geofence-Management **Priorität: MITTEL | Geschätzter Aufwand: Hoch** 1. **Geofence-Management-Seite erstellen** - Liste aller Geofences - Add/Edit/Delete Actions - Zonenlimit-Anzeige - Zu bearbeitende Datei: `app/admin/geofences/page.tsx` (neu) 2. **Geofence-Editor Modal implementieren** - Formular für Create/Edit - Leaflet-Karte für Positionswahl - Radius-Slider mit Live-Preview - Validierung - Zu bearbeitende Datei: `components/geofence/GeofenceEditor.tsx` (neu) 3. **Geofence-Layer für Karte implementieren** - Kreise auf Karte zeichnen - Popup mit Zone-Info - Toggle-Button zum Ein/Ausblenden - Zu bearbeitende Datei: `components/map/GeofenceLayer.tsx` (neu) - Zu bearbeitende Datei: `components/map/MapView.tsx` (erweitern) **Testen:** - Manuell: Geofence erstellen, bearbeiten, löschen - Visuell: Zonen auf Karte korrekt angezeigt - Edge-Cases: Zonenlimit erreicht, ungültige Koordinaten --- ### Phase 5: Frontend - Event-Dashboard **Priorität: MITTEL | Geschätzter Aufwand: Mittel** 1. **Event-History-Seite erstellen** - Tabelle mit allen Events - Filter (Device, Geofence, Event-Type, Datum) - Auto-Refresh (30s) - CSV-Export - Zu bearbeitende Datei: `app/admin/geofences/events/page.tsx` (neu) 2. **Dashboard-Widget implementieren** - Anzeige auf Admin-Startseite - Aktive Zonen, Events 24h, Devices in Zonen - Link zu /admin/geofences - Zu bearbeitende Datei: `components/admin/GeofenceWidget.tsx` (neu) - Zu bearbeitende Datei: `app/admin/page.tsx` (erweitern) 3. **Notification-Status-Badges** - "Sent", "Failed", "Pending" Badges - Tooltip mit Error-Message bei Failed - Zu bearbeitende Datei: `components/geofence/NotificationBadge.tsx` (neu) **Testen:** - Manuell: Events in History sichtbar - Manuell: Filter funktionieren korrekt - Performance: 1000+ Events laden --- ### Phase 6: Optimierungen & Cleanup **Priorität: NIEDRIG | Geschätzter Aufwand: Gering** 1. **Automatischer Cleanup implementieren** - Cron-Job oder Scheduled Task - Events älter als 30 Tage löschen - Logging & Monitoring - Zu bearbeitende Datei: `lib/geofence-cleanup.ts` (neu) 2. **Performance-Optimierungen** - Index-Tuning für Datenbank - Caching von aktiven Geofences - Batch-Processing für Notifications - Zu bearbeitende Dateien: Diverse (Profiling erforderlich) 3. **Dokumentation aktualisieren** - README.md mit Geofence-Sektion - API-Dokumentation - Setup-Guide - Zu bearbeitende Datei: `README.md` 4. **Unit-Tests schreiben** - Geofence-Engine Tests - API-Endpoint Tests - Zu bearbeitende Dateien: `__tests__/geofence-*.test.ts` (neu) **Testen:** - Automated: Jest Unit-Tests - Load-Testing: 100+ Devices, 50+ Geofences - Edge-Cases: Timezone-Handling, Duplikate --- ## Technische Details ### 1. Haversine-Distanzberechnung ```typescript function calculateDistance( lat1: number, lon1: number, lat2: number, lon2: number ): number { const R = 6371e3; // Erdradius in Metern const φ1 = lat1 * Math.PI / 180; const φ2 = lat2 * Math.PI / 180; const Δφ = (lat2 - lat1) * Math.PI / 180; const Δλ = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // Distanz in Metern } ``` **Genauigkeit:** - Fehler < 0.5% für Distanzen < 1000km - Ausreichend für Geofencing-Zwecke - Alternative für höhere Genauigkeit: Vincenty-Formel ### 2. State-Tracking-Logik ```typescript async function checkGeofences( location: Location, deviceId: string ): Promise { const events: GeofenceEvent[] = []; // 1. Alle aktiven Geofences für Device/Owner laden const geofences = await geofenceDb.findActiveForDevice( deviceId, location.owner_id ); for (const geofence of geofences) { // 2. Prüfen ob aktuell drinnen const isInside = isInsideGeofence( parseFloat(location.latitude), parseFloat(location.longitude), geofence ); // 3. Status aus Datenbank holen const status = await geofenceDb.getStatus(deviceId, geofence.id); const wasInside = status ? status.is_inside === 1 : false; // 4. Enter/Exit Events generieren if (isInside && !wasInside) { // ENTER Event if (geofence.notify_on_enter) { events.push({ geofence_id: geofence.id, device_id: deviceId, location_id: location.id, event_type: 'enter', latitude: location.latitude, longitude: location.longitude, timestamp: location.timestamp, distance_from_center: calculateDistance( parseFloat(location.latitude), parseFloat(location.longitude), geofence.center_latitude, geofence.center_longitude ), }); } } else if (!isInside && wasInside) { // EXIT Event if (geofence.notify_on_exit) { events.push({ geofence_id: geofence.id, device_id: deviceId, location_id: location.id, event_type: 'exit', latitude: location.latitude, longitude: location.longitude, timestamp: location.timestamp, distance_from_center: calculateDistance( parseFloat(location.latitude), parseFloat(location.longitude), geofence.center_latitude, geofence.center_longitude ), }); } } // 5. Status aktualisieren await geofenceDb.updateStatus(deviceId, geofence.id, isInside); } return events; } ``` **Wichtig:** - State-Tracking verhindert Doppel-Events - `GeofenceStatus.is_inside` wird bei jeder Location aktualisiert - Nur Zustandswechsel (0→1 oder 1→0) triggern Events ### 3. Performance-Überlegungen **Problem:** Geofence-Check bei jeder Location kann bei vielen Zonen langsam werden. **Lösungen:** 1. **Spatial Indexing (später)** - RTree oder QuadTree für schnelle Umkreissuche - Vorerst nicht nötig (<100 Zonen pro User) 2. **Caching** - Aktive Geofences in Memory halten - Cache invalidieren bei Create/Update/Delete - Cache-TTL: 60 Sekunden 3. **Asynchrone Verarbeitung** - `setImmediate()` für non-blocking Execution - Location-Speicherung wird nicht verzögert - Events werden nachgelagert verarbeitet 4. **Batch-Processing für Notifications** - Sammeln von Events über 1-2 Minuten - Eine Email mit mehreren Events statt vieler Emails - Reduziert SMTP-Load --- ## Erweiterungsmöglichkeiten (Zukunft) ### 1. Polygon-Zonen - Statt Kreis: Beliebige Vielecke definieren - Point-in-Polygon-Algorithmus (Ray-Casting) - UI: Leaflet Draw für interaktives Zeichnen ### 2. Zeit-basierte Zonen - Geofence nur zu bestimmten Zeiten aktiv (z.B. Montag-Freitag 9-17 Uhr) - Use-Case: "Benachrichtige mich nur wenn Kind nach 18 Uhr die Schule verlässt" ### 3. Aufenthaltszeit-Tracking - Wie lange war Device in Zone? - Statistiken: Durchschnittliche Aufenthaltsdauer - Heatmap: Meistbesuchte Zonen ### 4. Bedingte Notifications - "Benachrichtige mich nur wenn Device länger als X Minuten in Zone" - "Benachrichtige mich nur bei Exit, wenn vorher mindestens Y Minuten drinnen" ### 5. Geofence-Gruppen - Mehrere Zonen zu Gruppe zusammenfassen (z.B. "Arbeit": Büro + Parkplatz + Kantine) - Events nur beim Betreten/Verlassen der Gesamtgruppe ### 6. Webhook-Integration - Statt Email: POST-Request an externe URL - Ermöglicht Integration mit Zapier, IFTTT, Home-Assistant, etc. ### 7. Push-Notifications - Zusätzlich zu Email: Browser-Push oder Mobile-Push - Schnellere Benachrichtigung --- ## Sicherheitsüberlegungen 1. **Authorization:** - User kann nur eigene Geofences sehen/bearbeiten - Device-Zuordnung prüfen (darf User dieses Device nutzen?) - Admin kann alle Geofences sehen (für Support) 2. **Zonenlimit:** - Verhindert DoS durch zu viele Zonen - Admin: unbegrenzt (vertrauenswürdig) - User: maximal 5 Zonen 3. **Radius-Limit:** - Maximal 50km Radius - Verhindert unsinnig große Zonen - Kann später konfigurierbar gemacht werden 4. **Rate-Limiting:** - Email-Notifications: Maximal 1 Email pro 5 Minuten pro Zone/Device - Verhindert Spam bei GPS-Jitter am Zone-Rand 5. **Input-Validierung:** - Koordinaten: -90/+90 (Lat), -180/+180 (Lon) - Radius: 50-50000 Meter - Name: XSS-Protection --- ## FAQ **Q: Was passiert bei GPS-Ungenauigkeit am Zone-Rand?** A: Implementiere Hysterese (z.B. 10 Meter): Zone wird erst als "verlassen" gewertet, wenn Device 10m außerhalb ist. Verhindert Flapping. **Q: Wie wird Batterie geschont?** A: Geofence-Check läuft nur serverseitig, nicht auf dem Device. OwnTracks sendet Location wie gewohnt. **Q: Können mehrere User dieselbe Zone nutzen?** A: Nein, jede Zone gehört einem Owner. Alternative: "Template-Zonen" die geklont werden können. **Q: Was passiert wenn MQTT-Subscriber offline ist?** A: Events werden nicht generiert. Lösung: Batch-Check beim Subscriber-Start für verpasste Locations (aufwendig, vorerst nicht implementieren). **Q: Wie werden Zeitzonen behandelt?** A: Alle Timestamps in UTC speichern. Frontend konvertiert zur lokalen Timezone des Users. **Q: Kann ich Geofences teilen?** A: Vorerst nein. Erweiterung: `shared_with_users` Feld in Geofence-Tabelle für Multi-User-Zonen. --- ## Zusammenfassung Dieses Design bietet eine solide, erweiterbare Grundlage für Geofencing: ✅ **Simpel starten:** Kreisförmige Zonen sind einfach zu berechnen und visualisieren ✅ **Skalierbar:** Design ermöglicht später Polygone, Gruppen, Webhooks ✅ **Performant:** Asynchrone Verarbeitung, Caching, Spatial-Indexing möglich ✅ **User-Freundlich:** Visuelle Zone-Erstellung auf Karte, klare Events ✅ **Sicher:** Authorization, Limits, Input-Validierung **Geschätzter Gesamt-Aufwand:** 3-5 Entwicklungstage (je nach Erfahrung mit Leaflet & Spatial-Queries) **Empfohlene Reihenfolge:** 1. Phase 1 (Backend-Fundament) 2. Phase 2 (MQTT-Integration) 3. Phase 3 (APIs) 4. Phase 4 (Geofence-Management UI) 5. Phase 5 (Dashboard) 6. Phase 6 (Optimierungen) **Next Steps:** 1. Dieses Dokument reviewen 2. Prioritäten festlegen 3. Mit Phase 1 starten 4. Regelmäßig testen & iterieren