diff --git a/package.json b/package.json index 3dd2ed0d..75d02e7f 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "shvl": "^3.0.0", "swr": "^1.3.0", "tough-cookie": "^4.1.2", - "winston": "^3.8.2" + "winston": "^3.8.2", + "xml-js": "^1.6.11" }, "devDependencies": { "@tailwindcss/forms": "^0.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49394c8e..73e30e54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,7 @@ specifiers: tough-cookie: ^4.1.2 typescript: ^4.8.3 winston: ^3.8.2 + xml-js: ^1.6.11 dependencies: '@headlessui/react': 1.7.2_biqbaboplfbrettd7655fr4n2y @@ -65,6 +66,7 @@ dependencies: swr: 1.3.0_react@18.2.0 tough-cookie: 4.1.2 winston: 3.8.2 + xml-js: 1.6.11 devDependencies: '@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8 @@ -2554,6 +2556,10 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: false + /sax/1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + dev: false + /scheduler/0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -3000,6 +3006,13 @@ packages: /wrappy/1.0.2: 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: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/public/locales/bg/common.json b/public/locales/bg/common.json new file mode 100644 index 00000000..2efeb981 --- /dev/null +++ b/public/locales/bg/common.json @@ -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" + } +} diff --git a/public/locales/ca/common.json b/public/locales/ca/common.json index 4f46fff0..cea7d15b 100644 --- a/public/locales/ca/common.json +++ b/public/locales/ca/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/de/common.json b/public/locales/de/common.json index 0070cb5a..3dbe8beb 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index b1097904..d53d480c 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -66,6 +66,11 @@ "remaining": "Remaining", "downloaded": "Downloaded" }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, "sabnzbd": { "rate": "Rate", "queue": "Queue", @@ -202,5 +207,10 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 3184d8b7..7335f88b 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -191,5 +191,15 @@ "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" } } diff --git a/public/locales/fi/common.json b/public/locales/fi/common.json index c4654377..a972a141 100644 --- a/public/locales/fi/common.json +++ b/public/locales/fi/common.json @@ -191,5 +191,15 @@ "wan": "WAN", "up": "UP", "down": "DOWN" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "wait": "Please wait", + "cpu": "CPU", + "mem": "MEM" } } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index e893fccb..2f49a82e 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -182,14 +182,24 @@ "vms": "VMs" }, "unifi": { - "users": "Users", - "uptime": "System Uptime", - "days": "Days", + "users": "Utilisateurs", + "uptime": "Disponibilité du système", + "days": "Jours", "wan": "WAN", - "lan_users": "LAN Users", - "wlan_users": "WLAN Users", + "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" } } diff --git a/public/locales/he/common.json b/public/locales/he/common.json index 532fce39..ff48c10b 100644 --- a/public/locales/he/common.json +++ b/public/locales/he/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/hr/common.json b/public/locales/hr/common.json index e11164b4..096f6e0b 100644 --- a/public/locales/hr/common.json +++ b/public/locales/hr/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/hu/common.json b/public/locales/hu/common.json index 581420ea..c36c15a0 100644 --- a/public/locales/hu/common.json +++ b/public/locales/hu/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/it/common.json b/public/locales/it/common.json index 9403315c..405a6e0b 100644 --- a/public/locales/it/common.json +++ b/public/locales/it/common.json @@ -191,5 +191,15 @@ "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" } } diff --git a/public/locales/nb-NO/common.json b/public/locales/nb-NO/common.json index dc9992d4..b163e998 100644 --- a/public/locales/nb-NO/common.json +++ b/public/locales/nb-NO/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json index 07250217..562a50de 100644 --- a/public/locales/nl/common.json +++ b/public/locales/nl/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/pl/common.json b/public/locales/pl/common.json index d34b7c80..8de9d629 100644 --- a/public/locales/pl/common.json +++ b/public/locales/pl/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/pt-BR/common.json b/public/locales/pt-BR/common.json index 61e704d8..1ec421ea 100644 --- a/public/locales/pt-BR/common.json +++ b/public/locales/pt-BR/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json index a8257926..10658f6e 100644 --- a/public/locales/pt/common.json +++ b/public/locales/pt/common.json @@ -202,5 +202,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/ro/common.json b/public/locales/ro/common.json index c3371cd0..3a821807 100644 --- a/public/locales/ro/common.json +++ b/public/locales/ro/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 57e1eb93..5f905808 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json index b36e77ae..7c8c592f 100644 --- a/public/locales/sv/common.json +++ b/public/locales/sv/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/te/common.json b/public/locales/te/common.json index c5a5323d..ae793770 100644 --- a/public/locales/te/common.json +++ b/public/locales/te/common.json @@ -31,9 +31,9 @@ "placeholder": "వెతకండి…" }, "resources": { - "cpu": "సీ పి యూ", + "cpu": "సీపియూ", "total": "మొత్తం", - "free": "ఉచిత", + "free": "మిగిలింది", "used": "ఉపయోగించబడిన", "load": "లోడ్" }, @@ -41,7 +41,7 @@ "rx": "RX", "tx": "TX", "mem": "MEM", - "cpu": "CPU", + "cpu": "సీపియూ", "offline": "ఆఫ్‌లైన్" }, "emby": { @@ -128,8 +128,8 @@ "ping": "పింగ్" }, "portainer": { - "running": "నడుస్తోంది", - "stopped": "ఆగిపోయింది", + "running": "నడుస్తున్నవి", + "stopped": "ఆగిపోయినవి", "total": "మొత్తం" }, "traefik": { @@ -139,7 +139,7 @@ }, "npm": { "enabled": "ప్రారంభించబడింది", - "disabled": "Disabled", + "disabled": "డిసేబ్లెడ్", "total": "మొత్తం" }, "coinmarketcap": { @@ -177,19 +177,29 @@ }, "proxmox": { "mem": "MEM", - "cpu": "CPU", + "cpu": "సీపియూ", "lxc": "LXC", - "vms": "VMs" + "vms": "విఎంలు" }, "unifi": { - "users": "Users", - "uptime": "System Uptime", - "days": "Days", + "users": "వినియోగదారులు", + "uptime": "సిస్టమ్ సమయము", + "days": "రోజులు", "wan": "WAN", - "lan_users": "LAN Users", - "wlan_users": "WLAN Users", - "up": "UP", - "down": "DOWN", + "lan_users": "LAN వినియోగదారులు", + "wlan_users": "WLAN వినియోగదారులు", + "up": "అప్", + "down": "డౌన్", + "wait": "దయచేసి వేచి ఉండండి" + }, + "plex": { + "streams": "యాక్టివ్ స్ట్రీమ్‌లు", + "movies": "సినిమాలు", + "tv": "దూరదర్శిని కార్యక్రమాలు" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", "wait": "Please wait" } } diff --git a/public/locales/vi/common.json b/public/locales/vi/common.json index 5a27fd85..7f74ebd9 100644 --- a/public/locales/vi/common.json +++ b/public/locales/vi/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/yue/common.json b/public/locales/yue/common.json index a49767c4..e3909728 100644 --- a/public/locales/yue/common.json +++ b/public/locales/yue/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/public/locales/zh-CN/common.json b/public/locales/zh-CN/common.json index d80a0534..6592fb1e 100644 --- a/public/locales/zh-CN/common.json +++ b/public/locales/zh-CN/common.json @@ -92,7 +92,7 @@ "current": "当前定位", "allow": "点击并允许", "updating": "更新中", - "wait": "请稍后" + "wait": "请稍候" }, "overseerr": { "pending": "待办", @@ -190,6 +190,16 @@ "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" } } diff --git a/public/locales/zh-Hant/common.json b/public/locales/zh-Hant/common.json index 3ad0ed06..9418e244 100644 --- a/public/locales/zh-Hant/common.json +++ b/public/locales/zh-Hant/common.json @@ -191,5 +191,15 @@ "up": "UP", "down": "DOWN", "wait": "Please wait" + }, + "plex": { + "streams": "Active Streams", + "movies": "Movies", + "tv": "TV Shows" + }, + "glances": { + "cpu": "CPU", + "mem": "MEM", + "wait": "Please wait" } } diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx index 870eb4f4..ed0cef2d 100644 --- a/src/components/services/item.jsx +++ b/src/components/services/item.jsx @@ -9,19 +9,37 @@ import Docker from "widgets/docker/component"; import { SettingsContext } from "utils/contexts/settings"; function resolveIcon(icon) { - if (icon.startsWith("http")) { - return icon; + // direct or relative URLs + if (icon.startsWith("http") || icon.startsWith("/")) { + return logo; } - if (icon.startsWith("/")) { - return icon; + // mdi- prefixed, material design icons + if (icon.startsWith("mdi-")) { + const iconName = icon.replace("mdi-", "").replace(".svg", ""); + return ( +
+ ); } - if (icon.endsWith(".png")) { - return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}`; - } - - return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`; + // fallback to dashboard-icons + const iconName = icon.replace(".png", ""); + return ( + logo + ); } export default function Item({ service }) { @@ -57,12 +75,10 @@ export default function Item({ service }) { rel="noreferrer" className="flex-shrink-0 flex items-center justify-center w-12 " > - logo + {resolveIcon(service.icon)} ) : ( -
- logo -
+
{resolveIcon(service.icon)}
))} {hasLink ? ( diff --git a/src/components/widgets/glances/glances.jsx b/src/components/widgets/glances/glances.jsx new file mode 100644 index 00000000..a48cef50 --- /dev/null +++ b/src/components/widgets/glances/glances.jsx @@ -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 ( +
+
+
+ +
+ {t("widget.api_error")} +
+
+
+
+ ); + } + + if (!data) { + return ( +
+
+
+ +
+
+
+ {t("glances.wait")} +
+
+ +
+
+
+ +
+
+
+ {t("glances.wait")} +
+
+ +
+
+
+ {options.label && ( +
{options.label}
+ )} +
+ ); + } + + return ( +
+
+
+ +
+
+
+ {t("common.number", { + value: data.cpu, + style: "unit", + unit: "percent", + maximumFractionDigits: 0, + })} +
+
{t("glances.cpu")}
+
+ +
+
+
+ +
+
+
+ {t("common.number", { + value: data.mem, + style: "unit", + unit: "percent", + maximumFractionDigits: 0, + })} +
+
{t("glances.mem")}
+
+ +
+
+
+ {options.label && ( +
{options.label}
+ )} +
+ ); +} diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx index ac5353eb..bd31ed93 100644 --- a/src/components/widgets/widget.jsx +++ b/src/components/widgets/widget.jsx @@ -11,6 +11,7 @@ const widgetMappings = { datetime: dynamic(() => import("components/widgets/datetime/datetime")), 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 }) { diff --git a/src/pages/api/widgets/glances.js b/src/pages/api/widgets/glances.js new file mode 100644 index 00000000..86992dd1 --- /dev/null +++ b/src/pages/api/widgets/glances.js @@ -0,0 +1,52 @@ +import { httpProxy } from "utils/proxy/http"; +import createLogger from "utils/logger"; +import { getSettings } from "utils/config/config"; + +const logger = createLogger("glances"); + +export default async function handler(req, res) { + const { id } = req.query; + + let errorMessage; + + let instanceID = "glances"; + if (id) { // multiple instances + instanceID = id; + } + const settings = getSettings(); + const instanceSettings = settings[instanceID]; + if (!instanceSettings) { + errorMessage = id ? `There is no glances section with id '${id}' in settings.yaml` : "There is no glances section in settings.yaml"; + logger.error(errorMessage); + return res.status(400).json({ error: errorMessage }); + } + + const url = instanceSettings?.url; + if (!url) { + 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 (instanceSettings.username && instanceSettings.password) { + headers.Authorization = `Basic ${Buffer.from(`${instanceSettings.username}:${instanceSettings.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); +} diff --git a/src/pages/site.webmanifest.jsx b/src/pages/site.webmanifest.jsx index 10a448a3..80513ddb 100644 --- a/src/pages/site.webmanifest.jsx +++ b/src/pages/site.webmanifest.jsx @@ -1,33 +1,13 @@ import checkAndCopyConfig, { getSettings } from "utils/config/config"; import themes from "utils/styles/themes"; -import { servicesResponse, bookmarksResponse } from "utils/config/api-response"; export async function getServerSideProps({ res }) { checkAndCopyConfig("settings.yaml"); const settings = getSettings(); - const services = await servicesResponse(); - const bookmarks = await bookmarksResponse(); const color = settings.color || "slate"; const theme = settings.theme || "dark"; - const serviceShortcuts = services.map((group) => - group.services.map((service) => ({ - name: service.name, - url: service.href, - description: service.description, - })) - ); - - const bookmarkShortcuts = bookmarks.map((group) => - group.bookmarks.map((service) => ({ - name: service.name, - url: service.href, - })) - ); - - const shortcuts = [...serviceShortcuts, ...bookmarkShortcuts].flat(); - const manifest = { name: settings.title || "Homepage", short_name: settings.title || "Homepage", @@ -43,7 +23,6 @@ export async function getServerSideProps({ res }) { type: "image/png", }, ], - shortcuts, theme_color: themes[color][theme], background_color: themes[color][theme], display: "standalone", diff --git a/src/widgets/components.js b/src/widgets/components.js index c2a6705e..f6e075d8 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -18,6 +18,7 @@ const components = { ombi: dynamic(() => import("./ombi/component")), overseerr: dynamic(() => import("./overseerr/component")), pihole: dynamic(() => import("./pihole/component")), + plex: dynamic(() => import("./plex/component")), portainer: dynamic(() => import("./portainer/component")), prowlarr: dynamic(() => import("./prowlarr/component")), proxmox: dynamic(() => import("./proxmox/component")), diff --git a/src/widgets/plex/component.jsx b/src/widgets/plex/component.jsx new file mode 100644 index 00000000..9de60fd1 --- /dev/null +++ b/src/widgets/plex/component.jsx @@ -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 ; + } + + if (!plexData) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +} diff --git a/src/widgets/plex/proxy.js b/src/widgets/plex/proxy.js new file mode 100644 index 00000000..46ebb27c --- /dev/null +++ b/src/widgets/plex/proxy.js @@ -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); +} diff --git a/src/widgets/plex/widget.js b/src/widgets/plex/widget.js new file mode 100644 index 00000000..de3cf25b --- /dev/null +++ b/src/widgets/plex/widget.js @@ -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; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index a4cab76b..cd7f5b19 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -13,6 +13,7 @@ import nzbget from "./nzbget/widget"; import ombi from "./ombi/widget"; import overseerr from "./overseerr/widget"; import pihole from "./pihole/widget"; +import plex from "./plex/widget"; import portainer from "./portainer/widget"; import prowlarr from "./prowlarr/widget"; import proxmox from "./proxmox/widget"; @@ -46,6 +47,7 @@ const widgets = { ombi, overseerr, pihole, + plex, portainer, prowlarr, proxmox,