diff --git a/src/app/api/telegram/test/route.ts b/src/app/api/telegram/test/route.ts
new file mode 100644
index 0000000..1036080
--- /dev/null
+++ b/src/app/api/telegram/test/route.ts
@@ -0,0 +1,46 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { sendTelegramMessage } from '@/lib/telegram';
+
+export const runtime = 'nodejs';
+export const dynamic = 'force-dynamic';
+
+export async function POST(req: NextRequest) {
+ try {
+ const botToken = process.env.TELEGRAM_BOT_TOKEN;
+ const chatId = process.env.TELEGRAM_CHAT_ID;
+
+ if (!botToken || !chatId) {
+ return NextResponse.json({
+ success: false,
+ error: 'TELEGRAM_BOT_TOKEN oder TELEGRAM_CHAT_ID ist in .env nicht gesetzt.',
+ configured: false,
+ }, { status: 400 });
+ }
+
+ const testMessage = `🤖 Telegram-Benachrichtigungstest
+
+✅ Die Verbindung zwischen deiner Travel Tracker App und dem Telegram Bot funktioniert einwandfrei!
+
+🔔 Sobald ein Hotelpreis sinkt oder ein neuer Bestpreis erreicht wird, erhältst du hier automatisch Push-Nachrichten.`;
+
+ const res = await sendTelegramMessage(testMessage);
+
+ if (res.success) {
+ return NextResponse.json({
+ success: true,
+ message: 'Test-Nachricht wurde erfolgreich an Telegram gesendet!',
+ chatId: chatId.slice(0, 4) + '***',
+ });
+ } else {
+ return NextResponse.json({
+ success: false,
+ error: res.error || 'Fehler beim Senden an Telegram API',
+ }, { status: 500 });
+ }
+ } catch (error: any) {
+ return NextResponse.json({
+ success: false,
+ error: error.message,
+ }, { status: 500 });
+ }
+}
diff --git a/src/app/api/webhooks/scraper/route.ts b/src/app/api/webhooks/scraper/route.ts
index c6ce98b..77890d8 100644
--- a/src/app/api/webhooks/scraper/route.ts
+++ b/src/app/api/webhooks/scraper/route.ts
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
+import { notifyPriceChange } from '@/lib/telegram';
function parseGermanPrice(priceStr: any): number | null {
if (!priceStr) return null;
@@ -42,11 +43,52 @@ export async function POST(req: NextRequest) {
if (numericPrice !== null && numericPrice > 0) {
try {
+ // Vorherigen Preis und bisherigen Allzeit-Tiefstpreis abfragen
+ const prevRes = await query(
+ `SELECT
+ h.name, h.booking_url, h.checkin_date, h.checkout_date,
+ t.name as trip_name,
+ (SELECT price_numeric FROM hotel_price_history WHERE hotel_id = h.id ORDER BY tracked_at DESC, id DESC LIMIT 1) as last_price,
+ (SELECT MIN(price_numeric) FROM hotel_price_history WHERE hotel_id = h.id AND price_numeric > 0) as min_price
+ FROM hotels h
+ LEFT JOIN trips t ON h.trip_id = t.id
+ WHERE h.id = $1::uuid`,
+ [hotelId]
+ );
+
+ const hotelRecord = prevRes.rows[0] || null;
+ const previousPrice = hotelRecord?.last_price ? Math.round(Number(hotelRecord.last_price)) : null;
+ const allTimeBestPrice = hotelRecord?.min_price ? Math.round(Number(hotelRecord.min_price)) : null;
+
await query(
`INSERT INTO hotel_price_history (hotel_id, price_numeric, genius_price_numeric, price_raw, currency, nights, persons, tracked_at)
- VALUES ($1::uuid, $2, $3, $4, 'EUR', $5, $6, NOW())`,
+ VALUES ($1::uuid, $2, $3, $4, 'EUR', $5, $6, NOW())`,
[hotelId, numericPrice, geniusPriceNumeric, String(body.price || ''), body.nights || 1, body.persons || 2]
);
+
+ // Telegram Push-Benachrichtigung bei Preissenkung oder neuem Allzeit-Bestpreis
+ if (hotelRecord && previousPrice !== null && previousPrice > 0) {
+ const isPriceDrop = numericPrice < previousPrice;
+ const isNewBestPrice = allTimeBestPrice !== null ? (numericPrice < allTimeBestPrice) : true;
+
+ if (isPriceDrop || isNewBestPrice) {
+ notifyPriceChange({
+ hotelName: hotelRecord.name || body.hotelName || 'Hotel',
+ tripName: hotelRecord.trip_name || null,
+ oldPrice: previousPrice,
+ newPrice: numericPrice,
+ newGeniusPrice: geniusPriceNumeric,
+ allTimeBestPrice: allTimeBestPrice,
+ isNewBestPrice,
+ isPriceDrop,
+ checkinDate: hotelRecord.checkin_date || body.checkin || null,
+ checkoutDate: hotelRecord.checkout_date || body.checkout || null,
+ nights: body.nights || null,
+ persons: body.persons || null,
+ bookingUrl: hotelRecord.booking_url || body.url || ''
+ }).catch(e => console.error('[Telegram] Fehler beim Senden der Benachrichtigung:', e.message));
+ }
+ }
} catch (err: any) {
console.error('DB Insert Error (hotel_price_history):', err.message);
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 31962cb..8dd2069 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -136,6 +136,26 @@ export default function TravelDashboard() {
// Preishistorie Zeitraum Filter (7 Tage vs Gesamter Verlauf)
const [priceHistoryRange, setPriceHistoryRange] = useState<'7d' | 'all'>('7d');
+ // Telegram Test Status
+ const [isTestingTelegram, setIsTestingTelegram] = useState(false);
+
+ const testTelegramNotification = async () => {
+ setIsTestingTelegram(true);
+ try {
+ const res = await fetch('/api/telegram/test', { method: 'POST' });
+ const data = await res.json();
+ if (data.success) {
+ alert('✅ Telegram-Test erfolgreich! Nachricht wurde an deinen Chat gesendet.');
+ } else {
+ alert(`❌ Telegram-Test fehlgeschlagen:\n${data.error || 'Unbekannter Fehler'}\n\nBitte prüfe TELEGRAM_BOT_TOKEN und TELEGRAM_CHAT_ID in deiner .env Datei.`);
+ }
+ } catch (err: any) {
+ alert('Fehler beim Testen von Telegram: ' + err.message);
+ } finally {
+ setIsTestingTelegram(false);
+ }
+ };
+
// Scraper Debug-Modus Toggle
const [isDebugMode, setIsDebugMode] = useState(false);
@@ -920,6 +940,32 @@ export default function TravelDashboard() {
{isDebugMode ? 'AN' : 'AUS'}
+
+ {/* TELEGRAM TEST BUTTON */}
+
diff --git a/src/lib/telegram.ts b/src/lib/telegram.ts
new file mode 100644
index 0000000..653ad64
--- /dev/null
+++ b/src/lib/telegram.ts
@@ -0,0 +1,111 @@
+export interface PriceNotificationPayload {
+ hotelName: string;
+ tripName?: string | null;
+ oldPrice: number | null;
+ newPrice: number;
+ newGeniusPrice?: number | null;
+ allTimeBestPrice?: number | null;
+ isNewBestPrice: boolean;
+ isPriceDrop: boolean;
+ checkinDate?: string | null;
+ checkoutDate?: string | null;
+ nights?: number | null;
+ persons?: number | null;
+ bookingUrl: string;
+}
+
+export async function sendTelegramMessage(htmlText: string): Promise<{ success: boolean; error?: string }> {
+ const botToken = process.env.TELEGRAM_BOT_TOKEN;
+ const chatId = process.env.TELEGRAM_CHAT_ID;
+
+ if (!botToken || !chatId) {
+ console.log('[Telegram] Keine TELEGRAM_BOT_TOKEN oder TELEGRAM_CHAT_ID in .env gesetzt – überspringe Benachrichtigung.');
+ return { success: false, error: 'Telegram Bot Token oder Chat ID nicht konfiguriert.' };
+ }
+
+ try {
+ const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ chat_id: chatId,
+ text: htmlText,
+ parse_mode: 'HTML',
+ disable_web_page_preview: false,
+ }),
+ });
+
+ const data = await response.json();
+ if (!data.ok) {
+ console.error('[Telegram] API Fehler:', data.description);
+ return { success: false, error: data.description };
+ }
+
+ console.log('[Telegram] ✅ Nachricht erfolgreich versendet!');
+ return { success: true };
+ } catch (err: any) {
+ console.error('[Telegram] Netzwerk- oder Request-Fehler:', err.message);
+ return { success: false, error: err.message };
+ }
+}
+
+export async function notifyPriceChange(payload: PriceNotificationPayload): Promise<{ success: boolean; error?: string }> {
+ const {
+ hotelName,
+ tripName,
+ oldPrice,
+ newPrice,
+ newGeniusPrice,
+ isNewBestPrice,
+ isPriceDrop,
+ checkinDate,
+ checkoutDate,
+ nights,
+ persons,
+ bookingUrl
+ } = payload;
+
+ let badge = '📉 PREIS GESUNKEN!';
+ if (isNewBestPrice) {
+ badge = '🔥 NEUER ALLZEIT-TIEFSTPREIS!';
+ }
+
+ let deltaStr = '';
+ if (oldPrice !== null && oldPrice > 0 && oldPrice !== newPrice) {
+ const diff = newPrice - oldPrice;
+ const pct = Math.round(((oldPrice - newPrice) / oldPrice) * 100);
+ deltaStr = `\n📉 Ersparnis: ${Math.abs(diff)} € (${pct}% günstiger! Vorher: ${oldPrice} €)`;
+ }
+
+ let geniusStr = '';
+ if (newGeniusPrice && newGeniusPrice > 0) {
+ geniusStr = `\n💙 Genius-Preis (-10%): ${newGeniusPrice} €`;
+ }
+
+ let dateStr = '';
+ if (checkinDate && checkoutDate) {
+ const formatDate = (d: string) => {
+ try {
+ return new Date(d).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
+ } catch {
+ return d;
+ }
+ };
+ dateStr = `\n🗓️ Reisezeitraum: ${formatDate(checkinDate)} – ${formatDate(checkoutDate)}`;
+ if (nights) {
+ dateStr += ` (${nights} ${nights === 1 ? 'Nacht' : 'Nächte'}${persons ? ` · ${persons} Pers.` : ''})`;
+ }
+ }
+
+ const tripStr = tripName ? `\n🌍 Reise: ${tripName}` : '';
+
+ const message = `${badge}
+
+🏨 ${hotelName}${tripStr}
+💰 Neuer Preis: ${newPrice} €${geniusStr}${deltaStr}${dateStr}
+
+👉 Direkt zu Booking.com öffnen ↗`;
+
+ return sendTelegramMessage(message);
+}