diff --git a/Dockerfile b/Dockerfile
index e5ee795..bb77fe1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -36,6 +36,7 @@ RUN npm ci --omit=dev
# Kopiere Build Artifacts
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/data ./data
+COPY --from=builder /app/public ./public
# Kopiere App Code (benötigt für instrumentation.ts, lib/, etc.)
COPY --from=builder /app/instrumentation.ts ./
diff --git a/README.md b/README.md
index bd464b2..cdbdd15 100644
--- a/README.md
+++ b/README.md
@@ -1093,7 +1093,6 @@ npm run email:dev # Email Template Dev Server
# Linting
npm run lint # ESLint ausführen
```
-
---
## 📚 Automatische Dokumentations-Synchronisation
diff --git a/app/admin/settings/notifications/page.tsx b/app/admin/settings/notifications/page.tsx
new file mode 100644
index 0000000..595122a
--- /dev/null
+++ b/app/admin/settings/notifications/page.tsx
@@ -0,0 +1,208 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useSession } from 'next-auth/react';
+
+export default function NotificationSettingsPage() {
+ const { data: session } = useSession();
+ const [settings, setSettings] = useState({
+ email_enabled: true,
+ telegram_enabled: false,
+ telegram_chat_id: '',
+ });
+ const [loading, setLoading] = useState(false);
+ const [message, setMessage] = useState('');
+
+ useEffect(() => {
+ if (session?.user) {
+ loadSettings();
+ }
+ }, [session]);
+
+ async function loadSettings() {
+ try {
+ const userId = (session!.user as any).id;
+ const res = await fetch(`/api/users/${userId}/notification-settings`);
+ const data = await res.json();
+ setSettings(data.settings);
+ } catch (error) {
+ console.error('Failed to load settings:', error);
+ }
+ }
+
+ async function handleSave() {
+ setLoading(true);
+ setMessage('');
+
+ try {
+ const userId = (session!.user as any).id;
+ const res = await fetch(`/api/users/${userId}/notification-settings`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(settings),
+ });
+
+ if (res.ok) {
+ setMessage('✅ Einstellungen gespeichert!');
+ } else {
+ setMessage('❌ Fehler beim Speichern');
+ }
+ } catch (error) {
+ setMessage('❌ Fehler beim Speichern');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleTest() {
+ setLoading(true);
+ setMessage('');
+
+ try {
+ const userId = (session!.user as any).id;
+ const res = await fetch(
+ `/api/users/${userId}/notification-settings/test`,
+ { method: 'POST' }
+ );
+
+ if (res.ok) {
+ setMessage('✅ Test-Nachricht gesendet!');
+ } else {
+ const data = await res.json();
+ setMessage(`❌ Fehler: ${data.error}`);
+ }
+ } catch (error) {
+ setMessage('❌ Fehler beim Senden der Test-Nachricht');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
Benachrichtigungseinstellungen
+
+
+ {/* Email Settings */}
+
+
+
+ Geofence-Ereignisse per E-Mail erhalten
+
+
+
+ {/* Telegram Settings */}
+
+
+
+ Geofence-Ereignisse per Telegram erhalten (inkl. Karte und Buttons)
+
+
+ {settings.telegram_enabled && (
+
+
+
+ setSettings({ ...settings, telegram_chat_id: e.target.value })
+ }
+ placeholder="z.B. 123456789"
+ className="w-full px-3 py-2 border border-gray-300 rounded-md"
+ />
+
+ Deine Telegram Chat ID findest du über @userinfobot
+
+
+ )}
+
+
+ {/* Action Buttons */}
+
+
+
+ {settings.telegram_enabled && settings.telegram_chat_id && (
+
+ )}
+
+
+ {/* Status Message */}
+ {message && (
+
{message}
+ )}
+
+
+ {/* Help Section */}
+
+
📱 Telegram Bot aktivieren
+
+
+ {/* QR Code */}
+
+
Schritt 1: Bot starten
+

+
+ Scanne den QR-Code mit deinem Smartphone
+
+
+ oder öffne: @MeinTracking_bot
+
+
+
+ {/* Instructions */}
+
+
Schritt 2: Chat ID holen
+
+ - Starte den Bot mit
/start
+ - Suche in Telegram nach
@myidbot
+ - Sende
/getid an @myidbot
+ - Der Bot antwortet mit deiner Chat ID
+ - Kopiere die Nummer und trage sie oben ein
+
+
+ Alternative: @userinfobot mit /start
+
+
+
+
+
+ );
+}
diff --git a/app/api/users/[id]/notification-settings/route.ts b/app/api/users/[id]/notification-settings/route.ts
new file mode 100644
index 0000000..5bde578
--- /dev/null
+++ b/app/api/users/[id]/notification-settings/route.ts
@@ -0,0 +1,75 @@
+import { NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import {
+ getUserNotificationSettings,
+ updateUserNotificationSettings,
+} from '@/lib/notification-settings-db';
+
+// GET /api/users/[id]/notification-settings
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { id: userId } = await params;
+ const currentUserId = (session.user as any).id;
+
+ // Users can only view their own settings
+ if (userId !== currentUserId) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+
+ const settings = getUserNotificationSettings(userId);
+
+ return NextResponse.json({ settings });
+ } catch (error) {
+ console.error('[GET /api/users/[id]/notification-settings]', error);
+ return NextResponse.json(
+ { error: 'Failed to get settings' },
+ { status: 500 }
+ );
+ }
+}
+
+// PATCH /api/users/[id]/notification-settings
+export async function PATCH(
+ request: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { id: userId } = await params;
+ const currentUserId = (session.user as any).id;
+
+ // Users can only update their own settings
+ if (userId !== currentUserId) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+
+ const body = await request.json();
+ const { email_enabled, telegram_enabled, telegram_chat_id } = body;
+
+ const settings = updateUserNotificationSettings(userId, {
+ email_enabled,
+ telegram_enabled,
+ telegram_chat_id,
+ });
+
+ return NextResponse.json({ settings });
+ } catch (error) {
+ console.error('[PATCH /api/users/[id]/notification-settings]', error);
+ return NextResponse.json(
+ { error: 'Failed to update settings' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/users/[id]/notification-settings/test/route.ts b/app/api/users/[id]/notification-settings/test/route.ts
new file mode 100644
index 0000000..18f1575
--- /dev/null
+++ b/app/api/users/[id]/notification-settings/test/route.ts
@@ -0,0 +1,52 @@
+import { NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { telegramService } from '@/lib/telegram-service';
+import { getUserNotificationSettings } from '@/lib/notification-settings-db';
+
+// POST /api/users/[id]/notification-settings/test
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { id: userId } = await params;
+ const currentUserId = (session.user as any).id;
+
+ if (userId !== currentUserId) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+
+ const settings = getUserNotificationSettings(userId);
+
+ if (!settings.telegram_chat_id) {
+ return NextResponse.json(
+ { error: 'No Telegram chat ID configured' },
+ { status: 400 }
+ );
+ }
+
+ const success = await telegramService.testConnection(
+ settings.telegram_chat_id
+ );
+
+ if (success) {
+ return NextResponse.json({ success: true, message: 'Test message sent' });
+ } else {
+ return NextResponse.json(
+ { error: 'Failed to send test message' },
+ { status: 500 }
+ );
+ }
+ } catch (error) {
+ console.error('[POST /api/users/[id]/notification-settings/test]', error);
+ return NextResponse.json(
+ { error: 'Failed to send test message' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index 56ca4b7..db43194 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -30,6 +30,8 @@ services:
- SMTP_FROM_EMAIL=${SMTP_FROM_EMAIL}
- SMTP_FROM_NAME=${SMTP_FROM_NAME:-Location Tracker}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
+ # Telegram Configuration
+ - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
volumes:
- ./data:/app/data
- ./public:/app/public # Mount public directory for static files
diff --git a/docs/telegram-anleitung.md b/docs/telegram-anleitung.md
new file mode 100644
index 0000000..909de37
--- /dev/null
+++ b/docs/telegram-anleitung.md
@@ -0,0 +1,163 @@
+# 📱 Telegram-Benachrichtigungen aktivieren
+
+## Übersicht
+
+Erhalte Geofence-Benachrichtigungen direkt in Telegram mit:
+- 📍 Standort-Pin auf der Karte
+- 🔔 Sofortige Push-Benachrichtigungen
+- 🔗 Direktlinks zur Karte und zum Dashboard
+
+---
+
+## 🚀 Schritt-für-Schritt Anleitung
+
+### Schritt 1: Bot starten
+
+#### Option A: QR-Code scannen (empfohlen für Smartphone)
+
+
+
+
+
+1. Öffne die Kamera-App auf deinem Smartphone
+2. Scanne den QR-Code
+3. Telegram öffnet sich automatisch
+4. Klicke auf **"Start"** oder sende `/start`
+
+#### Option B: Link öffnen
+
+1. Klicke auf: **[https://t.me/MeinTracking_bot](https://t.me/MeinTracking_bot)**
+2. Telegram öffnet sich automatisch
+3. Klicke auf **"Start"** oder sende `/start`
+
+✅ Du solltest eine Willkommensnachricht vom Bot erhalten
+
+---
+
+### Schritt 2: Chat ID herausfinden
+
+#### Option A: Mit @myidbot (empfohlen)
+
+1. Suche in Telegram nach: **@myidbot**
+2. Sende: `/getid`
+3. Der Bot antwortet sofort mit deiner Chat ID:
+ ```
+ Your user ID: 123456789
+ ```
+4. **Kopiere die Nummer** (z.B. `123456789`)
+
+#### Option B: Mit @userinfobot
+
+1. Suche in Telegram nach: **@userinfobot**
+2. Sende: `/start`
+3. Der Bot antwortet mit deinen Informationen:
+ ```
+ Id: 123456789
+ First name: Dein Name
+ ...
+ ```
+4. **Kopiere die Nummer** bei "Id:" (z.B. `123456789`)
+
+---
+
+### Schritt 3: Chat ID im Dashboard eintragen
+
+1. Melde dich im Location Tracker an
+2. Gehe zu: **Einstellungen** → **Benachrichtigungen**
+3. Aktiviere: ☑️ **Telegram Benachrichtigungen**
+4. Trage deine **Chat ID** ein (die Nummer aus Schritt 2)
+5. Klicke auf **"Speichern"**
+
+---
+
+### Schritt 4: Verbindung testen
+
+1. Klicke auf den Button **"Telegram Test"**
+2. Du solltest sofort eine Test-Nachricht in Telegram erhalten:
+ ```
+ ✅ Telegram Connection Test
+
+ Die Verbindung funktioniert!
+ ```
+
+✅ **Fertig!** Ab jetzt erhältst du alle Geofence-Benachrichtigungen auch per Telegram.
+
+---
+
+## 📨 Beispiel-Benachrichtigung
+
+So sieht eine Geofence-Benachrichtigung aus:
+
+```
+🟢 Geofence BETRETEN
+
+📱 Device: Mein iPhone
+📍 Geofence: Zuhause
+🕐 Zeit: 04.12.25, 14:30
+📊 Ereignis: Hat Zuhause betreten
+```
+
+**+ Standort-Pin auf der Karte**
+
+**+ Buttons:**
+- 🗺️ Auf Karte zeigen
+- 📊 Dashboard öffnen
+
+---
+
+## ❓ Häufige Fragen
+
+### Kann ich sowohl Email als auch Telegram aktivieren?
+
+Ja! Du kannst beide Kanäle gleichzeitig nutzen:
+- ☑️ E-Mail Benachrichtigungen
+- ☑️ Telegram Benachrichtigungen
+
+### Sehen andere User meine Benachrichtigungen?
+
+**Nein!** Jede Chat ID ist ein privater 1-zu-1 Chat. Andere User können deine Nachrichten nicht sehen, auch wenn sie den gleichen Bot nutzen.
+
+### Was ist, wenn die Chat ID falsch ist?
+
+Dann erhältst du keine Benachrichtigungen. Nutze den "Telegram Test" Button, um die Verbindung zu prüfen.
+
+### Kann ich Benachrichtigungen pausieren?
+
+Ja! Deaktiviere einfach "Telegram Benachrichtigungen" in den Einstellungen. Deine Chat ID bleibt gespeichert.
+
+### Wo finde ich meine Chat ID nochmal?
+
+Sende `/getid` an **@myidbot** oder `/start` an **@userinfobot** in Telegram.
+
+---
+
+## 🛠️ Troubleshooting
+
+### "No Telegram chat ID configured"
+
+→ Du hast noch keine Chat ID eingetragen. Folge Schritt 2 und 3.
+
+### "Failed to send test message"
+
+Mögliche Ursachen:
+1. **Chat ID falsch** → Überprüfe die Nummer bei @userinfobot
+2. **Bot nicht gestartet** → Sende `/start` an @MeinTracking_bot
+3. **Bot blockiert** → Entsperre den Bot in Telegram
+
+### Test-Nachricht kommt nicht an
+
+1. Überprüfe, ob du den Bot gestartet hast (`/start` an @MeinTracking_bot)
+2. Prüfe, ob die Chat ID korrekt ist (keine Leerzeichen, keine Buchstaben)
+ - Hole sie neu mit `/getid` an @myidbot
+3. Stelle sicher, dass du den Bot nicht blockiert hast
+
+---
+
+## 📞 Support
+
+Bei Problemen wende dich an den Administrator oder überprüfe die Logs im System.
+
+---
+
+**Bot:** [@MeinTracking_bot](https://t.me/MeinTracking_bot)
+**Letzte Aktualisierung:** 04.12.2025
diff --git a/docs/telegram-setup.md b/docs/telegram-setup.md
new file mode 100644
index 0000000..c0535d8
--- /dev/null
+++ b/docs/telegram-setup.md
@@ -0,0 +1,345 @@
+# 🚀 Telegram Integration - Setup Guide
+
+## ✅ Was wurde implementiert
+
+### Dateien erstellt:
+
+1. **Backend Services:**
+ - `lib/telegram-service.ts` - Telegram API Integration
+ - `lib/notification-settings-db.ts` - Datenbank-Layer für Settings
+ - `lib/geofence-notifications.ts` - Erweitert für Multi-Channel (Email + Telegram)
+
+2. **Database:**
+ - `scripts/init-notification-settings.js` - DB Init Script
+ - Tabelle: `UserNotificationSettings` erstellt
+
+3. **API Endpoints:**
+ - `GET/PATCH /api/users/[id]/notification-settings` - Settings verwalten
+ - `POST /api/users/[id]/notification-settings/test` - Test-Nachricht
+
+4. **Admin UI:**
+ - `app/admin/settings/notifications/page.tsx` - Settings Page mit QR Code
+
+5. **Dokumentation:**
+ - `docs/telegram-anleitung.md` - User-Anleitung
+ - `docs/telegram-setup.md` - Dieses Dokument
+ - `public/telegram-bot-qr.png` - QR Code für Bot
+
+---
+
+## 🔧 Installation & Setup
+
+### 1. Dependencies installiert
+
+```bash
+✅ npm install axios
+✅ npm install qrcode
+```
+
+### 2. Datenbank initialisiert
+
+```bash
+✅ node scripts/init-notification-settings.js
+```
+
+Die Tabelle `UserNotificationSettings` wurde erstellt mit:
+- `user_id` (PK, FK → User.id)
+- `email_enabled` (0/1)
+- `telegram_enabled` (0/1)
+- `telegram_chat_id` (TEXT)
+- `created_at`, `updated_at`
+
+---
+
+## 📱 Bot Setup
+
+### Telegram Bot erstellen (für Admin):
+
+1. Öffne Telegram und suche: **@BotFather**
+2. Sende: `/newbot`
+3. Folge den Anweisungen:
+ - Bot Name: `MeinTracking Bot` (oder anders)
+ - Bot Username: `MeinTracking_bot` (muss auf `_bot` enden)
+4. BotFather gibt dir einen Token: `1234567890:ABCdefGHIjklMNOpqrsTUVwxyz`
+5. **Wichtig:** Token NIEMALS committen!
+
+### .env konfigurieren:
+
+```bash
+# .env oder .env.local
+TELEGRAM_BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz
+```
+
+---
+
+## 🎯 Für User: Bot aktivieren
+
+**Schritt-für-Schritt siehe:** `docs/telegram-anleitung.md`
+
+**Kurzversion:**
+1. Bot starten: [https://t.me/MeinTracking_bot](https://t.me/MeinTracking_bot) oder QR scannen
+2. Chat ID holen: `@myidbot` → `/getid` (oder `@userinfobot` → `/start`)
+3. Im Dashboard: Settings → Notifications → Chat ID eintragen
+4. Test Button drücken
+
+---
+
+## 🧪 Testing
+
+### 1. QR Code generieren
+
+```bash
+npm run telegram:qr
+```
+
+Erstellt: `public/telegram-bot-qr.png`
+
+### 2. Datenbank neu initialisieren
+
+```bash
+npm run db:init:notifications
+```
+
+Oder alles zusammen:
+```bash
+npm run db:init
+```
+
+### 3. Test-Nachricht senden
+
+**Option A: Über Admin UI**
+- Navigiere zu: `/admin/settings/notifications`
+- Telegram aktivieren
+- Chat ID eintragen
+- Button "Telegram Test" klicken
+
+**Option B: Direkt via Script**
+
+Erstelle `scripts/test-telegram.js`:
+```javascript
+const { telegramService } = require('../lib/telegram-service');
+
+const CHAT_ID = '123456789'; // Deine Chat ID
+
+async function test() {
+ await telegramService.testConnection(CHAT_ID);
+}
+test();
+```
+
+```bash
+TELEGRAM_BOT_TOKEN=dein_token node scripts/test-telegram.js
+```
+
+---
+
+## 🔐 Sicherheit & Privacy
+
+### Wie funktioniert die Privacy?
+
+- **1 Bot für alle User** (in `.env`)
+- **Jeder User hat eigene Chat ID** (in DB)
+- **Bot sendet nur an spezifische Chat ID** → Privater 1-zu-1 Chat
+- **User A sieht NIEMALS Nachrichten von User B**
+
+### Telegram API Garantie:
+
+```typescript
+telegram.sendMessage(chatId: "123456789", text: "...")
+→ Geht NUR an Chat ID 123456789
+→ Andere Chat IDs können dies NICHT sehen
+```
+
+### Vergleich:
+Wie Email: 1 SMTP-Server (Bot) sendet an verschiedene Adressen (Chat IDs)
+
+---
+
+## 📊 Wie es funktioniert
+
+### Notification Flow:
+
+```
+1. Device triggers Geofence
+ ↓
+2. geofence-engine.ts erkennt Event
+ ↓
+3. GeofenceEvent in DB erstellt
+ ↓
+4. geofence-notifications.ts aufgerufen
+ ↓
+5. getUserNotificationSettings(owner_id)
+ ↓
+6. Parallel execution:
+ ├─→ Email (if enabled)
+ └─→ Telegram (if enabled)
+ ↓
+ ├─→ Text mit Emoji, Device, Geofence, Zeit
+ ├─→ Inline Buttons (Karte, Dashboard)
+ └─→ Location Pin
+```
+
+### Multi-Channel Strategy:
+
+- **Promise.allSettled()** → Beide parallel
+- **Mind. 1 erfolgreich** → Event als "sent" markiert
+- **Beide fehlgeschlagen** → Event als "failed" markiert
+- **Robust:** Email-Fehler betrifft nicht Telegram (und umgekehrt)
+
+---
+
+## 🎨 Admin UI Features
+
+**Page:** `/admin/settings/notifications`
+
+**Komponenten:**
+- ☑️ Email Benachrichtigungen Toggle
+- ☑️ Telegram Benachrichtigungen Toggle
+- 📝 Telegram Chat ID Input
+- 💾 Speichern Button
+- 🧪 Telegram Test Button
+- 📱 QR Code Display (400x400px)
+- 📖 Schritt-für-Schritt Anleitung
+
+---
+
+## 🛠️ NPM Scripts
+
+```bash
+# Datenbank initialisieren
+npm run db:init # Alles (inkl. Notifications)
+npm run db:init:notifications # Nur Notifications Tabelle
+
+# QR Code generieren
+npm run telegram:qr # Erstellt public/telegram-bot-qr.png
+
+# Testing
+npm run test:geofence # Geofence Event simulieren
+npm run test:geofence:email # Email Notification testen
+```
+
+---
+
+## 📝 Telegram Message Format
+
+### Enter Event:
+```
+🟢 Geofence BETRETEN
+
+📱 Device: Mein iPhone
+📍 Geofence: Zuhause
+🕐 Zeit: 04.12.25, 14:30
+📊 Ereignis: Hat Zuhause betreten
+```
+
+**+ Location Pin** (Karte mit exakter Position)
+
+**+ Buttons:**
+- 🗺️ Auf Karte zeigen → `/map?lat=...&lon=...`
+- 📊 Dashboard öffnen → `/admin/geofences/events`
+
+### Exit Event:
+```
+🔴 Geofence VERLASSEN
+
+📱 Device: Mein iPhone
+📍 Geofence: Zuhause
+🕐 Zeit: 04.12.25, 18:45
+📊 Ereignis: Hat Zuhause verlassen
+```
+
+---
+
+## 🚨 Troubleshooting
+
+### "Cannot find module telegram-service"
+
+→ TypeScript nicht kompiliert. Lösung:
+```bash
+npm run build
+```
+
+Oder für Development:
+```bash
+npm run dev
+```
+
+### "Telegram bot token not configured"
+
+→ `.env` fehlt `TELEGRAM_BOT_TOKEN`
+
+### "Failed to send test message"
+
+**Mögliche Ursachen:**
+1. Bot nicht gestartet (`/start` an @MeinTracking_bot)
+2. Chat ID falsch
+3. Bot blockiert
+4. Token falsch/abgelaufen
+
+**Debug:**
+```bash
+# Check .env
+cat .env | grep TELEGRAM
+
+# Check DB
+sqlite3 data/database.sqlite "SELECT * FROM UserNotificationSettings;"
+
+# Test Telegram API direkt
+curl https://api.telegram.org/bot/getMe
+```
+
+### QR Code wird nicht angezeigt
+
+→ Prüfen ob Datei existiert:
+```bash
+ls -la public/telegram-bot-qr.png
+```
+
+Falls nicht:
+```bash
+npm run telegram:qr
+```
+
+---
+
+## 📚 Environment Variables
+
+```bash
+# Required für Telegram
+TELEGRAM_BOT_TOKEN=
+
+# Optional (für Map/Dashboard Links)
+NEXTAUTH_URL=https://deine-domain.de
+```
+
+---
+
+## 🎯 Nächste Schritte
+
+**Für Produktion:**
+
+1. ✅ `.env.production` mit echtem Bot-Token erstellen
+2. ✅ Datenbank migrieren: `npm run db:init`
+3. ✅ QR Code generieren: `npm run telegram:qr`
+4. ✅ User-Anleitung teilen: `docs/telegram-anleitung.md`
+5. ✅ Test durchführen mit echtem Device
+
+**Optional:**
+- Push Notifications hinzufügen
+- SMS Integration
+- Webhook Support
+- Notification History UI
+- Per-Geofence Settings (nicht global)
+
+---
+
+## 📞 Support
+
+**Bot:** [@MeinTracking_bot](https://t.me/MeinTracking_bot)
+
+**Dokumentation:**
+- User-Anleitung: `docs/telegram-anleitung.md`
+- API Implementierung: `docs/telegram.md`
+- Dieses Setup-Guide: `docs/telegram-setup.md`
+
+**Letzte Aktualisierung:** 04.12.2025
diff --git a/docs/telegram.md b/docs/telegram.md
new file mode 100644
index 0000000..371690e
--- /dev/null
+++ b/docs/telegram.md
@@ -0,0 +1,1162 @@
+# Telegram Notifications Implementation Guide
+
+## 📋 Overview
+
+This document describes how to implement Telegram notifications for the geofencing system, allowing users to receive geofence alerts via Telegram instead of or in addition to email notifications.
+
+## 🎯 Features
+
+- ✅ **Text notifications** - Rich formatted messages with device and geofence info
+- ✅ **Location pins** - Telegram map with exact coordinates where event occurred
+- ✅ **Inline buttons** - Interactive buttons to view on map or open dashboard
+- ✅ **User-configurable** - Each user can choose: Email, Telegram, both, or neither
+- ✅ **Parallel execution** - Email and Telegram sent simultaneously for speed
+- ✅ **Robust** - If one channel fails, the other still works
+
+## 📐 Architecture
+
+```
+Geofence Event
+ ↓
+geofence-notifications.ts
+ ↓
+getUserNotificationSettings(userId)
+ ↓
+ ├─→ Email Service (if email_enabled && email exists)
+ └─→ Telegram Service (if telegram_enabled && chat_id exists)
+ ↓
+ Send Text + Location Pin + Inline Buttons
+```
+
+## 🗄️ Database Changes
+
+### Option A: Extend User Table
+
+```sql
+-- Add to existing User table
+ALTER TABLE User ADD COLUMN telegram_chat_id TEXT;
+ALTER TABLE User ADD COLUMN notification_email INTEGER DEFAULT 1;
+ALTER TABLE User ADD COLUMN notification_telegram INTEGER DEFAULT 0;
+```
+
+### Option B: Separate Settings Table (Recommended)
+
+```sql
+-- Create dedicated notification settings table
+CREATE TABLE UserNotificationSettings (
+ user_id TEXT PRIMARY KEY,
+ email_enabled INTEGER DEFAULT 1,
+ telegram_enabled INTEGER DEFAULT 0,
+ telegram_chat_id TEXT,
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now')),
+
+ FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE,
+ CHECK (email_enabled IN (0, 1)),
+ CHECK (telegram_enabled IN (0, 1))
+);
+
+-- Create index for performance
+CREATE INDEX idx_user_notification_settings_user
+ ON UserNotificationSettings(user_id);
+```
+
+**Why Option B is better:**
+- Cleaner separation of concerns
+- Easier to add more notification channels later (Push, SMS, etc.)
+- Can track settings changes history
+- Doesn't pollute User table
+
+## 📝 Implementation Steps
+
+### 1. Create Telegram Service
+
+**File:** `lib/telegram-service.ts`
+
+```typescript
+import axios from 'axios';
+
+interface TelegramMessage {
+ chatId: string;
+ text: string;
+ latitude?: number;
+ longitude?: number;
+ buttons?: Array<{ text: string; url: string }>;
+}
+
+interface GeofenceNotificationParams {
+ chatId: string;
+ deviceName: string;
+ geofenceName: string;
+ eventType: 'enter' | 'exit';
+ latitude: number;
+ longitude: number;
+ timestamp: string;
+ mapUrl?: string;
+ dashboardUrl?: string;
+}
+
+class TelegramService {
+ private botToken: string;
+ private baseUrl: string;
+
+ constructor() {
+ this.botToken = process.env.TELEGRAM_BOT_TOKEN || '';
+ if (!this.botToken) {
+ console.warn('[TelegramService] No TELEGRAM_BOT_TOKEN configured');
+ }
+ this.baseUrl = `https://api.telegram.org/bot${this.botToken}`;
+ }
+
+ /**
+ * Check if Telegram is configured
+ */
+ isConfigured(): boolean {
+ return !!this.botToken;
+ }
+
+ /**
+ * Send text message with optional inline buttons
+ */
+ async sendMessage(params: TelegramMessage): Promise {
+ if (!this.isConfigured()) {
+ throw new Error('Telegram bot token not configured');
+ }
+
+ const { chatId, text, buttons } = params;
+
+ const payload: any = {
+ chat_id: chatId,
+ text: text,
+ parse_mode: 'HTML',
+ };
+
+ // Add inline buttons if provided
+ if (buttons && buttons.length > 0) {
+ payload.reply_markup = {
+ inline_keyboard: [
+ buttons.map(btn => ({
+ text: btn.text,
+ url: btn.url,
+ })),
+ ],
+ };
+ }
+
+ const response = await axios.post(`${this.baseUrl}/sendMessage`, payload);
+
+ if (!response.data.ok) {
+ throw new Error(`Telegram API error: ${response.data.description}`);
+ }
+ }
+
+ /**
+ * Send location pin on map
+ */
+ async sendLocation(
+ chatId: string,
+ latitude: number,
+ longitude: number
+ ): Promise {
+ if (!this.isConfigured()) {
+ throw new Error('Telegram bot token not configured');
+ }
+
+ const response = await axios.post(`${this.baseUrl}/sendLocation`, {
+ chat_id: chatId,
+ latitude,
+ longitude,
+ });
+
+ if (!response.data.ok) {
+ throw new Error(`Telegram API error: ${response.data.description}`);
+ }
+ }
+
+ /**
+ * Send geofence notification (complete with text + location + buttons)
+ */
+ async sendGeofenceNotification(
+ params: GeofenceNotificationParams
+ ): Promise {
+ const {
+ chatId,
+ deviceName,
+ geofenceName,
+ eventType,
+ latitude,
+ longitude,
+ timestamp,
+ mapUrl,
+ dashboardUrl,
+ } = params;
+
+ // Format message
+ const emoji = eventType === 'enter' ? '🟢' : '🔴';
+ const action = eventType === 'enter' ? 'BETRETEN' : 'VERLASSEN';
+ const verb = eventType === 'enter' ? 'betreten' : 'verlassen';
+
+ const formattedDate = new Date(timestamp).toLocaleString('de-DE', {
+ dateStyle: 'short',
+ timeStyle: 'short',
+ });
+
+ const text = `
+${emoji} Geofence ${action}
+
+📱 Device: ${deviceName}
+📍 Geofence: ${geofenceName}
+🕐 Zeit: ${formattedDate}
+📊 Ereignis: Hat ${geofenceName} ${verb}
+ `.trim();
+
+ // Prepare inline buttons
+ const buttons = [];
+ if (mapUrl) {
+ buttons.push({ text: '🗺️ Auf Karte zeigen', url: mapUrl });
+ }
+ if (dashboardUrl) {
+ buttons.push({ text: '📊 Dashboard öffnen', url: dashboardUrl });
+ }
+
+ // Send text message with buttons
+ await this.sendMessage({ chatId, text, buttons });
+
+ // Send location pin
+ await this.sendLocation(chatId, latitude, longitude);
+ }
+
+ /**
+ * Test connection by sending a simple message
+ */
+ async testConnection(chatId: string): Promise {
+ try {
+ await this.sendMessage({
+ chatId,
+ text: '✅ Telegram Connection Test\n\nDie Verbindung funktioniert!',
+ });
+ return true;
+ } catch (error) {
+ console.error('[TelegramService] Test failed:', error);
+ return false;
+ }
+ }
+}
+
+export const telegramService = new TelegramService();
+```
+
+### 2. Create Notification Settings Database Layer
+
+**File:** `lib/notification-settings-db.ts`
+
+```typescript
+import Database from 'better-sqlite3';
+import path from 'path';
+
+const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
+
+export interface UserNotificationSettings {
+ user_id: string;
+ email_enabled: boolean;
+ telegram_enabled: boolean;
+ telegram_chat_id: string | null;
+ created_at?: string;
+ updated_at?: string;
+}
+
+/**
+ * Get notification settings for a user
+ * Returns default settings if not found
+ */
+export function getUserNotificationSettings(
+ userId: string
+): UserNotificationSettings {
+ const db = new Database(dbPath);
+ try {
+ const stmt = db.prepare(`
+ SELECT
+ user_id,
+ email_enabled,
+ telegram_enabled,
+ telegram_chat_id,
+ created_at,
+ updated_at
+ FROM UserNotificationSettings
+ WHERE user_id = ?
+ `);
+
+ const result = stmt.get(userId) as UserNotificationSettings | undefined;
+
+ // Return defaults if no settings found
+ if (!result) {
+ return {
+ user_id: userId,
+ email_enabled: true,
+ telegram_enabled: false,
+ telegram_chat_id: null,
+ };
+ }
+
+ return result;
+ } finally {
+ db.close();
+ }
+}
+
+/**
+ * Update notification settings for a user
+ */
+export function updateUserNotificationSettings(
+ userId: string,
+ settings: Partial
+): UserNotificationSettings {
+ const db = new Database(dbPath);
+ try {
+ // Check if settings exist
+ const existing = db
+ .prepare('SELECT user_id FROM UserNotificationSettings WHERE user_id = ?')
+ .get(userId);
+
+ if (existing) {
+ // Update existing
+ const updates: string[] = [];
+ const values: any[] = [];
+
+ if (settings.email_enabled !== undefined) {
+ updates.push('email_enabled = ?');
+ values.push(settings.email_enabled ? 1 : 0);
+ }
+ if (settings.telegram_enabled !== undefined) {
+ updates.push('telegram_enabled = ?');
+ values.push(settings.telegram_enabled ? 1 : 0);
+ }
+ if (settings.telegram_chat_id !== undefined) {
+ updates.push('telegram_chat_id = ?');
+ values.push(settings.telegram_chat_id);
+ }
+
+ updates.push('updated_at = datetime(\'now\')');
+ values.push(userId);
+
+ const stmt = db.prepare(`
+ UPDATE UserNotificationSettings
+ SET ${updates.join(', ')}
+ WHERE user_id = ?
+ `);
+
+ stmt.run(...values);
+ } else {
+ // Insert new
+ const stmt = db.prepare(`
+ INSERT INTO UserNotificationSettings (
+ user_id,
+ email_enabled,
+ telegram_enabled,
+ telegram_chat_id
+ ) VALUES (?, ?, ?, ?)
+ `);
+
+ stmt.run(
+ userId,
+ settings.email_enabled ?? 1,
+ settings.telegram_enabled ?? 0,
+ settings.telegram_chat_id ?? null
+ );
+ }
+
+ return getUserNotificationSettings(userId);
+ } finally {
+ db.close();
+ }
+}
+
+/**
+ * Initialize notification settings table
+ */
+export function initNotificationSettingsTable(): void {
+ const db = new Database(dbPath);
+ try {
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS UserNotificationSettings (
+ user_id TEXT PRIMARY KEY,
+ email_enabled INTEGER DEFAULT 1,
+ telegram_enabled INTEGER DEFAULT 0,
+ telegram_chat_id TEXT,
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now')),
+
+ FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE,
+ CHECK (email_enabled IN (0, 1)),
+ CHECK (telegram_enabled IN (0, 1))
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_user_notification_settings_user
+ ON UserNotificationSettings(user_id);
+ `);
+
+ console.log('✓ UserNotificationSettings table initialized');
+ } finally {
+ db.close();
+ }
+}
+```
+
+### 3. Update Geofence Notifications Handler
+
+**File:** `lib/geofence-notifications.ts` (extend existing)
+
+```typescript
+import { emailService } from './email-service';
+import { telegramService } from './telegram-service';
+import {
+ getUserNotificationSettings,
+ UserNotificationSettings,
+} from './notification-settings-db';
+import {
+ renderGeofenceEnterEmail,
+ renderGeofenceExitEmail,
+ GeofenceEmailData,
+} from './email-renderer';
+// ... existing imports ...
+
+/**
+ * Send notification for a single geofence event
+ * Supports both Email and Telegram based on user preferences
+ */
+async function sendEventNotification(event: GeofenceEvent): Promise {
+ 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) {
+ throw new Error(`Owner not found: ${geofence.owner_id}`);
+ }
+
+ // Get notification preferences
+ const settings = getUserNotificationSettings(owner.id);
+
+ // Prepare notification tasks
+ const promises: Promise[] = [];
+
+ // Send Email if enabled
+ if (settings.email_enabled && owner.email) {
+ promises.push(
+ sendEmailNotification(event, owner, device, geofence)
+ );
+ }
+
+ // Send Telegram if enabled
+ if (settings.telegram_enabled && settings.telegram_chat_id) {
+ promises.push(
+ sendTelegramNotification(
+ event,
+ owner,
+ device,
+ geofence,
+ settings.telegram_chat_id
+ )
+ );
+ }
+
+ // If no notification channel enabled, skip
+ if (promises.length === 0) {
+ console.log(
+ `[GeofenceNotification] No notification channels enabled for user ${owner.id}`
+ );
+ geofenceDb.markNotificationSent(event.id!, true);
+ return;
+ }
+
+ // Send notifications in parallel
+ const results = await Promise.allSettled(promises);
+
+ // Check if at least one succeeded
+ const anySuccess = results.some((r) => r.status === 'fulfilled');
+
+ if (anySuccess) {
+ geofenceDb.markNotificationSent(event.id!, true);
+ console.log(
+ `[GeofenceNotification] Sent notification for geofence ${geofence.name}`
+ );
+ } else {
+ // All failed
+ const errors = results
+ .filter((r) => r.status === 'rejected')
+ .map((r: any) => r.reason.message)
+ .join('; ');
+
+ geofenceDb.markNotificationSent(event.id!, false, errors);
+ throw new Error(`All notification channels failed: ${errors}`);
+ }
+ } catch (error) {
+ console.error('[GeofenceNotification] Failed to send notification:', error);
+
+ const errorMessage =
+ error instanceof Error ? error.message : 'Unknown error';
+ geofenceDb.markNotificationSent(event.id!, false, errorMessage);
+
+ throw error;
+ }
+}
+
+/**
+ * Send email notification (existing function - extracted)
+ */
+async function sendEmailNotification(
+ event: GeofenceEvent,
+ owner: UserInfo,
+ device: DeviceInfo,
+ geofence: GeofenceInfo
+): Promise {
+ try {
+ 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,
+ };
+
+ 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`;
+ }
+
+ await emailService['sendEmail'](owner.email!, subject, html);
+
+ console.log(
+ `[GeofenceNotification] Sent email to ${owner.email}`
+ );
+ } catch (error) {
+ console.error('[GeofenceNotification] Email error:', error);
+ throw error;
+ }
+}
+
+/**
+ * Send Telegram notification (new function)
+ */
+async function sendTelegramNotification(
+ event: GeofenceEvent,
+ owner: UserInfo,
+ device: DeviceInfo,
+ geofence: GeofenceInfo,
+ chatId: string
+): Promise {
+ try {
+ const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
+
+ await telegramService.sendGeofenceNotification({
+ chatId,
+ deviceName: device.name,
+ geofenceName: geofence.name,
+ eventType: event.event_type,
+ latitude: event.latitude,
+ longitude: event.longitude,
+ timestamp: event.timestamp,
+ mapUrl: `${baseUrl}/map?lat=${event.latitude}&lon=${event.longitude}`,
+ dashboardUrl: `${baseUrl}/admin/geofences/events`,
+ });
+
+ console.log(
+ `[GeofenceNotification] Sent Telegram notification to ${chatId}`
+ );
+ } catch (error) {
+ console.error('[GeofenceNotification] Telegram error:', error);
+ throw error;
+ }
+}
+
+// Export for other modules
+export { sendEventNotification };
+```
+
+### 4. Create Database Init Script
+
+**File:** `scripts/init-notification-settings.js`
+
+```javascript
+#!/usr/bin/env node
+/**
+ * Initialize UserNotificationSettings table
+ */
+
+const { initNotificationSettingsTable } = require('../lib/notification-settings-db');
+
+try {
+ initNotificationSettingsTable();
+ console.log('✅ Notification settings table initialized!');
+} catch (error) {
+ console.error('❌ Error:', error.message);
+ process.exit(1);
+}
+```
+
+### 5. Add API Routes for Settings
+
+**File:** `app/api/users/[id]/notification-settings/route.ts`
+
+```typescript
+import { NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import {
+ getUserNotificationSettings,
+ updateUserNotificationSettings,
+} from '@/lib/notification-settings-db';
+
+// GET /api/users/[id]/notification-settings
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { id: userId } = await params;
+ const currentUserId = (session.user as any).id;
+
+ // Users can only view their own settings
+ if (userId !== currentUserId) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+
+ const settings = getUserNotificationSettings(userId);
+
+ return NextResponse.json({ settings });
+ } catch (error) {
+ console.error('[GET /api/users/[id]/notification-settings]', error);
+ return NextResponse.json(
+ { error: 'Failed to get settings' },
+ { status: 500 }
+ );
+ }
+}
+
+// PATCH /api/users/[id]/notification-settings
+export async function PATCH(
+ request: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { id: userId } = await params;
+ const currentUserId = (session.user as any).id;
+
+ // Users can only update their own settings
+ if (userId !== currentUserId) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+
+ const body = await request.json();
+ const { email_enabled, telegram_enabled, telegram_chat_id } = body;
+
+ const settings = updateUserNotificationSettings(userId, {
+ email_enabled,
+ telegram_enabled,
+ telegram_chat_id,
+ });
+
+ return NextResponse.json({ settings });
+ } catch (error) {
+ console.error('[PATCH /api/users/[id]/notification-settings]', error);
+ return NextResponse.json(
+ { error: 'Failed to update settings' },
+ { status: 500 }
+ );
+ }
+}
+```
+
+**File:** `app/api/users/[id]/notification-settings/test/route.ts`
+
+```typescript
+import { NextResponse } from 'next/server';
+import { auth } from '@/lib/auth';
+import { telegramService } from '@/lib/telegram-service';
+import { getUserNotificationSettings } from '@/lib/notification-settings-db';
+
+// POST /api/users/[id]/notification-settings/test
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { id: userId } = await params;
+ const currentUserId = (session.user as any).id;
+
+ if (userId !== currentUserId) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ }
+
+ const settings = getUserNotificationSettings(userId);
+
+ if (!settings.telegram_chat_id) {
+ return NextResponse.json(
+ { error: 'No Telegram chat ID configured' },
+ { status: 400 }
+ );
+ }
+
+ const success = await telegramService.testConnection(
+ settings.telegram_chat_id
+ );
+
+ if (success) {
+ return NextResponse.json({ success: true, message: 'Test message sent' });
+ } else {
+ return NextResponse.json(
+ { error: 'Failed to send test message' },
+ { status: 500 }
+ );
+ }
+ } catch (error) {
+ console.error('[POST /api/users/[id]/notification-settings/test]', error);
+ return NextResponse.json(
+ { error: 'Failed to send test message' },
+ { status: 500 }
+ );
+ }
+}
+```
+
+### 6. Add Admin UI Page
+
+**File:** `app/admin/settings/notifications/page.tsx`
+
+```tsx
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useSession } from 'next-auth/react';
+
+export default function NotificationSettingsPage() {
+ const { data: session } = useSession();
+ const [settings, setSettings] = useState({
+ email_enabled: true,
+ telegram_enabled: false,
+ telegram_chat_id: '',
+ });
+ const [loading, setLoading] = useState(false);
+ const [message, setMessage] = useState('');
+
+ useEffect(() => {
+ if (session?.user) {
+ loadSettings();
+ }
+ }, [session]);
+
+ async function loadSettings() {
+ try {
+ const userId = (session!.user as any).id;
+ const res = await fetch(`/api/users/${userId}/notification-settings`);
+ const data = await res.json();
+ setSettings(data.settings);
+ } catch (error) {
+ console.error('Failed to load settings:', error);
+ }
+ }
+
+ async function handleSave() {
+ setLoading(true);
+ setMessage('');
+
+ try {
+ const userId = (session!.user as any).id;
+ const res = await fetch(`/api/users/${userId}/notification-settings`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(settings),
+ });
+
+ if (res.ok) {
+ setMessage('✅ Einstellungen gespeichert!');
+ } else {
+ setMessage('❌ Fehler beim Speichern');
+ }
+ } catch (error) {
+ setMessage('❌ Fehler beim Speichern');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleTest() {
+ setLoading(true);
+ setMessage('');
+
+ try {
+ const userId = (session!.user as any).id;
+ const res = await fetch(
+ `/api/users/${userId}/notification-settings/test`,
+ { method: 'POST' }
+ );
+
+ if (res.ok) {
+ setMessage('✅ Test-Nachricht gesendet!');
+ } else {
+ const data = await res.json();
+ setMessage(`❌ Fehler: ${data.error}`);
+ }
+ } catch (error) {
+ setMessage('❌ Fehler beim Senden der Test-Nachricht');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
Benachrichtigungseinstellungen
+
+
+ {/* Email Settings */}
+
+
+
+ Geofence-Ereignisse per E-Mail erhalten
+
+
+
+ {/* Telegram Settings */}
+
+
+
+ Geofence-Ereignisse per Telegram erhalten (inkl. Karte und Buttons)
+
+
+ {settings.telegram_enabled && (
+
+
+
+ setSettings({ ...settings, telegram_chat_id: e.target.value })
+ }
+ placeholder="z.B. 123456789"
+ className="w-full px-3 py-2 border border-gray-300 rounded-md"
+ />
+
+ Deine Telegram Chat ID findest du über @userinfobot
+
+
+ )}
+
+
+ {/* Action Buttons */}
+
+
+
+ {settings.telegram_enabled && settings.telegram_chat_id && (
+
+ )}
+
+
+ {/* Status Message */}
+ {message && (
+
{message}
+ )}
+
+
+ {/* Help Section */}
+
+
📖 Wie bekomme ich meine Chat ID?
+
+ - Öffne Telegram und suche nach @userinfobot
+ - Starte den Bot mit /start
+ - Der Bot sendet dir deine Chat ID
+ - Kopiere die Nummer und füge sie oben ein
+
+
+
+ );
+}
+```
+
+## 🔧 Environment Configuration
+
+Add to `.env` or `.env.local`:
+
+```env
+# Telegram Bot Configuration
+TELEGRAM_BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz
+```
+
+## 🧪 Testing
+
+### 1. Initialize Database
+
+```bash
+# Run the initialization script
+node scripts/init-notification-settings.js
+
+# Or add to main db init
+npm run db:init
+```
+
+### 2. Test Telegram Service
+
+Create `scripts/test-telegram.js`:
+
+```javascript
+#!/usr/bin/env node
+const { telegramService } = require('../lib/telegram-service');
+
+// Replace with your chat ID
+const CHAT_ID = '123456789';
+
+async function test() {
+ console.log('🧪 Testing Telegram service...\n');
+
+ try {
+ // Test 1: Simple message
+ console.log('1. Sending simple message...');
+ await telegramService.sendMessage({
+ chatId: CHAT_ID,
+ text: '✅ Test erfolgreich!\n\nDie Telegram-Integration funktioniert.',
+ });
+ console.log(' ✓ Message sent\n');
+
+ // Test 2: Location
+ console.log('2. Sending location...');
+ await telegramService.sendLocation(CHAT_ID, 52.520008, 13.404954);
+ console.log(' ✓ Location sent\n');
+
+ // Test 3: Full geofence notification
+ console.log('3. Sending geofence notification...');
+ await telegramService.sendGeofenceNotification({
+ chatId: CHAT_ID,
+ deviceName: 'Test Device',
+ geofenceName: 'Test Geofence',
+ eventType: 'enter',
+ latitude: 52.520008,
+ longitude: 13.404954,
+ timestamp: new Date().toISOString(),
+ mapUrl: 'https://example.com/map',
+ dashboardUrl: 'https://example.com/admin',
+ });
+ console.log(' ✓ Geofence notification sent\n');
+
+ console.log('✅ All tests passed!');
+ } catch (error) {
+ console.error('❌ Test failed:', error.message);
+ process.exit(1);
+ }
+}
+
+test();
+```
+
+Run it:
+
+```bash
+chmod +x scripts/test-telegram.js
+TELEGRAM_BOT_TOKEN=your_token node scripts/test-telegram.js
+```
+
+### 3. Test via Admin UI
+
+1. Navigate to `/admin/settings/notifications`
+2. Enable Telegram notifications
+3. Enter your Chat ID
+4. Click "Telegram Test"
+5. Check your Telegram for the test message
+
+## 📦 Package Dependencies
+
+Add to `package.json`:
+
+```json
+{
+ "dependencies": {
+ "axios": "^1.6.0"
+ }
+}
+```
+
+Install:
+
+```bash
+npm install axios
+```
+
+## 🎯 Example Notification Flow
+
+```
+1. Device enters geofence
+ ↓
+2. geofence-engine.ts detects event
+ ↓
+3. GeofenceEvent created in database
+ ↓
+4. geofence-notifications.ts triggered
+ ↓
+5. getUserNotificationSettings(owner_id)
+ ↓
+6. Parallel execution:
+ ├─→ sendEmailNotification (if enabled)
+ └─→ sendTelegramNotification (if enabled)
+ ↓
+ Telegram receives:
+ ├─→ Text message with emoji, device, geofence, time
+ ├─→ Inline buttons (map, dashboard)
+ └─→ Location pin on map
+```
+
+## 💡 Tips & Best Practices
+
+### Getting Chat ID
+
+Users can get their Telegram Chat ID:
+1. Search for `@userinfobot` in Telegram
+2. Send `/start`
+3. Bot replies with Chat ID
+
+### Bot Setup
+
+To create a Telegram Bot:
+1. Message `@BotFather` in Telegram
+2. Send `/newbot`
+3. Follow the prompts
+4. Save the token provided
+
+### Rate Limiting
+
+Telegram API has rate limits:
+- 30 messages per second per bot
+- 20 messages per minute per chat
+
+The current implementation sends 2 messages per geofence event (text + location), so you can handle ~15 events per minute per user.
+
+### Error Handling
+
+The implementation uses `Promise.allSettled()`, meaning:
+- If Telegram fails, Email still works (and vice versa)
+- At least one successful channel = event marked as "sent"
+- Both fail = event marked as "failed" with error details
+
+### Security
+
+- Bot token should be in environment variables, never committed to git
+- Chat IDs are user-specific and stored per user
+- Users can only configure their own notification settings (enforced by API)
+
+## 🔄 Migration Guide
+
+If you already have users, run this migration:
+
+```sql
+-- Add default settings for existing users
+INSERT INTO UserNotificationSettings (user_id, email_enabled, telegram_enabled)
+SELECT id, 1, 0 FROM User
+WHERE id NOT IN (SELECT user_id FROM UserNotificationSettings);
+```
+
+## 📊 Database Schema Reference
+
+```sql
+UserNotificationSettings
+├── user_id (TEXT, PK, FK → User.id)
+├── email_enabled (INTEGER, 0 or 1)
+├── telegram_enabled (INTEGER, 0 or 1)
+├── telegram_chat_id (TEXT, nullable)
+├── created_at (TEXT)
+└── updated_at (TEXT)
+
+Indexes:
+└── idx_user_notification_settings_user ON UserNotificationSettings(user_id)
+```
+
+## 🚀 Future Enhancements
+
+Possible extensions:
+- [ ] Push notifications (web push)
+- [ ] SMS notifications (Twilio)
+- [ ] Webhook notifications (custom endpoints)
+- [ ] Notification scheduling (quiet hours)
+- [ ] Notification grouping (batch multiple events)
+- [ ] Per-geofence notification settings
+- [ ] Rich Telegram messages with photos/videos
+
+## 📝 Changelog
+
+### Version 1.0 (Initial Implementation)
+- Basic Telegram notification support
+- Text messages with emoji and formatting
+- Location pins on map
+- Inline buttons for map and dashboard
+- User-configurable settings (Email, Telegram, both)
+- Test functionality via Admin UI
+- Parallel notification delivery
+- Robust error handling
+
+---
+
+**Author:** Claude Code
+**Last Updated:** 2025-12-04
diff --git a/lib/geofence-notifications.ts b/lib/geofence-notifications.ts
index 4ee29b0..065dcf4 100644
--- a/lib/geofence-notifications.ts
+++ b/lib/geofence-notifications.ts
@@ -1,15 +1,19 @@
/**
* Geofence Notification Service
- * Handles sending email notifications for geofence events
+ * Handles sending email and Telegram notifications for geofence events
*/
import { emailService } from './email-service';
+import { telegramService } from './telegram-service';
import {
renderGeofenceEnterEmail,
renderGeofenceExitEmail,
GeofenceEmailData,
} from './email-renderer';
import { geofenceDb } from './geofence-db';
+import {
+ getUserNotificationSettings,
+} from './notification-settings-db';
import type { GeofenceEvent } from './types';
import Database from 'better-sqlite3';
import path from 'path';
@@ -62,6 +66,7 @@ function getUser(userId: string): UserInfo | null {
/**
* Send notification for a single geofence event
+ * Supports both Email and Telegram based on user preferences
*/
async function sendEventNotification(event: GeofenceEvent): Promise {
try {
@@ -79,13 +84,87 @@ async function sendEventNotification(event: GeofenceEvent): Promise {
// 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)
+ if (!owner) {
+ throw new Error(`Owner not found: ${geofence.owner_id}`);
+ }
+
+ // Get notification preferences
+ const settings = getUserNotificationSettings(owner.id);
+
+ // Prepare notification tasks
+ const promises: Promise[] = [];
+
+ // Send Email if enabled
+ if (settings.email_enabled && owner.email) {
+ promises.push(
+ sendEmailNotification(event, owner, device, geofence)
+ );
+ }
+
+ // Send Telegram if enabled
+ if (settings.telegram_enabled && settings.telegram_chat_id) {
+ promises.push(
+ sendTelegramNotification(
+ event,
+ owner,
+ device,
+ geofence,
+ settings.telegram_chat_id
+ )
+ );
+ }
+
+ // If no notification channel enabled, skip
+ if (promises.length === 0) {
+ console.log(
+ `[GeofenceNotification] No notification channels enabled for user ${owner.id}`
+ );
+ geofenceDb.markNotificationSent(event.id!, true);
return;
}
- // Prepare email data
+ // Send notifications in parallel
+ const results = await Promise.allSettled(promises);
+
+ // Check if at least one succeeded
+ const anySuccess = results.some((r) => r.status === 'fulfilled');
+
+ if (anySuccess) {
+ geofenceDb.markNotificationSent(event.id!, true);
+ console.log(
+ `[GeofenceNotification] Sent notification for geofence ${geofence.name}`
+ );
+ } else {
+ // All failed
+ const errors = results
+ .filter((r) => r.status === 'rejected')
+ .map((r: any) => r.reason.message)
+ .join('; ');
+
+ geofenceDb.markNotificationSent(event.id!, false, errors);
+ throw new Error(`All notification channels failed: ${errors}`);
+ }
+ } catch (error) {
+ console.error('[GeofenceNotification] Failed to send notification:', error);
+
+ const errorMessage =
+ error instanceof Error ? error.message : 'Unknown error';
+ geofenceDb.markNotificationSent(event.id!, false, errorMessage);
+
+ throw error;
+ }
+}
+
+/**
+ * Send email notification (extracted from main function)
+ */
+async function sendEmailNotification(
+ event: GeofenceEvent,
+ owner: UserInfo,
+ device: DeviceInfo,
+ geofence: GeofenceInfo
+): Promise {
+ try {
const emailData: GeofenceEmailData = {
username: owner.username,
deviceName: device.name,
@@ -94,11 +173,8 @@ async function sendEventNotification(event: GeofenceEvent): Promise {
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;
@@ -110,20 +186,51 @@ async function sendEventNotification(event: GeofenceEvent): Promise {
subject = `${device.name} hat ${geofence.name} verlassen`;
}
- // Send via existing email service
- await emailService['sendEmail'](owner.email, subject, html);
+ 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}`);
+ console.log(
+ `[GeofenceNotification] Sent email to ${owner.email}`
+ );
} catch (error) {
- console.error('[GeofenceNotification] Failed to send notification:', error);
+ console.error('[GeofenceNotification] Email error:', error);
+ throw error;
+ }
+}
- // Mark notification as failed
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
- geofenceDb.markNotificationSent(event.id!, false, errorMessage);
+/**
+ * Send Telegram notification (new function)
+ */
+async function sendTelegramNotification(
+ event: GeofenceEvent,
+ owner: UserInfo,
+ device: DeviceInfo,
+ geofence: GeofenceInfo,
+ chatId: string
+): Promise {
+ try {
+ const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
+ // Ensure latitude and longitude are numbers
+ const lat = typeof event.latitude === 'number' ? event.latitude : parseFloat(event.latitude);
+ const lon = typeof event.longitude === 'number' ? event.longitude : parseFloat(event.longitude);
+
+ await telegramService.sendGeofenceNotification({
+ chatId,
+ deviceName: device.name,
+ geofenceName: geofence.name,
+ eventType: event.event_type,
+ latitude: lat,
+ longitude: lon,
+ timestamp: event.timestamp,
+ mapUrl: `${baseUrl}/map?lat=${lat}&lon=${lon}`,
+ dashboardUrl: `${baseUrl}/admin/geofences/events`,
+ });
+
+ console.log(
+ `[GeofenceNotification] Sent Telegram notification to ${chatId}`
+ );
+ } catch (error) {
+ console.error('[GeofenceNotification] Telegram error:', error);
throw error;
}
}
diff --git a/lib/notification-settings-db.ts b/lib/notification-settings-db.ts
new file mode 100644
index 0000000..959f4a2
--- /dev/null
+++ b/lib/notification-settings-db.ts
@@ -0,0 +1,157 @@
+import Database from 'better-sqlite3';
+import path from 'path';
+
+const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
+
+export interface UserNotificationSettings {
+ user_id: string;
+ email_enabled: boolean;
+ telegram_enabled: boolean;
+ telegram_chat_id: string | null;
+ created_at?: string;
+ updated_at?: string;
+}
+
+/**
+ * Get notification settings for a user
+ * Returns default settings if not found
+ */
+export function getUserNotificationSettings(
+ userId: string
+): UserNotificationSettings {
+ const db = new Database(dbPath);
+ try {
+ const stmt = db.prepare(`
+ SELECT
+ user_id,
+ email_enabled,
+ telegram_enabled,
+ telegram_chat_id,
+ created_at,
+ updated_at
+ FROM UserNotificationSettings
+ WHERE user_id = ?
+ `);
+
+ const result = stmt.get(userId) as any;
+
+ // Return defaults if no settings found
+ if (!result) {
+ return {
+ user_id: userId,
+ email_enabled: true,
+ telegram_enabled: false,
+ telegram_chat_id: null,
+ };
+ }
+
+ // Convert SQLite integers to booleans
+ return {
+ user_id: result.user_id,
+ email_enabled: result.email_enabled === 1,
+ telegram_enabled: result.telegram_enabled === 1,
+ telegram_chat_id: result.telegram_chat_id,
+ created_at: result.created_at,
+ updated_at: result.updated_at,
+ };
+ } finally {
+ db.close();
+ }
+}
+
+/**
+ * Update notification settings for a user
+ */
+export function updateUserNotificationSettings(
+ userId: string,
+ settings: Partial
+): UserNotificationSettings {
+ const db = new Database(dbPath);
+ try {
+ // Check if settings exist
+ const existing = db
+ .prepare('SELECT user_id FROM UserNotificationSettings WHERE user_id = ?')
+ .get(userId);
+
+ if (existing) {
+ // Update existing
+ const updates: string[] = [];
+ const values: any[] = [];
+
+ if (settings.email_enabled !== undefined) {
+ updates.push('email_enabled = ?');
+ values.push(settings.email_enabled ? 1 : 0);
+ }
+ if (settings.telegram_enabled !== undefined) {
+ updates.push('telegram_enabled = ?');
+ values.push(settings.telegram_enabled ? 1 : 0);
+ }
+ if (settings.telegram_chat_id !== undefined) {
+ updates.push('telegram_chat_id = ?');
+ values.push(settings.telegram_chat_id);
+ }
+
+ updates.push('updated_at = datetime(\'now\')');
+ values.push(userId);
+
+ const stmt = db.prepare(`
+ UPDATE UserNotificationSettings
+ SET ${updates.join(', ')}
+ WHERE user_id = ?
+ `);
+
+ stmt.run(...values);
+ } else {
+ // Insert new
+ const stmt = db.prepare(`
+ INSERT INTO UserNotificationSettings (
+ user_id,
+ email_enabled,
+ telegram_enabled,
+ telegram_chat_id
+ ) VALUES (?, ?, ?, ?)
+ `);
+
+ stmt.run(
+ userId,
+ settings.email_enabled !== undefined ? (settings.email_enabled ? 1 : 0) : 1,
+ settings.telegram_enabled !== undefined ? (settings.telegram_enabled ? 1 : 0) : 0,
+ settings.telegram_chat_id ?? null
+ );
+ }
+
+ return getUserNotificationSettings(userId);
+ } finally {
+ db.close();
+ }
+}
+
+/**
+ * Initialize notification settings table
+ */
+export function initNotificationSettingsTable(): void {
+ const db = new Database(dbPath);
+ try {
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS UserNotificationSettings (
+ user_id TEXT PRIMARY KEY,
+ email_enabled INTEGER DEFAULT 1,
+ telegram_enabled INTEGER DEFAULT 0,
+ telegram_chat_id TEXT,
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now')),
+
+ FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE,
+ CHECK (email_enabled IN (0, 1)),
+ CHECK (telegram_enabled IN (0, 1))
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_user_notification_settings_user
+ ON UserNotificationSettings(user_id);
+ `);
+
+ console.log('✓ UserNotificationSettings table initialized');
+ } finally {
+ db.close();
+ }
+}
diff --git a/lib/telegram-service.ts b/lib/telegram-service.ts
new file mode 100644
index 0000000..ef2e60d
--- /dev/null
+++ b/lib/telegram-service.ts
@@ -0,0 +1,170 @@
+import axios from 'axios';
+
+interface TelegramMessage {
+ chatId: string;
+ text: string;
+ latitude?: number;
+ longitude?: number;
+ buttons?: Array<{ text: string; url: string }>;
+}
+
+interface GeofenceNotificationParams {
+ chatId: string;
+ deviceName: string;
+ geofenceName: string;
+ eventType: 'enter' | 'exit';
+ latitude: number;
+ longitude: number;
+ timestamp: string;
+ mapUrl?: string;
+ dashboardUrl?: string;
+}
+
+class TelegramService {
+ private botToken: string;
+ private baseUrl: string;
+
+ constructor() {
+ this.botToken = process.env.TELEGRAM_BOT_TOKEN || '';
+ if (!this.botToken) {
+ console.warn('[TelegramService] No TELEGRAM_BOT_TOKEN configured');
+ }
+ this.baseUrl = `https://api.telegram.org/bot${this.botToken}`;
+ }
+
+ /**
+ * Check if Telegram is configured
+ */
+ isConfigured(): boolean {
+ return !!this.botToken;
+ }
+
+ /**
+ * Send text message with optional inline buttons
+ */
+ async sendMessage(params: TelegramMessage): Promise {
+ if (!this.isConfigured()) {
+ throw new Error('Telegram bot token not configured');
+ }
+
+ const { chatId, text, buttons } = params;
+
+ const payload: any = {
+ chat_id: chatId,
+ text: text,
+ parse_mode: 'HTML',
+ };
+
+ // Add inline buttons if provided
+ if (buttons && buttons.length > 0) {
+ payload.reply_markup = {
+ inline_keyboard: [
+ buttons.map(btn => ({
+ text: btn.text,
+ url: btn.url,
+ })),
+ ],
+ };
+ }
+
+ const response = await axios.post(`${this.baseUrl}/sendMessage`, payload);
+
+ if (!response.data.ok) {
+ throw new Error(`Telegram API error: ${response.data.description}`);
+ }
+ }
+
+ /**
+ * Send location pin on map
+ */
+ async sendLocation(
+ chatId: string,
+ latitude: number,
+ longitude: number
+ ): Promise {
+ if (!this.isConfigured()) {
+ throw new Error('Telegram bot token not configured');
+ }
+
+ const response = await axios.post(`${this.baseUrl}/sendLocation`, {
+ chat_id: chatId,
+ latitude,
+ longitude,
+ });
+
+ if (!response.data.ok) {
+ throw new Error(`Telegram API error: ${response.data.description}`);
+ }
+ }
+
+ /**
+ * Send geofence notification (complete with text + location + buttons)
+ */
+ async sendGeofenceNotification(
+ params: GeofenceNotificationParams
+ ): Promise {
+ const {
+ chatId,
+ deviceName,
+ geofenceName,
+ eventType,
+ latitude,
+ longitude,
+ timestamp,
+ mapUrl,
+ dashboardUrl,
+ } = params;
+
+ // Format message
+ const emoji = eventType === 'enter' ? '🟢' : '🔴';
+ const action = eventType === 'enter' ? 'BETRETEN' : 'VERLASSEN';
+ const verb = eventType === 'enter' ? 'betreten' : 'verlassen';
+
+ const formattedDate = new Date(timestamp).toLocaleString('de-DE', {
+ dateStyle: 'short',
+ timeStyle: 'short',
+ });
+
+ const text = `
+${emoji} Geofence ${action}
+
+📱 Device: ${deviceName}
+📍 Geofence: ${geofenceName}
+🕐 Zeit: ${formattedDate}
+📊 Ereignis: Hat ${geofenceName} ${verb}
+ `.trim();
+
+ // Prepare inline buttons
+ const buttons = [];
+ if (mapUrl) {
+ buttons.push({ text: '🗺️ Auf Karte zeigen', url: mapUrl });
+ }
+ if (dashboardUrl) {
+ buttons.push({ text: '📊 Dashboard öffnen', url: dashboardUrl });
+ }
+
+ // Send text message with buttons
+ await this.sendMessage({ chatId, text, buttons });
+
+ // Send location pin
+ await this.sendLocation(chatId, latitude, longitude);
+ }
+
+ /**
+ * Test connection by sending a simple message
+ */
+ async testConnection(chatId: string): Promise {
+ try {
+ await this.sendMessage({
+ chatId,
+ text: '✅ Telegram Connection Test\n\nDie Verbindung funktioniert!',
+ });
+ return true;
+ } catch (error) {
+ console.error('[TelegramService] Test failed:', error);
+ return false;
+ }
+ }
+}
+
+export const telegramService = new TelegramService();
diff --git a/package-lock.json b/package-lock.json
index e79fbe2..e6c9c19 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.4",
"@types/react-dom": "^19.2.3",
+ "axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.4.1",
"leaflet": "^1.9.4",
@@ -23,6 +24,7 @@
"next": "^16.0.7",
"next-auth": "^5.0.0-beta.30",
"nodemailer": "^7.0.10",
+ "qrcode": "^1.5.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-email": "^4.3.2",
@@ -3416,6 +3418,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/autoprefixer": {
"version": "10.4.22",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
@@ -3454,6 +3462,17 @@
"postcss": "^8.1.0"
}
},
+ "node_modules/axios": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3619,6 +3638,28 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001754",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
@@ -3714,6 +3755,87 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3732,6 +3854,18 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
@@ -3848,6 +3982,15 @@
}
}
},
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -3881,6 +4024,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3890,6 +4042,12 @@
"node": ">=8"
}
},
+ "node_modules/dijkstrajs": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -3945,6 +4103,20 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -4044,6 +4216,51 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -4172,6 +4389,39 @@
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -4188,6 +4438,43 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -4208,6 +4495,24 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
"node_modules/get-east-asian-width": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
@@ -4220,6 +4525,43 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -4249,12 +4591,63 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
@@ -4722,6 +5115,18 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/log-symbols": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
@@ -4768,6 +5173,15 @@
"node": ">= 18"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@@ -5352,6 +5766,42 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -5371,6 +5821,15 @@
"url": "https://ko-fi.com/killymxi"
}
},
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -5428,6 +5887,15 @@
"pathe": "^2.0.3"
}
},
+ "node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -5560,6 +6028,12 @@
"node": ">= 6"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -5570,6 +6044,23 @@
"once": "^1.3.1"
}
},
+ "node_modules/qrcode": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -5695,6 +6186,21 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
@@ -5767,6 +6273,12 @@
"node": ">=10"
}
},
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -6385,6 +6897,12 @@
"node": ">= 8"
}
},
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
"node_modules/worker-factory": {
"version": "7.0.46",
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.46.tgz",
@@ -6550,6 +7068,88 @@
}
}
},
+ "node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/yoctocolors": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
diff --git a/package.json b/package.json
index dc35c18..8cd2f62 100644
--- a/package.json
+++ b/package.json
@@ -8,10 +8,11 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
- "db:init": "node scripts/init-database.js && node scripts/init-locations-db.js && node scripts/init-geofence-db.js",
+ "db:init": "node scripts/init-database.js && node scripts/init-locations-db.js && node scripts/init-geofence-db.js && node scripts/init-notification-settings.js",
"db:init:app": "node scripts/init-database.js",
"db:init:locations": "node scripts/init-locations-db.js",
"db:init:geofence": "node scripts/init-geofence-db.js",
+ "db:init:notifications": "node scripts/init-notification-settings.js",
"db:cleanup": "node scripts/cleanup-old-locations.js",
"db:cleanup:7d": "node scripts/cleanup-old-locations.js 168",
"db:cleanup:30d": "node scripts/cleanup-old-locations.js 720",
@@ -21,7 +22,8 @@
"test:geofence:mqtt": "node scripts/test-mqtt-geofence.js",
"docs:check": "node scripts/check-docs.js",
"docs:sync": "node scripts/sync-docs.js",
- "email:dev": "email dev"
+ "email:dev": "email dev",
+ "telegram:qr": "node scripts/generate-telegram-qr.js"
},
"keywords": [],
"author": "",
@@ -34,6 +36,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.4",
"@types/react-dom": "^19.2.3",
+ "axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.4.1",
"leaflet": "^1.9.4",
@@ -41,6 +44,7 @@
"next": "^16.0.7",
"next-auth": "^5.0.0-beta.30",
"nodemailer": "^7.0.10",
+ "qrcode": "^1.5.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-email": "^4.3.2",
diff --git a/public/telegram-bot-qr.png b/public/telegram-bot-qr.png
new file mode 100644
index 0000000..91d9aad
Binary files /dev/null and b/public/telegram-bot-qr.png differ
diff --git a/scripts/generate-telegram-qr.js b/scripts/generate-telegram-qr.js
new file mode 100755
index 0000000..ccec32b
--- /dev/null
+++ b/scripts/generate-telegram-qr.js
@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+/**
+ * Generate QR Code for Telegram Bot
+ */
+
+const QRCode = require('qrcode');
+const fs = require('fs');
+const path = require('path');
+
+const BOT_URL = 'https://t.me/MeinTracking_bot';
+const OUTPUT_DIR = path.join(process.cwd(), 'public');
+const OUTPUT_FILE = path.join(OUTPUT_DIR, 'telegram-bot-qr.png');
+
+async function generateQR() {
+ try {
+ // Ensure public directory exists
+ if (!fs.existsSync(OUTPUT_DIR)) {
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
+ }
+
+ // Generate QR Code
+ await QRCode.toFile(OUTPUT_FILE, BOT_URL, {
+ width: 400,
+ margin: 2,
+ color: {
+ dark: '#000000',
+ light: '#FFFFFF'
+ }
+ });
+
+ console.log(`✅ QR Code generated: ${OUTPUT_FILE}`);
+ console.log(`📱 Bot URL: ${BOT_URL}`);
+ console.log(`🖼️ QR Code size: 400x400px`);
+ } catch (error) {
+ console.error('❌ Error generating QR Code:', error.message);
+ process.exit(1);
+ }
+}
+
+generateQR();
diff --git a/scripts/init-notification-settings.js b/scripts/init-notification-settings.js
new file mode 100755
index 0000000..b27f17a
--- /dev/null
+++ b/scripts/init-notification-settings.js
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+/**
+ * Initialize UserNotificationSettings table
+ */
+
+const Database = require('better-sqlite3');
+const path = require('path');
+
+const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
+
+try {
+ const db = new Database(dbPath);
+
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS UserNotificationSettings (
+ user_id TEXT PRIMARY KEY,
+ email_enabled INTEGER DEFAULT 1,
+ telegram_enabled INTEGER DEFAULT 0,
+ telegram_chat_id TEXT,
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now')),
+
+ FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE,
+ CHECK (email_enabled IN (0, 1)),
+ CHECK (telegram_enabled IN (0, 1))
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_user_notification_settings_user
+ ON UserNotificationSettings(user_id);
+ `);
+
+ db.close();
+
+ console.log('✅ Notification settings table initialized!');
+} catch (error) {
+ console.error('❌ Error:', error.message);
+ process.exit(1);
+}
diff --git a/scripts/test-telegram-token.js b/scripts/test-telegram-token.js
new file mode 100755
index 0000000..945ff53
--- /dev/null
+++ b/scripts/test-telegram-token.js
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+/**
+ * Test Telegram Bot Token
+ */
+
+const axios = require('axios');
+const fs = require('fs');
+const path = require('path');
+
+// Read .env manually
+const envPath = path.join(process.cwd(), '.env');
+const envContent = fs.readFileSync(envPath, 'utf-8');
+const BOT_TOKEN = envContent
+ .split('\n')
+ .find(line => line.startsWith('TELEGRAM_BOT_TOKEN='))
+ ?.split('=')[1]
+ ?.trim();
+
+async function testToken() {
+ if (!BOT_TOKEN) {
+ console.error('❌ TELEGRAM_BOT_TOKEN not found in .env');
+ process.exit(1);
+ }
+
+ console.log('🧪 Testing Telegram Bot Token...\n');
+ console.log(`Token: ${BOT_TOKEN.substring(0, 15)}...`);
+
+ try {
+ const response = await axios.get(`https://api.telegram.org/bot${BOT_TOKEN}/getMe`);
+
+ if (response.data.ok) {
+ const bot = response.data.result;
+ console.log('\n✅ Bot Token is VALID!\n');
+ console.log('📱 Bot Information:');
+ console.log(` ID: ${bot.id}`);
+ console.log(` Name: ${bot.first_name}`);
+ console.log(` Username: @${bot.username}`);
+ console.log(` Can Join Groups: ${bot.can_join_groups}`);
+ console.log(` Can Read Messages: ${bot.can_read_all_group_messages}`);
+ console.log('\n🔗 Bot Link: https://t.me/' + bot.username);
+ } else {
+ console.error('❌ Invalid response:', response.data);
+ process.exit(1);
+ }
+ } catch (error) {
+ console.error('\n❌ Token test FAILED!');
+ if (error.response) {
+ console.error('Error:', error.response.data.description);
+ } else {
+ console.error('Error:', error.message);
+ }
+ process.exit(1);
+ }
+}
+
+testToken();