mirror of
				https://github.com/karl0ss/homepage.git
				synced 2025-11-04 00:10:57 +00:00 
			
		
		
		
	Merge branch 'main' into kubernetes
This commit is contained in:
		
						commit
						174cb651b4
					
				
							
								
								
									
										6
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
ARG VARIANT="16-buster"
 | 
			
		||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:${VARIANT}
 | 
			
		||||
 | 
			
		||||
RUN npm install -g pnpm
 | 
			
		||||
 | 
			
		||||
ENV PATH="${PATH}:./node_modules/.bin"
 | 
			
		||||
							
								
								
									
										27
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "homepage",
 | 
			
		||||
	"build": {
 | 
			
		||||
		"dockerfile": "Dockerfile",
 | 
			
		||||
		"args": {
 | 
			
		||||
			"VARIANT": "18-buster"
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"customizations": {
 | 
			
		||||
		"vscode": {
 | 
			
		||||
			"extensions": [
 | 
			
		||||
				"dbaeumer.vscode-eslint",
 | 
			
		||||
				"mhutchie.git-graph",
 | 
			
		||||
				"streetsidesoftware.code-spell-checker",
 | 
			
		||||
			],
 | 
			
		||||
			"settings": {
 | 
			
		||||
				"eslint.format.enable": true,
 | 
			
		||||
				"eslint.lintTask.enable": true,
 | 
			
		||||
				"eslint.packageManager": "pnpm"
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"postCreateCommand": ".devcontainer/setup.sh",
 | 
			
		||||
	"forwardPorts": [
 | 
			
		||||
		3000
 | 
			
		||||
	]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								.devcontainer/setup.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								.devcontainer/setup.sh
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
# Install Node packages
 | 
			
		||||
pnpm install
 | 
			
		||||
 | 
			
		||||
# Copy in skeleton configuration if there is no existing configuration
 | 
			
		||||
if [ ! -d "config/" ]; then
 | 
			
		||||
  echo "Adding skeleton config"
 | 
			
		||||
  mkdir config/
 | 
			
		||||
  cp -r src/skeleton/* config
 | 
			
		||||
fi
 | 
			
		||||
							
								
								
									
										10
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
								
							@ -59,6 +59,16 @@ body:
 | 
			
		||||
      label: Configuration
 | 
			
		||||
      description: Please provide any relevant service, widget or otherwise related configuration here
 | 
			
		||||
      render: yaml
 | 
			
		||||
  - type: textarea
 | 
			
		||||
    id: container-logs
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: Container Logs
 | 
			
		||||
      description: Please review and provide any logs from the container, if relevant
 | 
			
		||||
  - type: textarea
 | 
			
		||||
    id: browser-logs
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: Browser Logs
 | 
			
		||||
      description: Please review and provide any relevant logs from the browser, if relevant
 | 
			
		||||
  - type: textarea
 | 
			
		||||
    id: other
 | 
			
		||||
    attributes:
 | 
			
		||||
 | 
			
		||||
@ -47,12 +47,12 @@
 | 
			
		||||
- 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, Authentic, Proxmox
 | 
			
		||||
  - Portainer, Traefik, Speedtest Tracker, PiHole, AdGuard Home, Nginx Proxy Manager, Gotify, Syncthing Relay Server, Authentik, Proxmox
 | 
			
		||||
- Information Providers
 | 
			
		||||
  - Coin Market Cap, Mastodon
 | 
			
		||||
- Information & Utility Widgets
 | 
			
		||||
  - System Stats (Disk, CPU, Memory)
 | 
			
		||||
  - Weather via WeatherAPI.com or OpenWeatherMap
 | 
			
		||||
  - Weather via [OpenWeatherMap](https://openweathermap.org/) or [Open-Meteo](https://open-meteo.com/)
 | 
			
		||||
  - Search Bar
 | 
			
		||||
- Customizable
 | 
			
		||||
  - 21 theme colors with light and dark mode support
 | 
			
		||||
 | 
			
		||||
@ -103,7 +103,7 @@ module.exports = {
 | 
			
		||||
          const bits = options.bits ? value : value / 8;
 | 
			
		||||
          const k = 1024;
 | 
			
		||||
          const dm = options.decimals ? options.decimals : 0;
 | 
			
		||||
          const sizes = ["Bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps", "Ybps"];
 | 
			
		||||
          const sizes = ["Bps", "KiBps", "MiBps", "GiBps", "TiBps", "PiBps", "EiBps", "ZiBps", "YiBps"];
 | 
			
		||||
 | 
			
		||||
          const i = Math.floor(Math.log(bits) / Math.log(k));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -103,7 +103,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "الرام",
 | 
			
		||||
        "cpu": "المعالج",
 | 
			
		||||
        "offline": "غير متصل"
 | 
			
		||||
        "offline": "غير متصل",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "يعمل الان",
 | 
			
		||||
@ -191,7 +193,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "approved": "Approved",
 | 
			
		||||
        "available": "Available"
 | 
			
		||||
        "available": "Available",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Queries",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,9 @@
 | 
			
		||||
        "rx": "RX",
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU"
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Възпроизвежда",
 | 
			
		||||
@ -129,7 +131,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "approved": "Approved",
 | 
			
		||||
        "available": "Available"
 | 
			
		||||
        "available": "Available",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Queries",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "ping": "Ping",
 | 
			
		||||
        "error": "Error"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,9 @@
 | 
			
		||||
        "tx": "Transmès",
 | 
			
		||||
        "mem": "Memòria",
 | 
			
		||||
        "cpu": "Processador",
 | 
			
		||||
        "offline": "Fora de línia"
 | 
			
		||||
        "offline": "Fora de línia",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Reproduint",
 | 
			
		||||
@ -98,7 +100,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pendent",
 | 
			
		||||
        "approved": "Aprovat",
 | 
			
		||||
        "available": "Disponible"
 | 
			
		||||
        "available": "Disponible",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Consultes",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -57,7 +57,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "RAM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Přehrává",
 | 
			
		||||
@ -145,7 +147,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Čeká",
 | 
			
		||||
        "approved": "Schváleno",
 | 
			
		||||
        "available": "Dostupný"
 | 
			
		||||
        "available": "Dostupný",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Dotazy",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "download": "Download"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Afventer",
 | 
			
		||||
        "approved": "Godkendt",
 | 
			
		||||
        "available": "Tilgængelig"
 | 
			
		||||
        "available": "Tilgængelig",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "adguard": {
 | 
			
		||||
        "queries": "Forespørgsler",
 | 
			
		||||
@ -181,7 +182,9 @@
 | 
			
		||||
        "rx": "RX",
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "RAM",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Afspiller",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,9 @@
 | 
			
		||||
        "tx": "Tx",
 | 
			
		||||
        "mem": "Mem",
 | 
			
		||||
        "cpu": "Prozessor",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Spielen",
 | 
			
		||||
@ -102,7 +104,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Ausstehend",
 | 
			
		||||
        "approved": "Genehmigt",
 | 
			
		||||
        "available": "Verfügbar"
 | 
			
		||||
        "available": "Verfügbar",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "Geschwindigkeit",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "ping": "Ping",
 | 
			
		||||
        "error": "Error"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,13 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Playing",
 | 
			
		||||
@ -64,6 +70,12 @@
 | 
			
		||||
        "bitrate": "Bitrate",
 | 
			
		||||
        "no_active": "No Active Streams"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "changedetectionio": {
 | 
			
		||||
        "totalObserved": "Total Observed",
 | 
			
		||||
        "diffsDetected": "Diffs Detected"
 | 
			
		||||
@ -106,6 +118,18 @@
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "sonarr": {
 | 
			
		||||
        "wanted": "Wanted",
 | 
			
		||||
        "queued": "Queued",
 | 
			
		||||
@ -143,6 +167,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "processing": "Processing",
 | 
			
		||||
        "approved": "Approved",
 | 
			
		||||
        "available": "Available"
 | 
			
		||||
    },
 | 
			
		||||
@ -301,7 +326,7 @@
 | 
			
		||||
        "child_bridges": "Child Bridges",
 | 
			
		||||
        "child_bridges_status": "{{ok}}/{{total}}"
 | 
			
		||||
    },
 | 
			
		||||
    "watchtower":{
 | 
			
		||||
    "watchtower": {
 | 
			
		||||
        "containers_scanned": "Scanned",
 | 
			
		||||
        "containers_updated": "Updated",
 | 
			
		||||
        "containers_failed": "Failed"
 | 
			
		||||
@ -338,5 +363,14 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										365
									
								
								public/locales/eo/common.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										365
									
								
								public/locales/eo/common.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,365 @@
 | 
			
		||||
{
 | 
			
		||||
    "widget": {
 | 
			
		||||
        "missing_type": "Missing Widget Type: {{type}}",
 | 
			
		||||
        "api_error": "API Error",
 | 
			
		||||
        "information": "Informo",
 | 
			
		||||
        "status": "Stato",
 | 
			
		||||
        "url": "URL",
 | 
			
		||||
        "raw_error": "Raw Error",
 | 
			
		||||
        "response_data": "Response Data"
 | 
			
		||||
    },
 | 
			
		||||
    "weather": {
 | 
			
		||||
        "current": "Aktuala loko",
 | 
			
		||||
        "allow": "Click to allow",
 | 
			
		||||
        "updating": "Updating",
 | 
			
		||||
        "wait": "Please wait"
 | 
			
		||||
    },
 | 
			
		||||
    "search": {
 | 
			
		||||
        "placeholder": "Serĉi…"
 | 
			
		||||
    },
 | 
			
		||||
    "resources": {
 | 
			
		||||
        "cpu": "Ĉefprocesoro",
 | 
			
		||||
        "total": "Totalo",
 | 
			
		||||
        "free": "Libera",
 | 
			
		||||
        "used": "Uzata",
 | 
			
		||||
        "load": "Ŝarĝo"
 | 
			
		||||
    },
 | 
			
		||||
    "unifi": {
 | 
			
		||||
        "users": "Uzantoj",
 | 
			
		||||
        "uptime": "System Uptime",
 | 
			
		||||
        "days": "Tagoj",
 | 
			
		||||
        "wan": "WAN",
 | 
			
		||||
        "lan": "LAN",
 | 
			
		||||
        "wlan": "WLAN",
 | 
			
		||||
        "devices": "Aparatoj",
 | 
			
		||||
        "lan_devices": "LAN Devices",
 | 
			
		||||
        "wlan_devices": "WLAN Devices",
 | 
			
		||||
        "lan_users": "LAN Users",
 | 
			
		||||
        "wlan_users": "WLAN Users",
 | 
			
		||||
        "up": "UP",
 | 
			
		||||
        "down": "DOWN",
 | 
			
		||||
        "wait": "Please wait"
 | 
			
		||||
    },
 | 
			
		||||
    "docker": {
 | 
			
		||||
        "rx": "RX",
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "Memoro",
 | 
			
		||||
        "cpu": "Ĉefprocesoro",
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Eraro",
 | 
			
		||||
        "unknown": "Nekonata"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Eraro",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Ludante",
 | 
			
		||||
        "transcoding": "Transcoding",
 | 
			
		||||
        "bitrate": "Bitrate",
 | 
			
		||||
        "no_active": "No Active Streams"
 | 
			
		||||
    },
 | 
			
		||||
    "changedetectionio": {
 | 
			
		||||
        "totalObserved": "Total Observed",
 | 
			
		||||
        "diffsDetected": "Diffs Detected"
 | 
			
		||||
    },
 | 
			
		||||
    "tautulli": {
 | 
			
		||||
        "playing": "Playing",
 | 
			
		||||
        "transcoding": "Transcoding",
 | 
			
		||||
        "bitrate": "Bitrate",
 | 
			
		||||
        "no_active": "No Active Streams"
 | 
			
		||||
    },
 | 
			
		||||
    "nzbget": {
 | 
			
		||||
        "rate": "Rate",
 | 
			
		||||
        "remaining": "Remaining",
 | 
			
		||||
        "downloaded": "Downloaded"
 | 
			
		||||
    },
 | 
			
		||||
    "plex": {
 | 
			
		||||
        "streams": "Active Streams",
 | 
			
		||||
        "movies": "Filmoj",
 | 
			
		||||
        "tv": "Televidprogramoj"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "Rate",
 | 
			
		||||
        "queue": "Queue",
 | 
			
		||||
        "timeleft": "Time Left"
 | 
			
		||||
    },
 | 
			
		||||
    "rutorrent": {
 | 
			
		||||
        "active": "Active",
 | 
			
		||||
        "upload": "Alŝuto",
 | 
			
		||||
        "download": "Elŝuto"
 | 
			
		||||
    },
 | 
			
		||||
    "transmission": {
 | 
			
		||||
        "download": "Elŝuto",
 | 
			
		||||
        "upload": "Alŝuto",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "qbittorrent": {
 | 
			
		||||
        "download": "Elŝuto",
 | 
			
		||||
        "upload": "Alŝuto",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Elŝuto",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "sonarr": {
 | 
			
		||||
        "wanted": "Wanted",
 | 
			
		||||
        "queued": "Queued",
 | 
			
		||||
        "series": "Serio"
 | 
			
		||||
    },
 | 
			
		||||
    "radarr": {
 | 
			
		||||
        "wanted": "Wanted",
 | 
			
		||||
        "missing": "Missing",
 | 
			
		||||
        "queued": "Queued",
 | 
			
		||||
        "movies": "Filmoj"
 | 
			
		||||
    },
 | 
			
		||||
    "lidarr": {
 | 
			
		||||
        "wanted": "Wanted",
 | 
			
		||||
        "queued": "Queued",
 | 
			
		||||
        "albums": "Albumoj"
 | 
			
		||||
    },
 | 
			
		||||
    "readarr": {
 | 
			
		||||
        "wanted": "Wanted",
 | 
			
		||||
        "queued": "Queued",
 | 
			
		||||
        "books": "Libroj"
 | 
			
		||||
    },
 | 
			
		||||
    "bazarr": {
 | 
			
		||||
        "missingEpisodes": "Missing Episodes",
 | 
			
		||||
        "missingMovies": "Missing Movies"
 | 
			
		||||
    },
 | 
			
		||||
    "ombi": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "approved": "Aprobita",
 | 
			
		||||
        "available": "Havebla"
 | 
			
		||||
    },
 | 
			
		||||
    "jellyseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "approved": "Aprobita",
 | 
			
		||||
        "available": "Havebla"
 | 
			
		||||
    },
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "processing": "Processing",
 | 
			
		||||
        "approved": "Aprobita",
 | 
			
		||||
        "available": "Havebla"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Queries",
 | 
			
		||||
        "blocked": "Blocked",
 | 
			
		||||
        "gravity": "Gravity"
 | 
			
		||||
    },
 | 
			
		||||
    "adguard": {
 | 
			
		||||
        "queries": "Queries",
 | 
			
		||||
        "blocked": "Blokitaj",
 | 
			
		||||
        "filtered": "Filtritaj",
 | 
			
		||||
        "latency": "Latency"
 | 
			
		||||
    },
 | 
			
		||||
    "speedtest": {
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "portainer": {
 | 
			
		||||
        "running": "Running",
 | 
			
		||||
        "stopped": "Stopped",
 | 
			
		||||
        "total": "Totalo"
 | 
			
		||||
    },
 | 
			
		||||
    "traefik": {
 | 
			
		||||
        "routers": "Routers",
 | 
			
		||||
        "services": "Servoj",
 | 
			
		||||
        "middleware": "Middleware"
 | 
			
		||||
    },
 | 
			
		||||
    "navidrome": {
 | 
			
		||||
        "nothing_streaming": "No Active Streams",
 | 
			
		||||
        "please_wait": "Please Wait"
 | 
			
		||||
    },
 | 
			
		||||
    "npm": {
 | 
			
		||||
        "enabled": "Enabled",
 | 
			
		||||
        "disabled": "Disabled",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "coinmarketcap": {
 | 
			
		||||
        "configure": "Configure one or more crypto currencies to track",
 | 
			
		||||
        "1hour": "1 horo",
 | 
			
		||||
        "1day": "1 tago",
 | 
			
		||||
        "7days": "7 tagoj",
 | 
			
		||||
        "30days": "30 tagoj"
 | 
			
		||||
    },
 | 
			
		||||
    "gotify": {
 | 
			
		||||
        "apps": "Applications",
 | 
			
		||||
        "clients": "Klientoj",
 | 
			
		||||
        "messages": "Mesaĝoj"
 | 
			
		||||
    },
 | 
			
		||||
    "prowlarr": {
 | 
			
		||||
        "enableIndexers": "Indexers",
 | 
			
		||||
        "numberOfGrabs": "Grabs",
 | 
			
		||||
        "numberOfQueries": "Queries",
 | 
			
		||||
        "numberOfFailGrabs": "Fail Grabs",
 | 
			
		||||
        "numberOfFailQueries": "Fail Queries"
 | 
			
		||||
    },
 | 
			
		||||
    "jackett": {
 | 
			
		||||
        "configured": "Configured",
 | 
			
		||||
        "errored": "Errored"
 | 
			
		||||
    },
 | 
			
		||||
    "strelaysrv": {
 | 
			
		||||
        "numActiveSessions": "Seancoj",
 | 
			
		||||
        "numConnections": "Konektoj",
 | 
			
		||||
        "dataRelayed": "Relayed",
 | 
			
		||||
        "transferRate": "Rate"
 | 
			
		||||
    },
 | 
			
		||||
    "mastodon": {
 | 
			
		||||
        "user_count": "Uzantoj",
 | 
			
		||||
        "status_count": "Afiŝoj",
 | 
			
		||||
        "domain_count": "Domains"
 | 
			
		||||
    },
 | 
			
		||||
    "authentik": {
 | 
			
		||||
        "users": "Users",
 | 
			
		||||
        "loginsLast24H": "Logins (24h)",
 | 
			
		||||
        "failedLoginsLast24H": "Failed Logins (24h)"
 | 
			
		||||
    },
 | 
			
		||||
    "proxmox": {
 | 
			
		||||
        "mem": "Memoro",
 | 
			
		||||
        "cpu": "Ĉefprocesoro",
 | 
			
		||||
        "lxc": "LXC",
 | 
			
		||||
        "vms": "VMs"
 | 
			
		||||
    },
 | 
			
		||||
    "glances": {
 | 
			
		||||
        "cpu": "Ĉefprocesoro",
 | 
			
		||||
        "mem": "Memoro",
 | 
			
		||||
        "wait": "Bonvolu atendi"
 | 
			
		||||
    },
 | 
			
		||||
    "quicklaunch": {
 | 
			
		||||
        "bookmark": "Bookmark",
 | 
			
		||||
        "service": "Servo"
 | 
			
		||||
    },
 | 
			
		||||
    "wmo": {
 | 
			
		||||
        "0-day": "Suna",
 | 
			
		||||
        "0-night": "Sennuba",
 | 
			
		||||
        "1-day": "Mainly Sunny",
 | 
			
		||||
        "1-night": "Mainly Clear",
 | 
			
		||||
        "2-day": "Nubeta",
 | 
			
		||||
        "2-night": "Nubeta",
 | 
			
		||||
        "3-day": "Nuba",
 | 
			
		||||
        "3-night": "Nuba",
 | 
			
		||||
        "45-day": "Nebula",
 | 
			
		||||
        "45-night": "Nebula",
 | 
			
		||||
        "48-day": "Nebula",
 | 
			
		||||
        "48-night": "Nebula",
 | 
			
		||||
        "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",
 | 
			
		||||
        "57-day": "Freezing Drizzle",
 | 
			
		||||
        "57-night": "Freezing Drizzle",
 | 
			
		||||
        "61-day": "Light Rain",
 | 
			
		||||
        "61-night": "Light Rain",
 | 
			
		||||
        "63-day": "Pluvo",
 | 
			
		||||
        "63-night": "Pluvo",
 | 
			
		||||
        "65-day": "Pluvego",
 | 
			
		||||
        "65-night": "Pluvego",
 | 
			
		||||
        "66-day": "Frosta pluvo",
 | 
			
		||||
        "66-night": "Frosta pluvo",
 | 
			
		||||
        "67-day": "Frosta pluvo",
 | 
			
		||||
        "67-night": "Frosta pluvo",
 | 
			
		||||
        "71-day": "Light Snow",
 | 
			
		||||
        "71-night": "Light Snow",
 | 
			
		||||
        "73-day": "Neĝo",
 | 
			
		||||
        "73-night": "Neĝo",
 | 
			
		||||
        "75-day": "Neĝego",
 | 
			
		||||
        "75-night": "Neĝego",
 | 
			
		||||
        "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",
 | 
			
		||||
        "95-day": "Fulmotondro",
 | 
			
		||||
        "95-night": "Fulmotondro",
 | 
			
		||||
        "96-day": "Fulmotondro kun hajlo",
 | 
			
		||||
        "96-night": "Fulmotondro kun hajlo",
 | 
			
		||||
        "99-day": "Fulmotondro kun hajlo",
 | 
			
		||||
        "99-night": "Fulmotondro kun hajlo"
 | 
			
		||||
    },
 | 
			
		||||
    "homebridge": {
 | 
			
		||||
        "available_update": "Sistemo",
 | 
			
		||||
        "updates": "Updates",
 | 
			
		||||
        "update_available": "Update Available",
 | 
			
		||||
        "up_to_date": "Up to Date",
 | 
			
		||||
        "child_bridges": "Child Bridges",
 | 
			
		||||
        "child_bridges_status": "{{ok}}/{{total}}"
 | 
			
		||||
    },
 | 
			
		||||
    "watchtower": {
 | 
			
		||||
        "containers_scanned": "Scanned",
 | 
			
		||||
        "containers_updated": "Updated",
 | 
			
		||||
        "containers_failed": "Failed"
 | 
			
		||||
    },
 | 
			
		||||
    "autobrr": {
 | 
			
		||||
        "approvedPushes": "Approved",
 | 
			
		||||
        "rejectedPushes": "Rejected",
 | 
			
		||||
        "filters": "Filtriloj",
 | 
			
		||||
        "indexers": "Indexers"
 | 
			
		||||
    },
 | 
			
		||||
    "tubearchivist": {
 | 
			
		||||
        "downloads": "Queue",
 | 
			
		||||
        "videos": "Videos",
 | 
			
		||||
        "channels": "Kanaloj",
 | 
			
		||||
        "playlists": "Playlists"
 | 
			
		||||
    },
 | 
			
		||||
    "truenas": {
 | 
			
		||||
        "load": "System Load",
 | 
			
		||||
        "uptime": "Uptime",
 | 
			
		||||
        "alerts": "Alerts",
 | 
			
		||||
        "time": "{{value, number(style: unit; unitDisplay: long;)}}"
 | 
			
		||||
    },
 | 
			
		||||
    "pyload": {
 | 
			
		||||
        "speed": "Speed",
 | 
			
		||||
        "active": "Aktiva",
 | 
			
		||||
        "queue": "Queue",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "gluetun": {
 | 
			
		||||
        "public_ip": "Public IP",
 | 
			
		||||
        "region": "Regiono",
 | 
			
		||||
        "country": "Lando"
 | 
			
		||||
    },
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Kanaloj",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Totalo"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -3,10 +3,10 @@
 | 
			
		||||
        "missing_type": "Falta el tipo de widget: {{type}}",
 | 
			
		||||
        "api_error": "Error de API",
 | 
			
		||||
        "status": "Estado",
 | 
			
		||||
        "information": "Information",
 | 
			
		||||
        "information": "Información",
 | 
			
		||||
        "url": "URL",
 | 
			
		||||
        "raw_error": "Raw Error",
 | 
			
		||||
        "response_data": "Response Data"
 | 
			
		||||
        "raw_error": "Error sin procesar",
 | 
			
		||||
        "response_data": "Datos de respuesta"
 | 
			
		||||
    },
 | 
			
		||||
    "search": {
 | 
			
		||||
        "placeholder": "Buscar…"
 | 
			
		||||
@ -23,7 +23,9 @@
 | 
			
		||||
        "tx": "Transmitido",
 | 
			
		||||
        "mem": "Memoria",
 | 
			
		||||
        "cpu": "Procesador",
 | 
			
		||||
        "offline": "Desconectado"
 | 
			
		||||
        "offline": "Desconectado",
 | 
			
		||||
        "error": "Fallo",
 | 
			
		||||
        "unknown": "Desconocido"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Reproduciendo",
 | 
			
		||||
@ -51,7 +53,7 @@
 | 
			
		||||
        "wanted": "Buscando",
 | 
			
		||||
        "queued": "En cola",
 | 
			
		||||
        "movies": "Películas",
 | 
			
		||||
        "missing": "No Encontrado"
 | 
			
		||||
        "missing": "Faltan"
 | 
			
		||||
    },
 | 
			
		||||
    "readarr": {
 | 
			
		||||
        "wanted": "Buscando",
 | 
			
		||||
@ -102,7 +104,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pendiente",
 | 
			
		||||
        "approved": "Aprobado",
 | 
			
		||||
        "available": "Disponible"
 | 
			
		||||
        "available": "Disponible",
 | 
			
		||||
        "processing": "Procesando"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "Tasa",
 | 
			
		||||
@ -136,7 +139,7 @@
 | 
			
		||||
    "transmission": {
 | 
			
		||||
        "download": "Bajada",
 | 
			
		||||
        "upload": "Subida",
 | 
			
		||||
        "leech": "Compañeros",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Semillas"
 | 
			
		||||
    },
 | 
			
		||||
    "jackett": {
 | 
			
		||||
@ -161,7 +164,7 @@
 | 
			
		||||
    "qbittorrent": {
 | 
			
		||||
        "download": "Bajada",
 | 
			
		||||
        "upload": "Subida",
 | 
			
		||||
        "leech": "Compañeros",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Semillas"
 | 
			
		||||
    },
 | 
			
		||||
    "mastodon": {
 | 
			
		||||
@ -325,7 +328,38 @@
 | 
			
		||||
        "country": "País"
 | 
			
		||||
    },
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
        "channels": "Canales",
 | 
			
		||||
        "hd": "Alta definición"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Aprobado",
 | 
			
		||||
        "failed": "Fallido",
 | 
			
		||||
        "unknown": "Desconocido"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Bandeja de entrada",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Descarga",
 | 
			
		||||
        "upload": "Subida",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Semilla"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Descargar",
 | 
			
		||||
        "upload": "Cargar",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Semilla"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Descargar",
 | 
			
		||||
        "upload": "Subir",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "RAM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Toistaa",
 | 
			
		||||
@ -108,7 +110,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Vireillä",
 | 
			
		||||
        "approved": "Hyväksytty",
 | 
			
		||||
        "available": "Saatavilla"
 | 
			
		||||
        "available": "Saatavilla",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Kyselyjä",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "download": "Download"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,9 @@
 | 
			
		||||
        "tx": "Tx",
 | 
			
		||||
        "mem": "Mém",
 | 
			
		||||
        "cpu": "Cpu",
 | 
			
		||||
        "offline": "Hors ligne"
 | 
			
		||||
        "offline": "Hors ligne",
 | 
			
		||||
        "error": "Erreur",
 | 
			
		||||
        "unknown": "Inconnu"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "En lecture",
 | 
			
		||||
@ -102,7 +104,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "En attente",
 | 
			
		||||
        "approved": "Demande",
 | 
			
		||||
        "available": "Disponible"
 | 
			
		||||
        "available": "Disponible",
 | 
			
		||||
        "processing": "En traitement"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "Débit",
 | 
			
		||||
@ -325,7 +328,38 @@
 | 
			
		||||
        "country": "Pays"
 | 
			
		||||
    },
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "channels": "Canaux",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Erreur",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Réussi",
 | 
			
		||||
        "failed": "Échoué",
 | 
			
		||||
        "unknown": "Inconnu"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Boîte de réception",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Récep.",
 | 
			
		||||
        "upload": "Envoi",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Réception",
 | 
			
		||||
        "upload": "Envoi",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Récep.",
 | 
			
		||||
        "upload": "Envoi",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "זיכרון",
 | 
			
		||||
        "cpu": "מעבד",
 | 
			
		||||
        "offline": "כבוי"
 | 
			
		||||
        "offline": "כבוי",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "מנגן",
 | 
			
		||||
@ -108,7 +110,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "ממתין",
 | 
			
		||||
        "approved": "מאושר",
 | 
			
		||||
        "available": "זמין"
 | 
			
		||||
        "available": "זמין",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "שאילתות",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Playing",
 | 
			
		||||
@ -148,7 +150,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "approved": "Approved",
 | 
			
		||||
        "available": "Available"
 | 
			
		||||
        "available": "Available",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Queries",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "available": "Dostupno",
 | 
			
		||||
        "pending": "Predstoji",
 | 
			
		||||
        "approved": "Odobreno"
 | 
			
		||||
        "approved": "Odobreno",
 | 
			
		||||
        "processing": "Obrada"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Upiti",
 | 
			
		||||
@ -59,17 +60,19 @@
 | 
			
		||||
        "missing_type": "Nedostajuća vrsta widgeta: {{type}}",
 | 
			
		||||
        "api_error": "API greška",
 | 
			
		||||
        "status": "Stanje",
 | 
			
		||||
        "information": "Information",
 | 
			
		||||
        "information": "Informacije",
 | 
			
		||||
        "url": "URL",
 | 
			
		||||
        "raw_error": "Raw Error",
 | 
			
		||||
        "response_data": "Response Data"
 | 
			
		||||
        "raw_error": "Raw greška",
 | 
			
		||||
        "response_data": "Podaci odgovora"
 | 
			
		||||
    },
 | 
			
		||||
    "docker": {
 | 
			
		||||
        "rx": "RX",
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Nepovezan"
 | 
			
		||||
        "offline": "Nepovezan",
 | 
			
		||||
        "error": "Greška",
 | 
			
		||||
        "unknown": "Nepoznato"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Reprodukcija",
 | 
			
		||||
@ -320,12 +323,43 @@
 | 
			
		||||
        "total": "Ukupno"
 | 
			
		||||
    },
 | 
			
		||||
    "gluetun": {
 | 
			
		||||
        "public_ip": "Public IP",
 | 
			
		||||
        "region": "Region",
 | 
			
		||||
        "country": "Country"
 | 
			
		||||
        "public_ip": "Javni IP",
 | 
			
		||||
        "region": "Regija",
 | 
			
		||||
        "country": "Zemlja"
 | 
			
		||||
    },
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "channels": "Kanali",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Greška",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Prošlo",
 | 
			
		||||
        "failed": "Neuspjelo",
 | 
			
		||||
        "unknown": "Nepoznato"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Ulazni sandučić",
 | 
			
		||||
        "total": "Ukupno"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Preuzimanje",
 | 
			
		||||
        "upload": "Prijenos",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Preuzimanje",
 | 
			
		||||
        "upload": "Prijenos",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Preuzimanje",
 | 
			
		||||
        "upload": "Prijenos",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "lidarr": {
 | 
			
		||||
        "albums": "Albumok",
 | 
			
		||||
@ -108,7 +110,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Függőben",
 | 
			
		||||
        "approved": "Engedélyezett",
 | 
			
		||||
        "available": "Elérhető"
 | 
			
		||||
        "available": "Elérhető",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Lekérdezések",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,9 @@
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "rx": "RX"
 | 
			
		||||
        "rx": "RX",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "In riproduzione",
 | 
			
		||||
@ -102,7 +104,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "In attesa",
 | 
			
		||||
        "approved": "Approvati",
 | 
			
		||||
        "available": "Disponibili"
 | 
			
		||||
        "available": "Disponibili",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "Rapporto",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -126,10 +126,10 @@
 | 
			
		||||
        "missing_type": "Jenis Widget Hilang: {{type}}",
 | 
			
		||||
        "api_error": "Masalah API",
 | 
			
		||||
        "status": "Status",
 | 
			
		||||
        "information": "Information",
 | 
			
		||||
        "information": "Informasi",
 | 
			
		||||
        "url": "URL",
 | 
			
		||||
        "raw_error": "Raw Error",
 | 
			
		||||
        "response_data": "Response Data"
 | 
			
		||||
        "raw_error": "Ralat Mentah",
 | 
			
		||||
        "response_data": "Data Respon"
 | 
			
		||||
    },
 | 
			
		||||
    "weather": {
 | 
			
		||||
        "current": "Lokasi Sekarang",
 | 
			
		||||
@ -150,7 +150,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Luar talian"
 | 
			
		||||
        "offline": "Luar talian",
 | 
			
		||||
        "error": "Ralat",
 | 
			
		||||
        "unknown": "Tidak Diketahui"
 | 
			
		||||
    },
 | 
			
		||||
    "changedetectionio": {
 | 
			
		||||
        "totalObserved": "Jumlah Diperhatikan",
 | 
			
		||||
@ -218,7 +220,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Tertangguh",
 | 
			
		||||
        "approved": "Lulus",
 | 
			
		||||
        "available": "Sudah Ada"
 | 
			
		||||
        "available": "Sudah Ada",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Permintaan",
 | 
			
		||||
@ -320,12 +323,43 @@
 | 
			
		||||
        "total": "Jumlah"
 | 
			
		||||
    },
 | 
			
		||||
    "gluetun": {
 | 
			
		||||
        "public_ip": "Public IP",
 | 
			
		||||
        "region": "Region",
 | 
			
		||||
        "country": "Country"
 | 
			
		||||
        "public_ip": "IP Awam",
 | 
			
		||||
        "region": "Rantau",
 | 
			
		||||
        "country": "Negara"
 | 
			
		||||
    },
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "channels": "Saluran",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Ralat",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Lulus",
 | 
			
		||||
        "failed": "Gagal",
 | 
			
		||||
        "unknown": "Tidak Diketahui"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Peti Masuk",
 | 
			
		||||
        "total": "Jumlah"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Muat Turun",
 | 
			
		||||
        "upload": "Muat Naik",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,9 @@
 | 
			
		||||
        "tx": "Sendt",
 | 
			
		||||
        "mem": "Minne",
 | 
			
		||||
        "cpu": "Prosessor",
 | 
			
		||||
        "offline": "Frakoblet"
 | 
			
		||||
        "offline": "Frakoblet",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Spiller",
 | 
			
		||||
@ -102,7 +104,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Venter",
 | 
			
		||||
        "approved": "Godkjent",
 | 
			
		||||
        "available": "Tilgjengelig"
 | 
			
		||||
        "available": "Tilgjengelig",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "Takt",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "speedtest": {
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
@ -102,7 +104,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "approved": "Approved",
 | 
			
		||||
        "available": "Available"
 | 
			
		||||
        "available": "Available",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "Rate",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -53,17 +53,19 @@
 | 
			
		||||
        "missing_type": "Brakujący typ widżetu: {{type}}",
 | 
			
		||||
        "api_error": "Błąd API",
 | 
			
		||||
        "status": "Stan",
 | 
			
		||||
        "url": "URL",
 | 
			
		||||
        "information": "Information",
 | 
			
		||||
        "raw_error": "Raw Error",
 | 
			
		||||
        "response_data": "Response Data"
 | 
			
		||||
        "url": "Adres URL",
 | 
			
		||||
        "information": "Informacje",
 | 
			
		||||
        "raw_error": "Niesformatowany błąd",
 | 
			
		||||
        "response_data": "Dane odpowiedzi"
 | 
			
		||||
    },
 | 
			
		||||
    "docker": {
 | 
			
		||||
        "rx": "RX",
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "rx": "Rx",
 | 
			
		||||
        "tx": "Tx",
 | 
			
		||||
        "mem": "Pamięć",
 | 
			
		||||
        "cpu": "Procesor",
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Błąd",
 | 
			
		||||
        "unknown": "Nieznany"
 | 
			
		||||
    },
 | 
			
		||||
    "nzbget": {
 | 
			
		||||
        "rate": "Szybkość",
 | 
			
		||||
@ -95,7 +97,7 @@
 | 
			
		||||
        "wanted": "Poszukiwane",
 | 
			
		||||
        "queued": "W kolejce",
 | 
			
		||||
        "movies": "Filmy",
 | 
			
		||||
        "missing": "Missing"
 | 
			
		||||
        "missing": "Brakujące"
 | 
			
		||||
    },
 | 
			
		||||
    "lidarr": {
 | 
			
		||||
        "wanted": "Poszukiwane",
 | 
			
		||||
@ -124,7 +126,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Oczekiwane",
 | 
			
		||||
        "approved": "Zaakceptowane",
 | 
			
		||||
        "available": "Dostępne"
 | 
			
		||||
        "available": "Dostępne",
 | 
			
		||||
        "processing": "Przetwarzane"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Zapytania",
 | 
			
		||||
@ -166,14 +169,14 @@
 | 
			
		||||
    },
 | 
			
		||||
    "mastodon": {
 | 
			
		||||
        "user_count": "Użytkownicy",
 | 
			
		||||
        "status_count": "Posts",
 | 
			
		||||
        "status_count": "Posty",
 | 
			
		||||
        "domain_count": "Domeny"
 | 
			
		||||
    },
 | 
			
		||||
    "strelaysrv": {
 | 
			
		||||
        "numActiveSessions": "Sesje",
 | 
			
		||||
        "numConnections": "Połączenia",
 | 
			
		||||
        "dataRelayed": "Relayed",
 | 
			
		||||
        "transferRate": "Rate"
 | 
			
		||||
        "dataRelayed": "Przekazane",
 | 
			
		||||
        "transferRate": "Przesył"
 | 
			
		||||
    },
 | 
			
		||||
    "authentik": {
 | 
			
		||||
        "users": "Użytkownicy",
 | 
			
		||||
@ -181,10 +184,10 @@
 | 
			
		||||
        "failedLoginsLast24H": "Nieudane logowania (24h)"
 | 
			
		||||
    },
 | 
			
		||||
    "proxmox": {
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "lxc": "LXC",
 | 
			
		||||
        "vms": "VMs"
 | 
			
		||||
        "mem": "Pamięć",
 | 
			
		||||
        "cpu": "Procesor",
 | 
			
		||||
        "lxc": "Kontenery LXC",
 | 
			
		||||
        "vms": "Maszyn wirtualnych"
 | 
			
		||||
    },
 | 
			
		||||
    "unifi": {
 | 
			
		||||
        "users": "Użytkownicy",
 | 
			
		||||
@ -203,25 +206,25 @@
 | 
			
		||||
        "wlan_devices": "Urządzenia WLAN"
 | 
			
		||||
    },
 | 
			
		||||
    "plex": {
 | 
			
		||||
        "streams": "Active Streams",
 | 
			
		||||
        "streams": "Aktywne strumienie",
 | 
			
		||||
        "movies": "Filmy",
 | 
			
		||||
        "tv": "Seriale"
 | 
			
		||||
    },
 | 
			
		||||
    "glances": {
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "Procesor",
 | 
			
		||||
        "mem": "Pamięć",
 | 
			
		||||
        "wait": "Proszę czekać"
 | 
			
		||||
    },
 | 
			
		||||
    "changedetectionio": {
 | 
			
		||||
        "diffsDetected": "Diffs Detected",
 | 
			
		||||
        "totalObserved": "Total Observed"
 | 
			
		||||
        "diffsDetected": "Wykryto różnic",
 | 
			
		||||
        "totalObserved": "Obserwowanych ogółem"
 | 
			
		||||
    },
 | 
			
		||||
    "wmo": {
 | 
			
		||||
        "77-day": "Snow Grains",
 | 
			
		||||
        "77-day": "Ziarnisty śnieg",
 | 
			
		||||
        "0-day": "Słoneczny",
 | 
			
		||||
        "0-night": "Clear",
 | 
			
		||||
        "1-day": "Mainly Sunny",
 | 
			
		||||
        "1-night": "Mainly Clear",
 | 
			
		||||
        "0-night": "Bezchmurny",
 | 
			
		||||
        "1-day": "Głównie słoneczny",
 | 
			
		||||
        "1-night": "Głównie bezchmurny",
 | 
			
		||||
        "2-day": "Częściowo pochmurnie",
 | 
			
		||||
        "2-night": "Częściowo pochmurnie",
 | 
			
		||||
        "3-day": "Pochmurnie",
 | 
			
		||||
@ -236,10 +239,10 @@
 | 
			
		||||
        "53-night": "Mżawka",
 | 
			
		||||
        "55-day": "Ciężka mżawka",
 | 
			
		||||
        "55-night": "Ciężka mżawka",
 | 
			
		||||
        "56-day": "Light Freezing Drizzle",
 | 
			
		||||
        "56-night": "Light Freezing Drizzle",
 | 
			
		||||
        "57-day": "Freezing Drizzle",
 | 
			
		||||
        "57-night": "Freezing Drizzle",
 | 
			
		||||
        "56-day": "Lekko chłodna mżawka",
 | 
			
		||||
        "56-night": "Lekko chłodna mżawka",
 | 
			
		||||
        "57-day": "Chłodna mżawka",
 | 
			
		||||
        "57-night": "Chłodna mżawka",
 | 
			
		||||
        "61-day": "Lekki deszcz",
 | 
			
		||||
        "61-night": "Lekki deszcz",
 | 
			
		||||
        "63-day": "Deszcz",
 | 
			
		||||
@ -256,7 +259,7 @@
 | 
			
		||||
        "73-night": "Śnieg",
 | 
			
		||||
        "75-day": "Ciężki śnieg",
 | 
			
		||||
        "75-night": "Ciężki śnieg",
 | 
			
		||||
        "77-night": "Snow Grains",
 | 
			
		||||
        "77-night": "Ziarnisty śnieg",
 | 
			
		||||
        "80-day": "Lekkie opady",
 | 
			
		||||
        "80-night": "Lekkie opady",
 | 
			
		||||
        "81-day": "Opady",
 | 
			
		||||
@ -275,7 +278,7 @@
 | 
			
		||||
        "99-night": "Burza z gradobiciem"
 | 
			
		||||
    },
 | 
			
		||||
    "quicklaunch": {
 | 
			
		||||
        "bookmark": "Bookmark",
 | 
			
		||||
        "bookmark": "Zakładka",
 | 
			
		||||
        "service": "Usługi"
 | 
			
		||||
    },
 | 
			
		||||
    "homebridge": {
 | 
			
		||||
@ -287,45 +290,76 @@
 | 
			
		||||
        "child_bridges_status": "{{ok}}/{{total}}"
 | 
			
		||||
    },
 | 
			
		||||
    "autobrr": {
 | 
			
		||||
        "approvedPushes": "Approved",
 | 
			
		||||
        "rejectedPushes": "Rejected",
 | 
			
		||||
        "filters": "Filters",
 | 
			
		||||
        "indexers": "Indexers"
 | 
			
		||||
        "approvedPushes": "Zaakceptowane",
 | 
			
		||||
        "rejectedPushes": "Odrzucone",
 | 
			
		||||
        "filters": "Filtry",
 | 
			
		||||
        "indexers": "Indeksery"
 | 
			
		||||
    },
 | 
			
		||||
    "watchtower": {
 | 
			
		||||
        "containers_scanned": "Scanned",
 | 
			
		||||
        "containers_updated": "Updated",
 | 
			
		||||
        "containers_failed": "Failed"
 | 
			
		||||
        "containers_scanned": "Zeskanowane",
 | 
			
		||||
        "containers_updated": "Zaktualizowane",
 | 
			
		||||
        "containers_failed": "Niepowodzenie"
 | 
			
		||||
    },
 | 
			
		||||
    "tubearchivist": {
 | 
			
		||||
        "downloads": "Queue",
 | 
			
		||||
        "videos": "Videos",
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "playlists": "Playlists"
 | 
			
		||||
        "downloads": "Kolejka",
 | 
			
		||||
        "videos": "Pliki wideo",
 | 
			
		||||
        "channels": "Kanały",
 | 
			
		||||
        "playlists": "Playlisty"
 | 
			
		||||
    },
 | 
			
		||||
    "truenas": {
 | 
			
		||||
        "load": "System Load",
 | 
			
		||||
        "uptime": "Uptime",
 | 
			
		||||
        "alerts": "Alerts",
 | 
			
		||||
        "load": "Obciążenie systemu",
 | 
			
		||||
        "uptime": "Czas działania",
 | 
			
		||||
        "alerts": "Ostrzeżenia",
 | 
			
		||||
        "time": "{{value, number(style: unit; unitDisplay: long;)}}"
 | 
			
		||||
    },
 | 
			
		||||
    "navidrome": {
 | 
			
		||||
        "please_wait": "Please Wait",
 | 
			
		||||
        "nothing_streaming": "No Active Streams"
 | 
			
		||||
        "please_wait": "Proszę czekać",
 | 
			
		||||
        "nothing_streaming": "Brak aktywnych strumieni"
 | 
			
		||||
    },
 | 
			
		||||
    "pyload": {
 | 
			
		||||
        "speed": "Speed",
 | 
			
		||||
        "active": "Active",
 | 
			
		||||
        "queue": "Queue",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
        "speed": "Prędkość",
 | 
			
		||||
        "active": "Aktywne",
 | 
			
		||||
        "queue": "Kolejka",
 | 
			
		||||
        "total": "Razem"
 | 
			
		||||
    },
 | 
			
		||||
    "gluetun": {
 | 
			
		||||
        "public_ip": "Public IP",
 | 
			
		||||
        "public_ip": "Adres publiczny",
 | 
			
		||||
        "region": "Region",
 | 
			
		||||
        "country": "Country"
 | 
			
		||||
        "country": "Państwo"
 | 
			
		||||
    },
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "channels": "Kanały",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Błąd",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Powodzenie",
 | 
			
		||||
        "failed": "Niepowodzenie",
 | 
			
		||||
        "unknown": "Nieznane"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Skrzynka odbiorcza",
 | 
			
		||||
        "total": "W sumie"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Pobieranie",
 | 
			
		||||
        "upload": "Wysyłanie",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Pobieranie",
 | 
			
		||||
        "upload": "Wysyłanie",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Pobieranie",
 | 
			
		||||
        "upload": "Wysyłanie",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -57,7 +57,9 @@
 | 
			
		||||
        "tx": "Tx",
 | 
			
		||||
        "mem": "Mem",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Desligado"
 | 
			
		||||
        "offline": "Desligado",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Reproduzindo",
 | 
			
		||||
@ -126,7 +128,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pendente",
 | 
			
		||||
        "approved": "Aprovado",
 | 
			
		||||
        "available": "Disponível"
 | 
			
		||||
        "available": "Disponível",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Consultas",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,10 @@
 | 
			
		||||
        "missing_type": "Widget ausente: {{type}}",
 | 
			
		||||
        "api_error": "Erro da API",
 | 
			
		||||
        "status": "Status",
 | 
			
		||||
        "information": "Information",
 | 
			
		||||
        "url": "URL",
 | 
			
		||||
        "information": "Informação",
 | 
			
		||||
        "url": "Endereço URL",
 | 
			
		||||
        "raw_error": "Raw Error",
 | 
			
		||||
        "response_data": "Response Data"
 | 
			
		||||
        "response_data": "Dados da Resposta"
 | 
			
		||||
    },
 | 
			
		||||
    "search": {
 | 
			
		||||
        "placeholder": "Pesquisar…"
 | 
			
		||||
@ -23,7 +23,9 @@
 | 
			
		||||
        "tx": "Tx",
 | 
			
		||||
        "mem": "Mem",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Desligado"
 | 
			
		||||
        "offline": "Desligado",
 | 
			
		||||
        "error": "Erro",
 | 
			
		||||
        "unknown": "Desconhecido"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "A reproduzir",
 | 
			
		||||
@ -113,7 +115,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pendente",
 | 
			
		||||
        "approved": "Aprovado",
 | 
			
		||||
        "available": "Disponível"
 | 
			
		||||
        "available": "Disponível",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "Taxa",
 | 
			
		||||
@ -147,7 +150,7 @@
 | 
			
		||||
    "transmission": {
 | 
			
		||||
        "download": "Baixando",
 | 
			
		||||
        "upload": "Enviando",
 | 
			
		||||
        "leech": "Sanguessugas",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Semeadores"
 | 
			
		||||
    },
 | 
			
		||||
    "jackett": {
 | 
			
		||||
@ -198,25 +201,25 @@
 | 
			
		||||
        "vms": "VMs"
 | 
			
		||||
    },
 | 
			
		||||
    "unifi": {
 | 
			
		||||
        "users": "Users",
 | 
			
		||||
        "uptime": "System Uptime",
 | 
			
		||||
        "days": "Days",
 | 
			
		||||
        "users": "Utilizadores",
 | 
			
		||||
        "uptime": "Tempo de Atividade do Sistema",
 | 
			
		||||
        "days": "Dias",
 | 
			
		||||
        "wan": "WAN",
 | 
			
		||||
        "lan_users": "LAN Users",
 | 
			
		||||
        "wlan_users": "WLAN Users",
 | 
			
		||||
        "lan_users": "Utilizadores LAN",
 | 
			
		||||
        "wlan_users": "Utilizadores WLAN",
 | 
			
		||||
        "up": "UP",
 | 
			
		||||
        "down": "DOWN",
 | 
			
		||||
        "wait": "Please wait",
 | 
			
		||||
        "wait": "Por favor aguarde",
 | 
			
		||||
        "lan": "LAN",
 | 
			
		||||
        "wlan": "WLAN",
 | 
			
		||||
        "devices": "Devices",
 | 
			
		||||
        "lan_devices": "LAN Devices",
 | 
			
		||||
        "wlan_devices": "WLAN Devices"
 | 
			
		||||
        "devices": "Dispositivos",
 | 
			
		||||
        "lan_devices": "Dispositivos LAN",
 | 
			
		||||
        "wlan_devices": "Dispositivos WLAN"
 | 
			
		||||
    },
 | 
			
		||||
    "plex": {
 | 
			
		||||
        "streams": "Active Streams",
 | 
			
		||||
        "movies": "Movies",
 | 
			
		||||
        "tv": "TV Shows"
 | 
			
		||||
        "streams": "Streams Ativas",
 | 
			
		||||
        "movies": "Filmes",
 | 
			
		||||
        "tv": "Series de TV"
 | 
			
		||||
    },
 | 
			
		||||
    "glances": {
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
@ -224,8 +227,8 @@
 | 
			
		||||
        "wait": "Please wait"
 | 
			
		||||
    },
 | 
			
		||||
    "changedetectionio": {
 | 
			
		||||
        "totalObserved": "Total Observed",
 | 
			
		||||
        "diffsDetected": "Diffs Detected"
 | 
			
		||||
        "totalObserved": "Total Observado",
 | 
			
		||||
        "diffsDetected": "Diferenças Detetadas"
 | 
			
		||||
    },
 | 
			
		||||
    "wmo": {
 | 
			
		||||
        "0-day": "Sunny",
 | 
			
		||||
@ -338,5 +341,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Erro",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Descarregar",
 | 
			
		||||
        "upload": "Carregar",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "jellyseerr": {
 | 
			
		||||
        "approved": "Aprobate",
 | 
			
		||||
@ -21,7 +23,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "În așteptare",
 | 
			
		||||
        "approved": "Aprobate",
 | 
			
		||||
        "available": "Disponibile"
 | 
			
		||||
        "available": "Disponibile",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Cereri",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,10 @@
 | 
			
		||||
        "missing_type": "Отсутствует тип виджета: {{type}}",
 | 
			
		||||
        "api_error": "Ошибка API",
 | 
			
		||||
        "status": "Статус",
 | 
			
		||||
        "information": "Information",
 | 
			
		||||
        "information": "Информация",
 | 
			
		||||
        "url": "URL",
 | 
			
		||||
        "raw_error": "Raw Error",
 | 
			
		||||
        "response_data": "Response Data"
 | 
			
		||||
        "response_data": "Данные ответа"
 | 
			
		||||
    },
 | 
			
		||||
    "search": {
 | 
			
		||||
        "placeholder": "Поиск…"
 | 
			
		||||
@ -15,21 +15,23 @@
 | 
			
		||||
        "total": "Всего",
 | 
			
		||||
        "free": "Свободно",
 | 
			
		||||
        "used": "Использовано",
 | 
			
		||||
        "load": "Load",
 | 
			
		||||
        "cpu": "CPU"
 | 
			
		||||
        "load": "Загрузка",
 | 
			
		||||
        "cpu": "Процессор"
 | 
			
		||||
    },
 | 
			
		||||
    "docker": {
 | 
			
		||||
        "rx": "Rx",
 | 
			
		||||
        "tx": "Тx",
 | 
			
		||||
        "mem": "Память",
 | 
			
		||||
        "cpu": "Процессор",
 | 
			
		||||
        "offline": "Не в сети"
 | 
			
		||||
        "offline": "Не в сети",
 | 
			
		||||
        "error": "Ошибка",
 | 
			
		||||
        "unknown": "Неизвестный"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Воспроизведение",
 | 
			
		||||
        "transcoding": "Транскодирование",
 | 
			
		||||
        "bitrate": "Битрейт",
 | 
			
		||||
        "no_active": "No Active Streams"
 | 
			
		||||
        "no_active": "Нет активных потоков"
 | 
			
		||||
    },
 | 
			
		||||
    "tautulli": {
 | 
			
		||||
        "playing": "Воспроизведение",
 | 
			
		||||
@ -102,7 +104,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "approved": "Approved",
 | 
			
		||||
        "available": "Available"
 | 
			
		||||
        "available": "Available",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "Rate",
 | 
			
		||||
@ -187,19 +190,19 @@
 | 
			
		||||
        "vms": "VMs"
 | 
			
		||||
    },
 | 
			
		||||
    "unifi": {
 | 
			
		||||
        "users": "Users",
 | 
			
		||||
        "uptime": "System Uptime",
 | 
			
		||||
        "days": "Days",
 | 
			
		||||
        "users": "Пользователи",
 | 
			
		||||
        "uptime": "Время работы системы",
 | 
			
		||||
        "days": "Дней",
 | 
			
		||||
        "wan": "WAN",
 | 
			
		||||
        "lan_users": "LAN Users",
 | 
			
		||||
        "wlan_users": "WLAN Users",
 | 
			
		||||
        "lan_users": "Пользователи LAN",
 | 
			
		||||
        "wlan_users": "Пользователи WLAN",
 | 
			
		||||
        "up": "UP",
 | 
			
		||||
        "down": "DOWN",
 | 
			
		||||
        "wait": "Please wait",
 | 
			
		||||
        "wait": "Подождите",
 | 
			
		||||
        "lan": "LAN",
 | 
			
		||||
        "wlan": "WLAN",
 | 
			
		||||
        "devices": "Devices",
 | 
			
		||||
        "lan_devices": "LAN Devices",
 | 
			
		||||
        "devices": "Устройства",
 | 
			
		||||
        "lan_devices": "Устройства подключённые по LAN",
 | 
			
		||||
        "wlan_devices": "WLAN Devices"
 | 
			
		||||
    },
 | 
			
		||||
    "plex": {
 | 
			
		||||
@ -213,8 +216,8 @@
 | 
			
		||||
        "wait": "Please wait"
 | 
			
		||||
    },
 | 
			
		||||
    "changedetectionio": {
 | 
			
		||||
        "totalObserved": "Total Observed",
 | 
			
		||||
        "diffsDetected": "Diffs Detected"
 | 
			
		||||
        "totalObserved": "Всего наблюдаемых",
 | 
			
		||||
        "diffsDetected": "Обнаружены различия"
 | 
			
		||||
    },
 | 
			
		||||
    "wmo": {
 | 
			
		||||
        "0-day": "Sunny",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Ошибка",
 | 
			
		||||
        "ping": "Пинг"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown",
 | 
			
		||||
        "passed": "Passed"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "leech": "Leech"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Playing",
 | 
			
		||||
@ -129,7 +131,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "approved": "Approved",
 | 
			
		||||
        "available": "Available"
 | 
			
		||||
        "available": "Available",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Queries",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Offline"
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "search": {
 | 
			
		||||
        "placeholder": "Sök…"
 | 
			
		||||
@ -102,7 +104,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Avvaktar",
 | 
			
		||||
        "approved": "Godkända",
 | 
			
		||||
        "available": "Tillgänglig"
 | 
			
		||||
        "available": "Tillgänglig",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "blocked": "Blockerad",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "సీపియూ",
 | 
			
		||||
        "offline": "ఆఫ్లైన్"
 | 
			
		||||
        "offline": "ఆఫ్లైన్",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "ఆడుతున్నారు",
 | 
			
		||||
@ -120,7 +122,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "పెండింగ్",
 | 
			
		||||
        "approved": "ఆమోదించబడింది",
 | 
			
		||||
        "available": "అందుబాటులో"
 | 
			
		||||
        "available": "అందుబాటులో",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "ప్రశ్నలు",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,9 @@
 | 
			
		||||
        "tx": "Giden Veri",
 | 
			
		||||
        "mem": "Bellek",
 | 
			
		||||
        "cpu": "İşlemci",
 | 
			
		||||
        "offline": "Çevrimdışı"
 | 
			
		||||
        "offline": "Çevrimdışı",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Oynatılıyor",
 | 
			
		||||
@ -129,7 +131,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Bekliyor",
 | 
			
		||||
        "approved": "Onaylı",
 | 
			
		||||
        "available": "Kullanılabilir"
 | 
			
		||||
        "available": "Kullanılabilir",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Sorgular",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,9 @@
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "BỘ NHỚ",
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "offline": "Ngoại tuyến"
 | 
			
		||||
        "offline": "Ngoại tuyến",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Đang chơi",
 | 
			
		||||
@ -102,7 +104,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "approved": "Đã duyệt",
 | 
			
		||||
        "available": "Available"
 | 
			
		||||
        "available": "Available",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "Rate",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "seed": "Seed",
 | 
			
		||||
        "leech": "Leech"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,9 @@
 | 
			
		||||
        "tx": "發送",
 | 
			
		||||
        "mem": "內存",
 | 
			
		||||
        "cpu": "處理器",
 | 
			
		||||
        "offline": "離線"
 | 
			
		||||
        "offline": "離線",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "nzbget": {
 | 
			
		||||
        "rate": "速度",
 | 
			
		||||
@ -108,7 +110,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "待定",
 | 
			
		||||
        "approved": "批准",
 | 
			
		||||
        "available": "可用"
 | 
			
		||||
        "available": "可用",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "查詢",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,9 @@
 | 
			
		||||
        "tx": "发送",
 | 
			
		||||
        "mem": "内存",
 | 
			
		||||
        "cpu": "处理器",
 | 
			
		||||
        "offline": "离线"
 | 
			
		||||
        "offline": "离线",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "播放中",
 | 
			
		||||
@ -102,7 +104,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "待办",
 | 
			
		||||
        "approved": "已批准",
 | 
			
		||||
        "available": "可用"
 | 
			
		||||
        "available": "可用",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "sabnzbd": {
 | 
			
		||||
        "rate": "速率",
 | 
			
		||||
@ -293,9 +296,9 @@
 | 
			
		||||
        "indexers": "Indexers"
 | 
			
		||||
    },
 | 
			
		||||
    "watchtower": {
 | 
			
		||||
        "containers_scanned": "Scanned",
 | 
			
		||||
        "containers_updated": "Updated",
 | 
			
		||||
        "containers_failed": "Failed"
 | 
			
		||||
        "containers_scanned": "以扫描",
 | 
			
		||||
        "containers_updated": "以升级",
 | 
			
		||||
        "containers_failed": "失败"
 | 
			
		||||
    },
 | 
			
		||||
    "tubearchivist": {
 | 
			
		||||
        "downloads": "Queue",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,9 @@
 | 
			
		||||
        "offline": "Offline",
 | 
			
		||||
        "tx": "TX",
 | 
			
		||||
        "mem": "MEM",
 | 
			
		||||
        "cpu": "CPU"
 | 
			
		||||
        "cpu": "CPU",
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "emby": {
 | 
			
		||||
        "playing": "Playing",
 | 
			
		||||
@ -87,7 +89,8 @@
 | 
			
		||||
    "overseerr": {
 | 
			
		||||
        "pending": "Pending",
 | 
			
		||||
        "approved": "Approved",
 | 
			
		||||
        "available": "Available"
 | 
			
		||||
        "available": "Available",
 | 
			
		||||
        "processing": "Processing"
 | 
			
		||||
    },
 | 
			
		||||
    "pihole": {
 | 
			
		||||
        "queries": "Queries",
 | 
			
		||||
@ -327,5 +330,36 @@
 | 
			
		||||
    "hdhomerun": {
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "hd": "HD"
 | 
			
		||||
    },
 | 
			
		||||
    "ping": {
 | 
			
		||||
        "error": "Error",
 | 
			
		||||
        "ping": "Ping"
 | 
			
		||||
    },
 | 
			
		||||
    "scrutiny": {
 | 
			
		||||
        "passed": "Passed",
 | 
			
		||||
        "failed": "Failed",
 | 
			
		||||
        "unknown": "Unknown"
 | 
			
		||||
    },
 | 
			
		||||
    "paperlessngx": {
 | 
			
		||||
        "inbox": "Inbox",
 | 
			
		||||
        "total": "Total"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "diskstation": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "flood": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -107,18 +107,19 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
 | 
			
		||||
 | 
			
		||||
  function highlightText(text) {
 | 
			
		||||
    const parts = text.split(new RegExp(`(${searchString})`, 'gi'));
 | 
			
		||||
    return <span>{parts.map(part => part.toLowerCase() === searchString.toLowerCase() ? <span className="bg-theme-300/10">{part}</span> : part)}</span>;
 | 
			
		||||
    // eslint-disable-next-line react/no-array-index-key
 | 
			
		||||
    return <span>{parts.map((part, i) => part.toLowerCase() === searchString.toLowerCase() ? <span key={`${searchString}_${i}`} className="bg-theme-300/10">{part}</span> : part)}</span>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={classNames(
 | 
			
		||||
      "relative z-10 ease-in-out duration-300 transition-opacity",
 | 
			
		||||
      "relative z-20 ease-in-out duration-300 transition-opacity",
 | 
			
		||||
      hidden && !isOpen && "hidden",
 | 
			
		||||
      !hidden && isOpen && "opacity-100",
 | 
			
		||||
      !isOpen && "opacity-0",
 | 
			
		||||
    )} role="dialog" aria-modal="true">
 | 
			
		||||
      <div className="fixed inset-0 bg-gray-500 bg-opacity-50" />
 | 
			
		||||
      <div className="fixed inset-0 z-10 overflow-y-auto">
 | 
			
		||||
      <div className="fixed inset-0 z-20 overflow-y-auto">
 | 
			
		||||
        <div className="flex min-h-full min-w-full items-start justify-center text-center">
 | 
			
		||||
          <dialog className="mt-[10%] min-w-[80%] max-w-[90%] md:min-w-[40%] rounded-md p-0 block font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-50 dark:bg-theme-800">
 | 
			
		||||
            <input placeholder="Search" className={classNames(
 | 
			
		||||
@ -147,7 +148,7 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
 | 
			
		||||
                        }
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="text-xs text-theme-600 font-bold pointer-events-none">{r.abbr ? t("quicklaunch.bookmark") : t("quicklaunch.service")}</div>
 | 
			
		||||
                    <div className="text-xs text-theme-600 font-bold pointer-events-none">{r.type === 'service' ? t("quicklaunch.service") : t("quicklaunch.bookmark")}</div>
 | 
			
		||||
                  </button>
 | 
			
		||||
                </li>
 | 
			
		||||
              ))}
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { useContext, useState } from "react";
 | 
			
		||||
 | 
			
		||||
import Status from "./status";
 | 
			
		||||
import Widget from "./widget";
 | 
			
		||||
import Ping from "./ping";
 | 
			
		||||
import KubernetesStatus from "./kubernetes-status";
 | 
			
		||||
 | 
			
		||||
import Docker from "widgets/docker/component";
 | 
			
		||||
@ -32,7 +33,7 @@ export default function Item({ service }) {
 | 
			
		||||
      <div
 | 
			
		||||
        className={`${
 | 
			
		||||
          hasLink ? "cursor-pointer " : " "
 | 
			
		||||
        }transition-all h-15 mb-3 p-1 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10`}
 | 
			
		||||
        }transition-all h-15 mb-3 p-1 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10 relative`}
 | 
			
		||||
      >
 | 
			
		||||
        <div className="flex select-none">
 | 
			
		||||
          {service.icon &&
 | 
			
		||||
@ -72,26 +73,35 @@ export default function Item({ service }) {
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          {service.container && (
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
 | 
			
		||||
              className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer"
 | 
			
		||||
            >
 | 
			
		||||
              <Status service={service} />
 | 
			
		||||
              <span className="sr-only">View container stats</span>
 | 
			
		||||
            </button>
 | 
			
		||||
          )}
 | 
			
		||||
          {service.app && (
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
 | 
			
		||||
              className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer"
 | 
			
		||||
            >
 | 
			
		||||
              <KubernetesStatus service={service} />
 | 
			
		||||
              <span className="sr-only">View container stats</span>
 | 
			
		||||
            </button>
 | 
			
		||||
          )}
 | 
			
		||||
          <div className="absolute top-0 right-0 w-1/2 flex flex-row justify-end gap-2 mr-2">
 | 
			
		||||
              {service.ping && (
 | 
			
		||||
                <div className="flex-shrink-0 flex items-center justify-center cursor-pointer">
 | 
			
		||||
                  <Ping service={service} />
 | 
			
		||||
                  <span className="sr-only">Ping status</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              {service.container && (
 | 
			
		||||
                <button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
 | 
			
		||||
                  className="flex-shrink-0 flex items-center justify-center cursor-pointer"
 | 
			
		||||
                >
 | 
			
		||||
                  <Status service={service} />
 | 
			
		||||
                  <span className="sr-only">View container stats</span>
 | 
			
		||||
                </button>
 | 
			
		||||
              )}
 | 
			
		||||
              {service.app && (
 | 
			
		||||
                <button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
 | 
			
		||||
                  className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer"
 | 
			
		||||
                >
 | 
			
		||||
                  <KubernetesStatus service={service} />
 | 
			
		||||
                  <span className="sr-only">View container stats</span>
 | 
			
		||||
                </button>
 | 
			
		||||
              )}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        {service.container && service.server && (
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										44
									
								
								src/components/services/ping.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/components/services/ping.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import useSWR from "swr";
 | 
			
		||||
 | 
			
		||||
export default function Ping({ service }) {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const { data, error } = useSWR(`/api/ping?${new URLSearchParams({ping: service.ping}).toString()}`, {
 | 
			
		||||
    refreshInterval: 30000
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (error) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden">
 | 
			
		||||
        <div className="text-[8px] font-bold text-rose-500 uppercase">{t("ping.error")}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (!data) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden">
 | 
			
		||||
        <div className="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("ping.ping")}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const statusText = `${service.ping}: HTTP status ${data.status}`;
 | 
			
		||||
  
 | 
			
		||||
  if (data && data.status !== 200) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={statusText}>
 | 
			
		||||
        <div className="text-[8px] font-bold text-rose-500/80">{data.status}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (data && data.status === 200) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={statusText}>
 | 
			
		||||
        <div className="text-[8px] font-bold text-emerald-500/80">{t("common.ms", { value: data.latency, style: "unit", unit: "millisecond", unitDisplay: "narrow", maximumFractionDigits: 0 })}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,19 +1,52 @@
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import useSWR from "swr";
 | 
			
		||||
 | 
			
		||||
export default function Status({ service }) {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const { data, error } = useSWR(`/api/docker/status/${service.container}/${service.server || ""}`);
 | 
			
		||||
 | 
			
		||||
  if (error) {
 | 
			
		||||
    return <div className="w-3 h-3 bg-rose-300 dark:bg-rose-500 rounded-full" />;
 | 
			
		||||
    <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.status}>
 | 
			
		||||
      <div className="text-[8px] font-bold text-rose-500/80 uppercase">{t("docker.error")}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (data && data.status === "running") {
 | 
			
		||||
    return <div className="w-3 h-3 bg-emerald-300 dark:bg-emerald-500 rounded-full" />;
 | 
			
		||||
    if (data.health === "starting") {
 | 
			
		||||
      return (
 | 
			
		||||
        <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.health}>
 | 
			
		||||
          <div className="text-[8px] font-bold text-blue-500/80 uppercase">{data.health}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (data.health === "unhealthy") {
 | 
			
		||||
      return (
 | 
			
		||||
        <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.health}>
 | 
			
		||||
          <div className="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{data.health}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.health ?? data.status}>
 | 
			
		||||
        <div className="text-[8px] font-bold text-emerald-500/80 uppercase">{data.health ?? data.status}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (data && data.status === "not found") {
 | 
			
		||||
    return <div className="h-2.5 w-2.5 bg-orange-400/50 dark:bg-yellow-200/40 -rotate-45" />;
 | 
			
		||||
  if (data && (data.status === "not found" || data.status === "exited")) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.status}>
 | 
			
		||||
        <div className="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{data.status}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <div className="w-3 h-3 bg-black/20 dark:bg-white/40 rounded-full" />;
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden">
 | 
			
		||||
      <div className="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("docker.unknown")}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,9 +8,9 @@ import cachedFetch from "utils/proxy/cached-fetch";
 | 
			
		||||
export default function Version() {
 | 
			
		||||
  const { t, i18n } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const buildTime = process.env.NEXT_PUBLIC_BUILDTIME ?? new Date().toISOString();
 | 
			
		||||
  const revision = process.env.NEXT_PUBLIC_REVISION ?? "dev";
 | 
			
		||||
  const version = process.env.NEXT_PUBLIC_VERSION ?? "dev";
 | 
			
		||||
  const buildTime = process.env.NEXT_PUBLIC_BUILDTIME?.length ? process.env.NEXT_PUBLIC_BUILDTIME : new Date().toISOString();
 | 
			
		||||
  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());
 | 
			
		||||
 | 
			
		||||
@ -36,17 +36,14 @@ export default function Version() {
 | 
			
		||||
            {version} ({revision.substring(0, 7)}, {formatDate(buildTime)})
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
          releaseData &&
 | 
			
		||||
          compareVersions(latestRelease.tag_name, version) > 0 && (
 | 
			
		||||
            <a
 | 
			
		||||
              href={`https://github.com/benphelps/homepage/releases/tag/${version}`}
 | 
			
		||||
              target="_blank"
 | 
			
		||||
              rel="noopener noreferrer"
 | 
			
		||||
              className="ml-2 text-xs text-theme-500 dark:text-theme-400 flex flex-row items-center"
 | 
			
		||||
            >
 | 
			
		||||
              {version} ({revision.substring(0, 7)}, {formatDate(buildTime)})
 | 
			
		||||
            </a>
 | 
			
		||||
          )
 | 
			
		||||
          <a
 | 
			
		||||
            href={`https://github.com/benphelps/homepage/releases/tag/${version}`}
 | 
			
		||||
            target="_blank"
 | 
			
		||||
            rel="noopener noreferrer"
 | 
			
		||||
            className="ml-2 text-xs text-theme-500 dark:text-theme-400 flex flex-row items-center"
 | 
			
		||||
          >
 | 
			
		||||
            {version} ({revision.substring(0, 7)}, {formatDate(buildTime)})
 | 
			
		||||
          </a>
 | 
			
		||||
        )}
 | 
			
		||||
      </span>
 | 
			
		||||
      {version === "main" || version === "dev" || version === "nightly"
 | 
			
		||||
 | 
			
		||||
@ -15,22 +15,21 @@ const textSizes = {
 | 
			
		||||
export default function DateTime({ options }) {
 | 
			
		||||
  const { text_size: textSize, format } = options;
 | 
			
		||||
  const { i18n } = useTranslation();
 | 
			
		||||
  const [date, setDate] = useState(new Date());
 | 
			
		||||
  const [date, setDate] = useState("");
 | 
			
		||||
  
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const dateFormat = new Intl.DateTimeFormat(i18n.language, { ...format });
 | 
			
		||||
    const interval = setInterval(() => {
 | 
			
		||||
      setDate(new Date());
 | 
			
		||||
      setDate(dateFormat.format(new Date()));
 | 
			
		||||
    }, 1000);
 | 
			
		||||
    return () => clearInterval(interval);
 | 
			
		||||
  }, [setDate]);
 | 
			
		||||
 | 
			
		||||
  const dateFormat = new Intl.DateTimeFormat(i18n.language, { ...format });
 | 
			
		||||
  }, [date, setDate, i18n.language, format]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col justify-center first:ml-0 ml-4">
 | 
			
		||||
      <div className="flex flex-row items-center grow justify-end">
 | 
			
		||||
        <span className={`text-theme-800 dark:text-theme-200 ${textSizes[textSize || "lg"]}`}>
 | 
			
		||||
          {dateFormat.format(date)}
 | 
			
		||||
          {date}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import mapIcon from "utils/weather/owm-condition-map";
 | 
			
		||||
import mapIcon from "utils/weather/openmeteo-condition-map";
 | 
			
		||||
 | 
			
		||||
export default function Icon({ condition, timeOfDay }) {
 | 
			
		||||
  const IconComponent = mapIcon(condition, timeOfDay);
 | 
			
		||||
 | 
			
		||||
@ -52,7 +52,7 @@ export default function Memory({ expanded }) {
 | 
			
		||||
      <div className="flex flex-col ml-3 text-left min-w-[85px]">
 | 
			
		||||
        <span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
 | 
			
		||||
          <div className="pl-0.5">
 | 
			
		||||
            {t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024, maximumFractionDigits: 0, binary: true })}
 | 
			
		||||
            {t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024, maximumFractionDigits: 1, binary: true })}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="pr-1">{t("resources.free")}</div>
 | 
			
		||||
        </span>
 | 
			
		||||
@ -61,7 +61,7 @@ export default function Memory({ expanded }) {
 | 
			
		||||
            <div className="pl-0.5">
 | 
			
		||||
              {t("common.bytes", {
 | 
			
		||||
                value: data.memory.totalMemMb * 1024 * 1024,
 | 
			
		||||
                maximumFractionDigits: 0,
 | 
			
		||||
                maximumFractionDigits: 1,
 | 
			
		||||
                binary: true,
 | 
			
		||||
              })}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,7 @@ export default async function handler(req, res) {
 | 
			
		||||
 | 
			
		||||
    return res.status(200).json({
 | 
			
		||||
      status: info.State.Status,
 | 
			
		||||
      health: info.State.Health?.Status
 | 
			
		||||
    });
 | 
			
		||||
  } catch {
 | 
			
		||||
    return res.status(500).send({
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								src/pages/api/ping.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/pages/api/ping.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
import { performance } from "perf_hooks";
 | 
			
		||||
 | 
			
		||||
import createLogger from "utils/logger";
 | 
			
		||||
import { httpProxy } from "utils/proxy/http";
 | 
			
		||||
 | 
			
		||||
const logger = createLogger("ping");
 | 
			
		||||
 | 
			
		||||
export default async function handler(req, res) {
 | 
			
		||||
    const { ping: pingURL } = req.query;
 | 
			
		||||
 | 
			
		||||
    if (!pingURL) {
 | 
			
		||||
        logger.debug("No ping URL specified");
 | 
			
		||||
        return res.status(400).send({
 | 
			
		||||
        error: "No ping URL given",
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    let startTime = performance.now();
 | 
			
		||||
    let [status] = await httpProxy(pingURL, {
 | 
			
		||||
      method: "HEAD"
 | 
			
		||||
    });
 | 
			
		||||
    let endTime = performance.now();
 | 
			
		||||
    
 | 
			
		||||
    if (status >= 400) {
 | 
			
		||||
      // try one more time as a GET in case HEAD is rejected for whatever reason
 | 
			
		||||
      startTime = performance.now();
 | 
			
		||||
      [status] = await httpProxy(pingURL);
 | 
			
		||||
      endTime = performance.now();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return res.status(200).json({
 | 
			
		||||
      status,
 | 
			
		||||
      latency: endTime - startTime
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
import cachedFetch from "utils/proxy/cached-fetch";
 | 
			
		||||
 | 
			
		||||
export default async function handler(req, res) {
 | 
			
		||||
  const { latitude, longitude, units, cache } = req.query;
 | 
			
		||||
  const { latitude, longitude, units, cache, timezone } = req.query;
 | 
			
		||||
  const degrees = units === "imperial" ? "fahrenheit" : "celsius";
 | 
			
		||||
  const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset¤t_weather=true&temperature_unit=${degrees}&timezone=auto`;
 | 
			
		||||
  const timezeone = timezone ?? 'auto'
 | 
			
		||||
  const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset¤t_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`;
 | 
			
		||||
  return res.send(await cachedFetch(apiUrl, cache));
 | 
			
		||||
}
 | 
			
		||||
@ -94,7 +94,8 @@ export async function servicesResponse() {
 | 
			
		||||
    ].flat()),
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const mergedGroups = [];
 | 
			
		||||
  const sortedGroups = [];
 | 
			
		||||
  const unsortedGroups = [];
 | 
			
		||||
  const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null;
 | 
			
		||||
 | 
			
		||||
  mergedGroupsNames.forEach((groupName) => {
 | 
			
		||||
@ -113,12 +114,12 @@ export async function servicesResponse() {
 | 
			
		||||
 | 
			
		||||
    if (definedLayouts) {
 | 
			
		||||
      const layoutIndex = definedLayouts.findIndex(layout => layout === mergedGroup.name);
 | 
			
		||||
      if (layoutIndex > -1) mergedGroups.splice(layoutIndex, 0, mergedGroup);
 | 
			
		||||
      else mergedGroups.push(mergedGroup);
 | 
			
		||||
      if (layoutIndex > -1) sortedGroups[layoutIndex] = mergedGroup;
 | 
			
		||||
      else unsortedGroups.push(mergedGroup);
 | 
			
		||||
    } else {
 | 
			
		||||
      mergedGroups.push(mergedGroup);
 | 
			
		||||
      unsortedGroups.push(mergedGroup);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return mergedGroups;
 | 
			
		||||
  return [...sortedGroups.filter(g => g), ...unsortedGroups];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,5 +32,5 @@ export function getSettings() {
 | 
			
		||||
 | 
			
		||||
  const settingsYaml = join(process.cwd(), "config", "settings.yaml");
 | 
			
		||||
  const fileContents = readFileSync(settingsYaml, "utf8");
 | 
			
		||||
  return yaml.load(fileContents);
 | 
			
		||||
  return yaml.load(fileContents) ?? {};
 | 
			
		||||
}
 | 
			
		||||
@ -202,6 +202,7 @@ export function cleanServiceGroups(groups) {
 | 
			
		||||
          container,
 | 
			
		||||
          currency, // coinmarketcap widget
 | 
			
		||||
          symbols,
 | 
			
		||||
          defaultinterval
 | 
			
		||||
          namespace, // kubernetes widget
 | 
			
		||||
          app
 | 
			
		||||
        } = cleanedService.widget;
 | 
			
		||||
@ -215,6 +216,7 @@ export function cleanServiceGroups(groups) {
 | 
			
		||||
 | 
			
		||||
        if (currency) cleanedService.widget.currency = currency;
 | 
			
		||||
        if (symbols) cleanedService.widget.symbols = symbols;
 | 
			
		||||
        if (defaultinterval) cleanedService.widget.defaultinterval = defaultinterval;
 | 
			
		||||
 | 
			
		||||
        if (type === "docker") {
 | 
			
		||||
          if (server) cleanedService.widget.server = server;
 | 
			
		||||
 | 
			
		||||
@ -4,10 +4,15 @@ import { format as utilFormat } from "node:util";
 | 
			
		||||
 | 
			
		||||
import winston from "winston";
 | 
			
		||||
 | 
			
		||||
import checkAndCopyConfig, { getSettings } from "utils/config/config";
 | 
			
		||||
 | 
			
		||||
let winstonLogger;
 | 
			
		||||
 | 
			
		||||
function init() {
 | 
			
		||||
  const configPath = join(process.cwd(), "config");
 | 
			
		||||
  checkAndCopyConfig("settings.yaml");
 | 
			
		||||
  const settings = getSettings();
 | 
			
		||||
  const logpath = settings.logpath || configPath;
 | 
			
		||||
 | 
			
		||||
  function combineMessageAndSplat() {
 | 
			
		||||
    return {
 | 
			
		||||
@ -57,7 +62,7 @@ function init() {
 | 
			
		||||
          winston.format.timestamp(),
 | 
			
		||||
          winston.format.printf(messageFormatter)
 | 
			
		||||
        ),
 | 
			
		||||
        filename: `${configPath}/logs/homepage.log`,
 | 
			
		||||
        filename: `${logpath}/logs/homepage.log`,
 | 
			
		||||
        handleExceptions: true,
 | 
			
		||||
        handleRejections: true,
 | 
			
		||||
      }),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										82
									
								
								src/utils/proxy/handlers/jsonrpc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/utils/proxy/handlers/jsonrpc.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
			
		||||
import { JSONRPCClient, JSONRPCErrorException } from "json-rpc-2.0";
 | 
			
		||||
 | 
			
		||||
import { formatApiCall } from "utils/proxy/api-helpers";
 | 
			
		||||
import { httpProxy } from "utils/proxy/http";
 | 
			
		||||
import getServiceWidget from "utils/config/service-helpers";
 | 
			
		||||
import createLogger from "utils/logger";
 | 
			
		||||
import widgets from "widgets/widgets";
 | 
			
		||||
 | 
			
		||||
const logger = createLogger("jsonrpcProxyHandler");
 | 
			
		||||
 | 
			
		||||
export async function sendJsonRpcRequest(url, method, params, username, password) {
 | 
			
		||||
  const headers = {
 | 
			
		||||
    "content-type": "application/json",
 | 
			
		||||
    "accept": "application/json"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (username && password) {
 | 
			
		||||
    headers.authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const client = new JSONRPCClient(async (rpcRequest) => {
 | 
			
		||||
    const body = JSON.stringify(rpcRequest);
 | 
			
		||||
    const httpRequestParams = {
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      headers,
 | 
			
		||||
      body
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line no-unused-vars
 | 
			
		||||
    const [status, contentType, data] = await httpProxy(url, httpRequestParams);
 | 
			
		||||
    if (status === 200) {
 | 
			
		||||
      const json = JSON.parse(data.toString());
 | 
			
		||||
 | 
			
		||||
      // in order to get access to the underlying error object in the JSON response
 | 
			
		||||
      // you must set `result` equal to undefined
 | 
			
		||||
      if (json.error && (json.result === null)) {
 | 
			
		||||
        json.result = undefined;
 | 
			
		||||
      }
 | 
			
		||||
      return client.receive(json);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Promise.reject(data?.error ? data : new Error(data.toString()));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await client.request(method, params);
 | 
			
		||||
    return [200, "application/json", JSON.stringify(response)];
 | 
			
		||||
  }
 | 
			
		||||
  catch (e) {
 | 
			
		||||
    if (e instanceof JSONRPCErrorException) {
 | 
			
		||||
      logger.debug("Error calling JSONPRC endpoint: %s.  %s", url, e.message);
 | 
			
		||||
      return [200, "application/json", JSON.stringify({result: null, error: {code: e.code, message: e.message}})];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logger.warn("Error calling JSONPRC endpoint: %s.  %s", url, e);
 | 
			
		||||
    return [500, "application/json", JSON.stringify({result: null, error: {code: 2, message: e.toString()}})];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async function jsonrpcProxyHandler(req, res) {
 | 
			
		||||
  const { group, service, endpoint: method } = req.query;
 | 
			
		||||
 | 
			
		||||
  if (group && service) {
 | 
			
		||||
    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" });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (widget) {
 | 
			
		||||
      const url = formatApiCall(api, { ...widget });
 | 
			
		||||
 | 
			
		||||
      // eslint-disable-next-line no-unused-vars
 | 
			
		||||
      const [status, contentType, data] = await sendJsonRpcRequest(url, method, null, widget.username, widget.password);
 | 
			
		||||
      return res.status(status).end(data);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group);
 | 
			
		||||
  return res.status(400).json({ error: "Invalid proxy service type" });
 | 
			
		||||
}
 | 
			
		||||
@ -18,10 +18,15 @@ function addCookieHandler(url, params) {
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function httpsRequest(url, params) {
 | 
			
		||||
function handleRequest(requestor, url, params) {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    addCookieHandler(url, params);
 | 
			
		||||
    const request = https.request(url, params, (response) => {
 | 
			
		||||
    if (params?.body) {
 | 
			
		||||
      params.headers = params.headers ?? {};
 | 
			
		||||
      params.headers['content-length'] = Buffer.byteLength(params.body);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const request = requestor.request(url, params, (response) => {
 | 
			
		||||
      const data = [];
 | 
			
		||||
 | 
			
		||||
      response.on("data", (chunk) => {
 | 
			
		||||
@ -38,7 +43,7 @@ export function httpsRequest(url, params) {
 | 
			
		||||
      reject([500, error]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (params.body) {
 | 
			
		||||
    if (params?.body) {
 | 
			
		||||
      request.write(params.body);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -46,32 +51,12 @@ export function httpsRequest(url, params) {
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function httpsRequest(url, params) {
 | 
			
		||||
  return handleRequest(https, url, params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function httpRequest(url, params) {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    addCookieHandler(url, params);
 | 
			
		||||
    const request = http.request(url, params, (response) => {
 | 
			
		||||
      const data = [];
 | 
			
		||||
 | 
			
		||||
      response.on("data", (chunk) => {
 | 
			
		||||
        data.push(chunk);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      response.on("end", () => {
 | 
			
		||||
        addCookieToJar(url, response.headers);
 | 
			
		||||
        resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    request.on("error", (error) => {
 | 
			
		||||
      reject([500, error]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (params.body) {
 | 
			
		||||
      request.write(params.body);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    request.end();
 | 
			
		||||
  });
 | 
			
		||||
  return handleRequest(http, url, params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function httpProxy(url, params = {}) {
 | 
			
		||||
@ -96,7 +81,7 @@ export async function httpProxy(url, params = {}) {
 | 
			
		||||
    return [status, contentType, data, responseHeaders];
 | 
			
		||||
  }
 | 
			
		||||
  catch (err) {
 | 
			
		||||
    logger.error("Error calling %s//%s%s...", url.protocol, url.hostname, url.pathname);
 | 
			
		||||
    logger.error("Error calling %s//%s%s...", constructedUrl.protocol, constructedUrl.hostname, constructedUrl.pathname);
 | 
			
		||||
    logger.error(err);
 | 
			
		||||
    return [500, "application/json", { error: {message: err?.message ?? "Unknown error", url, rawError: err} }, null];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										211
									
								
								src/utils/weather/openmeteo-condition-map.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								src/utils/weather/openmeteo-condition-map.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,211 @@
 | 
			
		||||
import * as Icons from "react-icons/wi";
 | 
			
		||||
 | 
			
		||||
// see https://open-meteo.com/en/docs
 | 
			
		||||
 | 
			
		||||
const conditions = [
 | 
			
		||||
  {
 | 
			
		||||
    code: 1,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayCloudy,
 | 
			
		||||
      night: Icons.WiNightAltCloudy,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 2,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayCloudy,
 | 
			
		||||
      night: Icons.WiNightAltCloudy,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 3,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayCloudy,
 | 
			
		||||
      night: Icons.WiNightAltCloudy,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 45,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayFog,
 | 
			
		||||
      night: Icons.WiNightFog,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 48,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayFog,
 | 
			
		||||
      night: Icons.WiNightFog,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 51,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySprinkle,
 | 
			
		||||
      night: Icons.WiNightAltSprinkle,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 53,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySprinkle,
 | 
			
		||||
      night: Icons.WiNightAltSprinkle,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 55,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySprinkle,
 | 
			
		||||
      night: Icons.WiNightAltSprinkle,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 56,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySleet,
 | 
			
		||||
      night: Icons.WiNightAltSleet,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 57,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySleet,
 | 
			
		||||
      night: Icons.WiNightAltSleet,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 61,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayShowers,
 | 
			
		||||
      night: Icons.WiNightAltShowers,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 63,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayShowers,
 | 
			
		||||
      night: Icons.WiNightAltShowers,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 65,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayShowers,
 | 
			
		||||
      night: Icons.WiNightAltShowers,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 66,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySleet,
 | 
			
		||||
      night: Icons.WiNightAltSleet,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 67,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySleet,
 | 
			
		||||
      night: Icons.WiNightAltSleet,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 71,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySnow,
 | 
			
		||||
      night: Icons.WiNightAltSnow,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 73,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySnow,
 | 
			
		||||
      night: Icons.WiNightAltSnow,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 75,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySnow,
 | 
			
		||||
      night: Icons.WiNightAltSnow,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 77,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySnow,
 | 
			
		||||
      night: Icons.WiNightAltSnow,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 80,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySnow,
 | 
			
		||||
      night: Icons.WiNightAltSnow,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 81,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySnow,
 | 
			
		||||
      night: Icons.WiNightAltSnow,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 82,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySnow,
 | 
			
		||||
      night: Icons.WiNightAltSnow,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 85,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySnow,
 | 
			
		||||
      night: Icons.WiNightAltSnow,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 86,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDaySnow,
 | 
			
		||||
      night: Icons.WiNightAltSnow,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 95,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayThunderstorm,
 | 
			
		||||
      night: Icons.WiNightAltThunderstorm,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 96,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayThunderstorm,
 | 
			
		||||
      night: Icons.WiNightAltThunderstorm,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    code: 99,
 | 
			
		||||
    icon: {
 | 
			
		||||
      day: Icons.WiDayThunderstorm,
 | 
			
		||||
      night: Icons.WiNightAltThunderstorm,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default function mapIcon(weatherStatusCode, timeOfDay) {
 | 
			
		||||
  const mapping = conditions.find((condition) => condition.code === weatherStatusCode);
 | 
			
		||||
 | 
			
		||||
  if (mapping) {
 | 
			
		||||
    if (timeOfDay === "day") {
 | 
			
		||||
      return mapping.icon.day;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (timeOfDay === "night") {
 | 
			
		||||
      return mapping.icon.night;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return Icons.WiDaySunny;
 | 
			
		||||
}
 | 
			
		||||
@ -14,6 +14,11 @@ export default function Component({ service }) {
 | 
			
		||||
  if (error) {
 | 
			
		||||
    return <Container error={error} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!data) {
 | 
			
		||||
    return <Container service={service} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const totalObserved = Object.keys(data).length;
 | 
			
		||||
  let diffsDetected = 0;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,11 +17,12 @@ export default function Component({ service }) {
 | 
			
		||||
    { label: t("coinmarketcap.30days"), value: "30d" },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const [dateRange, setDateRange] = useState(dateRangeOptions[0].value);
 | 
			
		||||
 | 
			
		||||
  const { widget } = service;
 | 
			
		||||
  const { symbols } = widget;
 | 
			
		||||
  const currencyCode = widget.currency ?? "USD";
 | 
			
		||||
  const interval = widget.defaultinterval ?? dateRangeOptions[0].value;
 | 
			
		||||
 | 
			
		||||
  const [dateRange, setDateRange] = useState(interval);
 | 
			
		||||
 | 
			
		||||
  const { data: statsData, error: statsError } = useWidgetAPI(widget, "v1/cryptocurrency/quotes/latest", {
 | 
			
		||||
    symbol: `${symbols.join(",")}`,
 | 
			
		||||
 | 
			
		||||
@ -7,9 +7,12 @@ const components = {
 | 
			
		||||
  bazarr: dynamic(() => import("./bazarr/component")),
 | 
			
		||||
  changedetectionio: dynamic(() => import("./changedetectionio/component")),
 | 
			
		||||
  coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
 | 
			
		||||
  deluge: dynamic(() => import("./deluge/component")),
 | 
			
		||||
  diskstation: dynamic(() => import("./diskstation/component")),
 | 
			
		||||
  docker: dynamic(() => import("./docker/component")),
 | 
			
		||||
  kubernetes: dynamic(() => import("./kubernetes/component")),
 | 
			
		||||
  emby: dynamic(() => import("./emby/component")),
 | 
			
		||||
  flood: dynamic(() => import("./flood/component")),
 | 
			
		||||
  gluetun: dynamic(() => import("./gluetun/component")),
 | 
			
		||||
  gotify: dynamic(() => import("./gotify/component")),
 | 
			
		||||
  hdhomerun: dynamic(() => import("./hdhomerun/component")),
 | 
			
		||||
@ -24,6 +27,7 @@ const components = {
 | 
			
		||||
  nzbget: dynamic(() => import("./nzbget/component")),
 | 
			
		||||
  ombi: dynamic(() => import("./ombi/component")),
 | 
			
		||||
  overseerr: dynamic(() => import("./overseerr/component")),
 | 
			
		||||
  paperlessngx: dynamic(() => import("./paperlessngx/component")),
 | 
			
		||||
  pihole: dynamic(() => import("./pihole/component")),
 | 
			
		||||
  plex: dynamic(() => import("./plex/component")),
 | 
			
		||||
  portainer: dynamic(() => import("./portainer/component")),
 | 
			
		||||
@ -35,6 +39,7 @@ const components = {
 | 
			
		||||
  readarr: dynamic(() => import("./readarr/component")),
 | 
			
		||||
  rutorrent: dynamic(() => import("./rutorrent/component")),
 | 
			
		||||
  sabnzbd: dynamic(() => import("./sabnzbd/component")),
 | 
			
		||||
  scrutiny: dynamic(() => import("./scrutiny/component")),
 | 
			
		||||
  sonarr: dynamic(() => import("./sonarr/component")),
 | 
			
		||||
  speedtest: dynamic(() => import("./speedtest/component")),
 | 
			
		||||
  strelaysrv: dynamic(() => import("./strelaysrv/component")),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										52
									
								
								src/widgets/deluge/component.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/widgets/deluge/component.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
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: torrentData, error: torrentError } = useWidgetAPI(widget);
 | 
			
		||||
 | 
			
		||||
  if (torrentError) {
 | 
			
		||||
    return <Container error={torrentError} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!torrentData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="deluge.leech" />
 | 
			
		||||
        <Block label="deluge.download" />
 | 
			
		||||
        <Block label="deluge.seed" />
 | 
			
		||||
        <Block label="deluge.upload" />
 | 
			
		||||
      </Container>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { torrents } = torrentData;
 | 
			
		||||
  const keys = torrents ? Object.keys(torrents) : [];
 | 
			
		||||
 | 
			
		||||
  let rateDl = 0;
 | 
			
		||||
  let rateUl = 0;
 | 
			
		||||
  let completed = 0;
 | 
			
		||||
  for (let i = 0; i < keys.length; i += 1) {
 | 
			
		||||
    const torrent = torrents[keys[i]];
 | 
			
		||||
    rateDl += torrent.download_payload_rate;
 | 
			
		||||
    rateUl += torrent.upload_payload_rate;
 | 
			
		||||
    completed += torrent.total_remaining === 0 ? 1 : 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const leech = keys.length - completed || 0;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container service={service}>
 | 
			
		||||
      <Block label="deluge.leech" value={t("common.number", { value: leech })} />
 | 
			
		||||
      <Block label="deluge.download" value={t("common.bitrate", { value: rateDl })} />
 | 
			
		||||
      <Block label="deluge.seed" value={t("common.number", { value: completed })} />
 | 
			
		||||
      <Block label="deluge.upload" value={t("common.bitrate", { value: rateUl })} />
 | 
			
		||||
    </Container>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								src/widgets/deluge/proxy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/widgets/deluge/proxy.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,63 @@
 | 
			
		||||
import { formatApiCall } from "utils/proxy/api-helpers";
 | 
			
		||||
import { sendJsonRpcRequest } from "utils/proxy/handlers/jsonrpc";
 | 
			
		||||
import getServiceWidget from "utils/config/service-helpers";
 | 
			
		||||
import createLogger from "utils/logger";
 | 
			
		||||
import widgets from "widgets/widgets";
 | 
			
		||||
 | 
			
		||||
const logger = createLogger("delugeProxyHandler");
 | 
			
		||||
 | 
			
		||||
const dataMethod = "web.update_ui";
 | 
			
		||||
const dataParams = [
 | 
			
		||||
  ["queue", "name", "total_wanted", "state", "progress", "download_payload_rate", "upload_payload_rate", "total_remaining"],
 | 
			
		||||
  {}
 | 
			
		||||
];
 | 
			
		||||
const loginMethod = "auth.login";
 | 
			
		||||
 | 
			
		||||
async function sendRpc(url, method, params) {
 | 
			
		||||
  const [status, contentType, data] = await sendJsonRpcRequest(url, method, params);
 | 
			
		||||
  const json = JSON.parse(data.toString());
 | 
			
		||||
  if (json?.error) {
 | 
			
		||||
    if (json.error.code === 1) {
 | 
			
		||||
      return [403, contentType, data];
 | 
			
		||||
    }
 | 
			
		||||
    return [500, contentType, data];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [status, contentType, data];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function login(url, password) {
 | 
			
		||||
  return sendRpc(url, loginMethod, [password]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async function delugeProxyHandler(req, res) {
 | 
			
		||||
  const { group, service } = 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 api = widgets?.[widget.type]?.api
 | 
			
		||||
  const url = new URL(formatApiCall(api, { ...widget }));
 | 
			
		||||
 | 
			
		||||
  let [status, contentType, data] = await sendRpc(url, dataMethod, dataParams);
 | 
			
		||||
  if (status === 403) {
 | 
			
		||||
    [status, contentType, data] = await login(url, widget.password);
 | 
			
		||||
    if (status !== 200) {
 | 
			
		||||
      return res.status(status).end(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line no-unused-vars
 | 
			
		||||
    [status, contentType, data] = await sendRpc(url, dataMethod, dataParams);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return res.status(status).end(data);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/widgets/deluge/widget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/widgets/deluge/widget.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
import delugeProxyHandler from "./proxy";
 | 
			
		||||
 | 
			
		||||
const widget = {
 | 
			
		||||
  api: "{url}/json",
 | 
			
		||||
  proxyHandler: delugeProxyHandler,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default widget;
 | 
			
		||||
							
								
								
									
										41
									
								
								src/widgets/diskstation/component.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/widgets/diskstation/component.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
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: listData, error: listError } = useWidgetAPI(widget, "list");
 | 
			
		||||
 | 
			
		||||
  if (listError) {
 | 
			
		||||
    return <Container error={listError} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const tasks = listData?.data?.tasks;
 | 
			
		||||
  if (!tasks) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="diskstation.leech" />
 | 
			
		||||
        <Block label="diskstation.download" />
 | 
			
		||||
        <Block label="diskstation.seed" />
 | 
			
		||||
        <Block label="diskstation.upload" />
 | 
			
		||||
      </Container>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const rateDl = tasks.reduce((acc, task) => acc + (task?.additional?.transfer?.speed_download ?? 0), 0);
 | 
			
		||||
  const rateUl = tasks.reduce((acc, task) => acc + (task?.additional?.transfer?.speed_upload ?? 0), 0);
 | 
			
		||||
  const completed = tasks.filter((task) => task?.additional?.transfer?.size_downloaded === task?.size)?.length || 0;
 | 
			
		||||
  const leech = tasks.length - completed || 0;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container service={service}>
 | 
			
		||||
      <Block label="diskstation.leech" value={t("common.number", { value: leech })} />
 | 
			
		||||
      <Block label="diskstation.download" value={t("common.bitrate", { value: rateDl })} />
 | 
			
		||||
      <Block label="diskstation.seed" value={t("common.number", { value: completed })} />
 | 
			
		||||
      <Block label="diskstation.upload" value={t("common.bitrate", { value: rateUl })} />
 | 
			
		||||
    </Container>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								src/widgets/diskstation/proxy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/widgets/diskstation/proxy.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
			
		||||
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("diskstationProxyHandler");
 | 
			
		||||
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) {
 | 
			
		||||
  const loginUrl = formatApiCall(authApi, widget);
 | 
			
		||||
  const [status, contentType, data] = await httpProxy(loginUrl);
 | 
			
		||||
  if (status !== 200) {
 | 
			
		||||
    return [status, contentType, data];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const json = JSON.parse(data.toString());
 | 
			
		||||
  if (json?.success !== true) {
 | 
			
		||||
    // from https://global.download.synology.com/download/Document/Software/DeveloperGuide/Package/DownloadStation/All/enu/Synology_Download_Station_Web_API.pdf
 | 
			
		||||
    /*
 | 
			
		||||
      Code Description
 | 
			
		||||
      400  No such account or incorrect password
 | 
			
		||||
      401  Account disabled
 | 
			
		||||
      402  Permission denied
 | 
			
		||||
      403  2-step verification code required
 | 
			
		||||
      404  Failed to authenticate 2-step verification code
 | 
			
		||||
    */
 | 
			
		||||
    let message = "Authentication failed.";
 | 
			
		||||
    if (json?.error?.code >= 403) message += " 2FA enabled.";
 | 
			
		||||
    logger.warn("Unable to login.  Code: %d", json?.error?.code);
 | 
			
		||||
    return [401, "application/json", JSON.stringify({ code: json?.error?.code, message })];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [status, contentType, data];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async function diskstationProxyHandler(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 });
 | 
			
		||||
  let [status, contentType, data] = await httpProxy(url);
 | 
			
		||||
  if (status !== 200) {
 | 
			
		||||
    logger.debug("Error %d calling endpoint %s", status, url);
 | 
			
		||||
    return res.status(status, data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const json = JSON.parse(data.toString());
 | 
			
		||||
  if (json?.success !== true) {
 | 
			
		||||
    logger.debug("Logging in to DiskStation");
 | 
			
		||||
    [status, contentType, data] = await login(widget);
 | 
			
		||||
    if (status !== 200) {
 | 
			
		||||
      return res.status(status).end(data)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [status, contentType, data] = await httpProxy(url);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (contentType) res.setHeader("Content-Type", contentType);
 | 
			
		||||
  return res.status(status).send(data);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								src/widgets/diskstation/widget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/widgets/diskstation/widget.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
import diskstationProxyHandler from "./proxy";
 | 
			
		||||
 | 
			
		||||
const widget = {
 | 
			
		||||
  api: "{url}/webapi/DownloadStation/task.cgi?api=SYNO.DownloadStation.Task&version=1&method={endpoint}",
 | 
			
		||||
  proxyHandler: diskstationProxyHandler,
 | 
			
		||||
 | 
			
		||||
  mappings: {
 | 
			
		||||
    "list": {
 | 
			
		||||
      endpoint: "list&additional=transfer",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default widget;
 | 
			
		||||
@ -46,7 +46,9 @@ export default function Component({ service }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Container service={service}>
 | 
			
		||||
      <Block label="docker.cpu" value={t("common.percent", { value: calculateCPUPercent(statsData.stats) })} />
 | 
			
		||||
      <Block label="docker.mem" value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
 | 
			
		||||
      {statsData.stats.memory_stats.usage && 
 | 
			
		||||
        <Block label="docker.mem" value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
 | 
			
		||||
      }
 | 
			
		||||
      {network && (
 | 
			
		||||
        <>
 | 
			
		||||
          <Block label="docker.rx" value={t("common.bytes", { value: network.rx_bytes })} />
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										53
									
								
								src/widgets/flood/component.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/widgets/flood/component.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
			
		||||
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: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents");
 | 
			
		||||
 | 
			
		||||
  if (torrentError || !torrentData?.torrents) {
 | 
			
		||||
    return <Container error={torrentError ?? {message: "No torrent data returned"}} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!torrentData || !torrentData.torrents) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="flood.leech" />
 | 
			
		||||
        <Block label="flood.download" />
 | 
			
		||||
        <Block label="flood.seed" />
 | 
			
		||||
        <Block label="flood.upload" />
 | 
			
		||||
      </Container>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let rateDl = 0;
 | 
			
		||||
  let rateUl = 0;
 | 
			
		||||
  let completed = 0;
 | 
			
		||||
  let leech = 0;
 | 
			
		||||
 | 
			
		||||
  Object.values(torrentData.torrents).forEach(torrent => {
 | 
			
		||||
    rateDl += torrent.downRate;
 | 
			
		||||
    rateUl += torrent.upRate;
 | 
			
		||||
    if(torrent.status.includes('complete')){
 | 
			
		||||
      completed += 1;
 | 
			
		||||
    }
 | 
			
		||||
    if(torrent.status.includes('downloading')){
 | 
			
		||||
      leech += 1;
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container service={service}>
 | 
			
		||||
      <Block label="flood.leech" value={t("common.number", { value: leech })} />
 | 
			
		||||
      <Block label="flood.download" value={t("common.bitrate", { value: rateDl })} />
 | 
			
		||||
      <Block label="flood.seed" value={t("common.number", { value: completed })} />
 | 
			
		||||
      <Block label="flood.upload" value={t("common.bitrate", { value: rateUl })} />
 | 
			
		||||
    </Container>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								src/widgets/flood/proxy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/widgets/flood/proxy.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
import { formatApiCall } from "utils/proxy/api-helpers";
 | 
			
		||||
import { httpProxy } from "utils/proxy/http";
 | 
			
		||||
import getServiceWidget from "utils/config/service-helpers";
 | 
			
		||||
import createLogger from "utils/logger";
 | 
			
		||||
 | 
			
		||||
const logger = createLogger("floodProxyHandler");
 | 
			
		||||
 | 
			
		||||
async function login(widget) {
 | 
			
		||||
  logger.debug("flood is rejecting the request, logging in.");
 | 
			
		||||
  const loginUrl = new URL(`${widget.url}/api/auth/authenticate`).toString();
 | 
			
		||||
 | 
			
		||||
  const loginParams = { 
 | 
			
		||||
    method: "POST", 
 | 
			
		||||
    headers: { "Content-Type": "application/json" }, 
 | 
			
		||||
    body: null
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (widget.username && widget.password) {
 | 
			
		||||
    loginParams.body = JSON.stringify({
 | 
			
		||||
      "username": widget.username,
 | 
			
		||||
      "password": widget.password
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // eslint-disable-next-line no-unused-vars
 | 
			
		||||
  const [status, contentType, data] = await httpProxy(loginUrl, loginParams);
 | 
			
		||||
  return [status, data];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async function floodProxyHandler(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("{url}/api/{endpoint}", { endpoint, ...widget }));
 | 
			
		||||
  const params = { method: "GET", headers: {} };
 | 
			
		||||
 | 
			
		||||
  let [status, contentType, data] = await httpProxy(url, params);
 | 
			
		||||
  if (status === 401) {
 | 
			
		||||
    [status, data] = await login(widget);
 | 
			
		||||
 | 
			
		||||
    if (status !== 200) {
 | 
			
		||||
      logger.error("HTTP %d logging in to flood.  Data: %s", status, data);
 | 
			
		||||
      return res.status(status).end(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [status, contentType, data] = await httpProxy(url, params);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (status !== 200) {
 | 
			
		||||
    logger.error("HTTP %d getting data from flood.  Data: %s", status, data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (contentType) res.setHeader("Content-Type", contentType);
 | 
			
		||||
  return res.status(status).send(data);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/widgets/flood/widget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/widgets/flood/widget.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
import floodProxyHandler from "./proxy";
 | 
			
		||||
 | 
			
		||||
const widget = {
 | 
			
		||||
  proxyHandler: floodProxyHandler,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default widget;
 | 
			
		||||
@ -20,12 +20,18 @@ async function login(loginUrl, username, password) {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const status = authResponse[0];
 | 
			
		||||
  const data = JSON.parse(Buffer.from(authResponse[2]).toString());
 | 
			
		||||
  let data = authResponse[2];
 | 
			
		||||
 | 
			
		||||
  if (status === 200) {
 | 
			
		||||
    cache.put(tokenCacheKey, data.token);
 | 
			
		||||
  try {
 | 
			
		||||
    data = JSON.parse(Buffer.from(authResponse[2]).toString());
 | 
			
		||||
    
 | 
			
		||||
    if (status === 200) {
 | 
			
		||||
      const expiration = new Date(data.expires) - Date.now();
 | 
			
		||||
      cache.put(tokenCacheKey, data.token, expiration - (5 * 60 * 1000)); // expiration -5 minutes
 | 
			
		||||
    }
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    logger.error(`Error ${status} logging into npm`, authResponse[2]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [status, data.token ?? data];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -51,8 +57,8 @@ export default async function npmProxyHandler(req, res) {
 | 
			
		||||
      if (!token) {
 | 
			
		||||
        [status, token] = await login(loginUrl, widget.username, widget.password);
 | 
			
		||||
        if (status !== 200) {
 | 
			
		||||
          logger.debug(`HTTTP ${status} logging into npm api: ${data}`);
 | 
			
		||||
          return res.status(status).send(data);
 | 
			
		||||
          logger.debug(`HTTTP ${status} logging into npm api: ${token}`);
 | 
			
		||||
          return res.status(status).send(token);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,40 +0,0 @@
 | 
			
		||||
import { JSONRPCClient } from "json-rpc-2.0";
 | 
			
		||||
 | 
			
		||||
import getServiceWidget from "utils/config/service-helpers";
 | 
			
		||||
 | 
			
		||||
export default async function nzbgetProxyHandler(req, res) {
 | 
			
		||||
  const { group, service, endpoint } = req.query;
 | 
			
		||||
 | 
			
		||||
  if (group && service) {
 | 
			
		||||
    const widget = await getServiceWidget(group, service);
 | 
			
		||||
 | 
			
		||||
    if (widget) {
 | 
			
		||||
      const constructedUrl = new URL(widget.url);
 | 
			
		||||
      constructedUrl.pathname = "jsonrpc";
 | 
			
		||||
 | 
			
		||||
      const authorization = Buffer.from(`${widget.username}:${widget.password}`).toString("base64");
 | 
			
		||||
 | 
			
		||||
      const client = new JSONRPCClient((jsonRPCRequest) =>
 | 
			
		||||
        fetch(constructedUrl.toString(), {
 | 
			
		||||
          method: "POST",
 | 
			
		||||
          headers: {
 | 
			
		||||
            "content-type": "application/json",
 | 
			
		||||
            authorization: `Basic ${authorization}`,
 | 
			
		||||
          },
 | 
			
		||||
          body: JSON.stringify(jsonRPCRequest),
 | 
			
		||||
        }).then(async (response) => {
 | 
			
		||||
          if (response.status === 200) {
 | 
			
		||||
            const jsonRPCResponse = await response.json();
 | 
			
		||||
            return client.receive(jsonRPCResponse);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return Promise.reject(new Error(response.statusText));
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return res.send(await client.request(endpoint));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return res.status(400).json({ error: "Invalid proxy service type" });
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
import nzbgetProxyHandler from "./proxy";
 | 
			
		||||
import jsonrpcProxyHandler from "utils/proxy/handlers/jsonrpc";
 | 
			
		||||
 | 
			
		||||
const widget = {
 | 
			
		||||
  proxyHandler: nzbgetProxyHandler,
 | 
			
		||||
  api: "{url}/jsonrpc",
 | 
			
		||||
  proxyHandler: jsonrpcProxyHandler,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default widget;
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ export default function Component({ service }) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="overseerr.pending" />
 | 
			
		||||
        <Block label="overseerr.processing" />
 | 
			
		||||
        <Block label="overseerr.approved" />
 | 
			
		||||
        <Block label="overseerr.available" />
 | 
			
		||||
      </Container>
 | 
			
		||||
@ -24,6 +25,7 @@ export default function Component({ service }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Container service={service}>
 | 
			
		||||
      <Block label="overseerr.pending" value={statsData.pending} />
 | 
			
		||||
      <Block label="overseerr.processing" value={statsData.processing} />
 | 
			
		||||
      <Block label="overseerr.approved" value={statsData.approved} />
 | 
			
		||||
      <Block label="overseerr.available" value={statsData.available} />
 | 
			
		||||
    </Container>
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ const widget = {
 | 
			
		||||
      endpoint: "request/count",
 | 
			
		||||
      validate: [
 | 
			
		||||
        "pending",
 | 
			
		||||
        "processing",
 | 
			
		||||
        "approved",
 | 
			
		||||
        "available",
 | 
			
		||||
      ],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										29
									
								
								src/widgets/paperlessngx/component.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/widgets/paperlessngx/component.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
import Container from "components/services/widget/container";
 | 
			
		||||
import Block from "components/services/widget/block";
 | 
			
		||||
import useWidgetAPI from "utils/proxy/use-widget-api";
 | 
			
		||||
 | 
			
		||||
export default function Component({ service }) {
 | 
			
		||||
  const { widget } = service;
 | 
			
		||||
 | 
			
		||||
  const { data: statisticsData, error: statisticsError } = useWidgetAPI(widget, "statistics");
 | 
			
		||||
 | 
			
		||||
  if (statisticsError) {
 | 
			
		||||
    return <Container error={statisticsError} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!statisticsData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="paperlessngx.inbox" />
 | 
			
		||||
        <Block label="paperlessngx.total" />
 | 
			
		||||
      </Container>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container service={service}>
 | 
			
		||||
      {statisticsData.documents_inbox !== undefined && <Block label="paperlessngx.inbox" value={statisticsData.documents_inbox} />}
 | 
			
		||||
      <Block label="paperlessngx.total" value={statisticsData.documents_total} />
 | 
			
		||||
    </Container>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/widgets/paperlessngx/widget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/widgets/paperlessngx/widget.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
import genericProxyHandler from "utils/proxy/handlers/generic";
 | 
			
		||||
 | 
			
		||||
const widget = {
 | 
			
		||||
  api: "{url}/api/{endpoint}",
 | 
			
		||||
  proxyHandler: genericProxyHandler,
 | 
			
		||||
 | 
			
		||||
  mappings: {
 | 
			
		||||
    "statistics": {
 | 
			
		||||
      endpoint: "statistics/?format=json",
 | 
			
		||||
      validate: [
 | 
			
		||||
        "documents_total"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default widget;
 | 
			
		||||
@ -1,30 +1,23 @@
 | 
			
		||||
import { formatApiCall } from "utils/proxy/api-helpers";
 | 
			
		||||
import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar";
 | 
			
		||||
import { httpProxy } from "utils/proxy/http";
 | 
			
		||||
import getServiceWidget from "utils/config/service-helpers";
 | 
			
		||||
import createLogger from "utils/logger";
 | 
			
		||||
 | 
			
		||||
const logger = createLogger("qbittorrentProxyHandler");
 | 
			
		||||
 | 
			
		||||
async function login(widget, params) {
 | 
			
		||||
async function login(widget) {
 | 
			
		||||
  logger.debug("qBittorrent is rejecting the request, logging in.");
 | 
			
		||||
  const loginUrl = new URL(`${widget.url}/api/v2/auth/login`).toString();
 | 
			
		||||
  const loginBody = `username=${encodeURI(widget.username)}&password=${encodeURI(widget.password)}`;
 | 
			
		||||
 | 
			
		||||
  // using fetch intentionally, for login only, as the httpProxy method causes qBittorrent to
 | 
			
		||||
  // complain about header encoding
 | 
			
		||||
  return fetch(loginUrl, {
 | 
			
		||||
  const loginParams = {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
 | 
			
		||||
    body: loginBody,
 | 
			
		||||
  })
 | 
			
		||||
  .then(async (response) => {
 | 
			
		||||
    addCookieToJar(loginUrl, response.headers);
 | 
			
		||||
    setCookieHeader(loginUrl, params);
 | 
			
		||||
    const data = await response.text();
 | 
			
		||||
    return [response.status, data];
 | 
			
		||||
  })
 | 
			
		||||
  .catch((err) => [500, err]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // eslint-disable-next-line no-unused-vars
 | 
			
		||||
  const [status, contentType, data] = await httpProxy(loginUrl, loginParams);
 | 
			
		||||
  return [status, data];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async function qbittorrentProxyHandler(req, res) {
 | 
			
		||||
@ -44,11 +37,10 @@ export default async function qbittorrentProxyHandler(req, res) {
 | 
			
		||||
 | 
			
		||||
  const url = new URL(formatApiCall("{url}/api/v2/{endpoint}", { endpoint, ...widget }));
 | 
			
		||||
  const params = { method: "GET", headers: {} };
 | 
			
		||||
  setCookieHeader(url, params);
 | 
			
		||||
 | 
			
		||||
  let [status, contentType, data] = await httpProxy(url, params);
 | 
			
		||||
  if (status === 403) {
 | 
			
		||||
    [status, data] = await login(widget, params);
 | 
			
		||||
    [status, data] = await login(widget);
 | 
			
		||||
 | 
			
		||||
    if (status !== 200) {
 | 
			
		||||
      logger.error("HTTP %d logging in to qBittorrent.  Data: %s", status, data);
 | 
			
		||||
@ -59,9 +51,9 @@ export default async function qbittorrentProxyHandler(req, res) {
 | 
			
		||||
      logger.error("Error logging in to qBittorrent: Data: %s", data);
 | 
			
		||||
      return res.status(401).end(data);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [status, contentType, data] = await httpProxy(url, params);
 | 
			
		||||
    [status, contentType, data] = await httpProxy(url, params);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (status !== 200) {
 | 
			
		||||
    logger.error("HTTP %d getting data from qBittorrent.  Data: %s", status, data);
 | 
			
		||||
 | 
			
		||||
@ -11,9 +11,14 @@ export default async function rutorrentProxyHandler(req, res) {
 | 
			
		||||
    if (widget) {
 | 
			
		||||
      const constructedUrl = new URL(widget.url);
 | 
			
		||||
 | 
			
		||||
      let rtPort = constructedUrl.port;
 | 
			
		||||
      if (rtPort === '') {
 | 
			
		||||
        rtPort = constructedUrl.protocol === "https:" ? 443 : 80;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const rutorrent = new RuTorrent({
 | 
			
		||||
        host: constructedUrl.hostname,
 | 
			
		||||
        port: constructedUrl.port,
 | 
			
		||||
        port: rtPort,
 | 
			
		||||
        path: constructedUrl.pathname,
 | 
			
		||||
        ssl: constructedUrl.protocol === "https:",
 | 
			
		||||
        username: widget.username,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										62
									
								
								src/widgets/scrutiny/component.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/widgets/scrutiny/component.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
			
		||||
import Container from "components/services/widget/container";
 | 
			
		||||
import Block from "components/services/widget/block";
 | 
			
		||||
import useWidgetAPI from "utils/proxy/use-widget-api";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// @see https://github.com/AnalogJ/scrutiny/blob/d8d56f77f9e868127c4849dac74d65512db658e8/webapp/frontend/src/app/shared/device-status.pipe.ts
 | 
			
		||||
const DeviceStatus = {
 | 
			
		||||
  passed: 0,
 | 
			
		||||
  failed_smart: 1,
 | 
			
		||||
  failed_scrutiny: 2,
 | 
			
		||||
  failed_both: 3,
 | 
			
		||||
 | 
			
		||||
  isFailed(s){ return s > this.passed && s <= this.failed_both},
 | 
			
		||||
  isUnknown(s){ return s < this.passed || s > this.failed_both}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// @see https://github.com/AnalogJ/scrutiny/blob/d8d56f77f9e868127c4849dac74d65512db658e8/webapp/frontend/src/app/core/config/app.config.ts
 | 
			
		||||
const DeviceStatusThreshold = {
 | 
			
		||||
  smart: 1,
 | 
			
		||||
  scrutiny: 2,
 | 
			
		||||
  both: 3
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Component({ service }) {
 | 
			
		||||
  const { widget } = service;
 | 
			
		||||
 | 
			
		||||
  const { data: scrutinySettings, error: scrutinySettingsError } = useWidgetAPI(widget, "settings");
 | 
			
		||||
  const { data: scrutinyData, error: scrutinyError } = useWidgetAPI(widget, "summary");
 | 
			
		||||
 | 
			
		||||
  if (scrutinyError || scrutinySettingsError) {
 | 
			
		||||
    const finalError = scrutinyError ?? scrutinySettingsError;
 | 
			
		||||
    return <Container error={finalError} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!scrutinyData || !scrutinySettings) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="scrutiny.passed" />
 | 
			
		||||
        <Block label="scrutiny.failed" />
 | 
			
		||||
        <Block label="scrutiny.unknown" />
 | 
			
		||||
      </Container>
 | 
			
		||||
    );
 | 
			
		||||
  } 
 | 
			
		||||
 | 
			
		||||
  const deviceIds = Object.values(scrutinyData.data.summary);
 | 
			
		||||
  const statusThreshold = scrutinySettings.settings.metrics.status_threshold;
 | 
			
		||||
 | 
			
		||||
  const failed = deviceIds.filter(deviceId => (DeviceStatus.isFailed(deviceId.device.device_status) && statusThreshold === DeviceStatusThreshold.both) || [statusThreshold, DeviceStatus.failed_both].includes(deviceId.device.device_status))?.length || 0;
 | 
			
		||||
  const unknown = deviceIds.filter(deviceId => DeviceStatus.isUnknown(deviceId.device.device_status))?.length || 0;
 | 
			
		||||
  const passed = deviceIds.length - (failed + unknown);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container service={service}>
 | 
			
		||||
      <Block label="scrutiny.passed" value={passed} />
 | 
			
		||||
      <Block label="scrutiny.failed" value={failed} />
 | 
			
		||||
      <Block label="scrutiny.unknown" value={unknown} />
 | 
			
		||||
    </Container>
 | 
			
		||||
    
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								src/widgets/scrutiny/widget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/widgets/scrutiny/widget.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
import genericProxyHandler from "utils/proxy/handlers/generic";
 | 
			
		||||
 | 
			
		||||
const widget = {
 | 
			
		||||
  api: "{url}/api/{endpoint}",
 | 
			
		||||
  proxyHandler: genericProxyHandler,
 | 
			
		||||
 | 
			
		||||
  mappings: {
 | 
			
		||||
    summary: {
 | 
			
		||||
      endpoint: "summary",
 | 
			
		||||
      validate: [
 | 
			
		||||
        "data",
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    settings: {
 | 
			
		||||
      endpoint: "settings",
 | 
			
		||||
      validate: [
 | 
			
		||||
        "settings",
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default widget;
 | 
			
		||||
@ -4,7 +4,10 @@ import autobrr from "./autobrr/widget";
 | 
			
		||||
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 emby from "./emby/widget";
 | 
			
		||||
import flood from "./flood/widget";
 | 
			
		||||
import gluetun from "./gluetun/widget";
 | 
			
		||||
import gotify from "./gotify/widget";
 | 
			
		||||
import hdhomerun from "./hdhomerun/widget";
 | 
			
		||||
@ -18,6 +21,7 @@ import npm from "./npm/widget";
 | 
			
		||||
import nzbget from "./nzbget/widget";
 | 
			
		||||
import ombi from "./ombi/widget";
 | 
			
		||||
import overseerr from "./overseerr/widget";
 | 
			
		||||
import paperlessngx from "./paperlessngx/widget";
 | 
			
		||||
import pihole from "./pihole/widget";
 | 
			
		||||
import plex from "./plex/widget";
 | 
			
		||||
import portainer from "./portainer/widget";
 | 
			
		||||
@ -29,6 +33,7 @@ import radarr from "./radarr/widget";
 | 
			
		||||
import readarr from "./readarr/widget";
 | 
			
		||||
import rutorrent from "./rutorrent/widget";
 | 
			
		||||
import sabnzbd from "./sabnzbd/widget";
 | 
			
		||||
import scrutiny from "./scrutiny/widget";
 | 
			
		||||
import sonarr from "./sonarr/widget";
 | 
			
		||||
import speedtest from "./speedtest/widget";
 | 
			
		||||
import strelaysrv from "./strelaysrv/widget";
 | 
			
		||||
@ -47,7 +52,10 @@ const widgets = {
 | 
			
		||||
  bazarr,
 | 
			
		||||
  changedetectionio,
 | 
			
		||||
  coinmarketcap,
 | 
			
		||||
  deluge,
 | 
			
		||||
  diskstation,
 | 
			
		||||
  emby,
 | 
			
		||||
  flood,
 | 
			
		||||
  gluetun,
 | 
			
		||||
  gotify,
 | 
			
		||||
  hdhomerun,
 | 
			
		||||
@ -62,6 +70,7 @@ const widgets = {
 | 
			
		||||
  nzbget,
 | 
			
		||||
  ombi,
 | 
			
		||||
  overseerr,
 | 
			
		||||
  paperlessngx,
 | 
			
		||||
  pihole,
 | 
			
		||||
  plex,
 | 
			
		||||
  portainer,
 | 
			
		||||
@ -73,6 +82,7 @@ const widgets = {
 | 
			
		||||
  readarr,
 | 
			
		||||
  rutorrent,
 | 
			
		||||
  sabnzbd,
 | 
			
		||||
  scrutiny,
 | 
			
		||||
  sonarr,
 | 
			
		||||
  speedtest,
 | 
			
		||||
  strelaysrv,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user