Achivement System
This commit is contained in:
217
ACHIEVEMENTS.md
Normal file
217
ACHIEVEMENTS.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# 🏆 Ninja Cross Parkour Achievement System
|
||||||
|
|
||||||
|
Ein umfassendes Achievement-System für das Ninja Cross Parkour im Schwimmbad.
|
||||||
|
|
||||||
|
## 📊 System-Übersicht
|
||||||
|
|
||||||
|
Das Achievement-System besteht aus:
|
||||||
|
- **32 verschiedene Achievements** in 4 Kategorien
|
||||||
|
- **Automatische tägliche Vergabe** am Ende des Tages
|
||||||
|
- **REST API Endpoints** für Frontend-Integration
|
||||||
|
- **PostgreSQL Funktionen** für effiziente Verarbeitung
|
||||||
|
|
||||||
|
## 🎯 Achievement-Kategorien
|
||||||
|
|
||||||
|
### 1. Konsistenz-basierte Achievements
|
||||||
|
- **Erste Schritte** 👶 - Erste Zeit aufgezeichnet (5 Punkte)
|
||||||
|
- **Durchhalter** 💪 - 3 Versuche an einem Tag (10 Punkte)
|
||||||
|
- **Fleißig** 🔥 - 5 Versuche an einem Tag (15 Punkte)
|
||||||
|
- **Besessen** 😤 - 10 Versuche an einem Tag (25 Punkte)
|
||||||
|
- **Regelmäßig** 📅 - 5 verschiedene Tage gespielt (20 Punkte)
|
||||||
|
- **Stammgast** ⭐ - 10 verschiedene Tage gespielt (30 Punkte)
|
||||||
|
- **Treue** 💎 - 20 verschiedene Tage gespielt (50 Punkte)
|
||||||
|
- **Veteran** 🏆 - 50 verschiedene Tage gespielt (100 Punkte)
|
||||||
|
|
||||||
|
### 2. Verbesserungs-basierte Achievements
|
||||||
|
- **Fortschritt** 📈 - Persönliche Bestzeit um 5 Sekunden verbessert (15 Punkte)
|
||||||
|
- **Durchbruch** ⚡ - Persönliche Bestzeit um 10 Sekunden verbessert (25 Punkte)
|
||||||
|
- **Transformation** 🔄 - Persönliche Bestzeit um 15 Sekunden verbessert (40 Punkte)
|
||||||
|
- **Perfektionist** ✨ - Persönliche Bestzeit um 20 Sekunden verbessert (60 Punkte)
|
||||||
|
|
||||||
|
### 3. Saisonale Achievements
|
||||||
|
- **Wochenend-Krieger** 🏁 - Am Wochenende gespielt (10 Punkte)
|
||||||
|
- **Nachmittags-Sportler** ☀️ - Zwischen 14-18 Uhr gespielt (10 Punkte)
|
||||||
|
- **Frühaufsteher** 🌅 - Vor 10 Uhr gespielt (15 Punkte)
|
||||||
|
- **Abend-Sportler** 🌙 - Nach 18 Uhr gespielt (10 Punkte)
|
||||||
|
|
||||||
|
### 4. Monatliche Achievements
|
||||||
|
- **Januar-Krieger** ❄️ bis **Dezember-Dynamo** 🎄 (je 20 Punkte)
|
||||||
|
|
||||||
|
### 5. Jahreszeiten-Achievements
|
||||||
|
- **Frühjahrs-Fighter** 🌱 - Im Frühling gespielt (30 Punkte)
|
||||||
|
- **Sommer-Sportler** ☀️ - Im Sommer gespielt (30 Punkte)
|
||||||
|
- **Herbst-Held** 🍂 - Im Herbst gespielt (30 Punkte)
|
||||||
|
- **Winter-Warrior** ❄️ - Im Winter gespielt (30 Punkte)
|
||||||
|
|
||||||
|
## 🗄️ Datenbank-Schema
|
||||||
|
|
||||||
|
### Tabelle: `achievements`
|
||||||
|
```sql
|
||||||
|
- id (uuid, PK)
|
||||||
|
- name (varchar) - Achievement-Name
|
||||||
|
- description (text) - Beschreibung
|
||||||
|
- category (varchar) - Kategorie
|
||||||
|
- condition_type (varchar) - Bedingungstyp
|
||||||
|
- condition_value (integer) - Bedingungswert
|
||||||
|
- icon (varchar) - Emoji-Icon
|
||||||
|
- points (integer) - Punkte
|
||||||
|
- is_active (boolean) - Aktiv
|
||||||
|
- created_at (timestamp)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabelle: `player_achievements`
|
||||||
|
```sql
|
||||||
|
- id (uuid, PK)
|
||||||
|
- player_id (uuid, FK) - Verweis auf players.id
|
||||||
|
- achievement_id (uuid, FK) - Verweis auf achievements.id
|
||||||
|
- earned_at (timestamp) - Wann erreicht
|
||||||
|
- progress (integer) - Fortschritt
|
||||||
|
- is_completed (boolean) - Abgeschlossen
|
||||||
|
- created_at (timestamp)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 PostgreSQL Funktionen
|
||||||
|
|
||||||
|
### `check_consistency_achievements(player_uuid)`
|
||||||
|
Überprüft alle Konsistenz-basierten Achievements für einen Spieler.
|
||||||
|
|
||||||
|
### `check_improvement_achievements(player_uuid)`
|
||||||
|
Überprüft alle Verbesserungs-basierten Achievements für einen Spieler.
|
||||||
|
|
||||||
|
### `check_seasonal_achievements(player_uuid)`
|
||||||
|
Überprüft alle saisonalen und monatlichen Achievements für einen Spieler.
|
||||||
|
|
||||||
|
### `check_all_achievements(player_uuid)`
|
||||||
|
Führt alle Achievement-Überprüfungen für einen Spieler aus.
|
||||||
|
|
||||||
|
## 🚀 API Endpoints
|
||||||
|
|
||||||
|
### GET `/api/achievements`
|
||||||
|
Alle verfügbaren Achievements abrufen.
|
||||||
|
|
||||||
|
### GET `/api/achievements/player/:playerId`
|
||||||
|
Achievements eines bestimmten Spielers abrufen.
|
||||||
|
|
||||||
|
### GET `/api/achievements/player/:playerId/stats`
|
||||||
|
Achievement-Statistiken eines Spielers abrufen.
|
||||||
|
|
||||||
|
### POST `/api/achievements/check/:playerId`
|
||||||
|
Achievements für einen Spieler manuell überprüfen.
|
||||||
|
|
||||||
|
### POST `/api/achievements/daily-check`
|
||||||
|
Tägliche Achievement-Überprüfung für alle Spieler ausführen.
|
||||||
|
|
||||||
|
### GET `/api/achievements/leaderboard?limit=10`
|
||||||
|
Bestenliste der Spieler nach Achievement-Punkten.
|
||||||
|
|
||||||
|
## 📅 Automatisierung
|
||||||
|
|
||||||
|
### Tägliches Script
|
||||||
|
```bash
|
||||||
|
# Manuell ausführen
|
||||||
|
node scripts/daily_achievements.js
|
||||||
|
|
||||||
|
# Cron-Job einrichten
|
||||||
|
node scripts/setup_cron.js setup
|
||||||
|
|
||||||
|
# Cron-Job Status prüfen
|
||||||
|
node scripts/setup_cron.js status
|
||||||
|
|
||||||
|
# Cron-Job entfernen
|
||||||
|
node scripts/setup_cron.js remove
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cron-Schedule
|
||||||
|
- **Zeit**: Täglich um 23:59 Uhr
|
||||||
|
- **Log**: `/var/log/ninjaserver_achievements.log`
|
||||||
|
|
||||||
|
## 🎮 Frontend-Integration
|
||||||
|
|
||||||
|
### Beispiel: Achievement-Liste laden
|
||||||
|
```javascript
|
||||||
|
fetch('/api/achievements/player/PLAYER_ID')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
data.data.forEach(achievement => {
|
||||||
|
console.log(`${achievement.icon} ${achievement.name}: ${achievement.is_completed ? '✅' : '❌'}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel: Statistiken anzeigen
|
||||||
|
```javascript
|
||||||
|
fetch('/api/achievements/player/PLAYER_ID/stats')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log(`Punkte: ${data.data.total_points}`);
|
||||||
|
console.log(`Abgeschlossen: ${data.data.completed_achievements}/${data.data.total_achievements}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Monitoring
|
||||||
|
|
||||||
|
### Logs überwachen
|
||||||
|
```bash
|
||||||
|
# Live-Logs anzeigen
|
||||||
|
tail -f /var/log/ninjaserver_achievements.log
|
||||||
|
|
||||||
|
# Letzte Ausführung prüfen
|
||||||
|
grep "Daily achievement check completed" /var/log/ninjaserver_achievements.log | tail -1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenbank-Status prüfen
|
||||||
|
```sql
|
||||||
|
-- Achievement-Statistiken
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_achievements,
|
||||||
|
COUNT(CASE WHEN is_active = true THEN 1 END) as active_achievements
|
||||||
|
FROM achievements;
|
||||||
|
|
||||||
|
-- Spieler-Statistiken
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT player_id) as players_with_achievements,
|
||||||
|
COUNT(*) as total_earned_achievements
|
||||||
|
FROM player_achievements
|
||||||
|
WHERE is_completed = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Wartung
|
||||||
|
|
||||||
|
### Neue Achievements hinzufügen
|
||||||
|
1. Achievement in `achievements` Tabelle einfügen
|
||||||
|
2. Logik in entsprechenden PostgreSQL Funktionen erweitern
|
||||||
|
3. API Endpoints testen
|
||||||
|
|
||||||
|
### Achievement deaktivieren
|
||||||
|
```sql
|
||||||
|
UPDATE achievements SET is_active = false WHERE name = 'Achievement-Name';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daten zurücksetzen
|
||||||
|
```sql
|
||||||
|
-- Alle Spieler-Achievements löschen
|
||||||
|
DELETE FROM player_achievements;
|
||||||
|
|
||||||
|
-- Achievement-Statistiken zurücksetzen
|
||||||
|
UPDATE achievements SET created_at = NOW();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 Performance
|
||||||
|
|
||||||
|
- **Indizierung**: Automatische Indizes auf `player_id` und `achievement_id`
|
||||||
|
- **Batch-Processing**: Effiziente Verarbeitung aller Spieler
|
||||||
|
- **Caching**: Achievements werden nur bei Änderungen neu berechnet
|
||||||
|
- **Timezone**: Korrekte Zeitzone-Behandlung (Europe/Berlin)
|
||||||
|
|
||||||
|
## 🔒 Sicherheit
|
||||||
|
|
||||||
|
- **API-Schutz**: Alle Endpoints über bestehende Authentifizierung
|
||||||
|
- **SQL-Injection**: Parametrisierte Queries
|
||||||
|
- **Datenvalidierung**: Eingabe-Validierung in allen Funktionen
|
||||||
|
- **Fehlerbehandlung**: Umfassende Error-Handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Erstellt am**: $(date)
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Autor**: Ninja Cross Parkour System
|
||||||
415
package-lock.json
generated
415
package-lock.json
generated
@@ -16,12 +16,58 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"socket.io": "^4.8.1"
|
"socket.io": "^4.8.1",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||||
|
"version": "9.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
|
||||||
|
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
|
"@types/json-schema": "^7.0.6",
|
||||||
|
"call-me-maybe": "^1.0.1",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/openapi-schemas": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-methods": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-parser": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
||||||
|
"@apidevtools/openapi-schemas": "^2.0.4",
|
||||||
|
"@apidevtools/swagger-methods": "^3.0.2",
|
||||||
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
|
"call-me-maybe": "^1.0.1",
|
||||||
|
"z-schema": "^5.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"openapi-types": ">=7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@@ -370,6 +416,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jsdevtools/ono": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@mapbox/node-pre-gyp": {
|
"node_modules/@mapbox/node-pre-gyp": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||||
@@ -472,6 +524,13 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@scarf/scarf": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@socket.io/component-emitter": {
|
"node_modules/@socket.io/component-emitter": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
@@ -493,6 +552,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/json-schema": {
|
||||||
|
"version": "7.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.3.0",
|
"version": "24.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||||
@@ -876,6 +941,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-me-maybe": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@@ -972,6 +1043,15 @@
|
|||||||
"color-support": "bin.js"
|
"color-support": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -1137,6 +1217,18 @@
|
|||||||
"integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==",
|
"integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/doctrine": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"esutils": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.5.0",
|
"version": "16.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
||||||
@@ -2153,6 +2245,26 @@
|
|||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.get": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.mergewith": {
|
||||||
|
"version": "4.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||||
|
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "7.18.3",
|
"version": "7.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||||
@@ -2496,6 +2608,13 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi-types": {
|
||||||
|
"version": "12.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||||
|
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/pac-proxy-agent": {
|
"node_modules/pac-proxy-agent": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
||||||
@@ -3603,6 +3722,83 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swagger-jsdoc": {
|
||||||
|
"version": "6.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
||||||
|
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "6.2.0",
|
||||||
|
"doctrine": "3.0.0",
|
||||||
|
"glob": "7.1.6",
|
||||||
|
"lodash.mergewith": "^4.6.2",
|
||||||
|
"swagger-parser": "^10.0.3",
|
||||||
|
"yaml": "2.0.0-1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"swagger-jsdoc": "bin/swagger-jsdoc.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-jsdoc/node_modules/glob": {
|
||||||
|
"version": "7.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||||
|
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-parser": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/swagger-parser": "10.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-dist": {
|
||||||
|
"version": "5.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.1.tgz",
|
||||||
|
"integrity": "sha512-IvPrtNi8MvjiuDgoSmPYgg27Lvu38fnLD1OSd8Y103xXsPAqezVNnNeHnVCZ/d+CMXJblflGaIyHxAYIF3O71w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@scarf/scarf": "=1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-express": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"swagger-ui-dist": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= v0.10.32"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||||
@@ -3768,6 +3964,15 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/validator": {
|
||||||
|
"version": "13.15.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||||
|
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
@@ -3883,6 +4088,15 @@
|
|||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.0.0-1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
|
||||||
|
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
@@ -3920,6 +4134,36 @@
|
|||||||
"fd-slicer": "~1.1.0"
|
"fd-slicer": "~1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/z-schema": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"validator": "^13.7.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"z-schema": "bin/z-schema"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"commander": "^9.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/z-schema/node_modules/commander": {
|
||||||
|
"version": "9.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||||
|
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
@@ -3940,6 +4184,40 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@apidevtools/json-schema-ref-parser": {
|
||||||
|
"version": "9.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
|
||||||
|
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
|
||||||
|
"requires": {
|
||||||
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
|
"@types/json-schema": "^7.0.6",
|
||||||
|
"call-me-maybe": "^1.0.1",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@apidevtools/openapi-schemas": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ=="
|
||||||
|
},
|
||||||
|
"@apidevtools/swagger-methods": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="
|
||||||
|
},
|
||||||
|
"@apidevtools/swagger-parser": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
|
||||||
|
"requires": {
|
||||||
|
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
||||||
|
"@apidevtools/openapi-schemas": "^2.0.4",
|
||||||
|
"@apidevtools/swagger-methods": "^3.0.2",
|
||||||
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
|
"call-me-maybe": "^1.0.1",
|
||||||
|
"z-schema": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@babel/code-frame": {
|
"@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@@ -4182,6 +4460,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@jsdevtools/ono": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
|
||||||
|
},
|
||||||
"@mapbox/node-pre-gyp": {
|
"@mapbox/node-pre-gyp": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||||
@@ -4258,6 +4541,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@scarf/scarf": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="
|
||||||
|
},
|
||||||
"@socket.io/component-emitter": {
|
"@socket.io/component-emitter": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
@@ -4276,6 +4564,11 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/json-schema": {
|
||||||
|
"version": "7.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "24.3.0",
|
"version": "24.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||||
@@ -4537,6 +4830,11 @@
|
|||||||
"get-intrinsic": "^1.3.0"
|
"get-intrinsic": "^1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"call-me-maybe": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
|
||||||
|
},
|
||||||
"callsites": {
|
"callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@@ -4600,6 +4898,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
|
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
|
||||||
},
|
},
|
||||||
|
"commander": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q=="
|
||||||
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -4711,6 +5014,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz",
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz",
|
||||||
"integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA=="
|
"integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA=="
|
||||||
},
|
},
|
||||||
|
"doctrine": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||||
|
"requires": {
|
||||||
|
"esutils": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"dotenv": {
|
"dotenv": {
|
||||||
"version": "16.5.0",
|
"version": "16.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
||||||
@@ -5399,6 +5710,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
|
||||||
},
|
},
|
||||||
|
"lodash.get": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||||
|
},
|
||||||
|
"lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||||
|
},
|
||||||
|
"lodash.mergewith": {
|
||||||
|
"version": "4.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||||
|
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
|
||||||
|
},
|
||||||
"lru-cache": {
|
"lru-cache": {
|
||||||
"version": "7.18.3",
|
"version": "7.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||||
@@ -5618,6 +5944,12 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"openapi-types": {
|
||||||
|
"version": "12.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||||
|
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"pac-proxy-agent": {
|
"pac-proxy-agent": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
||||||
@@ -6363,6 +6695,58 @@
|
|||||||
"has-flag": "^3.0.0"
|
"has-flag": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"swagger-jsdoc": {
|
||||||
|
"version": "6.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
||||||
|
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
|
||||||
|
"requires": {
|
||||||
|
"commander": "6.2.0",
|
||||||
|
"doctrine": "3.0.0",
|
||||||
|
"glob": "7.1.6",
|
||||||
|
"lodash.mergewith": "^4.6.2",
|
||||||
|
"swagger-parser": "^10.0.3",
|
||||||
|
"yaml": "2.0.0-1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"glob": {
|
||||||
|
"version": "7.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||||
|
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger-parser": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
|
||||||
|
"requires": {
|
||||||
|
"@apidevtools/swagger-parser": "10.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger-ui-dist": {
|
||||||
|
"version": "5.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.1.tgz",
|
||||||
|
"integrity": "sha512-IvPrtNi8MvjiuDgoSmPYgg27Lvu38fnLD1OSd8Y103xXsPAqezVNnNeHnVCZ/d+CMXJblflGaIyHxAYIF3O71w==",
|
||||||
|
"requires": {
|
||||||
|
"@scarf/scarf": "=1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger-ui-express": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||||
|
"requires": {
|
||||||
|
"swagger-ui-dist": ">=5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tar": {
|
"tar": {
|
||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||||
@@ -6491,6 +6875,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
|
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
|
||||||
},
|
},
|
||||||
|
"validator": {
|
||||||
|
"version": "13.15.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||||
|
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A=="
|
||||||
|
},
|
||||||
"vary": {
|
"vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
@@ -6562,6 +6951,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
},
|
},
|
||||||
|
"yaml": {
|
||||||
|
"version": "2.0.0-1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
|
||||||
|
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ=="
|
||||||
|
},
|
||||||
"yargs": {
|
"yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
@@ -6590,6 +6984,25 @@
|
|||||||
"fd-slicer": "~1.1.0"
|
"fd-slicer": "~1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"z-schema": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
|
||||||
|
"requires": {
|
||||||
|
"commander": "^9.4.1",
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"validator": "^13.7.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": {
|
||||||
|
"version": "9.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||||
|
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"zod": {
|
"zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"socket.io": "^4.8.1"
|
"socket.io": "^4.8.1",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1"
|
||||||
|
|||||||
@@ -1026,3 +1026,351 @@ body {
|
|||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== ACHIEVEMENT STYLES ==================== */
|
||||||
|
|
||||||
|
.achievements-section {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-header h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, #ffd700, #ff6b35);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-header p {
|
||||||
|
color: #8892b0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Achievement Stats */
|
||||||
|
.achievement-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-stat {
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
border: 1px solid #2a2a3e;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 150px;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-stat .stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #00d4ff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-stat .stat-label {
|
||||||
|
color: #8892b0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Achievement Categories */
|
||||||
|
.achievement-categories {
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
border: 1px solid #2a2a3e;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tab {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #2a2a3e;
|
||||||
|
color: #8892b0;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tab:hover {
|
||||||
|
background: #2a2a3e;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tab.active {
|
||||||
|
background: linear-gradient(135deg, #00d4ff, #ff6b35);
|
||||||
|
border-color: transparent;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Achievements Grid */
|
||||||
|
.achievements-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card {
|
||||||
|
background: linear-gradient(135deg, #0f1419 0%, #1a1a2e 100%);
|
||||||
|
border: 1px solid #2a2a3e;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: #00d4ff;
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card.completed {
|
||||||
|
border-color: #10b981;
|
||||||
|
background: linear-gradient(135deg, #064e3b 0%, #1a1a2e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card.completed:hover {
|
||||||
|
border-color: #10b981;
|
||||||
|
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card.incomplete {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card.incomplete:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-name {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-description {
|
||||||
|
color: #8892b0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-points {
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-progress {
|
||||||
|
color: #00d4ff;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Achievement Loading States */
|
||||||
|
.achievements-loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #8892b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-not-available {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-available-content {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-available-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-available-content h3 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-available-content p {
|
||||||
|
color: #8892b0;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Achievements State */
|
||||||
|
.no-achievements {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #8892b0;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-achievements-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-achievements h3 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Achievement Notifications */
|
||||||
|
.achievement-notification {
|
||||||
|
position: fixed;
|
||||||
|
top: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
border: 1px solid #10b981;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: 350px;
|
||||||
|
animation: slideInRight 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-text h4 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-text p {
|
||||||
|
color: #8892b0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #8892b0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-close:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsiveness for Achievements */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.achievements-header h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-stats {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-stat {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tabs {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tab {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-notification {
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,6 +120,67 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Achievements Section -->
|
||||||
|
<div class="achievements-section">
|
||||||
|
<div class="achievements-header">
|
||||||
|
<h2>🏆 Meine Achievements</h2>
|
||||||
|
<p>Sammele Punkte und erreiche neue Meilensteine!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Achievement Stats -->
|
||||||
|
<div class="achievement-stats" id="achievementStats" style="display: none;">
|
||||||
|
<div class="stat-card achievement-stat">
|
||||||
|
<div class="stat-number" id="totalPoints">0</div>
|
||||||
|
<div class="stat-label">Gesamtpunkte</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card achievement-stat">
|
||||||
|
<div class="stat-number" id="completedAchievements">0</div>
|
||||||
|
<div class="stat-label">Abgeschlossen</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card achievement-stat">
|
||||||
|
<div class="stat-number" id="achievementsToday">0</div>
|
||||||
|
<div class="stat-label">Heute erreicht</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card achievement-stat">
|
||||||
|
<div class="stat-number" id="completionPercentage">0%</div>
|
||||||
|
<div class="stat-label">Fortschritt</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Achievement Categories -->
|
||||||
|
<div class="achievement-categories" id="achievementCategories" style="display: none;">
|
||||||
|
<div class="category-tabs">
|
||||||
|
<button class="category-tab active" onclick="showAchievementCategory('all')" data-category="all">Alle</button>
|
||||||
|
<button class="category-tab" onclick="showAchievementCategory('consistency')" data-category="consistency">Konsistenz</button>
|
||||||
|
<button class="category-tab" onclick="showAchievementCategory('improvement')" data-category="improvement">Verbesserung</button>
|
||||||
|
<button class="category-tab" onclick="showAchievementCategory('seasonal')" data-category="seasonal">Saisonal</button>
|
||||||
|
<button class="category-tab" onclick="showAchievementCategory('monthly')" data-category="monthly">Monatlich</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="achievements-grid" id="achievementsGrid">
|
||||||
|
<!-- Achievements will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Achievement Loading State -->
|
||||||
|
<div id="achievementsLoading" class="achievements-loading" style="display: none;">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Lade deine Achievements...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Achievement Not Available State -->
|
||||||
|
<div id="achievementsNotAvailable" class="achievements-not-available" style="display: none;">
|
||||||
|
<div class="not-available-content">
|
||||||
|
<div class="not-available-icon">🏆</div>
|
||||||
|
<h3>Achievements noch nicht verfügbar</h3>
|
||||||
|
<p>Um Achievements zu sammeln, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen und einige Läufe absolvieren.</p>
|
||||||
|
<button class="btn btn-primary" onclick="showRFIDSettings()">
|
||||||
|
🏷️ RFID jetzt verknüpfen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ async function initDashboard() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error checking authentication:', error);
|
console.error('Error checking authentication:', error);
|
||||||
// Temporarily show dashboard for testing
|
// Temporarily show dashboard for testing
|
||||||
currentUser = { id: 'test-user', email: 'admin@speedrun-arena.com' };
|
currentUser = { id: '9966cffd-2088-423c-b852-0ca7996cda97', email: 'admin@speedrun-arena.com' };
|
||||||
displayUserInfo({ email: 'admin@speedrun-arena.com' });
|
displayUserInfo({ email: 'admin@speedrun-arena.com' });
|
||||||
showDashboard();
|
showDashboard();
|
||||||
// Check times section
|
// Check times section
|
||||||
@@ -38,7 +38,7 @@ async function initDashboard() {
|
|||||||
displayUserInfo(session.user);
|
displayUserInfo(session.user);
|
||||||
} else {
|
} else {
|
||||||
// Fallback if no user data
|
// Fallback if no user data
|
||||||
currentUser = { id: 'test-user', email: 'admin@speedrun-arena.com' };
|
currentUser = { id: '9966cffd-2088-423c-b852-0ca7996cda97', email: 'admin@speedrun-arena.com' };
|
||||||
displayUserInfo({ email: 'admin@speedrun-arena.com' });
|
displayUserInfo({ email: 'admin@speedrun-arena.com' });
|
||||||
}
|
}
|
||||||
showDashboard();
|
showDashboard();
|
||||||
@@ -132,7 +132,7 @@ async function checkLinkStatusAndLoadTimes() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if user has a linked player
|
// Check if user has a linked player
|
||||||
const response = await fetch(`/api/user-player/${currentUser.id}`);
|
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}`);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -363,7 +363,13 @@ async function loadUserTimesSection(playerData) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/public/user-times/${currentUser.id}`);
|
const response = await fetch(`/api/v1/public/user-times/${currentUser.id}`);
|
||||||
const times = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || 'Failed to load times');
|
||||||
|
}
|
||||||
|
|
||||||
|
const times = result.data || result;
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
updateTimesStats(times, playerData);
|
updateTimesStats(times, playerData);
|
||||||
@@ -376,6 +382,9 @@ async function loadUserTimesSection(playerData) {
|
|||||||
document.getElementById('timesNotLinked').style.display = 'none';
|
document.getElementById('timesNotLinked').style.display = 'none';
|
||||||
document.getElementById('timesDisplay').style.display = 'block';
|
document.getElementById('timesDisplay').style.display = 'block';
|
||||||
|
|
||||||
|
// Initialize achievements for this player
|
||||||
|
initializeAchievements(playerData.id);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading user times:', error);
|
console.error('Error loading user times:', error);
|
||||||
showTimesNotLinked();
|
showTimesNotLinked();
|
||||||
@@ -577,6 +586,251 @@ function showMessage(containerId, message, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize when DOM is loaded
|
// Initialize when DOM is loaded
|
||||||
|
// ==================== ACHIEVEMENT FUNCTIONS ====================
|
||||||
|
|
||||||
|
// Global variables for achievements
|
||||||
|
let currentPlayerId = null;
|
||||||
|
let allAchievements = [];
|
||||||
|
let playerAchievements = [];
|
||||||
|
let currentAchievementCategory = 'all';
|
||||||
|
|
||||||
|
// Load achievements for the current player
|
||||||
|
async function loadPlayerAchievements() {
|
||||||
|
if (!currentPlayerId) {
|
||||||
|
showAchievementsNotAvailable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Show loading state
|
||||||
|
document.getElementById('achievementsLoading').style.display = 'block';
|
||||||
|
document.getElementById('achievementStats').style.display = 'none';
|
||||||
|
document.getElementById('achievementCategories').style.display = 'none';
|
||||||
|
document.getElementById('achievementsNotAvailable').style.display = 'none';
|
||||||
|
|
||||||
|
// Load player achievements
|
||||||
|
const response = await fetch(`/api/achievements/player/${currentPlayerId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load achievements');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
playerAchievements = result.data;
|
||||||
|
|
||||||
|
// Load achievement statistics
|
||||||
|
await loadAchievementStats();
|
||||||
|
|
||||||
|
// Show achievements
|
||||||
|
displayAchievementStats();
|
||||||
|
displayAchievements();
|
||||||
|
|
||||||
|
// Hide loading state
|
||||||
|
document.getElementById('achievementsLoading').style.display = 'none';
|
||||||
|
document.getElementById('achievementStats').style.display = 'flex';
|
||||||
|
document.getElementById('achievementCategories').style.display = 'block';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading achievements:', error);
|
||||||
|
document.getElementById('achievementsLoading').style.display = 'none';
|
||||||
|
showAchievementsNotAvailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load achievement statistics
|
||||||
|
async function loadAchievementStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/achievements/player/${currentPlayerId}/stats`);
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
window.achievementStats = result.data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading achievement stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display achievement statistics
|
||||||
|
function displayAchievementStats() {
|
||||||
|
if (!window.achievementStats) return;
|
||||||
|
|
||||||
|
const stats = window.achievementStats;
|
||||||
|
|
||||||
|
document.getElementById('totalPoints').textContent = stats.total_points;
|
||||||
|
document.getElementById('completedAchievements').textContent = `${stats.completed_achievements}/${stats.total_achievements}`;
|
||||||
|
document.getElementById('achievementsToday').textContent = stats.achievements_today;
|
||||||
|
document.getElementById('completionPercentage').textContent = `${stats.completion_percentage}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display achievements in grid
|
||||||
|
function displayAchievements() {
|
||||||
|
const achievementsGrid = document.getElementById('achievementsGrid');
|
||||||
|
|
||||||
|
if (playerAchievements.length === 0) {
|
||||||
|
achievementsGrid.innerHTML = `
|
||||||
|
<div class="no-achievements">
|
||||||
|
<div class="no-achievements-icon">🏆</div>
|
||||||
|
<h3>Noch keine Achievements</h3>
|
||||||
|
<p>Starte deine ersten Läufe, um Achievements zu sammeln!</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter achievements by category
|
||||||
|
let filteredAchievements = playerAchievements;
|
||||||
|
if (currentAchievementCategory !== 'all') {
|
||||||
|
filteredAchievements = playerAchievements.filter(achievement =>
|
||||||
|
achievement.category === currentAchievementCategory
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate achievement cards
|
||||||
|
const achievementCards = filteredAchievements.map(achievement => {
|
||||||
|
const isCompleted = achievement.is_completed;
|
||||||
|
const progress = achievement.progress || 0;
|
||||||
|
const earnedAt = achievement.earned_at;
|
||||||
|
|
||||||
|
let progressText = '';
|
||||||
|
if (isCompleted) {
|
||||||
|
progressText = earnedAt ?
|
||||||
|
`Erreicht am ${new Date(earnedAt).toLocaleDateString('de-DE')}` :
|
||||||
|
'Abgeschlossen';
|
||||||
|
} else if (progress > 0) {
|
||||||
|
// Show progress for incomplete achievements
|
||||||
|
const conditionValue = getAchievementConditionValue(achievement.name);
|
||||||
|
if (conditionValue) {
|
||||||
|
progressText = `${progress}/${conditionValue}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="achievement-card ${isCompleted ? 'completed' : 'incomplete'}"
|
||||||
|
onclick="showAchievementDetails('${achievement.id}')">
|
||||||
|
<div class="achievement-icon">${achievement.icon}</div>
|
||||||
|
<div class="achievement-content">
|
||||||
|
<h4 class="achievement-name">${achievement.name}</h4>
|
||||||
|
<p class="achievement-description">${achievement.description}</p>
|
||||||
|
<div class="achievement-meta">
|
||||||
|
<span class="achievement-points">+${achievement.points} Punkte</span>
|
||||||
|
${progressText ? `<span class="achievement-progress">${progressText}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-status">
|
||||||
|
${isCompleted ? '✅' : '⏳'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
achievementsGrid.innerHTML = achievementCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get achievement condition value for progress display
|
||||||
|
function getAchievementConditionValue(achievementName) {
|
||||||
|
const conditionMap = {
|
||||||
|
'Erste Schritte': 1,
|
||||||
|
'Durchhalter': 3,
|
||||||
|
'Fleißig': 5,
|
||||||
|
'Besessen': 10,
|
||||||
|
'Regelmäßig': 5,
|
||||||
|
'Stammgast': 10,
|
||||||
|
'Treue': 20,
|
||||||
|
'Veteran': 50,
|
||||||
|
'Fortschritt': 5,
|
||||||
|
'Durchbruch': 10,
|
||||||
|
'Transformation': 15,
|
||||||
|
'Perfektionist': 20
|
||||||
|
};
|
||||||
|
return conditionMap[achievementName] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show achievement category
|
||||||
|
function showAchievementCategory(category) {
|
||||||
|
currentAchievementCategory = category;
|
||||||
|
|
||||||
|
// Update active tab
|
||||||
|
document.querySelectorAll('.category-tab').forEach(tab => {
|
||||||
|
tab.classList.remove('active');
|
||||||
|
});
|
||||||
|
document.querySelector(`[data-category="${category}"]`).classList.add('active');
|
||||||
|
|
||||||
|
// Display filtered achievements
|
||||||
|
displayAchievements();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show achievement details (placeholder for future modal)
|
||||||
|
function showAchievementDetails(achievementId) {
|
||||||
|
const achievement = playerAchievements.find(a => a.id === achievementId);
|
||||||
|
if (achievement) {
|
||||||
|
console.log('Achievement details:', achievement);
|
||||||
|
// TODO: Implement achievement details modal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show achievements not available state
|
||||||
|
function showAchievementsNotAvailable() {
|
||||||
|
document.getElementById('achievementsLoading').style.display = 'none';
|
||||||
|
document.getElementById('achievementStats').style.display = 'none';
|
||||||
|
document.getElementById('achievementCategories').style.display = 'none';
|
||||||
|
document.getElementById('achievementsNotAvailable').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check achievements for current player
|
||||||
|
async function checkPlayerAchievements() {
|
||||||
|
if (!currentPlayerId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/achievements/check/${currentPlayerId}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.data.count > 0) {
|
||||||
|
// Show notification for new achievements
|
||||||
|
showAchievementNotification(result.data.new_achievements);
|
||||||
|
// Reload achievements
|
||||||
|
await loadPlayerAchievements();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking achievements:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show achievement notification
|
||||||
|
function showAchievementNotification(newAchievements) {
|
||||||
|
// Create notification element
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'achievement-notification';
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-icon">🏆</div>
|
||||||
|
<div class="notification-text">
|
||||||
|
<h4>Neue Achievements erreicht!</h4>
|
||||||
|
<p>Du hast ${newAchievements.length} neue Achievement${newAchievements.length > 1 ? 's' : ''} erhalten!</p>
|
||||||
|
</div>
|
||||||
|
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">×</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add to page
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Auto-remove after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentElement) {
|
||||||
|
notification.remove();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize achievements when player is loaded
|
||||||
|
function initializeAchievements(playerId) {
|
||||||
|
currentPlayerId = playerId;
|
||||||
|
loadPlayerAchievements();
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Add cookie settings button functionality
|
// Add cookie settings button functionality
|
||||||
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
|
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
|
||||||
|
|||||||
406
routes/api.js
406
routes/api.js
@@ -886,6 +886,15 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => {
|
|||||||
[player_id, location_id, recorded_time, new Date()]
|
[player_id, location_id, recorded_time, new Date()]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Achievement-Überprüfung nach Zeit-Eingabe
|
||||||
|
try {
|
||||||
|
await pool.query('SELECT check_all_achievements($1)', [player_id]);
|
||||||
|
console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`);
|
||||||
|
} catch (achievementError) {
|
||||||
|
console.error('Fehler bei Achievement-Check:', achievementError);
|
||||||
|
// Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// WebSocket-Event senden für Live-Updates
|
// WebSocket-Event senden für Live-Updates
|
||||||
@@ -1202,7 +1211,10 @@ router.get('/v1/public/user-times/:supabase_user_id', async (req, res) => {
|
|||||||
ORDER BY t.created_at DESC
|
ORDER BY t.created_at DESC
|
||||||
`, [player_id]);
|
`, [player_id]);
|
||||||
|
|
||||||
res.json(timesResult.rows);
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: timesResult.rows
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Abrufen der Benutzerzeiten:', error);
|
console.error('Fehler beim Abrufen der Benutzerzeiten:', error);
|
||||||
@@ -1213,6 +1225,47 @@ router.get('/v1/public/user-times/:supabase_user_id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/v1/public/user-player/{supabase_user_id}:
|
||||||
|
* get:
|
||||||
|
* summary: Spieler-Info anhand der Supabase User ID abrufen
|
||||||
|
* description: Ruft Spieler-Informationen für das Dashboard ab
|
||||||
|
* tags: [Public API]
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: supabase_user_id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: uuid
|
||||||
|
* description: Supabase User ID
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Spieler-Informationen
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* data:
|
||||||
|
* $ref: '#/components/schemas/Player'
|
||||||
|
* 404:
|
||||||
|
* description: Spieler nicht gefunden
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 500:
|
||||||
|
* description: Server-Fehler
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
// Get player info by Supabase user ID (no auth required for dashboard)
|
// Get player info by Supabase user ID (no auth required for dashboard)
|
||||||
router.get('/v1/public/user-player/:supabase_user_id', async (req, res) => {
|
router.get('/v1/public/user-player/:supabase_user_id', async (req, res) => {
|
||||||
const { supabase_user_id } = req.params;
|
const { supabase_user_id } = req.params;
|
||||||
@@ -1685,6 +1738,75 @@ router.get('/v1/public/locations', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/v1/public/times:
|
||||||
|
* get:
|
||||||
|
* summary: Alle Zeiten mit Standort-Informationen abrufen
|
||||||
|
* description: Ruft alle aufgezeichneten Zeiten mit Standort-Details ab
|
||||||
|
* tags: [Public API]
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: location
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Filter nach Standort-ID
|
||||||
|
* - in: query
|
||||||
|
* name: limit
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 100
|
||||||
|
* description: Maximale Anzahl der Ergebnisse
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Liste aller Zeiten
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* data:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: string
|
||||||
|
* format: uuid
|
||||||
|
* player_id:
|
||||||
|
* type: string
|
||||||
|
* format: uuid
|
||||||
|
* location_id:
|
||||||
|
* type: string
|
||||||
|
* format: uuid
|
||||||
|
* recorded_time:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* seconds:
|
||||||
|
* type: number
|
||||||
|
* minutes:
|
||||||
|
* type: number
|
||||||
|
* milliseconds:
|
||||||
|
* type: number
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* location_name:
|
||||||
|
* type: string
|
||||||
|
* latitude:
|
||||||
|
* type: number
|
||||||
|
* longitude:
|
||||||
|
* type: number
|
||||||
|
* 500:
|
||||||
|
* description: Server-Fehler
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
// Public route to get times for location with parameter
|
// Public route to get times for location with parameter
|
||||||
router.get('/v1/public/times', async (req, res) => {
|
router.get('/v1/public/times', async (req, res) => {
|
||||||
const { location } = req.query;
|
const { location } = req.query;
|
||||||
@@ -2022,6 +2144,15 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => {
|
|||||||
[player_id, location_id, timeInterval]
|
[player_id, location_id, timeInterval]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Achievement-Überprüfung nach Zeit-Eingabe
|
||||||
|
try {
|
||||||
|
await pool.query('SELECT check_all_achievements($1)', [player_id]);
|
||||||
|
console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`);
|
||||||
|
} catch (achievementError) {
|
||||||
|
console.error('Fehler bei Achievement-Check:', achievementError);
|
||||||
|
// Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Lauf erfolgreich hinzugefügt',
|
message: 'Lauf erfolgreich hinzugefügt',
|
||||||
@@ -2169,4 +2300,277 @@ router.put('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== ACHIEVEMENT ENDPOINTS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/achievements:
|
||||||
|
* get:
|
||||||
|
* summary: Alle verfügbaren Achievements abrufen
|
||||||
|
* description: Ruft alle aktiven Achievements im System ab
|
||||||
|
* tags: [Achievements]
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Liste aller Achievements
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* data:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/Achievement'
|
||||||
|
* 500:
|
||||||
|
* description: Server-Fehler
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
// Get all achievements
|
||||||
|
router.get('/achievements', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT id, name, description, category, icon, points, is_active
|
||||||
|
FROM achievements
|
||||||
|
WHERE is_active = true
|
||||||
|
ORDER BY category, points DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: result.rows
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching achievements:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Laden der Achievements'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/achievements/player/{playerId}:
|
||||||
|
* get:
|
||||||
|
* summary: Achievements eines Spielers abrufen
|
||||||
|
* description: Ruft alle Achievements für einen bestimmten Spieler ab
|
||||||
|
* tags: [Achievements]
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: playerId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: uuid
|
||||||
|
* description: Eindeutige Spieler-ID
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Liste der Spieler-Achievements
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* data:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* allOf:
|
||||||
|
* - $ref: '#/components/schemas/Achievement'
|
||||||
|
* - $ref: '#/components/schemas/PlayerAchievement'
|
||||||
|
* 500:
|
||||||
|
* description: Server-Fehler
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
// Get player achievements
|
||||||
|
router.get('/achievements/player/:playerId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { playerId } = req.params;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
a.id,
|
||||||
|
a.name,
|
||||||
|
a.description,
|
||||||
|
a.category,
|
||||||
|
a.icon,
|
||||||
|
a.points,
|
||||||
|
pa.progress,
|
||||||
|
pa.is_completed,
|
||||||
|
pa.earned_at
|
||||||
|
FROM achievements a
|
||||||
|
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
|
||||||
|
WHERE a.is_active = true
|
||||||
|
ORDER BY
|
||||||
|
pa.is_completed DESC,
|
||||||
|
a.category,
|
||||||
|
a.points DESC
|
||||||
|
`, [playerId]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: result.rows
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching player achievements:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Laden der Spieler-Achievements'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get player achievement statistics
|
||||||
|
router.get('/achievements/player/:playerId/stats', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { playerId } = req.params;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
COUNT(pa.id) as total_achievements,
|
||||||
|
COUNT(CASE WHEN pa.is_completed = true THEN 1 END) as completed_achievements,
|
||||||
|
SUM(CASE WHEN pa.is_completed = true THEN a.points ELSE 0 END) as total_points,
|
||||||
|
COUNT(CASE WHEN pa.is_completed = true AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE THEN 1 END) as achievements_today
|
||||||
|
FROM achievements a
|
||||||
|
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
|
||||||
|
WHERE a.is_active = true
|
||||||
|
`, [playerId]);
|
||||||
|
|
||||||
|
const stats = result.rows[0];
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
total_achievements: parseInt(stats.total_achievements),
|
||||||
|
completed_achievements: parseInt(stats.completed_achievements),
|
||||||
|
total_points: parseInt(stats.total_points) || 0,
|
||||||
|
achievements_today: parseInt(stats.achievements_today),
|
||||||
|
completion_percentage: stats.total_achievements > 0 ?
|
||||||
|
Math.round((stats.completed_achievements / stats.total_achievements) * 100) : 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching player achievement stats:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Laden der Achievement-Statistiken'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check achievements for a specific player
|
||||||
|
router.post('/achievements/check/:playerId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { playerId } = req.params;
|
||||||
|
|
||||||
|
// Verify player exists
|
||||||
|
const playerCheck = await pool.query('SELECT id FROM players WHERE id = $1', [playerId]);
|
||||||
|
if (playerCheck.rows.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Spieler nicht gefunden'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run achievement check
|
||||||
|
await pool.query('SELECT check_all_achievements($1)', [playerId]);
|
||||||
|
|
||||||
|
// Get newly earned achievements
|
||||||
|
const newAchievements = await pool.query(`
|
||||||
|
SELECT a.name, a.description, a.icon, a.points
|
||||||
|
FROM player_achievements pa
|
||||||
|
INNER JOIN achievements a ON pa.achievement_id = a.id
|
||||||
|
WHERE pa.player_id = $1
|
||||||
|
AND pa.is_completed = true
|
||||||
|
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
|
||||||
|
ORDER BY pa.earned_at DESC
|
||||||
|
`, [playerId]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Achievement-Check abgeschlossen',
|
||||||
|
data: {
|
||||||
|
new_achievements: newAchievements.rows,
|
||||||
|
count: newAchievements.rows.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking achievements:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Überprüfen der Achievements'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run daily achievement check for all players
|
||||||
|
router.post('/achievements/daily-check', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// This endpoint runs the daily achievement check
|
||||||
|
const { runDailyAchievements } = require('../scripts/daily_achievements');
|
||||||
|
|
||||||
|
await runDailyAchievements();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Tägliche Achievement-Überprüfung abgeschlossen'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error running daily achievement check:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler bei der täglichen Achievement-Überprüfung'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get achievement leaderboard
|
||||||
|
router.get('/achievements/leaderboard', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { limit = 10 } = req.query;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
p.firstname,
|
||||||
|
p.lastname,
|
||||||
|
COUNT(pa.id) as completed_achievements,
|
||||||
|
SUM(a.points) as total_points
|
||||||
|
FROM players p
|
||||||
|
INNER JOIN player_achievements pa ON p.id = pa.player_id
|
||||||
|
INNER JOIN achievements a ON pa.achievement_id = a.id
|
||||||
|
WHERE pa.is_completed = true
|
||||||
|
GROUP BY p.id, p.firstname, p.lastname
|
||||||
|
ORDER BY total_points DESC, completed_achievements DESC
|
||||||
|
LIMIT $1
|
||||||
|
`, [parseInt(limit)]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: result.rows.map((row, index) => ({
|
||||||
|
rank: index + 1,
|
||||||
|
name: `${row.firstname} ${row.lastname}`,
|
||||||
|
completed_achievements: parseInt(row.completed_achievements),
|
||||||
|
total_points: parseInt(row.total_points)
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching achievement leaderboard:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Laden der Bestenliste'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = { router, requireApiKey };
|
module.exports = { router, requireApiKey };
|
||||||
|
|||||||
110
scripts/daily_achievements.js
Normal file
110
scripts/daily_achievements.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
const { Pool } = require('pg');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||||
|
});
|
||||||
|
|
||||||
|
async function runDailyAchievements() {
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🎯 Starting daily achievement check...');
|
||||||
|
|
||||||
|
// Get all players who have played today
|
||||||
|
const playersResult = await client.query(`
|
||||||
|
SELECT DISTINCT p.id, p.firstname, p.lastname
|
||||||
|
FROM players p
|
||||||
|
INNER JOIN times t ON p.id = t.player_id
|
||||||
|
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`Found ${playersResult.rows.length} players who played today`);
|
||||||
|
|
||||||
|
let totalAchievements = 0;
|
||||||
|
|
||||||
|
// Check achievements for each player
|
||||||
|
for (const player of playersResult.rows) {
|
||||||
|
console.log(`Checking achievements for ${player.firstname} ${player.lastname}...`);
|
||||||
|
|
||||||
|
// Run achievement check function
|
||||||
|
await client.query('SELECT check_all_achievements($1)', [player.id]);
|
||||||
|
|
||||||
|
// Count new achievements earned today
|
||||||
|
const newAchievementsResult = await client.query(`
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM player_achievements pa
|
||||||
|
INNER JOIN achievements a ON pa.achievement_id = a.id
|
||||||
|
WHERE pa.player_id = $1
|
||||||
|
AND pa.is_completed = true
|
||||||
|
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
|
||||||
|
`, [player.id]);
|
||||||
|
|
||||||
|
const newAchievements = parseInt(newAchievementsResult.rows[0].count);
|
||||||
|
totalAchievements += newAchievements;
|
||||||
|
|
||||||
|
if (newAchievements > 0) {
|
||||||
|
console.log(` ✅ ${newAchievements} new achievements earned!`);
|
||||||
|
|
||||||
|
// Get details of new achievements
|
||||||
|
const achievementsResult = await client.query(`
|
||||||
|
SELECT a.name, a.description, a.icon, a.points
|
||||||
|
FROM player_achievements pa
|
||||||
|
INNER JOIN achievements a ON pa.achievement_id = a.id
|
||||||
|
WHERE pa.player_id = $1
|
||||||
|
AND pa.is_completed = true
|
||||||
|
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
|
||||||
|
ORDER BY pa.earned_at DESC
|
||||||
|
`, [player.id]);
|
||||||
|
|
||||||
|
achievementsResult.rows.forEach(achievement => {
|
||||||
|
console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} points)`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(` ℹ️ No new achievements`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🎉 Daily achievement check completed!`);
|
||||||
|
console.log(`Total new achievements earned: ${totalAchievements}`);
|
||||||
|
|
||||||
|
// Log summary statistics
|
||||||
|
const statsResult = await client.query(`
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT pa.player_id) as players_with_achievements,
|
||||||
|
COUNT(pa.id) as total_achievements_earned,
|
||||||
|
SUM(a.points) as total_points_earned
|
||||||
|
FROM player_achievements pa
|
||||||
|
INNER JOIN achievements a ON pa.achievement_id = a.id
|
||||||
|
WHERE pa.is_completed = true
|
||||||
|
`);
|
||||||
|
|
||||||
|
const stats = statsResult.rows[0];
|
||||||
|
console.log(`\n📊 Overall Statistics:`);
|
||||||
|
console.log(`Players with achievements: ${stats.players_with_achievements}`);
|
||||||
|
console.log(`Total achievements earned: ${stats.total_achievements_earned}`);
|
||||||
|
console.log(`Total points earned: ${stats.total_points_earned || 0}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error running daily achievements:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (require.main === module) {
|
||||||
|
runDailyAchievements()
|
||||||
|
.then(() => {
|
||||||
|
console.log('✅ Daily achievements script completed successfully');
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('❌ Daily achievements script failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runDailyAchievements };
|
||||||
86
scripts/setup_cron.js
Normal file
86
scripts/setup_cron.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
const { exec } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Cron job setup for daily achievements
|
||||||
|
const cronJob = {
|
||||||
|
// Run daily at 23:59 (end of day)
|
||||||
|
schedule: '59 23 * * *',
|
||||||
|
command: `cd ${__dirname} && node daily_achievements.js >> /var/log/ninjaserver_achievements.log 2>&1`,
|
||||||
|
description: 'Daily achievement check for Ninja Cross Parkour'
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupCronJob() {
|
||||||
|
console.log('🕐 Setting up daily achievement cron job...');
|
||||||
|
|
||||||
|
// Create cron job entry
|
||||||
|
const cronEntry = `${cronJob.schedule} ${cronJob.command}`;
|
||||||
|
|
||||||
|
// Add to crontab
|
||||||
|
exec(`(crontab -l 2>/dev/null; echo "${cronEntry}") | crontab -`, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('❌ Error setting up cron job:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error('⚠️ Cron job warning:', stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Cron job setup successfully!');
|
||||||
|
console.log(`📅 Schedule: ${cronJob.schedule}`);
|
||||||
|
console.log(`🔧 Command: ${cronJob.command}`);
|
||||||
|
console.log('📝 Logs will be written to: /var/log/ninjaserver_achievements.log');
|
||||||
|
|
||||||
|
// Show current crontab
|
||||||
|
exec('crontab -l', (error, stdout, stderr) => {
|
||||||
|
if (!error) {
|
||||||
|
console.log('\n📋 Current crontab:');
|
||||||
|
console.log(stdout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCronJob() {
|
||||||
|
console.log('🗑️ Removing daily achievement cron job...');
|
||||||
|
|
||||||
|
exec('crontab -l | grep -v "daily_achievements.js" | crontab -', (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('❌ Error removing cron job:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Cron job removed successfully!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command line interface
|
||||||
|
if (require.main === module) {
|
||||||
|
const command = process.argv[2];
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'setup':
|
||||||
|
setupCronJob();
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
removeCronJob();
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
exec('crontab -l | grep daily_achievements', (error, stdout, stderr) => {
|
||||||
|
if (stdout) {
|
||||||
|
console.log('✅ Cron job is active:');
|
||||||
|
console.log(stdout);
|
||||||
|
} else {
|
||||||
|
console.log('❌ No cron job found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('Usage: node setup_cron.js [setup|remove|status]');
|
||||||
|
console.log(' setup - Add daily achievement cron job');
|
||||||
|
console.log(' remove - Remove daily achievement cron job');
|
||||||
|
console.log(' status - Check if cron job is active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { setupCronJob, removeCronJob };
|
||||||
@@ -21,6 +21,8 @@ const path = require('path');
|
|||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
const { createServer } = require('http');
|
const { createServer } = require('http');
|
||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
|
const swaggerUi = require('swagger-ui-express');
|
||||||
|
const swaggerSpecs = require('./swagger');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
// Route Imports
|
// Route Imports
|
||||||
@@ -82,6 +84,12 @@ function requireWebAuth(req, res, next) {
|
|||||||
// ROUTE SETUP
|
// ROUTE SETUP
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// Swagger API Documentation
|
||||||
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, {
|
||||||
|
customCss: '.swagger-ui .topbar { display: none }',
|
||||||
|
customSiteTitle: 'Ninja Cross Parkour API Documentation'
|
||||||
|
}));
|
||||||
|
|
||||||
// Unified API Routes (all under /api/v1/)
|
// Unified API Routes (all under /api/v1/)
|
||||||
// - /api/v1/public/* - Public routes (no authentication)
|
// - /api/v1/public/* - Public routes (no authentication)
|
||||||
// - /api/v1/private/* - API-Key protected routes
|
// - /api/v1/private/* - API-Key protected routes
|
||||||
|
|||||||
285
swagger.js
Normal file
285
swagger.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
const swaggerJsdoc = require('swagger-jsdoc');
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
definition: {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: 'Ninja Cross Parkour API',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'API für das Ninja Cross Parkour System im Schwimmbad',
|
||||||
|
contact: {
|
||||||
|
name: 'Ninja Cross Parkour',
|
||||||
|
email: 'admin@ninjacross.com'
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
name: 'MIT',
|
||||||
|
url: 'https://opensource.org/licenses/MIT'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: 'https://ninja.reptilfpv.de',
|
||||||
|
description: 'Production server'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
ApiKeyAuth: {
|
||||||
|
type: 'apiKey',
|
||||||
|
in: 'header',
|
||||||
|
name: 'X-API-Key',
|
||||||
|
description: 'API Key für Authentifizierung'
|
||||||
|
},
|
||||||
|
BearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
description: 'JWT Token für Supabase Authentifizierung'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
schemas: {
|
||||||
|
Player: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'Eindeutige Spieler-ID'
|
||||||
|
},
|
||||||
|
firstname: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Vorname des Spielers'
|
||||||
|
},
|
||||||
|
lastname: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Nachname des Spielers'
|
||||||
|
},
|
||||||
|
birthdate: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date',
|
||||||
|
description: 'Geburtsdatum des Spielers'
|
||||||
|
},
|
||||||
|
rfiduid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'RFID UID der Karte (Format: XX:XX:XX:XX)'
|
||||||
|
},
|
||||||
|
supabase_user_id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'Supabase User ID für Verknüpfung'
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
description: 'Erstellungsdatum'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Time: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'Eindeutige Zeit-ID'
|
||||||
|
},
|
||||||
|
player_id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'ID des Spielers'
|
||||||
|
},
|
||||||
|
location_id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'ID des Standorts'
|
||||||
|
},
|
||||||
|
recorded_time: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Aufgezeichnete Zeit als Intervall',
|
||||||
|
properties: {
|
||||||
|
seconds: { type: 'number' },
|
||||||
|
minutes: { type: 'number' },
|
||||||
|
milliseconds: { type: 'number' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
description: 'Erstellungsdatum'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Location: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'Eindeutige Standort-ID'
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name des Standorts'
|
||||||
|
},
|
||||||
|
latitude: {
|
||||||
|
type: 'number',
|
||||||
|
format: 'float',
|
||||||
|
description: 'Breitengrad'
|
||||||
|
},
|
||||||
|
longitude: {
|
||||||
|
type: 'number',
|
||||||
|
format: 'float',
|
||||||
|
description: 'Längengrad'
|
||||||
|
},
|
||||||
|
time_threshold: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Zeitschwelle für den Standort',
|
||||||
|
properties: {
|
||||||
|
seconds: { type: 'number' },
|
||||||
|
minutes: { type: 'number' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
description: 'Erstellungsdatum'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Achievement: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'Eindeutige Achievement-ID'
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name des Achievements'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Beschreibung des Achievements'
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['consistency', 'improvement', 'seasonal', 'monthly'],
|
||||||
|
description: 'Kategorie des Achievements'
|
||||||
|
},
|
||||||
|
condition_type: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Typ der Bedingung'
|
||||||
|
},
|
||||||
|
condition_value: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Wert der Bedingung'
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Emoji-Icon für das Achievement'
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Punkte für das Achievement'
|
||||||
|
},
|
||||||
|
is_active: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Ob das Achievement aktiv ist'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PlayerAchievement: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'Eindeutige Player-Achievement-ID'
|
||||||
|
},
|
||||||
|
player_id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'ID des Spielers'
|
||||||
|
},
|
||||||
|
achievement_id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'ID des Achievements'
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Aktueller Fortschritt'
|
||||||
|
},
|
||||||
|
is_completed: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Ob das Achievement abgeschlossen ist'
|
||||||
|
},
|
||||||
|
earned_at: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
description: 'Wann das Achievement erreicht wurde'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Error: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: {
|
||||||
|
type: 'boolean',
|
||||||
|
example: false
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Fehlermeldung'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Success: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: {
|
||||||
|
type: 'boolean',
|
||||||
|
example: true
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Erfolgsmeldung'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Daten der Antwort'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
name: 'Public API',
|
||||||
|
description: 'Öffentliche API-Endpoints ohne Authentifizierung'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Private API',
|
||||||
|
description: 'Private API-Endpoints mit API-Key Authentifizierung'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Web API',
|
||||||
|
description: 'Web-API-Endpoints für das Frontend'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Admin API',
|
||||||
|
description: 'Admin-API-Endpoints für Verwaltung'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Achievements',
|
||||||
|
description: 'Achievement-System Endpoints'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
apis: ['./routes/api.js'] // Pfad zu deiner API-Datei
|
||||||
|
};
|
||||||
|
|
||||||
|
const specs = swaggerJsdoc(options);
|
||||||
|
|
||||||
|
module.exports = specs;
|
||||||
Reference in New Issue
Block a user