diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2972922b0..8ab44df5b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -68,9 +68,20 @@ body: id: browser-logs attributes: 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 id: other attributes: 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 diff --git a/.gitignore b/.gitignore index 9bd31081c..5649354a5 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,7 @@ next-env.d.ts # homepage /config -# idea -.idea/ +# IDEs +/.idea/ + + diff --git a/README.md b/README.md index 6409c2d23..ec2e1e682 100644 --- a/README.md +++ b/README.md @@ -45,15 +45,17 @@ - Container status (Running / Stopped) & statistics (CPU, Memory, Network) - Automatic service discovery (via labels) - 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 - - Coin Market Cap, Mastodon + - Coin Market Cap, Mastodon and more - Information & Utility Widgets - System Stats (Disk, CPU, Memory) - 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 - 21 theme colors with light and dark mode 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 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 @@ -117,7 +119,7 @@ pnpm start ## 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 the specific configuration options. diff --git a/next-i18next.config.js b/next-i18next.config.js index ee6eaaa1d..54aeebb4a 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -98,20 +98,21 @@ module.exports = { ); 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 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( - 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) => diff --git a/public/locales/ar/common.json b/public/locales/ar/common.json index cfe661e6e..853ad4c76 100644 --- a/public/locales/ar/common.json +++ b/public/locales/ar/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { - "leech": "Leech", - "seed": "Seed", + "flood": { "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", "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" } } diff --git a/public/locales/bg/common.json b/public/locales/bg/common.json index abbd3cca4..c30b4c3d9 100644 --- a/public/locales/bg/common.json +++ b/public/locales/bg/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { - "seed": "Seed", - "download": "Download", - "upload": "Upload", - "leech": "Leech" - }, "flood": { "leech": "Leech", "seed": "Seed", "download": "Download", "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" } } diff --git a/public/locales/ca/common.json b/public/locales/ca/common.json index dcf1ca70d..2b2fdd9e9 100644 --- a/public/locales/ca/common.json +++ b/public/locales/ca/common.json @@ -3,10 +3,10 @@ "missing_type": "Falta el tipus de widget: {{type}}", "api_error": "Error d'API", "status": "Estat", - "information": "Information", + "information": "Informació", "url": "URL", - "raw_error": "Raw Error", - "response_data": "Response Data" + "raw_error": "Error sense processar", + "response_data": "Dades de resposta" }, "weather": { "allow": "Feu clic per permetre", @@ -20,8 +20,8 @@ "transmission": { "seed": "Llavors", "download": "Descàrrega", - "upload": "Càrrega", - "leech": "Companys" + "upload": "Pujada", + "leech": "Company" }, "sonarr": { "wanted": "Volgut", @@ -30,13 +30,13 @@ }, "speedtest": { "ping": "Ping", - "upload": "Càrrega", + "upload": "Pujada", "download": "Descàrrega" }, "resources": { "total": "Total", "free": "Lliure", - "used": "Usat", + "used": "Utilitzat", "load": "Càrrega", "cpu": "CPU" }, @@ -47,13 +47,13 @@ "cpu": "Processador", "offline": "Fora de línia", "error": "Error", - "unknown": "Unknown" + "unknown": "Desconegut" }, "emby": { "playing": "Reproduint", "transcoding": "Transcodificant", "bitrate": "Taxa de bits", - "no_active": "Sense transmissions actives" + "no_active": "Sense reproduccions actives" }, "tautulli": { "playing": "Reproduint", @@ -73,14 +73,14 @@ }, "rutorrent": { "active": "Actiu", - "upload": "Càrrega", + "upload": "Pujada", "download": "Descàrrega" }, "radarr": { "wanted": "Volgut", "queued": "En cua", "movies": "Pel·lícules", - "missing": "Missing" + "missing": "Faltant" }, "readarr": { "wanted": "Volgut", @@ -101,7 +101,7 @@ "pending": "Pendent", "approved": "Aprovat", "available": "Disponible", - "processing": "Processing" + "processing": "Processant" }, "pihole": { "queries": "Consultes", @@ -163,8 +163,8 @@ }, "qbittorrent": { "download": "Descàrrega", - "upload": "Càrrega", - "leech": "Companys", + "upload": "Pujada", + "leech": "Company", "seed": "Llavors" }, "mastodon": { @@ -184,26 +184,26 @@ "failedLoginsLast24H": "Errors d'inici de sessió (24h)" }, "proxmox": { - "vms": "VMs", + "vms": "Màquines Virtuals", "mem": "Memòria", "cpu": "Processador", "lxc": "LXC" }, "unifi": { "users": "Usuaris", - "uptime": "System Uptime", - "days": "Días", + "uptime": "Temps actiu", + "days": "Dies", "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", "lan": "LAN", "wlan": "WLAN", - "devices": "Devices", - "lan_devices": "LAN Devices", - "wlan_devices": "WLAN Devices" + "devices": "Dispositius", + "lan_devices": "Dispositius LAN", + "wlan_devices": "Dispositius WLAN" }, "plex": { "streams": "Transmissions actives", @@ -216,119 +216,119 @@ "wait": "Si us plau, espereu" }, "changedetectionio": { - "totalObserved": "Total Observed", - "diffsDetected": "Diffs Detected" + "totalObserved": "Total d'observats", + "diffsDetected": "Diferències detectades" }, "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": { - "bookmark": "Bookmark", - "service": "Service" + "bookmark": "Marcador", + "service": "Servei" }, "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_status": "{{ok}}/{{total}}" }, "autobrr": { - "approvedPushes": "Approved", - "rejectedPushes": "Rejected", - "filters": "Filters", - "indexers": "Indexers" + "approvedPushes": "Aprovat", + "rejectedPushes": "Rebutjat", + "filters": "Filtres", + "indexers": "Indexadors" }, "watchtower": { - "containers_scanned": "Scanned", - "containers_updated": "Updated", - "containers_failed": "Failed" + "containers_scanned": "Escanejat", + "containers_updated": "Actualitzat", + "containers_failed": "Error" }, "tubearchivist": { - "downloads": "Queue", - "videos": "Videos", - "channels": "Channels", - "playlists": "Playlists" + "downloads": "Cua", + "videos": "Vídeos", + "channels": "Canals", + "playlists": "Llistes de reproducció" }, "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;)}}" }, "navidrome": { - "nothing_streaming": "No Active Streams", - "please_wait": "Please Wait" + "nothing_streaming": "Cap reproducció activa", + "please_wait": "Espereu si us plau" }, "pyload": { - "speed": "Speed", - "active": "Active", - "queue": "Queue", + "speed": "Velocitat", + "active": "Actiu", + "queue": "Cua", "total": "Total" }, "gluetun": { - "public_ip": "Public IP", - "region": "Region", - "country": "Country" + "public_ip": "IP Pública", + "region": "Regió", + "country": "País" }, "hdhomerun": { - "channels": "Channels", + "channels": "Canals", "hd": "HD" }, "ping": { @@ -336,30 +336,72 @@ "ping": "Ping" }, "scrutiny": { - "passed": "Passed", - "failed": "Failed", - "unknown": "Unknown" + "passed": "Aprobat", + "failed": "Error", + "unknown": "Desconegut" }, "paperlessngx": { - "inbox": "Inbox", + "inbox": "Safata d'entrada", "total": "Total" }, "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": { + "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", "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" } } diff --git a/public/locales/cs/common.json b/public/locales/cs/common.json index 66da71bd7..170c4a217 100644 --- a/public/locales/cs/common.json +++ b/public/locales/cs/common.json @@ -350,16 +350,58 @@ "seed": "Seed", "download": "Download" }, - "diskstation": { + "flood": { "download": "Download", "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", "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" } } diff --git a/public/locales/da/common.json b/public/locales/da/common.json index fae5a68c5..c8845105f 100644 --- a/public/locales/da/common.json +++ b/public/locales/da/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { + "leech": "Leech", "download": "Download", "upload": "Upload", - "leech": "Leech", "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", "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" } } diff --git a/public/locales/de/common.json b/public/locales/de/common.json index 76da6f02b..e32ba18b1 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -6,7 +6,7 @@ "url": "URL", "information": "Information", "raw_error": "Raw Error", - "response_data": "Response Data" + "response_data": "Empfangene Daten" }, "search": { "placeholder": "Suche…" @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Download", "upload": "Upload", "leech": "Leech", "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", "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" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 80ba5357e..032c1dfef 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -3,9 +3,11 @@ "bytes": "{{value, bytes}}", "bits": "{{value, bytes(bits: 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)}}", + "bibitrate": "{{value, rate(bits: true; binary: true)}}", "percent": "{{value, percent}}", "number": "{{value, number}}", "ms": "{{value, number}}" @@ -86,6 +88,13 @@ "bitrate": "Bitrate", "no_active": "No Active Streams" }, + "omada": { + "connectedAp": "Connected APs", + "activeUser": "Active devices", + "alerts": "Alerts", + "connectedGateway": "Connected gateways", + "connectedSwitches": "Connected switches" + }, "nzbget": { "rate": "Rate", "remaining": "Remaining", @@ -124,7 +133,7 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "downloadstation": { "download": "Download", "upload": "Upload", "leech": "Leech", @@ -192,6 +201,12 @@ "stopped": "Stopped", "total": "Total" }, + "tdarr": { + "queue": "Queue", + "processed": "Processed", + "errored": "Errored", + "saved": "Saved" + }, "traefik": { "routers": "Routers", "services": "Services", @@ -240,6 +255,10 @@ "status_count": "Posts", "domain_count": "Domains" }, + "miniflux": { + "read": "Read", + "unread": "Unread" + }, "authentik": { "users": "Users", "loginsLast24H": "Logins (24h)", @@ -372,5 +391,26 @@ "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" } -} \ No newline at end of file +} diff --git a/public/locales/eo/common.json b/public/locales/eo/common.json index c8a236763..a073234fa 100644 --- a/public/locales/eo/common.json +++ b/public/locales/eo/common.json @@ -350,16 +350,58 @@ "inbox": "Inbox", "total": "Totalo" }, - "diskstation": { + "flood": { "download": "Download", - "leech": "Leech", "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", "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" } } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 1eaceaa45..c0026c8f4 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Semilla" }, - "diskstation": { - "download": "Descargar", - "upload": "Cargar", - "leech": "Leech", - "seed": "Semilla" - }, "flood": { "download": "Descargar", "upload": "Subir", "leech": "Leech", "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" } } diff --git a/public/locales/fi/common.json b/public/locales/fi/common.json index 54ac45c42..f7871dac7 100644 --- a/public/locales/fi/common.json +++ b/public/locales/fi/common.json @@ -350,16 +350,58 @@ "seed": "Seed", "download": "Download" }, - "diskstation": { + "flood": { "download": "Download", "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", + "connectedSwitches": "Connected switches", + "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" } } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index 34a15897f..ab46de39d 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { - "download": "Réception", + "flood": { + "download": "Récep.", "upload": "Envoi", "leech": "Leech", "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.", "upload": "Envoi", "leech": "Leech", "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." } } diff --git a/public/locales/he/common.json b/public/locales/he/common.json index 53439ae0e..e211074c9 100644 --- a/public/locales/he/common.json +++ b/public/locales/he/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { + "download": "Download", "upload": "Upload", "leech": "Leech", - "download": "Download", "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", "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" } } diff --git a/public/locales/hi/common.json b/public/locales/hi/common.json index e2f5091b9..098362eef 100644 --- a/public/locales/hi/common.json +++ b/public/locales/hi/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Download", "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", "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" } } diff --git a/public/locales/hr/common.json b/public/locales/hr/common.json index 58539904d..b555aa0b0 100644 --- a/public/locales/hr/common.json +++ b/public/locales/hr/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Preuzimanje", "upload": "Prijenos", "leech": "Leech", "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", "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" } } diff --git a/public/locales/hu/common.json b/public/locales/hu/common.json index 6ad93d4a0..0d77fd4c7 100644 --- a/public/locales/hu/common.json +++ b/public/locales/hu/common.json @@ -350,16 +350,58 @@ "upload": "Upload", "leech": "Leech" }, - "diskstation": { + "flood": { "download": "Download", "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": { + "activeUser": "Active devices", + "alerts": "Alerts", + "connectedAp": "Connected APs", + "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" } } diff --git a/public/locales/it/common.json b/public/locales/it/common.json index 169e42bdc..466b2655e 100644 --- a/public/locales/it/common.json +++ b/public/locales/it/common.json @@ -5,18 +5,18 @@ "cpu": "CPU", "offline": "Offline", "rx": "RX", - "error": "Error", - "unknown": "Unknown" + "error": "Errore", + "unknown": "Sconosciuto" }, "emby": { "playing": "In riproduzione", - "transcoding": "Transcoding", + "transcoding": "Transcodifica", "bitrate": "Bitrate", "no_active": "Nessuno Stream Attivo" }, "tautulli": { "playing": "In riproduzione", - "transcoding": "Transcoding", + "transcoding": "Transcodifica", "bitrate": "Bitrate", "no_active": "Nessuno Stream Attivo" }, @@ -31,7 +31,7 @@ "total": "Totali" }, "traefik": { - "routers": "Routers", + "routers": "Router", "services": "Servizi", "middleware": "Middleware" }, @@ -40,9 +40,9 @@ "api_error": "Errore API", "status": "Stato", "url": "URL", - "information": "Information", + "information": "Informazione", "raw_error": "Raw Error", - "response_data": "Response Data" + "response_data": "Dati risposta" }, "search": { "placeholder": "Cerca…" @@ -105,7 +105,7 @@ "pending": "In attesa", "approved": "Approvati", "available": "Disponibili", - "processing": "Processing" + "processing": "In lavorazione" }, "sabnzbd": { "rate": "Rapporto", @@ -126,13 +126,13 @@ }, "gotify": { "apps": "Applicazioni", - "clients": "Clients", + "clients": "Client", "messages": "Messaggi" }, "prowlarr": { "enableIndexers": "Indicizzatori", "numberOfGrabs": "Grabs", - "numberOfQueries": "Queries", + "numberOfQueries": "Interrogazioni", "numberOfFailGrabs": "Grabs Falliti", "numberOfFailQueries": "Queries Fallite" }, @@ -153,10 +153,10 @@ "lidarr": { "wanted": "Mancanti", "queued": "In coda", - "albums": "Albums" + "albums": "Album" }, "adguard": { - "queries": "Queries", + "queries": "Interrogazioni", "blocked": "Bloccati", "filtered": "Filtrati", "latency": "Latenza" @@ -304,12 +304,12 @@ "downloads": "Coda", "videos": "Video", "channels": "Canali", - "playlists": "Playlists" + "playlists": "Playlist" }, "truenas": { "load": "Carico di Sistema", - "uptime": "Uptime", - "alerts": "Alerts", + "uptime": "Tempo di attività", + "alerts": "Avvisi", "time": "{{value, number(style: unit; unitDisplay: long;)}}" }, "navidrome": { @@ -323,26 +323,26 @@ "total": "Totale" }, "gluetun": { - "public_ip": "Public IP", - "region": "Region", - "country": "Country" + "public_ip": "IP pubblico", + "region": "Località", + "country": "Stato" }, "hdhomerun": { - "channels": "Channels", + "channels": "Canali", "hd": "HD" }, "ping": { - "error": "Error", + "error": "Errore", "ping": "Ping" }, "scrutiny": { - "passed": "Passed", - "failed": "Failed", - "unknown": "Unknown" + "passed": "Passati", + "failed": "Falliti", + "unknown": "Sconosciuto" }, "paperlessngx": { - "inbox": "Inbox", - "total": "Total" + "inbox": "In arrivo", + "total": "Totali" }, "deluge": { "download": "Download", @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Download", "upload": "Upload", "leech": "Leech", "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", "upload": "Upload", "leech": "Leech", "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" } } diff --git a/public/locales/ms/common.json b/public/locales/ms/common.json index 5bb77f769..ed9e49032 100644 --- a/public/locales/ms/common.json +++ b/public/locales/ms/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { - "upload": "Upload", + "flood": { "download": "Download", + "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", "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" } } diff --git a/public/locales/nb-NO/common.json b/public/locales/nb-NO/common.json index 8a6d7965b..7f9b2c803 100644 --- a/public/locales/nb-NO/common.json +++ b/public/locales/nb-NO/common.json @@ -350,16 +350,58 @@ "upload": "Upload", "seed": "Seed" }, - "diskstation": { - "leech": "Leech", + "flood": { "download": "Download", "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", "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" } } diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json index 9a16f6d83..c5c9ea0b0 100644 --- a/public/locales/nl/common.json +++ b/public/locales/nl/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Download", "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", "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" } } diff --git a/public/locales/pl/common.json b/public/locales/pl/common.json index c4accc5aa..07ce69f2b 100644 --- a/public/locales/pl/common.json +++ b/public/locales/pl/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Pobieranie", "upload": "Wysyłanie", "leech": "Leech", "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", "upload": "Wysyłanie", "leech": "Leech", "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" } } diff --git a/public/locales/pt-BR/common.json b/public/locales/pt-BR/common.json index 0bfa8ecc1..68aacbf44 100644 --- a/public/locales/pt-BR/common.json +++ b/public/locales/pt-BR/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Download", "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", "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" } } diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json index 3cde6047e..92a388707 100644 --- a/public/locales/pt/common.json +++ b/public/locales/pt/common.json @@ -2,10 +2,10 @@ "widget": { "missing_type": "Widget ausente: {{type}}", "api_error": "Erro da API", - "status": "Status", + "status": "Estado", "information": "Informação", "url": "Endereço URL", - "raw_error": "Raw Error", + "raw_error": "Erro", "response_data": "Dados da Resposta" }, "search": { @@ -104,7 +104,9 @@ "byterate": "{{value, bytes}}", "ms": "{{value, number}}", "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": { "current": "Localização atual", @@ -207,9 +209,9 @@ "wan": "WAN", "lan_users": "Utilizadores LAN", "wlan_users": "Utilizadores WLAN", - "up": "UP", - "down": "DOWN", - "wait": "Por favor aguarde", + "up": "Ligados", + "down": "Desligados", + "wait": "Por favor, aguarde", "lan": "LAN", "wlan": "WLAN", "devices": "Dispositivos", @@ -224,32 +226,32 @@ "glances": { "cpu": "CPU", "mem": "MEM", - "wait": "Please wait" + "wait": "Por favor, aguarde" }, "changedetectionio": { "totalObserved": "Total Observado", "diffsDetected": "Diferenças Detetadas" }, "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", - "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-night": "Light Freezing Drizzle", "57-day": "Freezing Drizzle", @@ -289,8 +291,8 @@ "99-day": "Thunderstorm With Hail" }, "quicklaunch": { - "bookmark": "Bookmark", - "service": "Service" + "bookmark": "Marcador", + "service": "Serviço" }, "homebridge": { "available_update": "System", @@ -361,16 +363,54 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { - "download": "Download", - "upload": "Upload", - "leech": "Leech", - "seed": "Seed" - }, "flood": { "download": "Descarregar", "upload": "Carregar", "leech": "Leech", "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" } } diff --git a/public/locales/ro/common.json b/public/locales/ro/common.json index 6c2a635f2..0a1080e12 100644 --- a/public/locales/ro/common.json +++ b/public/locales/ro/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Download", "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", "upload": "Upload", "leech": "Leech", "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" } } diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 1b7688eac..6fb5d4f4c 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { - "download": "Download", - "upload": "Upload", - "seed": "Seed", - "leech": "Leech" - }, "flood": { "upload": "Upload", "download": "Download", "leech": "Leech", "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" } } diff --git a/public/locales/sr/common.json b/public/locales/sr/common.json index 6b03fa0f2..6e220e049 100644 --- a/public/locales/sr/common.json +++ b/public/locales/sr/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { - "download": "Download", - "upload": "Upload", - "leech": "Leech", - "seed": "Seed" - }, "flood": { "download": "Download", "seed": "Seed", "upload": "Upload", "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" } } diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json index e670291bd..d682d3688 100644 --- a/public/locales/sv/common.json +++ b/public/locales/sv/common.json @@ -350,16 +350,58 @@ "upload": "Upload", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Download", "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", "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" } } diff --git a/public/locales/te/common.json b/public/locales/te/common.json index 85effa8d5..f5c14b37a 100644 --- a/public/locales/te/common.json +++ b/public/locales/te/common.json @@ -350,16 +350,58 @@ "upload": "Upload", "leech": "Leech" }, - "diskstation": { - "leech": "Leech", + "flood": { "download": "Download", "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", "upload": "Upload", "leech": "Leech", "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" } } diff --git a/public/locales/tr/common.json b/public/locales/tr/common.json index 5312035f0..b4d093bb9 100644 --- a/public/locales/tr/common.json +++ b/public/locales/tr/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Download", "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", "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" } } diff --git a/public/locales/uk/common.json b/public/locales/uk/common.json new file mode 100644 index 000000000..fa4c12fb7 --- /dev/null +++ b/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" + } +} diff --git a/public/locales/vi/common.json b/public/locales/vi/common.json index e01e4fd4c..870e3fe56 100644 --- a/public/locales/vi/common.json +++ b/public/locales/vi/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { - "download": "Download", - "upload": "Upload", - "leech": "Leech", - "seed": "Seed" - }, "flood": { "download": "Download", "upload": "Upload", "seed": "Seed", "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" } } diff --git a/public/locales/yue/common.json b/public/locales/yue/common.json index 80e3a7881..1b3468273 100644 --- a/public/locales/yue/common.json +++ b/public/locales/yue/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Download", "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", "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" } } diff --git a/public/locales/zh-CN/common.json b/public/locales/zh-CN/common.json index 512fbcc11..cf4c4a3fd 100644 --- a/public/locales/zh-CN/common.json +++ b/public/locales/zh-CN/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { + "leech": "Leech", "download": "Download", "upload": "Upload", - "leech": "Leech", "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", "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" } } diff --git a/public/locales/zh-Hant/common.json b/public/locales/zh-Hant/common.json index f760d7953..fd00cccda 100644 --- a/public/locales/zh-Hant/common.json +++ b/public/locales/zh-Hant/common.json @@ -350,16 +350,58 @@ "leech": "Leech", "seed": "Seed" }, - "diskstation": { + "flood": { "download": "Download", "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", "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" } } diff --git a/src/components/resolvedicon.jsx b/src/components/resolvedicon.jsx index 2a3701e83..3dad2b0e2 100644 --- a/src/components/resolvedicon.jsx +++ b/src/components/resolvedicon.jsx @@ -1,9 +1,9 @@ import Image from "next/future/image"; -export default function ResolvedIcon({ icon }) { +export default function ResolvedIcon({ icon, width = 32, height = 32 }) { // direct or relative URLs if (icon.startsWith("http") || icon.startsWith("/")) { - return logo; + return logo; } // mdi- prefixed, material design icons @@ -12,8 +12,8 @@ export default function ResolvedIcon({ icon }) { return (
); diff --git a/src/components/version.jsx b/src/components/version.jsx index 9b1d8cdec..de79647ba 100644 --- a/src/components/version.jsx +++ b/src/components/version.jsx @@ -3,8 +3,6 @@ import useSWR from "swr"; import { compareVersions } from "compare-versions"; import { MdNewReleases } from "react-icons/md"; -import cachedFetch from "utils/proxy/cached-fetch"; - export default function Version() { 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 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 const formatDate = (date) => { @@ -48,7 +44,7 @@ export default function Version() { {version === "main" || version === "dev" || version === "nightly" ? null - : releaseData && + : releaseData && latestRelease && compareVersions(latestRelease.tag_name, version) > 0 && ( - - - - - - : + // fallback to homepage logo + + + + - + + + + - - - - + + + }
- ); + ) } diff --git a/src/components/widgets/resources/cpu.jsx b/src/components/widgets/resources/cpu.jsx index 6b021193b..0382270cf 100644 --- a/src/components/widgets/resources/cpu.jsx +++ b/src/components/widgets/resources/cpu.jsx @@ -38,7 +38,7 @@ export default function Cpu({ expanded }) {
{t("resources.load")}
)} - + ); diff --git a/src/components/widgets/resources/disk.jsx b/src/components/widgets/resources/disk.jsx index 69f560f62..bed7caef9 100644 --- a/src/components/widgets/resources/disk.jsx +++ b/src/components/widgets/resources/disk.jsx @@ -38,7 +38,7 @@ export default function Disk({ options, expanded }) {
{t("resources.total")}
)} - + ); diff --git a/src/components/widgets/resources/memory.jsx b/src/components/widgets/resources/memory.jsx index 2888f9070..068177df0 100644 --- a/src/components/widgets/resources/memory.jsx +++ b/src/components/widgets/resources/memory.jsx @@ -38,7 +38,7 @@ export default function Memory({ expanded }) {
{t("resources.total")}
)} - + ); diff --git a/src/pages/api/docker/stats/[...service].js b/src/pages/api/docker/stats/[...service].js index d214ffb28..82604f8c7 100644 --- a/src/pages/api/docker/stats/[...service].js +++ b/src/pages/api/docker/stats/[...service].js @@ -14,7 +14,8 @@ export default async function handler(req, res) { } try { - const docker = new Docker(getDockerArguments(containerServer)); + const dockerArgs = getDockerArguments(containerServer); + const docker = new Docker(dockerArgs.conn); const containers = await docker.listContainers({ all: true, }); @@ -31,18 +32,44 @@ export default async function handler(req, res) { const containerNames = containers.map((container) => container.Names[0].replace(/^\//, "")); 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; } - 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 { res.status(500).send({ diff --git a/src/pages/api/docker/status/[...service].js b/src/pages/api/docker/status/[...service].js index 2861a1e58..fa54e6f3b 100644 --- a/src/pages/api/docker/status/[...service].js +++ b/src/pages/api/docker/status/[...service].js @@ -13,7 +13,8 @@ export default async function handler(req, res) { } try { - const docker = new Docker(getDockerArguments(containerServer)); + const dockerArgs = getDockerArguments(containerServer); + const docker = new Docker(dockerArgs.conn); const containers = await docker.listContainers({ all: true, }); @@ -29,18 +30,43 @@ export default async function handler(req, res) { const containerNames = containers.map((container) => container.Names[0].replace(/^\//, "")); 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 { return res.status(500).send({ diff --git a/src/pages/api/releases.js b/src/pages/api/releases.js new file mode 100644 index 000000000..b5b3df009 --- /dev/null +++ b/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)); +} diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index 52dffd559..854bfe621 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -56,9 +56,12 @@ export async function servicesResponse() { try { discoveredDockerServices = cleanServiceGroups(await servicesFromDocker()); + if (discoveredDockerServices?.length === 0) { + console.debug("No containers were found with homepage labels."); + } } catch (e) { 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 = []; } @@ -66,7 +69,7 @@ export async function servicesResponse() { discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes()); } catch (e) { 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 = []; } @@ -74,7 +77,7 @@ export async function servicesResponse() { configuredServices = cleanServiceGroups(await servicesFromConfig()); } catch (e) { console.error("Failed to load services.yaml, please check for errors"); - if (e) console.error(e); + if (e) console.error(e.toString()); configuredServices = []; } @@ -82,7 +85,7 @@ export async function servicesResponse() { initialSettings = await getSettings(); } catch (e) { console.error("Failed to load settings.yaml, please check for errors"); - if (e) console.error(e); + if (e) console.error(e.toString()); initialSettings = {}; } diff --git a/src/utils/config/docker.js b/src/utils/config/docker.js index 9aef74086..7e1422e0f 100644 --- a/src/utils/config/docker.js +++ b/src/utils/config/docker.js @@ -22,11 +22,14 @@ export default function getDockerArguments(server) { if (servers[server]) { if (servers[server].socket) { - return { socketPath: servers[server].socket }; + return { conn: { socketPath: servers[server].socket }, swarm: !!servers[server].swarm }; } 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]; diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index d34c07f30..e83c76f17 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -49,36 +49,41 @@ export async function servicesFromDocker() { const serviceServers = await Promise.all( 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 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 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: [] }; + } }) ); diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index 38d09ccb0..5d34264df 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -7,7 +7,7 @@ import widgets from "widgets/widgets"; 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; if (group && service) { @@ -36,6 +36,8 @@ export default async function credentialedProxyHandler(req, res) { headers["X-API-Token"] = `${widget.key}`; } else if (widget.type === "tubearchivist") { headers.Authorization = `Token ${widget.key}`; + } else if (widget.type === "miniflux") { + headers["X-Auth-Token"] = `${widget.key}`; } else { headers["X-API-Key"] = `${widget.key}`; } @@ -47,6 +49,8 @@ export default async function credentialedProxyHandler(req, res) { headers, }); + let resultData = data; + if (status === 204 || status === 304) { 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}}); } + if (status === 200 && map) { + resultData = map(data); + } + if (contentType) res.setHeader("Content-Type", contentType); - return res.status(status).send(data); + return res.status(status).send(resultData); } } diff --git a/src/widgets/components.js b/src/widgets/components.js index aa5c8407f..86bc98bf6 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -8,7 +8,7 @@ const components = { changedetectionio: dynamic(() => import("./changedetectionio/component")), coinmarketcap: dynamic(() => import("./coinmarketcap/component")), deluge: dynamic(() => import("./deluge/component")), - diskstation: dynamic(() => import("./diskstation/component")), + downloadstation: dynamic(() => import("./downloadstation/component")), docker: dynamic(() => import("./docker/component")), kubernetes: dynamic(() => import("./kubernetes/component")), emby: dynamic(() => import("./emby/component")), @@ -22,10 +22,15 @@ const components = { jellyseerr: dynamic(() => import("./jellyseerr/component")), lidarr: dynamic(() => import("./lidarr/component")), mastodon: dynamic(() => import("./mastodon/component")), + miniflux: dynamic(() => import("./miniflux/component")), + mikrotik: dynamic(() => import("./mikrotik/component")), navidrome: dynamic(() => import("./navidrome/component")), + nextdns: dynamic(() => import("./nextdns/component")), npm: dynamic(() => import("./npm/component")), nzbget: dynamic(() => import("./nzbget/component")), + omada: dynamic(() => import("./omada/component")), ombi: dynamic(() => import("./ombi/component")), + opnsense: dynamic(() => import("./opnsense/component")), overseerr: dynamic(() => import("./overseerr/component")), paperlessngx: dynamic(() => import("./paperlessngx/component")), pihole: dynamic(() => import("./pihole/component")), @@ -44,12 +49,14 @@ const components = { speedtest: dynamic(() => import("./speedtest/component")), strelaysrv: dynamic(() => import("./strelaysrv/component")), tautulli: dynamic(() => import("./tautulli/component")), + tdarr: dynamic(() => import("./tdarr/component")), traefik: dynamic(() => import("./traefik/component")), transmission: dynamic(() => import("./transmission/component")), tubearchivist: dynamic(() => import("./tubearchivist/component")), truenas: dynamic(() => import("./truenas/component")), unifi: dynamic(() => import("./unifi/component")), watchtower: dynamic(() => import("./watchtower/component")), + xteve: dynamic(() => import("./xteve/component")), }; export default components; diff --git a/src/widgets/diskstation/component.jsx b/src/widgets/downloadstation/component.jsx similarity index 64% rename from src/widgets/diskstation/component.jsx rename to src/widgets/downloadstation/component.jsx index 3a87eebc3..a91d1891c 100644 --- a/src/widgets/diskstation/component.jsx +++ b/src/widgets/downloadstation/component.jsx @@ -17,10 +17,10 @@ export default function Component({ service }) { if (!tasks) { return ( - - - - + + + + ); } @@ -32,10 +32,10 @@ export default function Component({ service }) { return ( - - - - + + + + ); } diff --git a/src/widgets/diskstation/proxy.js b/src/widgets/downloadstation/proxy.js similarity index 92% rename from src/widgets/diskstation/proxy.js rename to src/widgets/downloadstation/proxy.js index af7678152..dc76a3cb9 100644 --- a/src/widgets/diskstation/proxy.js +++ b/src/widgets/downloadstation/proxy.js @@ -4,7 +4,7 @@ import createLogger from "utils/logger"; import widgets from "widgets/widgets"; 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" async function login(widget) { @@ -34,7 +34,7 @@ async function login(widget) { return [status, contentType, data]; } -export default async function diskstationProxyHandler(req, res) { +export default async function downloadstationProxyHandler(req, res) { const { group, service, endpoint } = req.query; if (!group || !service) { @@ -56,7 +56,7 @@ export default async function diskstationProxyHandler(req, res) { const json = JSON.parse(data.toString()); if (json?.success !== true) { - logger.debug("Logging in to DiskStation"); + logger.debug("Logging in to DownloadStation"); [status, contentType, data] = await login(widget); if (status !== 200) { return res.status(status).end(data) diff --git a/src/widgets/diskstation/widget.js b/src/widgets/downloadstation/widget.js similarity index 71% rename from src/widgets/diskstation/widget.js rename to src/widgets/downloadstation/widget.js index 71187425d..38245adfd 100644 --- a/src/widgets/diskstation/widget.js +++ b/src/widgets/downloadstation/widget.js @@ -1,8 +1,8 @@ -import diskstationProxyHandler from "./proxy"; +import downloadstationProxyHandler from "./proxy"; const widget = { api: "{url}/webapi/DownloadStation/task.cgi?api=SYNO.DownloadStation.Task&version=1&method={endpoint}", - proxyHandler: diskstationProxyHandler, + proxyHandler: downloadstationProxyHandler, mappings: { "list": { diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js index 3f81051f2..0ed6d8591 100644 --- a/src/widgets/homebridge/proxy.js +++ b/src/widgets/homebridge/proxy.js @@ -10,7 +10,7 @@ const proxyName = "homebridgeProxyHandler"; const sessionTokenCacheKey = `${proxyName}__sessionToken`; const logger = createLogger(proxyName); -async function login(widget) { +async function login(widget, service) { const endpoint = "auth/login"; const api = widgets?.[widget.type]?.api const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget })); @@ -26,7 +26,7 @@ async function login(widget) { try { 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 }; } catch (e) { logger.error("Unable to login to Homebridge API: %s", e); @@ -35,10 +35,11 @@ async function login(widget) { return { accessToken: false }; } -async function apiCall(widget, endpoint) { +async function apiCall(widget, endpoint, service) { + const key = `${sessionTokenCacheKey}.${service}`; const headers = { "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 })); @@ -51,7 +52,7 @@ async function apiCall(widget, endpoint) { if (status === 401) { 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}`; // 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" }); } - 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({ status: statusData?.status, diff --git a/src/widgets/mikrotik/component.jsx b/src/widgets/mikrotik/component.jsx new file mode 100644 index 000000000..37a8f7064 --- /dev/null +++ b/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 ; + } + + if (!statsData || !leasesData) { + return ( + + + + + + + ); + } + + const memoryUsed = 100 - (statsData['free-memory'] / statsData['total-memory'])*100 + + const numberOfLeases = leasesData.length + + return ( + + + + + + + ); +} diff --git a/src/widgets/mikrotik/widget.js b/src/widgets/mikrotik/widget.js new file mode 100644 index 000000000..dfb5f626c --- /dev/null +++ b/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; diff --git a/src/widgets/miniflux/component.jsx b/src/widgets/miniflux/component.jsx new file mode 100644 index 000000000..dbfd60483 --- /dev/null +++ b/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 ; + } + + if (!minifluxData) { + return ( + + + + + ); + } + + return ( + + + + + ); +} diff --git a/src/widgets/miniflux/widget.js b/src/widgets/miniflux/widget.js new file mode 100644 index 000000000..d4efbe2f6 --- /dev/null +++ b/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; diff --git a/src/widgets/nextdns/component.jsx b/src/widgets/nextdns/component.jsx new file mode 100644 index 000000000..ae2397701 --- /dev/null +++ b/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 ; + } + + if (!nextdnsData) { + return ( + + + + ); + } + + if (!nextdnsData?.data?.length) { + return ( + + + + ); + } + + return ( + + {nextdnsData.data.map(d => )} + + ); +} diff --git a/src/widgets/nextdns/widget.js b/src/widgets/nextdns/widget.js new file mode 100644 index 000000000..012ef0298 --- /dev/null +++ b/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; diff --git a/src/widgets/npm/proxy.js b/src/widgets/npm/proxy.js index d6499e58c..837a77437 100644 --- a/src/widgets/npm/proxy.js +++ b/src/widgets/npm/proxy.js @@ -10,7 +10,7 @@ const proxyName = "npmProxyHandler"; const tokenCacheKey = `${proxyName}__token`; const logger = createLogger(proxyName); -async function login(loginUrl, username, password) { +async function login(loginUrl, username, password, service) { const authResponse = await httpProxy(loginUrl, { method: "POST", body: JSON.stringify({ identity: username, secret: password }), @@ -27,7 +27,7 @@ async function login(loginUrl, username, password) { if (status === 200) { 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) { logger.error(`Error ${status} logging into npm`, authResponse[2]); @@ -53,9 +53,9 @@ export default async function npmProxyHandler(req, res) { let contentType; let data; - let token = cache.get(tokenCacheKey); + let token = cache.get(`${tokenCacheKey}.${service}`); if (!token) { - [status, token] = await login(loginUrl, widget.username, widget.password); + [status, token] = await login(loginUrl, widget.username, widget.password, service); if (status !== 200) { logger.debug(`HTTTP ${status} logging into npm api: ${token}`); return res.status(status).send(token); @@ -72,8 +72,8 @@ export default async function npmProxyHandler(req, res) { if (status === 403) { 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) { logger.debug(`HTTTP ${status} logging into npm api: ${data}`); diff --git a/src/widgets/omada/component.jsx b/src/widgets/omada/component.jsx new file mode 100644 index 000000000..dee60842f --- /dev/null +++ b/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 ; + } + + if (!omadaData) { + return ( + + + + + + ); + } + + return ( + + + + + { omadaData.connectedGateways > 0 && } + { omadaData.connectedSwitches > 0 && } + + ); +} diff --git a/src/widgets/omada/proxy.js b/src/widgets/omada/proxy.js new file mode 100644 index 000000000..6d476f045 --- /dev/null +++ b/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}¤tPage=1¤tPageSize=1000`; + break; + case 5: + sitesUrl = `${url}/${cId}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=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}¤tPage=1¤tPageSize=1000` : + `${url}/${cId}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}¤tPage=1¤tPageSize=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}¤tPage=1¤tPageSize=1000` : + `${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=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" }); +} diff --git a/src/widgets/omada/widget.js b/src/widgets/omada/widget.js new file mode 100644 index 000000000..5e32edad7 --- /dev/null +++ b/src/widgets/omada/widget.js @@ -0,0 +1,7 @@ +import omadaProxyHandler from "./proxy"; + +const widget = { + proxyHandler: omadaProxyHandler, +}; + +export default widget; diff --git a/src/widgets/opnsense/component.jsx b/src/widgets/opnsense/component.jsx new file mode 100644 index 000000000..53396b31e --- /dev/null +++ b/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 ; + } + + if (!activityData || !interfaceData) { + return ( + + + + + + + ); + } + + + 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 ( + + + + + + + + ); +} diff --git a/src/widgets/opnsense/widget.js b/src/widgets/opnsense/widget.js new file mode 100644 index 000000000..a144ee4cc --- /dev/null +++ b/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; diff --git a/src/widgets/overseerr/component.jsx b/src/widgets/overseerr/component.jsx index 230a166ce..dcb5237cc 100644 --- a/src/widgets/overseerr/component.jsx +++ b/src/widgets/overseerr/component.jsx @@ -1,8 +1,11 @@ +import { useTranslation } from "next-i18next"; + import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { + const { t } = useTranslation(); const { widget } = service; const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count"); @@ -24,10 +27,10 @@ export default function Component({ service }) { return ( - - - - + + + + ); } diff --git a/src/widgets/pihole/component.jsx b/src/widgets/pihole/component.jsx index f213ac6db..c39250d23 100644 --- a/src/widgets/pihole/component.jsx +++ b/src/widgets/pihole/component.jsx @@ -9,7 +9,7 @@ export default function Component({ service }) { const { widget } = service; - const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "api.php"); + const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "summaryRaw"); if (piholeError) { return ; @@ -27,9 +27,9 @@ export default function Component({ service }) { return ( - - - + + + ); } diff --git a/src/widgets/pihole/widget.js b/src/widgets/pihole/widget.js index b392cdede..e75938101 100644 --- a/src/widgets/pihole/widget.js +++ b/src/widgets/pihole/widget.js @@ -1,12 +1,12 @@ import genericProxyHandler from "utils/proxy/handlers/generic"; const widget = { - api: "{url}/admin/{endpoint}", + api: "{url}/admin/api.php?{endpoint}&auth={key}", proxyHandler: genericProxyHandler, mappings: { - "api.php": { - endpoint: "api.php", + "summaryRaw": { + endpoint: "summaryRaw", validate: [ "dns_queries_today", "ads_blocked_today", diff --git a/src/widgets/plex/proxy.js b/src/widgets/plex/proxy.js index 36b7a268e..135b2b081 100644 --- a/src/widgets/plex/proxy.js +++ b/src/widgets/plex/proxy.js @@ -58,6 +58,9 @@ async function fetchFromPlexAPI(endpoint, widget) { export default async function plexProxyHandler(req, res) { const widget = await getWidget(req); + + const { service } = req.query; + if (!widget) { 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; } - let libraries = cache.get(librariesCacheKey); + let libraries = cache.get(`${librariesCacheKey}.${service}`); if (libraries === null) { logger.debug("Getting libraries from Plex API"); [status, apiData] = await fetchFromPlexAPI("/library/sections", widget); if (apiData && apiData.MediaContainer) { - libraries = apiData.MediaContainer.Directory; - cache.put(librariesCacheKey, libraries, 1000 * 60 * 60 * 6); + 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) { movies = 0; tv = 0; logger.debug("Getting movie + tv counts from Plex API"); - libraries.filter(l => ["movie", "show"].includes(l._attributes.type)).forEach(async (library) => { + 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); if (apiData && apiData.MediaContainer) { const size = parseInt(apiData.MediaContainer._attributes.size, 10); @@ -100,9 +104,9 @@ export default async function plexProxyHandler(req, res) { 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 = { diff --git a/src/widgets/prowlarr/component.jsx b/src/widgets/prowlarr/component.jsx index 890679e52..c5840e24e 100644 --- a/src/widgets/prowlarr/component.jsx +++ b/src/widgets/prowlarr/component.jsx @@ -1,8 +1,11 @@ +import { useTranslation } from "react-i18next"; + import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { + const { t } = useTranslation(); const { widget } = service; const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexer"); @@ -40,11 +43,11 @@ export default function Component({ service }) { return ( - - - - - + + + + + ); } diff --git a/src/widgets/pyload/proxy.js b/src/widgets/pyload/proxy.js index 4a866d9cf..9cee5fb26 100644 --- a/src/widgets/pyload/proxy.js +++ b/src/widgets/pyload/proxy.js @@ -11,7 +11,7 @@ const logger = createLogger(proxyName); const sessionCacheKey = `${proxyName}__sessionId`; const isNgCacheKey = `${proxyName}__isNg`; -async function fetchFromPyloadAPI(url, sessionId, params) { +async function fetchFromPyloadAPI(url, sessionId, params, service) { const options = { body: params ? Object.keys(params) @@ -25,10 +25,10 @@ async function fetchFromPyloadAPI(url, sessionId, params) { }; // see https://github.com/benphelps/homepage/issues/517 - const isNg = cache.get(isNgCacheKey); + const isNg = cache.get(`${isNgCacheKey}.${service}`); if (isNg && !params) { delete options.body; - options.headers.Cookie = cache.get(sessionCacheKey); + options.headers.Cookie = cache.get(`${sessionCacheKey}.${service}`); } // eslint-disable-next-line no-unused-vars @@ -43,19 +43,19 @@ async function fetchFromPyloadAPI(url, sessionId, params) { 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 if (status !== 200 || sessionId === false) { logger.error(`HTTP ${status} logging into Pyload API, returned: ${JSON.stringify(sessionId)}`); } else if (responseHeaders['set-cookie']?.join().includes('pyload_session')) { // 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]; - cache.put(sessionCacheKey, sessionCookie, 60 * 60 * 23 * 1000); // cache for 23h + cache.put(`${sessionCacheKey}.${service}`, sessionCookie, 60 * 60 * 23 * 1000); // cache for 23h } else { - cache.put(sessionCacheKey, sessionId); + cache.put(`${sessionCacheKey}.${service}`, 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 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) { 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) { 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) { - 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) { 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' }); diff --git a/src/widgets/qbittorrent/component.jsx b/src/widgets/qbittorrent/component.jsx index 3abc933da..d3836a6ba 100644 --- a/src/widgets/qbittorrent/component.jsx +++ b/src/widgets/qbittorrent/component.jsx @@ -44,9 +44,9 @@ export default function Component({ service }) { return ( - + - + ); } diff --git a/src/widgets/radarr/component.jsx b/src/widgets/radarr/component.jsx index f2620b789..4d8eeee85 100644 --- a/src/widgets/radarr/component.jsx +++ b/src/widgets/radarr/component.jsx @@ -1,8 +1,11 @@ +import { useTranslation } from "next-i18next"; + import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { + const { t } = useTranslation(); const { widget } = service; const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movie"); @@ -26,10 +29,10 @@ export default function Component({ service }) { return ( - - - - + + + + ); } diff --git a/src/widgets/sonarr/component.jsx b/src/widgets/sonarr/component.jsx index 14dd33287..fd6ba9dc2 100644 --- a/src/widgets/sonarr/component.jsx +++ b/src/widgets/sonarr/component.jsx @@ -1,8 +1,11 @@ +import { useTranslation } from "next-i18next"; + import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { + const { t } = useTranslation(); const { widget } = service; const { data: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing"); @@ -26,9 +29,9 @@ export default function Component({ service }) { return ( - - - + + + ); } diff --git a/src/widgets/speedtest/component.jsx b/src/widgets/speedtest/component.jsx index 3067dbf3d..7d722fafe 100644 --- a/src/widgets/speedtest/component.jsx +++ b/src/widgets/speedtest/component.jsx @@ -29,9 +29,9 @@ export default function Component({ service }) { - + ; + } + + if (!tdarrData) { + return ( + + + + + + + ); + } + + 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 ( + + + + + + + ); +} diff --git a/src/widgets/tdarr/proxy.js b/src/widgets/tdarr/proxy.js new file mode 100644 index 000000000..7f7dd803b --- /dev/null +++ b/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); +} diff --git a/src/widgets/tdarr/widget.js b/src/widgets/tdarr/widget.js new file mode 100644 index 000000000..f26713f21 --- /dev/null +++ b/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; diff --git a/src/widgets/transmission/proxy.js b/src/widgets/transmission/proxy.js index cdc1e9c92..83ca141e3 100644 --- a/src/widgets/transmission/proxy.js +++ b/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" }); } - let headers = cache.get(headerCacheKey); + let headers = cache.get(`${headerCacheKey}.${service}`); if (!headers) { headers = { "content-type": "application/json", } - cache.put(headerCacheKey, headers); + cache.put(`${headerCacheKey}.${service}`, headers); } 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) { logger.debug("Transmission is rejecting the request, but returning a CSRF token"); headers[csrfHeaderName] = responseHeaders[csrfHeaderName]; - cache.put(headerCacheKey, headers); + cache.put(`${headerCacheKey}.${service}`, headers); // retry the request, now with the CSRF token [status, contentType, data, responseHeaders] = await httpProxy(url, { diff --git a/src/widgets/unifi/proxy.js b/src/widgets/unifi/proxy.js index abb5986f4..9fbeafded 100644 --- a/src/widgets/unifi/proxy.js +++ b/src/widgets/unifi/proxy.js @@ -58,6 +58,7 @@ async function login(widget) { export default async function unifiProxyHandler(req, res) { const widget = await getWidget(req); + const { service } = req.query; if (!widget) { 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 prefix = cache.get(prefixCacheKey); + let prefix = cache.get(`${prefixCacheKey}.${service}`); if (prefix === null) { // auto detect if we're talking to a UDM Pro, and cache the result so that we // don't make two requests each time data from Unifi is required @@ -77,7 +78,7 @@ export default async function unifiProxyHandler(req, res) { if (responseHeaders?.["x-csrf-token"]) { prefix = udmpPrefix; } - cache.put(prefixCacheKey, prefix); + cache.put(`${prefixCacheKey}.${service}`, prefix); } widget.prefix = prefix; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index b3eaa8855..3e73e55f3 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -5,7 +5,7 @@ import bazarr from "./bazarr/widget"; import changedetectionio from "./changedetectionio/widget"; import coinmarketcap from "./coinmarketcap/widget"; import deluge from "./deluge/widget"; -import diskstation from "./diskstation/widget"; +import downloadstation from "./downloadstation/widget"; import emby from "./emby/widget"; import flood from "./flood/widget"; import gluetun from "./gluetun/widget"; @@ -16,10 +16,15 @@ import jackett from "./jackett/widget"; import jellyseerr from "./jellyseerr/widget"; import lidarr from "./lidarr/widget"; import mastodon from "./mastodon/widget"; +import miniflux from "./miniflux/widget"; +import mikrotik from "./mikrotik/widget"; import navidrome from "./navidrome/widget"; +import nextdns from "./nextdns/widget"; import npm from "./npm/widget"; import nzbget from "./nzbget/widget"; +import omada from "./omada/widget"; import ombi from "./ombi/widget"; +import opnsense from "./opnsense/widget"; import overseerr from "./overseerr/widget"; import paperlessngx from "./paperlessngx/widget"; import pihole from "./pihole/widget"; @@ -38,12 +43,14 @@ import sonarr from "./sonarr/widget"; import speedtest from "./speedtest/widget"; import strelaysrv from "./strelaysrv/widget"; import tautulli from "./tautulli/widget"; +import tdarr from "./tdarr/widget"; import traefik from "./traefik/widget"; import transmission from "./transmission/widget"; import tubearchivist from "./tubearchivist/widget"; import truenas from "./truenas/widget"; import unifi from "./unifi/widget"; import watchtower from './watchtower/widget' +import xteve from './xteve/widget' const widgets = { adguard, @@ -53,7 +60,8 @@ const widgets = { changedetectionio, coinmarketcap, deluge, - diskstation, + diskstation: downloadstation, + downloadstation, emby, flood, gluetun, @@ -65,10 +73,15 @@ const widgets = { jellyseerr, lidarr, mastodon, + miniflux, + mikrotik, navidrome, + nextdns, npm, nzbget, + omada, ombi, + opnsense, overseerr, paperlessngx, pihole, @@ -87,6 +100,7 @@ const widgets = { speedtest, strelaysrv, tautulli, + tdarr, traefik, transmission, tubearchivist, @@ -94,6 +108,7 @@ const widgets = { unifi, unifi_console: unifi, watchtower, + xteve, }; export default widgets; diff --git a/src/widgets/xteve/component.jsx b/src/widgets/xteve/component.jsx new file mode 100644 index 000000000..9d22e8a18 --- /dev/null +++ b/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 ; + } + + if (!xteveData) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +} diff --git a/src/widgets/xteve/proxy.js b/src/widgets/xteve/proxy.js new file mode 100644 index 000000000..ac1b49dc8 --- /dev/null +++ b/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); +} diff --git a/src/widgets/xteve/widget.js b/src/widgets/xteve/widget.js new file mode 100644 index 000000000..59b6e3fab --- /dev/null +++ b/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;