From 843e93a274593a6b55f4d0f82dce9250bf27c0a8 Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Mon, 24 Nov 2025 16:30:37 +0000 Subject: [PATCH] first commit --- .dockerignore | 13 + .env.example | 51 + .gitignore | 55 + Dockerfile | 52 + MQTT_INTEGRATION.md | 559 ++ N8N_INTEGRATION.md | 349 + OWNTRACKS_SETUP.md | 338 + README.md | 875 +++ app/admin/devices/page.tsx | 595 ++ app/admin/emails/page.tsx | 171 + app/admin/layout.tsx | 106 + app/admin/mqtt/page.tsx | 638 ++ app/admin/page.tsx | 519 ++ app/admin/settings/page.tsx | 411 + app/admin/setup/page.tsx | 461 ++ app/admin/users/page.tsx | 543 ++ app/api/admin/emails/preview/route.ts | 60 + app/api/admin/emails/send-test/route.ts | 106 + app/api/admin/settings/smtp/route.ts | 149 + app/api/admin/settings/smtp/test/route.ts | 78 + app/api/auth/[...nextauth]/route.ts | 3 + app/api/auth/forgot-password/route.ts | 77 + app/api/auth/register/route.ts | 121 + app/api/auth/reset-password/route.ts | 106 + app/api/devices/[id]/route.ts | 129 + app/api/devices/public/route.ts | 44 + app/api/devices/route.ts | 120 + app/api/locations/cleanup/route.ts | 73 + app/api/locations/ingest/route.ts | 101 + app/api/locations/optimize/route.ts | 62 + app/api/locations/route.ts | 230 + app/api/locations/stats/route.ts | 77 + app/api/locations/sync/route.ts | 86 + app/api/locations/test/route.ts | 91 + app/api/mqtt/acl/[id]/route.ts | 146 + app/api/mqtt/acl/route.ts | 104 + app/api/mqtt/credentials/[device_id]/route.ts | 143 + app/api/mqtt/credentials/route.ts | 126 + app/api/mqtt/send-credentials/route.ts | 90 + app/api/mqtt/sync/route.ts | 50 + app/api/system/status/route.ts | 42 + app/api/users/[id]/route.ts | 209 + app/api/users/route.ts | 155 + app/forgot-password/page.tsx | 113 + app/globals.css | 26 + app/layout.tsx | 23 + app/login/page.tsx | 146 + app/map/page.tsx | 174 + app/page.tsx | 253 + app/register/page.tsx | 166 + app/reset-password/page.tsx | 216 + app/unauthorized/page.tsx | 74 + components/AuthProvider.tsx | 11 + components/demo/DemoMap.tsx | 157 + components/map/MapView.tsx | 394 + data/.gitkeep | 0 data/README.md | 58 + docker-compose.yml | 56 + docs/SMTP-SETUP.md | 144 + .../2025-11-17-smtp-integration-design.md | 555 ++ docs/plans/2025-11-17-smtp-integration.md | 3453 +++++++++ ...1-23-device-access-control-security-fix.md | 208 + emails/components/email-footer.tsx | 34 + emails/components/email-header.tsx | 34 + emails/components/email-layout.tsx | 40 + emails/mqtt-credentials.tsx | 171 + emails/password-reset.tsx | 105 + emails/welcome.tsx | 106 + instrumentation.ts | 9 + lib/auth.ts | 92 + lib/crypto-utils.ts | 83 + lib/db.ts | 532 ++ lib/demo-data.ts | 96 + lib/devices.ts | 16 + lib/email-renderer.ts | 57 + lib/email-service.ts | 227 + lib/mosquitto-sync.ts | 239 + lib/mqtt-db.ts | 361 + lib/mqtt-subscriber.ts | 225 + lib/password-reset-db.ts | 96 + lib/settings-db.ts | 102 + lib/startup.ts | 36 + lib/types/smtp.ts | 48 + middleware.ts | 65 + mosquitto/config/mosquitto.conf | 44 + next.config.js | 4 + package-lock.json | 6603 +++++++++++++++++ package.json | 54 + pictures/Readme.md | 1 + pictures/n8n-MQTT-GPS-Tracking.png | Bin 0 -> 61843 bytes postcss.config.js | 6 + scripts/add-mqtt-tables.js | 103 + scripts/add-parent-user-column.js | 49 + scripts/add-test-location.js | 68 + scripts/check-admin.js | 51 + scripts/check-user-password.js | 63 + scripts/cleanup-old-locations.js | 76 + scripts/init-database.js | 147 + scripts/init-locations-db.js | 79 + scripts/migrate-device-ownership.js | 75 + scripts/remove-duplicates.js | 39 + scripts/reset-admin.js | 48 + scripts/reset-joachim-password.js | 22 + scripts/show-schema.js | 19 + scripts/test-device-access.js | 89 + scripts/test-joachim-password.js | 25 + scripts/test-password.js | 55 + scripts/test-smtp.js | 58 + scripts/test-time-filter.js | 68 + scripts/test-user-visibility.js | 101 + scripts/update-acl-permission.js | 33 + tailwind.config.ts | 19 + tsconfig.json | 41 + types/location.ts | 30 + 114 files changed, 25585 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 MQTT_INTEGRATION.md create mode 100644 N8N_INTEGRATION.md create mode 100644 OWNTRACKS_SETUP.md create mode 100644 README.md create mode 100644 app/admin/devices/page.tsx create mode 100644 app/admin/emails/page.tsx create mode 100644 app/admin/layout.tsx create mode 100644 app/admin/mqtt/page.tsx create mode 100644 app/admin/page.tsx create mode 100644 app/admin/settings/page.tsx create mode 100644 app/admin/setup/page.tsx create mode 100644 app/admin/users/page.tsx create mode 100644 app/api/admin/emails/preview/route.ts create mode 100644 app/api/admin/emails/send-test/route.ts create mode 100644 app/api/admin/settings/smtp/route.ts create mode 100644 app/api/admin/settings/smtp/test/route.ts create mode 100644 app/api/auth/[...nextauth]/route.ts create mode 100644 app/api/auth/forgot-password/route.ts create mode 100644 app/api/auth/register/route.ts create mode 100644 app/api/auth/reset-password/route.ts create mode 100644 app/api/devices/[id]/route.ts create mode 100644 app/api/devices/public/route.ts create mode 100644 app/api/devices/route.ts create mode 100644 app/api/locations/cleanup/route.ts create mode 100644 app/api/locations/ingest/route.ts create mode 100644 app/api/locations/optimize/route.ts create mode 100644 app/api/locations/route.ts create mode 100644 app/api/locations/stats/route.ts create mode 100644 app/api/locations/sync/route.ts create mode 100644 app/api/locations/test/route.ts create mode 100644 app/api/mqtt/acl/[id]/route.ts create mode 100644 app/api/mqtt/acl/route.ts create mode 100644 app/api/mqtt/credentials/[device_id]/route.ts create mode 100644 app/api/mqtt/credentials/route.ts create mode 100644 app/api/mqtt/send-credentials/route.ts create mode 100644 app/api/mqtt/sync/route.ts create mode 100644 app/api/system/status/route.ts create mode 100644 app/api/users/[id]/route.ts create mode 100644 app/api/users/route.ts create mode 100644 app/forgot-password/page.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/login/page.tsx create mode 100644 app/map/page.tsx create mode 100644 app/page.tsx create mode 100644 app/register/page.tsx create mode 100644 app/reset-password/page.tsx create mode 100644 app/unauthorized/page.tsx create mode 100644 components/AuthProvider.tsx create mode 100644 components/demo/DemoMap.tsx create mode 100644 components/map/MapView.tsx create mode 100644 data/.gitkeep create mode 100644 data/README.md create mode 100644 docker-compose.yml create mode 100644 docs/SMTP-SETUP.md create mode 100644 docs/plans/2025-11-17-smtp-integration-design.md create mode 100644 docs/plans/2025-11-17-smtp-integration.md create mode 100644 docs/plans/2025-11-23-device-access-control-security-fix.md create mode 100644 emails/components/email-footer.tsx create mode 100644 emails/components/email-header.tsx create mode 100644 emails/components/email-layout.tsx create mode 100644 emails/mqtt-credentials.tsx create mode 100644 emails/password-reset.tsx create mode 100644 emails/welcome.tsx create mode 100644 instrumentation.ts create mode 100644 lib/auth.ts create mode 100644 lib/crypto-utils.ts create mode 100644 lib/db.ts create mode 100644 lib/demo-data.ts create mode 100644 lib/devices.ts create mode 100644 lib/email-renderer.ts create mode 100644 lib/email-service.ts create mode 100644 lib/mosquitto-sync.ts create mode 100644 lib/mqtt-db.ts create mode 100644 lib/mqtt-subscriber.ts create mode 100644 lib/password-reset-db.ts create mode 100644 lib/settings-db.ts create mode 100644 lib/startup.ts create mode 100644 lib/types/smtp.ts create mode 100644 middleware.ts create mode 100644 mosquitto/config/mosquitto.conf create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pictures/Readme.md create mode 100644 pictures/n8n-MQTT-GPS-Tracking.png create mode 100644 postcss.config.js create mode 100644 scripts/add-mqtt-tables.js create mode 100644 scripts/add-parent-user-column.js create mode 100644 scripts/add-test-location.js create mode 100644 scripts/check-admin.js create mode 100644 scripts/check-user-password.js create mode 100644 scripts/cleanup-old-locations.js create mode 100644 scripts/init-database.js create mode 100644 scripts/init-locations-db.js create mode 100644 scripts/migrate-device-ownership.js create mode 100644 scripts/remove-duplicates.js create mode 100644 scripts/reset-admin.js create mode 100644 scripts/reset-joachim-password.js create mode 100644 scripts/show-schema.js create mode 100644 scripts/test-device-access.js create mode 100644 scripts/test-joachim-password.js create mode 100644 scripts/test-password.js create mode 100755 scripts/test-smtp.js create mode 100755 scripts/test-time-filter.js create mode 100644 scripts/test-user-visibility.js create mode 100644 scripts/update-acl-permission.js create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 types/location.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b83a3f5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +.next +.git +.env +*.log +npm-debug.log* +.DS_Store +*.md +!README.md +!MQTT_INTEGRATION.md +data/*.sqlite* +.vscode +.idea diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..842abc6 --- /dev/null +++ b/.env.example @@ -0,0 +1,51 @@ +# Authentication +# Generate with: openssl rand -base64 32 +AUTH_SECRET=your-secret-key-here + +# NextAuth URL +# Development +NEXTAUTH_URL=http://localhost:3000 +# Production (change to your domain) +# NEXTAUTH_URL=https://your-domain.com + +# n8n API (optional - currently using client-side fetch) +N8N_API_URL=https://n8n.example.com/webhook/location + +# SMTP Configuration (Fallback when DB config is empty) +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=your-email@gmail.com +SMTP_PASS=your-app-password +SMTP_FROM_EMAIL=noreply@example.com +SMTP_FROM_NAME=Location Tracker + +# Encryption for SMTP passwords in database +# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +ENCRYPTION_KEY=your-32-byte-hex-key-here + +# Push Notifications (future feature) +# VAPID_PUBLIC_KEY= +# VAPID_PRIVATE_KEY= + +# ====================================== +# MQTT Configuration +# ====================================== + +# MQTT Broker URL +# Development (local): mqtt://localhost:1883 +# Docker Compose: mqtt://mosquitto:1883 +# Production: mqtt://your-mqtt-broker:1883 +MQTT_BROKER_URL=mqtt://mosquitto:1883 + +# MQTT Admin Credentials (für MQTT Subscriber und Password File Generation) +MQTT_ADMIN_USERNAME=admin +MQTT_ADMIN_PASSWORD=admin + +# Mosquitto Configuration File Paths +# Diese Pfade müssen vom App-Container aus erreichbar sein +MOSQUITTO_PASSWORD_FILE=/mosquitto/config/password.txt +MOSQUITTO_ACL_FILE=/mosquitto/config/acl.txt + +# Mosquitto Container Name (für Config Reload via Docker) +MOSQUITTO_CONTAINER_NAME=mosquitto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f71692 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Environment variables +.env.local +.env.production + +# Database +/data/*.db +/data/*.sqlite +/data/*.db-* +/data/*.db-shm +/data/*.db-wal +/data/*.sqlite-shm +/data/*.sqlite-wal +!/data/.gitkeep +*.db +*.sqlite + +# mosquitto + +/mosquitto/config/*.txt +/mosquitto/logs/*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..26ce60c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# Dockerfile für Location Tracker App mit MQTT Integration + +FROM node:20-alpine AS base + +# Installiere docker-cli für Mosquitto Container Management +RUN apk add --no-cache docker-cli + +# Dependencies Stage +FROM base AS deps +WORKDIR /app +COPY package*.json ./ +RUN npm ci + +# Builder Stage +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build Next.js App +RUN npm run build + +# Initialisiere Datenbanken +RUN npm run db:init && \ + node scripts/add-mqtt-tables.js + +# Runner Stage +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production + +# Kopiere nur Production Dependencies +COPY package*.json ./ +RUN npm ci --omit=dev + +# Kopiere Build Artifacts +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/data ./data + +# Kopiere App Code (benötigt für instrumentation.ts, lib/, etc.) +COPY --from=builder /app/instrumentation.ts ./ +COPY --from=builder /app/lib ./lib +COPY --from=builder /app/scripts ./scripts +COPY --from=builder /app/next.config.js ./ +COPY --from=builder /app/middleware.ts ./ + +# Exponiere Port +EXPOSE 3000 + +# Start App +CMD ["npm", "start"] diff --git a/MQTT_INTEGRATION.md b/MQTT_INTEGRATION.md new file mode 100644 index 0000000..ec52d8c --- /dev/null +++ b/MQTT_INTEGRATION.md @@ -0,0 +1,559 @@ +# MQTT Provisioning Integration + +Diese Anleitung beschreibt die Integration des MQTT Provisioning Systems (aus `mosquitto-automation`) in die Location Tracker App. + +## 🎯 Übersicht + +Die Integration vereint zwei vormals separate Systeme: +- **mosquitto-automation**: Device Provisioning und MQTT Credential Management +- **location-tracker-app**: GPS Tracking Visualisierung mit OwnTracks + +### Was wurde integriert? + +✅ **MQTT Credentials Management** - Direkt im Admin Panel +✅ **ACL (Access Control List) Management** - Feine Kontrolle über Topic-Berechtigungen +✅ **Mosquitto Sync** - Password & ACL Files werden automatisch generiert +✅ **MQTT Subscriber** - Direkte Verarbeitung von OwnTracks Messages (kein n8n mehr nötig) +✅ **Docker Compose Setup** - All-in-One Deployment + +--- + +## 📋 Features + +### Admin Panel: MQTT Provisioning + +**Route:** `/admin/mqtt` + +#### Device Provisioning +- Erstelle MQTT Credentials für registrierte Devices +- Automatische Generierung von Username & Passwort +- Passwörter werden mit `mosquitto_passwd` gehasht +- Default ACL Regel: `owntracks/[device-id]/#` (readwrite) + +#### Credentials Management +- Liste aller provisionierten Devices +- Enable/Disable MQTT Zugriff pro Device +- Passwort Regenerierung +- Credentials löschen (inkl. ACL Regeln) + +#### ACL Management +- Custom Topic Patterns definieren +- Berechtigungen: `read`, `write`, `readwrite` +- Wildcard Support mit `#` +- Regeln pro Device verwalten + +#### Mosquitto Sync +- **"Zu Mosquitto Syncen"** Button im Admin Panel +- Generiert `/mosquitto/config/password.txt` +- Generiert `/mosquitto/config/acl.txt` +- Sendet SIGHUP an Mosquitto Container (Config Reload) +- Zeigt ausstehende Änderungen an + +--- + +## 🗄️ Datenbankschema + +### Neue Tabellen + +#### `mqtt_credentials` +```sql +CREATE TABLE mqtt_credentials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL UNIQUE, -- Referenz zu Device Tabelle + mqtt_username TEXT NOT NULL UNIQUE, + mqtt_password_hash TEXT NOT NULL, -- Mosquitto-kompatible Hash + enabled INTEGER DEFAULT 1, -- 0 = disabled, 1 = enabled + created_at TEXT, + updated_at TEXT, + FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE +); +``` + +#### `mqtt_acl_rules` +```sql +CREATE TABLE mqtt_acl_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL, + topic_pattern TEXT NOT NULL, -- z.B. "owntracks/device10/#" + permission TEXT NOT NULL, -- read | write | readwrite + created_at TEXT, + FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE +); +``` + +#### `mqtt_sync_status` +```sql +CREATE TABLE mqtt_sync_status ( + id INTEGER PRIMARY KEY CHECK (id = 1), -- Singleton + pending_changes INTEGER DEFAULT 0, + last_sync_at TEXT, + last_sync_status TEXT, -- success | error: ... + created_at TEXT, + updated_at TEXT +); +``` + +### Migration + +```bash +# Datenbanken initialisieren +npm run db:init + +# MQTT Tabellen hinzufügen +node scripts/add-mqtt-tables.js +``` + +--- + +## 🚀 Installation & Setup + +### Voraussetzungen + +- Docker & Docker Compose +- Node.js 20+ (für lokale Entwicklung) + +### 1. Repository Setup + +```bash +cd location-tracker-app + +# Dependencies installieren +npm install + +# .env Datei erstellen +cp .env.example .env +``` + +### 2. Environment Variables + +Bearbeite `.env`: + +```env +# MQTT Configuration +MQTT_BROKER_URL=mqtt://mosquitto:1883 +MQTT_ADMIN_USERNAME=admin +MQTT_ADMIN_PASSWORD=dein-sicheres-passwort + +MOSQUITTO_PASSWORD_FILE=/mosquitto/config/password.txt +MOSQUITTO_ACL_FILE=/mosquitto/config/acl.txt +MOSQUITTO_CONTAINER_NAME=mosquitto + +# NextAuth +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET= + +# Verschlüsselung für SMTP Passwords +ENCRYPTION_KEY= +``` + +### 3. Docker Compose Start + +```bash +# Build und Start +docker-compose up -d + +# Logs verfolgen +docker-compose logs -f + +# Status prüfen +docker-compose ps +``` + +Die App läuft auf: `http://localhost:3000` +Mosquitto MQTT Broker: `mqtt://localhost:1883` + +### 4. Admin Zugang + +**Default Credentials:** +- Username: `admin` +- Password: `admin123` + +⚠️ **Ändere das Passwort nach dem ersten Login!** + +--- + +## 🔧 Entwicklung + +### Lokale Entwicklung (ohne Docker) + +```bash +# 1. Mosquitto extern starten (oder Docker Compose nur Mosquitto) +docker run -d -p 1883:1883 -p 9001:9001 \ + -v $(pwd)/mosquitto.conf:/mosquitto/config/mosquitto.conf \ + -v mosquitto_data:/mosquitto/data \ + eclipse-mosquitto:2 + +# 2. .env anpassen +MQTT_BROKER_URL=mqtt://localhost:1883 + +# 3. Datenbanken initialisieren +npm run db:init +node scripts/add-mqtt-tables.js + +# 4. App starten +npm run dev +``` + +### Neue MQTT Credentials testen + +```bash +# Mit mosquitto_sub testen +mosquitto_sub -h localhost -p 1883 \ + -u "device_10_abc123" \ + -P "dein-generiertes-passwort" \ + -t "owntracks/10/#" \ + -v + +# OwnTracks Message simulieren +mosquitto_pub -h localhost -p 1883 \ + -u "device_10_abc123" \ + -P "dein-generiertes-passwort" \ + -t "owntracks/10/device" \ + -m '{"_type":"location","lat":52.5200,"lon":13.4050,"tst":1234567890,"batt":85,"vel":5.2}' +``` + +--- + +## 📡 MQTT Subscriber + +Der MQTT Subscriber läuft automatisch beim App-Start und verarbeitet OwnTracks Messages. + +### Implementierung + +- **Service:** `lib/mqtt-subscriber.ts` +- **Startup:** `instrumentation.ts` (Next.js Hook) +- **Topic:** `owntracks/+/+` +- **Datenbank:** Schreibt direkt in `locations.sqlite` + +### OwnTracks Message Format + +```json +{ + "_type": "location", + "tid": "XX", + "lat": 52.5200, + "lon": 13.4050, + "tst": 1234567890, + "batt": 85, + "vel": 5.2, + "acc": 10, + "alt": 50 +} +``` + +### Logs + +```bash +# Docker Logs +docker-compose logs -f app + +# Du solltest sehen: +# ✓ Connected to MQTT broker +# ✓ Subscribed to owntracks/+/+ +# ✓ Location saved: device10 at (52.52, 13.405) +``` + +--- + +## 🔐 Sicherheit + +### Mosquitto Authentication + +- **Keine Anonymous Connections:** `allow_anonymous false` +- **Password File:** Mosquitto-kompatible Hashes (SHA512) +- **ACL File:** Topic-basierte Access Control + +### Best Practices + +1. **Starke Admin Passwörter:** Ändere `MQTT_ADMIN_PASSWORD` in `.env` +2. **Device Passwörter:** Auto-generierte Passwörter haben 128 Bit Entropie +3. **ACL Regeln:** Gib Devices nur Zugriff auf ihre eigenen Topics +4. **Docker Socket:** Container benötigt Zugriff für Mosquitto Reload (optional) + +### ACL Beispiele + +```text +# Device darf nur in eigenes Topic schreiben +user device_10_abc123 +topic readwrite owntracks/10/# + +# Device mit zusätzlichem Read-only Topic +user device_11_xyz789 +topic readwrite owntracks/11/# +topic read status/# + +# Admin hat vollen Zugriff +user admin +topic readwrite # +``` + +--- + +## 🐛 Troubleshooting + +### Problem: "Mosquitto configuration reloaded" fehlgeschlagen + +**Symptom:** Nach Sync kommt Warnung "Could not reload Mosquitto automatically" + +**Lösung:** Docker Socket Zugriff fehlt. Entweder: + +```bash +# Option 1: Manuelle Mosquitto Neustart +docker-compose restart mosquitto + +# Option 2: Docker Socket in docker-compose.yml freigeben (bereits konfiguriert) +volumes: + - /var/run/docker.sock:/var/run/docker.sock +``` + +### Problem: MQTT Subscriber verbindet nicht + +**Debug Steps:** + +```bash +# 1. Prüfe Mosquitto läuft +docker-compose ps mosquitto + +# 2. Prüfe Mosquitto Logs +docker-compose logs mosquitto + +# 3. Prüfe App Logs +docker-compose logs app | grep MQTT + +# 4. Teste MQTT Verbindung manuell +mosquitto_sub -h localhost -p 1883 -u admin -P admin -t '#' +``` + +### Problem: Password Hash falsch + +**Symptom:** Authentication failed im Mosquitto Log + +**Lösung:** `mosquitto_passwd` Tool muss im Container verfügbar sein (ist im Dockerfile installiert) + +```bash +# Im Container testen +docker exec -it location-tracker-app mosquitto_passwd -h +``` + +### Problem: ACL Regeln funktionieren nicht + +**Debug:** + +```bash +# ACL File prüfen +docker exec -it location-tracker-app cat /mosquitto/config/acl.txt + +# Mosquitto Logs auf "Access denied" prüfen +docker-compose logs mosquitto | grep -i denied +``` + +--- + +## 📊 API Endpoints + +### MQTT Credentials + +```http +# Liste aller Credentials +GET /api/mqtt/credentials + +# Credential für Device abrufen +GET /api/mqtt/credentials/{device_id} + +# Neue Credentials erstellen +POST /api/mqtt/credentials +Content-Type: application/json + +{ + "device_id": "10", + "auto_generate": true +} + +# Credentials aktualisieren +PATCH /api/mqtt/credentials/{device_id} +Content-Type: application/json + +{ + "regenerate_password": true, + "enabled": true +} + +# Credentials löschen +DELETE /api/mqtt/credentials/{device_id} +``` + +### ACL Rules + +```http +# ACL Regeln für Device +GET /api/mqtt/acl?device_id=10 + +# Neue ACL Regel erstellen +POST /api/mqtt/acl +Content-Type: application/json + +{ + "device_id": "10", + "topic_pattern": "owntracks/10/#", + "permission": "readwrite" +} + +# ACL Regel löschen +DELETE /api/mqtt/acl/{rule_id} +``` + +### Mosquitto Sync + +```http +# Sync Status abrufen +GET /api/mqtt/sync + +# Sync triggern +POST /api/mqtt/sync +``` + +Alle Endpoints erfordern Admin-Authentifizierung (Role: ADMIN). + +--- + +## 🔄 Migration von mosquitto-automation + +Wenn du bereits Devices in `mosquitto-automation` hast: + +### Automatische Migration (TODO) + +```bash +# Script erstellen das aus der alten DB liest +node scripts/migrate-from-mosquitto-automation.js \ + --old-db /pfad/zu/mosquitto-automation/data/devices.db +``` + +### Manuelle Migration + +1. Exportiere Devices aus alter DB: +```sql +SELECT id, name, username, password_hash, permissions +FROM devices +WHERE active = 1; +``` + +2. Erstelle Devices im neuen System über Admin Panel +3. Provisioniere MQTT Credentials manuell +4. Importiere ACL Regeln + +--- + +## 📚 Architektur + +### Komponenten + +``` +┌─────────────────────────────────────────────────────┐ +│ Location Tracker App (Next.js) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ +│ │ Admin Panel │ │ MQTT Service │ │ API │ │ +│ │ /admin/mqtt │ │ Subscriber │ │ Routes │ │ +│ └──────────────┘ └──────────────┘ └──────────┘ │ +│ │ │ │ │ +│ └──────────────────┴────────────────┘ │ +│ │ │ +│ ┌────▼────┐ │ +│ │ SQLite │ │ +│ │ DB │ │ +│ └────┬────┘ │ +│ │ │ +│ ┌────▼────────┐ │ +│ │ Mosquitto │ │ +│ │ Sync Service│ │ +│ └────┬────────┘ │ +└─────────────────────────┼──────────────────────────┘ + │ + ┌───────────▼───────────┐ + │ Mosquitto Broker │ + │ │ + │ • password.txt │ + │ • acl.txt │ + └───────────┬───────────┘ + │ + ┌───────────▼───────────┐ + │ GPS Tracking │ + │ Devices │ + │ (OwnTracks) │ + └───────────────────────┘ +``` + +### Datei-Struktur + +``` +location-tracker-app/ +├── app/ +│ ├── admin/ +│ │ └── mqtt/ +│ │ └── page.tsx # MQTT Provisioning UI +│ └── api/ +│ └── mqtt/ +│ ├── credentials/ # Credentials Management +│ ├── acl/ # ACL Management +│ └── sync/ # Mosquitto Sync +├── lib/ +│ ├── mqtt-db.ts # MQTT Datenbank Operations +│ ├── mqtt-subscriber.ts # MQTT Message Processing +│ ├── mosquitto-sync.ts # Config File Generation +│ └── startup.ts # Service Initialization +├── scripts/ +│ └── add-mqtt-tables.js # Datenbank Migration +├── docker-compose.yml # Docker Setup +├── Dockerfile # App Container +├── mosquitto.conf # Mosquitto Config +└── instrumentation.ts # Next.js Startup Hook +``` + +--- + +## ✨ Vorteile der Integration + +### Vorher (Separate Systeme) + +``` +GPS Device → MQTT → n8n → HTTP API → location-tracker-app → UI + ↓ + mosquitto-automation (separate) +``` + +**Probleme:** +- n8n als zusätzliche Dependency +- Zwei separate Admin Panels +- Keine zentrale User/Device Verwaltung +- Komplexes Setup + +### Nachher (Integriert) + +``` +GPS Device → MQTT → location-tracker-app → UI + ↓ + (integriertes Provisioning) +``` + +**Vorteile:** +✅ Ein Admin Panel für alles +✅ Direkte MQTT Verarbeitung (schneller) +✅ Einfaches Docker Compose Setup +✅ Zentrale Datenbank +✅ Weniger Dependencies + +--- + +## 🎉 Fertig! + +Die Integration ist komplett. Du kannst jetzt: + +1. **Devices verwalten** unter `/admin/devices` +2. **MQTT Credentials provisionieren** unter `/admin/mqtt` +3. **ACL Regeln definieren** im MQTT Panel +4. **Zu Mosquitto syncen** mit einem Klick +5. **GPS Tracking visualisieren** auf der Hauptseite + +Bei Fragen oder Problemen: Siehe Troubleshooting oder check die Logs. + +Happy Tracking! 🚀📍 diff --git a/N8N_INTEGRATION.md b/N8N_INTEGRATION.md new file mode 100644 index 0000000..1851d67 --- /dev/null +++ b/N8N_INTEGRATION.md @@ -0,0 +1,349 @@ +# n8n Integration Anleitung + +## Übersicht + +Die poc-app verwendet nun eine **Dual-Datenbank-Architektur** mit lokalem SQLite-Caching: + +- **database.sqlite** - Benutzerkonten, Geräteverwaltung (kritische Daten, wenige Schreibvorgänge) +- **locations.sqlite** - Standortverfolgung Cache (viele Schreibvorgänge, temporär) + +Diese Architektur bietet: +- **Performance**: Schnelle Abfragen aus lokalem SQLite anstatt NocoDB API +- **Skalierbarkeit**: Unbegrenzter Verlauf ohne Paginierungslimits +- **Resilienz**: Auth-System isoliert von der Tracking-Datenbank +- **Flexibilität**: Einfaches Löschen alter Daten + +--- + +## Erforderliche n8n Workflow-Änderungen + +### Aktueller Flow (ALT) + +``` +MQTT Trigger (owntracks/#) + ↓ +MQTT Location verarbeiten (Parse & Transform) + ↓ +Speichere in NocoDB +``` + +### Neuer Flow (ERFORDERLICH) + +``` +MQTT Trigger (owntracks/#) + ↓ +MQTT Location verarbeiten (Parse & Transform) + ↓ +Speichere in NocoDB + ↓ +[NEU] Push to Next.js Cache (HTTP Request) +``` + +--- + +## Schritt-für-Schritt: HTTP Request Node hinzufügen + +### 1. HTTP Request Node hinzufügen + +Nach dem "Speichere in NocoDB" Node einen neuen **HTTP Request** Node hinzufügen: + +**Node-Konfiguration:** +- **Name**: "Push to Next.js Cache" +- **Methode**: POST +- **URL**: `https://deine-nextjs-domain.com/api/locations/ingest` +- **Authentifizierung**: None (API-Key in Produktion hinzufügen!) +- **Body Content Type**: JSON +- **Specify Body**: Using Fields Below + +### 2. Felder zuordnen + +Im Bereich "Body Parameters" die folgenden Felder zuordnen: + +| Parameter | Wert (Expression) | Beschreibung | +|-----------|-------------------|-------------| +| `latitude` | `{{ $json.latitude }}` | Geografischer Breitengrad | +| `longitude` | `{{ $json.longitude }}` | Geografischer Längengrad | +| `timestamp` | `{{ $json.timestamp }}` | ISO 8601 Zeitstempel | +| `user_id` | `{{ $json.user_id }}` | Benutzer-ID (0 für MQTT) | +| `first_name` | `{{ $json.first_name }}` | Tracker ID | +| `last_name` | `{{ $json.last_name }}` | Quelltyp | +| `username` | `{{ $json.username }}` | Geräte-Benutzername | +| `marker_label` | `{{ $json.marker_label }}` | Anzeige-Label | +| `display_time` | `{{ $json.display_time }}` | Formatierte Zeit | +| `chat_id` | `{{ $json.chat_id }}` | Chat-ID (0 für MQTT) | +| `battery` | `{{ $json.battery }}` | Batterieprozent | +| `speed` | `{{ $json.speed }}` | Geschwindigkeit (m/s) | + +### 3. Fehlerbehandlung (Optional aber empfohlen) + +Einen **Error Trigger** Node hinzufügen, um fehlgeschlagene API-Aufrufe zu behandeln: + +- **Workflow**: Current Workflow +- **Error Type**: All Errors +- **Connected to**: Push to Next.js Cache node + +Einen **Slack/Email** Benachrichtigungs-Node hinzufügen, um über Fehler informiert zu werden. + +--- + +## Beispiel n8n HTTP Request Node (JSON) + +```json +{ + "parameters": { + "method": "POST", + "url": "https://deine-domain.com/api/locations/ingest", + "authentication": "none", + "options": {}, + "bodyParametersJson": "={{ {\n \"latitude\": $json.latitude,\n \"longitude\": $json.longitude,\n \"timestamp\": $json.timestamp,\n \"user_id\": $json.user_id,\n \"first_name\": $json.first_name,\n \"last_name\": $json.last_name,\n \"username\": $json.username,\n \"marker_label\": $json.marker_label,\n \"display_time\": $json.display_time,\n \"chat_id\": $json.chat_id,\n \"battery\": $json.battery,\n \"speed\": $json.speed\n} }}" + }, + "name": "Push to Next.js Cache", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [1200, 300] +} +``` + +--- + +## Testen + +### 1. Ingest-Endpunkt testen + +```bash +curl -X POST https://deine-domain.com/api/locations/ingest \ + -H "Content-Type: application/json" \ + -d '{ + "latitude": 48.1351, + "longitude": 11.5820, + "timestamp": "2024-01-15T10:30:00Z", + "user_id": 0, + "username": "10", + "marker_label": "Test Gerät", + "battery": 85, + "speed": 2.5 + }' +``` + +Erwartete Antwort: +```json +{ + "success": true, + "inserted": 1, + "message": "Successfully stored 1 location(s)" +} +``` + +### 2. Daten überprüfen + +```bash +curl https://deine-domain.com/api/locations?username=10&timeRangeHours=1 +``` + +### 3. Statistiken prüfen + +```bash +curl https://deine-domain.com/api/locations/ingest +``` + +--- + +## Produktions-Überlegungen + +### 1. API-Key-Authentifizierung hinzufügen + +**Aktualisieren** von `app/api/locations/ingest/route.ts`: + +```typescript +export async function POST(request: NextRequest) { + // API-Key validieren + const apiKey = request.headers.get('x-api-key'); + if (apiKey !== process.env.N8N_API_KEY) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // ... restlicher Code +} +``` + +**n8n HTTP Request Node aktualisieren:** +- Header hinzufügen: `x-api-key` = `dein-secret-key` + +### 2. Automatisches Aufräumen einrichten + +Einen Cron-Job hinzufügen, um alte Daten zu löschen (hält die Datenbankgröße überschaubar): + +```bash +# /etc/crontab +# Tägliche Bereinigung um 2 Uhr morgens - löscht Daten älter als 7 Tage +0 2 * * * cd /pfad/zu/poc-app && node scripts/cleanup-old-locations.js 168 +``` + +Oder verwende einen systemd Timer, PM2 Cron oder ähnliches. + +### 3. Datenbankgröße überwachen + +```bash +# Datenbankstatistiken prüfen +curl https://deine-domain.com/api/locations/ingest + +# Erwartete Ausgabe: +# { +# "total": 5432, +# "oldest": "2024-01-08T10:00:00Z", +# "newest": "2024-01-15T10:30:00Z", +# "sizeKB": 1024 +# } +``` + +### 4. Backup-Strategie + +**database.sqlite** (kritisch): +- Tägliche automatisierte Backups +- 30-Tage Aufbewahrung + +**locations.sqlite** (Cache): +- Optional: Wöchentliche Backups +- Oder keine Backups (Daten existieren in NocoDB) + +--- + +## Migration: Vorhandene NocoDB-Daten importieren + +Falls du bereits Standortdaten in NocoDB hast, kannst du diese importieren: + +### Option 1: CSV aus NocoDB exportieren + +1. Daten aus NocoDB als CSV exportieren +2. In JSON konvertieren +3. In Batches an `/api/locations/ingest` senden (POST) + +### Option 2: Direkter NocoDB API Import (empfohlen) + +Ein Skript `scripts/import-from-nocodb.js` erstellen: + +```javascript +const fetch = require('node-fetch'); +const { locationDb } = require('../lib/db'); + +async function importFromNocoDB() { + // Alle Daten von NocoDB API abrufen + const response = await fetch('https://n8n.example.com/webhook/location'); + const data = await response.json(); + + // Bulk-Insert in locations.sqlite + const count = locationDb.createMany(data.history); + console.log(`Importiert: ${count} Standorte`); +} + +importFromNocoDB().catch(console.error); +``` + +Ausführen: `node scripts/import-from-nocodb.js` + +--- + +## Fehlerbehebung + +### Problem: "directory does not exist" Fehler + +**Lösung**: Init-Skript ausführen: +```bash +cd poc-app +node scripts/init-locations-db.js +``` + +### Problem: n8n gibt 500-Fehler beim Push zu Next.js zurück + +**Prüfen**: +1. Next.js App läuft und ist erreichbar +2. URL in n8n ist korrekt +3. Next.js Logs prüfen: `pm2 logs` oder `docker logs` + +### Problem: Keine Daten im Frontend sichtbar + +**Überprüfen**: +1. Daten sind in SQLite: `curl https://deine-domain.com/api/locations/ingest` +2. API gibt Daten zurück: `curl https://deine-domain.com/api/locations` +3. Browser-Konsole auf Fehler prüfen + +### Problem: Datenbank wird zu groß + +**Lösung**: Cleanup-Skript ausführen: +```bash +node scripts/cleanup-old-locations.js 168 # Behalte 7 Tage +``` + +Oder die Aufbewahrungsdauer im Cron-Job reduzieren. + +--- + +## Architektur-Diagramm + +``` +┌─────────────────┐ +│ OwnTracks App │ +└────────┬────────┘ + │ MQTT + ▼ +┌─────────────────┐ +│ MQTT Broker │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ n8n Workflow │ +│ │ +│ 1. Parse MQTT │ +│ 2. Save NocoDB │───────────┐ +│ 3. Push Next.js│ │ +└────────┬────────┘ │ + │ │ + │ HTTP POST │ (Backup) + ▼ ▼ +┌─────────────────┐ ┌──────────────┐ +│ Next.js API │ │ NocoDB │ +│ /api/locations │ │ (Cloud DB) │ +│ /ingest │ └──────────────┘ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ locations.sqlite│ (Lokaler Cache) +│ - Schnelle │ +│ Abfragen │ +│ - Auto Cleanup │ +│ - Isoliert │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Frontend API │ +│ /api/locations │ +│ (Read-only) │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Map UI │ +│ - 24h Verlauf │ +│ - Schnelle │ +│ Filter │ +│ - Echtzeit │ +└─────────────────┘ +``` + +--- + +## Zusammenfassung + +✅ **Dual-Datenbank** isoliert kritische Auth von hochvolumiger Standortverfolgung +✅ **Lokaler Cache** ermöglicht schnelle 24h+ Abfragen ohne NocoDB-Paginierungslimits +✅ **WAL-Modus** bietet Absturzsicherheit und bessere Nebenläufigkeit +✅ **Auto Cleanup** hält die Datenbankgröße überschaubar +✅ **Rückwärtskompatibel** - gleiches API-Antwortformat wie n8n Webhook + +🚀 **Die Next.js App ist jetzt produktionsreif mit unbegrenztem Standortverlauf!** diff --git a/OWNTRACKS_SETUP.md b/OWNTRACKS_SETUP.md new file mode 100644 index 0000000..8a6f962 --- /dev/null +++ b/OWNTRACKS_SETUP.md @@ -0,0 +1,338 @@ +# OwnTracks App Setup Anleitung + +## Übersicht + +Diese Anleitung erklärt Schritt-für-Schritt, wie Sie die OwnTracks App auf Ihrem Smartphone installieren und mit dem Location Tracker System verbinden. + +--- + +## 1. Installation + +### iOS (iPhone/iPad) +1. Öffnen Sie den **App Store** +2. Suchen Sie nach **"OwnTracks"** +3. Laden Sie die App herunter und installieren Sie sie +4. App-Link: https://apps.apple.com/app/owntracks/id692424691 + +### Android +1. Öffnen Sie den **Google Play Store** +2. Suchen Sie nach **"OwnTracks"** +3. Laden Sie die App herunter und installieren Sie sie +4. App-Link: https://play.google.com/store/apps/details?id=org.owntracks.android + +--- + +## 2. MQTT Credentials erhalten + +Bevor Sie die App konfigurieren können, benötigen Sie Ihre MQTT-Zugangsdaten: + +1. Melden Sie sich im **Location Tracker** an: `http://192.168.10.118:3000/login` +2. Navigieren Sie zu: **Admin → MQTT Provisioning** (`/admin/mqtt`) +3. Klicken Sie auf **"Device Provisionieren"** +4. Wählen Sie Ihr Device aus der Liste +5. Aktivieren Sie **"Automatisch Username & Passwort generieren"** +6. Klicken Sie auf **"Erstellen"** +7. **WICHTIG:** Kopieren Sie sofort die angezeigten Credentials: + ``` + Username: device_10_abc123 + Password: xxxxxxxxxxxxxxxx + ``` +8. Speichern Sie diese Daten sicher - das Passwort wird nur einmal angezeigt! + +--- + +## 3. OwnTracks App Konfiguration + +### Schritt 1: App öffnen +Starten Sie die OwnTracks App auf Ihrem Smartphone. + +### Schritt 2: Zu Einstellungen navigieren +- **iOS:** Tippen Sie auf das ⚙️ Symbol (oben rechts) +- **Android:** Tippen Sie auf ☰ (Hamburger-Menü) → Einstellungen + +### Schritt 3: Verbindung konfigurieren + +#### 3.1 Modus auswählen +1. Gehen Sie zu **"Verbindung"** oder **"Connection"** +2. Wählen Sie **"Modus"** → **"MQTT"** + - ✅ **MQTT** (Private Server) + - ❌ Nicht: HTTP oder andere Modi + +#### 3.2 MQTT Server-Einstellungen + +Tragen Sie folgende Werte ein: + +| Einstellung | Wert | Beschreibung | +|------------|------|--------------| +| **Hostname** | `192.168.10.118` | IP-Adresse Ihres Servers | +| **Port** | `1883` | Standard MQTT Port (ohne Websocket) | +| **Websockets nutzen** | ❌ **DEAKTIVIERT** | Websockets nur bei Port 9001 aktivieren | +| **TLS** | ❌ **DEAKTIVIERT** | TLS/SSL nicht aktivieren (lokales Netzwerk) | +| **Client ID** | Automatisch generiert | Kann leer gelassen werden | + +#### 3.3 Authentifizierung + +| Einstellung | Wert | Beispiel | +|------------|------|----------| +| **Benutzername** | Ihr MQTT Username | `device_10_f06e935e` | +| **Passwort** | Ihr MQTT Passwort | `n5DkMF+xEi9p56DFa7ewUg==` | + +#### 3.4 Device Identifikation + +| Einstellung | Wert | Beschreibung | +|------------|------|--------------| +| **Geräte ID** / **Device ID** | `10` | Muss mit Ihrer Device-ID im System übereinstimmen | +| **Tracker ID** | `10` | Identisch mit Device ID | + +**WICHTIG:** Die Device ID und Tracker ID müssen mit der Device-ID übereinstimmen, die Sie im Location Tracker System konfiguriert haben (z.B. `10`, `11`, `12`, `15`). + +--- + +## 4. Erweiterte Einstellungen (Optional) + +### 4.1 Standort-Tracking Einstellungen + +**Empfohlene Werte für präzises Tracking:** + +| Einstellung | Empfohlener Wert | Beschreibung | +|------------|------------------|--------------| +| **Monitoring Modus** | Significant Changes | Spart Akku, trackt bei größeren Bewegungen | +| **Move Intervall** | 60 Sekunden | Sendet alle 60 Sekunden bei Bewegung | +| **Standby Intervall** | 300 Sekunden | Sendet alle 5 Minuten im Ruhezustand | + +### 4.2 Benachrichtigungen + +- **iOS:** Erlauben Sie Standortzugriff "Immer" für Hintergrund-Tracking +- **Android:** Aktivieren Sie "Standortzugriff im Hintergrund" + +### 4.3 Akkuoptimierung (Android) + +**WICHTIG für zuverlässiges Tracking:** +1. Gehen Sie zu **Systemeinstellungen → Apps → OwnTracks** +2. Wählen Sie **"Akku"** oder **"Akkuoptimierung"** +3. Wählen Sie **"Nicht optimieren"** oder deaktivieren Sie Akkuoptimierung +4. Dies verhindert, dass Android die App im Hintergrund beendet + +--- + +## 5. Verbindung testen + +### Schritt 1: Verbindung prüfen +1. Kehren Sie zum OwnTracks Hauptbildschirm zurück +2. Sie sollten ein **grünes Symbol** oder "Connected" sehen +3. Bei Problemen: Rotes Symbol oder "Disconnected" + +### Schritt 2: Testpunkt senden +1. Tippen Sie auf den **Location-Button** (Fadenkreuz-Symbol) +2. Dies sendet sofort Ihre aktuelle Position + +### Schritt 3: Im Location Tracker prüfen +1. Öffnen Sie den Location Tracker im Browser: `http://192.168.10.118:3000/map` +2. Ihre Position sollte jetzt auf der Karte erscheinen +3. Bei erfolgreicher Verbindung sehen Sie: + - Marker mit Ihrer Device-Farbe + - Aktuelle Koordinaten + - Zeitstempel der letzten Position + +--- + +## 6. Port 1883 vs. Port 9001 - Was ist der Unterschied? + +### Port 1883 (Standard MQTT) +- **Protokoll:** Standard MQTT (TCP) +- **Verwendung:** Normale MQTT-Clients (OwnTracks, IoT-Geräte) +- **Websockets:** ❌ Nein +- **Empfohlen für:** Mobile Apps, eingebettete Geräte + +**Konfiguration:** +``` +Port: 1883 +Websockets: DEAKTIVIERT +``` + +### Port 9001 (MQTT over WebSockets) +- **Protokoll:** MQTT über WebSocket +- **Verwendung:** Browser-basierte Clients, Web-Anwendungen +- **Websockets:** ✅ Ja +- **Empfohlen für:** Web-Apps, JavaScript-Clients + +**Konfiguration:** +``` +Port: 9001 +Websockets: AKTIVIERT +``` + +### Welchen Port sollten Sie verwenden? + +| Client-Typ | Empfohlener Port | Websockets | +|-----------|------------------|------------| +| **OwnTracks App (iOS/Android)** | **1883** | ❌ Nein | +| **Browser/Web-App** | **9001** | ✅ Ja | +| **IoT-Geräte** | **1883** | ❌ Nein | +| **Node.js/Python Scripts** | **1883** | ❌ Nein | + +**Für die OwnTracks Mobile App verwenden Sie immer Port 1883 ohne Websockets!** + +--- + +## 7. Troubleshooting - Häufige Probleme + +### Problem: "Verbindung fehlgeschlagen" + +**Mögliche Ursachen und Lösungen:** + +#### 1. Falsche IP-Adresse oder Port +- ✅ **Lösung:** Überprüfen Sie Hostname: `192.168.10.118` und Port: `1883` +- Stellen Sie sicher, dass Ihr Smartphone im selben Netzwerk ist + +#### 2. TLS aktiviert (sollte deaktiviert sein) +- ✅ **Lösung:** Deaktivieren Sie TLS/SSL in den Verbindungseinstellungen +- Lokale Verbindungen benötigen kein TLS + +#### 3. Websockets fälschlicherweise aktiviert +- ✅ **Lösung:** Deaktivieren Sie "Websockets nutzen" bei Port 1883 +- Websockets nur bei Port 9001 verwenden + +#### 4. Falsche Credentials +- ✅ **Lösung:** Überprüfen Sie Username und Passwort +- Regenerieren Sie ggf. das Passwort über `/admin/mqtt` + +#### 5. Firewall blockiert Port 1883 +- ✅ **Lösung:** Prüfen Sie Firewall-Einstellungen auf dem Server +- Port 1883 muss für eingehende Verbindungen geöffnet sein + +### Problem: "Verbunden, aber keine Daten auf der Karte" + +**Mögliche Ursachen:** + +#### 1. Falsche Device ID / Tracker ID +- ✅ **Lösung:** Device ID und Tracker ID müssen mit dem konfigurierten Device im System übereinstimmen +- Beispiel: Wenn Sie "Device 10" provisioniert haben, muss Tracker ID `10` sein + +#### 2. Standortberechtigungen nicht erteilt +- ✅ **Lösung (iOS):** Einstellungen → Datenschutz → Ortungsdienste → OwnTracks → "Immer" +- ✅ **Lösung (Android):** App-Einstellungen → Berechtigungen → Standort → "Immer zulassen" + +#### 3. Akkuoptimierung beendet App (Android) +- ✅ **Lösung:** Akkuoptimierung für OwnTracks deaktivieren (siehe Abschnitt 4.3) + +### Problem: "Tracking stoppt im Hintergrund" + +**Lösungen:** + +#### iOS +1. Einstellungen → Allgemein → Hintergrundaktualisierung → OwnTracks aktivieren +2. Standortzugriff: "Immer" (nicht "Beim Verwenden der App") + +#### Android +1. Akkuoptimierung deaktivieren +2. Einstellungen → Apps → OwnTracks → Berechtigungen → Standort → "Immer zulassen" +3. Bei manchen Herstellern: "Autostart" erlauben + +--- + +## 8. Sicherheitshinweise + +### WICHTIG - Nur im lokalen Netzwerk verwenden! + +**Aktuelle Konfiguration ist NICHT für öffentliches Internet geeignet:** + +- ❌ **TLS ist deaktiviert** - Daten werden unverschlüsselt übertragen +- ❌ **Keine VPN-Verbindung** - Direkter Zugriff erforderlich +- ⚠️ **Nur im sicheren WLAN** verwenden + +### Für Zugriff von außerhalb: + +Wenn Sie von außerhalb Ihres Heimnetzwerks zugreifen möchten, sollten Sie: + +1. **VPN einrichten** (z.B. WireGuard, OpenVPN) +2. **TLS/SSL aktivieren** für verschlüsselte Verbindung +3. **Starke Passwörter verwenden** (automatisch generiert durch System) +4. **Firewall korrekt konfigurieren** (nur VPN-Zugriff) + +--- + +## 9. Konfigurationsübersicht + +### ✅ Korrekte Konfiguration für OwnTracks Mobile App + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Verbindungseinstellungen +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Modus: MQTT +Hostname: 192.168.10.118 +Port: 1883 +Websockets nutzen: ❌ NEIN +TLS: ❌ NEIN +Client ID: (automatisch) + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Authentifizierung +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Benutzername: device_XX_xxxxxxxx +Passwort: ****************** + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Device Identifikation +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Geräte ID: 10 (Beispiel) +Tracker ID: 10 (identisch) + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + +## 10. Schnellstart-Checkliste + +- [ ] OwnTracks App aus App Store / Play Store installiert +- [ ] MQTT Credentials über `/admin/mqtt` generiert +- [ ] Credentials sicher gespeichert +- [ ] **Modus auf MQTT** gesetzt +- [ ] **Hostname:** `192.168.10.118` eingetragen +- [ ] **Port:** `1883` eingetragen +- [ ] **Websockets:** ❌ Deaktiviert +- [ ] **TLS:** ❌ Deaktiviert +- [ ] **Benutzername** und **Passwort** eingetragen +- [ ] **Device ID** und **Tracker ID** korrekt gesetzt +- [ ] Standortberechtigungen "Immer" erteilt +- [ ] Akkuoptimierung deaktiviert (Android) +- [ ] Verbindung erfolgreich (grünes Symbol) +- [ ] Testpunkt gesendet +- [ ] Position auf Karte sichtbar unter `/map` + +--- + +## 11. Weiterführende Informationen + +### Offizielle OwnTracks Dokumentation +- Website: https://owntracks.org +- Dokumentation: https://owntracks.org/booklet/ +- GitHub: https://github.com/owntracks + +### Location Tracker System +- Dashboard: `http://192.168.10.118:3000/admin` +- Live-Karte: `http://192.168.10.118:3000/map` +- MQTT Provisioning: `http://192.168.10.118:3000/admin/mqtt` + +--- + +## Support + +Bei Problemen oder Fragen: +1. Überprüfen Sie zuerst die Troubleshooting-Sektion (Abschnitt 7) +2. Prüfen Sie die Verbindung im Location Tracker Dashboard +3. Kontrollieren Sie die Server-Logs auf Fehler + +**Wichtige Logs prüfen:** +```bash +# Next.js Server Logs +npm run dev + +# Mosquitto MQTT Broker Logs +docker logs mosquitto +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..4489782 --- /dev/null +++ b/README.md @@ -0,0 +1,875 @@ +# Location Tracker - Next.js Anwendung + +Eine moderne Location-Tracking Anwendung basierend auf Next.js 14 mit MQTT/OwnTracks Integration, SQLite-Datenbank, Admin-Panel und Authentifizierung. + +![Location Tracker Screenshot](./pictures/n8n-MQTT-GPS-Tracking.png) + +## 📋 Inhaltsverzeichnis + +- [Features](#-features) +- [Tech Stack](#-tech-stack) +- [Installation](#-installation) +- [Datenbank-Setup](#-datenbank-setup) +- [Verwendung](#-verwendung) +- [Architektur](#-architektur) +- [API-Endpunkte](#-api-endpunkte) +- [Device Management](#-device-management) +- [Wartung](#-wartung) +- [Deployment](#-deployment) + +--- + +## ✨ Features + +### Öffentliche Features +- 🗺️ **Interaktive Karte** - Echtzeit-Standortverfolgung mit Leaflet.js +- 🎨 **Mehrere Kartenansichten** - Standard, Satellit, Dark Mode +- 🔍 **Device-Filterung** - Filtern nach Gerät und Zeitraum +- ⏱️ **Flexible Zeitfilter**: + - **Quick Filters:** 1h, 3h, 6h, 12h, 24h, All + - **Custom Range:** Benutzerdefinierter Zeitraum mit DateTime-Picker (z.B. 16.11.2025 16:00 - 17.11.2025 06:00) + - Kompakte UI - Custom Range nur sichtbar wenn aktiviert +- 🔄 **Auto-Refresh** - Automatische Aktualisierung alle 5 Sekunden mit Pause/Resume Button +- 🎯 **Auto-Center** - Karte zentriert automatisch auf neueste Position +- ⏸️ **Pause/Resume** - Toggle-Button zum Stoppen/Starten des Auto-Refresh +- 📱 **Responsive Design** - Optimiert für Desktop und Mobile +- 📊 **Polylines** - Bewegungspfade mit farbcodierter Darstellung +- 🎨 **Marker-Sortierung** - Neueste Position immer im Vordergrund (z-index optimiert) +- 📍 **Zoom-basierte Icon-Skalierung** - Marker passen sich automatisch an Zoom-Level an + +### Admin-Panel (Login erforderlich) +- 🔐 **Authentifizierung** - NextAuth.js v5 mit bcrypt-Hashing +- 📊 **Dashboard** - Übersicht über Geräte, Statistiken und Datenbankstatus +- ⏱️ **System Status** - Live-Uptime, Memory Usage, Runtime Info +- 📱 **Device Management** - Geräte hinzufügen, bearbeiten, löschen +- 💾 **Datenbank-Wartung**: + - 🔄 Manueller Sync von n8n + - 🧹 Cleanup alter Daten (7, 15, 30, 90 Tage) + - ⚡ Datenbank-Optimierung (VACUUM) + - 📈 Detaillierte Statistiken +- 🟢 **Online/Offline Status** - Echtzeit-Status (< 10 Min = Online) +- 🔋 **Telemetrie-Daten** - Batterie, Geschwindigkeit, letzte Position (speed=0 wird korrekt behandelt) + +--- + +## 🛠 Tech Stack + +- **Framework:** Next.js 14 (App Router) +- **Sprache:** TypeScript 5.9 +- **Styling:** Tailwind CSS v4 +- **Karten:** Leaflet 1.9.4 + React-Leaflet 5.0 +- **Authentifizierung:** NextAuth.js v5 (beta) +- **Datenbank:** SQLite (better-sqlite3) +- **Passwort-Hashing:** bcryptjs +- **Datenquelle:** n8n Webhook API + lokale SQLite-Cache + +### Dual-Database Architektur +- **database.sqlite** - User, Geräte (kritische Daten) +- **locations.sqlite** - Location-Tracking (hohe Schreibrate, isoliert) + +--- + +## 📦 Installation + +### Voraussetzungen +- Node.js 18+ +- npm oder yarn + +### Schritte + +1. **Repository klonen** +```bash +git clone https://github.com/yourusername/location-tracker-app.git +cd location-tracker-app +``` + +2. **Dependencies installieren** +```bash +npm install +``` + +3. **Datenbank initialisieren** +```bash +npm run db:init +``` + +Dies erstellt: +- `data/database.sqlite` (User + Devices) +- `data/locations.sqlite` (Location-Tracking) +- Standard Admin-User: `admin` / `admin123` +- Standard Devices (ID 10, 11) + +4. **Development Server starten** +```bash +npm run dev +``` + +5. **Im Browser öffnen** +- Karte: http://localhost:3000 +- Login: http://localhost:3000/login +- Admin: http://localhost:3000/admin +- Devices: http://localhost:3000/admin/devices + +--- + +## 🗄️ Datenbank-Setup + +### Initialisierung + +**Beide Datenbanken erstellen:** +```bash +npm run db:init +``` + +**Nur database.sqlite (User/Devices):** +```bash +npm run db:init:app +``` + +**Nur locations.sqlite (Tracking):** +```bash +npm run db:init:locations +``` + +### Datenbank zurücksetzen + +**Admin-User neu anlegen:** +```bash +node scripts/reset-admin.js +``` + +**Alte Locations löschen:** +```bash +npm run db:cleanup # Älter als 7 Tage +npm run db:cleanup:7d # Älter als 7 Tage +npm run db:cleanup:30d # Älter als 30 Tage +``` + +**Duplikate entfernen (falls vorhanden):** +```bash +node scripts/remove-duplicates.js +``` + +### Schema + +#### **database.sqlite** (User & Devices) + +**User Tabelle:** +```sql +id TEXT PRIMARY KEY +username TEXT UNIQUE NOT NULL +email TEXT +passwordHash TEXT NOT NULL +role TEXT NOT NULL DEFAULT 'VIEWER' -- ADMIN oder VIEWER +createdAt TEXT DEFAULT (datetime('now')) +updatedAt TEXT DEFAULT (datetime('now')) +lastLoginAt TEXT +``` + +**Device Tabelle:** +```sql +id TEXT PRIMARY KEY +name TEXT NOT NULL +color TEXT NOT NULL +ownerId TEXT -- FK zu User.id +isActive INTEGER DEFAULT 1 -- 0 oder 1 +description TEXT +icon TEXT +createdAt TEXT DEFAULT (datetime('now')) +updatedAt TEXT DEFAULT (datetime('now')) +``` + +**Indexes:** +- `idx_user_username` ON User(username) +- `idx_device_owner` ON Device(ownerId) +- `idx_device_active` ON Device(isActive) + +--- + +#### **locations.sqlite** (Tracking Data) + +**Location Tabelle:** +```sql +id INTEGER PRIMARY KEY AUTOINCREMENT +latitude REAL NOT NULL -- -90 bis +90 +longitude REAL NOT NULL -- -180 bis +180 +timestamp TEXT NOT NULL -- ISO 8601 format +user_id INTEGER DEFAULT 0 +first_name TEXT +last_name TEXT +username TEXT -- Device Tracker ID +marker_label TEXT +display_time TEXT -- Formatierte Zeit für UI +chat_id INTEGER DEFAULT 0 +battery INTEGER -- Batteriestand in % +speed REAL -- Geschwindigkeit in km/h +created_at TEXT DEFAULT (datetime('now')) +``` + +**Indexes:** +- `idx_location_timestamp` ON Location(timestamp DESC) +- `idx_location_username` ON Location(username) +- `idx_location_user_id` ON Location(user_id) +- `idx_location_composite` ON Location(user_id, username, timestamp DESC) +- `idx_location_unique` UNIQUE ON Location(timestamp, username, latitude, longitude) + +**Constraints:** +- Latitude: -90 bis +90 +- Longitude: -180 bis +180 +- UNIQUE: Kombination aus timestamp, username, latitude, longitude verhindert Duplikate + +**WAL Mode:** Beide Datenbanken nutzen Write-Ahead Logging für bessere Concurrency + +--- + +## 🚀 Verwendung + +### Login + +Standard-Zugangsdaten: +``` +Benutzername: admin +Passwort: admin123 +``` + +⚠️ **Wichtig:** Für Production neuen User anlegen und Passwort ändern! + +### Geräte hinzufügen + +1. Admin-Panel öffnen: `/admin/devices` +2. "Add Device" klicken +3. Device ID (muss mit OwnTracks `tid` übereinstimmen) +4. Name und Farbe festlegen +5. Speichern + +**Wichtig:** Die Device ID muss mit der OwnTracks Tracker-ID übereinstimmen! + +### OwnTracks konfigurieren + +In der OwnTracks App: +- **Tracker ID (tid):** z.B. `12` +- **Topic:** `owntracks/user/12` +- MQTT Broker wie gewohnt + +Die n8n-Workflow holt die Daten, und die App synct automatisch alle 5 Sekunden. + +### Zeitfilter verwenden + +Die App bietet zwei Modi für die Zeitfilterung: + +#### **Quick Filters** (Schnellauswahl) +Vordefinierte Zeiträume für schnellen Zugriff: +- **1 Hour** - Locations der letzten Stunde +- **3 Hours** - Locations der letzten 3 Stunden +- **6 Hours** - Locations der letzten 6 Stunden +- **12 Hours** - Locations der letzten 12 Stunden +- **24 Hours** - Locations der letzten 24 Stunden +- **All** - Alle verfügbaren Locations + +**Verwendung:** +1. Im Header unter "Time:" gewünschten Zeitraum auswählen +2. Die Karte aktualisiert sich automatisch + +#### **Custom Range** (Benutzerdefiniert) +Für spezifische Zeiträume, z.B. "Route von gestern Abend 18:00 bis heute Morgen 08:00": + +**Verwendung:** +1. Auf den **"📅 Custom"** Button klicken +2. Custom Range Felder erscheinen: + - **From:** Start-Datum und -Zeit wählen (z.B. 16.11.2025 16:00) + - **To:** End-Datum und -Zeit wählen (z.B. 17.11.2025 06:00) +3. Die Route wird automatisch für den gewählten Zeitraum angezeigt +4. Zum Zurückschalten: **"📅 Quick"** Button klicken + +**Hinweis:** Custom Range Controls sind nur sichtbar wenn aktiviert - spart Platz im Header! + +--- + +## 🏗 Architektur + +### Datenfluss + +```mermaid +flowchart TD + A[📱 OwnTracks App] -->|MQTT Publish| B[🔌 MQTT Broker] + B -->|Subscribe| C[⚙️ n8n MQTT Trigger] + C -->|Store| D[💾 NocoDB] + D -->|Webhook API| E[🌐 n8n Webhook
/webhook/location] + + F[🖥️ Browser Client] -->|GET /api/locations
alle 5 Sek| G[📡 Next.js API Route] + + G -->|1. Fetch Fresh Data| E + E -->|JSON Response| G + + G -->|2. Sync New Locations| H[(🗄️ SQLite Cache
locations.sqlite)] + + H -->|3. Query Filtered Data| G + G -->|JSON Response| F + + F -->|Render| I[🗺️ React Leaflet Map] + + J[👤 Admin User] -->|Login| K[🔐 NextAuth.js] + K -->|Authenticated| L[📊 Admin Panel] + L -->|CRUD Operations| M[(💼 SQLite DB
database.sqlite)] + + style A fill:#4CAF50 + style B fill:#FF9800 + style C fill:#2196F3 + style D fill:#9C27B0 + style E fill:#F44336 + style G fill:#00BCD4 + style H fill:#FFC107 + style I fill:#8BC34A + style K fill:#E91E63 + style M fill:#FFC107 +``` + +### Komponenten-Übersicht + +```mermaid +graph LR + subgraph "External Services" + A[OwnTracks App] + B[MQTT Broker] + C[n8n Automation] + D[NocoDB] + end + + subgraph "Next.js Application" + E[Frontend
React/Leaflet] + F[API Routes] + G[Auth Layer
NextAuth.js] + end + + subgraph "Data Layer" + H[locations.sqlite
Tracking Data] + I[database.sqlite
Users & Devices] + end + + A -->|MQTT| B + B -->|Subscribe| C + C -->|Store| D + C -->|Webhook| F + + E -->|HTTP| F + F -->|Read/Write| H + F -->|Read/Write| I + + E -->|Auth| G + G -->|Validate| I + + style A fill:#4CAF50,color:#fff + style B fill:#FF9800,color:#fff + style C fill:#2196F3,color:#fff + style D fill:#9C27B0,color:#fff + style E fill:#00BCD4,color:#000 + style F fill:#00BCD4,color:#000 + style G fill:#E91E63,color:#fff + style H fill:#FFC107,color:#000 + style I fill:#FFC107,color:#000 +``` + +### Datenbank-Architektur + +```mermaid +erDiagram + USER ||--o{ DEVICE : owns + DEVICE ||--o{ LOCATION : tracks + + USER { + string id PK + string username UK + string email + string passwordHash + string role + datetime createdAt + datetime lastLoginAt + } + + DEVICE { + string id PK + string name + string color + string ownerId FK + boolean isActive + string description + datetime createdAt + } + + LOCATION { + int id PK + float latitude + float longitude + datetime timestamp + string username FK + int user_id + string display_time + int battery + float speed + int chat_id + } +``` + +### Auto-Sync Mechanismus + +Die App verwendet einen **Hybrid-Ansatz**: + +1. **Frontend polling** (alle 5 Sek.) → `/api/locations` +2. **API prüft** ob neue Daten in n8n verfügbar +3. **Nur neue Locations** werden in SQLite gespeichert +4. **Duplikate** werden durch UNIQUE Index verhindert +5. **Antwort** kommt aus lokalem SQLite Cache + +**Vorteile:** +- Schnelle Antwortzeiten (SQLite statt n8n) +- Längere Zeiträume abrufbar (24h+) +- Funktioniert auch wenn n8n nicht erreichbar ist (Fallback auf n8n-Daten) +- Duplikate werden automatisch verhindert (UNIQUE Index) + +### Datenvalidierung & Normalisierung + +Die App behandelt spezielle Fälle bei speed/battery korrekt: + +**speed = 0 Behandlung:** +- n8n sendet `speed: 0` (gültig - Gerät steht still) +- Wird mit `typeof === 'number'` Check explizit als `0` gespeichert +- Wird NICHT zu `null` konvertiert (wichtig für Telemetrie) +- Popup zeigt "Speed: 0.0 km/h" an + +**n8n Fallback:** +- Wenn DB leer ist, gibt API direkt n8n-Daten zurück +- Alle Filter (username, timeRangeHours) funktionieren auch mit Fallback +- Ermöglicht sofortigen Betrieb ohne DB-Initialisierung + +**Debug-Logging:** +- Server-Logs zeigen n8n Sync-Aktivität +- Browser Console zeigt Daten-Flow (MapView, Popup) +- Hilfreich für Troubleshooting + +--- + +## 📡 API-Endpunkte + +### Öffentlich + +**GET /api/locations** +- Location-Daten abrufen (mit Auto-Sync) +- Query-Parameter: + - `username` - Device-Filter (z.B. "10", "11") + - **Zeitfilter (wähle eine Methode):** + - `timeRangeHours` - Quick Filter (1, 3, 6, 12, 24) + - `startTime` & `endTime` - Custom Range (ISO 8601 Format) + - `limit` - Max. Anzahl (Standard: 1000) + - `sync=false` - Nur Cache ohne n8n Sync + +**Beispiele:** +```bash +# Quick Filter: Letzte 3 Stunden +GET /api/locations?timeRangeHours=3 + +# Custom Range: Spezifischer Zeitraum +GET /api/locations?startTime=2025-11-16T16:00:00.000Z&endTime=2025-11-17T06:00:00.000Z + +# Kombiniert: Device + Custom Range +GET /api/locations?username=10&startTime=2025-11-16T16:00:00.000Z&endTime=2025-11-17T06:00:00.000Z +``` + +**GET /api/devices/public** +- Öffentliche Device-Liste (nur ID, Name, Color) + +### Geschützt (Login erforderlich) + +**GET /api/devices** +- Alle Geräte mit Latest Location und Telemetrie + +**POST /api/devices** +- Neues Gerät erstellen +- Body: `{ id, name, color, description? }` + +**PATCH /api/devices/:id** +- Gerät aktualisieren +- Body: `{ name?, color?, description? }` + +**DELETE /api/devices/:id** +- Gerät löschen (soft delete) + +**POST /api/locations/sync** +- Manueller Sync von n8n +- Gibt Anzahl neu eingefügter Locations zurück + +**POST /api/locations/cleanup** +- Alte Locations löschen +- Body: `{ retentionHours }` + +**POST /api/locations/optimize** +- VACUUM + ANALYZE ausführen +- Gibt freigegebenen Speicher zurück + +**GET /api/locations/stats** +- Detaillierte DB-Statistiken +- Größe, Zeitraum, Locations pro Device + +**GET /api/system/status** +- System-Status abrufen +- Returns: Uptime, Memory Usage, Node.js Version, Platform +- Auto-Refresh: Alle 10 Sekunden im Admin-Panel + +--- + +## 📱 Device Management + +### Device-Karte zeigt: + +- 🟢/⚫ **Online/Offline Status** + - Online = letzte Location < 10 Minuten + - Offline = letzte Location > 10 Minuten +- 🕒 **Last Seen** - Zeitstempel letzter Location +- 🔋 **Batterie** - Prozent (Rot bei < 20%) +- 🚗 **Geschwindigkeit** - km/h (umgerechnet von m/s) +- 📍 **Koordinaten** - Lat/Lon mit 5 Dezimalen + +### Auto-Refresh +- Devices-Seite aktualisiert sich alle 10 Sekunden +- Online/Offline Status wird automatisch aktualisiert + +--- + +## 🧹 Wartung + +### Datenbank aufräumen + +**Via Admin-Panel:** +- `/admin` → Database Maintenance → Cleanup Buttons + +**Via CLI:** +```bash +npm run db:cleanup # 7 Tage +npm run db:cleanup:30d # 30 Tage +``` + +### Datenbank optimieren + +**Via Admin-Panel:** +- `/admin` → Database Maintenance → Optimize Button + +**Via CLI:** +```bash +# Manuell +node scripts/optimize-db.js +``` + +**Was macht Optimize:** +- `VACUUM` - Speicherplatz freigeben +- `ANALYZE` - Query-Statistiken aktualisieren + +### Sync von n8n + +**Via Admin-Panel:** +- `/admin` → Database Maintenance → Sync Now + +**Automatisch:** +- Passiert alle 5 Sekunden beim Abruf der Karte + +### Logs prüfen + +```bash +# Development Server Logs +npm run dev + +# Production Logs (PM2) +pm2 logs location-tracker-app +``` + +### Zeitfilter debuggen + +Bei Problemen mit der Zeitfilterung (z.B. alte Locations werden nicht ausgefiltert): + +```bash +node scripts/test-time-filter.js +``` + +**Das Script zeigt:** +- Aktuelle Zeit (UTC und lokal) +- Letzte Locations in der Datenbank +- Welche Locations mit 1-Stunden-Filter angezeigt werden sollten +- Vergleich zwischen alter (SQLite datetime) und neuer (JavaScript) Methode +- Anzahl der gefilterten Locations + +**Verwendung:** +1. Script ausführen um zu sehen, welche Locations in der DB sind +2. Überprüfen ob die Zeitfilterung korrekt funktioniert +3. Bei Problemen: App neu starten nach Code-Updates + +--- + +## 🚀 Deployment + +### Environment Variables + +Erstelle `.env.local`: + +```env +# NextAuth +AUTH_SECRET= +NEXTAUTH_URL=https://your-domain.com + +# Optional: n8n API URL (Standard in Code definiert) +N8N_API_URL=https://n8n.example.com/webhook/location +``` + +**Secret generieren:** +```bash +openssl rand -base64 32 +``` + +### Production Build + +```bash +# Build +npm run build + +# Start +npm run start +``` + +### Mit PM2 (empfohlen) + +```bash +# PM2 installieren +npm install -g pm2 + +# App starten +pm2 start npm --name "location-tracker-app" -- start + +# Auto-Start bei Server-Neustart +pm2 startup +pm2 save +``` + +### Nginx Reverse Proxy + +```nginx +server { + listen 80; + server_name your-domain.com; + + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} +``` + +--- + +## 🔒 Sicherheit + +### Production Checklist + +- [ ] `AUTH_SECRET` mit starkem Wert setzen +- [ ] `NEXTAUTH_URL` auf Production-Domain setzen +- [ ] Admin-Passwort ändern (nicht `admin123`) +- [ ] Ggf. weitere User anlegen mit VIEWER Rolle +- [ ] HTTPS aktivieren (Let's Encrypt) +- [ ] Firewall-Regeln prüfen +- [ ] Regelmäßige Backups einrichten + +### User-Rollen + +- **ADMIN** - Voller Zugriff auf alle Admin-Funktionen +- **VIEWER** - Nur lesender Zugriff (geplant, noch nicht implementiert) + +--- + +## 📂 Projektstruktur + +``` +location-tracker-app/ +├── app/ +│ ├── api/ +│ │ ├── auth/[...nextauth]/ # NextAuth API +│ │ ├── devices/ # Device CRUD +│ │ ├── locations/ # Location API + Sync/Cleanup/Stats +│ │ └── system/status/ # System Status (Uptime, Memory) +│ ├── admin/ +│ │ ├── devices/ # Device Management +│ │ └── page.tsx # Dashboard +│ ├── login/ # Login-Seite +│ ├── page.tsx # Öffentliche Karte +│ └── layout.tsx # Root Layout +├── components/ +│ └── map/ +│ └── MapView.tsx # Leaflet Map Component +├── lib/ +│ ├── auth.ts # NextAuth Config +│ └── db.ts # SQLite Database Layer +├── scripts/ +│ ├── init-database.js # Database.sqlite Setup +│ ├── init-locations-db.js # Locations.sqlite Setup +│ ├── reset-admin.js # Admin User Reset +│ ├── remove-duplicates.js # Duplikate bereinigen +│ └── cleanup-old-locations.js # Alte Daten löschen +├── data/ +│ ├── database.sqlite # User + Devices +│ └── locations.sqlite # Location Tracking +├── types/ +│ └── location.ts # TypeScript Interfaces +└── middleware.ts # Route Protection +``` + +--- + +## 📝 Changelog + +### Version 1.1.0 - November 2025 + +#### 🆕 Neue Features +- **Custom Date Range Filter** + - Benutzerdefinierte Zeiträume mit DateTime-Picker (z.B. 16.11.2025 16:00 - 17.11.2025 06:00) + - Toggle-Button zwischen Quick Filters und Custom Range + - Kompakte UI - Custom Range nur sichtbar wenn aktiviert + - Backend-Support mit `startTime` und `endTime` API-Parametern + +#### 🐛 Bug Fixes +- **Zeitfilter-Bug behoben** + - Alte Locations (z.B. 6+ Stunden alt) werden jetzt korrekt ausgefiltert + - JavaScript-basierte Zeitberechnung statt SQLite `datetime('now')` + - Verhindert Zeitversatz-Probleme + +#### 🛠️ Verbesserungen +- Zoom-basierte Icon-Skalierung für bessere Sichtbarkeit +- Optimierte Zeitfilter-Logik in Datenbank-Queries +- Debug-Script `test-time-filter.js` für Troubleshooting + +#### 📚 Dokumentation +- README aktualisiert mit Custom Range Anleitung +- API-Endpunkte Dokumentation erweitert +- Wartungs-Abschnitt mit Debug-Script Information + +--- + +## 🐛 Troubleshooting + +### "Invalid username or password" + +**Lösung:** +```bash +node scripts/reset-admin.js +``` + +### Datenbank-Dateien fehlen + +**Lösung:** +```bash +npm run db:init +``` + +### Duplikate in locations.sqlite + +**Lösung:** +```bash +# Erst Duplikate entfernen +node scripts/remove-duplicates.js + +# Dann UNIQUE Index hinzufügen +node scripts/init-locations-db.js +``` + +### Map zeigt keine Daten + +1. n8n Webhook erreichbar? `curl https://n8n.example.com/webhook/location` +2. Locations in Datenbank? `/admin` → Database Statistics prüfen +3. Auto-Sync aktiv? Browser Console öffnen + +### "ENOENT: no such file or directory, open 'data/database.sqlite'" + +**Lösung:** +```bash +mkdir -p data +npm run db:init +``` + +--- + +## 📝 NPM Scripts + +```bash +# Development +npm run dev # Dev Server starten + +# Production +npm run build # Production Build +npm run start # Production Server + +# Database +npm run db:init # Beide DBs initialisieren +npm run db:init:app # Nur database.sqlite +npm run db:init:locations # Nur locations.sqlite +npm run db:cleanup # Cleanup 7 Tage +npm run db:cleanup:7d # Cleanup 7 Tage +npm run db:cleanup:30d # Cleanup 30 Tage + +# Linting +npm run lint # ESLint ausführen +``` + +--- + +## 📄 Lizenz + +MIT License - Open Source + +--- + +## 📜 Open Source Lizenzen + +Diese Anwendung verwendet folgende Open-Source-Software: + +### MIT License +- [Next.js](https://github.com/vercel/next.js) - Copyright (c) Vercel, Inc. +- [React](https://github.com/facebook/react) - Copyright (c) Meta Platforms, Inc. +- [React-DOM](https://github.com/facebook/react) - Copyright (c) Meta Platforms, Inc. +- [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) - Copyright (c) Tailwind Labs, Inc. +- [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) - Copyright (c) Joshua Wise +- [bcryptjs](https://github.com/dcodeIO/bcrypt.js) - Copyright (c) Daniel Wirtz + +### Apache License 2.0 +- [TypeScript](https://github.com/microsoft/TypeScript) - Copyright (c) Microsoft Corporation + +### ISC License +- [NextAuth.js](https://github.com/nextauthjs/next-auth) - Copyright (c) NextAuth.js Contributors + +### BSD-2-Clause License +- [Leaflet](https://github.com/Leaflet/Leaflet) - Copyright (c) Vladimir Agafonkin + +### Hippocratic License 2.1 +- [React-Leaflet](https://github.com/PaulLeCam/react-leaflet) - Copyright (c) Paul Le Cam + +**Vollständige Lizenztexte:** +Alle vollständigen Lizenztexte der verwendeten Bibliotheken finden Sie in den jeweiligen GitHub-Repositories oder in der `node_modules` Directory nach Installation. + +--- + +## 🙏 Credits + +- **Next.js 14** - React Framework +- **Leaflet.js** - Karten-Bibliothek +- **NextAuth.js** - Authentifizierung +- **better-sqlite3** - SQLite für Node.js +- **Tailwind CSS** - Utility-First CSS +- **n8n** - Workflow Automation (Backend) +- **OwnTracks** - Location Tracking Apps + +--- + +## 📞 Support + +Bei Fragen oder Problemen: +1. Logs prüfen (`npm run dev` Output) +2. Browser Console öffnen (F12) +3. Datenbank-Status in `/admin` prüfen +4. Issues im Repository erstellen diff --git a/app/admin/devices/page.tsx b/app/admin/devices/page.tsx new file mode 100644 index 0000000..3536885 --- /dev/null +++ b/app/admin/devices/page.tsx @@ -0,0 +1,595 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; + +interface Device { + id: string; + name: string; + color: string; + description?: string; + isActive: boolean; + createdAt: string; + updatedAt: string; + owner?: { + id: string; + username: string; + }; + latestLocation?: { + latitude: string | number; + longitude: string | number; + timestamp: string; + battery?: number; + speed?: number; + }; + _count?: { + locations: number; + }; +} + +export default function DevicesPage() { + const { data: session } = useSession(); + const userRole = (session?.user as any)?.role; + const isAdmin = userRole === 'ADMIN'; + + const [devices, setDevices] = useState([]); + const [loading, setLoading] = useState(true); + const [showAddModal, setShowAddModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [selectedDevice, setSelectedDevice] = useState(null); + const [error, setError] = useState(null); + + // Form state + const [formData, setFormData] = useState({ + id: "", + name: "", + color: "#3498db", + description: "", + }); + + useEffect(() => { + fetchDevices(); + + // Auto-refresh every 10 seconds to update online/offline status + const interval = setInterval(fetchDevices, 10000); + return () => clearInterval(interval); + }, []); + + const fetchDevices = async () => { + try { + const response = await fetch("/api/devices"); + if (!response.ok) throw new Error("Failed to fetch devices"); + const data = await response.json(); + setDevices(data.devices || []); + setError(null); + } catch (err) { + console.error("Failed to fetch devices", err); + setError("Failed to load devices"); + } finally { + setLoading(false); + } + }; + + const handleAdd = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const response = await fetch("/api/devices", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to create device"); + } + + await fetchDevices(); + setShowAddModal(false); + setFormData({ id: "", name: "", color: "#3498db", description: "" }); + } catch (err: any) { + alert(err.message); + } + }; + + const handleEdit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedDevice) return; + + try { + // If ID changed, we need to delete old and create new device + if (formData.id !== selectedDevice.id) { + // Delete old device + const deleteResponse = await fetch(`/api/devices/${selectedDevice.id}`, { + method: "DELETE", + }); + if (!deleteResponse.ok) { + throw new Error("Failed to delete old device"); + } + + // Create new device with new ID + const createResponse = await fetch("/api/devices", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }); + if (!createResponse.ok) { + const error = await createResponse.json(); + throw new Error(error.error || "Failed to create device with new ID"); + } + } else { + // Just update existing device + const response = await fetch(`/api/devices/${selectedDevice.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: formData.name, + color: formData.color, + description: formData.description, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to update device"); + } + } + + await fetchDevices(); + setShowEditModal(false); + setSelectedDevice(null); + } catch (err: any) { + alert(err.message); + } + }; + + const handleDelete = async () => { + if (!selectedDevice) return; + + try { + const response = await fetch(`/api/devices/${selectedDevice.id}`, { + method: "DELETE", + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to delete device"); + } + + await fetchDevices(); + setShowDeleteModal(false); + setSelectedDevice(null); + } catch (err: any) { + alert(err.message); + } + }; + + const openEditModal = (device: Device) => { + setSelectedDevice(device); + setFormData({ + id: device.id, + name: device.name, + color: device.color, + description: device.description || "", + }); + setShowEditModal(true); + }; + + const openDeleteModal = (device: Device) => { + setSelectedDevice(device); + setShowDeleteModal(true); + }; + + if (loading) { + return ( +
+
Loading devices...
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Device Management

+ {!isAdmin && ( +

Read-only view

+ )} +
+ {isAdmin && ( + + )} +
+ + {error && ( +
+ {error} +
+ )} + + {/* Device Cards */} +
+ {devices.map((device) => { + const lastSeen = device.latestLocation + ? new Date(device.latestLocation.timestamp) + : null; + const isRecent = lastSeen + ? Date.now() - lastSeen.getTime() < 10 * 60 * 1000 + : false; + + return ( +
+
+
+
+ 📱 +
+
+

+ {device.name} +

+

ID: {device.id}

+
+
+ + {isRecent ? "Online" : "Offline"} + +
+ + {device.description && ( +

{device.description}

+ )} + + {device.latestLocation && ( +
+
+ + 🕒 + Last Seen: + + + {new Date(device.latestLocation.timestamp).toLocaleString()} + +
+ + {device.latestLocation.battery !== undefined && ( +
+ + 🔋 + Battery: + + 20 ? 'text-green-600' : 'text-red-600' + }`}> + {device.latestLocation.battery}% + +
+ )} + + {device.latestLocation.speed !== undefined && ( +
+ + 🚗 + Speed: + + + {(Number(device.latestLocation.speed) * 3.6).toFixed(1)} km/h + +
+ )} + +
+ + 📍 + Location: + + + {Number(device.latestLocation.latitude).toFixed(5)},{" "} + {Number(device.latestLocation.longitude).toFixed(5)} + +
+
+ )} + + {device._count && ( +
+ {device._count.locations} location points +
+ )} + + {isAdmin && ( +
+ + +
+ )} +
+ ); + })} +
+ + {devices.length === 0 && ( +
+ No devices found. Add a device to get started. +
+ )} + + {/* Add Device Modal */} + {showAddModal && ( +
+
+

Add New Device

+
+
+ + + setFormData({ ...formData, id: e.target.value }) + } + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="e.g., 12, 13" + /> +

+ Must match OwnTracks tracker ID (tid) +

+
+ +
+ + + setFormData({ ...formData, name: e.target.value }) + } + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="e.g., iPhone 13" + /> +
+ +
+ +
+ + setFormData({ ...formData, color: e.target.value }) + } + className="h-10 w-20 rounded border border-gray-300" + /> + + setFormData({ ...formData, color: e.target.value }) + } + className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="#3498db" + /> +
+
+ +
+ +