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

168
scripts/init-geofence-db.js Normal file
View File

@@ -0,0 +1,168 @@
#!/usr/bin/env node
/**
* Initialize geofence tables in database.sqlite
* This creates the schema for geofencing functionality
*/
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
const dataDir = path.join(__dirname, '..', 'data');
const dbPath = path.join(dataDir, 'database.sqlite');
// Ensure data directory exists
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
console.log('✓ Created data directory');
}
// Open existing database
const db = new Database(dbPath);
// Enable WAL mode for better concurrency
db.pragma('journal_mode = WAL');
// Create Geofence table (simplified for MVP)
db.exec(`
CREATE TABLE IF NOT EXISTS Geofence (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
-- Geometry (circle only for MVP)
shape_type TEXT NOT NULL DEFAULT 'circle',
center_latitude REAL NOT NULL,
center_longitude REAL NOT NULL,
radius_meters INTEGER NOT NULL,
-- Assignment (device_id is required for MVP)
owner_id TEXT NOT NULL,
device_id TEXT NOT NULL,
-- Status & Metadata
is_active INTEGER DEFAULT 1,
color TEXT DEFAULT '#3b82f6',
-- Timestamps
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (owner_id) REFERENCES User(id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE,
CHECK (shape_type = 'circle'),
CHECK (radius_meters > 0 AND radius_meters <= 50000),
CHECK (center_latitude BETWEEN -90 AND 90),
CHECK (center_longitude BETWEEN -180 AND 180),
CHECK (is_active IN (0, 1))
);
`);
console.log('✓ Created Geofence table');
// Create GeofenceEvent table
db.exec(`
CREATE TABLE IF NOT EXISTS GeofenceEvent (
id INTEGER PRIMARY KEY AUTOINCREMENT,
geofence_id TEXT NOT NULL,
device_id TEXT NOT NULL,
location_id INTEGER NOT NULL,
-- Event details
event_type TEXT NOT NULL,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
-- Metadata
distance_from_center REAL,
notification_sent INTEGER DEFAULT 0,
notification_error TEXT,
-- Timestamps
timestamp TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (geofence_id) REFERENCES Geofence(id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE,
CHECK (event_type IN ('enter', 'exit')),
CHECK (notification_sent IN (0, 1, 2))
);
`);
console.log('✓ Created GeofenceEvent table');
// Create GeofenceStatus table (for state tracking)
db.exec(`
CREATE TABLE IF NOT EXISTS GeofenceStatus (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
geofence_id TEXT NOT NULL,
-- Current status
is_inside INTEGER NOT NULL DEFAULT 0,
last_enter_time TEXT,
last_exit_time TEXT,
last_checked_at TEXT,
-- Timestamps
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE,
FOREIGN KEY (geofence_id) REFERENCES Geofence(id) ON DELETE CASCADE,
UNIQUE(device_id, geofence_id),
CHECK (is_inside IN (0, 1))
);
`);
console.log('✓ Created GeofenceStatus table');
// Create indexes for performance
db.exec(`
CREATE INDEX IF NOT EXISTS idx_geofence_owner
ON Geofence(owner_id);
CREATE INDEX IF NOT EXISTS idx_geofence_device
ON Geofence(device_id);
CREATE INDEX IF NOT EXISTS idx_geofence_active
ON Geofence(is_active);
CREATE INDEX IF NOT EXISTS idx_geofence_event_geofence
ON GeofenceEvent(geofence_id);
CREATE INDEX IF NOT EXISTS idx_geofence_event_device
ON GeofenceEvent(device_id);
CREATE INDEX IF NOT EXISTS idx_geofence_event_timestamp
ON GeofenceEvent(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_geofence_event_notification
ON GeofenceEvent(notification_sent);
CREATE INDEX IF NOT EXISTS idx_geofence_event_composite
ON GeofenceEvent(device_id, geofence_id, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_geofence_status_device
ON GeofenceStatus(device_id);
CREATE INDEX IF NOT EXISTS idx_geofence_status_geofence
ON GeofenceStatus(geofence_id);
CREATE INDEX IF NOT EXISTS idx_geofence_status_inside
ON GeofenceStatus(is_inside);
`);
console.log('✓ Created indexes');
// Get stats
const geofenceCount = db.prepare('SELECT COUNT(*) as count FROM Geofence').get();
const eventCount = db.prepare('SELECT COUNT(*) as count FROM GeofenceEvent').get();
const statusCount = db.prepare('SELECT COUNT(*) as count FROM GeofenceStatus').get();
console.log(`\n✓ Geofence tables initialized successfully!`);
console.log(` Path: ${dbPath}`);
console.log(` Geofences: ${geofenceCount.count}`);
console.log(` Events: ${eventCount.count}`);
console.log(` Status records: ${statusCount.count}`);
db.close();