diff --git a/docs/geofence.md b/docs/geofence.md new file mode 100644 index 0000000..7dcd4cf --- /dev/null +++ b/docs/geofence.md @@ -0,0 +1,943 @@ +# 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 diff --git a/docs/locations.md b/docs/locations.md new file mode 100644 index 0000000..354743e --- /dev/null +++ b/docs/locations.md @@ -0,0 +1,84 @@ +# Location Database Documentation + +## Database File +- **Filename:** `locations.sqlite` +- **Type:** SQLite 3.x database +- **Encoding:** UTF-8 +- **Current Records:** 367 location entries + +## Database Schema + +### Location Table + +| Column | Type | Required | Default | Description | +|--------|------|----------|---------|-------------| +| `id` | INTEGER | - | AUTO_INCREMENT | Primary key | +| `latitude` | REAL | Yes | - | GPS latitude coordinate | +| `longitude` | REAL | Yes | - | GPS longitude coordinate | +| `timestamp` | TEXT | Yes | - | Time when location was recorded | +| `user_id` | INTEGER | No | 0 | Telegram user ID | +| `first_name` | TEXT | No | - | User's first name | +| `last_name` | TEXT | No | - | User's last name | +| `username` | TEXT | No | - | Telegram username | +| `marker_label` | TEXT | No | - | Custom label for map marker | +| `display_time` | TEXT | No | - | Formatted time for display | +| `chat_id` | INTEGER | No | 0 | Telegram chat ID | +| `battery` | INTEGER | No | - | Device battery level (0-100) | +| `speed` | REAL | No | - | Movement speed | +| `created_at` | TEXT | No | datetime('now') | Database insertion timestamp | + +## Database Tables +- `Location` - Main table storing location data +- `sqlite_sequence` - SQLite internal table for auto-increment tracking + +## Verification Commands + +### Check database file +```bash +file locations.sqlite +``` + +### List all tables +```bash +sqlite3 locations.sqlite "SELECT name FROM sqlite_master WHERE type='table';" +``` + +### View table schema +```bash +sqlite3 locations.sqlite "PRAGMA table_info(Location);" +``` + +### Count records +```bash +sqlite3 locations.sqlite "SELECT COUNT(*) FROM Location;" +``` + +### Sample query +```bash +sqlite3 locations.sqlite "SELECT id, latitude, longitude, timestamp, user_id FROM Location LIMIT 5;" +``` + +## Future Development Ideas + +### Potential Features +- [ ] Location history visualization +- [ ] Geofencing alerts +- [ ] Route tracking and analysis +- [ ] Speed monitoring and statistics +- [ ] Battery level tracking over time +- [ ] Multi-user location comparison +- [ ] Export to GPX/KML formats +- [ ] Privacy controls and data retention policies + +### Optimization Opportunities +- [ ] Add indexes on frequently queried columns (user_id, timestamp) +- [ ] Implement data archival for old records +- [ ] Add spatial queries support (SQLite R*Tree extension) +- [ ] Implement location clustering for map display + +### Integration Points +- [ ] REST API for location queries +- [ ] Real-time location updates via WebSocket/MQTT +- [ ] Map visualization frontend +- [ ] Mobile app data sync +- [ ] Export/backup functionality