Files
location-mqtt-tracker-app/docs/geofence.md
Joachim Hummel 5369fe3963 Add documentation for geofence and locations features
- Add comprehensive geofence feature implementation plan
- Add locations documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 17:52:44 +00:00

944 lines
28 KiB
Markdown

# 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<GeofenceEvent[]> {
// 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<GeofenceEvent[]> {
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 (01 oder 10) 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