Update
This commit is contained in:
198
scripts/best_time_achievements.js
Normal file
198
scripts/best_time_achievements.js
Normal 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 };
|
||||
141
scripts/best_time_notifications.js
Normal file
141
scripts/best_time_notifications.js
Normal 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
109
scripts/create-user.js
Normal 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 };
|
||||
110
scripts/daily_achievements.js
Normal file
110
scripts/daily_achievements.js
Normal 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 };
|
||||
285
scripts/enhanced_player_notifications.js
Normal file
285
scripts/enhanced_player_notifications.js
Normal 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
|
||||
};
|
||||
134
scripts/fix_player_subscriptions.js
Normal file
134
scripts/fix_player_subscriptions.js
Normal 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
107
scripts/init-db.js
Normal 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 };
|
||||
129
scripts/iphone_notifications.js
Normal file
129
scripts/iphone_notifications.js
Normal 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
87
scripts/setup-players.js
Normal 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
108
scripts/setup_cron.js
Normal 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 };
|
||||
94
scripts/simulate-new-time.js
Normal file
94
scripts/simulate-new-time.js
Normal 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();
|
||||
50
scripts/test-achievements.js
Normal file
50
scripts/test-achievements.js
Normal 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();
|
||||
48
scripts/test-immediate-achievements.js
Normal file
48
scripts/test-immediate-achievements.js
Normal 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();
|
||||
75
scripts/test-multiple-achievements.js
Normal file
75
scripts/test-multiple-achievements.js
Normal 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();
|
||||
Reference in New Issue
Block a user