Compare commits

...

32 Commits

Author SHA1 Message Date
Ben Phelps
08afa0b747 check that content type exists before setting it 2022-09-05 10:08:02 +03:00
Ben Phelps
bad436b858 fix jellyfin widget api calls 2022-09-05 08:19:50 +03:00
Ben Phelps
7f0345a56a remove lodash, update packages 2022-09-04 21:59:20 +03:00
Ben Phelps
97bf174b78 refactor widget api design
this passes all widget API calls through the backend, with a pluggable design and reusable API handlers
2022-09-04 21:58:42 +03:00
Ben Phelps
975f79f6cc add Tautulli widget 2022-09-03 15:36:59 +03:00
Ben Phelps
e72efe7fd0 fix movies count in radarr widget 2022-09-03 14:55:16 +03:00
Ben Phelps
d94b3e829d update readme attributions 2022-09-03 13:15:17 +03:00
Ben Phelps
10c63939dc cleanup search widget 2022-09-03 13:12:09 +03:00
Ben Phelps
972ede9249 fix mismatched labels 2022-09-03 12:40:15 +03:00
Ben Phelps
8f001ad88a tweak mobile layouts for widgets 2022-09-03 12:40:04 +03:00
Ben Phelps
229c5dac59 Merge pull request #45 from aidenpwnz/main
Feature/Searchbar
2022-09-02 21:15:36 +03:00
aidenpwnz
0622395ec7 FIX: leftover 2022-09-02 16:54:42 +02:00
Luca Pellegrino
66073ed460 Merge pull request #1 from aidenpwnz/feature/npm
Feature/npm
2022-09-02 16:52:46 +02:00
aidenpwnz
533f40b536 Merge remote-tracking branch 'origin' into feature/npm 2022-09-02 16:49:46 +02:00
aidenpwnz
13afe82fa5 FEAT: NGINX Proxy Manager 2022-09-02 16:48:28 +02:00
Luca Pellegrino
10c27dfd84 Merge branch 'benphelps:main' into main 2022-09-02 12:16:25 +02:00
aidenpwnz
057d5eca8f FEAT: NGINX Proxy Manager 2022-09-02 12:13:15 +02:00
Ben Phelps
e89f3668a9 Merge pull request #46 from quod/main
Fix typos
2022-09-02 11:03:18 +03:00
Ben Phelps
c46306fc1d allow services to be display only 2022-09-02 10:55:19 +03:00
Benjamin Carson
76d534583b Fix typos 2022-09-01 17:24:08 -05:00
aidenpwnz
7b4f360a5e FIX: minor issue with abbr 2022-09-01 19:30:15 +02:00
aidenpwnz
992b18c9de FEAT: Searchbar 2022-09-01 19:21:44 +02:00
aidenpwnz
6291a5422a FIX: overflows 2022-09-01 19:13:51 +02:00
aidenpwnz
4581c4eeb0 FEAT: Searchbar || FIX: spacings, overflows 2022-09-01 19:11:45 +02:00
Ben Phelps
d6d93e3c03 add Jellyseerr attribution 2022-08-29 18:41:36 +03:00
Ben Phelps
f40ca1e25c Merge pull request #34 from ilusi0n/jellyseerr-integration
Implement Jellyseerr integration
2022-08-28 14:03:18 +03:00
ilusi0n
2d764ce59b update readme 2022-08-28 11:27:45 +01:00
ilusi0n
a1841f26bb merge with main 2022-08-28 11:26:29 +01:00
ilusi0n
c4ab3eb992 add jellyseerr integration 2022-08-28 11:22:43 +01:00
Ben Phelps
617cbcaee1 fix docker widget when network_mode is host 2022-08-28 10:15:25 +03:00
Ben Phelps
a9a28e14df switch to using the latest tag 2022-08-28 00:53:23 +03:00
Ben Phelps
169c64f687 attempt to add latest docker tag 2022-08-28 00:17:18 +03:00
39 changed files with 687 additions and 304 deletions

View File

@@ -79,6 +79,8 @@ jobs:
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=auto
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action

View File

@@ -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

View File

@@ -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
View File

@@ -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'

View File

@@ -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>
)}

View File

@@ -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 }) {

View File

@@ -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>
);
}

View File

@@ -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`} />;

View 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>
);
}

View 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>
);
}

View File

@@ -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" />;

View File

@@ -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" />;

View File

@@ -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" />;

View File

@@ -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" />;

View File

@@ -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>
);
}

View File

@@ -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" />;

View File

@@ -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" />;

View File

@@ -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>
);

View 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>
);
}

View File

@@ -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" />;

View File

@@ -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 }) {

View File

@@ -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

View File

@@ -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">

View 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>
);
}

View File

@@ -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"} />

View File

@@ -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;
}),
};
});

View 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" });
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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 😎

View File

@@ -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
View 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()}`;
}

View File

@@ -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);
}
}

View 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",
},
});
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data);
}
}
return res.status(400).json({ error: "Invalid proxy service type" });
}

View 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);
if (contentType) 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
View 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" });
}

View 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" });
}

View 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" });
}

View 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;
}