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>
151 lines
4.2 KiB
TypeScript
151 lines
4.2 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|
|
}
|
|
}
|