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

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

  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:

# 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:

# 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:

# 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

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_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