diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..5a9e97f1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.exclude": { + "**/.next": true, + "**/node_modules": true + } +} \ No newline at end of file diff --git a/README.md b/README.md index c1f5e23f..2eeb6dc9 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ - Images built for AMD64 (x86_64), ARM64, ARMv7 and ARMv6 - Supports all Raspberry Pi's, most SBCs & Apple Silicon - Full i18n support with automatic language detection - - Translantions for Chinese, Dutch, Finnish, French, German, Hebrew, Hungarian, Norwegian Bokmål, Polish, Portuguese, Portuguese (Brazil), Romainian, Russian, Spanish, Swedish and Yue + - Translantions for Catalan, Chinese, Dutch, Finnish, French, German, Hebrew, Hungarian, Norwegian Bokmål, Polish, Portuguese, Portuguese (Brazil), Romainian, Russian, Spanish, Swedish and Yue - Want to help translate? [Join the Weblate project](https://hosted.weblate.org/engage/homepage/) - Service & Web Bookmarks - Docker Integration diff --git a/public/locales/ca/common.json b/public/locales/ca/common.json index 8ef784cb..4f46fff0 100644 --- a/public/locales/ca/common.json +++ b/public/locales/ca/common.json @@ -177,8 +177,19 @@ }, "proxmox": { "vms": "VMs", - "mem": "MEM", - "cpu": "CPU", + "mem": "Memòria", + "cpu": "Processador", "lxc": "LXC" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/de/common.json b/public/locales/de/common.json index 0b84664f..0070cb5a 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index dfc1a5ba..b1097904 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -31,6 +31,17 @@ "used": "Used", "load": "Load" }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" + }, "docker": { "rx": "RX", "tx": "TX", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index fe9a7253..3184d8b7 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -176,9 +176,20 @@ "failedLoginsLast24H": "Inicios de sesión fallidos (24h)" }, "proxmox": { - "mem": "MEM", - "cpu": "CPU", + "mem": "Memoria", + "cpu": "Procesador", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "up": "UP", + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/fi/common.json b/public/locales/fi/common.json index 133a0f32..c4654377 100644 --- a/public/locales/fi/common.json +++ b/public/locales/fi/common.json @@ -176,9 +176,20 @@ "failedLoginsLast24H": "Epäonnistuneita kirjautumisia (24h)" }, "proxmox": { - "mem": "MEM", + "mem": "RAM", "cpu": "CPU", "lxc": "LXC", - "vms": "VMs" + "vms": "VKt" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "wait": "Please wait", + "days": "Days", + "wan": "WAN", + "up": "UP", + "down": "DOWN" } } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index c0a72e95..e893fccb 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -176,9 +176,20 @@ "failedLoginsLast24H": "Cnx. échouées (24h)" }, "proxmox": { - "mem": "MEM", - "cpu": "CPU", - "lxc": "LXC", + "mem": "Mém", + "cpu": "Cpu", + "lxc": "LxC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/he/common.json b/public/locales/he/common.json index 9414f038..532fce39 100644 --- a/public/locales/he/common.json +++ b/public/locales/he/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/hr/common.json b/public/locales/hr/common.json index 36b970a0..e11164b4 100644 --- a/public/locales/hr/common.json +++ b/public/locales/hr/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/hu/common.json b/public/locales/hu/common.json index ac4d7fff..581420ea 100644 --- a/public/locales/hu/common.json +++ b/public/locales/hu/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/it/common.json b/public/locales/it/common.json index 09f32213..9403315c 100644 --- a/public/locales/it/common.json +++ b/public/locales/it/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wait": "Please wait", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN" } } diff --git a/public/locales/nb-NO/common.json b/public/locales/nb-NO/common.json index cea70d93..dc9992d4 100644 --- a/public/locales/nb-NO/common.json +++ b/public/locales/nb-NO/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json index b13cd912..07250217 100644 --- a/public/locales/nl/common.json +++ b/public/locales/nl/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "lan_users": "LAN Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/pl/common.json b/public/locales/pl/common.json index 4eb9ffa1..d34b7c80 100644 --- a/public/locales/pl/common.json +++ b/public/locales/pl/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/pt-BR/common.json b/public/locales/pt-BR/common.json index 5596ac64..61e704d8 100644 --- a/public/locales/pt-BR/common.json +++ b/public/locales/pt-BR/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json index fc7b3c9c..a8257926 100644 --- a/public/locales/pt/common.json +++ b/public/locales/pt/common.json @@ -191,5 +191,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/ro/common.json b/public/locales/ro/common.json index eaa9af3a..c3371cd0 100644 --- a/public/locales/ro/common.json +++ b/public/locales/ro/common.json @@ -180,5 +180,16 @@ "mem": "MEM", "cpu": "CPU", "lxc": "LXC" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index d06131fd..57e1eb93 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json index c42ecabb..b36e77ae 100644 --- a/public/locales/sv/common.json +++ b/public/locales/sv/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/te/common.json b/public/locales/te/common.json new file mode 100644 index 00000000..c5a5323d --- /dev/null +++ b/public/locales/te/common.json @@ -0,0 +1,195 @@ +{ + "readarr": { + "books": "పుస్తకాలు", + "wanted": "కావలెను", + "queued": "క్యూయూఎడ్" + }, + "adguard": { + "blocked": "నిరోధించబడింది", + "filtered": "ఫిల్టర్ చేయబడింది", + "latency": "జాప్యం", + "queries": "ప్రశ్నలు" + }, + "strelaysrv": { + "numActiveSessions": "సెషన్స్", + "numConnections": "కనెక్షన్లు", + "dataRelayed": "రెలయెడఁ", + "transferRate": "రేటు" + }, + "widget": { + "missing_type": "విడ్జెట్ లేదు: {{type}}", + "api_error": "API లోపం", + "status": "హోదా" + }, + "weather": { + "current": "ప్రస్తుత స్తలం", + "allow": "అనుమతించడానికి క్లిక్ చేయండి", + "updating": "నవీకరిస్తోంది", + "wait": "దయచేసి వేచి ఉండండి" + }, + "search": { + "placeholder": "వెతకండి…" + }, + "resources": { + "cpu": "సీ పి యూ", + "total": "మొత్తం", + "free": "ఉచిత", + "used": "ఉపయోగించబడిన", + "load": "లోడ్" + }, + "docker": { + "rx": "RX", + "tx": "TX", + "mem": "MEM", + "cpu": "CPU", + "offline": "ఆఫ్‌లైన్" + }, + "emby": { + "playing": "ఆడుతున్నారు", + "transcoding": "ట్రాన్స్‌కోడింగ్", + "bitrate": "బిట్రేట్", + "no_active": "యాక్టివ్ స్ట్రీమ్‌లు లేవు" + }, + "tautulli": { + "playing": "ఆడుతున్నారు", + "transcoding": "ట్రాన్స్‌కోడింగ్", + "bitrate": "బిట్రేట్", + "no_active": "యాక్టివ్ స్ట్రీమ్‌లు లేవు" + }, + "nzbget": { + "rate": "రేట్", + "remaining": "మిగిలింది", + "downloaded": "డౌన్‌లోడ్ చేయబడింది" + }, + "sabnzbd": { + "rate": "రేట్", + "queue": "వరుస", + "timeleft": "మిగిలి వున్న సమయం" + }, + "rutorrent": { + "active": "చురుకుగా", + "upload": "అప్‌లోడ్", + "download": "డౌన్‌లోడ్" + }, + "transmission": { + "download": "డౌన్‌లోడ్", + "upload": "అప్‌లోడ్", + "leech": "జలగ", + "seed": "సీడ్" + }, + "qbittorrent": { + "download": "డౌన్‌లోడ్", + "upload": "అప్లోడ్", + "leech": "లీచ్", + "seed": "సీడ్" + }, + "sonarr": { + "wanted": "కావలెను", + "queued": "క్యూయూఎడ్", + "series": "సిరీస్" + }, + "radarr": { + "wanted": "కావలెను", + "queued": "క్యూయూఎడ్", + "movies": "సినిమాలు" + }, + "lidarr": { + "wanted": "కావలెను", + "queued": "క్యూయూఎడ్", + "albums": "ఆల్బములు" + }, + "bazarr": { + "missingEpisodes": "ఎపిసోడ్‌లు లేవు", + "missingMovies": "సినిమాలు లేవు" + }, + "ombi": { + "pending": "పెండింగ్", + "approved": "ఆమోదించబడింది", + "available": "అందుబాటులో వున్నవి" + }, + "jellyseerr": { + "pending": "పెండింగ్", + "approved": "ఆమోదించబడింది", + "available": "అందుబాటులో" + }, + "overseerr": { + "pending": "పెండింగ్", + "approved": "ఆమోదించబడింది", + "available": "అందుబాటులో" + }, + "pihole": { + "queries": "ప్రశ్నలు", + "blocked": "నిరోధించబడింది", + "gravity": "గురుత్వాకర్షణ" + }, + "speedtest": { + "upload": "అప్లోడ్", + "download": "డౌన్‌లోడ్", + "ping": "పింగ్" + }, + "portainer": { + "running": "నడుస్తోంది", + "stopped": "ఆగిపోయింది", + "total": "మొత్తం" + }, + "traefik": { + "routers": "రౌటర్లు", + "services": "సేవలు", + "middleware": "మిడిల్వేర్" + }, + "npm": { + "enabled": "ప్రారంభించబడింది", + "disabled": "Disabled", + "total": "మొత్తం" + }, + "coinmarketcap": { + "configure": "ట్రాక్ చేయడానికి ఒకటి లేదా అంతకంటే ఎక్కువ క్రిప్టో కరెన్సీలను కాన్ఫిగర్ చేయండి", + "1hour": "1 గంట", + "1day": "1 రోజు", + "7days": "7 రోజులు", + "30days": "30 రోజులు" + }, + "gotify": { + "apps": "అప్లికేషన్లు", + "clients": "ఖాతాదారులు", + "messages": "సందేశాలు" + }, + "prowlarr": { + "enableIndexers": "సూచికలు", + "numberOfGrabs": "గ్రాబ్స్", + "numberOfQueries": "ప్రశ్నలు", + "numberOfFailGrabs": "ఫెయిల్ గ్రాబ్స్", + "numberOfFailQueries": "విఫలమైన ప్రశ్నలు" + }, + "jackett": { + "configured": "కాన్ఫిగర్ చేయబడింది", + "errored": "పొరపాటు జరిగింది" + }, + "mastodon": { + "user_count": "వినియోగదారులు", + "status_count": "పోస్ట్‌లు", + "domain_count": "డొమైన్‌లు" + }, + "authentik": { + "users": "వినియోగదారులు", + "loginsLast24H": "లాగిన్లు (24గం)", + "failedLoginsLast24H": "విఫలమైన లాగిన్‌లు (24గం)" + }, + "proxmox": { + "mem": "MEM", + "cpu": "CPU", + "lxc": "LXC", + "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" + } +} diff --git a/public/locales/vi/common.json b/public/locales/vi/common.json index 736120cf..5a27fd85 100644 --- a/public/locales/vi/common.json +++ b/public/locales/vi/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/yue/common.json b/public/locales/yue/common.json index 72e2ddde..a49767c4 100644 --- a/public/locales/yue/common.json +++ b/public/locales/yue/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/zh-CN/common.json b/public/locales/zh-CN/common.json index 4fcc1702..d80a0534 100644 --- a/public/locales/zh-CN/common.json +++ b/public/locales/zh-CN/common.json @@ -162,23 +162,34 @@ "mastodon": { "user_count": "用户", "status_count": "Posts", - "domain_count": "Domains" + "domain_count": "域" }, "strelaysrv": { - "numActiveSessions": "Sessions", - "dataRelayed": "Relayed", - "numConnections": "Connections", - "transferRate": "Rate" + "numActiveSessions": "会话", + "dataRelayed": "中继", + "numConnections": "连接", + "transferRate": "速度" }, "authentik": { - "users": "Users", - "loginsLast24H": "Logins (24h)", - "failedLoginsLast24H": "Failed Logins (24h)" + "users": "用户", + "loginsLast24H": "登录 (24h)", + "failedLoginsLast24H": "登录失败 (24h)" }, "proxmox": { - "mem": "MEM", - "cpu": "CPU", + "mem": "内存", + "cpu": "处理器", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/public/locales/zh-Hant/common.json b/public/locales/zh-Hant/common.json index 70dba911..3ad0ed06 100644 --- a/public/locales/zh-Hant/common.json +++ b/public/locales/zh-Hant/common.json @@ -180,5 +180,16 @@ "cpu": "CPU", "lxc": "LXC", "vms": "VMs" + }, + "unifi": { + "users": "Users", + "uptime": "System Uptime", + "days": "Days", + "wan": "WAN", + "lan_users": "LAN Users", + "wlan_users": "WLAN Users", + "up": "UP", + "down": "DOWN", + "wait": "Please wait" } } diff --git a/src/components/widgets/unifi_console/unifi_console.jsx b/src/components/widgets/unifi_console/unifi_console.jsx new file mode 100644 index 00000000..7427bd23 --- /dev/null +++ b/src/components/widgets/unifi_console/unifi_console.jsx @@ -0,0 +1,119 @@ +import { BiError, BiWifi, BiCheckCircle, BiXCircle } from "react-icons/bi"; +import { MdSettingsEthernet } from "react-icons/md"; +import { useTranslation } from "next-i18next"; +import { SiUbiquiti } from "react-icons/si"; + +import useWidgetAPI from "utils/proxy/use-widget-api"; + +export default function Widget({ options }) { + const { t } = useTranslation(); + + // eslint-disable-next-line no-param-reassign + options.type = "unifi_console"; + const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites"); + + if (statsError || statsData?.error) { + return ( +
+
+
+ +
+ {t("widget.api_error")} + - +
+
+
+
+ ); + } + + const defaultSite = statsData?.data?.find(s => s.name === "default"); + + if (!defaultSite) { + return ( +
+
+
+ +
+
+ {t("unifi.wait")} +
+
+
+ ); + } + + const wan = defaultSite.health.find(h => h.subsystem === "wan"); + const lan = defaultSite.health.find(h => h.subsystem === "lan"); + const wlan = defaultSite.health.find(h => h.subsystem === "wlan"); + const data = { + name: wan.gw_name, + uptime: wan["gw_system-stats"].uptime, + up: wan.status === 'ok', + wlan: { + users: wlan.num_user, + status: wlan.status + }, + lan: { + users: lan.num_user, + status: lan.status + } + }; + + return ( +
+
+
+ +
+ {data.name} +
+
+
+
+
+ {t("common.number", { + value: data.uptime / 86400, + maximumFractionDigits: 1, + })} +
+
{t("unifi.days")}
+
+
+
{t("unifi.wan")}
+ { data.up + ? + : + } +
+
+
+
+
+ +
+
+ {t("common.number", { + value: data.wlan.users, + maximumFractionDigits: 0, + })} +
+
+
+
+ +
+
+ {t("common.number", { + value: data.lan.users, + maximumFractionDigits: 0, + })} +
+
+
+
+
+ ); +} diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx index cc0288c2..ac5353eb 100644 --- a/src/components/widgets/widget.jsx +++ b/src/components/widgets/widget.jsx @@ -10,6 +10,7 @@ const widgetMappings = { greeting: dynamic(() => import("components/widgets/greeting/greeting")), datetime: dynamic(() => import("components/widgets/datetime/datetime")), logo: dynamic(() => import("components/widgets/logo/logo"), { ssr: false }), + unifi_console: dynamic(() => import("components/widgets/unifi_console/unifi_console")), }; export default function Widget({ widget }) { diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 92605f95..b4eb50e1 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -263,8 +263,6 @@ function Home({ initialSettings }) { export default function Wrapper({ initialSettings, fallback }) { const wrappedStyle = {}; if (initialSettings && initialSettings.background) { - // wrappedStyle.backgroundImage = `url(${initialSettings.background})`; - // wrappedStyle.backgroundSize = "cover"; const opacity = initialSettings.backgroundOpacity ?? 1; const opacityValue = 1 - opacity; wrappedStyle.backgroundImage = ` diff --git a/src/pages/site.webmanifest.jsx b/src/pages/site.webmanifest.jsx index ff9d638f..10a448a3 100644 --- a/src/pages/site.webmanifest.jsx +++ b/src/pages/site.webmanifest.jsx @@ -1,16 +1,36 @@ import checkAndCopyConfig, { getSettings } from "utils/config/config"; import themes from "utils/styles/themes"; +import { servicesResponse, bookmarksResponse } from "utils/config/api-response"; export async function getServerSideProps({ res }) { checkAndCopyConfig("settings.yaml"); const settings = getSettings(); + const services = await servicesResponse(); + const bookmarks = await bookmarksResponse(); const color = settings.color || "slate"; const theme = settings.theme || "dark"; + const serviceShortcuts = services.map((group) => + group.services.map((service) => ({ + name: service.name, + url: service.href, + description: service.description, + })) + ); + + const bookmarkShortcuts = bookmarks.map((group) => + group.bookmarks.map((service) => ({ + name: service.name, + url: service.href, + })) + ); + + const shortcuts = [...serviceShortcuts, ...bookmarkShortcuts].flat(); + const manifest = { - name: "Homepage", - short_name: "Homepage", + name: settings.title || "Homepage", + short_name: settings.title || "Homepage", icons: [ { src: "/android-chrome-192x192.png?v=2", @@ -23,6 +43,7 @@ export async function getServerSideProps({ res }) { type: "image/png", }, ], + shortcuts, theme_color: themes[color][theme], background_color: themes[color][theme], display: "standalone", diff --git a/src/utils/proxy/api-helpers.js b/src/utils/proxy/api-helpers.js index 55cd333c..904c9e96 100644 --- a/src/utils/proxy/api-helpers.js +++ b/src/utils/proxy/api-helpers.js @@ -2,7 +2,7 @@ export function formatApiCall(url, args) { const find = /\{.*?\}/g; const replace = (match) => { const key = match.replace(/\{|\}/g, ""); - return args[key]; + return args[key] || ""; }; return url.replace(/\/+$/, "").replace(find, replace); diff --git a/src/utils/proxy/cached-fetch.js b/src/utils/proxy/cached-fetch.js index 22eba37f..0ed39562 100644 --- a/src/utils/proxy/cached-fetch.js +++ b/src/utils/proxy/cached-fetch.js @@ -1,8 +1,13 @@ import cache from "memory-cache"; +const defaultDuration = 5; + export default async function cachedFetch(url, duration) { const cached = cache.get(url); + // eslint-disable-next-line no-param-reassign + duration = duration || defaultDuration; + if (cached) { return cached; } diff --git a/src/widgets/components.js b/src/widgets/components.js index 5357c070..c2a6705e 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -32,6 +32,7 @@ const components = { tautulli: dynamic(() => import("./tautulli/component")), traefik: dynamic(() => import("./traefik/component")), transmission: dynamic(() => import("./transmission/component")), + unifi: dynamic(() => import("./unifi/component")), }; export default components; diff --git a/src/widgets/docker/component.jsx b/src/widgets/docker/component.jsx index 10e55fcb..542fbde7 100644 --- a/src/widgets/docker/component.jsx +++ b/src/widgets/docker/component.jsx @@ -40,14 +40,16 @@ export default function Component({ service }) { ); } + const network = statsData.stats.networks?.eth0 || statsData.stats.networks?.network; + return ( - {statsData.stats.networks && ( + {network && ( <> - - + + )} diff --git a/src/widgets/unifi/component.jsx b/src/widgets/unifi/component.jsx new file mode 100644 index 00000000..e2db8e77 --- /dev/null +++ b/src/widgets/unifi/component.jsx @@ -0,0 +1,58 @@ +import { useTranslation } from "next-i18next"; + +import Container from "components/services/widget/container"; +import Block from "components/services/widget/block"; +import useWidgetAPI from "utils/proxy/use-widget-api"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const { data: statsData, error: statsError } = useWidgetAPI(widget, "stat/sites"); + + if (statsError || statsData?.error) { + return ; + } + + const defaultSite = statsData?.data?.find(s => s.name === "default"); + + if (!defaultSite) { + return ( + + + + + + + ); + } + + const wan = defaultSite.health.find(h => h.subsystem === "wan"); + const lan = defaultSite.health.find(h => h.subsystem === "lan"); + const wlan = defaultSite.health.find(h => h.subsystem === "wlan"); + const data = { + name: wan.gw_name, + uptime: wan["gw_system-stats"].uptime, + up: wan.status === 'ok', + wlan: { + users: wlan.num_user, + status: wlan.status + }, + lan: { + users: lan.num_user, + status: lan.status + }, + }; + + const uptime = `${t("common.number", { value: data.uptime / 86400, maximumFractionDigits: 1 })} ${t("unifi.days")}`; + + return ( + + + + + + + ); +} diff --git a/src/widgets/unifi/proxy.js b/src/widgets/unifi/proxy.js new file mode 100644 index 00000000..53ee49f0 --- /dev/null +++ b/src/widgets/unifi/proxy.js @@ -0,0 +1,119 @@ +import cache from "memory-cache"; + +import { formatApiCall } from "utils/proxy/api-helpers"; +import { httpProxy } from "utils/proxy/http"; +import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar"; +import { getSettings } from "utils/config/config"; +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +import widgets from "widgets/widgets"; + +const udmpPrefix = "/proxy/network"; +const proxyName = "unifiProxyHandler"; +const prefixCacheKey = `${proxyName}__prefix`; +const logger = createLogger(proxyName); + +async function getWidget(req) { + const { group, service, type } = req.query; + + let widget = null; + if (type === "unifi_console") { + const settings = getSettings(); + widget = settings.unifi_console; + if (!widget) { + logger.debug("There is no unifi_console section in settings.yaml"); + return null; + } + widget.type = "unifi"; + } else { + if (!group || !service) { + logger.debug("Invalid or missing service '%s' or group '%s'", service, group); + return null; + } + + widget = await getServiceWidget(group, service); + + if (!widget) { + logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); + return null; + } + } + + return widget; +} + +async function login(widget) { + const endpoint = (widget.prefix === udmpPrefix) ? "auth/login" : "login"; + const api = widgets?.[widget.type]?.api?.replace("{prefix}", ""); // no prefix for login url + const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget })); + const loginBody = { username: widget.username, password: widget.password, remember: true }; + const headers = { "Content-Type": "application/json" }; + const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, { + method: "POST", + body: JSON.stringify(loginBody), + headers, + }); + return [status, contentType, data, responseHeaders]; +} + +export default async function unifiProxyHandler(req, res) { + const widget = await getWidget(req); + if (!widget) { + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const api = widgets?.[widget.type]?.api; + if (!api) { + return res.status(403).json({ error: "Service does not support API calls" }); + } + + let [status, contentType, data, responseHeaders] = []; + let prefix = cache.get(prefixCacheKey); + if (prefix === null) { + // auto detect if we're talking to a UDM Pro, and cache the result so that we + // don't make two requests each time data from Unifi is required + [status, contentType, data, responseHeaders] = await httpProxy(widget.url); + prefix = ""; + if (responseHeaders["x-csrf-token"]) { + prefix = udmpPrefix; + } + cache.put(prefixCacheKey, prefix); + } + + widget.prefix = prefix; + + const { endpoint } = req.query; + const url = new URL(formatApiCall(api, { endpoint, ...widget })); + const params = { method: "GET", headers: {} }; + setCookieHeader(url, params); + + [status, contentType, data, responseHeaders] = await httpProxy(url, params); + if (status === 401) { + logger.debug("Unifi isn't logged in or rejected the reqeust, attempting login."); + [status, contentType, data, responseHeaders] = await login(widget); + + if (status !== 200) { + logger.error("HTTP %d logging in to Unifi. Data: %s", status, data); + return res.status(status).end(data); + } + + const json = JSON.parse(data.toString()); + if (!(json?.meta?.rc === "ok" || json.login_time)) { + logger.error("Error logging in to Unifi: Data: %s", data); + return res.status(401).end(data); + } + + addCookieToJar(url, responseHeaders); + setCookieHeader(url, params); + + logger.debug("Retrying Unifi request after login."); + [status, contentType, data, responseHeaders] = await httpProxy(url, params); + } + + if (status !== 200) { + logger.error("HTTP %d getting data from Unifi endpoint %s. Data: %s", status, url.href, data); + } + + if (contentType) res.setHeader("Content-Type", contentType); + return res.status(status).send(data); +} diff --git a/src/widgets/unifi/widget.js b/src/widgets/unifi/widget.js new file mode 100644 index 00000000..928ebd76 --- /dev/null +++ b/src/widgets/unifi/widget.js @@ -0,0 +1,14 @@ +import unifiProxyHandler from "./proxy"; + +const widget = { + api: "{url}{prefix}/api/{endpoint}", + proxyHandler: unifiProxyHandler, + + mappings: { + "stat/sites": { + endpoint: "stat/sites", + }, + } +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 04665c78..a4cab76b 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -27,6 +27,7 @@ import strelaysrv from "./strelaysrv/widget"; import tautulli from "./tautulli/widget"; import traefik from "./traefik/widget"; import transmission from "./transmission/widget"; +import unifi from "./unifi/widget"; const widgets = { adguard, @@ -59,6 +60,8 @@ const widgets = { tautulli, traefik, transmission, + unifi, + unifi_console: unifi }; export default widgets;