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>
This commit is contained in:
943
docs/geofence.md
Normal file
943
docs/geofence.md
Normal file
@@ -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<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 (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
|
||||
84
docs/locations.md
Normal file
84
docs/locations.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user