Compare commits
10 Commits
0326a5b635
...
7f808b159d
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f808b159d | |||
| cfbe7594e8 | |||
| ca1badad00 | |||
| f1b46b788d | |||
| be4c25d333 | |||
| 9f7e1cf7ba | |||
| fdf10196a0 | |||
| 8450226211 | |||
| d9d2d5c5af | |||
| 74e10a44da |
405
package-lock.json
generated
405
package-lock.json
generated
@@ -12,7 +12,8 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"humanize-duration-ts": "^2.1.1",
|
"humanize-duration-ts": "^2.1.1",
|
||||||
"ip-range-check": "^0.2.0",
|
"ip-range-check": "^0.2.0",
|
||||||
"js-yaml": "^4.1.0"
|
"js-yaml": "^4.1.0",
|
||||||
|
"pino": "^8.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"@sveltejs/adapter-node": "^1.3.1",
|
"@sveltejs/adapter-node": "^1.3.1",
|
||||||
"@sveltejs/kit": "^1.20.4",
|
"@sveltejs/kit": "^1.20.4",
|
||||||
"@tailwindcss/forms": "^0.5.6",
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
|
"@types/js-yaml": "^4.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"@vitest/ui": "^0.34.1",
|
"@vitest/ui": "^0.34.1",
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-svelte": "^2.30.0",
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
|
"pino-pretty": "^10.2.0",
|
||||||
"postcss": "^8.4.30",
|
"postcss": "^8.4.30",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
@@ -966,6 +969,12 @@
|
|||||||
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
|
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-yaml": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.12",
|
"version": "7.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
|
||||||
@@ -1314,6 +1323,17 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/abort-controller": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.10.0",
|
"version": "8.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||||
@@ -1441,6 +1461,14 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/atomic-sleep": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.15",
|
"version": "10.4.15",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz",
|
||||||
@@ -1493,6 +1521,25 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
@@ -1556,6 +1603,29 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-crc32": {
|
"node_modules/buffer-crc32": {
|
||||||
"version": "0.2.13",
|
"version": "0.2.13",
|
||||||
"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",
|
||||||
@@ -1749,6 +1819,12 @@
|
|||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/colorette": {
|
||||||
|
"version": "2.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||||
|
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
@@ -1818,6 +1894,15 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dateformat": {
|
||||||
|
"version": "4.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
||||||
|
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
@@ -1948,6 +2033,15 @@
|
|||||||
"integrity": "sha512-9AreocSUWnzNtvLcbpng6N+GkXnCcBR80IQkxRC9Dfdyg4gaWNUPBujAHUpKkiUkoSoR9UlhA4zD/IgBklmhzg==",
|
"integrity": "sha512-9AreocSUWnzNtvLcbpng6N+GkXnCcBR80IQkxRC9Dfdyg4gaWNUPBujAHUpKkiUkoSoR9UlhA4zD/IgBklmhzg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es6-promise": {
|
"node_modules/es6-promise": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
|
||||||
@@ -2254,6 +2348,28 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-target-shim": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/events": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-copy": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -2300,6 +2416,20 @@
|
|||||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-redact": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-safe-stringify": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||||
@@ -2522,11 +2652,94 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/help-me": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^8.0.0",
|
||||||
|
"readable-stream": "^3.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/help-me/node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/help-me/node_modules/glob": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^5.0.1",
|
||||||
|
"once": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/help-me/node_modules/minimatch": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/help-me/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/humanize-duration-ts": {
|
"node_modules/humanize-duration-ts": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/humanize-duration-ts/-/humanize-duration-ts-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/humanize-duration-ts/-/humanize-duration-ts-2.1.1.tgz",
|
||||||
"integrity": "sha512-TibNF2/fkypjAfHdGpWL/dmWUS0G6Qi+3mKyiB6LDCowbMy+PtzbgPTnFMNTOVAJXDau01jYrJ3tFoz5AJSqhA=="
|
"integrity": "sha512-TibNF2/fkypjAfHdGpWL/dmWUS0G6Qi+3mKyiB6LDCowbMy+PtzbgPTnFMNTOVAJXDau01jYrJ3tFoz5AJSqhA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.2.4",
|
"version": "5.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||||
@@ -2719,6 +2932,15 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/joycon": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
@@ -3073,6 +3295,11 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-exit-leak-free": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w=="
|
||||||
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@@ -3236,6 +3463,66 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pino": {
|
||||||
|
"version": "8.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino/-/pino-8.15.1.tgz",
|
||||||
|
"integrity": "sha512-Cp4QzUQrvWCRJaQ8Lzv0mJzXVk4z2jlq8JNKMGaixC2Pz5L4l2p95TkuRvYbrEbe85NQsDKrAd4zalf7Ml6WiA==",
|
||||||
|
"dependencies": {
|
||||||
|
"atomic-sleep": "^1.0.0",
|
||||||
|
"fast-redact": "^3.1.1",
|
||||||
|
"on-exit-leak-free": "^2.1.0",
|
||||||
|
"pino-abstract-transport": "v1.1.0",
|
||||||
|
"pino-std-serializers": "^6.0.0",
|
||||||
|
"process-warning": "^2.0.0",
|
||||||
|
"quick-format-unescaped": "^4.0.3",
|
||||||
|
"real-require": "^0.2.0",
|
||||||
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"sonic-boom": "^3.1.0",
|
||||||
|
"thread-stream": "^2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pino": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-abstract-transport": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "^4.0.0",
|
||||||
|
"split2": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-pretty": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-tRvpyEmGtc2D+Lr3FulIZ+R1baggQ4S3xD2Ar93KixFEDx6SEAUP3W5aYuEw1C73d6ROrNcB2IXLteW8itlwhA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"colorette": "^2.0.7",
|
||||||
|
"dateformat": "^4.6.3",
|
||||||
|
"fast-copy": "^3.0.0",
|
||||||
|
"fast-safe-stringify": "^2.1.1",
|
||||||
|
"help-me": "^4.0.1",
|
||||||
|
"joycon": "^3.1.1",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"on-exit-leak-free": "^2.1.0",
|
||||||
|
"pino-abstract-transport": "^1.0.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"readable-stream": "^4.0.0",
|
||||||
|
"secure-json-parse": "^2.4.0",
|
||||||
|
"sonic-boom": "^3.0.0",
|
||||||
|
"strip-json-comments": "^3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pino-pretty": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-std-serializers": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="
|
||||||
|
},
|
||||||
"node_modules/pirates": {
|
"node_modules/pirates": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
||||||
@@ -3571,6 +3858,29 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process-warning": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg=="
|
||||||
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||||
@@ -3600,6 +3910,11 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/quick-format-unescaped": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
@@ -3615,6 +3930,21 @@
|
|||||||
"pify": "^2.3.0"
|
"pify": "^2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@@ -3627,6 +3957,14 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/real-require": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.4",
|
"version": "1.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
|
||||||
@@ -3729,6 +4067,33 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/safe-stable-stringify": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sander": {
|
"node_modules/sander": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
|
||||||
@@ -3772,6 +4137,12 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/secure-json-parse": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.5.4",
|
"version": "7.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||||
@@ -3843,6 +4214,14 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sonic-boom": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"atomic-sleep": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sorcery": {
|
"node_modules/sorcery": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
|
||||||
@@ -3867,6 +4246,14 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split2": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stackback": {
|
"node_modules/stackback": {
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
@@ -3888,6 +4275,14 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
@@ -4288,6 +4683,14 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/thread-stream": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-xZYtOtmnA63zj04Q+F9bdEay5r47bvpo1CaNqsKi7TpoJHcotUez8Fkfo2RJWpW91lnnaApdpRbVwCWsy+ifcw==",
|
||||||
|
"dependencies": {
|
||||||
|
"real-require": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@sveltejs/adapter-node": "^1.3.1",
|
"@sveltejs/adapter-node": "^1.3.1",
|
||||||
"@sveltejs/kit": "^1.20.4",
|
"@sveltejs/kit": "^1.20.4",
|
||||||
"@tailwindcss/forms": "^0.5.6",
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
|
"@types/js-yaml": "^4.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"@vitest/ui": "^0.34.1",
|
"@vitest/ui": "^0.34.1",
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-svelte": "^2.30.0",
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
|
"pino-pretty": "^10.2.0",
|
||||||
"postcss": "^8.4.30",
|
"postcss": "^8.4.30",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
@@ -46,6 +48,7 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"humanize-duration-ts": "^2.1.1",
|
"humanize-duration-ts": "^2.1.1",
|
||||||
"ip-range-check": "^0.2.0",
|
"ip-range-check": "^0.2.0",
|
||||||
"js-yaml": "^4.1.0"
|
"js-yaml": "^4.1.0",
|
||||||
|
"pino": "^8.15.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,20 +18,17 @@ services:
|
|||||||
- title: '/Media'
|
- title: '/Media'
|
||||||
icon: 'fas fa-photo-film'
|
icon: 'fas fa-photo-film'
|
||||||
items:
|
items:
|
||||||
- title: 'Jellyfin'
|
- type: jellyfin
|
||||||
url: 'https://eagle.tuleu.me'
|
url: 'https://eagle.tuleu.me'
|
||||||
type: jellyfin
|
|
||||||
keywords: 'cloud storage files'
|
keywords: 'cloud storage files'
|
||||||
|
|
||||||
- title: 'Sonarr'
|
- type: sonarr
|
||||||
url: 'http://sonarr.lan'
|
url: 'http://sonarr.lan'
|
||||||
type: sonarr
|
|
||||||
api_key: 43f13770f9a0419bbdc3224dae76e886
|
api_key: 43f13770f9a0419bbdc3224dae76e886
|
||||||
keywords: 'shows tracker torrent usenet'
|
keywords: 'shows tracker torrent usenet'
|
||||||
|
|
||||||
- title: 'Radarr'
|
- type: radarr
|
||||||
url: 'http://radarr.lan'
|
url: 'http://radarr.lan'
|
||||||
type: radarr
|
|
||||||
keywords: 'movies tracker torrent usenet'
|
keywords: 'movies tracker torrent usenet'
|
||||||
|
|
||||||
- title: '/Cloud'
|
- title: '/Cloud'
|
||||||
@@ -46,8 +43,7 @@ services:
|
|||||||
- title: '/Infra'
|
- title: '/Infra'
|
||||||
icon: 'fas fa-network-wired'
|
icon: 'fas fa-network-wired'
|
||||||
items:
|
items:
|
||||||
- title: 'PiHole'
|
- type: pihole
|
||||||
url: 'http://pihole.lan/admin'
|
url: 'http://pihole.lan/admin'
|
||||||
type: 'pihole'
|
|
||||||
keywords: 'dns ads blocker internet'
|
keywords: 'dns ads blocker internet'
|
||||||
api_token: a3996b80e3d9cdb86b338396a164a8814e8d6f44d2986261fe573bfea53a75fb
|
api_token: a3996b80e3d9cdb86b338396a164a8814e8d6f44d2986261fe573bfea53a75fb
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { watchDymamicConfig } from '$lib/server/config';
|
import { initConfig, watchDymamicConfig } from '$lib/server/config';
|
||||||
import { initServicePolling } from '$lib/server/serviceDataPolling';
|
|
||||||
import { initComponents, initServices } from '$lib/services/services';
|
import { initComponents, initServices } from '$lib/services/services';
|
||||||
|
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
watchDymamicConfig();
|
watchDymamicConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Order does matter here, we want the default data , loaded by initServices to be available when
|
||||||
|
// the config is built. and polling need a built config too.
|
||||||
await initServices();
|
await initServices();
|
||||||
await initComponents();
|
await initComponents();
|
||||||
initServicePolling();
|
initConfig();
|
||||||
|
|||||||
16
src/lib/components/Button.svelte
Normal file
16
src/lib/components/Button.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script>
|
||||||
|
let buttonProps = {
|
||||||
|
class: [$$restProps.class]
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click
|
||||||
|
on:mouseover
|
||||||
|
on:mouseenter
|
||||||
|
on:mouseleave
|
||||||
|
class="{$$restProps.class ||
|
||||||
|
''} flex h-8 w-8 items-center justify-center rounded-full p-0 transition-all hover:bg-neutral-600 active:bg-neutral-700 dark:active:bg-neutral-500"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
@@ -1,42 +1,40 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { LinkConfig, SectionConfig } from '$lib/config';
|
import type { LinkConfig, SectionConfig } from '$lib/config';
|
||||||
import Brand from './Brand.svelte';
|
import Brand from './Brand.svelte';
|
||||||
|
import Button from './Button.svelte';
|
||||||
import LayoutButton from './LayoutButton.svelte';
|
import LayoutButton from './LayoutButton.svelte';
|
||||||
|
import LinkMenu from './LinkMenu.svelte';
|
||||||
import ThemeVariantButton from './ThemeVariantButton.svelte';
|
import ThemeVariantButton from './ThemeVariantButton.svelte';
|
||||||
|
|
||||||
export let section: SectionConfig;
|
export let section: SectionConfig;
|
||||||
export let links: Array<LinkConfig>;
|
export let links: Array<LinkConfig>;
|
||||||
|
|
||||||
|
links = [];
|
||||||
|
let show = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="flex flex-col justify-between gap-8">
|
<header class="flex flex-col justify-between gap-8">
|
||||||
<section class="flex h-16 flex-row items-center gap-4">
|
<section class="flex h-16 flex-row items-center gap-4">
|
||||||
<div class="h-16 w-16">
|
<div class="h-12 w-12">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<Brand brand={section} color="var(--logo-color)" />
|
<Brand brand={section} color="var(--logo-color)" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{#if section.subtitle}
|
{#if section.subtitle}
|
||||||
<h2 class="text-xl">{section.subtitle}</h2>
|
<h2 class="text-lg">{section.subtitle}</h2>
|
||||||
{/if}
|
{/if}
|
||||||
<h1 class="text-4xl">{section.title}</h1>
|
<h1 class="text-3xl">{section.title}</h1>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<nav class="navbar flex flex-row justify-between" aria-label="main navigation">
|
<nav class="navbar relative flex flex-row justify-between text-lg" aria-label="main navigation">
|
||||||
<div class="justfiy-start flex flex-row gap-4">
|
{#if links.length > 0}
|
||||||
{#each links as link}
|
<LinkMenu {links} />
|
||||||
<a class="justify-self-start" href={link.url}>
|
|
||||||
{#if link.icon}
|
|
||||||
<i class={link.icon} />
|
|
||||||
{/if}
|
{/if}
|
||||||
<span class="link-label">{link.title}</span>
|
<div class="flex flex-row items-center gap-1 justify-self-end">
|
||||||
</a>
|
<ThemeVariantButton />
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row items-center justify-start gap-4">
|
|
||||||
<ThemeVariantButton class="" />
|
|
||||||
|
|
||||||
<LayoutButton class="" />
|
<LayoutButton />
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="">
|
<div class="">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { layoutDirection } from '$lib/stores/layout';
|
import { layoutDirection } from '$lib/stores/layout';
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if ($layoutDirection == 'column') {
|
if ($layoutDirection == 'column') {
|
||||||
@@ -10,10 +11,10 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a on:click={toggle} class={$$restProps.class || ''}>
|
<Button on:click={toggle} class={$$restProps.class || ''}>
|
||||||
{#if $layoutDirection == 'column'}
|
{#if $layoutDirection == 'column'}
|
||||||
<i class="fa fa-table-columns" />
|
<i class="fa fa-table-columns" />
|
||||||
{:else}
|
{:else}
|
||||||
<i class="fa fa-list-ul" />
|
<i class="fa fa-list-ul" />
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</Button>
|
||||||
|
|||||||
72
src/lib/components/LinkMenu.svelte
Normal file
72
src/lib/components/LinkMenu.svelte
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
|
||||||
|
let show = false;
|
||||||
|
export let links: Array<LinkConfig>;
|
||||||
|
|
||||||
|
function clickOutside(node) {
|
||||||
|
const handleClick = (event) => {
|
||||||
|
if (
|
||||||
|
node &&
|
||||||
|
!node.contains(event.target) &&
|
||||||
|
!event.defaultPrevented &&
|
||||||
|
node.offsetParent !== null
|
||||||
|
) {
|
||||||
|
node.dispatchEvent(new CustomEvent('click_outside', node));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', handleClick, true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
document.removeEventListener('click', handleClick, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- small device menu button -->
|
||||||
|
<Button
|
||||||
|
class="md:hidden"
|
||||||
|
on:click={() => {
|
||||||
|
show = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i class="fa fa-bars" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- small device menu -->
|
||||||
|
<div
|
||||||
|
use:clickOutside
|
||||||
|
on:click_outside={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
class:hidden={!show}
|
||||||
|
class="min-w-40 absolute left-0 top-0 block flex flex-col gap-1 rounded border border-neutral-600 bg-neutral-600 p-1 transition-all"
|
||||||
|
>
|
||||||
|
{#each links as link}
|
||||||
|
<a
|
||||||
|
class="rounded p-2 transition-all hover:bg-neutral-500 active:bg-neutral-600 dark:hover:bg-neutral-700"
|
||||||
|
href={link.url}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{#if link.icon}
|
||||||
|
<i class={link.icon} />
|
||||||
|
{/if}
|
||||||
|
<span class="link-label">{link.title}</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- medium device menu -->
|
||||||
|
<div class="hidden flex-row justify-start gap-4 md:flex">
|
||||||
|
{#each links as link}
|
||||||
|
<a href={link.url} target="_blank">
|
||||||
|
{#if link.icon}
|
||||||
|
<i class={link.icon} />
|
||||||
|
{/if}
|
||||||
|
<span class="link-label">{link.title}</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -1,27 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { BrandConfig, ServiceConfig } from '$lib/config';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
import type { ServiceData } from '$lib/services/service';
|
import type { ServiceStatus } from '$lib/services/service';
|
||||||
import { getServiceComponent } from '$lib/services/services';
|
import { getServiceComponent } from '$lib/services/services';
|
||||||
import Brand from './Brand.svelte';
|
import Brand from './Brand.svelte';
|
||||||
|
|
||||||
export let service: ServiceConfig;
|
export let service: ServiceConfig;
|
||||||
export let data: ServiceData;
|
export let status: ServiceStatus | undefined;
|
||||||
|
export let data: Record<string, unknown>;
|
||||||
|
|
||||||
const component = getServiceComponent(service.type || '');
|
const component = getServiceComponent(service.type || '');
|
||||||
|
|
||||||
function brand(): BrandConfig {
|
function computeClasses(status: ServiceStatus | undefined): string {
|
||||||
if (data?.logo != undefined && service.logo == undefined && service.icon == undefined) {
|
switch (status) {
|
||||||
return { logo: data.logo, asmask: service.asmask || false };
|
|
||||||
}
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
|
|
||||||
function subtitle(): string {
|
|
||||||
return service.subtitle || data.subtitle || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeClasses(): string {
|
|
||||||
switch (data.status) {
|
|
||||||
case 'offline':
|
case 'offline':
|
||||||
return 'bg-error-400';
|
return 'bg-error-400';
|
||||||
case 'online':
|
case 'online':
|
||||||
@@ -30,22 +20,22 @@
|
|||||||
return 'bg-warning-400 animate-pulse';
|
return 'bg-warning-400 animate-pulse';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: statusClasses = computeClasses();
|
$: statusClasses = computeClasses(status);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="min-h-24 flex w-full flex-col overflow-hidden rounded-2xl bg-neutral-600 bg-opacity-30 p-0 transition-all hover:-translate-y-2 focus:-translate-y-2 focus:outline-none"
|
class="min-h-24 flex w-full flex-col overflow-hidden rounded-2xl bg-neutral-600 bg-opacity-30 p-0 transition-all hover:-translate-y-2 focus:-translate-y-2 focus:outline-none"
|
||||||
href={service.url}
|
href={service.url}
|
||||||
target={service.target || ''}
|
target={service.target || '_blank'}
|
||||||
>
|
>
|
||||||
<div class="flex flex-row gap-2 px-6 pt-6">
|
<div class="flex flex-row gap-2 px-6 pt-6">
|
||||||
<div class="flex flex h-12 w-12 flex-row text-4xl">
|
<div class="flex flex h-12 w-12 flex-row text-4xl">
|
||||||
<Brand brand={brand()} />
|
<Brand brand={service} />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-12">
|
<div class="h-12">
|
||||||
<p class="text-xl">{service.title}</p>
|
<p class="text-lg">{service.title || ''}</p>
|
||||||
<p class="text-md min-h-12 text-neutral-700 dark:text-neutral-500">
|
<p class="min-h-12 text-neutral-700 dark:text-neutral-500">
|
||||||
{subtitle()}
|
{service.subtitle || ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ServiceGroupConfig } from '$lib/config';
|
import type { ServiceGroupConfig } from '$lib/config';
|
||||||
import { layoutDirection } from '$lib/stores/layout';
|
import type { ServiceData } from '$lib/services/service';
|
||||||
|
import { layoutDirection, type LayoutDirection } from '$lib/stores/layout';
|
||||||
import ServiceCard from './ServiceCard.svelte';
|
import ServiceCard from './ServiceCard.svelte';
|
||||||
|
|
||||||
export let group: ServiceGroupConfig;
|
export let group: ServiceGroupConfig;
|
||||||
export let groupData: Array<unknown>;
|
export let groupData: Array<Partial<ServiceData>>;
|
||||||
export let columns: number;
|
export let columns: number;
|
||||||
|
|
||||||
function renderClasses(columns: number, direction: LayoutDirection): string {
|
function renderClasses(columns: number, direction: LayoutDirection): string {
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-6 text-2xl">
|
<div class="mb-6 text-xl">
|
||||||
<h3>
|
<h3>
|
||||||
{#if group.icon}
|
{#if group.icon}
|
||||||
<i class={group.icon} />
|
<i class={group.icon} />
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
|
|
||||||
<div class="gap-4 {layoutClasses}">
|
<div class="gap-4 {layoutClasses}">
|
||||||
{#each group.items as service, i}
|
{#each group.items as service, i}
|
||||||
<ServiceCard {service} data={groupData[i]} />
|
<ServiceCard {service} data={groupData[i]?.data || {}} status={groupData[i].status} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { themeVariant, type ThemeVariant } from '$lib/stores/themeVariant';
|
import { themeVariant, type ThemeVariant } from '$lib/stores/themeVariant';
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
|
||||||
const variants: Array<ThemeVariant> = ['default', 'dark', 'light'];
|
const variants: Array<ThemeVariant> = ['default', 'dark', 'light'];
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a on:click={next} class={$$restProps.class || ''}>
|
<Button on:click={next} class={$$restProps.class || ''}>
|
||||||
{#if $themeVariant == 'default'}
|
{#if $themeVariant == 'default'}
|
||||||
<i class="fa fa-circle-half-stroke" />
|
<i class="fa fa-circle-half-stroke" />
|
||||||
{:else if $themeVariant == 'light'}
|
{:else if $themeVariant == 'light'}
|
||||||
@@ -17,4 +18,4 @@
|
|||||||
{:else if $themeVariant == 'dark'}
|
{:else if $themeVariant == 'dark'}
|
||||||
<i class="fa-solid fa-circle" />
|
<i class="fa-solid fa-circle" />
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</Button>
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import type {
|
|||||||
} from '$lib/config';
|
} from '$lib/config';
|
||||||
|
|
||||||
import * as ipRangeCheck from 'ip-range-check';
|
import * as ipRangeCheck from 'ip-range-check';
|
||||||
import { initializeServiceData } from './serviceDataPolling';
|
import { getServiceRecord } from '$lib/services/services';
|
||||||
|
import { rootLogger } from './logger';
|
||||||
|
import { resetServiceData } from './dataPolling';
|
||||||
|
|
||||||
const requiredService: Required<ServiceConfig> = {
|
const requiredService: Required<ServiceConfig> = {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -23,7 +25,7 @@ const requiredService: Required<ServiceConfig> = {
|
|||||||
target: '',
|
target: '',
|
||||||
type: '',
|
type: '',
|
||||||
tag: '',
|
tag: '',
|
||||||
keywords: []
|
keywords: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
const requiredServiceGroup: Required<ServiceGroupConfig> = {
|
const requiredServiceGroup: Required<ServiceGroupConfig> = {
|
||||||
@@ -86,8 +88,29 @@ function merge<Type extends SPOJO>(a: Type, b: SPOJO): Type {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function injectDefaultServiceConfigs(config: Config) {
|
||||||
|
for (const group of config.services.values()) {
|
||||||
|
for (const service of group.items.values()) {
|
||||||
|
if (!service.type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const def = getServiceRecord(service.type).config;
|
||||||
|
for (const [key, value] of Object.entries(def)) {
|
||||||
|
const isUnknownKey = !(key in requiredService);
|
||||||
|
const keyAlreadyDefined = key in service && service[key] != false;
|
||||||
|
if (isUnknownKey || keyAlreadyDefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
service[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function mergeConfig(a: Config, b: SPOJO): Config {
|
export function mergeConfig(a: Config, b: SPOJO): Config {
|
||||||
return merge<Config>(a, b);
|
const res = merge<Config>(a, b);
|
||||||
|
injectDefaultServiceConfigs(res);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLightConfig: ColorConfig = {};
|
const defaultLightConfig: ColorConfig = {};
|
||||||
@@ -96,7 +119,7 @@ const defaultDarkConfig: ColorConfig = {};
|
|||||||
|
|
||||||
export const defaultConfig: Config = {
|
export const defaultConfig: Config = {
|
||||||
title: 'Flanders',
|
title: 'Flanders',
|
||||||
columns: 3,
|
columns: 4,
|
||||||
colors: {
|
colors: {
|
||||||
light: defaultLightConfig,
|
light: defaultLightConfig,
|
||||||
dark: defaultDarkConfig
|
dark: defaultDarkConfig
|
||||||
@@ -112,7 +135,7 @@ function strip<Type extends SPOJO>(toStrip: Type, reference: Type): Type {
|
|||||||
if (reference == undefined) {
|
if (reference == undefined) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
const referenceNames = Object.entries(reference).map(([key, value]) => key);
|
const referenceNames = Object.entries(reference).map((v) => v[0]);
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(res)) {
|
for (const [key, value] of Object.entries(res)) {
|
||||||
if (referenceNames.includes(key) == false) {
|
if (referenceNames.includes(key) == false) {
|
||||||
@@ -161,9 +184,21 @@ export function clientConfig(): Config {
|
|||||||
return _clientConfig;
|
return _clientConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
let _serverConfig = mergeConfig(defaultConfig, configData);
|
let _serverConfig: Config = defaultConfig;
|
||||||
|
|
||||||
let _clientConfig = stripPrivateFields(_serverConfig);
|
let _clientConfig: Config = defaultConfig;
|
||||||
|
|
||||||
|
function buildServerAndClientConfig(config: SPOJO) {
|
||||||
|
_serverConfig = mergeConfig(defaultConfig, config);
|
||||||
|
_clientConfig = stripPrivateFields(_serverConfig);
|
||||||
|
resetServiceData();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initConfig() {
|
||||||
|
buildServerAndClientConfig(configData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = rootLogger.child({ name: 'config' });
|
||||||
|
|
||||||
export function watchDymamicConfig() {
|
export function watchDymamicConfig() {
|
||||||
const __filepath = '/dynamic/config.yml';
|
const __filepath = '/dynamic/config.yml';
|
||||||
@@ -171,11 +206,9 @@ export function watchDymamicConfig() {
|
|||||||
const reloadConfig = async () => {
|
const reloadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const dynamicConfig = yml.load(await readFile(__filepath, 'utf8'));
|
const dynamicConfig = yml.load(await readFile(__filepath, 'utf8'));
|
||||||
_serverConfig = mergeConfig(defaultConfig, dynamicConfig);
|
buildServerAndClientConfig(dynamicConfig as SPOJO);
|
||||||
_clientConfig = stripPrivateFields(_serverConfig);
|
|
||||||
initializeServiceData();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('could not read or parse config: ' + err);
|
logger.error({ err }, 'could not read or parse config');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,11 +217,12 @@ export function watchDymamicConfig() {
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const watcher = watch(__filepath);
|
const watcher = watch(__filepath);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
for await (const event of watcher) {
|
for await (const event of watcher) {
|
||||||
reloadConfig();
|
reloadConfig();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('could not watch config: ' + err);
|
logger.error({ err }, 'could not watch config');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
87
src/lib/server/dataPolling.ts
Normal file
87
src/lib/server/dataPolling.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import type { ServiceData, ServicePoller } from '$lib/services/service';
|
||||||
|
import { getServiceRecord } from '$lib/services/services';
|
||||||
|
import { readable } from 'svelte/store';
|
||||||
|
import { serverConfig } from './config';
|
||||||
|
import { rootLogger } from './logger';
|
||||||
|
import type { Unsubscriber } from 'svelte/motion';
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
|
const logger = rootLogger.child({ name: 'ServiceData' });
|
||||||
|
|
||||||
|
export interface ServiceDataEvent {
|
||||||
|
group: number;
|
||||||
|
item: number;
|
||||||
|
data: ServiceData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeToDataEvent(onEvent: (event: ServiceDataEvent) => void): Unsubscriber {
|
||||||
|
for (const [group, items] of serviceData.entries()) {
|
||||||
|
for (const [item, data] of items.entries()) {
|
||||||
|
onEvent({ group, item, data: data as ServiceData });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serviceDataEvents.subscribe(onEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceDataEvents = readable<ServiceDataEvent>(undefined, (set) => {
|
||||||
|
logger.debug({ interval: pollInterval }, 'starting polling loop');
|
||||||
|
|
||||||
|
pushServiceData = set;
|
||||||
|
|
||||||
|
logger.debug('polling for initial state');
|
||||||
|
pollAllServices();
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
logger.debug('polling for service update');
|
||||||
|
pollAllServices();
|
||||||
|
}, pollInterval);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
logger.debug('stopping polling loop and clearing data');
|
||||||
|
clearInterval(timer);
|
||||||
|
serviceData = [];
|
||||||
|
pushServiceData = undefined;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function resetServiceData() {
|
||||||
|
serviceData = [];
|
||||||
|
if (pushServiceData != undefined) {
|
||||||
|
pollAllServices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pushServiceData: ((value: ServiceDataEvent) => void) | undefined = undefined;
|
||||||
|
|
||||||
|
const pollInterval = dev ? 2000 : 60000;
|
||||||
|
|
||||||
|
let serviceData: Array<Array<Partial<ServiceData>>> = [];
|
||||||
|
|
||||||
|
function pollAllServices() {
|
||||||
|
logger.trace('pollAllServices()');
|
||||||
|
const config = serverConfig();
|
||||||
|
|
||||||
|
for (const [idxGroup, group] of config.services.entries()) {
|
||||||
|
if (serviceData[idxGroup] == undefined) {
|
||||||
|
serviceData[idxGroup] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [idxItem, service] of group.items.entries()) {
|
||||||
|
const poller: ServicePoller = getServiceRecord(service.type || '').poll;
|
||||||
|
const serviceLogger = logger.child({ group: idxGroup, item: idxItem, service });
|
||||||
|
serviceData[idxGroup][idxItem] = {};
|
||||||
|
poller(service)
|
||||||
|
.then((data: ServiceData) => {
|
||||||
|
serviceLogger.trace({ data }, 'pollAllService:result()');
|
||||||
|
serviceData[idxGroup][idxItem] = data;
|
||||||
|
if (pushServiceData != undefined) {
|
||||||
|
pushServiceData({ group: idxGroup, item: idxItem, data });
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
serviceLogger.warn({ err }, 'could not poll service');
|
||||||
|
return { status: 'offline' };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/lib/server/logger.ts
Normal file
8
src/lib/server/logger.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { dev } from '$app/environment';
|
||||||
|
import pino from 'pino';
|
||||||
|
|
||||||
|
export const rootLogger = pino({
|
||||||
|
level: dev ? 'debug' : 'info'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const requestLogger = rootLogger.child({ name: 'request' });
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import type { Config, ServiceConfig } from '$lib/config';
|
|
||||||
import type { AsyncServiceHandler, ServiceData, ServiceHandler } from '$lib/services/service';
|
|
||||||
import { getServiceHandler } from '$lib/services/services';
|
|
||||||
import { serverConfig } from './config';
|
|
||||||
|
|
||||||
export const serviceData: Array<Array<unknown>> = [];
|
|
||||||
|
|
||||||
function isPromise(p: any): boolean {
|
|
||||||
return typeof p === 'object' && typeof p.then === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollGeneric(upstream: ServiceData, url: string): Promise<ServiceData> {
|
|
||||||
upstream.status = undefined;
|
|
||||||
return fetch(url)
|
|
||||||
.then((resp: Response): ServiceData => {
|
|
||||||
upstream.status = resp.ok ? 'online' : 'offline';
|
|
||||||
return upstream;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.warn("could not fetch status for '" + url + "': " + error);
|
|
||||||
upstream.status = 'offline';
|
|
||||||
return upstream;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapHandler(handler: ServiceHandler): AsyncServiceHandler {
|
|
||||||
return async (config: ServiceConfig): Promise<ServiceData> => {
|
|
||||||
const value = handler(config);
|
|
||||||
if (isPromise(value) === true) {
|
|
||||||
return (value as Promise<ServiceData>)
|
|
||||||
.then((data: ServiceData) => {
|
|
||||||
if (data.status != undefined) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
return pollGeneric(data, config.url);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.warn("could not resolve service '" + config.url + "': " + error);
|
|
||||||
return pollGeneric({}, config.url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return pollGeneric(value as ServiceData, config.url);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollServices() {
|
|
||||||
const config: Config = serverConfig();
|
|
||||||
|
|
||||||
for (const [i, group] of config.services.entries()) {
|
|
||||||
for (const [j, service] of group.items.entries()) {
|
|
||||||
const handler: ServiceHandler =
|
|
||||||
getServiceHandler(service.type || '') ||
|
|
||||||
(() => {
|
|
||||||
return {} as ServiceData;
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapHandler(handler)(service).then((data: ServiceData) => {
|
|
||||||
serviceData[i][j] = data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initializeServiceData() {
|
|
||||||
const config = serverConfig();
|
|
||||||
serviceData.length = config.services.length;
|
|
||||||
for (let i = 0; i < serviceData.length; i++) {
|
|
||||||
serviceData[i] = [];
|
|
||||||
for (let j = 0; j < config.services[i].items.length; j++) {
|
|
||||||
serviceData[i][j] = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initServicePolling() {
|
|
||||||
initializeServiceData();
|
|
||||||
pollServices();
|
|
||||||
setInterval(pollServices, 30000);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { ServiceHandler } from '../service';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
|
|
||||||
export const handle: ServiceHandler = () => {
|
export const config: Partial<ServiceConfig> = {
|
||||||
return {
|
title: 'Jellyfin',
|
||||||
logo: 'https://cdn.rawgit.com/jellyfin/jellyfin-ux/master/branding/SVG/icon-transparent.svg',
|
logo: 'https://cdn.rawgit.com/jellyfin/jellyfin-ux/master/branding/SVG/icon-transparent.svg',
|
||||||
subtitle: 'Media Server'
|
subtitle: 'Media Server'
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CircularProgressBar from '$lib/components/CircularProgressBar.svelte';
|
import CircularProgressBar from '$lib/components/CircularProgressBar.svelte';
|
||||||
import type { ServiceData } from '../service';
|
|
||||||
|
|
||||||
export let data: ServiceData;
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
$: missingClasses = data.data == undefined ? 'animate-pulse' : '';
|
export let data: Record<string, any>;
|
||||||
|
|
||||||
|
$: missingClasses =
|
||||||
|
'status' in data && 'ads_percentage' in data && 'queries_today' in data && 'clients' in data
|
||||||
|
? ''
|
||||||
|
: 'animate-pulse';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-row justify-start gap-4 {missingClasses} whitespace-nowrap">
|
<div class="flex flex-row justify-start gap-4 {missingClasses} whitespace-nowrap">
|
||||||
<span class="">
|
<span class="">
|
||||||
{#if data.data?.status == 'enabled'}
|
{#if data?.status == 'enabled'}
|
||||||
<i class="fa fa-play text-success-400 opacity-70" />
|
<i class="fa fa-play text-success-400 opacity-70" />
|
||||||
{:else if data.data?.status == 'disabled'}
|
{:else if data?.status == 'disabled'}
|
||||||
<i class="fa fa-stop text-error-400 opacity-70" />
|
<i class="fa fa-stop text-error-400 opacity-70" />
|
||||||
{:else}
|
{:else}
|
||||||
<i class="fa fa-question" />
|
<i class="fa fa-question" />
|
||||||
@@ -19,16 +23,16 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span class="m-0 inline-block h-5 w-5 p-0 align-text-bottom">
|
<span class="m-0 inline-block h-5 w-5 p-0 align-text-bottom">
|
||||||
<CircularProgressBar ratio={(data.data?.ads_percentage || 0) / 100.0} />
|
<CircularProgressBar ratio={(data?.ads_percentage || 0) / 100.0} />
|
||||||
</span>
|
</span>
|
||||||
{data.data?.ads_percentage?.toFixed(1) || 'N.A.'}%
|
{(data?.ads_percentage || NaN).toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<i class="fa-solid fa-globe" />
|
<i class="fa-solid fa-globe" />
|
||||||
{data.data?.queries_today || 'N.A.'}
|
{data?.queries_today || 'N.A.'}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<i class="fa fa-dharmachakra" />
|
<i class="fa fa-dharmachakra" />
|
||||||
{data.data?.client || 'N.A.'}
|
{data.data?.clients || 'N.A.'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
import type { ServiceConfig } from '$lib/config';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
import { type ServiceData, type ServiceHandler } from '../service';
|
import type { ServicePoller } from '../service';
|
||||||
|
|
||||||
export const handle: ServiceHandler = async (config: ServiceConfig) => {
|
export const config: Partial<ServiceConfig> = {
|
||||||
const res: ServiceData = {
|
title: 'Pi-hole',
|
||||||
logo: 'https://cdn.rawgit.com/pi-hole/graphics/master/Vortex/Vortex.svg',
|
logo: 'https://cdn.rawgit.com/pi-hole/graphics/master/Vortex/Vortex.svg',
|
||||||
subtitle: 'Sends ads into a black hole'
|
subtitle: 'Sends ads into a black hole'
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
export const poll: ServicePoller = async (config: ServiceConfig) => {
|
||||||
const resp: Response = await fetch(
|
const resp: Response = await fetch(config.url + '/api.php?summaryRaw&auth=' + config.api_token);
|
||||||
config.url + '/api.php?summaryRaw&auth=' + config.api_token
|
|
||||||
);
|
|
||||||
res.status = resp.ok ? 'online' : 'offline';
|
|
||||||
const raw = await resp.json();
|
const raw = await resp.json();
|
||||||
res.data = {
|
return {
|
||||||
|
status: resp.ok ? 'online' : 'offline',
|
||||||
|
data: {
|
||||||
queries_today: raw.dns_queries_today,
|
queries_today: raw.dns_queries_today,
|
||||||
ads_percentage: raw.ads_percentage_today,
|
ads_percentage: raw.ads_percentage_today,
|
||||||
status: raw.status,
|
status: raw.status,
|
||||||
client: raw.unique_clients
|
clients: raw.unique_clients
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
res.status = 'offline';
|
|
||||||
console.warn('could not fetch pihole status: ' + error);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
return res;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { ServiceHandler } from '../service';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
|
|
||||||
export const handle: ServiceHandler = () => {
|
export const config: Partial<ServiceConfig> = {
|
||||||
return { logo: 'https://cdn.rawgit.com/Prowlarr/Prowlarr/develop/Logo/Prowlarr.svg' };
|
title: 'Prowlarr',
|
||||||
|
logo: 'https://cdn.rawgit.com/Prowlarr/Prowlarr/develop/Logo/Prowlarr.svg',
|
||||||
|
subtitle: 'Indexer of indexer'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { ServiceHandler } from '../service';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
|
|
||||||
export const handle: ServiceHandler = () => {
|
export const config: Partial<ServiceConfig> = {
|
||||||
return {
|
title: 'Sonarr',
|
||||||
logo: 'https://cdn.rawgit.com/Radarr/Radarr/develop/Logo/Radarr.svg',
|
logo: 'https://cdn.rawgit.com/Radarr/Radarr/develop/Logo/Radarr.svg',
|
||||||
subtitle: 'TV Shows Tracker'
|
subtitle: 'TV Shows Tracker'
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import type { ServiceConfig } from '$lib/config';
|
|||||||
export type ServiceStatus = 'online' | 'offline';
|
export type ServiceStatus = 'online' | 'offline';
|
||||||
|
|
||||||
export interface ServiceData {
|
export interface ServiceData {
|
||||||
status?: ServiceStatus;
|
status: ServiceStatus;
|
||||||
|
|
||||||
[x: string]: unknown;
|
data?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceHandler = (input: ServiceConfig) => ServiceData | Promise<ServiceData>;
|
export type ServicePoller = (input: ServiceConfig) => Promise<ServiceData>;
|
||||||
export type AsyncServiceHandler = (input: ServiceConfig) => Promise<ServiceData>;
|
|
||||||
|
|||||||
@@ -1,56 +1,73 @@
|
|||||||
import type { ServiceHandler } from './service';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
|
import type { ServiceData, ServicePoller } from './service';
|
||||||
|
|
||||||
const services: Record<string, ServiceHandler> = {};
|
interface ServiceRecord {
|
||||||
|
poll: ServicePoller;
|
||||||
const components: Record<string, any> = {};
|
config: Partial<ServiceConfig>;
|
||||||
|
|
||||||
function registerService(type: string, handler: ServiceHandler) {
|
|
||||||
services[type] = handler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const services: Record<string, ServiceRecord> = {};
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const components: Record<string, any> = {};
|
||||||
|
|
||||||
|
async function pollURL(config: ServiceConfig): Promise<ServiceData> {
|
||||||
|
const resp = await fetch(config.url);
|
||||||
|
return { status: resp.ok ? 'online' : 'offline' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerService(type: string, service: Partial<ServiceRecord>) {
|
||||||
|
const record: ServiceRecord = {
|
||||||
|
poll: service.poll || pollURL,
|
||||||
|
config: service.config || {}
|
||||||
|
};
|
||||||
|
|
||||||
|
services[type] = record;
|
||||||
|
}
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function registerComponent(type: string, component: any) {
|
function registerComponent(type: string, component: any) {
|
||||||
components[type] = component;
|
components[type] = component;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServiceHandler(type: string): ServiceHandler | undefined {
|
export function getServiceRecord(type: string): ServiceRecord {
|
||||||
return services[type];
|
return services[type] || { poll: pollURL, config: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function getServiceComponent(type: string): any {
|
export function getServiceComponent(type: string): any {
|
||||||
return components[type];
|
return components[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initServices() {
|
export async function initServices() {
|
||||||
const services = import.meta.glob('/src/lib/services/**/+service.ts');
|
const modules = import.meta.glob('/src/lib/services/**/+service.ts');
|
||||||
|
|
||||||
for (const [modulePath, load] of Object.entries(services)) {
|
return Promise.allSettled(
|
||||||
|
Object.entries(modules).map(async ([path, load]) => {
|
||||||
try {
|
try {
|
||||||
const { handle } = (await load()) as any;
|
const { poll, config } = (await load()) as any;
|
||||||
if (handle == undefined) {
|
registerService(path.slice(18, -12), { poll, config });
|
||||||
throw new Error(`${modulePath} does not export 'handle'`);
|
} catch (error) {
|
||||||
}
|
console.warn(`could not load ${path}: ${error}`);
|
||||||
const typeName = modulePath.slice(18, -12);
|
|
||||||
registerService(typeName, handle);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Could not load service definition from '${modulePath}': ${err}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initComponents() {
|
export async function initComponents() {
|
||||||
const services = import.meta.glob('/src/lib/services/**/+content.svelte');
|
const modules = import.meta.glob('/src/lib/services/**/+content.svelte');
|
||||||
|
|
||||||
for (const [componentPath, load] of Object.entries(services)) {
|
return Promise.allSettled(
|
||||||
|
Object.entries(modules).map(async ([path, load]) => {
|
||||||
try {
|
try {
|
||||||
const module = (await load()) as any;
|
const module = (await load()) as any;
|
||||||
|
|
||||||
if (module == undefined) {
|
if (module == undefined) {
|
||||||
throw new Error(`${componentPath} does not load`);
|
throw new Error('load returned and undefined value');
|
||||||
}
|
|
||||||
const typeName = componentPath.slice(18, -16);
|
|
||||||
registerComponent(typeName, module.default);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Could not load component definition from '${componentPath}': ${err}`);
|
|
||||||
}
|
}
|
||||||
|
registerComponent(path.slice(18, -16), module.default);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`could not load ${path}: ${error}`);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { humanizeRelativeTime } from '$lib/humanize';
|
import { humanizeRelativeTime } from '$lib/humanize';
|
||||||
import type { ServiceData } from '../service';
|
|
||||||
|
|
||||||
export let data: ServiceData;
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export let data: Record<string, any>;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-row justify-start gap-4 whitespace-nowrap">
|
<div class="flex flex-row justify-start gap-4 whitespace-nowrap">
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { ServiceConfig } from '$lib/config';
|
import type { ServiceConfig } from '$lib/config';
|
||||||
import type { ServiceHandler } from '../service';
|
import type { ServicePoller, ServiceStatus } from '../service';
|
||||||
|
|
||||||
interface Status {
|
interface Status {
|
||||||
warnings: number;
|
warnings: number;
|
||||||
errors: number;
|
errors: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function buildStatus(statuses: any[]): Status {
|
function buildStatus(statuses: any[]): Status {
|
||||||
let warnings = 0;
|
let warnings = 0;
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
@@ -26,6 +27,8 @@ interface Queue {
|
|||||||
nextDate?: Date;
|
nextDate?: Date;
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function recordEstimatedCompletionTime(record: any): number {
|
function recordEstimatedCompletionTime(record: any): number {
|
||||||
if (record.estimatedCompletionTime == undefined) {
|
if (record.estimatedCompletionTime == undefined) {
|
||||||
return Infinity;
|
return Infinity;
|
||||||
@@ -33,6 +36,7 @@ function recordEstimatedCompletionTime(record: any): number {
|
|||||||
return new Date(record.estimatedCompletionTime).getTime();
|
return new Date(record.estimatedCompletionTime).getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function buildQueue(queue: any): Queue {
|
function buildQueue(queue: any): Queue {
|
||||||
if (queue?.records?.length === 0) {
|
if (queue?.records?.length === 0) {
|
||||||
return { total: 0 };
|
return { total: 0 };
|
||||||
@@ -51,12 +55,13 @@ function buildQueue(queue: any): Queue {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handle: ServiceHandler = async (config: ServiceConfig) => {
|
export const config: Partial<ServiceConfig> = {
|
||||||
const res = {
|
title: 'Sonarr',
|
||||||
logo: 'https://cdn.rawgit.com/Sonarr/Sonarr/develop/Logo/Sonarr.svg',
|
logo: 'https://cdn.rawgit.com/Sonarr/Sonarr/develop/Logo/Sonarr.svg',
|
||||||
subtitle: 'TV Show tracker'
|
subtitle: 'TV Show tracker'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const poll: ServicePoller = async (config: ServiceConfig) => {
|
||||||
const params = '?apikey=' + config.api_key;
|
const params = '?apikey=' + config.api_key;
|
||||||
|
|
||||||
const requests = [
|
const requests = [
|
||||||
@@ -65,21 +70,20 @@ export const handle: ServiceHandler = async (config: ServiceConfig) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const [health, queue] = await Promise.allSettled(requests);
|
const [health, queue] = await Promise.allSettled(requests);
|
||||||
res.status = 'online';
|
let status: ServiceStatus = 'online';
|
||||||
|
const data: Record<string, unknown> = {};
|
||||||
|
|
||||||
if (health.status != 'fulfilled') {
|
if (health.status != 'fulfilled') {
|
||||||
console.warn("Could not fetch '" + config.url + "' status: " + health.value);
|
throw new Error("could not fetch '" + config.url + "' health status: " + health.reason);
|
||||||
res.status = 'offline';
|
|
||||||
} else if (health.value.ok == true) {
|
} else if (health.value.ok == true) {
|
||||||
res.health = buildStatus(await health.value.json());
|
data.health = buildStatus(await health.value.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queue.status != 'fulfilled') {
|
if (queue.status != 'fulfilled') {
|
||||||
console.warn("Could not fetch '" + config.url + "' queue: " + queue.value);
|
throw new Error("could not fetch '" + config.url + "' queue status: " + queue.reason);
|
||||||
res.status = 'offline';
|
|
||||||
} else if (queue.value.ok == true) {
|
} else if (queue.value.ok == true) {
|
||||||
res.queue = buildQueue(await queue.value.json());
|
data.queue = buildQueue(await queue.value.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return { status, data };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { clientAddressIsPrivate, clientConfig, serverConfig } from '$lib/server/config';
|
import { clientAddressIsPrivate, clientConfig, serverConfig } from '$lib/server/config';
|
||||||
import type { PageServerLoad } from './$types.d';
|
import type { PageServerLoad } from './$types.d';
|
||||||
import { serviceData } from '$lib/server/serviceDataPolling';
|
import { requestLogger } from '$lib/server/logger';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ getClientAddress }) => {
|
export const load: PageServerLoad = async ({ getClientAddress }) => {
|
||||||
|
const logger = requestLogger.child({ url: 'GET /', address: getClientAddress() });
|
||||||
|
|
||||||
|
logger.info('got request');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config: clientConfig(),
|
config: clientConfig(),
|
||||||
serviceData: serviceData,
|
|
||||||
location: getClientAddress(),
|
location: getClientAddress(),
|
||||||
privateAccess: clientAddressIsPrivate(getClientAddress(), serverConfig())
|
privateAccess: clientAddressIsPrivate(getClientAddress(), serverConfig())
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,33 @@
|
|||||||
import Header from '$lib/components/Header.svelte';
|
import Header from '$lib/components/Header.svelte';
|
||||||
import ServiceGroup from '$lib/components/ServiceGroup.svelte';
|
import ServiceGroup from '$lib/components/ServiceGroup.svelte';
|
||||||
import { layoutDirection, type LayoutDirection } from '$lib/stores/layout';
|
import { layoutDirection, type LayoutDirection } from '$lib/stores/layout';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import type { ServiceDataEvent } from '$lib/server/dataPolling';
|
||||||
|
import type { ServiceData } from '$lib/services/service';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
const serviceData: Array<Array<Partial<ServiceData>>> = [];
|
||||||
|
for (const [i, group] of data.config.services.entries()) {
|
||||||
|
serviceData[i] = new Array(group.items.length).fill({});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const sse = new EventSource('/api/updates');
|
||||||
|
|
||||||
|
sse.onmessage = (e) => {
|
||||||
|
if (e.data == undefined || e.data === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const event = JSON.parse(e.data) as ServiceDataEvent;
|
||||||
|
serviceData[event.group][event.item] = event.data;
|
||||||
|
console.log(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
sse.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
function renderClasses(columns: number, direction: LayoutDirection): string {
|
function renderClasses(columns: number, direction: LayoutDirection): string {
|
||||||
if (direction === 'row') {
|
if (direction === 'row') {
|
||||||
@@ -37,9 +61,9 @@
|
|||||||
|
|
||||||
<Header section={data.config} links={data.config.links} />
|
<Header section={data.config} links={data.config.links} />
|
||||||
|
|
||||||
<main class="mb-auto mt-16 gap-12 {layoutClasses}" style="--columns: {data.config.columns}">
|
<main class="mb-auto mt-16 gap-12 {layoutClasses}">
|
||||||
{#each data.config.services as group, i}
|
{#each data.config.services as group, i}
|
||||||
<ServiceGroup {group} groupData={data.serviceData[i]} {columns} />
|
<ServiceGroup {group} groupData={serviceData[i]} {columns} />
|
||||||
{/each}
|
{/each}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
41
src/routes/api/updates/+server.ts
Normal file
41
src/routes/api/updates/+server.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { subscribeToDataEvent } from '$lib/server/dataPolling';
|
||||||
|
import { requestLogger } from '$lib/server/logger';
|
||||||
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
|
import type { Unsubscriber } from 'svelte/store';
|
||||||
|
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
export const GET: RequestHandler = ({ getClientAddress }) => {
|
||||||
|
id++;
|
||||||
|
const logger = requestLogger.child({
|
||||||
|
url: 'GET /api/updates',
|
||||||
|
address: getClientAddress(),
|
||||||
|
id: id
|
||||||
|
});
|
||||||
|
let unsubscribe: Unsubscriber | undefined = undefined;
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start: (controller) => {
|
||||||
|
logger.debug('new update request stream started');
|
||||||
|
|
||||||
|
unsubscribe = subscribeToDataEvent((event) => {
|
||||||
|
logger.trace({ event: event }, 'sending new event');
|
||||||
|
const data = `event:message\ndata:${JSON.stringify(event)}\n\n`;
|
||||||
|
controller.enqueue(data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cancel: (reason) => {
|
||||||
|
logger.debug({ reason }, 'stopping request stream');
|
||||||
|
if (unsubscribe != undefined) {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('got request');
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
title: 'Hello World !!'
|
|
||||||
subtitle: 'actually, I am a new pilot.'
|
|
||||||
|
|
||||||
services:
|
|
||||||
- title: '/Cloud'
|
|
||||||
subtitle: 'Private Cloud Utilities'
|
|
||||||
icon: 'fas fa-cloud'
|
|
||||||
items:
|
|
||||||
- title: 'NAS'
|
|
||||||
subtitle: 'Network Attached Storage'
|
|
||||||
icon: 'fas fa-hard-drive'
|
|
||||||
target: '_blank'
|
|
||||||
url: '/NAS'
|
|
||||||
keywords: 'cloud storage files'
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import ViteYaml from '@modyfi/vite-plugin-yaml';
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
import ViteYaml from '@modyfi/vite-plugin-yaml';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [ViteYaml(), sveltekit()],
|
plugins: [ViteYaml(), sveltekit()],
|
||||||
@@ -8,5 +8,6 @@ export default defineConfig({
|
|||||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||||
},
|
},
|
||||||
|
|
||||||
build: { target: 'esnext' }
|
build: { target: 'esnext' },
|
||||||
|
logLevel: 'info'
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user