Notifications, div fixes, kekse für last location
This commit is contained in:
264
package-lock.json
generated
264
package-lock.json
generated
@@ -15,10 +15,12 @@
|
|||||||
"enhanced-postgres-mcp-server": "^1.0.1",
|
"enhanced-postgres-mcp-server": "^1.0.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1"
|
"swagger-ui-express": "^5.0.1",
|
||||||
|
"web-push": "^3.6.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1"
|
||||||
@@ -714,6 +716,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/asn1.js": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bn.js": "^4.0.0",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"minimalistic-assert": "^1.0.0",
|
||||||
|
"safer-buffer": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ast-types": {
|
"node_modules/ast-types": {
|
||||||
"version": "0.13.4",
|
"version": "0.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
|
||||||
@@ -853,6 +867,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bn.js": {
|
||||||
|
"version": "4.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||||
|
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
@@ -906,6 +926,12 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -1253,6 +1279,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -1978,6 +2013,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http_ece": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
@@ -2239,6 +2283,27 @@
|
|||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.0",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lines-and-columns": {
|
"node_modules/lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"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",
|
||||||
@@ -2360,6 +2425,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimalistic-assert": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -2371,6 +2442,15 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||||
@@ -2451,6 +2531,15 @@
|
|||||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-cron": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
@@ -3981,6 +4070,70 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-push": {
|
||||||
|
"version": "3.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
|
||||||
|
"integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"asn1.js": "^5.3.0",
|
||||||
|
"http_ece": "1.2.0",
|
||||||
|
"https-proxy-agent": "^7.0.0",
|
||||||
|
"jws": "^4.0.0",
|
||||||
|
"minimist": "^1.2.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"web-push": "src/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/web-push/node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/web-push/node_modules/debug": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/web-push/node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/web-push/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
@@ -4681,6 +4834,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||||
},
|
},
|
||||||
|
"asn1.js": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||||
|
"requires": {
|
||||||
|
"bn.js": "^4.0.0",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"minimalistic-assert": "^1.0.0",
|
||||||
|
"safer-buffer": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ast-types": {
|
"ast-types": {
|
||||||
"version": "0.13.4",
|
"version": "0.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
|
||||||
@@ -4765,6 +4929,11 @@
|
|||||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"bn.js": {
|
||||||
|
"version": "4.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||||
|
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="
|
||||||
|
},
|
||||||
"body-parser": {
|
"body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
@@ -4807,6 +4976,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="
|
||||||
},
|
},
|
||||||
|
"buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||||
|
},
|
||||||
"bytes": {
|
"bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -5037,6 +5211,14 @@
|
|||||||
"gopd": "^1.2.0"
|
"gopd": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ee-first": {
|
"ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -5520,6 +5702,11 @@
|
|||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"http_ece": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA=="
|
||||||
|
},
|
||||||
"http-errors": {
|
"http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
@@ -5705,6 +5892,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||||
},
|
},
|
||||||
|
"jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"requires": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jws": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||||
|
"requires": {
|
||||||
|
"jwa": "^2.0.0",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"lines-and-columns": {
|
"lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"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",
|
||||||
@@ -5783,6 +5989,11 @@
|
|||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"minimalistic-assert": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
|
||||||
|
},
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -5791,6 +6002,11 @@
|
|||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
|
||||||
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||||
@@ -5845,6 +6061,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
|
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
|
||||||
},
|
},
|
||||||
|
"node-cron": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg=="
|
||||||
|
},
|
||||||
"node-fetch": {
|
"node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
@@ -6885,6 +7106,47 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
|
||||||
},
|
},
|
||||||
|
"web-push": {
|
||||||
|
"version": "3.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
|
||||||
|
"integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
|
||||||
|
"requires": {
|
||||||
|
"asn1.js": "^5.3.0",
|
||||||
|
"http_ece": "1.2.0",
|
||||||
|
"https-proxy-agent": "^7.0.0",
|
||||||
|
"jws": "^4.0.0",
|
||||||
|
"minimist": "^1.2.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"requires": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"webidl-conversions": {
|
"webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|||||||
@@ -16,10 +16,12 @@
|
|||||||
"enhanced-postgres-mcp-server": "^1.0.1",
|
"enhanced-postgres-mcp-server": "^1.0.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1"
|
"swagger-ui-express": "^5.0.1",
|
||||||
|
"web-push": "^3.6.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1"
|
||||||
|
|||||||
@@ -1374,3 +1374,9 @@ body {
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.last-location-info small {
|
||||||
|
color: #b3e5fc;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,68 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>SPEEDRUN ARENA - Admin Dashboard</title>
|
<title>SPEEDRUN ARENA - Admin Dashboard</title>
|
||||||
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Ninja Cross">
|
||||||
|
<link rel="apple-touch-icon" href="/pictures/favicon.ico">
|
||||||
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
|
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
|
||||||
<!-- QR Code Scanner Library -->
|
<!-- QR Code Scanner Library -->
|
||||||
<script src="https://unpkg.com/jsqr@1.4.0/dist/jsQR.js"></script>
|
<script src="https://unpkg.com/jsqr@1.4.0/dist/jsQR.js"></script>
|
||||||
<link rel="stylesheet" href="/css/dashboard.css">
|
<link rel="stylesheet" href="/css/dashboard.css">
|
||||||
|
|
||||||
|
<!-- Notification Permission Script -->
|
||||||
|
<script>
|
||||||
|
// Register Service Worker for iPhone Notifications
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then(function(registration) {
|
||||||
|
console.log('✅ Service Worker registered:', registration);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.log('❌ Service Worker registration failed:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request notification permission on page load
|
||||||
|
if ('Notification' in window) {
|
||||||
|
if (Notification.permission === 'default') {
|
||||||
|
Notification.requestPermission().then(function(permission) {
|
||||||
|
if (permission === 'granted') {
|
||||||
|
console.log('✅ Notification permission granted');
|
||||||
|
// Subscribe to push notifications
|
||||||
|
subscribeToPush();
|
||||||
|
} else {
|
||||||
|
console.log('❌ Notification permission denied');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to push notifications
|
||||||
|
async function subscribeToPush() {
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: 'BEl62iUYgUivxIkv69yViEuiBIa40HI6F2B5L4h7Q8Y'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send subscription to server
|
||||||
|
await fetch('/api/v1/public/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(subscription)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Push subscription successful');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Push subscription failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
@@ -34,6 +92,7 @@
|
|||||||
<div class="welcome-card">
|
<div class="welcome-card">
|
||||||
<h2>Dein Dashboard 🥷</h2>
|
<h2>Dein Dashboard 🥷</h2>
|
||||||
<p>Willkommen in Deinem Dashboard-Panel! Deine übersichtliche Übersicht aller deiner Läufe.</p>
|
<p>Willkommen in Deinem Dashboard-Panel! Deine übersichtliche Übersicht aller deiner Läufe.</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
@@ -252,6 +311,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/js/cookie-consent.js"></script>
|
<script src="/js/cookie-consent.js"></script>
|
||||||
<script src="/js/dashboard.js"></script>
|
<script src="/js/dashboard.js?v=1.1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>Timer Leaderboard</title>
|
<title>Timer Leaderboard</title>
|
||||||
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
|
||||||
<script src="/js/page-tracking.js"></script>
|
<script src="/js/page-tracking.js"></script>
|
||||||
|
<script src="/js/cookie-utils.js"></script>
|
||||||
<link rel="stylesheet" href="/css/leaderboard.css">
|
<link rel="stylesheet" href="/css/leaderboard.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
105
public/js/cookie-utils.js
Normal file
105
public/js/cookie-utils.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Cookie Utility Functions
|
||||||
|
class CookieManager {
|
||||||
|
// Set a cookie
|
||||||
|
static setCookie(name, value, days = 30) {
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
|
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a cookie
|
||||||
|
static getCookie(name) {
|
||||||
|
const nameEQ = name + "=";
|
||||||
|
const ca = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
||||||
|
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a cookie
|
||||||
|
static deleteCookie(name) {
|
||||||
|
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if cookies are enabled
|
||||||
|
static areCookiesEnabled() {
|
||||||
|
try {
|
||||||
|
this.setCookie('test', 'test');
|
||||||
|
const enabled = this.getCookie('test') === 'test';
|
||||||
|
this.deleteCookie('test');
|
||||||
|
return enabled;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location-specific cookie functions
|
||||||
|
class LocationCookieManager {
|
||||||
|
static COOKIE_NAME = 'ninjacross_last_location';
|
||||||
|
static COOKIE_EXPIRY_DAYS = 90; // 3 months
|
||||||
|
|
||||||
|
// Save last selected location
|
||||||
|
static saveLastLocation(locationId, locationName) {
|
||||||
|
if (!locationId || !locationName) return;
|
||||||
|
|
||||||
|
const locationData = {
|
||||||
|
id: locationId,
|
||||||
|
name: locationName,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
CookieManager.setCookie(
|
||||||
|
this.COOKIE_NAME,
|
||||||
|
JSON.stringify(locationData),
|
||||||
|
this.COOKIE_EXPIRY_DAYS
|
||||||
|
);
|
||||||
|
console.log('✅ Location saved to cookie:', locationName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to save location to cookie:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get last selected location
|
||||||
|
static getLastLocation() {
|
||||||
|
try {
|
||||||
|
const cookieValue = CookieManager.getCookie(this.COOKIE_NAME);
|
||||||
|
if (!cookieValue) return null;
|
||||||
|
|
||||||
|
const locationData = JSON.parse(cookieValue);
|
||||||
|
|
||||||
|
// Check if cookie is not too old (optional: 30 days max)
|
||||||
|
const cookieDate = new Date(locationData.timestamp);
|
||||||
|
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
|
||||||
|
if (Date.now() - cookieDate.getTime() > maxAge) {
|
||||||
|
this.clearLastLocation();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return locationData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to parse location cookie:', error);
|
||||||
|
this.clearLastLocation();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear last location
|
||||||
|
static clearLastLocation() {
|
||||||
|
CookieManager.deleteCookie(this.COOKIE_NAME);
|
||||||
|
console.log('🗑️ Location cookie cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if location cookie exists
|
||||||
|
static hasLastLocation() {
|
||||||
|
return this.getLastLocation() !== null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in other scripts
|
||||||
|
window.CookieManager = CookieManager;
|
||||||
|
window.LocationCookieManager = LocationCookieManager;
|
||||||
@@ -132,12 +132,13 @@ async function checkLinkStatusAndLoadTimes() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if user has a linked player
|
// Check if user has a linked player
|
||||||
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}`);
|
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}?t=${Date.now()}`);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
// User is linked, load times
|
// User is linked, load times
|
||||||
await loadUserTimesSection(result.data);
|
await loadUserTimesSection(result.data);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// User is not linked
|
// User is not linked
|
||||||
showTimesNotLinked();
|
showTimesNotLinked();
|
||||||
@@ -362,7 +363,7 @@ async function loadUserTimesSection(playerData) {
|
|||||||
showTimesLoading();
|
showTimesLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/public/user-times/${currentUser.id}`);
|
const response = await fetch(`/api/v1/public/user-times/${currentUser.id}?t=${Date.now()}`);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -608,14 +609,15 @@ async function loadPlayerAchievements() {
|
|||||||
document.getElementById('achievementCategories').style.display = 'none';
|
document.getElementById('achievementCategories').style.display = 'none';
|
||||||
document.getElementById('achievementsNotAvailable').style.display = 'none';
|
document.getElementById('achievementsNotAvailable').style.display = 'none';
|
||||||
|
|
||||||
// Load player achievements
|
// Load player achievements (includes all achievements with player status)
|
||||||
const response = await fetch(`/api/achievements/player/${currentPlayerId}`);
|
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to load achievements');
|
throw new Error('Failed to load player achievements');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
playerAchievements = result.data;
|
window.allAchievements = result.data;
|
||||||
|
playerAchievements = result.data.filter(achievement => achievement.is_completed);
|
||||||
|
|
||||||
// Load achievement statistics
|
// Load achievement statistics
|
||||||
await loadAchievementStats();
|
await loadAchievementStats();
|
||||||
@@ -639,7 +641,7 @@ async function loadPlayerAchievements() {
|
|||||||
// Load achievement statistics
|
// Load achievement statistics
|
||||||
async function loadAchievementStats() {
|
async function loadAchievementStats() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/achievements/player/${currentPlayerId}/stats`);
|
const response = await fetch(`/api/achievements/player/${currentPlayerId}/stats?t=${Date.now()}`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
window.achievementStats = result.data;
|
window.achievementStats = result.data;
|
||||||
@@ -665,7 +667,7 @@ function displayAchievementStats() {
|
|||||||
function displayAchievements() {
|
function displayAchievements() {
|
||||||
const achievementsGrid = document.getElementById('achievementsGrid');
|
const achievementsGrid = document.getElementById('achievementsGrid');
|
||||||
|
|
||||||
if (playerAchievements.length === 0) {
|
if (!window.allAchievements || window.allAchievements.length === 0) {
|
||||||
achievementsGrid.innerHTML = `
|
achievementsGrid.innerHTML = `
|
||||||
<div class="no-achievements">
|
<div class="no-achievements">
|
||||||
<div class="no-achievements-icon">🏆</div>
|
<div class="no-achievements-icon">🏆</div>
|
||||||
@@ -677,9 +679,9 @@ function displayAchievements() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter achievements by category
|
// Filter achievements by category
|
||||||
let filteredAchievements = playerAchievements;
|
let filteredAchievements = window.allAchievements;
|
||||||
if (currentAchievementCategory !== 'all') {
|
if (currentAchievementCategory !== 'all') {
|
||||||
filteredAchievements = playerAchievements.filter(achievement =>
|
filteredAchievements = window.allAchievements.filter(achievement =>
|
||||||
achievement.category === currentAchievementCategory
|
achievement.category === currentAchievementCategory
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -690,6 +692,11 @@ function displayAchievements() {
|
|||||||
const progress = achievement.progress || 0;
|
const progress = achievement.progress || 0;
|
||||||
const earnedAt = achievement.earned_at;
|
const earnedAt = achievement.earned_at;
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
if (achievement.name === 'Tageskönig') {
|
||||||
|
console.log('Tageskönig Debug:', { isCompleted, progress, earnedAt });
|
||||||
|
}
|
||||||
|
|
||||||
let progressText = '';
|
let progressText = '';
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
progressText = earnedAt ?
|
progressText = earnedAt ?
|
||||||
@@ -780,7 +787,7 @@ async function checkPlayerAchievements() {
|
|||||||
if (!currentPlayerId) return;
|
if (!currentPlayerId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/achievements/check/${currentPlayerId}`, {
|
const response = await fetch(`/api/achievements/check/${currentPlayerId}?t=${Date.now()}`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -831,6 +838,109 @@ function initializeAchievements(playerId) {
|
|||||||
loadPlayerAchievements();
|
loadPlayerAchievements();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Web Notification Functions
|
||||||
|
function showWebNotification(title, message, icon = '🏆') {
|
||||||
|
if ('Notification' in window && Notification.permission === 'granted') {
|
||||||
|
const notification = new Notification(title, {
|
||||||
|
body: message,
|
||||||
|
icon: '/pictures/favicon.ico',
|
||||||
|
badge: '/pictures/favicon.ico',
|
||||||
|
tag: 'ninjacross-achievement',
|
||||||
|
requireInteraction: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-close after 10 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.close();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
// Handle click
|
||||||
|
notification.onclick = function() {
|
||||||
|
window.focus();
|
||||||
|
notification.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for best time achievements and show notifications
|
||||||
|
async function checkBestTimeNotifications() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/public/best-times');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
const { daily, weekly, monthly } = result.data;
|
||||||
|
|
||||||
|
// Check if current player has best times
|
||||||
|
if (currentPlayerId) {
|
||||||
|
if (daily && daily.player_id === currentPlayerId) {
|
||||||
|
showWebNotification(
|
||||||
|
'🏆 Tageskönig!',
|
||||||
|
`Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!`,
|
||||||
|
'👑'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weekly && weekly.player_id === currentPlayerId) {
|
||||||
|
showWebNotification(
|
||||||
|
'🏆 Wochenchampion!',
|
||||||
|
`Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!`,
|
||||||
|
'🏆'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monthly && monthly.player_id === currentPlayerId) {
|
||||||
|
showWebNotification(
|
||||||
|
'🏆 Monatsmeister!',
|
||||||
|
`Unglaublich! Du bist der Monatsmeister mit ${monthly.best_time}!`,
|
||||||
|
'🥇'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking best time notifications:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for new achievements and show notifications
|
||||||
|
async function checkAchievementNotifications() {
|
||||||
|
try {
|
||||||
|
if (!currentPlayerId) return;
|
||||||
|
|
||||||
|
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
const newAchievements = result.data.filter(achievement => {
|
||||||
|
// Check if achievement was earned in the last 5 minutes
|
||||||
|
const earnedAt = new Date(achievement.earned_at);
|
||||||
|
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
||||||
|
return earnedAt > fiveMinutesAgo;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newAchievements.length > 0) {
|
||||||
|
newAchievements.forEach(achievement => {
|
||||||
|
showWebNotification(
|
||||||
|
`🏆 ${achievement.name}`,
|
||||||
|
achievement.description,
|
||||||
|
achievement.icon || '🏆'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking achievement notifications:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Periodic check for notifications (every 30 seconds)
|
||||||
|
setInterval(() => {
|
||||||
|
checkBestTimeNotifications();
|
||||||
|
checkAchievementNotifications();
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
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');
|
||||||
|
|||||||
@@ -10,6 +10,58 @@ const socket = io();
|
|||||||
|
|
||||||
// Global variable to store locations with coordinates
|
// Global variable to store locations with coordinates
|
||||||
let locationsData = [];
|
let locationsData = [];
|
||||||
|
let lastSelectedLocation = null;
|
||||||
|
|
||||||
|
// Cookie Functions (inline implementation)
|
||||||
|
function setCookie(name, value, days = 30) {
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
|
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
const nameEQ = name + "=";
|
||||||
|
const ca = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
||||||
|
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLastSelectedLocation() {
|
||||||
|
try {
|
||||||
|
const cookieValue = getCookie('ninjacross_last_location');
|
||||||
|
if (cookieValue) {
|
||||||
|
const lastLocation = JSON.parse(cookieValue);
|
||||||
|
lastSelectedLocation = lastLocation;
|
||||||
|
console.log('📍 Last selected location loaded:', lastLocation.name);
|
||||||
|
return lastLocation;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading last location:', error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLocationSelection(locationId, locationName) {
|
||||||
|
try {
|
||||||
|
// Remove emoji from location name for storage
|
||||||
|
const cleanName = locationName.replace(/^📍\s*/, '');
|
||||||
|
const locationData = {
|
||||||
|
id: locationId,
|
||||||
|
name: cleanName,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
setCookie('ninjacross_last_location', JSON.stringify(locationData), 90);
|
||||||
|
lastSelectedLocation = { id: locationId, name: cleanName };
|
||||||
|
console.log('💾 Location saved to cookie:', cleanName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving location:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WebSocket Event Handlers
|
// WebSocket Event Handlers
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
@@ -129,6 +181,23 @@ async function loadLocations() {
|
|||||||
locationSelect.appendChild(option);
|
locationSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load and set last selected location
|
||||||
|
const lastLocation = loadLastSelectedLocation();
|
||||||
|
if (lastLocation) {
|
||||||
|
// Find the option that matches the last location name
|
||||||
|
const matchingOption = Array.from(locationSelect.options).find(option =>
|
||||||
|
option.textContent === `📍 ${lastLocation.name}` || option.value === lastLocation.name
|
||||||
|
);
|
||||||
|
if (matchingOption) {
|
||||||
|
locationSelect.value = matchingOption.value;
|
||||||
|
console.log('📍 Last selected location restored:', lastLocation.name);
|
||||||
|
// Update the current selection display
|
||||||
|
updateCurrentSelection();
|
||||||
|
// Load data for the restored location
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading locations:', error);
|
console.error('Error loading locations:', error);
|
||||||
}
|
}
|
||||||
@@ -489,7 +558,15 @@ function updateLeaderboard(data) {
|
|||||||
// Event Listeners Setup
|
// Event Listeners Setup
|
||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
// Location select event listener
|
// Location select event listener
|
||||||
document.getElementById('locationSelect').addEventListener('change', loadData);
|
document.getElementById('locationSelect').addEventListener('change', function() {
|
||||||
|
// Save location selection to cookie
|
||||||
|
const selectedOption = this.options[this.selectedIndex];
|
||||||
|
if (selectedOption.value) {
|
||||||
|
saveLocationSelection(selectedOption.value, selectedOption.textContent);
|
||||||
|
}
|
||||||
|
// Load data
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
// Time tab event listeners
|
// Time tab event listeners
|
||||||
document.querySelectorAll('.time-tab').forEach(tab => {
|
document.querySelectorAll('.time-tab').forEach(tab => {
|
||||||
|
|||||||
43
public/manifest.json
Normal file
43
public/manifest.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "Ninja Cross Parkour",
|
||||||
|
"short_name": "NinjaCross",
|
||||||
|
"description": "Dein persönliches Dashboard für die Ninja Cross Arena",
|
||||||
|
"start_url": "/index.html",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#667eea",
|
||||||
|
"theme_color": "#764ba2",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/pictures/favicon.ico",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/x-icon",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/pictures/favicon.ico",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/x-icon",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": ["sports", "fitness", "entertainment"],
|
||||||
|
"lang": "de",
|
||||||
|
"dir": "ltr",
|
||||||
|
"scope": "/",
|
||||||
|
"prefer_related_applications": false,
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "Dashboard",
|
||||||
|
"short_name": "Dashboard",
|
||||||
|
"description": "Öffne dein Dashboard",
|
||||||
|
"url": "/index.html",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/pictures/favicon.ico",
|
||||||
|
"sizes": "192x192"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
94
public/sw.js
Normal file
94
public/sw.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Service Worker für iPhone Notifications
|
||||||
|
const CACHE_NAME = 'ninjacross-v1';
|
||||||
|
const urlsToCache = [
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/css/leaderboard.css',
|
||||||
|
'/js/leaderboard.js',
|
||||||
|
'/pictures/favicon.ico'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Install event
|
||||||
|
self.addEventListener('install', function(event) {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then(function(cache) {
|
||||||
|
return cache.addAll(urlsToCache);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch event
|
||||||
|
self.addEventListener('fetch', function(event) {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request)
|
||||||
|
.then(function(response) {
|
||||||
|
// Return cached version or fetch from network
|
||||||
|
return response || fetch(event.request);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push event (für iPhone Notifications)
|
||||||
|
self.addEventListener('push', function(event) {
|
||||||
|
console.log('Push received:', event);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
body: 'Du hast eine neue Notification!',
|
||||||
|
icon: '/pictures/favicon.ico',
|
||||||
|
badge: '/pictures/favicon.ico',
|
||||||
|
vibrate: [100, 50, 100],
|
||||||
|
data: {
|
||||||
|
dateOfArrival: Date.now(),
|
||||||
|
primaryKey: 1
|
||||||
|
},
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'explore',
|
||||||
|
title: 'Dashboard öffnen',
|
||||||
|
icon: '/pictures/favicon.ico'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'close',
|
||||||
|
title: 'Schließen',
|
||||||
|
icon: '/pictures/favicon.ico'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.data) {
|
||||||
|
const data = event.data.json();
|
||||||
|
options.body = data.body || options.body;
|
||||||
|
options.title = data.title || 'Ninja Cross';
|
||||||
|
}
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification('Ninja Cross', options)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notification click event
|
||||||
|
self.addEventListener('notificationclick', function(event) {
|
||||||
|
console.log('Notification clicked:', event);
|
||||||
|
|
||||||
|
event.notification.close();
|
||||||
|
|
||||||
|
if (event.action === 'explore') {
|
||||||
|
event.waitUntil(
|
||||||
|
clients.openWindow('/dashboard.html')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Background sync (für offline functionality)
|
||||||
|
self.addEventListener('sync', function(event) {
|
||||||
|
if (event.tag === 'background-sync') {
|
||||||
|
event.waitUntil(doBackgroundSync());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function doBackgroundSync() {
|
||||||
|
// Sync data when back online
|
||||||
|
console.log('Background sync triggered');
|
||||||
|
}
|
||||||
299
routes/api.js
299
routes/api.js
@@ -65,6 +65,11 @@ function convertTimeToSeconds(timeStr) {
|
|||||||
function convertIntervalToSeconds(interval) {
|
function convertIntervalToSeconds(interval) {
|
||||||
if (!interval) return 0;
|
if (!interval) return 0;
|
||||||
|
|
||||||
|
// Handle PostgreSQL interval object format
|
||||||
|
if (typeof interval === 'object' && interval.seconds !== undefined) {
|
||||||
|
return parseFloat(interval.seconds);
|
||||||
|
}
|
||||||
|
|
||||||
// PostgreSQL interval format: "HH:MM:SS" or "MM:SS" or just seconds
|
// PostgreSQL interval format: "HH:MM:SS" or "MM:SS" or just seconds
|
||||||
if (typeof interval === 'string') {
|
if (typeof interval === 'string') {
|
||||||
const parts = interval.split(':');
|
const parts = interval.split(':');
|
||||||
@@ -853,30 +858,27 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => {
|
|||||||
const thresholdSeconds = convertIntervalToSeconds(location.time_threshold);
|
const thresholdSeconds = convertIntervalToSeconds(location.time_threshold);
|
||||||
|
|
||||||
if (recordedTimeSeconds < thresholdSeconds) {
|
if (recordedTimeSeconds < thresholdSeconds) {
|
||||||
|
// Convert threshold to readable format
|
||||||
|
const thresholdMinutes = Math.floor(thresholdSeconds / 60);
|
||||||
|
const thresholdSecs = Math.floor(thresholdSeconds % 60);
|
||||||
|
const thresholdMs = Math.floor((thresholdSeconds % 1) * 1000);
|
||||||
|
const thresholdDisplay = thresholdMinutes > 0
|
||||||
|
? `${thresholdMinutes}:${thresholdSecs.toString().padStart(2, '0')}.${thresholdMs.toString().padStart(3, '0')}`
|
||||||
|
: `${thresholdSecs}.${thresholdMs.toString().padStart(3, '0')}`;
|
||||||
|
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: `Zeit ${recorded_time} liegt unter dem Schwellenwert von ${location.time_threshold} für diesen Standort`,
|
message: `Zeit ${recorded_time} liegt unter dem Schwellenwert von ${thresholdDisplay} für diesen Standort`,
|
||||||
data: {
|
data: {
|
||||||
recorded_time: recorded_time,
|
recorded_time: recorded_time,
|
||||||
threshold: location.time_threshold,
|
threshold: location.time_threshold,
|
||||||
|
threshold_display: thresholdDisplay,
|
||||||
location_name: location_name
|
location_name: location_name
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfen ob Zeit bereits existiert (optional - kann entfernt werden)
|
|
||||||
const existingTime = await pool.query(
|
|
||||||
'SELECT id FROM times WHERE player_id = $1 AND location_id = $2 AND recorded_time = $3',
|
|
||||||
[player_id, location_id, recorded_time]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingTime.rows.length > 0) {
|
|
||||||
return res.status(409).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Zeit existiert bereits in der Datenbank'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zeit in Datenbank einfügen
|
// Zeit in Datenbank einfügen
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
@@ -2134,6 +2136,38 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { player_id, location_id, time_seconds } = req.body;
|
const { player_id, location_id, time_seconds } = req.body;
|
||||||
|
|
||||||
|
// Prüfen ob Location existiert und Threshold abrufen
|
||||||
|
const locationResult = await pool.query(
|
||||||
|
'SELECT id, name, time_threshold FROM locations WHERE id = $1',
|
||||||
|
[location_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (locationResult.rows.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Standort nicht gefunden'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const location = locationResult.rows[0];
|
||||||
|
|
||||||
|
// Prüfen ob die Zeit über dem Schwellenwert liegt
|
||||||
|
if (location.time_threshold) {
|
||||||
|
const thresholdSeconds = convertIntervalToSeconds(location.time_threshold);
|
||||||
|
|
||||||
|
if (time_seconds < thresholdSeconds) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Zeit ${time_seconds}s liegt unter dem Schwellenwert von ${location.time_threshold} für diesen Standort`,
|
||||||
|
data: {
|
||||||
|
recorded_time: `${time_seconds}s`,
|
||||||
|
threshold: location.time_threshold,
|
||||||
|
location_name: location.name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Zeit in INTERVAL konvertieren
|
// Zeit in INTERVAL konvertieren
|
||||||
const timeInterval = `${time_seconds} seconds`;
|
const timeInterval = `${time_seconds} seconds`;
|
||||||
|
|
||||||
@@ -2300,6 +2334,198 @@ router.put('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== BEST TIMES ENDPOINTS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/v1/public/best-times:
|
||||||
|
* get:
|
||||||
|
* summary: Beste Zeiten abrufen
|
||||||
|
* description: Ruft die besten Zeiten des Tages, der Woche und des Monats ab
|
||||||
|
* tags: [Times]
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Beste Zeiten erfolgreich abgerufen
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* daily:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* player_id:
|
||||||
|
* type: string
|
||||||
|
* format: uuid
|
||||||
|
* player_name:
|
||||||
|
* type: string
|
||||||
|
* best_time:
|
||||||
|
* type: string
|
||||||
|
* format: time
|
||||||
|
* weekly:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* player_id:
|
||||||
|
* type: string
|
||||||
|
* format: uuid
|
||||||
|
* player_name:
|
||||||
|
* type: string
|
||||||
|
* best_time:
|
||||||
|
* type: string
|
||||||
|
* format: time
|
||||||
|
* monthly:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* player_id:
|
||||||
|
* type: string
|
||||||
|
* format: uuid
|
||||||
|
* player_name:
|
||||||
|
* type: string
|
||||||
|
* best_time:
|
||||||
|
* type: string
|
||||||
|
* format: time
|
||||||
|
* 500:
|
||||||
|
* description: Server-Fehler
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
// Get best times (daily, weekly, monthly)
|
||||||
|
router.get('/v1/public/best-times', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const currentDate = new Date();
|
||||||
|
const today = currentDate.toISOString().split('T')[0];
|
||||||
|
const weekStart = new Date(currentDate.setDate(currentDate.getDate() - currentDate.getDay()));
|
||||||
|
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
|
||||||
|
|
||||||
|
// Get daily best
|
||||||
|
const dailyResult = await pool.query(`
|
||||||
|
WITH daily_best AS (
|
||||||
|
SELECT
|
||||||
|
t.player_id,
|
||||||
|
MIN(t.recorded_time) as best_time,
|
||||||
|
CONCAT(p.firstname, ' ', p.lastname) as player_name
|
||||||
|
FROM times t
|
||||||
|
JOIN players p ON t.player_id = p.id
|
||||||
|
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $1
|
||||||
|
GROUP BY t.player_id, p.firstname, p.lastname
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
player_id,
|
||||||
|
player_name,
|
||||||
|
best_time
|
||||||
|
FROM daily_best
|
||||||
|
WHERE best_time = (SELECT MIN(best_time) FROM daily_best)
|
||||||
|
LIMIT 1
|
||||||
|
`, [today]);
|
||||||
|
|
||||||
|
// Get weekly best
|
||||||
|
const weeklyResult = await pool.query(`
|
||||||
|
WITH weekly_best AS (
|
||||||
|
SELECT
|
||||||
|
t.player_id,
|
||||||
|
MIN(t.recorded_time) as best_time,
|
||||||
|
CONCAT(p.firstname, ' ', p.lastname) as player_name
|
||||||
|
FROM times t
|
||||||
|
JOIN players p ON t.player_id = p.id
|
||||||
|
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
|
||||||
|
GROUP BY t.player_id, p.firstname, p.lastname
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
player_id,
|
||||||
|
player_name,
|
||||||
|
best_time
|
||||||
|
FROM weekly_best
|
||||||
|
WHERE best_time = (SELECT MIN(best_time) FROM weekly_best)
|
||||||
|
LIMIT 1
|
||||||
|
`, [weekStart.toISOString().split('T')[0], today]);
|
||||||
|
|
||||||
|
// Get monthly best
|
||||||
|
const monthlyResult = await pool.query(`
|
||||||
|
WITH monthly_best AS (
|
||||||
|
SELECT
|
||||||
|
t.player_id,
|
||||||
|
MIN(t.recorded_time) as best_time,
|
||||||
|
CONCAT(p.firstname, ' ', p.lastname) as player_name
|
||||||
|
FROM times t
|
||||||
|
JOIN players p ON t.player_id = p.id
|
||||||
|
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
|
||||||
|
GROUP BY t.player_id, p.firstname, p.lastname
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
player_id,
|
||||||
|
player_name,
|
||||||
|
best_time
|
||||||
|
FROM monthly_best
|
||||||
|
WHERE best_time = (SELECT MIN(best_time) FROM monthly_best)
|
||||||
|
LIMIT 1
|
||||||
|
`, [monthStart.toISOString().split('T')[0], today]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
daily: dailyResult.rows[0] || null,
|
||||||
|
weekly: weeklyResult.rows[0] || null,
|
||||||
|
monthly: monthlyResult.rows[0] || null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching best times:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Abrufen der Bestzeiten',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== PUSH NOTIFICATION ENDPOINTS ====================
|
||||||
|
|
||||||
|
// Subscribe to push notifications
|
||||||
|
router.post('/v1/public/subscribe', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { endpoint, keys } = req.body;
|
||||||
|
|
||||||
|
// Store subscription in database
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO player_subscriptions (player_id, endpoint, p256dh, auth, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, NOW())
|
||||||
|
ON CONFLICT (player_id)
|
||||||
|
DO UPDATE SET
|
||||||
|
endpoint = EXCLUDED.endpoint,
|
||||||
|
p256dh = EXCLUDED.p256dh,
|
||||||
|
auth = EXCLUDED.auth,
|
||||||
|
updated_at = NOW()
|
||||||
|
`, [
|
||||||
|
req.session.userId || 'anonymous',
|
||||||
|
endpoint,
|
||||||
|
keys.p256dh,
|
||||||
|
keys.auth
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Push subscription erfolgreich gespeichert'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error storing push subscription:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Speichern der Push Subscription',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== ACHIEVEMENT ENDPOINTS ====================
|
// ==================== ACHIEVEMENT ENDPOINTS ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2396,6 +2622,13 @@ router.get('/achievements', async (req, res) => {
|
|||||||
// Get player achievements
|
// Get player achievements
|
||||||
router.get('/achievements/player/:playerId', async (req, res) => {
|
router.get('/achievements/player/:playerId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Set no-cache headers
|
||||||
|
res.set({
|
||||||
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
'Expires': '0'
|
||||||
|
});
|
||||||
|
|
||||||
const { playerId } = req.params;
|
const { playerId } = req.params;
|
||||||
|
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
@@ -2406,8 +2639,8 @@ router.get('/achievements/player/:playerId', async (req, res) => {
|
|||||||
a.category,
|
a.category,
|
||||||
a.icon,
|
a.icon,
|
||||||
a.points,
|
a.points,
|
||||||
pa.progress,
|
COALESCE(pa.progress, 0) as progress,
|
||||||
pa.is_completed,
|
COALESCE(pa.is_completed, false) as is_completed,
|
||||||
pa.earned_at
|
pa.earned_at
|
||||||
FROM achievements a
|
FROM achievements a
|
||||||
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
|
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
|
||||||
@@ -2434,11 +2667,25 @@ router.get('/achievements/player/:playerId', async (req, res) => {
|
|||||||
// Get player achievement statistics
|
// Get player achievement statistics
|
||||||
router.get('/achievements/player/:playerId/stats', async (req, res) => {
|
router.get('/achievements/player/:playerId/stats', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Set no-cache headers
|
||||||
|
res.set({
|
||||||
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
'Expires': '0'
|
||||||
|
});
|
||||||
|
|
||||||
const { playerId } = req.params;
|
const { playerId } = req.params;
|
||||||
|
|
||||||
const result = await pool.query(`
|
// Get total available achievements
|
||||||
|
const totalResult = await pool.query(`
|
||||||
|
SELECT COUNT(*) as total_achievements
|
||||||
|
FROM achievements
|
||||||
|
WHERE is_active = true
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get player's earned achievements
|
||||||
|
const playerResult = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(pa.id) as total_achievements,
|
|
||||||
COUNT(CASE WHEN pa.is_completed = true THEN 1 END) as completed_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,
|
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
|
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
|
||||||
@@ -2447,17 +2694,21 @@ router.get('/achievements/player/:playerId/stats', async (req, res) => {
|
|||||||
WHERE a.is_active = true
|
WHERE a.is_active = true
|
||||||
`, [playerId]);
|
`, [playerId]);
|
||||||
|
|
||||||
const stats = result.rows[0];
|
const totalStats = totalResult.rows[0];
|
||||||
|
const playerStats = playerResult.rows[0];
|
||||||
|
|
||||||
|
const totalAchievements = parseInt(totalStats.total_achievements);
|
||||||
|
const completedAchievements = parseInt(playerStats.completed_achievements) || 0;
|
||||||
|
const completionPercentage = totalAchievements > 0 ? Math.round((completedAchievements / totalAchievements) * 100) : 0;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
total_achievements: parseInt(stats.total_achievements),
|
total_achievements: totalAchievements,
|
||||||
completed_achievements: parseInt(stats.completed_achievements),
|
completed_achievements: completedAchievements,
|
||||||
total_points: parseInt(stats.total_points) || 0,
|
total_points: parseInt(playerStats.total_points) || 0,
|
||||||
achievements_today: parseInt(stats.achievements_today),
|
achievements_today: parseInt(playerStats.achievements_today) || 0,
|
||||||
completion_percentage: stats.total_achievements > 0 ?
|
completion_percentage: completionPercentage
|
||||||
Math.round((stats.completed_achievements / stats.total_achievements) * 100) : 0
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
141
scripts/best_time_notifications.js
Normal file
141
scripts/best_time_notifications.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
const { Pool } = require('pg');
|
||||||
|
const cron = require('node-cron');
|
||||||
|
|
||||||
|
// Database connection
|
||||||
|
const pool = new Pool({
|
||||||
|
user: 'postgres',
|
||||||
|
host: 'localhost',
|
||||||
|
database: 'ninjacross',
|
||||||
|
password: 'postgres',
|
||||||
|
port: 5432,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkAndNotifyBestTimes() {
|
||||||
|
try {
|
||||||
|
console.log('🔔 Checking best times for notifications...');
|
||||||
|
|
||||||
|
const currentDate = new Date();
|
||||||
|
const today = currentDate.toISOString().split('T')[0];
|
||||||
|
const weekStart = new Date(currentDate.setDate(currentDate.getDate() - currentDate.getDay()));
|
||||||
|
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
|
||||||
|
|
||||||
|
// Check daily best times
|
||||||
|
const dailyBestQuery = `
|
||||||
|
WITH daily_best AS (
|
||||||
|
SELECT
|
||||||
|
t.player_id,
|
||||||
|
MIN(t.recorded_time) as best_time,
|
||||||
|
p.name as player_name,
|
||||||
|
p.email
|
||||||
|
FROM times t
|
||||||
|
JOIN players p ON t.player_id = p.id
|
||||||
|
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $1
|
||||||
|
GROUP BY t.player_id, p.name, p.email
|
||||||
|
),
|
||||||
|
global_daily_best AS (
|
||||||
|
SELECT MIN(best_time) as global_best
|
||||||
|
FROM daily_best
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
db.player_id,
|
||||||
|
db.player_name,
|
||||||
|
db.email,
|
||||||
|
db.best_time,
|
||||||
|
gdb.global_best
|
||||||
|
FROM daily_best db
|
||||||
|
CROSS JOIN global_daily_best gdb
|
||||||
|
WHERE db.best_time = gdb.global_best
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dailyResult = await pool.query(dailyBestQuery, [today]);
|
||||||
|
|
||||||
|
for (const row of dailyResult.rows) {
|
||||||
|
console.log(`🏆 Daily best time: ${row.player_name} with ${row.best_time}`);
|
||||||
|
// Here we would send the notification
|
||||||
|
// For now, just log it
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check weekly best times
|
||||||
|
const weeklyBestQuery = `
|
||||||
|
WITH weekly_best AS (
|
||||||
|
SELECT
|
||||||
|
t.player_id,
|
||||||
|
MIN(t.recorded_time) as best_time,
|
||||||
|
p.name as player_name,
|
||||||
|
p.email
|
||||||
|
FROM times t
|
||||||
|
JOIN players p ON t.player_id = p.id
|
||||||
|
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
|
||||||
|
GROUP BY t.player_id, p.name, p.email
|
||||||
|
),
|
||||||
|
global_weekly_best AS (
|
||||||
|
SELECT MIN(best_time) as global_best
|
||||||
|
FROM weekly_best
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
wb.player_id,
|
||||||
|
wb.player_name,
|
||||||
|
wb.email,
|
||||||
|
wb.best_time,
|
||||||
|
gwb.global_best
|
||||||
|
FROM weekly_best wb
|
||||||
|
CROSS JOIN global_weekly_best gwb
|
||||||
|
WHERE wb.best_time = gwb.global_best
|
||||||
|
`;
|
||||||
|
|
||||||
|
const weeklyResult = await pool.query(weeklyBestQuery, [weekStart.toISOString().split('T')[0], today]);
|
||||||
|
|
||||||
|
for (const row of weeklyResult.rows) {
|
||||||
|
console.log(`🏆 Weekly best time: ${row.player_name} with ${row.best_time}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check monthly best times
|
||||||
|
const monthlyBestQuery = `
|
||||||
|
WITH monthly_best AS (
|
||||||
|
SELECT
|
||||||
|
t.player_id,
|
||||||
|
MIN(t.recorded_time) as best_time,
|
||||||
|
p.name as player_name,
|
||||||
|
p.email
|
||||||
|
FROM times t
|
||||||
|
JOIN players p ON t.player_id = p.id
|
||||||
|
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
|
||||||
|
GROUP BY t.player_id, p.name, p.email
|
||||||
|
),
|
||||||
|
global_monthly_best AS (
|
||||||
|
SELECT MIN(best_time) as global_best
|
||||||
|
FROM monthly_best
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
mb.player_id,
|
||||||
|
mb.player_name,
|
||||||
|
mb.email,
|
||||||
|
mb.best_time,
|
||||||
|
gmb.global_best
|
||||||
|
FROM monthly_best mb
|
||||||
|
CROSS JOIN global_monthly_best gmb
|
||||||
|
WHERE mb.best_time = gmb.global_best
|
||||||
|
`;
|
||||||
|
|
||||||
|
const monthlyResult = await pool.query(monthlyBestQuery, [monthStart.toISOString().split('T')[0], today]);
|
||||||
|
|
||||||
|
for (const row of monthlyResult.rows) {
|
||||||
|
console.log(`🏆 Monthly best time: ${row.player_name} with ${row.best_time}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Best time notifications check completed');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error checking best times:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule to run every day at 19:00 (7 PM)
|
||||||
|
cron.schedule('0 19 * * *', () => {
|
||||||
|
console.log('🕐 Running best time notifications check...');
|
||||||
|
checkAndNotifyBestTimes();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📅 Best time notifications scheduler started - runs daily at 19:00');
|
||||||
135
scripts/iphone_notifications.js
Normal file
135
scripts/iphone_notifications.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
const { Pool } = require('pg');
|
||||||
|
const webpush = require('web-push');
|
||||||
|
|
||||||
|
// Database connection
|
||||||
|
const pool = new Pool({
|
||||||
|
user: 'postgres',
|
||||||
|
host: 'localhost',
|
||||||
|
database: 'ninjacross',
|
||||||
|
password: 'postgres',
|
||||||
|
port: 5432,
|
||||||
|
});
|
||||||
|
|
||||||
|
// VAPID Keys (generate with: webpush.generateVAPIDKeys())
|
||||||
|
const vapidKeys = {
|
||||||
|
publicKey: 'BEl62iUYgUivxIkv69yViEuiBIa40HI6F2B5L4h7Q8Y',
|
||||||
|
privateKey: 'your-private-key-here'
|
||||||
|
};
|
||||||
|
|
||||||
|
webpush.setVapidDetails(
|
||||||
|
'mailto:admin@ninjacross.es',
|
||||||
|
vapidKeys.publicKey,
|
||||||
|
vapidKeys.privateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store subscription endpoint for each player
|
||||||
|
async function storePlayerSubscription(playerId, subscription) {
|
||||||
|
try {
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO player_subscriptions (player_id, endpoint, p256dh, auth)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT (player_id)
|
||||||
|
DO UPDATE SET
|
||||||
|
endpoint = EXCLUDED.endpoint,
|
||||||
|
p256dh = EXCLUDED.p256dh,
|
||||||
|
auth = EXCLUDED.auth,
|
||||||
|
updated_at = NOW()
|
||||||
|
`, [
|
||||||
|
playerId,
|
||||||
|
subscription.endpoint,
|
||||||
|
subscription.keys.p256dh,
|
||||||
|
subscription.keys.auth
|
||||||
|
]);
|
||||||
|
console.log(`✅ Subscription stored for player ${playerId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error storing subscription:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send push notification to player
|
||||||
|
async function sendPushNotification(playerId, title, message, icon = '🏆') {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT endpoint, p256dh, auth
|
||||||
|
FROM player_subscriptions
|
||||||
|
WHERE player_id = $1
|
||||||
|
`, [playerId]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
console.log(`No subscription found for player ${playerId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = {
|
||||||
|
endpoint: result.rows[0].endpoint,
|
||||||
|
keys: {
|
||||||
|
p256dh: result.rows[0].p256dh,
|
||||||
|
auth: result.rows[0].auth
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
title: title,
|
||||||
|
body: message,
|
||||||
|
icon: '/pictures/favicon.ico',
|
||||||
|
badge: '/pictures/favicon.ico',
|
||||||
|
data: {
|
||||||
|
url: '/dashboard.html'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await webpush.sendNotification(subscription, payload);
|
||||||
|
console.log(`✅ Push notification sent to player ${playerId}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending push notification:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send best time notifications
|
||||||
|
async function sendBestTimeNotifications() {
|
||||||
|
try {
|
||||||
|
console.log('🔔 Sending best time notifications...');
|
||||||
|
|
||||||
|
// Get daily best
|
||||||
|
const dailyResult = await pool.query(`
|
||||||
|
WITH daily_best AS (
|
||||||
|
SELECT
|
||||||
|
t.player_id,
|
||||||
|
MIN(t.recorded_time) as best_time,
|
||||||
|
CONCAT(p.firstname, ' ', p.lastname) as player_name
|
||||||
|
FROM times t
|
||||||
|
JOIN players p ON t.player_id = p.id
|
||||||
|
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
|
||||||
|
GROUP BY t.player_id, p.firstname, p.lastname
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
player_id,
|
||||||
|
player_name,
|
||||||
|
best_time
|
||||||
|
FROM daily_best
|
||||||
|
WHERE best_time = (SELECT MIN(best_time) FROM daily_best)
|
||||||
|
LIMIT 1
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (dailyResult.rows.length > 0) {
|
||||||
|
const daily = dailyResult.rows[0];
|
||||||
|
await sendPushNotification(
|
||||||
|
daily.player_id,
|
||||||
|
'🏆 Tageskönig!',
|
||||||
|
`Glückwunsch ${daily.player_name}! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Best time notifications sent');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error sending best time notifications:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
storePlayerSubscription,
|
||||||
|
sendPushNotification,
|
||||||
|
sendBestTimeNotifications
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user