Diverse änderungen am Push system

This commit is contained in:
2025-09-16 21:00:12 +02:00
parent 69e3985af3
commit b2fc63e2d0
7 changed files with 992 additions and 164 deletions

View File

@@ -89,7 +89,17 @@ class AchievementSystem {
});
});
console.log(`📋 ${result.rows.length} Achievements für Spieler ${playerId} geladen`);
// Count unique achievements and duplicates
const uniqueAchievements = new Set(result.rows.map(row => row.achievement_id));
const totalCount = result.rows.length;
const uniqueCount = uniqueAchievements.size;
const duplicateCount = totalCount - uniqueCount;
if (duplicateCount > 0) {
console.log(`📋 ${totalCount} Achievements für Spieler ${playerId} geladen (${uniqueCount} eindeutige, ${duplicateCount} doppelt)`);
} else {
console.log(`📋 ${totalCount} Achievements für Spieler ${playerId} geladen`);
}
return true;
} catch (error) {
console.error(`❌ Fehler beim Laden der Spieler-Achievements für ${playerId}:`, error);
@@ -535,106 +545,138 @@ class AchievementSystem {
}
/**
* Prüft Tageskönig Achievement
* Prüft Tageskönig Achievement pro Standort
*/
async checkDailyBest(playerId, currentDate, newAchievements) {
const achievement = Array.from(this.achievements.values())
.find(a => a.category === 'best_time' && a.condition_type === 'daily_best');
.find(a => a.category === 'time' && a.condition_type === 'best_time_daily_location');
if (!achievement) return;
// Prüfe ob das Achievement heute bereits vergeben wurde
const alreadyEarnedToday = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
`, [playerId, achievement.id]);
if (parseInt(alreadyEarnedToday.rows[0].count) > 0) return;
// Hole beste Zeit des Spielers heute
const playerResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
// Hole alle Standorte, an denen der Spieler heute gespielt hat
const locationsResult = await pool.query(`
SELECT DISTINCT t.location_id, l.name as location_name
FROM times t
INNER JOIN locations l ON t.location_id = l.id
WHERE t.player_id = $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $2
`, [playerId, currentDate]);
// Hole beste Zeit des Tages
const dailyResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
FROM times t
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $1
`, [currentDate]);
for (const location of locationsResult.rows) {
// Prüfe ob das Achievement heute bereits für diesen Standort vergeben wurde
const alreadyEarnedToday = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND pa.location_id = $3
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
`, [playerId, achievement.id, location.location_id]);
if (parseInt(alreadyEarnedToday.rows[0].count) > 0) continue;
const playerBest = playerResult.rows[0].best_time;
const dailyBest = dailyResult.rows[0].best_time;
// Hole beste Zeit des Spielers heute an diesem Standort
const playerResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
FROM times t
WHERE t.player_id = $1
AND t.location_id = $2
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $3
`, [playerId, location.location_id, currentDate]);
if (playerBest && dailyBest && playerBest === dailyBest) {
await this.awardAchievement(playerId, achievement, 1, newAchievements);
// Hole beste Zeit des Tages an diesem Standort
const dailyResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
FROM times t
WHERE t.location_id = $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $2
`, [location.location_id, currentDate]);
const playerBest = playerResult.rows[0].best_time;
const dailyBest = dailyResult.rows[0].best_time;
if (playerBest && dailyBest && playerBest === dailyBest) {
await this.awardAchievement(playerId, achievement, 1, newAchievements, location.location_id);
console.log(`🏆 Tageskönig Achievement vergeben für Standort: ${location.location_name}`);
}
}
}
/**
* Prüft Wochenchampion Achievement
* Prüft Wochenchampion Achievement pro Standort
*/
async checkWeeklyBest(playerId, currentDate, newAchievements) {
const achievement = Array.from(this.achievements.values())
.find(a => a.category === 'best_time' && a.condition_type === 'weekly_best');
.find(a => a.category === 'time' && a.condition_type === 'best_time_weekly_location');
if (!achievement) return;
// Prüfe ob das Achievement diese Woche bereits vergeben wurde
// Berechne Woche
const currentDateObj = new Date(currentDate);
const dayOfWeek = currentDateObj.getDay();
const weekStart = new Date(currentDateObj);
weekStart.setDate(currentDateObj.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));
const weekStartStr = weekStart.toISOString().split('T')[0];
const alreadyEarnedThisWeek = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') >= $3
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') <= $4
`, [playerId, achievement.id, weekStartStr, currentDate]);
if (parseInt(alreadyEarnedThisWeek.rows[0].count) > 0) return;
// Hole beste Zeit des Spielers diese Woche
const playerResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
// Hole alle Standorte, an denen der Spieler diese Woche gespielt hat
const locationsResult = await pool.query(`
SELECT DISTINCT t.location_id, l.name as location_name
FROM times t
INNER JOIN locations l ON t.location_id = l.id
WHERE t.player_id = $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3
`, [playerId, weekStartStr, currentDate]);
// Hole beste Zeit der Woche
const weeklyResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
FROM times t
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
`, [weekStartStr, currentDate]);
for (const location of locationsResult.rows) {
// Prüfe ob das Achievement diese Woche bereits für diesen Standort vergeben wurde
const alreadyEarnedThisWeek = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND pa.location_id = $3
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') >= $4
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') <= $5
`, [playerId, achievement.id, location.location_id, weekStartStr, currentDate]);
if (parseInt(alreadyEarnedThisWeek.rows[0].count) > 0) continue;
const playerBest = playerResult.rows[0].best_time;
const weeklyBest = weeklyResult.rows[0].best_time;
// Hole beste Zeit des Spielers diese Woche an diesem Standort
const playerResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
FROM times t
WHERE t.player_id = $1
AND t.location_id = $2
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $3
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $4
`, [playerId, location.location_id, weekStartStr, currentDate]);
if (playerBest && weeklyBest && playerBest === weeklyBest) {
await this.awardAchievement(playerId, achievement, 1, newAchievements);
// Hole beste Zeit der Woche an diesem Standort
const weeklyResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
FROM times t
WHERE t.location_id = $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3
`, [location.location_id, weekStartStr, currentDate]);
const playerBest = playerResult.rows[0].best_time;
const weeklyBest = weeklyResult.rows[0].best_time;
if (playerBest && weeklyBest && playerBest === weeklyBest) {
await this.awardAchievement(playerId, achievement, 1, newAchievements, location.location_id);
console.log(`🏆 Wochenchampion Achievement vergeben für Standort: ${location.location_name}`);
}
}
}
/**
* Prüft Monatsmeister Achievement
* Prüft Monatsmeister Achievement pro Standort
*/
async checkMonthlyBest(playerId, currentDate, newAchievements) {
const achievement = Array.from(this.achievements.values())
.find(a => a.category === 'best_time' && a.condition_type === 'monthly_best');
.find(a => a.category === 'time' && a.condition_type === 'best_time_monthly_location');
if (!achievement) return;
@@ -642,41 +684,57 @@ class AchievementSystem {
const currentDateObj = new Date(currentDate);
const monthStart = new Date(currentDateObj.getFullYear(), currentDateObj.getMonth(), 1);
const monthStartStr = monthStart.toISOString().split('T')[0];
// Prüfe ob das Achievement diesen Monat bereits vergeben wurde
const alreadyEarnedThisMonth = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') >= $3
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') <= $4
`, [playerId, achievement.id, monthStartStr, currentDate]);
if (parseInt(alreadyEarnedThisMonth.rows[0].count) > 0) return;
// Hole beste Zeit des Spielers diesen Monat
const playerResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
// Hole alle Standorte, an denen der Spieler diesen Monat gespielt hat
const locationsResult = await pool.query(`
SELECT DISTINCT t.location_id, l.name as location_name
FROM times t
INNER JOIN locations l ON t.location_id = l.id
WHERE t.player_id = $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3
`, [playerId, monthStartStr, currentDate]);
// Hole beste Zeit des Monats
const monthlyResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
FROM times t
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
`, [monthStartStr, currentDate]);
for (const location of locationsResult.rows) {
// Prüfe ob das Achievement diesen Monat bereits für diesen Standort vergeben wurde
const alreadyEarnedThisMonth = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND pa.location_id = $3
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') >= $4
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') <= $5
`, [playerId, achievement.id, location.location_id, monthStartStr, currentDate]);
if (parseInt(alreadyEarnedThisMonth.rows[0].count) > 0) continue;
const playerBest = playerResult.rows[0].best_time;
const monthlyBest = monthlyResult.rows[0].best_time;
// Hole beste Zeit des Spielers diesen Monat an diesem Standort
const playerResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
FROM times t
WHERE t.player_id = $1
AND t.location_id = $2
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $3
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $4
`, [playerId, location.location_id, monthStartStr, currentDate]);
if (playerBest && monthlyBest && playerBest === monthlyBest) {
await this.awardAchievement(playerId, achievement, 1, newAchievements);
// Hole beste Zeit des Monats an diesem Standort
const monthlyResult = await pool.query(`
SELECT MIN(recorded_time) as best_time
FROM times t
WHERE t.location_id = $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3
`, [location.location_id, monthStartStr, currentDate]);
const playerBest = playerResult.rows[0].best_time;
const monthlyBest = monthlyResult.rows[0].best_time;
if (playerBest && monthlyBest && playerBest === monthlyBest) {
await this.awardAchievement(playerId, achievement, 1, newAchievements, location.location_id);
console.log(`🏆 Monatsmeister Achievement vergeben für Standort: ${location.location_name}`);
}
}
}
@@ -684,12 +742,12 @@ class AchievementSystem {
* Vergibt ein Achievement an einen Spieler
* Erstellt immer einen neuen Eintrag (keine Updates mehr)
*/
async awardAchievement(playerId, achievement, progress, newAchievements) {
async awardAchievement(playerId, achievement, progress, newAchievements, locationId = null) {
try {
await pool.query(`
INSERT INTO player_achievements (player_id, achievement_id, progress, is_completed, earned_at)
VALUES ($1, $2, $3, true, NOW())
`, [playerId, achievement.id, progress]);
INSERT INTO player_achievements (player_id, achievement_id, progress, is_completed, earned_at, location_id)
VALUES ($1, $2, $3, true, NOW(), $4)
`, [playerId, achievement.id, progress, locationId]);
newAchievements.push({
id: achievement.id,
@@ -697,10 +755,12 @@ class AchievementSystem {
description: achievement.description,
icon: achievement.icon,
points: achievement.points,
progress: progress
progress: progress,
locationId: locationId
});
console.log(`🏆 Achievement vergeben: ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`);
const locationText = locationId ? ` (Standort: ${locationId})` : '';
console.log(`🏆 Achievement vergeben: ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)${locationText}`);
} catch (error) {
console.error(`❌ Fehler beim Vergeben des Achievements ${achievement.name}:`, error);
}

View File

@@ -137,6 +137,52 @@ body {
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
}
/* Push notification button */
.btn-push {
background: #10b981;
color: white;
position: relative;
}
.btn-push:hover {
background: #059669;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
.btn-push.active {
background: #ef4444;
}
.btn-push.active:hover {
background: #dc2626;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}
.btn-push::after {
content: '';
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
background: #fbbf24;
border-radius: 50%;
opacity: 0;
transition: opacity 0.3s ease;
}
.btn-push.active::after {
opacity: 1;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
/* Logout button */
.btn-logout {
background: #dc2626;

View File

@@ -29,42 +29,259 @@
});
}
// Request notification permission on page load
if ('Notification' in window) {
if (Notification.permission === 'default') {
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
console.log('✅ Notification permission granted');
// Subscribe to push notifications
subscribeToPush();
} else {
console.log('❌ Notification permission denied');
}
});
}
}
// Don't automatically request notification permission
// User must click the button to enable push notifications
// Convert VAPID key from base64url to Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// Convert ArrayBuffer to Base64 string
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
// Push notification state
let pushSubscription = null;
let pushEnabled = false;
// Subscribe to push notifications
async function subscribeToPush() {
try {
console.log('🔔 Starting push subscription...');
const registration = await navigator.serviceWorker.ready;
const vapidPublicKey = 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds';
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds'
applicationServerKey: applicationServerKey
});
// Send subscription to server
await fetch('/api/v1/public/subscribe', {
pushSubscription = subscription;
// Generate or get player ID
let playerId = window.currentPlayerId;
if (!playerId) {
// Generate a UUID for the player
playerId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
console.log(`📱 Generated player ID: ${playerId}`);
} else {
console.log(`📱 Using existing player ID: ${playerId}`);
}
// Convert ArrayBuffer keys to Base64 strings
const p256dhKey = subscription.getKey('p256dh');
const authKey = subscription.getKey('auth');
// Convert ArrayBuffer to Base64 URL-safe string
const p256dhString = arrayBufferToBase64(p256dhKey);
const authString = arrayBufferToBase64(authKey);
console.log('📱 Converted keys to Base64 strings');
console.log('📱 p256dh length:', p256dhString.length);
console.log('📱 auth length:', authString.length);
// Send subscription to server with player ID
const response = await fetch('/api/v1/public/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(subscription)
body: JSON.stringify({
endpoint: subscription.endpoint,
keys: {
p256dh: p256dhString,
auth: authString
},
playerId: playerId
})
});
console.log('✅ Push subscription successful');
const result = await response.json();
if (result.success) {
pushEnabled = true;
updatePushButton();
console.log('✅ Push subscription successful');
// Store player ID for notifications
if (result.playerId) {
localStorage.setItem('pushPlayerId', result.playerId);
}
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('❌ Push subscription failed:', error);
pushEnabled = false;
updatePushButton();
}
}
// Unsubscribe from push notifications
async function unsubscribeFromPush() {
try {
console.log('🔕 Unsubscribing from push notifications...');
// Get player ID from localStorage
const playerId = localStorage.getItem('pushPlayerId');
if (pushSubscription) {
await pushSubscription.unsubscribe();
pushSubscription = null;
console.log('✅ Local push subscription removed');
}
// Notify server to remove from database
if (playerId) {
try {
const response = await fetch('/api/v1/public/unsubscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ playerId: playerId })
});
const result = await response.json();
if (result.success) {
console.log('✅ Server notified - subscription removed from database');
} else {
console.warn('⚠️ Server notification failed:', result.message);
}
} catch (error) {
console.warn('⚠️ Failed to notify server:', error);
}
}
// Clear stored player ID
localStorage.removeItem('pushPlayerId');
pushEnabled = false;
updatePushButton();
console.log('🔕 Push notifications disabled');
} catch (error) {
console.error('❌ Push unsubscribe failed:', error);
pushEnabled = false;
updatePushButton();
}
}
// Toggle push notifications
async function togglePushNotifications() {
if (pushEnabled) {
await unsubscribeFromPush();
} else {
// Check notification permission first
if (Notification.permission === 'denied') {
alert('Push-Benachrichtigungen sind blockiert. Bitte erlaube sie in den Browser-Einstellungen.');
return;
}
if (Notification.permission === 'default') {
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
alert('Push-Benachrichtigungen wurden nicht erlaubt.');
return;
}
}
await subscribeToPush();
}
}
// Update push button appearance
function updatePushButton() {
const button = document.getElementById('pushButton');
if (!button) {
console.log('❌ Push button not found in DOM');
return;
}
console.log(`🔔 Updating push button - Status: ${pushEnabled ? 'ENABLED' : 'DISABLED'}`);
if (pushEnabled) {
button.classList.add('active');
button.setAttribute('data-de', '🔕 Push deaktivieren');
button.setAttribute('data-en', '🔕 Disable Push');
button.textContent = '🔕 Push deaktivieren';
console.log('✅ Button updated to: Push deaktivieren (RED)');
} else {
button.classList.remove('active');
button.setAttribute('data-de', '🔔 Push aktivieren');
button.setAttribute('data-en', '🔔 Enable Push');
button.textContent = '🔔 Push aktivieren';
console.log('✅ Button updated to: Push aktivieren (GREEN)');
}
}
// Check existing push subscription on page load
async function checkPushStatus() {
try {
console.log('🔍 Checking push status...');
if (!('serviceWorker' in navigator)) {
console.log('❌ Service Worker not supported');
pushEnabled = false;
updatePushButton();
return;
}
if (!('PushManager' in window)) {
console.log('❌ Push Manager not supported');
pushEnabled = false;
updatePushButton();
return;
}
const registration = await navigator.serviceWorker.ready;
console.log('✅ Service Worker ready');
const subscription = await registration.pushManager.getSubscription();
console.log('📱 Current subscription:', subscription ? 'EXISTS' : 'NONE');
if (subscription) {
pushSubscription = subscription;
pushEnabled = true;
updatePushButton();
console.log('✅ Existing push subscription found and activated');
// Also check if we have a stored player ID
const storedPlayerId = localStorage.getItem('pushPlayerId');
if (storedPlayerId) {
console.log(`📱 Push subscription linked to player: ${storedPlayerId}`);
}
} else {
pushEnabled = false;
updatePushButton();
console.log(' No existing push subscription found');
}
} catch (error) {
console.error('❌ Error checking push status:', error);
pushEnabled = false;
updatePushButton();
}
}
</script>
@@ -84,6 +301,7 @@
<div class="user-avatar" id="userAvatar">U</div>
<span id="userEmail">user@example.com</span>
</div>
<button class="btn btn-push" id="pushButton" onclick="togglePushNotifications()" data-de="🔔 Push aktivieren" data-en="🔔 Enable Push">🔔 Push aktivieren</button>
<a href="/" class="btn btn-primary" data-de="Zurück zu Zeiten" data-en="Back to Times">Back to Times</a>
<button class="btn btn-logout" onclick="logout()" data-de="Logout" data-en="Logout">Logout</button>
</div>

View File

@@ -324,6 +324,9 @@ document.addEventListener('DOMContentLoaded', function () {
loadLanguagePreference();
changeLanguage(); // Apply saved language
initDashboard();
// Push notifications are now handled manually via the button
// No automatic subscription on page load
});
// Modal functions
@@ -1739,6 +1742,25 @@ function initializeAchievements(playerId) {
loadPlayerAchievements();
}
// Convert VAPID key from base64url to Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// Push notifications are now handled in dashboard.html
// This function has been moved to the HTML file for better integration
// Web Notification Functions
function showWebNotification(title, message, icon = '🏆') {
if ('Notification' in window && Notification.permission === 'granted') {
@@ -1763,9 +1785,23 @@ function showWebNotification(title, message, icon = '🏆') {
}
}
// Track which notifications have been sent today
let notificationsSentToday = {
daily: false,
weekly: false,
monthly: false
};
// Check for best time achievements and show notifications
async function checkBestTimeNotifications() {
try {
// Check if push notifications are enabled
const pushPlayerId = localStorage.getItem('pushPlayerId');
if (!pushPlayerId) {
console.log('🔕 Push notifications disabled, skipping best time check');
return;
}
const response = await fetch('/api/v1/public/best-times');
const result = await response.json();
@@ -1774,28 +1810,95 @@ async function checkBestTimeNotifications() {
// Check if current player has best times
if (currentPlayerId) {
if (daily && daily.player_id === currentPlayerId) {
const title = currentLanguage === 'de' ? '🏆 Tageskönig!' : '🏆 Daily King!';
const message = currentLanguage === 'de' ?
`Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!` :
`Congratulations! You achieved the best time of the day with ${daily.best_time}!`;
showWebNotification(title, message, '👑');
const now = new Date();
const isEvening = now.getHours() >= 19;
// Check daily best time (only in the evening, only once per day)
if (daily && daily.player_id === currentPlayerId && isEvening) {
// Check if notification was already sent today
const dailyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/daily_best`);
const dailyResult = await dailyCheck.json();
if (!dailyResult.wasSent) {
const title = currentLanguage === 'de' ? '🏆 Tageskönig!' : '🏆 Daily King!';
const message = currentLanguage === 'de' ?
`Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!` :
`Congratulations! You achieved the best time of the day with ${daily.best_time}!`;
showWebNotification(title, message, '👑');
// Mark as sent in database
await fetch('/api/v1/public/notification-sent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: currentPlayerId,
notificationType: 'daily_best'
})
});
console.log('🏆 Daily best notification sent');
}
}
// Check weekly best time (only on Sunday evening, only once per week)
if (weekly && weekly.player_id === currentPlayerId) {
const title = currentLanguage === 'de' ? '🏆 Wochenchampion!' : '🏆 Weekly Champion!';
const message = currentLanguage === 'de' ?
`Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!` :
`Fantastic! You are the weekly champion with ${weekly.best_time}!`;
showWebNotification(title, message, '🏆');
const isSunday = now.getDay() === 0; // 0 = Sunday
if (isSunday && isEvening) {
// Check if notification was already sent this week
const weeklyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/weekly_best`);
const weeklyResult = await weeklyCheck.json();
if (!weeklyResult.wasSent) {
const title = currentLanguage === 'de' ? '🏆 Wochenchampion!' : '🏆 Weekly Champion!';
const message = currentLanguage === 'de' ?
`Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!` :
`Fantastic! You are the weekly champion with ${weekly.best_time}!`;
showWebNotification(title, message, '🏆');
// Mark as sent in database
await fetch('/api/v1/public/notification-sent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: currentPlayerId,
notificationType: 'weekly_best'
})
});
console.log('🏆 Weekly best notification sent');
}
}
}
// Check monthly best time (only on last evening of month at 19:00, only once per month)
if (monthly && monthly.player_id === currentPlayerId) {
const title = currentLanguage === 'de' ? '🏆 Monatsmeister!' : '🏆 Monthly Master!';
const message = currentLanguage === 'de' ?
`Unglaublich! Du bist der Monatsmeister mit ${monthly.best_time}!` :
`Incredible! You are the monthly master with ${monthly.best_time}!`;
showWebNotification(title, message, '🥇');
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const isLastDayOfMonth = now.getDate() === lastDayOfMonth.getDate();
// Only show monthly notification on last day of month at 19:00 or later
if (isLastDayOfMonth && isEvening) {
// Check if notification was already sent this month
const monthlyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/monthly_best`);
const monthlyResult = await monthlyCheck.json();
if (!monthlyResult.wasSent) {
const title = currentLanguage === 'de' ? '🏆 Monatsmeister!' : '🏆 Monthly Master!';
const message = currentLanguage === 'de' ?
`Unglaublich! Du bist der Monatsmeister mit ${monthly.best_time}!` :
`Incredible! You are the monthly master with ${monthly.best_time}!`;
showWebNotification(title, message, '🥇');
// Mark as sent in database
await fetch('/api/v1/public/notification-sent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: currentPlayerId,
notificationType: 'monthly_best'
})
});
console.log('🏆 Monthly best notification sent');
}
}
}
}
}
@@ -1809,6 +1912,13 @@ async function checkAchievementNotifications() {
try {
if (!currentPlayerId) return;
// Check if push notifications are enabled
const pushPlayerId = localStorage.getItem('pushPlayerId');
if (!pushPlayerId) {
console.log('🔕 Push notifications disabled, skipping achievement check');
return;
}
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
const result = await response.json();
@@ -1821,14 +1931,33 @@ async function checkAchievementNotifications() {
});
if (newAchievements.length > 0) {
newAchievements.forEach(achievement => {
const translatedAchievement = translateAchievement(achievement);
showWebNotification(
`🏆 ${translatedAchievement.name}`,
translatedAchievement.description,
achievement.icon || '🏆'
);
});
for (const achievement of newAchievements) {
// Check if notification was already sent for this achievement
const achievementCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/achievement?achievementId=${achievement.achievement_id}&locationId=${achievement.location_id || ''}`);
const achievementResult = await achievementCheck.json();
if (!achievementResult.wasSent) {
const translatedAchievement = translateAchievement(achievement);
showWebNotification(
`🏆 ${translatedAchievement.name}`,
translatedAchievement.description,
achievement.icon || '🏆'
);
// Mark as sent in database
await fetch('/api/v1/public/notification-sent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: currentPlayerId,
notificationType: 'achievement',
achievementId: achievement.achievement_id,
locationId: achievement.location_id || null
})
});
console.log(`🏆 Achievement notification sent: ${achievement.name}`);
}
}
}
}
} catch (error) {
@@ -1967,4 +2096,9 @@ document.addEventListener('DOMContentLoaded', function () {
}
});
}
// Check push notification status on dashboard load
if (typeof checkPushStatus === 'function') {
checkPushStatus();
}
});

View File

@@ -2,22 +2,65 @@
const CACHE_NAME = 'ninjacross-v1';
const urlsToCache = [
'/',
'/index.html',
'/css/leaderboard.css',
'/js/leaderboard.js',
'/pictures/favicon.ico'
'/test-push.html',
'/sw.js'
];
// Install event
self.addEventListener('install', function(event) {
console.log('Service Worker installing...');
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
// Add files one by one to handle failures gracefully
return Promise.allSettled(
urlsToCache.map(url =>
cache.add(url).catch(err => {
console.warn(`Failed to cache ${url}:`, err);
return null; // Continue with other files
})
)
);
})
.then(() => {
console.log('Service Worker installation completed');
// Skip waiting to activate immediately
return self.skipWaiting();
})
.catch(err => {
console.error('Service Worker installation failed:', err);
// Still try to skip waiting
return self.skipWaiting();
})
);
});
// Activate event
self.addEventListener('activate', function(event) {
console.log('Service Worker activating...');
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
}).then(() => {
// Take control of all clients immediately
return self.clients.claim();
})
);
});
// Listen for skip waiting message
self.addEventListener('message', function(event) {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// Fetch event
self.addEventListener('fetch', function(event) {
event.respondWith(

View File

@@ -105,6 +105,7 @@
<input type="text" id="testMessage" placeholder="Test-Nachricht" value="Das ist eine Test-Push-Notification!">
<button onclick="sendTestPush()">Test-Push senden</button>
<button onclick="sendTestWebNotification()">Web-Notification senden</button>
<button onclick="sendWindowsNotification()">Windows-Notification senden</button>
</div>
<div>
@@ -120,6 +121,22 @@
<script>
let currentSubscription = null;
// Convert VAPID key from base64url to Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
function log(message, type = 'info') {
const logDiv = document.getElementById('log');
const timestamp = new Date().toLocaleTimeString();
@@ -155,14 +172,19 @@
async function registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
log('Service Worker wird registriert...', 'info');
const registration = await navigator.serviceWorker.register('/sw.js');
log('Service Worker erfolgreich registriert', 'success');
log(`SW Scope: ${registration.scope}`);
updateStatus('Service Worker registriert', 'success');
} catch (error) {
log(`Service Worker Registrierung fehlgeschlagen: ${error.message}`, 'error');
log(`Error Details: ${JSON.stringify(error)}`, 'error');
updateStatus(`Service Worker Registrierung fehlgeschlagen: ${error.message}`, 'error');
}
} else {
log('Service Worker nicht unterstützt', 'error');
updateStatus('Service Worker nicht unterstützt', 'error');
}
}
@@ -189,16 +211,115 @@
// Push Subscription Functions
async function subscribeToPush() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
log('Push Subscription gestartet...', 'info');
// Check basic requirements
if (!('serviceWorker' in navigator)) {
log('Service Worker nicht unterstützt', 'error');
updateStatus('Service Worker nicht unterstützt', 'error');
return;
}
if (!('PushManager' in window)) {
log('Push Manager nicht unterstützt', 'error');
updateStatus('Push Manager nicht unterstützt', 'error');
return;
}
// Check notification permission first
if (Notification.permission !== 'granted') {
log('Notification Permission nicht erteilt. Bitte zuerst "Berechtigung anfordern" klicken!', 'error');
updateStatus('Notification Permission erforderlich', 'error');
return;
}
try {
const registration = await navigator.serviceWorker.ready;
log('Service Worker wird geladen...', 'info');
// First check if service worker is already registered
let registration;
const existingRegistrations = await navigator.serviceWorker.getRegistrations();
if (existingRegistrations.length > 0) {
log('Service Worker bereits registriert, verwende bestehende...', 'info');
registration = existingRegistrations[0];
} else {
log('Service Worker nicht registriert, registriere jetzt...', 'info');
registration = await navigator.serviceWorker.register('/sw.js');
log('Service Worker registriert', 'success');
}
// Wait for service worker to be ready with timeout
log('Warte auf Service Worker ready...', 'info');
// Check if service worker is active
if (registration.active) {
log('Service Worker ist bereits aktiv', 'success');
} else if (registration.installing && registration.installing.state) {
log('Service Worker wird installiert, warte...', 'info');
await new Promise((resolve) => {
const installingWorker = registration.installing;
if (installingWorker) {
installingWorker.addEventListener('statechange', () => {
if (installingWorker.state === 'installed') {
resolve();
}
});
} else {
resolve();
}
});
log('Service Worker Installation abgeschlossen', 'success');
} else if (registration.waiting && registration.waiting.state) {
log('Service Worker wartet, aktiviere...', 'info');
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
await new Promise((resolve) => {
const waitingWorker = registration.waiting;
if (waitingWorker) {
waitingWorker.addEventListener('statechange', () => {
if (waitingWorker.state === 'activated') {
resolve();
}
});
} else {
resolve();
}
});
log('Service Worker aktiviert', 'success');
} else {
log('Service Worker Status unbekannt, warte auf ready...', 'info');
try {
await navigator.serviceWorker.ready;
log('Service Worker bereit', 'success');
} catch (error) {
log(`Service Worker ready fehlgeschlagen: ${error.message}`, 'error');
throw error;
}
}
// Convert VAPID key from base64url to ArrayBuffer
const vapidPublicKey = 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds';
log('VAPID Key wird konvertiert...', 'info');
let applicationServerKey;
try {
applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
log('VAPID Key konvertiert', 'success');
} catch (error) {
log(`VAPID Key Konvertierung fehlgeschlagen: ${error.message}`, 'error');
throw error;
}
log('Push Subscription wird erstellt...', 'info');
// Check if push manager is available
if (!registration.pushManager) {
throw new Error('Push Manager nicht verfügbar in diesem Service Worker');
}
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds'
applicationServerKey: applicationServerKey
});
currentSubscription = subscription;
@@ -206,6 +327,7 @@
log(`Endpoint: ${subscription.endpoint.substring(0, 50)}...`);
// Send to server
log('Subscription wird an Server gesendet...', 'info');
const response = await fetch('/api/v1/public/subscribe', {
method: 'POST',
headers: {
@@ -214,17 +336,32 @@
body: JSON.stringify(subscription)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.success) {
log('Subscription erfolgreich an Server gesendet', 'success');
log(`Player ID: ${result.playerId || 'anonymous'}`, 'success');
// Store the player ID for later use
if (result.playerId) {
localStorage.setItem('pushPlayerId', result.playerId);
}
updateStatus('Push Subscription erfolgreich!', 'success');
// Store the subscription endpoint for later use
localStorage.setItem('pushSubscriptionEndpoint', subscription.endpoint);
} else {
log(`Server-Fehler: ${result.message}`, 'error');
updateStatus(`Server-Fehler: ${result.message}`, 'error');
}
} catch (error) {
log(`Push Subscription fehlgeschlagen: ${error.message}`, 'error');
log(`Error Details: ${JSON.stringify(error)}`, 'error');
updateStatus(`Push Subscription fehlgeschlagen: ${error.message}`, 'error');
}
}
@@ -243,42 +380,53 @@
}
async function checkSubscription() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
currentSubscription = subscription;
log('Aktive Push Subscription gefunden', 'success');
log(`Endpoint: ${subscription.endpoint.substring(0, 50)}...`);
} else {
log('Keine Push Subscription gefunden', 'warning');
}
} catch (error) {
log(`Subscription Check fehlgeschlagen: ${error.message}`, 'error');
log('Überprüfe Push Subscription...', 'info');
if (!('serviceWorker' in navigator)) {
log('Service Worker nicht unterstützt', 'error');
return;
}
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
currentSubscription = subscription;
log('Aktive Push Subscription gefunden', 'success');
log(`Endpoint: ${subscription.endpoint.substring(0, 50)}...`);
updateStatus('Push Subscription aktiv', 'success');
} else {
log('Keine Push Subscription gefunden', 'warning');
updateStatus('Keine Push Subscription gefunden', 'warning');
}
} catch (error) {
log(`Subscription Check fehlgeschlagen: ${error.message}`, 'error');
updateStatus(`Subscription Check fehlgeschlagen: ${error.message}`, 'error');
}
}
// Test Functions
async function sendTestPush() {
const message = document.getElementById('testMessage').value;
log('Test-Push wird gesendet...', 'info');
// First check if we have a subscription
if (!currentSubscription) {
log('Keine Push Subscription gefunden. Bitte zuerst "Push abonnieren" klicken!', 'error');
updateStatus('Keine Push Subscription gefunden', 'error');
return;
}
// Use the stored subscription endpoint as identifier
const storedEndpoint = localStorage.getItem('pushSubscriptionEndpoint');
// Use the stored player ID from subscription
const storedPlayerId = localStorage.getItem('pushPlayerId');
let userId = 'test-user';
if (storedEndpoint) {
// Use the endpoint as a unique identifier
userId = storedEndpoint.split('/').pop().substring(0, 8);
if (storedPlayerId) {
userId = storedPlayerId;
}
log(`Sende Test-Push an Player ID: ${userId}`, 'info');
try {
const response = await fetch('/api/v1/public/test-push', {
method: 'POST',
@@ -295,11 +443,14 @@
if (result.success) {
log('Test-Push erfolgreich gesendet', 'success');
log(`An User ID: ${userId}`, 'success');
updateStatus('Test-Push erfolgreich gesendet!', 'success');
} else {
log(`Test-Push fehlgeschlagen: ${result.message}`, 'error');
updateStatus(`Test-Push fehlgeschlagen: ${result.message}`, 'error');
}
} catch (error) {
log(`Test-Push Fehler: ${error.message}`, 'error');
updateStatus(`Test-Push Fehler: ${error.message}`, 'error');
}
}
@@ -309,7 +460,10 @@
const notification = new Notification('🧪 Test Web Notification', {
body: message,
icon: '/pictures/icon-192.png',
badge: '/pictures/icon-192.png'
badge: '/pictures/icon-192.png',
tag: 'test-notification',
requireInteraction: true,
silent: false
});
notification.onclick = function() {
@@ -317,12 +471,52 @@
notification.close();
};
// Auto-close after 10 seconds
setTimeout(() => {
notification.close();
}, 10000);
log('Web-Notification gesendet', 'success');
} else {
log('Web-Notifications nicht verfügbar oder nicht erlaubt', 'error');
}
}
// Windows Desktop Notification (falls verfügbar)
function sendWindowsNotification() {
if ('Notification' in window && Notification.permission === 'granted') {
const message = document.getElementById('testMessage').value;
// Erstelle eine Windows-ähnliche Notification
const notification = new Notification('🏆 Ninja Cross - Achievement!', {
body: message,
icon: '/pictures/icon-192.png',
badge: '/pictures/icon-192.png',
tag: 'ninja-cross-achievement',
requireInteraction: true,
silent: false,
data: {
type: 'achievement',
timestamp: Date.now()
}
});
notification.onclick = function() {
window.focus();
notification.close();
};
// Auto-close after 15 seconds
setTimeout(() => {
notification.close();
}, 15000);
log('Windows-ähnliche Notification gesendet', 'success');
} else {
log('Web-Notifications nicht verfügbar oder nicht erlaubt', 'error');
}
}
async function getPushStatus() {
try {
const response = await fetch('/api/v1/public/push-status');
@@ -340,11 +534,27 @@
// Initialize
window.addEventListener('load', function() {
console.log('Push Notification Test Seite geladen');
log('Push Notification Test Seite geladen');
// Check if we're on HTTPS
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
log('WARNUNG: Push Notifications funktionieren nur über HTTPS!', 'error');
updateStatus('HTTPS erforderlich für Push Notifications', 'error');
} else {
log('HTTPS-Verbindung erkannt - Push Notifications möglich', 'success');
}
checkServiceWorker();
checkPermission();
checkSubscription();
});
// Also initialize on DOMContentLoaded as backup
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded');
log('DOM Content Loaded - Initialisierung gestartet');
});
</script>
</body>
</html>

View File

@@ -2848,12 +2848,33 @@ router.get('/v1/public/best-times', async (req, res) => {
// Subscribe to push notifications
router.post('/v1/public/subscribe', async (req, res) => {
try {
const { endpoint, keys } = req.body;
console.log('📱 Push subscription request received:', JSON.stringify(req.body, null, 2));
const { endpoint, keys, playerId: requestPlayerId } = req.body;
const userId = req.session.userId || 'anonymous';
// Generate a UUID for anonymous users or use existing UUID
// Validate required fields
if (!endpoint) {
console.error('❌ Missing endpoint in request');
return res.status(400).json({
success: false,
message: 'Endpoint ist erforderlich'
});
}
if (!keys || !keys.p256dh || !keys.auth) {
console.error('❌ Missing keys in request:', keys);
return res.status(400).json({
success: false,
message: 'Push-Keys sind erforderlich'
});
}
// Use playerId from request if provided, otherwise generate one
let playerId;
if (userId === 'anonymous') {
if (requestPlayerId) {
playerId = requestPlayerId;
} else if (userId === 'anonymous') {
// Generate a random UUID for anonymous users
const { v4: uuidv4 } = require('uuid');
playerId = uuidv4();
@@ -2861,6 +2882,8 @@ router.post('/v1/public/subscribe', async (req, res) => {
playerId = userId;
}
console.log(`📱 Processing subscription for player: ${playerId}`);
// Store subscription in database
await pool.query(`
INSERT INTO player_subscriptions (player_id, endpoint, p256dh, auth, created_at)
@@ -2890,7 +2913,8 @@ router.post('/v1/public/subscribe', async (req, res) => {
res.json({
success: true,
message: 'Push subscription erfolgreich gespeichert'
message: 'Push subscription erfolgreich gespeichert',
playerId: playerId
});
} catch (error) {
console.error('Error storing push subscription:', error);
@@ -2902,6 +2926,42 @@ router.post('/v1/public/subscribe', async (req, res) => {
}
});
// Unsubscribe from push notifications
router.post('/v1/public/unsubscribe', async (req, res) => {
try {
const { playerId } = req.body;
if (!playerId) {
return res.status(400).json({
success: false,
message: 'Player ID erforderlich'
});
}
// Remove from push service
pushService.unsubscribe(playerId);
// Remove from database
await pool.query(`
DELETE FROM player_subscriptions
WHERE player_id = $1
`, [playerId]);
console.log(`User ${playerId} unsubscribed from push notifications`);
res.json({
success: true,
message: 'Push subscription erfolgreich entfernt'
});
} catch (error) {
console.error('Error removing push subscription:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Entfernen der Push Subscription'
});
}
});
// Test push notification endpoint
router.post('/v1/public/test-push', async (req, res) => {
try {
@@ -2960,6 +3020,63 @@ router.get('/v1/public/push-status', async (req, res) => {
}
});
// Check if notification was already sent today
router.get('/v1/public/notification-sent/:playerId/:type', async (req, res) => {
try {
const { playerId, type } = req.params;
const { achievementId, locationId } = req.query;
const today = new Date().toISOString().split('T')[0];
const result = await pool.query(`
SELECT COUNT(*) as count
FROM sent_notifications
WHERE player_id = $1
AND notification_type = $2
AND (achievement_id = $3 OR ($3 IS NULL AND achievement_id IS NULL))
AND (location_id = $4 OR ($4 IS NULL AND location_id IS NULL))
AND DATE(sent_at) = $5
`, [playerId, type, achievementId || null, locationId || null, today]);
const wasSent = parseInt(result.rows[0].count) > 0;
res.json({
success: true,
wasSent: wasSent
});
} catch (error) {
console.error('Error checking notification status:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Prüfen des Notification-Status'
});
}
});
// Mark notification as sent
router.post('/v1/public/notification-sent', async (req, res) => {
try {
const { playerId, notificationType, achievementId, locationId } = req.body;
await pool.query(`
INSERT INTO sent_notifications (player_id, notification_type, achievement_id, location_id)
VALUES ($1, $2, $3, $4)
ON CONFLICT DO NOTHING
`, [playerId, notificationType, achievementId || null, locationId || null]);
res.json({
success: true,
message: 'Notification als gesendet markiert'
});
} catch (error) {
console.error('Error marking notification as sent:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Markieren der Notification'
});
}
});
// ==================== ANALYTICS HELPER FUNCTIONS ====================
async function getPerformanceTrends(playerId) {