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

123
lib/geofence-engine.ts Normal file
View File

@@ -0,0 +1,123 @@
/**
* Geofence Engine - Core logic for geofence calculations
* Handles distance calculations and geofence state tracking
*/
import type { Location, Geofence, GeofenceEvent, CreateGeofenceEventInput } from './types';
/**
* Calculate distance between two geographic points using Haversine formula
* Returns distance in meters
*/
export function calculateDistance(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371e3; // Earth radius in meters
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; // Distance in meters
}
/**
* Check if a point is inside a circular geofence
*/
export 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;
}
return false;
}
/**
* Check location against all active geofences for a device
* Returns array of events (enter/exit) that should be generated
*/
export async function checkGeofences(
location: Location,
deviceId: string,
geofenceDb: any
): Promise<CreateGeofenceEventInput[]> {
const events: CreateGeofenceEventInput[] = [];
// Convert latitude/longitude to numbers if they're strings
const lat = typeof location.latitude === 'string'
? parseFloat(location.latitude)
: location.latitude;
const lon = typeof location.longitude === 'string'
? parseFloat(location.longitude)
: location.longitude;
// Get all active geofences for this device
const geofences = await geofenceDb.findActiveForDevice(deviceId);
for (const geofence of geofences) {
// Check if currently inside
const isInside = isInsideGeofence(lat, lon, geofence);
// Get previous status
const status = await geofenceDb.getStatus(deviceId, geofence.id);
const wasInside = status ? status.is_inside === 1 : false;
// Calculate distance from center
const distanceFromCenter = calculateDistance(
lat,
lon,
geofence.center_latitude,
geofence.center_longitude
);
// Generate events based on state change
if (isInside && !wasInside) {
// ENTER event
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: distanceFromCenter,
});
} else if (!isInside && wasInside) {
// EXIT event
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: distanceFromCenter,
});
}
// Update status for next check
await geofenceDb.updateStatus(deviceId, geofence.id, isInside);
}
return events;
}