This commit is contained in:
2025-09-23 14:13:24 +02:00
commit 58b5e6b074
103 changed files with 44000 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'ninjacross',
user: process.env.DB_USER || '',
password: process.env.DB_PASSWORD || '',
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});
async function runBestTimeAchievements() {
const client = await pool.connect();
try {
console.log('🏆 Starting best-time achievement check at 19:00...');
const currentHour = new Date().getHours();
const currentDay = new Date().getDay(); // 0 = Sunday
const currentDate = new Date();
const isLastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate() === currentDate.getDate();
console.log(`Current time: ${currentHour}:00`);
console.log(`Is Sunday: ${currentDay === 0}`);
console.log(`Is last day of month: ${isLastDayOfMonth}`);
// Get all players who have played
const playersResult = await client.query(`
SELECT DISTINCT p.id, p.firstname, p.lastname
FROM players p
INNER JOIN times t ON p.id = t.player_id
`);
console.log(`Found ${playersResult.rows.length} players with times`);
let dailyAwards = 0;
let weeklyAwards = 0;
let monthlyAwards = 0;
// Check best-time achievements for each player
for (const player of playersResult.rows) {
console.log(`Checking best-time achievements for ${player.firstname} ${player.lastname}...`);
// Run best-time achievement check function
await client.query('SELECT check_best_time_achievements_timed($1)', [player.id]);
// Check if new daily achievement was earned today
const dailyResult = await client.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
INNER JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.player_id = $1
AND a.category = 'best_time'
AND a.condition_type = 'daily_best'
AND pa.is_completed = true
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
`, [player.id]);
if (parseInt(dailyResult.rows[0].count) > 0) {
dailyAwards++;
console.log(` 🥇 Daily best achievement earned!`);
}
// Check if new weekly achievement was earned (only on Sunday)
if (currentDay === 0) {
const weeklyResult = await client.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
INNER JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.player_id = $1
AND a.category = 'best_time'
AND a.condition_type = 'weekly_best'
AND pa.is_completed = true
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
`, [player.id]);
if (parseInt(weeklyResult.rows[0].count) > 0) {
weeklyAwards++;
console.log(` 🏆 Weekly best achievement earned!`);
}
}
// Check if new monthly achievement was earned (only on last day of month)
if (isLastDayOfMonth) {
const monthlyResult = await client.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
INNER JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.player_id = $1
AND a.category = 'best_time'
AND a.condition_type = 'monthly_best'
AND pa.is_completed = true
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
`, [player.id]);
if (parseInt(monthlyResult.rows[0].count) > 0) {
monthlyAwards++;
console.log(` 👑 Monthly best achievement earned!`);
}
}
}
console.log(`\n🎉 Best-time achievement check completed!`);
console.log(`Daily awards: ${dailyAwards}`);
console.log(`Weekly awards: ${weeklyAwards}`);
console.log(`Monthly awards: ${monthlyAwards}`);
// Get current best times for today
const bestTimesResult = await client.query(`
SELECT
'daily' as period,
p.firstname || ' ' || p.lastname as player_name,
MIN(t.recorded_time) as best_time
FROM times t
INNER JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
GROUP BY p.id, p.firstname, p.lastname
ORDER BY MIN(t.recorded_time) ASC
LIMIT 1
`);
if (bestTimesResult.rows.length > 0) {
const dailyBest = bestTimesResult.rows[0];
console.log(`\n🥇 Today's best time: ${dailyBest.player_name} - ${dailyBest.best_time}`);
}
// Get current best times for this week (if Sunday)
if (currentDay === 0) {
const weekStart = new Date();
weekStart.setDate(weekStart.getDate() - weekStart.getDay());
const weeklyBestResult = await client.query(`
SELECT
'weekly' as period,
p.firstname || ' ' || p.lastname as player_name,
MIN(t.recorded_time) as best_time
FROM times t
INNER JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= CURRENT_DATE
GROUP BY p.id, p.firstname, p.lastname
ORDER BY MIN(t.recorded_time) ASC
LIMIT 1
`, [weekStart.toISOString().split('T')[0]]);
if (weeklyBestResult.rows.length > 0) {
const weeklyBest = weeklyBestResult.rows[0];
console.log(`🏆 This week's best time: ${weeklyBest.player_name} - ${weeklyBest.best_time}`);
}
}
// Get current best times for this month (if last day of month)
if (isLastDayOfMonth) {
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
const monthlyBestResult = await client.query(`
SELECT
'monthly' as period,
p.firstname || ' ' || p.lastname as player_name,
MIN(t.recorded_time) as best_time
FROM times t
INNER JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= CURRENT_DATE
GROUP BY p.id, p.firstname, p.lastname
ORDER BY MIN(t.recorded_time) ASC
LIMIT 1
`, [monthStart.toISOString().split('T')[0]]);
if (monthlyBestResult.rows.length > 0) {
const monthlyBest = monthlyBestResult.rows[0];
console.log(`👑 This month's best time: ${monthlyBest.player_name} - ${monthlyBest.best_time}`);
}
}
} catch (error) {
console.error('❌ Error running best-time achievements:', error);
throw error;
} finally {
client.release();
}
}
// Run if called directly
if (require.main === module) {
runBestTimeAchievements()
.then(() => {
console.log('✅ Best-time achievements script completed successfully');
process.exit(0);
})
.catch((error) => {
console.error('❌ Best-time achievements script failed:', error);
process.exit(1);
});
}
module.exports = { runBestTimeAchievements };

View File

@@ -0,0 +1,141 @@
const { Pool } = require('pg');
const cron = require('node-cron');
// Database connection
const pool = new Pool({
user: 'postgres',
host: 'localhost',
database: 'ninjacross',
password: 'postgres',
port: 5432,
});
async function checkAndNotifyBestTimes() {
try {
console.log('🔔 Checking best times for notifications...');
const currentDate = new Date();
const today = currentDate.toISOString().split('T')[0];
const weekStart = new Date(currentDate.setDate(currentDate.getDate() - currentDate.getDay()));
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
// Check daily best times
const dailyBestQuery = `
WITH daily_best AS (
SELECT
t.player_id,
MIN(t.recorded_time) as best_time,
p.name as player_name,
p.email
FROM times t
JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $1
GROUP BY t.player_id, p.name, p.email
),
global_daily_best AS (
SELECT MIN(best_time) as global_best
FROM daily_best
)
SELECT
db.player_id,
db.player_name,
db.email,
db.best_time,
gdb.global_best
FROM daily_best db
CROSS JOIN global_daily_best gdb
WHERE db.best_time = gdb.global_best
`;
const dailyResult = await pool.query(dailyBestQuery, [today]);
for (const row of dailyResult.rows) {
console.log(`🏆 Daily best time: ${row.player_name} with ${row.best_time}`);
// Here we would send the notification
// For now, just log it
}
// Check weekly best times
const weeklyBestQuery = `
WITH weekly_best AS (
SELECT
t.player_id,
MIN(t.recorded_time) as best_time,
p.name as player_name,
p.email
FROM times t
JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
GROUP BY t.player_id, p.name, p.email
),
global_weekly_best AS (
SELECT MIN(best_time) as global_best
FROM weekly_best
)
SELECT
wb.player_id,
wb.player_name,
wb.email,
wb.best_time,
gwb.global_best
FROM weekly_best wb
CROSS JOIN global_weekly_best gwb
WHERE wb.best_time = gwb.global_best
`;
const weeklyResult = await pool.query(weeklyBestQuery, [weekStart.toISOString().split('T')[0], today]);
for (const row of weeklyResult.rows) {
console.log(`🏆 Weekly best time: ${row.player_name} with ${row.best_time}`);
}
// Check monthly best times
const monthlyBestQuery = `
WITH monthly_best AS (
SELECT
t.player_id,
MIN(t.recorded_time) as best_time,
p.name as player_name,
p.email
FROM times t
JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
GROUP BY t.player_id, p.name, p.email
),
global_monthly_best AS (
SELECT MIN(best_time) as global_best
FROM monthly_best
)
SELECT
mb.player_id,
mb.player_name,
mb.email,
mb.best_time,
gmb.global_best
FROM monthly_best mb
CROSS JOIN global_monthly_best gmb
WHERE mb.best_time = gmb.global_best
`;
const monthlyResult = await pool.query(monthlyBestQuery, [monthStart.toISOString().split('T')[0], today]);
for (const row of monthlyResult.rows) {
console.log(`🏆 Monthly best time: ${row.player_name} with ${row.best_time}`);
}
console.log('✅ Best time notifications check completed');
} catch (error) {
console.error('❌ Error checking best times:', error);
}
}
// Schedule to run every day at 19:00 (7 PM)
cron.schedule('0 19 * * *', () => {
console.log('🕐 Running best time notifications check...');
checkAndNotifyBestTimes();
});
console.log('📅 Best time notifications scheduler started - runs daily at 19:00');

109
scripts/create-user.js Normal file
View File

@@ -0,0 +1,109 @@
// scripts/create-user.js
const { Pool } = require('pg');
const bcrypt = require('bcrypt');
const readline = require('readline');
require('dotenv').config();
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
});
// Readline Interface für Benutzereingaben
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Hilfsfunktion für Benutzereingaben
function askQuestion(question) {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer.trim());
});
});
}
async function createUser() {
try {
console.log('👤 Erstelle neuen Benutzer...\n');
// Verbindung testen
await pool.query('SELECT NOW()');
console.log('✅ Datenbankverbindung erfolgreich\n');
// Benutzereingaben abfragen
const username = await askQuestion('Benutzername eingeben: ');
if (!username) {
console.log('❌ Benutzername darf nicht leer sein!');
return;
}
const password = await askQuestion('Passwort eingeben: ');
if (!password) {
console.log('❌ Passwort darf nicht leer sein!');
return;
}
const confirmPassword = await askQuestion('Passwort bestätigen: ');
if (password !== confirmPassword) {
console.log('❌ Passwörter stimmen nicht überein!');
return;
}
console.log('\n🔄 Erstelle Benutzer...');
// Prüfen ob Benutzer bereits existiert
const existingUser = await pool.query(
'SELECT id FROM adminusers WHERE username = $1',
[username]
);
if (existingUser.rows.length > 0) {
console.log(` Benutzer "${username}" existiert bereits`);
const update = await askQuestion('Passwort aktualisieren? (j/n): ');
if (update.toLowerCase() === 'j' || update.toLowerCase() === 'ja' || update.toLowerCase() === 'y' || update.toLowerCase() === 'yes') {
const passwordHash = await bcrypt.hash(password, 10);
await pool.query(
'UPDATE adminusers SET password_hash = $1 WHERE username = $2',
[passwordHash, username]
);
console.log(`✅ Passwort für Benutzer "${username}" aktualisiert`);
} else {
console.log('❌ Vorgang abgebrochen');
return;
}
} else {
// Neuen Benutzer erstellen
const passwordHash = await bcrypt.hash(password, 10);
await pool.query(
'INSERT INTO adminusers (username, password_hash) VALUES ($1, $2)',
[username, passwordHash]
);
console.log(`✅ Benutzer "${username}" erfolgreich erstellt`);
}
console.log('\n📝 Anmeldedaten:');
console.log(` Benutzername: ${username}`);
console.log(` Passwort: ${password}`);
} catch (error) {
console.error('❌ Fehler beim Erstellen des Benutzers:', error);
process.exit(1);
} finally {
rl.close();
await pool.end();
}
}
// Skript ausführen wenn direkt aufgerufen
if (require.main === module) {
createUser();
}
module.exports = { createUser };

View File

@@ -0,0 +1,110 @@
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});
async function runDailyAchievements() {
const client = await pool.connect();
try {
console.log('🎯 Starting daily achievement check...');
// Get all players who have played today
const playersResult = await client.query(`
SELECT DISTINCT p.id, p.firstname, p.lastname
FROM players p
INNER JOIN times t ON p.id = t.player_id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
`);
console.log(`Found ${playersResult.rows.length} players who played today`);
let totalAchievements = 0;
// Check achievements for each player
for (const player of playersResult.rows) {
console.log(`Checking achievements for ${player.firstname} ${player.lastname}...`);
// Run achievement check function
await client.query('SELECT check_all_achievements($1)', [player.id]);
// Count new achievements earned today
const newAchievementsResult = await client.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
INNER JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.player_id = $1
AND pa.is_completed = true
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
`, [player.id]);
const newAchievements = parseInt(newAchievementsResult.rows[0].count);
totalAchievements += newAchievements;
if (newAchievements > 0) {
console.log(`${newAchievements} new achievements earned!`);
// Get details of new achievements
const achievementsResult = await client.query(`
SELECT a.name, a.description, a.icon, a.points
FROM player_achievements pa
INNER JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.player_id = $1
AND pa.is_completed = true
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
ORDER BY pa.earned_at DESC
`, [player.id]);
achievementsResult.rows.forEach(achievement => {
console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} points)`);
});
} else {
console.log(` No new achievements`);
}
}
console.log(`\n🎉 Daily achievement check completed!`);
console.log(`Total new achievements earned: ${totalAchievements}`);
// Log summary statistics
const statsResult = await client.query(`
SELECT
COUNT(DISTINCT pa.player_id) as players_with_achievements,
COUNT(pa.id) as total_achievements_earned,
SUM(a.points) as total_points_earned
FROM player_achievements pa
INNER JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.is_completed = true
`);
const stats = statsResult.rows[0];
console.log(`\n📊 Overall Statistics:`);
console.log(`Players with achievements: ${stats.players_with_achievements}`);
console.log(`Total achievements earned: ${stats.total_achievements_earned}`);
console.log(`Total points earned: ${stats.total_points_earned || 0}`);
} catch (error) {
console.error('❌ Error running daily achievements:', error);
throw error;
} finally {
client.release();
}
}
// Run if called directly
if (require.main === module) {
runDailyAchievements()
.then(() => {
console.log('✅ Daily achievements script completed successfully');
process.exit(0);
})
.catch((error) => {
console.error('❌ Daily achievements script failed:', error);
process.exit(1);
});
}
module.exports = { runDailyAchievements };

View File

@@ -0,0 +1,285 @@
const { Pool } = require('pg');
const webpush = require('web-push');
// Database connection
const pool = new Pool({
user: process.env.DB_USER || 'postgres',
host: process.env.DB_HOST || 'localhost',
database: process.env.DB_NAME || 'ninjacross',
password: process.env.DB_PASSWORD || 'postgres',
port: process.env.DB_PORT || 5432,
});
// VAPID keys (same as push-service.js)
const vapidKeys = {
publicKey: 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds',
privateKey: 'HBdRCtmZUAzsWpVjZ2LDaoWliIPHldAb5ExAt8bvDeg'
};
webpush.setVapidDetails(
'mailto:admin@ninjacross.com',
vapidKeys.publicKey,
vapidKeys.privateKey
);
/**
* Send push notification to a specific player
*/
async function sendPushToPlayer(playerId, title, message, data = {}) {
try {
console.log(`📱 Sending push to player ${playerId}: ${title}`);
const result = await pool.query(`
SELECT endpoint, p256dh, auth
FROM player_subscriptions
WHERE player_id = $1
`, [playerId]);
if (result.rows.length === 0) {
console.log(`❌ No subscription found for player ${playerId}`);
return { success: false, reason: 'No subscription' };
}
// Send to all subscriptions for this player
const results = [];
for (const row of result.rows) {
const subscription = {
endpoint: row.endpoint,
keys: {
p256dh: row.p256dh,
auth: row.auth
}
};
const payload = JSON.stringify({
title: title,
body: message,
icon: '/pictures/favicon.ico',
badge: '/pictures/favicon.ico',
data: {
url: '/dashboard.html',
timestamp: Date.now(),
...data
},
actions: [
{
action: 'view',
title: 'Dashboard öffnen'
}
]
});
try {
await webpush.sendNotification(subscription, payload);
results.push({ success: true });
} catch (error) {
console.error(`❌ Error sending to subscription:`, error);
results.push({ success: false, error: error.message });
}
}
const successCount = results.filter(r => r.success).length;
console.log(`✅ Push notification sent to ${successCount}/${result.rows.length} subscriptions for player ${playerId}`);
return {
success: successCount > 0,
sent: successCount,
total: result.rows.length
};
} catch (error) {
console.error(`❌ Error sending push to player ${playerId}:`, error);
return { success: false, error: error.message };
}
}
/**
* Send achievement notification to player
*/
async function sendAchievementNotification(playerId, achievement) {
const title = `🏆 ${achievement.name}`;
const message = achievement.description;
return await sendPushToPlayer(playerId, title, message, {
type: 'achievement',
achievementId: achievement.id,
points: achievement.points
});
}
/**
* Send best time notification to player
*/
async function sendBestTimeNotification(playerId, timeType, locationName, time) {
const title = `🏁 ${timeType} Bestzeit!`;
const message = `Du hast die beste Zeit in ${locationName} mit ${time} erreicht!`;
return await sendPushToPlayer(playerId, title, message, {
type: 'best_time',
timeType: timeType,
location: locationName,
time: time
});
}
/**
* Send daily summary notification to player
*/
async function sendDailySummaryNotification(playerId, summary) {
const title = `📊 Dein Tagesrückblick`;
const message = `Du hattest ${summary.runs} Läufe heute. Beste Zeit: ${summary.bestTime}`;
return await sendPushToPlayer(playerId, title, message, {
type: 'daily_summary',
runs: summary.runs,
bestTime: summary.bestTime
});
}
/**
* Send weekly summary notification to player
*/
async function sendWeeklySummaryNotification(playerId, summary) {
const title = `📈 Deine Wochenzusammenfassung`;
const message = `Diese Woche: ${summary.runs} Läufe, ${summary.improvement}% Verbesserung!`;
return await sendPushToPlayer(playerId, title, message, {
type: 'weekly_summary',
runs: summary.runs,
improvement: summary.improvement
});
}
/**
* Send notification to all logged in players
*/
async function sendNotificationToAllPlayers(title, message, data = {}) {
try {
console.log(`📢 Sending notification to all players: ${title}`);
const result = await pool.query(`
SELECT ps.player_id, ps.endpoint, ps.p256dh, ps.auth, p.firstname, p.lastname
FROM player_subscriptions ps
JOIN players p ON ps.player_id = p.id
WHERE ps.player_id IS NOT NULL
`);
if (result.rows.length === 0) {
console.log('❌ No player subscriptions found');
return { success: false, reason: 'No subscriptions' };
}
const promises = result.rows.map(async (row) => {
const subscription = {
endpoint: row.endpoint,
keys: {
p256dh: row.p256dh,
auth: row.auth
}
};
const payload = JSON.stringify({
title: title,
body: message,
icon: '/pictures/favicon.ico',
badge: '/pictures/favicon.ico',
data: {
url: '/dashboard.html',
timestamp: Date.now(),
...data
}
});
try {
await webpush.sendNotification(subscription, payload);
return {
playerId: row.player_id,
playerName: `${row.firstname} ${row.lastname}`,
success: true
};
} catch (error) {
console.error(`❌ Error sending to player ${row.player_id}:`, error);
return {
playerId: row.player_id,
playerName: `${row.firstname} ${row.lastname}`,
success: false,
error: error.message
};
}
});
const results = await Promise.all(promises);
const successCount = results.filter(r => r.success).length;
console.log(`✅ Sent notifications to ${successCount}/${results.length} players`);
return {
success: true,
sent: successCount,
total: results.length,
results: results
};
} catch (error) {
console.error('❌ Error sending notifications to all players:', error);
return { success: false, error: error.message };
}
}
/**
* Get player statistics for notifications
*/
async function getPlayerStats(playerId) {
try {
const result = await pool.query(`
SELECT
p.firstname,
p.lastname,
COUNT(t.id) as total_runs,
MIN(t.recorded_time) as best_time,
COUNT(DISTINCT t.location_id) as locations_visited
FROM players p
LEFT JOIN times t ON p.id = t.player_id
WHERE p.id = $1
GROUP BY p.id, p.firstname, p.lastname
`, [playerId]);
if (result.rows.length === 0) {
return null;
}
return result.rows[0];
} catch (error) {
console.error('Error getting player stats:', error);
return null;
}
}
/**
* Check if player has push notifications enabled
*/
async function isPlayerPushEnabled(playerId) {
try {
const result = await pool.query(`
SELECT COUNT(*) as count
FROM player_subscriptions
WHERE player_id = $1
`, [playerId]);
return result.rows[0].count > 0;
} catch (error) {
console.error('Error checking push status:', error);
return false;
}
}
module.exports = {
sendPushToPlayer,
sendAchievementNotification,
sendBestTimeNotification,
sendDailySummaryNotification,
sendWeeklySummaryNotification,
sendNotificationToAllPlayers,
getPlayerStats,
isPlayerPushEnabled
};

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env node
/**
* Fix Player Subscriptions Script
*
* This script fixes the player_subscriptions table by:
* 1. Identifying orphaned subscriptions (UUIDs that don't match any player)
* 2. Optionally migrating them to real player IDs
* 3. Cleaning up invalid subscriptions
*/
const { Pool } = require('pg');
// Database connection
const pool = new Pool({
user: process.env.DB_USER || 'postgres',
host: process.env.DB_HOST || 'localhost',
database: process.env.DB_NAME || 'ninjacross',
password: process.env.DB_PASSWORD || 'postgres',
port: process.env.DB_PORT || 5432,
});
async function fixPlayerSubscriptions() {
console.log('🔧 Starting Player Subscriptions Fix...\n');
try {
// 1. Find orphaned subscriptions (UUIDs that don't match any player)
console.log('📊 Step 1: Finding orphaned subscriptions...');
const orphanedQuery = `
SELECT ps.player_id, ps.created_at, ps.endpoint
FROM player_subscriptions ps
LEFT JOIN players p ON ps.player_id = p.id
WHERE p.id IS NULL
`;
const orphanedResult = await pool.query(orphanedQuery);
console.log(`Found ${orphanedResult.rows.length} orphaned subscriptions`);
if (orphanedResult.rows.length > 0) {
console.log('\n📋 Orphaned subscriptions:');
orphanedResult.rows.forEach((sub, index) => {
console.log(` ${index + 1}. Player ID: ${sub.player_id}`);
console.log(` Created: ${sub.created_at}`);
console.log(` Endpoint: ${sub.endpoint.substring(0, 50)}...`);
});
}
// 2. Find players without subscriptions
console.log('\n📊 Step 2: Finding players without subscriptions...');
const playersWithoutSubsQuery = `
SELECT p.id, p.firstname, p.lastname, p.supabase_user_id
FROM players p
LEFT JOIN player_subscriptions ps ON p.id = ps.player_id
WHERE ps.player_id IS NULL
`;
const playersWithoutSubsResult = await pool.query(playersWithoutSubsQuery);
console.log(`Found ${playersWithoutSubsResult.rows.length} players without subscriptions`);
if (playersWithoutSubsResult.rows.length > 0) {
console.log('\n📋 Players without subscriptions:');
playersWithoutSubsResult.rows.forEach((player, index) => {
console.log(` ${index + 1}. ${player.firstname} ${player.lastname} (${player.id})`);
console.log(` Supabase User ID: ${player.supabase_user_id || 'None'}`);
});
}
// 3. Show current subscription statistics
console.log('\n📊 Step 3: Current subscription statistics...');
const statsQuery = `
SELECT
COUNT(*) as total_subscriptions,
COUNT(DISTINCT ps.player_id) as unique_player_ids,
COUNT(p.id) as linked_to_players,
COUNT(*) - COUNT(p.id) as orphaned_count
FROM player_subscriptions ps
LEFT JOIN players p ON ps.player_id = p.id
`;
const statsResult = await pool.query(statsQuery);
const stats = statsResult.rows[0];
console.log(`Total subscriptions: ${stats.total_subscriptions}`);
console.log(`Unique player IDs: ${stats.unique_player_ids}`);
console.log(`Linked to real players: ${stats.linked_to_players}`);
console.log(`Orphaned subscriptions: ${stats.orphaned_count}`);
// 4. Ask user what to do
console.log('\n🔧 Step 4: What would you like to do?');
console.log('1. Clean up orphaned subscriptions (DELETE)');
console.log('2. Keep orphaned subscriptions (no action)');
console.log('3. Show detailed analysis only');
// For now, just show the analysis
console.log('\n✅ Analysis complete. No changes made.');
console.log('\n💡 Recommendations:');
console.log('- Orphaned subscriptions should be cleaned up');
console.log('- New subscriptions will now use real player IDs');
console.log('- Existing valid subscriptions will continue to work');
} catch (error) {
console.error('❌ Error fixing player subscriptions:', error);
} finally {
await pool.end();
}
}
// Clean up orphaned subscriptions
async function cleanupOrphanedSubscriptions() {
console.log('🧹 Cleaning up orphaned subscriptions...');
try {
const deleteQuery = `
DELETE FROM player_subscriptions
WHERE player_id NOT IN (SELECT id FROM players)
`;
const result = await pool.query(deleteQuery);
console.log(`✅ Deleted ${result.rowCount} orphaned subscriptions`);
} catch (error) {
console.error('❌ Error cleaning up subscriptions:', error);
}
}
// Run the script
if (require.main === module) {
fixPlayerSubscriptions();
}
module.exports = {
fixPlayerSubscriptions,
cleanupOrphanedSubscriptions
};

107
scripts/init-db.js Normal file
View File

@@ -0,0 +1,107 @@
// scripts/init-db.js
const { Pool } = require('pg');
const bcrypt = require('bcrypt');
require('dotenv').config();
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
});
async function initDatabase() {
try {
console.log('🚀 Initialisiere Datenbank...');
// Verbindung testen
await pool.query('SELECT NOW()');
console.log('✅ Datenbankverbindung erfolgreich');
// Adminusers Tabelle erstellen
await pool.query(`
CREATE TABLE IF NOT EXISTS adminusers (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP
)
`);
console.log('✅ adminusers Tabelle erstellt/überprüft');
// API Tokens Tabelle erstellen
await pool.query(`
CREATE TABLE IF NOT EXISTS api_tokens (
id SERIAL PRIMARY KEY,
token VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
standorte TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
is_active BOOLEAN DEFAULT true
)
`);
console.log('✅ api_tokens Tabelle erstellt/überprüft');
// Locations Tabelle erstellen
await pool.query(`
CREATE TABLE IF NOT EXISTS locations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('✅ locations Tabelle erstellt/überprüft');
// Standardbenutzer erstellen (falls nicht vorhanden)
const existingUser = await pool.query(
'SELECT id FROM adminusers WHERE username = $1',
['admin']
);
if (existingUser.rows.length === 0) {
const passwordHash = await bcrypt.hash('admin123', 10);
await pool.query(
'INSERT INTO adminusers (username, password_hash) VALUES ($1, $2)',
['admin', passwordHash]
);
console.log('✅ Standardbenutzer "admin" mit Passwort "admin123" erstellt');
} else {
console.log(' Standardbenutzer "admin" existiert bereits');
}
// Index für bessere Performance
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_adminusers_username ON adminusers(username);
CREATE INDEX IF NOT EXISTS idx_adminusers_active ON adminusers(is_active);
CREATE INDEX IF NOT EXISTS idx_api_tokens_token ON api_tokens(token);
CREATE INDEX IF NOT EXISTS idx_api_tokens_active ON api_tokens(is_active);
CREATE INDEX IF NOT EXISTS idx_locations_name ON locations(name);
CREATE INDEX IF NOT EXISTS idx_locations_coords ON locations(latitude, longitude);
`);
console.log('✅ Indizes erstellt/überprüft');
console.log('🎉 Datenbank erfolgreich initialisiert!');
console.log('📝 Standardanmeldung: admin / admin123');
console.log('⚠️ Ändern Sie das Standardpasswort in der Produktion!');
} catch (error) {
console.error('❌ Fehler bei der Datenbankinitialisierung:', error);
process.exit(1);
} finally {
await pool.end();
}
}
// Skript ausführen wenn direkt aufgerufen
if (require.main === module) {
initDatabase();
}
module.exports = { initDatabase };

View File

@@ -0,0 +1,129 @@
const { Pool } = require('pg');
const webpush = require('web-push');
// Database connection
const pool = new Pool({
user: 'postgres',
host: 'localhost',
database: 'ninjacross',
password: 'postgres',
port: 5432,
});
// VAPID Keys (generate with: webpush.generateVAPIDKeys())
const vapidKeys = {
publicKey: 'BEl62iUYgUivxIkv69yViEuiBIa40HI6F2B5L4h7Q8Y',
privateKey: 'your-private-key-here'
};
webpush.setVapidDetails(
'mailto:admin@ninjacross.es',
vapidKeys.publicKey,
vapidKeys.privateKey
);
// Store subscription endpoint for each player
async function storePlayerSubscription(playerId, subscription) {
try {
await pool.query(`
INSERT INTO player_subscriptions (player_id, endpoint, p256dh, auth)
VALUES ($1, $2, $3, $4)
`, [
playerId,
subscription.endpoint,
subscription.keys.p256dh,
subscription.keys.auth
]);
console.log(`✅ Subscription stored for player ${playerId}`);
} catch (error) {
console.error('Error storing subscription:', error);
}
}
// Send push notification to player
async function sendPushNotification(playerId, title, message, icon = '🏆') {
try {
const result = await pool.query(`
SELECT endpoint, p256dh, auth
FROM player_subscriptions
WHERE player_id = $1
`, [playerId]);
if (result.rows.length === 0) {
console.log(`No subscription found for player ${playerId}`);
return;
}
const subscription = {
endpoint: result.rows[0].endpoint,
keys: {
p256dh: result.rows[0].p256dh,
auth: result.rows[0].auth
}
};
const payload = JSON.stringify({
title: title,
body: message,
icon: '/pictures/favicon.ico',
badge: '/pictures/favicon.ico',
data: {
url: '/dashboard.html'
}
});
await webpush.sendNotification(subscription, payload);
console.log(`✅ Push notification sent to player ${playerId}`);
} catch (error) {
console.error('Error sending push notification:', error);
}
}
// Send best time notifications
async function sendBestTimeNotifications() {
try {
console.log('🔔 Sending best time notifications...');
// Get daily best
const dailyResult = await pool.query(`
WITH daily_best AS (
SELECT
t.player_id,
MIN(t.recorded_time) as best_time,
CONCAT(p.firstname, ' ', p.lastname) as player_name
FROM times t
JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
GROUP BY t.player_id, p.firstname, p.lastname
)
SELECT
player_id,
player_name,
best_time
FROM daily_best
WHERE best_time = (SELECT MIN(best_time) FROM daily_best)
LIMIT 1
`);
if (dailyResult.rows.length > 0) {
const daily = dailyResult.rows[0];
await sendPushNotification(
daily.player_id,
'🏆 Tageskönig!',
`Glückwunsch ${daily.player_name}! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!`
);
}
console.log('✅ Best time notifications sent');
} catch (error) {
console.error('❌ Error sending best time notifications:', error);
}
}
module.exports = {
storePlayerSubscription,
sendPushNotification,
sendBestTimeNotifications
};

87
scripts/setup-players.js Normal file
View File

@@ -0,0 +1,87 @@
// scripts/setup-players.js
// Script to set up players table and fix the player name issue
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
});
async function setupPlayers() {
try {
console.log('🚀 Setting up players table and views...');
// Test connection
await pool.query('SELECT NOW()');
console.log('✅ Database connection successful');
// Check if players table exists and has the expected structure
const tableCheck = await pool.query(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'players'
`);
if (tableCheck.rows.length === 0) {
console.log('❌ Players table not found. Please create it first with the structure:');
console.log(' - id (UUID)');
console.log(' - firstname (VARCHAR)');
console.log(' - lastname (VARCHAR)');
console.log(' - birthdate (DATE)');
console.log(' - created_at (TIMESTAMP)');
console.log(' - rfiduid (VARCHAR)');
process.exit(1);
}
console.log('✅ Players table structure verified');
console.log('📋 Available columns:', tableCheck.rows.map(r => r.column_name).join(', '));
// Create the updated view using your actual table structure
await pool.query(`
CREATE OR REPLACE VIEW "GetTimesWithPlayerAndLocation" AS
SELECT
gt.*,
l.name as location_name,
l.latitude,
l.longitude,
COALESCE(CONCAT(p.firstname, ' ', p.lastname), 'Unknown Player') as player_name
FROM "gettimes" gt
JOIN locations l ON gt.location_id = l.id
LEFT JOIN players p ON gt.player_id = p.id
`);
console.log('✅ Updated view created');
// Test the view
const testResult = await pool.query('SELECT COUNT(*) as count FROM "GetTimesWithPlayerAndLocation"');
console.log(`✅ View test successful: ${testResult.rows[0].count} records found`);
// Show sample data
const sampleData = await pool.query('SELECT player_name, location_name, recorded_time FROM "GetTimesWithPlayerAndLocation" LIMIT 3');
console.log('📊 Sample data from view:');
sampleData.rows.forEach((row, index) => {
console.log(` ${index + 1}. ${row.player_name} at ${row.location_name}: ${row.recorded_time}`);
});
console.log('\n🎉 Setup completed successfully!');
console.log('📝 Your index.html should now display player names instead of IDs');
console.log('🔄 Restart your server to use the new view');
} catch (error) {
console.error('❌ Error during setup:', error);
process.exit(1);
} finally {
await pool.end();
}
}
// Run if called directly
if (require.main === module) {
setupPlayers();
}
module.exports = { setupPlayers };

108
scripts/setup_cron.js Normal file
View File

@@ -0,0 +1,108 @@
const { exec } = require('child_process');
const path = require('path');
// Cron job setup for achievements
const cronJobs = [
{
name: 'daily_achievements',
// Run daily at 19:00 for best-time achievements
schedule: '0 19 * * *',
command: `cd ${__dirname} && node best_time_achievements.js >> /var/log/ninjaserver_achievements.log 2>&1`,
description: 'Daily best-time achievement check at 19:00'
},
{
name: 'weekly_achievements',
// Run every Sunday at 19:00 for weekly best-time achievements
schedule: '0 19 * * 0',
command: `cd ${__dirname} && node best_time_achievements.js >> /var/log/ninjaserver_achievements.log 2>&1`,
description: 'Weekly best-time achievement check on Sunday at 19:00'
},
{
name: 'monthly_achievements',
// Run on last day of month at 19:00 for monthly best-time achievements
schedule: '0 19 28-31 * * [ $(date -d tomorrow +\\%d) -eq 1 ]',
command: `cd ${__dirname} && node best_time_achievements.js >> /var/log/ninjaserver_achievements.log 2>&1`,
description: 'Monthly best-time achievement check on last day of month at 19:00'
}
];
function setupCronJobs() {
console.log('🕐 Setting up best-time achievement cron jobs...');
let cronEntries = [];
// Create cron job entries
cronJobs.forEach(job => {
const cronEntry = `${job.schedule} ${job.command}`;
cronEntries.push(cronEntry);
console.log(`📅 ${job.name}: ${job.schedule} - ${job.description}`);
});
// Add all cron jobs to crontab
const allCronEntries = cronEntries.join('\n');
exec(`(crontab -l 2>/dev/null; echo "${allCronEntries}") | crontab -`, (error, stdout, stderr) => {
if (error) {
console.error('❌ Error setting up cron jobs:', error);
return;
}
if (stderr) {
console.error('⚠️ Cron job warning:', stderr);
}
console.log('✅ All cron jobs setup successfully!');
console.log('📝 Logs will be written to: /var/log/ninjaserver_achievements.log');
// Show current crontab
exec('crontab -l', (error, stdout, stderr) => {
if (!error) {
console.log('\n📋 Current crontab:');
console.log(stdout);
}
});
});
}
function removeCronJobs() {
console.log('🗑️ Removing best-time achievement cron jobs...');
exec('crontab -l | grep -v "best_time_achievements.js" | crontab -', (error, stdout, stderr) => {
if (error) {
console.error('❌ Error removing cron jobs:', error);
return;
}
console.log('✅ All cron jobs removed successfully!');
});
}
// Command line interface
if (require.main === module) {
const command = process.argv[2];
switch (command) {
case 'setup':
setupCronJobs();
break;
case 'remove':
removeCronJobs();
break;
case 'status':
exec('crontab -l | grep best_time_achievements', (error, stdout, stderr) => {
if (stdout) {
console.log('✅ Best-time achievement cron jobs are active:');
console.log(stdout);
} else {
console.log('❌ No best-time achievement cron jobs found');
}
});
break;
default:
console.log('Usage: node setup_cron.js [setup|remove|status]');
console.log(' setup - Add best-time achievement cron jobs (daily 19:00, Sunday 19:00, last day of month 19:00)');
console.log(' remove - Remove all best-time achievement cron jobs');
console.log(' status - Check if best-time achievement cron jobs are active');
}
}
module.exports = { setupCronJobs, removeCronJobs };

View File

@@ -0,0 +1,94 @@
/**
* Simuliert das Aufzeichnen einer neuen Zeit und testet sofortige Achievements
*
* Dieses Script simuliert, was passiert, wenn ein Spieler eine neue Zeit aufzeichnet
*/
const { Pool } = require('pg');
const AchievementSystem = require('../lib/achievementSystem');
require('dotenv').config();
// Database connection
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
});
async function simulateNewTime() {
console.log('🎮 Simuliere neue Zeit-Aufzeichnung...\n');
try {
// Hole einen Test-Spieler
const playerResult = await pool.query(`
SELECT p.id, p.firstname, p.lastname, p.rfiduid
FROM players p
WHERE p.rfiduid IS NOT NULL
LIMIT 1
`);
if (playerResult.rows.length === 0) {
console.log('❌ Kein Spieler mit RFID gefunden');
return;
}
const player = playerResult.rows[0];
console.log(`👤 Teste mit Spieler: ${player.firstname} ${player.lastname} (${player.rfiduid})`);
// Hole eine Test-Location
const locationResult = await pool.query(`
SELECT id, name FROM locations LIMIT 1
`);
if (locationResult.rows.length === 0) {
console.log('❌ Keine Location gefunden');
return;
}
const location = locationResult.rows[0];
console.log(`📍 Teste mit Location: ${location.name}`);
// Simuliere eine neue Zeit (etwas langsamer als die beste Zeit)
const testTime = '00:01:30.500'; // 1:30.500
console.log(`⏱️ Simuliere Zeit: ${testTime}`);
// Füge die Zeit zur Datenbank hinzu
const timeResult = await pool.query(
`INSERT INTO times (player_id, location_id, recorded_time, created_at)
VALUES ($1, $2, $3, $4)
RETURNING id, player_id, created_at`,
[player.id, location.id, testTime, new Date()]
);
console.log(`✅ Zeit erfolgreich gespeichert (ID: ${timeResult.rows[0].id})`);
// Teste sofortige Achievements
console.log('\n🏆 Prüfe sofortige Achievements...');
const achievementSystem = new AchievementSystem();
await achievementSystem.loadAchievements();
const newAchievements = await achievementSystem.checkImmediateAchievements(player.id);
if (newAchievements.length > 0) {
console.log(`\n🎉 ${newAchievements.length} neue Achievements vergeben:`);
newAchievements.forEach(achievement => {
console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`);
});
} else {
console.log('\n Keine neuen Achievements vergeben');
}
console.log('\n✅ Simulation erfolgreich abgeschlossen!');
} catch (error) {
console.error('❌ Simulation fehlgeschlagen:', error);
} finally {
await pool.end();
}
}
// Führe Simulation aus
simulateNewTime();

View File

@@ -0,0 +1,50 @@
/**
* Test Script für das JavaScript Achievement System
*
* Testet die Achievement-Logik ohne auf 19:00 Uhr zu warten
*/
const AchievementSystem = require('../lib/achievementSystem');
require('dotenv').config();
async function testAchievementSystem() {
console.log('🧪 Starte Achievement-System Test...\n');
try {
const achievementSystem = new AchievementSystem();
// Lade Achievements
console.log('📋 Lade Achievements...');
await achievementSystem.loadAchievements();
// Führe tägliche Achievement-Prüfung durch
console.log('\n🎯 Führe tägliche Achievement-Prüfung durch...');
const result = await achievementSystem.runDailyAchievementCheck();
// Zeige Ergebnisse
console.log('\n📊 Test-Ergebnisse:');
console.log(` 🏆 ${result.totalNewAchievements} neue Achievements vergeben`);
console.log(` 👥 ${result.playerAchievements.length} Spieler haben neue Achievements erhalten`);
if (result.playerAchievements.length > 0) {
console.log('\n📋 Neue Achievements im Detail:');
result.playerAchievements.forEach(player => {
console.log(` 👤 ${player.player}:`);
player.achievements.forEach(achievement => {
console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`);
});
});
} else {
console.log('\n Keine neuen Achievements vergeben');
}
console.log('\n✅ Test erfolgreich abgeschlossen!');
} catch (error) {
console.error('❌ Test fehlgeschlagen:', error);
process.exit(1);
}
}
// Führe Test aus
testAchievementSystem();

View File

@@ -0,0 +1,48 @@
/**
* Test Script für sofortige Achievements
*
* Testet die sofortige Achievement-Logik für einen einzelnen Spieler
*/
const AchievementSystem = require('../lib/achievementSystem');
require('dotenv').config();
async function testImmediateAchievements() {
console.log('⚡ Starte sofortige Achievement-Test...\n');
try {
const achievementSystem = new AchievementSystem();
// Lade Achievements
console.log('📋 Lade Achievements...');
await achievementSystem.loadAchievements();
// Teste mit einem spezifischen Spieler (Carsten Graf)
const testPlayerId = '313ceee3-8040-44b4-98d2-e63703579e5d';
console.log(`\n🎯 Teste sofortige Achievements für Spieler ${testPlayerId}...`);
const newAchievements = await achievementSystem.checkImmediateAchievements(testPlayerId);
// Zeige Ergebnisse
console.log('\n📊 Sofortige Achievement-Test Ergebnisse:');
console.log(` 🏆 ${newAchievements.length} neue sofortige Achievements vergeben`);
if (newAchievements.length > 0) {
console.log('\n📋 Neue sofortige Achievements:');
newAchievements.forEach(achievement => {
console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`);
});
} else {
console.log('\n Keine neuen sofortigen Achievements vergeben');
}
console.log('\n✅ Sofortige Achievement-Test erfolgreich abgeschlossen!');
} catch (error) {
console.error('❌ Test fehlgeschlagen:', error);
process.exit(1);
}
}
// Führe Test aus
testImmediateAchievements();

View File

@@ -0,0 +1,75 @@
/**
* Test Script für mehrfache Achievements
*
* Demonstriert, wie Achievements mehrmals erreicht werden können
* und wie die Gesamtpunkte berechnet werden
*/
const AchievementSystem = require('../lib/achievementSystem');
require('dotenv').config();
async function testMultipleAchievements() {
console.log('🔄 Teste mehrfache Achievements...\n');
try {
const achievementSystem = new AchievementSystem();
// Lade Achievements
console.log('📋 Lade Achievements...');
await achievementSystem.loadAchievements();
// Teste mit Carsten Graf
const testPlayerId = '313ceee3-8040-44b4-98d2-e63703579e5d';
console.log(`\n👤 Teste mit Spieler: ${testPlayerId}`);
// Zeige aktuelle Punkte
console.log('\n📊 Aktuelle Gesamtpunkte:');
const currentPoints = await achievementSystem.getPlayerTotalPoints(testPlayerId);
console.log(` 🏆 Gesamtpunkte: ${currentPoints.totalPoints}`);
console.log(` 🔢 Gesamt-Completions: ${currentPoints.totalCompletions}`);
// Führe Achievement-Check durch
console.log('\n🎯 Führe Achievement-Check durch...');
const newAchievements = await achievementSystem.checkImmediateAchievements(testPlayerId);
if (newAchievements.length > 0) {
console.log(`\n🏆 ${newAchievements.length} neue Achievements vergeben:`);
newAchievements.forEach(achievement => {
console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`);
});
} else {
console.log('\n Keine neuen Achievements vergeben');
}
// Zeige neue Gesamtpunkte
console.log('\n📊 Neue Gesamtpunkte:');
const newPoints = await achievementSystem.getPlayerTotalPoints(testPlayerId);
console.log(` 🏆 Gesamtpunkte: ${newPoints.totalPoints} (${newPoints.totalPoints - currentPoints.totalPoints > 0 ? '+' : ''}${newPoints.totalPoints - currentPoints.totalPoints})`);
console.log(` 🔢 Gesamt-Completions: ${newPoints.totalCompletions} (${newPoints.totalCompletions - currentPoints.totalCompletions > 0 ? '+' : ''}${newPoints.totalCompletions - currentPoints.totalCompletions})`);
// Zeige alle Achievements mit Completions
console.log('\n📋 Alle Achievements mit Completions:');
await achievementSystem.loadPlayerAchievements(testPlayerId);
const playerAchievements = achievementSystem.playerAchievements.get(testPlayerId);
if (playerAchievements && playerAchievements.size > 0) {
for (const [achievementId, data] of playerAchievements) {
const achievement = Array.from(achievementSystem.achievements.values())
.find(a => a.id === achievementId);
if (achievement) {
console.log(` ${achievement.icon} ${achievement.name}: ${data.completion_count}x (${data.completion_count * achievement.points} Punkte)`);
}
}
}
console.log('\n✅ Mehrfache Achievement-Test erfolgreich abgeschlossen!');
} catch (error) {
console.error('❌ Test fehlgeschlagen:', error);
process.exit(1);
}
}
// Führe Test aus
testMultipleAchievements();