浏览代码

Merge branch 'main' into kubernetes

James Wynn 2 年之前
父节点
当前提交
e15ba1c82c
共有 86 个文件被更改,包括 3138 次插入500 次删除
  1. 13 2
      .github/ISSUE_TEMPLATE/bug_report.yml
  2. 4 2
      .gitignore
  3. 9 7
      README.md
  4. 8 7
      next-i18next.config.js
  5. 47 5
      public/locales/ar/common.json
  6. 48 6
      public/locales/bg/common.json
  7. 167 125
      public/locales/ca/common.json
  8. 44 2
      public/locales/cs/common.json
  9. 46 4
      public/locales/da/common.json
  10. 45 3
      public/locales/de/common.json
  11. 44 4
      public/locales/en/common.json
  12. 45 3
      public/locales/eo/common.json
  13. 48 6
      public/locales/es/common.json
  14. 44 2
      public/locales/fi/common.json
  15. 45 3
      public/locales/fr/common.json
  16. 45 3
      public/locales/he/common.json
  17. 44 2
      public/locales/hi/common.json
  18. 46 4
      public/locales/hr/common.json
  19. 44 2
      public/locales/hu/common.json
  20. 69 27
      public/locales/it/common.json
  21. 45 3
      public/locales/ms/common.json
  22. 45 3
      public/locales/nb-NO/common.json
  23. 44 2
      public/locales/nl/common.json
  24. 44 2
      public/locales/pl/common.json
  25. 44 2
      public/locales/pt-BR/common.json
  26. 73 33
      public/locales/pt/common.json
  27. 44 2
      public/locales/ro/common.json
  28. 48 6
      public/locales/ru/common.json
  29. 48 6
      public/locales/sr/common.json
  30. 44 2
      public/locales/sv/common.json
  31. 45 3
      public/locales/te/common.json
  32. 44 2
      public/locales/tr/common.json
  33. 407 0
      public/locales/uk/common.json
  34. 48 6
      public/locales/vi/common.json
  35. 44 2
      public/locales/yue/common.json
  36. 46 4
      public/locales/zh-CN/common.json
  37. 44 2
      public/locales/zh-Hant/common.json
  38. 6 6
      src/components/resolvedicon.jsx
  39. 2 6
      src/components/version.jsx
  40. 52 46
      src/components/widgets/logo/logo.jsx
  41. 1 1
      src/components/widgets/resources/cpu.jsx
  42. 1 1
      src/components/widgets/resources/disk.jsx
  43. 1 1
      src/components/widgets/resources/memory.jsx
  44. 35 8
      src/pages/api/docker/stats/[...service].js
  45. 35 9
      src/pages/api/docker/status/[...service].js
  46. 6 0
      src/pages/api/releases.js
  47. 7 4
      src/utils/config/api-response.js
  48. 5 2
      src/utils/config/docker.js
  49. 30 25
      src/utils/config/service-helpers.js
  50. 10 2
      src/utils/proxy/handlers/credentialed.js
  51. 8 1
      src/widgets/components.js
  52. 8 8
      src/widgets/downloadstation/component.jsx
  53. 3 3
      src/widgets/downloadstation/proxy.js
  54. 2 2
      src/widgets/downloadstation/widget.js
  55. 12 11
      src/widgets/homebridge/proxy.js
  56. 43 0
      src/widgets/mikrotik/component.jsx
  57. 24 0
      src/widgets/mikrotik/widget.js
  58. 33 0
      src/widgets/miniflux/component.jsx
  59. 19 0
      src/widgets/miniflux/widget.js
  60. 39 0
      src/widgets/nextdns/component.jsx
  61. 17 0
      src/widgets/nextdns/widget.js
  62. 6 6
      src/widgets/npm/proxy.js
  63. 39 0
      src/widgets/omada/component.jsx
  64. 252 0
      src/widgets/omada/proxy.js
  65. 7 0
      src/widgets/omada/widget.js
  66. 48 0
      src/widgets/opnsense/component.jsx
  67. 24 0
      src/widgets/opnsense/widget.js
  68. 7 4
      src/widgets/overseerr/component.jsx
  69. 4 4
      src/widgets/pihole/component.jsx
  70. 3 3
      src/widgets/pihole/widget.js
  71. 13 9
      src/widgets/plex/proxy.js
  72. 8 5
      src/widgets/prowlarr/component.jsx
  73. 16 16
      src/widgets/pyload/proxy.js
  74. 2 2
      src/widgets/qbittorrent/component.jsx
  75. 7 4
      src/widgets/radarr/component.jsx
  76. 6 3
      src/widgets/sonarr/component.jsx
  77. 2 2
      src/widgets/speedtest/component.jsx
  78. 42 0
      src/widgets/tdarr/component.jsx
  79. 48 0
      src/widgets/tdarr/proxy.js
  80. 8 0
      src/widgets/tdarr/widget.js
  81. 3 3
      src/widgets/transmission/proxy.js
  82. 3 2
      src/widgets/unifi/proxy.js
  83. 17 2
      src/widgets/widgets.js
  84. 35 0
      src/widgets/xteve/component.jsx
  85. 63 0
      src/widgets/xteve/proxy.js
  86. 14 0
      src/widgets/xteve/widget.js

+ 13 - 2
.github/ISSUE_TEMPLATE/bug_report.yml

@@ -68,9 +68,20 @@ body:
     id: browser-logs
     id: browser-logs
     attributes:
     attributes:
       label: Browser Logs
       label: Browser Logs
-      description: Please review and provide any relevant logs from the browser, if relevant
+      description: Please review and provide any logs from the browser, if relevant
   - type: textarea
   - type: textarea
     id: other
     id: other
     attributes:
     attributes:
       label: Other
       label: Other
-      description: Any other relevant details. E.g. service version or API version, docker version, etc.
+      description: Please include output from your troubleshooting tests, if relevant. Include any other relevant details. E.g. service version or API version, docker version, etc.
+  - type: checkboxes
+    id: pre-flight
+    attributes:
+      label: Before submitting, I have made sure to
+      options:
+        - label: Check [the documentation](https://gethomepage.dev/)
+          required: true
+        - label: Follow [the troubleshooting guide](https://gethomepage.dev/en/more/troubleshooting/) (please include output above if applicable).
+          required: true
+        - label: Search [existing issues](https://github.com/benphelps/homepage/search?q=&type=issues) and [discussions](https://github.com/benphelps/homepage/search?q=&type=discussions).
+          required: true

+ 4 - 2
.gitignore

@@ -42,5 +42,7 @@ next-env.d.ts
 # homepage
 # homepage
 /config
 /config
 
 
-# idea
-.idea/
+# IDEs
+/.idea/
+
+

+ 9 - 7
README.md

@@ -45,15 +45,17 @@
   - Container status (Running / Stopped) & statistics (CPU, Memory, Network)
   - Container status (Running / Stopped) & statistics (CPU, Memory, Network)
   - Automatic service discovery (via labels)
   - Automatic service discovery (via labels)
 - Service Integration
 - Service Integration
-  - Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli (Plex)
-  - Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission, qBittorrent
-  - Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify, Syncthing Relay Server, Authentik, Proxmox
+  - Sonarr, Radarr, Readarr, Prowlarr, Bazarr, Lidarr, Emby, Jellyfin, Tautulli, Plex and more
+  - Ombi, Overseerr, Jellyseerr, Jackett, NZBGet, SABnzbd, ruTorrent, Transmission, qBittorrent and more
+  - Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify, Syncthing Relay Server, Authentik, Proxmox and more
 - Information Providers
 - Information Providers
-  - Coin Market Cap, Mastodon
+  - Coin Market Cap, Mastodon and more
 - Information & Utility Widgets
 - Information & Utility Widgets
   - System Stats (Disk, CPU, Memory)
   - System Stats (Disk, CPU, Memory)
   - Weather via [OpenWeatherMap](https://openweathermap.org/) or [Open-Meteo](https://open-meteo.com/)
   - Weather via [OpenWeatherMap](https://openweathermap.org/) or [Open-Meteo](https://open-meteo.com/)
-  - Search Bar
+  - Web Search Bar
+  - UniFi Console, Glances and more
+- Instant "Quick-launch" search
 - Customizable
 - Customizable
   - 21 theme colors with light and dark mode support
   - 21 theme colors with light and dark mode support
   - Background image support
   - Background image support
@@ -63,7 +65,7 @@
 
 
 If you have any questions, suggestions, or general issues, please start a discussion on the [Discussions](https://github.com/benphelps/homepage/discussions) page.
 If you have any questions, suggestions, or general issues, please start a discussion on the [Discussions](https://github.com/benphelps/homepage/discussions) page.
 
 
-If you have a more specific issue, please open an issue on the [Issues](https://github.com/benphelps/homepage/issues) page.
+For bug reports, please open an issue on the [Issues](https://github.com/benphelps/homepage/issues) page.
 
 
 ## Getting Started
 ## Getting Started
 
 
@@ -117,7 +119,7 @@ pnpm start
 
 
 ## Configuration
 ## Configuration
 
 
-Configuration files will be genereted and placed on the first request.
+Configuration files will be generated and placed on the first request.
 
 
 Configuration is done in the /config directory using .yaml files. Refer to each config for
 Configuration is done in the /config directory using .yaml files. Refer to each config for
 the specific configuration options.
 the specific configuration options.

+ 8 - 7
next-i18next.config.js

@@ -98,20 +98,21 @@ module.exports = {
         );
         );
 
 
         i18next.services.formatter.add("rate", (value, lng, options) => {
         i18next.services.formatter.add("rate", (value, lng, options) => {
-          if (value === 0) return "0 Bps";
 
 
-          const bits = options.bits ? value : value / 8;
-          const k = 1024;
+          const k = options.binary ? 1024 : 1000;
+          const sizes = options.bits ? (options.binary ? BIBIT_UNITS : BIT_UNITS) : (options.binary ? BIBYTE_UNITS : BYTE_UNITS);
+
+          if (value === 0) return `0 ${sizes[0]}/s`;
+
           const dm = options.decimals ? options.decimals : 0;
           const dm = options.decimals ? options.decimals : 0;
-          const sizes = ["Bps", "KiBps", "MiBps", "GiBps", "TiBps", "PiBps", "EiBps", "ZiBps", "YiBps"];
 
 
-          const i = Math.floor(Math.log(bits) / Math.log(k));
+          const i = options.binary ? 2 : Math.floor(Math.log(value) / Math.log(k));
 
 
           const formatted = new Intl.NumberFormat(lng, { maximumFractionDigits: dm, minimumFractionDigits: dm }).format(
           const formatted = new Intl.NumberFormat(lng, { maximumFractionDigits: dm, minimumFractionDigits: dm }).format(
-            parseFloat(bits / k ** i)
+            parseFloat(value / k ** i)
           );
           );
 
 
-          return `${formatted} ${sizes[i]}`;
+          return `${formatted} ${sizes[i]}/s`;
         });
         });
 
 
         i18next.services.formatter.add("percent", (value, lng, options) =>
         i18next.services.formatter.add("percent", (value, lng, options) =>

+ 47 - 5
public/locales/ar/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
-        "leech": "Leech",
-        "seed": "Seed",
+    "flood": {
         "download": "Download",
         "download": "Download",
-        "upload": "Upload"
+        "upload": "Upload",
+        "leech": "Leech",
+        "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 48 - 6
public/locales/bg/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
-        "seed": "Seed",
-        "download": "Download",
-        "upload": "Upload",
-        "leech": "Leech"
-    },
     "flood": {
     "flood": {
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed",
         "seed": "Seed",
         "download": "Download",
         "download": "Download",
         "upload": "Upload"
         "upload": "Upload"
+    },
+    "tdarr": {
+        "saved": "Saved",
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
+        "download": "Download",
+        "upload": "Upload",
+        "leech": "Leech",
+        "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 167 - 125
public/locales/ca/common.json

@@ -3,10 +3,10 @@
         "missing_type": "Falta el tipus de widget: {{type}}",
         "missing_type": "Falta el tipus de widget: {{type}}",
         "api_error": "Error d'API",
         "api_error": "Error d'API",
         "status": "Estat",
         "status": "Estat",
-        "information": "Information",
+        "information": "Informació",
         "url": "URL",
         "url": "URL",
-        "raw_error": "Raw Error",
-        "response_data": "Response Data"
+        "raw_error": "Error sense processar",
+        "response_data": "Dades de resposta"
     },
     },
     "weather": {
     "weather": {
         "allow": "Feu clic per permetre",
         "allow": "Feu clic per permetre",
@@ -20,8 +20,8 @@
     "transmission": {
     "transmission": {
         "seed": "Llavors",
         "seed": "Llavors",
         "download": "Descàrrega",
         "download": "Descàrrega",
-        "upload": "Càrrega",
-        "leech": "Companys"
+        "upload": "Pujada",
+        "leech": "Company"
     },
     },
     "sonarr": {
     "sonarr": {
         "wanted": "Volgut",
         "wanted": "Volgut",
@@ -30,13 +30,13 @@
     },
     },
     "speedtest": {
     "speedtest": {
         "ping": "Ping",
         "ping": "Ping",
-        "upload": "Càrrega",
+        "upload": "Pujada",
         "download": "Descàrrega"
         "download": "Descàrrega"
     },
     },
     "resources": {
     "resources": {
         "total": "Total",
         "total": "Total",
         "free": "Lliure",
         "free": "Lliure",
-        "used": "Usat",
+        "used": "Utilitzat",
         "load": "Càrrega",
         "load": "Càrrega",
         "cpu": "CPU"
         "cpu": "CPU"
     },
     },
@@ -47,13 +47,13 @@
         "cpu": "Processador",
         "cpu": "Processador",
         "offline": "Fora de línia",
         "offline": "Fora de línia",
         "error": "Error",
         "error": "Error",
-        "unknown": "Unknown"
+        "unknown": "Desconegut"
     },
     },
     "emby": {
     "emby": {
         "playing": "Reproduint",
         "playing": "Reproduint",
         "transcoding": "Transcodificant",
         "transcoding": "Transcodificant",
         "bitrate": "Taxa de bits",
         "bitrate": "Taxa de bits",
-        "no_active": "Sense transmissions actives"
+        "no_active": "Sense reproduccions actives"
     },
     },
     "tautulli": {
     "tautulli": {
         "playing": "Reproduint",
         "playing": "Reproduint",
@@ -73,14 +73,14 @@
     },
     },
     "rutorrent": {
     "rutorrent": {
         "active": "Actiu",
         "active": "Actiu",
-        "upload": "Càrrega",
+        "upload": "Pujada",
         "download": "Descàrrega"
         "download": "Descàrrega"
     },
     },
     "radarr": {
     "radarr": {
         "wanted": "Volgut",
         "wanted": "Volgut",
         "queued": "En cua",
         "queued": "En cua",
         "movies": "Pel·lícules",
         "movies": "Pel·lícules",
-        "missing": "Missing"
+        "missing": "Faltant"
     },
     },
     "readarr": {
     "readarr": {
         "wanted": "Volgut",
         "wanted": "Volgut",
@@ -101,7 +101,7 @@
         "pending": "Pendent",
         "pending": "Pendent",
         "approved": "Aprovat",
         "approved": "Aprovat",
         "available": "Disponible",
         "available": "Disponible",
-        "processing": "Processing"
+        "processing": "Processant"
     },
     },
     "pihole": {
     "pihole": {
         "queries": "Consultes",
         "queries": "Consultes",
@@ -163,8 +163,8 @@
     },
     },
     "qbittorrent": {
     "qbittorrent": {
         "download": "Descàrrega",
         "download": "Descàrrega",
-        "upload": "Càrrega",
-        "leech": "Companys",
+        "upload": "Pujada",
+        "leech": "Company",
         "seed": "Llavors"
         "seed": "Llavors"
     },
     },
     "mastodon": {
     "mastodon": {
@@ -184,26 +184,26 @@
         "failedLoginsLast24H": "Errors d'inici de sessió (24h)"
         "failedLoginsLast24H": "Errors d'inici de sessió (24h)"
     },
     },
     "proxmox": {
     "proxmox": {
-        "vms": "VMs",
+        "vms": "Màquines Virtuals",
         "mem": "Memòria",
         "mem": "Memòria",
         "cpu": "Processador",
         "cpu": "Processador",
         "lxc": "LXC"
         "lxc": "LXC"
     },
     },
     "unifi": {
     "unifi": {
         "users": "Usuaris",
         "users": "Usuaris",
-        "uptime": "System Uptime",
-        "days": "Días",
+        "uptime": "Temps actiu",
+        "days": "Dies",
         "wan": "WAN",
         "wan": "WAN",
-        "lan_users": "LAN Users",
-        "wlan_users": "WLAN Users",
-        "up": "UP",
-        "down": "DOWN",
+        "lan_users": "Usuaris LAN",
+        "wlan_users": "Usuaris WLAN",
+        "up": "ACTIU",
+        "down": "INACTIU",
         "wait": "Si us plau, espereu",
         "wait": "Si us plau, espereu",
         "lan": "LAN",
         "lan": "LAN",
         "wlan": "WLAN",
         "wlan": "WLAN",
-        "devices": "Devices",
-        "lan_devices": "LAN Devices",
-        "wlan_devices": "WLAN Devices"
+        "devices": "Dispositius",
+        "lan_devices": "Dispositius LAN",
+        "wlan_devices": "Dispositius WLAN"
     },
     },
     "plex": {
     "plex": {
         "streams": "Transmissions actives",
         "streams": "Transmissions actives",
@@ -216,119 +216,119 @@
         "wait": "Si us plau, espereu"
         "wait": "Si us plau, espereu"
     },
     },
     "changedetectionio": {
     "changedetectionio": {
-        "totalObserved": "Total Observed",
-        "diffsDetected": "Diffs Detected"
+        "totalObserved": "Total d'observats",
+        "diffsDetected": "Diferències detectades"
     },
     },
     "wmo": {
     "wmo": {
-        "66-day": "Freezing Rain",
-        "95-day": "Thunderstorm",
-        "95-night": "Thunderstorm",
-        "96-day": "Thunderstorm With Hail",
-        "0-day": "Sunny",
-        "0-night": "Clear",
-        "1-day": "Mainly Sunny",
-        "1-night": "Mainly Clear",
-        "2-day": "Partly Cloudy",
-        "2-night": "Partly Cloudy",
-        "3-day": "Cloudy",
-        "3-night": "Cloudy",
-        "45-day": "Foggy",
-        "45-night": "Foggy",
-        "48-day": "Foggy",
-        "48-night": "Foggy",
-        "51-day": "Light Drizzle",
-        "51-night": "Light Drizzle",
-        "53-day": "Drizzle",
-        "53-night": "Drizzle",
-        "55-day": "Heavy Drizzle",
-        "55-night": "Heavy Drizzle",
-        "56-day": "Light Freezing Drizzle",
-        "56-night": "Light Freezing Drizzle",
-        "65-night": "Heavy Rain",
-        "57-day": "Freezing Drizzle",
-        "57-night": "Freezing Drizzle",
-        "61-day": "Light Rain",
-        "61-night": "Light Rain",
-        "63-day": "Rain",
-        "63-night": "Rain",
-        "65-day": "Heavy Rain",
-        "66-night": "Freezing Rain",
-        "67-day": "Freezing Rain",
-        "67-night": "Freezing Rain",
-        "71-day": "Light Snow",
-        "71-night": "Light Snow",
-        "73-day": "Snow",
-        "73-night": "Snow",
-        "75-day": "Heavy Snow",
-        "75-night": "Heavy Snow",
-        "77-day": "Snow Grains",
-        "77-night": "Snow Grains",
-        "80-day": "Light Showers",
-        "80-night": "Light Showers",
-        "81-day": "Showers",
-        "81-night": "Showers",
-        "82-day": "Heavy Showers",
-        "82-night": "Heavy Showers",
-        "85-day": "Snow Showers",
-        "85-night": "Snow Showers",
-        "86-day": "Snow Showers",
-        "86-night": "Snow Showers",
-        "96-night": "Thunderstorm With Hail",
-        "99-day": "Thunderstorm With Hail",
-        "99-night": "Thunderstorm With Hail"
+        "66-day": "Pluja gelada",
+        "95-day": "Tempesta",
+        "95-night": "Tempesta",
+        "96-day": "Tempesta amb calamarsa",
+        "0-day": "Assolellat",
+        "0-night": "Cel clar",
+        "1-day": "Majorment assolellat",
+        "1-night": "Majorment clar",
+        "2-day": "Parcialment ennuvolat",
+        "2-night": "Parcialment ennuvolat",
+        "3-day": "Ennuvolat",
+        "3-night": "Ennuvolat",
+        "45-day": "Boirós",
+        "45-night": "Boirós",
+        "48-day": "Boirós",
+        "48-night": "Boirós",
+        "51-day": "Ruixats lleugers",
+        "51-night": "Ruixats lleugers",
+        "53-day": "Ruixat",
+        "53-night": "Ruxiat",
+        "55-day": "Ruixat intens",
+        "55-night": "Ruixat intens",
+        "56-day": "Lleuger ruixat gelat",
+        "56-night": "Lleuger ruixat gelat",
+        "65-night": "Pluja intensa",
+        "57-day": "Ruixat gelat",
+        "57-night": "Ruixat gelat",
+        "61-day": "Pluja lleugera",
+        "61-night": "Pluja lleugera",
+        "63-day": "Pluja",
+        "63-night": "Pluja",
+        "65-day": "Pluja intensa",
+        "66-night": "Pluja gelada",
+        "67-day": "Pluja gelada",
+        "67-night": "Pluja gelada",
+        "71-day": "Neu lleugera",
+        "71-night": "Neu lleugera",
+        "73-day": "Neu",
+        "73-night": "Neu",
+        "75-day": "Neu intensa",
+        "75-night": "Neu intensa",
+        "77-day": "Neu lleugera",
+        "77-night": "Neu lleugera",
+        "80-day": "Plovisqueig",
+        "80-night": "Plovisqueig",
+        "81-day": "Xàfecs",
+        "81-night": "Xàfecs",
+        "82-day": "Xàfecs intensos",
+        "82-night": "Xàfecs intensos",
+        "85-day": "Xàfecs de neu",
+        "85-night": "Xàfecs de neu",
+        "86-day": "Xàfecs de neu",
+        "86-night": "Xàfecs de neu",
+        "96-night": "Tempesta amb calamarsa",
+        "99-day": "Tempesta amb calamarsa",
+        "99-night": "Tempesta amb calamarsa"
     },
     },
     "quicklaunch": {
     "quicklaunch": {
-        "bookmark": "Bookmark",
-        "service": "Service"
+        "bookmark": "Marcador",
+        "service": "Servei"
     },
     },
     "homebridge": {
     "homebridge": {
-        "available_update": "System",
-        "updates": "Updates",
-        "update_available": "Update Available",
-        "up_to_date": "Up to Date",
+        "available_update": "Sistema",
+        "updates": "Actualitzacions",
+        "update_available": "Actualització disponible",
+        "up_to_date": "Actualitzat",
         "child_bridges": "Child Bridges",
         "child_bridges": "Child Bridges",
         "child_bridges_status": "{{ok}}/{{total}}"
         "child_bridges_status": "{{ok}}/{{total}}"
     },
     },
     "autobrr": {
     "autobrr": {
-        "approvedPushes": "Approved",
-        "rejectedPushes": "Rejected",
-        "filters": "Filters",
-        "indexers": "Indexers"
+        "approvedPushes": "Aprovat",
+        "rejectedPushes": "Rebutjat",
+        "filters": "Filtres",
+        "indexers": "Indexadors"
     },
     },
     "watchtower": {
     "watchtower": {
-        "containers_scanned": "Scanned",
-        "containers_updated": "Updated",
-        "containers_failed": "Failed"
+        "containers_scanned": "Escanejat",
+        "containers_updated": "Actualitzat",
+        "containers_failed": "Error"
     },
     },
     "tubearchivist": {
     "tubearchivist": {
-        "downloads": "Queue",
-        "videos": "Videos",
-        "channels": "Channels",
-        "playlists": "Playlists"
+        "downloads": "Cua",
+        "videos": "Vídeos",
+        "channels": "Canals",
+        "playlists": "Llistes de reproducció"
     },
     },
     "truenas": {
     "truenas": {
-        "load": "System Load",
-        "uptime": "Uptime",
-        "alerts": "Alerts",
+        "load": "Càrrega del sistema",
+        "uptime": "Temps actiu",
+        "alerts": "Alertes",
         "time": "{{value, number(style: unit; unitDisplay: long;)}}"
         "time": "{{value, number(style: unit; unitDisplay: long;)}}"
     },
     },
     "navidrome": {
     "navidrome": {
-        "nothing_streaming": "No Active Streams",
-        "please_wait": "Please Wait"
+        "nothing_streaming": "Cap reproducció activa",
+        "please_wait": "Espereu si us plau"
     },
     },
     "pyload": {
     "pyload": {
-        "speed": "Speed",
-        "active": "Active",
-        "queue": "Queue",
+        "speed": "Velocitat",
+        "active": "Actiu",
+        "queue": "Cua",
         "total": "Total"
         "total": "Total"
     },
     },
     "gluetun": {
     "gluetun": {
-        "public_ip": "Public IP",
-        "region": "Region",
-        "country": "Country"
+        "public_ip": "IP Pública",
+        "region": "Regió",
+        "country": "País"
     },
     },
     "hdhomerun": {
     "hdhomerun": {
-        "channels": "Channels",
+        "channels": "Canals",
         "hd": "HD"
         "hd": "HD"
     },
     },
     "ping": {
     "ping": {
@@ -336,30 +336,72 @@
         "ping": "Ping"
         "ping": "Ping"
     },
     },
     "scrutiny": {
     "scrutiny": {
-        "passed": "Passed",
-        "failed": "Failed",
-        "unknown": "Unknown"
+        "passed": "Aprobat",
+        "failed": "Error",
+        "unknown": "Desconegut"
     },
     },
     "paperlessngx": {
     "paperlessngx": {
-        "inbox": "Inbox",
+        "inbox": "Safata d'entrada",
         "total": "Total"
         "total": "Total"
     },
     },
     "deluge": {
     "deluge": {
-        "seed": "Seed",
-        "download": "Download",
-        "upload": "Upload",
-        "leech": "Leech"
-    },
-    "diskstation": {
-        "download": "Download",
-        "upload": "Upload",
-        "leech": "Leech",
-        "seed": "Seed"
+        "seed": "Llavor",
+        "download": "Descàrrega",
+        "upload": "Pujada",
+        "leech": "Company"
     },
     },
     "flood": {
     "flood": {
+        "download": "Descarregar",
+        "upload": "Pujada",
+        "leech": "Company",
+        "seed": "Llavor"
+    },
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/cs/common.json

@@ -350,16 +350,58 @@
         "seed": "Seed",
         "seed": "Seed",
         "download": "Download"
         "download": "Download"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 46 - 4
public/locales/da/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
+        "leech": "Leech",
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
-        "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
-        "leech": "Leech",
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
+        "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 45 - 3
public/locales/de/common.json

@@ -6,7 +6,7 @@
         "url": "URL",
         "url": "URL",
         "information": "Information",
         "information": "Information",
         "raw_error": "Raw Error",
         "raw_error": "Raw Error",
-        "response_data": "Response Data"
+        "response_data": "Empfangene Daten"
     },
     },
     "search": {
     "search": {
         "placeholder": "Suche…"
         "placeholder": "Suche…"
@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "unread": "Unread",
+        "read": "Read"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 4
public/locales/en/common.json

@@ -3,9 +3,11 @@
         "bytes": "{{value, bytes}}",
         "bytes": "{{value, bytes}}",
         "bits": "{{value, bytes(bits: true)}}",
         "bits": "{{value, bytes(bits: true)}}",
         "bbytes": "{{value, bytes(binary: true)}}",
         "bbytes": "{{value, bytes(binary: true)}}",
-        "bbits": "{{value, bytes(bits: true, binary: true)}}",
-        "byterate": "{{value, rate}}",
+        "bbits": "{{value, bytes(bits: true; binary: true)}}",
+        "byterate": "{{value, rate(bits: false)}}",
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
         "bitrate": "{{value, rate(bits: true)}}",
         "bitrate": "{{value, rate(bits: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}",
         "percent": "{{value, percent}}",
         "percent": "{{value, percent}}",
         "number": "{{value, number}}",
         "number": "{{value, number}}",
         "ms": "{{value, number}}"
         "ms": "{{value, number}}"
@@ -86,6 +88,13 @@
         "bitrate": "Bitrate",
         "bitrate": "Bitrate",
         "no_active": "No Active Streams"
         "no_active": "No Active Streams"
     },
     },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
     "nzbget": {
     "nzbget": {
         "rate": "Rate",
         "rate": "Rate",
         "remaining": "Remaining",
         "remaining": "Remaining",
@@ -124,7 +133,7 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
@@ -192,6 +201,12 @@
         "stopped": "Stopped",
         "stopped": "Stopped",
         "total": "Total"
         "total": "Total"
     },
     },
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
     "traefik": {
     "traefik": {
         "routers": "Routers",
         "routers": "Routers",
         "services": "Services",
         "services": "Services",
@@ -240,6 +255,10 @@
         "status_count": "Posts",
         "status_count": "Posts",
         "domain_count": "Domains"
         "domain_count": "Domains"
     },
     },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
     "authentik": {
     "authentik": {
         "users": "Users",
         "users": "Users",
         "loginsLast24H": "Logins (24h)",
         "loginsLast24H": "Logins (24h)",
@@ -372,5 +391,26 @@
     "paperlessngx": {
     "paperlessngx": {
         "inbox": "Inbox",
         "inbox": "Inbox",
         "total": "Total"
         "total": "Total"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
-}
+}

+ 45 - 3
public/locales/eo/common.json

@@ -350,16 +350,58 @@
         "inbox": "Inbox",
         "inbox": "Inbox",
         "total": "Totalo"
         "total": "Totalo"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
-        "leech": "Leech",
         "upload": "Upload",
         "upload": "Upload",
+        "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 48 - 6
public/locales/es/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Semilla"
         "seed": "Semilla"
     },
     },
-    "diskstation": {
-        "download": "Descargar",
-        "upload": "Cargar",
-        "leech": "Leech",
-        "seed": "Semilla"
-    },
     "flood": {
     "flood": {
         "download": "Descargar",
         "download": "Descargar",
         "upload": "Subir",
         "upload": "Subir",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "tdarr": {
+        "queue": "Cola",
+        "processed": "Procesado",
+        "saved": "Guardado",
+        "errored": "Error"
+    },
+    "miniflux": {
+        "read": "Leer",
+        "unread": "Sin leer"
+    },
+    "nextdns": {
+        "wait": "Espere, por favor",
+        "no_devices": "No se reciben datos del dispositivo"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "AP conectados",
+        "activeUser": "Dispositivos activos",
+        "alerts": "Alertas",
+        "connectedGateway": "Pasarelas conectadas",
+        "connectedSwitches": "Interruptores conectados"
+    },
+    "downloadstation": {
+        "download": "Descargar",
+        "upload": "Subir",
+        "leech": "Sanguijuela",
+        "seed": "Semilla"
+    },
+    "mikrotik": {
+        "cpuLoad": "Carga de la CPU",
+        "memoryUsed": "Memoria utilizada",
+        "uptime": "Tiempo en funcionamiento",
+        "numberOfLeases": "Alquileres"
+    },
+    "xteve": {
+        "streams_all": "Todas las corrientes",
+        "streams_active": "Corrientes activas",
+        "streams_xepg": "Canales XEPG"
+    },
+    "opnsense": {
+        "cpu": "Carga de la CPU",
+        "memory": "Memoria activa",
+        "wanUpload": "Carga WAN",
+        "wanDownload": "Descargar WAN"
     }
     }
 }
 }

+ 44 - 2
public/locales/fi/common.json

@@ -350,16 +350,58 @@
         "seed": "Seed",
         "seed": "Seed",
         "download": "Download"
         "download": "Download"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedSwitches": "Connected switches",
+        "connectedGateway": "Connected gateways"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 45 - 3
public/locales/fr/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
-        "download": "Réception",
+    "flood": {
+        "download": "Récep.",
         "upload": "Envoi",
         "upload": "Envoi",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "À traiter",
+        "processed": "Traité",
+        "errored": "En erreur",
+        "saved": "Enregistré"
+    },
+    "miniflux": {
+        "read": "Lu",
+        "unread": "Non lu"
+    },
+    "nextdns": {
+        "wait": "Patientez...",
+        "no_devices": "Aucune donnée d'appareil reçue"
+    },
+    "common": {
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}",
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "APs connectées",
+        "activeUser": "Équipts actifs",
+        "alerts": "Alertes",
+        "connectedGateway": "Passerelles connectées",
+        "connectedSwitches": "Switches connectés"
+    },
+    "downloadstation": {
         "download": "Récep.",
         "download": "Récep.",
         "upload": "Envoi",
         "upload": "Envoi",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "Charge CPU",
+        "memoryUsed": "Mém. Utilisée",
+        "uptime": "Disponibilité",
+        "numberOfLeases": "Baux"
+    },
+    "xteve": {
+        "streams_all": "Tous les flux",
+        "streams_active": "Flux actif",
+        "streams_xepg": "Canal XEPG"
+    },
+    "opnsense": {
+        "cpu": "Charge CPU",
+        "memory": "Mém. Utilisée",
+        "wanUpload": "WAN Envoi",
+        "wanDownload": "WAN Récep."
     }
     }
 }
 }

+ 45 - 3
public/locales/he/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
+        "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
-        "download": "Download",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/hi/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 46 - 4
public/locales/hr/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Preuzimanje",
         "download": "Preuzimanje",
         "upload": "Prijenos",
         "upload": "Prijenos",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
-        "download": "Preuzimanje",
-        "upload": "Prijenos",
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
+        "download": "Download",
+        "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/hu/common.json

@@ -350,16 +350,58 @@
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech"
         "leech": "Leech"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedAp": "Connected APs",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 69 - 27
public/locales/it/common.json

@@ -5,18 +5,18 @@
         "cpu": "CPU",
         "cpu": "CPU",
         "offline": "Offline",
         "offline": "Offline",
         "rx": "RX",
         "rx": "RX",
-        "error": "Error",
-        "unknown": "Unknown"
+        "error": "Errore",
+        "unknown": "Sconosciuto"
     },
     },
     "emby": {
     "emby": {
         "playing": "In riproduzione",
         "playing": "In riproduzione",
-        "transcoding": "Transcoding",
+        "transcoding": "Transcodifica",
         "bitrate": "Bitrate",
         "bitrate": "Bitrate",
         "no_active": "Nessuno Stream Attivo"
         "no_active": "Nessuno Stream Attivo"
     },
     },
     "tautulli": {
     "tautulli": {
         "playing": "In riproduzione",
         "playing": "In riproduzione",
-        "transcoding": "Transcoding",
+        "transcoding": "Transcodifica",
         "bitrate": "Bitrate",
         "bitrate": "Bitrate",
         "no_active": "Nessuno Stream Attivo"
         "no_active": "Nessuno Stream Attivo"
     },
     },
@@ -31,7 +31,7 @@
         "total": "Totali"
         "total": "Totali"
     },
     },
     "traefik": {
     "traefik": {
-        "routers": "Routers",
+        "routers": "Router",
         "services": "Servizi",
         "services": "Servizi",
         "middleware": "Middleware"
         "middleware": "Middleware"
     },
     },
@@ -40,9 +40,9 @@
         "api_error": "Errore API",
         "api_error": "Errore API",
         "status": "Stato",
         "status": "Stato",
         "url": "URL",
         "url": "URL",
-        "information": "Information",
+        "information": "Informazione",
         "raw_error": "Raw Error",
         "raw_error": "Raw Error",
-        "response_data": "Response Data"
+        "response_data": "Dati risposta"
     },
     },
     "search": {
     "search": {
         "placeholder": "Cerca…"
         "placeholder": "Cerca…"
@@ -105,7 +105,7 @@
         "pending": "In attesa",
         "pending": "In attesa",
         "approved": "Approvati",
         "approved": "Approvati",
         "available": "Disponibili",
         "available": "Disponibili",
-        "processing": "Processing"
+        "processing": "In lavorazione"
     },
     },
     "sabnzbd": {
     "sabnzbd": {
         "rate": "Rapporto",
         "rate": "Rapporto",
@@ -126,13 +126,13 @@
     },
     },
     "gotify": {
     "gotify": {
         "apps": "Applicazioni",
         "apps": "Applicazioni",
-        "clients": "Clients",
+        "clients": "Client",
         "messages": "Messaggi"
         "messages": "Messaggi"
     },
     },
     "prowlarr": {
     "prowlarr": {
         "enableIndexers": "Indicizzatori",
         "enableIndexers": "Indicizzatori",
         "numberOfGrabs": "Grabs",
         "numberOfGrabs": "Grabs",
-        "numberOfQueries": "Queries",
+        "numberOfQueries": "Interrogazioni",
         "numberOfFailGrabs": "Grabs Falliti",
         "numberOfFailGrabs": "Grabs Falliti",
         "numberOfFailQueries": "Queries Fallite"
         "numberOfFailQueries": "Queries Fallite"
     },
     },
@@ -153,10 +153,10 @@
     "lidarr": {
     "lidarr": {
         "wanted": "Mancanti",
         "wanted": "Mancanti",
         "queued": "In coda",
         "queued": "In coda",
-        "albums": "Albums"
+        "albums": "Album"
     },
     },
     "adguard": {
     "adguard": {
-        "queries": "Queries",
+        "queries": "Interrogazioni",
         "blocked": "Bloccati",
         "blocked": "Bloccati",
         "filtered": "Filtrati",
         "filtered": "Filtrati",
         "latency": "Latenza"
         "latency": "Latenza"
@@ -304,12 +304,12 @@
         "downloads": "Coda",
         "downloads": "Coda",
         "videos": "Video",
         "videos": "Video",
         "channels": "Canali",
         "channels": "Canali",
-        "playlists": "Playlists"
+        "playlists": "Playlist"
     },
     },
     "truenas": {
     "truenas": {
         "load": "Carico di Sistema",
         "load": "Carico di Sistema",
-        "uptime": "Uptime",
-        "alerts": "Alerts",
+        "uptime": "Tempo di attività",
+        "alerts": "Avvisi",
         "time": "{{value, number(style: unit; unitDisplay: long;)}}"
         "time": "{{value, number(style: unit; unitDisplay: long;)}}"
     },
     },
     "navidrome": {
     "navidrome": {
@@ -323,26 +323,26 @@
         "total": "Totale"
         "total": "Totale"
     },
     },
     "gluetun": {
     "gluetun": {
-        "public_ip": "Public IP",
-        "region": "Region",
-        "country": "Country"
+        "public_ip": "IP pubblico",
+        "region": "Località",
+        "country": "Stato"
     },
     },
     "hdhomerun": {
     "hdhomerun": {
-        "channels": "Channels",
+        "channels": "Canali",
         "hd": "HD"
         "hd": "HD"
     },
     },
     "ping": {
     "ping": {
-        "error": "Error",
+        "error": "Errore",
         "ping": "Ping"
         "ping": "Ping"
     },
     },
     "scrutiny": {
     "scrutiny": {
-        "passed": "Passed",
-        "failed": "Failed",
-        "unknown": "Unknown"
+        "passed": "Passati",
+        "failed": "Falliti",
+        "unknown": "Sconosciuto"
     },
     },
     "paperlessngx": {
     "paperlessngx": {
-        "inbox": "Inbox",
-        "total": "Total"
+        "inbox": "In arrivo",
+        "total": "Totali"
     },
     },
     "deluge": {
     "deluge": {
         "download": "Download",
         "download": "Download",
@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "In coda",
+        "processed": "Elaborati",
+        "errored": "Errori",
+        "saved": "Salvati"
+    },
+    "miniflux": {
+        "unread": "Non letti",
+        "read": "Letti"
+    },
+    "nextdns": {
+        "wait": "Attendi",
+        "no_devices": "Nessun dato del dispositivo ricevuto"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "AP Connessi",
+        "activeUser": "Dispositivi attivi",
+        "alerts": "Allarmi",
+        "connectedGateway": "Gateway connessi",
+        "connectedSwitches": "Switch connessi"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "Carico della CPU",
+        "memoryUsed": "Memoria Utilizzata",
+        "uptime": "Tempo di attività",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 45 - 3
public/locales/ms/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
-        "upload": "Upload",
+    "flood": {
         "download": "Download",
         "download": "Download",
+        "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 45 - 3
public/locales/nb-NO/common.json

@@ -350,16 +350,58 @@
         "upload": "Upload",
         "upload": "Upload",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
-        "leech": "Leech",
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
+        "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases",
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/nl/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/pl/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Pobieranie",
         "download": "Pobieranie",
         "upload": "Wysyłanie",
         "upload": "Wysyłanie",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Kolejka",
+        "processed": "Przetworzone",
+        "errored": "Błędne",
+        "saved": "Zapisane"
+    },
+    "miniflux": {
+        "read": "Przeczytane",
+        "unread": "Nieprzeczytane"
+    },
+    "nextdns": {
+        "wait": "Proszę czekać",
+        "no_devices": "Nie otrzymano danych urządzenia"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedSwitches": "Połączone przełączniki",
+        "connectedAp": "Połączone punkty dostępowe",
+        "activeUser": "Aktywne urządzenia",
+        "alerts": "Alarmy",
+        "connectedGateway": "Połączone bramy"
+    },
+    "downloadstation": {
         "download": "Pobieranie",
         "download": "Pobieranie",
         "upload": "Wysyłanie",
         "upload": "Wysyłanie",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "Obciążenie procesora",
+        "memoryUsed": "Zuyżyta pamięć",
+        "uptime": "Czas działania",
+        "numberOfLeases": "Dzierżawy"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/pt-BR/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 73 - 33
public/locales/pt/common.json

@@ -2,10 +2,10 @@
     "widget": {
     "widget": {
         "missing_type": "Widget ausente: {{type}}",
         "missing_type": "Widget ausente: {{type}}",
         "api_error": "Erro da API",
         "api_error": "Erro da API",
-        "status": "Status",
+        "status": "Estado",
         "information": "Informação",
         "information": "Informação",
         "url": "Endereço URL",
         "url": "Endereço URL",
-        "raw_error": "Raw Error",
+        "raw_error": "Erro",
         "response_data": "Dados da Resposta"
         "response_data": "Dados da Resposta"
     },
     },
     "search": {
     "search": {
@@ -104,7 +104,9 @@
         "byterate": "{{value, bytes}}",
         "byterate": "{{value, bytes}}",
         "ms": "{{value, number}}",
         "ms": "{{value, number}}",
         "bitrate": "{{value, bytes(bits: true)}}",
         "bitrate": "{{value, bytes(bits: true)}}",
-        "percent": "{{value, percent}}"
+        "percent": "{{value, percent}}",
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
     },
     },
     "weather": {
     "weather": {
         "current": "Localização atual",
         "current": "Localização atual",
@@ -207,9 +209,9 @@
         "wan": "WAN",
         "wan": "WAN",
         "lan_users": "Utilizadores LAN",
         "lan_users": "Utilizadores LAN",
         "wlan_users": "Utilizadores WLAN",
         "wlan_users": "Utilizadores WLAN",
-        "up": "UP",
-        "down": "DOWN",
-        "wait": "Por favor aguarde",
+        "up": "Ligados",
+        "down": "Desligados",
+        "wait": "Por favor, aguarde",
         "lan": "LAN",
         "lan": "LAN",
         "wlan": "WLAN",
         "wlan": "WLAN",
         "devices": "Dispositivos",
         "devices": "Dispositivos",
@@ -224,32 +226,32 @@
     "glances": {
     "glances": {
         "cpu": "CPU",
         "cpu": "CPU",
         "mem": "MEM",
         "mem": "MEM",
-        "wait": "Please wait"
+        "wait": "Por favor, aguarde"
     },
     },
     "changedetectionio": {
     "changedetectionio": {
         "totalObserved": "Total Observado",
         "totalObserved": "Total Observado",
         "diffsDetected": "Diferenças Detetadas"
         "diffsDetected": "Diferenças Detetadas"
     },
     },
     "wmo": {
     "wmo": {
-        "0-day": "Sunny",
-        "0-night": "Clear",
-        "1-day": "Mainly Sunny",
-        "1-night": "Mainly Clear",
-        "2-day": "Partly Cloudy",
-        "2-night": "Partly Cloudy",
-        "3-day": "Cloudy",
-        "3-night": "Cloudy",
+        "0-day": "Solarengo",
+        "0-night": "Limpo",
+        "1-day": "Maioritariamente ensolarado",
+        "1-night": "Maioritariamente Limpo",
+        "2-day": "Parcialmente Nublado",
+        "2-night": "Parcialmente nublado",
+        "3-day": "Nublado",
+        "3-night": "Nublado",
         "99-night": "Thunderstorm With Hail",
         "99-night": "Thunderstorm With Hail",
-        "45-day": "Foggy",
-        "45-night": "Foggy",
-        "48-day": "Foggy",
-        "48-night": "Foggy",
-        "51-day": "Light Drizzle",
-        "51-night": "Light Drizzle",
-        "53-day": "Drizzle",
-        "53-night": "Drizzle",
-        "55-day": "Heavy Drizzle",
-        "55-night": "Heavy Drizzle",
+        "45-day": "Nevoeiro",
+        "45-night": "Nevoeiro",
+        "48-day": "Nevoeiro",
+        "48-night": "Nevoeiro",
+        "51-day": "Aguaceiros",
+        "51-night": "Aguaceiros",
+        "53-day": "Chuvisco",
+        "53-night": "Chuvisco",
+        "55-day": "Aguaceiro Forte",
+        "55-night": "Aguaceiro Forte",
         "56-day": "Light Freezing Drizzle",
         "56-day": "Light Freezing Drizzle",
         "56-night": "Light Freezing Drizzle",
         "56-night": "Light Freezing Drizzle",
         "57-day": "Freezing Drizzle",
         "57-day": "Freezing Drizzle",
@@ -289,8 +291,8 @@
         "99-day": "Thunderstorm With Hail"
         "99-day": "Thunderstorm With Hail"
     },
     },
     "quicklaunch": {
     "quicklaunch": {
-        "bookmark": "Bookmark",
-        "service": "Service"
+        "bookmark": "Marcador",
+        "service": "Serviço"
     },
     },
     "homebridge": {
     "homebridge": {
         "available_update": "System",
         "available_update": "System",
@@ -361,16 +363,54 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
-        "download": "Download",
-        "upload": "Upload",
-        "leech": "Leech",
-        "seed": "Seed"
-    },
     "flood": {
     "flood": {
         "download": "Descarregar",
         "download": "Descarregar",
         "upload": "Carregar",
         "upload": "Carregar",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
+        "download": "Download",
+        "upload": "Upload",
+        "leech": "Leech",
+        "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/ro/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "numberOfLeases": "Leases",
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 48 - 6
public/locales/ru/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
-        "download": "Download",
-        "upload": "Upload",
-        "seed": "Seed",
-        "leech": "Leech"
-    },
     "flood": {
     "flood": {
         "upload": "Upload",
         "upload": "Upload",
         "download": "Download",
         "download": "Download",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedSwitches": "Connected switches",
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways"
+    },
+    "downloadstation": {
+        "download": "Download",
+        "upload": "Upload",
+        "leech": "Leech",
+        "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 48 - 6
public/locales/sr/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
-        "download": "Download",
-        "upload": "Upload",
-        "leech": "Leech",
-        "seed": "Seed"
-    },
     "flood": {
     "flood": {
         "download": "Download",
         "download": "Download",
         "seed": "Seed",
         "seed": "Seed",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech"
         "leech": "Leech"
+    },
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
+        "download": "Download",
+        "upload": "Upload",
+        "leech": "Leech",
+        "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/sv/common.json

@@ -350,16 +350,58 @@
         "upload": "Upload",
         "upload": "Upload",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 45 - 3
public/locales/te/common.json

@@ -350,16 +350,58 @@
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech"
         "leech": "Leech"
     },
     },
-    "diskstation": {
-        "leech": "Leech",
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
+        "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases",
+        "memoryUsed": "Memory Used"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/tr/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 407 - 0
public/locales/uk/common.json

@@ -0,0 +1,407 @@
+{
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "alerts": "Оповіщення",
+        "connectedGateway": "Підключені шлюзи",
+        "connectedSwitches": "Підключені перемикачі",
+        "connectedAp": "Підключені точки доступу",
+        "activeUser": "Активні пристрої"
+    },
+    "sabnzbd": {
+        "rate": "Швидкість",
+        "queue": "Черга",
+        "timeleft": "Залишилось"
+    },
+    "rutorrent": {
+        "active": "Активний",
+        "upload": "Відправлення",
+        "download": "Завантаження"
+    },
+    "deluge": {
+        "download": "Завантаження",
+        "upload": "Відправлення",
+        "leech": "Leech",
+        "seed": "Seed"
+    },
+    "readarr": {
+        "wanted": "Розшукується",
+        "queued": "У черзі",
+        "books": "Книжки"
+    },
+    "wmo": {
+        "55-day": "Heavy Drizzle",
+        "55-night": "Heavy Drizzle",
+        "56-day": "Light Freezing Drizzle",
+        "56-night": "Light Freezing Drizzle",
+        "0-day": "Sunny",
+        "0-night": "Clear",
+        "1-day": "Mainly Sunny",
+        "1-night": "Mainly Clear",
+        "2-day": "Partly Cloudy",
+        "2-night": "Partly Cloudy",
+        "3-day": "Cloudy",
+        "3-night": "Cloudy",
+        "53-day": "Drizzle",
+        "45-day": "Foggy",
+        "45-night": "Foggy",
+        "48-day": "Foggy",
+        "48-night": "Foggy",
+        "51-day": "Light Drizzle",
+        "51-night": "Light Drizzle",
+        "53-night": "Drizzle",
+        "57-day": "Freezing Drizzle",
+        "57-night": "Freezing Drizzle",
+        "61-day": "Light Rain",
+        "61-night": "Light Rain",
+        "63-day": "Rain",
+        "63-night": "Rain",
+        "65-day": "Heavy Rain",
+        "65-night": "Heavy Rain",
+        "66-day": "Freezing Rain",
+        "66-night": "Freezing Rain",
+        "67-day": "Freezing Rain",
+        "67-night": "Freezing Rain",
+        "71-day": "Light Snow",
+        "71-night": "Light Snow",
+        "73-day": "Snow",
+        "73-night": "Snow",
+        "75-day": "Heavy Snow",
+        "75-night": "Heavy Snow",
+        "77-day": "Snow Grains",
+        "77-night": "Snow Grains",
+        "80-day": "Light Showers",
+        "80-night": "Light Showers",
+        "81-day": "Showers",
+        "82-day": "Heavy Showers",
+        "82-night": "Heavy Showers",
+        "81-night": "Showers",
+        "85-day": "Snow Showers",
+        "85-night": "Snow Showers",
+        "86-day": "Snow Showers",
+        "86-night": "Snow Showers",
+        "95-day": "Thunderstorm",
+        "95-night": "Thunderstorm",
+        "96-day": "Thunderstorm With Hail",
+        "96-night": "Thunderstorm With Hail",
+        "99-day": "Thunderstorm With Hail",
+        "99-night": "Thunderstorm With Hail"
+    },
+    "pyload": {
+        "speed": "Speed",
+        "active": "Active",
+        "queue": "Queue",
+        "total": "Total"
+    },
+    "gluetun": {
+        "country": "Country",
+        "public_ip": "Public IP",
+        "region": "Region"
+    },
+    "hdhomerun": {
+        "channels": "Channels",
+        "hd": "HD"
+    },
+    "widget": {
+        "missing_type": "Відсутній тип віджета: {{type}}",
+        "api_error": "Помилка API",
+        "information": "Інформація",
+        "status": "Стан",
+        "url": "URL",
+        "raw_error": "Помилка Raw",
+        "response_data": "Дані відповіді"
+    },
+    "weather": {
+        "current": "Поточне розташування",
+        "allow": "Натисніть, щоб дозволити",
+        "updating": "Оновлення",
+        "wait": "Будь ласка, зачекайте"
+    },
+    "search": {
+        "placeholder": "Пошук…"
+    },
+    "resources": {
+        "cpu": "CPU",
+        "total": "Всього",
+        "free": "Вільно",
+        "used": "Використано",
+        "load": "Навантаження"
+    },
+    "unifi": {
+        "users": "Користувачі",
+        "uptime": "Час роботи системи",
+        "days": "Днів",
+        "wan": "WAN",
+        "lan": "LAN",
+        "wlan": "WLAN",
+        "devices": "Пристрої",
+        "lan_devices": "LAN пристрої",
+        "wlan_devices": "WLAN пристрої",
+        "lan_users": "LAN користувачі",
+        "wlan_users": "WLAN користувачі",
+        "up": "Відправка",
+        "down": "Завантаження",
+        "wait": "Будь ласка, зачекайте"
+    },
+    "docker": {
+        "rx": "RX",
+        "tx": "TX",
+        "mem": "Пам'ять",
+        "cpu": "CPU",
+        "offline": "Офлайн",
+        "error": "Помилка",
+        "unknown": "Невідомий"
+    },
+    "ping": {
+        "error": "Помилка",
+        "ping": "Пінг"
+    },
+    "emby": {
+        "playing": "Відтворення",
+        "transcoding": "Перекодування",
+        "bitrate": "Бітрейт",
+        "no_active": "Немає активних потоків"
+    },
+    "flood": {
+        "download": "Завантаження",
+        "upload": "Відправлення",
+        "leech": "Leech",
+        "seed": "Seed"
+    },
+    "changedetectionio": {
+        "totalObserved": "Всього спостережень",
+        "diffsDetected": "Виявлено відмінності"
+    },
+    "tautulli": {
+        "playing": "Відтворення",
+        "transcoding": "Перекодування",
+        "bitrate": "Бітрейт",
+        "no_active": "Немає активних потоків"
+    },
+    "nzbget": {
+        "rate": "Швидкість",
+        "downloaded": "Завантажено",
+        "remaining": "Залишилося"
+    },
+    "plex": {
+        "streams": "Активні потоки",
+        "movies": "Фільми",
+        "tv": "TБ шоу"
+    },
+    "transmission": {
+        "download": "Завантаження",
+        "upload": "Відправлення",
+        "leech": "Leech",
+        "seed": "Seed"
+    },
+    "qbittorrent": {
+        "download": "Завантаження",
+        "upload": "Відправлення",
+        "leech": "Leech",
+        "seed": "Seed"
+    },
+    "downloadstation": {
+        "download": "Завантаження",
+        "upload": "Відправлення",
+        "leech": "Leech",
+        "seed": "Seed"
+    },
+    "sonarr": {
+        "wanted": "Розшукується",
+        "queued": "У черзі",
+        "series": "Серії"
+    },
+    "radarr": {
+        "wanted": "Розшукується",
+        "missing": "Відсутній",
+        "queued": "У черзі",
+        "movies": "Фільми"
+    },
+    "lidarr": {
+        "wanted": "Розшукується",
+        "queued": "У черзі",
+        "albums": "Альбоми"
+    },
+    "traefik": {
+        "middleware": "Middleware",
+        "routers": "Роутери",
+        "services": "Сервіси"
+    },
+    "navidrome": {
+        "nothing_streaming": "No Active Streams",
+        "please_wait": "Please Wait"
+    },
+    "bazarr": {
+        "missingEpisodes": "Відсутні епізоди",
+        "missingMovies": "Відсутні фільми"
+    },
+    "ombi": {
+        "pending": "В очікуванні",
+        "approved": "Затверджено",
+        "available": "Доступно"
+    },
+    "jellyseerr": {
+        "pending": "В очікуванні",
+        "approved": "Затверджено",
+        "available": "Доступно"
+    },
+    "overseerr": {
+        "pending": "В очікуванні",
+        "processing": "Обробка",
+        "approved": "Затверджено",
+        "available": "Доступно"
+    },
+    "pihole": {
+        "queries": "Запити",
+        "blocked": "Заблоковано",
+        "gravity": "Гравітація"
+    },
+    "adguard": {
+        "queries": "Запити",
+        "blocked": "Заблоковано",
+        "filtered": "Відфільтровано",
+        "latency": "Затримка"
+    },
+    "speedtest": {
+        "upload": "Відправлення",
+        "download": "Завантаження",
+        "ping": "Пінг"
+    },
+    "portainer": {
+        "running": "Запущено",
+        "stopped": "Зупинено",
+        "total": "Всього"
+    },
+    "tdarr": {
+        "queue": "Черга",
+        "processed": "Обробка",
+        "errored": "Помилка",
+        "saved": "Збережено"
+    },
+    "npm": {
+        "enabled": "Enabled",
+        "disabled": "Disabled",
+        "total": "Total"
+    },
+    "coinmarketcap": {
+        "configure": "Configure one or more crypto currencies to track",
+        "1hour": "1 Hour",
+        "1day": "1 Day",
+        "7days": "7 Days",
+        "30days": "30 Days"
+    },
+    "mastodon": {
+        "domain_count": "Domains",
+        "user_count": "Users",
+        "status_count": "Posts"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "gotify": {
+        "apps": "Applications",
+        "clients": "Clients",
+        "messages": "Messages"
+    },
+    "prowlarr": {
+        "enableIndexers": "Indexers",
+        "numberOfGrabs": "Grabs",
+        "numberOfQueries": "Queries",
+        "numberOfFailGrabs": "Fail Grabs",
+        "numberOfFailQueries": "Fail Queries"
+    },
+    "jackett": {
+        "configured": "Configured",
+        "errored": "Errored"
+    },
+    "strelaysrv": {
+        "numActiveSessions": "Sessions",
+        "numConnections": "Connections",
+        "dataRelayed": "Relayed",
+        "transferRate": "Rate"
+    },
+    "authentik": {
+        "users": "Users",
+        "loginsLast24H": "Logins (24h)",
+        "failedLoginsLast24H": "Failed Logins (24h)"
+    },
+    "proxmox": {
+        "mem": "MEM",
+        "cpu": "CPU",
+        "vms": "VMs",
+        "lxc": "LXC"
+    },
+    "glances": {
+        "cpu": "CPU",
+        "mem": "MEM",
+        "wait": "Please wait"
+    },
+    "quicklaunch": {
+        "bookmark": "Bookmark",
+        "service": "Service"
+    },
+    "homebridge": {
+        "available_update": "System",
+        "updates": "Updates",
+        "child_bridges_status": "{{ok}}/{{total}}",
+        "update_available": "Update Available",
+        "up_to_date": "Up to Date",
+        "child_bridges": "Child Bridges"
+    },
+    "watchtower": {
+        "containers_scanned": "Scanned",
+        "containers_updated": "Updated",
+        "containers_failed": "Failed"
+    },
+    "autobrr": {
+        "approvedPushes": "Approved",
+        "rejectedPushes": "Rejected",
+        "filters": "Filters",
+        "indexers": "Indexers"
+    },
+    "tubearchivist": {
+        "downloads": "Queue",
+        "videos": "Videos",
+        "channels": "Channels",
+        "playlists": "Playlists"
+    },
+    "truenas": {
+        "load": "System Load",
+        "uptime": "Uptime",
+        "alerts": "Alerts",
+        "time": "{{value, number(style: unit; unitDisplay: long;)}}"
+    },
+    "scrutiny": {
+        "passed": "Passed",
+        "failed": "Failed",
+        "unknown": "Unknown"
+    },
+    "paperlessngx": {
+        "inbox": "Inbox",
+        "total": "Total"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
+    }
+}

+ 48 - 6
public/locales/vi/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
-        "download": "Download",
-        "upload": "Upload",
-        "leech": "Leech",
-        "seed": "Seed"
-    },
     "flood": {
     "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "seed": "Seed",
         "seed": "Seed",
         "leech": "Leech"
         "leech": "Leech"
+    },
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
+        "download": "Download",
+        "upload": "Upload",
+        "leech": "Leech",
+        "seed": "Seed"
+    },
+    "mikrotik": {
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases",
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/yue/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 46 - 4
public/locales/zh-CN/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
+        "leech": "Leech",
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
-        "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
-        "leech": "Leech",
+    "tdarr": {
+        "saved": "Saved",
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
+        "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 44 - 2
public/locales/zh-Hant/common.json

@@ -350,16 +350,58 @@
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "diskstation": {
+    "flood": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
     },
     },
-    "flood": {
+    "tdarr": {
+        "queue": "Queue",
+        "processed": "Processed",
+        "errored": "Errored",
+        "saved": "Saved"
+    },
+    "miniflux": {
+        "read": "Read",
+        "unread": "Unread"
+    },
+    "nextdns": {
+        "wait": "Please Wait",
+        "no_devices": "No Device Data Received"
+    },
+    "common": {
+        "bibyterate": "{{value, rate(bits: false; binary: true)}}",
+        "bibitrate": "{{value, rate(bits: true; binary: true)}}"
+    },
+    "omada": {
+        "connectedAp": "Connected APs",
+        "activeUser": "Active devices",
+        "alerts": "Alerts",
+        "connectedGateway": "Connected gateways",
+        "connectedSwitches": "Connected switches"
+    },
+    "downloadstation": {
         "download": "Download",
         "download": "Download",
         "upload": "Upload",
         "upload": "Upload",
         "leech": "Leech",
         "leech": "Leech",
         "seed": "Seed"
         "seed": "Seed"
+    },
+    "mikrotik": {
+        "cpuLoad": "CPU Load",
+        "memoryUsed": "Memory Used",
+        "uptime": "Uptime",
+        "numberOfLeases": "Leases"
+    },
+    "xteve": {
+        "streams_all": "All Streams",
+        "streams_active": "Active Streams",
+        "streams_xepg": "XEPG Channels"
+    },
+    "opnsense": {
+        "cpu": "CPU Load",
+        "memory": "Active Memory",
+        "wanUpload": "WAN Upload",
+        "wanDownload": "WAN Download"
     }
     }
 }
 }

+ 6 - 6
src/components/resolvedicon.jsx

@@ -1,9 +1,9 @@
 import Image from "next/future/image";
 import Image from "next/future/image";
 
 
-export default function ResolvedIcon({ icon }) {
+export default function ResolvedIcon({ icon, width = 32, height = 32 }) {
   // direct or relative URLs
   // direct or relative URLs
   if (icon.startsWith("http") || icon.startsWith("/")) {
   if (icon.startsWith("http") || icon.startsWith("/")) {
-    return <Image src={`${icon}`} width={32} height={32} alt="logo" />;
+    return <Image src={`${icon}`} width={width} height={height} alt="logo" />;
   }
   }
 
 
   // mdi- prefixed, material design icons
   // mdi- prefixed, material design icons
@@ -12,8 +12,8 @@ export default function ResolvedIcon({ icon }) {
     return (
     return (
       <div
       <div
         style={{
         style={{
-          width: 32,
-          height: 32,
+          width,
+          height,
           maxWidth: '100%',
           maxWidth: '100%',
           maxHeight: '100%',
           maxHeight: '100%',
           background: "linear-gradient(180deg, rgb(var(--color-logo-start)), rgb(var(--color-logo-stop)))",
           background: "linear-gradient(180deg, rgb(var(--color-logo-start)), rgb(var(--color-logo-stop)))",
@@ -29,8 +29,8 @@ export default function ResolvedIcon({ icon }) {
   return (
   return (
     <Image
     <Image
       src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${iconName}.png`}
       src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${iconName}.png`}
-      width={32}
-      height={32}
+      width={width}
+      height={height}
       alt="logo"
       alt="logo"
     />
     />
   );
   );

+ 2 - 6
src/components/version.jsx

@@ -3,8 +3,6 @@ import useSWR from "swr";
 import { compareVersions } from "compare-versions";
 import { compareVersions } from "compare-versions";
 import { MdNewReleases } from "react-icons/md";
 import { MdNewReleases } from "react-icons/md";
 
 
-import cachedFetch from "utils/proxy/cached-fetch";
-
 export default function Version() {
 export default function Version() {
   const { t, i18n } = useTranslation();
   const { t, i18n } = useTranslation();
 
 
@@ -12,9 +10,7 @@ export default function Version() {
   const revision = process.env.NEXT_PUBLIC_REVISION?.length ? process.env.NEXT_PUBLIC_REVISION : "dev";
   const revision = process.env.NEXT_PUBLIC_REVISION?.length ? process.env.NEXT_PUBLIC_REVISION : "dev";
   const version = process.env.NEXT_PUBLIC_VERSION?.length ?  process.env.NEXT_PUBLIC_VERSION : "dev";
   const version = process.env.NEXT_PUBLIC_VERSION?.length ?  process.env.NEXT_PUBLIC_VERSION : "dev";
 
 
-  const cachedFetcher = (resource) => cachedFetch(resource, 5).then((res) => res.json());
-
-  const { data: releaseData } = useSWR("https://api.github.com/repos/benphelps/homepage/releases", cachedFetcher);
+  const { data: releaseData } = useSWR("/api/releases");
 
 
   // use Intl.DateTimeFormat to format the date
   // use Intl.DateTimeFormat to format the date
   const formatDate = (date) => {
   const formatDate = (date) => {
@@ -48,7 +44,7 @@ export default function Version() {
       </span>
       </span>
       {version === "main" || version === "dev" || version === "nightly"
       {version === "main" || version === "dev" || version === "nightly"
         ? null
         ? null
-        : releaseData &&
+        : releaseData && latestRelease &&
           compareVersions(latestRelease.tag_name, version) > 0 && (
           compareVersions(latestRelease.tag_name, version) > 0 && (
             <a
             <a
               href={latestRelease.html_url}
               href={latestRelease.html_url}

+ 52 - 46
src/components/widgets/logo/logo.jsx

@@ -1,56 +1,62 @@
-export default function Logo() {
+import ResolvedIcon from "components/resolvedicon"
+
+export default function Logo({ options }) {
   return (
   return (
     <div className="w-12 h-12 flex flex-row items-center align-middle mr-3 self-center">
     <div className="w-12 h-12 flex flex-row items-center align-middle mr-3 self-center">
-      <svg
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="0 0 1024 1024"
-        style={{
-          enableBackground: "new 0 0 1024 1024",
-        }}
-        xmlSpace="preserve"
-        className="w-full h-full"
-      >
-        <style>
-          {
-            ".st0{display:none}.st3{stroke-linecap:square}.st3,.st4{fill:none;stroke:#fff;stroke-miterlimit:10}.st6{display:inline;fill:#333}.st7{fill:#fff}"
-          }
-        </style>
-        <g id="Icon">
-          <path
-            d="M771.9 191c27.7 0 50.1 26.5 50.1 59.3v186.4l-100.2.3V250.3c0-32.8 22.4-59.3 50.1-59.3z"
-            style={{
-              fill: "rgba(var(--color-logo-start))",
-            }}
-          />
-          <linearGradient
-            id="homepage_logo_gradient"
-            gradientUnits="userSpaceOnUse"
-            x1={200.746}
-            y1={225.015}
-            x2={764.986}
-            y2={789.255}
-          >
-            <stop
-              offset={0}
+      {options.icon ?
+        <ResolvedIcon icon={options.icon} width={48} height={48} /> :
+        // fallback to homepage logo
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          viewBox="0 0 1024 1024"
+          style={{
+            enableBackground: "new 0 0 1024 1024",
+          }}
+          xmlSpace="preserve"
+          className="w-full h-full"
+        >
+          <style>
+            {
+              ".st0{display:none}.st3{stroke-linecap:square}.st3,.st4{fill:none;stroke:#fff;stroke-miterlimit:10}.st6{display:inline;fill:#333}.st7{fill:#fff}"
+            }
+          </style>
+          <g id="Icon">
+            <path
+              d="M771.9 191c27.7 0 50.1 26.5 50.1 59.3v186.4l-100.2.3V250.3c0-32.8 22.4-59.3 50.1-59.3z"
               style={{
               style={{
-                stopColor: "rgba(var(--color-logo-start))",
+                fill: "rgba(var(--color-logo-start))",
               }}
               }}
             />
             />
-            <stop
-              offset={1}
+            <linearGradient
+              id="homepage_logo_gradient"
+              gradientUnits="userSpaceOnUse"
+              x1={200.746}
+              y1={225.015}
+              x2={764.986}
+              y2={789.255}
+            >
+              <stop
+                offset={0}
+                style={{
+                  stopColor: "rgba(var(--color-logo-start))",
+                }}
+              />
+              <stop
+                offset={1}
+                style={{
+                  stopColor: "rgba(var(--color-logo-stop))",
+                }}
+              />
+            </linearGradient>
+            <path
+              d="M721.8 250.3c0-32.7 22.4-59.3 50.1-59.3H253.1c-27.7 0-50.1 26.5-50.1 59.3v582.2l90.2-75.7-.1-130.3H375v61.8l88-73.8 258.8 217.9V250.6"
               style={{
               style={{
-                stopColor: "rgba(var(--color-logo-stop))",
+                fill: "url(#homepage_logo_gradient)",
               }}
               }}
             />
             />
-          </linearGradient>
-          <path
-            d="M721.8 250.3c0-32.7 22.4-59.3 50.1-59.3H253.1c-27.7 0-50.1 26.5-50.1 59.3v582.2l90.2-75.7-.1-130.3H375v61.8l88-73.8 258.8 217.9V250.6"
-            style={{
-              fill: "url(#homepage_logo_gradient)",
-            }}
-          />
-        </g>
-      </svg>
+          </g>
+        </svg>
+      }
     </div>
     </div>
-  );
+  )
 }
 }

+ 1 - 1
src/components/widgets/resources/cpu.jsx

@@ -38,7 +38,7 @@ export default function Cpu({ expanded }) {
               <div className="pr-1">{t("resources.load")}</div>
               <div className="pr-1">{t("resources.load")}</div>
             </div>
             </div>
           )}
           )}
-          <UsageBar percent={100} />
+          <UsageBar percent={0} />
         </div>
         </div>
       </div>
       </div>
     );
     );

+ 1 - 1
src/components/widgets/resources/disk.jsx

@@ -38,7 +38,7 @@ export default function Disk({ options, expanded }) {
               <div className="pr-1">{t("resources.total")}</div>
               <div className="pr-1">{t("resources.total")}</div>
             </span>
             </span>
           )}
           )}
-          <UsageBar percent={100} />
+          <UsageBar percent={0} />
         </div>
         </div>
       </div>
       </div>
     );
     );

+ 1 - 1
src/components/widgets/resources/memory.jsx

@@ -38,7 +38,7 @@ export default function Memory({ expanded }) {
               <div className="pr-1">{t("resources.total")}</div>
               <div className="pr-1">{t("resources.total")}</div>
             </span>
             </span>
           )}
           )}
-          <UsageBar percent={100} />
+          <UsageBar percent={0} />
         </div>
         </div>
       </div>
       </div>
     );
     );

+ 35 - 8
src/pages/api/docker/stats/[...service].js

@@ -14,7 +14,8 @@ export default async function handler(req, res) {
   }
   }
 
 
   try {
   try {
-    const docker = new Docker(getDockerArguments(containerServer));
+    const dockerArgs = getDockerArguments(containerServer);
+    const docker = new Docker(dockerArgs.conn);
     const containers = await docker.listContainers({
     const containers = await docker.listContainers({
       all: true,
       all: true,
     });
     });
@@ -31,18 +32,44 @@ export default async function handler(req, res) {
     const containerNames = containers.map((container) => container.Names[0].replace(/^\//, ""));
     const containerNames = containers.map((container) => container.Names[0].replace(/^\//, ""));
     const containerExists = containerNames.includes(containerName);
     const containerExists = containerNames.includes(containerName);
 
 
-    if (!containerExists) {
-      res.status(200).send({
-        error: "not found",
+    if (containerExists) {
+      const container = docker.getContainer(containerName);
+      const stats = await container.stats({ stream: false });
+
+      res.status(200).json({
+        stats,
       });
       });
       return;
       return;
     }
     }
 
 
-    const container = docker.getContainer(containerName);
-    const stats = await container.stats({ stream: false });
+    // Try with a service deployed in Docker Swarm, if enabled
+    if (dockerArgs.swarm) {
+      const tasks = await docker.listTasks({
+          filters: {
+            service: [containerName],
+            // A service can have several offline containers, so we only look for an active one.
+            "desired-state": ["running"],
+          },
+        })
+        .catch(() => []);
+
+      // For now we are only interested in the first one (in case replicas > 1).
+      // TODO: Show the result for all replicas/containers?
+      const taskContainerId = tasks.at(0)?.Status?.ContainerStatus?.ContainerID;
+
+      if (taskContainerId) {
+        const container = docker.getContainer(taskContainerId);
+        const stats = await container.stats({ stream: false });
+
+        res.status(200).json({
+          stats,
+        });
+        return;
+      }
+    }
 
 
-    res.status(200).json({
-      stats,
+    res.status(200).send({
+      error: "not found",
     });
     });
   } catch {
   } catch {
     res.status(500).send({
     res.status(500).send({

+ 35 - 9
src/pages/api/docker/status/[...service].js

@@ -13,7 +13,8 @@ export default async function handler(req, res) {
   }
   }
 
 
   try {
   try {
-    const docker = new Docker(getDockerArguments(containerServer));
+    const dockerArgs = getDockerArguments(containerServer);
+    const docker = new Docker(dockerArgs.conn);
     const containers = await docker.listContainers({
     const containers = await docker.listContainers({
       all: true,
       all: true,
     });
     });
@@ -29,18 +30,43 @@ export default async function handler(req, res) {
     const containerNames = containers.map((container) => container.Names[0].replace(/^\//, ""));
     const containerNames = containers.map((container) => container.Names[0].replace(/^\//, ""));
     const containerExists = containerNames.includes(containerName);
     const containerExists = containerNames.includes(containerName);
 
 
-    if (!containerExists) {
-      return res.status(200).send({
-        error: "not found",
+    if (containerExists) {
+      const container = docker.getContainer(containerName);
+      const info = await container.inspect();
+
+      return res.status(200).json({
+        status: info.State.Status,
+        health: info.State.Health?.Status,
       });
       });
     }
     }
 
 
-    const container = docker.getContainer(containerName);
-    const info = await container.inspect();
+    if (dockerArgs.swarm) {
+      const tasks = await docker.listTasks({
+          filters: {
+            service: [containerName],
+            // A service can have several offline containers, we only look for an active one.
+            "desired-state": ["running"],
+          },
+        })
+        .catch(() => []);
+
+      // For now we are only interested in the first one (in case replicas > 1).
+      // TODO: Show the result for all replicas/containers?
+      const taskContainerId = tasks.at(0)?.Status?.ContainerStatus?.ContainerID;
+
+      if (taskContainerId) {
+        const container = docker.getContainer(taskContainerId);
+        const info = await container.inspect();
+
+        return res.status(200).json({
+          status: info.State.Status,
+          health: info.State.Health?.Status,
+        });
+      }
+    }
 
 
-    return res.status(200).json({
-      status: info.State.Status,
-      health: info.State.Health?.Status
+    return res.status(200).send({
+      error: "not found",
     });
     });
   } catch {
   } catch {
     return res.status(500).send({
     return res.status(500).send({

+ 6 - 0
src/pages/api/releases.js

@@ -0,0 +1,6 @@
+import cachedFetch from "utils/proxy/cached-fetch";
+
+export default async function handler(req, res) {
+  const releasesURL = "https://api.github.com/repos/benphelps/homepage/releases";
+  return res.send(await cachedFetch(releasesURL, 5));
+}

+ 7 - 4
src/utils/config/api-response.js

@@ -56,9 +56,12 @@ export async function servicesResponse() {
 
 
   try {
   try {
     discoveredDockerServices = cleanServiceGroups(await servicesFromDocker());
     discoveredDockerServices = cleanServiceGroups(await servicesFromDocker());
+    if (discoveredDockerServices?.length === 0) {
+      console.debug("No containers were found with homepage labels.");
+    }
   } catch (e) {
   } catch (e) {
     console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
     console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
-    if (e) console.error(e);
+    if (e) console.error(e.toString());
     discoveredDockerServices = [];
     discoveredDockerServices = [];
   }
   }
 
 
@@ -66,7 +69,7 @@ export async function servicesResponse() {
     discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes());
     discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes());
   } catch (e) {
   } catch (e) {
     console.error("Failed to discover services, please check kubernetes.yaml for errors or remove example entries.");
     console.error("Failed to discover services, please check kubernetes.yaml for errors or remove example entries.");
-    if (e) console.error(e);
+    if (e) console.error(e.toString());
     discoveredKubernetesServices = [];
     discoveredKubernetesServices = [];
   }
   }
 
 
@@ -74,7 +77,7 @@ export async function servicesResponse() {
     configuredServices = cleanServiceGroups(await servicesFromConfig());
     configuredServices = cleanServiceGroups(await servicesFromConfig());
   } catch (e) {
   } catch (e) {
     console.error("Failed to load services.yaml, please check for errors");
     console.error("Failed to load services.yaml, please check for errors");
-    if (e) console.error(e);
+    if (e) console.error(e.toString());
     configuredServices = [];
     configuredServices = [];
   }
   }
 
 
@@ -82,7 +85,7 @@ export async function servicesResponse() {
     initialSettings = await getSettings();
     initialSettings = await getSettings();
   } catch (e) {
   } catch (e) {
     console.error("Failed to load settings.yaml, please check for errors");
     console.error("Failed to load settings.yaml, please check for errors");
-    if (e) console.error(e);
+    if (e) console.error(e.toString());
     initialSettings = {};
     initialSettings = {};
   }
   }
 
 

+ 5 - 2
src/utils/config/docker.js

@@ -22,11 +22,14 @@ export default function getDockerArguments(server) {
 
 
   if (servers[server]) {
   if (servers[server]) {
     if (servers[server].socket) {
     if (servers[server].socket) {
-      return { socketPath: servers[server].socket };
+      return { conn: { socketPath: servers[server].socket }, swarm: !!servers[server].swarm };
     }
     }
 
 
     if (servers[server].host) {
     if (servers[server].host) {
-      return { host: servers[server].host, port: servers[server].port || null };
+      return {
+        conn: { host: servers[server].host, port: servers[server].port || null },
+        swarm: !!servers[server].swarm,
+      };
     }
     }
 
 
     return servers[server];
     return servers[server];

+ 30 - 25
src/utils/config/service-helpers.js

@@ -49,36 +49,41 @@ export async function servicesFromDocker() {
 
 
   const serviceServers = await Promise.all(
   const serviceServers = await Promise.all(
     Object.keys(servers).map(async (serverName) => {
     Object.keys(servers).map(async (serverName) => {
-      const docker = new Docker(getDockerArguments(serverName));
-      const containers = await docker.listContainers({
-        all: true,
-      });
-
-      // bad docker connections can result in a <Buffer ...> object?
-      // in any case, this ensures the result is the expected array
-      if (!Array.isArray(containers)) {
-        return [];
-      }
+      try {
+        const docker = new Docker(getDockerArguments(serverName).conn);
+        const containers = await docker.listContainers({
+          all: true,
+        });
 
 
-      const discovered = containers.map((container) => {
-        let constructedService = null;
+        // bad docker connections can result in a <Buffer ...> object?
+        // in any case, this ensures the result is the expected array
+        if (!Array.isArray(containers)) {
+          return [];
+        }
 
 
-        Object.keys(container.Labels).forEach((label) => {
-          if (label.startsWith("homepage.")) {
-            if (!constructedService) {
-              constructedService = {
-                container: container.Names[0].replace(/^\//, ""),
-                server: serverName,
-              };
+        const discovered = containers.map((container) => {
+          let constructedService = null;
+
+          Object.keys(container.Labels).forEach((label) => {
+            if (label.startsWith("homepage.")) {
+              if (!constructedService) {
+                constructedService = {
+                  container: container.Names[0].replace(/^\//, ""),
+                  server: serverName,
+                };
+              }
+              shvl.set(constructedService, label.replace("homepage.", ""), container.Labels[label]);
             }
             }
-            shvl.set(constructedService, label.replace("homepage.", ""), container.Labels[label]);
-          }
-        });
+          });
 
 
-        return constructedService;
-      });
+          return constructedService;
+        });
 
 
-      return { server: serverName, services: discovered.filter((filteredService) => filteredService) };
+        return { server: serverName, services: discovered.filter((filteredService) => filteredService) };
+      } catch (e) {
+        // a server failed, but others may succeed
+        return { server: serverName, services: [] };
+      }
     })
     })
   );
   );
 
 

+ 10 - 2
src/utils/proxy/handlers/credentialed.js

@@ -7,7 +7,7 @@ import widgets from "widgets/widgets";
 
 
 const logger = createLogger("credentialedProxyHandler");
 const logger = createLogger("credentialedProxyHandler");
 
 
-export default async function credentialedProxyHandler(req, res) {
+export default async function credentialedProxyHandler(req, res, map) {
   const { group, service, endpoint } = req.query;
   const { group, service, endpoint } = req.query;
 
 
   if (group && service) {
   if (group && service) {
@@ -36,6 +36,8 @@ export default async function credentialedProxyHandler(req, res) {
         headers["X-API-Token"] = `${widget.key}`;
         headers["X-API-Token"] = `${widget.key}`;
       } else if (widget.type === "tubearchivist") {
       } else if (widget.type === "tubearchivist") {
         headers.Authorization = `Token ${widget.key}`;
         headers.Authorization = `Token ${widget.key}`;
+      } else if (widget.type === "miniflux") {
+        headers["X-Auth-Token"] = `${widget.key}`;
       } else {
       } else {
         headers["X-API-Key"] = `${widget.key}`;
         headers["X-API-Key"] = `${widget.key}`;
       }
       }
@@ -47,6 +49,8 @@ export default async function credentialedProxyHandler(req, res) {
         headers,
         headers,
       });
       });
 
 
+      let resultData = data;
+
       if (status === 204 || status === 304) {
       if (status === 204 || status === 304) {
         return res.status(status).end();
         return res.status(status).end();
       }
       }
@@ -59,8 +63,12 @@ export default async function credentialedProxyHandler(req, res) {
         return res.status(500).json({error: {message: "Invalid data", url, data}});
         return res.status(500).json({error: {message: "Invalid data", url, data}});
       }
       }
 
 
+      if (status === 200 && map) {
+        resultData = map(data);
+      }
+
       if (contentType) res.setHeader("Content-Type", contentType);
       if (contentType) res.setHeader("Content-Type", contentType);
-      return res.status(status).send(data);
+      return res.status(status).send(resultData);
     }
     }
   }
   }
 
 

+ 8 - 1
src/widgets/components.js

@@ -8,7 +8,7 @@ const components = {
   changedetectionio: dynamic(() => import("./changedetectionio/component")),
   changedetectionio: dynamic(() => import("./changedetectionio/component")),
   coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
   coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
   deluge: dynamic(() => import("./deluge/component")),
   deluge: dynamic(() => import("./deluge/component")),
-  diskstation: dynamic(() => import("./diskstation/component")),
+  downloadstation: dynamic(() => import("./downloadstation/component")),
   docker: dynamic(() => import("./docker/component")),
   docker: dynamic(() => import("./docker/component")),
   kubernetes: dynamic(() => import("./kubernetes/component")),
   kubernetes: dynamic(() => import("./kubernetes/component")),
   emby: dynamic(() => import("./emby/component")),
   emby: dynamic(() => import("./emby/component")),
@@ -22,10 +22,15 @@ const components = {
   jellyseerr: dynamic(() => import("./jellyseerr/component")),
   jellyseerr: dynamic(() => import("./jellyseerr/component")),
   lidarr: dynamic(() => import("./lidarr/component")),
   lidarr: dynamic(() => import("./lidarr/component")),
   mastodon: dynamic(() => import("./mastodon/component")),
   mastodon: dynamic(() => import("./mastodon/component")),
+  miniflux: dynamic(() => import("./miniflux/component")),
+  mikrotik: dynamic(() => import("./mikrotik/component")),
   navidrome: dynamic(() => import("./navidrome/component")),
   navidrome: dynamic(() => import("./navidrome/component")),
+  nextdns: dynamic(() => import("./nextdns/component")),
   npm: dynamic(() => import("./npm/component")),
   npm: dynamic(() => import("./npm/component")),
   nzbget: dynamic(() => import("./nzbget/component")),
   nzbget: dynamic(() => import("./nzbget/component")),
+  omada: dynamic(() => import("./omada/component")),
   ombi: dynamic(() => import("./ombi/component")),
   ombi: dynamic(() => import("./ombi/component")),
+  opnsense: dynamic(() => import("./opnsense/component")),
   overseerr: dynamic(() => import("./overseerr/component")),
   overseerr: dynamic(() => import("./overseerr/component")),
   paperlessngx: dynamic(() => import("./paperlessngx/component")),
   paperlessngx: dynamic(() => import("./paperlessngx/component")),
   pihole: dynamic(() => import("./pihole/component")),
   pihole: dynamic(() => import("./pihole/component")),
@@ -44,12 +49,14 @@ const components = {
   speedtest: dynamic(() => import("./speedtest/component")),
   speedtest: dynamic(() => import("./speedtest/component")),
   strelaysrv: dynamic(() => import("./strelaysrv/component")),
   strelaysrv: dynamic(() => import("./strelaysrv/component")),
   tautulli: dynamic(() => import("./tautulli/component")),
   tautulli: dynamic(() => import("./tautulli/component")),
+  tdarr: dynamic(() => import("./tdarr/component")),
   traefik: dynamic(() => import("./traefik/component")),
   traefik: dynamic(() => import("./traefik/component")),
   transmission: dynamic(() => import("./transmission/component")),
   transmission: dynamic(() => import("./transmission/component")),
   tubearchivist: dynamic(() => import("./tubearchivist/component")),
   tubearchivist: dynamic(() => import("./tubearchivist/component")),
   truenas: dynamic(() => import("./truenas/component")),
   truenas: dynamic(() => import("./truenas/component")),
   unifi: dynamic(() => import("./unifi/component")),
   unifi: dynamic(() => import("./unifi/component")),
   watchtower: dynamic(() => import("./watchtower/component")),
   watchtower: dynamic(() => import("./watchtower/component")),
+  xteve: dynamic(() => import("./xteve/component")),
 };
 };
 
 
 export default components;
 export default components;

+ 8 - 8
src/widgets/diskstation/component.jsx → src/widgets/downloadstation/component.jsx

@@ -17,10 +17,10 @@ export default function Component({ service }) {
   if (!tasks) {
   if (!tasks) {
     return (
     return (
       <Container service={service}>
       <Container service={service}>
-        <Block label="diskstation.leech" />
-        <Block label="diskstation.download" />
-        <Block label="diskstation.seed" />
-        <Block label="diskstation.upload" />
+        <Block label="downloadstation.leech" />
+        <Block label="downloadstation.download" />
+        <Block label="downloadstation.seed" />
+        <Block label="downloadstation.upload" />
       </Container>
       </Container>
     );
     );
   }
   }
@@ -32,10 +32,10 @@ export default function Component({ service }) {
 
 
   return (
   return (
     <Container service={service}>
     <Container service={service}>
-      <Block label="diskstation.leech" value={t("common.number", { value: leech })} />
-      <Block label="diskstation.download" value={t("common.bitrate", { value: rateDl })} />
-      <Block label="diskstation.seed" value={t("common.number", { value: completed })} />
-      <Block label="diskstation.upload" value={t("common.bitrate", { value: rateUl })} />
+      <Block label="downloadstation.leech" value={t("common.number", { value: leech })} />
+      <Block label="downloadstation.download" value={t("common.bitrate", { value: rateDl })} />
+      <Block label="downloadstation.seed" value={t("common.number", { value: completed })} />
+      <Block label="downloadstation.upload" value={t("common.bitrate", { value: rateUl })} />
     </Container>
     </Container>
   );
   );
 }
 }

+ 3 - 3
src/widgets/diskstation/proxy.js → src/widgets/downloadstation/proxy.js

@@ -4,7 +4,7 @@ import createLogger from "utils/logger";
 import widgets from "widgets/widgets";
 import widgets from "widgets/widgets";
 import getServiceWidget from "utils/config/service-helpers";
 import getServiceWidget from "utils/config/service-helpers";
 
 
-const logger = createLogger("diskstationProxyHandler");
+const logger = createLogger("downloadstationProxyHandler");
 const authApi = "{url}/webapi/auth.cgi?api=SYNO.API.Auth&version=2&method=login&account={username}&passwd={password}&session=DownloadStation&format=cookie"
 const authApi = "{url}/webapi/auth.cgi?api=SYNO.API.Auth&version=2&method=login&account={username}&passwd={password}&session=DownloadStation&format=cookie"
 
 
 async function login(widget) {
 async function login(widget) {
@@ -34,7 +34,7 @@ async function login(widget) {
   return [status, contentType, data];
   return [status, contentType, data];
 }
 }
 
 
-export default async function diskstationProxyHandler(req, res) {
+export default async function downloadstationProxyHandler(req, res) {
   const { group, service, endpoint } = req.query;
   const { group, service, endpoint } = req.query;
 
 
   if (!group || !service) {
   if (!group || !service) {
@@ -56,7 +56,7 @@ export default async function diskstationProxyHandler(req, res) {
 
 
   const json = JSON.parse(data.toString());
   const json = JSON.parse(data.toString());
   if (json?.success !== true) {
   if (json?.success !== true) {
-    logger.debug("Logging in to DiskStation");
+    logger.debug("Logging in to DownloadStation");
     [status, contentType, data] = await login(widget);
     [status, contentType, data] = await login(widget);
     if (status !== 200) {
     if (status !== 200) {
       return res.status(status).end(data)
       return res.status(status).end(data)

+ 2 - 2
src/widgets/diskstation/widget.js → src/widgets/downloadstation/widget.js

@@ -1,8 +1,8 @@
-import diskstationProxyHandler from "./proxy";
+import downloadstationProxyHandler from "./proxy";
 
 
 const widget = {
 const widget = {
   api: "{url}/webapi/DownloadStation/task.cgi?api=SYNO.DownloadStation.Task&version=1&method={endpoint}",
   api: "{url}/webapi/DownloadStation/task.cgi?api=SYNO.DownloadStation.Task&version=1&method={endpoint}",
-  proxyHandler: diskstationProxyHandler,
+  proxyHandler: downloadstationProxyHandler,
 
 
   mappings: {
   mappings: {
     "list": {
     "list": {

+ 12 - 11
src/widgets/homebridge/proxy.js

@@ -10,7 +10,7 @@ const proxyName = "homebridgeProxyHandler";
 const sessionTokenCacheKey = `${proxyName}__sessionToken`;
 const sessionTokenCacheKey = `${proxyName}__sessionToken`;
 const logger = createLogger(proxyName);
 const logger = createLogger(proxyName);
 
 
-async function login(widget) {
+async function login(widget, service) {
   const endpoint = "auth/login";
   const endpoint = "auth/login";
   const api = widgets?.[widget.type]?.api
   const api = widgets?.[widget.type]?.api
   const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
   const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
@@ -26,7 +26,7 @@ async function login(widget) {
   try {
   try {
     const { access_token: accessToken, expires_in: expiresIn } = JSON.parse(data.toString());
     const { access_token: accessToken, expires_in: expiresIn } = JSON.parse(data.toString());
   
   
-    cache.put(sessionTokenCacheKey, accessToken, (expiresIn * 1000) - 5 * 60 * 1000); // expiresIn (s) - 5m
+    cache.put(`${sessionTokenCacheKey}.${service}`, accessToken, (expiresIn * 1000) - 5 * 60 * 1000); // expiresIn (s) - 5m
     return { accessToken };
     return { accessToken };
   } catch (e) {
   } catch (e) {
     logger.error("Unable to login to Homebridge API: %s", e);
     logger.error("Unable to login to Homebridge API: %s", e);
@@ -35,10 +35,11 @@ async function login(widget) {
   return { accessToken: false };
   return { accessToken: false };
 }
 }
 
 
-async function apiCall(widget, endpoint) {
+async function apiCall(widget, endpoint, service) {
+  const key = `${sessionTokenCacheKey}.${service}`;
   const headers = {
   const headers = {
     "content-type": "application/json",
     "content-type": "application/json",
-    "Authorization": `Bearer ${cache.get(sessionTokenCacheKey)}`,
+    "Authorization": `Bearer ${cache.get(key)}`,
   }
   }
 
 
   const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
   const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
@@ -51,7 +52,7 @@ async function apiCall(widget, endpoint) {
 
 
   if (status === 401) {
   if (status === 401) {
     logger.debug("Homebridge API rejected the request, attempting to obtain new session token");
     logger.debug("Homebridge API rejected the request, attempting to obtain new session token");
-    const { accessToken } = login(widget);
+    const { accessToken } = login(widget, service);
     headers.Authorization = `Bearer ${accessToken}`;
     headers.Authorization = `Bearer ${accessToken}`;
 
 
     // retry the request, now with the new session token
     // retry the request, now with the new session token
@@ -83,14 +84,14 @@ export default async function homebridgeProxyHandler(req, res) {
     return res.status(400).json({ error: "Invalid proxy service type" });
     return res.status(400).json({ error: "Invalid proxy service type" });
   }
   }
 
 
-  if (!cache.get(sessionTokenCacheKey)) {
-    await login(widget);
+  if (!cache.get(`${sessionTokenCacheKey}.${service}`)) {
+    await login(widget, service);
   }
   }
 
 
-  const { data: statusData } = await apiCall(widget, "status/homebridge");
-  const { data: versionData } = await apiCall(widget, "status/homebridge-version");
-  const { data: childBridgeData } = await apiCall(widget, "status/homebridge/child-bridges");
-  const { data: pluginsData } = await apiCall(widget, "plugins");
+  const { data: statusData } = await apiCall(widget, "status/homebridge", service);
+  const { data: versionData } = await apiCall(widget, "status/homebridge-version", service);
+  const { data: childBridgeData } = await apiCall(widget, "status/homebridge/child-bridges", service);
+  const { data: pluginsData } = await apiCall(widget, "plugins", service);
 
 
   return res.status(200).send({
   return res.status(200).send({
       status: statusData?.status,
       status: statusData?.status,

+ 43 - 0
src/widgets/mikrotik/component.jsx

@@ -0,0 +1,43 @@
+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, "system");
+  const { data: leasesData, error: leasesError } = useWidgetAPI(widget, "leases");
+
+  if (statsError || leasesError) {
+    const finalError = statsError ?? leasesError;
+    return <Container error={ finalError } />;
+  }
+
+  if (!statsData || !leasesData) {
+    return (
+      <Container service={service}>
+        <Block label="mikrotik.uptime" />
+        <Block label="mikrotik.cpuLoad" />
+        <Block label="mikrotik.memoryUsed" />
+        <Block label="mikrotik.numberOfLeases" />
+      </Container>
+    );
+  }
+
+  const memoryUsed = 100 - (statsData['free-memory'] / statsData['total-memory'])*100
+
+  const numberOfLeases = leasesData.length
+
+  return (
+    <Container service={service}>
+      <Block label="mikrotik.uptime" value={ statsData.uptime } />
+      <Block label="mikrotik.cpuLoad" value={t("common.percent", { value: statsData['cpu-load'] })} />
+      <Block label="mikrotik.memoryUsed" value={t("common.percent", { value: memoryUsed })} />
+      <Block label="mikrotik.numberOfLeases" value={t("common.number", { value: numberOfLeases })} />
+    </Container>
+  );
+}

+ 24 - 0
src/widgets/mikrotik/widget.js

@@ -0,0 +1,24 @@
+
+import genericProxyHandler from "utils/proxy/handlers/generic";
+
+const widget = {
+  api: "{url}/rest/{endpoint}",
+  proxyHandler: genericProxyHandler,
+
+  mappings: {
+    system: {
+      endpoint: "system/resource",
+      validate: [
+        "cpu-load",
+        "free-memory",
+        "total-memory",
+        "uptime"
+      ]
+    },
+    leases: {
+      endpoint: "ip/dhcp-server/lease",
+    }
+  },
+};
+
+export default widget;

+ 33 - 0
src/widgets/miniflux/component.jsx

@@ -0,0 +1,33 @@
+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: minifluxData, error: minifluxError } = useWidgetAPI(widget, "counters");
+
+  if (minifluxError) {
+    return <Container error={minifluxError} />;
+  }
+
+  if (!minifluxData) {
+    return (
+      <Container service={service}>
+        <Block label="miniflux.unread" />
+        <Block label="miniflux.read" />
+      </Container>
+    );
+  }
+
+  return (
+    <Container service={service}>
+      <Block label="miniflux.unread" value={t("common.number", { value: minifluxData.unread })} />
+      <Block label="miniflux.read" value={t("common.number", { value: minifluxData.read })} />
+    </Container>
+  );
+}

+ 19 - 0
src/widgets/miniflux/widget.js

@@ -0,0 +1,19 @@
+import { asJson } from "utils/proxy/api-helpers";
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
+
+const widget = {
+  api: "{url}/v1/{endpoint}",
+  proxyHandler: credentialedProxyHandler,
+  
+  mappings: {
+    counters: {
+      endpoint: "feeds/counters",
+      map: (data) => ({
+        read: Object.values(asJson(data).reads).reduce((acc, i) => acc + i, 0),
+        unread: Object.values(asJson(data).unreads).reduce((acc, i) => acc + i, 0)
+      }),
+    },
+  }
+};
+
+export default widget;

+ 39 - 0
src/widgets/nextdns/component.jsx

@@ -0,0 +1,39 @@
+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: nextdnsData, error: nextdnsError } = useWidgetAPI(widget, "analytics/status");
+
+  if (nextdnsError) {
+    return <Container error={nextdnsError} />;
+  }
+
+  if (!nextdnsData) {
+    return (
+      <Container service={service}>
+        <Block key="status" label="widget.status" value={t("nextdns.wait")} />
+      </Container>
+    );
+  }
+
+  if (!nextdnsData?.data?.length) {
+    return (
+      <Container service={service}>
+        <Block key="status" label="widget.status" value={t("nextdns.no_devices")} />
+      </Container>
+    );
+  }
+
+  return (
+    <Container service={service}>
+      {nextdnsData.data.map(d => <Block key={d.status} label={d.status} value={t("common.number", { value: d.queries })} />)}
+    </Container>
+  );
+}

+ 17 - 0
src/widgets/nextdns/widget.js

@@ -0,0 +1,17 @@
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
+
+const widget = {
+  api: "https://api.nextdns.io/profiles/{profile}/{endpoint}",
+  proxyHandler: credentialedProxyHandler,
+
+  mappings: {
+    "analytics/status": {
+      endpoint: "analytics/status",
+      validate: [
+        "data",
+      ]
+    },
+  },
+};
+
+export default widget;

+ 6 - 6
src/widgets/npm/proxy.js

@@ -10,7 +10,7 @@ const proxyName = "npmProxyHandler";
 const tokenCacheKey = `${proxyName}__token`;
 const tokenCacheKey = `${proxyName}__token`;
 const logger = createLogger(proxyName);
 const logger = createLogger(proxyName);
 
 
-async function login(loginUrl, username, password) {
+async function login(loginUrl, username, password, service) {
   const authResponse = await httpProxy(loginUrl, {
   const authResponse = await httpProxy(loginUrl, {
     method: "POST",
     method: "POST",
     body: JSON.stringify({ identity: username, secret: password }),
     body: JSON.stringify({ identity: username, secret: password }),
@@ -27,7 +27,7 @@ async function login(loginUrl, username, password) {
     
     
     if (status === 200) {
     if (status === 200) {
       const expiration = new Date(data.expires) - Date.now();
       const expiration = new Date(data.expires) - Date.now();
-      cache.put(tokenCacheKey, data.token, expiration - (5 * 60 * 1000)); // expiration -5 minutes
+      cache.put(`${tokenCacheKey}.${service}`, data.token, expiration - (5 * 60 * 1000)); // expiration -5 minutes
     }
     }
   } catch (e) {
   } catch (e) {
     logger.error(`Error ${status} logging into npm`, authResponse[2]);
     logger.error(`Error ${status} logging into npm`, authResponse[2]);
@@ -53,9 +53,9 @@ export default async function npmProxyHandler(req, res) {
       let contentType;
       let contentType;
       let data;
       let data;
       
       
-      let token = cache.get(tokenCacheKey);
+      let token = cache.get(`${tokenCacheKey}.${service}`);
       if (!token) {
       if (!token) {
-        [status, token] = await login(loginUrl, widget.username, widget.password);
+        [status, token] = await login(loginUrl, widget.username, widget.password, service);
         if (status !== 200) {
         if (status !== 200) {
           logger.debug(`HTTTP ${status} logging into npm api: ${token}`);
           logger.debug(`HTTTP ${status} logging into npm api: ${token}`);
           return res.status(status).send(token);
           return res.status(status).send(token);
@@ -72,8 +72,8 @@ export default async function npmProxyHandler(req, res) {
 
 
       if (status === 403) {
       if (status === 403) {
         logger.debug(`HTTTP ${status} retrieving data from npm api, logging in and trying again.`);
         logger.debug(`HTTTP ${status} retrieving data from npm api, logging in and trying again.`);
-        cache.del(tokenCacheKey);
-        [status, token] = await login(loginUrl, widget.username, widget.password);
+        cache.del(`${tokenCacheKey}.${service}`);
+        [status, token] = await login(loginUrl, widget.username, widget.password, service);
 
 
         if (status !== 200) {
         if (status !== 200) {
           logger.debug(`HTTTP ${status} logging into npm api: ${data}`);
           logger.debug(`HTTTP ${status} logging into npm api: ${data}`);

+ 39 - 0
src/widgets/omada/component.jsx

@@ -0,0 +1,39 @@
+import { useTranslation } from "next-i18next";
+
+import useWidgetAPI from "../../utils/proxy/use-widget-api";
+import Container from "../../components/services/widget/container";
+import Block from "../../components/services/widget/block";
+
+export default function Component({ service }) {
+  const { t } = useTranslation();
+
+  const { widget } = service;
+
+  const { data: omadaData, error: omadaAPIError } = useWidgetAPI(widget, {
+    refreshInterval: 5000,
+  });
+
+  if (omadaAPIError) {
+    return <Container error={omadaAPIError} />;
+  }
+
+  if (!omadaData) {
+    return (
+      <Container service={service}>
+        <Block label="omada.connectedAp" />
+        <Block label="omada.activeUser" />
+        <Block label="omada.alerts" />
+      </Container>
+    );
+  }
+
+  return (
+    <Container service={service}>
+      <Block label="omada.connectedAp" value={t( "common.number", { value: omadaData.connectedAp})} />
+      <Block label="omada.activeUser" value={t( "common.number", { value: omadaData.activeUser })} />
+      <Block label="omada.alerts" value={t( "common.number", { value: omadaData.alerts })} />
+      { omadaData.connectedGateways > 0 && <Block label="omada.connectedGateway" value={t("common.number", { value: omadaData.connectedGateways})} /> }
+      { omadaData.connectedSwitches > 0 && <Block label="omada.connectedSwitches" value={t("common.number", { value: omadaData.connectedSwitches})} /> }
+    </Container>
+  );
+}

+ 252 - 0
src/widgets/omada/proxy.js

@@ -0,0 +1,252 @@
+
+import { httpProxy } from "utils/proxy/http";
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+
+const proxyName = "omadaProxyHandler";
+
+const logger = createLogger(proxyName);
+
+async function login(loginUrl, username, password, controllerVersionMajor) {
+  const params = {
+    username,
+    password
+  }
+
+  if (controllerVersionMajor === 3) {
+    params.method = "login";
+    params.params = {
+      name: username,
+      password
+    };
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  const [status, contentType, data] = await httpProxy(loginUrl, {
+    method: "POST",
+    body: JSON.stringify(params),
+    headers: {
+      "Content-Type": "application/json",
+    },
+  });
+
+  return [status, JSON.parse(data.toString())];
+}
+
+
+export default async function omadaProxyHandler(req, res) {
+  const { group, service } = req.query;
+
+  if (group && service) {
+    const widget = await getServiceWidget(group, service);
+
+    if (widget) {
+
+      const { url } = widget;
+
+      const controllerInfoURL = `${url}/api/info`;
+
+      let [status, contentType, data] = await httpProxy(controllerInfoURL, {
+          headers: {
+            "Content-Type": "application/json",
+          },
+      });
+
+      if (status !== 200) {
+        logger.error("Unable to retrieve Omada controller info");
+        return res.status(status).json({error: {message: `HTTP Error ${status}`, url: controllerInfoURL, data}});
+      }
+
+      let cId;
+      let controllerVersion;
+
+      try {
+        cId = JSON.parse(data).result.omadacId;
+        controllerVersion = JSON.parse(data).result.controllerVer;
+      } catch (e) {
+        controllerVersion = "3.2.x"
+      }
+
+      const controllerVersionMajor = parseInt(controllerVersion.split('.')[0], 10)
+
+      if (![3,4,5].includes(controllerVersionMajor)) {
+        return res.status(500).json({error: {message: "Error determining controller version", data}});
+      }
+
+      let loginUrl;
+
+      switch (controllerVersionMajor) {
+        case 3:
+          loginUrl = `${url}/api/user/login?ajax`;
+          break;
+        case 4:
+          loginUrl = `${url}/api/v2/login`;
+          break;
+        case 5:
+          loginUrl = `${url}/${cId}/api/v2/login`;
+          break;
+        default:
+          break;
+      }
+      
+      const [loginStatus, loginResponseData] = await login(loginUrl, widget.username, widget.password, controllerVersionMajor);
+
+      if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
+        return res.status(status).json({error: {message: "Error logging in to Oamda controller", url: loginUrl, data: loginResponseData}});
+      }
+
+      const { token } = loginResponseData.result;
+      
+      let sitesUrl;
+      let body = {};
+      let params = { token };
+      let headers = { "Csrf-Token": token };
+      let method = "GET";
+
+      switch (controllerVersionMajor) {
+        case 3:
+          sitesUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
+          body = {
+            "method": "getUserSites",
+            "params": {
+              "userName": widget.username
+            }
+          };
+          method = "POST";
+          break;
+        case 4:
+          sitesUrl = `${url}/api/v2/sites?token=${token}&currentPage=1&currentPageSize=1000`;          
+          break;
+        case 5:
+          sitesUrl = `${url}/${cId}/api/v2/sites?token=${token}&currentPage=1&currentPageSize=1000`;
+          break;
+        default:
+          break;
+      }
+      
+      [status, contentType, data] = await httpProxy(sitesUrl, {
+        method,
+        params,
+        body: JSON.stringify(body),
+        headers,
+      });
+
+      const sitesResponseData = JSON.parse(data);
+
+      if (status !== 200 || sitesResponseData.errorCode > 0) {
+        logger.debug(`HTTTP ${status} getting sites list: ${sitesResponseData.msg}`);
+        return res.status(status).json({error: {message: "Error getting sites list", url, data: sitesResponseData}});
+      }
+
+      const site = (controllerVersionMajor === 3) ? 
+        sitesResponseData.result.siteList.find(s => s.name === widget.site):
+        sitesResponseData.result.data.find(s => s.name === widget.site);
+
+      if (!site) {
+        return res.status(status).json({error: {message: `Site ${widget.site} is not found`, url: sitesUrl, data}});
+      }
+
+      let siteResponseData;
+
+      let connectedAp;
+      let activeUser;
+      let connectedSwitches;
+      let connectedGateways;
+      let alerts;
+
+      if (controllerVersionMajor === 3) {
+        // Omada v3 controller requires switching site
+        const switchUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
+        method = "POST";
+        body = {
+          method: "switchSite",
+          params: {
+            siteName: site.siteName,
+            userName: widget.username
+          }
+        };
+        headers = { "Content-Type": "application/json" };
+        params = { token };
+
+        [status, contentType, data] = await httpProxy(switchUrl, {
+          method,
+          params,
+          body: JSON.stringify(body),
+          headers,
+        });
+
+        const switchResponseData = JSON.parse(data);
+        if (status !== 200 || switchResponseData.errorCode > 0) {
+          logger.error(`HTTP ${status} getting sites list: ${data}`);
+          return res.status(status).json({error: {message: "Error switching site", url: switchUrl, data}});
+        }
+        
+        const statsUrl = `${url}/web/v1/controller?getGlobalStat=&token=${token}`;
+        [status, contentType, data] = await httpProxy(statsUrl, {
+          method,
+          params,
+          body: JSON.stringify({
+            "method": "getGlobalStat",
+          }),
+          headers
+        });
+
+        siteResponseData = JSON.parse(data);
+
+        if (status !== 200 || siteResponseData.errorCode > 0) {
+          return res.status(status).json({error: {message: "Error getting stats", url: statsUrl, data}});
+        }
+
+        connectedAp = siteResponseData.result.connectedAp;
+        activeUser = siteResponseData.result.activeUser;
+        alerts = siteResponseData.result.alerts;
+      } else if (controllerVersionMajor === 4 || controllerVersionMajor === 5) {
+        const siteName = (controllerVersionMajor === 5) ? site.id : site.key;
+        const siteStatsUrl = (controllerVersionMajor === 4) ? 
+          `${url}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000` :
+          `${url}/${cId}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000`;
+
+        [status, contentType, data] = await httpProxy(siteStatsUrl, {
+          headers: {
+            "Csrf-Token": token,
+          },
+        });
+
+        siteResponseData = JSON.parse(data);
+        
+        if (status !== 200 || siteResponseData.errorCode > 0) {
+          logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`);
+          return res.status(500).send(data);
+        }
+
+        const alertUrl = (controllerVersionMajor === 4) ? 
+          `${url}/api/v2/sites/${siteName}/alerts/num?token=${token}&currentPage=1&currentPageSize=1000` :
+          `${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}&currentPage=1&currentPageSize=1000`;
+
+        // eslint-disable-next-line no-unused-vars
+        [status, contentType, data] = await httpProxy(alertUrl, {
+          headers: {
+            "Csrf-Token": token,
+          },
+        });
+        const alertResponseData = JSON.parse(data);
+
+        activeUser = siteResponseData.result.totalClientNum;
+        connectedAp = siteResponseData.result.connectedApNum;
+        connectedGateways = siteResponseData.result.connectedGatewayNum;
+        connectedSwitches = siteResponseData.result.connectedSwitchNum;
+        alerts = alertResponseData.result.alertNum;
+      }
+
+      return res.send(JSON.stringify({
+        connectedAp,
+        activeUser,
+        alerts,
+        connectedGateways,
+        connectedSwitches,
+      }));
+    }
+  }
+
+  return res.status(400).json({ error: "Invalid proxy service type" });
+}

+ 7 - 0
src/widgets/omada/widget.js

@@ -0,0 +1,7 @@
+import omadaProxyHandler from "./proxy";
+
+const widget = {
+  proxyHandler: omadaProxyHandler,
+};
+
+export default widget;

+ 48 - 0
src/widgets/opnsense/component.jsx

@@ -0,0 +1,48 @@
+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: activityData, error: activityError } = useWidgetAPI(widget, "activity");
+  const { data: interfaceData, error: interfaceError } = useWidgetAPI(widget, "interface");
+
+  if (activityError || interfaceError) {
+    const finalError = activityError ?? interfaceError;
+    return <Container error={ finalError } />;
+  }
+
+  if (!activityData || !interfaceData) {
+    return (
+      <Container service={service}>
+        <Block label="opnsense.cpu" />
+        <Block label="opnsense.memory" />
+        <Block label="opnsense.wanUpload" />
+        <Block label="opnsense.wanDownload" />
+      </Container>
+    );
+  }
+
+
+  const cpuIdle = activityData.headers[2].match(/ ([0-9.]+)% idle/)[1];
+  const cpu = 100 - parseFloat(cpuIdle);
+  const memory = activityData.headers[3].match(/Mem: (.+) Active,/)[1];
+
+  const wanUpload = interfaceData.interfaces.wan['bytes transmitted'];
+  const wanDownload = interfaceData.interfaces.wan['bytes received'];
+
+  return (
+    <Container service={service}>
+      <Block label="opnsense.cpu" value={t("common.percent", { value: cpu.toFixed(2) })}  />
+      <Block label="opnsense.memory" value={memory} />
+      <Block label="opnsense.wanUpload" value={t("common.bytes", { value: wanUpload })} />
+      <Block label="opnsense.wanDownload" value={t("common.bytes", { value: wanDownload })} />
+
+    </Container>
+  );
+}

+ 24 - 0
src/widgets/opnsense/widget.js

@@ -0,0 +1,24 @@
+
+import genericProxyHandler from "utils/proxy/handlers/generic";
+
+const widget = {
+  api: "{url}/api/{endpoint}",
+  proxyHandler: genericProxyHandler,
+
+  mappings: {
+    activity: {
+      endpoint: "diagnostics/activity/getActivity",
+      validate: [
+        "headers"
+      ]
+    },
+    interface: {
+      endpoint: "diagnostics/traffic/interface",
+      validate: [
+        "interfaces"
+      ]
+    }
+  },
+};
+
+export default widget;

+ 7 - 4
src/widgets/overseerr/component.jsx

@@ -1,8 +1,11 @@
+import { useTranslation } from "next-i18next";
+
 import Container from "components/services/widget/container";
 import Container from "components/services/widget/container";
 import Block from "components/services/widget/block";
 import Block from "components/services/widget/block";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 
 
 export default function Component({ service }) {
 export default function Component({ service }) {
+  const { t } = useTranslation();
   const { widget } = service;
   const { widget } = service;
 
 
   const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count");
   const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count");
@@ -24,10 +27,10 @@ export default function Component({ service }) {
 
 
   return (
   return (
     <Container service={service}>
     <Container service={service}>
-      <Block label="overseerr.pending" value={statsData.pending} />
-      <Block label="overseerr.processing" value={statsData.processing} />
-      <Block label="overseerr.approved" value={statsData.approved} />
-      <Block label="overseerr.available" value={statsData.available} />
+      <Block label="overseerr.pending" value={t("common.number", { value: statsData.pending })} />
+      <Block label="overseerr.processing" value={t("common.number", { value: statsData.processing })} />
+      <Block label="overseerr.approved" value={t("common.number", { value: statsData.approved })} />
+      <Block label="overseerr.available" value={t("common.number", { value: statsData.available })} />
     </Container>
     </Container>
   );
   );
 }
 }

+ 4 - 4
src/widgets/pihole/component.jsx

@@ -9,7 +9,7 @@ export default function Component({ service }) {
 
 
   const { widget } = service;
   const { widget } = service;
 
 
-  const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "api.php");
+  const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "summaryRaw");
 
 
   if (piholeError) {
   if (piholeError) {
     return <Container error={piholeError} />;
     return <Container error={piholeError} />;
@@ -27,9 +27,9 @@ export default function Component({ service }) {
 
 
   return (
   return (
     <Container service={service}>
     <Container service={service}>
-      <Block label="pihole.queries" value={t("common.number", { value: piholeData.dns_queries_today })} />
-      <Block label="pihole.blocked" value={t("common.number", { value: piholeData.ads_blocked_today })} />
-      <Block label="pihole.gravity" value={t("common.number", { value: piholeData.domains_being_blocked })} />
+      <Block label="pihole.queries" value={t("common.number", { value: parseInt(piholeData.dns_queries_today, 10) })} />
+      <Block label="pihole.blocked" value={t("common.number", { value: parseInt(piholeData.ads_blocked_today, 10) })} />
+      <Block label="pihole.gravity" value={t("common.number", { value: parseInt(piholeData.domains_being_blocked, 10) })} />
     </Container>
     </Container>
   );
   );
 }
 }

+ 3 - 3
src/widgets/pihole/widget.js

@@ -1,12 +1,12 @@
 import genericProxyHandler from "utils/proxy/handlers/generic";
 import genericProxyHandler from "utils/proxy/handlers/generic";
 
 
 const widget = {
 const widget = {
-  api: "{url}/admin/{endpoint}",
+  api: "{url}/admin/api.php?{endpoint}&auth={key}",
   proxyHandler: genericProxyHandler,
   proxyHandler: genericProxyHandler,
 
 
   mappings: {
   mappings: {
-    "api.php": {
-      endpoint: "api.php",
+    "summaryRaw": {
+      endpoint: "summaryRaw",
       validate: [
       validate: [
         "dns_queries_today",
         "dns_queries_today",
         "ads_blocked_today",
         "ads_blocked_today",

+ 13 - 9
src/widgets/plex/proxy.js

@@ -58,6 +58,9 @@ async function fetchFromPlexAPI(endpoint, widget) {
 
 
 export default async function plexProxyHandler(req, res) {
 export default async function plexProxyHandler(req, res) {
   const widget = await getWidget(req);
   const widget = await getWidget(req);
+  
+  const { service } = req.query;
+
   if (!widget) {
   if (!widget) {
     return res.status(400).json({ error: "Invalid proxy service type" });
     return res.status(400).json({ error: "Invalid proxy service type" });
   }
   }
@@ -74,23 +77,24 @@ export default async function plexProxyHandler(req, res) {
     streams = apiData.MediaContainer._attributes.size;
     streams = apiData.MediaContainer._attributes.size;
   }
   }
 
 
-  let libraries = cache.get(librariesCacheKey);
+  let libraries = cache.get(`${librariesCacheKey}.${service}`);
   if (libraries === null) {
   if (libraries === null) {
     logger.debug("Getting libraries from Plex API");
     logger.debug("Getting libraries from Plex API");
     [status, apiData] = await fetchFromPlexAPI("/library/sections", widget);
     [status, apiData] = await fetchFromPlexAPI("/library/sections", widget);
     if (apiData && apiData.MediaContainer) {
     if (apiData && apiData.MediaContainer) {
-      libraries = apiData.MediaContainer.Directory;
-      cache.put(librariesCacheKey, libraries, 1000 * 60 * 60 * 6);
+      libraries = [].concat(apiData.MediaContainer.Directory);
+      cache.put(`${librariesCacheKey}.${service}`, libraries, 1000 * 60 * 60 * 6);
     }
     }
   }
   }
 
 
-  let movies = cache.get(moviesCacheKey);
-  let tv = cache.get(tvCacheKey);
+  let movies = cache.get(`${moviesCacheKey}.${service}`);
+  let tv = cache.get(`${tvCacheKey}.${service}`);
   if (movies === null || tv === null) {
   if (movies === null || tv === null) {
     movies = 0;
     movies = 0;
     tv = 0;
     tv = 0;
     logger.debug("Getting movie + tv counts from Plex API");
     logger.debug("Getting movie + tv counts from Plex API");
-    libraries.filter(l => ["movie", "show"].includes(l._attributes.type)).forEach(async (library) => {
+    const movieTVLibraries = libraries.filter(l => ["movie", "show"].includes(l._attributes.type));
+    await Promise.all(movieTVLibraries.map(async (library) => {
       [status, apiData] = await fetchFromPlexAPI(`/library/sections/${library._attributes.key}/all`, widget);
       [status, apiData] = await fetchFromPlexAPI(`/library/sections/${library._attributes.key}/all`, widget);
       if (apiData && apiData.MediaContainer) {
       if (apiData && apiData.MediaContainer) {
         const size = parseInt(apiData.MediaContainer._attributes.size, 10);
         const size = parseInt(apiData.MediaContainer._attributes.size, 10);
@@ -100,9 +104,9 @@ export default async function plexProxyHandler(req, res) {
           tv += size;
           tv += size;
         }
         }
       }
       }
-      cache.put(tvCacheKey, tv, 1000 * 60 * 10);
-      cache.put(moviesCacheKey, movies, 1000 * 60 * 10);
-    });
+    }));
+    cache.put(`${tvCacheKey}.${service}`, tv, 1000 * 60 * 10);
+    cache.put(`${moviesCacheKey}.${service}`, movies, 1000 * 60 * 10);
   }
   }
   
   
   const data = {
   const data = {

+ 8 - 5
src/widgets/prowlarr/component.jsx

@@ -1,8 +1,11 @@
+import { useTranslation } from "react-i18next";
+
 import Container from "components/services/widget/container";
 import Container from "components/services/widget/container";
 import Block from "components/services/widget/block";
 import Block from "components/services/widget/block";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 
 
 export default function Component({ service }) {
 export default function Component({ service }) {
+  const { t } = useTranslation();
   const { widget } = service;
   const { widget } = service;
 
 
   const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexer");
   const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexer");
@@ -40,11 +43,11 @@ export default function Component({ service }) {
 
 
   return (
   return (
     <Container service={service}>
     <Container service={service}>
-      <Block label="prowlarr.enableIndexers" value={indexers.length} />
-      <Block label="prowlarr.numberOfGrabs" value={numberOfGrabs} />
-      <Block label="prowlarr.numberOfQueries" value={numberOfQueries} />
-      <Block label="prowlarr.numberOfFailGrabs" value={numberOfFailedGrabs} />
-      <Block label="prowlarr.numberOfFailQueries" value={numberOfFailedQueries} />
+      <Block label="prowlarr.enableIndexers" value={t("common.number", { value: indexers.length })} />
+      <Block label="prowlarr.numberOfGrabs" value={t("common.number", { value: numberOfGrabs })} />
+      <Block label="prowlarr.numberOfQueries" value={t("common.number", { value: numberOfQueries })} />
+      <Block label="prowlarr.numberOfFailGrabs" value={t("common.number", { value: numberOfFailedGrabs })} />
+      <Block label="prowlarr.numberOfFailQueries" value={t("common.number", { value: numberOfFailedQueries })} />
     </Container>
     </Container>
   );
   );
 }
 }

+ 16 - 16
src/widgets/pyload/proxy.js

@@ -11,7 +11,7 @@ const logger = createLogger(proxyName);
 const sessionCacheKey = `${proxyName}__sessionId`;
 const sessionCacheKey = `${proxyName}__sessionId`;
 const isNgCacheKey = `${proxyName}__isNg`;
 const isNgCacheKey = `${proxyName}__isNg`;
 
 
-async function fetchFromPyloadAPI(url, sessionId, params) {
+async function fetchFromPyloadAPI(url, sessionId, params, service) {
   const options = {
   const options = {
     body: params
     body: params
       ? Object.keys(params)
       ? Object.keys(params)
@@ -25,10 +25,10 @@ async function fetchFromPyloadAPI(url, sessionId, params) {
   };
   };
 
 
   // see https://github.com/benphelps/homepage/issues/517
   // see https://github.com/benphelps/homepage/issues/517
-  const isNg = cache.get(isNgCacheKey);
+  const isNg = cache.get(`${isNgCacheKey}.${service}`);
   if (isNg && !params) {
   if (isNg && !params) {
     delete options.body;
     delete options.body;
-    options.headers.Cookie = cache.get(sessionCacheKey);
+    options.headers.Cookie = cache.get(`${sessionCacheKey}.${service}`);
   }
   }
 
 
   // eslint-disable-next-line no-unused-vars
   // eslint-disable-next-line no-unused-vars
@@ -43,19 +43,19 @@ async function fetchFromPyloadAPI(url, sessionId, params) {
   return [status, returnData, responseHeaders];
   return [status, returnData, responseHeaders];
 }
 }
 
 
-async function login(loginUrl, username, password = '') {
-  const [status, sessionId, responseHeaders] = await fetchFromPyloadAPI(loginUrl, null, { username, password });
+async function login(loginUrl, service, username, password = '') {
+  const [status, sessionId, responseHeaders] = await fetchFromPyloadAPI(loginUrl, null, { username, password }, service);
   
   
   // this API actually returns status 200 even on login failure
   // this API actually returns status 200 even on login failure
   if (status !== 200 || sessionId === false) {
   if (status !== 200 || sessionId === false) {
     logger.error(`HTTP ${status} logging into Pyload API, returned: ${JSON.stringify(sessionId)}`);
     logger.error(`HTTP ${status} logging into Pyload API, returned: ${JSON.stringify(sessionId)}`);
   } else if (responseHeaders['set-cookie']?.join().includes('pyload_session')) {
   } else if (responseHeaders['set-cookie']?.join().includes('pyload_session')) {
     // Support pyload-ng, see https://github.com/benphelps/homepage/issues/517
     // Support pyload-ng, see https://github.com/benphelps/homepage/issues/517
-    cache.put(isNgCacheKey, true);
+    cache.put(`${isNgCacheKey}.${service}`, true);
     const sessionCookie = responseHeaders['set-cookie'][0];
     const sessionCookie = responseHeaders['set-cookie'][0];
-    cache.put(sessionCacheKey, sessionCookie, 60 * 60 * 23 * 1000); // cache for 23h
+    cache.put(`${sessionCacheKey}.${service}`, sessionCookie, 60 * 60 * 23 * 1000); // cache for 23h
   } else {
   } else {
-    cache.put(sessionCacheKey, sessionId);
+    cache.put(`${sessionCacheKey}.${service}`, sessionId);
   }
   }
 
 
   return sessionId;
   return sessionId;
@@ -72,21 +72,21 @@ export default async function pyloadProxyHandler(req, res) {
         const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
         const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
         const loginUrl = `${widget.url}/api/login`;
         const loginUrl = `${widget.url}/api/login`;
 
 
-        let sessionId = cache.get(sessionCacheKey) ?? await login(loginUrl, widget.username, widget.password);
-        let [status, data] = await fetchFromPyloadAPI(url, sessionId);
+        let sessionId = cache.get(`${sessionCacheKey}.${service}`) ?? await login(loginUrl, service, widget.username, widget.password);
+        let [status, data] = await fetchFromPyloadAPI(url, sessionId, null, service);
 
 
         if (status === 403 || status === 401) {
         if (status === 403 || status === 401) {
           logger.info('Failed to retrieve data from Pyload API, trying to login again...');
           logger.info('Failed to retrieve data from Pyload API, trying to login again...');
-          cache.del(sessionCacheKey);
-          sessionId = await login(loginUrl, widget.username, widget.password);
-          [status, data] = await fetchFromPyloadAPI(url, sessionId);
+          cache.del(`${sessionCacheKey}.${service}`);
+          sessionId = await login(loginUrl, service, widget.username, widget.password);
+          [status, data] = await fetchFromPyloadAPI(url, sessionId, null, service);
         }
         }
 
 
         if (data?.error || status !== 200) {
         if (data?.error || status !== 200) {
           try {
           try {
-            return res.status(status).send({error: {message: "HTTP error communicating with Plex API", data: Buffer.from(data).toString()}});
+            return res.status(status).send({error: {message: "HTTP error communicating with Pyload API", data: Buffer.from(data).toString()}});
           } catch (e) {
           } catch (e) {
-            return res.status(status).send({error: {message: "HTTP error communicating with Plex API", data}});
+            return res.status(status).send({error: {message: "HTTP error communicating with Pyload API", data}});
           }
           }
         }
         }
 
 
@@ -95,7 +95,7 @@ export default async function pyloadProxyHandler(req, res) {
     }
     }
   } catch (e) {
   } catch (e) {
     logger.error(e);
     logger.error(e);
-    return res.status(500).send({error: {message: `Error communicating with Plex API: ${e.toString()}`}});
+    return res.status(500).send({error: {message: `Error communicating with Pyload API: ${e.toString()}`}});
   }
   }
 
 
   return res.status(400).json({ error: 'Invalid proxy service type' });
   return res.status(400).json({ error: 'Invalid proxy service type' });

+ 2 - 2
src/widgets/qbittorrent/component.jsx

@@ -44,9 +44,9 @@ export default function Component({ service }) {
   return (
   return (
     <Container service={service}>
     <Container service={service}>
       <Block label="qbittorrent.leech" value={t("common.number", { value: leech })} />
       <Block label="qbittorrent.leech" value={t("common.number", { value: leech })} />
-      <Block label="qbittorrent.download" value={t("common.bitrate", { value: rateDl })} />
+      <Block label="qbittorrent.download" value={t("common.bibyterate", { value: rateDl, decimals: 1 })} />
       <Block label="qbittorrent.seed" value={t("common.number", { value: completed })} />
       <Block label="qbittorrent.seed" value={t("common.number", { value: completed })} />
-      <Block label="qbittorrent.upload" value={t("common.bitrate", { value: rateUl })} />
+      <Block label="qbittorrent.upload" value={t("common.bibyterate", { value: rateUl, decimals: 1 })} />
     </Container>
     </Container>
   );
   );
 }
 }

+ 7 - 4
src/widgets/radarr/component.jsx

@@ -1,8 +1,11 @@
+import { useTranslation } from "next-i18next";
+
 import Container from "components/services/widget/container";
 import Container from "components/services/widget/container";
 import Block from "components/services/widget/block";
 import Block from "components/services/widget/block";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 
 
 export default function Component({ service }) {
 export default function Component({ service }) {
+  const { t } = useTranslation();
   const { widget } = service;
   const { widget } = service;
 
 
   const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movie");
   const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movie");
@@ -26,10 +29,10 @@ export default function Component({ service }) {
 
 
   return (
   return (
     <Container service={service}>
     <Container service={service}>
-      <Block label="radarr.wanted" value={moviesData.wanted} />
-      <Block label="radarr.missing" value={moviesData.missing} />
-      <Block label="radarr.queued" value={queuedData.totalCount} />
-      <Block label="radarr.movies" value={moviesData.have} />
+      <Block label="radarr.wanted" value={t("common.number", { value: moviesData.wanted })} />
+      <Block label="radarr.missing" value={t("common.number", { value: moviesData.missing })} />
+      <Block label="radarr.queued" value={t("common.number", { value: queuedData.totalCount })} />
+      <Block label="radarr.movies" value={t("common.number", { value: moviesData.have })} />
     </Container>
     </Container>
   );
   );
 }
 }

+ 6 - 3
src/widgets/sonarr/component.jsx

@@ -1,8 +1,11 @@
+import { useTranslation } from "next-i18next";
+
 import Container from "components/services/widget/container";
 import Container from "components/services/widget/container";
 import Block from "components/services/widget/block";
 import Block from "components/services/widget/block";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 
 
 export default function Component({ service }) {
 export default function Component({ service }) {
+  const { t } = useTranslation();
   const { widget } = service;
   const { widget } = service;
 
 
   const { data: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing");
   const { data: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing");
@@ -26,9 +29,9 @@ export default function Component({ service }) {
 
 
   return (
   return (
     <Container service={service}>
     <Container service={service}>
-      <Block label="sonarr.wanted" value={wantedData.totalRecords} />
-      <Block label="sonarr.queued" value={queuedData.totalRecords} />
-      <Block label="sonarr.series" value={seriesData.total} />
+      <Block label="sonarr.wanted" value={t("common.number", { value: wantedData.totalRecords })} />
+      <Block label="sonarr.queued" value={t("common.number", { value: queuedData.totalRecords })} />
+      <Block label="sonarr.series" value={t("common.number", { value: seriesData.total })} />
     </Container>
     </Container>
   );
   );
 }
 }

+ 2 - 2
src/widgets/speedtest/component.jsx

@@ -29,9 +29,9 @@ export default function Component({ service }) {
     <Container service={service}>
     <Container service={service}>
       <Block
       <Block
         label="speedtest.download"
         label="speedtest.download"
-        value={t("common.bitrate", { value: speedtestData.data.download * 1024 * 1024 })}
+        value={t("common.bitrate", { value: speedtestData.data.download * 1000 * 1000 })}
       />
       />
-      <Block label="speedtest.upload" value={t("common.bitrate", { value: speedtestData.data.upload * 1024 * 1024 })} />
+      <Block label="speedtest.upload" value={t("common.bitrate", { value: speedtestData.data.upload * 1000 * 1000 })} />
       <Block
       <Block
         label="speedtest.ping"
         label="speedtest.ping"
         value={t("common.ms", {
         value={t("common.ms", {

+ 42 - 0
src/widgets/tdarr/component.jsx

@@ -0,0 +1,42 @@
+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: tdarrData, error: tdarrError } = useWidgetAPI(widget);
+
+  if (tdarrError) {
+    return <Container error={tdarrError} />;
+  }
+
+  if (!tdarrData) {
+    return (
+      <Container service={service}>
+        <Block label="tdarr.queue" />
+        <Block label="tdarr.processed" />
+        <Block label="tdarr.errored" />
+        <Block label="tdarr.saved" />
+      </Container>
+    );
+  }
+
+  const queue = parseInt(tdarrData.table1Count, 10) + parseInt(tdarrData.table4Count, 10);
+  const processed = parseInt(tdarrData.table2Count, 10) + parseInt(tdarrData.table5Count, 10);
+  const errored = parseInt(tdarrData.table3Count, 10) + parseInt(tdarrData.table6Count, 10);
+  const saved = parseFloat(tdarrData.sizeDiff, 10) * 1000000000;
+
+  return (
+    <Container service={service}>
+      <Block label="tdarr.queue" value={t("common.number", { value: queue })} />
+      <Block label="tdarr.processed" value={t("common.number", { value: processed })} />
+      <Block label="tdarr.errored" value={t("common.number", { value: errored })} />
+      <Block label="tdarr.saved" value={t("common.bytes", { value: saved })} />    
+    </Container>
+  );
+}

+ 48 - 0
src/widgets/tdarr/proxy.js

@@ -0,0 +1,48 @@
+import { httpProxy } from "utils/proxy/http";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+import widgets from "widgets/widgets";
+
+const proxyName = "tdarrProxyHandler";
+const logger = createLogger(proxyName);
+
+export default async function tdarrProxyHandler(req, res) {
+  const { group, service, endpoint } = 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);
+
+  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" });
+  }
+
+  const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
+
+  const [status, contentType, data] = await httpProxy(url, {
+    method: "POST",
+    body: JSON.stringify({
+      "data": {
+        "collection": "StatisticsJSONDB",
+        "mode": "getById",
+        "docID": "statistics"
+      },
+    }),
+    headers: {
+      "content-type": "application/json",
+    },
+  });
+
+  if (status !== 200) {
+    logger.error("Error getting data from Tdarr: %d.  Data: %s", status, data);
+    return res.status(500).send({error: {message:"Error getting data from Tdarr", url, data}});
+  }
+
+  if (contentType) res.setHeader("Content-Type", contentType);
+  return res.status(status).send(data);
+}

+ 8 - 0
src/widgets/tdarr/widget.js

@@ -0,0 +1,8 @@
+import tdarrProxyHandler from "./proxy";
+
+const widget = {
+  api: "{url}/api/v2/cruddb",
+  proxyHandler: tdarrProxyHandler,
+};
+
+export default widget;

+ 3 - 3
src/widgets/transmission/proxy.js

@@ -25,12 +25,12 @@ export default async function transmissionProxyHandler(req, res) {
     return res.status(400).json({ error: "Invalid proxy service type" });
     return res.status(400).json({ error: "Invalid proxy service type" });
   }
   }
 
 
-  let headers = cache.get(headerCacheKey);
+  let headers = cache.get(`${headerCacheKey}.${service}`);
   if (!headers) {
   if (!headers) {
     headers = {
     headers = {
       "content-type": "application/json",
       "content-type": "application/json",
     }
     }
-    cache.put(headerCacheKey, headers);
+    cache.put(`${headerCacheKey}.${service}`, headers);
   }
   }
 
 
   const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
   const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
@@ -55,7 +55,7 @@ export default async function transmissionProxyHandler(req, res) {
   if (status === 409) {
   if (status === 409) {
     logger.debug("Transmission is rejecting the request, but returning a CSRF token");
     logger.debug("Transmission is rejecting the request, but returning a CSRF token");
     headers[csrfHeaderName] = responseHeaders[csrfHeaderName];
     headers[csrfHeaderName] = responseHeaders[csrfHeaderName];
-    cache.put(headerCacheKey, headers);
+    cache.put(`${headerCacheKey}.${service}`, headers);
 
 
     // retry the request, now with the CSRF token
     // retry the request, now with the CSRF token
     [status, contentType, data, responseHeaders] = await httpProxy(url, {
     [status, contentType, data, responseHeaders] = await httpProxy(url, {

+ 3 - 2
src/widgets/unifi/proxy.js

@@ -58,6 +58,7 @@ async function login(widget) {
 
 
 export default async function unifiProxyHandler(req, res) {
 export default async function unifiProxyHandler(req, res) {
   const widget = await getWidget(req);
   const widget = await getWidget(req);
+  const { service } = req.query;
   if (!widget) {
   if (!widget) {
     return res.status(400).json({ error: "Invalid proxy service type" });
     return res.status(400).json({ error: "Invalid proxy service type" });
   }
   }
@@ -68,7 +69,7 @@ export default async function unifiProxyHandler(req, res) {
   }
   }
 
 
   let [status, contentType, data, responseHeaders] = [];
   let [status, contentType, data, responseHeaders] = [];
-  let prefix = cache.get(prefixCacheKey);
+  let prefix = cache.get(`${prefixCacheKey}.${service}`);
   if (prefix === null) {
   if (prefix === null) {
     // auto detect if we're talking to a UDM Pro, and cache the result so that we
     // auto detect if we're talking to a UDM Pro, and cache the result so that we
     // don't make two requests each time data from Unifi is required
     // don't make two requests each time data from Unifi is required
@@ -77,7 +78,7 @@ export default async function unifiProxyHandler(req, res) {
     if (responseHeaders?.["x-csrf-token"]) {
     if (responseHeaders?.["x-csrf-token"]) {
       prefix = udmpPrefix;
       prefix = udmpPrefix;
     }
     }
-    cache.put(prefixCacheKey, prefix);
+    cache.put(`${prefixCacheKey}.${service}`, prefix);
   }
   }
 
 
   widget.prefix = prefix;
   widget.prefix = prefix;

+ 17 - 2
src/widgets/widgets.js

@@ -5,7 +5,7 @@ import bazarr from "./bazarr/widget";
 import changedetectionio from "./changedetectionio/widget";
 import changedetectionio from "./changedetectionio/widget";
 import coinmarketcap from "./coinmarketcap/widget";
 import coinmarketcap from "./coinmarketcap/widget";
 import deluge from "./deluge/widget";
 import deluge from "./deluge/widget";
-import diskstation from "./diskstation/widget";
+import downloadstation from "./downloadstation/widget";
 import emby from "./emby/widget";
 import emby from "./emby/widget";
 import flood from "./flood/widget";
 import flood from "./flood/widget";
 import gluetun from "./gluetun/widget";
 import gluetun from "./gluetun/widget";
@@ -16,10 +16,15 @@ import jackett from "./jackett/widget";
 import jellyseerr from "./jellyseerr/widget";
 import jellyseerr from "./jellyseerr/widget";
 import lidarr from "./lidarr/widget";
 import lidarr from "./lidarr/widget";
 import mastodon from "./mastodon/widget";
 import mastodon from "./mastodon/widget";
+import miniflux from "./miniflux/widget";
+import mikrotik from "./mikrotik/widget";
 import navidrome from "./navidrome/widget";
 import navidrome from "./navidrome/widget";
+import nextdns from "./nextdns/widget";
 import npm from "./npm/widget";
 import npm from "./npm/widget";
 import nzbget from "./nzbget/widget";
 import nzbget from "./nzbget/widget";
+import omada from "./omada/widget";
 import ombi from "./ombi/widget";
 import ombi from "./ombi/widget";
+import opnsense from "./opnsense/widget";
 import overseerr from "./overseerr/widget";
 import overseerr from "./overseerr/widget";
 import paperlessngx from "./paperlessngx/widget";
 import paperlessngx from "./paperlessngx/widget";
 import pihole from "./pihole/widget";
 import pihole from "./pihole/widget";
@@ -38,12 +43,14 @@ import sonarr from "./sonarr/widget";
 import speedtest from "./speedtest/widget";
 import speedtest from "./speedtest/widget";
 import strelaysrv from "./strelaysrv/widget";
 import strelaysrv from "./strelaysrv/widget";
 import tautulli from "./tautulli/widget";
 import tautulli from "./tautulli/widget";
+import tdarr from "./tdarr/widget";
 import traefik from "./traefik/widget";
 import traefik from "./traefik/widget";
 import transmission from "./transmission/widget";
 import transmission from "./transmission/widget";
 import tubearchivist from "./tubearchivist/widget";
 import tubearchivist from "./tubearchivist/widget";
 import truenas from "./truenas/widget";
 import truenas from "./truenas/widget";
 import unifi from "./unifi/widget";
 import unifi from "./unifi/widget";
 import watchtower from './watchtower/widget'
 import watchtower from './watchtower/widget'
+import xteve from './xteve/widget'
 
 
 const widgets = {
 const widgets = {
   adguard,
   adguard,
@@ -53,7 +60,8 @@ const widgets = {
   changedetectionio,
   changedetectionio,
   coinmarketcap,
   coinmarketcap,
   deluge,
   deluge,
-  diskstation,
+  diskstation: downloadstation,
+  downloadstation,
   emby,
   emby,
   flood,
   flood,
   gluetun,
   gluetun,
@@ -65,10 +73,15 @@ const widgets = {
   jellyseerr,
   jellyseerr,
   lidarr,
   lidarr,
   mastodon,
   mastodon,
+  miniflux,
+  mikrotik,
   navidrome,
   navidrome,
+  nextdns,
   npm,
   npm,
   nzbget,
   nzbget,
+  omada,
   ombi,
   ombi,
+  opnsense,
   overseerr,
   overseerr,
   paperlessngx,
   paperlessngx,
   pihole,
   pihole,
@@ -87,6 +100,7 @@ const widgets = {
   speedtest,
   speedtest,
   strelaysrv,
   strelaysrv,
   tautulli,
   tautulli,
+  tdarr,
   traefik,
   traefik,
   transmission,
   transmission,
   tubearchivist,
   tubearchivist,
@@ -94,6 +108,7 @@ const widgets = {
   unifi,
   unifi,
   unifi_console: unifi,
   unifi_console: unifi,
   watchtower,
   watchtower,
+  xteve,
 };
 };
 
 
 export default widgets;
 export default widgets;

+ 35 - 0
src/widgets/xteve/component.jsx

@@ -0,0 +1,35 @@
+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: xteveData, error: xteveError } = useWidgetAPI(widget, "api");
+
+  if (xteveError) {
+    return <Container error={xteveError} />;
+  }
+
+  if (!xteveData) {
+    return (
+      <Container service={service}>
+        <Block label="xteve.streams_all" />
+        <Block label="xteve.streams_active " />
+        <Block label="xteve.streams_xepg" />
+      </Container>
+    );
+  }
+
+  return (
+    <Container service={service}>
+      <Block label="xteve.streams_all" value={t("common.number", { value: xteveData["streams.all"] ?? 0 })} />
+      <Block label="xteve.streams_active" value={t("common.number", { value: xteveData["streams.active"] ?? 0 })} />
+      <Block label="xteve.streams_xepg" value={t("common.number", { value: xteveData["streams.xepg"] ?? 0 })} />
+    </Container>
+  );
+}

+ 63 - 0
src/widgets/xteve/proxy.js

@@ -0,0 +1,63 @@
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
+import createLogger from "utils/logger";
+import widgets from "widgets/widgets";
+import getServiceWidget from "utils/config/service-helpers";
+
+const logger = createLogger("xteveProxyHandler");
+
+export default async function xteveProxyHandler(req, res) {
+  const { group, service, endpoint } = req.query;
+
+  if (!group || !service) {
+    return res.status(400).json({ error: "Invalid proxy service type" });
+  }
+
+  const widget = await getServiceWidget(group, service);
+  const api = widgets?.[widget.type]?.api;
+  if (!api) {
+    return res.status(403).json({ error: "Service does not support API calls" });
+  }
+
+  const url = formatApiCall(api, { endpoint, ...widget });
+  const method = "POST";
+  const payload = { cmd: "status" };
+
+  if (widget.username && widget.password) {
+    // eslint-disable-next-line no-unused-vars
+     const [status, contentType, data] = await httpProxy(url, {
+      method,
+      body: JSON.stringify({
+        cmd: "login",
+        username: widget.username,
+        password: widget.password,
+      })
+    });
+
+    if (status !== 200) {
+      logger.debug("Error logging into xteve", status, url);
+      return res.status(status).json({error: {message: `HTTP Error ${status} logging into xteve`, url, data}});
+    }
+
+    const json = JSON.parse(data.toString());
+
+    if (json?.status !== true) {
+      return res.status(401).json({error: {message: "Authentication failed", url, data}});
+    }
+
+    payload.token = json.token;
+  }
+
+  const [status, contentType, data] = await httpProxy(url, {
+    method,
+    body: JSON.stringify(payload)
+  });
+
+  if (status !== 200) {
+    logger.debug("Error %d calling xteve endpoint %s", status, url);
+    return res.status(status).json({error: {message: `HTTP Error ${status}`, url, data}});
+  }
+
+  if (contentType) res.setHeader("Content-Type", contentType);
+  return res.status(status).send(data);
+}

+ 14 - 0
src/widgets/xteve/widget.js

@@ -0,0 +1,14 @@
+import xteveProxyHandler from "./proxy";
+
+const widget = {
+  api: "{url}/{endpoint}",
+  proxyHandler: xteveProxyHandler,
+
+  mappings: {
+    "api": {
+      endpoint: "api/",
+    },
+  },
+};
+
+export default widget;