mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-05 21:47:48 +01:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bad436b858 | ||
|
|
7f0345a56a | ||
|
|
97bf174b78 | ||
|
|
975f79f6cc | ||
|
|
e72efe7fd0 | ||
|
|
d94b3e829d | ||
|
|
10c63939dc | ||
|
|
972ede9249 | ||
|
|
8f001ad88a | ||
|
|
229c5dac59 | ||
|
|
0622395ec7 | ||
|
|
66073ed460 | ||
|
|
533f40b536 | ||
|
|
13afe82fa5 | ||
|
|
10c27dfd84 | ||
|
|
057d5eca8f | ||
|
|
e89f3668a9 | ||
|
|
c46306fc1d | ||
|
|
76d534583b | ||
|
|
7b4f360a5e | ||
|
|
992b18c9de | ||
|
|
6291a5422a | ||
|
|
4581c4eeb0 | ||
|
|
d6d93e3c03 | ||
|
|
f40ca1e25c | ||
|
|
2d764ce59b | ||
|
|
a1841f26bb | ||
|
|
c4ab3eb992 | ||
|
|
617cbcaee1 | ||
|
|
a9a28e14df |
11
README.md
11
README.md
@@ -9,11 +9,12 @@
|
||||
- Docker Integration
|
||||
- Status light + CPU, Memory & Network Reporting *(click on the status light)*
|
||||
- Service Integration
|
||||
- Currently supports Sonarr, Radarr, Ombi, Emby, Jellyfin, NZBGet, ruTorrent
|
||||
- Portainer, Traefik, Speedtest Tracker, PiHole
|
||||
- Currently supports Sonarr, Radarr, Ombi, Emby, Jellyfin, Jellyseerr ([by ilusi0n](https://github.com/benphelps/homepage/pull/34)), NZBGet, ruTorrent
|
||||
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager ([by aidenpwnz](https://github.com/benphelps/homepage/pull/45))
|
||||
* Homepage Widgets
|
||||
- System Stats (Disk, CPU, Memory)
|
||||
- Weather via WeatherAPI.com or OpenWeatherMap ([thanks to AlexFullmoon](https://github.com/benphelps/homepage/pull/25))
|
||||
- Weather via WeatherAPI.com or OpenWeatherMap ([by AlexFullmoon](https://github.com/benphelps/homepage/pull/25))
|
||||
- Search Bar ([by aidenpwnz](https://github.com/benphelps/homepage/pull/45))
|
||||
* Customizable
|
||||
- 21 theme colors with light and dark mode support
|
||||
|
||||
@@ -35,7 +36,7 @@ Using docker compose:
|
||||
version: '3.3'
|
||||
services:
|
||||
homepage:
|
||||
image: ghcr.io/benphelps/homepage:main
|
||||
image: ghcr.io/benphelps/homepage:latest
|
||||
container_name: homepage
|
||||
ports:
|
||||
- 3000:3000
|
||||
@@ -47,7 +48,7 @@ services:
|
||||
or docker run:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 -v /path/to/config:/app/config -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/benphelps/homepage:main
|
||||
docker run -p 3000:3000 -v /path/to/config:/app/config -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/benphelps/homepage:latest
|
||||
```
|
||||
|
||||
### With Node
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"dockerode": "^3.3.3",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"dockerode": "^3.3.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-rpc-2.0": "^1.3.0",
|
||||
"json-rpc-2.0": "^1.4.1",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "12.2.5",
|
||||
"node-os-utils": "^1.3.7",
|
||||
@@ -30,6 +30,6 @@
|
||||
"eslint-config-next": "12.2.5",
|
||||
"postcss": "^8.4.16",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
207
pnpm-lock.yaml
generated
207
pnpm-lock.yaml
generated
@@ -2,13 +2,13 @@ lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
'@headlessui/react': ^1.6.6
|
||||
'@tailwindcss/forms': ^0.5.2
|
||||
'@tailwindcss/forms': ^0.5.3
|
||||
autoprefixer: ^10.4.8
|
||||
dockerode: ^3.3.3
|
||||
dockerode: ^3.3.4
|
||||
eslint: 8.22.0
|
||||
eslint-config-next: 12.2.5
|
||||
js-yaml: ^4.1.0
|
||||
json-rpc-2.0: ^1.3.0
|
||||
json-rpc-2.0: ^1.4.1
|
||||
memory-cache: ^0.2.0
|
||||
next: 12.2.5
|
||||
node-os-utils: ^1.3.7
|
||||
@@ -20,14 +20,14 @@ specifiers:
|
||||
rutorrent-promise: ^2.0.0
|
||||
swr: ^1.3.0
|
||||
tailwindcss: ^3.1.8
|
||||
typescript: ^4.7.4
|
||||
typescript: ^4.8.2
|
||||
|
||||
dependencies:
|
||||
'@headlessui/react': 1.6.6_biqbaboplfbrettd7655fr4n2y
|
||||
'@tailwindcss/forms': 0.5.2_tailwindcss@3.1.8
|
||||
dockerode: 3.3.3
|
||||
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
|
||||
dockerode: 3.3.4
|
||||
js-yaml: 4.1.0
|
||||
json-rpc-2.0: 1.3.0
|
||||
json-rpc-2.0: 1.4.1
|
||||
memory-cache: 0.2.0
|
||||
next: 12.2.5_biqbaboplfbrettd7655fr4n2y
|
||||
node-os-utils: 1.3.7
|
||||
@@ -41,10 +41,10 @@ dependencies:
|
||||
devDependencies:
|
||||
autoprefixer: 10.4.8_postcss@8.4.16
|
||||
eslint: 8.22.0
|
||||
eslint-config-next: 12.2.5_4rv7y5c6xz3vfxwhbrcxxi73bq
|
||||
eslint-config-next: 12.2.5_shit3uhl6a7megkzgoz6xssnfa
|
||||
postcss: 8.4.16
|
||||
tailwindcss: 3.1.8
|
||||
typescript: 4.7.4
|
||||
tailwindcss: 3.1.8_postcss@8.4.16
|
||||
typescript: 4.8.2
|
||||
|
||||
packages:
|
||||
|
||||
@@ -52,7 +52,7 @@ packages:
|
||||
resolution: {integrity: sha512-qZEWeccZCrHA2Au4/X05QW5CMdm4VjUDCrGq5gf1ZDcM4hRqreKrtwAn7yci9zfgAS9apvnsFXiGBHBAxZdK9A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
core-js-pure: 3.24.1
|
||||
core-js-pure: 3.25.0
|
||||
regenerator-runtime: 0.13.9
|
||||
dev: true
|
||||
|
||||
@@ -63,13 +63,17 @@ packages:
|
||||
regenerator-runtime: 0.13.9
|
||||
dev: true
|
||||
|
||||
/@eslint/eslintrc/1.3.0:
|
||||
resolution: {integrity: sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==}
|
||||
/@balena/dockerignore/1.0.2:
|
||||
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
|
||||
dev: false
|
||||
|
||||
/@eslint/eslintrc/1.3.1:
|
||||
resolution: {integrity: sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.3.4
|
||||
espree: 9.3.3
|
||||
espree: 9.4.0
|
||||
globals: 13.17.0
|
||||
ignore: 5.2.0
|
||||
import-fresh: 3.3.0
|
||||
@@ -265,21 +269,21 @@ packages:
|
||||
tslib: 2.4.0
|
||||
dev: false
|
||||
|
||||
/@tailwindcss/forms/0.5.2_tailwindcss@3.1.8:
|
||||
resolution: {integrity: sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==}
|
||||
/@tailwindcss/forms/0.5.3_tailwindcss@3.1.8:
|
||||
resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==}
|
||||
peerDependencies:
|
||||
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
|
||||
dependencies:
|
||||
mini-svg-data-uri: 1.4.4
|
||||
tailwindcss: 3.1.8
|
||||
tailwindcss: 3.1.8_postcss@8.4.16
|
||||
dev: false
|
||||
|
||||
/@types/json5/0.0.29:
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/parser/5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq:
|
||||
resolution: {integrity: sha512-cgM5cJrWmrDV2KpvlcSkelTBASAs1mgqq+IUGKJvFxWrapHpaRy5EXPQz9YaKF3nZ8KY18ILTiVpUtbIac86/w==}
|
||||
/@typescript-eslint/parser/5.36.1_shit3uhl6a7megkzgoz6xssnfa:
|
||||
resolution: {integrity: sha512-/IsgNGOkBi7CuDfUbwt1eOqUXF9WGVBW9dwEe1pi+L32XrTsZIgmDFIi2RxjzsvB/8i+MIf5JIoTEH8LOZ368A==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -288,31 +292,31 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 5.33.0
|
||||
'@typescript-eslint/types': 5.33.0
|
||||
'@typescript-eslint/typescript-estree': 5.33.0_typescript@4.7.4
|
||||
'@typescript-eslint/scope-manager': 5.36.1
|
||||
'@typescript-eslint/types': 5.36.1
|
||||
'@typescript-eslint/typescript-estree': 5.36.1_typescript@4.8.2
|
||||
debug: 4.3.4
|
||||
eslint: 8.22.0
|
||||
typescript: 4.7.4
|
||||
typescript: 4.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/scope-manager/5.33.0:
|
||||
resolution: {integrity: sha512-/Jta8yMNpXYpRDl8EwF/M8It2A9sFJTubDo0ATZefGXmOqlaBffEw0ZbkbQ7TNDK6q55NPHFshGBPAZvZkE8Pw==}
|
||||
/@typescript-eslint/scope-manager/5.36.1:
|
||||
resolution: {integrity: sha512-pGC2SH3/tXdu9IH3ItoqciD3f3RRGCh7hb9zPdN2Drsr341zgd6VbhP5OHQO/reUqihNltfPpMpTNihFMarP2w==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.33.0
|
||||
'@typescript-eslint/visitor-keys': 5.33.0
|
||||
'@typescript-eslint/types': 5.36.1
|
||||
'@typescript-eslint/visitor-keys': 5.36.1
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/types/5.33.0:
|
||||
resolution: {integrity: sha512-nIMt96JngB4MYFYXpZ/3ZNU4GWPNdBbcB5w2rDOCpXOVUkhtNlG2mmm8uXhubhidRZdwMaMBap7Uk8SZMU/ppw==}
|
||||
/@typescript-eslint/types/5.36.1:
|
||||
resolution: {integrity: sha512-jd93ShpsIk1KgBTx9E+hCSEuLCUFwi9V/urhjOWnOaksGZFbTOxAT47OH2d4NLJnLhkVD+wDbB48BuaycZPLBg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree/5.33.0_typescript@4.7.4:
|
||||
resolution: {integrity: sha512-tqq3MRLlggkJKJUrzM6wltk8NckKyyorCSGMq4eVkyL5sDYzJJcMgZATqmF8fLdsWrW7OjjIZ1m9v81vKcaqwQ==}
|
||||
/@typescript-eslint/typescript-estree/5.36.1_typescript@4.8.2:
|
||||
resolution: {integrity: sha512-ih7V52zvHdiX6WcPjsOdmADhYMDN15SylWRZrT2OMy80wzKbc79n8wFW0xpWpU0x3VpBz/oDgTm2xwDAnFTl+g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
@@ -320,23 +324,23 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.33.0
|
||||
'@typescript-eslint/visitor-keys': 5.33.0
|
||||
'@typescript-eslint/types': 5.36.1
|
||||
'@typescript-eslint/visitor-keys': 5.36.1
|
||||
debug: 4.3.4
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.3.7
|
||||
tsutils: 3.21.0_typescript@4.7.4
|
||||
typescript: 4.7.4
|
||||
tsutils: 3.21.0_typescript@4.8.2
|
||||
typescript: 4.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/visitor-keys/5.33.0:
|
||||
resolution: {integrity: sha512-/XsqCzD4t+Y9p5wd9HZiptuGKBlaZO5showwqODii5C0nZawxWLF+Q6k5wYHBrQv96h6GYKyqqMHCSTqta8Kiw==}
|
||||
/@typescript-eslint/visitor-keys/5.36.1:
|
||||
resolution: {integrity: sha512-ojB9aRyRFzVMN3b5joSYni6FAS10BBSCAfKJhjJAV08t/a95aM6tAhz+O1jF+EtgxktuSO3wJysp2R+Def/IWQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.33.0
|
||||
'@typescript-eslint/types': 5.36.1
|
||||
eslint-visitor-keys: 3.3.0
|
||||
dev: true
|
||||
|
||||
@@ -418,7 +422,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
get-intrinsic: 1.1.2
|
||||
is-string: 1.0.7
|
||||
dev: true
|
||||
@@ -434,7 +438,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
es-shim-unscopables: 1.0.0
|
||||
dev: true
|
||||
|
||||
@@ -444,7 +448,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
es-shim-unscopables: 1.0.0
|
||||
dev: true
|
||||
|
||||
@@ -470,7 +474,7 @@ packages:
|
||||
postcss: ^8.1.0
|
||||
dependencies:
|
||||
browserslist: 4.21.3
|
||||
caniuse-lite: 1.0.30001375
|
||||
caniuse-lite: 1.0.30001388
|
||||
fraction.js: 4.2.0
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.0.0
|
||||
@@ -531,10 +535,10 @@ packages:
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001375
|
||||
electron-to-chromium: 1.4.219
|
||||
caniuse-lite: 1.0.30001388
|
||||
electron-to-chromium: 1.4.241
|
||||
node-releases: 2.0.6
|
||||
update-browserslist-db: 1.0.5_browserslist@4.21.3
|
||||
update-browserslist-db: 1.0.7_browserslist@4.21.3
|
||||
dev: true
|
||||
|
||||
/buffer/5.7.1:
|
||||
@@ -571,8 +575,8 @@ packages:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
/caniuse-lite/1.0.30001375:
|
||||
resolution: {integrity: sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==}
|
||||
/caniuse-lite/1.0.30001388:
|
||||
resolution: {integrity: sha512-znVbq4OUjqgLxMxoNX2ZeeLR0d7lcDiE5uJ4eUiWdml1J1EkxbnQq6opT9jb9SMfJxB0XA16/ziHwni4u1I3GQ==}
|
||||
|
||||
/chalk/4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
@@ -621,8 +625,8 @@ packages:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
dev: true
|
||||
|
||||
/core-js-pure/3.24.1:
|
||||
resolution: {integrity: sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg==}
|
||||
/core-js-pure/3.25.0:
|
||||
resolution: {integrity: sha512-IeHpLwk3uoci37yoI2Laty59+YqH9x5uR65/yiA0ARAJrTrN4YU0rmauLWfvqOuk77SlNJXj2rM6oT/dBD87+A==}
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
|
||||
@@ -734,8 +738,8 @@ packages:
|
||||
/dlv/1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
|
||||
/docker-modem/3.0.5:
|
||||
resolution: {integrity: sha512-x1E6jxWdtoK3+ifAUWj4w5egPdTDGBpesSCErm+aKET5BnnEOvDtTP6GxcnMB1zZiv2iQ0qJZvJie+1wfIRg6Q==}
|
||||
/docker-modem/3.0.6:
|
||||
resolution: {integrity: sha512-h0Ow21gclbYsZ3mkHDfsYNDqtRhXS8fXr51bU0qr1dxgTMJj0XufbzX+jhNOvA8KuEEzn6JbvLVhXyv+fny9Uw==}
|
||||
engines: {node: '>= 8.0'}
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
@@ -746,11 +750,12 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/dockerode/3.3.3:
|
||||
resolution: {integrity: sha512-lvKV6/NGf2/CYLt5V4c0fd6Fl9XZSCo1Z2HBT9ioKrKLMB2o+gA62Uza8RROpzGvYv57KJx2dKu+ZwSpB//OIA==}
|
||||
/dockerode/3.3.4:
|
||||
resolution: {integrity: sha512-3EUwuXnCU+RUlQEheDjmBE0B7q66PV9Rw5NiH1sXwINq0M9c5ERP9fxgkw36ZHOtzf4AGEEYySnkx/sACC9EgQ==}
|
||||
engines: {node: '>= 8.0'}
|
||||
dependencies:
|
||||
docker-modem: 3.0.5
|
||||
'@balena/dockerignore': 1.0.2
|
||||
docker-modem: 3.0.6
|
||||
tar-fs: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -770,8 +775,8 @@ packages:
|
||||
esutils: 2.0.3
|
||||
dev: true
|
||||
|
||||
/electron-to-chromium/1.4.219:
|
||||
resolution: {integrity: sha512-zoQJsXOUw0ZA0YxbjkmzBumAJRtr6je5JySuL/bAoFs0DuLiLJ+5FzRF7/ZayihxR2QcewlRZVm5QZdUhwjOgA==}
|
||||
/electron-to-chromium/1.4.241:
|
||||
resolution: {integrity: sha512-e7Wsh4ilaioBZ5bMm6+F4V5c11dh56/5Jwz7Hl5Tu1J7cnB+Pqx5qIF2iC7HPpfyQMqGSvvLP5bBAIDd2gAtGw==}
|
||||
dev: true
|
||||
|
||||
/emoji-regex/9.2.2:
|
||||
@@ -784,8 +789,8 @@ packages:
|
||||
once: 1.4.0
|
||||
dev: false
|
||||
|
||||
/es-abstract/1.20.1:
|
||||
resolution: {integrity: sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==}
|
||||
/es-abstract/1.20.2:
|
||||
resolution: {integrity: sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
@@ -806,7 +811,7 @@ packages:
|
||||
is-weakref: 1.0.2
|
||||
object-inspect: 1.12.2
|
||||
object-keys: 1.1.1
|
||||
object.assign: 4.1.3
|
||||
object.assign: 4.1.4
|
||||
regexp.prototype.flags: 1.4.3
|
||||
string.prototype.trimend: 1.0.5
|
||||
string.prototype.trimstart: 1.0.5
|
||||
@@ -838,7 +843,7 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/eslint-config-next/12.2.5_4rv7y5c6xz3vfxwhbrcxxi73bq:
|
||||
/eslint-config-next/12.2.5_shit3uhl6a7megkzgoz6xssnfa:
|
||||
resolution: {integrity: sha512-SOowilkqPzW6DxKp3a3SYlrfPi5Ajs9MIzp9gVfUDxxH9QFM5ElkR1hX5m/iICJuvCbWgQqFBiA3mCMozluniw==}
|
||||
peerDependencies:
|
||||
eslint: ^7.23.0 || ^8.0.0
|
||||
@@ -849,15 +854,15 @@ packages:
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 12.2.5
|
||||
'@rushstack/eslint-patch': 1.1.4
|
||||
'@typescript-eslint/parser': 5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq
|
||||
'@typescript-eslint/parser': 5.36.1_shit3uhl6a7megkzgoz6xssnfa
|
||||
eslint: 8.22.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-import-resolver-typescript: 2.7.1_2iahngt3u2tkbdlu6s4gkur3pu
|
||||
eslint-plugin-import: 2.26.0_42jdfezp7lcuhr3fexihng3k3a
|
||||
eslint-plugin-import: 2.26.0_oqagwj4pgbbpoeodu52b4a34xi
|
||||
eslint-plugin-jsx-a11y: 6.6.1_eslint@8.22.0
|
||||
eslint-plugin-react: 7.30.1_eslint@8.22.0
|
||||
eslint-plugin-react: 7.31.5_eslint@8.22.0
|
||||
eslint-plugin-react-hooks: 4.6.0_eslint@8.22.0
|
||||
typescript: 4.7.4
|
||||
typescript: 4.8.2
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
@@ -881,7 +886,7 @@ packages:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
eslint: 8.22.0
|
||||
eslint-plugin-import: 2.26.0_42jdfezp7lcuhr3fexihng3k3a
|
||||
eslint-plugin-import: 2.26.0_oqagwj4pgbbpoeodu52b4a34xi
|
||||
glob: 7.2.3
|
||||
is-glob: 4.0.3
|
||||
resolve: 1.22.1
|
||||
@@ -890,7 +895,7 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-module-utils/2.7.4_2znkvn5grarov6xvamxzzjpwtu:
|
||||
/eslint-module-utils/2.7.4_56k6siz3uc4k5fooi777xiw6ie:
|
||||
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
@@ -911,7 +916,7 @@ packages:
|
||||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq
|
||||
'@typescript-eslint/parser': 5.36.1_shit3uhl6a7megkzgoz6xssnfa
|
||||
debug: 3.2.7
|
||||
eslint: 8.22.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
@@ -920,7 +925,7 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-import/2.26.0_42jdfezp7lcuhr3fexihng3k3a:
|
||||
/eslint-plugin-import/2.26.0_oqagwj4pgbbpoeodu52b4a34xi:
|
||||
resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
@@ -930,14 +935,14 @@ packages:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.33.0_4rv7y5c6xz3vfxwhbrcxxi73bq
|
||||
'@typescript-eslint/parser': 5.36.1_shit3uhl6a7megkzgoz6xssnfa
|
||||
array-includes: 3.1.5
|
||||
array.prototype.flat: 1.3.0
|
||||
debug: 2.6.9
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.22.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-module-utils: 2.7.4_2znkvn5grarov6xvamxzzjpwtu
|
||||
eslint-module-utils: 2.7.4_56k6siz3uc4k5fooi777xiw6ie
|
||||
has: 1.0.3
|
||||
is-core-module: 2.10.0
|
||||
is-glob: 4.0.3
|
||||
@@ -982,8 +987,8 @@ packages:
|
||||
eslint: 8.22.0
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-react/7.30.1_eslint@8.22.0:
|
||||
resolution: {integrity: sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==}
|
||||
/eslint-plugin-react/7.31.5_eslint@8.22.0:
|
||||
resolution: {integrity: sha512-y7472VAcqns17rsQUk6tQCnqBi+boYjGdYarX022719+wGd1T4U1fOYJ2T2Trd3Od2q5M92e42zJ2uZOGmWamA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
|
||||
@@ -1038,7 +1043,7 @@ packages:
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@eslint/eslintrc': 1.3.0
|
||||
'@eslint/eslintrc': 1.3.1
|
||||
'@humanwhocodes/config-array': 0.10.4
|
||||
'@humanwhocodes/gitignore-to-minimatch': 1.0.2
|
||||
ajv: 6.12.6
|
||||
@@ -1050,7 +1055,7 @@ packages:
|
||||
eslint-scope: 7.1.1
|
||||
eslint-utils: 3.0.0_eslint@8.22.0
|
||||
eslint-visitor-keys: 3.3.0
|
||||
espree: 9.3.3
|
||||
espree: 9.4.0
|
||||
esquery: 1.4.0
|
||||
esutils: 2.0.3
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -1081,8 +1086,8 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/espree/9.3.3:
|
||||
resolution: {integrity: sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==}
|
||||
/espree/9.4.0:
|
||||
resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
acorn: 8.8.0
|
||||
@@ -1166,12 +1171,12 @@ packages:
|
||||
resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
dependencies:
|
||||
flatted: 3.2.6
|
||||
flatted: 3.2.7
|
||||
rimraf: 3.0.2
|
||||
dev: true
|
||||
|
||||
/flatted/3.2.6:
|
||||
resolution: {integrity: sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==}
|
||||
/flatted/3.2.7:
|
||||
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
|
||||
dev: true
|
||||
|
||||
/form-data/3.0.1:
|
||||
@@ -1211,7 +1216,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
functions-have-names: 1.2.3
|
||||
dev: true
|
||||
|
||||
@@ -1498,8 +1503,8 @@ packages:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
/json-rpc-2.0/1.3.0:
|
||||
resolution: {integrity: sha512-pA85D9LG4B+b8njdb8DzktltNfGPcxGJJydEg6jRqePfwhK008lRdbX7bco6aokfw9oDFw7fEe+LRxxXwP11HA==}
|
||||
/json-rpc-2.0/1.4.1:
|
||||
resolution: {integrity: sha512-OX1NJhpIfuK4GjDnJ/gKtZy1HOYo0l4eL0a4rb0rNeQheX1xlyQ9+JMmPzs/sFNthpS/TXKPWlGo09X7B5l81A==}
|
||||
dev: false
|
||||
|
||||
/json-schema-traverse/0.4.1:
|
||||
@@ -1522,7 +1527,7 @@ packages:
|
||||
engines: {node: '>=4.0'}
|
||||
dependencies:
|
||||
array-includes: 3.1.5
|
||||
object.assign: 4.1.3
|
||||
object.assign: 4.1.4
|
||||
dev: true
|
||||
|
||||
/language-subtag-registry/0.3.22:
|
||||
@@ -1661,7 +1666,7 @@ packages:
|
||||
dependencies:
|
||||
'@next/env': 12.2.5
|
||||
'@swc/helpers': 0.4.3
|
||||
caniuse-lite: 1.0.30001375
|
||||
caniuse-lite: 1.0.30001388
|
||||
postcss: 8.4.14
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
@@ -1733,8 +1738,8 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
dev: true
|
||||
|
||||
/object.assign/4.1.3:
|
||||
resolution: {integrity: sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==}
|
||||
/object.assign/4.1.4:
|
||||
resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
@@ -1749,7 +1754,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/object.fromentries/2.0.5:
|
||||
@@ -1758,14 +1763,14 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/object.hasown/1.1.1:
|
||||
resolution: {integrity: sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==}
|
||||
dependencies:
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/object.values/1.1.5:
|
||||
@@ -1774,7 +1779,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/once/1.4.0:
|
||||
@@ -2163,7 +2168,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
get-intrinsic: 1.1.2
|
||||
has-symbols: 1.0.3
|
||||
internal-slot: 1.0.3
|
||||
@@ -2176,7 +2181,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/string.prototype.trimstart/1.0.5:
|
||||
@@ -2184,7 +2189,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
es-abstract: 1.20.2
|
||||
dev: true
|
||||
|
||||
/string_decoder/1.3.0:
|
||||
@@ -2245,10 +2250,12 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/tailwindcss/3.1.8:
|
||||
/tailwindcss/3.1.8_postcss@8.4.16:
|
||||
resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
postcss: ^8.0.9
|
||||
dependencies:
|
||||
arg: 5.0.2
|
||||
chokidar: 3.5.3
|
||||
@@ -2331,14 +2338,14 @@ packages:
|
||||
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
|
||||
dev: false
|
||||
|
||||
/tsutils/3.21.0_typescript@4.7.4:
|
||||
/tsutils/3.21.0_typescript@4.8.2:
|
||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||
engines: {node: '>= 6'}
|
||||
peerDependencies:
|
||||
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
|
||||
dependencies:
|
||||
tslib: 1.14.1
|
||||
typescript: 4.7.4
|
||||
typescript: 4.8.2
|
||||
dev: true
|
||||
|
||||
/tweetnacl/0.14.5:
|
||||
@@ -2357,8 +2364,8 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/typescript/4.7.4:
|
||||
resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
|
||||
/typescript/4.8.2:
|
||||
resolution: {integrity: sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==}
|
||||
engines: {node: '>=4.2.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
@@ -2377,8 +2384,8 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/update-browserslist-db/1.0.5_browserslist@4.21.3:
|
||||
resolution: {integrity: sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==}
|
||||
/update-browserslist-db/1.0.7_browserslist@4.21.3:
|
||||
resolution: {integrity: sha512-iN/XYesmZ2RmmWAiI4Z5rq0YqSiv0brj9Ce9CfhNE4xIW2h+MFxcgkxIzZ+ShkFPUkjU3gQ+3oypadD3RAMtrg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
@@ -21,14 +21,19 @@ function resolveIcon(icon) {
|
||||
|
||||
export default function Item({ service }) {
|
||||
return (
|
||||
<li key={service.name} className="">
|
||||
<li key={service.name}>
|
||||
<Disclosure>
|
||||
<div className="transition-all h-15 overflow-hidden mb-3 cursor-pointer p-1 rounded-md font-medium text-theme-700 hover:text-theme-800 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/40 bg-white/50 hover:bg-theme-300/10 dark:bg-white/5 dark:hover:bg-white/10">
|
||||
<div className={
|
||||
(service.href && service.href !== "#" ? 'cursor-pointer ' : 'cursor-default ') +
|
||||
'transition-all h-15 overflow-hidden mb-3 p-1 rounded-md font-medium text-theme-700 hover:text-theme-800 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/40 bg-white/50 hover:bg-theme-300/10 dark:bg-white/5 dark:hover:bg-white/10'
|
||||
}>
|
||||
<div className="flex">
|
||||
{service.icon && (
|
||||
<div
|
||||
onClick={() => {
|
||||
window.open(service.href, "_blank").focus();
|
||||
if (service.href && service.href !== "#") {
|
||||
window.open(service.href, "_blank").focus();
|
||||
}
|
||||
}}
|
||||
className="flex-shrink-0 flex items-center justify-center w-12 "
|
||||
>
|
||||
@@ -38,7 +43,9 @@ export default function Item({ service }) {
|
||||
|
||||
<div
|
||||
onClick={() => {
|
||||
window.open(service.href, "_blank").focus();
|
||||
if (service.href && service.href !== "#") {
|
||||
window.open(service.href, "_blank").focus();
|
||||
}
|
||||
}}
|
||||
className="flex-1 flex items-center justify-between rounded-r-md "
|
||||
>
|
||||
@@ -48,7 +55,7 @@ export default function Item({ service }) {
|
||||
</div>
|
||||
</div>
|
||||
{service.container && (
|
||||
<Disclosure.Button as="div" className="flex-shrink-0 flex items-center justify-center w-12 ">
|
||||
<Disclosure.Button as="div" className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer">
|
||||
<Status service={service} />
|
||||
</Disclosure.Button>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,9 @@ import Rutorrent from "./widgets/service/rutorrent";
|
||||
import Jellyfin from "./widgets/service/jellyfin";
|
||||
import Speedtest from "./widgets/service/speedtest";
|
||||
import Traefik from "./widgets/service/traefik";
|
||||
import Jellyseerr from "./widgets/service/jellyseerr";
|
||||
import Npm from "./widgets/service/npm";
|
||||
import Tautulli from "./widgets/service/tautulli";
|
||||
|
||||
const widgetMappings = {
|
||||
docker: Docker,
|
||||
@@ -24,6 +27,9 @@ const widgetMappings = {
|
||||
rutorrent: Rutorrent,
|
||||
speedtest: Speedtest,
|
||||
traefik: Traefik,
|
||||
jellyseerr: Jellyseerr,
|
||||
npm: Npm,
|
||||
tautulli: Tautulli,
|
||||
};
|
||||
|
||||
export default function Widget({ service }) {
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
|
||||
|
||||
export default function Docker({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(
|
||||
`/api/docker/status/${config.container}/${config.server || ""}`,
|
||||
{
|
||||
refreshInterval: 1500,
|
||||
refreshInterval: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(
|
||||
`/api/docker/stats/${config.container}/${config.server || ""}`,
|
||||
{
|
||||
refreshInterval: 1500,
|
||||
refreshInterval: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -49,8 +49,12 @@ export default function Docker({ service }) {
|
||||
<Widget>
|
||||
<Block label="CPU" value={`${calculateCPUPercent(statsData.stats)}%`} />
|
||||
<Block label="MEM" value={formatBytes(statsData.stats.memory_stats.usage, 0)} />
|
||||
<Block label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
|
||||
<Block label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
|
||||
{statsData.stats.networks && (
|
||||
<>
|
||||
<Block label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
|
||||
<Block label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
|
||||
</>
|
||||
)}
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,17 +3,12 @@ import useSWR from "swr";
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Emby({ service, title = "Emby" }) {
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, key } = config;
|
||||
return `${url}/emby/${endpoint}?api_key=${key}`;
|
||||
}
|
||||
|
||||
const { data: sessionsData, error: sessionsError } = useSWR(buildApiUrl(`Sessions`), {
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions"));
|
||||
|
||||
if (sessionsError) {
|
||||
return <Widget error={`${title} API Error`} />;
|
||||
|
||||
34
src/components/services/widgets/service/jellyseerr.jsx
Normal file
34
src/components/services/widgets/service/jellyseerr.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Jellyseerr({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error="Jellyseerr API Error" />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" />
|
||||
<Block label="Approved" />
|
||||
<Block label="Available" />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" value={statsData.pending} />
|
||||
<Block label="Approved" value={statsData.approved} />
|
||||
<Block label="Available" value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
38
src/components/services/widgets/service/npm.jsx
Normal file
38
src/components/services/widgets/service/npm.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Npm({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts"));
|
||||
|
||||
if (infoError) {
|
||||
return <Widget error="NGINX Proxy Manager API Error" />;
|
||||
}
|
||||
|
||||
if (!infoData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Enabled" />
|
||||
<Block label="Disabled" />
|
||||
<Block label="Total" />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const enabled = infoData.filter((c) => c.enabled === 1).length;
|
||||
const disabled = infoData.filter((c) => c.enabled === 0).length;
|
||||
const total = infoData.length;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Enabled" value={enabled} />
|
||||
<Block label="Disabled" value={disabled} />
|
||||
<Block label="Total" value={total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +1,15 @@
|
||||
import useSWR from "swr";
|
||||
import { JSONRPCClient } from "json-rpc-2.0";
|
||||
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
|
||||
export default function Nzbget({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
const constructedUrl = new URL(config.url);
|
||||
constructedUrl.pathname = "jsonrpc";
|
||||
|
||||
const client = new JSONRPCClient((jsonRPCRequest) =>
|
||||
fetch(constructedUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: `Basic ${btoa(`${config.username}:${config.password}`)}`,
|
||||
},
|
||||
body: JSON.stringify(jsonRPCRequest),
|
||||
}).then(async (response) => {
|
||||
if (response.status === 200) {
|
||||
const jsonRPCResponse = await response.json();
|
||||
return client.receive(jsonRPCResponse);
|
||||
} else if (jsonRPCRequest.id !== undefined) {
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(
|
||||
"status",
|
||||
(resource) => {
|
||||
return client.request(resource).then((response) => response);
|
||||
},
|
||||
{
|
||||
refreshInterval: 1000,
|
||||
}
|
||||
);
|
||||
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status"));
|
||||
|
||||
if (statusError) {
|
||||
return <Widget error="Nzbget API Error" />;
|
||||
|
||||
@@ -3,27 +3,12 @@ import useSWR from "swr";
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Ombi({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url } = config;
|
||||
return `${url}/api/v1/${endpoint}`;
|
||||
}
|
||||
|
||||
const fetcher = (url) => {
|
||||
return fetch(url, {
|
||||
method: "GET",
|
||||
withCredentials: true,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
ApiKey: `${config.key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
};
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(buildApiUrl(`Request/count`), fetcher);
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error="Ombi API Error" />;
|
||||
|
||||
@@ -3,21 +3,12 @@ import useSWR from "swr";
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Pihole({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, proxy } = config;
|
||||
|
||||
if (proxy) {
|
||||
const fullUrl = `${url}/admin/${endpoint}`;
|
||||
return "/api/proxy?url=" + encodeURIComponent(fullUrl);
|
||||
}
|
||||
|
||||
return `${url}/admin/${endpoint}`;
|
||||
}
|
||||
|
||||
const { data: piholeData, error: piholeError } = useSWR(buildApiUrl("api.php"));
|
||||
const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php"));
|
||||
|
||||
if (piholeError) {
|
||||
return <Widget error="PiHole API Error" />;
|
||||
|
||||
@@ -3,29 +3,12 @@ import useSWR from "swr";
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Portainer({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, env } = config;
|
||||
const reqUrl = new URL(`/api/endpoints/${env}/${endpoint}`, url);
|
||||
return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
|
||||
}
|
||||
|
||||
const fetcher = async (url) => {
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
withCredentials: true,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"X-API-Key": `${config.key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
const { data: containersData, error: containersError } = useSWR(buildApiUrl(`docker/containers/json?all=1`), fetcher);
|
||||
const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`));
|
||||
|
||||
if (containersError) {
|
||||
return <Widget error="Portainer API Error" />;
|
||||
|
||||
@@ -3,16 +3,13 @@ import useSWR from "swr";
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Radarr({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, key } = config;
|
||||
return `${url}/api/v3/${endpoint}?apikey=${key}`;
|
||||
}
|
||||
|
||||
const { data: moviesData, error: moviesError } = useSWR(buildApiUrl("movie"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue/status"));
|
||||
const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status"));
|
||||
|
||||
if (moviesError || queuedError) {
|
||||
return <Widget error="Radarr API Error" />;
|
||||
@@ -35,7 +32,7 @@ export default function Radarr({ service }) {
|
||||
<Widget>
|
||||
<Block label="Wanted" value={wanted.length} />
|
||||
<Block label="Queued" value={queuedData.totalCount} />
|
||||
<Block label="Movies" value={moviesData.length} />
|
||||
<Block label="Movies" value={have.length} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
import useSWR from "swr";
|
||||
import RuTorrent from "rutorrent-promise";
|
||||
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
|
||||
export default function Rutorrent({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl() {
|
||||
const { url, username, password } = config;
|
||||
|
||||
const options = {
|
||||
url: `${url}/plugins/httprpc/action.php`,
|
||||
};
|
||||
|
||||
if (username && password) {
|
||||
options.username = username;
|
||||
options.password = password;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(options);
|
||||
|
||||
return `/api/widgets/rutorrent?${params.toString()}`;
|
||||
}
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(buildApiUrl());
|
||||
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config));
|
||||
|
||||
if (statusError) {
|
||||
return <Widget error="Nzbget API Error" />;
|
||||
|
||||
@@ -3,17 +3,14 @@ import useSWR from "swr";
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Sonarr({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url, key } = config;
|
||||
return `${url}/api/v3/${endpoint}?apikey=${key}`;
|
||||
}
|
||||
|
||||
const { data: wantedData, error: wantedError } = useSWR(buildApiUrl("wanted/missing"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue"));
|
||||
const { data: seriesData, error: seriesError } = useSWR(buildApiUrl("series"));
|
||||
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing"));
|
||||
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue"));
|
||||
const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series"));
|
||||
|
||||
if (wantedError || queuedError || seriesError) {
|
||||
return <Widget error="Sonar API Error" />;
|
||||
|
||||
@@ -4,16 +4,12 @@ import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatBits } from "utils/stats-helpers";
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Speedtest({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url } = config;
|
||||
return `${url}/api/${endpoint}`;
|
||||
}
|
||||
|
||||
const { data: speedtestData, error: speedtestError } = useSWR(buildApiUrl("speedtest/latest"));
|
||||
const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest"));
|
||||
|
||||
if (speedtestError) {
|
||||
return <Widget error="Speedtest API Error" />;
|
||||
@@ -31,8 +27,8 @@ export default function Speedtest({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Download" value={`${formatBits(speedtestData.data.download * 1024 * 1024)}ps`} />
|
||||
<Block label="Upload" value={`${formatBits(speedtestData.data.upload * 1024 * 1024)}ps`} />
|
||||
<Block label="Download" value={`${formatBits(speedtestData.data.download * 1024 * 1024, 0)}ps`} />
|
||||
<Block label="Upload" value={`${formatBits(speedtestData.data.upload * 1024 * 1024, 0)}ps`} />
|
||||
<Block label="Ping" value={`${speedtestData.data.ping} ms`} />
|
||||
</Widget>
|
||||
);
|
||||
|
||||
37
src/components/services/widgets/service/tautulli.jsx
Normal file
37
src/components/services/widgets/service/tautulli.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Tautulli({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, "get_activity"));
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error="Tautulli API Error" />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" />
|
||||
<Block label="Transcoding" />
|
||||
<Block label="Bitrate" />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const data = statsData.response.data;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" value={data.stream_count} />
|
||||
<Block label="Transcoding" value={data.stream_count_transcode} />
|
||||
{/* We divide by 1000 here because thats how Tautulli reports it on its own dashboard */}
|
||||
<Block label="Bitrate" value={`${Math.round((data.total_bandwidth / 1000) * 100) / 100} Mbps`} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@@ -3,16 +3,12 @@ import useSWR from "swr";
|
||||
import Widget from "../widget";
|
||||
import Block from "../block";
|
||||
|
||||
import { formatApiUrl } from "utils/api-helpers";
|
||||
|
||||
export default function Traefik({ service }) {
|
||||
const config = service.widget;
|
||||
|
||||
function buildApiUrl(endpoint) {
|
||||
const { url } = config;
|
||||
const fullUrl = `${url}/api/${endpoint}`;
|
||||
return `/api/proxy?url=${encodeURIComponent(fullUrl)}`;
|
||||
}
|
||||
|
||||
const { data: traefikData, error: traefikError } = useSWR(buildApiUrl("overview"));
|
||||
const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview"));
|
||||
|
||||
if (traefikError) {
|
||||
return <Widget error="Traefik API Error" />;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import WeatherApi from "components/widgets/weather/weather";
|
||||
import OpenWeatherMap from "components/widgets/openweathermap/weather";
|
||||
import Resources from "components/widgets/resources/resources";
|
||||
import Search from "components/widgets/search/search";
|
||||
|
||||
const widgetMappings = {
|
||||
weather: WeatherApi, // This key will be deprecated in the future
|
||||
weatherapi: WeatherApi,
|
||||
openweathermap: OpenWeatherMap,
|
||||
resources: Resources,
|
||||
search: Search,
|
||||
};
|
||||
|
||||
export default function Widget({ widget }) {
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function OpenWeatherMap({ options }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon
|
||||
|
||||
@@ -5,11 +5,11 @@ import Memory from "./memory";
|
||||
export default function Resources({ options }) {
|
||||
return (
|
||||
<>
|
||||
<div className="pr-2 flex flex-col">
|
||||
<div className="flex flex-row space-x-4">
|
||||
{options.disk && <Disk options={options} />}
|
||||
<div className="flex flex-col max-w:full basis-1/2 sm:basis-auto self-center">
|
||||
<div className="flex flex-row space-x-4 self-center">
|
||||
{options.cpu && <Cpu />}
|
||||
{options.memory && <Memory />}
|
||||
{options.disk && <Disk options={options} />}
|
||||
</div>
|
||||
{options.label && (
|
||||
<div className="border-t-2 border-theme-800 dark:border-theme-200 mt-1 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">
|
||||
|
||||
69
src/components/widgets/search/search.jsx
Normal file
69
src/components/widgets/search/search.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useState } from "react";
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
import { SiDuckduckgo, SiMicrosoftbing, SiGoogle } from "react-icons/si";
|
||||
|
||||
const providers = {
|
||||
google: {
|
||||
name: "Google",
|
||||
url: "https://www.google.com/search?q=",
|
||||
icon: SiGoogle,
|
||||
},
|
||||
duckduckgo: {
|
||||
name: "DuckDuckGo",
|
||||
url: "https://duckduckgo.com/?q=",
|
||||
icon: SiDuckduckgo,
|
||||
},
|
||||
bing: {
|
||||
name: "Bing",
|
||||
url: "https://www.bing.com/search?q=",
|
||||
icon: SiMicrosoftbing,
|
||||
},
|
||||
custom: {
|
||||
name: "Custom",
|
||||
url: false,
|
||||
icon: FiSearch,
|
||||
},
|
||||
};
|
||||
|
||||
export default function Search({ options }) {
|
||||
const provider = providers[options.provider];
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
if (!provider) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
const q = encodeURIComponent(query);
|
||||
|
||||
if (provider.url) {
|
||||
window.open(`${provider.url}${q}`, options.target || "_blank");
|
||||
} else {
|
||||
window.open(`${options.url}${q}`, options.target || "_blank");
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.target.reset();
|
||||
setQuery("");
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex-col relative h-8 my-4 min-w-full md:min-w-fit grow" onSubmit={handleSubmit}>
|
||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-theme-200"></div>
|
||||
<input
|
||||
type="search"
|
||||
autoFocus
|
||||
className={`overflow-hidden w-full placeholder-theme-900 text-xs text-theme-900 bg-theme-50 rounded-md border border-theme-300 focus:ring-theme-500 focus:border-theme-500 dark:bg-theme-800 dark:border-theme-600 dark:placeholder-theme-400 dark:text-white dark:focus:ring-theme-500 dark:focus:border-theme-500 h-full`}
|
||||
placeholder="Search..."
|
||||
onChange={(s) => setQuery(s.currentTarget.value)}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="text-white absolute right-0.5 bottom-0.5 bg-theme-700 hover:bg-theme-800 border-1 focus:ring-2 focus:ring-theme-300 font-medium rounded-r-md text-sm px-4 py-2 dark:bg-theme-600 dark:hover:bg-theme-700 dark:focus:ring-theme-500"
|
||||
>
|
||||
<provider.icon className="text-theme-800 dark:text-theme-200 w-3 h-3" />
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export default function WeatherApi({ options }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />
|
||||
|
||||
@@ -15,10 +15,23 @@ export default async function handler(req, res) {
|
||||
return {
|
||||
name: Object.keys(group)[0],
|
||||
services: group[Object.keys(group)[0]].map((entries) => {
|
||||
return {
|
||||
const { widget, ...service } = entries[Object.keys(entries)[0]];
|
||||
let res = {
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]],
|
||||
...service,
|
||||
};
|
||||
|
||||
if (widget) {
|
||||
const { type } = widget;
|
||||
|
||||
res.widget = {
|
||||
type: type,
|
||||
service_group: Object.keys(group)[0],
|
||||
service_name: Object.keys(entries)[0],
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
37
src/pages/api/services/proxy.js
Normal file
37
src/pages/api/services/proxy.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import genericProxyHandler from "utils/proxies/generic";
|
||||
import credentialedProxyHandler from "utils/proxies/credentialed";
|
||||
import rutorrentProxyHandler from "utils/proxies/rutorrent";
|
||||
import nzbgetProxyHandler from "utils/proxies/nzbget";
|
||||
import npmProxyHandler from "utils/proxies/npm";
|
||||
|
||||
const serviceProxyHandlers = {
|
||||
// uses query param auth
|
||||
emby: genericProxyHandler,
|
||||
jellyfin: genericProxyHandler,
|
||||
pihole: genericProxyHandler,
|
||||
radarr: genericProxyHandler,
|
||||
sonarr: genericProxyHandler,
|
||||
speedtest: genericProxyHandler,
|
||||
tautulli: genericProxyHandler,
|
||||
traefik: genericProxyHandler,
|
||||
// uses X-API-Key header auth
|
||||
portainer: credentialedProxyHandler,
|
||||
jellyseerr: credentialedProxyHandler,
|
||||
ombi: credentialedProxyHandler,
|
||||
// super specific handlers
|
||||
rutorrent: rutorrentProxyHandler,
|
||||
nzbget: nzbgetProxyHandler,
|
||||
npm: npmProxyHandler,
|
||||
};
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { type } = req.query;
|
||||
|
||||
const serviceProxyHandler = serviceProxyHandlers[type];
|
||||
|
||||
if (serviceProxyHandler) {
|
||||
return serviceProxyHandler(req, res);
|
||||
}
|
||||
|
||||
res.status(403).json({ error: "Unkown proxy service type" });
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import RuTorrent from "rutorrent-promise";
|
||||
|
||||
// TODO: Remove the 3rd party dependency once I figure out how to
|
||||
// call this myself with fetch. Just need to destruct the package.
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { url, username, password } = req.query;
|
||||
|
||||
const constructedUrl = new URL(url);
|
||||
|
||||
const rutorrent = new RuTorrent({
|
||||
host: constructedUrl.hostname, // default: localhost
|
||||
port: constructedUrl.port, // default: 80
|
||||
path: constructedUrl.pathname, // default: /rutorrent
|
||||
ssl: constructedUrl.protocol === "https:", // default: false
|
||||
username: username, // default: none
|
||||
password: password, // default: none
|
||||
});
|
||||
|
||||
const data = await rutorrent.get(["d.get_down_rate", "d.get_up_rate", "d.get_state"]);
|
||||
|
||||
res.status(200).send(data);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import ServicesGroup from "components/services/group";
|
||||
import BookmarksGroup from "components/bookmarks/group";
|
||||
import Widget from "components/widget";
|
||||
import { ColorProvider } from "utils/color-context";
|
||||
import Search from "components/widgets/search/search";
|
||||
|
||||
const ThemeToggle = dynamic(() => import("components/theme-toggle"), {
|
||||
ssr: false,
|
||||
@@ -17,7 +18,8 @@ const ColorToggle = dynamic(() => import("components/color-toggle"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather"];
|
||||
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search"];
|
||||
const expandedWidgets = ["search"];
|
||||
|
||||
export default function Home() {
|
||||
const { data: services, error: servicesError } = useSWR("/api/services");
|
||||
@@ -31,7 +33,7 @@ export default function Home() {
|
||||
<title>Welcome</title>
|
||||
</Head>
|
||||
<div className="w-full container m-auto flex flex-col h-screen justify-between">
|
||||
<div className="flex flex-wrap space-x-4 m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200">
|
||||
<div className="flex flex-row flex-wrap space-x-0 sm:space-x-4 m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between md:justify-start">
|
||||
{widgets && (
|
||||
<>
|
||||
{widgets
|
||||
@@ -39,12 +41,14 @@ export default function Home() {
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
))}
|
||||
<div className="grow"></div>
|
||||
{widgets
|
||||
.filter((widget) => rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
))}
|
||||
|
||||
<div className="flex flex-wrap basis-full space-x-0 sm:space-x-4 grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
|
||||
{widgets
|
||||
.filter((widget) => rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,15 +3,20 @@
|
||||
|
||||
- My First Group:
|
||||
- My First Service:
|
||||
href: http://localhosdt/
|
||||
href: http://localhost/
|
||||
description: Homepage is awesome
|
||||
# widget:
|
||||
# type: npm # npm for NGINX Proxy Manager
|
||||
# url: http://localhost # no slash at the end
|
||||
# username: email@example.com # your email
|
||||
# password: secretpassword # your password
|
||||
|
||||
- My Second Group:
|
||||
- My Second Service:
|
||||
href: http://localhosdt/
|
||||
href: http://localhost/
|
||||
description: Homepage is the best
|
||||
|
||||
- My Third Group:
|
||||
- My Third Service:
|
||||
href: http://localhosdt/
|
||||
href: http://localhost/
|
||||
description: Homepage is 😎
|
||||
|
||||
@@ -5,3 +5,9 @@
|
||||
cpu: true
|
||||
memory: true
|
||||
disk: /
|
||||
# - search: # Searchbar in widgets area
|
||||
# provider: custom # Can be google, duckduckgo, bing or custom.
|
||||
# target: _blank # Can be _blank, _top, _self or _parent.
|
||||
# customdata:
|
||||
# url: https://startpage.com/search?q= # Required for custom provider. Remember to add the q param as per your provider.
|
||||
# abbr: G # Can be omitted. Only the first 2 characters will be considered.
|
||||
|
||||
35
src/utils/api-helpers.js
Normal file
35
src/utils/api-helpers.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const formats = {
|
||||
emby: `{url}/emby/{endpoint}?api_key={key}`,
|
||||
jellyfin: `{url}/emby/{endpoint}?api_key={key}`,
|
||||
pihole: `{url}/admin/{endpoint}`,
|
||||
radarr: `{url}/api/v3/{endpoint}?apikey={key}`,
|
||||
sonarr: `{url}/api/v3/{endpoint}?apikey={key}`,
|
||||
speedtest: `{url}/api/{endpoint}`,
|
||||
tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`,
|
||||
traefik: `{url}/api/{endpoint}`,
|
||||
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
|
||||
rutorrent: `{url}/plugins/httprpc/action.php`,
|
||||
jellyseerr: `{url}/api/v1/{endpoint}`,
|
||||
ombi: `{url}/api/v1/{endpoint}`,
|
||||
npm: `{url}/api/{endpoint}`,
|
||||
};
|
||||
|
||||
export function formatApiCall(api, args) {
|
||||
const match = /\{.*?\}/g;
|
||||
const replace = (match) => {
|
||||
const key = match.replace(/\{|\}/g, "");
|
||||
return args[key];
|
||||
};
|
||||
|
||||
return formats[api].replace(match, replace);
|
||||
}
|
||||
|
||||
export function formatApiUrl(widget, endpoint) {
|
||||
const params = new URLSearchParams({
|
||||
type: widget.type,
|
||||
group: widget.service_group,
|
||||
service: widget.service_name,
|
||||
endpoint,
|
||||
});
|
||||
return `/api/services/proxy?${params.toString()}`;
|
||||
}
|
||||
@@ -44,3 +44,20 @@ export function httpRequest(url, params) {
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
export function httpProxy(url, params = {}) {
|
||||
const constructedUrl = new URL(url);
|
||||
|
||||
if (constructedUrl.protocol === "https:") {
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
return httpsRequest(constructedUrl, {
|
||||
agent: httpsAgent,
|
||||
...params,
|
||||
});
|
||||
} else {
|
||||
return httpRequest(constructedUrl, params);
|
||||
}
|
||||
}
|
||||
|
||||
28
src/utils/proxies/credentialed.js
Normal file
28
src/utils/proxies/credentialed.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getServiceWidget } from "utils/service-helpers";
|
||||
import { formatApiCall } from "utils/api-helpers";
|
||||
import { httpProxy } from "utils/http";
|
||||
|
||||
export default async function credentialedProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
withCredentials: true,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"X-API-Key": `${widget.key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
21
src/utils/proxies/generic.js
Normal file
21
src/utils/proxies/generic.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getServiceWidget } from "utils/service-helpers";
|
||||
import { formatApiCall } from "utils/api-helpers";
|
||||
import { httpProxy } from "utils/http";
|
||||
|
||||
export default async function genericProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||
const [status, contentType, data] = await httpProxy(url);
|
||||
|
||||
res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
37
src/utils/proxies/npm.js
Normal file
37
src/utils/proxies/npm.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getServiceWidget } from "utils/service-helpers";
|
||||
import { formatApiCall } from "utils/api-helpers";
|
||||
|
||||
export default async function npmProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
|
||||
|
||||
const loginUrl = `${widget.url}/api/tokens`;
|
||||
const body = { identity: widget.username, secret: widget.password };
|
||||
|
||||
const authResponse = await fetch(loginUrl, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((response) => response.json());
|
||||
|
||||
const apiResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + authResponse.token,
|
||||
},
|
||||
}).then((response) => response.json());
|
||||
|
||||
return res.send(apiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
39
src/utils/proxies/nzbget.js
Normal file
39
src/utils/proxies/nzbget.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { JSONRPCClient } from "json-rpc-2.0";
|
||||
import { getServiceWidget } from "utils/service-helpers";
|
||||
|
||||
export default async function nzbgetProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const constructedUrl = new URL(widget.url);
|
||||
constructedUrl.pathname = "jsonrpc";
|
||||
|
||||
const authorization = Buffer.from(`${widget.username}:${widget.password}`).toString("base64");
|
||||
|
||||
const client = new JSONRPCClient((jsonRPCRequest) =>
|
||||
fetch(constructedUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: `Basic ${authorization}`,
|
||||
},
|
||||
body: JSON.stringify(jsonRPCRequest),
|
||||
}).then(async (response) => {
|
||||
if (response.status === 200) {
|
||||
const jsonRPCResponse = await response.json();
|
||||
return client.receive(jsonRPCResponse);
|
||||
} else if (jsonRPCRequest.id !== undefined) {
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return res.send(await client.request(endpoint));
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
30
src/utils/proxies/rutorrent.js
Normal file
30
src/utils/proxies/rutorrent.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import RuTorrent from "rutorrent-promise";
|
||||
|
||||
import { getServiceWidget } from "utils/service-helpers";
|
||||
|
||||
export default async function rutorrentProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (widget) {
|
||||
const constructedUrl = new URL(widget.url);
|
||||
|
||||
const rutorrent = new RuTorrent({
|
||||
host: constructedUrl.hostname,
|
||||
port: constructedUrl.port,
|
||||
path: constructedUrl.pathname,
|
||||
ssl: constructedUrl.protocol === "https:",
|
||||
username: widget.username,
|
||||
password: widget.password,
|
||||
});
|
||||
|
||||
const data = await rutorrent.get(["d.get_down_rate", "d.get_up_rate", "d.get_state"]);
|
||||
|
||||
return res.status(200).send(data);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
33
src/utils/service-helpers.js
Normal file
33
src/utils/service-helpers.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
export async function getServiceWidget(group, service) {
|
||||
const servicesYaml = path.join(process.cwd(), "config", "services.yaml");
|
||||
const fileContents = await fs.readFile(servicesYaml, "utf8");
|
||||
const services = yaml.load(fileContents);
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
const servicesArray = services.map((group) => {
|
||||
return {
|
||||
name: Object.keys(group)[0],
|
||||
services: group[Object.keys(group)[0]].map((entries) => {
|
||||
return {
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]],
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const serviceGroup = servicesArray.find((g) => g.name === group);
|
||||
if (serviceGroup) {
|
||||
const serviceEntry = serviceGroup.services.find((s) => s.name === service);
|
||||
if (serviceEntry) {
|
||||
const { widget } = serviceEntry;
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user