Nichtbetrachten und Daten zusammen

This commit is contained in:
2026-08-16 07:55:51 +00:00
parent 550da31038
commit 90a7d8d3d2
10 changed files with 2415 additions and 489 deletions

80
copyChat.sh Executable file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bash
OUTPUT_FILE="${1:-llm_context.txt}"
ROOT_DIR="${2:-.}"
# Absolute Pfade auflösen
OUTPUT_PATH="$(cd "$(dirname "$OUTPUT_FILE")" 2>/dev/null && pwd)/$(basename "$OUTPUT_FILE")"
cd "$ROOT_DIR" || exit 1
# Ausgabedatei initialisieren / leeren
> "$OUTPUT_PATH"
echo "========================================" >> "$OUTPUT_PATH"
echo "PROJEKTSTRUKTUR" >> "$OUTPUT_PATH"
echo "========================================" >> "$OUTPUT_PATH"
# Prüfen, ob Git verfügbar und das Verzeichnis ein Git-Repo ist
IS_GIT_REPO=false
if command -v git &> /dev/null && git rev-parse --is-inside-work-tree &> /dev/null; then
IS_GIT_REPO=true
fi
if [ "$IS_GIT_REPO" = true ]; then
echo "(Ermittelt via Git unter Berücksichtigung von .gitignore)" >> "$OUTPUT_PATH"
# Zeigt alle getrackten + ungetrackten Dateien an, ignoriert .gitignore-Treffer
git ls-files -co --exclude-standard >> "$OUTPUT_PATH"
else
if command -v tree &> /dev/null; then
tree -I 'node_modules|.git|venv|.venv|__pycache__|dist|build|.next|target' >> "$OUTPUT_PATH"
else
find . -maxdepth 3 -not -path '*/.*' -not -path '*/node_modules*' -not -path '*/venv*' >> "$OUTPUT_PATH"
fi
fi
echo -e "\n========================================" >> "$OUTPUT_PATH"
echo "DATEIINHALTE" >> "$OUTPUT_PATH"
echo "========================================" >> "$OUTPUT_PATH"
# Dateiliste ermitteln
get_file_list() {
if [ "$IS_GIT_REPO" = true ]; then
# -c = cached/tracked, -o = others/untracked, --exclude-standard = .gitignore beachten
git ls-files -co --exclude-standard
else
find . -type f \
-not -path '*/.git/*' \
-not -path '*/node_modules/*' \
-not -path '*/venv/*' \
-not -path '*/.venv/*' \
-not -path '*/__pycache__/*' \
-not -path '*/dist/*' \
-not -path '*/build/*' \
-not -path '*/.next/*'
fi
}
# Dateien verarbeiten
get_file_list | sort | while IFS= read -r file; do
# Überspringe leere Pfade, die Zieldatei selbst oder typische Binär-Endungen
[ -z "$file" ] && continue
[ "$file" = "$OUTPUT_FILE" ] && continue
[ "$(realpath "$file" 2>/dev/null)" = "$OUTPUT_PATH" ] && continue
case "$file" in
*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.zip|*.tar.gz|*.lock|package-lock.json|yarn.lock)
continue
;;
esac
# Prüfen, ob die Datei existiert und lesbarer Text ist
if [ -f "$file" ] && (grep -Iq . "$file" 2>/dev/null || [ ! -s "$file" ]); then
echo -e "\n--- DATEI: $file ---" >> "$OUTPUT_PATH"
echo '```' >> "$OUTPUT_PATH"
cat "$file" >> "$OUTPUT_PATH"
echo -e "\n"'```' >> "$OUTPUT_PATH"
fi
done
echo "Fertig! Projektkontext liegt in: $OUTPUT_PATH"

1404
llm_context.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
export const dynamic = 'force-dynamic';
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
try {
const { id } = params;
const res = await query(
`SELECT
id,
price_numeric::float AS price,
price_per_night::float AS price_per_night,
price_per_person::float AS price_per_person,
nights,
persons,
tracked_at AS created_at
FROM hotel_price_history
WHERE hotel_id::text = $1::text
ORDER BY tracked_at ASC`,
[id]
);
return NextResponse.json({ success: true, prices: res.rows });
} catch (error: any) {
console.error('Fehler beim Laden der Preishistorie:', error.message);
return NextResponse.json({ success: false, error: error.message, prices: [] }, { status: 500 });
}
}

View File

@@ -4,7 +4,7 @@ import { query } from '@/lib/db';
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
try {
const { id } = params;
const { summary_positive, summary_passable, summary_negative, summary_bathroom } = await req.json();
const { summary_positive, summary_passable, summary_negative, summary_bathroom, summary_count } = await req.json();
if (!id) {
return NextResponse.json({ error: 'Hotel-ID fehlt' }, { status: 400 });
@@ -16,9 +16,10 @@ export async function POST(req: NextRequest, { params }: { params: { id: string
review_summary_passable = $2,
review_summary_negative = $3,
review_summary_bathroom = $4,
review_summary_count = $5,
reviews_updated_at = NOW()
WHERE id = $5`,
[summary_positive, summary_passable, summary_negative, summary_bathroom || null, id]
WHERE id = $6`,
[summary_positive, summary_passable, summary_negative, summary_bathroom || null, summary_count || null, id]
);
return NextResponse.json({ success: true, message: 'Review-Zusammenfassungen gespeichert.' });

View File

@@ -0,0 +1,64 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const bullmq = typeof window === 'undefined' ? eval('require')('bullmq') : null;
if (!bullmq || !bullmq.Queue) {
throw new Error('BullMQ Queue Klasse konnte nicht geladen werden.');
}
const { Queue } = bullmq;
const connection = {
host: process.env.REDIS_HOST || '172.17.0.1',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
connectTimeout: 5000,
maxRetriesPerRequest: 3,
};
const scrapeQueue = new Queue('scrape-jobs', { connection });
// Nur dieses eine Hotel laden
const res = await query('SELECT id, booking_url, name FROM hotels WHERE id = $1', [id]);
if (res.rows.length === 0) {
await scrapeQueue.close();
return NextResponse.json({ error: 'Hotel nicht gefunden' }, { status: 404 });
}
const hotel = res.rows[0];
const n8nBaseUrl = process.env.APP_WEBHOOK_URL || 'https://n8n.prinz-pilaw.de/webhook-test/scrape-reviews';
const n8nWebhookUrl = n8nBaseUrl.includes('?')
? `${n8nBaseUrl}&hotelId=${hotel.id}`
: `${n8nBaseUrl}?hotelId=${hotel.id}`;
const appWebhookUrl = `http://172.17.0.1:3002/api/webhooks/scraper?hotelId=${hotel.id}`;
// Nur DIESEN einzelnen Job in die Queue schreiben
await scrapeQueue.add('scrape-hotel', {
hotelId: hotel.id,
url: hotel.booking_url,
appWebhookUrl,
webhookUrl: n8nWebhookUrl,
});
await scrapeQueue.close();
return NextResponse.json({
success: true,
message: `Scrape-Job für "${hotel.name}" erfolgreich gestartet.`,
});
} catch (error: any) {
console.error('Fehler beim Starten des Einzel-Scrapes:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}

View File

@@ -1,21 +1,70 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
export const dynamic = 'force-dynamic';
export async function GET() {
try {
const res = await query(`
SELECT
h.*,
t.name AS trip_name,
ph.price_numeric::float AS latest_price,
ph.price_per_night::float AS latest_price_per_night,
ph.price_per_person::float AS latest_price_per_person,
ph.nights AS latest_nights,
ph.persons AS latest_persons
FROM hotels h
LEFT JOIN trips t ON h.trip_id = t.id
LEFT JOIN LATERAL (
SELECT price_numeric, price_per_night, price_per_person, nights, persons
FROM hotel_price_history
WHERE hotel_id::text = h.id::text
ORDER BY tracked_at DESC NULLS LAST, id DESC
LIMIT 1
) ph ON true
ORDER BY h.checkin_date ASC NULLS LAST, h.id ASC
`);
return NextResponse.json({ success: true, hotels: res.rows });
} catch (error: any) {
console.error('Fehler in GET /api/hotels:', error.message);
try {
const fallback = await query(`SELECT * FROM hotels ORDER BY id ASC`);
return NextResponse.json({ success: true, hotels: fallback.rows });
} catch (fallbackErr: any) {
return NextResponse.json({ success: false, error: error.message, hotels: [] }, { status: 500 });
}
}
}
export async function POST(req: NextRequest) {
try {
const { trip_id, booking_url, name } = await req.json();
if (!trip_id || !booking_url) {
return NextResponse.json({ error: 'trip_id und booking_url sind erforderlich' }, { status: 400 });
const { name, booking_url, trip_id } = await req.json();
if (!booking_url) {
return NextResponse.json({ error: 'Booking.com URL erforderlich' }, { status: 400 });
}
let checkin_date: string | null = null;
let checkout_date: string | null = null;
try {
const parsedUrl = new URL(booking_url);
checkin_date = parsedUrl.searchParams.get('checkin');
checkout_date = parsedUrl.searchParams.get('checkout');
} catch (e) {}
const res = await query(
`INSERT INTO hotels (trip_id, booking_url, name, is_active)
VALUES ($1, $2, $3, true) RETURNING *`,
[trip_id, booking_url, name || 'Booking Hotel']
`INSERT INTO hotels (name, booking_url, checkin_date, checkout_date, trip_id, is_active)
VALUES ($1, $2, $3, $4, $5, true)
RETURNING *`,
[name || 'Neues Hotel', booking_url, checkin_date, checkout_date, trip_id || null]
);
return NextResponse.json({ success: true, hotel: res.rows[0] });
} catch (e: any) {
return NextResponse.json({ success: false, error: e.message }, { status: 500 });
} catch (error: any) {
console.error('Fehler in POST /api/hotels:', error.message);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}

View File

@@ -33,21 +33,15 @@ export async function POST() {
});
}
const n8nBaseUrl = process.env.APP_WEBHOOK_URL || 'https://n8n.prinz-pilaw.de/webhook-test/scrape-reviews';
// Für ALLE aktiven Hotels nur den Preis-Scan anwerfen (OHNE n8n / OHNE LLM)
for (const hotel of activeHotels) {
const n8nWebhookUrl = n8nBaseUrl.includes('?')
? `${n8nBaseUrl}&hotelId=${hotel.id}`
: `${n8nBaseUrl}?hotelId=${hotel.id}`;
// Direkt-URL zur Next.js App für Preishistorie
const appWebhookUrl = `http://172.17.0.1:3002/api/webhooks/scraper?hotelId=${hotel.id}`;
await scrapeQueue.add('scrape-hotel', {
hotelId: hotel.id,
url: hotel.booking_url,
appWebhookUrl, // Für Preishistorie in Postgres
webhookUrl: n8nWebhookUrl, // Für n8n KI-Reviews
appWebhookUrl, // Speichert den Preis in Postgres
webhookUrl: null, // KEIN n8n -> KOSTENLOS!
});
}
@@ -55,13 +49,10 @@ export async function POST() {
return NextResponse.json({
success: true,
message: `${activeHotels.length} Scrape-Jobs erfolgreich gestartet.`,
message: `Preis-Scan für ${activeHotels.length} Hotels gestartet.`,
});
} catch (error: any) {
console.error('Fehler beim Starten der Jobs:', error);
return NextResponse.json(
{ success: false, error: error.message },
{ status: 500 }
);
console.error('Fehler beim Starten des Preis-Scans:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}

View File

@@ -1,88 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
export const dynamic = 'force-dynamic';
export async function GET() {
try {
const tripsRes = await query('SELECT * FROM trips ORDER BY created_at DESC');
const trips = tripsRes.rows;
const hotelsRes = await query(`
SELECT h.*,
ph.price_numeric as latest_price,
ph.genius_price_numeric as latest_genius_price,
ph.price_per_night,
ph.price_per_person,
ph.nights,
ph.persons,
ph.tracked_at as last_tracked
FROM hotels h
LEFT JOIN LATERAL (
SELECT * FROM hotel_price_history
WHERE hotel_id = h.id
ORDER BY tracked_at DESC LIMIT 1
) ph ON true
ORDER BY h.created_at DESC
`);
// Zahlen-Typen parsen & fehlende Raten bei älteren Einträgen live berechnen
const parsedHotels = hotelsRes.rows.map((h) => {
const latest_price = h.latest_price ? parseFloat(h.latest_price) : null;
let latest_genius_price = h.latest_genius_price ? parseFloat(h.latest_genius_price) : null;
if (!latest_genius_price && latest_price) {
latest_genius_price = parseFloat((latest_price * 0.90).toFixed(2));
}
const nights = h.nights ? parseInt(h.nights, 10) : 1;
const persons = h.persons ? parseInt(h.persons, 10) : 2;
const activePrice = latest_genius_price || latest_price;
// Fallback-Berechnung falls DB-Feld NULL ist
let price_per_night = h.price_per_night ? parseFloat(h.price_per_night) : null;
if (!price_per_night && activePrice && nights > 0) {
price_per_night = parseFloat((activePrice / nights).toFixed(2));
}
let price_per_person = h.price_per_person ? parseFloat(h.price_per_person) : null;
if (!price_per_person && activePrice && nights > 0 && persons > 0) {
price_per_person = parseFloat((activePrice / (nights * persons)).toFixed(2));
}
return {
...h,
rating: h.rating ? parseFloat(h.rating) : null,
latest_price,
latest_genius_price,
price_per_night,
price_per_person,
nights,
persons,
};
});
const tripsWithHotels = trips.map((trip) => ({
...trip,
hotels: parsedHotels.filter((h) => h.trip_id === trip.id),
}));
return NextResponse.json({ success: true, trips: tripsWithHotels });
} catch (e: any) {
console.error('Error in GET /api/trips:', e);
return NextResponse.json({ success: false, error: e.message }, { status: 500 });
const res = await query(`SELECT * FROM trips ORDER BY id ASC`);
return NextResponse.json({ success: true, trips: res.rows });
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message, trips: [] }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
try {
const { title, destination } = await req.json();
if (!title) return NextResponse.json({ error: 'Titel ist erforderlich' }, { status: 400 });
const { name } = await req.json();
if (!name) {
return NextResponse.json({ error: 'Name der Reise erforderlich' }, { status: 400 });
}
const res = await query(
'INSERT INTO trips (title, destination) VALUES ($1, $2) RETURNING *',
[title, destination || null]
`INSERT INTO trips (name) VALUES ($1) RETURNING *`,
[name]
);
return NextResponse.json({ success: true, trip: res.rows[0] });
} catch (e: any) {
return NextResponse.json({ success: false, error: e.message }, { status: 500 });
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}

View File

@@ -1,90 +1,79 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
function parseGermanPrice(priceStr: any): number | null {
if (!priceStr) return null;
if (typeof priceStr === 'number') return Math.round(priceStr);
let cleaned = String(priceStr).replace(/[^0-9.,]/g, '').trim();
if (!cleaned) return null;
if (cleaned.includes('.') && !cleaned.includes(',')) {
cleaned = cleaned.replace(/\./g, '');
} else if (cleaned.includes('.') && cleaned.includes(',')) {
cleaned = cleaned.replace(/\./g, '').replace(',', '.');
} else if (cleaned.includes(',')) {
cleaned = cleaned.replace(',', '.');
}
const parsed = parseFloat(cleaned);
return isNaN(parsed) ? null : Math.round(parsed);
}
export async function POST(req: NextRequest) {
try {
const body = await req.json().catch(() => ({}));
const { searchParams } = new URL(req.url);
const hotelId = searchParams.get('hotelId');
const body = await req.json();
const {
price,
roomSizeM2,
hotelName,
rating,
reviewCount,
address,
image,
nights = 1,
persons = 2,
} = body;
const hotelId = searchParams.get('hotelId') || body.hotelId;
if (!hotelId) {
return NextResponse.json({ error: 'hotelId fehlt' }, { status: 400 });
return NextResponse.json({ success: false, error: 'hotelId fehlt' }, { status: 400 });
}
const parseNum = (val: any): number | null => {
if (typeof val === 'number') return val;
if (!val || typeof val !== 'string') return null;
const match = val.replace(',', '.').match(/\d+(\.\d+)?/);
return match ? parseFloat(match[0]) : null;
};
const numericPrice = parseGermanPrice(body.price);
const numPrice = parseNum(price);
// Automatisch 10 % Genius Rabatt berechnen
let numGenius: number | null = null;
if (numPrice !== null) {
numGenius = parseFloat((numPrice * 0.90).toFixed(2));
// 1. In hotel_price_history eintragen mit echten Spalten
if (numericPrice !== null && numericPrice > 0) {
try {
await query(
`INSERT INTO hotel_price_history (hotel_id, price_numeric, price_raw, currency, nights, persons, tracked_at)
VALUES ($1::uuid, $2, $3, 'EUR', $4, $5, NOW())`,
[hotelId, numericPrice, String(body.price || ''), body.nights || 1, body.persons || 2]
);
} catch (err: any) {
console.error('DB Insert Error (hotel_price_history):', err.message);
}
}
// Ratenberechnung auf Basis des Genius-Preises
const basePrice = numGenius || numPrice;
let pricePerNight = null;
let pricePerPersonPerNight = null;
if (basePrice !== null && nights > 0 && persons > 0) {
// 1. Zimmerpreis pro Nacht
pricePerNight = parseFloat((basePrice / nights).toFixed(2));
// 2. Preis pro Person & Nacht
pricePerPersonPerNight = parseFloat((basePrice / (nights * persons)).toFixed(2));
}
// 1. In Preishistorie eintragen
if (numPrice !== null) {
// 2. Stammdaten aktualisieren
try {
await query(
`INSERT INTO hotel_price_history
(hotel_id, price_numeric, genius_price_numeric, price_raw, price_per_night, price_per_person, nights, persons)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
`UPDATE hotels
SET name = COALESCE($1, name),
rating = COALESCE($2, rating),
review_count = COALESCE($3, review_count),
address = COALESCE($4, address),
image_url = COALESCE($5, image_url),
room_size_m2 = COALESCE($6, room_size_m2),
updated_at = NOW()
WHERE id = $7::uuid`,
[
hotelId,
numPrice,
numGenius,
price ? String(price) : null,
pricePerNight,
pricePerPersonPerNight,
nights,
persons,
body.hotelName || null,
body.rating || null,
body.reviewCount || null,
body.address || null,
body.image || null,
body.roomSizeM2 || null,
hotelId
]
);
} catch (err: any) {
console.error('DB Update Error (hotels):', err.message);
}
// 2. Hotel Stammdaten aktualisieren
await query(
`UPDATE hotels
SET name = COALESCE($1, name),
rating = COALESCE($2, rating),
review_count = COALESCE($3, review_count),
address = COALESCE($4, address),
image_url = COALESCE($5, image_url),
room_size_m2 = COALESCE($6, room_size_m2)
WHERE id = $7`,
[hotelName, rating, reviewCount, address, image, roomSizeM2, hotelId]
);
return NextResponse.json({ success: true });
return NextResponse.json({ success: true, hotelId, parsedPrice: numericPrice });
} catch (error: any) {
console.error('Webhook Error:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
console.error('Allgemeiner Fehler im Webhook:', error.message);
return NextResponse.json({ success: false, error: error.message }, { status: 200 });
}
}

File diff suppressed because it is too large Load Diff