diff --git a/public/locales/ar/common.json b/public/locales/ar/common.json
index 6d565287..b099cf51 100644
--- a/public/locales/ar/common.json
+++ b/public/locales/ar/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "نوع القطعة مفقود: {{type}}",
"api_error": "API خطأ",
- "status": "الحالة"
+ "status": "الحالة",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "الموقع الحالي",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/bg/common.json b/public/locales/bg/common.json
index 0365e329..c7824518 100644
--- a/public/locales/bg/common.json
+++ b/public/locales/bg/common.json
@@ -12,7 +12,11 @@
"widget": {
"missing_type": "Липсваща приставка: {{type}}",
"api_error": "API Грешка",
- "status": "Статус"
+ "status": "Статус",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Текущо местоположение",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/ca/common.json b/public/locales/ca/common.json
index fac4a04c..f9aa5969 100644
--- a/public/locales/ca/common.json
+++ b/public/locales/ca/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Falta el tipus de widget: {{type}}",
"api_error": "Error d'API",
- "status": "Estat"
+ "status": "Estat",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"allow": "Feu clic per permetre",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/cs/common.json b/public/locales/cs/common.json
index 7e9da1b5..fd11ea9d 100644
--- a/public/locales/cs/common.json
+++ b/public/locales/cs/common.json
@@ -14,7 +14,11 @@
"widget": {
"missing_type": "Chybí typ widgetu: {{type}}",
"api_error": "Chyba API",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Aktuální poloha",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/da/common.json b/public/locales/da/common.json
index 99db4725..de0ac1d2 100644
--- a/public/locales/da/common.json
+++ b/public/locales/da/common.json
@@ -138,7 +138,11 @@
"widget": {
"missing_type": "Manglende Widget Type: {{type}}",
"api_error": "API fejl",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Nuværende lokation",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/de/common.json b/public/locales/de/common.json
index f8c59567..22402020 100644
--- a/public/locales/de/common.json
+++ b/public/locales/de/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Fehlender Widget-Typ: {{type}}",
"api_error": "API-Fehler",
- "status": "Status"
+ "status": "Status",
+ "url": "URL",
+ "information": "Information",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"search": {
"placeholder": "Suche…"
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index cc946e78..05c0fe5d 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -13,7 +13,11 @@
"widget": {
"missing_type": "Missing Widget Type: {{type}}",
"api_error": "API Error",
- "status": "Status"
+ "information": "Information",
+ "status": "Status",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Current Location",
@@ -330,5 +334,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/es/common.json b/public/locales/es/common.json
index 4e98e30e..e348d534 100644
--- a/public/locales/es/common.json
+++ b/public/locales/es/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Falta el tipo de widget: {{type}}",
"api_error": "Error de API",
- "status": "Estado"
+ "status": "Estado",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"search": {
"placeholder": "Buscar…"
@@ -316,8 +320,12 @@
"total": "Total"
},
"gluetun": {
- "public_ip": "Public IP",
- "region": "Region",
- "country": "Country"
+ "public_ip": "IP pública",
+ "region": "Región",
+ "country": "País"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/fi/common.json b/public/locales/fi/common.json
index 07b480f0..d35449e8 100644
--- a/public/locales/fi/common.json
+++ b/public/locales/fi/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Puuttuva härpäkkeen tyyppi: {{type}}",
"api_error": "API-virhe",
- "status": "Tila"
+ "status": "Tila",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Nykyinen sijainti",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json
index b9b61611..e56e056a 100644
--- a/public/locales/fr/common.json
+++ b/public/locales/fr/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Type de widget manquant: {{type}}",
"api_error": "Erreur de l'API",
- "status": "Statut"
+ "status": "Statut",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"search": {
"placeholder": "Recherche…"
@@ -316,8 +320,12 @@
"total": "Total"
},
"gluetun": {
- "public_ip": "Public IP",
- "region": "Region",
- "country": "Country"
+ "public_ip": "IP Publique",
+ "region": "Région",
+ "country": "Pays"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/he/common.json b/public/locales/he/common.json
index 296bfc4e..5c89ac33 100644
--- a/public/locales/he/common.json
+++ b/public/locales/he/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "סוג ווידג'ט חסר: {{type}}",
"api_error": "שגיאת API",
- "status": "סטטוס"
+ "status": "סטטוס",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "מיקום נוכחי",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/hi/common.json b/public/locales/hi/common.json
index d3075aba..7289852f 100644
--- a/public/locales/hi/common.json
+++ b/public/locales/hi/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Missing Widget Type: {{type}}",
"api_error": "API Error",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Current Location",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/hr/common.json b/public/locales/hr/common.json
index 12c97262..191a3d79 100644
--- a/public/locales/hr/common.json
+++ b/public/locales/hr/common.json
@@ -58,7 +58,11 @@
"widget": {
"missing_type": "Nedostajuća vrsta widgeta: {{type}}",
"api_error": "API greška",
- "status": "Stanje"
+ "status": "Stanje",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"docker": {
"rx": "RX",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/hu/common.json b/public/locales/hu/common.json
index f00dc975..6ba895ae 100644
--- a/public/locales/hu/common.json
+++ b/public/locales/hu/common.json
@@ -30,7 +30,11 @@
"widget": {
"missing_type": "Hiányzó Widget Típus: {{type}}",
"api_error": "API Hiba",
- "status": "Státusz"
+ "status": "Státusz",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Aktuális hely",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/it/common.json b/public/locales/it/common.json
index 9478d420..0bf60639 100644
--- a/public/locales/it/common.json
+++ b/public/locales/it/common.json
@@ -36,7 +36,11 @@
"widget": {
"missing_type": "Tipo del Widget Mancante: {{type}}",
"api_error": "Errore API",
- "status": "Stato"
+ "status": "Stato",
+ "url": "URL",
+ "information": "Information",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"search": {
"placeholder": "Cerca…"
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/ms/common.json b/public/locales/ms/common.json
index 761d85e9..cb6bbb2a 100644
--- a/public/locales/ms/common.json
+++ b/public/locales/ms/common.json
@@ -125,7 +125,11 @@
"widget": {
"missing_type": "Jenis Widget Hilang: {{type}}",
"api_error": "Masalah API",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Lokasi Sekarang",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/nb-NO/common.json b/public/locales/nb-NO/common.json
index 0f7680f7..813422e2 100644
--- a/public/locales/nb-NO/common.json
+++ b/public/locales/nb-NO/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Manglende miniprogramstype: {{type}}",
"api_error": "API-feil",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"search": {
"placeholder": "Søk …"
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json
index ae5cd06e..5502dd8b 100644
--- a/public/locales/nl/common.json
+++ b/public/locales/nl/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Missing Widget Type: {{type}}",
"api_error": "API Error",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"resources": {
"total": "Totaal",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/pl/common.json b/public/locales/pl/common.json
index 41495b3e..6754fcda 100644
--- a/public/locales/pl/common.json
+++ b/public/locales/pl/common.json
@@ -52,7 +52,11 @@
"widget": {
"missing_type": "Brakujący typ widżetu: {{type}}",
"api_error": "Błąd API",
- "status": "Stan"
+ "status": "Stan",
+ "url": "URL",
+ "information": "Information",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"docker": {
"rx": "RX",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/pt-BR/common.json b/public/locales/pt-BR/common.json
index a73d6b48..f6101ee0 100644
--- a/public/locales/pt-BR/common.json
+++ b/public/locales/pt-BR/common.json
@@ -30,7 +30,11 @@
"widget": {
"missing_type": "Tipo de Widget ausente: {{type}}",
"api_error": "Erro da API",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Localização atual",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json
index 5f22a5b8..1431ac43 100644
--- a/public/locales/pt/common.json
+++ b/public/locales/pt/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Widget ausente: {{type}}",
"api_error": "Erro da API",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"search": {
"placeholder": "Pesquisar…"
@@ -330,5 +334,9 @@
"region": "Region",
"country": "Country",
"public_ip": "Public IP"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/ro/common.json b/public/locales/ro/common.json
index a57ea01b..c98705f2 100644
--- a/public/locales/ro/common.json
+++ b/public/locales/ro/common.json
@@ -60,7 +60,11 @@
"widget": {
"missing_type": "Lipsește Tipul de Widget: {{type}}",
"api_error": "Eroare API",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"search": {
"placeholder": "Caută…"
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json
index 762b3e90..ac9a9823 100644
--- a/public/locales/ru/common.json
+++ b/public/locales/ru/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Отсутствует тип виджета: {{type}}",
"api_error": "Ошибка API",
- "status": "Статус"
+ "status": "Статус",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"search": {
"placeholder": "Поиск…"
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/sr/common.json b/public/locales/sr/common.json
index 8fe0a6e0..ed646d45 100644
--- a/public/locales/sr/common.json
+++ b/public/locales/sr/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Missing Widget Type: {{type}}",
"api_error": "API Error",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Current Location",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json
index 87f2061f..6bdc8ee0 100644
--- a/public/locales/sv/common.json
+++ b/public/locales/sv/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Saknar Widget-typ: {{type}}",
"api_error": "API-fel",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Nuvarande plats",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/te/common.json b/public/locales/te/common.json
index 7ad2dace..d6ab211f 100644
--- a/public/locales/te/common.json
+++ b/public/locales/te/common.json
@@ -19,7 +19,11 @@
"widget": {
"missing_type": "విడ్జెట్ లేదు: {{type}}",
"api_error": "API లోపం",
- "status": "హోదా"
+ "status": "హోదా",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "ప్రస్తుత స్తలం",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/tr/common.json b/public/locales/tr/common.json
index f431239d..053bd1b6 100644
--- a/public/locales/tr/common.json
+++ b/public/locales/tr/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Kayıp Araç Türü: {{type}}",
"api_error": "API Hatası",
- "status": "Durum"
+ "status": "Durum",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Mevcut Konum",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/vi/common.json b/public/locales/vi/common.json
index cf3db360..228e7d93 100644
--- a/public/locales/vi/common.json
+++ b/public/locales/vi/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Thiếu loại Widget: {{type}}",
"api_error": "Lỗi API",
- "status": "Trạng thái"
+ "status": "Trạng thái",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"search": {
"placeholder": "Tìm kiếm…"
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/yue/common.json b/public/locales/yue/common.json
index 7932f908..6c7d84de 100644
--- a/public/locales/yue/common.json
+++ b/public/locales/yue/common.json
@@ -20,7 +20,11 @@
"widget": {
"missing_type": "缺少小部件類型:{{type}}",
"api_error": "API 錯誤",
- "status": "狀況"
+ "status": "狀況",
+ "url": "URL",
+ "information": "Information",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "依家位置",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/zh-CN/common.json b/public/locales/zh-CN/common.json
index 5459d7c6..2337bb02 100644
--- a/public/locales/zh-CN/common.json
+++ b/public/locales/zh-CN/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "缺少小部件类型:{{type}}",
"api_error": "API错误",
- "status": "状态"
+ "status": "状态",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"search": {
"placeholder": "搜索…"
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/public/locales/zh-Hant/common.json b/public/locales/zh-Hant/common.json
index c4673c32..cd508662 100644
--- a/public/locales/zh-Hant/common.json
+++ b/public/locales/zh-Hant/common.json
@@ -2,7 +2,11 @@
"widget": {
"missing_type": "Missing Widget Type: {{type}}",
"api_error": "API Error",
- "status": "Status"
+ "status": "Status",
+ "information": "Information",
+ "url": "URL",
+ "raw_error": "Raw Error",
+ "response_data": "Response Data"
},
"weather": {
"current": "Current Location",
@@ -319,5 +323,9 @@
"public_ip": "Public IP",
"region": "Region",
"country": "Country"
+ },
+ "hdhomerun": {
+ "channels": "Channels",
+ "hd": "HD"
}
}
diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx
index be6cf6c5..6b8a2e1c 100644
--- a/src/components/services/item.jsx
+++ b/src/components/services/item.jsx
@@ -97,7 +97,7 @@ export default function Item({ service }) {
{service.container && service.server && (
diff --git a/src/components/services/widget/container.jsx b/src/components/services/widget/container.jsx
index 60536e86..945b8f6f 100644
--- a/src/components/services/widget/container.jsx
+++ b/src/components/services/widget/container.jsx
@@ -1,10 +1,8 @@
+import Error from "./error";
+
export default function Container({ error = false, children, service }) {
if (error) {
- return (
-
- );
+ return
}
let visibleChildren = children;
diff --git a/src/components/services/widget/error.jsx b/src/components/services/widget/error.jsx
new file mode 100644
index 00000000..cf5e1366
--- /dev/null
+++ b/src/components/services/widget/error.jsx
@@ -0,0 +1,50 @@
+import { useTranslation } from "react-i18next";
+import { IoAlertCircle } from "react-icons/io5";
+
+function displayError(error) {
+ return JSON.stringify(error[1] ? error[1] : error, null, 4);
+}
+
+function displayData(data) {
+ return (data.type === 'Buffer') ? Buffer.from(data).toString() : JSON.stringify(data, 4);
+}
+
+export default function Error({ error }) {
+ const { t } = useTranslation();
+
+ if (error?.data?.error) {
+ error = error.data.error; // eslint-disable-line no-param-reassign
+ }
+
+ return (
+
+
+
+ {t("widget.api_error")} {error.message && t("widget.information")}
+
+
+
+
+ );
+}
diff --git a/src/components/widgets/unifi_console/unifi_console.jsx b/src/components/widgets/unifi_console/unifi_console.jsx
index 3e3e135e..af8fd458 100644
--- a/src/components/widgets/unifi_console/unifi_console.jsx
+++ b/src/components/widgets/unifi_console/unifi_console.jsx
@@ -12,7 +12,7 @@ export default function Widget({ options }) {
options.type = "unifi_console";
const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index });
- if (statsError || statsData?.error) {
+ if (statsError) {
return (
diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx
index 7e02a005..8e34e2bc 100644
--- a/src/pages/_document.jsx
+++ b/src/pages/_document.jsx
@@ -9,9 +9,6 @@ export default function Document() {
content="A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations."
/>
-
-
-
diff --git a/src/pages/api/docker/stats/[...service].js b/src/pages/api/docker/stats/[...service].js
index ca8c8bd3..d214ffb2 100644
--- a/src/pages/api/docker/stats/[...service].js
+++ b/src/pages/api/docker/stats/[...service].js
@@ -46,7 +46,7 @@ export default async function handler(req, res) {
});
} catch {
res.status(500).send({
- error: "unknown error",
+ error: {message: "Unknown error"},
});
}
}
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index d9e79786..e891e2f9 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -219,9 +219,17 @@ function Home({ initialSettings }) {
{initialSettings.title || "Homepage"}
{initialSettings.base && }
{initialSettings.favicon ? (
-
+ <>
+
+
+ >
) : (
-
+ <>
+
+
+
+
+ >
)}
group.name),
@@ -86,6 +95,7 @@ export async function servicesResponse() {
];
const mergedGroups = [];
+ const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null;
mergedGroupsNames.forEach((groupName) => {
const discoveredDockerGroup = discoveredDockerServices.find((group) => group.name === groupName) || { services: [] };
@@ -101,7 +111,13 @@ export async function servicesResponse() {
].filter((service) => service),
};
- mergedGroups.push(mergedGroup);
+ if (definedLayouts) {
+ const layoutIndex = definedLayouts.findIndex(layout => layout === mergedGroup.name);
+ if (layoutIndex > -1) mergedGroups.splice(layoutIndex, 0, mergedGroup);
+ else mergedGroups.push(mergedGroup);
+ } else {
+ mergedGroups.push(mergedGroup);
+ }
});
return mergedGroups;
diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js
index 54c393b1..38d09ccb 100644
--- a/src/utils/proxy/handlers/credentialed.js
+++ b/src/utils/proxy/handlers/credentialed.js
@@ -1,5 +1,6 @@
import getServiceWidget from "utils/config/service-helpers";
import { formatApiCall } from "utils/proxy/api-helpers";
+import validateWidgetData from "utils/proxy/validate-widget-data";
import { httpProxy } from "utils/proxy/http";
import createLogger from "utils/logger";
import widgets from "widgets/widgets";
@@ -51,7 +52,11 @@ export default async function credentialedProxyHandler(req, res) {
}
if (status >= 400) {
- logger.debug("HTTP Error %d calling %s//%s%s...", status, url.protocol, url.hostname, url.pathname);
+ logger.error("HTTP Error %d calling %s", status, url.toString());
+ }
+
+ if (!validateWidgetData(widget, endpoint, data)) {
+ return res.status(500).json({error: {message: "Invalid data", url, data}});
}
if (contentType) res.setHeader("Content-Type", contentType);
diff --git a/src/utils/proxy/handlers/generic.js b/src/utils/proxy/handlers/generic.js
index f93c83f2..02c3d4c3 100644
--- a/src/utils/proxy/handlers/generic.js
+++ b/src/utils/proxy/handlers/generic.js
@@ -1,5 +1,6 @@
import getServiceWidget from "utils/config/service-helpers";
import { formatApiCall } from "utils/proxy/api-helpers";
+import validateWidgetData from "utils/proxy/validate-widget-data";
import { httpProxy } from "utils/proxy/http";
import createLogger from "utils/logger";
import widgets from "widgets/widgets";
@@ -32,6 +33,11 @@ export default async function genericProxyHandler(req, res, map) {
});
let resultData = data;
+
+ if (!validateWidgetData(widget, endpoint, resultData)) {
+ return res.status(status).json({error: {message: "Invalid data", url, data: resultData}});
+ }
+
if (status === 200 && map) {
resultData = map(data);
}
@@ -44,6 +50,7 @@ export default async function genericProxyHandler(req, res, map) {
if (status >= 400) {
logger.debug("HTTP Error %d calling %s//%s%s...", status, url.protocol, url.hostname, url.pathname);
+ return res.status(status).json({error: {message: "HTTP Error", url, data}});
}
return res.status(status).send(resultData);
diff --git a/src/utils/proxy/http.js b/src/utils/proxy/http.js
index 4eba83f3..93538202 100644
--- a/src/utils/proxy/http.js
+++ b/src/utils/proxy/http.js
@@ -98,6 +98,6 @@ export async function httpProxy(url, params = {}) {
catch (err) {
logger.error("Error calling %s//%s%s...", url.protocol, url.hostname, url.pathname);
logger.error(err);
- return [500, "application/json", { error: "Unexpected error" }, null];
+ return [500, "application/json", { error: {message: err?.message ?? "Unknown error", url, rawError: err} }, null];
}
}
diff --git a/src/utils/proxy/use-widget-api.js b/src/utils/proxy/use-widget-api.js
index b196e62c..05eb220e 100644
--- a/src/utils/proxy/use-widget-api.js
+++ b/src/utils/proxy/use-widget-api.js
@@ -3,5 +3,11 @@ import useSWR from "swr";
import { formatProxyUrl } from "./api-helpers";
export default function useWidgetAPI(widget, ...options) {
- return useSWR(formatProxyUrl(widget, ...options));
+ const config = {};
+ if (options?.refreshInterval) {
+ config.refreshInterval = options.refreshInterval;
+ }
+ const { data, error } = useSWR(formatProxyUrl(widget, ...options), config);
+ // make the data error the top-level error
+ return { data, error: data?.error ?? error }
}
diff --git a/src/utils/proxy/validate-widget-data.js b/src/utils/proxy/validate-widget-data.js
new file mode 100644
index 00000000..c498357d
--- /dev/null
+++ b/src/utils/proxy/validate-widget-data.js
@@ -0,0 +1,22 @@
+import widgets from "widgets/widgets";
+
+export default function validateWidgetData(widget, endpoint, data) {
+ let valid = true;
+ let dataParsed;
+ try {
+ dataParsed = JSON.parse(data);
+ } catch (e) {
+ valid = false;
+ }
+
+ if (dataParsed && Object.entries(dataParsed).length) {
+ const validate = widgets[widget.type]?.mappings?.[endpoint]?.validate;
+ validate?.forEach(key => {
+ if (dataParsed[key] === undefined) {
+ valid = false;
+ }
+ });
+ }
+
+ return valid;
+}
diff --git a/src/widgets/adguard/component.jsx b/src/widgets/adguard/component.jsx
index 0c78113d..bab969ad 100644
--- a/src/widgets/adguard/component.jsx
+++ b/src/widgets/adguard/component.jsx
@@ -12,9 +12,9 @@ export default function Component({ service }) {
const { data: adguardData, error: adguardError } = useWidgetAPI(widget, "stats");
if (adguardError) {
- return ;
+ return ;
}
-
+
if (!adguardData) {
return (
diff --git a/src/widgets/authentik/component.jsx b/src/widgets/authentik/component.jsx
index 31f864d1..84498db4 100644
--- a/src/widgets/authentik/component.jsx
+++ b/src/widgets/authentik/component.jsx
@@ -14,7 +14,8 @@ export default function Component({ service }) {
const { data: failedLoginsData, error: failedLoginsError } = useWidgetAPI(widget, "login_failed");
if (usersError || loginsError || failedLoginsError) {
- return ;
+ const finalError = usersError ?? loginsError ?? failedLoginsError;
+ return ;
}
if (!usersData || !loginsData || !failedLoginsData) {
diff --git a/src/widgets/autobrr/component.jsx b/src/widgets/autobrr/component.jsx
index b78f48f6..3c170243 100644
--- a/src/widgets/autobrr/component.jsx
+++ b/src/widgets/autobrr/component.jsx
@@ -14,7 +14,8 @@ export default function Component({ service }) {
const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexers");
if (statsError || filtersError || indexersError) {
- return ;
+ const finalError = statsError ?? filtersError ?? indexersError;
+ return ;
}
if (!statsData || !filtersData || !indexersData) {
diff --git a/src/widgets/autobrr/widget.js b/src/widgets/autobrr/widget.js
index 0254029e..fb03f9d3 100644
--- a/src/widgets/autobrr/widget.js
+++ b/src/widgets/autobrr/widget.js
@@ -7,6 +7,10 @@ const widget = {
mappings: {
stats: {
endpoint: "release/stats",
+ validate: [
+ "push_approved_count",
+ "push_rejected_count"
+ ]
},
filters: {
endpoint: "filters",
diff --git a/src/widgets/bazarr/component.jsx b/src/widgets/bazarr/component.jsx
index 24fef1ce..0537c180 100644
--- a/src/widgets/bazarr/component.jsx
+++ b/src/widgets/bazarr/component.jsx
@@ -12,8 +12,9 @@ export default function Component({ service }) {
const { data: episodesData, error: episodesError } = useWidgetAPI(widget, "episodes");
const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movies");
- if (episodesError || moviesError) {
- return ;
+ if (moviesError || episodesError) {
+ const finalError = moviesError ?? episodesError;
+ return ;
}
if (!episodesData || !moviesData) {
diff --git a/src/widgets/changedetectionio/component.jsx b/src/widgets/changedetectionio/component.jsx
index 70936489..0a4ce8d2 100644
--- a/src/widgets/changedetectionio/component.jsx
+++ b/src/widgets/changedetectionio/component.jsx
@@ -9,12 +9,11 @@ export default function Component({ service }) {
const { widget } = service;
- const { data } = useWidgetAPI(widget, "info");
+ const { data, error } = useWidgetAPI(widget, "info");
- if (!data) {
- return ;
+ if (error) {
+ return ;
}
-
const totalObserved = Object.keys(data).length;
let diffsDetected = 0;
diff --git a/src/widgets/coinmarketcap/component.jsx b/src/widgets/coinmarketcap/component.jsx
index 0b970b31..90b91a5e 100644
--- a/src/widgets/coinmarketcap/component.jsx
+++ b/src/widgets/coinmarketcap/component.jsx
@@ -37,7 +37,7 @@ export default function Component({ service }) {
}
if (statsError) {
- return ;
+ return ;
}
if (!statsData || !dateRange) {
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 7dc7fcac..963be47f 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -12,6 +12,7 @@ const components = {
emby: dynamic(() => import("./emby/component")),
gluetun: dynamic(() => import("./gluetun/component")),
gotify: dynamic(() => import("./gotify/component")),
+ hdhomerun: dynamic(() => import("./hdhomerun/component")),
homebridge: dynamic(() => import("./homebridge/component")),
jackett: dynamic(() => import("./jackett/component")),
jellyfin: dynamic(() => import("./emby/component")),
diff --git a/src/widgets/docker/component.jsx b/src/widgets/docker/component.jsx
index 542fbde7..bdc49be3 100644
--- a/src/widgets/docker/component.jsx
+++ b/src/widgets/docker/component.jsx
@@ -17,8 +17,9 @@ export default function Component({ service }) {
const { data: statsData, error: statsError } = useSWR(`/api/docker/stats/${widget.container}/${widget.server || ""}`);
- if (statsError || statusError) {
- return ;
+ if (statsError || statsData?.error || statusError || statusData?.error) {
+ const finalError = statsError ?? statsData?.error ?? statusError ?? statusData?.error;
+ return ;
}
if (statusData && statusData.status !== "running") {
diff --git a/src/widgets/emby/component.jsx b/src/widgets/emby/component.jsx
index 59d66c05..a61cd7aa 100644
--- a/src/widgets/emby/component.jsx
+++ b/src/widgets/emby/component.jsx
@@ -1,10 +1,10 @@
-import useSWR from "swr";
import { useTranslation } from "next-i18next";
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
import { MdOutlineSmartDisplay } from "react-icons/md";
import Container from "components/services/widget/container";
-import { formatProxyUrl, formatProxyUrlWithSegments } from "utils/proxy/api-helpers";
+import { formatProxyUrlWithSegments } from "utils/proxy/api-helpers";
+import useWidgetAPI from "utils/proxy/use-widget-api";
function ticksToTime(ticks) {
const milliseconds = ticks / 10000;
@@ -157,7 +157,7 @@ export default function Component({ service }) {
data: sessionsData,
error: sessionsError,
mutate: sessionMutate,
- } = useSWR(formatProxyUrl(widget, "Sessions"), {
+ } = useWidgetAPI(widget, "Sessions", {
refreshInterval: 5000,
});
@@ -171,8 +171,8 @@ export default function Component({ service }) {
});
}
- if (sessionsError || sessionsData?.error) {
- return ;
+ if (sessionsError) {
+ return ;
}
if (!sessionsData) {
diff --git a/src/widgets/gluetun/component.jsx b/src/widgets/gluetun/component.jsx
index f20c400b..59e490ce 100644
--- a/src/widgets/gluetun/component.jsx
+++ b/src/widgets/gluetun/component.jsx
@@ -1,18 +1,14 @@
-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: gluetunData, error: gluetunError } = useWidgetAPI(widget, "ip");
if (gluetunError) {
- return ;
+ return ;
}
if (!gluetunData) {
diff --git a/src/widgets/gluetun/widget.js b/src/widgets/gluetun/widget.js
index 59aa39ef..009adbf3 100644
--- a/src/widgets/gluetun/widget.js
+++ b/src/widgets/gluetun/widget.js
@@ -7,6 +7,11 @@ const widget = {
mappings: {
ip: {
endpoint: "publicip/ip",
+ validate: [
+ "public_ip",
+ "region",
+ "country"
+ ]
},
},
};
diff --git a/src/widgets/gotify/component.jsx b/src/widgets/gotify/component.jsx
index 40f5793b..7cd5c135 100644
--- a/src/widgets/gotify/component.jsx
+++ b/src/widgets/gotify/component.jsx
@@ -1,12 +1,8 @@
-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: appsData, error: appsError } = useWidgetAPI(widget, "application");
@@ -14,7 +10,8 @@ export default function Component({ service }) {
const { data: clientsData, error: clientsError } = useWidgetAPI(widget, "client");
if (appsError || messagesError || clientsError) {
- return ;
+ const finalError = appsError ?? messagesError ?? clientsError;
+ return ;
}
diff --git a/src/widgets/hdhomerun/component.jsx b/src/widgets/hdhomerun/component.jsx
new file mode 100644
index 00000000..79160dc3
--- /dev/null
+++ b/src/widgets/hdhomerun/component.jsx
@@ -0,0 +1,32 @@
+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 { widget } = service;
+
+ const { data: channelsData, error: channelsError } = useWidgetAPI(widget, "lineup");
+
+ if (channelsError) {
+ return ;
+ }
+
+ if (!channelsData) {
+ return (
+
+
+
+
+ );
+ }
+
+const hdChannels = channelsData?.filter((channel) => channel.HD === 1);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/hdhomerun/widget.js b/src/widgets/hdhomerun/widget.js
new file mode 100644
index 00000000..b36b9b1c
--- /dev/null
+++ b/src/widgets/hdhomerun/widget.js
@@ -0,0 +1,14 @@
+import genericProxyHandler from "utils/proxy/handlers/generic";
+
+const widget = {
+ api: "{url}/{endpoint}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ "lineup": {
+ endpoint: "lineup.json",
+ }
+ },
+};
+
+export default widget;
diff --git a/src/widgets/homebridge/component.jsx b/src/widgets/homebridge/component.jsx
index 807cc49a..a1e2f2e1 100644
--- a/src/widgets/homebridge/component.jsx
+++ b/src/widgets/homebridge/component.jsx
@@ -11,8 +11,8 @@ export default function Component({ service }) {
const { data: homebridgeData, error: homebridgeError } = useWidgetAPI(widget, "info");
- if (homebridgeError || homebridgeData?.error) {
- return ;
+ if (homebridgeError) {
+ return ;
}
if (!homebridgeData) {
diff --git a/src/widgets/jackett/component.jsx b/src/widgets/jackett/component.jsx
index 9629e266..b70cbd95 100644
--- a/src/widgets/jackett/component.jsx
+++ b/src/widgets/jackett/component.jsx
@@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexers");
if (indexersError) {
- return ;
+ return ;
}
if (!indexersData) {
diff --git a/src/widgets/jellyseerr/component.jsx b/src/widgets/jellyseerr/component.jsx
index 217e406e..a129909e 100644
--- a/src/widgets/jellyseerr/component.jsx
+++ b/src/widgets/jellyseerr/component.jsx
@@ -1,18 +1,14 @@
-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, "request/count");
if (statsError) {
- return ;
+ return ;
}
if (!statsData) {
diff --git a/src/widgets/jellyseerr/widget.js b/src/widgets/jellyseerr/widget.js
index d752e339..3895d2c5 100644
--- a/src/widgets/jellyseerr/widget.js
+++ b/src/widgets/jellyseerr/widget.js
@@ -7,6 +7,11 @@ const widget = {
mappings: {
"request/count": {
endpoint: "request/count",
+ validate: [
+ "pending",
+ "approved",
+ "available"
+ ]
},
},
};
diff --git a/src/widgets/lidarr/component.jsx b/src/widgets/lidarr/component.jsx
index 343760e7..b612ae32 100644
--- a/src/widgets/lidarr/component.jsx
+++ b/src/widgets/lidarr/component.jsx
@@ -14,7 +14,8 @@ export default function Component({ service }) {
const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue/status");
if (albumsError || wantedError || queueError) {
- return ;
+ const finalError = albumsError ?? wantedError ?? queueError;
+ return ;
}
if (!albumsData || !wantedData || !queueData) {
diff --git a/src/widgets/mastodon/component.jsx b/src/widgets/mastodon/component.jsx
index ec12fca1..fd4f0ece 100644
--- a/src/widgets/mastodon/component.jsx
+++ b/src/widgets/mastodon/component.jsx
@@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: statsData, error: statsError } = useWidgetAPI(widget, "instance");
if (statsError) {
- return ;
+ return ;
}
if (!statsData) {
diff --git a/src/widgets/navidrome/component.jsx b/src/widgets/navidrome/component.jsx
index f4f3e672..e45ac655 100644
--- a/src/widgets/navidrome/component.jsx
+++ b/src/widgets/navidrome/component.jsx
@@ -26,8 +26,8 @@ export default function Component({ service }) {
const { data: navidromeData, error: navidromeError } = useWidgetAPI(widget, "getNowPlaying");
- if (navidromeError || navidromeData?.error || navidromeData?.["subsonic-response"]?.error) {
- return ;
+ if (navidromeError || navidromeData?.["subsonic-response"]?.error) {
+ return ;
}
if (!navidromeData) {
diff --git a/src/widgets/npm/component.jsx b/src/widgets/npm/component.jsx
index 92aef035..b6d42ad9 100644
--- a/src/widgets/npm/component.jsx
+++ b/src/widgets/npm/component.jsx
@@ -1,18 +1,14 @@
-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: infoData, error: infoError } = useWidgetAPI(widget, "nginx/proxy-hosts");
- if (infoError || infoData?.error) {
- return ;
+ if (infoError) {
+ return ;
}
if (!infoData) {
diff --git a/src/widgets/nzbget/component.jsx b/src/widgets/nzbget/component.jsx
index f9ace707..b33e0830 100644
--- a/src/widgets/nzbget/component.jsx
+++ b/src/widgets/nzbget/component.jsx
@@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: statusData, error: statusError } = useWidgetAPI(widget, "status");
if (statusError) {
- return ;
+ return ;
}
if (!statusData) {
diff --git a/src/widgets/ombi/component.jsx b/src/widgets/ombi/component.jsx
index 60128c37..722d5c45 100644
--- a/src/widgets/ombi/component.jsx
+++ b/src/widgets/ombi/component.jsx
@@ -1,18 +1,14 @@
-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, "Request/count");
if (statsError) {
- return ;
+ return ;
}
if (!statsData) {
diff --git a/src/widgets/overseerr/component.jsx b/src/widgets/overseerr/component.jsx
index 47131f6e..6f5ae8ff 100644
--- a/src/widgets/overseerr/component.jsx
+++ b/src/widgets/overseerr/component.jsx
@@ -1,18 +1,14 @@
-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, "request/count");
if (statsError) {
- return ;
+ return ;
}
if (!statsData) {
diff --git a/src/widgets/overseerr/widget.js b/src/widgets/overseerr/widget.js
index d752e339..945af5ed 100644
--- a/src/widgets/overseerr/widget.js
+++ b/src/widgets/overseerr/widget.js
@@ -7,6 +7,11 @@ const widget = {
mappings: {
"request/count": {
endpoint: "request/count",
+ validate: [
+ "pending",
+ "approved",
+ "available",
+ ],
},
},
};
diff --git a/src/widgets/pihole/component.jsx b/src/widgets/pihole/component.jsx
index 17a18627..f213ac6d 100644
--- a/src/widgets/pihole/component.jsx
+++ b/src/widgets/pihole/component.jsx
@@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "api.php");
if (piholeError) {
- return ;
+ return ;
}
if (!piholeData) {
diff --git a/src/widgets/pihole/widget.js b/src/widgets/pihole/widget.js
index 2e20fe8a..b392cded 100644
--- a/src/widgets/pihole/widget.js
+++ b/src/widgets/pihole/widget.js
@@ -7,6 +7,11 @@ const widget = {
mappings: {
"api.php": {
endpoint: "api.php",
+ validate: [
+ "dns_queries_today",
+ "ads_blocked_today",
+ "domains_being_blocked"
+ ]
},
},
};
diff --git a/src/widgets/plex/component.jsx b/src/widgets/plex/component.jsx
index 9de60fd1..6fe15bd5 100644
--- a/src/widgets/plex/component.jsx
+++ b/src/widgets/plex/component.jsx
@@ -1,21 +1,20 @@
-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";
+import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
- const { data: plexData, error: plexAPIError } = useSWR(formatProxyUrl(widget, "unified"), {
+ const { data: plexData, error: plexAPIError } = useWidgetAPI(widget, "unified", {
refreshInterval: 5000,
});
if (plexAPIError) {
- return ;
+ return ;
}
if (!plexData) {
diff --git a/src/widgets/plex/proxy.js b/src/widgets/plex/proxy.js
index 46ebb27c..36b7a268 100644
--- a/src/widgets/plex/proxy.js
+++ b/src/widgets/plex/proxy.js
@@ -44,7 +44,7 @@ async function fetchFromPlexAPI(endpoint, widget) {
if (status !== 200) {
logger.error("HTTP %d communicating with Plex. Data: %s", status, data.toString());
- return [status, data.toString()];
+ return [status, data];
}
try {
@@ -65,6 +65,11 @@ export default async function plexProxyHandler(req, res) {
logger.debug("Getting streams from Plex API");
let streams;
let [status, apiData] = await fetchFromPlexAPI("/status/sessions", widget);
+
+ if (status !== 200) {
+ return res.status(status).json({error: {message: "HTTP error communicating with Plex API", data: Buffer.from(apiData).toString()}});
+ }
+
if (apiData && apiData.MediaContainer) {
streams = apiData.MediaContainer._attributes.size;
}
diff --git a/src/widgets/portainer/component.jsx b/src/widgets/portainer/component.jsx
index bd44d77e..ccc26b7f 100644
--- a/src/widgets/portainer/component.jsx
+++ b/src/widgets/portainer/component.jsx
@@ -14,7 +14,7 @@ export default function Component({ service }) {
});
if (containersError) {
- return ;
+ return ;
}
if (!containersData) {
diff --git a/src/widgets/prowlarr/component.jsx b/src/widgets/prowlarr/component.jsx
index bb082519..890679e5 100644
--- a/src/widgets/prowlarr/component.jsx
+++ b/src/widgets/prowlarr/component.jsx
@@ -1,19 +1,16 @@
-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: indexersData, error: indexersError } = useWidgetAPI(widget, "indexer");
const { data: grabsData, error: grabsError } = useWidgetAPI(widget, "indexerstats");
if (indexersError || grabsError) {
- return ;
+ const finalError = indexersError ?? grabsError;
+ return ;
}
if (!indexersData || !grabsData) {
diff --git a/src/widgets/proxmox/component.jsx b/src/widgets/proxmox/component.jsx
index 9cdb26f7..1d384b54 100644
--- a/src/widgets/proxmox/component.jsx
+++ b/src/widgets/proxmox/component.jsx
@@ -16,7 +16,7 @@ export default function Component({ service }) {
const { data: clusterData, error: clusterError } = useWidgetAPI(widget, "cluster/resources");
if (clusterError) {
- return ;
+ return ;
}
if (!clusterData || !clusterData.data) {
diff --git a/src/widgets/pyload/component.jsx b/src/widgets/pyload/component.jsx
index 958733c3..3bb4b0c4 100644
--- a/src/widgets/pyload/component.jsx
+++ b/src/widgets/pyload/component.jsx
@@ -9,8 +9,8 @@ export default function Component({ service }) {
const { widget } = service;
const { data: pyloadData, error: pyloadError } = useWidgetAPI(widget, "status");
- if (pyloadError || pyloadData?.error) {
- return ;
+ if (pyloadError) {
+ return ;
}
if (!pyloadData) {
diff --git a/src/widgets/pyload/proxy.js b/src/widgets/pyload/proxy.js
index 46b28684..4a866d9c 100644
--- a/src/widgets/pyload/proxy.js
+++ b/src/widgets/pyload/proxy.js
@@ -84,9 +84,9 @@ export default async function pyloadProxyHandler(req, res) {
if (data?.error || status !== 200) {
try {
- return res.status(status).send(Buffer.from(data).toString());
+ return res.status(status).send({error: {message: "HTTP error communicating with Plex API", data: Buffer.from(data).toString()}});
} catch (e) {
- return res.status(status).send(data);
+ return res.status(status).send({error: {message: "HTTP error communicating with Plex API", data}});
}
}
@@ -95,7 +95,7 @@ export default async function pyloadProxyHandler(req, res) {
}
} catch (e) {
logger.error(e);
- return res.status(500).send(e.toString());
+ return res.status(500).send({error: {message: `Error communicating with Plex API: ${e.toString()}`}});
}
return res.status(400).json({ error: 'Invalid proxy service type' });
diff --git a/src/widgets/qbittorrent/component.jsx b/src/widgets/qbittorrent/component.jsx
index 4d3a3585..3abc933d 100644
--- a/src/widgets/qbittorrent/component.jsx
+++ b/src/widgets/qbittorrent/component.jsx
@@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents/info");
if (torrentError) {
- return ;
+ return ;
}
if (!torrentData) {
diff --git a/src/widgets/radarr/component.jsx b/src/widgets/radarr/component.jsx
index fe5a6968..f2620b78 100644
--- a/src/widgets/radarr/component.jsx
+++ b/src/widgets/radarr/component.jsx
@@ -1,19 +1,16 @@
-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: moviesData, error: moviesError } = useWidgetAPI(widget, "movie");
const { data: queuedData, error: queuedError } = useWidgetAPI(widget, "queue/status");
if (moviesError || queuedError) {
- return ;
+ const finalError = moviesError ?? queuedError;
+ return ;
}
if (!moviesData || !queuedData) {
diff --git a/src/widgets/radarr/widget.js b/src/widgets/radarr/widget.js
index 5a457ea7..78054219 100644
--- a/src/widgets/radarr/widget.js
+++ b/src/widgets/radarr/widget.js
@@ -16,6 +16,9 @@ const widget = {
},
"queue/status": {
endpoint: "queue/status",
+ validate: [
+ "totalCount"
+ ]
},
},
};
diff --git a/src/widgets/readarr/component.jsx b/src/widgets/readarr/component.jsx
index 1e55d7cd..4eee11f4 100644
--- a/src/widgets/readarr/component.jsx
+++ b/src/widgets/readarr/component.jsx
@@ -14,7 +14,8 @@ export default function Component({ service }) {
const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue/status");
if (booksError || wantedError || queueError) {
- return ;
+ const finalError = booksError ?? wantedError ?? queueError;
+ return ;
}
if (!booksData || !wantedData || !queueData) {
diff --git a/src/widgets/rutorrent/component.jsx b/src/widgets/rutorrent/component.jsx
index 279bdf0e..ef3a278f 100644
--- a/src/widgets/rutorrent/component.jsx
+++ b/src/widgets/rutorrent/component.jsx
@@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: statusData, error: statusError } = useWidgetAPI(widget);
if (statusError) {
- return ;
+ return ;
}
if (!statusData) {
diff --git a/src/widgets/sabnzbd/component.jsx b/src/widgets/sabnzbd/component.jsx
index c4e64c9a..b0cdb951 100644
--- a/src/widgets/sabnzbd/component.jsx
+++ b/src/widgets/sabnzbd/component.jsx
@@ -22,7 +22,7 @@ export default function Component({ service }) {
const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue");
if (queueError) {
- return ;
+ return ;
}
if (!queueData) {
diff --git a/src/widgets/sabnzbd/widget.js b/src/widgets/sabnzbd/widget.js
index e3097376..0e7ea43a 100644
--- a/src/widgets/sabnzbd/widget.js
+++ b/src/widgets/sabnzbd/widget.js
@@ -7,6 +7,9 @@ const widget = {
mappings: {
queue: {
endpoint: "queue",
+ validate: [
+ "queue"
+ ]
},
},
};
diff --git a/src/widgets/sonarr/component.jsx b/src/widgets/sonarr/component.jsx
index 8618b512..14dd3328 100644
--- a/src/widgets/sonarr/component.jsx
+++ b/src/widgets/sonarr/component.jsx
@@ -1,12 +1,8 @@
-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: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing");
@@ -14,7 +10,8 @@ export default function Component({ service }) {
const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series");
if (wantedError || queuedError || seriesError) {
- return ;
+ const finalError = wantedError ?? queuedError ?? seriesError;
+ return ;
}
if (!wantedData || !queuedData || !seriesData) {
diff --git a/src/widgets/sonarr/widget.js b/src/widgets/sonarr/widget.js
index 32780bda..c1413975 100644
--- a/src/widgets/sonarr/widget.js
+++ b/src/widgets/sonarr/widget.js
@@ -10,13 +10,19 @@ const widget = {
endpoint: "series",
map: (data) => ({
total: asJson(data).length,
- }),
+ })
},
queue: {
endpoint: "queue",
+ validate: [
+ "totalRecords"
+ ]
},
"wanted/missing": {
endpoint: "wanted/missing",
+ validate: [
+ "totalRecords"
+ ]
},
},
};
diff --git a/src/widgets/speedtest/component.jsx b/src/widgets/speedtest/component.jsx
index e93c6963..3067dbf3 100644
--- a/src/widgets/speedtest/component.jsx
+++ b/src/widgets/speedtest/component.jsx
@@ -11,8 +11,8 @@ export default function Component({ service }) {
const { data: speedtestData, error: speedtestError } = useWidgetAPI(widget, "speedtest/latest");
- if (speedtestError || (speedtestData && !speedtestData.data)) {
- return ;
+ if (speedtestError) {
+ return ;
}
if (!speedtestData) {
diff --git a/src/widgets/speedtest/widget.js b/src/widgets/speedtest/widget.js
index b227848a..a5ba0634 100644
--- a/src/widgets/speedtest/widget.js
+++ b/src/widgets/speedtest/widget.js
@@ -7,6 +7,9 @@ const widget = {
mappings: {
"speedtest/latest": {
endpoint: "speedtest/latest",
+ validate: [
+ "data"
+ ]
},
},
};
diff --git a/src/widgets/strelaysrv/component.jsx b/src/widgets/strelaysrv/component.jsx
index c7b887db..f58f8830 100644
--- a/src/widgets/strelaysrv/component.jsx
+++ b/src/widgets/strelaysrv/component.jsx
@@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: statsData, error: statsError } = useWidgetAPI(widget, "status");
if (statsError) {
- return ;
+ return ;
}
if (!statsData) {
diff --git a/src/widgets/strelaysrv/widget.js b/src/widgets/strelaysrv/widget.js
index 713f05b4..2141e2e2 100644
--- a/src/widgets/strelaysrv/widget.js
+++ b/src/widgets/strelaysrv/widget.js
@@ -7,6 +7,11 @@ const widget = {
mappings: {
status: {
endpoint: "status",
+ validate: [
+ "numActiveSessions",
+ "numConnections",
+ "bytesProxied"
+ ]
},
},
};
diff --git a/src/widgets/tautulli/component.jsx b/src/widgets/tautulli/component.jsx
index 98207c43..44b1eb6d 100644
--- a/src/widgets/tautulli/component.jsx
+++ b/src/widgets/tautulli/component.jsx
@@ -1,11 +1,10 @@
/* eslint-disable camelcase */
-import useSWR from "swr";
import { useTranslation } from "next-i18next";
import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
import Container from "components/services/widget/container";
-import { formatProxyUrl } from "utils/proxy/api-helpers";
+import useWidgetAPI from "utils/proxy/use-widget-api";
function millisecondsToTime(milliseconds) {
const seconds = Math.floor((milliseconds / 1000) % 60);
@@ -119,12 +118,12 @@ export default function Component({ service }) {
const { widget } = service;
- const { data: activityData, error: activityError } = useSWR(formatProxyUrl(widget, "get_activity"), {
+ const { data: activityData, error: activityError } = useWidgetAPI(widget, "get_activity", {
refreshInterval: 5000,
});
if (activityError) {
- return ;
+ return ;
}
if (!activityData) {
diff --git a/src/widgets/traefik/component.jsx b/src/widgets/traefik/component.jsx
index d24edb9e..7739e62a 100644
--- a/src/widgets/traefik/component.jsx
+++ b/src/widgets/traefik/component.jsx
@@ -1,18 +1,14 @@
-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: traefikData, error: traefikError } = useWidgetAPI(widget, "overview");
if (traefikError) {
- return ;
+ return ;
}
if (!traefikData) {
diff --git a/src/widgets/traefik/widget.js b/src/widgets/traefik/widget.js
index aa92fa1e..5811b1b9 100644
--- a/src/widgets/traefik/widget.js
+++ b/src/widgets/traefik/widget.js
@@ -7,6 +7,9 @@ const widget = {
mappings: {
overview: {
endpoint: "overview",
+ validate: [
+ "http"
+ ]
},
},
};
diff --git a/src/widgets/transmission/component.jsx b/src/widgets/transmission/component.jsx
index 3c2f38ef..7de276c2 100644
--- a/src/widgets/transmission/component.jsx
+++ b/src/widgets/transmission/component.jsx
@@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: torrentData, error: torrentError } = useWidgetAPI(widget);
if (torrentError) {
- return ;
+ return ;
}
if (!torrentData) {
diff --git a/src/widgets/transmission/proxy.js b/src/widgets/transmission/proxy.js
index 34bbd480..cdc1e9c9 100644
--- a/src/widgets/transmission/proxy.js
+++ b/src/widgets/transmission/proxy.js
@@ -68,6 +68,7 @@ export default async function transmissionProxyHandler(req, res) {
if (status !== 200) {
logger.error("Error getting data from Transmission: %d. Data: %s", status, data);
+ return res.status(500).send({error: {message:"Error getting data from Transmission", url, data}});
}
if (contentType) res.setHeader("Content-Type", contentType);
diff --git a/src/widgets/truenas/component.jsx b/src/widgets/truenas/component.jsx
index 4244d833..70f917ab 100644
--- a/src/widgets/truenas/component.jsx
+++ b/src/widgets/truenas/component.jsx
@@ -42,7 +42,8 @@ export default function Component({ service }) {
const { data: statusData, error: statusError } = useWidgetAPI(widget, "status");
if (alertError || statusError) {
- return ;
+ const finalError = alertError ?? statusError;
+ return ;
}
if (!alertData || !statusData) {
diff --git a/src/widgets/truenas/widget.js b/src/widgets/truenas/widget.js
index 7269e36a..4c479e9f 100644
--- a/src/widgets/truenas/widget.js
+++ b/src/widgets/truenas/widget.js
@@ -14,6 +14,10 @@ const widget = {
},
status: {
endpoint: "system/info",
+ validate: [
+ "loadavg",
+ "uptime_seconds"
+ ]
},
},
};
diff --git a/src/widgets/tubearchivist/component.jsx b/src/widgets/tubearchivist/component.jsx
index 5b548443..b1b310d7 100644
--- a/src/widgets/tubearchivist/component.jsx
+++ b/src/widgets/tubearchivist/component.jsx
@@ -15,7 +15,8 @@ export default function Component({ service }) {
const { data: playlistsData, error: playlistsError } = useWidgetAPI(widget, "playlists");
if (downloadsError || videosError || channelsError || playlistsError) {
- return ;
+ const finalError = downloadsError ?? videosError ?? channelsError ?? playlistsError;
+ return ;
}
if (!downloadsData || !videosData || !channelsData || !playlistsData) {
diff --git a/src/widgets/tubearchivist/widget.js b/src/widgets/tubearchivist/widget.js
index c73070f0..6610bf12 100644
--- a/src/widgets/tubearchivist/widget.js
+++ b/src/widgets/tubearchivist/widget.js
@@ -7,15 +7,27 @@ const widget = {
mappings: {
downloads: {
endpoint: "download",
+ validate: [
+ "paginate",
+ ]
},
videos: {
endpoint: "video",
+ validate: [
+ "paginate",
+ ]
},
channels: {
endpoint: "channel",
+ validate: [
+ "paginate",
+ ]
},
playlists: {
endpoint: "playlist",
+ validate: [
+ "paginate",
+ ]
},
},
};
diff --git a/src/widgets/unifi/component.jsx b/src/widgets/unifi/component.jsx
index 9a323259..6df43dfd 100644
--- a/src/widgets/unifi/component.jsx
+++ b/src/widgets/unifi/component.jsx
@@ -11,8 +11,8 @@ export default function Component({ service }) {
const { data: statsData, error: statsError } = useWidgetAPI(widget, "stat/sites");
- if (statsError || statsData?.error) {
- return ;
+ if (statsError) {
+ return ;
}
const defaultSite = statsData?.data?.find(s => s.name === "default");
diff --git a/src/widgets/unifi/proxy.js b/src/widgets/unifi/proxy.js
index 95ac331e..abb5986f 100644
--- a/src/widgets/unifi/proxy.js
+++ b/src/widgets/unifi/proxy.js
@@ -74,7 +74,7 @@ export default async function unifiProxyHandler(req, res) {
// 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"]) {
+ if (responseHeaders?.["x-csrf-token"]) {
prefix = udmpPrefix;
}
cache.put(prefixCacheKey, prefix);
@@ -88,13 +88,14 @@ export default async function unifiProxyHandler(req, res) {
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);
+ return res.status(status).json({error: {message: `HTTP Error ${status}`, url, data}});
}
const json = JSON.parse(data.toString());
@@ -112,6 +113,7 @@ export default async function unifiProxyHandler(req, res) {
if (status !== 200) {
logger.error("HTTP %d getting data from Unifi endpoint %s. Data: %s", status, url.href, data);
+ return res.status(status).json({error: {message: `HTTP Error ${status}`, url, data}});
}
if (contentType) res.setHeader("Content-Type", contentType);
diff --git a/src/widgets/watchtower/component.jsx b/src/widgets/watchtower/component.jsx
index 68c5531f..3550e3f4 100644
--- a/src/widgets/watchtower/component.jsx
+++ b/src/widgets/watchtower/component.jsx
@@ -12,8 +12,8 @@ export default function Component({ service }) {
const { data: watchData, error: watchError } = useWidgetAPI(widget, "watchtower");
- if (watchError || !watchData) {
- return ;
+ if (watchError) {
+ return ;
}
if (!watchData) {
diff --git a/src/widgets/watchtower/proxy.js b/src/widgets/watchtower/proxy.js
index 2d54928c..b37fc5f8 100644
--- a/src/widgets/watchtower/proxy.js
+++ b/src/widgets/watchtower/proxy.js
@@ -33,15 +33,16 @@ export default async function watchtowerProxyHandler(req, res) {
if (status !== 200 || !data) {
logger.error("Error getting data from WatchTower: %d. Data: %s", status, data);
+ return res.status(status).json({error: {message: `HTTP Error ${status}`, url, data}});
}
- const cleanData = data.toString().split("\n").filter(s => s.startsWith("watchtower"))
+ const cleanData = data.toString().split("\n").filter(s => s.startsWith("watchtower"));
const jsonRes = {}
cleanData.map(e => e.split(" ")).forEach(strArray => {
const [key, value] = strArray
jsonRes[key] = value
- })
+ });
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(jsonRes);
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 66e29086..fe432832 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -7,6 +7,7 @@ import coinmarketcap from "./coinmarketcap/widget";
import emby from "./emby/widget";
import gluetun from "./gluetun/widget";
import gotify from "./gotify/widget";
+import hdhomerun from "./hdhomerun/widget";
import homebridge from "./homebridge/widget";
import jackett from "./jackett/widget";
import jellyseerr from "./jellyseerr/widget";
@@ -49,6 +50,7 @@ const widgets = {
emby,
gluetun,
gotify,
+ hdhomerun,
homebridge,
jackett,
jellyfin: emby,