Diverse änderungen am Push system
This commit is contained in:
@@ -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]);
|
||||
|
||||
const playerBest = playerResult.rows[0].best_time;
|
||||
const dailyBest = dailyResult.rows[0].best_time;
|
||||
if (parseInt(alreadyEarnedToday.rows[0].count) > 0) continue;
|
||||
|
||||
if (playerBest && dailyBest && playerBest === dailyBest) {
|
||||
await this.awardAchievement(playerId, achievement, 1, newAchievements);
|
||||
// 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]);
|
||||
|
||||
// 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]);
|
||||
|
||||
const playerBest = playerResult.rows[0].best_time;
|
||||
const weeklyBest = weeklyResult.rows[0].best_time;
|
||||
if (parseInt(alreadyEarnedThisWeek.rows[0].count) > 0) continue;
|
||||
|
||||
if (playerBest && weeklyBest && playerBest === weeklyBest) {
|
||||
await this.awardAchievement(playerId, achievement, 1, newAchievements);
|
||||
// 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]);
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -643,40 +685,56 @@ class AchievementSystem {
|
||||
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]);
|
||||
|
||||
const playerBest = playerResult.rows[0].best_time;
|
||||
const monthlyBest = monthlyResult.rows[0].best_time;
|
||||
if (parseInt(alreadyEarnedThisMonth.rows[0].count) > 0) continue;
|
||||
|
||||
if (playerBest && monthlyBest && playerBest === monthlyBest) {
|
||||
await this.awardAchievement(playerId, achievement, 1, newAchievements);
|
||||
// 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]);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
53
public/sw.js
53
public/sw.js
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
log('Überprüfe Push Subscription...', 'info');
|
||||
|
||||
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');
|
||||
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>
|
||||
|
||||
125
routes/api.js
125
routes/api.js
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user