Files
location-mqtt-tracker-app/lib/geofence-notifications.ts
Joachim Hummel bd6a7ab187 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>
2025-12-02 18:14:24 +00:00

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);
}
}
}