Add Geofence MVP feature implementation

Implemented complete MVP for geofencing functionality with database,
backend logic, MQTT integration, and API endpoints.

**Phase 1: Database & Core Logic**
- scripts/init-geofence-db.js: Database initialization for Geofence tables
- lib/types.ts: TypeScript types for Geofence, GeofenceEvent, GeofenceStatus
- lib/geofence-engine.ts: Core geofencing logic (Haversine distance, state tracking)
- lib/geofence-db.ts: Database layer with CRUD operations
- package.json: Added db:init:geofence script

**Phase 2: MQTT Integration & Email Notifications**
- emails/geofence-enter.tsx: React Email template for enter events
- emails/geofence-exit.tsx: React Email template for exit events
- lib/email-renderer.ts: Added geofence email rendering functions
- lib/geofence-notifications.ts: Notification service for geofence events
- lib/mqtt-subscriber.ts: Integrated automatic geofence checking on location updates

**Phase 3: Minimal API**
- app/api/geofences/route.ts: GET (list) and POST (create) endpoints
- app/api/geofences/[id]/route.ts: DELETE endpoint
- All endpoints with authentication and ownership checks

**MVP Simplifications:**
- No zone limit enforcement (unlimited for all users)
- No notification flags (always send Enter + Exit emails)
- Device assignment required (no NULL device logic)
- Circular geofences only

**Features:**
 Automatic geofence detection on MQTT location updates
 Email notifications for enter/exit events
 State tracking to prevent duplicate events
 REST API for geofence management
 Non-blocking async processing

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-02 18:14:24 +00:00
parent 5369fe3963
commit bd6a7ab187
13 changed files with 1313 additions and 46 deletions

134
emails/geofence-exit.tsx Normal file
View File

@@ -0,0 +1,134 @@
import { Link, Section, Text } from '@react-email/components';
import * as React from 'react';
import { EmailLayout } from './components/email-layout';
import { EmailHeader } from './components/email-header';
import { EmailFooter } from './components/email-footer';
interface GeofenceExitEmailProps {
username: string;
deviceName: string;
geofenceName: string;
timestamp: string;
latitude: number | string;
longitude: number | string;
distanceFromCenter: number;
mapUrl?: string;
}
export const GeofenceExitEmail = ({
username = 'User',
deviceName = 'Device',
geofenceName = 'Zone',
timestamp = new Date().toISOString(),
latitude = 0,
longitude = 0,
distanceFromCenter = 0,
mapUrl,
}: GeofenceExitEmailProps) => {
const formattedTimestamp = new Date(timestamp).toLocaleString('de-DE', {
dateStyle: 'long',
timeStyle: 'short',
});
const formattedDistance = Math.round(distanceFromCenter);
return (
<EmailLayout preview={`${deviceName} hat ${geofenceName} verlassen`}>
<EmailHeader title="Geofence Benachrichtigung" />
<Section style={content}>
<Text style={paragraph}>Hallo {username},</Text>
<Text style={paragraphBold}>
Ihr Gerät "{deviceName}" hat die Zone "{geofenceName}" verlassen.
</Text>
<Section style={detailsBox}>
<Text style={detailsTitle}>Details:</Text>
<Text style={detailItem}>
<strong>Zeit:</strong> {formattedTimestamp}
</Text>
<Text style={detailItem}>
<strong>Position:</strong> {latitude}, {longitude}
</Text>
<Text style={detailItem}>
<strong>Distanz vom Zentrum:</strong> {formattedDistance} Meter
</Text>
</Section>
{mapUrl && (
<Text style={paragraph}>
<Link href={mapUrl} style={link}>
Position auf Karte anzeigen
</Link>
</Text>
)}
<Text style={footerText}>
Diese Benachrichtigung wurde automatisch von Ihrem Location Tracker System gesendet.
</Text>
</Section>
<EmailFooter />
</EmailLayout>
);
};
export default GeofenceExitEmail;
const content = {
padding: '20px 40px',
};
const paragraph = {
color: '#374151',
fontSize: '16px',
lineHeight: '1.6',
margin: '0 0 16px',
};
const paragraphBold = {
...paragraph,
fontSize: '18px',
fontWeight: '600',
color: '#dc2626', // Red for exit event
};
const detailsBox = {
backgroundColor: '#f9fafb',
border: '1px solid #e5e7eb',
borderRadius: '8px',
padding: '16px',
margin: '20px 0',
};
const detailsTitle = {
color: '#111827',
fontSize: '14px',
fontWeight: '600',
margin: '0 0 12px',
textTransform: 'uppercase' as const,
letterSpacing: '0.5px',
};
const detailItem = {
color: '#374151',
fontSize: '15px',
lineHeight: '1.6',
margin: '0 0 8px',
};
const link = {
color: '#2563eb',
textDecoration: 'underline',
fontSize: '16px',
};
const footerText = {
color: '#6b7280',
fontSize: '14px',
lineHeight: '1.5',
margin: '24px 0 0',
paddingTop: '16px',
borderTop: '1px solid #e5e7eb',
};