Compare commits

..

22 Commits

Author SHA1 Message Date
shamoon
a1a818727b Log websocket protocol upgrade
Some checks are pending
Docker CI / Linting Checks (push) Waiting to run
Docker CI / Docker Build & Push (push) Blocked by required conditions
2026-01-06 17:56:58 -08:00
shamoon
0f5188b140 Minor refactoring 2026-01-06 17:56:58 -08:00
shamoon
6f2878b4f3 Enforce wss for api key 2026-01-06 17:56:58 -08:00
shamoon
9f1172ae9c A little more error handling 2026-01-06 17:56:58 -08:00
shamoon
7bee140166 Update proxy.js 2026-01-06 17:56:58 -08:00
shamoon
b45028c107 Remove this too 2026-01-06 17:56:58 -08:00
shamoon
ce74c974fc Refactor 2026-01-06 17:56:58 -08:00
shamoon
775c68675e Switch to /api/current as that seems less likely to be removed, use pure jsonrpc 2026-01-06 17:56:58 -08:00
shamoon
d24033dec2 Update truenas.md 2026-01-06 17:56:58 -08:00
shamoon
89e82c87ea Handle some JSON parsing issues 2026-01-06 17:56:58 -08:00
shamoon
67ee98ca73 Update TrueNAS widget documentation
Removed outdated API version table for TrueNAS widget.
2026-01-06 17:56:58 -08:00
shamoon
4b1cce7269 [revert] more logging 2026-01-06 17:56:58 -08:00
shamoon
b55aeba8e2 Update proxy.js 2026-01-06 17:56:58 -08:00
shamoon
f6b80550f0 UNify this 2026-01-06 17:56:58 -08:00
shamoon
408a96c355 Fix wait for open 2026-01-06 17:56:58 -08:00
shamoon
8502520904 Remove fallback 2026-01-06 17:56:58 -08:00
shamoon
e545fd7ac7 [revert me] Add some verbose logging 2026-01-06 17:56:58 -08:00
shamoon
7f08f48604 lint 2026-01-06 17:56:58 -08:00
shamoon
6ed2677687 Ok, truenas custom proxy 2026-01-06 17:56:58 -08:00
shamoon
2bc5e0de62 Setup for custom proxy 2026-01-06 17:56:58 -08:00
shamoon
e892432b72 doc 2026-01-06 17:56:58 -08:00
shamoon
189ee5742f Add explicit ws dep 2026-01-06 17:56:58 -08:00
48 changed files with 496 additions and 345 deletions

View File

@@ -5,6 +5,11 @@ description: TrueNas Scale Widget Configuration
Learn more about [TrueNas](https://www.truenas.com/).
| TrueNAS Version | Homepage widget version |
| ----------------------- | ----------------------- |
| < 26.04 (REST API) | 1 (default) |
| > 25.04 (Websocket API) | 2 |
Allowed fields: `["load", "uptime", "alerts"]`.
To create an API Key, follow [the official TrueNAS documentation](https://www.truenas.com/docs/scale/scaletutorials/toptoolbar/managingapikeys/).
@@ -17,6 +22,7 @@ To use the `enablePools` option with TrueNAS Core, the `nasType` parameter is re
widget:
type: truenas
url: http://truenas.host.or.ip
version: 2 # optional, defaults to 1
username: user # not required if using api key
password: pass # not required if using api key
key: yourtruenasapikey # not required if using username / password

View File

@@ -40,6 +40,7 @@
"tough-cookie": "^6.0.0",
"urbackup-server-api": "^0.91.0",
"winston": "^3.17.0",
"ws": "^8.18.3",
"xml-js": "^1.6.11"
},
"devDependencies": {

17
pnpm-lock.yaml generated
View File

@@ -95,6 +95,9 @@ importers:
winston:
specifier: ^3.17.0
version: 3.17.0
ws:
specifier: ^8.18.3
version: 8.18.3
xml-js:
specifier: ^1.6.11
version: 1.6.11
@@ -3011,8 +3014,8 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -3345,7 +3348,7 @@ snapshots:
'@types/tar': 6.1.13
'@types/ws': 8.5.14
form-data: 4.0.2
isomorphic-ws: 5.0.0(ws@8.18.0)
isomorphic-ws: 5.0.0(ws@8.18.3)
js-yaml: 4.1.1
jsonpath-plus: 10.3.0
node-fetch: 2.7.0
@@ -3355,7 +3358,7 @@ snapshots:
tar: 7.4.3
tmp-promise: 3.0.3
tslib: 2.8.1
ws: 8.18.0
ws: 8.18.3
transitivePeerDependencies:
- bufferutil
- encoding
@@ -5033,9 +5036,9 @@ snapshots:
isexe@2.0.0: {}
isomorphic-ws@5.0.0(ws@8.18.0):
isomorphic-ws@5.0.0(ws@8.18.3):
dependencies:
ws: 8.18.0
ws: 8.18.3
iterator.prototype@1.1.5:
dependencies:
@@ -6215,7 +6218,7 @@ snapshots:
wrappy@1.0.2: {}
ws@8.18.0: {}
ws@8.18.3: {}
xml-js@1.6.11:
dependencies:

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Op",
"pending": "Afwagtend",
"down": "Af",
"ok": "Ok"
"down": "Af"
},
"healthchecks": {
"new": "Nuut",
@@ -770,7 +769,7 @@
"gross_percent_today": "Vandag",
"gross_percent_1y": "Een jaar",
"gross_percent_max": "Alle tyd",
"net_worth": "Netto Waarde"
"net_worth": "Net Worth"
},
"audiobookshelf": {
"podcasts": "Podsendinge",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "جديد(ة)",

View File

@@ -63,7 +63,7 @@
"wlan_users": "WLAN Потребители",
"up": "UP",
"down": "DOWN",
"wait": "Моля изчакайте",
"wait": "Please wait",
"empty_data": "Неизвестен статус на подсистема"
},
"docker": {
@@ -83,7 +83,7 @@
"partial": "Частично"
},
"ping": {
"error": "Грешка",
"error": "Error",
"ping": "Пинг",
"down": "Down",
"up": "Up",
@@ -91,11 +91,11 @@
},
"siteMonitor": {
"http_status": "HTTP статус",
"error": "Грешка",
"error": "Error",
"response": "Отговор",
"down": "Down",
"up": "Up",
"not_available": "Не е налично"
"not_available": "Not Available"
},
"emby": {
"playing": "Възпроизвежда",
@@ -111,7 +111,7 @@
"offline": "Offline",
"offline_alt": "Offline",
"online": "Онлайн",
"total": "Общо",
"total": "Total",
"unknown": "Unknown"
},
"evcc": {
@@ -133,7 +133,7 @@
"unread": "Непрочетени"
},
"fritzbox": {
"connectionStatus": "Статус",
"connectionStatus": "Status",
"connectionStatusUnconfigured": "Неконфигуриран",
"connectionStatusConnecting": "Свързване",
"connectionStatusAuthenticating": "Удостоверяване",
@@ -141,7 +141,7 @@
"connectionStatusDisconnecting": "Прекъсване на връзката",
"connectionStatusDisconnected": "Не е свързан",
"connectionStatusConnected": "Свързан",
"uptime": "Време на работа",
"uptime": "Uptime",
"maxDown": "Макс сваляне",
"maxUp": "Макс качване",
"down": "Down",
@@ -170,8 +170,8 @@
"tautulli": {
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Битрейт",
"no_active": "Няма активни потоци",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"plex_connection_error": "Провери връзка с Plex"
},
"omada": {
@@ -189,7 +189,7 @@
"plex": {
"streams": "Активни Потоци",
"albums": "Албуми",
"movies": "Филми",
"movies": "Movies",
"tv": "Сериали"
},
"sabnzbd": {
@@ -362,8 +362,8 @@
},
"trilium": {
"version": "Version",
"notesCount": "Бележки",
"dbSize": "Размер на базата данни",
"notesCount": "Notes",
"dbSize": "Database Size",
"unknown": "Unknown"
},
"navidrome": {
@@ -373,7 +373,7 @@
"npm": {
"enabled": "Активирано",
"disabled": "Деактивирано",
"total": "Общо"
"total": "Total"
},
"coinmarketcap": {
"configure": "Настрой за следене една или повече крипто валути",
@@ -384,7 +384,7 @@
},
"gotify": {
"apps": "Приложения",
"clients": "Клиенти",
"clients": "Clients",
"messages": "Съобщения"
},
"prowlarr": {
@@ -405,7 +405,7 @@
"transferRate": "Rate"
},
"mastodon": {
"user_count": "Потребители",
"user_count": "Users",
"status_count": "Posts",
"domain_count": "Domains"
},
@@ -416,17 +416,17 @@
},
"minecraft": {
"players": "Играчи",
"version": "Версия",
"status": "Статус",
"up": "Онлайн",
"down": "Офлайн"
"version": "Version",
"status": "Status",
"up": "Online",
"down": "Offline"
},
"miniflux": {
"read": "Read",
"unread": "Unread"
},
"authentik": {
"users": "Потребители",
"users": "Users",
"loginsLast24H": "Logins (24h)",
"failedLoginsLast24H": "Failed Logins (24h)"
},
@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Нови",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Nou",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Nové",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Ny",

View File

@@ -45,9 +45,9 @@
"free": "Frei",
"used": "In Benutzung",
"load": "Last",
"temp": "Temp",
"temp": "TEMP",
"max": "Max",
"uptime": "Betriebszeit"
"uptime": "BETRIEBSZEIT"
},
"unifi": {
"users": "Benutzer",
@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Online",
"pending": "Wartend",
"down": "Offline",
"ok": "Ok"
"down": "Offline"
},
"healthchecks": {
"new": "Neu",
@@ -603,7 +602,7 @@
"pangolin": {
"orgs": "Orgs",
"sites": "Sites",
"resources": "Ressourcen",
"resources": "Resources",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Activo",
"pending": "Pendiente",
"down": "Inactivo",
"ok": "Ok"
"down": "Inactivo"
},
"healthchecks": {
"new": "Nuevo",
@@ -602,12 +601,12 @@
},
"pangolin": {
"orgs": "Orgs",
"sites": "Sitios",
"resources": "Recursos",
"targets": "Destinos",
"traffic": "Tráfico",
"in": "Entrante",
"out": "Saliente"
"sites": "Sites",
"resources": "Resources",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",
"out": "Out"
},
"peanut": {
"battery_charge": "Carga de la batería",
@@ -770,7 +769,7 @@
"gross_percent_today": "Hoy",
"gross_percent_1y": "Un año",
"gross_percent_max": "Todo el tiempo",
"net_worth": "Patrimonio neto"
"net_worth": "Net Worth"
},
"audiobookshelf": {
"podcasts": "Podcasts",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "En ligne",
"pending": "En attente",
"down": "Hors ligne",
"ok": "Ok"
"down": "Hors ligne"
},
"healthchecks": {
"new": "Nouveau",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "למעלה",
"pending": "ממתין",
"down": "למטה",
"ok": "Ok"
"down": "למטה"
},
"healthchecks": {
"new": "חדש",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Aktivno",
"pending": "U tijeku",
"down": "Neaktivno",
"ok": "Ok"
"down": "Neaktivno"
},
"healthchecks": {
"new": "Novo",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Fut",
"pending": "Függőben lévő",
"down": "Leállt",
"ok": "Ok"
"down": "Leállt"
},
"healthchecks": {
"new": "Új",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Baru",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Nuovo",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "新着",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "업",
"pending": "대기 중",
"down": "다운",
"ok": "Ok"
"down": "다운"
},
"healthchecks": {
"new": "신규",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Baharu",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Online",
"pending": "In afwachting",
"down": "Offline",
"ok": "Ok"
"down": "Offline"
},
"healthchecks": {
"new": "Nieuw",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Ny",

View File

@@ -61,7 +61,7 @@
"wlan_devices": "Urządzenia WLAN",
"lan_users": "Użytkownicy LAN",
"wlan_users": "Użytkownicy WLAN",
"up": "DZIAŁA",
"up": "UP",
"down": "Pobieranie",
"wait": "Proszę czekać",
"empty_data": "Status podsystemu nieznany"
@@ -69,7 +69,7 @@
"docker": {
"rx": "Rx",
"tx": "Tx",
"mem": "PAM",
"mem": "MEM",
"cpu": "Procesor",
"running": "Działa",
"offline": "Nieosiągalny",
@@ -93,8 +93,8 @@
"http_status": "Status HTTP",
"error": "Błąd",
"response": "Odpowiedź",
"down": "Nie działa",
"up": "Działa",
"down": "Down",
"up": "Up",
"not_available": "Niedostępny"
},
"emby": {
@@ -111,8 +111,8 @@
"offline": "Offline",
"offline_alt": "Offline",
"online": "Dostępny",
"total": "Razem",
"unknown": "Nieznany"
"total": "Total",
"unknown": "Unknown"
},
"evcc": {
"pv_power": "Produkcja",
@@ -141,11 +141,11 @@
"connectionStatusDisconnecting": "Rozłączanie",
"connectionStatusDisconnected": "Rozłączono",
"connectionStatusConnected": "Połączono",
"uptime": "Czas działania",
"uptime": "Uptime",
"maxDown": "Maks. Pobieranie",
"maxUp": "Maks. Wysyłanie",
"down": "Nie działa",
"up": "Działa",
"down": "Down",
"up": "Up",
"received": "Odebrane",
"sent": "Wysłane",
"externalIPAddress": "Pub. IP",
@@ -168,10 +168,10 @@
"passes": "Przebiegi"
},
"tautulli": {
"playing": "Odtwarza",
"transcoding": "Transkoduje",
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "Brak aktywnych strumieni",
"no_active": "No Active Streams",
"plex_connection_error": "Sprawdź połączenie z Plex"
},
"omada": {
@@ -193,24 +193,24 @@
"tv": "Seriale"
},
"sabnzbd": {
"rate": "Szybkość",
"rate": "Rate",
"queue": "Kolejka",
"timeleft": "Pozostało"
},
"rutorrent": {
"active": "Aktywny",
"upload": "Wysyłanie",
"upload": "Upload",
"download": "Pobieranie"
},
"transmission": {
"download": "Pobieranie",
"upload": "Wysyłanie",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"qbittorrent": {
"download": "Pobieranie",
"upload": "Wysyłanie",
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
@@ -223,8 +223,8 @@
"invalid": "Nieprawidłowy"
},
"deluge": {
"download": "Pobieranie",
"upload": "Wysyłanie",
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
@@ -233,8 +233,8 @@
"cachemissbytes": "Straty cache'u"
},
"downloadstation": {
"download": "Pobieranie",
"upload": "Wysyłanie",
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
@@ -251,16 +251,16 @@
"queued": "W kolejce",
"movies": "Filmy",
"queue": "Kolejka",
"unknown": "Nieznane"
"unknown": "Unknown"
},
"lidarr": {
"wanted": "Poszukiwane",
"queued": "W kolejce",
"wanted": "Wanted",
"queued": "Queued",
"artists": "Artyści"
},
"readarr": {
"wanted": "Poszukiwane",
"queued": "W kolejce",
"wanted": "Wanted",
"queued": "Queued",
"books": "Książki"
},
"bazarr": {
@@ -276,7 +276,7 @@
"pending": "Oczekujące",
"approved": "Zaakceptowane",
"available": "Dostępne",
"issues": "Otwarte zgłoszenia"
"issues": "Open Issues"
},
"overseerr": {
"pending": "Oczekujące",
@@ -285,8 +285,8 @@
"available": "Dostępne"
},
"netalertx": {
"total": "Razem",
"connected": "Połączono",
"total": "Total",
"connected": "Connected",
"new_devices": "Nowe urządzenia",
"down_alerts": "Alerty niedostępności"
},
@@ -303,20 +303,20 @@
"latency": "Opóźnienia"
},
"speedtest": {
"upload": "Wysyłanie",
"download": "Pobieranie",
"upload": "Upload",
"download": "Download",
"ping": "Ping"
},
"portainer": {
"running": "Działa",
"running": "Running",
"stopped": "Zatrzymane",
"total": "Razem"
"total": "Total"
},
"suwayomi": {
"download": "Pobrano",
"nondownload": "Niepobrane",
"read": "Przeczytane",
"unread": "Nieprzeczytane",
"read": "Read",
"unread": "Unread",
"downloadedread": "Pobrane i przeczytane",
"downloadedunread": "Pobrane i nieprzeczytane",
"nondownloadedread": "Niepobrane i przeczytane",
@@ -337,7 +337,7 @@
"ago": "{{value}} temu"
},
"technitium": {
"totalQueries": "Zapytania",
"totalQueries": "Queries",
"totalNoError": "Sukces",
"totalServerFailure": "Porażki",
"totalNxDomain": "Domeny NX",
@@ -345,12 +345,12 @@
"totalAuthoritative": "Autorytatywne",
"totalRecursive": "Rekursywne",
"totalCached": "Zbuforowane",
"totalBlocked": "Zablokowane",
"totalBlocked": "Blocked",
"totalDropped": "Upuszczone",
"totalClients": "Klienci"
},
"tdarr": {
"queue": "W kolejce",
"queue": "Queue",
"processed": "Przetworzone",
"errored": "Błędne",
"saved": "Zapisane"
@@ -364,7 +364,7 @@
"version": "Wersja",
"notesCount": "Notatki",
"dbSize": "Rozmiar bazy danych",
"unknown": "Nieznane"
"unknown": "Unknown"
},
"navidrome": {
"nothing_streaming": "Brak aktywnych strumieni",
@@ -373,7 +373,7 @@
"npm": {
"enabled": "Włączone",
"disabled": "Wyłączone",
"total": "Razem"
"total": "Total"
},
"coinmarketcap": {
"configure": "Wybierz jedną lub więcej kryptowalut do śledzenia",
@@ -390,19 +390,19 @@
"prowlarr": {
"enableIndexers": "Indeksery",
"numberOfGrabs": "Pochwycenia",
"numberOfQueries": "Zapytania",
"numberOfQueries": "Queries",
"numberOfFailGrabs": "Nieudane pochwycenia",
"numberOfFailQueries": "Nieudane zapytania"
},
"jackett": {
"configured": "Skonfigurowane",
"errored": "Z błędami"
"errored": "Errored"
},
"strelaysrv": {
"numActiveSessions": "Sesje",
"numConnections": "Połączenia",
"dataRelayed": "Przekazane",
"transferRate": "Szybkość"
"transferRate": "Rate"
},
"mastodon": {
"user_count": "Użytkownicy",
@@ -410,9 +410,9 @@
"domain_count": "Domeny"
},
"medusa": {
"wanted": "Poszukiwane",
"queued": "W kolejce",
"series": "Seriale"
"wanted": "Wanted",
"queued": "Queued",
"series": "Series"
},
"minecraft": {
"players": "Gracze",
@@ -423,7 +423,7 @@
},
"miniflux": {
"read": "Przeczytane",
"unread": "Nieprzeczytane"
"unread": "Unread"
},
"authentik": {
"users": "Użytkownicy",
@@ -443,14 +443,14 @@
"temp": "TEMP.",
"_temp": "Temperatura",
"warn": "Ostrzeżenie",
"uptime": "DZIAŁA",
"total": "Razem",
"uptime": "UP",
"total": "Total",
"free": "Wolne",
"used": "Użyte",
"used": "Used",
"days": "d",
"hours": "godz",
"hours": "h",
"crit": "Krytyczyny",
"read": "Odczyt",
"read": "Read",
"write": "Zapis",
"gpu": "GPU",
"mem": "Pamięć",
@@ -530,16 +530,15 @@
"up_to_date": "Aktualny",
"child_bridges": "Mostki podrzędne",
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Działa",
"pending": "Oczekujące",
"down": "Nie działa",
"ok": "Ok"
"up": "Up",
"pending": "Pending",
"down": "Down"
},
"healthchecks": {
"new": "Nowy",
"up": "Działa",
"up": "Up",
"grace": "W okresie karencji",
"down": "Nie działa",
"down": "Down",
"paused": "Wstrzymane",
"status": "Status",
"last_ping": "Ostatni ping",
@@ -551,63 +550,63 @@
"containers_failed": "Niepowodzenie"
},
"autobrr": {
"approvedPushes": "Zaakceptowane",
"approvedPushes": "Approved",
"rejectedPushes": "Odrzucone",
"filters": "Filtry",
"indexers": "Indeksery"
"indexers": "Indexers"
},
"tubearchivist": {
"downloads": "W kolejce",
"downloads": "Queue",
"videos": "Pliki wideo",
"channels": "Kanały",
"playlists": "Playlisty"
},
"truenas": {
"load": "Obciążenie systemu",
"uptime": "Czas działania",
"alerts": "Alerty"
"uptime": "Uptime",
"alerts": "Alerts"
},
"pyload": {
"speed": "Prędkość",
"active": "Aktywne",
"queue": "W kolejce",
"total": "Razem"
"active": "Active",
"queue": "Queue",
"total": "Total"
},
"gluetun": {
"public_ip": "Adres publiczny",
"region": "Region",
"country": "Państwo",
"port_forwarded": "Port otwarty"
"port_forwarded": "Port Forwarded"
},
"hdhomerun": {
"channels": "Kanały",
"channels": "Channels",
"hd": "HD",
"tunerCount": "Tunery",
"channelNumber": "Kanał",
"channelNetwork": "Sieć",
"signalStrength": "Siła sygnału",
"signalQuality": "Jakość",
"symbolQuality": "Jakość",
"symbolQuality": "Quality",
"networkRate": "Bitrate",
"clientIP": "Klient"
},
"scrutiny": {
"passed": "Powodzenie",
"failed": "Nieudane",
"unknown": "Nieznane"
"failed": "Failed",
"unknown": "Unknown"
},
"paperlessngx": {
"inbox": "Skrzynka odbiorcza",
"total": "Razem"
"total": "Total"
},
"pangolin": {
"orgs": "Organizacje",
"sites": "Strony",
"resources": "Zasoby",
"targets": "Cele",
"traffic": "Ruch",
"in": "Do",
"out": "Z"
"orgs": "Orgs",
"sites": "Sites",
"resources": "Resources",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",
"out": "Out"
},
"peanut": {
"battery_charge": "Stan baterii",
@@ -618,18 +617,18 @@
"low_battery": "Niski poziom baterii"
},
"nextdns": {
"wait": "Proszę czekać",
"wait": "Please Wait",
"no_devices": "Nie otrzymano danych urządzenia"
},
"mikrotik": {
"cpuLoad": "Obciążenie procesora",
"memoryUsed": "Zużyta pamięć",
"uptime": "Czas działania",
"uptime": "Uptime",
"numberOfLeases": "Dzierżawy"
},
"xteve": {
"streams_all": "Wszystkie strumienie",
"streams_active": "Aktywne strumienie",
"streams_active": "Active Streams",
"streams_xepg": "Kanały XEPG"
},
"opendtu": {
@@ -664,9 +663,9 @@
"load": "Śr. Obciążenie",
"memory": "Użycie pamięci",
"wanStatus": "Status WAN",
"up": "Działa",
"down": "Nie działa",
"temp": "Temperatura",
"up": "Up",
"down": "Down",
"temp": "Temp",
"disk": "Użycie dysku",
"wanIP": "WAN IP"
},
@@ -677,38 +676,38 @@
"memory_usage": "Pamięć"
},
"immich": {
"users": "Użytkownicy",
"users": "Users",
"photos": "Zdjęcia",
"videos": "Filmy",
"videos": "Videos",
"storage": "Pamięć"
},
"uptimekuma": {
"up": "Działające",
"down": "Niedziałające",
"uptime": "Czas działania",
"uptime": "Uptime",
"incident": "Incydent",
"m": "min"
"m": "m"
},
"atsumeru": {
"series": "Serie",
"series": "Series",
"archives": "Archiwa",
"chapters": "Rozdziały",
"categories": "Kategorie"
},
"komga": {
"libraries": "Biblioteki",
"series": "Serie",
"books": "Książki"
"series": "Series",
"books": "Books"
},
"diskstation": {
"days": "Dni",
"uptime": "Czas działania",
"volumeAvailable": "Dostępne"
"days": "Days",
"uptime": "Uptime",
"volumeAvailable": "Available"
},
"mylar": {
"series": "Seriale",
"series": "Series",
"issues": "Zgłoszenia",
"wanted": "Poszukiwane"
"wanted": "Wanted"
},
"photoprism": {
"albums": "Albumy",
@@ -717,9 +716,9 @@
"people": "Ludzie"
},
"fileflows": {
"queue": "W kolejce",
"processing": "Przetwarzane",
"processed": "Przetworzone",
"queue": "Queue",
"processing": "Processing",
"processed": "Processed",
"time": "Czas"
},
"firefly": {
@@ -745,7 +744,7 @@
"size": "Rozmiar",
"lastrun": "Ostatnie uruchomienie",
"nextrun": "Następne uruchomienie",
"failed": "Nieudane"
"failed": "Failed"
},
"unmanic": {
"active_workers": "Aktywni pracownicy",
@@ -762,15 +761,15 @@
"targets_total": "Wszystkich Celi"
},
"gatus": {
"up": "Działające strony",
"down": "Niedziałające strony",
"uptime": "Czas działania"
"up": "Sites Up",
"down": "Sites Down",
"uptime": "Uptime"
},
"ghostfolio": {
"gross_percent_today": "Dzisiaj",
"gross_percent_1y": "Rok",
"gross_percent_max": "Od początku",
"net_worth": "Wartość netto"
"net_worth": "Net Worth"
},
"audiobookshelf": {
"podcasts": "Podcasty",
@@ -785,22 +784,22 @@
},
"whatsupdocker": {
"monitoring": "Monitoring",
"updates": "Aktualizacje"
"updates": "Updates"
},
"calibreweb": {
"books": "Książki",
"authors": "Autorzy",
"categories": "Kategorie",
"series": "Serie"
"series": "Series"
},
"jdownloader": {
"downloadCount": "W kolejce",
"downloadBytesRemaining": "Pozostało",
"downloadTotalBytes": "Rozmiar",
"downloadCount": "Queue",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size",
"downloadSpeed": "Prędkość"
},
"kavita": {
"seriesCount": "Serie",
"seriesCount": "Series",
"totalFiles": "Pliki"
},
"azuredevops": {
@@ -814,7 +813,7 @@
"inProgress": "W trakcie",
"totalPrs": "Łącznie PRs",
"myPrs": "Moje PRs",
"approved": "Zaakceptowane"
"approved": "Approved"
},
"gamedig": {
"status": "Status",
@@ -842,33 +841,33 @@
},
"openmediavault": {
"downloading": "Pobieranie",
"total": "Razem",
"running": "Działające",
"stopped": "Zatrzymane",
"passed": "Zaliczony",
"failed": "Nieudany"
"total": "Total",
"running": "Running",
"stopped": "Stopped",
"passed": "Passed",
"failed": "Failed"
},
"openwrt": {
"uptime": "Czas działania",
"uptime": "Uptime",
"cpuLoad": "Śr. obciążenie CPU (5m)",
"up": "Działa",
"down": "Nie działa",
"up": "Up",
"down": "Down",
"bytesTx": "Przesłane",
"bytesRx": "Odebrano"
"bytesRx": "Received"
},
"uptimerobot": {
"status": "Status",
"uptime": "Czas działania",
"uptime": "Uptime",
"lastDown": "Ostatni downtime",
"downDuration": "Długość downtime'u",
"sitesUp": "Działające strony",
"sitesDown": "Niedziałające strony",
"paused": "Zatrzymane",
"sitesUp": "Sites Up",
"sitesDown": "Sites Down",
"paused": "Paused",
"notyetchecked": "Nie sprawdzono",
"up": "Działa",
"up": "Up",
"seemsdown": "Możliwe, że wyłączony",
"down": "Nie działa",
"unknown": "Nieznane"
"down": "Down",
"unknown": "Unknown"
},
"calendar": {
"inCinemas": "W kinach",
@@ -887,10 +886,10 @@
"totalfilesize": "Rozmiar całkowity"
},
"mailcow": {
"domains": "Domeny",
"domains": "Domains",
"mailboxes": "Skrzynki",
"mails": "Poczta",
"storage": "Pamięć"
"storage": "Storage"
},
"netdata": {
"warnings": "Ostrzeżenia",
@@ -899,12 +898,12 @@
"plantit": {
"events": "Wydarzenia",
"plants": "Rośliny",
"photos": "Zdjęcia",
"photos": "Photos",
"species": "Gatunki"
},
"gitea": {
"notifications": "Powiadomienia",
"issues": "Zgłoszenia",
"issues": "Issues",
"pulls": "Żądania Pull",
"repositories": "Repozytoria"
},
@@ -920,13 +919,13 @@
"galleries": "Galerie",
"performers": "Artyści",
"studios": "Studia",
"movies": "Filmy",
"tags": "Tagi",
"movies": "Movies",
"tags": "Tags",
"oCount": "O Licznik"
},
"tandoor": {
"users": "Użytkownicy",
"recipes": "Przepisy",
"users": "Users",
"recipes": "Recipes",
"keywords": "Słowa kluczowe"
},
"homebox": {
@@ -934,18 +933,18 @@
"totalWithWarranty": "Z gwarancją",
"locations": "Lokalizacje",
"labels": "Etykiety",
"users": "Użytkownicy",
"users": "Users",
"totalValue": "Wartość całkowita"
},
"crowdsec": {
"alerts": "Alerty",
"alerts": "Alerts",
"bans": "Bany"
},
"wgeasy": {
"connected": "Połączonych",
"enabled": "Włączone",
"disabled": "Wyłączone",
"total": "Razem"
"connected": "Connected",
"enabled": "Enabled",
"disabled": "Disabled",
"total": "Total"
},
"swagdashboard": {
"proxied": "Proxy",
@@ -967,7 +966,7 @@
},
"frigate": {
"cameras": "Kamery",
"uptime": "Czas działania",
"uptime": "Uptime",
"version": "Wersja"
},
"linkwarden": {
@@ -977,7 +976,7 @@
},
"zabbix": {
"unclassified": "Niezaklasyfikowane",
"information": "Informacja",
"information": "Information",
"warning": "Ostrzeżenie",
"average": "Średnia",
"high": "Wysokie",
@@ -1008,14 +1007,14 @@
"beszel": {
"name": "Nazwa",
"systems": "Systemy",
"up": "Działa",
"down": "Nie działa",
"paused": "Wstrzymane",
"pending": "Oczekujące",
"up": "Up",
"down": "Down",
"paused": "Paused",
"pending": "Pending",
"status": "Status",
"updated": "Zaktualizowane",
"updated": "Updated",
"cpu": "Procesor",
"memory": "PAM",
"memory": "MEM",
"disk": "Dysk",
"network": "NET"
},
@@ -1023,14 +1022,14 @@
"apps": "Aplikacje",
"synced": "Synchronizowane",
"outOfSync": "Bez synchronizacji",
"healthy": "Zdrowe",
"healthy": "Healthy",
"degraded": "Zdegradowane",
"progressing": "Postępujące",
"missing": "Brakujące",
"missing": "Missing",
"suspended": "Zawieszone"
},
"spoolman": {
"loading": "Ładowanie"
"loading": "Loading"
},
"gitlab": {
"groups": "Grupy",
@@ -1040,9 +1039,9 @@
},
"apcups": {
"status": "Status",
"load": "Obciążenie",
"bcharge": "Naładowanie baterii",
"timeleft": "Pozostały czas"
"load": "Load",
"bcharge": "Battery Charge",
"timeleft": "Time Left"
},
"karakeep": {
"bookmarks": "Zakładki",
@@ -1053,11 +1052,11 @@
"tags": "Tagi"
},
"slskd": {
"slskStatus": "Sieć",
"connected": "Połączono",
"disconnected": "Rozłączono",
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Aktualizacja",
"update_yes": "Dostępne",
"update_yes": "Available",
"update_no": "Aktualny",
"downloads": "Pobieranie",
"uploads": "Przesyłanie",
@@ -1070,65 +1069,65 @@
"other": "Inne"
},
"checkmk": {
"serviceErrors": "Problem z usługą",
"hostErrors": "Problemy hosta"
"serviceErrors": "Service issues",
"hostErrors": "Host issues"
},
"komodo": {
"total": "Razem",
"running": "Działające",
"stopped": "Zatrzymane",
"down": "Nie działa",
"unhealthy": "Uszkodzony",
"unknown": "Nieznane",
"total": "Total",
"running": "Running",
"stopped": "Stopped",
"down": "Down",
"unhealthy": "Unhealthy",
"unknown": "Unknown",
"servers": "Serwery",
"stacks": "Stosy",
"containers": "Kontenery"
"stacks": "Stacks",
"containers": "Containers"
},
"filebrowser": {
"available": "Dostępne",
"used": "Użyte",
"total": "Razem"
"available": "Available",
"used": "Used",
"total": "Total"
},
"wallos": {
"activeSubscriptions": "Subskrypcje",
"thisMonthlyCost": "Ten Miesiąc",
"nextMonthlyCost": "Następny miesiąc",
"previousMonthlyCost": "Poprzedni miesiąc",
"nextRenewingSubscription": "Następna płatność"
"activeSubscriptions": "Subscriptions",
"thisMonthlyCost": "This Month",
"nextMonthlyCost": "Next Month",
"previousMonthlyCost": "Prev. Month",
"nextRenewingSubscription": "Next Payment"
},
"unraid": {
"STARTED": "Rozpoczęte",
"STOPPED": "Zatrzymane",
"NEW_ARRAY": "Nowa macierz",
"RECON_DISK": "Odbudowa dysku",
"DISABLE_DISK": "Dysk wyłączony",
"SWAP_DSBL": "Przestrzeń wymiany wyłączona",
"INVALID_EXPANSION": "Nieprawidłowe rozszerzenie",
"PARITY_NOT_BIGGEST": "Parzystość nie największa",
"TOO_MANY_MISSING_DISKS": "Zbyt wiele brakujących dysków",
"NEW_DISK_TOO_SMALL": "Nowy dysk zbyt mały",
"NO_DATA_DISKS": "Brak dysków danych",
"notifications": "Powiadomienia",
"STARTED": "Started",
"STOPPED": "Stopped",
"NEW_ARRAY": "New Array",
"RECON_DISK": "Reconstructing Disk",
"DISABLE_DISK": "Disk Disabled",
"SWAP_DSBL": "Swap Disable",
"INVALID_EXPANSION": "Invalid Expansion",
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
"NO_DATA_DISKS": "No Data Disks",
"notifications": "Notifications",
"status": "Status",
"cpu": "CPU",
"memoryUsed": "Użyta pamięć",
"memoryAvailable": "Dostępna pamięć",
"arrayUsed": "Użyto macierzy",
"arrayFree": "Wolne na macierzy",
"poolUsed": "Użyto {{pool}}",
"poolFree": "{{pool}} Wolne"
"memoryUsed": "Memory Used",
"memoryAvailable": "Memory Available",
"arrayUsed": "Array Used",
"arrayFree": "Array Free",
"poolUsed": "{{pool}} Used",
"poolFree": "{{pool}} Free"
},
"backrest": {
"num_plans": "Planowane",
"num_success_30": "Powodzenia",
"num_failure_30": "Niepowodzenia",
"num_success_latest": "Powodzenie",
"num_failure_latest": "Niepowodzenie",
"bytes_added_30": "Dodane bajty"
"num_plans": "Plans",
"num_success_30": "Successes",
"num_failure_30": "Failures",
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Piosenki",
"time": "Czas",
"artists": "Wykonawcy"
"songs": "Songs",
"time": "Time",
"artists": "Artists"
}
}

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Novo",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Ativo",
"pending": "Pendente",
"down": "Inativo",
"ok": "Ok"
"down": "Inativo"
},
"healthchecks": {
"new": "Novo",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Nou",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "В сети",
"pending": "Ожидают",
"down": "Не в сети",
"ok": "Ok"
"down": "Не в сети"
},
"healthchecks": {
"new": "Новый",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Beží",
"pending": "Čakajúce",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Nový",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Nov",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Горе",
"pending": "На чекању",
"down": "Доле",
"ok": "Ok"
"down": "Доле"
},
"healthchecks": {
"new": "Сада",
@@ -601,13 +600,13 @@
"total": "Укупно"
},
"pangolin": {
"orgs": "Организације",
"sites": "Сајтови",
"resources": "Ресурси",
"targets": "Циљеви",
"traffic": "Саобраћај",
"in": "Улазак",
"out": "Излазак"
"orgs": "Orgs",
"sites": "Sites",
"resources": "Resources",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",
"out": "Out"
},
"peanut": {
"battery_charge": "Напуњеност батерије",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Çalışıyor",
"pending": "Bekleyen",
"down": "Çalışmayan",
"ok": "Ok"
"down": "Çalışmayan"
},
"healthchecks": {
"new": "Yeni",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "Новий",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "New",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "新建立",

View File

@@ -364,7 +364,7 @@
"version": "版本",
"notesCount": "笔记",
"dbSize": "数据库大小",
"unknown": "未知"
"unknown": "Unknown"
},
"navidrome": {
"nothing_streaming": "",
@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "新建立",
@@ -801,7 +800,7 @@
},
"kavita": {
"seriesCount": "系列",
"totalFiles": "文件"
"totalFiles": "Files"
},
"azuredevops": {
"result": "Result",
@@ -1098,7 +1097,7 @@
},
"unraid": {
"STARTED": "Started",
"STOPPED": "已停止",
"STOPPED": "Stopped",
"NEW_ARRAY": "New Array",
"RECON_DISK": "Reconstructing Disk",
"DISABLE_DISK": "Disk Disabled",

View File

@@ -532,8 +532,7 @@
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
"down": "Down"
},
"healthchecks": {
"new": "新建",

View File

@@ -12,8 +12,8 @@ export default function Component({ service }) {
const { data: alertData, error: alertError } = useWidgetAPI(widget, "alerts");
const { data: statusData, error: statusError } = useWidgetAPI(widget, "status");
const { data: poolsData, error: poolsError } = useWidgetAPI(widget, widget?.enablePools ? "pools" : null);
const { data: datasetData, error: datasetError } = useWidgetAPI(widget, widget?.enablePools ? "dataset" : null);
const { data: poolsData, error: poolsError } = useWidgetAPI(widget, widget?.enablePools ? "pools" : "");
const { data: datasetData, error: datasetError } = useWidgetAPI(widget, widget?.enablePools ? "dataset" : "");
if (alertError || statusError || poolsError) {
const finalError = alertError ?? statusError ?? poolsError ?? datasetError;

View File

@@ -0,0 +1,172 @@
import WebSocket from "ws";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers";
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
import validateWidgetData from "utils/proxy/validate-widget-data";
import widgets from "widgets/widgets";
const logger = createLogger("truenasProxyHandler");
function waitForEvent(ws, handler, { event = "message", parseJson = true } = {}) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
cleanup();
reject(new Error("TrueNAS websocket wait timed out"));
}, 10000);
const handleEvent = (payload) => {
try {
let parsed = payload;
if (parseJson) {
if (Buffer.isBuffer(payload)) {
parsed = JSON.parse(payload.toString());
} else if (typeof payload === "string") {
parsed = JSON.parse(payload);
}
logger.info("Received TrueNAS websocket message: %o", parsed);
} else {
logger.info("Received TrueNAS websocket message: %o", payload);
}
const handlerResult = handler(parsed);
if (handlerResult !== undefined) {
cleanup();
if (handlerResult instanceof Error) {
reject(handlerResult);
} else {
resolve(handlerResult);
}
}
} catch (err) {
cleanup();
reject(err);
}
};
const handleError = (err) => {
cleanup();
logger.error("TrueNAS websocket error: %s", err?.message ?? err);
reject(err);
};
const handleClose = () => {
cleanup();
logger.error("TrueNAS websocket connection closed unexpectedly");
reject(new Error("TrueNAS websocket closed the connection"));
};
function cleanup() {
clearTimeout(timeout);
ws.off(event, handleEvent);
ws.off("error", handleError);
ws.off("close", handleClose);
}
ws.on(event, handleEvent);
ws.on("error", handleError);
ws.on("close", handleClose);
});
}
let nextId = 1;
async function sendMethod(ws, method, params = []) {
const id = nextId++;
const payload = { jsonrpc: "2.0", id, method, params };
logger.info("Sending TrueNAS websocket method %s with id %d", method, id);
ws.send(JSON.stringify(payload));
return waitForEvent(ws, (message) => {
if (message?.id !== id) return undefined;
if (message?.error) {
return new Error(message.error?.message || JSON.stringify(message.error));
}
return message?.result ?? message;
});
}
async function authenticate(ws, widget) {
if (widget?.key) {
try {
const apiKeyResult = await sendMethod(ws, "auth.login_with_api_key", [widget.key]);
if (apiKeyResult === true) return;
logger.warn("TrueNAS API key authentication failed, falling back to username/password when available.");
} catch (err) {
logger.warn("TrueNAS API key authentication failed: %s", err?.message ?? err);
}
}
if (widget?.username && widget?.password) {
const loginResult = await sendMethod(ws, "auth.login", [widget.username, widget.password]);
if (loginResult === true) return;
logger.warn("TrueNAS username/password authentication failed.");
}
throw new Error("TrueNAS authentication failed");
}
export default async function truenasProxyHandler(req, res, map) {
const { group, service, endpoint, index } = req.query;
if (!group || !service) {
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
return res.status(400).json({ error: "Invalid proxy service type" });
}
const widget = await getServiceWidget(group, service, index);
if (!widget) {
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
return res.status(400).json({ error: "Invalid proxy service type" });
}
if (!endpoint) {
return res.status(204).end();
}
const version = Number(widget.version ?? 1);
if (Number.isNaN(version) || version < 2) {
// Use legacy REST proxy for version 1
return credentialedProxyHandler(req, res, map);
}
const mappingEntry = Object.values(widgets[widget.type].mappings).find((mapping) => mapping.endpoint === endpoint);
const wsMethod = mappingEntry.wsMethod;
if (!wsMethod) {
logger.debug("Missing wsMethod mapping for TrueNAS endpoint %s", endpoint);
return res.status(500).json({ error: "Missing wsMethod mapping." });
}
try {
let data;
const wsUrl = new URL(formatApiCall(widgets[widget.type].wsAPI, { ...widget }));
const useSecure = wsUrl.protocol === "https:" || Boolean(widget.key); // API key requires secure connection
if (useSecure && wsUrl.protocol !== "https:")
logger.info("Upgrading TrueNAS websocket connection to secure wss://");
wsUrl.protocol = useSecure ? "wss:" : "ws:";
logger.info("Connecting to TrueNAS websocket at %s", wsUrl);
const ws = new WebSocket(wsUrl, { rejectUnauthorized: false });
await waitForEvent(ws, () => true, { event: "open", parseJson: false }); // wait for open
logger.info("Connected to TrueNAS websocket at %s", wsUrl);
try {
await authenticate(ws, widget);
data = await sendMethod(ws, wsMethod);
} finally {
ws.close();
}
if (!validateWidgetData(widget, endpoint, data)) {
return res.status(500).json({ error: { message: "Invalid data", url: sanitizeErrorURL(widget.url), data } });
}
if (map) data = map(data);
return res.status(200).json(data);
} catch (err) {
if (err?.status) {
return res.status(err.status).json({ error: err.message });
}
logger.warn("Websocket call for TrueNAS failed: %s", err?.message ?? err);
return res.status(500).json({ error: err?.message ?? "TrueNAS websocket call failed" });
}
}

View File

@@ -1,32 +1,43 @@
import truenasProxyHandler from "./proxy";
import { asJson, jsonArrayFilter } from "utils/proxy/api-helpers";
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/v2.0/{endpoint}",
proxyHandler: credentialedProxyHandler,
wsAPI: "{url}/api/current",
proxyHandler: truenasProxyHandler,
mappings: {
alerts: {
endpoint: "alert/list",
map: (data) => ({
pending: jsonArrayFilter(data, (item) => item?.dismissed === false).length,
}),
wsMethod: "alert.list",
map: (data) => {
if (Array.isArray(data)) {
return { pending: data.filter((item) => item?.dismissed === false).length };
}
return { pending: jsonArrayFilter(data, (item) => item?.dismissed === false).length };
},
},
status: {
endpoint: "system/info",
wsMethod: "system.info",
validate: ["loadavg", "uptime_seconds"],
},
pools: {
endpoint: "pool",
map: (data) =>
asJson(data).map((entry) => ({
wsMethod: "pool.query",
map: (data) => {
const list = Array.isArray(data) ? data : asJson(data);
return list.map((entry) => ({
id: entry.name,
name: entry.name,
healthy: entry.healthy,
})),
}));
},
},
dataset: {
endpoint: "pool/dataset",
wsMethod: "pool.dataset.query",
},
},
};