mirror of
https://github.com/gethomepage/homepage.git
synced 2026-01-09 18:02:08 +08:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ea279856f | ||
|
|
c7d1c05e6e | ||
|
|
48a09e5a99 | ||
|
|
dced918804 | ||
|
|
9dd259654a | ||
|
|
8f644ef7a4 | ||
|
|
61e82e2b2c | ||
|
|
17ec656b9b | ||
|
|
9669c463da | ||
|
|
02dd6d08b4 | ||
|
|
a8bd84467c | ||
|
|
770d52cbae | ||
|
|
73a200994c | ||
|
|
c1d14e3b16 | ||
|
|
e7215feee1 | ||
|
|
be4d02f5e9 | ||
|
|
92e47c45f9 | ||
|
|
73b44c3a2e | ||
|
|
bf666b3ec4 | ||
|
|
69a67f450d | ||
|
|
73e1d3dc20 | ||
|
|
95f811e4c1 | ||
|
|
65447f2b97 | ||
|
|
b6dd2975e7 | ||
|
|
911bd31c14 | ||
|
|
072e29025e | ||
|
|
7bb8274b95 | ||
|
|
280c8b0147 | ||
|
|
46a29ea7d6 | ||
|
|
b259ecf870 | ||
|
|
c1a55ff6d3 | ||
|
|
7c39cd8960 | ||
|
|
2bdd9eaa88 | ||
|
|
8e2ff61f1c | ||
|
|
99b70f96e4 | ||
|
|
802fe0f721 | ||
|
|
080bc44a6f | ||
|
|
321efd08cc | ||
|
|
37fed0ce01 | ||
|
|
778261f67e | ||
|
|
c95422b682 | ||
|
|
248c18d978 | ||
|
|
add17be5ba | ||
|
|
c7b5ec33a8 | ||
|
|
0625ce2bb9 | ||
|
|
860ae7f0f7 | ||
|
|
094b916c9e | ||
|
|
f50130971d | ||
|
|
05183a8861 | ||
|
|
92ffb8d082 | ||
|
|
9d79e7e72d | ||
|
|
6b201028cd | ||
|
|
46e0732bd1 | ||
|
|
375e513246 | ||
|
|
2c80dc63d1 | ||
|
|
d8a4d1ef5d | ||
|
|
2e4e998654 | ||
|
|
5b66941774 | ||
|
|
6f34ca50e0 | ||
|
|
59af5e1eb8 | ||
|
|
b34a44cf9f | ||
|
|
45c92ef2f7 | ||
|
|
8854fcdb9b | ||
|
|
8299c6ce8d | ||
|
|
d5861ea52b | ||
|
|
855665689c | ||
|
|
a4795c21dc | ||
|
|
2e2aeef77b | ||
|
|
5198b056cc | ||
|
|
3c23e59a70 | ||
|
|
2440da8e08 | ||
|
|
91795f2d07 | ||
|
|
9f55ec9a63 | ||
|
|
efeaa995e2 | ||
|
|
1524a6a6cf | ||
|
|
702ede8b9b | ||
|
|
7e6fcd8418 | ||
|
|
1f006a4093 | ||
|
|
d4a5245d1a | ||
|
|
60519a2881 | ||
|
|
182b66b53f | ||
|
|
4b110cf7a8 | ||
|
|
dc0744f9d7 | ||
|
|
90cd18735a | ||
|
|
494b2ebd08 | ||
|
|
797b966267 | ||
|
|
b13754f174 | ||
|
|
c9e7845154 | ||
|
|
a6f7c48cc3 | ||
|
|
7874eb1467 | ||
|
|
69626157ed | ||
|
|
6dc0eac9f8 | ||
|
|
a6fc171539 | ||
|
|
a8a715dd30 | ||
|
|
4ac6e43343 | ||
|
|
13ee38bd12 | ||
|
|
d186ff16dd | ||
|
|
49a3f3e249 | ||
|
|
2d14230763 | ||
|
|
ce86d06006 | ||
|
|
c1d476997b | ||
|
|
a79e9cd342 | ||
|
|
260201c2b4 | ||
|
|
85df467fdb | ||
|
|
04da8f3925 | ||
|
|
86b12debc5 | ||
|
|
69c9a449b1 | ||
|
|
0c20040f57 | ||
|
|
dedd725fae | ||
|
|
fe7fa5c060 | ||
|
|
d2dc51d49c | ||
|
|
d26ae30fa6 | ||
|
|
fbac27f504 | ||
|
|
d1b6dad14d | ||
|
|
fe1064b173 | ||
|
|
42da3eca28 | ||
|
|
7ee3113d8a | ||
|
|
2bd9c8eddc | ||
|
|
1249ecaa68 | ||
|
|
ba428cf3ae | ||
|
|
f06be8ca94 | ||
|
|
e03822df6e | ||
|
|
ad1d1e751d | ||
|
|
ac4dcd3222 | ||
|
|
952f0295cc | ||
|
|
5ba75bc62d | ||
|
|
b9b9449cd3 |
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.next": true,
|
||||||
|
"**/node_modules": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
A modern <em>(fully static, fast)</em>, secure <em>(fully proxied)</em>, highly customizable application dashboard with integrations for more than 25 services and translations for 15 languages. Easily configured via YAML files (or discovery via docker labels).
|
A modern <em>(fully static, fast)</em>, secure <em>(fully proxied)</em>, highly customizable application dashboard with integrations for more than 25 services and translations for over 15 languages. Easily configured via YAML files (or discovery via docker labels).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
- Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6
|
- Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6
|
||||||
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
|
- Supports all Raspberry Pi's, most SBCs & Apple Silicon
|
||||||
- Full i18n support with automatic language detection
|
- Full i18n support with automatic language detection
|
||||||
- Translantions for Chinese, Dutch, Finnish, French, German, Hebrew, Hungarian, Norwegian Bokmål, Polish, Portuguese, Portuguese (Brazil), Romainian, Russian, Spanish, Swedish and Yue
|
- Translantions for Catalan, Chinese, Dutch, Finnish, French, German, Hebrew, Hungarian, Norwegian Bokmål, Polish, Portuguese, Portuguese (Brazil), Romainian, Russian, Spanish, Swedish and Yue
|
||||||
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
|
- Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/)
|
||||||
- Service & Web Bookmarks
|
- Service & Web Bookmarks
|
||||||
- Docker Integration
|
- Docker Integration
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
- Service Integration
|
- Service Integration
|
||||||
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
|
- Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
|
||||||
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission, qBittorrent
|
- Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission, qBittorrent
|
||||||
- Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify, Syncthing Relay Server, Authentic
|
- Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify, Syncthing Relay Server, Authentic, Proxmox
|
||||||
- Information Providers
|
- Information Providers
|
||||||
- Coin Market Cap, Mastodon
|
- Coin Market Cap, Mastodon
|
||||||
- Information & Utility Widgets
|
- Information & Utility Widgets
|
||||||
@@ -152,7 +152,7 @@ Huge thanks to the all the contributors who have helped make this project what i
|
|||||||
- [DevPGSV](https://github.com/benphelps/homepage/commits?author=DevPGSV) - Syncthing Relay Server & Mastodon widgets
|
- [DevPGSV](https://github.com/benphelps/homepage/commits?author=DevPGSV) - Syncthing Relay Server & Mastodon widgets
|
||||||
- [ilusi0n](https://github.com/benphelps/homepage/commits?author=ilusi0n) - Jellyseerr Integration
|
- [ilusi0n](https://github.com/benphelps/homepage/commits?author=ilusi0n) - Jellyseerr Integration
|
||||||
- [ItsJustMeChris](https://github.com/benphelps/homepage/commits?author=ItsJustMeChris) - Coin Market Cap Widget
|
- [ItsJustMeChris](https://github.com/benphelps/homepage/commits?author=ItsJustMeChris) - Coin Market Cap Widget
|
||||||
- [JazzFisch](https://github.com/benphelps/homepage/commits?author=JazzFisch) - Readarr, Bazarr, Lidarr, SABnzbd, Transmission, qBittorrent Integrations & countless more improvements
|
- [JazzFisch](https://github.com/benphelps/homepage/commits?author=JazzFisch) - Readarr, Bazarr, Lidarr, SABnzbd, Transmission, qBittorrent, Proxmox Integrations & countless more improvements
|
||||||
- [josways](https://github.com/benphelps/homepage/commits?author=josways) - Baidu search provider
|
- [josways](https://github.com/benphelps/homepage/commits?author=josways) - Baidu search provider
|
||||||
- [mauricio-kalil](https://github.com/benphelps/homepage/commits?author=mauricio-kalil) - Portuguese (Brazil)
|
- [mauricio-kalil](https://github.com/benphelps/homepage/commits?author=mauricio-kalil) - Portuguese (Brazil)
|
||||||
- [modem7](https://github.com/benphelps/homepage/commits?author=modem7) - Impvoed Docker Image
|
- [modem7](https://github.com/benphelps/homepage/commits?author=modem7) - Impvoed Docker Image
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
"shvl": "^3.0.0",
|
"shvl": "^3.0.0",
|
||||||
"swr": "^1.3.0",
|
"swr": "^1.3.0",
|
||||||
"tough-cookie": "^4.1.2",
|
"tough-cookie": "^4.1.2",
|
||||||
"winston": "^3.8.2"
|
"winston": "^3.8.2",
|
||||||
|
"xml-js": "^1.6.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
|
|||||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -40,6 +40,7 @@ specifiers:
|
|||||||
tough-cookie: ^4.1.2
|
tough-cookie: ^4.1.2
|
||||||
typescript: ^4.8.3
|
typescript: ^4.8.3
|
||||||
winston: ^3.8.2
|
winston: ^3.8.2
|
||||||
|
xml-js: ^1.6.11
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@headlessui/react': 1.7.2_biqbaboplfbrettd7655fr4n2y
|
'@headlessui/react': 1.7.2_biqbaboplfbrettd7655fr4n2y
|
||||||
@@ -65,6 +66,7 @@ dependencies:
|
|||||||
swr: 1.3.0_react@18.2.0
|
swr: 1.3.0_react@18.2.0
|
||||||
tough-cookie: 4.1.2
|
tough-cookie: 4.1.2
|
||||||
winston: 3.8.2
|
winston: 3.8.2
|
||||||
|
xml-js: 1.6.11
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
|
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
|
||||||
@@ -2554,6 +2556,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/sax/1.2.4:
|
||||||
|
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/scheduler/0.23.0:
|
/scheduler/0.23.0:
|
||||||
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
|
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3000,6 +3006,13 @@ packages:
|
|||||||
/wrappy/1.0.2:
|
/wrappy/1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
/xml-js/1.6.11:
|
||||||
|
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
sax: 1.2.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/xtend/4.0.2:
|
/xtend/4.0.2:
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
|
|||||||
205
public/locales/bg/common.json
Normal file
205
public/locales/bg/common.json
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
{
|
||||||
|
"sabnzbd": {
|
||||||
|
"queue": "Опашка",
|
||||||
|
"timeleft": "Оставащо Време",
|
||||||
|
"rate": "Rate"
|
||||||
|
},
|
||||||
|
"rutorrent": {
|
||||||
|
"active": "Акитивен",
|
||||||
|
"upload": "Споделяне",
|
||||||
|
"download": "Сваляне"
|
||||||
|
},
|
||||||
|
"widget": {
|
||||||
|
"missing_type": "Липсваща приставка: {{type}}",
|
||||||
|
"api_error": "API Грешка",
|
||||||
|
"status": "Статус"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"current": "Текущо местоположение",
|
||||||
|
"allow": "Разреши",
|
||||||
|
"updating": "Обновяване",
|
||||||
|
"wait": "Моля изчакайте"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Търсене…"
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"total": "Общо",
|
||||||
|
"free": "Свободни",
|
||||||
|
"used": "Заети",
|
||||||
|
"load": "Натоварване"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Потребители",
|
||||||
|
"uptime": "Активен от",
|
||||||
|
"days": "Дни",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Потребители",
|
||||||
|
"wlan_users": "WLAN Потребители",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Моля изчакайте"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"offline": "Изключен",
|
||||||
|
"rx": "RX",
|
||||||
|
"tx": "TX",
|
||||||
|
"mem": "MEM",
|
||||||
|
"cpu": "CPU"
|
||||||
|
},
|
||||||
|
"emby": {
|
||||||
|
"playing": "Възпроизвежда",
|
||||||
|
"transcoding": "Конвертира",
|
||||||
|
"bitrate": "Bitrate",
|
||||||
|
"no_active": "Няма активни потоци"
|
||||||
|
},
|
||||||
|
"tautulli": {
|
||||||
|
"playing": "Възпроизвежда",
|
||||||
|
"transcoding": "Конвертира",
|
||||||
|
"bitrate": "Честота",
|
||||||
|
"no_active": "Няма активни потоци"
|
||||||
|
},
|
||||||
|
"nzbget": {
|
||||||
|
"rate": "Rate",
|
||||||
|
"remaining": "Остава",
|
||||||
|
"downloaded": "Изтеглени"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Активни Потоци",
|
||||||
|
"movies": "Филми",
|
||||||
|
"tv": "Сериали"
|
||||||
|
},
|
||||||
|
"transmission": {
|
||||||
|
"download": "Сваляне",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "Download",
|
||||||
|
"upload": "Upload",
|
||||||
|
"leech": "Leech",
|
||||||
|
"seed": "Seed"
|
||||||
|
},
|
||||||
|
"sonarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"series": "Series"
|
||||||
|
},
|
||||||
|
"radarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"movies": "Movies"
|
||||||
|
},
|
||||||
|
"lidarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"albums": "Albums"
|
||||||
|
},
|
||||||
|
"readarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"books": "Books"
|
||||||
|
},
|
||||||
|
"bazarr": {
|
||||||
|
"missingEpisodes": "Missing Episodes",
|
||||||
|
"missingMovies": "Missing Movies"
|
||||||
|
},
|
||||||
|
"ombi": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"approved": "Approved",
|
||||||
|
"available": "Available"
|
||||||
|
},
|
||||||
|
"jellyseerr": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"approved": "Approved",
|
||||||
|
"available": "Available"
|
||||||
|
},
|
||||||
|
"overseerr": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"approved": "Approved",
|
||||||
|
"available": "Available"
|
||||||
|
},
|
||||||
|
"pihole": {
|
||||||
|
"queries": "Queries",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"gravity": "Gravity"
|
||||||
|
},
|
||||||
|
"adguard": {
|
||||||
|
"queries": "Queries",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"filtered": "Filtered",
|
||||||
|
"latency": "Latency"
|
||||||
|
},
|
||||||
|
"speedtest": {
|
||||||
|
"upload": "Upload",
|
||||||
|
"download": "Download",
|
||||||
|
"ping": "Ping"
|
||||||
|
},
|
||||||
|
"portainer": {
|
||||||
|
"running": "Running",
|
||||||
|
"stopped": "Stopped",
|
||||||
|
"total": "Total"
|
||||||
|
},
|
||||||
|
"traefik": {
|
||||||
|
"routers": "Routers",
|
||||||
|
"services": "Services",
|
||||||
|
"middleware": "Middleware"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"total": "Total"
|
||||||
|
},
|
||||||
|
"coinmarketcap": {
|
||||||
|
"configure": "Configure one or more crypto currencies to track",
|
||||||
|
"1hour": "1 Hour",
|
||||||
|
"1day": "1 Day",
|
||||||
|
"7days": "7 Days",
|
||||||
|
"30days": "30 Days"
|
||||||
|
},
|
||||||
|
"gotify": {
|
||||||
|
"apps": "Applications",
|
||||||
|
"clients": "Clients",
|
||||||
|
"messages": "Messages"
|
||||||
|
},
|
||||||
|
"prowlarr": {
|
||||||
|
"enableIndexers": "Indexers",
|
||||||
|
"numberOfGrabs": "Grabs",
|
||||||
|
"numberOfQueries": "Queries",
|
||||||
|
"numberOfFailGrabs": "Fail Grabs",
|
||||||
|
"numberOfFailQueries": "Fail Queries"
|
||||||
|
},
|
||||||
|
"authentik": {
|
||||||
|
"loginsLast24H": "Logins (24h)",
|
||||||
|
"users": "Users",
|
||||||
|
"failedLoginsLast24H": "Failed Logins (24h)"
|
||||||
|
},
|
||||||
|
"jackett": {
|
||||||
|
"configured": "Configured",
|
||||||
|
"errored": "Errored"
|
||||||
|
},
|
||||||
|
"strelaysrv": {
|
||||||
|
"numActiveSessions": "Sessions",
|
||||||
|
"numConnections": "Connections",
|
||||||
|
"dataRelayed": "Relayed",
|
||||||
|
"transferRate": "Rate"
|
||||||
|
},
|
||||||
|
"mastodon": {
|
||||||
|
"user_count": "Users",
|
||||||
|
"status_count": "Posts",
|
||||||
|
"domain_count": "Domains"
|
||||||
|
},
|
||||||
|
"proxmox": {
|
||||||
|
"mem": "MEM",
|
||||||
|
"cpu": "CPU",
|
||||||
|
"lxc": "LXC",
|
||||||
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,8 +177,29 @@
|
|||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"vms": "VMs",
|
"vms": "VMs",
|
||||||
"mem": "MEM",
|
"mem": "Memòria",
|
||||||
"cpu": "CPU",
|
"cpu": "Processador",
|
||||||
"lxc": "LXC"
|
"lxc": "LXC"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,17 @@
|
|||||||
"used": "Used",
|
"used": "Used",
|
||||||
"load": "Load"
|
"load": "Load"
|
||||||
},
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "RX",
|
"rx": "RX",
|
||||||
"tx": "TX",
|
"tx": "TX",
|
||||||
@@ -55,6 +66,11 @@
|
|||||||
"remaining": "Remaining",
|
"remaining": "Remaining",
|
||||||
"downloaded": "Downloaded"
|
"downloaded": "Downloaded"
|
||||||
},
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
"sabnzbd": {
|
"sabnzbd": {
|
||||||
"rate": "Rate",
|
"rate": "Rate",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
@@ -191,5 +207,10 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,9 +176,30 @@
|
|||||||
"failedLoginsLast24H": "Inicios de sesión fallidos (24h)"
|
"failedLoginsLast24H": "Inicios de sesión fallidos (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "Memoria",
|
||||||
"cpu": "CPU",
|
"cpu": "Procesador",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"up": "UP",
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,9 +176,30 @@
|
|||||||
"failedLoginsLast24H": "Epäonnistuneita kirjautumisia (24h)"
|
"failedLoginsLast24H": "Epäonnistuneita kirjautumisia (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "RAM",
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VKt"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"wait": "Please wait",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"wait": "Please wait",
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,9 +176,30 @@
|
|||||||
"failedLoginsLast24H": "Cnx. échouées (24h)"
|
"failedLoginsLast24H": "Cnx. échouées (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "Mém",
|
||||||
"cpu": "CPU",
|
"cpu": "Cpu",
|
||||||
"lxc": "LXC",
|
"lxc": "LxC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Utilisateurs",
|
||||||
|
"uptime": "Disponibilité du système",
|
||||||
|
"days": "Jours",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "Utilisateurs LAN",
|
||||||
|
"wlan_users": "Utilisateurs WLAN",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Merci de patienter"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Flux actif",
|
||||||
|
"movies": "Films",
|
||||||
|
"tv": "Séries TV"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wait": "Please wait",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,5 +191,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC"
|
"lxc": "LXC"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
205
public/locales/te/common.json
Normal file
205
public/locales/te/common.json
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
{
|
||||||
|
"readarr": {
|
||||||
|
"books": "పుస్తకాలు",
|
||||||
|
"wanted": "కావలెను",
|
||||||
|
"queued": "క్యూయూఎడ్"
|
||||||
|
},
|
||||||
|
"adguard": {
|
||||||
|
"blocked": "నిరోధించబడింది",
|
||||||
|
"filtered": "ఫిల్టర్ చేయబడింది",
|
||||||
|
"latency": "జాప్యం",
|
||||||
|
"queries": "ప్రశ్నలు"
|
||||||
|
},
|
||||||
|
"strelaysrv": {
|
||||||
|
"numActiveSessions": "సెషన్స్",
|
||||||
|
"numConnections": "కనెక్షన్లు",
|
||||||
|
"dataRelayed": "రెలయెడఁ",
|
||||||
|
"transferRate": "రేటు"
|
||||||
|
},
|
||||||
|
"widget": {
|
||||||
|
"missing_type": "విడ్జెట్ లేదు: {{type}}",
|
||||||
|
"api_error": "API లోపం",
|
||||||
|
"status": "హోదా"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"current": "ప్రస్తుత స్తలం",
|
||||||
|
"allow": "అనుమతించడానికి క్లిక్ చేయండి",
|
||||||
|
"updating": "నవీకరిస్తోంది",
|
||||||
|
"wait": "దయచేసి వేచి ఉండండి"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "వెతకండి…"
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"cpu": "సీపియూ",
|
||||||
|
"total": "మొత్తం",
|
||||||
|
"free": "మిగిలింది",
|
||||||
|
"used": "ఉపయోగించబడిన",
|
||||||
|
"load": "లోడ్"
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"rx": "RX",
|
||||||
|
"tx": "TX",
|
||||||
|
"mem": "MEM",
|
||||||
|
"cpu": "సీపియూ",
|
||||||
|
"offline": "ఆఫ్లైన్"
|
||||||
|
},
|
||||||
|
"emby": {
|
||||||
|
"playing": "ఆడుతున్నారు",
|
||||||
|
"transcoding": "ట్రాన్స్కోడింగ్",
|
||||||
|
"bitrate": "బిట్రేట్",
|
||||||
|
"no_active": "యాక్టివ్ స్ట్రీమ్లు లేవు"
|
||||||
|
},
|
||||||
|
"tautulli": {
|
||||||
|
"playing": "ఆడుతున్నారు",
|
||||||
|
"transcoding": "ట్రాన్స్కోడింగ్",
|
||||||
|
"bitrate": "బిట్రేట్",
|
||||||
|
"no_active": "యాక్టివ్ స్ట్రీమ్లు లేవు"
|
||||||
|
},
|
||||||
|
"nzbget": {
|
||||||
|
"rate": "రేట్",
|
||||||
|
"remaining": "మిగిలింది",
|
||||||
|
"downloaded": "డౌన్లోడ్ చేయబడింది"
|
||||||
|
},
|
||||||
|
"sabnzbd": {
|
||||||
|
"rate": "రేట్",
|
||||||
|
"queue": "వరుస",
|
||||||
|
"timeleft": "మిగిలి వున్న సమయం"
|
||||||
|
},
|
||||||
|
"rutorrent": {
|
||||||
|
"active": "చురుకుగా",
|
||||||
|
"upload": "అప్లోడ్",
|
||||||
|
"download": "డౌన్లోడ్"
|
||||||
|
},
|
||||||
|
"transmission": {
|
||||||
|
"download": "డౌన్లోడ్",
|
||||||
|
"upload": "అప్లోడ్",
|
||||||
|
"leech": "జలగ",
|
||||||
|
"seed": "సీడ్"
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"download": "డౌన్లోడ్",
|
||||||
|
"upload": "అప్లోడ్",
|
||||||
|
"leech": "లీచ్",
|
||||||
|
"seed": "సీడ్"
|
||||||
|
},
|
||||||
|
"sonarr": {
|
||||||
|
"wanted": "కావలెను",
|
||||||
|
"queued": "క్యూయూఎడ్",
|
||||||
|
"series": "సిరీస్"
|
||||||
|
},
|
||||||
|
"radarr": {
|
||||||
|
"wanted": "కావలెను",
|
||||||
|
"queued": "క్యూయూఎడ్",
|
||||||
|
"movies": "సినిమాలు"
|
||||||
|
},
|
||||||
|
"lidarr": {
|
||||||
|
"wanted": "కావలెను",
|
||||||
|
"queued": "క్యూయూఎడ్",
|
||||||
|
"albums": "ఆల్బములు"
|
||||||
|
},
|
||||||
|
"bazarr": {
|
||||||
|
"missingEpisodes": "ఎపిసోడ్లు లేవు",
|
||||||
|
"missingMovies": "సినిమాలు లేవు"
|
||||||
|
},
|
||||||
|
"ombi": {
|
||||||
|
"pending": "పెండింగ్",
|
||||||
|
"approved": "ఆమోదించబడింది",
|
||||||
|
"available": "అందుబాటులో వున్నవి"
|
||||||
|
},
|
||||||
|
"jellyseerr": {
|
||||||
|
"pending": "పెండింగ్",
|
||||||
|
"approved": "ఆమోదించబడింది",
|
||||||
|
"available": "అందుబాటులో"
|
||||||
|
},
|
||||||
|
"overseerr": {
|
||||||
|
"pending": "పెండింగ్",
|
||||||
|
"approved": "ఆమోదించబడింది",
|
||||||
|
"available": "అందుబాటులో"
|
||||||
|
},
|
||||||
|
"pihole": {
|
||||||
|
"queries": "ప్రశ్నలు",
|
||||||
|
"blocked": "నిరోధించబడింది",
|
||||||
|
"gravity": "గురుత్వాకర్షణ"
|
||||||
|
},
|
||||||
|
"speedtest": {
|
||||||
|
"upload": "అప్లోడ్",
|
||||||
|
"download": "డౌన్లోడ్",
|
||||||
|
"ping": "పింగ్"
|
||||||
|
},
|
||||||
|
"portainer": {
|
||||||
|
"running": "నడుస్తున్నవి",
|
||||||
|
"stopped": "ఆగిపోయినవి",
|
||||||
|
"total": "మొత్తం"
|
||||||
|
},
|
||||||
|
"traefik": {
|
||||||
|
"routers": "రౌటర్లు",
|
||||||
|
"services": "సేవలు",
|
||||||
|
"middleware": "మిడిల్వేర్"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"enabled": "ప్రారంభించబడింది",
|
||||||
|
"disabled": "డిసేబ్లెడ్",
|
||||||
|
"total": "మొత్తం"
|
||||||
|
},
|
||||||
|
"coinmarketcap": {
|
||||||
|
"configure": "ట్రాక్ చేయడానికి ఒకటి లేదా అంతకంటే ఎక్కువ క్రిప్టో కరెన్సీలను కాన్ఫిగర్ చేయండి",
|
||||||
|
"1hour": "1 గంట",
|
||||||
|
"1day": "1 రోజు",
|
||||||
|
"7days": "7 రోజులు",
|
||||||
|
"30days": "30 రోజులు"
|
||||||
|
},
|
||||||
|
"gotify": {
|
||||||
|
"apps": "అప్లికేషన్లు",
|
||||||
|
"clients": "ఖాతాదారులు",
|
||||||
|
"messages": "సందేశాలు"
|
||||||
|
},
|
||||||
|
"prowlarr": {
|
||||||
|
"enableIndexers": "సూచికలు",
|
||||||
|
"numberOfGrabs": "గ్రాబ్స్",
|
||||||
|
"numberOfQueries": "ప్రశ్నలు",
|
||||||
|
"numberOfFailGrabs": "ఫెయిల్ గ్రాబ్స్",
|
||||||
|
"numberOfFailQueries": "విఫలమైన ప్రశ్నలు"
|
||||||
|
},
|
||||||
|
"jackett": {
|
||||||
|
"configured": "కాన్ఫిగర్ చేయబడింది",
|
||||||
|
"errored": "పొరపాటు జరిగింది"
|
||||||
|
},
|
||||||
|
"mastodon": {
|
||||||
|
"user_count": "వినియోగదారులు",
|
||||||
|
"status_count": "పోస్ట్లు",
|
||||||
|
"domain_count": "డొమైన్లు"
|
||||||
|
},
|
||||||
|
"authentik": {
|
||||||
|
"users": "వినియోగదారులు",
|
||||||
|
"loginsLast24H": "లాగిన్లు (24గం)",
|
||||||
|
"failedLoginsLast24H": "విఫలమైన లాగిన్లు (24గం)"
|
||||||
|
},
|
||||||
|
"proxmox": {
|
||||||
|
"mem": "MEM",
|
||||||
|
"cpu": "సీపియూ",
|
||||||
|
"lxc": "LXC",
|
||||||
|
"vms": "విఎంలు"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "వినియోగదారులు",
|
||||||
|
"uptime": "సిస్టమ్ సమయము",
|
||||||
|
"days": "రోజులు",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN వినియోగదారులు",
|
||||||
|
"wlan_users": "WLAN వినియోగదారులు",
|
||||||
|
"up": "అప్",
|
||||||
|
"down": "డౌన్",
|
||||||
|
"wait": "దయచేసి వేచి ఉండండి"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "యాక్టివ్ స్ట్రీమ్లు",
|
||||||
|
"movies": "సినిమాలు",
|
||||||
|
"tv": "దూరదర్శిని కార్యక్రమాలు"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
"current": "当前定位",
|
"current": "当前定位",
|
||||||
"allow": "点击并允许",
|
"allow": "点击并允许",
|
||||||
"updating": "更新中",
|
"updating": "更新中",
|
||||||
"wait": "请稍后"
|
"wait": "请稍候"
|
||||||
},
|
},
|
||||||
"overseerr": {
|
"overseerr": {
|
||||||
"pending": "待办",
|
"pending": "待办",
|
||||||
@@ -162,23 +162,44 @@
|
|||||||
"mastodon": {
|
"mastodon": {
|
||||||
"user_count": "用户",
|
"user_count": "用户",
|
||||||
"status_count": "Posts",
|
"status_count": "Posts",
|
||||||
"domain_count": "Domains"
|
"domain_count": "域"
|
||||||
},
|
},
|
||||||
"strelaysrv": {
|
"strelaysrv": {
|
||||||
"numActiveSessions": "Sessions",
|
"numActiveSessions": "会话",
|
||||||
"dataRelayed": "Relayed",
|
"dataRelayed": "中继",
|
||||||
"numConnections": "Connections",
|
"numConnections": "连接",
|
||||||
"transferRate": "Rate"
|
"transferRate": "速度"
|
||||||
},
|
},
|
||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "用户",
|
||||||
"loginsLast24H": "Logins (24h)",
|
"loginsLast24H": "登录 (24h)",
|
||||||
"failedLoginsLast24H": "Failed Logins (24h)"
|
"failedLoginsLast24H": "登录失败 (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "内存",
|
||||||
"cpu": "CPU",
|
"cpu": "处理器",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "请稍候"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,26 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VMs"
|
||||||
|
},
|
||||||
|
"unifi": {
|
||||||
|
"users": "Users",
|
||||||
|
"uptime": "System Uptime",
|
||||||
|
"days": "Days",
|
||||||
|
"wan": "WAN",
|
||||||
|
"lan_users": "LAN Users",
|
||||||
|
"wlan_users": "WLAN Users",
|
||||||
|
"up": "UP",
|
||||||
|
"down": "DOWN",
|
||||||
|
"wait": "Please wait"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"streams": "Active Streams",
|
||||||
|
"movies": "Movies",
|
||||||
|
"tv": "TV Shows"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"cpu": "CPU",
|
||||||
|
"mem": "MEM",
|
||||||
|
"wait": "Please wait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,37 @@ import Docker from "widgets/docker/component";
|
|||||||
import { SettingsContext } from "utils/contexts/settings";
|
import { SettingsContext } from "utils/contexts/settings";
|
||||||
|
|
||||||
function resolveIcon(icon) {
|
function resolveIcon(icon) {
|
||||||
if (icon.startsWith("http")) {
|
// direct or relative URLs
|
||||||
return icon;
|
if (icon.startsWith("http") || icon.startsWith("/")) {
|
||||||
|
return <Image src={`${icon}`} width={32} height={32} alt="logo" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (icon.startsWith("/")) {
|
// mdi- prefixed, material design icons
|
||||||
return icon;
|
if (icon.startsWith("mdi-")) {
|
||||||
|
const iconName = icon.replace("mdi-", "").replace(".svg", "");
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
background: "linear-gradient(180deg, rgb(var(--color-logo-start)), rgb(var(--color-logo-stop)))",
|
||||||
|
mask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${iconName}.svg) no-repeat center / contain`,
|
||||||
|
WebkitMask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${iconName}.svg) no-repeat center / contain`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (icon.endsWith(".png")) {
|
// fallback to dashboard-icons
|
||||||
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}`;
|
const iconName = icon.replace(".png", "");
|
||||||
}
|
return (
|
||||||
|
<Image
|
||||||
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`;
|
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${iconName}.png`}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
alt="logo"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Item({ service }) {
|
export default function Item({ service }) {
|
||||||
@@ -57,12 +75,10 @@ export default function Item({ service }) {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="flex-shrink-0 flex items-center justify-center w-12 "
|
className="flex-shrink-0 flex items-center justify-center w-12 "
|
||||||
>
|
>
|
||||||
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
|
{resolveIcon(service.icon)}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-shrink-0 flex items-center justify-center w-12 ">
|
<div className="flex-shrink-0 flex items-center justify-center w-12 ">{resolveIcon(service.icon)}</div>
|
||||||
<Image src={resolveIcon(service.icon)} width={32} height={32} alt="logo" />
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{hasLink ? (
|
{hasLink ? (
|
||||||
|
|||||||
110
src/components/widgets/glances/glances.jsx
Normal file
110
src/components/widgets/glances/glances.jsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import useSWR from "swr";
|
||||||
|
import { BiError } from "react-icons/bi";
|
||||||
|
import { FaMemory } from "react-icons/fa";
|
||||||
|
import { FiCpu } from "react-icons/fi";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
|
import UsageBar from "../resources/usage-bar";
|
||||||
|
|
||||||
|
export default function Widget({ options }) {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
const { data, error } = useSWR(
|
||||||
|
`/api/widgets/glances?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`, {
|
||||||
|
refreshInterval: 1500,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error || data?.error) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||||
|
<div className="flex flex-row items-center justify-end">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||||
|
<div className="flex flex-col ml-3 text-left">
|
||||||
|
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap ml-4">
|
||||||
|
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||||
|
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||||
|
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
|
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||||
|
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||||
|
<div className="pl-0.5 text-xs">
|
||||||
|
{t("glances.wait")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UsageBar percent="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||||
|
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
|
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||||
|
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||||
|
<div className="pl-0.5 text-xs">
|
||||||
|
{t("glances.wait")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UsageBar percent="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{options.label && (
|
||||||
|
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap ml-4">
|
||||||
|
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||||
|
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||||
|
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
|
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||||
|
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||||
|
<div className="pl-0.5">
|
||||||
|
{t("common.number", {
|
||||||
|
value: data.cpu,
|
||||||
|
style: "unit",
|
||||||
|
unit: "percent",
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="pr-1">{t("glances.cpu")}</div>
|
||||||
|
</div>
|
||||||
|
<UsageBar percent={data.cpu} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||||
|
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||||
|
<div className="flex flex-col ml-3 text-left min-w-[85px]">
|
||||||
|
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||||
|
<div className="pl-0.5">
|
||||||
|
{t("common.number", {
|
||||||
|
value: data.mem,
|
||||||
|
style: "unit",
|
||||||
|
unit: "percent",
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="pr-1">{t("glances.mem")}</div>
|
||||||
|
</div>
|
||||||
|
<UsageBar percent={data.mem} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{options.label && (
|
||||||
|
<div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
119
src/components/widgets/unifi_console/unifi_console.jsx
Normal file
119
src/components/widgets/unifi_console/unifi_console.jsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { BiError, BiWifi, BiCheckCircle, BiXCircle } from "react-icons/bi";
|
||||||
|
import { MdSettingsEthernet } from "react-icons/md";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import { SiUbiquiti } from "react-icons/si";
|
||||||
|
|
||||||
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
|
export default function Widget({ options }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
options.type = "unifi_console";
|
||||||
|
const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index });
|
||||||
|
|
||||||
|
if (statsError || statsData?.error) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||||
|
<div className="flex flex-row items-center justify-end">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||||
|
<div className="flex flex-col ml-3 text-left">
|
||||||
|
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||||
|
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSite = statsData?.data?.find(s => s.name === "default");
|
||||||
|
|
||||||
|
if (!defaultSite) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||||
|
<div className="flex flex-row items-center justify-end">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<SiUbiquiti className="w-5 h-5 text-theme-800 dark:text-theme-200" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col ml-3 text-left">
|
||||||
|
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("unifi.wait")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wan = defaultSite.health.find(h => h.subsystem === "wan");
|
||||||
|
const lan = defaultSite.health.find(h => h.subsystem === "lan");
|
||||||
|
const wlan = defaultSite.health.find(h => h.subsystem === "wlan");
|
||||||
|
const data = {
|
||||||
|
name: wan.gw_name,
|
||||||
|
uptime: wan["gw_system-stats"].uptime,
|
||||||
|
up: wan.status === 'ok',
|
||||||
|
wlan: {
|
||||||
|
users: wlan.num_user,
|
||||||
|
status: wlan.status
|
||||||
|
},
|
||||||
|
lan: {
|
||||||
|
users: lan.num_user,
|
||||||
|
status: lan.status
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row ml-3">
|
||||||
|
<SiUbiquiti className="text-theme-800 dark:text-theme-200 w-3 h-3 mr-1" />
|
||||||
|
<div className="text-theme-800 dark:text-theme-200 text-xs font-bold flex flex-row justify-between">
|
||||||
|
{data.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row ml-3 text-[10px] justify-between">
|
||||||
|
<div className="flex flex-row" title={t("unifi.uptime")}>
|
||||||
|
<div className="pr-0.5 text-theme-800 dark:text-theme-200">
|
||||||
|
{t("common.number", {
|
||||||
|
value: data.uptime / 86400,
|
||||||
|
maximumFractionDigits: 1,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.days")}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.wan")}</div>
|
||||||
|
{ data.up
|
||||||
|
? <BiCheckCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
|
||||||
|
: <BiXCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row ml-3 py-0.5">
|
||||||
|
<BiWifi className="text-theme-800 dark:text-theme-200 w-4 h-4 mr-1" />
|
||||||
|
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between" title={t("unifi.users")}>
|
||||||
|
<div className="pr-0.5">
|
||||||
|
{t("common.number", {
|
||||||
|
value: data.wlan.users,
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row ml-3 pb-0.5">
|
||||||
|
<MdSettingsEthernet className="text-theme-800 dark:text-theme-200 w-4 h-4 mr-1" />
|
||||||
|
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between" title={t("unifi.users")}>
|
||||||
|
<div className="pr-0.5">
|
||||||
|
{t("common.number", {
|
||||||
|
value: data.lan.users,
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ const widgetMappings = {
|
|||||||
greeting: dynamic(() => import("components/widgets/greeting/greeting")),
|
greeting: dynamic(() => import("components/widgets/greeting/greeting")),
|
||||||
datetime: dynamic(() => import("components/widgets/datetime/datetime")),
|
datetime: dynamic(() => import("components/widgets/datetime/datetime")),
|
||||||
logo: dynamic(() => import("components/widgets/logo/logo"), { ssr: false }),
|
logo: dynamic(() => import("components/widgets/logo/logo"), { ssr: false }),
|
||||||
|
unifi_console: dynamic(() => import("components/widgets/unifi_console/unifi_console")),
|
||||||
|
glances: dynamic(() => import("components/widgets/glances/glances")),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Widget({ widget }) {
|
export default function Widget({ widget }) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export default function Document() {
|
|||||||
name="description"
|
name="description"
|
||||||
content="A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations."
|
content="A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations."
|
||||||
/>
|
/>
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=4" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=4" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=4" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=4" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=4" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=4" />
|
||||||
|
|||||||
40
src/pages/api/widgets/glances.js
Normal file
40
src/pages/api/widgets/glances.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
import createLogger from "utils/logger";
|
||||||
|
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
||||||
|
|
||||||
|
const logger = createLogger("glances");
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
const { index } = req.query;
|
||||||
|
|
||||||
|
const privateWidgetOptions = await getPrivateWidgetOptions("glances", index);
|
||||||
|
|
||||||
|
const url = privateWidgetOptions?.url;
|
||||||
|
if (!url) {
|
||||||
|
const errorMessage = "Missing Glances URL";
|
||||||
|
logger.error(errorMessage);
|
||||||
|
return res.status(400).json({ error: errorMessage });
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiUrl = `${url}/api/3/quicklook`;
|
||||||
|
const headers = {
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
};
|
||||||
|
if (privateWidgetOptions.username && privateWidgetOptions.password) {
|
||||||
|
headers.Authorization = `Basic ${Buffer.from(`${privateWidgetOptions.username}:${privateWidgetOptions.password}`).toString("base64")}`
|
||||||
|
}
|
||||||
|
const params = { method: "GET", headers };
|
||||||
|
|
||||||
|
const [status, contentType, data] = await httpProxy(apiUrl, params);
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
logger.error("Authorization failure getting data from glances API. Data: %s", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
logger.error("HTTP %d getting data from glances API. Data: %s", status, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType) res.setHeader("Content-Type", contentType);
|
||||||
|
return res.status(status).send(data);
|
||||||
|
}
|
||||||
@@ -263,8 +263,6 @@ function Home({ initialSettings }) {
|
|||||||
export default function Wrapper({ initialSettings, fallback }) {
|
export default function Wrapper({ initialSettings, fallback }) {
|
||||||
const wrappedStyle = {};
|
const wrappedStyle = {};
|
||||||
if (initialSettings && initialSettings.background) {
|
if (initialSettings && initialSettings.background) {
|
||||||
// wrappedStyle.backgroundImage = `url(${initialSettings.background})`;
|
|
||||||
// wrappedStyle.backgroundSize = "cover";
|
|
||||||
const opacity = initialSettings.backgroundOpacity ?? 1;
|
const opacity = initialSettings.backgroundOpacity ?? 1;
|
||||||
const opacityValue = 1 - opacity;
|
const opacityValue = 1 - opacity;
|
||||||
wrappedStyle.backgroundImage = `
|
wrappedStyle.backgroundImage = `
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export async function getServerSideProps({ res }) {
|
|||||||
const theme = settings.theme || "dark";
|
const theme = settings.theme || "dark";
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
name: "Homepage",
|
name: settings.title || "Homepage",
|
||||||
short_name: "Homepage",
|
short_name: settings.title || "Homepage",
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: "/android-chrome-192x192.png?v=2",
|
src: "/android-chrome-192x192.png?v=2",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import yaml from "js-yaml";
|
|||||||
|
|
||||||
import checkAndCopyConfig from "utils/config/config";
|
import checkAndCopyConfig from "utils/config/config";
|
||||||
import { servicesFromConfig, servicesFromDocker, cleanServiceGroups } from "utils/config/service-helpers";
|
import { servicesFromConfig, servicesFromDocker, cleanServiceGroups } from "utils/config/service-helpers";
|
||||||
|
import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers";
|
||||||
|
|
||||||
export async function bookmarksResponse() {
|
export async function bookmarksResponse() {
|
||||||
checkAndCopyConfig("bookmarks.yaml");
|
checkAndCopyConfig("bookmarks.yaml");
|
||||||
@@ -29,21 +30,17 @@ export async function bookmarksResponse() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function widgetsResponse() {
|
export async function widgetsResponse() {
|
||||||
checkAndCopyConfig("widgets.yaml");
|
let configuredWidgets;
|
||||||
|
|
||||||
const widgetsYaml = path.join(process.cwd(), "config", "widgets.yaml");
|
try {
|
||||||
const fileContents = await fs.readFile(widgetsYaml, "utf8");
|
configuredWidgets = cleanWidgetGroups(await widgetsFromConfig());
|
||||||
const widgets = yaml.load(fileContents);
|
} catch (e) {
|
||||||
|
console.error("Failed to load widgets, please check widgets.yaml for errors or remove example entries.");
|
||||||
|
if (e) console.error(e);
|
||||||
|
configuredWidgets = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (!widgets) return [];
|
return configuredWidgets;
|
||||||
|
|
||||||
// map easy to write YAML objects into easy to consume JS arrays
|
|
||||||
const widgetsArray = widgets.map((group) => ({
|
|
||||||
type: Object.keys(group)[0],
|
|
||||||
options: { ...group[Object.keys(group)[0]] },
|
|
||||||
}));
|
|
||||||
|
|
||||||
return widgetsArray;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function servicesResponse() {
|
export async function servicesResponse() {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export async function servicesFromDocker() {
|
|||||||
let constructedService = null;
|
let constructedService = null;
|
||||||
|
|
||||||
Object.keys(container.Labels).forEach((label) => {
|
Object.keys(container.Labels).forEach((label) => {
|
||||||
if (label.startsWith("homepage")) {
|
if (label.startsWith("homepage.")) {
|
||||||
if (!constructedService) {
|
if (!constructedService) {
|
||||||
constructedService = {
|
constructedService = {
|
||||||
container: container.Names[0].replace(/^\//, ""),
|
container: container.Names[0].replace(/^\//, ""),
|
||||||
|
|||||||
73
src/utils/config/widget-helpers.js
Normal file
73
src/utils/config/widget-helpers.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
import checkAndCopyConfig from "utils/config/config";
|
||||||
|
|
||||||
|
export async function widgetsFromConfig() {
|
||||||
|
checkAndCopyConfig("widgets.yaml");
|
||||||
|
|
||||||
|
const widgetsYaml = path.join(process.cwd(), "config", "widgets.yaml");
|
||||||
|
const fileContents = await fs.readFile(widgetsYaml, "utf8");
|
||||||
|
const widgets = yaml.load(fileContents);
|
||||||
|
|
||||||
|
if (!widgets) return [];
|
||||||
|
|
||||||
|
// map easy to write YAML objects into easy to consume JS arrays
|
||||||
|
const widgetsArray = widgets.map((group, index) => ({
|
||||||
|
type: Object.keys(group)[0],
|
||||||
|
options: {
|
||||||
|
index,
|
||||||
|
...group[Object.keys(group)[0]]
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return widgetsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanWidgetGroups(widgets) {
|
||||||
|
return widgets.map((widget, index) => {
|
||||||
|
const sanitizedOptions = widget.options;
|
||||||
|
const optionKeys = Object.keys(sanitizedOptions);
|
||||||
|
["url", "username", "password", "key"].forEach((pO) => {
|
||||||
|
if (optionKeys.includes(pO)) {
|
||||||
|
delete sanitizedOptions[pO];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: widget.type,
|
||||||
|
options: {
|
||||||
|
index,
|
||||||
|
...sanitizedOptions
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPrivateWidgetOptions(type, widgetIndex) {
|
||||||
|
const widgets = await widgetsFromConfig();
|
||||||
|
|
||||||
|
const privateOptions = widgets.map((widget) => {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
key
|
||||||
|
} = widget.options;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: widget.type,
|
||||||
|
options: {
|
||||||
|
index,
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
key
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (type !== undefined && widgetIndex !== undefined) ? privateOptions.find(o => o.type === type && o.options.index === parseInt(widgetIndex, 10))?.options : privateOptions;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ export function formatApiCall(url, args) {
|
|||||||
const find = /\{.*?\}/g;
|
const find = /\{.*?\}/g;
|
||||||
const replace = (match) => {
|
const replace = (match) => {
|
||||||
const key = match.replace(/\{|\}/g, "");
|
const key = match.replace(/\{|\}/g, "");
|
||||||
return args[key];
|
return args[key] || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
return url.replace(/\/+$/, "").replace(find, replace);
|
return url.replace(/\/+$/, "").replace(find, replace);
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import cache from "memory-cache";
|
import cache from "memory-cache";
|
||||||
|
|
||||||
|
const defaultDuration = 5;
|
||||||
|
|
||||||
export default async function cachedFetch(url, duration) {
|
export default async function cachedFetch(url, duration) {
|
||||||
const cached = cache.get(url);
|
const cached = cache.get(url);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
duration = duration || defaultDuration;
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const components = {
|
|||||||
ombi: dynamic(() => import("./ombi/component")),
|
ombi: dynamic(() => import("./ombi/component")),
|
||||||
overseerr: dynamic(() => import("./overseerr/component")),
|
overseerr: dynamic(() => import("./overseerr/component")),
|
||||||
pihole: dynamic(() => import("./pihole/component")),
|
pihole: dynamic(() => import("./pihole/component")),
|
||||||
|
plex: dynamic(() => import("./plex/component")),
|
||||||
portainer: dynamic(() => import("./portainer/component")),
|
portainer: dynamic(() => import("./portainer/component")),
|
||||||
prowlarr: dynamic(() => import("./prowlarr/component")),
|
prowlarr: dynamic(() => import("./prowlarr/component")),
|
||||||
proxmox: dynamic(() => import("./proxmox/component")),
|
proxmox: dynamic(() => import("./proxmox/component")),
|
||||||
@@ -32,6 +33,7 @@ const components = {
|
|||||||
tautulli: dynamic(() => import("./tautulli/component")),
|
tautulli: dynamic(() => import("./tautulli/component")),
|
||||||
traefik: dynamic(() => import("./traefik/component")),
|
traefik: dynamic(() => import("./traefik/component")),
|
||||||
transmission: dynamic(() => import("./transmission/component")),
|
transmission: dynamic(() => import("./transmission/component")),
|
||||||
|
unifi: dynamic(() => import("./unifi/component")),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default components;
|
export default components;
|
||||||
|
|||||||
@@ -40,14 +40,16 @@ export default function Component({ service }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const network = statsData.stats.networks?.eth0 || statsData.stats.networks?.network;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container service={service}>
|
<Container service={service}>
|
||||||
<Block label="docker.cpu" value={t("common.percent", { value: calculateCPUPercent(statsData.stats) })} />
|
<Block label="docker.cpu" value={t("common.percent", { value: calculateCPUPercent(statsData.stats) })} />
|
||||||
<Block label="docker.mem" value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
|
<Block label="docker.mem" value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
|
||||||
{statsData.stats.networks && (
|
{network && (
|
||||||
<>
|
<>
|
||||||
<Block label="docker.rx" value={t("common.bytes", { value: statsData.stats.networks.eth0.rx_bytes })} />
|
<Block label="docker.rx" value={t("common.bytes", { value: network.rx_bytes })} />
|
||||||
<Block label="docker.tx" value={t("common.bytes", { value: statsData.stats.networks.eth0.tx_bytes })} />
|
<Block label="docker.tx" value={t("common.bytes", { value: network.tx_bytes })} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
38
src/widgets/plex/component.jsx
Normal file
38
src/widgets/plex/component.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
|
import Block from "components/services/widget/block";
|
||||||
|
import Container from "components/services/widget/container";
|
||||||
|
import { formatProxyUrl } from "utils/proxy/api-helpers";
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
const { data: plexData, error: plexAPIError } = useSWR(formatProxyUrl(widget, "unified"), {
|
||||||
|
refreshInterval: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (plexAPIError) {
|
||||||
|
return <Container error={t("widget.api_error")} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plexData) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="plex.streams" />
|
||||||
|
<Block label="plex.movies" />
|
||||||
|
<Block label="plex.tv" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="plex.streams" value={t("common.number", { value: plexData.streams })} />
|
||||||
|
<Block label="plex.movies" value={t("common.number", { value: plexData.movies })} />
|
||||||
|
<Block label="plex.tv" value={t("common.number", { value: plexData.tv })} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/widgets/plex/proxy.js
Normal file
110
src/widgets/plex/proxy.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
import cache from "memory-cache";
|
||||||
|
import { xml2json } from "xml-js";
|
||||||
|
|
||||||
|
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||||
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
|
import createLogger from "utils/logger";
|
||||||
|
import widgets from "widgets/widgets";
|
||||||
|
|
||||||
|
const proxyName = "plexProxyHandler";
|
||||||
|
const librariesCacheKey = `${proxyName}__libraries`;
|
||||||
|
const moviesCacheKey = `${proxyName}__movies`;
|
||||||
|
const tvCacheKey = `${proxyName}__tv`;
|
||||||
|
const logger = createLogger(proxyName);
|
||||||
|
|
||||||
|
async function getWidget(req) {
|
||||||
|
const { group, service } = req.query;
|
||||||
|
|
||||||
|
if (!group || !service) {
|
||||||
|
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = await getServiceWidget(group, service);
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFromPlexAPI(endpoint, widget) {
|
||||||
|
const api = widgets?.[widget.type]?.api;
|
||||||
|
if (!api) {
|
||||||
|
return [403, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(formatApiCall(api, { endpoint, ...widget }));
|
||||||
|
|
||||||
|
const [status, contentType, data] = await httpProxy(url);
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
logger.error("HTTP %d communicating with Plex. Data: %s", status, data.toString());
|
||||||
|
return [status, data.toString()];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataDecoded = xml2json(data.toString(), { compact: true });
|
||||||
|
return [status, JSON.parse(dataDecoded), contentType];
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Error decoding Plex API data. Data: %s", data.toString());
|
||||||
|
return [status, null];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function plexProxyHandler(req, res) {
|
||||||
|
const widget = await getWidget(req);
|
||||||
|
if (!widget) {
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Getting streams from Plex API");
|
||||||
|
let streams;
|
||||||
|
let [status, apiData] = await fetchFromPlexAPI("/status/sessions", widget);
|
||||||
|
if (apiData && apiData.MediaContainer) {
|
||||||
|
streams = apiData.MediaContainer._attributes.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
let libraries = cache.get(librariesCacheKey);
|
||||||
|
if (libraries === null) {
|
||||||
|
logger.debug("Getting libraries from Plex API");
|
||||||
|
[status, apiData] = await fetchFromPlexAPI("/library/sections", widget);
|
||||||
|
if (apiData && apiData.MediaContainer) {
|
||||||
|
libraries = apiData.MediaContainer.Directory;
|
||||||
|
cache.put(librariesCacheKey, libraries, 1000 * 60 * 60 * 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let movies = cache.get(moviesCacheKey);
|
||||||
|
let tv = cache.get(tvCacheKey);
|
||||||
|
if (movies === null || tv === null) {
|
||||||
|
movies = 0;
|
||||||
|
tv = 0;
|
||||||
|
logger.debug("Getting movie + tv counts from Plex API");
|
||||||
|
libraries.filter(l => ["movie", "show"].includes(l._attributes.type)).forEach(async (library) => {
|
||||||
|
[status, apiData] = await fetchFromPlexAPI(`/library/sections/${library._attributes.key}/all`, widget);
|
||||||
|
if (apiData && apiData.MediaContainer) {
|
||||||
|
const size = parseInt(apiData.MediaContainer._attributes.size, 10);
|
||||||
|
if (library._attributes.type === "movie") {
|
||||||
|
movies += size;
|
||||||
|
} else if (library._attributes.type === "show") {
|
||||||
|
tv += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.put(tvCacheKey, tv, 1000 * 60 * 10);
|
||||||
|
cache.put(moviesCacheKey, movies, 1000 * 60 * 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
streams,
|
||||||
|
tv,
|
||||||
|
movies
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(status).send(data);
|
||||||
|
}
|
||||||
14
src/widgets/plex/widget.js
Normal file
14
src/widgets/plex/widget.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import plexProxyHandler from "./proxy";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}{endpoint}?X-Plex-Token={key}",
|
||||||
|
proxyHandler: plexProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
unified: {
|
||||||
|
endpoint: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
58
src/widgets/unifi/component.jsx
Normal file
58
src/widgets/unifi/component.jsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
|
import Container from "components/services/widget/container";
|
||||||
|
import Block from "components/services/widget/block";
|
||||||
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
const { data: statsData, error: statsError } = useWidgetAPI(widget, "stat/sites");
|
||||||
|
|
||||||
|
if (statsError || statsData?.error) {
|
||||||
|
return <Container error={t("widget.api_error")} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSite = statsData?.data?.find(s => s.name === "default");
|
||||||
|
|
||||||
|
if (!defaultSite) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="unifi.uptime" />
|
||||||
|
<Block label="unifi.wan" />
|
||||||
|
<Block label="unifi.lan_users" />
|
||||||
|
<Block label="unifi.wlan_users" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wan = defaultSite.health.find(h => h.subsystem === "wan");
|
||||||
|
const lan = defaultSite.health.find(h => h.subsystem === "lan");
|
||||||
|
const wlan = defaultSite.health.find(h => h.subsystem === "wlan");
|
||||||
|
const data = {
|
||||||
|
name: wan.gw_name,
|
||||||
|
uptime: wan["gw_system-stats"].uptime,
|
||||||
|
up: wan.status === 'ok',
|
||||||
|
wlan: {
|
||||||
|
users: wlan.num_user,
|
||||||
|
status: wlan.status
|
||||||
|
},
|
||||||
|
lan: {
|
||||||
|
users: lan.num_user,
|
||||||
|
status: lan.status
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uptime = `${t("common.number", { value: data.uptime / 86400, maximumFractionDigits: 1 })} ${t("unifi.days")}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="unifi.uptime" value={ uptime } />
|
||||||
|
<Block label="unifi.wan" value={ data.up ? t("unifi.up") : t("unifi.down") } />
|
||||||
|
<Block label="unifi.lan_users" value={ t("common.number", { value: data.lan.users }) } />
|
||||||
|
<Block label="unifi.wlan_users" value={ t("common.number", { value: data.wlan.users }) } />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
119
src/widgets/unifi/proxy.js
Normal file
119
src/widgets/unifi/proxy.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import cache from "memory-cache";
|
||||||
|
|
||||||
|
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||||
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar";
|
||||||
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
|
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
|
||||||
|
import createLogger from "utils/logger";
|
||||||
|
import widgets from "widgets/widgets";
|
||||||
|
|
||||||
|
const udmpPrefix = "/proxy/network";
|
||||||
|
const proxyName = "unifiProxyHandler";
|
||||||
|
const prefixCacheKey = `${proxyName}__prefix`;
|
||||||
|
const logger = createLogger(proxyName);
|
||||||
|
|
||||||
|
async function getWidget(req) {
|
||||||
|
const { group, service, type } = req.query;
|
||||||
|
|
||||||
|
let widget = null;
|
||||||
|
if (type === "unifi_console") { // info widget
|
||||||
|
const index = req.query?.query ? JSON.parse(req.query.query).index : undefined;
|
||||||
|
widget = await getPrivateWidgetOptions(type, index);
|
||||||
|
if (!widget) {
|
||||||
|
logger.debug("Error retrieving settings for this Unifi widget");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
widget.type = "unifi";
|
||||||
|
} else {
|
||||||
|
if (!group || !service) {
|
||||||
|
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
widget = await getServiceWidget(group, service);
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(widget) {
|
||||||
|
const endpoint = (widget.prefix === udmpPrefix) ? "auth/login" : "login";
|
||||||
|
const api = widgets?.[widget.type]?.api?.replace("{prefix}", ""); // no prefix for login url
|
||||||
|
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
|
||||||
|
const loginBody = { username: widget.username, password: widget.password, remember: true };
|
||||||
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(loginBody),
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
return [status, contentType, data, responseHeaders];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function unifiProxyHandler(req, res) {
|
||||||
|
const widget = await getWidget(req);
|
||||||
|
if (!widget) {
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = widgets?.[widget.type]?.api;
|
||||||
|
if (!api) {
|
||||||
|
return res.status(403).json({ error: "Service does not support API calls" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let [status, contentType, data, responseHeaders] = [];
|
||||||
|
let prefix = cache.get(prefixCacheKey);
|
||||||
|
if (prefix === null) {
|
||||||
|
// auto detect if we're talking to a UDM Pro, and cache the result so that we
|
||||||
|
// don't make two requests each time data from Unifi is required
|
||||||
|
[status, contentType, data, responseHeaders] = await httpProxy(widget.url);
|
||||||
|
prefix = "";
|
||||||
|
if (responseHeaders["x-csrf-token"]) {
|
||||||
|
prefix = udmpPrefix;
|
||||||
|
}
|
||||||
|
cache.put(prefixCacheKey, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.prefix = prefix;
|
||||||
|
|
||||||
|
const { endpoint } = req.query;
|
||||||
|
const url = new URL(formatApiCall(api, { endpoint, ...widget }));
|
||||||
|
const params = { method: "GET", headers: {} };
|
||||||
|
setCookieHeader(url, params);
|
||||||
|
|
||||||
|
[status, contentType, data, responseHeaders] = await httpProxy(url, params);
|
||||||
|
if (status === 401) {
|
||||||
|
logger.debug("Unifi isn't logged in or rejected the reqeust, attempting login.");
|
||||||
|
[status, contentType, data, responseHeaders] = await login(widget);
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
logger.error("HTTP %d logging in to Unifi. Data: %s", status, data);
|
||||||
|
return res.status(status).end(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = JSON.parse(data.toString());
|
||||||
|
if (!(json?.meta?.rc === "ok" || json.login_time)) {
|
||||||
|
logger.error("Error logging in to Unifi: Data: %s", data);
|
||||||
|
return res.status(401).end(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCookieToJar(url, responseHeaders);
|
||||||
|
setCookieHeader(url, params);
|
||||||
|
|
||||||
|
logger.debug("Retrying Unifi request after login.");
|
||||||
|
[status, contentType, data, responseHeaders] = await httpProxy(url, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
logger.error("HTTP %d getting data from Unifi endpoint %s. Data: %s", status, url.href, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType) res.setHeader("Content-Type", contentType);
|
||||||
|
return res.status(status).send(data);
|
||||||
|
}
|
||||||
14
src/widgets/unifi/widget.js
Normal file
14
src/widgets/unifi/widget.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import unifiProxyHandler from "./proxy";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}{prefix}/api/{endpoint}",
|
||||||
|
proxyHandler: unifiProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
"stat/sites": {
|
||||||
|
endpoint: "stat/sites",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
@@ -13,6 +13,7 @@ import nzbget from "./nzbget/widget";
|
|||||||
import ombi from "./ombi/widget";
|
import ombi from "./ombi/widget";
|
||||||
import overseerr from "./overseerr/widget";
|
import overseerr from "./overseerr/widget";
|
||||||
import pihole from "./pihole/widget";
|
import pihole from "./pihole/widget";
|
||||||
|
import plex from "./plex/widget";
|
||||||
import portainer from "./portainer/widget";
|
import portainer from "./portainer/widget";
|
||||||
import prowlarr from "./prowlarr/widget";
|
import prowlarr from "./prowlarr/widget";
|
||||||
import proxmox from "./proxmox/widget";
|
import proxmox from "./proxmox/widget";
|
||||||
@@ -27,6 +28,7 @@ import strelaysrv from "./strelaysrv/widget";
|
|||||||
import tautulli from "./tautulli/widget";
|
import tautulli from "./tautulli/widget";
|
||||||
import traefik from "./traefik/widget";
|
import traefik from "./traefik/widget";
|
||||||
import transmission from "./transmission/widget";
|
import transmission from "./transmission/widget";
|
||||||
|
import unifi from "./unifi/widget";
|
||||||
|
|
||||||
const widgets = {
|
const widgets = {
|
||||||
adguard,
|
adguard,
|
||||||
@@ -45,6 +47,7 @@ const widgets = {
|
|||||||
ombi,
|
ombi,
|
||||||
overseerr,
|
overseerr,
|
||||||
pihole,
|
pihole,
|
||||||
|
plex,
|
||||||
portainer,
|
portainer,
|
||||||
prowlarr,
|
prowlarr,
|
||||||
proxmox,
|
proxmox,
|
||||||
@@ -59,6 +62,8 @@ const widgets = {
|
|||||||
tautulli,
|
tautulli,
|
||||||
traefik,
|
traefik,
|
||||||
transmission,
|
transmission,
|
||||||
|
unifi,
|
||||||
|
unifi_console: unifi
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widgets;
|
export default widgets;
|
||||||
|
|||||||
Reference in New Issue
Block a user