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:
150
lib/geofence-notifications.ts
Normal file
150
lib/geofence-notifications.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Geofence Notification Service
|
||||
* Handles sending email notifications for geofence events
|
||||
*/
|
||||
|
||||
import { emailService } from './email-service';
|
||||
import {
|
||||
renderGeofenceEnterEmail,
|
||||
renderGeofenceExitEmail,
|
||||
GeofenceEmailData,
|
||||
} from './email-renderer';
|
||||
import { geofenceDb } from './geofence-db';
|
||||
import type { GeofenceEvent } from './types';
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
|
||||
|
||||
interface DeviceInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
interface GeofenceInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
owner_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device information from database
|
||||
*/
|
||||
function getDevice(deviceId: string): DeviceInfo | null {
|
||||
const db = new Database(dbPath);
|
||||
try {
|
||||
const stmt = db.prepare('SELECT id, name, ownerId FROM Device WHERE id = ?');
|
||||
return stmt.get(deviceId) as DeviceInfo | null;
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user information from database
|
||||
*/
|
||||
function getUser(userId: string): UserInfo | null {
|
||||
const db = new Database(dbPath);
|
||||
try {
|
||||
const stmt = db.prepare('SELECT id, username, email FROM User WHERE id = ?');
|
||||
return stmt.get(userId) as UserInfo | null;
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification for a single geofence event
|
||||
*/
|
||||
async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
||||
try {
|
||||
// Get geofence details
|
||||
const geofence = geofenceDb.findById(event.geofence_id);
|
||||
if (!geofence) {
|
||||
throw new Error(`Geofence not found: ${event.geofence_id}`);
|
||||
}
|
||||
|
||||
// Get device details
|
||||
const device = getDevice(event.device_id);
|
||||
if (!device) {
|
||||
throw new Error(`Device not found: ${event.device_id}`);
|
||||
}
|
||||
|
||||
// Get owner details
|
||||
const owner = getUser(geofence.owner_id);
|
||||
if (!owner || !owner.email) {
|
||||
console.log(`[GeofenceNotification] No email for owner ${geofence.owner_id}, skipping notification`);
|
||||
geofenceDb.markNotificationSent(event.id!, true); // Mark as "sent" (no email needed)
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare email data
|
||||
const emailData: GeofenceEmailData = {
|
||||
username: owner.username,
|
||||
deviceName: device.name,
|
||||
geofenceName: geofence.name,
|
||||
timestamp: event.timestamp,
|
||||
latitude: event.latitude,
|
||||
longitude: event.longitude,
|
||||
distanceFromCenter: event.distance_from_center || 0,
|
||||
// Optional: Add map URL later
|
||||
// mapUrl: `${process.env.NEXT_PUBLIC_URL}/map?lat=${event.latitude}&lon=${event.longitude}`
|
||||
};
|
||||
|
||||
// Render and send email
|
||||
let html: string;
|
||||
let subject: string;
|
||||
|
||||
if (event.event_type === 'enter') {
|
||||
html = await renderGeofenceEnterEmail(emailData);
|
||||
subject = `${device.name} hat ${geofence.name} betreten`;
|
||||
} else {
|
||||
html = await renderGeofenceExitEmail(emailData);
|
||||
subject = `${device.name} hat ${geofence.name} verlassen`;
|
||||
}
|
||||
|
||||
// Send via existing email service
|
||||
await emailService['sendEmail'](owner.email, subject, html);
|
||||
|
||||
// Mark notification as sent
|
||||
geofenceDb.markNotificationSent(event.id!, true);
|
||||
|
||||
console.log(`[GeofenceNotification] Sent ${event.event_type} notification for geofence ${geofence.name} to ${owner.email}`);
|
||||
} catch (error) {
|
||||
console.error('[GeofenceNotification] Failed to send notification:', error);
|
||||
|
||||
// Mark notification as failed
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
geofenceDb.markNotificationSent(event.id!, false, errorMessage);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notifications for multiple geofence events
|
||||
* Processes events sequentially to avoid overwhelming SMTP server
|
||||
*/
|
||||
export async function sendGeofenceNotifications(events: GeofenceEvent[]): Promise<void> {
|
||||
if (events.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[GeofenceNotification] Processing ${events.length} geofence event(s)`);
|
||||
|
||||
for (const event of events) {
|
||||
try {
|
||||
await sendEventNotification(event);
|
||||
} catch (error) {
|
||||
// Log error but continue with other notifications
|
||||
console.error(`[GeofenceNotification] Failed to send notification for event ${event.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user