- 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>
28 KiB
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
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
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)
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:
// 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:
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:
// 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)
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:
{
"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:
{
"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
-
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)
- Neues Script
-
Geofence-Engine implementieren
- Haversine-Distanzberechnung
isInsideGeofence()FunktioncheckGeofences()Hauptlogik mit State-Tracking- Zu bearbeitende Datei:
lib/geofence-engine.ts(neu)
-
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)
- CRUD-Operationen für
-
TypeScript-Typen definieren
- Interfaces für
Geofence,GeofenceEvent,GeofenceStatus - Zu bearbeitende Datei:
lib/types.ts(erweitern)
- Interfaces für
Testen:
# 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
-
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
-
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)
-
Email-Templates erstellen
emails/geofence-enter.tsxemails/geofence-exit.tsx- React-Email-Komponenten mit Styling
- Zu bearbeitende Dateien:
emails/geofence-enter.tsx(neu)emails/geofence-exit.tsx(neu)
Testen:
# 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
-
CRUD-Endpunkte implementieren
POST /api/geofences- CreateGET /api/geofences- ListPATCH /api/geofences/:id- UpdateDELETE /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)
-
Event-Endpunkte implementieren
GET /api/geofences/events- History mit FilternGET /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)
-
Status-Endpunkt implementieren
GET /api/geofences/status- Aktueller Status aller Devices- Zu bearbeitende Datei:
app/api/geofences/status/route.ts(neu)
-
Cleanup-Endpunkt implementieren
POST /api/geofences/cleanup- Alte Events löschen (Admin-only)- Zu bearbeitende Datei:
app/api/geofences/cleanup/route.ts(neu)
Testen:
# 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
-
Geofence-Management-Seite erstellen
- Liste aller Geofences
- Add/Edit/Delete Actions
- Zonenlimit-Anzeige
- Zu bearbeitende Datei:
app/admin/geofences/page.tsx(neu)
-
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)
-
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
-
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)
-
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)
-
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
-
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)
-
Performance-Optimierungen
- Index-Tuning für Datenbank
- Caching von aktiven Geofences
- Batch-Processing für Notifications
- Zu bearbeitende Dateien: Diverse (Profiling erforderlich)
-
Dokumentation aktualisieren
- README.md mit Geofence-Sektion
- API-Dokumentation
- Setup-Guide
- Zu bearbeitende Datei:
README.md
-
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
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
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_insidewird 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:
-
Spatial Indexing (später)
- RTree oder QuadTree für schnelle Umkreissuche
- Vorerst nicht nötig (<100 Zonen pro User)
-
Caching
- Aktive Geofences in Memory halten
- Cache invalidieren bei Create/Update/Delete
- Cache-TTL: 60 Sekunden
-
Asynchrone Verarbeitung
setImmediate()für non-blocking Execution- Location-Speicherung wird nicht verzögert
- Events werden nachgelagert verarbeitet
-
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
-
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)
-
Zonenlimit:
- Verhindert DoS durch zu viele Zonen
- Admin: unbegrenzt (vertrauenswürdig)
- User: maximal 5 Zonen
-
Radius-Limit:
- Maximal 50km Radius
- Verhindert unsinnig große Zonen
- Kann später konfigurierbar gemacht werden
-
Rate-Limiting:
- Email-Notifications: Maximal 1 Email pro 5 Minuten pro Zone/Device
- Verhindert Spam bei GPS-Jitter am Zone-Rand
-
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:
- Phase 1 (Backend-Fundament)
- Phase 2 (MQTT-Integration)
- Phase 3 (APIs)
- Phase 4 (Geofence-Management UI)
- Phase 5 (Dashboard)
- Phase 6 (Optimierungen)
Next Steps:
- Dieses Dokument reviewen
- Prioritäten festlegen
- Mit Phase 1 starten
- Regelmäßig testen & iterieren