From 81fe4cd866dab46424c9800f613f856d55093eaa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 14 Sep 2026 23:30:37 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8D=95=20Sync=202026-09-14=2023:30:37?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agentflow/Makefile | 6 +- brook/Makefile | 4 +- cloudreve/Makefile | 4 +- dae/Makefile | 6 +- daed/Makefile | 6 +- daed/files/daed-cleanup.sh | 87 +- daed/files/daed-guard | 134 +- daed/files/daed.init | 50 +- luci-app-easytier-next/Makefile | 27 + .../resources/view/easytier/main.js | 2220 +++++++++++++++++ .../po/templates/easytier.pot | 383 +++ luci-app-easytier-next/po/zh-cn | 1 + luci-app-easytier-next/po/zh_Hans/easytier.po | 383 +++ luci-app-easytier-next/po/zh_Hant/easytier.po | 383 +++ .../root/etc/config/easytier | 21 + .../root/etc/easytier/config.toml | 15 + .../etc/hotplug.d/iface/99-easytier-routes | 24 + .../root/etc/init.d/easytier | 557 +++++ .../root/etc/uci-defaults/99_easytier-init | 20 + .../root/usr/libexec/rpcd/easytier | 471 ++++ .../share/luci/menu.d/luci-app-easytier.json | 13 + .../share/rpcd/acl.d/luci-app-easytier.json | 38 + .../resources/view/firewall/zones.js | 49 +- luci-app-firewall/po/templates/firewall.pot | 16 + luci-app-firewall/po/zh_Hans/firewall.po | 12 + luci-app-firewall/po/zh_Hant/firewall.po | 12 + .../root/usr/libexec/luci-firewall-status | 22 + .../share/rpcd/acl.d/luci-app-firewall.json | 4 +- luci-app-frpc/po/sv/frpc.po | 509 ++-- luci-app-frpc/po/zh_Hans/frpc.po | 2 +- luci-app-frpc/po/zh_Hant/frpc.po | 2 +- luci-app-frps/po/sv/frps.po | 311 +-- luci-app-frps/po/zh_Hans/frps.po | 2 +- luci-app-frps/po/zh_Hant/frps.po | 2 +- luci-app-fwlive/Makefile | 2 +- .../luci-static/resources/fwlive/constants.js | 7 + .../luci-static/resources/fwlive/css.js | 2 +- .../luci-static/resources/fwlive/fwlive.css | 10 + .../resources/view/status/fwlive.js | 327 ++- luci-app-fwlive/po/de/luci-app-fwlive.po | 360 +-- luci-app-fwlive/po/ru/luci-app-fwlive.po | 358 +-- .../po/templates/luci-app-fwlive.pot | 28 + luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po | 358 +-- luci-app-mosdns/Makefile | 2 +- .../root/usr/share/mosdns/mosdns.uc | 2 +- luci-app-openlist2/Makefile | 6 +- .../resources/view/openlist2/basic.js | 81 +- .../resources/view/openlist2/logs.js | 2 +- luci-app-openlist2/po/templates/openlist2.po | 28 +- luci-app-openlist2/po/zh_Hans/openlist2.po | 40 +- luci-app-openlist2/po/zh_Hant/openlist2.po | 40 +- luci-app-oui/.gitattributes | 2 + luci-app-oui/Makefile | 19 + luci-app-oui/README.md | 87 + .../luci-static/resources/oui/DISCLAIMER.md | 54 + .../luci-static/resources/oui/LICENSE.macdb | 21 + .../luci-static/resources/oui/LICENSE.md | 30 + .../luci-static/resources/oui/NOTICE.fnos | 4 + .../resources/oui/NOTICE.supplemental | 22 + .../htdocs/luci-static/resources/oui/acer.svg | 1 + .../luci-static/resources/oui/amazon.svg | 1 + .../luci-static/resources/oui/apple.svg | 1 + .../htdocs/luci-static/resources/oui/asus.svg | 1 + .../luci-static/resources/oui/broadcom.svg | 1 + .../luci-static/resources/oui/computer.svg | 1 + .../htdocs/luci-static/resources/oui/dell.svg | 1 + .../htdocs/luci-static/resources/oui/fnos.svg | 1 + .../luci-static/resources/oui/framework.svg | 1 + .../luci-static/resources/oui/fujitsu.svg | 1 + .../luci-static/resources/oui/gigabyte.svg | 30 + .../luci-static/resources/oui/google.svg | 1 + .../luci-static/resources/oui/honor.svg | 1 + .../htdocs/luci-static/resources/oui/hp.svg | 1 + .../luci-static/resources/oui/huawei.svg | 1 + .../luci-static/resources/oui/intel.svg | 1 + .../luci-static/resources/oui/lenovo.svg | 1 + .../htdocs/luci-static/resources/oui/lg.svg | 1 + .../luci-static/resources/oui/linksys.svg | 1 + .../luci-static/resources/oui/mediatek.svg | 1 + .../luci-static/resources/oui/microsoft.svg | 1 + .../luci-static/resources/oui/midea.svg | 43 + .../luci-static/resources/oui/motorola.svg | 1 + .../htdocs/luci-static/resources/oui/msi.svg | 1 + .../luci-static/resources/oui/netgear.svg | 1 + .../luci-static/resources/oui/nokia.svg | 1 + .../luci-static/resources/oui/nvidia.svg | 1 + .../luci-static/resources/oui/oneplus.svg | 1 + .../htdocs/luci-static/resources/oui/oppo.svg | 1 + .../htdocs/luci-static/resources/oui/oui.js | 111 + .../luci-static/resources/oui/panasonic.svg | 1 + .../htdocs/luci-static/resources/oui/qnap.svg | 1 + .../luci-static/resources/oui/raspberrypi.svg | 1 + .../luci-static/resources/oui/samsung.svg | 1 + .../luci-static/resources/oui/sharp.svg | 1 + .../htdocs/luci-static/resources/oui/sony.svg | 1 + .../luci-static/resources/oui/synology.svg | 1 + .../htdocs/luci-static/resources/oui/tcl.svg | 113 + .../luci-static/resources/oui/toshiba.svg | 1 + .../luci-static/resources/oui/tplink.svg | 1 + .../luci-static/resources/oui/ubiquiti.svg | 1 + .../resources/oui/vendors-451e5311befc.json | 1 + .../htdocs/luci-static/resources/oui/vivo.svg | 1 + .../luci-static/resources/oui/vmware.svg | 1 + .../luci-static/resources/oui/xiaomi.svg | 1 + luci-app-oui/root/etc/config/oui | 4 + .../usr/share/rpcd/acl.d/luci-app-oui.json | 10 + luci-app-oui/tools/.gitignore | 1 + luci-app-oui/tools/generate.py | 83 + luci-app-oui/tools/sources.json | 339 +++ luci-app-oui/tools/supplemental.json | 20 + luci-app-oui/tools/test_generate.py | 60 + luci-app-oui/tools/test_oui.js | 78 + luci-app-oui/tools/vendors.json | 299 +++ luci-app-passwall/Makefile | 2 +- .../model/cbi/passwall/client/global.lua | 47 +- luci-app-passwall/luasrc/passwall/api.lua | 16 +- .../luasrc/passwall/util_sing-box.lua | 69 +- .../luasrc/passwall/util_xray.lua | 108 +- luci-app-passwall/po/zh-cn/passwall.po | 30 - .../root/etc/hotplug.d/ntp/30-passwall-resync | 19 +- .../root/usr/share/passwall/haproxy.lua | 30 +- .../root/usr/share/passwall/nftables.sh | 72 +- .../usr/share/passwall/socks_auto_switch.sh | 2 - .../root/usr/share/passwall/utils.sh | 44 +- luci-app-ssr-plus/Makefile | 2 +- .../root/etc/init.d/shadowsocksr | 18 +- .../usr/share/shadowsocksr/clash_yaml.lua | 106 +- .../root/usr/share/shadowsocksr/subscribe.lua | 2 +- luci-app-systools/Makefile | 6 +- .../usr/share/systools/reset_rom_pkgs.run | 48 +- .../htdocs/luci-static/resources/network.js | 8 +- luci-base/po/templates/base.pot | 29 + luci-base/po/zh_Hans/base.po | 29 + luci-base/po/zh_Hant/base.po | 29 + luci-base/root/usr/share/rpcd/ucode/luci | 3 +- .../resources/view/network/wireless.js | 13 +- luci-mod-status/Makefile | 4 +- .../view/status/include/10_system.js | 28 +- .../view/status/include/20_memory.js | 11 +- .../view/status/include/30_network.js | 72 +- .../resources/view/status/include/40_dhcp.js | 472 +++- .../resources/view/status/index.js | 2 +- .../root/etc/init.d/luci-client-history | 12 + .../root/usr/libexec/luci-client-history | 27 + .../rpcd/acl.d/luci-mod-status-index.json | 2 + .../usr/share/rpcd/acl.d/luci-mod-status.json | 2 + .../usr/share/ucode/luci/client_history.uc | 49 + luci-mod-status/tests/README.md | 19 + luci-mod-status/tests/client-history.uc | 29 + .../resources/view/system/system.js | 50 +- .../usr/share/rpcd/acl.d/luci-mod-system.json | 4 +- luci-theme-argon/Makefile | 2 +- .../htdocs/luci-static/argon/css/cascade.css | 6 + luci-theme-argon/less/cascade.less | 8 + .../.claude/skills/luci-change/SKILL.md | 10 +- .../.claude/skills/luci-dom-compat/SKILL.md | 78 + .../luci-dom-compat/scripts/compat-probe.mjs | 182 ++ .../references/aurora-contract.md | 10 +- luci-theme-aurora/.dev/compat/README.md | 91 + ...rvices-dockerman-overview--info-table.html | 30 + ...ager-minidiskmanager--partition-table.html | 47 + .../.dev/compat/pages/admin-dashboard.md | 25 + .../admin-modem-modemdata-modempreview.md | 12 + .../.dev/compat/pages/admin-modem-qmodem.md | 12 + .../compat/pages/admin-network-network.md | 47 + .../compat/pages/admin-network-wireless.md | 14 + .../compat/pages/admin-services-dockerman.md | 13 + .../pages/admin-services-mini-diskmanager.md | 13 + .../pages/admin-services-openclash-config.md | 12 + .../pages/admin-services-openclash-log.md | 12 + .../admin-services-openclash-settings.md | 23 + .../compat/pages/admin-services-passwall.md | 14 + .../compat/pages/admin-services-samba4.md | 23 + .../compat/pages/admin-statistics-graphs.md | 12 + .../.dev/compat/pages/admin-status-logs.md | 12 + .../compat/pages/admin-status-overview.md | 34 + .../.dev/compat/pages/admin-system-diskman.md | 12 + .../compat/pages/admin-system-filemanager.md | 12 + .../.dev/compat/pages/admin-system-mounts.md | 14 + .../pages/admin-system-package-manager.md | 17 + luci-theme-aurora/.dev/package.json | 2 +- luci-theme-aurora/.dev/pnpm-lock.yaml | 158 ++ luci-theme-aurora/.dev/pnpm-workspace.yaml | 2 + .../.dev/src/media/components/_table.css | 4 +- luci-theme-aurora/.dev/tests/compat.test.js | 187 ++ luci-theme-aurora/.dev/tests/surfaces.test.js | 9 +- luci-theme-aurora/Makefile | 4 +- .../htdocs/luci-static/aurora/main.css | 2 +- luci-theme-footstrap/Makefile | 2 +- .../htdocs/luci-static/resources/fs-fit.js | 1268 +--------- .../htdocs/luci-static/resources/fs-router.js | 9 +- .../styles/theme/60-inputs.css | 13 +- .../ucode/template/themes/footstrap/header.ut | 14 +- luci-theme-shadcn/.dev/package.json | 2 +- luci-theme-shadcn/.dev/pnpm-lock.yaml | 158 ++ luci-theme-shadcn/.dev/pnpm-workspace.yaml | 2 + .../.dev/src/media/components/_table.css | 4 +- luci-theme-shadcn/Makefile | 4 +- .../htdocs/luci-static/shadcn/main.css | 2 +- mise/Makefile | 4 +- nikki/Makefile | 2 +- nikki/files/ucode/include.uc | 2 +- openlist2/Makefile | 7 +- openlist2/files/data.db | Bin 24576 -> 24576 bytes openlist2/files/openlist2.common | 15 +- openlist2/files/openlist2.config | 3 + openlist2/files/openlist2.init | 29 +- openlist2/files/openlist2.tls | 220 ++ openwrt-mwgp/Makefile | 58 +- openwrt-mwgp/files/mwgp.config | 2 + openwrt-mwgp/files/mwgp.init | 2 +- torrserver/Makefile | 6 +- tun2socks/Makefile | 4 +- wwand/Makefile | 4 +- 214 files changed, 10806 insertions(+), 2630 deletions(-) create mode 100644 luci-app-easytier-next/Makefile create mode 100644 luci-app-easytier-next/htdocs/luci-static/resources/view/easytier/main.js create mode 100644 luci-app-easytier-next/po/templates/easytier.pot create mode 120000 luci-app-easytier-next/po/zh-cn create mode 100644 luci-app-easytier-next/po/zh_Hans/easytier.po create mode 100644 luci-app-easytier-next/po/zh_Hant/easytier.po create mode 100644 luci-app-easytier-next/root/etc/config/easytier create mode 100644 luci-app-easytier-next/root/etc/easytier/config.toml create mode 100755 luci-app-easytier-next/root/etc/hotplug.d/iface/99-easytier-routes create mode 100755 luci-app-easytier-next/root/etc/init.d/easytier create mode 100755 luci-app-easytier-next/root/etc/uci-defaults/99_easytier-init create mode 100755 luci-app-easytier-next/root/usr/libexec/rpcd/easytier create mode 100644 luci-app-easytier-next/root/usr/share/luci/menu.d/luci-app-easytier.json create mode 100644 luci-app-easytier-next/root/usr/share/rpcd/acl.d/luci-app-easytier.json create mode 100755 luci-app-firewall/root/usr/libexec/luci-firewall-status create mode 100644 luci-app-oui/.gitattributes create mode 100644 luci-app-oui/Makefile create mode 100644 luci-app-oui/README.md create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/DISCLAIMER.md create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/LICENSE.macdb create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/LICENSE.md create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/NOTICE.fnos create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/NOTICE.supplemental create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/acer.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/amazon.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/apple.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/asus.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/broadcom.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/computer.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/dell.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/fnos.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/framework.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/fujitsu.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/gigabyte.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/google.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/honor.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/hp.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/huawei.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/intel.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/lenovo.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/lg.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/linksys.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/mediatek.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/microsoft.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/midea.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/motorola.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/msi.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/netgear.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/nokia.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/nvidia.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/oneplus.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/oppo.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/oui.js create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/panasonic.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/qnap.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/raspberrypi.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/samsung.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/sharp.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/sony.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/synology.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/tcl.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/toshiba.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/tplink.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/ubiquiti.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/vendors-451e5311befc.json create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/vivo.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/vmware.svg create mode 100644 luci-app-oui/htdocs/luci-static/resources/oui/xiaomi.svg create mode 100644 luci-app-oui/root/etc/config/oui create mode 100644 luci-app-oui/root/usr/share/rpcd/acl.d/luci-app-oui.json create mode 100644 luci-app-oui/tools/.gitignore create mode 100644 luci-app-oui/tools/generate.py create mode 100644 luci-app-oui/tools/sources.json create mode 100644 luci-app-oui/tools/supplemental.json create mode 100644 luci-app-oui/tools/test_generate.py create mode 100644 luci-app-oui/tools/test_oui.js create mode 100644 luci-app-oui/tools/vendors.json create mode 100755 luci-mod-status/root/etc/init.d/luci-client-history create mode 100755 luci-mod-status/root/usr/libexec/luci-client-history create mode 100644 luci-mod-status/root/usr/share/ucode/luci/client_history.uc create mode 100644 luci-mod-status/tests/README.md create mode 100644 luci-mod-status/tests/client-history.uc create mode 100644 luci-theme-aurora/.claude/skills/luci-dom-compat/SKILL.md create mode 100644 luci-theme-aurora/.claude/skills/luci-dom-compat/scripts/compat-probe.mjs create mode 100644 luci-theme-aurora/.dev/compat/README.md create mode 100644 luci-theme-aurora/.dev/compat/fixtures/admin-services-dockerman-overview--info-table.html create mode 100644 luci-theme-aurora/.dev/compat/fixtures/admin-services-mini-diskmanager-minidiskmanager--partition-table.html create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-dashboard.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-modem-modemdata-modempreview.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-modem-qmodem.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-network-network.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-network-wireless.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-services-dockerman.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-services-mini-diskmanager.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-services-openclash-config.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-services-openclash-log.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-services-openclash-settings.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-services-passwall.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-services-samba4.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-statistics-graphs.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-status-logs.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-status-overview.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-system-diskman.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-system-filemanager.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-system-mounts.md create mode 100644 luci-theme-aurora/.dev/compat/pages/admin-system-package-manager.md create mode 100644 luci-theme-aurora/.dev/pnpm-workspace.yaml create mode 100644 luci-theme-aurora/.dev/tests/compat.test.js create mode 100644 luci-theme-shadcn/.dev/pnpm-workspace.yaml create mode 100644 openlist2/files/openlist2.tls create mode 100755 openwrt-mwgp/files/mwgp.config diff --git a/agentflow/Makefile b/agentflow/Makefile index 6124f48b5..467c95252 100644 --- a/agentflow/Makefile +++ b/agentflow/Makefile @@ -10,12 +10,12 @@ AGENTFLOW_ARCH_x86_64:=amd64 AGENTFLOW_ARCH_aarch64:=arm64 AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH)) -AGENTFLOW_HASH_x86_64:=454857022d555b23979bd7b9116ef926ce7117e9e6034116f3223bb72cae07db -AGENTFLOW_HASH_aarch64:=34dfe07fd2a1bcb7de8eadb41dd0a67b46d5db78f4ca14d1835fe94608c0579a +AGENTFLOW_HASH_x86_64:=ccf06c24eee59f9638bc0ba42d2ea420723317dccef980a42a51499844fbde2c +AGENTFLOW_HASH_aarch64:=369017b7e7ea359018875cde2ee9260f0eb92d6fda329e82a219b98516bb6350 PKG_NAME:=agentflow PKG_VERSION:=0.3.1 -PKG_RELEASE:=9 +PKG_RELEASE:=10 AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH) AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH)) diff --git a/brook/Makefile b/brook/Makefile index 898631f90..79126d0bd 100644 --- a/brook/Makefile +++ b/brook/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=brook -PKG_VERSION:=20260101.0 -PKG_RELEASE:=4 +PKG_VERSION:=20270101 +PKG_RELEASE:=5 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/txthinking/brook/tar.gz/v$(PKG_VERSION)? diff --git a/cloudreve/Makefile b/cloudreve/Makefile index 026debc9b..4303dd417 100644 --- a/cloudreve/Makefile +++ b/cloudreve/Makefile @@ -6,11 +6,11 @@ include $(TOPDIR)/rules.mk PKG_NAME:=cloudreve PKG_VERSION:=4.18.0 -PKG_RELEASE:=16 +PKG_RELEASE:=17 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/cloudreve/Cloudreve.git -PKG_SOURCE_VERSION:=1c3fe4da3c4073e73a11ed93518b545bb8b8d5ce +PKG_SOURCE_VERSION:=ade1866be54a3614bc643ab5f513e93a1e0dc82f PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-3.0-only diff --git a/dae/Makefile b/dae/Makefile index 856667492..73398689d 100644 --- a/dae/Makefile +++ b/dae/Makefile @@ -5,10 +5,10 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dae -PKG_VERSION:=2026.09.06 -PKG_RELEASE:=43 +PKG_VERSION:=2026.09.12 +PKG_RELEASE:=44 -PKG_SOURCE:=dae-src-2026.09.06-80525dabf966.tar.gz +PKG_SOURCE:=dae-src-2026.09.12-187058462a1f.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_HASH:=skip diff --git a/daed/Makefile b/daed/Makefile index 157e21e25..b8fbf3188 100644 --- a/daed/Makefile +++ b/daed/Makefile @@ -5,10 +5,10 @@ include $(TOPDIR)/rules.mk PKG_NAME:=daed -PKG_VERSION:=2026.09.06 -PKG_RELEASE:=54 +PKG_VERSION:=2026.09.12 +PKG_RELEASE:=55 -PKG_SOURCE:=daed-src-2026.09.06-62f2e24ac52a.tar.gz +PKG_SOURCE:=daed-src-2026.09.12-a0181f729855.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_HASH:=skip diff --git a/daed/files/daed-cleanup.sh b/daed/files/daed-cleanup.sh index 22217482b..3fa3368cd 100644 --- a/daed/files/daed-cleanup.sh +++ b/daed/files/daed-cleanup.sh @@ -1,8 +1,65 @@ #!/bin/sh +# daed-cleanup.sh — reaper for stale daed kernel state. +# +# Removes: +# 1. Processes inside the `daens` netns (SIGTERM, then SIGKILL after 1s) +# 2. The `daens` network namespace itself (ip netns del, with +# umount+rm as fallback for zombie nsfs mounts) +# 3. The `dae0` veth pair in the host netns +# +# daed itself owns TC clsact detach. This opkg-side helper only +# reaps stale daens/dae0 state and never removes /sys/fs/bpf/daed. +# The pin directory is created during normal startup, so it is not +# a reliable leak indicator and must not block service lifecycle. +# +# The function stays sourceable by both init.d/daed and daed-guard. + +daed_process_probe() { + if command -v pgrep >/dev/null 2>&1; then + pgrep -f '^/usr/bin/daed([[:space:]]|$)' >/dev/null 2>&1 + case "$?" in + 0) return 0 ;; + 1) return 1 ;; + esac + fi + + if command -v pidof >/dev/null 2>&1; then + pidof daed >/dev/null 2>&1 + case "$?" in + 0) return 0 ;; + 1) return 1 ;; + esac + fi + + return 2 +} daed_cleanup_runtime() { - local pid + local pid rc=0 probe_rc + # If daed userspace is currently running, do not touch the + # netns, the veth, or the eBPF dataplane. They are in active + # use; removing them would make every connection through dae + # hang. + daed_process_probe + probe_rc=$? + case "$probe_rc" in + 0) + if [ "${DAED_GUARD_CLEANUP:-start}" = "start" ]; then + logger -t daed-init "cleanup: pre-start skipped because /usr/bin/daed is still running; refusing a second instance" + return 1 + fi + logger -t daed-init "cleanup: post-exit skipped because another /usr/bin/daed instance is still running" + return 0 + ;; + 1) ;; + *) + logger -t daed-init "cleanup: cannot determine whether daed is running; refusing to remove netns/veth" + return 1 + ;; + esac + + # 1. Kill processes inside daens. for pid in $(ip netns pids daens 2>/dev/null); do kill "$pid" 2>/dev/null done @@ -14,14 +71,36 @@ daed_cleanup_runtime() { done fi + # 2. Remove the daens netns. ip netns del can fail if a + # process still references it via /proc//ns/net or + # because the umount has already happened. Try the + # umount/rm fallback. if ! ip netns del daens 2>/dev/null; then umount -l /run/netns/daens 2>/dev/null - rm -f /run/netns/daens + if [ -e /run/netns/daens ] && ! rm -f /run/netns/daens 2>/dev/null; then + logger -t daed-init "cleanup: failed to remove /run/netns/daens (resource busy); a reboot may be required" + rc=1 + fi fi - ip link del dae0 2>/dev/null || true + # 3. Remove the dae0 veth pair. + if ip link show dae0 >/dev/null 2>&1; then + if ! ip link del dae0 2>/dev/null; then + logger -t daed-init "cleanup: failed to remove dae0 veth pair" + rc=1 + fi + fi + # 4. The pin root is normal persistent state. Never remove it or + # make its existence change the cleanup result. + if [ -e /sys/fs/bpf/daed ]; then + logger -t daed-init "cleanup: /sys/fs/bpf/daed exists; leaving normal pin root unchanged" + fi + + # Final verification covers only netns and veth state. [ ! -e /run/netns/daens ] || return 1 - ! ip netns list 2>/dev/null | awk '$1 == "daens" { found=1 } END { exit !found }' || return 1 + ! ip netns list 2>/dev/null | grep -Eq '^daens([[:space:]]|$)' || return 1 ! ip link show dae0 >/dev/null 2>&1 || return 1 + + return $rc } diff --git a/daed/files/daed-guard b/daed/files/daed-guard index cc6e5cf5c..aaaefc8d9 100644 --- a/daed/files/daed-guard +++ b/daed/files/daed-guard @@ -1,29 +1,131 @@ #!/bin/sh +# Keep daed as a child so signals can be forwarded and stale netns/veth +# state can be cleaned before start and after exit. daed owns TC detach; +# /sys/fs/bpf/daed is normal persistent state and is never removed here. . /usr/share/daed/cleanup.sh +# Pre-start cleanup refuses to remove runtime state while another +# daed instance is active, and fails closed if that probe is broken. +DAED_GUARD_CLEANUP=start if ! daed_cleanup_runtime; then - echo "daed: stale network state could not be removed" >&2 + echo "daed: stale /usr/bin/daed or netns state could not be verified or removed; refusing to start. Check process and netns state." >&2 + logger -t daed-init "pre-start cleanup failed: refusing to start daed" exit 1 fi -# Keep daed as a child so a panic or unexpected exit is followed by an -# immediate teardown of all kernel/runtime state before procd can respawn us. +# Keep daed as a child so post-exit cleanup runs before procd can +# respawn it. child_pid= -cleanup_child() { - [ -n "$child_pid" ] && kill "$child_pid" 2>/dev/null +pending_signal= +shutdown_signal= +shutdown_elapsed=0 +forced_kill=0 +child_term_timeout=20 + +forward_signal() { + local sig="$1" + if [ -z "$child_pid" ]; then + pending_signal="$sig" + logger -t daed-init "signal $sig received before daed child started; launch cancelled" + return 0 + fi + case "$sig" in + TERM|INT|QUIT) + if [ -z "$shutdown_signal" ]; then + shutdown_signal="$sig" + shutdown_elapsed=0 + fi + ;; + esac + kill -"$sig" "$child_pid" 2>/dev/null } -trap cleanup_child TERM INT -/usr/bin/daed "$@" & -child_pid=$! -wait "$child_pid" -status=$? -child_pid= -trap - TERM INT +exit_with_signal() { + local sig="$1" + trap - TERM INT HUP QUIT + kill -"$sig" "$$" 2>/dev/null + exit 1 +} -if ! daed_cleanup_runtime; then - echo "daed: runtime cleanup after exit failed" >&2 - status=1 +child_is_running() { + local state + kill -0 "$child_pid" 2>/dev/null || return 1 + state=$(awk '{ print $3 }' "/proc/$child_pid/stat" 2>/dev/null) + [ "$state" != "Z" ] +} +trap 'forward_signal TERM' TERM +trap 'forward_signal INT' INT +trap 'forward_signal HUP' HUP +trap 'forward_signal QUIT' QUIT + +start_child() { + local sig + # Keep this check inside the function as well as at the call site: + # it closes the ordinary pre-start window, while the pending-signal + # path below handles a signal arriving during the background fork. + [ -z "$pending_signal" ] || return 125 + /usr/bin/daed "$@" & + child_pid=$! + if [ -n "$pending_signal" ]; then + sig="$pending_signal" + pending_signal= + forward_signal "$sig" + fi +} + +if [ -n "$pending_signal" ]; then + logger -t daed-init "refusing to start daed after pending signal $pending_signal" + exit_with_signal "$pending_signal" fi -exit "$status" + +# Best-effort OOM preference; failure must not block startup. +if ! echo -16 > /proc/self/oom_score_adj 2>/dev/null; then + logger -t daed-init "warn: failed to set /proc/self/oom_score_adj; continuing without OOM preference" +fi +if ! start_child "$@"; then + logger -t daed-init "refusing to start daed after pending signal $pending_signal" + if [ -n "$pending_signal" ]; then + exit_with_signal "$pending_signal" + fi + exit 1 +fi +status=0 +reaped=0 +while [ "$reaped" -eq 0 ]; do + if [ -n "$shutdown_signal" ] && child_is_running; then + if [ "$shutdown_elapsed" -ge "$child_term_timeout" ]; then + if [ "$forced_kill" -eq 0 ]; then + logger -t daed-init "daed did not exit within ${child_term_timeout}s after $shutdown_signal; sending KILL" + kill -KILL "$child_pid" 2>/dev/null + forced_kill=1 + fi + else + sleep 1 + shutdown_elapsed=$((shutdown_elapsed + 1)) + continue + fi + sleep 1 + continue + fi + wait "$child_pid" 2>/dev/null + status=$? + if ! child_is_running; then + reaped=1 + fi +done +child_pid= +trap - TERM INT HUP QUIT + +# Post-exit cleanup runs after the child is reaped. +DAED_GUARD_CLEANUP=post-exit +cleanup_status=0 +daed_cleanup_runtime || cleanup_status=$? +if [ "$cleanup_status" -ne 0 ]; then + echo "daed: runtime cleanup after exit failed" >&2 + logger -t daed-init "post-exit cleanup failed; check ip netns / ip link show" +fi +if [ "$status" -ne 0 ]; then + exit "$status" +fi +exit "$cleanup_status" diff --git a/daed/files/daed.init b/daed/files/daed.init index 51e9989a3..be0a7282e 100644 --- a/daed/files/daed.init +++ b/daed/files/daed.init @@ -1,5 +1,7 @@ #!/bin/sh /etc/rc.common # Copyright (C) 2023 Tianling Shen +# daed-guard handles bounded child shutdown and post-exit netns/veth cleanup. +# Keep the log file and a pre-stop state snapshot for diagnostics. USE_PROCD=1 START=99 @@ -10,18 +12,28 @@ LOG="/var/log/daed/daed.log" . /usr/share/daed/cleanup.sh +log() { + logger -t daed-init "$@" +} + start_service() { + log "start: begin" config_load "$CONF" local enabled config_get_bool enabled "config" "enabled" "0" - [ "$enabled" -eq "1" ] || return 1 + if [ "$enabled" -ne "1" ]; then + log "start: config disabled, exit" + return 1 + fi local listen_addr log_maxbackups log_maxsize config_get listen_addr "config" "listen_addr" "0.0.0.0:2023" config_get log_maxbackups "config" "log_maxbackups" "1" config_get log_maxsize "config" "log_maxsize" "5" + log "start: listen=$listen_addr log_maxbackups=$log_maxbackups log_maxsize=$log_maxsize" + procd_open_instance "$CONF" procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)" procd_set_param command "$PROG" run @@ -33,25 +45,37 @@ start_service() { procd_set_param limits core="unlimited" procd_set_param limits nofile="1000000 1000000" - # Avoid an endless crash/respawn loop which can repeatedly reattach dae's - # data-plane hooks and make the router management plane unreachable. - procd_set_param respawn 3600 5 5 + # daed-guard escalates its child after 20 seconds. Leave time for + # reap and post-exit cleanup before procd kills the wrapper. + procd_set_param term_timeout 30 + # procd_set_param respawn: arguments are (threshold, timeout, retry). + # threshold = runtime that resets the short-lived exit counter + # timeout = seconds to wait between retries + # retry = maximum short-lived exits before procd gives up + # Reset the counter after one hour of stable runtime; otherwise retry + # after 5 seconds and stop after 10 failures. + procd_set_param respawn 3600 5 10 + # daed-guard sets oom_score_adj before forking; procd has no + # oom_adj/oom_score_adj parameter. # procd_set_param stdout 1 procd_set_param stderr 1 procd_close_instance + log "start: procd_open_instance done" } stop_service() { - rm -f "$LOG" - daed_cleanup_runtime -} - -restart() { - stop - sleep 1 - daed_cleanup_runtime - start + log "stop: begin" + # Cleanup runs in daed-guard after its child exits. Record only a + # pre-stop snapshot here; pin entries do not prove TC attachment. + local pinned="" ns_left="" + if [ -d /sys/fs/bpf/daed ]; then + pinned=$(ls /sys/fs/bpf/daed 2>/dev/null | tr '\n' ' ') + fi + if ip netns list 2>/dev/null | grep -q '^daens'; then + ns_left="daens" + fi + log "stop: pre-stop state — bpf_pin_entries=[${pinned:-none}] netns_left=[${ns_left:-none}]" } service_triggers() { diff --git a/luci-app-easytier-next/Makefile b/luci-app-easytier-next/Makefile new file mode 100644 index 000000000..3b4b9e6d4 --- /dev/null +++ b/luci-app-easytier-next/Makefile @@ -0,0 +1,27 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for EasyTier Next +LUCI_URL:=https://github.com/EasyTier/EasyTier +PKG_DESCRIPTION:=Provides a modern LuCI Web management interface for EasyTier decentralized mesh VPN. +PKG_MAINTAINER:=Dante +LUCI_DEPENDS:=+easytier-next +luci-base +LUCI_PKGARCH:=all + +define Package/luci-app-easytier-next/conffiles +/etc/config/easytier +/etc/easytier/ +endef + +define Package/luci-app-easytier-next/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + rm -f /tmp/luci-indexcache.* /tmp/luci-modulecache/* 2>/dev/null + chmod 0755 /etc/init.d/easytier /etc/hotplug.d/iface/99-easytier-routes 2>/dev/null || true + /etc/init.d/rpcd restart 2>/dev/null || true +} +exit 0 +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-easytier-next/htdocs/luci-static/resources/view/easytier/main.js b/luci-app-easytier-next/htdocs/luci-static/resources/view/easytier/main.js new file mode 100644 index 000000000..fc71ee8a2 --- /dev/null +++ b/luci-app-easytier-next/htdocs/luci-static/resources/view/easytier/main.js @@ -0,0 +1,2220 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; +'require rpc'; +'require ui'; +'require poll'; +'require dom'; + +const callGetStatus = rpc.declare({ + object: 'easytier', + method: 'get_status', + expect: { } +}); + +const callGetPeers = rpc.declare({ + object: 'easytier', + method: 'get_peers', + expect: { } +}); + +const callGetTopology = rpc.declare({ + object: 'easytier', + method: 'get_topology', + expect: { } +}); + +const callGetLogs = rpc.declare({ + object: 'easytier', + method: 'get_logs', + expect: { } +}); + +const callClearLogs = rpc.declare({ + object: 'easytier', + method: 'clear_logs' +}); + +const callGetSubroutes = rpc.declare({ + object: 'easytier', + method: 'get_subroutes', + expect: { routes: [] } +}); + +const callServiceAction = rpc.declare({ + object: 'easytier', + method: 'service_action', + params: ['action'] +}); + +function createActionBtn(label, btnClass, onClick) { + return E('button', { + 'class': 'btn cbi-button ' + btnClass, + 'style': 'margin-left: 10px; padding: 2px 10px; font-size: 12px; vertical-align: middle;', + 'click': onClick + }, label); +} + +function renderCoreStatus(stateObj) { + const state = stateObj ? stateObj.state : 'stopped'; + const pid = stateObj ? stateObj.pid : null; + const isRunning = (state === 'managed' || state === 'unmanaged'); + + let text = _('Stopped'); + let color = 'red'; + if (state === 'managed') { + text = _('Running'); + color = 'green'; + } else if (state === 'unmanaged') { + text = _('Running (Unmanaged)'); + color = 'orange'; + } + if (pid) text += ' [PID: ' + pid + ']'; + + const fields = [ + E('span', { 'style': 'font-weight: bold; color: ' + color + ';' }, text) + ]; + + if (isRunning) { + fields.push( + createActionBtn(_('Restart'), 'cbi-button-action', function(ev) { + ui.showModal(_('Action'), [ E('p', {}, _('Restarting service...')) ]); + return callServiceAction('restart').then(function() { + window.location.reload(); + }); + }) + ); + } + + return E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Core Service Status')), + E('div', { 'class': 'cbi-value-field' }, fields) + ]); +} + +function renderWebStatus(stateObj, webInstalled) { + if (webInstalled === false) { + return E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Web Console Status')), + E('div', { 'class': 'cbi-value-field' }, [ + E('span', { 'style': 'font-weight: bold; color: #6c757d;' }, _('Not Installed')) + ]) + ]); + } + + const state = stateObj ? stateObj.state : 'stopped'; + const pid = stateObj ? stateObj.pid : null; + const isRunning = (state === 'managed' || state === 'unmanaged'); + + let text = _('Stopped'); + let color = 'red'; + if (state === 'managed') { + text = _('Running'); + color = 'green'; + } else if (state === 'unmanaged') { + text = _('Running (Unmanaged)'); + color = 'orange'; + } + if (pid) text += ' [PID: ' + pid + ']'; + + const fields = [ + E('span', { 'style': 'font-weight: bold; color: ' + color + ';' }, text) + ]; + + if (isRunning) { + const port = uci.get('easytier', 'settings', 'web_html_port') || '22020'; + const url = 'http://' + window.location.hostname + ':' + port; + fields.push( + createActionBtn(_('Restart'), 'cbi-button-action', function(ev) { + ui.showModal(_('Action'), [ E('p', {}, _('Restarting service...')) ]); + return callServiceAction('restart').then(function() { + window.location.reload(); + }); + }), + E('a', { + 'class': 'btn cbi-button cbi-button-action', + 'style': 'margin-left: 10px; padding: 2px 10px; font-size: 12px; vertical-align: middle;', + 'href': url, + 'target': '_blank' + }, _('Open Console')) + ); + } + + return E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Web Console Status')), + E('div', { 'class': 'cbi-value-field' }, fields) + ]); +} + +function renderProxyCidrBadges(proxyStr) { + if (!proxyStr || typeof proxyStr !== 'string' || proxyStr.trim() === '' || proxyStr === '-') { + return '-'; + } + const list = proxyStr.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }); + if (list.length === 0) return '-'; + + return E('div', { 'style': 'display: flex; flex-direction: column; gap: 4px;' }, list.map(function(cidr) { + return E('span', { + 'style': 'display: inline-block; padding: 1px 6px; font-size: 11px; font-family: monospace; font-weight: 600; color: #047857; background: #ecfdf5; border: 1px solid #a7f3d0; border-radius: 3px; white-space: nowrap; width: fit-content;' + }, cidr); + })); +} + +function renderLocalNodeInfo(peerData) { + let peers = []; + if (Array.isArray(peerData)) { + peers = peerData; + } else if (peerData && Array.isArray(peerData.peers)) { + peers = peerData.peers; + } + + let local = null; + for (let i = 0; i < peers.length; i++) { + if (peers[i].cost && String(peers[i].cost).trim().toLowerCase() === 'local') { + local = peers[i]; + break; + } + } + + if (!local) { + return E('em', {}, _('Local service is not running.')); + } + + const ipv4Val = local.ipv4 ? String(local.ipv4).trim() : '-'; + const hostnameVal = local.hostname ? String(local.hostname).trim() : '-'; + const natVal = local.nat ? String(local.nat).trim() : '-'; + const versionVal = local.version ? String(local.version).trim() : '-'; + const netNameVal = uci.get('easytier', 'settings', 'network_name') || 'easytier'; + const devNameVal = uci.get('easytier', 'settings', 'dev_name') || 'easytier0'; + + let localProxy = local.proxy_cidrs ? String(local.proxy_cidrs).trim() : ''; + if (!localProxy) { + let cfgProxy = uci.get('easytier', 'settings', 'proxy_networks'); + if (Array.isArray(cfgProxy) && cfgProxy.length > 0) localProxy = cfgProxy.join(', '); + else if (typeof cfgProxy === 'string' && cfgProxy.trim() !== '') localProxy = cfgProxy.trim(); + } + + const proxyDisp = renderProxyCidrBadges(localProxy); + + const infoItems = [ + { label: _('Virtual IP'), value: E('strong', { 'style': 'color: #007bff;' }, ipv4Val) }, + { label: _('Hostname'), value: E('strong', {}, hostnameVal) }, + { label: _('Network Name'), value: netNameVal }, + { label: _('Proxy Networks'), value: proxyDisp }, + { label: _('Virtual Interface'), value: devNameVal }, + { label: _('NAT Type'), value: natVal }, + { label: _('Client Version'), value: versionVal } + ]; + + return E('div', { 'class': 'cbi-section-node' }, [ + E('div', { 'style': 'overflow-x: auto; -webkit-overflow-scrolling: touch; margin-bottom: 10px; border: 1px solid #e5e5e5; border-radius: 4px;' }, [ + E('table', { 'class': 'cbi-table', 'style': 'width: 100%; border-collapse: separate; border-spacing: 0;' }, [ + E('tr', { 'class': 'cbi-table-header' }, infoItems.map(item => E('th', { 'class': 'cbi-table-cell', 'style': 'padding: 10px 14px; text-align: left; white-space: nowrap; font-weight: bold; min-width: 120px;' }, item.label))), + E('tr', { 'class': 'cbi-row' }, infoItems.map(item => E('td', { 'class': 'cbi-value-field', 'style': 'padding: 8px 14px; text-align: left; white-space: nowrap;' }, item.value))) + ]) + ]) + ]); +} + +function isThemeDark() { + if (document.querySelector('[data-darkmode="true"]') || + document.body.getAttribute('data-darkmode') === 'true' || + document.documentElement.getAttribute('data-darkmode') === 'true' || + document.documentElement.getAttribute('data-theme') === 'dark' || + document.body.classList.contains('dark') || + document.body.classList.contains('theme-dark') || + (document.querySelector('link[href*="dark"]') !== null)) { + return true; + } + try { + const bg = window.getComputedStyle(document.body).backgroundColor; + const m = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (m) { + const r = parseInt(m[1]), g = parseInt(m[2]), b = parseInt(m[3]); + if ((r * 0.299 + g * 0.587 + b * 0.114) < 130) return true; + } + } catch(e) {} + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; +} + +function injectTooltipStyle() { + if (document.getElementById('et_custom_tooltip_style')) return; + const css = [ + '.et-tooltip-wrap { position: relative; display: inline-block; cursor: pointer; }', + '.et-tooltip-wrap .et-tooltip-bubble { visibility: hidden; opacity: 0; position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%) translateY(4px); background: #0f172a; color: #f8fafc; font-size: 11px; font-weight: 500; font-family: monospace, sans-serif; white-space: nowrap; padding: 5px 9px; border-radius: 4px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28); z-index: 9999; pointer-events: none; transition: opacity 0.15s ease, transform 0.15s ease; border: 1px solid #334155; }', + '.et-tooltip-wrap .et-tooltip-bubble::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #0f172a transparent transparent transparent; }', + '.et-tooltip-wrap:hover .et-tooltip-bubble { visibility: visible; opacity: 1; transform: translateX(-50%) translateY(0); }', + '', + '/* 拓扑图主题自适应样式 */', + '.et-node-title { fill: #0f172a; }', + '.et-node-title.is-local { fill: #1e3a8a; }', + '.et-node-ip { fill: #64748b; }', + '.et-node-ip.is-local { fill: #2563eb; }', + '.et-node-circle { fill: #ffffff; stroke: #94a3b8; }', + '.et-node-circle.is-local { fill: #2563eb; stroke: #1d4ed8; }', + '.et-node-server-rect { fill: #f8fafc; stroke: #475569; }', + '.et-node-proxy-bg { fill: #ecfdf5; stroke: #a7f3d0; }', + '.et-node-proxy-text { fill: #047857; }', + '.et-badge-rect { fill: #ffffff; }', + '.et-topo-container { background-color: #f8fafc; background-image: radial-gradient(#cbd5e1 1.2px, transparent 1.2px); border-color: #e2e8f0; }', + '.et-topo-legend { color: #475569; }', + '.et-btn-toolbar { background: rgba(255, 255, 255, 0.95); color: #475569; border-color: #cbd5e1; }', + '', + '/* 夜间/暗黑主题自适应 (响应系统偏好及各类 OpenWrt 暗色主题) */', + '@media (prefers-color-scheme: dark) {', + ' .et-node-title { fill: #f8fafc !important; }', + ' .et-node-title.is-local { fill: #93c5fd !important; }', + ' .et-node-ip { fill: #94a3b8 !important; }', + ' .et-node-ip.is-local { fill: #60a5fa !important; }', + ' .et-node-circle { fill: #1e293b !important; stroke: #475569 !important; }', + ' .et-node-server-rect { fill: #0f172a !important; stroke: #64748b !important; }', + ' .et-node-proxy-bg { fill: #064e3b !important; stroke: #059669 !important; }', + ' .et-node-proxy-text { fill: #6ee7b7 !important; }', + ' .et-badge-rect { fill: #1e293b !important; }', + ' .et-topo-container { background-color: #0f172a !important; background-image: radial-gradient(#334155 1.2px, transparent 1.2px) !important; border-color: #334155 !important; }', + ' .et-topo-legend { color: #94a3b8 !important; }', + ' .et-btn-toolbar { background: rgba(30, 41, 59, 0.95) !important; color: #f1f5f9 !important; border-color: #475569 !important; }', + '}', + '', + '[data-darkmode="true"] .et-node-title, [data-theme="dark"] .et-node-title, [data-mode="dark"] .et-node-title, .dark .et-node-title, .theme-dark .et-node-title, body[data-theme="dark"] .et-node-title, .et-dark .et-node-title { fill: #f8fafc !important; }', + '[data-darkmode="true"] .et-node-title.is-local, [data-theme="dark"] .et-node-title.is-local, [data-mode="dark"] .et-node-title.is-local, .dark .et-node-title.is-local, .theme-dark .et-node-title.is-local, body[data-theme="dark"] .et-node-title.is-local, .et-dark .et-node-title.is-local { fill: #93c5fd !important; }', + '[data-darkmode="true"] .et-node-ip, [data-theme="dark"] .et-node-ip, [data-mode="dark"] .et-node-ip, .dark .et-node-ip, .theme-dark .et-node-ip, body[data-theme="dark"] .et-node-ip, .et-dark .et-node-ip { fill: #94a3b8 !important; }', + '[data-darkmode="true"] .et-node-ip.is-local, [data-theme="dark"] .et-node-ip.is-local, [data-mode="dark"] .et-node-ip.is-local, .dark .et-node-ip.is-local, .theme-dark .et-node-ip.is-local, body[data-theme="dark"] .et-node-ip.is-local, .et-dark .et-node-ip.is-local { fill: #60a5fa !important; }', + '[data-darkmode="true"] .et-node-circle, [data-theme="dark"] .et-node-circle, [data-mode="dark"] .et-node-circle, .dark .et-node-circle, .theme-dark .et-node-circle, body[data-theme="dark"] .et-node-circle, .et-dark .et-node-circle { fill: #1e293b !important; stroke: #475569 !important; }', + '[data-darkmode="true"] .et-node-server-rect, [data-theme="dark"] .et-node-server-rect, [data-mode="dark"] .et-node-server-rect, .dark .et-node-server-rect, .theme-dark .et-node-server-rect, body[data-theme="dark"] .et-node-server-rect, .et-dark .et-node-server-rect { fill: #0f172a !important; stroke: #64748b !important; }', + '[data-darkmode="true"] .et-node-proxy-bg, [data-theme="dark"] .et-node-proxy-bg, [data-mode="dark"] .et-node-proxy-bg, .dark .et-node-proxy-bg, .theme-dark .et-node-proxy-bg, body[data-theme="dark"] .et-node-proxy-bg, .et-dark .et-node-proxy-bg { fill: #064e3b !important; stroke: #059669 !important; }', + '[data-darkmode="true"] .et-node-proxy-text, [data-theme="dark"] .et-node-proxy-text, [data-mode="dark"] .et-node-proxy-text, .dark .et-node-proxy-text, .theme-dark .et-node-proxy-text, body[data-theme="dark"] .et-node-proxy-text, .et-dark .et-node-proxy-text { fill: #6ee7b7 !important; }', + '[data-darkmode="true"] .et-badge-rect, [data-theme="dark"] .et-badge-rect, [data-mode="dark"] .et-badge-rect, .dark .et-badge-rect, .theme-dark .et-badge-rect, body[data-theme="dark"] .et-badge-rect, .et-dark .et-badge-rect { fill: #1e293b !important; }', + '[data-darkmode="true"] .et-topo-container, [data-theme="dark"] .et-topo-container, [data-mode="dark"] .et-topo-container, .dark .et-topo-container, .theme-dark .et-topo-container, body[data-theme="dark"] .et-topo-container, .et-dark.et-topo-container { background-color: #0f172a !important; background-image: radial-gradient(#334155 1.2px, transparent 1.2px) !important; border-color: #334155 !important; }', + '[data-darkmode="true"] .et-topo-legend, [data-theme="dark"] .et-topo-legend, [data-mode="dark"] .et-topo-legend, .dark .et-topo-legend, .theme-dark .et-topo-legend, body[data-theme="dark"] .et-topo-legend, .et-dark .et-topo-legend { color: #94a3b8 !important; }', + '[data-darkmode="true"] .et-btn-toolbar, [data-theme="dark"] .et-btn-toolbar, [data-mode="dark"] .et-btn-toolbar, .dark .et-btn-toolbar, .theme-dark .et-btn-toolbar, body[data-theme="dark"] .et-btn-toolbar, .et-dark .et-btn-toolbar { background: rgba(30, 41, 59, 0.95) !important; color: #f1f5f9 !important; border-color: #475569 !important; }', + '', + '/* 拓扑图连线防御宿主主题全局样式污染 */', + '#view div[style] > svg line.et-topo-link, line.et-topo-link { vector-effect: non-scaling-stroke; }', + '', + '/* 拓扑图链路流动动画 (自本端流向目标) */', + '@keyframes et-dash-flow {', + ' from { stroke-dashoffset: 24px; }', + ' to { stroke-dashoffset: 0px; }', + '}', + '@-webkit-keyframes et-dash-flow {', + ' from { stroke-dashoffset: 24px; }', + ' to { stroke-dashoffset: 0px; }', + '}', + '.et-path-flowing {', + ' animation: et-dash-flow 0.85s linear infinite !important;', + ' -webkit-animation: et-dash-flow 0.85s linear infinite !important;', + '}' + ].join('\n'); + document.head.appendChild(E('style', { id: 'et_custom_tooltip_style' }, css)); +} + +function renderPeersTable(peerData) { + injectTooltipStyle(); + let peers = []; + if (Array.isArray(peerData)) { + peers = peerData; + } else if (peerData && Array.isArray(peerData.peers)) { + peers = peerData.peers; + } + + const remotePeers = peers.filter(p => !p.cost || String(p.cost).trim().toLowerCase() !== 'local'); + + if (!remotePeers || remotePeers.length === 0) { + if (peerData && peerData.raw && peerData.raw.length > 0 && peers.length === 0) { + return E('pre', { 'style': 'padding: 10px; border-radius: 4px; overflow-x: auto; max-height: 350px; font-family: monospace;' }, peerData.raw); + } + return E('em', {}, _('No connected remote peers found.')); + } + + const headers = [ + { title: _('Virtual IP'), minWidth: '120px' }, + { title: _('Hostname'), minWidth: '110px' }, + { title: _('Proxy Networks'), minWidth: '110px' }, + { title: _('Cost'), minWidth: '70px' }, + { title: _('Latency / Loss'), minWidth: '100px' }, + { title: _('RX / TX'), minWidth: '120px' }, + { title: _('Tunnel'), minWidth: '65px' }, + { title: _('NAT Type'), minWidth: '100px' }, + { title: _('Version'), minWidth: '100px' } + ]; + + const thStyle = 'padding: 8px 10px; text-align: left; white-space: nowrap; font-weight: bold; vertical-align: middle;'; + const tdStyle = 'padding: 6px 10px; text-align: left; white-space: nowrap; vertical-align: middle;'; + + const rows = [ + E('tr', { 'class': 'cbi-table-header' }, headers.map(function(h) { + return E('th', { 'class': 'cbi-table-cell', 'style': thStyle + (h.minWidth ? (' min-width: ' + h.minWidth + ';') : '') }, h.title); + })) + ]; + + for (let i = 0; i < remotePeers.length; i++) { + const p = remotePeers[i]; + const costStr = p.cost ? String(p.cost).trim() : '-'; + let costNode = costStr; + + const isRelay = /relay/i.test(costStr); + const nextHopHost = p.next_hop_hostname ? String(p.next_hop_hostname).trim() : ''; + const nextHopIp = p.next_hop_ipv4 ? String(p.next_hop_ipv4).trim().split('/')[0] : ''; + const hasNextHop = nextHopHost && nextHopHost !== '-' && nextHopHost.toLowerCase() !== 'direct' && nextHopHost.toLowerCase() !== 'local'; + + if (isRelay) { + if (hasNextHop) { + const tipText = (nextHopIp && nextHopIp !== '-') ? + ('下一跳: ' + nextHopHost + ' (' + nextHopIp + ')') : + ('下一跳: ' + nextHopHost); + + costNode = E('div', { 'class': 'et-tooltip-wrap' }, [ + E('span', { + 'style': 'display: inline-block; padding: 1px 6px; font-size: 11px; font-weight: 600; color: #b45309; background: #fef3c7; border: 1px solid #fde68a; border-radius: 3px;' + }, costStr), + E('div', { 'class': 'et-tooltip-bubble' }, tipText) + ]); + } else { + costNode = E('span', { + 'style': 'display: inline-block; padding: 1px 6px; font-size: 11px; font-weight: 600; color: #b45309; background: #fef3c7; border: 1px solid #fde68a; border-radius: 3px;' + }, costStr); + } + } else if (costStr.toLowerCase() === 'p2p' || costStr.toLowerCase() === 'direct') { + costNode = E('span', { + 'style': 'display: inline-block; padding: 1px 6px; font-size: 11px; font-weight: 600; color: #047857; background: #ecfdf5; border: 1px solid #a7f3d0; border-radius: 3px;' + }, costStr); + } + + const latencyVal = p.latency ? String(p.latency).trim() : '-'; + let latencyColor = '#6c757d'; + const latNum = (latencyVal !== '-') ? parseFloat(latencyVal) : NaN; + if (!isNaN(latNum)) { + if (latNum > 150) { + latencyColor = '#d97706'; + } else if (latNum >= 50) { + latencyColor = '#2563eb'; + } else { + latencyColor = '#059669'; + } + } + const lossVal = p.loss_rate ? String(p.loss_rate).trim() : '-'; + const hasLoss = lossVal !== '-' && lossVal !== '0.0%' && lossVal !== '0%'; + + let latLossNode = '-'; + if (latencyVal !== '-') { + latLossNode = E('div', { 'style': 'display: inline-flex; align-items: baseline; gap: 4px;' }, [ + E('span', { 'style': 'color: ' + latencyColor + '; font-weight: bold;' }, latencyVal + ' ms'), + hasLoss ? E('span', { 'style': 'color: #dc2626; font-size: 10.5px; font-weight: 600;' }, '(' + lossVal + ')') : null + ].filter(Boolean)); + } + + const rxVal = p.rx ? String(p.rx).trim() : '-'; + const txVal = p.tx ? String(p.tx).trim() : '-'; + const trafficNode = (rxVal === '-' && txVal === '-') ? '-' : E('span', {}, [ + E('span', { 'style': 'color: #047857; font-weight: 500;' }, '↓ ' + rxVal), + E('span', { 'style': 'color: #94a3b8; margin: 0 4px;' }, '/'), + E('span', { 'style': 'color: #2563eb; font-weight: 500;' }, '↑ ' + txVal) + ]); + + const proxyNode = renderProxyCidrBadges(p.proxy_cidrs); + + rows.push(E('tr', { 'class': 'cbi-row' }, [ + E('td', { 'class': 'cbi-value-field', 'style': tdStyle }, E('strong', {}, p.ipv4 ? String(p.ipv4).trim() : '-')), + E('td', { 'class': 'cbi-value-field', 'style': tdStyle }, p.hostname ? String(p.hostname).trim() : '-'), + E('td', { 'class': 'cbi-value-field', 'style': tdStyle }, proxyNode), + E('td', { 'class': 'cbi-value-field', 'style': tdStyle }, costNode), + E('td', { 'class': 'cbi-value-field', 'style': tdStyle }, latLossNode), + E('td', { 'class': 'cbi-value-field', 'style': tdStyle }, trafficNode), + E('td', { 'class': 'cbi-value-field', 'style': tdStyle }, p.tunnel ? String(p.tunnel).trim() : '-'), + E('td', { 'class': 'cbi-value-field', 'style': tdStyle }, p.nat ? String(p.nat).trim() : '-'), + E('td', { 'class': 'cbi-value-field', 'style': tdStyle }, p.version ? String(p.version).trim() : '-') + ])); + } + + const tableNode = E('table', { 'class': 'cbi-table', 'style': 'width: 100%; border-collapse: separate; border-spacing: 0;' }, rows); + return E('div', { 'style': 'overflow-x: auto; -webkit-overflow-scrolling: touch; margin-bottom: 10px; border: 1px solid #e5e5e5; border-radius: 4px;' }, tableNode); +} + +function renderLogsView(logData) { + let lines = []; + if (Array.isArray(logData)) { + lines = logData; + } else if (logData && Array.isArray(logData.logs)) { + lines = logData.logs; + } else if (typeof logData === 'string') { + lines = logData.split('\n'); + } else if (logData && typeof logData.logs === 'string') { + lines = logData.logs.split('\n'); + } + + lines = lines.filter(function(l) { + return l && String(l).trim().length > 0; + }); + + const logText = (lines.length > 0) ? lines.join('\n') : _('No logs available.'); + + const textarea = E('textarea', { + 'class': 'cbi-input-textarea', + 'style': 'width: 100%; height: 450px; font-family: monospace; font-size: 12px; line-height: 1.5; resize: vertical; box-sizing: border-box; padding: 10px; border-radius: 4px;', + 'readonly': 'readonly', + 'wrap': 'off' + }, logText); + + return E('div', { 'style': 'width: 100%; margin-top: 5px;' }, textarea); +} + +const SVG_NS = 'http://www.w3.org/2000/svg'; + +function createSvg(tag, attrs, children) { + const el = document.createElementNS(SVG_NS, tag); + if (attrs) { + const keys = Object.keys(attrs); + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + el.setAttribute(k, attrs[k]); + } + } + if (children) { + if (Array.isArray(children)) { + for (let i = 0; i < children.length; i++) { + if (typeof children[i] === 'string') { + el.appendChild(document.createTextNode(children[i])); + } else if (children[i] instanceof Node) { + el.appendChild(children[i]); + } + } + } else if (typeof children === 'string') { + el.appendChild(document.createTextNode(children)); + } else if (children instanceof Node) { + el.appendChild(children); + } + } + return el; +} + +// 持久化拓扑图视口平移、缩放及自定义节点拖拽位置(跨定时刷新无缝保持) +const topologyViewState = { + scale: 1.0, + panX: 0, + panY: 0, + nodePositions: {}, + showAllLinks: false +}; + +// 计算从圆心 (cx, cy) 射向目标点 (targetX, targetY) 在圆周 (半径 radius) 上的精确交点 +function getCircleEdgePoint(cx, cy, targetX, targetY, radius) { + const dx = targetX - cx; + const dy = targetY - cy; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist === 0) return { x: cx, y: cy }; + const r = radius + 2; + return { + x: Math.round(cx + (dx / dist) * r), + y: Math.round(cy + (dy / dist) * r) + }; +} + +function renderTopologySvg(topoData, peerData) { + injectTooltipStyle(); + let rawNodes = []; + if (Array.isArray(topoData)) { + rawNodes = topoData; + } else if (topoData && Array.isArray(topoData.nodes)) { + rawNodes = topoData.nodes; + } + + // 过滤幽灵节点与无效 IP + rawNodes = (rawNodes || []).filter(function(n) { + if (!n) return false; + const host = n.hostname ? String(n.hostname).trim().toLowerCase() : ''; + const ip = n.ipv4 ? String(n.ipv4).trim() : ''; + if (!host || host === 'unknown') return false; + if (!ip || ip === '-' || ip === 'null') return false; + return true; + }); + + if (!rawNodes || rawNodes.length === 0) { + return E('em', {}, _('No topology data available.')); + } + + const validNodeIdMap = {}; + for (let i = 0; i < rawNodes.length; i++) { + if (rawNodes[i].node_id) { + validNodeIdMap[String(rawNodes[i].node_id)] = true; + } + } + + let localIpv4 = ''; + let peers = []; + if (Array.isArray(peerData)) peers = peerData; + else if (peerData && Array.isArray(peerData.peers)) peers = peerData.peers; + for (let i = 0; i < peers.length; i++) { + if (peers[i].cost && String(peers[i].cost).trim().toLowerCase() === 'local') { + if (peers[i].ipv4) localIpv4 = String(peers[i].ipv4).trim().split('/')[0]; + break; + } + } + + let nodes = []; + let localNode = null; + for (let i = 0; i < rawNodes.length; i++) { + const n = rawNodes[i]; + const nIp = n.ipv4 ? String(n.ipv4).trim().split('/')[0] : ''; + if (localIpv4 && nIp === localIpv4) { + localNode = n; + } else { + nodes.push(n); + } + } + if (localNode) { + nodes.unshift(localNode); + } + + const localPeerLatencyMap = {}; + for (let i = 0; i < peers.length; i++) { + const p = peers[i]; + if (!p.cost || String(p.cost).trim().toLowerCase() === 'local') continue; + const latVal = parseFloat(p.latency); + if (!isNaN(latVal) && latVal > 0) { + const pIp = p.ipv4 ? String(p.ipv4).trim().split('/')[0] : ''; + const pHost = p.hostname ? String(p.hostname).trim() : ''; + if (pIp) localPeerLatencyMap[pIp] = Math.round(latVal); + if (pHost) localPeerLatencyMap[pHost] = Math.round(latVal); + } + } + + const peerRouteInfoMap = {}; + for (let i = 0; i < peers.length; i++) { + const p = peers[i]; + const pIp = p.ipv4 ? String(p.ipv4).trim().split('/')[0] : ''; + const pHost = p.hostname ? String(p.hostname).trim() : ''; + const nextHopHost = p.next_hop_hostname ? String(p.next_hop_hostname).trim() : ''; + const nextHopIp = p.next_hop_ipv4 ? String(p.next_hop_ipv4).trim().split('/')[0] : ''; + const isRelay = /relay/i.test(p.cost || ''); + const entry = { + isRelay: isRelay, + cost: p.cost || '-', + nextHopHost: nextHopHost, + nextHopIp: nextHopIp, + pathLatency: p.path_latency, + latency: p.latency + }; + if (pIp) peerRouteInfoMap[pIp] = entry; + if (pHost) peerRouteInfoMap[pHost] = entry; + } + + const nodeCount = nodes.length; + const linkMap = {}; + const showAllLinks = !!topologyViewState.showAllLinks; + + // 生成链路:保留全部对端拓扑连线以支持仅本端模式下的点击路径追踪 + for (let i = 0; i < nodeCount; i++) { + const src = nodes[i]; + const dPeers = src.direct_peers || []; + for (let j = 0; j < dPeers.length; j++) { + const dst = dPeers[j]; + if (!dst.node_id) continue; + if (!validNodeIdMap[String(dst.node_id)]) continue; + const dstHost = dst.hostname ? String(dst.hostname).trim().toLowerCase() : ''; + if (dstHost === 'unknown') continue; + + const isLocalLink = localNode && (String(src.node_id) === String(localNode.node_id) || String(dst.node_id) === String(localNode.node_id)); + + const pairKey = [src.node_id, dst.node_id].sort().join('---'); + if (!linkMap[pairKey]) { + linkMap[pairKey] = { + srcId: src.node_id, + dstId: dst.node_id, + isLocalLink: isLocalLink, + latencies: [] + }; + } + if (dst.latency_ms !== undefined && dst.latency_ms !== null && !isNaN(dst.latency_ms)) { + linkMap[pairKey].latencies.push(Number(dst.latency_ms)); + } + } + } + + // 智能仲裁链路真实延迟:优先本地实测 RTT + const linkKeys = Object.keys(linkMap); + for (let k = 0; k < linkKeys.length; k++) { + const link = linkMap[linkKeys[k]]; + const isLocal = (localNode && (String(link.srcId) === String(localNode.node_id) || String(link.dstId) === String(localNode.node_id))); + const otherNodeId = (localNode && String(link.srcId) === String(localNode.node_id)) ? link.dstId : link.srcId; + const otherNode = nodes.find(function(n) { return String(n.node_id) === String(otherNodeId); }); + + let resolvedLat = null; + if (isLocal && otherNode) { + const otherIp = otherNode.ipv4 ? String(otherNode.ipv4).trim().split('/')[0] : ''; + const otherHost = otherNode.hostname ? String(otherNode.hostname).trim() : ''; + if (otherIp && localPeerLatencyMap[otherIp] !== undefined) { + resolvedLat = localPeerLatencyMap[otherIp]; + } else if (otherHost && localPeerLatencyMap[otherHost] !== undefined) { + resolvedLat = localPeerLatencyMap[otherHost]; + } + } + + if (resolvedLat === null && link.latencies && link.latencies.length > 0) { + const validLats = link.latencies.filter(function(v) { return v > 0; }); + if (validLats.length === 1) { + resolvedLat = validLats[0]; + } else if (validLats.length > 1) { + const realLats = validLats.filter(function(v) { return v > 2; }); + if (realLats.length > 0) { + resolvedLat = Math.round(realLats.reduce(function(a, b) { return a + b; }, 0) / realLats.length); + } else { + resolvedLat = Math.round(validLats.reduce(function(a, b) { return a + b; }, 0) / validLats.length); + } + } + } + link.latency = resolvedLat; + } + + // 标记中继节点与中间跳的连线为树状生成连通链路(isRelayTreeLink) + for (let i = 0; i < nodeCount; i++) { + const n = nodes[i]; + const nIp = n.ipv4 ? String(n.ipv4).trim().split('/')[0] : ''; + const nHost = n.hostname ? String(n.hostname).trim() : ''; + const rInfo = peerRouteInfoMap[nIp] || peerRouteInfoMap[nHost]; + + if (rInfo && rInfo.isRelay && rInfo.nextHopHost) { + const midNode = nodes.find(function(m) { + const mH = m.hostname ? String(m.hostname).trim() : ''; + const mIp = m.ipv4 ? String(m.ipv4).trim().split('/')[0] : ''; + return (mH && mH.toLowerCase() === rInfo.nextHopHost.toLowerCase()) || + (rInfo.nextHopIp && mIp === rInfo.nextHopIp.split('/')[0]); + }); + + if (midNode) { + const relayPairKey = [midNode.node_id, n.node_id].sort().join('---'); + if (linkMap[relayPairKey]) { + linkMap[relayPairKey].isRelayTreeLink = true; + } + } + } + } + + // 动态正方形雷达画布尺寸(720 × 720,聚焦舒展) + const width = 720; + const height = 720; + const cx = Math.round(width / 2); + const cy = Math.round(height / 2); + + // 360° 逆时针延迟比例散射引擎(Counter-Clockwise Latency Proportional Radial Engine) + const nodeMap = {}; + const simNodes = []; + let peerNodes = []; + let localSimNode = null; + + for (let i = 0; i < nodeCount; i++) { + const n = nodes[i]; + const isLocal = (localNode && String(n.node_id) === String(localNode.node_id)); + + let directLat = 999; + if (!isLocal && localNode) { + const pairKey = [n.node_id, localNode.node_id].sort().join('---'); + if (linkMap[pairKey] && linkMap[pairKey].latency !== null && linkMap[pairKey].latency !== undefined) { + directLat = Number(linkMap[pairKey].latency); + } + } + + let posX = null; + let posY = null; + if (topologyViewState.nodePositions && topologyViewState.nodePositions[n.node_id]) { + posX = topologyViewState.nodePositions[n.node_id].x; + posY = topologyViewState.nodePositions[n.node_id].y; + } + + const sn = { + id: String(n.node_id), + node: n, + x: posX !== null ? posX : cx, + y: posY !== null ? posY : cy, + radius: isLocal ? 28 : 24, + isLocal: isLocal, + directLat: directLat + }; + + nodeMap[sn.id] = sn; + simNodes.push(sn); + + if (isLocal) { + localSimNode = sn; + } else { + peerNodes.push(sn); + } + } + + // 自动排布:以本节点为中心,按延迟从小到大逆时针 360° 依次等比例散射展开 + const needLayout = simNodes.some(function(sn) { + return !topologyViewState.nodePositions || !topologyViewState.nodePositions[sn.id]; + }); + + if (needLayout) { + if (localSimNode) { + localSimNode.x = cx; + localSimNode.y = cy; + } + + // 按延迟从小到大排序 + peerNodes.sort(function(a, b) { + return a.directLat - b.directLat; + }); + + const pCount = peerNodes.length; + if (pCount > 0) { + const startAngle = -Math.PI / 2; // 起点:12点钟正上方(最低延迟) + const stepAngle = (2 * Math.PI) / pCount; + + for (let i = 0; i < pCount; i++) { + const sn = peerNodes[i]; + const angle = startAngle - i * stepAngle; // 逆时针展开 + + // 饱满舒展的星轨微梯度:基准 255px,按延迟轻微起伏(235px ~ 285px) + let distR = 285; + if (sn.directLat < 900) { + if (sn.directLat <= 10) { + distR = 235; + } else if (sn.directLat <= 60) { + distR = 250; + } else if (sn.directLat <= 150) { + distR = 268; + } else { + distR = 285; + } + } + + sn.x = Math.round(cx + distR * Math.cos(angle)); + sn.y = Math.round(cy + distR * Math.sin(angle)); + } + } + + if (!topologyViewState.nodePositions) topologyViewState.nodePositions = {}; + for (let i = 0; i < nodeCount; i++) { + const sn = simNodes[i]; + topologyViewState.nodePositions[sn.id] = { x: Math.round(sn.x), y: Math.round(sn.y) }; + } + } + + const posMap = {}; + for (let i = 0; i < nodeCount; i++) { + const sn = simNodes[i]; + posMap[sn.id] = { + x: Math.round(topologyViewState.nodePositions && topologyViewState.nodePositions[sn.id] ? topologyViewState.nodePositions[sn.id].x : sn.x), + y: Math.round(topologyViewState.nodePositions && topologyViewState.nodePositions[sn.id] ? topologyViewState.nodePositions[sn.id].y : sn.y) + }; + } + + function getLatencyColor(lat) { + if (lat === undefined || lat === null || isNaN(Number(lat))) { + return { + line: '#94a3b8', + text: '#64748b', + dash: '5,5', + width: '1.5', + opacity: '0.6' + }; + } + const val = Number(lat); + if (val > 150) { + return { + line: '#f59e0b', + text: '#d97706', + dash: '5,5', + width: '1.5', + opacity: '0.85' + }; + } else if (val >= 50) { + return { + line: '#3b82f6', + text: '#2563eb', + dash: '5,5', + width: '1.5', + opacity: '0.85' + }; + } else { + return { + line: '#10b981', + text: '#059669', + dash: '5,5', + width: '1.5', + opacity: '0.9' + }; + } + } + + function setLineVisual(el, strokeColor, strokeWidth, dashArray, opacityVal) { + if (!el) return; + el.setAttribute('stroke', strokeColor); + el.style.setProperty('stroke', strokeColor, 'important'); + if (strokeWidth !== undefined && strokeWidth !== null) { + el.setAttribute('stroke-width', strokeWidth); + el.style.setProperty('stroke-width', strokeWidth + 'px', 'important'); + } + if (dashArray !== undefined && dashArray !== null) { + el.setAttribute('stroke-dasharray', dashArray); + el.style.setProperty('stroke-dasharray', dashArray, 'important'); + } + if (opacityVal !== undefined && opacityVal !== null) { + el.setAttribute('opacity', opacityVal); + el.style.setProperty('opacity', opacityVal, 'important'); + } + } + + const lineElementsMap = {}; + const badgeElementsMap = {}; + const nodeElementsMap = {}; + + // SVG Defs:矩阵点阵背景网格与高质感阴影 + // SVG Defs:高质感阴影 + const defsNode = createSvg('defs', {}, [ + createSvg('filter', { + 'id': 'node-shadow', + 'x': '-30%', + 'y': '-30%', + 'width': '160%', + 'height': '160%' + }, [ + createSvg('feDropShadow', { + 'dx': '0', + 'dy': '3', + 'stdDeviation': '4', + 'flood-color': '#0f172a', + 'flood-opacity': '0.12' + }) + ]), + createSvg('filter', { + 'id': 'node-shadow-active', + 'x': '-50%', + 'y': '-50%', + 'width': '200%', + 'height': '200%' + }, [ + createSvg('feDropShadow', { + 'dx': '0', + 'dy': '5', + 'stdDeviation': '8', + 'flood-color': '#2563eb', + 'flood-opacity': '0.35' + }) + ]) + ]); + + const linesLayer = []; + const nodesLayer = []; + const badgesLayer = []; + + let hoveredLinkId = null; + let hoveredNodeId = null; + let selectedNodeId = null; + + const pathBanner = E('div', { + 'style': 'position: absolute; top: 12px; left: 12px; display: none; align-items: center; gap: 8px; padding: 6px 12px; background: rgba(15, 23, 42, 0.88); color: #f8fafc; font-size: 11.5px; font-weight: 600; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.25); z-index: 10; backdrop-filter: blur(4px);' + }); + + function traceRouteToNode(targetNodeId) { + const res = { + activePathLinks: {}, + traceNodes: [], + pathInfoText: '' + }; + if (!targetNodeId) return res; + + const targetNode = nodes.find(function(n) { return String(n.node_id) === String(targetNodeId); }); + if (!targetNode) return res; + + const isSelf = localNode && (String(targetNode.node_id) === String(localNode.node_id)); + const selIp = targetNode.ipv4 ? String(targetNode.ipv4).trim().split('/')[0] : ''; + const selHost = targetNode.hostname ? String(targetNode.hostname).trim() : ''; + const rInfo = peerRouteInfoMap[selIp] || peerRouteInfoMap[selHost] || {}; + + if (isSelf) { + res.pathInfoText = _('Local Node: %s').format(selHost || selIp); + res.traceNodes = [targetNode]; + return res; + } + + res.traceNodes = [targetNode]; + let curr = targetNode; + const visited = {}; + visited[String(curr.node_id)] = true; + + while (curr && localNode && String(curr.node_id) !== String(localNode.node_id)) { + const cIp = curr.ipv4 ? String(curr.ipv4).trim().split('/')[0] : ''; + const cHost = curr.hostname ? String(curr.hostname).trim() : ''; + const cInfo = peerRouteInfoMap[cIp] || peerRouteInfoMap[cHost]; + + let nextNode = null; + if (cInfo && cInfo.isRelay && cInfo.nextHopHost) { + nextNode = nodes.find(function(m) { + const mH = m.hostname ? String(m.hostname).trim() : ''; + const mIp = m.ipv4 ? String(m.ipv4).trim().split('/')[0] : ''; + return (mH && mH.toLowerCase() === cInfo.nextHopHost.toLowerCase()) || + (cInfo.nextHopIp && mIp === cInfo.nextHopIp.split('/')[0]); + }); + } else { + nextNode = localNode; + } + + if (!nextNode || visited[String(nextNode.node_id)]) { + if (curr !== localNode && localNode) { + const fallbackKey = [localNode.node_id, curr.node_id].sort().join('---'); + res.activePathLinks[fallbackKey] = { + upstreamId: localNode.node_id, + downstreamId: curr.node_id + }; + if (!visited[String(localNode.node_id)]) res.traceNodes.unshift(localNode); + } + break; + } + + visited[String(nextNode.node_id)] = true; + const pKey = [curr.node_id, nextNode.node_id].sort().join('---'); + res.activePathLinks[pKey] = { + upstreamId: nextNode.node_id, + downstreamId: curr.node_id + }; + + res.traceNodes.unshift(nextNode); + curr = nextNode; + } + + const nodeNames = res.traceNodes.map(function(n) { + const isNLocal = (localNode && String(n.node_id) === String(localNode.node_id)); + const h = n.hostname ? String(n.hostname).trim() : (n.ipv4 ? String(n.ipv4).trim().split('/')[0] : 'Node'); + return isNLocal ? (h + ' (Local)') : h; + }); + + const latStr = rInfo.pathLatency || rInfo.latency || '-'; + const hopCount = res.traceNodes.length - 1; + const hopLabel = (hopCount > 1) ? (' (' + hopCount + ' hops · ' + latStr + ' ms)') : (latStr !== '-' ? (' (' + latStr + ' ms)') : ''); + res.pathInfoText = nodeNames.join(' ➔ ') + hopLabel; + return res; + } + + function refreshLinkVisuals() { + const activeTargetId = selectedNodeId || hoveredNodeId; + const isFocusSelf = localNode && activeTargetId && (String(activeTargetId) === String(localNode.node_id)); + const routeData = (!isFocusSelf && activeTargetId) ? traceRouteToNode(activeTargetId) : { activePathLinks: {}, traceNodes: [], pathInfoText: '' }; + const activePathLinks = routeData.activePathLinks; + let pathInfoText = routeData.pathInfoText; + if (isFocusSelf) { + const selfHost = localNode.hostname ? String(localNode.hostname).trim() : (localNode.ipv4 || 'Local'); + pathInfoText = _('Local Node: %s').format(selfHost); + } + + // 更新路径横幅 + if (pathInfoText && activeTargetId) { + pathBanner.style.display = 'flex'; + const bannerChildren = [ + E('span', { 'style': 'color: #94a3b8; margin-right: 2px;' }, _('Path') + ':'), + E('span', { 'style': 'color: #f8fafc;' }, pathInfoText) + ]; + if (selectedNodeId) { + bannerChildren.push( + E('button', { + 'type': 'button', + 'title': _('Clear selection'), + 'style': 'margin-left: 6px; padding: 0 4px; background: transparent; border: none; color: #94a3b8; font-size: 13px; cursor: pointer;', + 'click': function(ev) { + ev.stopPropagation(); + selectedNodeId = null; + refreshLinkVisuals(); + } + }, '×') + ); + } + pathBanner.replaceChildren.apply(pathBanner, bannerChildren); + } else { + pathBanner.style.display = 'none'; + } + + const hasActivePath = Object.keys(activePathLinks).length > 0; + + // 更新所有链路显示 + for (let k = 0; k < linkKeys.length; k++) { + const lk = linkKeys[k]; + const link = linkMap[lk]; + const lineEl = lineElementsMap[lk]; + const badgeEl = badgeElementsMap[lk]; + if (!lineEl) continue; + + const isTreeLink = link.isLocalLink || link.isRelayTreeLink; + const isRelayTree = link.isRelayTreeLink; + const colorCfg = getLatencyColor(link.latency); + const pathLinkInfo = activePathLinks[lk]; + + if (hasActivePath) { + if (pathLinkInfo) { + // 路径上的链路:统一高亮、加粗、流动虚线动画,方向严格从上游流向下游目标 + lineEl.style.display = ''; + setLineVisual(lineEl, '#f59e0b', '3.2', '8,4', '1.0'); + lineEl.setAttribute('stroke-linecap', 'round'); + + const snUp = nodeMap[String(pathLinkInfo.upstreamId)]; + const snDown = nodeMap[String(pathLinkInfo.downstreamId)]; + const pUp = posMap[String(pathLinkInfo.upstreamId)]; + const pDown = posMap[String(pathLinkInfo.downstreamId)]; + if (snUp && snDown && pUp && pDown) { + const startPt = getCircleEdgePoint(pUp.x, pUp.y, pDown.x, pDown.y, snUp.radius); + const endPt = getCircleEdgePoint(pDown.x, pDown.y, pUp.x, pUp.y, snDown.radius); + lineEl.setAttribute('x1', startPt.x); + lineEl.setAttribute('y1', startPt.y); + lineEl.setAttribute('x2', endPt.x); + lineEl.setAttribute('y2', endPt.y); + } + + lineEl.classList.add('et-path-flowing'); + if (badgeEl) { + badgeEl.style.display = ''; + badgeEl.style.opacity = '1.0'; + } + } else { + lineEl.classList.remove('et-path-flowing'); + lineEl.setAttribute('x1', lineEl._origX1); + lineEl.setAttribute('y1', lineEl._origY1); + lineEl.setAttribute('x2', lineEl._origX2); + lineEl.setAttribute('y2', lineEl._origY2); + + if (!showAllLinks && !isTreeLink) { + lineEl.style.display = 'none'; + if (badgeEl) badgeEl.style.display = 'none'; + } else { + lineEl.style.display = ''; + setLineVisual(lineEl, colorCfg.line, '1.0', colorCfg.dash, '0.08'); + if (badgeEl) { + badgeEl.style.display = ''; + badgeEl.style.opacity = '0.08'; + } + } + } + } else { + // 未激活路径:恢复原始几何坐标与无流动类 + lineEl.classList.remove('et-path-flowing'); + lineEl.setAttribute('x1', lineEl._origX1); + lineEl.setAttribute('y1', lineEl._origY1); + lineEl.setAttribute('x2', lineEl._origX2); + lineEl.setAttribute('y2', lineEl._origY2); + + const isHoverFocused = (hoveredLinkId === lk) || + (hoveredNodeId && (String(link.srcId) === String(hoveredNodeId) || String(link.dstId) === String(hoveredNodeId))); + + if (!showAllLinks && !isTreeLink) { + lineEl.style.display = 'none'; + if (badgeEl) badgeEl.style.display = 'none'; + } else { + lineEl.style.display = ''; + if (badgeEl) badgeEl.style.display = ''; + + if (isHoverFocused) { + setLineVisual(lineEl, isRelayTree ? '#f59e0b' : colorCfg.line, '2.8', isRelayTree ? '6,4' : 'none', '1.0'); + if (badgeEl) badgeEl.style.opacity = '1.0'; + } else if (hoveredLinkId !== null || hoveredNodeId !== null) { + setLineVisual(lineEl, isRelayTree ? '#f59e0b' : colorCfg.line, '1.2', colorCfg.dash, '0.12'); + if (badgeEl) badgeEl.style.opacity = '0.12'; + } else { + setLineVisual(lineEl, isRelayTree ? '#f59e0b' : colorCfg.line, isRelayTree ? '1.8' : colorCfg.width, isRelayTree ? '5,4' : colorCfg.dash, isRelayTree ? '0.9' : colorCfg.opacity); + if (badgeEl) badgeEl.style.opacity = '0.9'; + } + } + } + } + + // 更新节点透明度 + for (let i = 0; i < nodeCount; i++) { + const n = nodes[i]; + const cardG = nodeElementsMap[String(n.node_id)]; + if (!cardG) continue; + + if (hasActivePath) { + const isSelected = (String(n.node_id) === String(selectedNodeId)); + const isTarget = (String(n.node_id) === String(activeTargetId)); + const isPathNode = isTarget || + (localNode && String(n.node_id) === String(localNode.node_id)) || + (Object.keys(activePathLinks).some(function(pk) { + const pl = linkMap[pk]; + return pl && (String(pl.srcId) === String(n.node_id) || String(pl.dstId) === String(n.node_id)); + })); + + if (isPathNode) { + cardG.style.opacity = '1.0'; + cardG.firstElementChild.setAttribute('filter', isTarget ? 'url(#node-shadow-active)' : 'url(#node-shadow)'); + } else { + cardG.style.opacity = '0.35'; + cardG.firstElementChild.setAttribute('filter', 'url(#node-shadow)'); + } + } else { + cardG.style.opacity = '1.0'; + cardG.firstElementChild.setAttribute('filter', 'url(#node-shadow)'); + } + } + } + + // 1. 绘制链路连线(纯净细腻虚线,交互悬停高亮) + for (let k = 0; k < linkKeys.length; k++) { + const linkKey = linkKeys[k]; + const link = linkMap[linkKey]; + const p1 = posMap[String(link.srcId)]; + const p2 = posMap[String(link.dstId)]; + const sn1 = nodeMap[String(link.srcId)]; + const sn2 = nodeMap[String(link.dstId)]; + if (!p1 || !p2 || !sn1 || !sn2) continue; + + const startPt = getCircleEdgePoint(p1.x, p1.y, p2.x, p2.y, sn1.radius); + const endPt = getCircleEdgePoint(p2.x, p2.y, p1.x, p1.y, sn2.radius); + const colorCfg = getLatencyColor(link.latency); + const isTreeLink = link.isLocalLink || link.isRelayTreeLink; + const isRelayTree = link.isRelayTreeLink; + const initDisplay = (!showAllLinks && !isTreeLink) ? 'none' : ''; + + const lineInitStroke = isRelayTree ? '#f59e0b' : colorCfg.line; + const lineInitWidth = isRelayTree ? '1.8' : colorCfg.width; + const lineInitDash = isRelayTree ? '5,4' : colorCfg.dash; + const lineInitOpacity = isRelayTree ? '0.9' : colorCfg.opacity; + + const lineSvg = createSvg('line', { + 'x1': startPt.x, 'y1': startPt.y, + 'x2': endPt.x, 'y2': endPt.y, + 'stroke': lineInitStroke, + 'stroke-width': lineInitWidth, + 'stroke-dasharray': lineInitDash, + 'stroke-linecap': 'round', + 'opacity': lineInitOpacity, + 'class': 'et-topo-link', + 'style': 'transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; display: ' + initDisplay + ';' + }); + setLineVisual(lineSvg, lineInitStroke, lineInitWidth, lineInitDash, lineInitOpacity); + lineSvg._origX1 = startPt.x; + lineSvg._origY1 = startPt.y; + lineSvg._origX2 = endPt.x; + lineSvg._origY2 = endPt.y; + + (function(lk) { + lineSvg.addEventListener('mouseenter', function() { + hoveredLinkId = lk; + refreshLinkVisuals(); + }); + lineSvg.addEventListener('mouseleave', function() { + if (hoveredLinkId === lk) { + hoveredLinkId = null; + refreshLinkVisuals(); + } + }); + })(linkKey); + + lineElementsMap[linkKey] = lineSvg; + linesLayer.push(lineSvg); + } + + // 2. 绘制圆形科技节点(支持自由拖拽与双翼规整) + let activeDraggingNodeId = null; + let mouseStartX = 0; + let mouseStartY = 0; + + const isDark = isThemeDark(); + + for (let i = 0; i < nodeCount; i++) { + const n = nodes[i]; + const sn = nodeMap[String(n.node_id)]; + const pos = posMap[String(n.node_id)]; + if (!pos || !sn) continue; + + const proxyList = (n.proxy_cidrs && String(n.proxy_cidrs).trim() !== '') ? + String(n.proxy_cidrs).split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) : []; + const proxyCount = proxyList.length; + + const isSelf = sn.isLocal; + const hostname = n.hostname ? String(n.hostname).trim() : 'Unknown'; + const ipv4 = n.ipv4 ? String(n.ipv4).trim() : '-'; + const titleStr = isSelf ? (hostname + ' (Local)') : hostname; + + const nodeR = sn.radius; + const circleColor = isSelf ? '#2563eb' : (isDark ? '#1e293b' : '#ffffff'); + const circleBorder = isSelf ? '#1d4ed8' : (isDark ? '#475569' : '#94a3b8'); + const borderWidth = isSelf ? '2.5' : '2'; + + const gChildren = []; + + // 本端脉冲外光环 + if (isSelf) { + gChildren.push(createSvg('circle', { + 'r': '34', + 'fill': 'none', + 'stroke': '#3b82f6', + 'stroke-width': '1.5', + 'stroke-dasharray': '4,3', + 'opacity': '0.75' + })); + } + + // 核心圆形徽标 + gChildren.push(createSvg('circle', { + 'r': nodeR, + 'fill': circleColor, + 'stroke': circleBorder, + 'stroke-width': borderWidth, + 'filter': 'url(#node-shadow)', + 'class': 'et-node-circle' + (isSelf ? ' is-local' : '') + })); + + // 精致矢量图标 + if (isSelf) { + gChildren.push( + createSvg('path', { + 'd': 'M -9 -2 L 9 -2 L 7 7 L -7 7 Z', + 'fill': 'none', + 'stroke': '#ffffff', + 'stroke-width': '1.8', + 'stroke-linejoin': 'round' + }), + createSvg('circle', { 'cx': '-4', 'cy': '2.5', 'r': '1', 'fill': '#ffffff' }), + createSvg('circle', { 'cx': '0', 'cy': '2.5', 'r': '1', 'fill': '#ffffff' }), + createSvg('circle', { 'cx': '4', 'cy': '2.5', 'r': '1', 'fill': '#ffffff' }), + createSvg('line', { 'x1': '-5', 'y1': '-2', 'x2': '-8', 'y2': '-8', 'stroke': '#ffffff', 'stroke-width': '1.8', 'stroke-linecap': 'round' }), + createSvg('line', { 'x1': '5', 'y1': '-2', 'x2': '8', 'y2': '-8', 'stroke': '#ffffff', 'stroke-width': '1.8', 'stroke-linecap': 'round' }) + ); + } else { + gChildren.push( + createSvg('rect', { 'x': '-9', 'y': '-8', 'width': '18', 'height': '6.5', 'rx': '1.5', 'fill': isDark ? '#0f172a' : '#f8fafc', 'stroke': isDark ? '#64748b' : '#475569', 'stroke-width': '1.5', 'class': 'et-node-server-rect' }), + createSvg('rect', { 'x': '-9', 'y': '1.5', 'width': '18', 'height': '6.5', 'rx': '1.5', 'fill': isDark ? '#0f172a' : '#f8fafc', 'stroke': isDark ? '#64748b' : '#475569', 'stroke-width': '1.5', 'class': 'et-node-server-rect' }), + createSvg('circle', { 'cx': '5', 'cy': '-4.7', 'r': '1', 'fill': '#10b981' }), + createSvg('circle', { 'cx': '5', 'cy': '4.8', 'r': '1', 'fill': '#10b981' }) + ); + gChildren.push(createSvg('circle', { + 'cx': '16', + 'cy': '-16', + 'r': '4', + 'fill': '#10b981', + 'stroke': '#ffffff', + 'stroke-width': '1.5' + })); + } + + // 下方文字标签(主机名 + 虚拟 IP) + const titleColor = isDark ? (isSelf ? '#93c5fd' : '#f8fafc') : (isSelf ? '#1e3a8a' : '#0f172a'); + const ipColor = isDark ? (isSelf ? '#60a5fa' : '#94a3b8') : (isSelf ? '#2563eb' : '#64748b'); + + gChildren.push( + createSvg('text', { + 'x': 0, + 'y': nodeR + 15, + 'text-anchor': 'middle', + 'font-family': 'sans-serif', + 'font-size': '11.5', + 'font-weight': isSelf ? 'bold' : '600', + 'fill': titleColor, + 'class': 'et-node-title' + (isSelf ? ' is-local' : '') + }, titleStr), + createSvg('text', { + 'x': 0, + 'y': nodeR + 29, + 'text-anchor': 'middle', + 'font-family': 'monospace, sans-serif', + 'font-size': '10', + 'font-weight': isSelf ? 'bold' : 'normal', + 'fill': ipColor, + 'class': 'et-node-ip' + (isSelf ? ' is-local' : '') + }, ipv4) + ); + + // 代理子网标签 + if (proxyCount > 0) { + for (let pIdx = 0; pIdx < proxyCount; pIdx++) { + const badgeY = nodeR + 35 + pIdx * 16; + const labelText = proxyList[pIdx]; + const bw = Math.max(90, labelText.length * 6.5 + 12); + gChildren.push( + createSvg('rect', { + 'x': -bw / 2, + 'y': badgeY, + 'width': bw, + 'height': '14', + 'rx': '3', + 'fill': '#ecfdf5', + 'stroke': '#a7f3d0', + 'stroke-width': '1', + 'class': 'et-node-proxy-bg' + }), + createSvg('text', { + 'x': 0, + 'y': badgeY + 10, + 'text-anchor': 'middle', + 'font-family': 'monospace, sans-serif', + 'font-size': '9', + 'font-weight': '600', + 'fill': '#047857', + 'class': 'et-node-proxy-text' + }, labelText) + ); + } + } + + const cardG = createSvg('g', { + 'transform': 'translate(' + pos.x + ',' + pos.y + ')', + 'style': 'cursor: pointer; user-select: none; transition: filter 0.15s;' + }, gChildren); + + // 节点鼠标交互 + (function(nodeId, gEl) { + let isDragMoved = false; + gEl.addEventListener('mouseenter', function() { + if (!activeDraggingNodeId) { + hoveredNodeId = nodeId; + refreshLinkVisuals(); + } + }); + gEl.addEventListener('mouseleave', function() { + if (!activeDraggingNodeId && hoveredNodeId === nodeId) { + hoveredNodeId = null; + refreshLinkVisuals(); + } + }); + gEl.addEventListener('mousedown', function(ev) { + if (ev.button !== 0) return; + ev.stopPropagation(); + activeDraggingNodeId = nodeId; + isDragMoved = false; + mouseStartX = ev.clientX; + mouseStartY = ev.clientY; + gEl.style.cursor = 'grabbing'; + gEl.firstElementChild.setAttribute('filter', 'url(#node-shadow-active)'); + }); + gEl.addEventListener('click', function(ev) { + ev.stopPropagation(); + if (isDragMoved) return; + if (selectedNodeId === nodeId) { + selectedNodeId = null; + } else { + selectedNodeId = nodeId; + } + refreshLinkVisuals(); + }); + })(n.node_id, cardG); + + nodeElementsMap[String(n.node_id)] = cardG; + nodesLayer.push(cardG); + } + + // 3. 绘制延迟标签(微胶囊,鼠标悬停时弹出高亮) + for (let k = 0; k < linkKeys.length; k++) { + const linkKey = linkKeys[k]; + const link = linkMap[linkKey]; + const p1 = posMap[String(link.srcId)]; + const p2 = posMap[String(link.dstId)]; + const sn1 = nodeMap[String(link.srcId)]; + const sn2 = nodeMap[String(link.dstId)]; + if (!p1 || !p2 || !sn1 || !sn2) continue; + + const lat = link.latency; + if (lat !== undefined && lat !== null) { + const startPt = getCircleEdgePoint(p1.x, p1.y, p2.x, p2.y, sn1.radius); + const endPt = getCircleEdgePoint(p2.x, p2.y, p1.x, p1.y, sn2.radius); + const mx = Math.round((startPt.x + endPt.x) / 2); + const my = Math.round((startPt.y + endPt.y) / 2); + + const colorCfg = getLatencyColor(lat); + const labelStr = lat + ' ms'; + + const textSvg = createSvg('text', { + 'x': 0, + 'y': 4, + 'text-anchor': 'middle', + 'font-family': 'monospace, sans-serif', + 'font-size': '11', + 'font-weight': '700', + 'fill': colorCfg.text + }, labelStr); + + const isTreeLink = link.isLocalLink || link.isRelayTreeLink; + const initDisplay = (!showAllLinks && !isTreeLink) ? 'none' : ''; + const badgeW = 56; + const badgeH = 18; + + const badgeG = createSvg('g', { + 'transform': 'translate(' + mx + ',' + my + ')', + 'style': 'cursor: pointer; opacity: 0.9; transition: opacity 0.2s; display: ' + initDisplay + ';' + }, [ + createSvg('rect', { + 'x': -badgeW / 2, + 'y': -badgeH / 2, + 'width': badgeW, + 'height': badgeH, + 'rx': '9', + 'fill': '#ffffff', + 'stroke': colorCfg.line, + 'stroke-width': '1.2', + 'filter': 'url(#node-shadow)', + 'class': 'et-badge-rect' + }), + textSvg + ]); + + (function(lk) { + badgeG.addEventListener('mouseenter', function() { + hoveredLinkId = lk; + refreshLinkVisuals(); + }); + badgeG.addEventListener('mouseleave', function() { + if (hoveredLinkId === lk) { + hoveredLinkId = null; + refreshLinkVisuals(); + } + }); + })(linkKey); + + badgeElementsMap[linkKey] = badgeG; + badgesLayer.push(badgeG); + } + } + + // 动态更新任意边几何位置 + function updateEdgeGeometry(linkKey) { + const link = linkMap[linkKey]; + if (!link) return; + const p1 = posMap[String(link.srcId)]; + const p2 = posMap[String(link.dstId)]; + const sn1 = nodeMap[String(link.srcId)]; + const sn2 = nodeMap[String(link.dstId)]; + if (!p1 || !p2 || !sn1 || !sn2) return; + + const startPt = getCircleEdgePoint(p1.x, p1.y, p2.x, p2.y, sn1.radius); + const endPt = getCircleEdgePoint(p2.x, p2.y, p1.x, p1.y, sn2.radius); + + const lineEl = lineElementsMap[linkKey]; + if (lineEl) { + lineEl.setAttribute('x1', startPt.x); + lineEl.setAttribute('y1', startPt.y); + lineEl.setAttribute('x2', endPt.x); + lineEl.setAttribute('y2', endPt.y); + lineEl._origX1 = startPt.x; + lineEl._origY1 = startPt.y; + lineEl._origX2 = endPt.x; + lineEl._origY2 = endPt.y; + } + + const badgeEl = badgeElementsMap[linkKey]; + if (badgeEl) { + const mx = Math.round((startPt.x + endPt.x) / 2); + const my = Math.round((startPt.y + endPt.y) / 2); + badgeEl.setAttribute('transform', 'translate(' + mx + ',' + my + ')'); + } + } + + const allElements = [defsNode].concat(linesLayer, badgesLayer, nodesLayer); + + const svgNode = createSvg('svg', { + 'id': 'easytier-topo-svg', + 'viewBox': '0 0 ' + width + ' ' + height, + 'style': 'width: 100%; height: 100%; display: block; margin: 0 auto; user-select: none; background: transparent; cursor: default;' + }, allElements); + + let currentScale = topologyViewState.scale || 1.0; + let panX = topologyViewState.panX || 0; + let panY = topologyViewState.panY || 0; + let isCanvasDragging = false; + let canvasDragStartX = 0; + let canvasDragStartY = 0; + + function applyViewBox() { + topologyViewState.scale = currentScale; + topologyViewState.panX = panX; + topologyViewState.panY = panY; + const vbW = width / currentScale; + const vbH = height / currentScale; + const vbX = (width - vbW) / 2 - (panX / currentScale); + const vbY = (height - vbH) / 2 - (panY / currentScale); + svgNode.setAttribute('viewBox', Math.round(vbX) + ' ' + Math.round(vbY) + ' ' + Math.round(vbW) + ' ' + Math.round(vbH)); + } + + applyViewBox(); + refreshLinkVisuals(); + + svgNode.addEventListener('wheel', function(ev) { + ev.preventDefault(); + const zoomFactor = ev.deltaY < 0 ? 1.15 : 0.85; + currentScale = Math.min(3.5, Math.max(0.35, currentScale * zoomFactor)); + applyViewBox(); + }, { passive: false }); + + svgNode.addEventListener('mousedown', function(ev) { + if (ev.button !== 0) return; + if (activeDraggingNodeId) return; + isCanvasDragging = true; + canvasDragStartX = ev.clientX - panX; + canvasDragStartY = ev.clientY - panY; + svgNode.style.cursor = 'grabbing'; + }); + + window.addEventListener('mousemove', function(ev) { + // 1. 处理节点自由拖拽 + if (activeDraggingNodeId) { + const dx = (ev.clientX - mouseStartX) / currentScale; + const dy = (ev.clientY - mouseStartY) / currentScale; + mouseStartX = ev.clientX; + mouseStartY = ev.clientY; + + const pos = posMap[activeDraggingNodeId]; + if (pos) { + pos.x = Math.round(pos.x + dx); + pos.y = Math.round(pos.y + dy); + if (!topologyViewState.nodePositions) topologyViewState.nodePositions = {}; + topologyViewState.nodePositions[activeDraggingNodeId] = { x: pos.x, y: pos.y }; + + const cardEl = nodeElementsMap[activeDraggingNodeId]; + if (cardEl) { + cardEl.setAttribute('transform', 'translate(' + pos.x + ',' + pos.y + ')'); + } + + for (let k = 0; k < linkKeys.length; k++) { + const lk = linkMap[linkKeys[k]]; + if (String(lk.srcId) === String(activeDraggingNodeId) || String(lk.dstId) === String(activeDraggingNodeId)) { + updateEdgeGeometry(linkKeys[k]); + } + } + } + return; + } + + // 2. 处理画布平移 + if (isCanvasDragging) { + panX = ev.clientX - canvasDragStartX; + panY = ev.clientY - canvasDragStartY; + applyViewBox(); + } + }); + + window.addEventListener('mouseup', function() { + if (activeDraggingNodeId) { + const cardEl = nodeElementsMap[activeDraggingNodeId]; + if (cardEl) { + cardEl.style.cursor = 'pointer'; + cardEl.firstElementChild.setAttribute('filter', 'url(#node-shadow)'); + } + activeDraggingNodeId = null; + } + if (isCanvasDragging) { + isCanvasDragging = false; + svgNode.style.cursor = 'default'; + } + }); + + const btnStyle = 'display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; font-size: 14px; font-weight: bold; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); cursor: pointer; user-select: none; transition: all 0.15s;'; + + const zoomInBtn = E('button', { + 'type': 'button', + 'class': 'btn cbi-button et-btn-toolbar', + 'style': btnStyle, + 'title': _('Zoom In'), + 'click': function(ev) { + ev.preventDefault(); + currentScale = Math.min(3.5, currentScale * 1.25); + applyViewBox(); + } + }, '+'); + + const zoomOutBtn = E('button', { + 'type': 'button', + 'class': 'btn cbi-button et-btn-toolbar', + 'style': btnStyle, + 'title': _('Zoom Out'), + 'click': function(ev) { + ev.preventDefault(); + currentScale = Math.max(0.35, currentScale * 0.8); + applyViewBox(); + } + }, '−'); + + const resetBtn = E('button', { + 'type': 'button', + 'class': 'btn cbi-button et-btn-toolbar', + 'style': btnStyle + ' width: auto; padding: 0 8px; font-size: 11px;', + 'title': _('Reset View'), + 'click': function(ev) { + ev.preventDefault(); + currentScale = 1.0; + panX = 0; + panY = 0; + applyViewBox(); + } + }, '1:1'); + + const resetLayoutBtn = E('button', { + 'type': 'button', + 'class': 'btn cbi-button et-btn-toolbar', + 'style': btnStyle + ' width: auto; padding: 0 8px; font-size: 13px;', + 'title': _('Reset Layout'), + 'click': function(ev) { + ev.preventDefault(); + topologyViewState.nodePositions = {}; + topologyViewState.scale = 1.0; + topologyViewState.panX = 0; + topologyViewState.panY = 0; + const display = document.getElementById('easytier_topology_display'); + if (display) { + display.replaceChildren(renderTopologySvg(topoData, peerData)); + } + } + }, '⟲'); + + const toggleLinksBtn = E('button', { + 'type': 'button', + 'class': 'btn cbi-button et-btn-toolbar' + (showAllLinks ? ' cbi-button-action' : ''), + 'style': btnStyle + ' width: auto; padding: 0 10px; font-size: 12px;' + (showAllLinks ? ' background: #2563eb !important; color: #ffffff !important; border-color: #1d4ed8 !important;' : ''), + 'title': showAllLinks ? _('Show Local Links Only') : _('Show All Peer Links'), + 'click': function(ev) { + ev.preventDefault(); + topologyViewState.showAllLinks = !topologyViewState.showAllLinks; + const display = document.getElementById('easytier_topology_display'); + if (display) { + display.replaceChildren(renderTopologySvg(topoData, peerData)); + } + } + }, showAllLinks ? _('All Links') : _('Local Only')); + + const toolbar = E('div', { + 'style': 'position: absolute; top: 12px; right: 12px; display: flex; gap: 6px; z-index: 10;' + }, [toggleLinksBtn, zoomInBtn, zoomOutBtn, resetBtn, resetLayoutBtn]); + + svgNode.addEventListener('click', function(ev) { + if (selectedNodeId) { + selectedNodeId = null; + refreshLinkVisuals(); + } + }); + + const graphContainer = E('div', { + 'class': 'et-topo-container' + (isDark ? ' et-dark' : ''), + 'style': 'position: relative; width: 100%; aspect-ratio: 1 / 1; min-height: 540px; max-height: 680px; overflow: hidden; background-size: 24px 24px; border: 1px solid #e2e8f0; border-radius: 6px;' + }, [pathBanner, toolbar, svgNode]); + + const legend = E('div', { + 'class': 'et-topo-legend' + (isDark ? ' et-dark' : ''), + 'style': 'text-align: center; margin-top: 10px; font-size: 12px; line-height: 1.6;' + }, [ + E('span', { 'style': 'display: inline-block; width: 16px; height: 3px; background: #10b981; border-radius: 2px; margin-right: 5px; vertical-align: middle;' }), + _('< 50ms (Optimal)'), + ' ', + E('span', { 'style': 'display: inline-block; width: 16px; height: 3px; background: #3b82f6; border-radius: 2px; margin-left: 14px; margin-right: 5px; vertical-align: middle;' }), + _('50 ~ 150ms (Good)'), + ' ', + E('span', { 'style': 'display: inline-block; width: 16px; height: 3px; background: #f59e0b; border-radius: 2px; margin-left: 14px; margin-right: 5px; vertical-align: middle;' }), + _('> 150ms (Fair)'), + ' ', + E('span', { 'style': 'display: inline-block; padding: 1px 6px; font-size: 11px; font-family: monospace; color: #047857; background: #ecfdf5; border: 1px solid #a7f3d0; border-radius: 3px; margin-left: 14px; vertical-align: middle;' }, 'CIDR'), + E('span', { 'style': 'margin-left: 4px; vertical-align: middle;' }, _('Proxy Network')), + E('span', { 'style': 'margin-left: 16px; color: #94a3b8; font-size: 11px; vertical-align: middle;' }, _('(Hover/click node to trace path, drag to adjust)')) + ]); + + return E('div', { + 'style': 'width: 100%; border: 1px solid #e5e5e5; border-radius: 6px; padding: 15px; box-sizing: border-box; margin-top: 5px;' + }, [graphContainer, legend]); +} + +return view.extend({ + load: function() { + return Promise.all([ + L.resolveDefault(callGetStatus(), {}), + L.resolveDefault(callGetPeers(), {}), + uci.load('easytier'), + L.resolveDefault(callGetSubroutes(), { routes: [] }) + ]); + }, + + render: function(data) { + const status = data[0] || {}; + const peerData = data[1] || {}; + const subroutesData = data[3] || {}; + const subroutes = (subroutesData && Array.isArray(subroutesData.routes)) ? subroutesData.routes : (Array.isArray(subroutesData) ? subroutesData : []); + + const map = new form.Map('easytier', _('EasyTier'), + _('EasyTier is a simple, secure, decentralized mesh VPN for intranet penetration, implemented in Rust.') + ); + + // ==================== Tab 1: Overview & Peers ==================== + const s_overview = map.section(form.NamedSection, '_status', '_status'); + s_overview.anonymous = true; + s_overview.render = function() { + poll.add(function() { + return Promise.all([ + L.resolveDefault(callGetStatus(), {}), + L.resolveDefault(callGetPeers(), {}), + L.resolveDefault(callGetTopology(), {}) + ]).then(function(res) { + const curStatus = res[0] || {}; + const curPeers = res[1] || {}; + const curTopo = res[2] || {}; + + const statusContainer = document.getElementById('easytier_service_status_display'); + if (statusContainer) { + statusContainer.replaceChildren( + renderCoreStatus(curStatus.core), + renderWebStatus(curStatus.web, curStatus.web_installed) + ); + } + + const localNodeContainer = document.getElementById('easytier_local_node_display'); + if (localNodeContainer) { + localNodeContainer.replaceChildren(renderLocalNodeInfo(curPeers)); + } + + const peersContainer = document.getElementById('easytier_peers_display'); + if (peersContainer) { + peersContainer.replaceChildren(renderPeersTable(curPeers)); + } + + const topoContainer = document.getElementById('easytier_topology_display'); + if (topoContainer) { + topoContainer.replaceChildren(renderTopologySvg(curTopo, curPeers)); + } + }); + }, 5); + + return E('div', {}, [ + E('hr', { 'style': 'margin: 5px 0 15px 0; border: 0; border-top: 1px solid #e5e5e5;' }), + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Service Status')), + E('div', { 'id': 'easytier_service_status_display' }, [ + renderCoreStatus(status.core), + renderWebStatus(status.web, status.web_installed) + ]) + ]), + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Local Node Information')), + E('div', { 'id': 'easytier_local_node_display' }, renderLocalNodeInfo(peerData)) + ]), + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;' }, [ + E('h3', { 'style': 'margin: 0;' }, _('Connected Peer Nodes')), + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': function(ev) { + const localContainer = document.getElementById('easytier_local_node_display'); + const peersContainer = document.getElementById('easytier_peers_display'); + if (peersContainer) { + peersContainer.replaceChildren(E('em', {}, _('Collecting data ...'))); + } + return callGetPeers().then(function(res) { + if (localContainer) { + localContainer.replaceChildren(renderLocalNodeInfo(res)); + } + if (peersContainer) { + peersContainer.replaceChildren(renderPeersTable(res)); + } + }).catch(function(err) { + ui.addTimeLimitedNotification(null, [ E('p', {}, _('Failed to load peers: %s').format(err.message || err)) ], 5000, 'error'); + }); + } + }, _('Refresh')) + ]), + E('div', { 'id': 'easytier_peers_display' }, renderPeersTable(peerData)) + ]) + ]); + }; + + // ==================== Tab 2: Settings ==================== + const s = map.section(form.NamedSection, 'settings', 'easytier', _('Settings')); + s.tab('general', _('Core Settings')); + s.tab('advanced', _('Advanced Options')); + if (status.web_installed !== false) { + s.tab('web', _('Web Console')); + } + s.tab('topology', _('Network Topology')); + s.tab('logs', _('Logs')); + + // --- Core Settings --- + let o; + o = s.taboption('general', form.Flag, 'enabled', _('Enable Core Service')); + o.rmempty = false; + + function cleanHiddenErrors(rootNode) { + const node = rootNode || document.querySelector('.cbi-map') || document.body; + if (!node || (rootNode && !rootNode.isConnected)) return; + let changed = false; + node.querySelectorAll('.cbi-input-invalid').forEach(function(el) { + if (el.offsetParent === null || window.getComputedStyle(el).display === 'none' || el.closest('[style*="display: none"]')) { + el.classList.remove('cbi-input-invalid'); + el.removeAttribute('aria-invalid'); + const parent = el.closest('.cbi-value'); + if (parent) parent.classList.remove('cbi-value-error'); + changed = true; + } + }); + if (changed && window.ui && typeof ui.updateTabs === 'function') { + ui.updateTabs(null, node); + } + } + + o = s.taboption('general', form.ListValue, 'etcmd', _('Startup Method')); + o.value('etcmd', _('Command-line')); + o.value('config', _('Configuration File')); + o.value('web', _('Cloud Web Config')); + o.default = 'etcmd'; + o.onchange = function(ev, section_id, value) { + const mapNode = this.map.node || document.querySelector('.cbi-map') || document.body; + cleanHiddenErrors(mapNode); + window.requestAnimationFrame(function() { cleanHiddenErrors(mapNode); }); + window.setTimeout(function() { cleanHiddenErrors(mapNode); }, 50); + window.setTimeout(function() { cleanHiddenErrors(mapNode); }, 150); + }; + + o = s.taboption('general', form.Value, 'network_name', _('Network Name'), + _('The VPN network name to identify this virtual network (Corresponding flag: --network-name).') + ); + o.placeholder = 'easytier'; + o.retain = true; + o.depends('etcmd', 'etcmd'); + + o = s.taboption('general', form.Value, 'network_secret', _('Network Secret'), + _('The secret phrase used to authorize and encrypt traffic (Corresponding flag: --network-secret).') + ); + o.password = true; + o.retain = true; + o.depends('etcmd', 'etcmd'); + + o = s.taboption('general', form.Flag, 'ip_dhcp', _('Enable DHCP IP Allocation'), + _('Automatically determine and assign a virtual IP address (Corresponding flag: -d, --dhcp).') + ); + o.default = '1'; + o.rmempty = false; + o.retain = true; + o.depends('etcmd', 'etcmd'); + + o = s.taboption('general', form.Value, 'ipaddr', _('Interface IPv4 Address'), + _('The static IPv4 address of this node (Corresponding flag: -i, --ipv4). Ignored when DHCP is enabled.') + ); + o.datatype = 'or(ip4addr, cidr4)'; + o.placeholder = '10.144.144.1/24'; + o.retain = true; + o.depends({ 'etcmd': 'etcmd', 'ip_dhcp': '0' }); + + o = s.taboption('general', form.Value, 'ip6addr', _('Interface IPv6 Address'), + _('The static IPv6 address of this node (Corresponding flag: --ipv6).') + ); + o.datatype = 'or(ip6addr, cidr6)'; + o.placeholder = 'fd00:144::1/64'; + o.retain = true; + o.depends('etcmd', 'etcmd'); + + o = s.taboption('general', form.DynamicList, 'peeradd', _('Peer Nodes'), + _('Initial connection peer node URLs (Corresponding flag: -p, --peers).') + ); + o.retain = true; + o.depends('etcmd', 'etcmd'); + + o = s.taboption('general', form.Value, 'external_node', _('Public Discovery Node'), + _('Public discovery node URL (Corresponding flag: -e, --external-node).') + ); + o.placeholder = 'tcp://public.easytier.top:11010'; + o.retain = true; + o.depends('etcmd', 'etcmd'); + + o = s.taboption('general', form.DynamicList, 'proxy_networks', _('Proxy Networks'), + _('Subnet CIDRs to proxy and announce through this node (Corresponding flag: -n, --proxy-networks). Select from the detected local subnets below or enter custom CIDRs.') + ); + if (subroutes && subroutes.length > 0) { + subroutes.forEach(function(subnet) { + o.value(subnet, subnet); + }); + } + o.rmempty = true; + o.retain = true; + o.depends('etcmd', 'etcmd'); + + o = s.taboption('general', form.Flag, 'enable_exit_node', _('Enable Exit Node'), + _('Allow other peers in the network to route their Internet traffic through this node (Corresponding flag: --enable-exit-node).') + ); + o.default = '0'; + o.rmempty = false; + o.retain = true; + o.depends('etcmd', 'etcmd'); + + o = s.taboption('general', form.Flag, 'magic_dns', _('Enable Magic DNS'), + _('Allow resolving peer node domain names (such as hostname.et.net) without memorizing virtual IP addresses (Corresponding flag: --accept-dns). Automatically forwards DNS queries via dnsmasq. Note: If third-party DNS tools (e.g. MosDNS, AdGuard Home, SmartDNS) are active, ensure this domain zone is excluded from hijacking.') + ); + o.default = '0'; + o.rmempty = false; + o.retain = true; + o.depends('etcmd', 'etcmd'); + + o = s.taboption('general', form.Value, 'tld_dns_zone', _('Magic DNS Domain Zone'), + _('Specify the top-level domain zone for Magic DNS (Corresponding flag: --tld-dns-zone). Default is et.net. (FQDN ending with dot).') + ); + o.placeholder = 'et.net.'; + o.default = 'et.net.'; + o.retain = true; + o.depends('magic_dns', '1'); + + // Config File Path under 'config' mode + o = s.taboption('general', form.Value, 'custom_config_file', _('Configuration File Path'), + _('Absolute path to the TOML configuration file (Corresponding flag: -c, --config-file). Ensure the file exists with valid EasyTier configuration.') + ); + o.default = '/etc/easytier/config.toml'; + o.placeholder = '/etc/easytier/config.toml'; + o.retain = true; + o.depends('etcmd', 'config'); + o.renderWidget = function() { + const node = form.Value.prototype.renderWidget.apply(this, arguments); + const input = node.querySelector ? (node.querySelector('input') || node) : node; + if (input && input.style) { + input.style.width = '295px'; + input.style.maxWidth = '100%'; + } + return node; + }; + + // Web Server Options under 'web' mode + o = s.taboption('general', form.Value, 'web_config', _('Web Config Server URL'), + _('Remote web configuration server address or username (Corresponding flag: -w, --web-config). Supports complete URL (e.g. udp://127.0.0.1:22020/admin) or username only (e.g. admin, connecting to official cloud server).') + ); + o.default = 'udp://127.0.0.1:22020/admin'; + o.placeholder = 'udp://127.0.0.1:22020/admin'; + o.retain = true; + o.rmempty = true; + o.depends('etcmd', 'web'); + o.validate = function(section_id, value) { + const etcmdOpt = this.section.children.find(c => c.option === 'etcmd'); + const currentEtcmd = etcmdOpt ? etcmdOpt.formvalue(section_id) : uci.get('easytier', 'settings', 'etcmd'); + if (currentEtcmd === 'web') { + if (!value || value.trim() === '') { + return _('Web Config Server URL cannot be empty.'); + } + const val = value.trim(); + // If URL schema is provided, must start with supported protocols + if (val.indexOf('://') !== -1) { + if (!/^(tcp|udp|ws|wss|wg|quic):\/\/.+/.test(val)) { + return _('Invalid URL format, must start with tcp://, udp://, ws://, wss://, wg://, or quic://'); + } + } else { + // Single username/network name token + if (!/^[a-zA-Z0-9_\-\.\@]+$/.test(val)) { + return _('Invalid username format, only letters, digits, and _ - . @ are allowed.'); + } + } + } + return true; + }; + o.renderWidget = function() { + const node = form.Value.prototype.renderWidget.apply(this, arguments); + const input = node.querySelector ? (node.querySelector('input') || node) : node; + if (input && input.style) { + input.style.width = '295px'; + input.style.maxWidth = '100%'; + } + return node; + }; + + o = s.taboption('general', form.Value, 'machine_id', _('Machine ID (UUID)'), + _('Unique device identifier UUID in the cloud management console (Corresponding flag: --machine-id). Automatically generated on initial install. Changing UUID will automatically clear local cloud cache.') + ); + o.placeholder = 'df33f4ba-c01b-4961-82f3-a424f39d5a9c'; + o.retain = true; + o.depends('etcmd', 'web'); + o.validate = function(section_id, value) { + const etcmdOpt = this.section.children.find(c => c.option === 'etcmd'); + const currentEtcmd = etcmdOpt ? etcmdOpt.formvalue(section_id) : uci.get('easytier', 'settings', 'etcmd'); + if (currentEtcmd === 'web') { + if (!value || value.length === 0) + return true; + if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value.trim())) + return _('Invalid UUID format (e.g. %s)').format('df33f4ba-c01b-4961-82f3-a424f39d5a9c'); + } + return true; + }; + o.renderWidget = function() { + const node = form.Value.prototype.renderWidget.apply(this, arguments); + const input = node.querySelector ? (node.querySelector('input') || node) : node; + if (input && input.style) { + input.style.width = '295px'; + input.style.maxWidth = '100%'; + input.style.fontFamily = 'monospace'; + } + return node; + }; + + // --- Advanced Options --- + o = s.taboption('advanced', form.Value, 'rpc_port', _('RPC Management Port'), + _('Port for local CLI and RPC management portal (Corresponding flag: --rpc-portal).') + ); + o.datatype = 'port'; + o.default = '15888'; + o.placeholder = '15888'; + + o = s.taboption('advanced', form.Value, 'dev_name', _('TUN Device Name'), + _('Virtual TUN network interface name (Corresponding flag: --dev-name).') + ); + o.default = 'easytier0'; + o.placeholder = 'easytier0'; + + o = s.taboption('advanced', form.ListValue, 'encryption_algorithm', _('Encryption Algorithm')); + o.value('aes-gcm', 'AES-GCM'); + o.value('chacha20-poly1305', 'ChaCha20-Poly1305'); + o.value('none', _('None')); + o.default = 'aes-gcm'; + o.rmempty = false; + + o = s.taboption('advanced', form.Flag, 'multi_thread', _('Multi-threaded Mode'), + _('Enable multi-threaded packet processing for higher throughput (Corresponding flag: --multi-thread).') + ); + o.default = '0'; + + o = s.taboption('advanced', form.Flag, 'allow_wan', _('Allow WAN Access'), + _('Automatically open and manage firewall traffic rules on WAN zone to allow incoming connections for external peers.') + ); + o.default = '0'; + o.rmempty = false; + + o = s.taboption('advanced', form.Flag, 'tunnel_snat', _('Enable Tunnel Traffic SNAT'), + _('Use OpenWrt system firewall to manage NAT for EasyTier tunnel and subnet traffic. Disabling this option disables source address masquerading to preserve real source IPs of remote peers and subnets in the local network.') + ); + o.default = '0'; + + o = s.taboption('advanced', form.Value, 'custom_params', _('Custom Command Parameters'), + _('Additional command-line parameters appended to easytier-core.') + ); + o.retain = true; + o.depends('etcmd', 'etcmd'); + + // --- Web Console --- + if (status.web_installed !== false) { + o = s.taboption('web', form.Flag, 'web_enabled', _('Enable Web Console Service'), + _('The web console may consume significant memory, please enable as needed.') + ); + o.rmempty = false; + + o = s.taboption('web', form.Value, 'web_html_port', _('Web Console Port'), + _('HTTP listen port for easytier-web embedded web dashboard.') + ); + o.datatype = 'port'; + o.default = '22020'; + o.placeholder = '22020'; + + o = s.taboption('web', form.Value, 'web_dir', _('Web Data Directory'), + _('Directory to store SQLite database for easytier-web.') + ); + o.default = '/etc/easytier'; + o.placeholder = '/etc/easytier'; + } + + // --- Network Topology --- + const topologyActions = s.taboption('topology', form.DummyValue, '_topology_actions'); + topologyActions.render = function() { + return E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Topology Actions')), + E('div', { 'class': 'cbi-value-field' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': function(ev) { + const display = document.getElementById('easytier_topology_display'); + if (display) { + display.replaceChildren(E('em', {}, _('Collecting data ...'))); + } + return Promise.all([ + callGetTopology(), + callGetPeers() + ]).then(function(results) { + if (display) { + display.replaceChildren(renderTopologySvg(results[0], results[1])); + } + }).catch(function(err) { + if (display) { + display.replaceChildren(E('em', {}, _('No topology data available.'))); + } + ui.addTimeLimitedNotification(null, [ E('p', {}, _('Failed to load topology: %s').format(err.message || err)) ], 5000, 'error'); + }); + } + }, _('Refresh Topology')) + ]) + ]); + }; + + const topologySection = s.taboption('topology', form.DummyValue, '_topology'); + topologySection.render = function() { + window.setTimeout(function() { + const display = document.getElementById('easytier_topology_display'); + if (display) { + Promise.all([ + callGetTopology(), + callGetPeers() + ]).then(function(results) { + display.replaceChildren(renderTopologySvg(results[0], results[1])); + }).catch(function(err) { + display.replaceChildren(E('em', {}, _('No topology data available.'))); + }); + } + }, 100); + + return E('div', { 'id': 'easytier_topology_display', 'class': 'cbi-value' }, + E('em', {}, _('Collecting data ...')) + ); + }; + + // --- Logs --- + const logActions = s.taboption('logs', form.DummyValue, '_log_actions'); + logActions.render = function() { + return E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Log Actions')), + E('div', { 'class': 'cbi-value-field' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': function(ev) { + const display = document.getElementById('easytier_logs_display'); + if (display) { + display.replaceChildren(E('em', {}, _('Collecting logs...'))); + } + return callGetLogs().then(function(res) { + if (display) { + display.replaceChildren(renderLogsView(res)); + } + }).catch(function(err) { + if (display) { + display.replaceChildren(E('em', {}, _('No logs available.'))); + } + ui.addTimeLimitedNotification(null, [ E('p', {}, _('Failed to load logs: %s').format(err.message || err)) ], 5000, 'error'); + }); + } + }, _('Refresh Logs')), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-reset', + 'click': function(ev) { + return callClearLogs().then(function() { + const display = document.getElementById('easytier_logs_display'); + if (display) { + display.replaceChildren(E('em', {}, _('Logs cleared.'))); + } + }); + } + }, _('Clear Logs')) + ]) + ]); + }; + + const logsSection = s.taboption('logs', form.DummyValue, '_logs'); + logsSection.render = function() { + window.setTimeout(function() { + const display = document.getElementById('easytier_logs_display'); + if (display) { + callGetLogs().then(function(res) { + display.replaceChildren(renderLogsView(res)); + }).catch(function(err) { + display.replaceChildren(E('em', {}, _('No logs available.'))); + }); + } + }, 100); + + return E('div', { 'id': 'easytier_logs_display', 'class': 'cbi-value' }, + E('em', {}, _('Collecting logs...')) + ); + }; + + return map.render().then(function(node) { + const cleanFn = function() { cleanHiddenErrors(node); }; + document.addEventListener('dependency-update', cleanFn); + document.addEventListener('cbi-tab-active', cleanFn); + return node; + }); + }, + + handleSaveApply: function(ev, mode) { + return this.super('handleSaveApply', [ev, mode]).then(function() { + return callServiceAction('restart'); + }); + } +}); diff --git a/luci-app-easytier-next/po/templates/easytier.pot b/luci-app-easytier-next/po/templates/easytier.pot new file mode 100644 index 000000000..127ecfc85 --- /dev/null +++ b/luci-app-easytier-next/po/templates/easytier.pot @@ -0,0 +1,383 @@ +msgid "(Drag to pan, scroll to zoom)" +msgstr "" + +msgid "50 ~ 150ms (Good)" +msgstr "" + +msgid "< 50ms (Optimal)" +msgstr "" + +msgid "> 150ms (Fair)" +msgstr "" + +msgid "Absolute path to the TOML configuration file (Corresponding flag: -c, --config-file). Ensure the file exists with valid EasyTier configuration." +msgstr "" + +msgid "Action" +msgstr "" + +msgid "Additional command-line parameters appended to easytier-core." +msgstr "" + +msgid "Advanced Options" +msgstr "" + +msgid "Allow WAN Access" +msgstr "" + +msgid "Allow other peers in the network to route their Internet traffic through this node (Corresponding flag: --enable-exit-node)." +msgstr "" + +msgid "Allow resolving peer node domain names (such as hostname.et.net) without memorizing virtual IP addresses (Corresponding flag: --accept-dns). Automatically forwards DNS queries via dnsmasq. Note: If third-party DNS tools (e.g. MosDNS, AdGuard Home, SmartDNS) are active, ensure this domain zone is excluded from hijacking." +msgstr "" + +msgid "Automatically determine and assign a virtual IP address (Corresponding flag: -d, --dhcp)." +msgstr "" + +msgid "Automatically open and manage firewall traffic rules on WAN zone to allow incoming connections for external peers." +msgstr "" + +msgid "Clear Logs" +msgstr "" + +msgid "Client Version" +msgstr "" + +msgid "Cloud Web Config" +msgstr "" + +msgid "Collecting data ..." +msgstr "" + +msgid "Collecting logs..." +msgstr "" + +msgid "Command-line" +msgstr "" + +msgid "Configuration File" +msgstr "" + +msgid "Configuration File Path" +msgstr "" + +msgid "Connected Peer Nodes" +msgstr "" + +msgid "Core Service Status" +msgstr "" + +msgid "Core Settings" +msgstr "" + +msgid "Cost" +msgstr "" + +msgid "Custom Command Parameters" +msgstr "" + +msgid "Directory to store SQLite database for easytier-web." +msgstr "" + +msgid "EasyTier" +msgstr "" + +msgid "EasyTier is a simple, secure, decentralized mesh VPN for intranet penetration, implemented in Rust." +msgstr "" + +msgid "Enable Core Service" +msgstr "" + +msgid "Enable DHCP IP Allocation" +msgstr "" + +msgid "Enable Exit Node" +msgstr "" + +msgid "Enable Magic DNS" +msgstr "" + +msgid "Enable Tunnel Traffic SNAT" +msgstr "" + +msgid "Enable Web Console Service" +msgstr "" + +msgid "Enable multi-threaded packet processing for higher throughput (Corresponding flag: --multi-thread)." +msgstr "" + +msgid "Encryption Algorithm" +msgstr "" + +msgid "Failed to load logs: %s" +msgstr "" + +msgid "Failed to load peers: %s" +msgstr "" + +msgid "Failed to load topology: %s" +msgstr "" + +msgid "HTTP listen port for easytier-web embedded web dashboard." +msgstr "" + +msgid "Hostname" +msgstr "" + +msgid "Initial connection peer node URLs (Corresponding flag: -p, --peers)." +msgstr "" + +msgid "Interface IPv4 Address" +msgstr "" + +msgid "Interface IPv6 Address" +msgstr "" + +msgid "Invalid URL format, must start with tcp://, udp://, ws://, wss://, wg://, or quic://" +msgstr "" + +msgid "Invalid UUID format (e.g. %s)" +msgstr "" + +msgid "Invalid username format, only letters, digits, and _ - . @ are allowed." +msgstr "" + +msgid "Latency" +msgstr "" + +msgid "Local Node Information" +msgstr "" + +msgid "Local service is not running." +msgstr "" + +msgid "Log Actions" +msgstr "" + +msgid "Logs" +msgstr "" + +msgid "Logs cleared." +msgstr "" + +msgid "Loss Rate" +msgstr "" + +msgid "Machine ID (UUID)" +msgstr "" + +msgid "Magic DNS Domain Zone" +msgstr "" + +msgid "Multi-threaded Mode" +msgstr "" + +msgid "NAT Type" +msgstr "" + +msgid "Network Name" +msgstr "" + +msgid "Network Secret" +msgstr "" + +msgid "Network Topology" +msgstr "" + +msgid "No connected remote peers found." +msgstr "" + +msgid "No logs available." +msgstr "" + +msgid "No topology data available." +msgstr "" + +msgid "None" +msgstr "" + +msgid "Not Installed" +msgstr "" + +msgid "Open Console" +msgstr "" + +msgid "Peer Nodes" +msgstr "" + +msgid "Port for local CLI and RPC management portal (Corresponding flag: --rpc-portal)." +msgstr "" + +msgid "Proxy Network" +msgstr "" + +msgid "Proxy Networks" +msgstr "" + +msgid "Public Discovery Node" +msgstr "" + +msgid "Public discovery node URL (Corresponding flag: -e, --external-node)." +msgstr "" + +msgid "RPC Management Port" +msgstr "" + +msgid "RX" +msgstr "" + +msgid "Refresh" +msgstr "" + +msgid "Refresh Logs" +msgstr "" + +msgid "Refresh Topology" +msgstr "" + +msgid "Remote web configuration server address or username (Corresponding flag: -w, --web-config). Supports complete URL (e.g. udp://127.0.0.1:22020/admin) or username only (e.g. admin, connecting to official cloud server)." +msgstr "" + +msgid "Reset View" +msgstr "" + +msgid "Restart" +msgstr "" + +msgid "Restarting service..." +msgstr "" + +msgid "Running" +msgstr "" + +msgid "Running (Unmanaged)" +msgstr "" + +msgid "Service Status" +msgstr "" + +msgid "Settings" +msgstr "" + +msgid "Specify the top-level domain zone for Magic DNS (Corresponding flag: --tld-dns-zone). Default is et.net. (FQDN ending with dot)." +msgstr "" + +msgid "Startup Method" +msgstr "" + +msgid "Stopped" +msgstr "" + +msgid "Subnet CIDRs to proxy and announce through this node (Corresponding flag: -n, --proxy-networks). Select from the detected local subnets below or enter custom CIDRs." +msgstr "" + +msgid "TUN Device Name" +msgstr "" + +msgid "TX" +msgstr "" + +msgid "The VPN network name to identify this virtual network (Corresponding flag: --network-name)." +msgstr "" + +msgid "The secret phrase used to authorize and encrypt traffic (Corresponding flag: --network-secret)." +msgstr "" + +msgid "The static IPv4 address of this node (Corresponding flag: -i, --ipv4). Ignored when DHCP is enabled." +msgstr "" + +msgid "The static IPv6 address of this node (Corresponding flag: --ipv6)." +msgstr "" + +msgid "The web console may consume significant memory, please enable as needed." +msgstr "" + +msgid "Topology Actions" +msgstr "" + +msgid "Tunnel" +msgstr "" + +msgid "Unique device identifier UUID in the cloud management console (Corresponding flag: --machine-id). Automatically generated on initial install. Changing UUID will automatically clear local cloud cache." +msgstr "" + +msgid "Use OpenWrt system firewall to manage NAT for EasyTier tunnel and subnet traffic. Disabling this option disables source address masquerading to preserve real source IPs of remote peers and subnets in the local network." +msgstr "" + +msgid "Version" +msgstr "" + +msgid "Virtual IP" +msgstr "" + +msgid "Virtual Interface" +msgstr "" + +msgid "Virtual TUN network interface name (Corresponding flag: --dev-name)." +msgstr "" + +msgid "Web Config Server URL" +msgstr "" + +msgid "Web Config Server URL cannot be empty." +msgstr "" + +msgid "Web Console" +msgstr "" + +msgid "Web Console Port" +msgstr "" + +msgid "Web Console Status" +msgstr "" + +msgid "Web Data Directory" +msgstr "" + +msgid "Zoom In" +msgstr "" + +msgid "Zoom Out" +msgstr "" + +msgid "Reset Layout" +msgstr "" + +msgid "(Hover to highlight link, drag nodes/canvas to adjust)" +msgstr "" + +msgid "All Links" +msgstr "" + +msgid "Local Only" +msgstr "" + +msgid "Show All Peer Links" +msgstr "" + +msgid "Show Local Links Only" +msgstr "" + +msgid "Next Hop: %s (%s)" +msgstr "" + +msgid "Next Hop: %s" +msgstr "" + +msgid "Latency / Loss" +msgstr "" + +msgid "RX / TX" +msgstr "" + +msgid "(Hover/click node to trace path, drag to adjust)" +msgstr "" + +msgid "Clear selection" +msgstr "" + +msgid "Local Node: %s" +msgstr "" + +msgid "Path" +msgstr "" diff --git a/luci-app-easytier-next/po/zh-cn b/luci-app-easytier-next/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-easytier-next/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-easytier-next/po/zh_Hans/easytier.po b/luci-app-easytier-next/po/zh_Hans/easytier.po new file mode 100644 index 000000000..05843ade8 --- /dev/null +++ b/luci-app-easytier-next/po/zh_Hans/easytier.po @@ -0,0 +1,383 @@ +msgid "(Drag to pan, scroll to zoom)" +msgstr "(支持拖拽画布平移,滚轮缩放视图)" + +msgid "50 ~ 150ms (Good)" +msgstr "50 ~ 150ms (良)" + +msgid "< 50ms (Optimal)" +msgstr "< 50ms (优)" + +msgid "> 150ms (Fair)" +msgstr "> 150ms (一般)" + +msgid "Absolute path to the TOML configuration file (Corresponding flag: -c, --config-file). Ensure the file exists with valid EasyTier configuration." +msgstr "TOML 格式配置文件的绝对路径 (对应参数:-c, --config-file)。请确保该文件已存在并包含有效的 EasyTier 配置。" + +msgid "Action" +msgstr "操作" + +msgid "Additional command-line parameters appended to easytier-core." +msgstr "附加到 easytier-core 进程末尾的自定义参数。" + +msgid "Advanced Options" +msgstr "高级设置" + +msgid "Allow WAN Access" +msgstr "允许从 WAN 访问" + +msgid "Allow other peers in the network to route their Internet traffic through this node (Corresponding flag: --enable-exit-node)." +msgstr "允许网络中的其他对等节点通过本节点路由外网流量 (对应参数:--enable-exit-node)。" + +msgid "Allow resolving peer node domain names (such as hostname.et.net) without memorizing virtual IP addresses (Corresponding flag: --accept-dns). Automatically forwards DNS queries via dnsmasq. Note: If third-party DNS tools (e.g. MosDNS, AdGuard Home, SmartDNS) are active, ensure this domain zone is excluded from hijacking." +msgstr "允许直接通过主机名解析节点 (对应参数:--accept-dns)。自动通过 dnsmasq 转发解析请求至内置 DNS 服务器 (100.100.100.101)。注意:若启用了第三方 DNS 代理工具(如 MosDNS、AdGuard Home、SmartDNS 或分流插件),请确保将该域名区域加入直连/放行名单以防止被劫持导致解析失败。" + +msgid "Automatically determine and assign a virtual IP address (Corresponding flag: -d, --dhcp)." +msgstr "自动协商并分配虚拟网卡 IP 地址 (对应参数:-d, --dhcp)。" + +msgid "Automatically open and manage firewall traffic rules on WAN zone to allow incoming connections for external peers." +msgstr "自动在 WAN 防火墙区域创建并管理通信规则,放行外部节点连接本节点的入站流量。" + +msgid "Clear Logs" +msgstr "清空日志" + +msgid "Client Version" +msgstr "客户端版本" + +msgid "Cloud Web Config" +msgstr "云端集中配置" + +msgid "Collecting data ..." +msgstr "正在收集数据..." + +msgid "Collecting logs..." +msgstr "正在收集日志..." + +msgid "Command-line" +msgstr "命令行参数" + +msgid "Configuration File" +msgstr "配置文件" + +msgid "Configuration File Path" +msgstr "配置文件路径" + +msgid "Connected Peer Nodes" +msgstr "已连接对等节点" + +msgid "Core Service Status" +msgstr "核心服务状态" + +msgid "Core Settings" +msgstr "基本设置" + +msgid "Cost" +msgstr "开销" + +msgid "Custom Command Parameters" +msgstr "自定义启动参数" + +msgid "Directory to store SQLite database for easytier-web." +msgstr "用于存放 easytier-web SQLite 数据库文件的目录。" + +msgid "EasyTier" +msgstr "EasyTier" + +msgid "EasyTier is a simple, secure, decentralized mesh VPN for intranet penetration, implemented in Rust." +msgstr "EasyTier 是一款基于 Rust 实现的简单、安全、去中心化的网状异地组网工具。" + +msgid "Enable Core Service" +msgstr "启用核心服务" + +msgid "Enable DHCP IP Allocation" +msgstr "启用 DHCP 分配" + +msgid "Enable Exit Node" +msgstr "作为出口节点" + +msgid "Enable Magic DNS" +msgstr "启用魔法 DNS" + +msgid "Enable Tunnel Traffic SNAT" +msgstr "开启隧道流量 SNAT" + +msgid "Enable Web Console Service" +msgstr "启用 Web 控制台服务" + +msgid "Enable multi-threaded packet processing for higher throughput (Corresponding flag: --multi-thread)." +msgstr "启用多线程数据包处理以提升高吞吐性能 (对应参数:--multi-thread)。" + +msgid "Encryption Algorithm" +msgstr "加密算法" + +msgid "Failed to load logs: %s" +msgstr "加载日志失败:%s" + +msgid "Failed to load peers: %s" +msgstr "加载对等节点失败:%s" + +msgid "Failed to load topology: %s" +msgstr "加载拓扑失败:%s" + +msgid "HTTP listen port for easytier-web embedded web dashboard." +msgstr "easytier-web 内置 Web 控制台的 HTTP 监听端口。" + +msgid "Hostname" +msgstr "主机名" + +msgid "Initial connection peer node URLs (Corresponding flag: -p, --peers)." +msgstr "初始连接的对等节点地址 (对应参数:-p, --peers)。" + +msgid "Interface IPv4 Address" +msgstr "虚拟 IPv4 地址" + +msgid "Interface IPv6 Address" +msgstr "虚拟 IPv6 地址" + +msgid "Invalid URL format, must start with tcp://, udp://, ws://, wss://, wg://, or quic://" +msgstr "URL 格式无效,必须以 tcp://、udp://、ws://、wss://、wg:// 或 quic:// 开头" + +msgid "Invalid UUID format (e.g. %s)" +msgstr "UUID 格式无效(示例:%s)" + +msgid "Invalid username format, only letters, digits, and _ - . @ are allowed." +msgstr "用户名格式无效,仅支持字母、数字及 _ - . @" + +msgid "Latency" +msgstr "延迟" + +msgid "Local Node Information" +msgstr "本地节点信息" + +msgid "Local service is not running." +msgstr "本地服务未运行。" + +msgid "Log Actions" +msgstr "日志操作" + +msgid "Logs" +msgstr "运行日志" + +msgid "Logs cleared." +msgstr "日志已清空。" + +msgid "Loss Rate" +msgstr "丢包率" + +msgid "Machine ID (UUID)" +msgstr "设备唯一标识 (UUID)" + +msgid "Magic DNS Domain Zone" +msgstr "魔法 DNS 域名区域" + +msgid "Multi-threaded Mode" +msgstr "多线程模式" + +msgid "NAT Type" +msgstr "NAT 类型" + +msgid "Network Name" +msgstr "网络名称" + +msgid "Network Secret" +msgstr "网络密钥" + +msgid "Network Topology" +msgstr "网络拓扑" + +msgid "No connected remote peers found." +msgstr "未找到已连接的远程对等节点。" + +msgid "No logs available." +msgstr "暂无可用的日志记录。" + +msgid "No topology data available." +msgstr "暂无拓扑数据。" + +msgid "None" +msgstr "无" + +msgid "Not Installed" +msgstr "未安装" + +msgid "Open Console" +msgstr "打开控制台" + +msgid "Peer Nodes" +msgstr "对等节点" + +msgid "Port for local CLI and RPC management portal (Corresponding flag: --rpc-portal)." +msgstr "本地 CLI 与 RPC 交互端口 (对应参数:--rpc-portal)。" + +msgid "Proxy Network" +msgstr "子网代理" + +msgid "Proxy Networks" +msgstr "子网代理" + +msgid "Public Discovery Node" +msgstr "公共发现节点" + +msgid "Public discovery node URL (Corresponding flag: -e, --external-node)." +msgstr "公共发现节点 URL (对应参数:-e, --external-node)。" + +msgid "RPC Management Port" +msgstr "RPC 管理端口" + +msgid "RX" +msgstr "RX" + +msgid "Refresh" +msgstr "刷新" + +msgid "Refresh Logs" +msgstr "刷新日志" + +msgid "Refresh Topology" +msgstr "刷新拓扑" + +msgid "Remote web configuration server address or username (Corresponding flag: -w, --web-config). Supports complete URL (e.g. udp://127.0.0.1:22020/admin) or username only (e.g. admin, connecting to official cloud server)." +msgstr "远程 Web 配置服务器地址或用户名 (对应参数:-w, --web-config)。支持完整 URL(如 udp://127.0.0.1:22020/admin)或仅填用户名/网络名(如 admin,自动连接官方云端服务器)。" + +msgid "Reset View" +msgstr "重置视图" + +msgid "Restart" +msgstr "重启" + +msgid "Restarting service..." +msgstr "正在重启服务..." + +msgid "Running" +msgstr "运行中" + +msgid "Running (Unmanaged)" +msgstr "运行中 (非托管)" + +msgid "Service Status" +msgstr "服务运行状态" + +msgid "Settings" +msgstr "配置" + +msgid "Specify the top-level domain zone for Magic DNS (Corresponding flag: --tld-dns-zone). Default is et.net. (FQDN ending with dot)." +msgstr "指定魔法 DNS 的顶级域名区域 (对应参数:--tld-dns-zone)。默认值为 et.net.(以点结尾的 FQDN 根域)。" + +msgid "Startup Method" +msgstr "启动方式" + +msgid "Stopped" +msgstr "已停止" + +msgid "Subnet CIDRs to proxy and announce through this node (Corresponding flag: -n, --proxy-networks). Select from the detected local subnets below or enter custom CIDRs." +msgstr "通过此节点代理并通告的子网网段 (对应参数:-n, --proxy-networks)。可直接下拉选择探测到的本地网段或手动输入。" + +msgid "TUN Device Name" +msgstr "虚拟网卡名称" + +msgid "TX" +msgstr "TX" + +msgid "The VPN network name to identify this virtual network (Corresponding flag: --network-name)." +msgstr "VPN 虚拟网络名称 (对应参数:--network-name)。" + +msgid "The secret phrase used to authorize and encrypt traffic (Corresponding flag: --network-secret)." +msgstr "用于节点认证与加密的密钥短语 (对应参数:--network-secret)。" + +msgid "The static IPv4 address of this node (Corresponding flag: -i, --ipv4). Ignored when DHCP is enabled." +msgstr "本节点的静态 IPv4 地址 (对应参数:-i, --ipv4)。启用 DHCP 时自动忽略。" + +msgid "The static IPv6 address of this node (Corresponding flag: --ipv6)." +msgstr "本节点的静态 IPv6 地址 (对应参数:--ipv6)。" + +msgid "The web console may consume significant memory, please enable as needed." +msgstr "Web 控制台可能会消耗较多内存,请按需启用。" + +msgid "Topology Actions" +msgstr "拓扑操作" + +msgid "Tunnel" +msgstr "连接方式" + +msgid "Unique device identifier UUID in the cloud management console (Corresponding flag: --machine-id). Automatically generated on initial install. Changing UUID will automatically clear local cloud cache." +msgstr "用于在云端控制台中标识当前设备的唯一 UUID 标识符 (对应参数:--machine-id)。初始化安装时自动生成,修改更换 UUID 时将自动重置并清理云端缓存。" + +msgid "Use OpenWrt system firewall to manage NAT for EasyTier tunnel and subnet traffic. Disabling this option disables source address masquerading to preserve real source IPs of remote peers and subnets in the local network." +msgstr "使用 OpenWrt 系统防火墙接管 EasyTier 虚拟网卡与子网转发流量的 NAT。关闭此选项将禁用源地址伪装,保留远程对等节点及子网在本地局域网中的真实源 IP。" + +msgid "Version" +msgstr "版本" + +msgid "Virtual IP" +msgstr "虚拟 IP" + +msgid "Virtual Interface" +msgstr "虚拟网卡" + +msgid "Virtual TUN network interface name (Corresponding flag: --dev-name)." +msgstr "创建的虚拟 TUN 网卡接口名称 (对应参数:--dev-name)。" + +msgid "Web Config Server URL" +msgstr "云端配置服务器地址" + +msgid "Web Config Server URL cannot be empty." +msgstr "云端配置服务器地址不能为空。" + +msgid "Web Console" +msgstr "Web 控制台" + +msgid "Web Console Port" +msgstr "控制台监听端口" + +msgid "Web Console Status" +msgstr "控制台服务状态" + +msgid "Web Data Directory" +msgstr "控制台数据目录" + +msgid "Zoom In" +msgstr "放大" + +msgid "Zoom Out" +msgstr "缩小" + +msgid "Reset Layout" +msgstr "重置布局" + +msgid "(Hover to highlight link, drag nodes/canvas to adjust)" +msgstr "(悬停高亮链路,拖拽节点/画布以调整)" + +msgid "All Links" +msgstr "全部链路" + +msgid "Local Only" +msgstr "仅本端" + +msgid "Show All Peer Links" +msgstr "显示所有对端连接线" + +msgid "Show Local Links Only" +msgstr "仅聚焦本端连接线" + +msgid "Next Hop: %s (%s)" +msgstr "下一跳中继: %s (%s)" + +msgid "Next Hop: %s" +msgstr "下一跳中继: %s" + +msgid "Latency / Loss" +msgstr "延迟 / 丢包" + +msgid "RX / TX" +msgstr "接收 / 发送" + +msgid "(Hover/click node to trace path, drag to adjust)" +msgstr "(悬停/点击节点追踪路径,支持拖拽调整)" + +msgid "Clear selection" +msgstr "清除选中" + +msgid "Local Node: %s" +msgstr "本地节点: %s" + +msgid "Path" +msgstr "路径" diff --git a/luci-app-easytier-next/po/zh_Hant/easytier.po b/luci-app-easytier-next/po/zh_Hant/easytier.po new file mode 100644 index 000000000..65c3b576e --- /dev/null +++ b/luci-app-easytier-next/po/zh_Hant/easytier.po @@ -0,0 +1,383 @@ +msgid "(Drag to pan, scroll to zoom)" +msgstr "(支援拖拽畫布平移,滾輪縮放視圖)" + +msgid "50 ~ 150ms (Good)" +msgstr "50 ~ 150ms (良)" + +msgid "< 50ms (Optimal)" +msgstr "< 50ms (優)" + +msgid "> 150ms (Fair)" +msgstr "> 150ms (一般)" + +msgid "Absolute path to the TOML configuration file (Corresponding flag: -c, --config-file). Ensure the file exists with valid EasyTier configuration." +msgstr "TOML 格式配置檔案的絕對路徑 (對應參數:-c, --config-file)。請確保該檔案已存在並包含有效的 EasyTier 配置。" + +msgid "Action" +msgstr "操作" + +msgid "Additional command-line parameters appended to easytier-core." +msgstr "附加到 easytier-core 進程末尾的自定義參數。" + +msgid "Advanced Options" +msgstr "進階設定" + +msgid "Allow WAN Access" +msgstr "允許從 WAN 訪問" + +msgid "Allow other peers in the network to route their Internet traffic through this node (Corresponding flag: --enable-exit-node)." +msgstr "允許網路中的其他對等節點透過本節點路由外網流量 (對應參數:--enable-exit-node)。" + +msgid "Allow resolving peer node domain names (such as hostname.et.net) without memorizing virtual IP addresses (Corresponding flag: --accept-dns). Automatically forwards DNS queries via dnsmasq. Note: If third-party DNS tools (e.g. MosDNS, AdGuard Home, SmartDNS) are active, ensure this domain zone is excluded from hijacking." +msgstr "允許直接透過主機名解析節點 (對應參數:--accept-dns)。自動透過 dnsmasq 轉發解析請求至內置 DNS 伺服器 (100.100.100.101)。注意:若啟用了第三方 DNS 代理工具(如 MosDNS、AdGuard Home、SmartDNS 或分流插件),請確保將該網域區域加入直連/放行名單以防止被劫持導致解析失敗。" + +msgid "Automatically determine and assign a virtual IP address (Corresponding flag: -d, --dhcp)." +msgstr "自動協商並分配虛擬網卡 IP 位址 (對應參數:-d, --dhcp)。" + +msgid "Automatically open and manage firewall traffic rules on WAN zone to allow incoming connections for external peers." +msgstr "自動在 WAN 防火牆區域創建並管理通信規則,放行外部節點連接本節點的入站流量。" + +msgid "Clear Logs" +msgstr "清空日誌" + +msgid "Client Version" +msgstr "客戶端版本" + +msgid "Cloud Web Config" +msgstr "雲端集中配置" + +msgid "Collecting data ..." +msgstr "正在收集數據..." + +msgid "Collecting logs..." +msgstr "正在收集日誌..." + +msgid "Command-line" +msgstr "命令行參數" + +msgid "Configuration File" +msgstr "配置檔案" + +msgid "Configuration File Path" +msgstr "配置檔案路徑" + +msgid "Connected Peer Nodes" +msgstr "已連線對等節點" + +msgid "Core Service Status" +msgstr "核心服務狀態" + +msgid "Core Settings" +msgstr "基本設定" + +msgid "Cost" +msgstr "開銷" + +msgid "Custom Command Parameters" +msgstr "自定義啟動參數" + +msgid "Directory to store SQLite database for easytier-web." +msgstr "用於存放 easytier-web SQLite 資料庫檔案的目錄。" + +msgid "EasyTier" +msgstr "EasyTier" + +msgid "EasyTier is a simple, secure, decentralized mesh VPN for intranet penetration, implemented in Rust." +msgstr "EasyTier 是一款基於 Rust 實現的簡單、安全、去中心化的網狀異地組網工具。" + +msgid "Enable Core Service" +msgstr "啟用核心服務" + +msgid "Enable DHCP IP Allocation" +msgstr "啟用 DHCP 分配" + +msgid "Enable Exit Node" +msgstr "作為出口節點" + +msgid "Enable Magic DNS" +msgstr "啟用魔法 DNS" + +msgid "Enable Tunnel Traffic SNAT" +msgstr "開啟隧道流量 SNAT" + +msgid "Enable Web Console Service" +msgstr "啟用 Web 控制台服務" + +msgid "Enable multi-threaded packet processing for higher throughput (Corresponding flag: --multi-thread)." +msgstr "啟用多線程數據包處理以提升高吞吐性能 (對應參數:--multi-thread)。" + +msgid "Encryption Algorithm" +msgstr "加密演算法" + +msgid "Failed to load logs: %s" +msgstr "載入日誌失敗:%s" + +msgid "Failed to load peers: %s" +msgstr "載入對等節點失敗:%s" + +msgid "Failed to load topology: %s" +msgstr "載入拓撲失敗:%s" + +msgid "HTTP listen port for easytier-web embedded web dashboard." +msgstr "easytier-web 內建 Web 控制台的 HTTP 監聽連接埠。" + +msgid "Hostname" +msgstr "主機名稱" + +msgid "Initial connection peer node URLs (Corresponding flag: -p, --peers)." +msgstr "初始連線的對等節點位址 (對應參數:-p, --peers)。" + +msgid "Interface IPv4 Address" +msgstr "虛擬 IPv4 位址" + +msgid "Interface IPv6 Address" +msgstr "虛擬 IPv6 位址" + +msgid "Invalid URL format, must start with tcp://, udp://, ws://, wss://, wg://, or quic://" +msgstr "URL 格式無效,必須以 tcp://、udp://、ws://、wss://、wg:// 或 quic:// 開頭" + +msgid "Invalid UUID format (e.g. %s)" +msgstr "UUID 格式無效(示例:%s)" + +msgid "Invalid username format, only letters, digits, and _ - . @ are allowed." +msgstr "使用者名稱格式無效,僅支援字母、數字及 _ - . @" + +msgid "Latency" +msgstr "延遲" + +msgid "Local Node Information" +msgstr "本地節點資訊" + +msgid "Local service is not running." +msgstr "本地服務未運行。" + +msgid "Log Actions" +msgstr "日誌操作" + +msgid "Logs" +msgstr "運行日誌" + +msgid "Logs cleared." +msgstr "日誌已清空。" + +msgid "Loss Rate" +msgstr "丟包率" + +msgid "Machine ID (UUID)" +msgstr "設備唯一標識 (UUID)" + +msgid "Magic DNS Domain Zone" +msgstr "魔法 DNS 網域區域" + +msgid "Multi-threaded Mode" +msgstr "多線程模式" + +msgid "NAT Type" +msgstr "NAT 類型" + +msgid "Network Name" +msgstr "網路名稱" + +msgid "Network Secret" +msgstr "網路密鑰" + +msgid "Network Topology" +msgstr "網路拓撲" + +msgid "No connected remote peers found." +msgstr "未找到已連線的遠端對等節點。" + +msgid "No logs available." +msgstr "暫無可用的日誌記錄。" + +msgid "No topology data available." +msgstr "暫無拓撲數據。" + +msgid "None" +msgstr "無" + +msgid "Not Installed" +msgstr "未安裝" + +msgid "Open Console" +msgstr "開啟控制台" + +msgid "Peer Nodes" +msgstr "對等節點" + +msgid "Port for local CLI and RPC management portal (Corresponding flag: --rpc-portal)." +msgstr "本地 CLI 與 RPC 互動連接埠 (對應參數:--rpc-portal)。" + +msgid "Proxy Network" +msgstr "子網代理" + +msgid "Proxy Networks" +msgstr "子網代理" + +msgid "Public Discovery Node" +msgstr "公共發現節點" + +msgid "Public discovery node URL (Corresponding flag: -e, --external-node)." +msgstr "公共發現節點 URL (對應參數:-e, --external-node)。" + +msgid "RPC Management Port" +msgstr "RPC 管理連接埠" + +msgid "RX" +msgstr "RX" + +msgid "Refresh" +msgstr "重新整理" + +msgid "Refresh Logs" +msgstr "重新整理日誌" + +msgid "Refresh Topology" +msgstr "重新整理拓撲" + +msgid "Remote web configuration server address or username (Corresponding flag: -w, --web-config). Supports complete URL (e.g. udp://127.0.0.1:22020/admin) or username only (e.g. admin, connecting to official cloud server)." +msgstr "遠程 Web 配置伺服器位址或使用者名稱 (對應參數:-w, --web-config)。支援完整 URL(如 udp://127.0.0.1:22020/admin)或僅填使用者名稱/網路名稱(如 admin,自動連線官方雲端伺服器)。" + +msgid "Reset View" +msgstr "重置視圖" + +msgid "Restart" +msgstr "重啟" + +msgid "Restarting service..." +msgstr "正在重啟服務..." + +msgid "Running" +msgstr "運行中" + +msgid "Running (Unmanaged)" +msgstr "運行中 (非託管)" + +msgid "Service Status" +msgstr "服務運行狀態" + +msgid "Settings" +msgstr "配置" + +msgid "Specify the top-level domain zone for Magic DNS (Corresponding flag: --tld-dns-zone). Default is et.net. (FQDN ending with dot)." +msgstr "指定魔法 DNS 的頂級網域區域 (對應參數:--tld-dns-zone)。預設值為 et.net.(以點結尾的 FQDN 根域)。" + +msgid "Startup Method" +msgstr "啟動方式" + +msgid "Stopped" +msgstr "已停止" + +msgid "Subnet CIDRs to proxy and announce through this node (Corresponding flag: -n, --proxy-networks). Select from the detected local subnets below or enter custom CIDRs." +msgstr "透過此節點代理並通告的子網網段 (對應參數:-n, --proxy-networks)。可直接下拉選擇探測到的本地網段或手動輸入。" + +msgid "TUN Device Name" +msgstr "虛擬網卡名稱" + +msgid "TX" +msgstr "TX" + +msgid "The VPN network name to identify this virtual network (Corresponding flag: --network-name)." +msgstr "VPN 虛擬網路名稱 (對應參數:--network-name)。" + +msgid "The secret phrase used to authorize and encrypt traffic (Corresponding flag: --network-secret)." +msgstr "用於節點認證與加密的密鑰短語 (對應參數:--network-secret)。" + +msgid "The static IPv4 address of this node (Corresponding flag: -i, --ipv4). Ignored when DHCP is enabled." +msgstr "本節點的靜態 IPv4 位址 (對應參數:-i, --ipv4)。啟用 DHCP 時自動忽略。" + +msgid "The static IPv6 address of this node (Corresponding flag: --ipv6)." +msgstr "本節點的靜態 IPv6 位址 (對應參數:--ipv6)。" + +msgid "The web console may consume significant memory, please enable as needed." +msgstr "Web 控制台可能會消耗較多記憶體,請按需啟用。" + +msgid "Topology Actions" +msgstr "拓撲操作" + +msgid "Tunnel" +msgstr "連線方式" + +msgid "Unique device identifier UUID in the cloud management console (Corresponding flag: --machine-id). Automatically generated on initial install. Changing UUID will automatically clear local cloud cache." +msgstr "用於在雲端控制台中識別當前設備的唯一 UUID 識別碼 (對應參數:--machine-id)。初始化安裝時自動生成,修改更換 UUID 時將自動重置並清理雲端快取。" + +msgid "Use OpenWrt system firewall to manage NAT for EasyTier tunnel and subnet traffic. Disabling this option disables source address masquerading to preserve real source IPs of remote peers and subnets in the local network." +msgstr "使用 OpenWrt 系統防火牆接管 EasyTier 虛擬網卡與子網轉發流量的 NAT。關閉此選項將禁用源位址偽裝,保留遠端對等節點及子網在本地局域網中的真實源 IP。" + +msgid "Version" +msgstr "版本" + +msgid "Virtual IP" +msgstr "虛擬 IP" + +msgid "Virtual Interface" +msgstr "虛擬網卡" + +msgid "Virtual TUN network interface name (Corresponding flag: --dev-name)." +msgstr "創建的虛擬 TUN 網卡介面名稱 (對應參數:--dev-name)。" + +msgid "Web Config Server URL" +msgstr "雲端配置伺服器位址" + +msgid "Web Config Server URL cannot be empty." +msgstr "雲端配置伺服器位址不能為空。" + +msgid "Web Console" +msgstr "Web 控制台" + +msgid "Web Console Port" +msgstr "控制台監聽連接埠" + +msgid "Web Console Status" +msgstr "控制台服務狀態" + +msgid "Web Data Directory" +msgstr "控制台數據目錄" + +msgid "Zoom In" +msgstr "放大" + +msgid "Zoom Out" +msgstr "縮小" + +msgid "Reset Layout" +msgstr "重設佈局" + +msgid "(Hover to highlight link, drag nodes/canvas to adjust)" +msgstr "(懸停高亮鏈路,拖曳節點/畫布以調整)" + +msgid "All Links" +msgstr "全部鏈路" + +msgid "Local Only" +msgstr "僅本端" + +msgid "Show All Peer Links" +msgstr "顯示所有對端連接線" + +msgid "Show Local Links Only" +msgstr "僅聚焦本端連接線" + +msgid "Next Hop: %s (%s)" +msgstr "下一跳中繼: %s (%s)" + +msgid "Next Hop: %s" +msgstr "下一跳中繼: %s" + +msgid "Latency / Loss" +msgstr "延遲 / 丟包" + +msgid "RX / TX" +msgstr "接收 / 發送" + +msgid "(Hover/click node to trace path, drag to adjust)" +msgstr "(懸停/點擊節點追蹤路徑,支援拖曳調整)" + +msgid "Clear selection" +msgstr "清除選取" + +msgid "Local Node: %s" +msgstr "本地節點: %s" + +msgid "Path" +msgstr "路徑" diff --git a/luci-app-easytier-next/root/etc/config/easytier b/luci-app-easytier-next/root/etc/config/easytier new file mode 100644 index 000000000..2f926fd2d --- /dev/null +++ b/luci-app-easytier-next/root/etc/config/easytier @@ -0,0 +1,21 @@ +config easytier 'settings' + option enabled '0' + option etcmd 'etcmd' + option network_name 'easytier' + option network_secret '' + option ip_dhcp '1' + option ipaddr '10.144.144.1' + option ip6addr '' + option external_node 'tcp://public.easytier.top:11010' + option rpc_port '15888' + option dev_name 'easytier0' + option encryption_algorithm 'aes-gcm' + option multi_thread '0' + option tunnel_snat '0' + option allow_wan '0' + option web_config 'udp://127.0.0.1:22020/admin' + option machine_id '' + option web_enabled '0' + option web_html_port '22020' + option web_dir '/etc/easytier' + list peeradd 'tcp://public.easytier.top:11010' diff --git a/luci-app-easytier-next/root/etc/easytier/config.toml b/luci-app-easytier-next/root/etc/easytier/config.toml new file mode 100644 index 000000000..765e93708 --- /dev/null +++ b/luci-app-easytier-next/root/etc/easytier/config.toml @@ -0,0 +1,15 @@ +instance_name = "default" +instance_id = "f9f05421-7c6b-47d6-a67d-4d58d49f51d8" +dhcp = true +listeners = [] +exit_nodes = [] +rpc_portal = "0.0.0.0:15888" + +[network_identity] +network_name = "mynetwork" +network_secret = "mysecret" + +[[peer]] +uri = "tcp://public.easytier.top:11010" + + diff --git a/luci-app-easytier-next/root/etc/hotplug.d/iface/99-easytier-routes b/luci-app-easytier-next/root/etc/hotplug.d/iface/99-easytier-routes new file mode 100755 index 000000000..b4b0784f0 --- /dev/null +++ b/luci-app-easytier-next/root/etc/hotplug.d/iface/99-easytier-routes @@ -0,0 +1,24 @@ +#!/bin/sh + +dev_name=$(uci -q get easytier.settings.dev_name) +[ -z "$dev_name" ] && dev_name="easytier0" + +[ "$DEVICE" = "$dev_name" ] || exit 0 +[ "$ACTION" = "ifup" ] || exit 0 + +LOCK=/tmp/easytier_hotplug.lock + +# Timestamp-based lock: bail out if another check fired within 10 s. +if [ -f "$LOCK" ]; then + ts=$(cat "$LOCK" 2>/dev/null) + [ -n "$ts" ] && [ $(( $(date +%s) - ts )) -lt 10 ] && exit 0 +fi + +# Skip if easytier is not configured or enabled +[ "$(uci -q get easytier.settings.enabled)" = "1" ] || exit 0 + +date +%s > "$LOCK" + +logger -t easytier-hotplug "${DEVICE} ifup detected: updating firewall rules" + +/etc/init.d/easytier update_firewall >/dev/null 2>&1 & diff --git a/luci-app-easytier-next/root/etc/init.d/easytier b/luci-app-easytier-next/root/etc/init.d/easytier new file mode 100755 index 000000000..32898d8f6 --- /dev/null +++ b/luci-app-easytier-next/root/etc/init.d/easytier @@ -0,0 +1,557 @@ +#!/bin/sh /etc/rc.common + +START=90 +USE_PROCD=1 +EXTRA_COMMANDS="update_firewall" +EXTRA_HELP=" update_firewall Update WAN firewall inbound rule from live easytier-cli listeners" + +NAME="easytier" +PROG="/usr/bin/easytier-core" +WEB_PROG="/usr/bin/easytier-web" +CONF_DIR="/etc/easytier" +LOG_FILE="/tmp/easytier.log" +WEB_LOG_FILE="/tmp/easytier-web.log" + +setup_firewall_and_network() { + local dev_name="$1" + [ -z "$dev_name" ] && dev_name="easytier0" + + local changed_net=0 + local changed_fw=0 + + # 1. Ensure network interface exists and is exactly synchronized with dev_name + local cur_dev cur_ifname + cur_dev=$(uci -q get network.easytier.device) + cur_ifname=$(uci -q get network.easytier.device) + + if ! uci -q get network.easytier >/dev/null; then + uci -q set network.easytier=interface + uci -q set network.easytier.proto='none' + uci -q set network.easytier.device="$dev_name" + uci -q set network.easytier.device="$dev_name" + changed_net=1 + else + if [ "$cur_dev" != "$dev_name" ] || [ "$cur_ifname" != "$dev_name" ]; then + uci -q set network.easytier.proto='none' + uci -q set network.easytier.device="$dev_name" + uci -q set network.easytier.device="$dev_name" + changed_net=1 + fi + fi + + # 2. Zone MASQUERADE (controlled by tunnel_snat) + local tunnel_snat target_masq="0" + tunnel_snat=$(uci -q get easytier.settings.tunnel_snat) + [ "$tunnel_snat" = "1" ] && target_masq="1" + + # Ensure firewall zone exists + if ! uci -q get firewall.easytier >/dev/null; then + uci -q set firewall.easytier=zone + uci -q set firewall.easytier.name='easytier' + uci -q set firewall.easytier.input='ACCEPT' + uci -q set firewall.easytier.output='ACCEPT' + uci -q set firewall.easytier.forward='ACCEPT' + uci -q set firewall.easytier.masq="$target_masq" + uci -q set firewall.easytier.mtu_fix='1' + uci -q add_list firewall.easytier.network='easytier' + uci -q add_list firewall.easytier.device="$dev_name" + + # Allow forwarding from LAN to EasyTier + uci -q set firewall.easytier_fwd_lan=forwarding + uci -q set firewall.easytier_fwd_lan.src='lan' + uci -q set firewall.easytier_fwd_lan.dest='easytier' + + # Allow forwarding from EasyTier to LAN + uci -q set firewall.easytier_fwd_et=forwarding + uci -q set firewall.easytier_fwd_et.src='easytier' + uci -q set firewall.easytier_fwd_et.dest='lan' + + # Allow forwarding from EasyTier to WAN + uci -q set firewall.easytier_fwd_wan=forwarding + uci -q set firewall.easytier_fwd_wan.src='easytier' + uci -q set firewall.easytier_fwd_wan.dest='wan' + changed_fw=1 + else + local cur_masq + cur_masq=$(uci -q get firewall.easytier.masq) + if [ "$cur_masq" != "$target_masq" ]; then + uci -q set firewall.easytier.masq="$target_masq" + changed_fw=1 + fi + + # Cleanly synchronize device list (remove stale devices, enforce single current dev_name) + local cur_devices + cur_devices=$(uci -q get firewall.easytier.device) + if [ "$cur_devices" != "$dev_name" ]; then + for d in $cur_devices; do + uci -q del_list firewall.easytier.device="$d" + done + uci -q add_list firewall.easytier.device="$dev_name" + changed_fw=1 + fi + + local has_net=0 + for n in $(uci -q get firewall.easytier.network); do + [ "$n" = "easytier" ] && has_net=1 + done + if [ "$has_net" = "0" ]; then + uci -q add_list firewall.easytier.network='easytier' + changed_fw=1 + fi + fi + + if [ "$changed_net" = "1" ]; then + uci -q commit network + /etc/init.d/network reload 2>/dev/null + fi + + if [ "$changed_fw" = "1" ]; then + uci -q commit firewall + /etc/init.d/firewall reload 2>/dev/null + fi + + # 3. Kernel forwarding + sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 + sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 +} + +apply_wan_firewall() { + local allow_wan + allow_wan=$(uci -q get easytier.settings.allow_wan) + + if [ "$allow_wan" != "1" ]; then + if uci -q get firewall.easytier_wan_inbound >/dev/null; then + uci -q delete firewall.easytier_wan_inbound + uci -q commit firewall + /etc/init.d/firewall reload 2>/dev/null + fi + return 0 + fi + + local rpc_port="$1" + [ -z "$rpc_port" ] && rpc_port=$(uci -q get easytier.settings.rpc_port) + [ -z "$rpc_port" ] && rpc_port="15888" + local rpc_arg="-p 127.0.0.1:${rpc_port}" + + # 纯净原子化探测:从运行态 easytier-core 查询实际生效的 listeners 端口 + local wan_ports="" + if [ -x /usr/bin/easytier-cli ]; then + local i=0 json_out="" + while [ $i -lt 10 ]; do + json_out=$(/usr/bin/easytier-cli $rpc_arg -o json node 2>/dev/null) + if [ -n "$json_out" ]; then + local extracted_ports="" + if command -v jsonfilter >/dev/null 2>&1; then + extracted_ports=$(echo "$json_out" | jsonfilter -e '@.listeners[*]' 2>/dev/null | grep -oE ':[0-9]{2,5}/?$' | tr -d ':/' | sort -n -u) + else + extracted_ports=$(echo "$json_out" | sed -n '/"listeners": \[/,/\]/p' | grep -oE ':[0-9]{2,5}/?$' | tr -d ':/' | sort -n -u) + fi + + if [ -n "$extracted_ports" ]; then + set -- $extracted_ports + wan_ports="$*" + break + fi + fi + i=$((i + 1)) + sleep 1 + done + fi + + # 严格原子化:仅当核心成功运行且实际处于监听状态时才下发放行规则;未启动或无端口绝不下发 + if [ -n "$wan_ports" ]; then + local cur_rule_ports cur_rule_enabled + cur_rule_ports=$(uci -q get firewall.easytier_wan_inbound.dest_port) + cur_rule_enabled=$(uci -q get firewall.easytier_wan_inbound.enabled) + + if ! uci -q get firewall.easytier_wan_inbound >/dev/null || [ "$cur_rule_ports" != "$wan_ports" ] || [ "$cur_rule_enabled" != "1" ]; then + uci -q set firewall.easytier_wan_inbound=rule + uci -q set firewall.easytier_wan_inbound.name='Allow-EasyTier-WAN-Inbound' + uci -q set firewall.easytier_wan_inbound.src='wan' + uci -q set firewall.easytier_wan_inbound.proto='tcp udp' + uci -q set firewall.easytier_wan_inbound.dest_port="$wan_ports" + uci -q set firewall.easytier_wan_inbound.target='ACCEPT' + uci -q set firewall.easytier_wan_inbound.enabled='1' + uci -q commit firewall + /etc/init.d/firewall reload 2>/dev/null + logger -t "$NAME" "WAN firewall inbound rule applied for active listeners: $wan_ports" + fi + else + logger -t "$NAME" "Warning: No active EasyTier listeners detected, keeping WAN firewall closed" + if uci -q get firewall.easytier_wan_inbound >/dev/null; then + uci -q delete firewall.easytier_wan_inbound + uci -q commit firewall + /etc/init.d/firewall reload 2>/dev/null + fi + fi +} + +setup_dnsmasq() { + local magic_dns tld_dns_zone + config_get_bool magic_dns settings magic_dns 0 + config_get tld_dns_zone settings tld_dns_zone 'et.net.' + [ -z "$tld_dns_zone" ] && tld_dns_zone='et.net.' + + # 防呆机制:无论用户输入是否带点,核心规范为以点结尾的 FQDN + case "$tld_dns_zone" in + *.) ;; + *) tld_dns_zone="${tld_dns_zone}." ;; + esac + local tld_clean + tld_clean=$(echo "$tld_dns_zone" | sed 's/\.$//') + + local changed_dhcp=0 + + # 1. Dynamically inspect all server rules pointing to 100.100.100.101, extract old zones, and clean corresponding rebind_domain + local cur_servers + cur_servers=$(uci -q get dhcp.@dnsmasq[0].server) + if [ -n "$cur_servers" ]; then + local s + for s in $cur_servers; do + case "$s" in + *100.100.100.101*) + local old_zone + old_zone=$(echo "$s" | sed -n 's|^/\([^/]*\)/100\.100\.100\.101.*|\1|p') + if [ -n "$old_zone" ]; then + uci -q del_list dhcp.@dnsmasq[0].rebind_domain="$old_zone" + uci -q del_list dhcp.@dnsmasq[0].rebind_domain="${old_zone}." + fi + uci -q del_list dhcp.@dnsmasq[0].server="$s" + changed_dhcp=1 + ;; + esac + done + fi + + # 2. Clean fallback domains + uci -q del_list dhcp.@dnsmasq[0].rebind_domain="$tld_dns_zone" + uci -q del_list dhcp.@dnsmasq[0].rebind_domain="$tld_clean" + uci -q del_list dhcp.@dnsmasq[0].rebind_domain="et.net" + uci -q del_list dhcp.@dnsmasq[0].rebind_domain="et.net." + + # 3. Add new Magic DNS forwarding and rebind whitelist if enabled + if [ "$magic_dns" -eq 1 ]; then + uci -q add_list dhcp.@dnsmasq[0].server="/${tld_clean}/100.100.100.101" + uci -q add_list dhcp.@dnsmasq[0].rebind_domain="${tld_clean}" + changed_dhcp=1 + fi + + if [ "$changed_dhcp" -eq 1 ]; then + uci -q commit dhcp + /etc/init.d/dnsmasq reload 2>/dev/null + fi +} + +cleanup_dnsmasq() { + local tld_dns_zone + config_get tld_dns_zone settings tld_dns_zone 'et.net.' + [ -z "$tld_dns_zone" ] && tld_dns_zone='et.net.' + + case "$tld_dns_zone" in + *.) ;; + *) tld_dns_zone="${tld_dns_zone}." ;; + esac + local tld_clean + tld_clean=$(echo "$tld_dns_zone" | sed 's/\.$//') + + local changed_dhcp=0 + + # 1. Dynamically inspect all server rules pointing to 100.100.100.101, extract old zones, and clean corresponding rebind_domain + local cur_servers + cur_servers=$(uci -q get dhcp.@dnsmasq[0].server) + if [ -n "$cur_servers" ]; then + local s + for s in $cur_servers; do + case "$s" in + *100.100.100.101*) + local old_zone + old_zone=$(echo "$s" | sed -n 's|^/\([^/]*\)/100\.100\.100\.101.*|\1|p') + if [ -n "$old_zone" ]; then + uci -q del_list dhcp.@dnsmasq[0].rebind_domain="$old_zone" + uci -q del_list dhcp.@dnsmasq[0].rebind_domain="${old_zone}." + fi + uci -q del_list dhcp.@dnsmasq[0].server="$s" + changed_dhcp=1 + ;; + esac + done + fi + + # 2. Clean fallback domains + uci -q del_list dhcp.@dnsmasq[0].rebind_domain="$tld_dns_zone" + uci -q del_list dhcp.@dnsmasq[0].rebind_domain="$tld_clean" + + if [ "$changed_dhcp" -eq 1 ]; then + uci -q commit dhcp + /etc/init.d/dnsmasq reload 2>/dev/null + fi +} + +atomic_clean_cloud_dir() { + local target_dir="$1" + local new_mid="$2" + + # Security boundary guard: strictly disallow root /etc/easytier to protect config.toml and easytier.db + case "$target_dir" in + /etc/easytier|/etc/easytier/|/etc|/etc/|/tmp|/tmp/) + logger -t "$NAME" "Security warning: Cannot clean root directory $target_dir" + return 1 + ;; + /etc/easytier/*|/tmp/easytier/*) ;; + *) + logger -t "$NAME" "Security warning: Refusing to clean unsafe path: $target_dir" + return 1 + ;; + esac + + local pid="$$" + local new_tmp="${target_dir}.new.${pid}" + local trash_tmp="${target_dir}.trash.${pid}" + + mkdir -p "$new_tmp" + [ -n "$new_mid" ] && echo "$new_mid" > "$new_tmp/.machine_id" + + if [ -d "$target_dir" ]; then + mv "$target_dir" "$trash_tmp" 2>/dev/null + fi + mv "$new_tmp" "$target_dir" 2>/dev/null + rm -rf "${target_dir}.trash."* 2>/dev/null + return 0 +} + +start_core() { + local enabled etcmd network_name network_secret ip_dhcp ipaddr ip6addr + local external_node rpc_port encryption_algorithm dev_name proxy_cidrs + local custom_params custom_config_file web_config multi_thread + local magic_dns tld_dns_zone + local machine_id config_dir + + config_get_bool enabled settings enabled 0 + [ "$enabled" -eq 1 ] || return 0 + + [ -x "$PROG" ] || { + logger -t "$NAME" "Core executable $PROG not found" + return 1 + } + + config_get etcmd settings etcmd 'etcmd' + config_get network_name settings network_name '' + config_get network_secret settings network_secret '' + config_get_bool ip_dhcp settings ip_dhcp 1 + config_get ipaddr settings ipaddr '' + config_get ip6addr settings ip6addr '' + config_get external_node settings external_node '' + config_get rpc_port settings rpc_port '15888' + config_get encryption_algorithm settings encryption_algorithm '' + config_get dev_name settings dev_name 'easytier0' + config_get custom_config_file settings custom_config_file '/etc/easytier/config.toml' + config_get web_config settings web_config '' + config_get machine_id settings machine_id '' + config_get custom_params settings custom_params '' + config_get_bool multi_thread settings multi_thread 0 + config_get_bool magic_dns settings magic_dns 0 + config_get tld_dns_zone settings tld_dns_zone 'et.net.' + [ -z "$tld_dns_zone" ] && tld_dns_zone='et.net.' + case "$tld_dns_zone" in + *.) ;; + *) tld_dns_zone="${tld_dns_zone}." ;; + esac + config_get_bool enable_exit_node settings enable_exit_node 0 + + mkdir -p "$CONF_DIR" + + procd_open_instance "core" + procd_set_param command "$PROG" + + if [ "$etcmd" = "config" ]; then + [ -f "$custom_config_file" ] || { + logger -t "$NAME" "Custom config file $custom_config_file not found" + return 1 + } + procd_append_param command -c "$custom_config_file" + elif [ "$etcmd" = "web" ]; then + [ -n "$web_config" ] || { + logger -t "$NAME" "Web config URL is not configured" + return 1 + } + procd_append_param command -w "$web_config" + + # Machine ID (ensure unique UUID exists) + if [ -z "$machine_id" ]; then + if [ -f /proc/sys/kernel/random/uuid ]; then + machine_id=$(cat /proc/sys/kernel/random/uuid 2>/dev/null) + fi + if [ -z "$machine_id" ]; then + machine_id=$(tr -dc 'a-f0-9' < /dev/urandom 2>/dev/null | head -c 32 | sed -r 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/') + fi + if [ -n "$machine_id" ]; then + uci -q set easytier.settings.machine_id="$machine_id" + uci -q commit easytier + fi + fi + [ -n "$machine_id" ] && procd_append_param command --machine-id "$machine_id" + + # Internal cloud config directory (isolated storage) + local config_dir='/etc/easytier/cloud' + + # Atomic check & clean for Machine ID change + local last_mid_file="$config_dir/.machine_id" + local last_mid="" + [ -f "$last_mid_file" ] && last_mid=$(cat "$last_mid_file" 2>/dev/null) + + if [ -d "$config_dir" ] && [ -n "$last_mid" ] && [ "$last_mid" != "$machine_id" ]; then + logger -t "$NAME" "Machine ID changed from $last_mid to $machine_id, atomically cleaning cloud config cache in $config_dir" + atomic_clean_cloud_dir "$config_dir" "$machine_id" + else + mkdir -p "$config_dir" + [ -n "$machine_id" ] && [ ! -f "$last_mid_file" ] && echo "$machine_id" > "$last_mid_file" 2>/dev/null + fi + + procd_append_param command --config-dir "$config_dir" + + # System proxy forward & device name + procd_append_param command --proxy-forward-by-system + [ -n "$dev_name" ] && procd_append_param command --dev-name "$dev_name" + + # Additional custom arguments + if [ -n "$custom_params" ]; then + for arg in $custom_params; do + procd_append_param command "$arg" + done + fi + else + # Default command-line configuration + [ -n "$network_name" ] && procd_append_param command --network-name "$network_name" + [ -n "$network_secret" ] && procd_append_param command --network-secret "$network_secret" + + if [ "$ip_dhcp" -eq 1 ]; then + procd_append_param command -d + elif [ -n "$ipaddr" ]; then + procd_append_param command -i "$ipaddr" + fi + + [ -n "$ip6addr" ] && procd_append_param command --ipv6 "$ip6addr" + [ -n "$external_node" ] && procd_append_param command -e "$external_node" + [ -n "$rpc_port" ] && procd_append_param command --rpc-portal "0.0.0.0:${rpc_port}" + [ -n "$encryption_algorithm" ] && procd_append_param command --encryption-algorithm "$encryption_algorithm" + [ -n "$dev_name" ] && procd_append_param command --dev-name "$dev_name" + [ "$multi_thread" -eq 1 ] && procd_append_param command --multi-thread + + # Always disable user-space built-in NAT and delegate forwarding to Linux kernel + procd_append_param command --proxy-forward-by-system + + # Exit Node + [ "$enable_exit_node" -eq 1 ] && procd_append_param command --enable-exit-node + + # Magic DNS + if [ "$magic_dns" -eq 1 ]; then + procd_append_param command --accept-dns true + [ -n "$tld_dns_zone" ] && procd_append_param command --tld-dns-zone "$tld_dns_zone" + fi + + # Peer nodes + append_peer() { + [ -n "$1" ] && procd_append_param command -p "$1" + } + config_list_foreach settings peeradd append_peer + + # Proxy networks / Subnet routes + append_proxy() { + [ -n "$1" ] && procd_append_param command -n "$1" + } + config_list_foreach settings proxy_networks append_proxy + + # Additional custom arguments + if [ -n "$custom_params" ]; then + for arg in $custom_params; do + procd_append_param command "$arg" + done + fi + fi + + procd_set_param respawn 3600 5 0 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param user root + procd_close_instance + + setup_firewall_and_network "$dev_name" "$rpc_port" + setup_dnsmasq + logger -t "$NAME" "EasyTier core service started" + + # 异步触发 WAN 放行策略更新(让 procd 优先 fork 启动核心进程,随后子进程原子联动查询 listeners 并下发规则) + local allow_wan + config_get_bool allow_wan settings allow_wan 0 + if [ "$allow_wan" -eq 1 ]; then + ( + apply_wan_firewall "$rpc_port" + ) >/dev/null 2>&1 & + else + if uci -q get firewall.easytier_wan_inbound >/dev/null; then + uci -q delete firewall.easytier_wan_inbound + uci -q commit firewall + /etc/init.d/firewall reload 2>/dev/null + fi + fi +} + +update_firewall() { + config_load easytier + local dev_name rpc_port + config_get dev_name settings dev_name 'easytier0' + config_get rpc_port settings rpc_port '15888' + setup_firewall_and_network "$dev_name" "$rpc_port" + apply_wan_firewall "$rpc_port" +} + +start_web() { + local web_enabled web_port web_dir + + config_get_bool web_enabled settings web_enabled 0 + [ "$web_enabled" -eq 1 ] || return 0 + + [ -x "$WEB_PROG" ] || { + logger -t "$NAME" "Web console executable $WEB_PROG not found" + return 1 + } + + config_get web_port settings web_html_port '22020' + config_get web_dir settings web_dir '/etc/easytier' + + mkdir -p "$web_dir" + + procd_open_instance "web" + procd_set_param command "$WEB_PROG" --db "$web_dir/easytier.db" --web-server-port "$web_port" --api-server-port "$web_port" --file-log-dir "$web_dir" + procd_set_param respawn 3600 5 0 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param user root + procd_close_instance + + logger -t "$NAME" "EasyTier web console started on port $web_port" +} + +start_service() { + config_load easytier + start_core + start_web +} + +stop_service() { + cleanup_dnsmasq + # 停止服务联动清理 WAN 防火墙放行,防止离线后端口空置暴露 + if uci -q get firewall.easytier_wan_inbound >/dev/null; then + uci -q delete firewall.easytier_wan_inbound + uci -q commit firewall + /etc/init.d/firewall reload 2>/dev/null + fi + logger -t "$NAME" "EasyTier services stopped" +} + +service_triggers() { + procd_add_reload_trigger "easytier" +} + +reload_service() { + restart +} diff --git a/luci-app-easytier-next/root/etc/uci-defaults/99_easytier-init b/luci-app-easytier-next/root/etc/uci-defaults/99_easytier-init new file mode 100755 index 000000000..850f97d8a --- /dev/null +++ b/luci-app-easytier-next/root/etc/uci-defaults/99_easytier-init @@ -0,0 +1,20 @@ +#!/bin/sh + +[ -z "$(uci -q get easytier.settings.machine_id)" ] && { + new_uuid="" + if [ -f /proc/sys/kernel/random/uuid ]; then + new_uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null) + fi + if [ -z "$new_uuid" ]; then + new_uuid=$(tr -dc 'a-f0-9' < /dev/urandom 2>/dev/null | head -c 32 | sed -r 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/') + fi + if [ -n "$new_uuid" ]; then + uci -q set easytier.settings.machine_id="$new_uuid" + uci -q commit easytier + fi +} + +rm -f /tmp/luci-indexcache* 2>/dev/null +rm -f /tmp/luci-modulecache/* 2>/dev/null + +exit 0 diff --git a/luci-app-easytier-next/root/usr/libexec/rpcd/easytier b/luci-app-easytier-next/root/usr/libexec/rpcd/easytier new file mode 100755 index 000000000..0db781866 --- /dev/null +++ b/luci-app-easytier-next/root/usr/libexec/rpcd/easytier @@ -0,0 +1,471 @@ +#!/usr/bin/env lua + +local json = require "luci.jsonc" +local nixio = require "nixio" +local fs = require "nixio.fs" + +local function trim(s) + if type(s) ~= 'string' then return '' end + return s:gsub("^%s+", ""):gsub("%s+$", "") +end + +local function exec_cmd(cmd) + local f = io.popen(cmd) + if not f then + return { code = -1, stdout = {}, raw = "" } + end + local content = f:read("*a") or "" + local success, msg, code = f:close() + content = trim(content) + local lines = {} + for line in string.gmatch(content .. "\n", "(.-)\r?\n") do + if line ~= "" then + table.insert(lines, line) + end + end + return { + code = (type(success) == "number" and success) or (code or 0), + stdout = lines, + raw = content + } +end + +local function get_procd_status(instance_name) + local ubus_res = exec_cmd("ubus call service list '{\"name\":\"easytier\"}' 2>/dev/null") + if ubus_res.raw and #ubus_res.raw > 0 then + local parsed = json.parse(ubus_res.raw) + if parsed and parsed.easytier and parsed.easytier.instances then + local inst = parsed.easytier.instances[instance_name or 'core'] + if inst and inst.running then + return { running = true, pid = inst.pid } + end + end + end + return { running = false } +end + +local function get_rogue_pid(bin_name, managed_pid) + local res = exec_cmd("pidof " .. bin_name .. " 2>/dev/null") + if res.raw and #res.raw > 0 then + for pid in string.gmatch(res.raw, "%d+") do + local num_pid = tonumber(pid) + if num_pid and (managed_pid == nil or num_pid ~= managed_pid) then + return num_pid + end + end + end + return nil +end + +local function get_service_state(bin_name, instance_name) + local procd_st = get_procd_status(instance_name) + local rogue_pid = get_rogue_pid(bin_name, procd_st.pid) + + local state = "stopped" + if procd_st.running then + state = "managed" + elseif rogue_pid then + state = "unmanaged" + end + + return { + state = state, + running = (state ~= "stopped"), + pid = procd_st.pid or rogue_pid + } +end + +local function get_cached_version() + local cache_file = '/tmp/easytier_version.cache' + local bin_path = '/usr/bin/easytier-core' + if not fs.access(bin_path) then return 'Unknown' end + + local bin_st = fs.stat(bin_path) + local cache_st = fs.stat(cache_file) + + if cache_st and (cache_st.size or 0) > 0 and bin_st and (cache_st.mtime or 0) >= (bin_st.mtime or 0) then + local cached = trim(fs.readfile(cache_file) or '') + if cached ~= '' then return cached end + end + + local ver_res = exec_cmd(bin_path .. ' --version 2>/dev/null') + if ver_res.stdout and #ver_res.stdout > 0 then + local ver_line = trim(ver_res.stdout[1]) + if ver_line ~= '' then + fs.writefile(cache_file, ver_line .. '\n') + return ver_line + end + end + return 'Unknown' +end + +local function get_route_info_map() + local route_map = {} + if not fs.access('/usr/bin/easytier-cli') then return route_map end + local r_res = exec_cmd('/usr/bin/easytier-cli -o json route 2>/dev/null') + if r_res.code == 0 and r_res.raw and #r_res.raw > 0 then + local r_data = json.parse(r_res.raw) + if r_data and type(r_data) == 'table' and #r_data > 0 then + for _, item in ipairs(r_data) do + local cidrs = trim(item.proxy_cidrs or '') + local ip = trim(item.ipv4 or '') + local host = trim(item.hostname or '') + local next_hop_host = trim(item.next_hop_hostname_lat_first or item.next_hop_hostname or '') + local next_hop_ip = trim(item.next_hop_ipv4_lat_first or item.next_hop_ipv4 or '') + local path_len = item.path_len_lat_first ~= nil and item.path_len_lat_first or item.path_len + local path_lat = item.path_latency_lat_first ~= nil and item.path_latency_lat_first or item.path_latency + + local entry = { + proxy_cidrs = cidrs, + next_hop_hostname = next_hop_host, + next_hop_ipv4 = next_hop_ip, + path_len = path_len, + path_latency = path_lat + } + + if ip ~= '' then + route_map[ip] = entry + local ip_clean = ip:match("^([^/]+)") or ip + route_map[ip_clean] = entry + end + if host ~= '' then + route_map[host] = entry + end + end + end + end + return route_map +end + +local methods = { + get_status = { + args = {}, + call = function(args) + local core_st = get_service_state('/usr/bin/easytier-core', 'core') + local web_st = get_service_state('/usr/bin/easytier-web', 'web') + + local data = { + installed = fs.access('/usr/bin/easytier-core') and true or false, + web_installed = fs.access('/usr/bin/easytier-web') and true or false, + version = get_cached_version(), + core = core_st, + web = web_st, + node_info = nil + } + + if core_st.running and fs.access('/usr/bin/easytier-cli') then + local node_res = exec_cmd('/usr/bin/easytier-cli node 2>/dev/null') + if node_res.raw and #node_res.raw > 0 then + data.node_info = node_res.raw + end + end + + return data + end + }, + + get_peers = { + args = {}, + call = function(args) + if not fs.access('/usr/bin/easytier-cli') then + return { peers = {}, raw = '' } + end + + local route_map = get_route_info_map() + + local json_res = exec_cmd('/usr/bin/easytier-cli -o json peer 2>/dev/null') + if json_res.code == 0 and json_res.raw and #json_res.raw > 0 then + local j_data = json.parse(json_res.raw) + if j_data and type(j_data) == 'table' and #j_data > 0 then + local peers = {} + for _, item in ipairs(j_data) do + local p_ip = trim(item.cidr or item.ipv4 or '') + local p_host = trim(item.hostname or '') + local p_ip_clean = p_ip:match("^([^/]+)") or p_ip + local r_entry = route_map[p_ip] or route_map[p_ip_clean] or route_map[p_host] or {} + local proxy_cidrs = r_entry.proxy_cidrs or '' + local next_hop_hostname = r_entry.next_hop_hostname or '' + local next_hop_ipv4 = r_entry.next_hop_ipv4 or '' + local path_latency = r_entry.path_latency ~= nil and r_entry.path_latency or '' + + table.insert(peers, { + ipv4 = p_ip, + hostname = p_host, + proxy_cidrs = proxy_cidrs, + next_hop_hostname = next_hop_hostname, + next_hop_ipv4 = next_hop_ipv4, + path_latency = path_latency, + cost = trim(item.cost or ''), + latency = (item.lat_ms ~= nil and item.lat_ms ~= '-') and tostring(item.lat_ms) or '-', + loss_rate = trim(item.loss_rate or '-'), + rx = trim(item.rx_bytes or '-'), + tx = trim(item.tx_bytes or '-'), + tunnel = trim(item.tunnel_proto or '-'), + nat = trim(item.nat_type or '-'), + version = trim(item.version or '-') + }) + end + return { peers = peers, raw = '' } + end + end + + local peer_res = exec_cmd('/usr/bin/easytier-cli peer 2>/dev/null') + local raw_output = peer_res.raw or '' + local peers = {} + + if peer_res.stdout and #peer_res.stdout > 0 then + for _, line in ipairs(peer_res.stdout) do + local trimmed = trim(line) + if trimmed ~= '' and not (trimmed:match("[Ii]pv4") and trimmed:match("|")) and not trimmed:match("^[%-%+─━%=| ]+$") then + local cols = {} + for part in string.gmatch(trimmed, "[^|]+") do + table.insert(cols, trim(part)) + end + if #cols >= 2 then + local peer_ip = trim(cols[1] or '') + local peer_host = trim(cols[2] or '') + local peer_ip_clean = peer_ip:match("^([^/]+)") or peer_ip + local r_entry = route_map[peer_ip] or route_map[peer_ip_clean] or route_map[peer_host] or {} + local proxy_cidrs = r_entry.proxy_cidrs or '' + local next_hop_hostname = r_entry.next_hop_hostname or '' + local next_hop_ipv4 = r_entry.next_hop_ipv4 or '' + local path_latency = r_entry.path_latency ~= nil and r_entry.path_latency or '' + + table.insert(peers, { + ipv4 = peer_ip, + hostname = peer_host, + proxy_cidrs = proxy_cidrs, + next_hop_hostname = next_hop_hostname, + next_hop_ipv4 = next_hop_ipv4, + path_latency = path_latency, + cost = cols[3] or '-', + latency = cols[4] or '-', + loss_rate = cols[5] or '-', + rx = cols[6] or '-', + tx = cols[7] or '-', + tunnel = cols[8] or '-', + nat = cols[9] or '-', + version = cols[10] or '-' + }) + end + end + end + end + + return { peers = peers, raw = raw_output } + end + }, + + get_topology = { + args = {}, + call = function(args) + if not fs.access('/usr/bin/easytier-cli') then + return { nodes = {} } + end + + local route_map = get_route_info_map() + local res = exec_cmd('/usr/bin/easytier-cli -o json peer-center 2>/dev/null') + if res.code == 0 and res.raw and #res.raw > 0 then + local data = json.parse(res.raw) + if data and type(data) == 'table' and #data > 0 then + local valid_nodes = {} + for _, n in ipairs(data) do + local n_ip = trim(n.ipv4 or '') + local n_host = trim(n.hostname or '') + if n_host ~= '' and not n_host:lower():match("^unknown$") and n_ip ~= '' and n_ip ~= '-' then + local n_ip_clean = n_ip:match("^([^/]+)") or n_ip + local r_entry = route_map[n_ip] or route_map[n_ip_clean] or route_map[n_host] or {} + n.proxy_cidrs = r_entry.proxy_cidrs or '' + table.insert(valid_nodes, n) + end + end + return { nodes = valid_nodes } + end + end + return { nodes = {} } + end + }, + + get_subroutes = { + args = {}, + call = function(args) + local subnets = {} + local seen = {} + + local res = exec_cmd("ip -j route 2>/dev/null") + if res.code == 0 and res.raw and #res.raw > 0 then + local routes_json = json.parse(res.raw) + if routes_json and type(routes_json) == 'table' then + for _, r in ipairs(routes_json) do + if r.dst and r.dst ~= 'default' and r.scope == 'link' and r.dst:find("%.") and r.dev ~= 'easytier0' and r.dev ~= 'lo' then + if not seen[r.dst] then + seen[r.dst] = true + table.insert(subnets, r.dst) + end + end + end + end + end + + if #subnets == 0 then + local txt_res = exec_cmd("ip route 2>/dev/null") + if txt_res.stdout and #txt_res.stdout > 0 then + for _, line in ipairs(txt_res.stdout) do + line = trim(line) + if line ~= '' and not line:lower():match("^default") and not line:find("easytier0") and not line:find("lo") then + local subnet, dev = line:match("^(%d+%.%d+%.%d+%.%d+/%d+)%s+dev%s+([^%s]+)") + if subnet and dev and dev ~= 'easytier0' and dev ~= 'lo' and not seen[subnet] then + seen[subnet] = true + table.insert(subnets, subnet) + end + end + end + end + end + + if #subnets == 0 then + local net_res = exec_cmd("ubus call network.interface dump 2>/dev/null") + if net_res.raw and #net_res.raw > 0 then + local net_data = json.parse(net_res.raw) + if net_data and net_data.interface then + for _, iface in ipairs(net_data.interface) do + if iface.interface ~= 'loopback' and iface.interface ~= 'easytier' and iface['ipv4-address'] then + for _, addr in ipairs(iface['ipv4-address']) do + local ip_str = addr.address + local mask = addr.mask + if ip_str and mask then + local p1, p2, p3, p4 = ip_str:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") + if p1 and p2 and p3 and p4 then + local subnet = '' + if mask == 24 then + subnet = string.format("%s.%s.%s.0/24", p1, p2, p3) + elseif mask == 16 then + subnet = string.format("%s.%s.0.0/16", p1, p2) + elseif mask == 8 then + subnet = string.format("%s.0.0.0/8", p1) + else + subnet = string.format("%s/%s", ip_str, mask) + end + if subnet ~= '' and not seen[subnet] then + seen[subnet] = true + table.insert(subnets, subnet) + end + end + end + end + end + end + end + end + end + + return { routes = subnets } + end + }, + + get_logs = { + args = {}, + call = function(args) + local logs = {} + local res = exec_cmd("logread -e easytier 2>/dev/null | tail -n 80") + if res.stdout and #res.stdout > 0 then + logs = res.stdout + elseif fs.access('/tmp/easytier.log') then + local f_res = exec_cmd("tail -n 80 /tmp/easytier.log 2>/dev/null") + if f_res.stdout then logs = f_res.stdout end + end + return { logs = logs } + end + }, + + clear_logs = { + args = {}, + call = function(args) + fs.writefile('/tmp/easytier.log', '') + fs.writefile('/tmp/easytier-web.log', '') + return { success = true } + end + }, + + service_action = { + args = { action = "action" }, + call = function(args) + local action = args and args.action + if not action then return { success = false, error = "Missing action" } end + + if action == "restart" or action == "stop" then + os.execute("killall -9 easytier-core easytier-web 2>/dev/null") + end + + if action == "start" or action == "restart" then + os.execute("/etc/init.d/easytier restart >/dev/null 2>&1") + elseif action == "stop" then + os.execute("/etc/init.d/easytier stop >/dev/null 2>&1") + end + + return { success = true } + end + } +} + +local function parseInput() + local parse = json.new() + local done, err + + while true do + local chunk = io.read(4096) + if not chunk or #chunk == 0 then + break + end + done, err = parse:parse(chunk) + if done then + break + end + end + + if not done then + print(json.stringify({ error = err or "Incomplete input" })) + os.exit(1) + end + + return parse:get() +end + +local function validateArgs(func, uargs) + local method = methods[func] + if not method then + print(json.stringify({ error = "Method not found" })) + os.exit(1) + end + + if type(uargs) ~= "table" then + print(json.stringify({ error = "Invalid arguments" })) + os.exit(1) + end + + uargs.ubus_rpc_session = nil + + local margs = method.args or {} + for k, _ in pairs(uargs) do + if margs[k] == nil then + print(json.stringify({ error = "Invalid argument: " .. tostring(k) })) + os.exit(1) + end + end + + return method +end + +if arg[1] == "list" then + local rv = {} + for name, method in pairs(methods) do + rv[name] = method.args or {} + end + print((json.stringify(rv):gsub(":%[%]", ":{}"))) +elseif arg[1] == "call" then + local args = parseInput() + local method = validateArgs(arg[2], args) + local result, code = method.call(args) + print((json.stringify(result):gsub("^%[%]$", "{}"))) + os.exit(code or 0) +end diff --git a/luci-app-easytier-next/root/usr/share/luci/menu.d/luci-app-easytier.json b/luci-app-easytier-next/root/usr/share/luci/menu.d/luci-app-easytier.json new file mode 100644 index 000000000..5e6faeda0 --- /dev/null +++ b/luci-app-easytier-next/root/usr/share/luci/menu.d/luci-app-easytier.json @@ -0,0 +1,13 @@ +{ + "admin/vpn/easytier": { + "title": "EasyTier", + "order": 92, + "action": { + "type": "view", + "path": "easytier/main" + }, + "depends": { + "acl": [ "luci-app-easytier" ] + } + } +} diff --git a/luci-app-easytier-next/root/usr/share/rpcd/acl.d/luci-app-easytier.json b/luci-app-easytier-next/root/usr/share/rpcd/acl.d/luci-app-easytier.json new file mode 100644 index 000000000..eb963f72e --- /dev/null +++ b/luci-app-easytier-next/root/usr/share/rpcd/acl.d/luci-app-easytier.json @@ -0,0 +1,38 @@ +{ + "luci-app-easytier": { + "description": "Allow user access to EasyTier", + "read": { + "ubus": { + "easytier": [ + "get_status", + "get_peers", + "get_topology", + "get_config", + "get_logs", + "get_subroutes", + "get_web_status", + "get_version_info" + ] + }, + "uci": [ "easytier" ], + "file": { + "/etc/easytier/config.toml": [ "read" ] + } + }, + "write": { + "ubus": { + "easytier": [ + "set_config", + "clear_logs", + "service_action", + "toggle_service", + "reset_web_db" + ] + }, + "uci": [ "easytier" ], + "file": { + "/etc/easytier/config.toml": [ "write" ] + } + } + } +} diff --git a/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js b/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js index a51100b2f..194a21095 100644 --- a/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js +++ b/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js @@ -1,5 +1,7 @@ 'use strict'; 'require view'; +'require fs'; +'require poll'; 'require rpc'; 'require uci'; 'require form'; @@ -15,10 +17,16 @@ return view.extend({ expect: { result: [] } }), + getRuntimeStatus() { + return L.resolveDefault( + fs.exec_direct('/usr/libexec/luci-firewall-status', [], 'json'), {}); + }, + load() { return Promise.all([ this.callConntrackHelpers(), - firewall.getDefaults() + firewall.getDefaults(), + this.getRuntimeStatus() ]); }, @@ -29,9 +37,24 @@ return view.extend({ return this.renderZones(data); }, - renderZones([ctHelpers, fwDefaults]) { + renderZones([ctHelpers, fwDefaults, runtimeStatus]) { let m, s, o, out; const fw4 = L.hasSystemFeature('firewall4'); + const runtimeStatusText = E('span', {}, [ '-' ]); + const updateRuntimeStatus = (status) => { + const natModes = { + fullcone: 'FULLCONENAT', + nat1: _('Broadcom Fullcone NAT1'), + disabled: _('Disabled') + }; + const natMode = natModes[status?.nat_mode] || _('Unknown'); + const tcpcca = status?.tcp_cca + ? status.tcp_cca.toUpperCase() + : _('Unknown'); + + runtimeStatusText.textContent = + _('Running status: %s, TCP congestion control algorithm is %s').format(natMode, tcpcca); + }; const addTCPCCAOption = () => { const tcpcca = s.option(form.ListValue, 'tcpcca', _('TCP CCA'), _('TCP congestion control algorithm.')); @@ -42,6 +65,8 @@ return view.extend({ tcpcca.rmempty = false; }; + updateRuntimeStatus(runtimeStatus); + m = new form.Map('firewall', _('Firewall - Zone Settings'), _('The firewall creates zones over your network interfaces to control network traffic flow.')); @@ -462,6 +487,24 @@ return view.extend({ o.filter = out.filter; o.cfgvalue = out.cfgvalue; - return m.render(); + return m.render().then(L.bind(function(mapEl) { + const firstSection = mapEl.querySelector('.cbi-section'); + const statusSection = E('div', { + 'class': 'cbi-section firewall-runtime-status', + 'style': 'overflow-x:auto' + }, [ + E('div', { + 'style': 'padding:.75em 1.5em;color:#008000;font-size:1em;font-weight:600;white-space:nowrap' + }, [ runtimeStatusText ]) + ]); + + firstSection.parentNode.insertBefore(statusSection, firstSection); + + poll.add(L.bind(function() { + return this.getRuntimeStatus().then(updateRuntimeStatus); + }, this), 5); + + return mapEl; + }, this)); } }); diff --git a/luci-app-firewall/po/templates/firewall.pot b/luci-app-firewall/po/templates/firewall.pot index ecdc6f39d..73ddc58aa 100644 --- a/luci-app-firewall/po/templates/firewall.pot +++ b/luci-app-firewall/po/templates/firewall.pot @@ -260,6 +260,22 @@ msgstr "" msgid "Destination zone" msgstr "" +#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js:49 +msgid "Disabled" +msgstr "" + +#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js:48 +msgid "Broadcom Fullcone NAT1" +msgstr "" + +#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js:56 +msgid "Running status: %s, TCP congestion control algorithm is %s" +msgstr "" + +#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js:54 +msgid "Unknown" +msgstr "" + #: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js:263 msgid "Device name" msgstr "" diff --git a/luci-app-firewall/po/zh_Hans/firewall.po b/luci-app-firewall/po/zh_Hans/firewall.po index ea9e2b2dd..713e7d391 100644 --- a/luci-app-firewall/po/zh_Hans/firewall.po +++ b/luci-app-firewall/po/zh_Hans/firewall.po @@ -344,6 +344,18 @@ msgstr "TCP 拥塞控制算法" msgid "TCP congestion control algorithm." msgstr "TCP 拥塞控制算法。" +msgid "Running status: %s, TCP congestion control algorithm is %s" +msgstr "当前运行状态:%s,TCP 拥塞控制算法为 %s" + +msgid "Broadcom Fullcone NAT1" +msgstr "Broadcom Fullcone NAT1" + +msgid "Disabled" +msgstr "已禁用" + +msgid "Unknown" +msgstr "未知" + #: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/forwards.js:346 #: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js:463 #: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/snats.js:339 diff --git a/luci-app-firewall/po/zh_Hant/firewall.po b/luci-app-firewall/po/zh_Hant/firewall.po index 95e1254fa..7ecb0c523 100644 --- a/luci-app-firewall/po/zh_Hant/firewall.po +++ b/luci-app-firewall/po/zh_Hant/firewall.po @@ -328,6 +328,18 @@ msgstr "啟用" msgid "Enable NAT Loopback" msgstr "啟用 NAT 回送" +msgid "Running status: %s, TCP congestion control algorithm is %s" +msgstr "目前執行狀態:%s,TCP 壅塞控制演算法為 %s" + +msgid "Broadcom Fullcone NAT1" +msgstr "Broadcom Fullcone NAT1" + +msgid "Disabled" +msgstr "已停用" + +msgid "Unknown" +msgstr "未知" + #: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js:43 msgid "Enable SYN-flood protection" msgstr "啟用 SYN 洪水保護" diff --git a/luci-app-firewall/root/usr/libexec/luci-firewall-status b/luci-app-firewall/root/usr/libexec/luci-firewall-status new file mode 100755 index 000000000..18ab673f6 --- /dev/null +++ b/luci-app-firewall/root/usr/libexec/luci-firewall-status @@ -0,0 +1,22 @@ +#!/bin/sh + +tcp_cca="$(cat /proc/sys/net/ipv4/tcp_congestion_control 2>/dev/null)" +nat_mode="disabled" + +rules="$(iptables-save -t nat 2>/dev/null)" + +if printf '%s\n' "$rules" | grep -q -- '--mode fullcone'; then + nat_mode="nat1" +elif printf '%s\n' "$rules" | grep -q -- '-j FULLCONENAT'; then + nat_mode="fullcone" +else + rules="$(nft list ruleset 2>/dev/null)" + + if printf '%s\n' "$rules" | grep -Eq 'masquerade[[:space:]]+fullcone'; then + nat_mode="nat1" + elif printf '%s\n' "$rules" | grep -Eq '(^|[[:space:]])fullcone([[:space:]]|$)'; then + nat_mode="fullcone" + fi +fi + +printf '{"nat_mode":"%s","tcp_cca":"%s"}\n' "$nat_mode" "$tcp_cca" diff --git a/luci-app-firewall/root/usr/share/rpcd/acl.d/luci-app-firewall.json b/luci-app-firewall/root/usr/share/rpcd/acl.d/luci-app-firewall.json index 17d1fbab1..433d1ccff 100644 --- a/luci-app-firewall/root/usr/share/rpcd/acl.d/luci-app-firewall.json +++ b/luci-app-firewall/root/usr/share/rpcd/acl.d/luci-app-firewall.json @@ -2,8 +2,10 @@ "luci-app-firewall": { "description": "Grant access to firewall configuration", "read": { + "cgi-io": [ "exec" ], "file": { - "/etc/firewall.user": [ "read" ] + "/etc/firewall.user": [ "read" ], + "/usr/libexec/luci-firewall-status": [ "exec" ] }, "ubus": { "file": [ "read" ], diff --git a/luci-app-frpc/po/sv/frpc.po b/luci-app-frpc/po/sv/frpc.po index 242e41de2..9b1b5e6aa 100644 --- a/luci-app-frpc/po/sv/frpc.po +++ b/luci-app-frpc/po/sv/frpc.po @@ -1,34 +1,34 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-08-13 09:16+0000\n" -"Last-Translator: Jonatan Nyberg \n" +"PO-Revision-Date: 2026-09-11 21:27+0000\n" +"Last-Translator: Daniel Nylander \n" "Language-Team: Swedish \n" "Language: sv\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 2026.9.dev0\n" +"X-Generator: Weblate 2026.10.dev0\n" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1464 msgid "Add new proxy or visitor..." -msgstr "" +msgstr "Lägg till ny proxy eller besökare …" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:370 msgid "Additional OIDC token endpoint params. Use key=value." -msgstr "" +msgstr "Ytterligare parametrar för OIDC-tokenändpunkt. Använd nyckel=värde." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:260 msgid "Additional auth scopes" -msgstr "" +msgstr "Ytterligare autentiseringsomfattningar" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:622 msgid "Additional client metadata. Use key=value." -msgstr "" +msgstr "Ytterligare klientmetadata. Använd nyckel=värde." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:236 msgid "Additional config files included by frpc." -msgstr "" +msgstr "Ytterligare konfigurationsfiler som inkluderas av frpc." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:184 msgid "Additional configs" @@ -36,11 +36,11 @@ msgstr "Ytterligare konfigurationer" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1070 msgid "Additional proxy metadata. Use key=value." -msgstr "" +msgstr "Ytterligare proxymetadata. Använd nyckel=värde." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:261 msgid "Additional scopes to include authentication information." -msgstr "" +msgstr "Ytterligare omfattningar som ska inkludera autentiseringsinformation." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:197 msgid "Address of the frps server." @@ -72,7 +72,7 @@ msgstr "" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:474 msgid "Assets dir" -msgstr "" +msgstr "Resurskatalog" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1436 msgid "Authentication" @@ -80,23 +80,23 @@ msgstr "Autentisering" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:241 msgid "Authentication method" -msgstr "" +msgstr "Autentiseringsmetod" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:242 msgid "Authentication method." -msgstr "" +msgstr "Autentiseringsmetod." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:690 msgid "Bandwidth limit" -msgstr "" +msgstr "Bandbreddsgräns" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:694 msgid "Bandwidth limit mode" -msgstr "" +msgstr "Läge för bandbreddsgräns" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:691 msgid "Bandwidth limit, for example 1MB." -msgstr "" +msgstr "Bandbreddsgräns, till exempel 1 MB." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:871 msgid "Bind address" @@ -108,33 +108,35 @@ msgstr "Bind port" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:883 msgid "Bind port is required for visitors." -msgstr "" +msgstr "Bindningsport krävs för besökare." #: applications/luci-app-frpc/root/usr/share/luci/menu.d/luci-app-frpc.json:11 msgid "Client" -msgstr "" +msgstr "Klient" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:193 msgid "Client ID" -msgstr "" +msgstr "Klient-ID" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:621 msgid "Client metadata" -msgstr "" +msgstr "Klientmetadata" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1427 msgid "Collecting data ..." -msgstr "Samlar in data ..." +msgstr "Samlar in data …" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:327 msgid "Command arguments passed to token source exec." -msgstr "" +msgstr "Kommandoargument som skickas till tokenkällans exec." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:304 msgid "" "Command used to obtain the token. The init script adds --allow-" "unsafe=TokenSourceExec when this source type is used." msgstr "" +"Kommando som används för att hämta token. Init-skriptet lägger till --allow-" +"unsafe=TokenSourceExec när den här källtypen används." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1435 msgid "Common Settings" @@ -145,10 +147,12 @@ msgid "" "Compatibility field for UCI. Server sections are emitted as proxy entries; " "visitor sections are emitted as visitor entries." msgstr "" +"Kompatibilitetsfält för UCI. Serversektioner skrivs ut som proxyposter; " +"besökarsektioner skrivs ut som besökarposter." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:703 msgid "Compress proxy traffic." -msgstr "" +msgstr "Komprimera proxytrafik." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:702 msgid "Compression" @@ -156,115 +160,115 @@ msgstr "Kompression" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:358 msgid "Configuration key: auth.oidc.audience." -msgstr "" +msgstr "Konfigurationsnyckel: auth.oidc.audience." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:350 msgid "Configuration key: auth.oidc.clientID." -msgstr "" +msgstr "Konfigurationsnyckel: auth.oidc.clientID." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:354 msgid "Configuration key: auth.oidc.clientSecret." -msgstr "" +msgstr "Konfigurationsnyckel: auth.oidc.clientSecret." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:362 msgid "Configuration key: auth.oidc.scope." -msgstr "" +msgstr "Konfigurationsnyckel: auth.oidc.scope." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:366 msgid "Configuration key: auth.oidc.tokenEndpointURL." -msgstr "" +msgstr "Konfigurationsnyckel: auth.oidc.tokenEndpointURL." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:398 msgid "Configuration key: log.level." -msgstr "" +msgstr "Konfigurationsnyckel: log.level." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:990 msgid "Configuration key: plugin.crtPath." -msgstr "" +msgstr "Konfigurationsnyckel: plugin.crtPath." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1014 msgid "Configuration key: plugin.hostHeaderRewrite." -msgstr "" +msgstr "Konfigurationsnyckel: plugin.hostHeaderRewrite." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:946 msgid "Configuration key: plugin.httpPassword." -msgstr "" +msgstr "Konfigurationsnyckel: plugin.httpPassword." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:942 msgid "Configuration key: plugin.httpUser." -msgstr "" +msgstr "Konfigurationsnyckel: plugin.httpUser." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1002 msgid "Configuration key: plugin.keyPath." -msgstr "" +msgstr "Konfigurationsnyckel: plugin.keyPath." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:969 msgid "Configuration key: plugin.localPath." -msgstr "" +msgstr "Konfigurationsnyckel: plugin.localPath." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:954 msgid "Configuration key: plugin.password." -msgstr "" +msgstr "Konfigurationsnyckel: plugin.password." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:973 msgid "Configuration key: plugin.stripPrefix." -msgstr "" +msgstr "Konfigurationsnyckel: plugin.stripPrefix." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:958 msgid "Configuration key: plugin.unixPath." -msgstr "" +msgstr "Konfigurationsnyckel: plugin.unixPath." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:950 msgid "Configuration key: plugin.username." -msgstr "" +msgstr "Konfigurationsnyckel: plugin.username." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:713 msgid "Configuration key: proxies[].customDomains." -msgstr "" +msgstr "Konfigurationsnyckel: proxies[].customDomains." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:774 msgid "Configuration key: proxies[].loadBalancer.group." -msgstr "" +msgstr "Konfigurationsnyckel: proxies[].loadBalancer.group." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:778 msgid "Configuration key: proxies[].loadBalancer.groupKey." -msgstr "" +msgstr "Konfigurationsnyckel: proxies[].loadBalancer.groupKey." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:717 msgid "Configuration key: proxies[].subdomain." -msgstr "" +msgstr "Konfigurationsnyckel: proxies[].subdomain." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:591 msgid "Configuration key: transport.quic.keepalivePeriod." -msgstr "" +msgstr "Konfigurationsnyckel: transport.quic.keepalivePeriod." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:595 msgid "Configuration key: transport.quic.maxIdleTimeout." -msgstr "" +msgstr "Konfigurationsnyckel: transport.quic.maxIdleTimeout." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:599 msgid "Configuration key: transport.quic.maxIncomingStreams." -msgstr "" +msgstr "Konfigurationsnyckel: transport.quic.maxIncomingStreams." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:532 msgid "Connect server local IP" -msgstr "" +msgstr "Lokal IP-adress vid anslutning till servern" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:504 msgid "Connection pool count" -msgstr "" +msgstr "Antal anslutningar i poolen" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:505 msgid "Connections established in advance." -msgstr "" +msgstr "Anslutningar upprättas i förväg." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:380 msgid "Custom CA certificate file for OIDC endpoint." -msgstr "" +msgstr "Anpassad CA-certifikatfil för OIDC-ändpunkten." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:224 msgid "Custom DNS server used by frpc." -msgstr "" +msgstr "Anpassad DNS-server som används av frpc." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:712 msgid "Custom domains" @@ -272,27 +276,29 @@ msgstr "Anpassade domäner" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:223 msgid "DNS server" -msgstr "" +msgstr "DNS-server" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:496 msgid "Dial server keepalive" -msgstr "" +msgstr "Keepalive för uppringning av server" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:492 msgid "Dial server timeout" -msgstr "" +msgstr "Tidsgräns vid uppringning av server" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:913 msgid "Disable NAT assisted addresses" -msgstr "" +msgstr "Inaktivera NAT-assisterade adresser" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:578 msgid "Disable custom TLS first byte" -msgstr "" +msgstr "Inaktivera anpassad första TLS-byte" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:914 msgid "Disable local interface assisted addresses for XTCP NAT traversal." msgstr "" +"Inaktivera adresser som assisteras av lokala gränssnitt för XTCP NAT-" +"traversering." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:405 msgid "Disable log color" @@ -300,7 +306,7 @@ msgstr "Inaktivera logg-färg" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:406 msgid "Disable log colors when log output is console." -msgstr "" +msgstr "Inaktivera loggfärger när loggutdata går till konsolen." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:268 #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:708 @@ -311,38 +317,42 @@ msgstr "Inaktiverad" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:898 msgid "Effective when XTCP keep tunnel open is enabled." -msgstr "" +msgstr "Gäller när Håll XTCP-tunnel öppen är aktiverat." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:479 msgid "Enable Go pprof handlers in web server." -msgstr "" +msgstr "Aktivera Go pprof-hanterare på webbservern." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1026 msgid "Enable HTTP/2 for HTTPS bridge plugins." -msgstr "" +msgstr "Aktivera HTTP/2 för HTTPS-brygginsticksprogram." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:428 msgid "" "Enable HTTPS for the frpc web server. This switch controls whether " "webServer.tls.certFile and webServer.tls.keyFile are emitted." msgstr "" +"Aktivera HTTPS för frpc-webbservern. Den här växeln styr om " +"webServer.tls.certFile och webServer.tls.keyFile genereras." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:509 msgid "Enable TCP stream multiplexing." -msgstr "" +msgstr "Aktivera TCP-strömmultiplexering." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:551 msgid "" "Enable TLS when communicating with frps. Since frp v0.50.0, default is true." msgstr "" +"Aktivera TLS vid kommunikation med frps. Sedan frp v0.50.0 är standardvärdet " +"sant." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:658 msgid "Enable or disable this proxy." -msgstr "" +msgstr "Aktivera eller inaktivera den här proxyn." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:478 msgid "Enable pprof" -msgstr "" +msgstr "Aktivera pprof" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:132 msgid "Enable service" @@ -350,7 +360,7 @@ msgstr "" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:427 msgid "Enable web HTTPS" -msgstr "" +msgstr "Aktivera HTTPS för webben" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:657 msgid "Enabled" @@ -358,7 +368,7 @@ msgstr "Aktiverad" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:699 msgid "Encrypt proxy traffic." -msgstr "" +msgstr "Kryptera proxytrafik." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:698 msgid "Encryption" @@ -370,23 +380,25 @@ msgstr "Miljövariabel" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1211 msgid "Environment variable must use KEY=value format." -msgstr "" +msgstr "Miljövariabeln måste använda formatet NYCKEL=värde." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:337 msgid "Environment variables passed to token source exec. Use KEY=value." -msgstr "" +msgstr "Miljövariabler som skickas till tokenkällans exec. Använd NYCKEL=värde." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:212 msgid "Exit frpc when the first login fails." -msgstr "" +msgstr "Avsluta frpc när den första inloggningen misslyckas." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:211 msgid "Exit when login fails" -msgstr "" +msgstr "Avsluta när inloggningen misslyckas" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:605 msgid "Experimental feature gates. Use key=value, for example VirtualNet=true." msgstr "" +"Experimentella funktionsgrindar. Använd nyckel=värde, till exempel " +"VirtualNet=true." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:185 msgid "" @@ -394,30 +406,33 @@ msgid "" "visitor sections. Use frp includes or per-proxy raw settings for proxy/" "visitor tables." msgstr "" +"Extra konfigurationsfragment på rotnivå som inkluderas före genererade " +"proxy- och besökarsektioner. Använd frp-inkluderingar eller råinställningar " +"per proxy för proxy-/besökartabeller." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:910 msgid "Fallback timeout in milliseconds for XTCP." -msgstr "" +msgstr "Reservtidsgräns i millisekunder för XTCP." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:909 msgid "Fallback timeout ms" -msgstr "" +msgstr "Reservtidsgräns i ms" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:905 msgid "Fallback to" -msgstr "" +msgstr "Reservalternativ" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:906 msgid "Fallback visitor name for XTCP." -msgstr "" +msgstr "Namn på reservbesökare för XTCP." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:604 msgid "Feature gates" -msgstr "" +msgstr "Funktionsgrindar" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1057 msgid "For virtual_net visitor plugin." -msgstr "" +msgstr "För insticksprogrammet virtual_net." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1470 msgid "General Settings" @@ -429,23 +444,23 @@ msgstr "Godkänn åtkomst till LuCi-appen frpc" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1472 msgid "HTTP / Domain" -msgstr "" +msgstr "HTTP / domän" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:830 msgid "HTTP health check headers. Use Header=Value." -msgstr "" +msgstr "HTTP-huvuden för hälsokontroll. Använd Huvud=Värde." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:811 msgid "HTTP health check path." -msgstr "" +msgstr "HTTP-sökväg för hälsokontroll." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:757 msgid "HTTP or TCPMUX basic auth password." -msgstr "" +msgstr "Lösenord för HTTP- eller TCPMUX-grundautentisering." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:754 msgid "HTTP or TCPMUX basic auth username." -msgstr "" +msgstr "Användarnamn för HTTP- eller TCPMUX-grundautentisering." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:756 #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:945 @@ -454,7 +469,7 @@ msgstr "Lösenord för HTTP" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1221 msgid "HTTP path must start with /." -msgstr "" +msgstr "HTTP-sökvägen måste börja med /." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:753 #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:941 @@ -463,47 +478,47 @@ msgstr "Användare för HTTP" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1475 msgid "Health Check" -msgstr "" +msgstr "Hälsokontroll" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:829 msgid "Health check headers" -msgstr "" +msgstr "Huvuden för hälsokontroll" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:806 msgid "Health check interval" -msgstr "" +msgstr "Intervall för hälsokontroll" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:807 msgid "Health check interval in seconds." -msgstr "" +msgstr "Intervall för hälsokontroll i sekunder." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:802 msgid "Health check max failed" -msgstr "" +msgstr "Maximalt antal misslyckade hälsokontroller" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:810 msgid "Health check path" -msgstr "" +msgstr "Sökväg för hälsokontroll" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:823 msgid "Health check path is required when health check type is http." -msgstr "" +msgstr "Sökväg för hälsokontroll krävs när typen av hälsokontroll är http." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:798 msgid "Health check timeout" -msgstr "" +msgstr "Tidsgräns för hälsokontroll" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:799 msgid "Health check timeout in seconds." -msgstr "" +msgstr "Tidsgräns för hälsokontroll i sekunder." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:794 msgid "Health check type" -msgstr "" +msgstr "Typ av hälsokontroll" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:795 msgid "Health check type." -msgstr "" +msgstr "Typ av hälsokontroll." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:540 msgid "Heartbeat interval" @@ -511,51 +526,51 @@ msgstr "Heartbeat-intervall" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:541 msgid "Heartbeat interval in seconds." -msgstr "" +msgstr "Hjärtslagsintervall i sekunder." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:544 msgid "Heartbeat timeout" -msgstr "" +msgstr "Tidsgräns för hjärtslag" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:545 msgid "Heartbeat timeout in seconds." -msgstr "" +msgstr "Tidsgräns för hjärtslag i sekunder." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:730 msgid "Host header rewrite" -msgstr "" +msgstr "Omskrivning av Host-huvud" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:235 msgid "Include config files" -msgstr "" +msgstr "Inkludera konfigurationsfiler" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:894 msgid "Keep XTCP tunnel open." -msgstr "" +msgstr "Håll XTCP-tunnel öppen." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:893 msgid "Keep tunnel open" -msgstr "" +msgstr "Håll tunnel öppen" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:521 msgid "Keepalive interval for TCP mux." -msgstr "" +msgstr "Keepalive-intervall för TCP-multiplexering." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1476 msgid "Load Balancer" -msgstr "" +msgstr "Lastbalanserare" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:773 msgid "Load balancer group" -msgstr "" +msgstr "Lastbalanseringsgrupp" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:786 msgid "Load balancer group is required when group key is set." -msgstr "" +msgstr "Lastbalanseringsgrupp krävs när gruppnyckeln är angiven." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:777 msgid "Load balancer group key" -msgstr "" +msgstr "Lastbalanseringsgruppnyckel" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:265 msgid "" @@ -563,6 +578,9 @@ msgid "" "exec runs a command and requires frp unsafe permission. Mutually exclusive " "with auth.token." msgstr "" +"Läs in token från en extern källa. File läser token från en lokal fil; exec " +"kör ett kommando och kräver frp:s osäkra behörighet. Kan inte användas " +"tillsammans med auth.token." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:669 msgid "Local IP" @@ -572,6 +590,8 @@ msgstr "Lokal IP-adress" msgid "" "Local IP bound when connecting to frps. Only valid for tcp, websocket or wss." msgstr "" +"Lokal IP-adress som binds vid anslutning till frps. Gäller endast tcp, " +"websocket eller wss." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:977 msgid "" @@ -579,14 +599,20 @@ msgid "" "and tls2raw, this is the local service address reached after frp terminates " "external TLS; for http2https/http2http, it is the local upstream address." msgstr "" +"Lokal serverdelsadress som används av brygginsticksprogram. För https2http, " +"https2https och tls2raw är detta den lokala tjänstadress som nås efter att " +"frp avslutar extern TLS; för http2https/http2http är det den lokala " +"uppströmsadressen." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:872 msgid "Local bind address for visitor." -msgstr "" +msgstr "Lokal bindningsadress för besökare." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:876 msgid "Local bind port for visitor. Use a negative value to avoid binding." msgstr "" +"Lokal bindningsport för besökare. Använd ett negativt värde för att undvika " +"bindning." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:673 msgid "Local port" @@ -600,7 +626,7 @@ msgstr "" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:674 msgid "Local service port." -msgstr "" +msgstr "Lokal tjänsteport." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:721 msgid "Locations" @@ -608,7 +634,7 @@ msgstr "Platser" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1441 msgid "Log Settings" -msgstr "" +msgstr "Logginställningar" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:397 msgid "Log level" @@ -616,15 +642,15 @@ msgstr "Loggnivå" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:401 msgid "Log max days" -msgstr "" +msgstr "Maximalt antal loggdagar" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:393 msgid "Log output" -msgstr "" +msgstr "Loggutdata" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:394 msgid "Log output path or console." -msgstr "" +msgstr "Sökväg för loggutdata eller konsol." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:152 msgid "Log stderr" @@ -636,27 +662,27 @@ msgstr "Logga stdout" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:897 msgid "Max retries per hour" -msgstr "" +msgstr "Maximalt antal återförsök per timme" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:402 msgid "Maximum number of days to keep logs." -msgstr "" +msgstr "Maximalt antal dagar som loggar ska behållas." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1477 msgid "Metadata" -msgstr "" +msgstr "Metadata" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:901 msgid "Min retry interval" -msgstr "" +msgstr "Minsta återförsöksintervall" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:902 msgid "Minimum XTCP retry interval." -msgstr "" +msgstr "Minsta intervall för XTCP-återförsök." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:767 msgid "Multiplexer" -msgstr "" +msgstr "Multiplexer" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:207 msgid "NAT hole STUN server" @@ -671,97 +697,103 @@ msgstr "KÖRS INTE" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:369 msgid "OIDC additional endpoint params" -msgstr "" +msgstr "Ytterligare OIDC-parametrar för ändpunkt" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:357 msgid "OIDC audience" -msgstr "" +msgstr "OIDC-målgrupp" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:349 msgid "OIDC client ID" -msgstr "" +msgstr "OIDC-klient-ID" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:353 msgid "OIDC client secret" -msgstr "" +msgstr "OIDC-klienthemlighet" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:383 msgid "OIDC insecure skip verify" -msgstr "" +msgstr "OIDC: osäker verifiering hoppas över" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:387 msgid "OIDC proxy URL" -msgstr "" +msgstr "OIDC-proxy-URL" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:361 msgid "OIDC scope" -msgstr "" +msgstr "OIDC-omfattning" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:365 msgid "OIDC token endpoint URL" -msgstr "" +msgstr "URL för OIDC-tokenändpunkt" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:379 msgid "OIDC trusted CA file" -msgstr "" +msgstr "OIDC-fil med betrott CA-certifikat" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:176 msgid "OS environments passed to frp for config file template, see %s." -msgstr "" +msgstr "OS-miljöer som skickas till frp för konfigurationsfilsmallen, se %s." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:228 msgid "Only start specified proxies or visitors. Empty means start all." msgstr "" +"Starta endast angivna proxyer eller besökare. Tomt värde betyder att alla " +"startas." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1241 msgid "Only the virtual_net plugin is valid for visitors." -msgstr "" +msgstr "Endast insticksprogrammet virtual_net är giltigt för besökare." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:722 msgid "Only valid for HTTP proxy." -msgstr "" +msgstr "Gäller endast för HTTP-proxy." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:850 msgid "" "Only visitors from specified users can connect. Use * to allow all users." msgstr "" +"Endast besökare från angivna användare får ansluta. Använd * för att tillåta " +"alla användare." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:194 msgid "Optional unique identifier for this frpc instance." -msgstr "" +msgstr "Valfri unik identifierare för den här frpc-instansen." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:281 msgid "Path of token file." -msgstr "" +msgstr "Sökväg till tokenfilen." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:563 msgid "Path to the TLS certificate file." -msgstr "" +msgstr "Sökväg till TLS-certifikatfilen." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:567 msgid "Path to the TLS private key file." -msgstr "" +msgstr "Sökväg till TLS-filen med privat nyckel." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:439 msgid "Path to the certificate file used by the HTTPS web server." -msgstr "" +msgstr "Sökväg till certifikatfilen som används av HTTPS-webbservern." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:457 msgid "Path to the private key file used by the HTTPS web server." -msgstr "" +msgstr "Sökväg till filen med privat nyckel som används av HTTPS-webbservern." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:571 msgid "Path to the trusted CA certificate file." -msgstr "" +msgstr "Sökväg till filen med betrott CA-certifikat." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:618 msgid "" "Persist runtime proxy and visitor configuration for Web UI or API management." msgstr "" +"Behåll körningskonfigurationen för proxy och besökare för hantering via " +"webbgränssnitt eller API." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1231 msgid "Please use KEY=value format." -msgstr "" +msgstr "Använd formatet NYCKEL=värde." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:919 #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1473 @@ -770,87 +802,89 @@ msgstr "Insticksmodul" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1025 msgid "Plugin HTTP/2" -msgstr "" +msgstr "HTTP/2 för insticksprogram" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:989 msgid "Plugin certificate path" -msgstr "" +msgstr "Sökväg till insticksprogramcertifikat" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1013 msgid "Plugin host header rewrite" -msgstr "" +msgstr "Omskrivning av Host-huvud för insticksprogram" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1001 msgid "Plugin key path" -msgstr "" +msgstr "Sökväg till insticksprogramnyckel" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:976 msgid "Plugin local address" -msgstr "" +msgstr "Lokal adress för insticksprogram" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1041 msgid "Plugin request headers" -msgstr "" +msgstr "Begärandehuvuden för insticksprogram" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:920 msgid "Plugin type." -msgstr "" +msgstr "Typ av insticksprogram." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1178 msgid "Port must be a number between 0 and 65535." -msgstr "" +msgstr "Porten måste vara ett tal mellan 0 och 65535." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1182 msgid "Port must be between 0 and 65535." -msgstr "" +msgstr "Porten måste vara mellan 0 och 65535." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1197 msgid "Port must be negative or a number between 1 and 65535." -msgstr "" +msgstr "Porten måste vara negativ eller ett tal mellan 1 och 65535." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1201 msgid "Port must be negative or between 1 and 65535." -msgstr "" +msgstr "Porten måste vara negativ eller mellan 1 och 65535." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:201 msgid "Port of the frps server. Default is 7000." -msgstr "" +msgstr "Port för frps-servern. Standardvärdet är 7000." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:205 msgid "Prefix for proxy names. If set, proxy names become {user}.{proxy}." -msgstr "" +msgstr "Prefix för proxynamn. Om det är angivet blir proxynamn {user}.{proxy}." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1459 msgid "Proxy / Visitor Settings" -msgstr "" +msgstr "Inställningar för proxy/besökare" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:500 msgid "Proxy URL" -msgstr "" +msgstr "Proxy-URL" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:388 msgid "Proxy URL for OIDC token endpoint." -msgstr "" +msgstr "Proxy-URL för OIDC-tokenändpunkten." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:501 msgid "Proxy URL used to connect to frps. Supports http, socks5 and ntlm." msgstr "" +"Proxy-URL som används för att ansluta till frps. Stöder http, socks5 och " +"ntlm." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1069 msgid "Proxy metadatas" -msgstr "" +msgstr "Proxymetadata" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:650 msgid "Proxy name" -msgstr "" +msgstr "Proxynamn" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:204 msgid "Proxy name prefix" -msgstr "" +msgstr "Prefix för proxynamn" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:706 msgid "Proxy protocol version" -msgstr "" +msgstr "Proxyprotokollversion" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:653 msgid "Proxy type" @@ -858,19 +892,19 @@ msgstr "Typ av proxy" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:654 msgid "Proxy type." -msgstr "" +msgstr "Proxytyp." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:590 msgid "QUIC keepalive period" -msgstr "" +msgstr "QUIC-keepalive-period" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:594 msgid "QUIC max idle timeout" -msgstr "" +msgstr "Högsta inaktivitetstid för QUIC" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:598 msgid "QUIC max incoming streams" -msgstr "" +msgstr "Maximalt antal inkommande QUIC-strömmar" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1400 msgid "RUNNING" @@ -883,26 +917,28 @@ msgstr "Fjärrport" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:678 msgid "Remote port listened by frps. If 0, frps assigns a random port." msgstr "" +"Fjärrport som frps lyssnar på. Om den är 0 tilldelar frps en slumpmässig " +"port." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:803 msgid "Remove proxy from frps after continuous failures." -msgstr "" +msgstr "Ta bort proxyn från frps efter kontinuerliga fel." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:733 msgid "Request headers" -msgstr "" +msgstr "Begärandehuvuden" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:165 msgid "Respawn when crashed" -msgstr "" +msgstr "Starta om efter krasch" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:742 msgid "Response headers" -msgstr "" +msgstr "Svarshuvuden" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:731 msgid "Rewrite Host header." -msgstr "" +msgstr "Skriv om Host-huvudet." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:841 msgid "Role" @@ -911,10 +947,12 @@ msgstr "Roll" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:763 msgid "Route HTTP or TCPMUX requests by HTTP basic auth user." msgstr "" +"Dirigera HTTP- eller TCPMUX-begäranden efter användare för HTTP-" +"grundautentisering." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:762 msgid "Route by HTTP user" -msgstr "" +msgstr "Dirigera efter HTTP-användare" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:163 msgid "Run daemon as group" @@ -926,27 +964,27 @@ msgstr "Kör demonen som användare" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:953 msgid "SOCKS5 password" -msgstr "" +msgstr "SOCKS5-lösenord" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:949 msgid "SOCKS5 user" -msgstr "" +msgstr "SOCKS5-användare" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1474 msgid "STCP / XTCP / SUDP" -msgstr "" +msgstr "STCP / XTCP / SUDP" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:208 msgid "STUN server used for NAT traversal." -msgstr "" +msgstr "STUN-server som används för NAT-traversering." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:845 msgid "Secret key" -msgstr "" +msgstr "Hemlig nyckel" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:846 msgid "Secret key used by STCP/XTCP/SUDP proxies and visitors." -msgstr "" +msgstr "Hemlig nyckel som används av STCP/XTCP/SUDP-proxyer och -besökare." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:196 msgid "Server address" @@ -958,11 +996,11 @@ msgstr "Namn på servern" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:865 msgid "Server name is required for visitors." -msgstr "" +msgstr "Servernamn krävs för besökare." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:575 msgid "Server name used for TLS verification." -msgstr "" +msgstr "Servernamn som används för TLS-verifiering." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:200 msgid "Server port" @@ -970,36 +1008,38 @@ msgstr "Port för server" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:858 msgid "Server proxy name to visit." -msgstr "" +msgstr "Serverproxynamn att besöka." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:853 msgid "Server user" -msgstr "" +msgstr "Serveranvändare" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:854 msgid "Server user for visitor." -msgstr "" +msgstr "Serveranvändare för besökare." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:734 msgid "Set request headers. Use Header=Value." -msgstr "" +msgstr "Ange begärandehuvuden. Använd Huvud=Värde." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:743 msgid "Set response headers. Use Header=Value." -msgstr "" +msgstr "Ange svarshuvuden. Använd Huvud=Värde." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:579 msgid "Since frp v0.50.0, default is true." -msgstr "" +msgstr "Sedan frp v0.50.0 är standardvärdet sant." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:384 msgid "" "Skip TLS verification for OIDC endpoint. Not recommended for production." msgstr "" +"Hoppa över TLS-verifiering för OIDC-ändpunkten. Rekommenderas inte för " +"produktion." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:227 msgid "Start proxies / visitors" -msgstr "" +msgstr "Starta proxyer/besökare" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1442 #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1452 @@ -1008,15 +1048,15 @@ msgstr "Inställningar för uppstart" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:968 msgid "Static file local path" -msgstr "" +msgstr "Lokal sökväg till statiska filer" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:972 msgid "Static file strip prefix" -msgstr "" +msgstr "Prefix som ska tas bort från sökväg till statiska filer" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:617 msgid "Store file path" -msgstr "" +msgstr "Sökväg till lagringsfil" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:716 msgid "Subdomain" @@ -1025,18 +1065,19 @@ msgstr "Underdomän" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:497 msgid "TCP keepalive interval in seconds. Negative value disables keepalive." msgstr "" +"TCP-keepalive-intervall i sekunder. Negativt värde inaktiverar keepalive." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:768 msgid "TCP multiplexer." -msgstr "" +msgstr "TCP-multiplexer." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:508 msgid "TCP mux" -msgstr "" +msgstr "TCP-multiplexering" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:520 msgid "TCP mux keepalive interval" -msgstr "" +msgstr "Keepalive-intervall för TCP-multiplexering" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:550 msgid "TLS" @@ -1044,37 +1085,33 @@ msgstr "TLS" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1438 msgid "TLS / QUIC" -msgstr "" +msgstr "TLS / QUIC" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:438 #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:562 msgid "TLS certificate path" -msgstr "" +msgstr "Sökväg till TLS-certifikat" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:450 msgid "TLS certificate path is required when web HTTPS is enabled." -msgstr "" +msgstr "Sökväg till TLS-certifikat krävs när HTTPS för webben är aktiverat." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:456 #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:566 msgid "TLS private key path" -msgstr "" +msgstr "Sökväg till TLS-privatnyckel" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:468 msgid "TLS private key path is required when web HTTPS is enabled." -msgstr "" +msgstr "Sökväg till TLS-privatnyckel krävs när HTTPS för webben är aktiverat." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:574 msgid "TLS server name" -msgstr "" +msgstr "TLS-servernamn" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:570 msgid "TLS trusted CA path" -msgstr "" -"TcpMux aktiverar eller inaktiverar multiplexering av TCP-strömmar. Det gör " -"att flera begäranden från en klient kan dela en enda TCP-anslutning. Om " -"värdet är true måste även servern ha TCP-multiplexering aktiverad.
Som " -"standard är värdet true." +msgstr "Sökväg till betrott TLS-CA" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:493 msgid "Timeout in seconds for connecting to frps." @@ -1089,151 +1126,155 @@ msgstr "Token" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:254 #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:274 msgid "Token and token source are mutually exclusive." -msgstr "" +msgstr "Token och tokenkälla kan inte användas tillsammans." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:303 msgid "Token source command" -msgstr "" +msgstr "Kommando för tokenkälla" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:326 msgid "Token source command arguments" -msgstr "" +msgstr "Kommandoargument för tokenkälla" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:320 msgid "Token source command is required when token source type is exec." -msgstr "" +msgstr "Kommandot för tokenkälla krävs när tokenkälltypen är exec." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:336 msgid "Token source environment" -msgstr "" +msgstr "Miljö för tokenkälla" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:280 msgid "Token source file" -msgstr "" +msgstr "Fil för tokenkälla" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:297 msgid "Token source file path is required when token source type is file." -msgstr "" +msgstr "Sökväg till fil för tokenkälla krävs när tokenkälltypen är file." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:264 msgid "Token source type" -msgstr "" +msgstr "Typ av tokenkälla" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:246 msgid "Token used for authentication." -msgstr "" +msgstr "Token som används för autentisering." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1471 msgid "Transport" -msgstr "" +msgstr "Transport" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1437 msgid "Transport Settings" -msgstr "" +msgstr "Transportinställningar" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:524 msgid "Transport protocol" -msgstr "" +msgstr "Transportprotokoll" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:525 msgid "Transport protocol used to connect to frps." -msgstr "" +msgstr "Transportprotokoll som används för att ansluta till frps." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:231 msgid "UDP packet size" -msgstr "" +msgstr "UDP-paketstorlek" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:232 msgid "UDP packet size in bytes. Should match frps. Default is 1500." msgstr "" +"UDP-paketstorlek i byte. Måste stämma överens med frps. Standardvärdet är " +"1500." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:957 msgid "Unix domain socket path" -msgstr "" +msgstr "Sökväg till Unix-domänsocket" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1042 msgid "Use Header=Value." -msgstr "" +msgstr "Använd Huvud=Värde." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:707 msgid "Use proxy protocol to transfer connection info to local service." msgstr "" +"Använd proxyprotokoll för att överföra anslutningsinformation till lokal " +"tjänst." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:614 msgid "Virtual network address. Requires VirtualNet feature gate." -msgstr "" +msgstr "Virtuell nätverksadress. Kräver funktionsgrinden VirtualNet." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:613 msgid "VirtualNet address" -msgstr "" +msgstr "VirtualNet-adress" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1056 msgid "VirtualNet destination IP" -msgstr "" +msgstr "VirtualNet-mål-IP" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1255 msgid "VirtualNet destination IP is required for virtual_net visitors." -msgstr "" +msgstr "VirtualNet-mål-IP krävs för virtual_net-besökare." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1439 msgid "Web Server" -msgstr "" +msgstr "Webbserver" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:411 msgid "Web server address" -msgstr "" +msgstr "Webbserveradress" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:475 msgid "Web server assets directory." -msgstr "" +msgstr "Resurskatalog för webbservern." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:412 msgid "Web server bind address." -msgstr "" +msgstr "Webbserverns bindningsadress." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:423 msgid "Web server password" -msgstr "" +msgstr "Webbserverlösenord" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:424 msgid "Web server password." -msgstr "" +msgstr "Webbserverlösenord." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:415 msgid "Web server port" -msgstr "" +msgstr "Webbserverport" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:416 msgid "Web server port. Leave empty or set to 0 to disable the web server." -msgstr "" +msgstr "Webbserverport. Lämna tomt eller ange 0 för att inaktivera webbservern." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:419 msgid "Web server user" -msgstr "" +msgstr "Webbserveranvändare" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:420 msgid "Web server username." -msgstr "" +msgstr "Webbserveranvändarnamn." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:695 msgid "Where to limit bandwidth." -msgstr "" +msgstr "Var bandbredd ska begränsas." #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:528 msgid "Wire protocol" -msgstr "" +msgstr "Trådprotokoll" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:889 msgid "XTCP protocol" -msgstr "" +msgstr "XTCP-protokoll" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:890 msgid "XTCP visitor tunnel protocol." -msgstr "" +msgstr "Protokoll för XTCP-besökartunnel." #: applications/luci-app-frpc/root/usr/share/luci/menu.d/luci-app-frpc.json:4 msgid "frp" -msgstr "" +msgstr "frp" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1403 #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:1410 @@ -1245,3 +1286,5 @@ msgid "" "frp internal wire protocol. v2 requires frps support and must be enabled " "explicitly." msgstr "" +"frp:s interna trådprotokoll. v2 kräver stöd i frps och måste aktiveras " +"uttryckligen." diff --git a/luci-app-frpc/po/zh_Hans/frpc.po b/luci-app-frpc/po/zh_Hans/frpc.po index caf68b1b9..27f3427d5 100644 --- a/luci-app-frpc/po/zh_Hans/frpc.po +++ b/luci-app-frpc/po/zh_Hans/frpc.po @@ -345,7 +345,7 @@ msgstr "启用 pprof" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:132 msgid "Enable service" -msgstr "" +msgstr "启用服务" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:427 msgid "Enable web HTTPS" diff --git a/luci-app-frpc/po/zh_Hant/frpc.po b/luci-app-frpc/po/zh_Hant/frpc.po index 4a80d6adb..02f9aa933 100644 --- a/luci-app-frpc/po/zh_Hant/frpc.po +++ b/luci-app-frpc/po/zh_Hant/frpc.po @@ -344,7 +344,7 @@ msgstr "啟用 pprof" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:132 msgid "Enable service" -msgstr "" +msgstr "啟用服務" #: applications/luci-app-frpc/htdocs/luci-static/resources/view/frpc.js:427 msgid "Enable web HTTPS" diff --git a/luci-app-frps/po/sv/frps.po b/luci-app-frps/po/sv/frps.po index b8afd5d1b..3fd2b6f25 100644 --- a/luci-app-frps/po/sv/frps.po +++ b/luci-app-frps/po/sv/frps.po @@ -1,14 +1,14 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-09-01 13:11+0000\n" -"Last-Translator: Martin Olmer \n" +"PO-Revision-Date: 2026-09-11 21:27+0000\n" +"Last-Translator: Daniel Nylander \n" "Language-Team: Swedish \n" "Language: sv\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 2026.9.dev0\n" +"X-Generator: Weblate 2026.10.dev0\n" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:1000 msgid "Access Control" @@ -16,11 +16,11 @@ msgstr "Åtkomstkontroll" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:1026 msgid "Add new HTTP plugin..." -msgstr "" +msgstr "Lägg till nytt HTTP-insticksprogram …" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:266 msgid "Additional auth scopes" -msgstr "" +msgstr "Ytterligare autentiseringsomfattningar" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:154 msgid "Additional configs" @@ -28,15 +28,15 @@ msgstr "Ytterligare konfigurationer" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:267 msgid "Additional scopes to include authentication information." -msgstr "" +msgstr "Ytterligare omfattningar som ska inkludera autentiseringsinformation." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:164 msgid "Address that frps binds to." -msgstr "" +msgstr "Adress som frps binder till." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:210 msgid "Address that proxy listeners bind to." -msgstr "" +msgstr "Adress som proxymottagare binder till." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:569 msgid "Allow ports" @@ -44,11 +44,11 @@ msgstr "Tillåt portar" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:459 msgid "Assets dir" -msgstr "" +msgstr "Resurskatalog" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:360 msgid "Audience OIDC tokens should contain." -msgstr "" +msgstr "Målgrupp som OIDC-token ska innehålla." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:996 msgid "Authentication" @@ -56,15 +56,15 @@ msgstr "Autentisering" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:247 msgid "Authentication method" -msgstr "" +msgstr "Autentiseringsmetod" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:248 msgid "Authentication method used to authenticate frpc." -msgstr "" +msgstr "Autentiseringsmetod som används för att autentisera frpc." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:601 msgid "Authorized keys file for SSH tunnel gateway." -msgstr "" +msgstr "Fil med auktoriserade nycklar för SSH-tunnelgatewayen." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:163 msgid "Bind address" @@ -76,21 +76,19 @@ msgstr "Bindningsport" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:987 msgid "Collecting data ..." -msgstr "Samlar in data ..." +msgstr "Samlar in data …" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:333 msgid "Command arguments passed to token source exec." -msgstr "" -"BindAddr anger adressen som servern binds till.
Som standard är värdet ”" -"0.0.0.0”." +msgstr "Kommandoargument som skickas till tokenkällans exec." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:310 msgid "" "Command used to obtain the token. The init script adds --allow-" "unsafe=TokenSourceExec when this source type is used." msgstr "" -"BindKcpPort anger KCP-porten som servern lyssnar på. Om värdet är 0 lyssnar " -"servern inte efter KCP-anslutningar.
Som standard är värdet 0." +"Kommando som används för att hämta token. Init-skriptet lägger till --allow-" +"unsafe=TokenSourceExec när den här källtypen används." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:995 msgid "Common Settings" @@ -98,34 +96,27 @@ msgstr "Vanliga inställningar" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:378 msgid "Configuration key: log.level." -msgstr "" -"BindPort anger porten som servern lyssnar på.
Som standard är värdet " -"7000." +msgstr "Konfigurationsnyckel: log.level." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:556 msgid "Configuration key: transport.quic.keepalivePeriod." -msgstr "" -"BindQuicPort anger QUIC-porten som servern lyssnar på. Om värdet är 0 " -"lyssnar servern inte efter QUIC-anslutningar.
Som standard är värdet 0." +msgstr "Konfigurationsnyckel: transport.quic.keepalivePeriod." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:560 msgid "Configuration key: transport.quic.maxIdleTimeout." -msgstr "" -"BindUdpPort anger UDP-porten som servern lyssnar på. Om värdet är 0 lyssnar " -"servern inte efter UDP-anslutningar.
Som standard är värdet 0." +msgstr "Konfigurationsnyckel: transport.quic.maxIdleTimeout." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:564 msgid "Configuration key: transport.quic.maxIncomingStreams." -msgstr "" -"Konfigurationsfiler som inkluderas i den tillfälliga konfigurationsfilen" +msgstr "Konfigurationsnyckel: transport.quic.maxIncomingStreams." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:237 msgid "Custom 404 page" -msgstr "" +msgstr "Anpassad 404-sida" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:517 msgid "Detailed errors to client" -msgstr "" +msgstr "Detaljerade fel till klienten" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:385 msgid "Disable log color" @@ -133,7 +124,7 @@ msgstr "Inaktivera logg-färg" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:386 msgid "Disable log colors when log output is console." -msgstr "" +msgstr "Inaktivera loggfärger när loggutdata går till konsolen." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:274 msgid "Disabled" @@ -147,31 +138,37 @@ msgstr "" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:234 msgid "Domain suffix for subdomain-based HTTP/HTTPS proxies." -msgstr "" +msgstr "Domänsuffix för HTTP/HTTPS-proxyer baserade på underdomäner." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:464 msgid "" "Enable Go pprof debug/profiling endpoints on the web server. It only takes " "effect when web server port is set." msgstr "" +"Aktivera Go pprof-slutpunkter för felsökning/profilering på webbservern. Det " +"får endast effekt när webbserverporten är angiven." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:413 msgid "" "Enable HTTPS for the frps web server. This switch controls whether " "webServer.tls.certFile and webServer.tls.keyFile are emitted." msgstr "" +"Aktivera HTTPS för frps-webbservern. Den här växeln styr om " +"webServer.tls.certFile och webServer.tls.keyFile genereras." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:473 msgid "Enable Prometheus" -msgstr "" +msgstr "Aktivera Prometheus" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:490 msgid "Enable TCP stream multiplexing. This setting must match frpc." msgstr "" +"Aktivera TCP-strömmultiplexering. Den här inställningen måste stämma överens " +"med frpc." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:463 msgid "Enable pprof" -msgstr "" +msgstr "Aktivera pprof" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:102 msgid "Enable service" @@ -179,7 +176,7 @@ msgstr "" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:412 msgid "Enable web HTTPS" -msgstr "" +msgstr "Aktivera HTTPS för webben" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:145 msgid "Environment variable" @@ -187,15 +184,11 @@ msgstr "Miljövariabel" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:773 msgid "Environment variable must use KEY=value format." -msgstr "" -"DashboardPort anger porten som instrumentpanelen lyssnar på. Om värdet är 0 " -"startas inte instrumentpanelen.
Som standard är värdet 0." +msgstr "Miljövariabeln måste använda formatet NYCKEL=värde." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:343 msgid "Environment variables passed to token source exec. Use KEY=value." -msgstr "" -"DashboardPwd anger lösenordet som instrumentpanelen använder för " -"inloggning.
Som standard är värdet ”admin”." +msgstr "Miljövariabler som skickas till tokenkällans exec. Använd NYCKEL=värde." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:474 msgid "" @@ -215,23 +208,23 @@ msgstr "" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:531 msgid "Force TLS" -msgstr "" +msgstr "Tvinga TLS" #: applications/luci-app-frps/root/usr/share/rpcd/acl.d/luci-app-frps.json:3 msgid "Grant access to LuCI app frps" -msgstr "" +msgstr "Ge åtkomst till LuCI-appen frps" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:1021 msgid "HTTP Plugin Settings" -msgstr "" +msgstr "Inställningar för HTTP-insticksprogram" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:799 msgid "HTTP plugin URL must include a host." -msgstr "" +msgstr "URL:en för HTTP-insticksprogrammet måste innehålla en värd." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:795 msgid "HTTP plugin address must be a single address." -msgstr "" +msgstr "Adressen till HTTP-insticksprogrammet måste vara en enda adress." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:807 msgid "HTTP plugin address must be host:port or an http:// or https:// URL." @@ -239,29 +232,31 @@ msgstr "" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:611 msgid "HTTP plugin backend address." -msgstr "" +msgstr "Serverdelsadress för HTTP-insticksprogrammet." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:815 msgid "" "HTTP plugin operation must be a frps operation name, for example Login or " "NewProxy." msgstr "" +"Åtgärden för HTTP-insticksprogrammet måste vara ett frps-åtgärdsnamn, till " +"exempel Login eller NewProxy." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:783 msgid "HTTP plugin path must start with /." -msgstr "" +msgstr "Sökvägen för HTTP-insticksprogrammet måste börja med /." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:622 msgid "HTTP plugin request path." -msgstr "" +msgstr "Sökväg för HTTP-insticksprogrammets begäran." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:509 msgid "Heartbeat timeout" -msgstr "" +msgstr "Tidsgräns för hjärtslag" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:510 msgid "Heartbeat timeout in seconds." -msgstr "" +msgstr "Tidsgräns för hjärtslag i sekunder." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:171 msgid "KCP bind port" @@ -270,11 +265,11 @@ msgstr "KCP-bindningsport" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:184 #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:203 msgid "KCP bind port and QUIC bind port must be different." -msgstr "" +msgstr "KCP-bindningsporten och QUIC-bindningsporten måste vara olika." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:502 msgid "Keepalive interval for TCP mux." -msgstr "" +msgstr "Keepalive-intervall för TCP-multiplexering." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:271 msgid "" @@ -282,10 +277,13 @@ msgid "" "exec runs a command and requires frp unsafe permission. Mutually exclusive " "with auth.token." msgstr "" +"Läs in token från en extern källa. File läser token från en lokal fil; exec " +"kör ett kommando och kräver frp:s osäkra behörighet. Kan inte användas " +"tillsammans med auth.token." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:1002 msgid "Log Settings" -msgstr "" +msgstr "Logginställningar" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:377 msgid "Log level" @@ -293,15 +291,15 @@ msgstr "Loggnivå" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:381 msgid "Log max days" -msgstr "" +msgstr "Maximalt antal loggdagar" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:373 msgid "Log output" -msgstr "" +msgstr "Loggutdata" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:374 msgid "Log output path or console." -msgstr "" +msgstr "Sökväg för loggutdata eller konsol." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:122 msgid "Log stderr" @@ -313,19 +311,19 @@ msgstr "Logga stdout" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:485 msgid "Max pool count" -msgstr "" +msgstr "Maximalt antal pooler" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:578 msgid "Max ports per client" -msgstr "" +msgstr "Maximalt antal portar per klient" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:382 msgid "Maximum number of days to keep logs." -msgstr "" +msgstr "Maximalt antal dagar som loggar ska behållas." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:486 msgid "Maximum pool count for each proxy." -msgstr "" +msgstr "Maximalt antal pooler för varje proxy." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:579 msgid "Maximum ports each client can use. 0 means no limit." @@ -336,7 +334,7 @@ msgstr "" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:514 msgid "Maximum time to wait for a work connection." -msgstr "" +msgstr "Längsta väntetid för en arbetsanslutning." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:582 msgid "NAT hole analysis reserve hours" @@ -351,141 +349,145 @@ msgstr "KÖRS INTE" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:359 msgid "OIDC audience" -msgstr "" +msgstr "OIDC-målgrupp" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:355 msgid "OIDC issuer" -msgstr "" +msgstr "OIDC-utfärdare" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:356 msgid "OIDC issuer used to verify tokens." -msgstr "" +msgstr "OIDC-utfärdare som används för att verifiera token." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:363 msgid "OIDC skip expiry check" -msgstr "" +msgstr "Hoppa över kontroll av OIDC-utgångstid" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:367 msgid "OIDC skip issuer check" -msgstr "" +msgstr "Hoppa över kontroll av OIDC-utfärdare" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:146 msgid "OS environments passed to frp for config file template, see %s." -msgstr "" +msgstr "OS-miljöer som skickas till frp för konfigurationsfilsmallen, se %s." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:532 msgid "Only accept TLS-encrypted frpc connections." -msgstr "" +msgstr "Acceptera endast TLS-krypterade frpc-anslutningar." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:597 msgid "Path for auto-generated private key." -msgstr "" +msgstr "Sökväg för automatiskt genererad privat nyckel." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:287 msgid "Path of token file." -msgstr "" +msgstr "Sökväg till tokenfilen." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:238 msgid "Path to custom 404 page for HTTP requests." -msgstr "" +msgstr "Sökväg till anpassad 404-sida för HTTP-begäranden." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:544 msgid "Path to the TLS certificate file." -msgstr "" +msgstr "Sökväg till TLS-certifikatfilen." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:548 msgid "Path to the TLS private key file." -msgstr "" +msgstr "Sökväg till TLS-filen med privat nyckel." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:424 msgid "Path to the certificate file used by the HTTPS web server." -msgstr "" +msgstr "Sökväg till certifikatfilen som används av HTTPS-webbservern." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:442 msgid "Path to the private key file used by the HTTPS web server." -msgstr "" +msgstr "Sökväg till filen med privat nyckel som används av HTTPS-webbservern." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:552 msgid "Path to the trusted CA certificate file." -msgstr "" +msgstr "Sökväg till filen med betrott CA-certifikat." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:610 msgid "Plugin address" -msgstr "" +msgstr "Insticksprogramadress" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:606 msgid "Plugin name" -msgstr "" +msgstr "Insticksprogramnamn" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:632 msgid "Plugin operations" -msgstr "" +msgstr "Insticksprogramåtgärder" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:621 msgid "Plugin path" -msgstr "" +msgstr "Insticksprogramsökväg" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:214 msgid "Port for HTTP virtual host requests. Empty or 0 disables it." msgstr "" +"Port för HTTP-begäranden till virtuella värdar. Tom eller 0 inaktiverar den." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:218 msgid "Port for HTTPS virtual host requests. Empty or 0 disables it." msgstr "" +"Port för HTTPS-begäranden till virtuella värdar. Tom eller 0 inaktiverar den." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:226 msgid "Port for TCPMUX HTTP CONNECT requests. Empty or 0 disables it." -msgstr "" +msgstr "Port för TCPMUX HTTP CONNECT-begäranden. Tom eller 0 inaktiverar den." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:737 msgid "Port must be a number between 0 and 65535." -msgstr "" +msgstr "Porten måste vara ett tal mellan 0 och 65535." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:741 msgid "Port must be between 0 and 65535." -msgstr "" +msgstr "Porten måste vara mellan 0 och 65535." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:760 msgid "Port must be between 1 and 65535." -msgstr "" +msgstr "Porten måste vara mellan 1 och 65535." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:763 msgid "Port range start must not be greater than end." -msgstr "" +msgstr "Starten för portintervallet får inte vara större än slutet." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:168 msgid "Port that frps listens on." -msgstr "" +msgstr "Port som frps lyssnar på." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:570 msgid "" "Ports clients are allowed to bind. Use single port like 3001 or range like " "2000-3000." msgstr "" +"Portar som klienter får binda till. Använd en enskild port, till exempel " +"3001, eller ett intervall som 2000–3000." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:593 msgid "Private key file for SSH tunnel gateway." -msgstr "" +msgstr "Fil med privat nyckel för SSH-tunnelgatewayen." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:209 msgid "Proxy bind address" -msgstr "" +msgstr "Bindningsadress för proxy" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:190 msgid "QUIC bind port" -msgstr "" +msgstr "QUIC-bindningsport" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:555 msgid "QUIC keepalive period" -msgstr "" +msgstr "QUIC-keepalive-period" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:559 msgid "QUIC max idle timeout" -msgstr "" +msgstr "Högsta inaktivitetstid för QUIC" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:563 msgid "QUIC max incoming streams" -msgstr "" +msgstr "Maximalt antal inkommande QUIC-strömmar" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:960 msgid "RUNNING" @@ -493,15 +495,15 @@ msgstr "KÖRS" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:135 msgid "Respawn when crashed" -msgstr "" +msgstr "Starta om efter krasch" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:222 msgid "Response header timeout for vhost HTTP server, in seconds." -msgstr "" +msgstr "Tidsgräns i sekunder för svarshuvud från HTTP-server för virtuell värd." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:583 msgid "Retention time for NAT hole punching strategy data." -msgstr "" +msgstr "Lagringstid för data om strategi för NAT-håltagning." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:133 msgid "Run daemon as group" @@ -513,31 +515,31 @@ msgstr "Kör demonen som användare" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:1001 msgid "SSH Tunnel" -msgstr "" +msgstr "SSH-tunnel" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:600 msgid "SSH tunnel authorized keys file" -msgstr "" +msgstr "Fil med auktoriserade SSH-tunnelnycklar" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:596 msgid "SSH tunnel auto-generated private key path" -msgstr "" +msgstr "Sökväg till automatiskt genererad privat nyckel för SSH-tunnel" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:588 msgid "SSH tunnel bind port" -msgstr "" +msgstr "Bindningsport för SSH-tunnel" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:589 msgid "SSH tunnel gateway bind port. Empty disables this feature." -msgstr "" +msgstr "Bindningsport för SSH-tunnelgateway. Tomt värde inaktiverar funktionen." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:592 msgid "SSH tunnel private key file" -msgstr "" +msgstr "Fil med privat SSH-tunnelnyckel" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:518 msgid "Send specific error details to frpc." -msgstr "" +msgstr "Skicka specifika feldetaljer till frpc." #: applications/luci-app-frps/root/usr/share/luci/menu.d/luci-app-frps.json:11 msgid "Server" @@ -546,10 +548,12 @@ msgstr "Server" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:368 msgid "Skip checking whether OIDC token issuer matches configured issuer." msgstr "" +"Hoppa över kontrollen av att OIDC-tokenutfärdaren motsvarar den " +"konfigurerade utfärdaren." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:364 msgid "Skip checking whether the OIDC token is expired." -msgstr "" +msgstr "Hoppa över kontrollen av om OIDC-token har löpt ut." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:1003 #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:1014 @@ -558,57 +562,57 @@ msgstr "Inställningar för uppstart" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:233 msgid "Subdomain host" -msgstr "" +msgstr "Värd för underdomäner" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:505 msgid "TCP keepalive" -msgstr "" +msgstr "TCP-keepalive" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:506 msgid "TCP keepalive interval. Negative value disables keepalive." -msgstr "" +msgstr "TCP-keepalive-intervall. Negativt värde inaktiverar keepalive." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:489 msgid "TCP mux" -msgstr "" +msgstr "TCP-multiplexering" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:501 msgid "TCP mux keepalive interval" -msgstr "" +msgstr "Keepalive-intervall för TCP-multiplexering" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:225 msgid "TCPMUX HTTP CONNECT port" -msgstr "" +msgstr "TCPMUX HTTP CONNECT-port" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:229 msgid "TCPMUX passthrough" -msgstr "" +msgstr "TCPMUX-genomsläpp" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:998 msgid "TLS / QUIC" -msgstr "" +msgstr "TLS / QUIC" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:423 #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:543 msgid "TLS certificate path" -msgstr "" +msgstr "Sökväg till TLS-certifikat" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:435 msgid "TLS certificate path is required when web HTTPS is enabled." -msgstr "" +msgstr "Sökväg till TLS-certifikat krävs när HTTPS för webben är aktiverat." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:441 #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:547 msgid "TLS private key path" -msgstr "" +msgstr "Sökväg till TLS-privatnyckel" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:453 msgid "TLS private key path is required when web HTTPS is enabled." -msgstr "" +msgstr "Sökväg till TLS-privatnyckel krävs när HTTPS för webben är aktiverat." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:551 msgid "TLS trusted CA path" -msgstr "" +msgstr "Sökväg till betrott TLS-CA" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:251 msgid "Token" @@ -617,149 +621,156 @@ msgstr "Tecken" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:260 #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:280 msgid "Token and token source are mutually exclusive." -msgstr "" +msgstr "Token och tokenkälla kan inte användas tillsammans." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:309 msgid "Token source command" -msgstr "" +msgstr "Kommando för tokenkälla" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:332 msgid "Token source command arguments" -msgstr "" +msgstr "Kommandoargument för tokenkälla" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:326 msgid "Token source command is required when token source type is exec." -msgstr "" +msgstr "Kommandot för tokenkälla krävs när tokenkälltypen är exec." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:342 msgid "Token source environment" -msgstr "" +msgstr "Miljö för tokenkälla" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:286 msgid "Token source file" -msgstr "" +msgstr "Fil för tokenkälla" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:303 msgid "Token source file path is required when token source type is file." -msgstr "" +msgstr "Sökväg till fil för tokenkälla krävs när tokenkälltypen är file." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:270 msgid "Token source type" -msgstr "" +msgstr "Typ av tokenkälla" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:252 msgid "Token used for authentication." -msgstr "" +msgstr "Token som används för autentisering." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:997 msgid "Transport Settings" -msgstr "" +msgstr "Transportinställningar" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:241 msgid "UDP packet size" -msgstr "" +msgstr "UDP-paketstorlek" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:242 msgid "UDP packet size in bytes. Should match frpc." -msgstr "" +msgstr "UDP-paketstorlek i byte. Måste stämma överens med frpc." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:172 msgid "" "UDP port used for KCP. Empty disables KCP. Do not reuse the same UDP port as " "QUIC." msgstr "" +"UDP-port som används för KCP. Tomt värde inaktiverar KCP. Återanvänd inte " +"samma UDP-port som QUIC." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:191 msgid "" "UDP port used for QUIC. Empty disables QUIC. Do not reuse the same UDP port " "as KCP." msgstr "" +"UDP-port som används för QUIC. Tomt värde inaktiverar QUIC. Återanvänd inte " +"samma UDP-port som KCP." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:607 msgid "Unique HTTP plugin name." -msgstr "" +msgstr "Unikt namn för HTTP-insticksprogram." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:754 msgid "Use a single port like 3001 or a range like 2000-3000." msgstr "" +"Använd en enskild port, till exempel 3001, eller ett intervall som 2000–3000." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:513 msgid "User connection timeout" -msgstr "" +msgstr "Tidsgräns för användaranslutning" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:644 msgid "Verify plugin TLS" -msgstr "" +msgstr "Verifiera TLS för insticksprogram" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:645 msgid "" "Verify the HTTP plugin TLS certificate when the plugin address uses HTTPS." msgstr "" +"Verifiera HTTP-insticksprogrammets TLS-certifikat när " +"insticksprogramadressen använder HTTPS." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:213 msgid "Vhost HTTP port" -msgstr "" +msgstr "HTTP-port för virtuell värd" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:221 msgid "Vhost HTTP timeout" -msgstr "" +msgstr "HTTP-tidsgräns för virtuell värd" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:217 msgid "Vhost HTTPS port" -msgstr "" +msgstr "HTTPS-port för virtuell värd" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:999 msgid "Web Server" -msgstr "" +msgstr "Webbserver" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:391 msgid "Web server address" -msgstr "" +msgstr "Webbserveradress" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:460 msgid "Web server assets directory." -msgstr "" +msgstr "Resurskatalog för webbservern." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:392 msgid "Web server bind address." -msgstr "" +msgstr "Webbserverns bindningsadress." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:405 msgid "Web server password" -msgstr "" +msgstr "Webbserverlösenord" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:406 msgid "Web server password." -msgstr "" +msgstr "Webbserverlösenord." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:395 msgid "Web server port" -msgstr "" +msgstr "Webbserverport" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:396 msgid "Web server port. Leave empty or set to 0 to disable the web server." -msgstr "" +msgstr "Webbserverport. Lämna tomt eller ange 0 för att inaktivera webbservern." #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:399 msgid "Web server user" -msgstr "" +msgstr "Webbserveranvändare" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:400 msgid "Web server username." -msgstr "" +msgstr "Webbserveranvändarnamn." #: applications/luci-app-frps/root/usr/share/luci/menu.d/luci-app-frps.json:4 msgid "frp" -msgstr "" +msgstr "frp" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:963 #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:970 msgid "frp Server" -msgstr "" +msgstr "frp-server" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:633 msgid "frps operations handled by this HTTP plugin." -msgstr "" +msgstr "frps-åtgärder som hanteras av detta HTTP-insticksprogram." #~ msgid "Advanced" #~ msgstr "Avancerat" diff --git a/luci-app-frps/po/zh_Hans/frps.po b/luci-app-frps/po/zh_Hans/frps.po index 681c86599..b6aa69728 100644 --- a/luci-app-frps/po/zh_Hans/frps.po +++ b/luci-app-frps/po/zh_Hans/frps.po @@ -171,7 +171,7 @@ msgstr "启用 pprof" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:102 msgid "Enable service" -msgstr "" +msgstr "启用服务" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:412 msgid "Enable web HTTPS" diff --git a/luci-app-frps/po/zh_Hant/frps.po b/luci-app-frps/po/zh_Hant/frps.po index b0fceeba2..b645b7531 100644 --- a/luci-app-frps/po/zh_Hant/frps.po +++ b/luci-app-frps/po/zh_Hant/frps.po @@ -170,7 +170,7 @@ msgstr "啟用 pprof" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:102 msgid "Enable service" -msgstr "" +msgstr "啟用服務" #: applications/luci-app-frps/htdocs/luci-static/resources/view/frps.js:412 msgid "Enable web HTTPS" diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index dbd3e0f36..be339b602 100644 --- a/luci-app-fwlive/Makefile +++ b/luci-app-fwlive/Makefile @@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter LUCI_PKGARCH:=all PKG_VERSION:=0.1.41 -PKG_RELEASE:=14 +PKG_RELEASE:=15 # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). ifdef SOURCE_DATE_EPOCH diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js index 3d7913648..668dfedbd 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js @@ -18,6 +18,13 @@ return baseclass.extend({ FETCH_LINES_MAX: 2000 /* ubus poll / logd ring cap (~2000 lines ≈ typical ring) */, /* DOM budget: ~250 new/updated rows painted per second on typical LuCI routers */ RENDER_CAP_PER_SEC: 250, + /* #306 Layer 2 — measured client RTT → poll cadence (seconds). */ + POLL_CADENCE_FAST_S: 1, + POLL_CADENCE_MID_S: 2, + POLL_CADENCE_SLOW_S: 5, + POLL_RTT_FAST_MS: 300, + POLL_RTT_SLOW_MS: 1500, + POLL_RTT_STREAK: 3, VIEW_MODES: ['simple', 'detailed'], COLUMN_SETS: { simple: ['action', 'time', 'iface', 'flow', 'proto', 'rule'], diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js index 16c8983e0..7bfcaffee 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js @@ -11,5 +11,5 @@ * GENERATED — do not edit. Edit fwlive.css and run: node scripts/embed-fwlive-css.js */ return baseclass.extend({ - styleText: "\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-display-bar-label {\n\tflex: 0 0 auto;\n\tfont-weight: 600;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-high);\n\twhite-space: nowrap;\n}\n.fwlive-display-controls {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmin-width: 0;\n\tflex: 1 1 auto;\n}\n.fwlive-display-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0;\n\tfont-size: 0.92em;\n\twhite-space: nowrap;\n}\n.fwlive-display-ctl input[type=\"checkbox\"] {\n\tmargin: 0;\n\tflex-shrink: 0;\n}\n.fwlive-display-ctl select {\n\tmargin-left: 0;\n}\n.fwlive-display-ctl.fwlive-hidden {\n\tdisplay: none;\n}\n/* Draw our own disclosure arrow — themes that restyle summary drop the native marker. */\n.fwlive-more-filters > summary,\n.fwlive-help > summary {\n\tcursor: pointer;\n\tlist-style: none;\n}\n.fwlive-more-filters > summary::-webkit-details-marker,\n.fwlive-help > summary::-webkit-details-marker { display: none; }\n.fwlive-more-filters > summary::before,\n.fwlive-help > summary::before {\n\tcontent: \"▸ \";\n\tcolor: var(--text-color-medium);\n}\n.fwlive-more-filters[open] > summary::before,\n.fwlive-help[open] > summary::before { content: \"▾ \"; }\n.fwlive-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.92em;\n\tflex-wrap: wrap;\n}\n.fwlive-ctl select { margin-left: 4px; }\n#fwlive-limit { width: auto; min-width: 4.5em; }\n.fwlive-grid { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px; }\n.fwlive-status { margin: 0; color: var(--text-color-medium); font-size: 0.9em; }\n.fwlive-status-paused { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-status-error { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-hint-line {\n\tmargin: 0 0 10px;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-empty-title {\n\tfont-weight: 600;\n\tmargin: 0 0 6px;\n\tcolor: var(--text-color-high);\n}\n.fwlive-find-row .fwlive-grid-core { margin-bottom: 0; }\n.fwlive-proto-pair {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);\n\tgap: 6px;\n\talign-items: stretch;\n\tmin-width: 0;\n}\n.fwlive-proto-pair select,\n.fwlive-proto-pair input {\n\twidth: 100%;\n\tmin-width: 0;\n}\n@media (max-width: 520px) {\n\t.fwlive-proto-pair { grid-template-columns: 1fr; }\n}\n.fwlive-scroll {\n\tmax-height: 78vh;\n\tmax-height: 78dvh;\n\toverflow: auto;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n\twidth: 100%;\n\tscrollbar-gutter: stable;\n}\n#fwlive-table { margin: 0; width: 100%; table-layout: auto; border-collapse: collapse; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table { table-layout: fixed; width: 100%; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-action { width: 5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-time { width: 5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-iface { width: 5.5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-proto { width: 4rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-rule { width: 12rem; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-scroll #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-time { width: 11rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-action { width: 4.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-rule { width: 8rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-in,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-out { width: 3.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dir { width: 3rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-proto { width: 3.25rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-src,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dst { width: 9rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-sport,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dport { width: 3.75rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-flags { width: 5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-len { width: 3.25rem; }\n.fwlive-scroll.fwlive-msg-oneline #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-scroll.fwlive-msg-oneline tbody td { vertical-align: middle; }\n#fwlive-table thead th {\n\tposition: sticky;\n\ttop: 0;\n\tz-index: 2;\n\tbackground: var(--background-color-low);\n\tborder-bottom: 2px solid var(--border-color-high);\n\twhite-space: nowrap;\n\tpadding: 5px 8px;\n\tfont-size: 0.9em;\n\tfont-weight: 600;\n\tvertical-align: top;\n\ttext-align: left;\n}\n#fwlive-table thead th.fwlive-port,\n#fwlive-table thead th.fwlive-len {\n\ttext-align: right;\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table thead th,\n.fwlive-map[data-view=\"simple\"] #fwlive-table tbody td {\n\tvertical-align: top;\n}\n#fwlive-table thead th.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n}\n#fwlive-table tbody td {\n\tpadding: 5px 8px;\n\tborder-bottom: 1px solid var(--border-color-low);\n\tvertical-align: top;\n\tfont-size: 0.92em;\n}\n.fwlive-row-alt td {\n\tbackground: #f9f9f9;\n\tbackground: var(--fwlive-bg-medium);\n}\n.fwlive-time, .fwlive-addr, .fwlive-port, .fwlive-len {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\twhite-space: nowrap;\n}\n.fwlive-port, .fwlive-len { text-align: right; }\n.fwlive-rule, .fwlive-iface, .fwlive-proto, .fwlive-dir, .fwlive-flags { white-space: nowrap; }\n.fwlive-rule { font-size: 0.88em; }\n.fwlive-rule-link {\n\tcolor: var(--primary-color-high);\n\ttext-decoration: none;\n\tfont-weight: 500;\n}\n.fwlive-rule-link:hover { text-decoration: underline; }\n.fwlive-iface-badge {\n\tdisplay: inline-block;\n\tpadding: 1px 6px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.85em;\n\ttext-decoration: none;\n}\n.fwlive-iface-badge:hover {\n\tbackground: var(--background-color-medium);\n\tborder-color: var(--primary-color-medium);\n}\n.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n\twhite-space: nowrap;\n}\n.fwlive-deny { color: var(--fwlive-deny-color); }\n.fwlive-pass { color: var(--fwlive-pass-color); }\n#fwlive-table td.fwlive-action.fwlive-pass,\n#fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n#fwlive-table td.fwlive-action.fwlive-deny,\n#fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n}\n.fwlive-unknown { color: var(--text-color-medium); font-weight: 500; }\n.fwlive-message {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-scroll.fwlive-msg-wrap .fwlive-message-wrap {\n\tdisplay: block;\n\tmin-width: 16em;\n\tmax-width: 42em;\n\twhite-space: normal;\n\tword-break: break-word;\n\tword-wrap: break-word;\n}\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twhite-space: nowrap;\n\tmax-width: none;\n\tmin-width: 32em;\n}\n#fwlive-table th.fwlive-th-message,\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twidth: 99%;\n}\n.fwlive-empty {\n\tmargin: 12px 0;\n\tpadding: 10px;\n\tbackground: var(--background-color-medium);\n\tborder: 1px dashed var(--border-color-high);\n}\n.fwlive-empty-muted {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin: 0 0 8px;\n}\n.fwlive-consent {\n\tmargin: 0 0 12px;\n\tpadding: 10px 12px;\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n}\n.fwlive-consent-list {\n\tmargin: 0 0 10px;\n\tpadding-left: 1.25em;\n}\n.fwlive-consent-list li {\n\tmargin: 0 0 4px;\n}\n.fwlive-consent-check {\n\tmargin: 0 0 10px;\n\tfont-size: 0.92em;\n}\n.fwlive-consent-actions {\n\tmargin: 0;\n}\n.fwlive-logging-bar {\n\tdisplay: contents;\n}\n.fwlive-logging-status {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin-right: 4px;\n}\n.fwlive-logging-notice {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-filter-link {\n\tcolor: inherit;\n\ttext-decoration: underline;\n\ttext-decoration-style: dotted;\n\tcursor: pointer;\n}\n.fwlive-filter-link:hover { color: var(--primary-color-high); }\n.fwlive-chips {\n\tdisplay: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0 0 10px;\n}\n.fwlive-chip-label {\n\tline-height: 1.3;\n}\n.fwlive-chip-not {\n\tfont-weight: 700;\n}\n.fwlive-chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tpadding: 2px 8px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.88em;\n}\n.fwlive-chip-negated {\n\tborder-style: dashed;\n\tbackground: var(--background-color-medium);\n}\n.fwlive-chip-include {\n}\n.fwlive-chip-polarity {\n\tfont-weight: 700;\n\tfont-size: 0.78em;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.03em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-chip-strike {\n\ttext-decoration: line-through;\n\ttext-decoration-thickness: 1.5px;\n}\n.fwlive-chip-sym {\n\tfont-weight: 700;\n\tline-height: 1;\n\tmin-width: 0.9em;\n\ttext-align: center;\n}\n.fwlive-chip-sym-light {\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.95em;\n}\n#fwlive-row-tint { width: auto; min-width: 9.5em; }\n/* labels (default) */\n.fwlive-chips-labels .fwlive-chip-include {\n\tborder-left: 3px solid var(--primary-color-medium, var(--primary-color-high, var(--border-color-high)));\n}\n.fwlive-chips-labels .fwlive-chip-negated .fwlive-chip-not {\n\tcolor: var(--text-color-high);\n}\n.fwlive-chip-invert-wrap {\n\tposition: relative;\n\tdisplay: inline-flex;\n}\n.fwlive-chip-invert-wrap::before {\n\tcontent: attr(data-tip);\n\tposition: absolute;\n\tbottom: calc(100% + 6px);\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\twhite-space: nowrap;\n\tpadding: 4px 8px;\n\tfont-size: 0.85em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-high);\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbox-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);\n\topacity: 0;\n\tvisibility: hidden;\n\tpointer-events: none;\n\tz-index: 20;\n\ttransition: opacity 0.15s ease 1.5s, visibility 0s linear 2s;\n}\n.fwlive-chip-invert-wrap:hover::before,\n.fwlive-chip-invert-wrap:focus-within::before {\n\topacity: 1;\n\tvisibility: visible;\n\ttransition: opacity 0.12s ease 0.35s, visibility 0s;\n}\n.fwlive-chip-invert {\n\tcolor: var(--text-color-medium);\n\tbackground: none;\n\tborder: none;\n\tpadding: 0;\n\tfont: inherit;\n\tfont-weight: 700;\n\tline-height: 1;\n\tcursor: pointer;\n}\n.fwlive-chip-invert:hover { color: var(--primary-color-high); }\n.fwlive-chip-remove {\n\tcolor: var(--text-color-medium);\n\ttext-decoration: none;\n\tfont-weight: 700;\n\tline-height: 1;\n}\n.fwlive-chip-remove:hover { color: var(--fwlive-deny-color); }\n.fwlive-chip-clear {\n\tfont-size: 0.88em;\n\tmargin-left: 4px;\n}\n.fwlive-flood {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-tint-warn {\n\tdisplay: none;\n\tcolor: var(--warn-color-high);\n\tfont-size: 0.78em;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\tpadding: 2px 8px;\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 999px;\n\tbackground: color-mix(in srgb, var(--warn-color-high) 10%, transparent);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-msg-group { display: none; }\n@media (max-width: 850px) {\n\t.fwlive-title-status {\n\t\tflex-basis: 100%;\n\t\tmargin-left: 0;\n\t}\n\t.fwlive-watch-group {\n\t\tborder-left: 0;\n\t\tpadding-left: 0;\n\t\tpadding-right: 10px;\n\t}\n\t.fwlive-display-bar {\n\t\talign-items: flex-start;\n\t}\n}\n.fwlive-help-row {\n\tdisplay: flex;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin: 10px 0 0;\n}\n.fwlive-help {\n\tmargin: 0;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmin-width: 0;\n\tflex: 1;\n}\n.fwlive-help ul { margin: 6px 0 0 1.2em; padding: 0; }\n.fwlive-help li { margin: 4px 0; }\n.fwlive-build {\n\tflex-shrink: 0;\n\tfont-size: 0.78em;\n\tcolor: var(--text-color-low, var(--text-color-medium));\n\tfont-variant-numeric: tabular-nums;\n\tuser-select: all;\n\twhite-space: nowrap;\n}\n.fwlive-backend {\n\tfont-size: 0.65em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-medium);\n\tmargin-left: 8px;\n\tvertical-align: middle;\n}\n.fwlive-backend.fwlive-backend-warn {\n\tcolor: var(--warn-color-high);\n\tfont-weight: 600;\n}\n.fwlive-filter-panel { margin-bottom: 12px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-core {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(3, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"simple\"] .fwlive-more-filters { margin-top: 8px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-extra {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n\tmargin-top: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters > summary { display: none; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-filter-panel {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-core,\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-extra,\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters { display: contents; }\n.fwlive-row-clickable { cursor: pointer; }\n.fwlive-row-expanded td { border-bottom-color: transparent; }\n.fwlive-msg-expand td {\n\tbackground: var(--background-color-low);\n\tpadding: 8px 12px 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n}\n.fwlive-msg-expand-label {\n\tfont-size: 0.82em;\n\tcolor: var(--text-color-medium);\n\tmargin-bottom: 4px;\n\tfont-weight: 600;\n}\n.fwlive-msg-expand-body {\n\tmargin: 0;\n\twhite-space: pre-wrap;\n\tword-break: break-word;\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-flow-arrow {\n\tcolor: var(--text-color-low);\n\tfont-weight: 700;\n}\n.fwlive-flow-cell { white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-iface-badge {\n\tborder-radius: 10px;\n\tpadding: 2px 8px;\n}\n" + styleText: "\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-display-bar-label {\n\tflex: 0 0 auto;\n\tfont-weight: 600;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-high);\n\twhite-space: nowrap;\n}\n.fwlive-display-controls {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmin-width: 0;\n\tflex: 1 1 auto;\n}\n.fwlive-display-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0;\n\tfont-size: 0.92em;\n\twhite-space: nowrap;\n}\n.fwlive-display-ctl input[type=\"checkbox\"] {\n\tmargin: 0;\n\tflex-shrink: 0;\n}\n.fwlive-display-ctl select {\n\tmargin-left: 0;\n}\n.fwlive-display-ctl.fwlive-hidden {\n\tdisplay: none;\n}\n/* Draw our own disclosure arrow — themes that restyle summary drop the native marker. */\n.fwlive-more-filters > summary,\n.fwlive-help > summary {\n\tcursor: pointer;\n\tlist-style: none;\n}\n.fwlive-more-filters > summary::-webkit-details-marker,\n.fwlive-help > summary::-webkit-details-marker { display: none; }\n.fwlive-more-filters > summary::before,\n.fwlive-help > summary::before {\n\tcontent: \"▸ \";\n\tcolor: var(--text-color-medium);\n}\n.fwlive-more-filters[open] > summary::before,\n.fwlive-help[open] > summary::before { content: \"▾ \"; }\n.fwlive-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.92em;\n\tflex-wrap: wrap;\n}\n.fwlive-ctl select { margin-left: 4px; }\n#fwlive-limit { width: auto; min-width: 4.5em; }\n.fwlive-grid { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px; }\n.fwlive-status { margin: 0; color: var(--text-color-medium); font-size: 0.9em; }\n.fwlive-status-paused { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-status-error { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-hint-line {\n\tmargin: 0 0 10px;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-empty-title {\n\tfont-weight: 600;\n\tmargin: 0 0 6px;\n\tcolor: var(--text-color-high);\n}\n.fwlive-find-row .fwlive-grid-core { margin-bottom: 0; }\n.fwlive-proto-pair {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);\n\tgap: 6px;\n\talign-items: stretch;\n\tmin-width: 0;\n}\n.fwlive-proto-pair select,\n.fwlive-proto-pair input {\n\twidth: 100%;\n\tmin-width: 0;\n}\n@media (max-width: 520px) {\n\t.fwlive-proto-pair { grid-template-columns: 1fr; }\n}\n.fwlive-scroll {\n\tmax-height: 78vh;\n\tmax-height: 78dvh;\n\toverflow: auto;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n\twidth: 100%;\n\tscrollbar-gutter: stable;\n}\n#fwlive-table { margin: 0; width: 100%; table-layout: auto; border-collapse: collapse; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table { table-layout: fixed; width: 100%; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-action { width: 5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-time { width: 5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-iface { width: 5.5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-proto { width: 4rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-rule { width: 12rem; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-scroll #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-time { width: 11rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-action { width: 4.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-rule { width: 8rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-in,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-out { width: 3.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dir { width: 3rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-proto { width: 3.25rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-src,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dst { width: 9rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-sport,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dport { width: 3.75rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-flags { width: 5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-len { width: 3.25rem; }\n.fwlive-scroll.fwlive-msg-oneline #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-scroll.fwlive-msg-oneline tbody td { vertical-align: middle; }\n#fwlive-table thead th {\n\tposition: sticky;\n\ttop: 0;\n\tz-index: 2;\n\tbackground: var(--background-color-low);\n\tborder-bottom: 2px solid var(--border-color-high);\n\twhite-space: nowrap;\n\tpadding: 5px 8px;\n\tfont-size: 0.9em;\n\tfont-weight: 600;\n\tvertical-align: top;\n\ttext-align: left;\n}\n#fwlive-table thead th.fwlive-port,\n#fwlive-table thead th.fwlive-len {\n\ttext-align: right;\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table thead th,\n.fwlive-map[data-view=\"simple\"] #fwlive-table tbody td {\n\tvertical-align: top;\n}\n#fwlive-table thead th.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n}\n#fwlive-table tbody td {\n\tpadding: 5px 8px;\n\tborder-bottom: 1px solid var(--border-color-low);\n\tvertical-align: top;\n\tfont-size: 0.92em;\n}\n.fwlive-row-alt td {\n\tbackground: #f9f9f9;\n\tbackground: var(--fwlive-bg-medium);\n}\n.fwlive-time, .fwlive-addr, .fwlive-port, .fwlive-len {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\twhite-space: nowrap;\n}\n.fwlive-port, .fwlive-len { text-align: right; }\n.fwlive-rule, .fwlive-iface, .fwlive-proto, .fwlive-dir, .fwlive-flags { white-space: nowrap; }\n.fwlive-rule { font-size: 0.88em; }\n.fwlive-rule-link {\n\tcolor: var(--primary-color-high);\n\ttext-decoration: none;\n\tfont-weight: 500;\n}\n.fwlive-rule-link:hover { text-decoration: underline; }\n.fwlive-iface-badge {\n\tdisplay: inline-block;\n\tpadding: 1px 6px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.85em;\n\ttext-decoration: none;\n}\n.fwlive-iface-badge:hover {\n\tbackground: var(--background-color-medium);\n\tborder-color: var(--primary-color-medium);\n}\n.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n\twhite-space: nowrap;\n}\n.fwlive-deny { color: var(--fwlive-deny-color); }\n.fwlive-pass { color: var(--fwlive-pass-color); }\n#fwlive-table td.fwlive-action.fwlive-pass,\n#fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n#fwlive-table td.fwlive-action.fwlive-deny,\n#fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n}\n.fwlive-unknown { color: var(--text-color-medium); font-weight: 500; }\n.fwlive-message {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-scroll.fwlive-msg-wrap .fwlive-message-wrap {\n\tdisplay: block;\n\tmin-width: 16em;\n\tmax-width: 42em;\n\twhite-space: normal;\n\tword-break: break-word;\n\tword-wrap: break-word;\n}\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twhite-space: nowrap;\n\tmax-width: none;\n\tmin-width: 32em;\n}\n#fwlive-table th.fwlive-th-message,\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twidth: 99%;\n}\n.fwlive-empty {\n\tmargin: 12px 0;\n\tpadding: 10px;\n\tbackground: var(--background-color-medium);\n\tborder: 1px dashed var(--border-color-high);\n}\n.fwlive-empty-muted {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin: 0 0 8px;\n}\n.fwlive-consent {\n\tmargin: 0 0 12px;\n\tpadding: 10px 12px;\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n}\n.fwlive-consent-list {\n\tmargin: 0 0 10px;\n\tpadding-left: 1.25em;\n}\n.fwlive-consent-list li {\n\tmargin: 0 0 4px;\n}\n.fwlive-consent-check {\n\tmargin: 0 0 10px;\n\tfont-size: 0.92em;\n}\n.fwlive-consent-actions {\n\tmargin: 0;\n}\n.fwlive-logging-bar {\n\tdisplay: contents;\n}\n.fwlive-logging-status {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin-right: 4px;\n}\n.fwlive-logging-notice {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-filter-link {\n\tcolor: inherit;\n\ttext-decoration: underline;\n\ttext-decoration-style: dotted;\n\tcursor: pointer;\n}\n.fwlive-filter-link:hover { color: var(--primary-color-high); }\n.fwlive-chips {\n\tdisplay: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0 0 10px;\n}\n.fwlive-chip-label {\n\tline-height: 1.3;\n}\n.fwlive-chip-not {\n\tfont-weight: 700;\n}\n.fwlive-chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tpadding: 2px 8px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.88em;\n}\n.fwlive-chip-negated {\n\tborder-style: dashed;\n\tbackground: var(--background-color-medium);\n}\n.fwlive-chip-include {\n}\n.fwlive-chip-polarity {\n\tfont-weight: 700;\n\tfont-size: 0.78em;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.03em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-chip-strike {\n\ttext-decoration: line-through;\n\ttext-decoration-thickness: 1.5px;\n}\n.fwlive-chip-sym {\n\tfont-weight: 700;\n\tline-height: 1;\n\tmin-width: 0.9em;\n\ttext-align: center;\n}\n.fwlive-chip-sym-light {\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.95em;\n}\n#fwlive-row-tint { width: auto; min-width: 9.5em; }\n/* labels (default) */\n.fwlive-chips-labels .fwlive-chip-include {\n\tborder-left: 3px solid var(--primary-color-medium, var(--primary-color-high, var(--border-color-high)));\n}\n.fwlive-chips-labels .fwlive-chip-negated .fwlive-chip-not {\n\tcolor: var(--text-color-high);\n}\n.fwlive-chip-invert-wrap {\n\tposition: relative;\n\tdisplay: inline-flex;\n}\n.fwlive-chip-invert-wrap::before {\n\tcontent: attr(data-tip);\n\tposition: absolute;\n\tbottom: calc(100% + 6px);\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\twhite-space: nowrap;\n\tpadding: 4px 8px;\n\tfont-size: 0.85em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-high);\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbox-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);\n\topacity: 0;\n\tvisibility: hidden;\n\tpointer-events: none;\n\tz-index: 20;\n\ttransition: opacity 0.15s ease 1.5s, visibility 0s linear 2s;\n}\n.fwlive-chip-invert-wrap:hover::before,\n.fwlive-chip-invert-wrap:focus-within::before {\n\topacity: 1;\n\tvisibility: visible;\n\ttransition: opacity 0.12s ease 0.35s, visibility 0s;\n}\n.fwlive-chip-invert {\n\tcolor: var(--text-color-medium);\n\tbackground: none;\n\tborder: none;\n\tpadding: 0;\n\tfont: inherit;\n\tfont-weight: 700;\n\tline-height: 1;\n\tcursor: pointer;\n}\n.fwlive-chip-invert:hover { color: var(--primary-color-high); }\n.fwlive-chip-remove {\n\tcolor: var(--text-color-medium);\n\ttext-decoration: none;\n\tfont-weight: 700;\n\tline-height: 1;\n}\n.fwlive-chip-remove:hover { color: var(--fwlive-deny-color); }\n.fwlive-chip-clear {\n\tfont-size: 0.88em;\n\tmargin-left: 4px;\n}\n.fwlive-flood {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-adaptive {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-tint-warn {\n\tdisplay: none;\n\tcolor: var(--warn-color-high);\n\tfont-size: 0.78em;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\tpadding: 2px 8px;\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 999px;\n\tbackground: color-mix(in srgb, var(--warn-color-high) 10%, transparent);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-msg-group { display: none; }\n@media (max-width: 850px) {\n\t.fwlive-title-status {\n\t\tflex-basis: 100%;\n\t\tmargin-left: 0;\n\t}\n\t.fwlive-watch-group {\n\t\tborder-left: 0;\n\t\tpadding-left: 0;\n\t\tpadding-right: 10px;\n\t}\n\t.fwlive-display-bar {\n\t\talign-items: flex-start;\n\t}\n}\n.fwlive-help-row {\n\tdisplay: flex;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin: 10px 0 0;\n}\n.fwlive-help {\n\tmargin: 0;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmin-width: 0;\n\tflex: 1;\n}\n.fwlive-help ul { margin: 6px 0 0 1.2em; padding: 0; }\n.fwlive-help li { margin: 4px 0; }\n.fwlive-build {\n\tflex-shrink: 0;\n\tfont-size: 0.78em;\n\tcolor: var(--text-color-low, var(--text-color-medium));\n\tfont-variant-numeric: tabular-nums;\n\tuser-select: all;\n\twhite-space: nowrap;\n}\n.fwlive-backend {\n\tfont-size: 0.65em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-medium);\n\tmargin-left: 8px;\n\tvertical-align: middle;\n}\n.fwlive-backend.fwlive-backend-warn {\n\tcolor: var(--warn-color-high);\n\tfont-weight: 600;\n}\n.fwlive-filter-panel { margin-bottom: 12px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-core {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(3, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"simple\"] .fwlive-more-filters { margin-top: 8px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-extra {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n\tmargin-top: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters > summary { display: none; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-filter-panel {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-core,\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-extra,\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters { display: contents; }\n.fwlive-row-clickable { cursor: pointer; }\n.fwlive-row-expanded td { border-bottom-color: transparent; }\n.fwlive-msg-expand td {\n\tbackground: var(--background-color-low);\n\tpadding: 8px 12px 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n}\n.fwlive-msg-expand-label {\n\tfont-size: 0.82em;\n\tcolor: var(--text-color-medium);\n\tmargin-bottom: 4px;\n\tfont-weight: 600;\n}\n.fwlive-msg-expand-body {\n\tmargin: 0;\n\twhite-space: pre-wrap;\n\tword-break: break-word;\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-flow-arrow {\n\tcolor: var(--text-color-low);\n\tfont-weight: 700;\n}\n.fwlive-flow-cell { white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-iface-badge {\n\tborder-radius: 10px;\n\tpadding: 2px 8px;\n}\n" }); diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css index ea0d9913e..aa1274a39 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css @@ -660,6 +660,16 @@ color: var(--text-color-high); font-size: 0.92em; } +.fwlive-adaptive { + display: none; + margin: 0 0 10px; + padding: 8px 12px; + background: var(--background-color-low); + border: 1px solid var(--border-color-medium); + border-radius: 3px; + color: var(--text-color-high); + font-size: 0.92em; +} .fwlive-tint-warn { display: none; color: var(--warn-color-high); diff --git a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js index 901c112a9..13d795529 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js @@ -35,8 +35,8 @@ const callFwliveRules = rpc.declare({ const callFwliveResolve = rpc.declare({ object: 'fwlive', method: 'resolve', - params: ['addresses'], - expect: { names: {} } + params: ['addresses'] + /* Full reply kept so disabled:"load" reaches the view (#306 Layer 2). */ }); const callFwliveLoggingStatus = rpc.declare({ @@ -102,12 +102,25 @@ return view.extend({ resumeMerge: false, pollFn: null, pollDataInFlight: false, + /* #306 Layer 2 — visibility / RTT cadence / shed surfacing. */ + pollEpoch: 0, + pollCadenceSec: 1, + rttStreakKind: null, + rttStreakCount: 0, + serverAdaptive: 1, + serverTruncated: 0, + serverShed: null, + degradedSampling: false, + resolveLoadShed: false, + visibilityBound: false, + renderRaf: 0, filterInputTimer: null, messageLayout: 'wrap', renderBucket: constants.RENDER_CAP_PER_SEC, renderBucketMs: 0, floodSuppressed: false, pendingForceRender: false, + pendingRenderEpoch: null, lastPollNewEvents: 0, showHostnames: false, rowTint: constants.DEFAULT_ROW_TINT, @@ -702,7 +715,11 @@ return view.extend({ async fetchEntries() { if (!this.sessionSeen) this.sessionSeen = new Set(); + const epoch = this.pollEpoch; + const resumeMerge = !!this.resumeMerge; + const t0 = this.nowMs(); let reply; + let errored = false; try { /* Raw logd lines, not post-filter rows. Fetch a multiple of the * display limit so mixed syslog still fills the table; pause @@ -714,25 +731,44 @@ return view.extend({ addresses: [String(fetchLines)] }); } catch (e) { - this.lastPollError = true; - return; + errored = true; + reply = null; } - /* Full poll object (no rpc expect strip). error → banner; log → rows. */ + + if (epoch !== this.pollEpoch) return; + + const rtt = this.nowMs() - t0; + if (!reply || typeof reply !== 'object' || Array.isArray(reply)) { this.lastPollError = true; + this.notePollRtt(rtt, true); + this.updateAdaptiveBanner(); return; } if (reply.error) { this.lastPollError = true; + this.notePollRtt(rtt, true); + this.updateAdaptiveBanner(); return; } const raw = reply.log; if (!Array.isArray(raw)) { this.lastPollError = true; + this.notePollRtt(rtt, true); + this.updateAdaptiveBanner(); return; } this.lastPollError = false; + this.serverAdaptive = reply.adaptive === 0 || reply.adaptive === false ? 0 : 1; + this.serverTruncated = reply.truncated ? 1 : 0; + this.serverShed = + reply.shed && typeof reply.shed === 'object' && !Array.isArray(reply.shed) + ? reply.shed + : null; + + this.notePollRtt(rtt, errored); + this.updateAdaptiveBanner(); const batch = this.normalizePollBatch(raw); this.lastPollNewEvents = batch.pollNew; @@ -740,10 +776,13 @@ return view.extend({ /* Oldest-first ring buffer; filteredRows() reverses for newest-first display. */ this.entries = buffer.applyFetchedEntries(this.entries, batch.rows, { paused: this.paused, - resumeMerge: this.resumeMerge, + resumeMerge: resumeMerge, rowLimit: this.rowLimit, fetchLinesMax: constants.FETCH_LINES_MAX }); + /* A stale request returns above. Keep this obligation until a current + * request has actually applied the merged batch. */ + if (resumeMerge) this.resumeMerge = false; }, rememberSessionId(id) { @@ -772,11 +811,217 @@ return view.extend({ const cap = this.ingestCap(); if (this.entries.length >= cap && cap > 0) bits.push(_('buffer full')); if (this.floodSuppressed) bits.push(_('render paused (high rate)')); + if (this.degradedSampling && this.serverAdaptive !== 0) bits.push(_('Degraded — sampling')); + if (this.serverTruncated && this.serverAdaptive !== 0) bits.push(_('truncated')); + if (this.resolveLoadShed && this.serverAdaptive !== 0) + bits.push(_('resolve paused (load)')); if (!this.paused && !this.followLive) bits.push(_('scroll frozen — scroll to top to follow live')); return bits.length ? ' — ' + bits.join(', ') : ''; }, + nowMs() { + if (typeof performance !== 'undefined' && performance.now) return performance.now(); + return Date.now(); + }, + + isTabHidden() { + return typeof document !== 'undefined' && !!document.hidden; + }, + + /* Classify one RTT sample into hysteresis buckets (#306). + * error and >1.5s share the slow/degraded streak so mixed failures still trip N=3. */ + rttKindFromMs(ms, errored) { + if (errored) return 'slow'; + if (!(ms >= 0)) return 'slow'; + if (ms < constants.POLL_RTT_FAST_MS) return 'fast'; + if (ms <= constants.POLL_RTT_SLOW_MS) return 'mid'; + return 'slow'; + }, + + cadenceForKind(kind) { + if (kind === 'fast') return constants.POLL_CADENCE_FAST_S; + if (kind === 'mid') return constants.POLL_CADENCE_MID_S; + return constants.POLL_CADENCE_SLOW_S; + }, + + resetRttHistory() { + this.rttStreakKind = null; + this.rttStreakCount = 0; + }, + + bumpPollEpoch() { + this.pollEpoch = (this.pollEpoch || 0) + 1; + return this.pollEpoch; + }, + + clientBackoffEnabled() { + return this.serverAdaptive !== 0; + }, + + setPollCadence(sec) { + const next = sec > 0 ? sec : constants.POLL_CADENCE_FAST_S; + if (!this.pollFn) { + this.pollCadenceSec = next; + return; + } + if (this.isTabHidden()) { + this.pollCadenceSec = next; + return; + } + try { + poll.remove(this.pollFn); + } catch (e) { + /* poll gone */ + } + this.pollCadenceSec = next; + try { + poll.add(this.pollFn, next); + } catch (e) { + /* poll gone */ + } + }, + + notePollRtt(ms, errored) { + if (!this.clientBackoffEnabled()) { + this.degradedSampling = false; + /* Drop any partial streak so a slow sample from before the + * adaptive:0 window cannot trip degraded on re-enable. */ + this.resetRttHistory(); + if (this.pollCadenceSec !== constants.POLL_CADENCE_FAST_S) + this.setPollCadence(constants.POLL_CADENCE_FAST_S); + return; + } + + const kind = this.rttKindFromMs(ms, errored); + if (kind === this.rttStreakKind) this.rttStreakCount++; + else { + this.rttStreakKind = kind; + this.rttStreakCount = 1; + } + + if (this.rttStreakCount < constants.POLL_RTT_STREAK) return; + + const cadence = this.cadenceForKind(kind); + this.degradedSampling = cadence === constants.POLL_CADENCE_SLOW_S; + if (cadence !== this.pollCadenceSec) this.setPollCadence(cadence); + }, + + stopPollingForHidden() { + if (!this.pollFn) return; + try { + poll.remove(this.pollFn); + } catch (e) { + /* poll gone */ + } + }, + + resumePollingAfterVisible() { + this.bumpPollEpoch(); + this.resetRttHistory(); + /* In-flight poll from the hidden epoch must not apply after catch-up. */ + this.pollDataInFlight = false; + if (!this.pollFn) this.pollFn = this.pollData.bind(this); + this.setPollCadence(this.pollCadenceSec || constants.POLL_CADENCE_FAST_S); + this.pollData(); + }, + + onVisibilityChange() { + if (this.isTabHidden()) { + this.stopPollingForHidden(); + this.bumpPollEpoch(); + return; + } + this.resumePollingAfterVisible(); + }, + + bindVisibility() { + if (this.visibilityBound) return; + if (typeof document === 'undefined' || !document.addEventListener) return; + this.visibilityBound = true; + this.visibilityHandler = function () { + this.onVisibilityChange(); + }.bind(this); + document.addEventListener('visibilitychange', this.visibilityHandler); + }, + + unbindVisibility() { + if ( + typeof document !== 'undefined' && + document.removeEventListener && + this.visibilityHandler + ) { + try { + document.removeEventListener('visibilitychange', this.visibilityHandler); + } catch (e) { + /* document gone */ + } + } + this.visibilityHandler = null; + this.visibilityBound = false; + }, + + updateAdaptiveBanner() { + const el = document.getElementById('fwlive-adaptive'); + if (!el) return; + if (!el.style) el.style = { display: '' }; + + if (!this.clientBackoffEnabled()) { + el.style.display = 'none'; + el.textContent = ''; + return; + } + + const parts = []; + if (this.degradedSampling) + parts.push(_('Degraded — sampling (slow poll RTT; cadence reduced).')); + if (this.serverShed && this.serverShed.limit) + parts.push( + _('Server shedding — at most %d log lines per poll.').format(this.serverShed.limit) + ); + else if (this.serverTruncated) parts.push(_('Server truncated this poll (adaptive cap).')); + if (this.resolveLoadShed) + parts.push(_('Hostname resolve paused while the router is under load.')); + + if (parts.length) { + el.style.display = 'block'; + el.textContent = parts.join(' '); + } else { + el.style.display = 'none'; + el.textContent = ''; + } + }, + + scheduleRenderRows(force) { + const doForce = !!force; + const epoch = this.pollEpoch; + if (typeof requestAnimationFrame !== 'function') { + this.renderRows(doForce); + return; + } + if (this.renderRaf) { + this.pendingForceRender = this.pendingForceRender || doForce; + this.pendingRenderEpoch = epoch; + return; + } + this.renderRaf = requestAnimationFrame( + function () { + this.renderRaf = 0; + const f = doForce || !!this.pendingForceRender; + const pendingEpoch = this.pendingRenderEpoch; + this.pendingForceRender = false; + this.pendingRenderEpoch = null; + if (epoch !== this.pollEpoch) { + /* A newer epoch may have requested a render while this frame was + * queued. Drop stale paint and requeue only that current request. */ + if (pendingEpoch === this.pollEpoch) this.scheduleRenderRows(f); + return; + } + this.renderRows(f); + }.bind(this) + ); + }, + refillRenderBucket() { const now = Date.now(); if (!this.renderBucketMs) this.renderBucketMs = now; @@ -828,6 +1073,7 @@ return view.extend({ updateFloodBanner() { const el = document.getElementById('fwlive-flood'); if (!el) return; + if (!el.style) el.style = { display: '' }; if (this.floodSuppressed) { el.style.display = 'block'; @@ -879,11 +1125,13 @@ return view.extend({ if (this.lastPollError) { status.className = 'fwlive-status fwlive-status-error'; status.textContent = _('Connection lost — retrying…') + suffix; + this.updateAdaptiveBanner(); return; } status.className = this.paused ? 'fwlive-status fwlive-status-paused' : 'fwlive-status'; status.textContent = this.compactCountText(matchCount); + this.updateAdaptiveBanner(); }, readRowLimit() { @@ -960,13 +1208,13 @@ return view.extend({ this.followLive = true; /* Merge pause buffer with the first live poll — do not replace (#43). */ this.resumeMerge = true; + const epoch = this.pollEpoch; this.fetchEntries() - .then(() => this.renderRows(true)) - .finally( - function () { - this.resumeMerge = false; - }.bind(this) - ); + .then(() => { + /* A hide/show bump abandons this epoch; the catch-up poll paints. */ + if (epoch === this.pollEpoch) this.renderRows(true); + }) + .catch(function () {}); } }, @@ -983,8 +1231,11 @@ return view.extend({ this.pendingForceRender = true; if (!this.paused) this.renderRows(true); else this.updateStatus(); + const epoch = this.pollEpoch; this.fetchEntries() .then(() => { + /* A hide/show bump abandons this epoch; the catch-up poll paints. */ + if (epoch !== this.pollEpoch) return; if (this.paused) this.updateStatus(); else this.renderRows(true); }) @@ -1063,6 +1314,9 @@ return view.extend({ const ips = this.collectIpsFromEntries(entries); const need = []; const now = Date.now(); + /* While the router sheds resolve load, hold the paused banner without + * re-asking every poll — retry after the cooldown expires. */ + if (this.resolveLoadShed && now < (this.resolveShedUntil || 0)) return; for (let i = 0; i < ips.length && need.length < 32; i++) { const ip = ips[i]; @@ -1081,8 +1335,28 @@ return view.extend({ const res = await callFwliveResolve({ addresses: need }); if (gen !== this.resolveGeneration) return; - /* rpc.declare expect: { names: {} } already unwraps — res IS the map (#243). */ - const names = res || {}; + if (res && typeof res === 'object' && res.disabled === 'load') { + this.resolveLoadShed = true; + this.resolveShedUntil = Date.now() + 60000; + this.updateAdaptiveBanner(); + return; + } + + this.resolveLoadShed = false; + this.resolveShedUntil = 0; + /* RPC-level failure (no_resolver, jshn_missing, invalid_input): the + * reply carries no per-address signal, so return without failMark — + * otherwise every address looks like "no PTR" and retries stall + * for the failure TTL. */ + if (res && typeof res === 'object' && typeof res.error === 'string' && res.error) + return; + /* Full reply: names map under .names; legacy expect-unwrap was the map. */ + const names = + res && typeof res === 'object' && res.names && typeof res.names === 'object' + ? res.names + : res && typeof res === 'object' && !Array.isArray(res) + ? res + : {}; let updated = false; for (let i = 0; i < need.length; i++) { @@ -1096,7 +1370,8 @@ return view.extend({ } } - if (updated) this.renderRows(true); + this.updateAdaptiveBanner(); + if (updated) this.scheduleRenderRows(true); } catch (e) { /* resolve unavailable — show IPs */ } finally { @@ -1221,7 +1496,7 @@ return view.extend({ renderRows(force) { const el = document.getElementById('fwlive-table'); - if (!el) return; + if (!el || typeof el.querySelector !== 'function') return; const body = el.querySelector('tbody'); const empty = document.getElementById('fwlive-empty'); @@ -1369,18 +1644,25 @@ return view.extend({ }, async pollData() { + if (this.isTabHidden()) return; if (this.pollDataInFlight) return; + const epoch = this.pollEpoch; this.pollDataInFlight = true; try { try { await this.fetchEntries(); } catch (e) { + /* fetchEntries already accounts the poll RTT for every rpc + * outcome; a throw here is a local normalize/buffer bug, not + * network slowness, so count nothing further. */ this.lastPollError = true; } + if (epoch !== this.pollEpoch) return; + if (this.paused) this.updateStatus(); - else this.renderRows(!!this.pendingForceRender); + else this.scheduleRenderRows(!!this.pendingForceRender); try { await this.resolveHostnamesForEntries(this.filteredRows()); @@ -1388,19 +1670,25 @@ return view.extend({ /* resolve unavailable — show IPs */ } } finally { - this.pollDataInFlight = false; + /* Only the owning epoch clears — a stale hidden-era poll must not + * drop the catch-up guard after resume (Grok #331). Resume clears + * explicitly before starting the catch-up poll. */ + if (epoch === this.pollEpoch) this.pollDataInFlight = false; } }, load() { + this.bindVisibility(); if (!this.pollFn) { this.pollFn = this.pollData.bind(this); - poll.add(this.pollFn, 1); + this.pollCadenceSec = constants.POLL_CADENCE_FAST_S; + if (!this.isTabHidden()) poll.add(this.pollFn, this.pollCadenceSec); /* Best-effort teardown when leaving the page (LuCI SPA may full-reload). */ if (typeof window !== 'undefined' && window.addEventListener) { window.addEventListener( 'pagehide', function () { + this.unbindVisibility(); if (this.pollFn) { try { poll.remove(this.pollFn); @@ -1553,6 +1841,7 @@ return view.extend({ ) ]), E('div', { 'id': 'fwlive-flood', 'class': 'fwlive-flood' }, ['']), + E('div', { 'id': 'fwlive-adaptive', 'class': 'fwlive-adaptive' }, ['']), E('div', { 'id': 'fwlive-display-drawer', 'class': 'fwlive-display-bar' }, [ E('span', { 'class': 'fwlive-display-bar-label' }, [_('Display options')]), E('div', { 'class': 'fwlive-display-controls' }, [ diff --git a/luci-app-fwlive/po/de/luci-app-fwlive.po b/luci-app-fwlive/po/de/luci-app-fwlive.po index 87dfeb7d2..a95722ae0 100644 --- a/luci-app-fwlive/po/de/luci-app-fwlive.po +++ b/luci-app-fwlive/po/de/luci-app-fwlive.po @@ -12,78 +12,78 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:926 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:926 msgid "%d matching · %d/%d stored" msgstr "%d matching · %d/%d stored" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:929 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:929 msgid "0 matching · %d/%d stored" msgstr "0 matching · %d/%d stored" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:251 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:251 msgid "Accessible (teal/orange)" msgstr "Barrierefrei (teal/orange)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:32 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:32 msgid "Action" msgstr "Aktion" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:618 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:618 msgid "Administrator access is required to disable logging." msgstr "" "Zum Deaktivieren der Protokollierung sind Administratorrechte erforderlich." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:586 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:586 msgid "Administrator access is required to enable logging." msgstr "" "Zum Aktivieren der Protokollierung sind Administratorrechte erforderlich." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1092 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1092 msgid "Also seen" msgstr "Weitere" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:571 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:607 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:571 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:607 msgid "Another change is staged for the firewall; apply or revert it first." msgstr "" "Eine andere Änderung an der Firewall ist vorgemerkt; zuerst anwenden oder " "verwerfen." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1658 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1658 msgid "Any action" msgstr "Beliebige Aktion" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1085 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1085 msgid "Any protocol" msgstr "Beliebiges Protokoll" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:113 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:113 msgid "Before you enable logging" msgstr "Before you enable logging" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:569 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:569 msgid "Cannot enable logging until kernel log modules are installed." msgstr "" "Protokollierung kann erst aktiviert werden, wenn die Kernel-Log-Module " "installiert sind." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:116 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:116 msgid "Changes:" msgstr "Changes:" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:250 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:250 msgid "Classic (green/red)" msgstr "Klassisch (grün/rot)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1642 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1642 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "Klassisch verwendet grün/rot; Barrierefrei verwendet teal/orange" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116 msgid "Clear all" msgstr "Alle löschen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1698 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1698 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, click a row for the full message" @@ -92,7 +92,7 @@ msgstr "" "auf eine Regel für Firewall-Einstellungen · in der Einfachen Ansicht Zeile " "anklicken für die vollständige Meldung" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1719 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1719 msgid "" "Click a row (Time or other non-link cells) to see the full log line (Simple " "view)." @@ -100,11 +100,11 @@ msgstr "" "Klicken Sie auf eine Zeile (Zeit oder andere Nicht-Link-Zellen), um die " "vollständige Log-Zeile zu sehen (Einfache Ansicht)." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:113 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:113 msgid "Click a row for the full message" msgstr "Zeile anklicken für die vollständige Meldung" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1720 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1720 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." @@ -112,87 +112,97 @@ msgstr "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1086 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1086 msgid "Common" msgstr "Häufig" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944 msgid "Connection lost — retrying…" msgstr "Connection lost — retrying…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:609 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:609 msgid "Could not disable logging." msgstr "Protokollierung konnte nicht deaktiviert werden." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:573 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:573 msgid "Could not enable logging." msgstr "Protokollierung konnte nicht aktiviert werden." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1680 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1680 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "" "Eigenes Protokoll (! zum Ausschließen). Überschreibt das Menü, wenn gesetzt." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 msgid "DPort" msgstr "Ziel-Port" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:814 +msgid "Degraded — sampling" +msgstr "Eingeschränkt — Stichproben" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 +msgid "Degraded — sampling (slow poll RTT; cadence reduced)." +msgstr "" +"Eingeschränkt — Stichproben (langsame Poll-Roundtrip-Zeit; Frequenz " +"reduziert)." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 msgid "Destination" msgstr "Ziel" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1691 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1691 msgid "Destination IP contains (! to exclude)" msgstr "Ziel-IP enthält (! zum Ausschließen)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692 msgid "Destination port (! to exclude)" msgstr "Ziel-Port (! zum Ausschließen)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1583 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1583 msgid "Detail" msgstr "Detail" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 msgid "Dir" msgstr "Richtung" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:85 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:85 msgid "Disabling…" msgstr "Disabling…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1616 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1616 msgid "Display options" msgstr "Anzeigeoptionen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1716 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1716 msgid "Display options on the bar set Limit, row tint, palette, and hostnames." msgstr "" "Anzeigeoptionen in der Leiste setzen Limit, Zeilenfärbung, Palette und " "Hostnamen." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:121 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:121 msgid "Does not change:" msgstr "Does not change:" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:138 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:138 msgid "Don’t show this again" msgstr "Don’t show this again" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 msgid "Enable WAN drop/reject logging" msgstr "Enable WAN drop/reject logging" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:104 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:104 msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)." msgstr "Enable WAN zone drop/reject logging (same as Network → Firewall)." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 msgid "Enable logging" msgstr "Protokollierung aktivieren" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1715 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1715 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." @@ -200,57 +210,57 @@ msgstr "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 msgid "Enabling…" msgstr "Enabling…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1099 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1099 msgid "Exclude" msgstr "Ausschließen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 msgid "Exclude instead" msgstr "Stattdessen ausschließen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:53 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:71 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:53 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:71 msgid "Filter by %s" msgstr "Filtern nach %s" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:136 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:136 msgid "Filter by interface" msgstr "Nach Schnittstelle filtern" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:112 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:112 msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings." msgstr "" "Logs nach Regel filtern (Hinweis: %s). Strg+Klick öffnet die Firewall-" "Einstellungen." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540 +#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "Firewall-Live-Ansicht" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 msgid "Flags" msgstr "Flags" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 msgid "Flow" msgstr "Fluss" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/root/usr/share/rpcd/acl.d/luci-app-fwlive.json:3 +#: openwrt-feed/luci-app-fwlive/root/usr/share/rpcd/acl.d/luci-app-fwlive.json:3 msgid "Grant access to firewall live log view" msgstr "Zugriff auf die Live-Ansicht der Firewall-Protokolle gewähren" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1712 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1712 msgid "Help" msgstr "Hilfe" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:895 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:895 msgid "" "High event rate — table refresh is throttled to protect the browser. The " "buffer still updates; refresh will resume automatically." @@ -258,15 +268,19 @@ msgstr "" "High event rate — table refresh is throttled to protect the browser. The " "buffer still updates; refresh will resume automatically." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1090 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:984 +msgid "Hostname resolve paused while the router is under load." +msgstr "Hostnamenauflösung pausiert, solange der Router ausgelastet ist." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1090 msgid "ICMPv6" msgstr "ICMPv6" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35 msgid "IN" msgstr "EINGANG" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1723 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1723 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " @@ -276,7 +290,7 @@ msgstr "" "möglicherweise keine Erfolgs/Fehler-CSS-Variablen; fwlive fällt auf lokale " "Farben zurück (air-gapped, keine Daten verlassen das Gerät)." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:205 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:205 msgid "" "If the WAN is quiet, wait for probes or use the optional ping check in " "Help / the enabling-logs guide." @@ -284,28 +298,28 @@ msgstr "" "If the WAN is quiet, wait for probes or use the optional ping check in " "Help / the enabling-logs guide." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 msgid "Include instead" msgstr "Stattdessen einschließen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34 msgid "Interface" msgstr "Schnittstelle" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1688 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1688 msgid "Interface (prefix ! to exclude)" msgstr "Schnittstelle (! voranstellen zum Ausschließen)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:165 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:232 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:165 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:232 msgid "I’ll configure this under Network → Firewall" msgstr "I’ll configure this under Network → Firewall" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:194 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:194 msgid "Kernel log modules missing" msgstr "Kernel log modules missing" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "" "Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" "nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall." @@ -314,74 +328,74 @@ msgstr "" "kmod-nf-log-ipv6 (oder kmod-nf-log / kmod-nf-log6) und laden Sie dann die " "Firewall neu." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 msgid "Len" msgstr "Länge" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1619 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1619 msgid "Limit" msgstr "Limit" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:536 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:536 msgid "Limited diagnostics — timeout command missing" msgstr "Eingeschränkte Diagnose — timeout-Befehl fehlt" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:210 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:210 msgid "Logging is off on this router" msgstr "Logging is off on this router" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:252 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:256 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:252 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:256 msgid "Manual test (System → Terminal):" msgstr "Manueller Test (System → Terminal):" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:221 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1590 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1595 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:221 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1590 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1595 msgid "Message" msgstr "Nachricht" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1686 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1686 msgid "More filters" msgstr "Weitere Filter" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:37 msgid "Network → Firewall" msgstr "Network → Firewall" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:187 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:187 msgid "" "No WAN firewall zone found in /etc/config/firewall. Configure zones under" msgstr "" "Keine WAN-Firewall-Zone in /etc/config/firewall gefunden. Konfigurieren Sie " "Zonen unter" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:185 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:185 msgid "No WAN zone found" msgstr "No WAN zone found" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163 msgid "Not now" msgstr "Not now" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:220 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:220 msgid "Nothing changes until you click Enable." msgstr "Nothing changes until you click Enable." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 msgid "OUT" msgstr "AUSGANG" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1610 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1610 msgid "One line" msgstr "Einzeilig" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206 msgid "Open firewall zone settings" msgstr "Firewall-Zonen-Einstellungen öffnen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211 msgid "" "OpenWrt does not write firewall events to the log until you turn logging on. " "Live View only shows what the firewall already logs — it does not add allow/" @@ -391,44 +405,44 @@ msgstr "" "Live View only shows what the firewall already logs — it does not add allow/" "deny rules." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1638 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1638 msgid "Palette" msgstr "Palette" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1560 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1560 msgid "Pause" msgstr "Pause" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 msgid "Paused" msgstr "Pausiert" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 msgid "Proto" msgstr "Protokoll" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1674 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1674 msgid "Protocol — common values" msgstr "Protokoll — häufige Werte" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1656 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1656 msgid "Quick search" msgstr "Schnellsuche" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91 msgid "Remove filter" msgstr "Filter entfernen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 msgid "Resume" msgstr "Fortsetzen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1631 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1631 msgid "Row tint" msgstr "Zeileneinfärbung" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1721 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1721 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " @@ -438,7 +452,7 @@ msgstr "" "Klassisch (grün/rot, Standard) oder Barrierefrei (teal/orange). Aktionstext " "bleibt in beiden Fällen farbig." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1550 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1550 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." @@ -446,51 +460,59 @@ msgstr "" "Die Zeileneinfärbung verwendete einen lokalen Farbfallback, da das aktive " "LuCI-Theme keine Erlauben/Verweigern-Hintergründe anwendet." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:33 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:33 msgid "Rule" msgstr "Regel" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:527 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:527 msgid "Rule labels incomplete — map truncated" msgstr "Rule labels incomplete — map truncated" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:531 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:531 msgid "Rule labels unavailable" msgstr "Rule labels unavailable" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:529 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:529 msgid "Rule labels unavailable — temp file failed" msgstr "Rule labels unavailable — temp file failed" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:980 +msgid "Server shedding — at most %d log lines per poll." +msgstr "Server reduziert — höchstens %d Protokollzeilen pro Abfrage." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982 +msgid "Server truncated this poll (adaptive cap)." +msgstr "Server hat diese Abfrage gekürzt (adaptives Limit)." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 msgid "SPort" msgstr "Quell-Port" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1650 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1650 msgid "Show hostnames" msgstr "Hostnamen anzeigen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1629 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1629 msgid "Show pass/deny row background colors" msgstr "Pass/Deny-Zeilenhintergrundfarben anzeigen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1576 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1576 msgid "Simple" msgstr "Einfach" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 msgid "Source" msgstr "Quelle" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1689 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1689 msgid "Source IP contains (! to exclude)" msgstr "Quell-IP enthält (! zum Ausschließen)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690 msgid "Source port (! to exclude)" msgstr "Quell-Port (! zum Ausschließen)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1717 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1717 msgid "" "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt " "defaults to 10/minute when no explicit limit is configured; fwlive does not " @@ -500,7 +522,7 @@ msgstr "" "OpenWrt-Standard ist 10/Minute ohne explizites Limit; fwlive setzt diese " "Grenze nicht." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1714 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1714 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." @@ -508,15 +530,15 @@ msgstr "" "Die Tabelle aktualisiert sich automatisch, wenn Ihre Firewall Verkehr " "protokolliert. Pause nutzen, wenn es zu schnell wird." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551 msgid "Theme tint fallback" msgstr "Theme-Farb-Fallback" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:31 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:31 msgid "Time" msgstr "Zeit" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219 msgid "" "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " @@ -526,25 +548,25 @@ msgstr "" "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " "LAN browsing is not logged." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 msgid "Undo:" msgstr "Undo:" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1722 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1722 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "" "Verwenden Sie „Detail“ für alle Spalten (Flags, Länge, rohe Nachricht)." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1564 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1568 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1564 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1568 msgid "View" msgstr "Ansicht" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:615 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:615 msgid "WAN drop/reject logging is off." msgstr "WAN drop/reject logging is off." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204 msgid "" "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "here. Normal LAN browsing will not." @@ -552,7 +574,7 @@ msgstr "" "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "here. Normal LAN browsing will not." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:579 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:579 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." @@ -560,157 +582,165 @@ msgstr "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:581 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:581 msgid "WAN logging is already enabled." msgstr "WAN-Protokollierung ist bereits aktiviert." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:88 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:88 msgid "WAN logging on" msgstr "WAN-Protokollierung: EIN" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:94 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:94 msgid "WAN logging on (%s). Click to disable." msgstr "WAN-Protokollierung: EIN (%s). Klicken zum Deaktivieren." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77 msgid "WAN logging unavailable: missing kernel log modules" msgstr "WAN-Protokollierung nicht verfügbar: fehlende Kernel-Log-Module" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:70 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:70 msgid "WAN logging unavailable: no WAN zone" msgstr "WAN-Protokollierung nicht verfügbar: keine WAN-Zone" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203 msgid "Waiting for firewall events" msgstr "Waiting for firewall events" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1545 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1545 msgid "Watching" msgstr "Beobachtet" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1603 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1603 msgid "Wrap" msgstr "Umbrechen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:123 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:123 msgid "allow/deny rules, LAN logging, or anything else." msgstr "allow/deny rules, LAN logging, or anything else." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:828 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:828 msgid "buffer full" msgstr "Puffer voll" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81 msgid "default 10/minute" msgstr "Standard 10/Minute" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28 msgid "is" msgstr "ist" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:823 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:823 msgid "loading buffer" msgstr "Lade Puffer" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:923 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:923 msgid "loading…" msgstr "loading…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44 msgid "not" msgstr "nicht" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1107 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1107 msgid "not AH" msgstr "nicht AH" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1106 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1106 msgid "not ESP" msgstr "nicht ESP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1105 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1105 msgid "not GRE" msgstr "nicht GRE" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1102 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1102 msgid "not ICMP" msgstr "nicht ICMP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1103 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1103 msgid "not ICMPv6" msgstr "nicht ICMPv6" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1104 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1104 msgid "not IGMP" msgstr "nicht IGMP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1108 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1108 msgid "not SCTP" msgstr "nicht SCTP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1100 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1100 msgid "not TCP" msgstr "nicht TCP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1101 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1101 msgid "not UDP" msgstr "nicht UDP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1666 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1666 msgid "not block" msgstr "nicht blockieren" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1665 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1665 msgid "not drop" msgstr "nicht verwerfen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1664 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1664 msgid "not pass" msgstr "nicht erlauben" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1667 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1667 msgid "not reject" msgstr "nicht zurückweisen" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1668 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1668 msgid "not unknown" msgstr "nicht unbekannt" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1679 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1679 msgid "or type…" msgstr "oder tippen…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830 msgid "render paused (high rate)" msgstr "Rendering pausiert (hohe Rate)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817 +msgid "resolve paused (load)" +msgstr "Auflösung pausiert (Last)" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 msgid "scroll frozen — scroll to top to follow live" msgstr "scroll frozen — scroll to top to follow live" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118 msgid "sets log on the WAN firewall zone and reloads the firewall." msgstr "sets log on the WAN firewall zone and reloads the firewall." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:254 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:258 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:254 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:258 msgid "then ping the router." msgstr "dann den Router anpingen." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815 +msgid "truncated" +msgstr "gekürzt" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128 msgid "turn it back off with the WAN logging on control on the watch strip." msgstr "turn it back off with the WAN logging on control on the watch strip." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:512 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:512 msgid "using fw4" msgstr "mit fw4" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:510 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:510 msgid "using iptables" msgstr "mit iptables" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89 msgid "· %s" msgstr "· %s" diff --git a/luci-app-fwlive/po/ru/luci-app-fwlive.po b/luci-app-fwlive/po/ru/luci-app-fwlive.po index 7c08ba3e1..1b44869e0 100644 --- a/luci-app-fwlive/po/ru/luci-app-fwlive.po +++ b/luci-app-fwlive/po/ru/luci-app-fwlive.po @@ -13,82 +13,82 @@ msgstr "" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:926 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:926 msgid "%d matching · %d/%d stored" msgstr "%d matching · %d/%d stored" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:929 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:929 msgid "0 matching · %d/%d stored" msgstr "0 matching · %d/%d stored" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:251 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:251 msgid "Accessible (teal/orange)" msgstr "Доступный (бирюзовый/оранжевый)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:32 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:32 msgid "Action" msgstr "Действие" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:618 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:618 msgid "Administrator access is required to disable logging." msgstr "" "Для отключения журналирования требуются права аÐ" "´Ð¼Ð¸Ð½Ð¸ÑÑ‚ратора." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:586 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:586 msgid "Administrator access is required to enable logging." msgstr "" "Для включения журналирования требуются права аÐ" "´Ð¼Ð¸Ð½Ð¸ÑÑ‚ратора." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1092 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1092 msgid "Also seen" msgstr "Также встречаются" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:571 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:607 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:571 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:607 msgid "Another change is staged for the firewall; apply or revert it first." msgstr "" "В брандмауэре уже есть другое незафиксированное изменение; сначала примените " "или отмените его." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1658 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1658 msgid "Any action" msgstr "Любое действие" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1085 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1085 msgid "Any protocol" msgstr "Любой протокол" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:113 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:113 msgid "Before you enable logging" msgstr "Before you enable logging" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:569 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:569 msgid "Cannot enable logging until kernel log modules are installed." msgstr "" "Невозможно включить журналирование, пока не " "установлены модули журнала ядра." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:116 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:116 msgid "Changes:" msgstr "Changes:" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:250 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:250 msgid "Classic (green/red)" msgstr "Классический (зелёный/красный)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1642 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1642 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "" "Классический — зелёный/красный; доступный — " "бирюзовый/оранжевый" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116 msgid "Clear all" msgstr "Очистить всё" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1698 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1698 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, click a row for the full message" @@ -97,7 +97,7 @@ msgstr "" "правилу для настроек МЭ · в Простом виде щёлкните строку для полного " "сообщения" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1719 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1719 msgid "" "Click a row (Time or other non-link cells) to see the full log line (Simple " "view)." @@ -105,11 +105,11 @@ msgstr "" "Щёлкните строку (Время или другие ячейки без ссылки), чтобы увидеть полную " "строку журнала (Простой вид)." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:113 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:113 msgid "Click a row for the full message" msgstr "Щёлкните строку для полного сообщения" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1720 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1720 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." @@ -117,86 +117,94 @@ msgstr "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1086 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1086 msgid "Common" msgstr "Частые" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944 msgid "Connection lost — retrying…" msgstr "Connection lost — retrying…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:609 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:609 msgid "Could not disable logging." msgstr "Не удалось отключить журналирование." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:573 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:573 msgid "Could not enable logging." msgstr "Не удалось включить журналирование." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1680 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1680 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "Свой протокол (! для исключения). Переопределяет меню, если задан." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 msgid "DPort" msgstr "Порт назн." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:814 +msgid "Degraded — sampling" +msgstr "Упрощённый режим — выборка" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 +msgid "Degraded — sampling (slow poll RTT; cadence reduced)." +msgstr "Упрощённый режим — выборка (медленный RTT опроса; интервал увеличен)." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 msgid "Destination" msgstr "Назначение" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1691 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1691 msgid "Destination IP contains (! to exclude)" msgstr "IP назначения содержит (! для исключения)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692 msgid "Destination port (! to exclude)" msgstr "Порт назначения (! для исключения)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1583 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1583 msgid "Detail" msgstr "Подробно" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 msgid "Dir" msgstr "Напр." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:85 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:85 msgid "Disabling…" msgstr "Disabling…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1616 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1616 msgid "Display options" msgstr "Параметры отображения" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1716 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1716 msgid "Display options on the bar set Limit, row tint, palette, and hostnames." msgstr "" "Параметры отображения на панели задают лимит, окраску строк, палитру и имена " "узлов." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:121 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:121 msgid "Does not change:" msgstr "Does not change:" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:138 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:138 msgid "Don’t show this again" msgstr "Don’t show this again" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 msgid "Enable WAN drop/reject logging" msgstr "Enable WAN drop/reject logging" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:104 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:104 msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)." msgstr "Enable WAN zone drop/reject logging (same as Network → Firewall)." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 msgid "Enable logging" msgstr "Включить журнал" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1715 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1715 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." @@ -204,57 +212,57 @@ msgstr "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 msgid "Enabling…" msgstr "Enabling…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1099 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1099 msgid "Exclude" msgstr "Исключить" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 msgid "Exclude instead" msgstr "Исключить вместо этого" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:53 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:71 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:53 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:71 msgid "Filter by %s" msgstr "Фильтр по %s" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:136 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:136 msgid "Filter by interface" msgstr "Фильтр по интерфейсу" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:112 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:112 msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings." msgstr "" "Фильтр журналов по правилу (подсказка: %s). " "Ctrl+клик открывает настройки файрвола." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540 +#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "Просмотр файрвола в реальном времени" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 msgid "Flags" msgstr "Флаги" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 msgid "Flow" msgstr "Поток" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/root/usr/share/rpcd/acl.d/luci-app-fwlive.json:3 +#: openwrt-feed/luci-app-fwlive/root/usr/share/rpcd/acl.d/luci-app-fwlive.json:3 msgid "Grant access to firewall live log view" msgstr "Предоставить доступ к живому просмотру журнала межсетевого экрана" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1712 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1712 msgid "Help" msgstr "Справка" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:895 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:895 msgid "" "High event rate — table refresh is throttled to protect the browser. The " "buffer still updates; refresh will resume automatically." @@ -262,15 +270,19 @@ msgstr "" "High event rate — table refresh is throttled to protect the browser. The " "buffer still updates; refresh will resume automatically." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1090 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:984 +msgid "Hostname resolve paused while the router is under load." +msgstr "Разрешение имён приостановлено из-за нагрузки на маршрутизатор." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1090 msgid "ICMPv6" msgstr "ICMPv6" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35 msgid "IN" msgstr "ВХОД" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1723 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1723 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " @@ -280,7 +292,7 @@ msgstr "" "активная тема LuCI может не содержать CSS-" "переменные успе" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:205 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:205 msgid "" "If the WAN is quiet, wait for probes or use the optional ping check in " "Help / the enabling-logs guide." @@ -288,28 +300,28 @@ msgstr "" "If the WAN is quiet, wait for probes or use the optional ping check in " "Help / the enabling-logs guide." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 msgid "Include instead" msgstr "Включить вместо этого" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34 msgid "Interface" msgstr "Интерфейс" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1688 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1688 msgid "Interface (prefix ! to exclude)" msgstr "Интерфейс (! в начале для исключения)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:165 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:232 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:165 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:232 msgid "I’ll configure this under Network → Firewall" msgstr "I’ll configure this under Network → Firewall" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:194 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:194 msgid "Kernel log modules missing" msgstr "Kernel log modules missing" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "" "Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" "nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall." @@ -318,73 +330,73 @@ msgstr "" "kmod-nf-log-ipv4 и kmod-nf-log-ipv6 (или kmod-nf-log / kmod-nf-log6), " "затем перезагрузите файрвол." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 msgid "Len" msgstr "Длина" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1619 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1619 msgid "Limit" msgstr "Лимит" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:536 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:536 msgid "Limited diagnostics — timeout command missing" msgstr "Ограниченная диагностика — отсутствует команда timeout" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:210 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:210 msgid "Logging is off on this router" msgstr "Logging is off on this router" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:252 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:256 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:252 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:256 msgid "Manual test (System → Terminal):" msgstr "Ручной тест (Система → Терминал):" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:221 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1590 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1595 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:221 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1590 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1595 msgid "Message" msgstr "Сообщение" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1686 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1686 msgid "More filters" msgstr "Больше фильтров" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:37 msgid "Network → Firewall" msgstr "Network → Firewall" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:187 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:187 msgid "" "No WAN firewall zone found in /etc/config/firewall. Configure zones under" msgstr "" "Зона WAN брандмауэра не найдена в /etc/config/firewall. Настройте зоны в" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:185 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:185 msgid "No WAN zone found" msgstr "No WAN zone found" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163 msgid "Not now" msgstr "Not now" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:220 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:220 msgid "Nothing changes until you click Enable." msgstr "Nothing changes until you click Enable." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 msgid "OUT" msgstr "ВЫХОД" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1610 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1610 msgid "One line" msgstr "Одна строка" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206 msgid "Open firewall zone settings" msgstr "Открыть настройки зон файрвола" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211 msgid "" "OpenWrt does not write firewall events to the log until you turn logging on. " "Live View only shows what the firewall already logs — it does not add allow/" @@ -394,44 +406,44 @@ msgstr "" "Live View only shows what the firewall already logs — it does not add allow/" "deny rules." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1638 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1638 msgid "Palette" msgstr "Палитра" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1560 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1560 msgid "Pause" msgstr "Пауза" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 msgid "Paused" msgstr "Пауза" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 msgid "Proto" msgstr "Протокол" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1674 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1674 msgid "Protocol — common values" msgstr "Протокол — частые значения" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1656 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1656 msgid "Quick search" msgstr "Быстрый поиск" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91 msgid "Remove filter" msgstr "Удалить фильтр" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 msgid "Resume" msgstr "Продолжить" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1631 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1631 msgid "Row tint" msgstr "Цвет строк" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1721 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1721 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " @@ -441,7 +453,7 @@ msgstr "" "классический (зелёный/красный, по умолчанию) или доступный (бирюзовый/" "оранжевый). Цвет текста действия сохраняется в любом случае." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1550 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1550 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." @@ -450,51 +462,59 @@ msgstr "" "локальный запасной цвет, так как активная тема " "LuCI не применяет фон пропуска/отказа." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:33 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:33 msgid "Rule" msgstr "Правило" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:527 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:527 msgid "Rule labels incomplete — map truncated" msgstr "Rule labels incomplete — map truncated" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:531 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:531 msgid "Rule labels unavailable" msgstr "Rule labels unavailable" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:529 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:529 msgid "Rule labels unavailable — temp file failed" msgstr "Rule labels unavailable — temp file failed" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:980 +msgid "Server shedding — at most %d log lines per poll." +msgstr "Сервер ограничивает выдачу — не более %d строк журнала за опрос." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982 +msgid "Server truncated this poll (adaptive cap)." +msgstr "Сервер сократил этот опрос (адаптивный предел)." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 msgid "SPort" msgstr "Порт ист." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1650 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1650 msgid "Show hostnames" msgstr "Показывать имена " -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1629 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1629 msgid "Show pass/deny row background colors" msgstr "Показывать цвета фона строк pass/deny" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1576 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1576 msgid "Simple" msgstr "Простой" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 msgid "Source" msgstr "Источник" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1689 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1689 msgid "Source IP contains (! to exclude)" msgstr "IP источника содержит (! для исключения)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690 msgid "Source port (! to exclude)" msgstr "Порт источника (! для исключения)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1717 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1717 msgid "" "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt " "defaults to 10/minute when no explicit limit is configured; fwlive does not " @@ -504,7 +524,7 @@ msgstr "" "умолчанию OpenWrt: 10/мин без явного лимита; fwlive " "сам лимит не задаёт." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1714 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1714 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." @@ -513,15 +533,15 @@ msgstr "" "файрвол пишет в журнал. Используйте Паузу, если " "слишком быстро." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551 msgid "Theme tint fallback" msgstr "Запасной цвет темы" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:31 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:31 msgid "Time" msgstr "Время" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219 msgid "" "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " @@ -531,25 +551,25 @@ msgstr "" "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " "LAN browsing is not logged." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 msgid "Undo:" msgstr "Undo:" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1722 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1722 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "" "Используйте «Подробно» для всех столбцов (флаги, длина, исходное сообщение)." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1564 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1568 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1564 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1568 msgid "View" msgstr "Вид" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:615 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:615 msgid "WAN drop/reject logging is off." msgstr "WAN drop/reject logging is off." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204 msgid "" "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "here. Normal LAN browsing will not." @@ -557,7 +577,7 @@ msgstr "" "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "here. Normal LAN browsing will not." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:579 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:579 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." @@ -565,159 +585,167 @@ msgstr "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:581 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:581 msgid "WAN logging is already enabled." msgstr "WAN-журналирование уже включено." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:88 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:88 msgid "WAN logging on" msgstr "WAN-журнал: ВКЛ" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:94 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:94 msgid "WAN logging on (%s). Click to disable." msgstr "WAN-журнал: ВКЛ (%s). Нажмите, чтобы отключить." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77 msgid "WAN logging unavailable: missing kernel log modules" msgstr "" "WAN-журналирование недоступно: отсутствуют модули " "журнала ядра" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:70 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:70 msgid "WAN logging unavailable: no WAN zone" msgstr "WAN-журналирование недоступно: нет зоны WAN" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203 msgid "Waiting for firewall events" msgstr "Waiting for firewall events" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1545 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1545 msgid "Watching" msgstr "Наблюдение" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1603 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1603 msgid "Wrap" msgstr "Перенос" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:123 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:123 msgid "allow/deny rules, LAN logging, or anything else." msgstr "allow/deny rules, LAN logging, or anything else." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:828 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:828 msgid "buffer full" msgstr "буфер заполнен" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81 msgid "default 10/minute" msgstr "по умолчанию 10/мин" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28 msgid "is" msgstr "есть" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:823 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:823 msgid "loading buffer" msgstr "загрузка буфера" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:923 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:923 msgid "loading…" msgstr "loading…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44 msgid "not" msgstr "не" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1107 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1107 msgid "not AH" msgstr "не AH" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1106 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1106 msgid "not ESP" msgstr "не ESP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1105 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1105 msgid "not GRE" msgstr "не GRE" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1102 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1102 msgid "not ICMP" msgstr "не ICMP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1103 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1103 msgid "not ICMPv6" msgstr "не ICMPv6" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1104 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1104 msgid "not IGMP" msgstr "не IGMP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1108 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1108 msgid "not SCTP" msgstr "не SCTP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1100 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1100 msgid "not TCP" msgstr "не TCP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1101 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1101 msgid "not UDP" msgstr "не UDP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1666 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1666 msgid "not block" msgstr "не блокировать" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1665 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1665 msgid "not drop" msgstr "не отбрасывать" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1664 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1664 msgid "not pass" msgstr "не пропускать" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1667 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1667 msgid "not reject" msgstr "не отклонять" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1668 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1668 msgid "not unknown" msgstr "не неизвестно" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1679 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1679 msgid "or type…" msgstr "или введите…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830 msgid "render paused (high rate)" msgstr "рендеринг приостановлен (высокая частота)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817 +msgid "resolve paused (load)" +msgstr "Разрешение приостановлено (нагрузка)" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 msgid "scroll frozen — scroll to top to follow live" msgstr "scroll frozen — scroll to top to follow live" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118 msgid "sets log on the WAN firewall zone and reloads the firewall." msgstr "sets log on the WAN firewall zone and reloads the firewall." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:254 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:258 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:254 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:258 msgid "then ping the router." msgstr "затем выполните ping маршрутизатора." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815 +msgid "truncated" +msgstr "обрезано" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128 msgid "turn it back off with the WAN logging on control on the watch strip." msgstr "turn it back off with the WAN logging on control on the watch strip." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:512 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:512 msgid "using fw4" msgstr "используется fw4" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:510 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:510 msgid "using iptables" msgstr "используется iptables" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89 msgid "· %s" msgstr "· %s" diff --git a/luci-app-fwlive/po/templates/luci-app-fwlive.pot b/luci-app-fwlive/po/templates/luci-app-fwlive.pot index 88cf6ab9d..487a412de 100644 --- a/luci-app-fwlive/po/templates/luci-app-fwlive.pot +++ b/luci-app-fwlive/po/templates/luci-app-fwlive.pot @@ -112,6 +112,14 @@ msgstr "" msgid "DPort" msgstr "" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:814 +msgid "Degraded — sampling" +msgstr "" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 +msgid "Degraded — sampling (slow poll RTT; cadence reduced)." +msgstr "" + #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 msgid "Destination" msgstr "" @@ -225,6 +233,10 @@ msgid "" "buffer still updates; refresh will resume automatically." msgstr "" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:984 +msgid "Hostname resolve paused while the router is under load." +msgstr "" + #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1090 msgid "ICMPv6" msgstr "" @@ -411,6 +423,14 @@ msgstr "" msgid "Rule labels unavailable — temp file failed" msgstr "" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:980 +msgid "Server shedding — at most %d log lines per poll." +msgstr "" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982 +msgid "Server truncated this poll (adaptive cap)." +msgstr "" + #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 msgid "SPort" msgstr "" @@ -622,6 +642,10 @@ msgstr "" msgid "render paused (high rate)" msgstr "" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817 +msgid "resolve paused (load)" +msgstr "" + #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 msgid "scroll frozen — scroll to top to follow live" msgstr "" @@ -635,6 +659,10 @@ msgstr "" msgid "then ping the router." msgstr "" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815 +msgid "truncated" +msgstr "" + #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128 msgid "turn it back off with the WAN logging on control on the watch strip." msgstr "" diff --git a/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po b/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po index 437d021f7..3e013519f 100644 --- a/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po +++ b/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po @@ -12,72 +12,72 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:926 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:926 msgid "%d matching · %d/%d stored" msgstr "%d matching · %d/%d stored" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:929 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:929 msgid "0 matching · %d/%d stored" msgstr "0 matching · %d/%d stored" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:251 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:251 msgid "Accessible (teal/orange)" msgstr "可访问(青绿/橙色)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:32 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:32 msgid "Action" msgstr "动作" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:618 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:618 msgid "Administrator access is required to disable logging." msgstr "需要管理员权限才能" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:586 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:586 msgid "Administrator access is required to enable logging." msgstr "需要管理员权限才能开启日志记录。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1092 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1092 msgid "Also seen" msgstr "其他常见" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:571 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:607 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:571 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:607 msgid "Another change is staged for the firewall; apply or revert it first." msgstr "防火墙另有未提交的更改;请先应用或还原。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1658 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1658 msgid "Any action" msgstr "所有动作" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1085 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1085 msgid "Any protocol" msgstr "任意协议" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:113 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:113 msgid "Before you enable logging" msgstr "Before you enable logging" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:569 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:569 msgid "Cannot enable logging until kernel log modules are installed." msgstr "无法启用日志记录,直到内核日志模块已安装。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:116 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:116 msgid "Changes:" msgstr "Changes:" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:250 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:250 msgid "Classic (green/red)" msgstr "经" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1642 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1642 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "经" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116 msgid "Clear all" msgstr "æ" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1698 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1698 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, click a row for the full message" @@ -85,17 +85,17 @@ msgstr "" "点击单元格筛选 · 芯片上的 ≠ 排除 · Ctrl+点击规则打开防火墙设置 · 在简单视图中" "点击行查看完整消息" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1719 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1719 msgid "" "Click a row (Time or other non-link cells) to see the full log line (Simple " "view)." msgstr "点击一行(时间或其他非链接单元格)可查看完整日志行(简单视图)。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:113 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:113 msgid "Click a row for the full message" msgstr "点击行可查看完整消息" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1720 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1720 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." @@ -103,84 +103,92 @@ msgstr "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1086 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1086 msgid "Common" msgstr "常用" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944 msgid "Connection lost — retrying…" msgstr "Connection lost — retrying…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:609 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:609 msgid "Could not disable logging." msgstr "无法" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:573 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:573 msgid "Could not enable logging." msgstr "无法开启日志记录。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1680 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1680 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "自定义协议(! 排除)。有内容时覆盖菜单。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 msgid "DPort" msgstr "目标端口" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:814 +msgid "Degraded — sampling" +msgstr "降级 — 抽样" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 +msgid "Degraded — sampling (slow poll RTT; cadence reduced)." +msgstr "降级 — 抽样(轮询 RTT 较慢;已降低频率)。" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 msgid "Destination" msgstr "目标地址" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1691 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1691 msgid "Destination IP contains (! to exclude)" msgstr "目标 IP å" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692 msgid "Destination port (! to exclude)" msgstr "目标端口(! 表示排除)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1583 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1583 msgid "Detail" msgstr "详细" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 msgid "Dir" msgstr "方向" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:85 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:85 msgid "Disabling…" msgstr "Disabling…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1616 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1616 msgid "Display options" msgstr "显示选项" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1716 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1716 msgid "Display options on the bar set Limit, row tint, palette, and hostnames." msgstr "栏上的显示选项可设置行数限制、行着色、调色板和主机名。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:121 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:121 msgid "Does not change:" msgstr "Does not change:" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:138 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:138 msgid "Don’t show this again" msgstr "Don’t show this again" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 msgid "Enable WAN drop/reject logging" msgstr "Enable WAN drop/reject logging" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:104 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:104 msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)." msgstr "Enable WAN zone drop/reject logging (same as Network → Firewall)." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 msgid "Enable logging" msgstr "开启日志" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1715 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1715 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." @@ -188,55 +196,55 @@ msgstr "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230 msgid "Enabling…" msgstr "Enabling…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1099 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1099 msgid "Exclude" msgstr "排除" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 msgid "Exclude instead" msgstr "改为排除" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:53 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:71 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:53 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:71 msgid "Filter by %s" msgstr "按 %s 筛选" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:136 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:136 msgid "Filter by interface" msgstr "按接口筛选" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:112 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:112 msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings." msgstr "按规则筛选日志(提示:%s)。Ctrl+点击可打开防火墙设置。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540 +#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "防火墙实时视图" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 msgid "Flags" msgstr "标志" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 msgid "Flow" msgstr "流" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/root/usr/share/rpcd/acl.d/luci-app-fwlive.json:3 +#: openwrt-feed/luci-app-fwlive/root/usr/share/rpcd/acl.d/luci-app-fwlive.json:3 msgid "Grant access to firewall live log view" msgstr "授予防火墙实时日志视图的访问权限" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1712 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1712 msgid "Help" msgstr "帮助" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:895 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:895 msgid "" "High event rate — table refresh is throttled to protect the browser. The " "buffer still updates; refresh will resume automatically." @@ -244,15 +252,19 @@ msgstr "" "High event rate — table refresh is throttled to protect the browser. The " "buffer still updates; refresh will resume automatically." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1090 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:984 +msgid "Hostname resolve paused while the router is under load." +msgstr "路由器负载较高,已暂停主机名解析。" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1090 msgid "ICMPv6" msgstr "ICMPv6" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35 msgid "IN" msgstr "IN" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1723 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1723 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " @@ -261,7 +273,7 @@ msgstr "" "如果行染色看起来缺失,当前 LuCI 主题可能未提供成功/错误 CSS 变量;fwlive " "会回退到本地颜色(隔离运行,数据不会离开本机)。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:205 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:205 msgid "" "If the WAN is quiet, wait for probes or use the optional ping check in " "Help / the enabling-logs guide." @@ -269,99 +281,99 @@ msgstr "" "If the WAN is quiet, wait for probes or use the optional ping check in " "Help / the enabling-logs guide." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80 msgid "Include instead" msgstr "改为å" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34 msgid "Interface" msgstr "接口" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1688 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1688 msgid "Interface (prefix ! to exclude)" msgstr "接口(! 前缀表示排除)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:165 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:232 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:165 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:232 msgid "I’ll configure this under Network → Firewall" msgstr "I’ll configure this under Network → Firewall" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:194 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:194 msgid "Kernel log modules missing" msgstr "Kernel log modules missing" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "" "Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" "nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall." msgstr "缺少å" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 msgid "Len" msgstr "长度" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1619 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1619 msgid "Limit" msgstr "限制" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:536 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:536 msgid "Limited diagnostics — timeout command missing" msgstr "诊断功能受限 — 缺少 timeout 命令" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:210 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:210 msgid "Logging is off on this router" msgstr "Logging is off on this router" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:252 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:256 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:252 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:256 msgid "Manual test (System → Terminal):" msgstr "手动测试(系统 → 终端):" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:221 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1590 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1595 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:221 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1590 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1595 msgid "Message" msgstr "消息" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1686 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1686 msgid "More filters" msgstr "更多筛选" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:37 msgid "Network → Firewall" msgstr "Network → Firewall" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:187 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:187 msgid "" "No WAN firewall zone found in /etc/config/firewall. Configure zones under" msgstr "在 /etc/config/firewall 中未找到 WAN 防火墙区域。请在以下位置配置区域" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:185 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:185 msgid "No WAN zone found" msgstr "No WAN zone found" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163 msgid "Not now" msgstr "Not now" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:220 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:220 msgid "Nothing changes until you click Enable." msgstr "Nothing changes until you click Enable." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 msgid "OUT" msgstr "出接口" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1610 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1610 msgid "One line" msgstr "单行" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206 msgid "Open firewall zone settings" msgstr "打开防火墙区域设置" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211 msgid "" "OpenWrt does not write firewall events to the log until you turn logging on. " "Live View only shows what the firewall already logs — it does not add allow/" @@ -371,44 +383,44 @@ msgstr "" "Live View only shows what the firewall already logs — it does not add allow/" "deny rules." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1638 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1638 msgid "Palette" msgstr "Palette" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1560 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1560 msgid "Pause" msgstr "暂停" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 msgid "Paused" msgstr "已暂停" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 msgid "Proto" msgstr "协议" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1674 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1674 msgid "Protocol — common values" msgstr "协议 — 常用值" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1656 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1656 msgid "Quick search" msgstr "快速搜索" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91 msgid "Remove filter" msgstr "移除筛选" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:995 msgid "Resume" msgstr "ç»§ç»­" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1631 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1631 msgid "Row tint" msgstr "行染色" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1721 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1721 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " @@ -417,78 +429,86 @@ msgstr "" "勾选后行着色显示放行/拒绝行背景。可选经典(绿/红,默认)或无障碍(青绿/橙)。" "操作文本在两种模式下均保持着色。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1550 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1550 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." msgstr "行染色使用了本地颜色回退,因为当前 LuCI 主题未应用放行/拒绝背景色。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:33 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:33 msgid "Rule" msgstr "规则" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:527 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:527 msgid "Rule labels incomplete — map truncated" msgstr "Rule labels incomplete — map truncated" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:531 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:531 msgid "Rule labels unavailable" msgstr "Rule labels unavailable" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:529 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:529 msgid "Rule labels unavailable — temp file failed" msgstr "Rule labels unavailable — temp file failed" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:980 +msgid "Server shedding — at most %d log lines per poll." +msgstr "服务器正在削减 — 每次轮询最多 %d 行日志。" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982 +msgid "Server truncated this poll (adaptive cap)." +msgstr "服务器已截断本次轮询(自适应上限)。" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 msgid "SPort" msgstr "源端口" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1650 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1650 msgid "Show hostnames" msgstr "显示主机名" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1629 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1629 msgid "Show pass/deny row background colors" msgstr "显示放行/拒绝行背景色" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1576 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1576 msgid "Simple" msgstr "简洁" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 msgid "Source" msgstr "源地址" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1689 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1689 msgid "Source IP contains (! to exclude)" msgstr "源 IP å" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690 msgid "Source port (! to exclude)" msgstr "源端口(! 表示排除)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1717 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1717 msgid "" "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt " "defaults to 10/minute when no explicit limit is configured; fwlive does not " "impose this cap." msgstr "WAN 日志显示的速率是防火墙区域的 log_limit。未" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1714 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1714 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." msgstr "防火墙写" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551 msgid "Theme tint fallback" msgstr "主题着色回退" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:31 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:31 msgid "Time" msgstr "时间" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219 msgid "" "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " @@ -498,24 +518,24 @@ msgstr "" "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " "LAN browsing is not logged." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 msgid "Undo:" msgstr "Undo:" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1722 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1722 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "使用「详细」可查看全部列(标志、长度、原始消息)。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1564 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1568 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1564 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1568 msgid "View" msgstr "视图" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:615 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:615 msgid "WAN drop/reject logging is off." msgstr "WAN drop/reject logging is off." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204 msgid "" "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "here. Normal LAN browsing will not." @@ -523,7 +543,7 @@ msgstr "" "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "here. Normal LAN browsing will not." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:579 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:579 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." @@ -531,157 +551,165 @@ msgstr "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:581 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:581 msgid "WAN logging is already enabled." msgstr "WAN 日志已处于开启状态。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:88 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:88 msgid "WAN logging on" msgstr "WAN 日志:开" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:94 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:94 msgid "WAN logging on (%s). Click to disable." msgstr "WAN 日志:开(%s)。点击可" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77 msgid "WAN logging unavailable: missing kernel log modules" msgstr "WAN 日志不可用:缺少å" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:70 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:70 msgid "WAN logging unavailable: no WAN zone" msgstr "WAN 日志不可用:无 WAN 区域" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203 msgid "Waiting for firewall events" msgstr "Waiting for firewall events" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1545 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1545 msgid "Watching" msgstr "监视中" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1603 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1603 msgid "Wrap" msgstr "自动换行" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:123 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:123 msgid "allow/deny rules, LAN logging, or anything else." msgstr "allow/deny rules, LAN logging, or anything else." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:828 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:828 msgid "buffer full" msgstr "缓冲区已满" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81 msgid "default 10/minute" msgstr "默认 10 条/分钟" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28 msgid "is" msgstr "是" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:823 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:823 msgid "loading buffer" msgstr "正在加载缓冲区" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:923 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:923 msgid "loading…" msgstr "loading…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44 msgid "not" msgstr "非" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1107 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1107 msgid "not AH" msgstr "非 AH" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1106 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1106 msgid "not ESP" msgstr "非 ESP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1105 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1105 msgid "not GRE" msgstr "非 GRE" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1102 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1102 msgid "not ICMP" msgstr "非 ICMP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1103 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1103 msgid "not ICMPv6" msgstr "非 ICMPv6" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1104 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1104 msgid "not IGMP" msgstr "非 IGMP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1108 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1108 msgid "not SCTP" msgstr "非 SCTP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1100 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1100 msgid "not TCP" msgstr "非 TCP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1101 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1101 msgid "not UDP" msgstr "非 UDP" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1666 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1666 msgid "not block" msgstr "非阻止" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1665 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1665 msgid "not drop" msgstr "非丢弃" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1664 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1664 msgid "not pass" msgstr "非放行" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1667 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1667 msgid "not reject" msgstr "非拒绝" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1668 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1668 msgid "not unknown" msgstr "非未知" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1679 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1679 msgid "or type…" msgstr "或输入…" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830 msgid "render paused (high rate)" msgstr "渲染暂停(速率过高)" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817 +msgid "resolve paused (load)" +msgstr "解析已暂停(负载)" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 msgid "scroll frozen — scroll to top to follow live" msgstr "scroll frozen — scroll to top to follow live" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118 msgid "sets log on the WAN firewall zone and reloads the firewall." msgstr "sets log on the WAN firewall zone and reloads the firewall." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:254 -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:258 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:254 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:258 msgid "then ping the router." msgstr "然后 ping 路由器。" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815 +msgid "truncated" +msgstr "已截断" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128 msgid "turn it back off with the WAN logging on control on the watch strip." msgstr "turn it back off with the WAN logging on control on the watch strip." -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:512 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:512 msgid "using fw4" msgstr "使用 fw4" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:510 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:510 msgid "using iptables" msgstr "使用 iptables" -#: /home/lalbers/gitroot/fwlive/openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89 msgid "· %s" msgstr "· %s" diff --git a/luci-app-mosdns/Makefile b/luci-app-mosdns/Makefile index b3be1173c..8211601cd 100644 --- a/luci-app-mosdns/Makefile +++ b/luci-app-mosdns/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-mosdns PKG_VERSION:=1.7.14 -PKG_RELEASE:=24 +PKG_RELEASE:=25 LUCI_TITLE:=LuCI Support for mosdns LUCI_PKGARCH:=all diff --git a/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc b/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc index 6fe17532b..7b1a1bcf9 100755 --- a/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc +++ b/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc @@ -72,7 +72,7 @@ function get_adlist() { let adblock = uci_cursor.get('mosdns', 'config', 'adblock'); if (adblock !== '1') { - mkdir('/etc/mosdns/rule', 0755); + mkdir('/var/mosdns', 0755); exec_sys('rm -rf /etc/mosdns/rule/adlist /etc/mosdns/rule/.ad_source'); writefile('/var/mosdns/disable-ads.txt', ''); print("/var/mosdns/disable-ads.txt\n"); diff --git a/luci-app-openlist2/Makefile b/luci-app-openlist2/Makefile index 357db008a..5b8a89abb 100644 --- a/luci-app-openlist2/Makefile +++ b/luci-app-openlist2/Makefile @@ -7,11 +7,11 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-openlist2 PKG_VERSION:=1.0.4 -PKG_RELEASE:=6 +PKG_RELEASE:=7 LUCI_TITLE:=LuCI support for openlist -# The virtual provide forces installation of a core package with the helper. -LUCI_DEPENDS:=+openlist2 +openlist2-helper +# Require a core package with both the restricted helper and automatic TLS. +LUCI_DEPENDS:=+openlist2 +openlist2-helper +openlist2-tls include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js b/luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js index b3fe25006..93a9db128 100644 --- a/luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +++ b/luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js @@ -545,7 +545,7 @@ return view.extend({ }; o = addOption('scheme', form.Value, 'listen_https_port', _('HTTPS listen port'), - _('The upstream default is -1 (disabled). Set a valid port number to enable HTTPS.')); + _('Set to -1 to disable HTTPS, or 1-65535 to enable it. Leave both certificate paths empty to use an automatically generated self-signed certificate.')); o.datatype = 'integer'; o.default = '-1'; o.rmempty = false; @@ -554,14 +554,15 @@ return view.extend({ return validateListenPort(value); }; - o = addOption('scheme', form.Flag, 'force_https', _('Force HTTPS')); + o = addOption('scheme', form.Flag, 'force_https', _('Force HTTPS'), + _('Redirect HTTP requests to HTTPS. The HTTPS listen port enables HTTPS independently of this option.')); o.rmempty = false; o = addOption('scheme', form.Value, 'ssl_cert', _('SSL cert'), - _('SSL certificate file path')); + _('Leave both paths empty to manage a self-signed certificate in the TLS subdirectory of the data directory. It is reused and renewed at startup when fewer than 30 days remain. Browsers do not trust self-signed certificates automatically.')); o = addOption('scheme', form.Value, 'ssl_key', _('SSL key'), - _('SSL key file path')); + _('For custom TLS, specify both certificate and private key paths. The certificate must be currently valid and match its private key. Custom files are never overwritten automatically.')); o = addOption('scheme', form.Value, 'listen_unix_file', _('Unix socket file')); @@ -572,7 +573,7 @@ return view.extend({ o.rmempty = false; o = addOption('scheme', form.Flag, 'listen_enable_h3', _('Enable HTTP/3/QUIC'), - _('Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled and certificate files must be configured.')); + _('Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled. Certificate settings are shared with HTTPS.')); o.rmempty = false; // tasks @@ -698,7 +699,8 @@ return view.extend({ o.default = 5246; o.rmempty = false; - o = addOption('s3', form.Flag, 's3_ssl', _('Enable SSL')); + o = addOption('s3', form.Flag, 's3_ssl', _('Enable SSL'), + _('Use the same custom or automatically generated certificate as HTTPS. Configure certificate paths on the Web Protocol tab.')); o.rmempty = false; // ftp @@ -708,7 +710,8 @@ return view.extend({ _('Ports or ranges to allow from WAN, for example 50000-50100. They must match the listening ports in OpenList passive port mapping. Empty means passive ports must be allowed manually.')); o.placeholder = '50000-50100'; o.depends({ ftp: '1', allow_wan: '1' }); - o = addOption('ftp', form.Flag, 'ftp', _('Enabled FTP')); + o = addOption('ftp', form.Flag, 'ftp', _('Enabled FTP'), + _('Enable FTP/SFTP access for the OpenList account in Management > Users. Also grant FTP/SFTP management permission to allow uploads and changes.')); o.rmempty = false; o = addOption('ftp', form.Value, 'ftp_port', _('FTP Port')); @@ -749,7 +752,8 @@ return view.extend({ o.rmempty = false; // sftp - o = addOption('sftp', form.Flag, 'sftp', _('Enabled SFTP')); + o = addOption('sftp', form.Flag, 'sftp', _('Enabled SFTP'), + _('Enable FTP/SFTP access for the OpenList account in Management > Users. Also grant FTP/SFTP management permission to allow uploads and changes.')); o.rmempty = false; o = addOption('sftp', form.Value, 'sftp_port', _('SFTP Port')); @@ -764,7 +768,7 @@ return view.extend({ o = addOption('mcp', form.DummyValue, '_mcp_endpoint', _('MCP endpoint'), - _('Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList user login token; retain MCP-Session-Id for subsequent requests.')); + _('Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList administrator login token; retain MCP-Session-Id for subsequent requests.')); o.depends('mcp', '1'); o.renderWidget = () => E('div', {}, [ E('code', {}, backendUrl('mcp') || _('Configure a web port or Site URL.')), @@ -793,26 +797,48 @@ return view.extend({ if (dist && (containsPath(dist, data) || containsPath(dist, cache) || containsPath(cache, dist) || containsPath(dist, index) || containsPath(index, dist))) return _('Frontend files must be separate from private data, cache and index files.'); + if (tlsRequired(section_id) && !fieldValue(section_id, 'ssl_cert') && !fieldValue(section_id, 'ssl_key')) { + const tls = data + '/tls'; + for (const directory of [cache, index, dist].filter(Boolean)) + if (containsPath(tls, directory) || containsPath(directory, tls)) + return _('Automatic TLS directory must be separate from cache, index and public frontend directories.'); + } return true; }; for (const name of ['data_dir', 'temp_dir', 'bleve_dir', 'dist_dir']) fields[name].validate = (section_id, value) => validateDirectories(section_id, name, value); - const tlsRequired = section_id => isEnabledPort(fieldValue(section_id, 'listen_https_port', '-1')) || - (fieldValue(section_id, 's3') === '1' && fieldValue(section_id, 's3_ssl') === '1'); - for (const name of ['ssl_cert', 'ssl_key']) { - fields[name].validate = (section_id, value) => { - if (tlsRequired(section_id) && !String(value || '').trim()) - return _('Certificate and key paths are required for HTTPS or S3 SSL.'); - return validateAbsolutePath(value, false); - }; - } + const tlsRequired = section_id => { + const httpsPort = fieldValue(section_id, 'listen_https_port', '-1'); + const s3Enabled = fieldValue(section_id, 's3') === '1'; + const s3Ssl = fieldValue(section_id, 's3_ssl') === '1'; + return isEnabledPort(httpsPort) || (s3Enabled && s3Ssl); + }; + const validateCertificatePaths = (section_id, changed, value) => { + if (!tlsRequired(section_id)) + return true; + const cert = changed === 'ssl_cert' ? value : fieldValue(section_id, 'ssl_cert'); + const key = changed === 'ssl_key' ? value : fieldValue(section_id, 'ssl_key'); + if (!cert && !key) + return true; + if (!cert || !key) + return _('Leave both certificate paths empty for automatic TLS, or provide both custom certificate and key paths.'); + for (const path of [cert, key]) { + const valid = validateAbsolutePath(path, true); + if (valid !== true) + return valid; + } + return true; + }; + for (const name of ['ssl_cert', 'ssl_key']) + fields[name].validate = (section_id, value) => validateCertificatePaths(section_id, name, value); + // LuCI passes input.value (always "1") for checkbox validation. + // Read formvalue() to distinguish checked and unchecked flags. for (const name of ['force_https', 'listen_enable_h3']) - fields[name].validate = (section_id, value) => value !== '1' || isEnabledPort(fieldValue(section_id, 'listen_https_port', '-1')) + fields[name].validate = section_id => fieldValue(section_id, name) !== '1' || isEnabledPort(fieldValue(section_id, 'listen_https_port', '-1')) ? true : _('Force HTTPS and HTTP/3 require an enabled HTTPS port.'); - fields.s3_ssl.validate = (section_id, value) => value !== '1' || fieldValue(section_id, 's3') !== '1' || - (fieldValue(section_id, 'ssl_cert').trim() && fieldValue(section_id, 'ssl_key').trim()) - ? true : _('Certificate and key paths are required for HTTPS or S3 SSL.'); + fields.s3_ssl.validate = section_id => fieldValue(section_id, 's3') !== '1' || fieldValue(section_id, 's3_ssl') !== '1' + ? true : validateCertificatePaths(section_id); fields.listen_unix_file.validate = (section_id, value) => validateAbsolutePath(value, false); fields.listen_unix_file_perm.validate = (section_id, value) => !value || /^[0-7]{3,4}$/.test(value) ? true : _('Use 3 or 4 octal digits, for example 660 or 0660.'); @@ -857,6 +883,17 @@ return view.extend({ }; for (const name of ['listen_http_port', 'listen_https_port', 's3_port', 'ftp_port', 'sftp_port', 'ftp_pasv_ports']) fields[name].validate = (section_id, value) => validatePorts(section_id, name, value); + const protocolFields = ['listen_http_port', 'listen_https_port', 'force_https', 'listen_enable_h3', + 'ssl_cert', 'ssl_key', 's3', 's3_ssl', 's3_port', 'ftp', 'ftp_port', 'sftp', 'sftp_port', + 'ftp_pasv_ports', 'allow_wan', 'data_dir', 'temp_dir', 'bleve_dir', 'dist_dir']; + const revalidateProtocolFields = (event, section_id) => { + for (const name of protocolFields) + if (fields[name].isActive(section_id)) + fields[name].triggerValidation(section_id); + ui.tabs.updateTabs(event, m.root); + }; + for (const name of protocolFields) + fields[name].onchange = revalidateProtocolFields; for (const name of ['ftp_pasv_ports', 'log_path', 'log_max_size', 'log_max_backups', 'log_max_age', 'log_compress', 'log_filter', 'log_filter_cidr', 'log_filter_path', 'log_filter_method']) fields[name].retain = true; diff --git a/luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/logs.js b/luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/logs.js index ad1b4260b..f78abd335 100644 --- a/luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/logs.js +++ b/luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/logs.js @@ -54,7 +54,7 @@ return view.extend({ const clearButton = E('button', { 'class': 'btn cbi-button-action', 'type': 'button', - 'disabled': !L.hasViewPermission(), + 'disabled': !L.hasViewPermission() || null, 'click': ui.createHandlerFn(this, 'handleCleanLogs') }, _('Clear current log')); diff --git a/luci-app-openlist2/po/templates/openlist2.po b/luci-app-openlist2/po/templates/openlist2.po index 4f5d961b9..394a9df87 100644 --- a/luci-app-openlist2/po/templates/openlist2.po +++ b/luci-app-openlist2/po/templates/openlist2.po @@ -439,19 +439,23 @@ msgid "HTTPS listen port" msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:548 -msgid "The upstream default is -1 (disabled). Set a valid port number to enable HTTPS." +msgid "Set to -1 to disable HTTPS, or 1-65535 to enable it. Leave both certificate paths empty to use an automatically generated self-signed certificate." msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:557 msgid "Force HTTPS" msgstr "" +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Redirect HTTP requests to HTTPS. The HTTPS listen port enables HTTPS independently of this option." +msgstr "" + #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:560 msgid "SSL cert" msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:561 -msgid "SSL certificate file path" +msgid "Leave both paths empty to manage a self-signed certificate in the TLS subdirectory of the data directory. It is reused and renewed at startup when fewer than 30 days remain. Browsers do not trust self-signed certificates automatically." msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:563 @@ -459,7 +463,7 @@ msgid "SSL key" msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:564 -msgid "SSL key file path" +msgid "For custom TLS, specify both certificate and private key paths. The certificate must be currently valid and match its private key. Custom files are never overwritten automatically." msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:566 @@ -483,7 +487,7 @@ msgid "Enable HTTP/3/QUIC" msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:575 -msgid "Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled and certificate files must be configured." +msgid "Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled. Certificate settings are shared with HTTPS." msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:579 @@ -632,6 +636,10 @@ msgstr "" msgid "Enabled FTP" msgstr "" +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Enable FTP/SFTP access for the OpenList account in Management > Users. Also grant FTP/SFTP management permission to allow uploads and changes." +msgstr "" + #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:714 msgid "FTP Port" msgstr "" @@ -689,7 +697,7 @@ msgid "MCP endpoint" msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:767 -msgid "Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList user login token; retain MCP-Session-Id for subsequent requests." +msgid "Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList administrator login token; retain MCP-Session-Id for subsequent requests." msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:770 @@ -718,7 +726,15 @@ msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:806 #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:815 -msgid "Certificate and key paths are required for HTTPS or S3 SSL." +msgid "Leave both certificate paths empty for automatic TLS, or provide both custom certificate and key paths." +msgstr "" + +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Automatic TLS directory must be separate from cache, index and public frontend directories." +msgstr "" + +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Use the same custom or automatically generated certificate as HTTPS. Configure certificate paths on the Web Protocol tab." msgstr "" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:812 diff --git a/luci-app-openlist2/po/zh_Hans/openlist2.po b/luci-app-openlist2/po/zh_Hans/openlist2.po index 84a970cec..9be20e329 100644 --- a/luci-app-openlist2/po/zh_Hans/openlist2.po +++ b/luci-app-openlist2/po/zh_Hans/openlist2.po @@ -440,28 +440,32 @@ msgid "HTTPS listen port" msgstr "HTTPS 监听端口" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:548 -msgid "The upstream default is -1 (disabled). Set a valid port number to enable HTTPS." -msgstr "上游默认值为 -1(禁用)。设为有效端口号可启用 HTTPS。" +msgid "Set to -1 to disable HTTPS, or 1-65535 to enable it. Leave both certificate paths empty to use an automatically generated self-signed certificate." +msgstr "设为 -1 可禁用 HTTPS,设为 1–65535 可启用 HTTPS。证书和私钥路径同时留空时,使用自动生成的自签名证书。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:557 msgid "Force HTTPS" msgstr "强制 HTTPS" +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Redirect HTTP requests to HTTPS. The HTTPS listen port enables HTTPS independently of this option." +msgstr "将 HTTP 请求重定向到 HTTPS。是否启用 HTTPS 由 HTTPS 监听端口决定,与此选项独立。" + #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:560 msgid "SSL cert" msgstr "SSL 证书" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:561 -msgid "SSL certificate file path" -msgstr "SSL 证书文件路径" +msgid "Leave both paths empty to manage a self-signed certificate in the TLS subdirectory of the data directory. It is reused and renewed at startup when fewer than 30 days remain. Browsers do not trust self-signed certificates automatically." +msgstr "两项路径同时留空时,在数据目录的 tls 子目录中自动管理自签名证书。有效证书会复用,剩余有效期不足 30 天时在启动时更新。浏览器不会自动信任自签名证书。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:563 msgid "SSL key" msgstr "SSL 密钥" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:564 -msgid "SSL key file path" -msgstr "SSL 密钥文件路径" +msgid "For custom TLS, specify both certificate and private key paths. The certificate must be currently valid and match its private key. Custom files are never overwritten automatically." +msgstr "使用自定义证书时,请同时填写证书和私钥路径。证书必须在有效期内且与私钥匹配。自定义文件不会被自动覆盖。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:566 msgid "Unix socket file" @@ -484,8 +488,8 @@ msgid "Enable HTTP/3/QUIC" msgstr "启用 HTTP/3/QUIC" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:575 -msgid "Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled and certificate files must be configured." -msgstr "在 HTTPS 监听端口上启用基于 QUIC 的 HTTP/3。必须先启用 HTTPS 并配置证书文件。" +msgid "Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled. Certificate settings are shared with HTTPS." +msgstr "在 HTTPS 监听端口上启用基于 QUIC 的 HTTP/3。必须先启用 HTTPS,并与 HTTPS 共用证书设置。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:579 msgid "Open traffic settings" @@ -633,6 +637,10 @@ msgstr "允许从 WAN 访问的端口或范围,例如 50000-50100,必须与 msgid "Enabled FTP" msgstr "启用 FTP" +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Enable FTP/SFTP access for the OpenList account in Management > Users. Also grant FTP/SFTP management permission to allow uploads and changes." +msgstr "还需在 OpenList 管理后台的“用户”中为账号开启 FTP/SFTP 访问权限;上传和修改文件还需要 FTP/SFTP 管理权限。" + #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:714 msgid "FTP Port" msgstr "FTP 端口" @@ -690,8 +698,8 @@ msgid "MCP endpoint" msgstr "MCP 端点" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:767 -msgid "Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList user login token; retain MCP-Session-Id for subsequent requests." -msgstr "使用 Streamable HTTP,认证头为 Authorization: TOKEN,不添加 Bearer 前缀。TOKEN 为 OpenList 用户登录令牌,后续请求需保留 MCP-Session-Id。" +msgid "Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList administrator login token; retain MCP-Session-Id for subsequent requests." +msgstr "使用 Streamable HTTP,认证头为 Authorization: TOKEN,不添加 Bearer 前缀。TOKEN 必须为 OpenList 管理员登录令牌,后续请求需保留 MCP-Session-Id。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:770 msgid "Configure a web port or Site URL." @@ -719,8 +727,16 @@ msgstr "前端文件必须与私有数据、缓存和索引分开。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:806 #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:815 -msgid "Certificate and key paths are required for HTTPS or S3 SSL." -msgstr "启用 HTTPS 或 S3 SSL 时必须填写证书和私钥路径。" +msgid "Leave both certificate paths empty for automatic TLS, or provide both custom certificate and key paths." +msgstr "请将证书和私钥路径同时留空以自动生成证书,或同时填写自定义证书和私钥路径。" + +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Automatic TLS directory must be separate from cache, index and public frontend directories." +msgstr "自动 TLS 目录必须与缓存、索引和公开前端目录分开。" + +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Use the same custom or automatically generated certificate as HTTPS. Configure certificate paths on the Web Protocol tab." +msgstr "与 HTTPS 共用自定义或自动生成的证书。请在“Web 协议”选项卡配置证书路径。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:812 #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:855 diff --git a/luci-app-openlist2/po/zh_Hant/openlist2.po b/luci-app-openlist2/po/zh_Hant/openlist2.po index ee8efc758..0d7e090d3 100644 --- a/luci-app-openlist2/po/zh_Hant/openlist2.po +++ b/luci-app-openlist2/po/zh_Hant/openlist2.po @@ -440,28 +440,32 @@ msgid "HTTPS listen port" msgstr "HTTPS 監聽埠" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:548 -msgid "The upstream default is -1 (disabled). Set a valid port number to enable HTTPS." -msgstr "上游預設值為 -1(停用)。設為有效埠號可啟用 HTTPS。" +msgid "Set to -1 to disable HTTPS, or 1-65535 to enable it. Leave both certificate paths empty to use an automatically generated self-signed certificate." +msgstr "設為 -1 可停用 HTTPS,設為 1–65535 可啟用 HTTPS。憑證與私密金鑰路徑同時留空時,使用自動產生的自簽憑證。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:557 msgid "Force HTTPS" msgstr "強制 HTTPS" +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Redirect HTTP requests to HTTPS. The HTTPS listen port enables HTTPS independently of this option." +msgstr "將 HTTP 請求重新導向至 HTTPS。是否啟用 HTTPS 由 HTTPS 監聽埠決定,與此選項獨立。" + #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:560 msgid "SSL cert" msgstr "SSL 證書" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:561 -msgid "SSL certificate file path" -msgstr "SSL 證書檔案路徑" +msgid "Leave both paths empty to manage a self-signed certificate in the TLS subdirectory of the data directory. It is reused and renewed at startup when fewer than 30 days remain. Browsers do not trust self-signed certificates automatically." +msgstr "兩項路徑同時留空時,在資料目錄的 tls 子目錄中自動管理自簽憑證。有效憑證會重複使用,剩餘有效期不足 30 天時於啟動時更新。瀏覽器不會自動信任自簽憑證。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:563 msgid "SSL key" msgstr "SSL 金鑰" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:564 -msgid "SSL key file path" -msgstr "SSL 金鑰檔案路徑" +msgid "For custom TLS, specify both certificate and private key paths. The certificate must be currently valid and match its private key. Custom files are never overwritten automatically." +msgstr "使用自訂憑證時,請同時填寫憑證與私密金鑰路徑。憑證必須在有效期內且與私密金鑰相符。自訂檔案不會被自動覆寫。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:566 msgid "Unix socket file" @@ -484,8 +488,8 @@ msgid "Enable HTTP/3/QUIC" msgstr "啟用 HTTP/3/QUIC" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:575 -msgid "Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled and certificate files must be configured." -msgstr "在 HTTPS 監聽連接埠上啟用基於 QUIC 的 HTTP/3。必須先啟用 HTTPS 並配置憑證檔案。" +msgid "Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled. Certificate settings are shared with HTTPS." +msgstr "在 HTTPS 監聽埠上啟用基於 QUIC 的 HTTP/3。必須先啟用 HTTPS,並與 HTTPS 共用憑證設定。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:579 msgid "Open traffic settings" @@ -633,6 +637,10 @@ msgstr "允許從 WAN 存取的連接埠或範圍,例如 50000-50100,必須 msgid "Enabled FTP" msgstr "啟用 FTP" +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Enable FTP/SFTP access for the OpenList account in Management > Users. Also grant FTP/SFTP management permission to allow uploads and changes." +msgstr "還需在 OpenList 管理後台的「使用者」中為帳號開啟 FTP/SFTP 存取權限;上傳和修改檔案還需要 FTP/SFTP 管理權限。" + #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:714 msgid "FTP Port" msgstr "FTP 埠" @@ -690,8 +698,8 @@ msgid "MCP endpoint" msgstr "MCP 端點" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:767 -msgid "Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList user login token; retain MCP-Session-Id for subsequent requests." -msgstr "使用 Streamable HTTP,認證標頭為 Authorization: TOKEN,不加 Bearer 前綴。TOKEN 為 OpenList 使用者登入權杖,後續請求須保留 MCP-Session-Id。" +msgid "Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList administrator login token; retain MCP-Session-Id for subsequent requests." +msgstr "使用 Streamable HTTP,認證標頭為 Authorization: TOKEN,不加 Bearer 前綴。TOKEN 必須為 OpenList 管理員登入權杖,後續請求須保留 MCP-Session-Id。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:770 msgid "Configure a web port or Site URL." @@ -719,8 +727,16 @@ msgstr "前端檔案必須與私人資料、快取及索引分開。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:806 #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:815 -msgid "Certificate and key paths are required for HTTPS or S3 SSL." -msgstr "啟用 HTTPS 或 S3 SSL 時必須填寫憑證及私密金鑰路徑。" +msgid "Leave both certificate paths empty for automatic TLS, or provide both custom certificate and key paths." +msgstr "請將憑證與私密金鑰路徑同時留空以自動產生憑證,或同時填寫自訂憑證與私密金鑰路徑。" + +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Automatic TLS directory must be separate from cache, index and public frontend directories." +msgstr "自動 TLS 目錄必須與快取、索引及公開前端目錄分開。" + +#: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js +msgid "Use the same custom or automatically generated certificate as HTTPS. Configure certificate paths on the Web Protocol tab." +msgstr "與 HTTPS 共用自訂或自動產生的憑證。請在「Web 協定」分頁設定憑證路徑。" #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:812 #: luci-app-openlist2/htdocs/luci-static/resources/view/openlist2/basic.js:855 diff --git a/luci-app-oui/.gitattributes b/luci-app-oui/.gitattributes new file mode 100644 index 000000000..882042b18 --- /dev/null +++ b/luci-app-oui/.gitattributes @@ -0,0 +1,2 @@ +# Preserve the original supplemental SVG bytes and source SHA256. +htdocs/luci-static/resources/oui/midea.svg -text whitespace=cr-at-eol diff --git a/luci-app-oui/Makefile b/luci-app-oui/Makefile new file mode 100644 index 000000000..949b2fe24 --- /dev/null +++ b/luci-app-oui/Makefile @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: GPL-2.0-only +include $(TOPDIR)/rules.mk + +PKG_VERSION:=1.0.6 +PKG_RELEASE:=1 +PKG_LICENSE:=GPL-2.0-only AND CC0-1.0 AND MIT AND LicenseRef-Public-Domain AND LicenseRef-fnOS-Trademark +LUCI_MAINTAINER:=coolsnowwolf + +LUCI_TITLE:=Local OUI vendor icons for DHCP leases +LUCI_DEPENDS:=+luci-base +LUCI_PKGARCH:=all + +define Package/luci-app-oui/conffiles +/etc/config/oui +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-oui/README.md b/luci-app-oui/README.md new file mode 100644 index 000000000..725bb83cd --- /dev/null +++ b/luci-app-oui/README.md @@ -0,0 +1,87 @@ +# luci-app-oui + +Optional local vendor SVG icons beside DHCP lease hostnames. Install together with +this tree's luci-base / luci-mod-status integration. No daemon, external lookup, +or scheduled download. Optional device overrides are available for devices using OEM network modules. + +The normal LuCI feature response checks for the installed helper. Without the +package, the homepage neither loads its JavaScript nor fetches the OUI database +or icons. With it, hostnames render immediately; icons are added asynchronously. +The database is fetched once per page, including caching failures, and its hash +in the filename invalidates browser caches after data updates. SVG requests are +shared by brand and use normal HTTP caching. The resident prefix table bounds +memory usage; no per-client MAC history is stored. No MAC is sent off-router. + +Data comes from [WH-2099/macdb](https://github.com/WH-2099/macdb), which combines +IEEE MA-L, MA-M and MA-S assignments. Longest-prefix matching supports 24/28/36 +bits. Multicast, locally administered/randomized and malformed MACs are ignored. +Unknown vendors, absent/random MACs and brands without a bundled icon use a +small generic desktop SVG. It is an original GPL-2.0-only asset, not a brand logo. +The icon identifies the registered MAC vendor: a PC using an Intel NIC can show +Intel rather than the motherboard manufacturer. DHCP hostnames are not used to +guess brands. + +Hosts with an open TCP port 5666 or 5667 in the existing LAN web-probe cache +show the fnOS logo instead of the OUI icon. The probe must be ready and match +the current IP and MAC; any active IPv4 address of a merged client can qualify. +This is a port-based display heuristic, not OS authentication. The vector logo +was traced from the supplied `fnos.PNG`; see `NOTICE.fnos`. + +Most icons are an unmodified subset of +[Simple Icons 11.15.0](https://github.com/simple-icons/simple-icons/tree/11.15.0), +pinned to retain brands removed from newer releases (including Microsoft). +The included Simple Icons CC0 license and disclaimer and macdb MIT license travel +with the package. Not all hardware brands exist in this snapshot: for example, +ASRock and Realme have no bundled official Simple Icons asset. They are +not relabeled as another manufacturer. GIGABYTE uses a supplemental public-domain +wordmark; its source and attribution are recorded in `NOTICE.supplemental`. + +## Updating the subset + +On a development machine with Python 3, unpack Simple Icons 11.15.0 and a macdb +snapshot, then run: + +```sh +python3 tools/generate.py /path/to/macdb /path/to/simple-icons-11.15.0 +``` + +`tools/vendors.json` contains normalized manufacturer alias patterns. Normalizing +case and punctuation merges ASUSTek COMPUTER INC., ASUSTeK Computer Inc., ASUS and +ASUS COMPUTER INTERNATIONAL under ASUS; patterns are anchored to avoid accidental +matches such as Pegasus or Asustor. Ambiguous matches abort generation. +`tools/sources.json` records the input CSV SHA256, source URLs, exact observed +aliases and generated database filename. Review these with any regenerated data. +Generation also updates the helper's database URL. Bump the package version and +the homepage helper query version when changing the helper or generated data. +No source downloads or Python dependency are needed when building/installing. + +Run the regression checks with `python3 tools/test_generate.py` and +`node tools/test_oui.js`. + +## OEM devices and supplemental brands + +Midea and TCL SVGs supplement the Simple Icons subset. Their individual sources, +authors and hashes are recorded in `tools/supplemental.json` and the packaged +`NOTICE.supplemental`. These are not claimed to be Simple Icons assets. + +For a known device whose MAC belongs to a module maker, add an exact device entry +to `/etc/config/oui` (vendor is the icon slug): + +```uci +config device + option mac '00:11:22:33:44:55' + option vendor 'tcl' +``` + +Only that full MAC is overridden, never the module maker's entire prefix. The +configuration is preserved on upgrades and read once asynchronously through the +authenticated UCI RPC. It is not published as a static file, and no device-specific +MAC is shipped in the package. Reload the homepage after changing it. + +Imilab / Chuangmi manufacturer aliases are intentionally grouped under Xiaomi +for Mi Home cameras, including all matching OUI assignments. This is a product +ecosystem grouping rather than a claim that the registered companies are identical. + +Hui Zhou / Huizhou Gaoshengda aliases are intentionally grouped under TCL. This +OEM heuristic applies to all registered prefixes for that module maker, including +0C:79:55. A device override can correct exceptions using the same OEM modules. diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/DISCLAIMER.md b/luci-app-oui/htdocs/luci-static/resources/oui/DISCLAIMER.md new file mode 100644 index 000000000..f1429e9eb --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/DISCLAIMER.md @@ -0,0 +1,54 @@ +# Disclaimer + +Simple Icons asks that its users read this disclaimer fully before including an icon in their project. + +## Table of Contents + +- [Licenses, Copyrights & Trademarks](#licenses-copyrights--trademarks) +- [Brand Guidelines](#brand-guidelines) +- [Update of Brands](#update-of-brands) +- [Removal of Brands](#removal-of-brands) + +## Licenses, Copyrights & Trademarks + +> [!IMPORTANT]\ +> The addition of licenses to Simple Icons is an ongoing project. Hence, the absence of licence data for a particular icon does not imply that the icon is not released under a license. + +> [!NOTE]\ +> Simple Icons is released under CC0 - though that doesn't mean to imply that all icons within the project are also CC0. Please see individual licenses where available. + +Simple Icons provides data on the license under which icons are available. We ask users to carefully consider this when using an icon. As licenses are subject to change we also ask our users to regularly check if the license of the icons they use have been changed. + +The license under which an icon is available can be found in the icon's entry in the JSON data file in the NPM and Packagist packages, or through the license link on the icon's card on [the Simple Icons website]. + +As we rely largely on our community to help us keep Simple Icons up-to-date, the license data for an icon may be outdated. If you find that the license data provided is outdated, please [submit an issue][icon-outdated-issues] to [the Simple Icons GitHub repository]. + +If an icon includes a registered trademark (`®`) or trademark symbol (`™`) the recommendations outlined in [the Simple Icons Contributing Guidelines] are followed to decide whether to include the symbol or not. + +Simple Icons cannot be held responsible for any legal activity raised by a brand, or users of the package. We ask that our users seek the correct permissions to use the icons relevant to their project. + +## Brand Guidelines + +> [!NOTE]\ +> The addition of guidelines to Simple Icons is an ongoing project. In the meantime, users of Simple Icons are instead encouraged to check the `source` URL as, in some cases, the icon will have been sourced from official guidelines. The lack of a `guidelines` entry for a particular brand does not imply that the brand has no guidelines. + +Simple Icons provides a link to a brand's _branding guidelines_ (or similar) if the brand provides one. We ask our users read these guidelines and ensure their usage of the brand's icon is in accordance with them. As guidelines are subject to change we also ask our users to regularly check if the brand guidelines of the icons they use have been updated. + +The brand guidelines can be found in the icon's entry in the JSON data file in the NPM and Packagist packages, or through the "Brand Guidelines" link on the icon's card on [the Simple Icons website]. + +As we rely largely on our community to help us keep Simple Icons up-to-date, the guidelines link for a brand may be outdated. If you find that the guidelines are outdated, please [submit an issue][icon-outdated-issues] to [the Simple Icons GitHub repository]. + +## Update of Brands + +Should a brand wish for their icon or data to be updated, please [submit an issue][icon-outdated-issues] to [the Simple Icons GitHub repository]. In the issue, please explain your affiliation with the company and reasons for the update. We generally release updates once a week, but can make exceptions for immediate updates of brands. + +## Removal of Brands + +Should a brand wish for their icon to be removed from the package, contact `removals at simpleicons dot org` explaining your affiliation with the company, and reasons for removal. Alternatively, it is also possible to [submit an issue][removal-issues] on [the Simple Icons GitHub repository] with the same information. We generally remove icons that no longer [meet our criteria] twice a year in our major releases - but can occasionally make exceptions for immediate removal of brands. + +[meet our criteria]: https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#requesting-an-icon +[icon-outdated-issues]: https://github.com/simple-icons/simple-icons/issues/new?assignees=&labels=update+icon%2Fdata&template=icon_update.yml&title=Update%3A+ +[removal-issues]: https://github.com/simple-icons/simple-icons/issues/new?assignees=&labels=breaking+change&template=icon_removal.yml&title=Remove%3A+ +[the simple icons contributing guidelines]: https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#icon-guidelines +[the simple icons github repository]: https://github.com/simple-icons/simple-icons +[the simple icons website]: https://simpleicons.org/ diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/LICENSE.macdb b/luci-app-oui/htdocs/luci-static/resources/oui/LICENSE.macdb new file mode 100644 index 000000000..92b8e1267 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/LICENSE.macdb @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 WH-2099 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/LICENSE.md b/luci-app-oui/htdocs/luci-static/resources/oui/LICENSE.md new file mode 100644 index 000000000..f0f4b0328 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/LICENSE.md @@ -0,0 +1,30 @@ +# CC0 1.0 Universal + +## Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an “owner”) of an original work of authorship and/or a database (each, a “Work”). + +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works (“Commons”) that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the “Affirmer”), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights (“Copyright and Related Rights”). Copyright and Related Rights include, but are not limited to, the following: + 1. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; + 2. moral rights retained by the original author(s) and/or performer(s); + 3. publicity and privacy rights pertaining to a person’s image or likeness depicted in a Work; + 4. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(i), below; + 5. rights protecting the extraction, dissemination, use and reuse of data in a Work; + 6. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and + 7. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer’s heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer’s express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer’s express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer’s Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the “License”). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer’s express Statement of Purpose. + +4. Limitations and Disclaimers. + 1. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. + 2. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. + 3. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person’s Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. + 4. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + +For more information, please see https://creativecommons.org/publicdomain/zero/1.0/. diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/NOTICE.fnos b/luci-app-oui/htdocs/luci-static/resources/oui/NOTICE.fnos new file mode 100644 index 000000000..347b7a3f1 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/NOTICE.fnos @@ -0,0 +1,4 @@ +fnOS (飞牛OS) logo, traced from the user-provided fnos.PNG reference. +Reference SHA256: 075d2d9505d3d53b08085dd8f06061ddb1f504b0f014c897ef944288501f1f7f +Vectorized with Potrace 1.16; original blue and transparent background retained. +The logo and trademark belong to their respective owner. diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/NOTICE.supplemental b/luci-app-oui/htdocs/luci-static/resources/oui/NOTICE.supplemental new file mode 100644 index 000000000..4643c4339 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/NOTICE.supplemental @@ -0,0 +1,22 @@ +Midea, TCL and GIGABYTE are supplemental SVGs, not Simple Icons assets. +Sources and attribution: +{ + "midea": { + "source": "https://commons.wikimedia.org/wiki/File:Midea.svg", + "sha256": "cd91efe06bdcb95055255d16b6a908e5dfba7530f92d7531f01b9e6f79f4fa6e", + "author": "Midea Group", + "license": "Public domain simple shape/text logo; trademark retained" + }, + "tcl": { + "source": "https://commons.wikimedia.org/wiki/File:Logo_of_the_TCL_Corporation.svg", + "sha256": "949117dc99503fe4a382afed3bd7f69fbfe61a6da91236b6842d8a4991df7fa8", + "author": "TCL Electronics", + "license": "Public domain simple shape/text logo; trademark retained" + }, + "gigabyte": { + "source": "https://commons.wikimedia.org/wiki/File:Gigabyte_Technology_logo_20080107.svg", + "sha256": "49693ef6522f58af4e059afe18c1fb3ddc8e6a5b15ac724d6a7493f47d84d849", + "author": "Koman90", + "license": "Public domain simple shape/text logo; trademark retained" + } +} diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/acer.svg b/luci-app-oui/htdocs/luci-static/resources/oui/acer.svg new file mode 100644 index 000000000..ca9b2bbf7 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/acer.svg @@ -0,0 +1 @@ +Acer \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/amazon.svg b/luci-app-oui/htdocs/luci-static/resources/oui/amazon.svg new file mode 100644 index 000000000..70316babd --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/amazon.svg @@ -0,0 +1 @@ +Amazon \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/apple.svg b/luci-app-oui/htdocs/luci-static/resources/oui/apple.svg new file mode 100644 index 000000000..de4b0d17b --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/apple.svg @@ -0,0 +1 @@ +Apple \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/asus.svg b/luci-app-oui/htdocs/luci-static/resources/oui/asus.svg new file mode 100644 index 000000000..18ef199da --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/asus.svg @@ -0,0 +1 @@ +ASUS \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/broadcom.svg b/luci-app-oui/htdocs/luci-static/resources/oui/broadcom.svg new file mode 100644 index 000000000..8c75d7bb7 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/broadcom.svg @@ -0,0 +1 @@ +Broadcom \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/computer.svg b/luci-app-oui/htdocs/luci-static/resources/oui/computer.svg new file mode 100644 index 000000000..a81e237e6 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/computer.svg @@ -0,0 +1 @@ + diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/dell.svg b/luci-app-oui/htdocs/luci-static/resources/oui/dell.svg new file mode 100644 index 000000000..d272bbf6f --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/dell.svg @@ -0,0 +1 @@ +Dell \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/fnos.svg b/luci-app-oui/htdocs/luci-static/resources/oui/fnos.svg new file mode 100644 index 000000000..e57639918 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/fnos.svg @@ -0,0 +1 @@ + diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/framework.svg b/luci-app-oui/htdocs/luci-static/resources/oui/framework.svg new file mode 100644 index 000000000..6f851ffba --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/framework.svg @@ -0,0 +1 @@ +Framework \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/fujitsu.svg b/luci-app-oui/htdocs/luci-static/resources/oui/fujitsu.svg new file mode 100644 index 000000000..70efe6752 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/fujitsu.svg @@ -0,0 +1 @@ +Fujitsu \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/gigabyte.svg b/luci-app-oui/htdocs/luci-static/resources/oui/gigabyte.svg new file mode 100644 index 000000000..a57862cf2 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/gigabyte.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/google.svg b/luci-app-oui/htdocs/luci-static/resources/oui/google.svg new file mode 100644 index 000000000..2eaf91554 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/google.svg @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/honor.svg b/luci-app-oui/htdocs/luci-static/resources/oui/honor.svg new file mode 100644 index 000000000..dfe61e15f --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/honor.svg @@ -0,0 +1 @@ +Honor \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/hp.svg b/luci-app-oui/htdocs/luci-static/resources/oui/hp.svg new file mode 100644 index 000000000..04b5f0ef8 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/hp.svg @@ -0,0 +1 @@ +HP \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/huawei.svg b/luci-app-oui/htdocs/luci-static/resources/oui/huawei.svg new file mode 100644 index 000000000..0c72bab82 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/huawei.svg @@ -0,0 +1 @@ +Huawei \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/intel.svg b/luci-app-oui/htdocs/luci-static/resources/oui/intel.svg new file mode 100644 index 000000000..47b79bf92 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/intel.svg @@ -0,0 +1 @@ +Intel \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/lenovo.svg b/luci-app-oui/htdocs/luci-static/resources/oui/lenovo.svg new file mode 100644 index 000000000..c03719a43 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/lenovo.svg @@ -0,0 +1 @@ +Lenovo \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/lg.svg b/luci-app-oui/htdocs/luci-static/resources/oui/lg.svg new file mode 100644 index 000000000..5eedc7978 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/lg.svg @@ -0,0 +1 @@ +LG \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/linksys.svg b/luci-app-oui/htdocs/luci-static/resources/oui/linksys.svg new file mode 100644 index 000000000..e23f8630d --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/linksys.svg @@ -0,0 +1 @@ +Linksys \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/mediatek.svg b/luci-app-oui/htdocs/luci-static/resources/oui/mediatek.svg new file mode 100644 index 000000000..f6d3d179b --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/mediatek.svg @@ -0,0 +1 @@ +MediaTek \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/microsoft.svg b/luci-app-oui/htdocs/luci-static/resources/oui/microsoft.svg new file mode 100644 index 000000000..eeacf2520 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/microsoft.svg @@ -0,0 +1 @@ +Microsoft \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/midea.svg b/luci-app-oui/htdocs/luci-static/resources/oui/midea.svg new file mode 100644 index 000000000..e2542bd76 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/midea.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/motorola.svg b/luci-app-oui/htdocs/luci-static/resources/oui/motorola.svg new file mode 100644 index 000000000..3e744bda2 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/motorola.svg @@ -0,0 +1 @@ +Motorola \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/msi.svg b/luci-app-oui/htdocs/luci-static/resources/oui/msi.svg new file mode 100644 index 000000000..6261c8426 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/msi.svg @@ -0,0 +1 @@ +MSI \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/netgear.svg b/luci-app-oui/htdocs/luci-static/resources/oui/netgear.svg new file mode 100644 index 000000000..e50a442de --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/netgear.svg @@ -0,0 +1 @@ +NETGEAR \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/nokia.svg b/luci-app-oui/htdocs/luci-static/resources/oui/nokia.svg new file mode 100644 index 000000000..a4a21137e --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/nokia.svg @@ -0,0 +1 @@ +Nokia \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/nvidia.svg b/luci-app-oui/htdocs/luci-static/resources/oui/nvidia.svg new file mode 100644 index 000000000..2c7ff66f8 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/nvidia.svg @@ -0,0 +1 @@ +NVIDIA \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/oneplus.svg b/luci-app-oui/htdocs/luci-static/resources/oui/oneplus.svg new file mode 100644 index 000000000..11c3131ad --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/oneplus.svg @@ -0,0 +1 @@ +OnePlus \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/oppo.svg b/luci-app-oui/htdocs/luci-static/resources/oui/oppo.svg new file mode 100644 index 000000000..b886af3e7 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/oppo.svg @@ -0,0 +1 @@ +OPPO \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/oui.js b/luci-app-oui/htdocs/luci-static/resources/oui/oui.js new file mode 100644 index 000000000..33db74e2a --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/oui.js @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +(function(window, document) { + 'use strict'; + if (window.luciOUI) + return; + + var base = document.currentScript.src.replace(/[^/]*$/, ''); + var database, pending, devices = Object.create(null); + + function normalize(mac) { + if (typeof mac !== 'string' || !/^(?:[\da-f]{12}|(?:[\da-f]{2}:){5}[\da-f]{2}|(?:[\da-f]{2}-){5}[\da-f]{2})$/i.test(mac)) + return null; + mac = mac.replace(/[:-]/g, '').toUpperCase(); + // Neither locally administered (randomized) nor multicast addresses have a reliable OUI. + return (parseInt(mac.slice(0, 2), 16) & 3) ? null : mac; + } + + function load() { + if (!pending) { + // One local request per page; failed requests are cached too, so polling never retries. + pending = new Promise(function(resolve) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', base + 'vendors-451e5311befc.json', true); + xhr.timeout = 5000; + xhr.onload = function() { + try { + if (xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + if (Array.isArray(data.vendors) && data.prefixes && typeof data.prefixes === 'object') + database = data; + } + } catch (e) { /* Leave the hostname unchanged on missing or invalid data. */ } + resolve(database); + }; + xhr.onerror = xhr.ontimeout = xhr.onabort = function() { resolve(null); }; + xhr.send(); + }); + } + return pending; + } + + function lookup(mac) { + mac = normalize(mac); + if (!mac || !database) + return null; + if (devices[mac]) { + for (var v = 0; v < database.vendors.length; v++) + if (database.vendors[v][0] === devices[mac]) + return database.vendors[v]; + } + // The resident prefix map also caches misses without retaining every client's MAC. + for (var i = 0, lengths = [9, 7, 6]; i < lengths.length; i++) { + var prefix = mac.slice(0, lengths[i]); + if (Object.prototype.hasOwnProperty.call(database.prefixes, prefix)) { + var index = database.prefixes[prefix]; + return index == null ? null : database.vendors[index]; + } + } + return null; + } + + function decorate(node, mac, fnos) { + if (node.querySelector('.luci-oui-icon')) + return; + var icon = document.createElement('img'); + icon.className = 'luci-oui-icon'; + icon.width = icon.height = 20; + icon.alt = ''; + icon.style.cssText = 'display:inline-block;width:20px;height:20px;object-fit:contain;vertical-align:middle;margin-inline-end:6px;background:#fff;border-radius:3px;padding:2px;box-sizing:content-box'; + function fallback() { + icon.title = 'Unknown vendor'; + icon.onerror = function() { icon.remove(); }; + icon.src = base + 'computer.svg'; + } + fallback(); + node.insertBefore(icon, node.firstChild); + if (fnos === true) { + icon.title = 'fnOS 飞牛OS'; + icon.onerror = fallback; + icon.src = base + 'fnos.svg'; + return; + } + // Random, malformed or absent addresses use the fallback without an OUI request. + if (!normalize(mac)) + return; + function apply() { + var vendor = lookup(mac); + if (!vendor || !/^[a-z0-9]+$/.test(vendor[0])) + return; + icon.title = vendor[1]; + icon.onerror = fallback; + icon.src = base + vendor[0] + '.svg'; + } + if (database) + apply(); + else + load().then(apply); + } + + window.luciOUI = { + decorate: decorate, + setDevices: function(sections) { + devices = Object.create(null); + (sections || []).forEach(function(section) { + var mac = normalize(section.mac); + if (mac && typeof section.vendor === 'string' && /^[a-z0-9]+$/.test(section.vendor)) + devices[mac] = section.vendor; + }); + } + }; +})(window, document); diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/panasonic.svg b/luci-app-oui/htdocs/luci-static/resources/oui/panasonic.svg new file mode 100644 index 000000000..e5902436c --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/panasonic.svg @@ -0,0 +1 @@ +Panasonic \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/qnap.svg b/luci-app-oui/htdocs/luci-static/resources/oui/qnap.svg new file mode 100644 index 000000000..1abe1b473 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/qnap.svg @@ -0,0 +1 @@ +QNAP \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/raspberrypi.svg b/luci-app-oui/htdocs/luci-static/resources/oui/raspberrypi.svg new file mode 100644 index 000000000..1325a023b --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/raspberrypi.svg @@ -0,0 +1 @@ +Raspberry Pi \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/samsung.svg b/luci-app-oui/htdocs/luci-static/resources/oui/samsung.svg new file mode 100644 index 000000000..627cbf2c1 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/samsung.svg @@ -0,0 +1 @@ +Samsung \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/sharp.svg b/luci-app-oui/htdocs/luci-static/resources/oui/sharp.svg new file mode 100644 index 000000000..22ff3cc6a --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/sharp.svg @@ -0,0 +1 @@ +sharp \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/sony.svg b/luci-app-oui/htdocs/luci-static/resources/oui/sony.svg new file mode 100644 index 000000000..38d17e13d --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/sony.svg @@ -0,0 +1 @@ +Sony \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/synology.svg b/luci-app-oui/htdocs/luci-static/resources/oui/synology.svg new file mode 100644 index 000000000..dd2aa9af7 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/synology.svg @@ -0,0 +1 @@ +Synology \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/tcl.svg b/luci-app-oui/htdocs/luci-static/resources/oui/tcl.svg new file mode 100644 index 000000000..996f04baf --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/tcl.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/toshiba.svg b/luci-app-oui/htdocs/luci-static/resources/oui/toshiba.svg new file mode 100644 index 000000000..23304c210 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/toshiba.svg @@ -0,0 +1 @@ +Toshiba \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/tplink.svg b/luci-app-oui/htdocs/luci-static/resources/oui/tplink.svg new file mode 100644 index 000000000..b760ca377 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/tplink.svg @@ -0,0 +1 @@ +TP-Link \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/ubiquiti.svg b/luci-app-oui/htdocs/luci-static/resources/oui/ubiquiti.svg new file mode 100644 index 000000000..a0d34fa8d --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/ubiquiti.svg @@ -0,0 +1 @@ +Ubiquiti \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/vendors-451e5311befc.json b/luci-app-oui/htdocs/luci-static/resources/oui/vendors-451e5311befc.json new file mode 100644 index 000000000..7d3a1af43 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/vendors-451e5311befc.json @@ -0,0 +1 @@ +{"vendors":[["apple","Apple"],["asus","ASUS"],["acer","Acer"],["msi","MSI"],["dell","Dell"],["hp","HP"],["lenovo","Lenovo"],["samsung","Samsung"],["huawei","Huawei"],["honor","HONOR"],["xiaomi","Xiaomi"],["oppo","OPPO"],["vivo","vivo"],["oneplus","OnePlus"],["motorola","Motorola"],["nokia","Nokia"],["sony","Sony"],["lg","LG"],["google","Google"],["microsoft","Microsoft"],["intel","Intel"],["nvidia","NVIDIA"],["broadcom","Broadcom"],["mediatek","MediaTek"],["fujitsu","Fujitsu"],["toshiba","Toshiba"],["panasonic","Panasonic"],["sharp","Sharp"],["raspberrypi","Raspberry Pi"],["framework","Framework"],["amazon","Amazon"],["tplink","TP-Link"],["ubiquiti","Ubiquiti"],["netgear","NETGEAR"],["linksys","Linksys"],["synology","Synology"],["qnap","QNAP"],["midea","Midea"],["tcl","TCL"],["gigabyte","GIGABYTE"],["vmware","VMware"]],"prefixes":{"00000E":24,"000039":25,"000095":16,"000097":4,"0000E2":2,"0000EB":26,"0000F0":7,"000124":2,"000144":4,"00014A":16,"0001E6":5,"0001E7":5,"000278":7,"0002A5":5,"0002B3":20,"0002EE":15,"000347":20,"000393":0,"0003FF":19,"00040F":1,"00041F":16,"000423":20,"00044B":21,"00047D":14,"0004EA":5,"000502":0,"000569":40,"0005B5":22,"000600":25,"00065B":4,"0007AB":7,"0007E9":20,"000802":5,"00080D":25,"000874":4,"000883":5,"0008C7":5,"000918":7,"00095B":33,"000A00":23,"000A27":0,"000A28":14,"000A57":5,"000A95":0,"000AD9":16,"000AE0":24,"000AEB":31,"000AF7":22,"000B5D":24,"000B97":26,"000BCD":5,"000BDB":4,"000BE1":15,"000C29":40,"000C43":23,"000C6E":1,"000C76":3,"000CE7":23,"000CF1":20,"000D2E":26,"000D3A":19,"000D56":4,"000D57":24,"000D61":39,"000D93":0,"000D9D":5,"000DAE":7,"000DB6":22,"000DE5":7,"000E07":16,"000E0C":20,"000E1F":38,"000E35":20,"000E7B":25,"000E7F":5,"000EA6":1,"000EB3":5,"000EC7":14,"000EED":15,"000F12":26,"000F1F":4,"000F20":5,"000F61":5,"000FB5":33,"000FBB":15,"000FDE":16,"000FEA":39,"001018":22,"00101E":26,"001055":24,"001083":5,"00108C":24,"0010B3":15,"0010DC":3,"0010E3":5,"0010FA":0,"001109":3,"00110A":5,"001111":20,"001124":0,"00112F":1,"001132":35,"001143":4,"001175":20,"001185":5,"00119F":15,"0011D8":1,"00123F":4,"001247":7,"001248":4,"00125A":19,"001262":15,"001267":26,"001279":5,"0012EE":16,"0012F0":20,"0012FB":7,"0012FE":6,"001302":20,"001315":16,"001320":20,"001321":5,"001343":26,"001370":15,"001372":4,"001377":7,"0013A9":16,"0013CE":20,"0013D3":3,"0013D4":1,"0013E8":20,"0013FD":15,"001422":4,"001451":0,"00146C":33,"001478":31,"0014A7":15,"0014C2":5,"001500":20,"001517":20,"00152A":15,"001530":4,"00155D":19,"001560":5,"00156D":32,"001599":7,"0015A0":15,"0015B7":25,"0015B9":7,"0015C1":16,"0015C5":4,"0015DE":15,"0015F2":1,"001617":3,"001620":16,"001632":7,"001635":5,"00164E":15,"00166B":7,"00166C":7,"00166F":20,"001676":20,"0016B8":16,"0016BC":15,"0016CB":0,"0016DB":7,"0016E6":39,"0016EA":20,"0016EB":20,"0016F0":4,"001708":5,"001731":1,"001735":20,"001742":24,"00174B":15,"00175C":27,"0017A4":5,"0017A5":23,"0017B0":15,"0017C9":7,"0017D5":7,"0017F2":0,"0017FA":19,"00180F":15,"001813":16,"001842":15,"00184D":33,"001871":5,"001882":8,"001885":14,"00188B":4,"00188D":15,"0018AF":7,"0018C5":15,"0018DE":20,"0018F3":1,"0018FE":5,"00192D":15,"00194F":15,"001963":16,"001979":15,"001987":26,"00198F":15,"001999":24,"0019B7":15,"0019B9":4,"0019BB":5,"0019C5":16,"0019D1":20,"0019D2":20,"0019DB":3,"0019E0":31,"0019E3":0,"001A11":18,"001A16":15,"001A4B":5,"001A4D":39,"001A75":16,"001A80":16,"001A89":15,"001A8A":7,"001A92":1,"001AA0":4,"001ADC":15,"001B21":20,"001B2F":33,"001B33":15,"001B59":16,"001B63":0,"001B77":20,"001B78":5,"001B98":7,"001BAF":15,"001BD3":26,"001BE9":22,"001BEE":15,"001BFC":1,"001C14":40,"001C23":4,"001C35":15,"001C43":7,"001C50":38,"001C62":17,"001C7E":25,"001C9A":15,"001CA4":16,"001CB3":0,"001CBF":20,"001CC0":20,"001CC4":5,"001CD4":15,"001CD6":15,"001CEE":27,"001D09":4,"001D0D":16,"001D0F":31,"001D25":7,"001D28":16,"001D3B":15,"001D40":20,"001D4F":0,"001D60":1,"001D6E":15,"001D7D":39,"001D92":3,"001D98":15,"001DBA":16,"001DD8":19,"001DE0":20,"001DE1":20,"001DE9":15,"001DF6":7,"001DFD":15,"001E0B":5,"001E10":8,"001E2A":33,"001E3A":15,"001E3B":15,"001E45":16,"001E4F":4,"001E52":0,"001E64":20,"001E65":20,"001E67":20,"001E75":17,"001E7D":7,"001E8C":1,"001EA3":15,"001EA4":15,"001EC2":0,"001EC9":4,"001EDC":16,"001EE1":7,"001EE2":7,"001F00":15,"001F01":15,"001F29":5,"001F33":33,"001F3B":20,"001F3C":20,"001F5B":0,"001F5C":15,"001F5D":15,"001F6B":17,"001F92":14,"001FA7":16,"001FC6":1,"001FCC":7,"001FCD":7,"001FCF":3,"001FD0":39,"001FDE":15,"001FDF":15,"001FE3":17,"001FE4":16,"001FF3":0,"002075":14,"00207B":20,"0020D9":26,"0020ED":39,"002108":15,"002109":15,"002119":7,"002127":31,"00214C":7,"00215A":5,"00215C":20,"00215D":20,"00216A":20,"00216B":20,"002170":4,"002185":3,"00219B":4,"00219E":16,"0021AA":15,"0021AB":15,"0021D1":7,"0021D2":7,"0021E9":0,"0021FB":17,"0021FC":15,"0021FE":15,"002215":1,"002219":4,"00223F":33,"002241":0,"002248":19,"002264":5,"002265":15,"002266":15,"002298":16,"0022A1":8,"0022A6":16,"0022A9":17,"0022DE":11,"0022F3":27,"0022FA":20,"0022FB":20,"0022FC":15,"0022FD":15,"002312":0,"002314":20,"002315":20,"002318":25,"002326":24,"002332":0,"002339":7,"00233A":7,"002345":16,"002354":1,"00236C":0,"00237D":5,"002399":7,"0023AE":4,"0023B4":15,"0023C2":7,"0023CD":31,"0023D6":7,"0023D7":7,"0023DF":0,"0023F1":16,"002403":15,"002404":15,"00241D":39,"002421":3,"002436":0,"002437":14,"002454":7,"00247C":15,"00247D":15,"002481":5,"002483":17,"00248C":1,"00248D":16,"002490":7,"002491":7,"002492":14,"0024B2":33,"0024BE":16,"0024D6":20,"0024D7":20,"0024E8":4,"0024E9":7,"0024EF":16,"002500":0,"002538":7,"002547":15,"002548":15,"00254B":0,"002564":4,"002566":7,"002567":7,"002568":8,"002586":31,"00259E":8,"0025AE":19,"0025B3":5,"0025BC":0,"0025CF":15,"0025D0":15,"0025E5":17,"0025E7":16,"002608":0,"002618":1,"002637":7,"00264A":0,"002655":5,"00265D":7,"00265F":7,"002668":15,"002669":15,"0026B0":0,"0026B9":4,"0026BB":0,"0026C6":20,"0026C7":20,"0026CC":15,"0026E2":17,"0026F2":33,"00270E":20,"002710":20,"002719":31,"002722":32,"0028F8":20,"002B70":7,"002EC7":8,"003005":24,"003065":0,"00306E":5,"0030C1":5,"003126":15,"003192":31,"0034DA":17,"0034FE":8,"003DE1":8,"003DE8":17,"003EC4":8,"003EE1":0,"004043":15,"004238":20,"00464B":8,"004B0D":8,"004E01":4,"004F1A":8,"005040":26,"005056":40,"00508B":5,"0050E4":0,"0050F2":19,"00566D":8,"0056CD":0,"0057C1":17,"005A13":8,"005B94":0,"005F67":31,"005FBF":25,"006048":4,"006067":2,"0060B0":5,"006151":8,"006168":15,"006171":0,"006201":14,"00620B":22,"006619":8,"00664B":8,"0066DC":0,"00682B":8,"0068EB":5,"006B6F":8,"006D52":0,"006F64":7,"007147":30,"007204":7,"0072EE":20,"0073E0":7,"0077E4":15,"007839":15,"007C2D":7,"007D3B":7,"007D60":0,"008045":26,"00805F":5,"0080A0":5,"0080F0":26,"00812A":0,"008320":8,"008621":30,"008701":7,"008865":0,"008A55":8,"008A76":0,"008EF2":33,"009027":20,"00919E":20,"009235":0,"009337":20,"0094EC":8,"0097F1":0,"00991D":8,"009ACD":8,"009C02":5,"009CC0":12,"009EC8":10,"00A040":0,"00A060":2,"00A077":24,"00A0C9":20,"00A0CA":24,"00A159":17,"00A307":9,"00A45F":8,"00A554":20,"00A91D":8,"00AA00":20,"00AA01":20,"00AA02":20,"00AA70":17,"00ADD5":8,"00B0D0":4,"00B362":0,"00B5D0":7,"00B8B6":14,"00BB1C":8,"00BB3A":30,"00BB60":20,"00BD3A":15,"00BE3B":8,"00BE43":4,"00BF61":7,"00BFAF":38,"00C04F":4,"00C08F":26,"00C2C6":20,"00C30A":10,"00C3F4":7,"00C585":0,"00C610":0,"00CAE0":11,"00CC05":8,"00CDFE":0,"00D060":26,"00D0B7":20,"00D0F6":15,"00D49E":20,"00D76D":20,"00D861":3,"00D8A2":8,"00D9D1":16,"00DB70":0,"00DBDF":20,"00E000":24,"00E003":15,"00E00C":14,"00E018":1,"00E064":7,"00E091":17,"00E0FC":8,"00E12F":8,"00E18C":20,"00E3B2":7,"00E406":8,"00E421":16,"00EB2D":16,"00EC0A":10,"00F361":30,"00F39F":0,"00F46F":7,"00F4B9":0,"00F5FD":8,"00F620":18,"00F76F":0,"00F7AD":8,"00F81C":8,"00F952":8,"00FA21":7,"00FADE":14,"00FC8B":30,"04006E":18,"04021F":8,"040CCE":0,"040E3C":5,"040F66":31,"04106B":10,"04137A":0,"041471":8,"041552":0,"04180F":7,"041892":8,"0418D6":32,"041B6D":17,"041BBA":7,"041C6C":20,"041E64":0,"041FB8":12,"04209A":26,"042405":11,"0425C5":8,"0425F0":15,"042665":0,"042728":19,"042758":8,"04292E":7,"042EC1":0,"043201":22,"04331F":8,"043389":8,"0433C2":20,"0434CF":0,"0434F6":14,"043D6E":15,"0441A5":0,"04421A":1,"04489A":0,"04495D":8,"044A6C":8,"044BB1":8,"044BED":0,"044F4C":8,"0452F3":0,"045453":0,"0455B8":8,"045665":15,"0456E5":20,"045A95":15,"045D4B":16,"0463D0":8,"0464FA":4,"046761":10,"046865":0,"0469F8":0,"046C59":20,"046F00":17,"047295":0,"0472EF":0,"04749E":8,"047503":8,"047970":8,"047975":9,"047A0B":10,"047AAE":8,"047C16":3,"04801A":8,"04885F":8,"048C16":8,"048C9A":8,"049226":1,"0499B9":0,"0499BB":0,"049D05":0,"049FCA":8,"04A151":33,"04A526":15,"04A6C8":20,"04A81C":8,"04A82A":15,"04AE47":10,"04B0E7":8,"04B167":10,"04B1A1":7,"04B429":7,"04B5B2":0,"04B9E3":7,"04BA1C":8,"04BA8D":7,"04BC6D":0,"04BD70":8,"04BDBF":7,"04BE58":8,"04BF1B":4,"04BFD5":0,"04C06F":8,"04C1D8":8,"04C241":15,"04C807":10,"04C845":31,"04C8B0":18,"04CAED":8,"04CB01":7,"04CCBC":8,"04CF4B":20,"04CF8C":10,"04D13A":10,"04D395":14,"04D3B0":20,"04D3B5":8,"04D3CF":0,"04D4C4":1,"04D6AA":7,"04D6F4":37,"04D9F5":1,"04DB56":0,"04DBD9":11,"04E4B6":7,"04E536":0,"04E598":10,"04E795":8,"04E8B9":20,"04EA56":20,"04ECA9C":22,"04ECD8":20,"04ED33":20,"04F03E":8,"04F0EE":20,"04F13E":0,"04F169":8,"04F352":8,"04F4D8":38,"04F778":16,"04F7E4":0,"04F938":8,"04F9F8":31,"04FE31":7,"04FE8D":8,"04FF08":8,"080007":0,"080009":5,"08001B":4,"08001F":27,"080023":26,"080046":16,"080205":8,"08023C":7,"08028E":33,"0808C2":7,"081093":7,"081196":20,"0812A5":30,"08152F":7,"0816E3":8,"0819A6":8,"081AFD":8,"081C6E":10,"081F71":31,"0821EF":7,"0823B2":12,"0823C6":8,"08240B":0,"082525":10,"082573":0,"08276B":8,"082CB6":0,"082E36":8,"082E5F":5,"082FE9":8,"08318B":8,"0831A4":8,"08357D":19,"0836C9":33,"08373D":7,"0838E6":14,"083D88":7,"083F21":14,"08474C":15,"0847D0":15,"084ACF":11,"084F0A":8,"085104":8,"085700":31,"0857FB":30,"085BD6":20,"085C1B":8,"085D53":0,"08606E":1,"086202":0,"086266":1,"086361":8,"086518":0,"086698":0,"086AC5":20,"086AE5":30,"086D41":0,"086E9C":8,"087045":0,"087073":8,"087190":20,"087402":0,"087808":7,"08798C":8,"087A4C":8,"087B0F":30,"087C39":30,"087C43":8,"087F98":12,"08849D":30,"0884FB":9,"0887C7":0,"088BC8":18,"088C2C":7,"088E90":20,"088EDC":0,"089115":30,"0891A3":30,"089204":4,"089356":8,"0894EC":8,"089542":0,"089BB9":15,"089C86":15,"089DF4":20,"089E08":18,"089E84":8,"08A5DF":7,"08A6BC":30,"08A842":8,"08AA55":14,"08AED6":7,"08B339":10,"08B3AF":12,"08B3D6":8,"08B4B1":18,"08B4D2":20,"08BD43":33,"08BFA0":7,"08BFB8":1,"08C021":8,"08C06C":8,"08C224":30,"08C3B3":38,"08C5E1":7,"08C729":0,"08C7B5":0,"08CC27":14,"08D23E":20,"08D40C":20,"08D42B":7,"08D46A":17,"08D945":8,"08DD03":11,"08DD82":8,"08E021":9,"08E64B":0,"08E689":0,"08E7E5":8,"08E84F":8,"08EB21":20,"08EBF6":8,"08ECA9":7,"08EE8B":7,"08F458":8,"08F4AB":0,"08F69C":0,"08F8BC":0,"08FA28":8,"08FA79":12,"08FC88":7,"08FD0E":7,"08FD58":8,"08FF44":0,"0C02BD":7,"0C07DF":10,"0C07F3":8,"0C0ECB":8,"0C1420":7,"0C1539":0,"0C1563":0,"0C1773":8,"0C184E":8,"0C19F8":0,"0C1B7B":19,"0C1DAF":10,"0C20D3":12,"0C238D":8,"0C29EF":4,"0C2C54":8,"0C2D71":0,"0C2E57":8,"0C2FB0":7,"0C3021":0,"0C31DC":8,"0C323A":7,"0C3526":19,"0C354F":15,"0C3623":15,"0C37DC":8,"0C3B50":0,"0C3E9F":0,"0C413E":19,"0C41E9":8,"0C43F9":30,"0C45BA":8,"0C47C9":30,"0C4885":17,"0C4B48":15,"0C4B54":31,"0C4DE9":0,"0C4F9B":8,"0C5101":0,"0C517E":0,"0C53B7":0,"0C5415":20,"0C54B9":15,"0C6046":12,"0C62A6":38,"0C659A":26,"0C6743":8,"0C6AC4":0,"0C6F8B":0,"0C7043":16,"0C704A":8,"0C715D":7,"0C7165":14,"0C722C":31,"0C74C2":0,"0C771A":0,"0C7955":38,"0C7A15":20,"0C7C28":15,"0C7DB0":14,"0C8063":31,"0C8268":31,"0C8306":8,"0C839A":8,"0C8408":8,"0C85E1":0,"0C8832":15,"0C8910":7,"0C8BA2":8,"0C8BFD":20,"0C8DCA":7,"0C8FFF":8,"0C9160":38,"0C9192":20,"0C938F":11,"0C96BF":8,"0C9838":10,"0C9A3C":20,"0C9D92":1,"0CA3B2":0,"0CA8A7":7,"0CAE39":20,"0CB319":7,"0CB527":8,"0CB5B3":8,"0CB787":8,"0CB789":9,"0CB78E":8,"0CB7EC":8,"0CB983":9,"0CBC9F":0,"0CBD75":11,"0CBEF1":8,"0CC413":18,"0CC56C":0,"0CC66A":15,"0CC6CC":8,"0CC6FD":10,"0CC98A":20,"0CCB85":14,"0CCC5D":0,"0CD292":20,"0CD6BD":8,"0CD746":0,"0CDBEA":0,"0CDC91":30,"0CDD24":20,"0CDDEF":15,"0CDFA4":7,"0CE0DC":7,"0CE441":0,"0CE4A0":8,"0CE5A1":0,"0CE5B5":8,"0CE623":8,"0CE725":19,"0CEA14":32,"0CEC8D":14,"0CEDC8":10,"0CEE99":30,"0CEF15":31,"0CF346":10,"0CFC18":8,"0CFE45":16,"100020":0,"100177":8,"1002B5":20,"1005E1":15,"1007B6":7,"1009F9":30,"100BA9":20,"100C6B":33,"100D7F":33,"100D8C":8,"101212":12,"1016B1":11,"101B54":8,"101C0C":0,"101DC0":7,"101F74":5,"102407":8,"1027F5":31,"102959":0,"1029AB":7,"102AB3":10,"102B1C":14,"102B41":7,"102C8D":37,"102E00":20,"102F6B":19,"102FCA":0,"103025":0,"103047":7,"10321D":8,"10327E":8,"103917":7,"103B54":8,"103B59":7,"103D0A":38,"103D1C":20,"103F44":10,"1040F3":0,"10417F":0,"104210":0,"104400":8,"104738":15,"104780":8,"10490E":8,"104A7D":20,"104FA8":16,"105107":20,"105172":8,"1052BD":8,"105A95":31,"105DDC":8,"105FAD":20,"10604B":5,"1062E5":5,"106530":4,"1067A3":8,"10683F":17,"107100":8,"1071FA":11,"10746F":14,"1077B1":7,"107A2A":19,"107B44":1,"107BCE":15,"107C61":1,"107D1A":4,"107DC8":0,"1086F4":8,"1088D3":8,"1089FB":7,"108A7B":15,"108EE0":7,"108FFE":8,"1091D1":20,"109266":7,"109327":39,"1093E9":0,"1094BB":0,"1094EF":8,"109693":30,"109819":4,"109826":15,"109836":4,"109ABA":20,"109ADD":0,"109D7A":8,"109E6B":0,"109F41":0,"10A1DA":0,"10A2D3":0,"10A30F":8,"10A4C9":0,"10A4DA":8,"10A51D":20,"10A879":20,"10ABC9":7,"10AE60":30,"10B1F8":8,"10B588":0,"10B676":5,"10B9C4":0,"10BC36":8,"10BC97":12,"10BD3A":0,"10BF48":1,"10BF67":30,"10C172":8,"10C197":10,"10C37B":1,"10C3AB":8,"10C595":6,"10C61F":8,"10C735":19,"10CD54":8,"10CE02":30,"10CEE9":0,"10CF0F":0,"10D38A":7,"10D542":7,"10D9A2":18,"10DA43":33,"10DA49":8,"10DA63":0,"10DDB1":0,"10E2C9":0,"10E4C2":7,"10E7C6":5,"10E878":15,"10E953":8,"10EC81":7,"10F005":20,"10F1F2":17,"10F60A":20,"10F681":12,"10F96F":17,"10F9EE":15,"10FC33":8,"10FEED":31,"10FFE0":39,"140152":7,"140338":8,"140589":14,"1409DC":8,"140AC5":30,"140B9E":7,"140F42":15,"14109F":0,"1413FB":8,"141416":38,"14147D":0,"141826":15,"141877":4,"1418C3":20,"141A97":0,"141AA3":14,"141BA0":0,"141F78":7,"14205E":0,"14223B":18,"14230A":8,"1423F2":22,"1423F3":22,"142876":0,"142D4D":0,"143004":8,"1430C6":14,"1432D1":7,"1435B7":0,"143605":15,"1436C6":6,"1438FA":14,"143B51":8,"143CC3":8,"143E60":15,"143EC2":20,"143FA6":16,"144658":8,"14472D":11,"144920":8,"1449C5":8,"1449D4":10,"1449E0":7,"144F8A":20,"145051":27,"145120":8,"145594":8,"1455B9":15,"14563A":8,"14568E":7,"14579F":8,"1458D0":5,"1459C0":33,"145A05":0,"145E69":11,"145EBC":8,"145F94":8,"1460CB":0,"1461A4":9,"14656A":8,"14755B":20,"147590":31,"147740":8,"147830":21,"147AE4":0,"147BAC":15,"147DDA":0,"147FCE":0,"148509":0,"14857F":20,"148692":31,"14876A":0,"1488E6":0,"1489CB":8,"1489FD":7,"148C4A":8,"148F79":0,"148FC6":0,"14907A":10,"149138":30,"14946C":0,"1495CE":0,"1496E5":7,"149877":0,"14993E":10,"1499E2":0,"149A10":19,"149AA3":8,"149BF3":11,"149D09":8,"149D99":0,"149ECF":4,"149F3C":7,"149FE8":6,"14A0F8":8,"14A32F":8,"14A364":7,"14A3B4":8,"14A51A":8,"14A6B9":7,"14AB02":8,"14ABC5":20,"14B31F":4,"14B484":7,"14B903":8,"14B968":8,"14BB6E":7,"14BD61":0,"14C126":15,"14C14E":18,"14C213":0,"14C697":11,"14C88B":0,"14C913":17,"14CB19":5,"14CB65":19,"14CC20":31,"14CF92":31,"14D00D":0,"14D11F":8,"14D169":8,"14D19E":0,"14D1D4":8,"14D864":31,"14D881":10,"14DAB9":8,"14DAE9":1,"14DD9C":12,"14DDA9":1,"14DE39":8,"14E01D":7,"14E6E4":31,"14EA63":38,"14EB08":8,"14EBB6":31,"14F287":0,"14F42A":7,"14F65A":10,"14F6D8":20,"14FB70":8,"14FEB5":4,"18002D":16,"1801F1":10,"18022D":8,"1802AE":12,"180373":4,"180403":12,"1806FF":2,"180B1B":30,"180BD0":8,"181456":15,"1814AE":15,"1816C9":7,"1819D6":7,"181DEA":20,"181EB0":7,"182032":0,"182195":7,"18227E":7,"182649":20,"182654":7,"182666":7,"182A57":8,"1831BF":1,"183386":8,"183451":0,"183A2D":7,"183CB7":8,"183D5E":8,"183DA2":20,"183EEF":0,"183F47":7,"183F70":0,"1841C3":37,"184617":7,"1848BE":30,"184A53":0,"184E16":7,"184ECB":7,"185345":15,"1854CF":7,"1855E3":0,"185644":8,"185680":20,"1856C3":0,"185936":10,"185A58":4,"185B00":15,"185BB3":7,"185E0F":20,"185F27":14,"186024":5,"186590":0,"1866DA":4,"1867B0":7,"186945":31,"1869D4":7,"18703B":8,"18742E":30,"187EB9":0,"18810E":0,"188331":7,"1886AC":15,"1886C3":15,"188740":10,"18895B":7,"189341":20,"189E2C":8,"189EFC":0,"18A084":0,"18A6F7":31,"18A905":5,"18A99B":4,"18AA0F":8,"18AB1D":7,"18ACC2":38,"18AF61":0,"18AF8F":0,"18B657":8,"18B83D":7,"18B842":0,"18BB1C":8,"18BB41":8,"18BFB3":7,"18C007":8,"18C04D":39,"18C086":22,"18C300":15,"18C58A":8,"18CC18":20,"18CE94":7,"18CF24":8,"18D0C5":11,"18D276":8,"18D6C7":31,"18D6DD":8,"18D717":11,"18D98F":8,"18DBF2":4,"18DED7":8,"18E215":15,"18E29F":12,"18E2C2":7,"18E671":0,"18E777":12,"18E7B0":0,"18E7F4":0,"18E829":32,"18E91D":8,"18EE69":0,"18F0E4":10,"18F1D8":0,"18F22C":31,"18F643":0,"18FAB7":0,"18FB7B":4,"18FF0F":20,"1C0219":11,"1C0B8B":32,"1C0D7D":0,"1C0EAF":8,"1C0EC2":0,"1C112F":12,"1C12B0":30,"1C1386":8,"1C13FA":8,"1C151F":8,"1C1AC0":0,"1C1ADF":19,"1C1B0D":39,"1C1BB5":20,"1C1D67":8,"1C1DD3":0,"1C1EE3":38,"1C1FF1":8,"1C20DB":8,"1C232C":7,"1C2575":10,"1C2A8B":15,"1C2AB0":10,"1C3008":38,"1C32AC":8,"1C3576":7,"1C36BB":0,"1C3ADE":7,"1C3BF3":31,"1C3C78":0,"1C3CD4":8,"1C3D2F":8,"1C4024":4,"1C427D":11,"1C42C2":8,"1C4363":8,"1C4419":31,"1C472F":8,"1C48CE":11,"1C4D66":30,"1C4D70":20,"1C53F9":18,"1C56FE":14,"1C57DC":0,"1C599B":8,"1C5A3E":7,"1C5CF2":0,"1C61B4":31,"1C61BF":0,"1C627E":8,"1C62B8":7,"1C64F0":14,"1C66AA":7,"1C6758":8,"1C6A1B":32,"1C6A76":0,"1C6F65":39,"1C7055":8,"1C7125":0,"1C721D":4,"1C73E2":8,"1C76F2":7,"1C7754":0,"1C77F6":11,"1C7A43":12,"1C7ACF":12,"1C7B21":16,"1C7F2C":8,"1C8682":0,"1C869A":7,"1C872C":1,"1C88798":25,"1C8BEF":10,"1C8E2A":0,"1C8E5C":8,"1C9148":0,"1C9180":0,"1C93C4":30,"1C9957":20,"1C99DB":8,"1C9E46":0,"1CA681":8,"1CABA7":0,"1CAECB":8,"1CAF05":7,"1CAF4A":7,"1CB3C9":0,"1CB46C":8,"1CB72C":1,"1CB796":8,"1CBAB8":12,"1CC10C":20,"1CC1DE":5,"1CC3EB":11,"1CC992":9,"1CCCD6":10,"1CDA27":12,"1CDDEA":11,"1CE209":0,"1CE504":8,"1CE57F":7,"1CE61D":7,"1CE62B":0,"1CE639":8,"1CE6AD":8,"1CEA1B":15,"1CEAAC":10,"1CF29A":18,"1CF42B":8,"1CF64C":0,"1CF8D0":7,"1CF9D5":0,"1CFA68":31,"1CFC2A":8,"1CFE2B":30,"1CFFAD":8,"20040F":4,"200484":0,"2004F3":9,"2008ED":8,"200A0DB":30,"200BC7":8,"200CC8":33,"200E0F":26,"200E2B":0,"2010B1":30,"2013E0":7,"2014C4":8,"201582":0,"2015DE":7,"201642":19,"2016B9":20,"201742":17,"201A94":0,"201E1D":8,"201E88":20,"201F3B":18,"2021A5":17,"202351":31,"2025CC":10,"202680":7,"20283E":8,"202BC1":8,"202D07":7,"202DF6":0,"20311C":12,"20326C":7,"2032C6":0,"203389":18,"203462":10,"2034FB":10,"203626":31,"2036D0":14,"2037A5":0,"203A43":20,"203B34":10,"203B67":7,"203B69":12,"203CAE":0,"203DB2":8,"20463A":0,"204747":4,"2047DA":10,"204E7F":33,"20521D":0,"205383":8,"205476":16,"2054FA":8,"205531":7,"205D47":12,"205E64":8,"205E97":15,"205EF7":7,"206274":19,"206432":7,"2064CB":11,"20658E":8,"206980":0,"206BD5":12,"206BE7":31,"206BF4":8,"206E9C":7,"2072A9":10,"207454":12,"20768F":0,"207693":6,"207852":15,"2078CD":0,"2078F0":0,"207918":20,"207D74":0,"20826A":11,"2082C0":10,"20845F":0,"2087EC":8,"208810":4,"208C86":8,"2091DF":0,"209952":10,"209BCD":0,"209BDD":8,"20A171":30,"20A200":8,"20A2E4":0,"20A366":12,"20A5CB":0,"20A60C":10,"20A680":8,"20A766":8,"20A8BF":8,"20A99B":19,"20AB37":0,"20AB48":8,"20AB53":14,"20AEB6":8,"20B780":25,"20B868":14,"20BD1D":20,"20BEB8":30,"20C19B":20,"20C2B0":8,"20C6EB":26,"20C9D0":0,"20CC73":7,"20CF30":1,"20D390":7,"20D5BF":7,"20D607":15,"20DA22":8,"20DBAB":7,"20DCE6":31,"20DCFD":8,"20DE1E":15,"20DF73":8,"20DFB9":18,"20E09C":15,"20E15D":31,"20E2A8":0,"20E46F":12,"20E525":8,"20E52A":33,"20E59B":26,"20E874":0,"20EE28":0,"20F094":18,"20F17C":8,"20F3A3":8,"20F44F":15,"20F478":10,"20F4D4":0,"20F543":38,"20F77C":12,"20FA85":0,"20FE00":30,"2400BA":8,"24016F":8,"240588":18,"2406AA":11,"240935":7,"240995":8,"240A3F":7,"241145":10,"241153":7,"241551":8,"24166D":8,"24181D":7,"241AE6":8,"241B7A":0,"241EEB":0,"241FA0":8,"242124":15,"2421AB":16,"242361":12,"24240E":0,"2424B7":7,"242642":27,"2426D6":8,"242730":37,"2427E5":8,"242934":18,"2429B0":8,"242AEA":0,"242E02":8,"242FD0":31,"242FFA":25,"2430F8":8,"243154":8,"243184":27,"243F75":38,"243FAA":8,"24418C":20,"244427":8,"24456B":8,"2446C8":14,"2446E4":8,"244885":8,"244B03":7,"244B81":7,"244BF1":8,"244BFE":1,"244C07":8,"244CE3":30,"2453ED":4,"24559A":0,"2459E5":37,"245A4C":32,"245A5F":31,"245AB5":7,"245BA7":0,"245CC5":8,"245E48":0,"245EBE":36,"245F9F":8,"246078":8,"2460B3":7,"2462C6":8,"246477":10,"24649F":8,"246800":7,"2468B0":7,"246968":31,"2469A5":8,"246A0E":5,"246C60":8,"246D10":0,"246E96":4,"246F8C":8,"247152":4,"24753A":11,"2475B3":11,"247645":8,"247703":20,"247755":8,"247823":26,"2478EF":15,"2479F3":11,"247F3C":8,"2481C7":8,"2482CA":12,"2491BB":8,"24920E":7,"24952F":18,"249745":8,"249EAB":8,"24A074":0,"24A10D2":16,"24A2E1":0,"24A43C":32,"24A452":7,"24A487":8,"24A52C":8,"24A799":8,"24A87D":26,"24AB81":0,"24AECC":9,"24B339":0,"24B5B9":14,"24B6FD":4,"24BCF8":8,"24BE05":5,"24C613":7,"24C696":7,"24CE33":30,"24CF24":10,"24D0DF":0,"24D337":10,"24D53B":14,"24DA33":8,"24DA9B":14,"24DBAC":8,"24DBED":7,"24DE8A":15,"24DEEB":8,"24DF6A":8,"24E29D":8,"24E314":0,"24E50F":18,"24E9CA":8,"24EA9B":8,"24EB16":20,"24EBED":8,"24EE9A":20,"24F094":0,"24F0D3":7,"24F40A":7,"24F5AA":7,"24F603":8,"24F677":0,"24F68D":15,"24FB65":8,"24FBE3":5,"24FCE5":7,"2800AF":4,"28022E":0,"280244":0,"2802D8":7,"2803E6":15,"280708":7,"280B5C":0,"280C50":20,"280DFC":16,"2811A8":20,"2811EC":8,"281293":9,"2812D0":14,"28167F":10,"2816A8":19,"2816AD":20,"281709":8,"281878":19,"281DFB":8,"28221E":8,"2824C9":30,"2827BF":7,"282B96":8,"282CB2":31,"282CC4":8,"282D7F":0,"283152":8,"283166":12,"2831F8":8,"283334":8,"2834FF":0,"28353A":8,"2836381":26,"2836F0":8,"283737":0,"28395E":7,"283CE4":8,"283DC2":7,"283F69":16,"2840DD":16,"2841C6":8,"2841EC":8,"2844F4":9,"2845AC":8,"2847AA":15,"2848E7":8,"2849E9":0,"284B54":0,"284E44":8,"28534E":8,"285471":8,"28575D":0,"285923":10,"285AEB":0,"285FDB":8,"2864B0":8,"2868D2":8,"286AB8":0,"286ABA":0,"286B35":20,"286C07":10,"286ED4":8,"286FB9":15,"28704E":32,"2872C6":7,"2873F6":30,"2874F5":15,"2877F1":0,"287AB4":8,"287B11":38,"287E80":38,"287FCF":20,"288023":5,"288088":33,"28808A":8,"288335":7,"2883C9":0,"2884FA":27,"2887BA":31,"288EEC":0,"288FF6":0,"289104":31,"289200":20,"28924A":5,"289401":33,"289529":20,"2896B0":8,"28987B":7,"289E97":8,"289F04":7,"28A02B":0,"28A06B":20,"28A44A":20,"28A53F":12,"28A6DB":8,"28A9AE":8,"28AD18":38,"28AF42":7,"28B2BD":20,"28B448":8,"28BAB5":7,"28BD89":18,"28BE43":12,"28C039":8,"28C1A0":0,"28C21F":7,"28C538":0,"28C5C8":5,"28C5D2":20,"28C63F":20,"28C68E":33,"28C709":0,"28CC01":7,"28CDC1":28,"28CFDA":0,"28CFE9":0,"28D0EA":20,"28D127":10,"28D1AF":15,"28D3EA":8,"28D555":12,"28D5B1":0,"28D6EC":8,"28DCC3":8,"28DE1C":7,"28DEE5":8,"28DFEB":20,"28E02C":0,"28E14C":0,"28E31F":10,"28E34E":8,"28E5B0":8,"28E6A9":7,"28E7CF":0,"28EA0B":19,"28EA2D":0,"28EC95":0,"28ED6A":0,"28EE52":31,"28EF01":30,"28F033":0,"28F076":0,"28F10E":4,"28F8C63":26,"28FAA0":12,"28FBAE":8,"28FF3C":0,"28FFB2":25,"2C0786":8,"2C08B4":8,"2C0B97":10,"2C0BAB":8,"2C0D27":8,"2C0DA7":20,"2C0DCF":10,"2C0E3D":7,"2C15BF":7,"2C15D9":8,"2C1809":0,"2C195C":10,"2C1A01":8,"2C1B3A":38,"2C1CF7":0,"2C1F23":0,"2C200B":0,"2C2080":8,"2C233A":5,"2C2768":8,"2C27D7":5,"2C2997":19,"2C3033":33,"2C326A":0,"2C3358":20,"2C3361":0,"2C36F2":8,"2C3A91":8,"2C3AB1":8,"2C4053":7,"2C4138":5,"2C4401":7,"2C44FD":5,"2C4881":12,"2C4D54":1,"2C52AF":8,"2C5491":19,"2C54CF":17,"2C55D3":8,"2C56DC":1,"2C57CE":0,"2C58B9":5,"2C58E8":8,"2C598A":17,"2C59E5":5,"2C5A05":15,"2C5BB8":11,"2C5D34":11,"2C61F6":0,"2C63A1":8,"2C691D5":17,"2C691DA":26,"2C693E":8,"2C6DC1":20,"2C6E85":20,"2C6F37":15,"2C71FF":30,"2C7600":0,"2C768A":5,"2C780E":8,"2C79BE":31,"2C7BA0":20,"2C7CF2":0,"2C81BF":0,"2C8217":0,"2C8DB1":20,"2C9452":8,"2C9520":0,"2C97B1":8,"2C97ED":16,"2C9975":7,"2C9D1E":8,"2C9D65":12,"2C9E00":16,"2CA042":8,"2CA797":8,"2CA79E":8,"2CA7EF":13,"2CA9F0":11,"2CAB00":8,"2CAE2B":7,"2CB05D":33,"2CB301":9,"2CB43A":0,"2CB68F":8,"2CB7A1":8,"2CBABA":7,"2CBC87":0,"2CBE08":0,"2CC1F4":15,"2CC253":0,"2CC546":8,"2CC8F5":8,"2CCA16":0,"2CCC15":15,"2CCC44":16,"2CCF58":8,"2CCF67":28,"2CD066":10,"2CD2E7":15,"2CD444":24,"2CD974":38,"2CDA46":7,"2CDB07":20,"2CDF68":0,"2CE032":38,"2CE2D9":8,"2CE5BD":32,"2CE64D":37,"2CEA7F":4,"2CEAFC":20,"2CECA6":8,"2CED89":8,"2CEDB0":8,"2CF05D":3,"2CF0A2":0,"2CF0EE":0,"2CF295":8,"2CFC8B":11,"2CFDA1":1,"2CFDAB":14,"2CFE4F":10,"2CFFEE":12,"3000FC":15,"300505":20,"30074D":7,"300916":0,"3009C0":14,"300D43":19,"300E43":0,"300EB8":17,"3010E4":0,"30138B":5,"3017C8":16,"301966":7,"301984":8,"301ABA":11,"302364":15,"302432":20,"3024A9":5,"30294B":8,"3035AD":0,"3035C5":8,"3037B3":8,"303855":15,"303926":16,"303A64":20,"303B7C":0,"303EA7":20,"3041DB":12,"304596":8,"30469A":33,"30499E":8,"304B07":14,"304C7E":26,"304D1F":30,"304E1B":8,"304F00":11,"3050CE":10,"305223":37,"30560F":39,"305714":0,"3059B7":19,"305A3A":1,"3061A2":8,"306222":7,"30636B":0,"3066D0":8,"306893":31,"306A85":7,"306DF9":11,"307467":7,"307496":8,"307512":16,"30766F":17,"307A05":8,"307AD2":0,"307C4A":8,"307F10":11,"308216":0,"3083D2":14,"308454":11,"3085A9":1,"308730":8,"30894A":20,"3089A6":8,"308AF7":8,"308D99":5,"308DD4":8,"308ECF":8,"309048":0,"3090AB":0,"309435":12,"309610":8,"30963B":8,"3096FB":7,"309C23":3,"309E62":8,"30A033":0,"30A1FA":8,"30A2C2":8,"30A30F":8,"30A8DB":16,"30A998":8,"30AAE4":8,"30AB6A":7,"30AFCE":12,"30B237":37,"30B49E":31,"30B4B8":17,"30B5C2":31,"30BB7D":13,"30C0AE":0,"30C50F":8,"30C599":1,"30C7AE":7,"30C9CC":7,"30CBF8":7,"30CDA7":7,"30D042":4,"30D17E":8,"30D4E2":8,"30D53E":0,"30D587":7,"30D6C9":7,"30D7A1":0,"30D875":0,"30D9D9":0,"30DE4B":31,"30DE52":7,"30E044":18,"30E04F":0,"30E171":5,"30E226":0,"30E37A":20,"30E396":8,"30E3A4":20,"30E4D8":8,"30E7BC":11,"30E98E":8,"30EB15":8,"30F335":8,"30F6EF":20,"30F7C5":0,"30F9ED":16,"30FBB8":8,"30FC68":31,"30FCEB":17,"30FD38":18,"30FD65":8,"30FE31":15,"30FE6C":0,"30FFFD":8,"3400A3":8,"340286":20,"34033D":5,"340557":12,"3408BC":0,"340A98":8,"340E22":0,"3410BE":0,"3410D0":8,"341298":0,"3412F9":8,"3413E8":20,"34145F":7,"341530":12,"34159E":0,"3417EB":4,"341CF0":10,"341E6B":8,"3423BA":7,"3425BE":30,"342601":8,"342840":0,"342912":8,"342B6E":0,"342D0D":7,"342EB6":8,"342EB7":20,"343111":7,"34317F":26,"34318F":0,"3432E6":26,"343638":8,"34363B":0,"343916":18,"343DA9":8,"34415D":20,"344262":0,"3446EC":8,"34479A":11,"3448ED":4,"344A86":9,"344DF7":17,"344EE2":8,"345180":38,"345184":8,"3451C9":0,"345840":8,"345A06":27,"345A60":3,"345BBB":37,"3460F9":31,"3464A9":5,"346679":8,"346691":0,"346AC2":8,"346BD3":8,"346E68":8,"347146":8,"34735A":4,"347916":8,"347C25":0,"347DF6":20,"347E00":8,"347E39":15,"3480B3":10,"3482C5":7,"3483D5":8,"3486DA":9,"348A3B":8,"348A7B":7,"348C5E":0,"349671":8,"349672":31,"3497F6":1,"3498B5":33,"34A137":8,"34A2A2":8,"34A395":0,"34A8A0":8,"34A8EB":0,"34AA8B":7,"34AA99":15,"34AB37":0,"34AFB3":30,"34B1EB":0,"34B20A":8,"34B354":8,"34B98D":10,"34BB26":14,"34BE00":7,"34C059":0,"34C232":7,"34C386":0,"34C3AC":7,"34C7E9":18,"34C803":15,"34C93D":20,"34CDBE":8,"34CE00":10,"34CE69":15,"34CFF6":20,"34D270":30,"34D693":8,"34D868":22,"34DAA1":0,"34DDCC":18,"34DE1A":20,"34E12D":20,"34E2FD":0,"34E3FB":7,"34E6AD":20,"34E6D7":4,"34E894":31,"34E911":12,"34EE16":0,"34EFD7":8,"34F015":10,"34F043":7,"34F084":7,"34F150":38,"34F39A":20,"34F5D7":8,"34F62D":27,"34F64B":20,"34F68D":0,"34F6D2":26,"34F716":31,"34F8DD":0,"34FA1C":10,"34FCEF":17,"34FD6A":0,"34FD70":20,"34FE77":0,"34FE9E":24,"34FFF3":8,"380025":20,"380195":7,"380484":0,"3809FB":0,"380A94":7,"380B40":7,"380F4A":0,"380FAD":8,"381428":4,"3816B3":5,"3816D1":7,"38184C":16,"381868":20,"38192F":15,"382028":8,"382187":37,"3822E2":5,"3822F4":8,"38256B":19,"382656":38,"38295A":11,"382C4A":1,"382DD1":7,"382DE8":7,"382FB0":37,"3830F9":17,"3833C5":19,"38378B":8,"38384B":12,"38396C":8,"3839CD":12,"3839DD":15,"383FE8":8,"3847BC":8,"38484C":0,"384A80":7,"384C4F":8,"384DD2":8,"38521A":15,"385247":8,"38539C":0,"38563D":19,"386233":0,"3863BB":5,"386504":9,"3865B2":0,"3866F0":0,"386893":20,"3868A4":7,"386A77":7,"386EA2":12,"386EB2":8,"386F6B":11,"386FF4":30,"3870F2":8,"3871DE":0,"38736E":8,"3873EAC":17,"387862":16,"387A0E":20,"387F8B":0,"3880DF":14,"388345":31,"3886F7":18,"3887D5":20,"38881E":8,"3888A4":0,"38892C":0,"388A06":7,"388ABE":11,"388B59":18,"388C50":17,"388CEF":7,"388F30":7,"389052":8,"389496":7,"3894ED":33,"3898E9":8,"389AF6":7,"389CB2":0,"38A067":15,"38A44B":8,"38A4ED":10,"38AA3C":7,"38AFD7":24,"38B3F7":8,"38B54D":0,"38BAB0":22,"38BAF8":20,"38BC01":8,"38C1ED":10,"38C43A":0,"38C6BD":10,"38C804":38,"38C986":0,"38CA84":5,"38CADA":0,"38D09C":8,"38D40B":7,"38D547":1,"38DEAD":20,"38E13D":0,"38E39F":14,"38E563":11,"38E60A":10,"38E7C0":38,"38EAA7":5,"38EB47":8,"38EC07":14,"38EC0D":0,"38ECE4":7,"38F195":8,"38F23E":19,"38F73D":30,"38F7F1":8,"38F889":8,"38F9D3":0,"38FB14":8,"38FC34":8,"38FC98":20,"38FF59":4,"3C01EF":16,"3C0518":7,"3C058E":8,"3C0630":0,"3C06A7":31,"3C0754":0,"3C0771":16,"3C07D7":0,"3C0A7A":7,"3C135A":10,"3C13BB":8,"3C155A":15,"3C15C2":0,"3C15FB":8,"3C189F":15,"3C195E":7,"3C1A65":15,"3C1EB5":0,"3C2093":37,"3C20F6":7,"3C219C":20,"3C22FB":0,"3C240A":8,"3C25D7":15,"3C25F8":4,"3C286D":18,"3C2983":7,"3C2C30":4,"3C2CA6":10,"3C2EF9":0,"3C2EFF":0,"3C306F":8,"3C3174":18,"3C318A":7,"3C32B9":11,"3C3464":0,"3C363D":15,"3C366A":8,"3C3786":33,"3C381F":8,"3C3824":10,"3C38F4":16,"3C39C8":0,"3C3B77":0,"3C46D8":31,"3C4711":8,"3C4A92":5,"3C4AC9":8,"3C4DBE":0,"3C5002":0,"3C5282":5,"3C52A1":31,"3C5447":8,"3C576C":7,"3C58C2":20,"3C591E":38,"3C59C0":8,"3C5A37":7,"3C5AB4":18,"3C5CC4":30,"3C6200":7,"3C64CF":31,"3C65D1":8,"3C678C":8,"3C6A48":31,"3C6AA7":20,"3C6AD2":31,"3C6D66":21,"3C6D89":0,"3C6FEA":26,"3C75DE":11,"3C7787":8,"3C7843":8,"3C7895":31,"3C7C3F":1,"3C7D0A":0,"3C7F6E":10,"3C8375":19,"3C846A":31,"3C869A":8,"3C86D1":12,"3C8BFE":7,"3C8D20":18,"3C90E0":8,"3C93F4":8,"3C9BC6":8,"3C9C0F":20,"3C9D56":8,"3CA10D":7,"3CA161":8,"3CA2C3":12,"3CA348":12,"3CA37E":8,"3CA581":12,"3CA616":12,"3CA6F6":0,"3CA80A":12,"3CA82A":5,"3CA916":8,"3CA9F4":20,"3CAB8E":0,"3CAFB7":10,"3CB233":8,"3CB6B7":12,"3CB922":8,"3CBBFD":7,"3CBD3E":10,"3CBD69":15,"3CBDD8":17,"3CBF60":0,"3CBFD7":0,"3CC03E":8,"3CC243":15,"3CC5C7":8,"3CC5DD":38,"3CCD36":0,"3CCD40":0,"3CCD57":10,"3CCD5D":8,"3CCD93":17,"3CD0F8":0,"3CD92B":5,"3CDCBC":7,"3CDD57":0,"3CDFBD":8,"3CE072":0,"3CE441":30,"3CE824":8,"3CE9F7":20,"3CF011":20,"3CF591":11,"3CF692":8,"3CF72A":15,"3CF7A4":7,"3CF808":8,"3CF862":20,"3CFA06":19,"3CFA43":8,"3CFA80":8,"3CFB02":0,"3CFDFE":20,"3CFFD8":8,"400634":8,"400877":10,"400E85":7,"400EB9":8,"4011C3":7,"401277":19,"4014AD":8,"40163B":7,"40167E":1,"40169F":31,"401C83":20,"401CD4":8,"4024D2":8,"4025C2":20,"402619":0,"402641":8,"402BA1":16,"403004":0,"40313C":10,"40331A":0,"4035E6":7,"403B7B":8,"403CFC":0,"403F8C":31,"4040A7":16,"40410D":8,"4044CE":8,"4045A0":12,"4045C4":8,"40486E":15,"404D7F":0,"404D8E":8,"404F42":8,"40538C":17,"405582":15,"405846":12,"405B7F":22,"405CFD":4,"405D82":33,"405EF6":7,"406186":3,"406768":8,"406C8F":0,"406F27":8,"4070F5":0,"4074E0":20,"4076A9":8,"40786A":14,"407911":0,"407A80":15,"407C7D":15,"407D0F":8,"40831D":0,"408805":14,"4089C2":0,"4089C6":30,"408BF6":38,"408C1F":11,"408D5C":39,"408E2C":19,"408EDF":8,"40921A":0,"409595":31,"4098AD":0,"409B21":15,"409C28":0,"40A108":14,"40A2DB":30,"40A3CC":20,"40A44A":18,"40A53B":15,"40A654":8,"40A6B7":20,"40A6D9":0,"40A8F0":5,"40A9CF":30,"40AE30":31,"40B034":5,"40B076":1,"40B0FA":17,"40B15C":8,"40B395":0,"40B3FA":0,"40B4CD":30,"40B607":11,"40B6E7":8,"40B70E":8,"40B837":16,"40BA09":4,"40BC60":0,"40C3BC":8,"40C711":0,"40C73C":20,"40CBA8":8,"40CBC0":0,"40D133":20,"40D160":0,"40D32D":0,"40D3AE":7,"40D4F6":9,"40DA5C":0,"40DCA5":8,"40DE24":7,"40E1E4":15,"40E4C6":15,"40E64B":0,"40E99B":7,"40EB21":8,"40EC99":20,"40ECBD":20,"40ED00":31,"40EDCF":0,"40EE6D":8,"40EEDD":8,"40F6BC":30,"40F946":0,"40FAFE":14,"440010":0,"440049":30,"44004D":8,"44032C":20,"4403774":6,"440444":11,"4405B8":8,"44070B":18,"4409C6":8,"4409DA":0,"440C4B":8,"441030":18,"441622":19,"4416FA":7,"4418FD":0,"441B88":0,"441C7F":14,"441EA1":5,"44227C":8,"44237C":10,"44272E":8,"442A60":0,"44303F":8,"443192":5,"44321D":8,"443583":0,"4435D3":37,"4438E8":20,"443D54":30,"444201":30,"444988":20,"4449C0":21,"444A37":10,"444A4C":12,"444ADB":0,"444C0C":0,"444E1A":7,"44552B":7,"4455B1":8,"4455C4":8,"44579F":10,"4459E3":8,"445CE9":7,"4463B6":0,"44650D":30,"446690":31,"4466FC":11,"446747":8,"446A2E":8,"446D6C":7,"446D7F":30,"446EE5":8,"447147":10,"44746C":16,"447654":8,"447831":8,"44783E":7,"447B45":30,"4480EB":14,"4482E5":8,"448500":20,"448A5B":3,"448F17":7,"449046":9,"4490BB":0,"4494FC":33,"449BC1":8,"449E8B":0,"449EF9":12,"449F46":8,"44A038":8,"44A10E":0,"44A191":8,"44A3BB":20,"44A56E":33,"44A7F4":0,"44A842":4,"44A8FC":0,"44AE44":8,"44AEAB":11,"44AF28":20,"44B32D":31,"44B3C5":8,"44B4B2":30,"44BB3B":18,"44BDC8":10,"44BE0B":8,"44C346":8,"44C3B6":8,"44C532":8,"44C63C":7,"44C65D":0,"44C7FC":8,"44CBAD":10,"44CE1D":15,"44D4E0":16,"44D5CC":30,"44D791":8,"44D878":38,"44D884":0,"44D9E7":32,"44DA30":0,"44DBBE":8,"44DF65":10,"44E213":10,"44E517":20,"44E59B":8,"44E66E":0,"44E853":0,"44E968":8,"44EA30":7,"44F09E":0,"44F21B":0,"44F459":7,"44F770":10,"44FB42":0,"44FB76":12,"44FEEF":11,"480031":8,"4801C5":13,"480234":8,"4805E2":8,"480A28":0,"480EEC":31,"480FCF":5,"481258":8,"48128F":8,"48137E":7,"482254":31,"4825F3":8,"48262C":0,"4827C5":8,"4827EA":7,"4829D6":11,"482CA0":10,"482CD0":8,"482CEA":14,"482FD7":8,"483106":21,"4831DB":8,"48352B":0,"483543":11,"483584":8,"4835AB":0,"483871":8,"483B38":0,"483C0C":8,"483F72":7,"483FE9":8,"4840D5":20,"48417B":15,"48435A":8,"48437C":0,"4843DD":30,"4844F7":7,"484520":20,"4846FB":8,"48474B":8,"484982":8,"484996":8,"4849C7":7,"484BAA":0,"484C29":8,"484C86":8,"484D7E":4,"485073":19,"485169":7,"4851B7":20,"4851C5":20,"485702":8,"4857D2":22,"485929":17,"485B39":1,"485F08":31,"485F2D":30,"48605F":17,"4860BC":0,"4861EE":7,"486276":8,"486345":8,"48684A":20,"48706F":8,"487412":13,"48746E":0,"4877BD":11,"48785E":30,"48794D":7,"487B2F":19,"487B6B":8,"487D2E":31,"4882DF":8,"4883B4":11,"4886E8":19,"488759":10,"488764":12,"4887B8":38,"4888CA":14,"4889E7":20,"488AE8":12,"488C63":8,"488EEF":8,"48902F":17,"4890F0":4,"489507":11,"489A58":7,"489DD1":7,"489E9D":38,"489EBD":5,"48A195":0,"48A472":20,"48A516":8,"48A91C":0,"48AD08":8,"48AD9A":20,"48B02D":21,"48B25D":8,"48B423":30,"48B8A3":0,"48BA4E":5,"48BCE1":7,"48BD4A":8,"48BDA7":9,"48BF6B":0,"48C1EE":9,"48C35A":6,"48C381":31,"48C461":11,"48C796":7,"48CA68":0,"48CDD3":8,"48CFA9":8,"48D539":8,"48D6D5":18,"48D705":0,"48DB50":8,"48DC2D":8,"48DCFB":15,"48E150":20,"48E15C":0,"48E1CA":0,"48E9F1":0,"48EA62":5,"48EC5B":15,"48EF1C":7,"48EF61":8,"48F17F":20,"48F7BC":8,"48F7F1":15,"48F8DB":8,"48F8E1":15,"48FC07":8,"48FD8E":8,"48FDA3":10,"4C0220":10,"4C034F":20,"4C0BBE":19,"4C0F3E":20,"4C10D5":31,"4C14A3":38,"4C1744":30,"4C189A":11,"4C1A3D":11,"4C1D96":20,"4C1FCC":8,"4C20B8":0,"4C2113":15,"4C218C":26,"4C21D0":16,"4C2578":15,"4C2B3B":8,"4C2E5E":7,"4C2EB4":0,"4C2FD7":8,"4C3275":0,"4C3488":20,"4C364E":26,"4C3946":7,"4C3BDF":19,"4C3C16":7,"4C3CE2":8,"4C445B":20,"4C4929":38,"4C496C":20,"4C49E3":10,"4C4FEE":13,"4C5077":8,"4C50DD":38,"4C50F1":11,"4C5262":24,"4C53FD":30,"4C5499":8,"4C55B2":10,"4C569D":0,"4C5739":7,"4C57CA":0,"4C5D6A":0,"4C5F70":20,"4C60AD":30,"4C60DE":33,"4C617E":8,"4C62CD":15,"4C6371":10,"4C63AD":8,"4C6641":7,"4C66A6":7,"4C6BB8":38,"4C6BE8":0,"4C6F9C":11,"4C74BF":0,"4C7625":4,"4C77CB":20,"4C796E":20,"4C7975":0,"4C79BA":20,"4C7C5F":0,"4C7CD9":0,"4C7F62":15,"4C8093":20,"4C80FB":18,"4C820C":0,"4C889E":8,"4C8BEF":8,"4C8D53":8,"4C8D79":0,"4C8E19":10,"4C92D2":11,"4C97CC":0,"4C9992":12,"4C9FF1":0,"4CA56D":7,"4CA954":20,"4CAB4F":0,"4CAD35":0,"4CAE13":8,"4CB04A":20,"4CB087":8,"4CB16C":8,"4CB199":0,"4CB910":0,"4CBB47":21,"4CBCA5":7,"4CC00A":12,"4CC5D9":4,"4CC64C":10,"4CC94F":15,"4CC95E":7,"4CCA95":8,"4CCBEA":8,"4CCC34":14,"4CCC6A":3,"4CCDB6":0,"4CCF7C":5,"4CD012":0,"4CD0CB":8,"4CD0DD":8,"4CD1A1":8,"4CD629":8,"4CD717":4,"4CD98F":4,"4CDD31":7,"4CDE48":8,"4CE0DB":10,"4CE1732":6,"4CE20F":10,"4CE4B6":14,"4CE650":0,"4CE65E":8,"4CE6C0":0,"4CEAAE":11,"4CEB42":20,"4CEBB0":7,"4CECEE":7,"4CEDFB":1,"4CEFC0":30,"4CF202":10,"4CF475":8,"4CF55B":8,"4CF95D":8,"4CFB45":8,"50016B":8,"5001BB":7,"5001D9":8,"500238":15,"5004B8":8,"50056E":11,"5007C3":30,"500B23":8,"500B26":8,"500B918":26,"50125C":16,"50131D":14,"5014C1":8,"5016F4":14,"501AC5":19,"501D93":8,"501FC6":0,"5021EC":8,"5023A2":0,"502690":24,"50284A":20,"502873":8,"5029F5":11,"502D1D":15,"502DA2":20,"502DBB":37,"502F9B":20,"502FBB":14,"503237":0,"503275":7,"503B70":0,"503CC4":6,"503CEA":11,"503D7F":15,"503DA1":7,"503DC6":10,"503DD1":31,"503EAA":31,"503F50":8,"504061":15,"504172":8,"504472":14,"50464A":8,"50465D":1,"504877":9,"5049B0":7,"504A6E":33,"504B9E":8,"504F3B":10,"5050A4":7,"50523B":15,"505527":17,"5056BF":7,"50578A":0,"50586F":8,"505DAC":8,"505E3A":37,"506382":8,"506391":8,"50642B":10,"5065F3":5,"5066E5":8,"50680A":8,"5068AC":8,"506A03":33,"506F77":8,"5076AF":20,"507705":7,"5078B0":8,"507A55":0,"507AC5":0,"507C6F":20,"508140":5,"5082D5":0,"508492":20,"508569":7,"50874D":11,"508811":10,"5089D1":8,"508A7F":8,"508D62":8,"508D9E":8,"508E49":10,"508F4C":10,"5091E3":31,"50926A":10,"5092B9":7,"5093CE":8,"509546":8,"509839":10,"50995A":30,"509A4C":4,"509A88":8,"509EA7":7,"509F27":8,"50A009":10,"50A0A4":15,"50A1F3":8,"50A4C8":7,"50A67F":0,"50A6D8":0,"50A72B":8,"50ACB9":8,"50B03B":16,"50B127":0,"50B7C3":7,"50BC96":0,"50BD5F":31,"50C7BF":31,"50C8E5":7,"50CCF8":7,"50D2F5":10,"50D45C":30,"50D4F7":31,"50DAD6":10,"50DCE7":30,"50DDAB":7,"50DE06":0,"50E085":20,"50E0EF":15,"50E549":39,"50E7B7":12,"50EAD6":0,"50EB71":20,"50EBF6":1,"50EC50":10,"50ED3C":0,"50F0D3":7,"50F265":0,"50F351":0,"50F4EB":0,"50F520":7,"50F5DA":30,"50F7ED":8,"50F958":8,"50FA84":31,"50FC9F":7,"50FE39":10,"540295":8,"5404A6":1,"540764":8,"54077D":33,"540910":0,"540DF9":8,"540E2D":12,"540E58":11,"54102E":8,"54104F":7,"541149":12,"5412CB":8,"541310":8,"5414F3":20,"5419C8":12,"541FCD":11,"54211D":8,"54219D":7,"542259":8,"542369":8,"5425EA":8,"542618":8,"54263D":16,"542696":0,"542758":14,"542906":0,"542A43":0,"542B1C":30,"542B8D":0,"542F2B":8,"5432C7":0,"5433CB":0,"5434EF":8,"543631":20,"5439DF":8,"543AD6":7,"5440AD":7,"544249":16,"544408":15,"54443B":8,"5444A3":7,"544810":4,"5448E6":10,"544C8A":19,"544E90":0,"54511B":8,"545284":8,"5453ED":16,"5455D5":8,"545618":8,"545925":8,"545B86":26,"546009":18,"54606D":8,"5462E2":0,"5464BC":11,"546706":11,"546749":18,"546990":8,"546CEB":20,"5471DD":8,"54724F":0,"54735A":8,"547595":31,"547975":15,"54880E":7,"548998":8,"548D5A":20,"549209":8,"54926A":37,"5492BE":7,"549963":0,"549B12":7,"549F06":15,"549F13":0,"549F35":4,"54A050":1,"54A51B":8,"54A637":8,"54A6DB":8,"54A703":31,"54AE27":0,"54AF97":31,"54B121":8,"54B802":7,"54B874":37,"54B8DB":0,"54BAD6":8,"54BD79":7,"54BF64":4,"54C480":8,"54C80F":31,"54CD10":26,"54CF8D":8,"54D17D":7,"54D9C6":8,"54DD21":8,"54DD4F":7,"54E15B":8,"54E43A":0,"54E4ED":20,"54E61B":0,"54E6FC":31,"54E6FD":16,"54EAA8":0,"54EAE1":9,"54EBE9":0,"54EF43":8,"54F201":7,"54F294":8,"54F607":8,"54F6E2":8,"54FA3E":7,"54FA96":15,"54FCF0":7,"58044F":31,"5807F8":15,"580987":30,"580AD4":0,"580FA5":0,"581122":1,"58170C":16,"581862":16,"581CF8":20,"581F28":8,"581FAA":0,"582059":10,"582071":7,"5820B1":5,"582429":18,"582575":8,"582A93":0,"582AF7":8,"58306E":15,"58355D":8,"583653":0,"583F54":17,"58404E":0,"584120":31,"5843AB":11,"584498":10,"584822":16,"585595":0,"5855CA":0,"5856AA":0,"5856C2":8,"58605F":8,"5864C4":0,"58666D":0,"586B14":0,"586C25":20,"586D67":20,"5873D1":8,"5873D8":0,"587961":19,"5879E0":7,"587A6A":11,"587F57":0,"587F66":8,"5882A8":19,"588336":8,"58879F":8,"588A5A":4,"5891CF":20,"589351":8,"5893E8":0,"58946B":20,"5894AE":8,"58957E":8,"58960A":17,"58961D":20,"589A3E":30,"58A023":20,"58A2B5":17,"58A639":7,"58A839":20,"58A8E8":30,"58ABFB":8,"58AD12":0,"58AE2B":8,"58AEA8":8,"58B035":0,"58B10F":7,"58B18F":8,"58B623":10,"58B965":0,"58BAD4":8,"58BE72":8,"58C38B":7,"58C41E8":10,"58C5CB":7,"58C6F0":11,"58CB52":18,"58CE2A":20,"58D061":8,"58D349":0,"58D61F":32,"58D697":11,"58D759":8,"58D812":31,"58D9C3":14,"58E28F":0,"58E488":30,"58E6BA":0,"58EA1F":10,"58F2FC":8,"58F8D7":8,"58F987":8,"58FB3E":8,"58FB84":20,"58FDB1":17,"5C0214":10,"5C0339":8,"5C07A6":8,"5C0947":0,"5C0979":8,"5C0A5B":7,"5C0B3B":8,"5C10C5":7,"5C13AC":0,"5C13CC":0,"5C1648":11,"5C167D":8,"5C1720":8,"5C17CF":13,"5C1BF4":0,"5C1CB9":12,"5C1DD9":0,"5C23C2":0,"5C260A":4,"5C2E59":7,"5C337B":18,"5C36B8":38,"5C3C27":7,"5C3E1B":0,"5C4071":10,"5C415A":30,"5C4879":8,"5C497D":7,"5C4CA9":8,"5C50D9":0,"5C5136":7,"5C514F":20,"5C5181":7,"5C5188":14,"5C5230":0,"5C5284":0,"5C546D":8,"5C57C8":15,"5C5948":0,"5C5E0A":7,"5C5EBB":8,"5C5F67":20,"5C60BA":5,"5C6117":8,"5C628B":31,"5C63BF":31,"5C647A":8,"5C666C":11,"5C6783":20,"5C6F69":22,"5C7017":0,"5C7075":8,"5C70A3":17,"5C76D5":15,"5C78F8":8,"5C7D5E":8,"5C80B6":20,"5C8382":15,"5C843C":16,"5C8505":8,"5C865C":7,"5C8730":0,"5C879C":20,"5C899A":31,"5C8A38":5,"5C8B6B":30,"5C8D4E":0,"5C9157":8,"5C9175":0,"5C95AE":0,"5C9666":16,"5C969D":0,"5C97F3":0,"5C9960":7,"5C9977":0,"5C9AA1":8,"5C9AD8":24,"5C9BA6":0,"5CA39D":7,"5CA64F":31,"5CA6E6":31,"5CA86A":8,"5CAC3D":7,"5CAD76":38,"5CADBA":0,"5CADCF":0,"5CAF06":17,"5CB00A":8,"5CB26D":20,"5CB395":8,"5CB43E":8,"5CB47E":20,"5CB524":16,"5CB8B7":0,"5CB901":5,"5CBA37":19,"5CBD9A":8,"5CC0A0":8,"5CC1D7":7,"5CC1F2":8,"5CC307":8,"5CC5D4":20,"5CC787":8,"5CCA1A":19,"5CCB99":7,"5CCD5B":20,"5CD06E":10,"5CD2E4":20,"5CD33D":7,"5CD89E":8,"5CDC49":7,"5CE0C5":20,"5CE42A":20,"5CE50C":10,"5CE747":8,"5CE7A0":15,"5CE883":8,"5CE8EB":7,"5CE91E":0,"5CE931":31,"5CEDF4":7,"5CF5DA":0,"5CF6DC":7,"5CF7E6":0,"5CF938":0,"5CF96A":8,"5CF9DD":4,"6001B1":8,"600308":0,"6006E3":0,"6007C4":11,"600810":8,"600F6B":0,"60109E":8,"60123C":8,"60156F":31,"60183A":8,"601895":4,"601D91":14,"601F56":8,"602101":11,"602232":32,"60292B":31,"602E20":8,"602ED5":0,"6030B3":8,"6030D4":0,"6032B1":31,"60334B":0,"6036DD":20,"603A7C":31,"603AAF":7,"603CEE":17,"603D29":8,"603E5F":0,"60452E":20,"6045BD":19,"6045CB":1,"604DE1":8,"604F5B":8,"605355":8,"605375":8,"6056B1":8,"605718":20,"6057C8":0,"605B30":4,"605E4F":8,"605FAA":8,"606525":0,"606720":20,"60684E":7,"606944":0,"606BBD":7,"606C66":20,"606EE8":10,"60706C":18,"6070C0":0,"60756C":17,"6077E2":7,"6079C9":8,"607AD8":37,"607EA4":10,"607EC9":0,"607ECD":8,"607EDD":19,"607FCB":7,"608110":0,"608246":0,"608306":8,"608334":8,"608373":0,"6083E7":31,"608B0E":0,"608C4A":0,"608E08":7,"608F5C":7,"608FA4":15,"6091F3":12,"609217":0,"609316":0,"609578":7,"6095BD":0,"6096A4":8,"609849":15,"609AC1":0,"609BB4":8,"60A10A":7,"60A2C6":8,"60A37D":0,"60A3E3":31,"60A44C":1,"60A4B7":31,"60A4D0":7,"60A5E2":20,"60A6C5":8,"60A751":8,"60A8FE":15,"60AAEF":8,"60AB67":10,"60AF6D":7,"60B0E8":8,"60B4A2":7,"60B76E":18,"60BD83":8,"60BEB5":14,"60BEC4":0,"60C547":0,"60C5AD":7,"60C9AA":15,"60CE41":8,"60CF84":1,"60D039":0,"60D0A9":7,"60D4AF":9,"60D4E9":11,"60D51B":24,"60D755":8,"60D9A0":6,"60D9C7":0,"60DD70":0,"60DD8E":20,"60DE18":0,"60DE44":8,"60DE94":8,"60DEF3":8,"60E327":31,"60E32B":20,"60E3AC":17,"60E701":8,"60F04D":9,"60F18A":8,"60F262":20,"60F445":0,"60F549":0,"60F677":20,"60F723":10,"60F81D":0,"60FA9D":8,"60FACD":0,"60FB42":0,"60FDA6":0,"60FEC5":0,"60FF12":7,"64006A":4,"64037F":7,"64078C":8,"6407F6":7,"640980":10,"640BD7":0,"640C91":0,"640D22":17,"6411A4":14,"6413AB":8,"6416F0":8,"6417CD":7,"641B2F":7,"641CAE":7,"641CB0":7,"64200C":0,"642315":8,"642753":8,"6429FF":8,"642C0F":12,"642CAC":8,"642E41":8,"642F1C":8,"643135":0,"643150":5,"6432A8":20,"643E0A":8,"643E8C":8,"6441E6":0,"64447B":12,"644842":0,"64497D":20,"644A7D":20,"644C36":20,"644ED7":5,"645106":5,"6451F4":8,"6453E0":8,"645601":31,"6456B5":7,"645725":38,"6457BA":20,"645A36":0,"645AED":0,"645D86":20,"645DF4":7,"645E10":8,"646140":8,"646306":10,"64644A":10,"6466B3":31,"6466D8":7,"6467CD":8,"646CB2":7,"646D2F":0,"646D4E":8,"646D6C":8,"646E97":31,"646EE0":20,"647002":31,"647033":0,"6476BA":0,"647791":7,"647924":8,"6479F0":20,"647BCE":7,"648099":20,"64876C":8,"64899A":17,"6489F1":7,"6490C1":10,"64956C":17,"649ABE":0,"649D38":18,"649E31":10,"64A198":8,"64A200":10,"64A28A":8,"64A2F9":13,"64A3CB":0,"64A5C3":0,"64A651":8,"64A963":8,"64ACE0":7,"64B0A6":0,"64B0E8":8,"64B310":7,"64B473":10,"64B5F2":7,"64B853":7,"64B94E":4,"64B9E8":0,"64BC0C":17,"64BC43":8,"64BC58":20,"64BD6D":0,"64BF6B":8,"64C045":0,"64C2DE":17,"64C394":8,"64C753":0,"64C905":0,"64C9F1":30,"64CC2E":10,"64CDC2":30,"64D0D6":7,"64D2C4":0,"64D4DA":20,"64D562":8,"64D69A":20,"64D7C0":8,"64DB43":14,"64DBF7":15,"64DDE9":10,"64DE6D":20,"64E003":38,"64E4A5":17,"64E682":0,"64E7D8":7,"64EC65":12,"64F705":8,"64F81C":8,"680571":7,"6805CA":20,"680715":20,"680927":0,"681324":8,"6813F3":30,"681729":20,"681A47":0,"681BEF":8,"6822E5":8,"682737":7,"68286C":16,"682E3C":32,"682F67":0,"683036":0,"683045":8,"683421":20,"6837E9":30,"683B09":0,"683E26":20,"683EC0":0,"684465":0,"684571":8,"6845CC":0,"6845F1":25,"6847C5":20,"684898":7,"684983":8,"684A5F":0,"684AAE":8,"684AE9":7,"684C25":8,"684DB6":10,"684F64":4,"68545A":20,"6854FD":30,"6858A0":9,"685ACF":7,"685B35":0,"685D43":20,"685EDD":0,"68628A":12,"686372":8,"68644B":0,"686CE6":19,"687251":32,"6872C3":7,"68764F":16,"687724":31,"6879ED":27,"687A64":20,"687D6B":7,"687DAC":7,"687FF0":31,"6881E0":8,"6883CB":0,"68847E":24,"6885A4":11,"68871C":14,"6889C1":8,"688F84":8,"689268":14,"68951B":8,"68962E":8,"68967B":0,"689A87":30,"689B43":8,"689C70":0,"689E6A":8,"689FD4":30,"68A03E":8,"68A0F6":8,"68A34F":15,"68A46A":8,"68A593":0,"68A729":0,"68A7B4":9,"68A828":8,"68A86D":0,"68AB09":15,"68AB1E":0,"68ABBC":10,"68AE20":0,"68B599":5,"68B5E3":8,"68B691":30,"68B8BB":10,"68BFC4":7,"68C44C":10,"68C44D":14,"68C6AC":20,"68CAC4":0,"68CC6E":8,"68D79A":32,"68D927":8,"68D93C":0,"68DBCA":0,"68DBF5":30,"68DDB7":31,"68DFDD":10,"68DFE4":7,"68E209":8,"68E580":0,"68E7C2":7,"68EBAE":7,"68ECC5":20,"68EF43":0,"68EFDC":0,"68F0B5":9,"68F543":8,"68F63B":30,"68F7D8":19,"68F90F":20,"68FB7E":0,"68FCB6":11,"68FCCA":7,"68FEF7":0,"68FF7B":31,"6C006B":7,"6C028C":8,"6C02E0":5,"6C047A":8,"6C0699":10,"6C06D6":8,"6C0B5E":5,"6C0C9A":30,"6C0D34":15,"6C0DC4":10,"6C0E0D":16,"6C1270":0,"6C146E":8,"6C1544":19,"6C1632":8,"6C19C0":0,"6C1A75":8,"6C1D2C":8,"6C1ED7":12,"6C1F8A":0,"6C22F7":15,"6C23B9":16,"6C2483":19,"6C24A6":12,"6C2636":8,"6C2779":19,"6C2995":20,"6C2B59":4,"6C2F2C":7,"6C2F80":20,"6C2F8A":7,"6C3491":8,"6C3AFF":0,"6C3BE5":5,"6C3C8C":4,"6C3E6D":0,"6C4008":0,"6C40E8":12,"6C41DE":8,"6C442A":8,"6C483F":10,"6C4A85":0,"6C4CBC":31,"6C4CE2":20,"6C4D73":0,"6C51BF":8,"6C51E4":8,"6C5563":7,"6C558D":8,"6C55B1":30,"6C5697":30,"6C5AB0":31,"6C5AB5":38,"6C5C14":11,"6C5D3A":19,"6C5F1C":6,"6C6016":13,"6C60D0":8,"6C626D":3,"6C6286":15,"6C63F8":32,"6C67EF":8,"6C688A":30,"6C6A77":20,"6C6C0F":8,"6C709F":0,"6C70CB":7,"6C71D2":8,"6C72E7":0,"6C7637":8,"6C77F0":8,"6C7E67":0,"6C7F49":8,"6C8243":8,"6C8336":7,"6C8375":22,"6C8814":20,"6C8DC1":0,"6C8FB5":19,"6C9188":15,"6C92CF":22,"6C9466":20,"6C94F8":0,"6C96CF":0,"6C976D":14,"6C999D":30,"6C9B02":15,"6CA100":20,"6CA780":15,"6CAB31":0,"6CACC2":7,"6CAEE3":15,"6CB0CE":33,"6CB133":0,"6CB158":31,"6CB227":16,"6CB4FD":8,"6CB749":8,"6CB7E2":8,"6CB7F4":7,"6CC217":5,"6CC26B":0,"6CC36A":12,"6CC7EC":7,"6CCB7F":37,"6CCDD6":33,"6CD032":17,"6CD199":12,"6CD1E5":8,"6CD63F":8,"6CD68A":17,"6CD704":8,"6CD71F":11,"6CD94C":12,"6CDDBC":7,"6CE5C9":0,"6CE85C":0,"6CE873":31,"6CE874":8,"6CE907":15,"6CEBB6":8,"6CF049":39,"6CF373":7,"6CF6DA":20,"6CF712":15,"6CF784":10,"6CFE54":20,"700514":17,"700810":20,"700971":7,"701124":0,"701384":0,"7014A6":0,"7015FB":20,"70192F":8,"701AB8":20,"701CE7":20,"701F3C":7,"70217F":10,"7022FE":0,"702526":15,"702605":16,"70287D":18,"70288B":7,"702AD5":7,"702F35":8,"70317F":0,"703217":20,"70357B":12,"703A51":10,"703ACB":18,"703C69":0,"703EAC":0,"7040FF":8,"704698":8,"7047E9":12,"70480F":0,"704CA2":0,"704D7B":1,"704E6B":8,"704EE0":7,"704F57":31,"7054F5":8,"705681":0,"705812":26,"705A0F":5,"705A6F5":2,"705AAC":7,"705FA3":10,"7062CB":0,"70662A":16,"7066B9":8,"7069791":34,"706E10":8,"70700D":0,"707013":8,"7070AA":30,"7070D5":8,"70720D":6,"70723C":8,"7072FE":0,"707362":8,"7073CB":0,"70788B":12,"707990":8,"707BE8":8,"707CE3":8,"7081EB":0,"7086CE":37,"708A09":8,"708BCD":1,"708CB6":8,"708CF2":0,"708D09":15,"708F47":12,"7090B7":8,"709684":0,"709751":10,"709AC4":8,"709C45":8,"709CD1":20,"709E29":16,"70A04B":20,"70A2B3":0,"70A6BD":9,"70A6CC":20,"70A703":9,"70A741":32,"70A8A5":19,"70A8D3":20,"70A8E3":8,"70AE2A":0,"70AED5":0,"70B13D":7,"70B306":0,"70B3D5257":17,"70B3D5270":30,"70B3D54AC":19,"70B3D54F1":17,"70B3D5723":17,"70B3D5765":17,"70B3D5884":17,"70B3D588D":17,"70B3D5927":17,"70B3D59E3":17,"70B3D5A23":17,"70B3D5D28":25,"70B3D5E6F":30,"70B3D5EC8":26,"70B51A":8,"70B5E8":4,"70B7AA":12,"70B7E4":22,"70BB5B":0,"70BBE9":10,"70BC10":19,"70C288":20,"70C7F2":8,"70CD0D":20,"70CD60":0,"70CE8C":7,"70CF49":20,"70D313":8,"70D823":20,"70D8C2":20,"70D923":12,"70DDA8":11,"70DDEF":8,"70DEE2":0,"70E72C":0,"70E997":8,"70EA5A":0,"70EBA5":8,"70ECE4":0,"70EF00":0,"70F087":0,"70F8AE":19,"70F927":7,"70F94A":0,"70FD45":8,"70FD46":7,"7400E8":8,"74042B":6,"7404F1":20,"7405A5":31,"740AE1":8,"740CEE":8,"740EA4":0,"7410E0":22,"7411A0":15,"741213":34,"74136A":14,"7413EA":20,"7414D0":0,"741575":10,"7415F5":0,"74190A":7,"741BB2":0,"741EB1":7,"7422BB":8,"742344":10,"742435":8,"742554":21,"742869":8,"742917":0,"742959":0,"742B62":24,"743174":0,"743357":12,"74342B":8,"743822":10,"743989":31,"743AF4":20,"743C24":8,"743F8E":0,"744218":0,"74428B":0,"744401":33,"74452D":8,"74458A":7,"7446A0":5,"744D6D":8,"7450CD":8,"7451BA":10,"74563C":39,"7458F3":30,"745909":8,"745AAA":8,"745C2E":9,"745F00":7,"7460FA":8,"7463C2":8,"74650C":0,"746DFA":7,"747069":8,"7470FD":20,"74718B":0,"7473B4":0,"747446":18,"747548":30,"74761F":19,"747786":0,"747827":4,"7478DE":12,"748114":0,"7483C2":32,"748669":11,"74867A":4,"7486E2":4,"74872E":8,"74882A":8,"748B23":7,"748D08":0,"748DAA":8,"748F3C":0,"748FBF":11,"749B89":8,"749D8F":8,"749EAF":0,"749EF5":7,"74A063":8,"74A528":8,"74A57E":26,"74A6CD":0,"74A722":17,"74A7EA":30,"74A981":8,"74ACB9":32,"74B059":14,"74B587":0,"74B725":8,"74B8A8":8,"74BC6B":20,"74BEF3":14,"74C14F":8,"74C17E":17,"74C246":30,"74C412":19,"74C530":12,"74CC40":0,"74D02B":1,"74D21D":8,"74D423":30,"74D435":39,"74D558":11,"74D5E8":0,"74D637":30,"74D6E5":8,"74D7CA":26,"74D809":19,"74D83E":20,"74DA78":5,"74DA88":31,"74E147":11,"74E1B6":0,"74E20C":30,"74E28C":19,"74E2F5":0,"74E50B":20,"74E5F9":20,"74E6B8":17,"74E6E2":4,"74E987":0,"74E9BF":8,"74EA3A":31,"74EB80":7,"74ECB2":30,"74EF4B":11,"74F2FA":10,"74F441":7,"74F67A":7,"74F90F":8,"74F92C":32,"74FA29":32,"74FC77":8,"74FECE":31,"78009E":7,"78028B":0,"7802F8":10,"78034F":15,"7804E3":8,"7806C9":8,"78078F":8,"78084D":8,"780CB8":20,"7811DC":10,"781699":8,"781735":15,"7817BE":8,"7818A8":8,"781A32":8,"781C5A":27,"781DBA":8,"781F7C":15,"781FDB":7,"782051":31,"782327":7,"7824AF":1,"782599":8,"7825AD":7,"782B46":20,"782B60":8,"782BCB":4,"782DAD":8,"782EEF":15,"782F02":9,"7831C1":0,"7833C6":7,"783409":8,"783486":15,"7834B4":8,"7834FD":12,"7836CC":11,"783716":7,"783A84":0,"783EA1":15,"783F4D":0,"7840E4":7,"7844FD":31,"784558":32,"7845B3":8,"7845C4":4,"7845DC":8,"7846D4":7,"78471D":7,"784859":5,"7849D7":7,"784F43":0,"78521A":7,"785333":10,"785773":8,"785860":8,"78595E":7,"785B64":8,"785C5E":8,"785DC8":17,"785ECC":0,"78605B":31,"786089":7,"786256":8,"7864C0":0,"78669D":38,"7867D7":0,"786A89":8,"786C1C":0,"786C84":30,"786CAB":11,"787462":20,"78753E":0,"787826":8,"787984":0,"787B8A":0,"787E61":0,"788371":8,"78843C":16,"7885F4":8,"78862E":19,"78886D":0,"788A20":32,"788CB5":31,"78923E":15,"78929C":20,"7893C3":38,"78960D":0,"78966E":8,"789987":10,"789ED0":7,"789F70":0,"789FAA":8,"78A03F":30,"78A106":31,"78A3E4":0,"78A7C7":0,"78A873":7,"78ABBB":7,"78AC44":4,"78ACC0":5,"78AF08":20,"78B46A":8,"78B554":8,"78B5F2":8,"78B6FE":7,"78BB5C":15,"78BDBC":7,"78C11D":7,"78C3E9":7,"78C5F8":8,"78C881":16,"78C884":8,"78CA04":15,"78CA39":0,"78CF2F":8,"78CFF9":8,"78D162":0,"78D294":33,"78D6B2":25,"78D6DC":14,"78D6F0":7,"78D752":8,"78D75F":0,"78D840":10,"78DAAF":8,"78DC87":8,"78DD33":8,"78DF72":10,"78E0C5":7,"78E103":30,"78E22C":8,"78E3B5":5,"78E3DE":0,"78E61C":9,"78E7D1":5,"78EB46":8,"78EDBC":13,"78F09B":8,"78F238":7,"78F557":8,"78F5FD":8,"78F7BE":7,"78F882":17,"78F9B4":15,"78FBD8":0,"78FD94":0,"78FF57":20,"7C004D":8,"7C0191":0,"7C035E":10,"7C03AB":10,"7C04D0":0,"7C0A3F":7,"7C0BC6":7,"7C0CF1":10,"7C0CFA":8,"7C10C9":1,"7C11BE":0,"7C11CB":8,"7C162A":8,"7C1960":8,"7C19E3":18,"7C1AC0":8,"7C1B93":8,"7C1C68":7,"7C1CF1":8,"7C1DD9":10,"7C1E52":19,"7C214A":20,"7C2302":7,"7C2499":0,"7C27BC":38,"7C296F":0,"7C2A31":20,"7C2ACA":0,"7C2ADB":10,"7C2EBD":18,"7C2EDD":7,"7C33F9":8,"7C3626":8,"7C38AD":7,"7C3985":8,"7C3B2D":0,"7C3D2B":8,"7C3E74":8,"7C41A2":15,"7C4685":14,"7C49EB":10,"7C4B26":0,"7C4D8F":5,"7C4FCD":0,"7C5049":0,"7C5079":20,"7C5758":5,"7C59B1":0,"7C5CF8":20,"7C6097":8,"7C6130":0,"7C6166":30,"7C6305":30,"7C6456":7,"7C646C":17,"7C669A":8,"7C67A2":20,"7C68B9":8,"7C692B":8,"7C6B9C":11,"7C6D12":19,"7C6D62":0,"7C6DF8":0,"7C70DB":20,"7C73EB":8,"7C752D":7,"7C7635":20,"7C7668":8,"7C787E":7,"7C7A91":20,"7C7B1C":14,"7C7BBF":7,"7C7D3D":8,"7C852F":8,"7C8530":15,"7C8931":8,"7C8956":7,"7C8BB5":7,"7C8BCA":31,"7C90E9":11,"7C9122":7,"7C942A":8,"7C97E1":8,"7C9A1D":0,"7CA05E":14,"7CA177":8,"7CA1AE":0,"7CA23E":8,"7CA449":10,"7CA53E":14,"7CA909":38,"7CAB60":0,"7CAF77":15,"7CB0C2":20,"7CB15D":8,"7CB232":38,"7CB27D":20,"7CB566":20,"7CB59B":31,"7CB59F":8,"7CC06F":0,"7CC0AA":19,"7CC180":0,"7CC225":7,"7CC294":10,"7CC2C6":31,"7CC385":8,"7CC3A1":0,"7CC518":12,"7CC537":0,"7CC882":8,"7CC8DF":0,"7CC95A":4,"7CCCB8":20,"7CD1AD":0,"7CD1C3":0,"7CD2DA":0,"7CD3E5":8,"7CD566":30,"7CD62C":0,"7CD661":10,"7CD95C":18,"7CD9A0":8,"7CDC73":8,"7CE53F":8,"7CECB1":0,"7CED8D":19,"7CEDC6":30,"7CF05F":0,"7CF0E5":13,"7CF17E":31,"7CF31B":17,"7CF34D":0,"7CF854":7,"7CF90E":7,"7CFADF":0,"7CFC16":0,"7CFD6B":10,"80000B":20,"80006E":0,"80045F":0,"800518":8,"800794":7,"800C67":0,"800CF9":30,"800D3F":7,"801242":0,"801316":20,"801382":8,"801844":4,"8018A7":7,"801934":20,"801970":7,"801D39":0,"8020FD":7,"802AA8":32,"802AF6":9,"802EC3":8,"802EDE":8,"8031F0":7,"803253":20,"8035C1":10,"803773":33,"803896":27,"8038BC":8,"8038FB":20,"80398C":7,"803C04":31,"803C20":8,"803E4F":37,"803FD4":0,"804126":8,"8045DD":20,"804715":0,"804786":7,"804971":0,"804A14":0,"804DCB":8,"804E70":7,"804E81":7,"80501B":15,"80542D":7,"80549C":7,"8054D9":8,"8054E3":0,"805719":7,"8058F8":14,"805A04":17,"805FC5":0,"806036":8,"80656D":7,"80657C":0,"806933":8,"806C1B":14,"806D71":30,"806F1C":8,"80717A":8,"807264":8,"8075BF":7,"8076C2":37,"807B3E":7,"807D14":8,"808223":0,"8083F6":0,"808489":20,"8086D9":7,"8086F2":20,"808917":31,"808943":7,"808A8B":12,"808ABD":7,"808F1D":31,"808F97":10,"80929F":0,"80953A":0,"809621":6,"809698":0,"8099E7":16,"809B20":20,"809FF5":7,"80A63C":30,"80A997":0,"80AB4D":15,"80AD16":10,"80AE54":31,"80AF19":0,"80B03D":0,"80B575":8,"80B655":20,"80B686":8,"80B946":15,"80B989":0,"80BE05":0,"80BF21":12,"80C01E":20,"80C16E":5,"80C5E6":19,"80C755":26,"80CC12":8,"80CC9C":33,"80CE62":5,"80CEB9":7,"80CF41":6,"80CFA2":8,"80D09B":8,"80D1CE":0,"80D4A5":8,"80D605":0,"80DACE":8,"80E1BF":8,"80E4BA":20,"80E63C":10,"80E650":0,"80E82C":5,"80EA07":31,"80EA96":0,"80ED2C":0,"80F1A4":8,"80FB06":8,"8400D2":16,"84016E":9,"840511":0,"84083A":20,"840B2D":7,"840F4C":0,"84100D":14,"84119E":7,"84144D":20,"8415D3":8,"84160C":22,"8416F9":31,"841985":7,"841B5E":33,"841B77":20,"8421F1":8,"842289":7,"842519":7,"8425DB":7,"84262B":15,"842789":0,"842859":30,"842999":0,"842AFD":5,"842B2B":4,"842E27":7,"842F57":0,"843497":5,"8437D5":7,"843835":0,"843838":7,"8439FC":15,"843A4B":20,"843CFC":8,"843E1D":38,"843E92":8,"844167":0,"844693":10,"8446FE":8,"844765":8,"844880":30,"845075":8,"845181":7,"8454DF":8,"8455A5":7,"845733":19,"8457EB":14,"845B12":8,"845C31":4,"845CF3":20,"845F04":7,"8463D6":19,"8464DD":8,"84683E":20,"846878":0,"846991":15,"846993":5,"846EBC":15,"846FCE":11,"84716A":8,"847637":8,"847848":32,"84788B":0,"847B57":20,"847BEB":4,"847C9B":37,"848506":0,"8488E1":0,"8489AD":0,"848C8D":0,"848E0C":0,"848EDF":16,"848F69":4,"849265":20,"8492E5":8,"8493A0":8,"849437":0,"849866":7,"849FB5":8,"84A134":0,"84A1B7":9,"84A466":7,"84A6C8":20,"84A824":18,"84A8E4":8,"84A93E":5,"84A9C4":8,"84AB1A":0,"84AC16":0,"84AD58":8,"84AD8D":0,"84AEDE":10,"84B153":0,"84B1E2":19,"84B1E4":0,"84B541":7,"84B890":31,"84B8B8":14,"84BE52":8,"84C0EF":7,"84C5A6":20,"84C7EA":16,"84C8A0":38,"84CC63":8,"84D1C1":20,"84D328":0,"84D3D5":8,"84D6D0":30,"84D7DE":8,"84D81B":31,"84DBA4":8,"84DBAC":8,"84DBFC":15,"84E657":16,"84E986":8,"84EE7F":8,"84EEE4":7,"84EF18":20,"84FCAC":0,"84FCFE":0,"84FDD1":20,"84FE40":8,"8803E9":11,"88074B":17,"881032":0,"88108F":8,"881196":8,"881566":8,"8815C5":8,"8817A8":0,"8818F1":15,"881908":0,"881E5A":0,"881FA1":0,"88200D":0,"882593":31,"8828B3":8,"88299C":7,"8829BF":30,"882F92":10,"88329B":7,"88365F":17,"8836CF":8,"883D24":18,"883F27":8,"883FD3":8,"884033":8,"88403B":8,"884477":8,"8844F6":15,"884604":10,"884D7C":0,"8851F2":0,"8851FB":5,"8852EB":10,"88532E":20,"885395":0,"8853D4":8,"88541F":18,"88548E":12,"885A06":11,"885E54":7,"8863C5":8,"8863DF":0,"886440":0,"886639":8,"88665A":0,"8866A5":0,"8867DC":8,"88684B":11,"88693D":8,"886AB1":12,"886B6E":0,"886BDB":0,"886C60":10,"886D2D":8,"886EEB":8,"886FD4":4,"887015":0,"88708C":6,"8871E5":30,"887384":25,"887477":8,"887598":7,"887873":20,"88797E":14,"887E9B":0,"8881B9":8,"888322":7,"888603":8,"88892F":8,"888E68":8,"888FA4":8,"889986":31,"889B39":7,"889F6F":7,"88A0BE":8,"88A29E":28,"88A2D7":8,"88A303":7,"88A479":0,"88A9B7":0,"88ADD2":7,"88AE07":0,"88B111":20,"88B291":0,"88B362":15,"88B4A6":14,"88B4BE":8,"88B7EB":0,"88B945":0,"88B951":10,"88BCC1":8,"88BD45":7,"88BFE4":8,"88C08B":0,"88C227":8,"88C344":18,"88C397":10,"88C663":0,"88C6E8":8,"88C9D0":17,"88C9E8":16,"88CB87":0,"88CE3F":8,"88CEFA":8,"88CF98":8,"88D50C":11,"88D546":0,"88D7F6":1,"88D82E":20,"88DA04":8,"88DDB8":8,"88E056":8,"88E3AB":8,"88E87F":0,"88E9FE":0,"88F2BD":37,"88F2CE":0,"88F4DA":20,"88F56E":8,"88F6DC":8,"88F7BF":12,"88F872":8,"8C006D":0,"8C04BA":4,"8C0572":8,"8C08AA":0,"8C0C87":15,"8C0D76":8,"8C0EE3":11,"8C0F7E":38,"8C0FC9":8,"8C15C7":8,"8C1759":20,"8C17B6":8,"8C1952":30,"8C1ABF":7,"8C1D96":20,"8C1F6434A":28,"8C1F64397":20,"8C1F64AF4":15,"8C1F64DC4":30,"8C210A":31,"8C2114":8,"8C2505":8,"8C26AA":0,"8C2937":0,"8C2A85":30,"8C2DAA":0,"8C2E72":7,"8C3066":32,"8C3396":0,"8C3401":11,"8C3446":8,"8C34FD":8,"8C3AE3":17,"8C3BAD":33,"8C3F44":37,"8C426D":8,"8C47BE":4,"8C49B6":12,"8C4E46":14,"8C4EBB":30,"8C5219":27,"8C53C3":10,"8C554A":20,"8C5646":17,"8C5877":0,"8C5AC1":8,"8C5AF8":10,"8C5EBD":8,"8C6422":16,"8C64A2":13,"8C6794":12,"8C683A":8,"8C6A3B":7,"8C6BDB":8,"8C6D77":8,"8C705A":20,"8C71F8":7,"8C736E":24,"8C7712":7,"8C79F5":7,"8C7A00":15,"8C7A3D":10,"8C7AAA":0,"8C7B9D":0,"8C7C92":0,"8C8283":0,"8C83DF":15,"8C83E1":7,"8C83E8":8,"8C8474":22,"8C8590":0,"8C861E":0,"8C862A":8,"8C86DD":31,"8C89A5":3,"8C8ACD":8,"8C8D28":20,"8C8EF2":0,"8C8FE9":0,"8C902D":31,"8C90D3":15,"8C91A4":0,"8C986B":0,"8C9885":7,"8CA2F5":8,"8CA3EC":7,"8CA5CF":8,"8CA6DF":31,"8CA96D":8,"8CA982":20,"8CAACE":10,"8CB0E9":7,"8CB84A":7,"8CB87E":20,"8CBEBE":10,"8CBFA6":7,"8CC121":26,"8CC246":0,"8CC5D0":7,"8CC681":20,"8CC8CD":7,"8CC9E9":8,"8CCF09":4,"8CD0B2":10,"8CD9D6":10,"8CDCD4":5,"8CDEE6":7,"8CDEF9":10,"8CDF2C":12,"8CE042":12,"8CE5C0":7,"8CE5EF":8,"8CE9EE":20,"8CE9FF":4,"8CEA48":7,"8CEBC6":8,"8CEC4B":4,"8CEC7B":0,"8CEDE1":32,"8CF112":14,"8CF5A3":7,"8CF773":15,"8CF8C5":20,"8CFABA":0,"8CFADD":8,"8CFD18":8,"8CFE57":0,"9000DB":7,"900117":8,"900325":8,"900628":7,"9006DB":8,"9009D0":35,"9009DF":20,"900A48":7,"900CC8":18,"901057":20,"901195":30,"9016BA":8,"90173F":8,"9017AC":8,"9017C8":8,"90187C":7,"901B0E":24,"901F94":31,"9020D7":19,"90235B":30,"9025F2":8,"9027E4":0,"902AEE":10,"902B34":39,"902BD2":8,"902C09":0,"902E1C":20,"9035A2":0,"90395F":30,"903AA0":15,"903C92":0,"903FC3":8,"903FEA":8,"9041B2":32,"904748":16,"9047C2":20,"9049FA":20,"904C02":12,"904CC5":0,"904DE2":0,"904E2B":8,"905E44":8,"905F7A":0,"9060F1":0,"9061AE":20,"90623F":0,"90633B":7,"9064AD":8,"906584":20,"90671C":8,"9068C3":14,"906989":7,"906AEB":19,"907240":0,"90735A":14,"9074F2":8,"9075BC":15,"907841":20,"9078B2":10,"90808F":8,"90812A":0,"908158":0,"908175":7,"90840D":0,"908C43":0,"908D6C":0,"908D6E":4,"909497":8,"909507":8,"9097F3":7,"909838":8,"909A4A":31,"909B6F":0,"909C4A":0,"90A25B":0,"90A57D":8,"90A5AF":8,"90A822":30,"90AC6D":0,"90ADF7":12,"90AE1B":31,"90B021":20,"90B0ED":0,"90B11C":4,"90B141":14,"90B144":7,"90B176":20,"90B21F":0,"90B622":7,"90B790":0,"90B931":0,"90B9F9":14,"90BA09":8,"90C115":16,"90C119":15,"90C1C6":0,"90C54A":12,"90C97E":8,"90CAFA":18,"90CC7A":8,"90CCDF":20,"90CDE8":0,"90CF15":15,"90D20B":15,"90D473":12,"90D733":8,"90DD5D":0,"90E17B":0,"90E2BA":20,"90E4B0":27,"90E6BA":1,"90ECE3":15,"90ECEA":0,"90EEC7":7,"90F04C":15,"90F1AA":7,"90F644":8,"90F652":31,"90F80C":7,"90F82E":30,"90F970":8,"90F9B7":8,"90FB5D":10,"90FD61":0,"90FFD6":9,"940070":15,"9400B0":8,"9401C2":7,"94049C":8,"9408C7":8,"940B19":8,"940BCD":0,"940C6D":31,"940C98":0,"940E5F":0,"940E6B":8,"940EE7":8,"94105A":4,"94147A":12,"9415B2":8,"941625":0,"941700":10,"941787":15,"941865":33,"941DD4":18,"942053":15,"942157":0,"942453":8,"942533":8,"94261D":8,"94270E":20,"942A6F":32,"942B68":0,"942DDC":7,"9431CB":12,"9432C1":9,"94350A":7,"943589":8,"94368B":37,"9437F7":8,"94390E":20,"943A91":30,"943AF0":15,"943B22":33,"943FD6":0,"9440F3":8,"944560":18,"944788":8,"9449F6":8,"944FDB":15,"945103":7,"945244":7,"9453FF":20,"9454CE":11,"9457A5":5,"945915":30,"945AEA":8,"945AFC":30,"945C9A":0,"945DBF":15,"946010":8,"94626D":10,"946372":12,"9463D1":7,"94652D":13,"94659C":20,"946A7C":13,"9476B7":7,"94772B":8,"947AF4":8,"947BAE":10,"947BE7":7,"947D77":8,"9487E0":10,"948978":0,"948B93":10,"948BC1":7,"948CD7":38,"949010":8,"949426":0,"9495A0":18,"9497AE":11,"949AA9":19,"949CBE":8,"949D57":26,"94A07D":8,"94A25D":8,"94A4F9":8,"94A67E":33,"94AA07":15,"94ABFE":15,"94AD05":8,"94AD23":0,"94B01F":0,"94B10A":7,"94B271":8,"94B3F7":38,"94B43A":7,"94B609":20,"94B819":15,"94B86D":20,"94BE46":14,"94BF2D":0,"94CE0F":8,"94CE2C":16,"94CFB0":8,"94D00D":8,"94D029":11,"94D2BC":8,"94D331":10,"94D54D":8,"94D771":7,"94D9B3":31,"94DB56":16,"94DBDA":8,"94DE80":39,"94DF34":8,"94E129":7,"94E23C":20,"94E300":8,"94E4BA":8,"94E6BA":7,"94E6F7":20,"94E70B":20,"94E7EA":8,"94E7F3":8,"94E96A":0,"94E98C":15,"94E9EE":8,"94EA32":0,"94EB2C":18,"94EF50":31,"94F6A3":0,"94F6D6":0,"94F6F2":9,"94F827":10,"94FE22":8,"9800C6":0,"9801A7":0,"98038E":31,"9803D8":0,"98045F":8,"98063C":7,"980709":8,"9809CF":13,"980A4B":15,"980C82":7,"980CA5":14,"980D51":8,"980D6F":7,"980DAF":0,"9810E8":0,"9812E0":10,"98171A":10,"981A35":8,"981CA2":0,"981DFA":7,"98226E":30,"98247B":8,"98254A":31,"982AFD":8,"982BA6":14,"982CBC":20,"982D68":7,"982F86":12,"982FF8":8,"9835ED":8,"98398E":7,"983A1F":18,"983B8F":20,"983C8C":0,"983F60":8,"983FE8":7,"9840BB":4,"9843FA":20,"9844CE":8,"98460A":0,"984827":31,"984874":8,"984B06":8,"984BE1":5,"984E8A":7,"984FEE":20,"98502E":0,"9851FC":8,"985207":8,"9852B1":7,"98535F":8,"98541B":20,"98597A":20,"985A98":8,"985AEB":0,"985F41":20,"985FD3":19,"9860CA":0,"986110":8,"98698A":0,"986DC8":25,"986EE84":24,"986F60":11,"98751A":8,"987552":10,"987A14":19,"987EB5":8,"987EE3":12,"9880EE":7,"98818A":8,"988389":7,"98865D":15,"98876C":8,"98886C":8,"988D46":20,"989096":4,"9893CC":17,"9897CC":31,"9898FB":18,"989C57":8,"989E63":0,"989E85":9,"989F1E":8,"98A375":8,"98A5F9":0,"98AD1D":8,"98AF65":20,"98B039":15,"98B08B":7,"98B379":0,"98B3EF":8,"98B8BA":17,"98B8BC":7,"98B8E3":0,"98BA5F":31,"98BD80":20,"98BEDC":9,"98C08A":8,"98C8B8":12,"98CA33":0,"98CCF3":30,"98CF7D":0,"98D293":18,"98D3D7":8,"98D6BB":0,"98D6F7":17,"98D742":7,"98DAC4":31,"98DD60":0,"98DED0":31,"98E081":10,"98E0D9":0,"98E743":4,"98E7F4":5,"98E7F5":8,"98E859":0,"98EE94":10,"98F083":8,"98F0AB":0,"98F3F6":8,"98F621":10,"98FA2E":16,"98FAE3":10,"98FB27":7,"98FE3E":20,"98FE54":28,"98FE94":0,"98FEE1":0,"98FFD0":6,"9C0298":7,"9C0351":8,"9C04EB":0,"9C0567":9,"9C05D6":32,"9C09CA":8,"9C0CDF":11,"9C1874":15,"9C1A25":0,"9C1D36":8,"9C207B":0,"9C216A":31,"9C2183":22,"9C2595":7,"9C28B3":0,"9C28EF":8,"9C28F7":10,"9C293F":0,"9C2976":20,"9C2A83":7,"9C2E7A":7,"9C2EA1":10,"9C35EB":0,"9C37CB":16,"9C37F4":8,"9C3928":7,"9C3AAF":7,"9C3DCF":33,"9C3E53":0,"9C4782":31,"9C47F4":15,"9C4929":8,"9C4A7B":15,"9C4E36":20,"9C4F5F":18,"9C4FDA":0,"9C52F8":8,"9C5322":31,"9C5467":15,"9C5636":8,"9C5766":8,"9C583C":0,"9C5884":0,"9C5A81":10,"9C5C8E":1,"9C5CF9":16,"9C5F5A":11,"9C5FB0":7,"9C6076":0,"9C611D":26,"9C61D7":8,"9C648B":0,"9C65B0":7,"9C65EB":20,"9C67D6":20,"9C69D1":8,"9C6C15":19,"9C713A":8,"9C7370":8,"9C73B1":7,"9C7403":11,"9C741A":8,"9C746F":8,"9C760E":0,"9C79E3":0,"9C7BEF":5,"9C7DA3":8,"9C823F":8,"9C8281":12,"9C8306":7,"9C84BF":0,"9C862B":14,"9C8BA0":0,"9C8C6E":7,"9C8E99":5,"9C8E9C":8,"9C924F":0,"9C9561":38,"9C9567":8,"9C9613":6,"9C971B":20,"9C9774":8,"9C9793":8,"9C99A0":10,"9C9D7E":10,"9C9E71":8,"9C9ED5":10,"9CA118":8,"9CA2F4":31,"9CA389":15,"9CA513":7,"9CA5C0":12,"9CA615":31,"9CA9C5":0,"9CAA1B":19,"9CAA5D":11,"9CB150":20,"9CB2B2":8,"9CB2E8":8,"9CB654":5,"9CBCF0":10,"9CBF0D":29,"9CBFCD":8,"9CC12D":37,"9CC172":8,"9CC394":0,"9CC7D1":27,"9CC8E9":30,"9CC9EB":33,"9CCAD9":15,"9CD35B":7,"9CD36D":33,"9CD917":14,"9CDA3E":20,"9CDAA8":0,"9CDBAF":8,"9CDF8A":8,"9CE041":15,"9CE063":7,"9CE33F":0,"9CE374":8,"9CE65E":0,"9CE6E7":7,"9CE82B":12,"9CEA97":9,"9CEC61":8,"9CF155":15,"9CF27E":7,"9CF387":0,"9CF3AC":0,"9CF48E":0,"9CF531":11,"9CFA76":0,"9CFB77":11,"9CFBD5":12,"9CFC01":0,"9CFC28":0,"9CFCE8":20,"A002A5":20,"A002DC":30,"A00460":33,"A00798":7,"A0086F":8,"A00A9A":8,"A00BBA":7,"A00E98":8,"A01081":7,"A01828":0,"A01B9E":7,"A01C8D":8,"A01D48":5,"A02195":7,"A021B7":33,"A022DE":12,"A027B6":7,"A02919":4,"A02942":20,"A02BB8":5,"A031DB":8,"A03299":6,"A03679":8,"A0369F":20,"A036BC":1,"A039F7":17,"A03BE3":0,"A0406F":8,"A040A0":33,"A04147":8,"A042D1":8,"A0445C":8,"A0465A":14,"A0481C":5,"A04839":8,"A04A5E":19,"A04E04":15,"A04EA7":0,"A04ECF":0,"A04F52":20,"A04F85":17,"A0510B":20,"A05272":0,"A0562C":7,"A056F3":0,"A057E3":8,"A05950":20,"A06090":7,"A06391":33,"A06610":24,"A067D6":15,"A0681C":37,"A06974":9,"A070B7":8,"A071A9":15,"A07591":7,"A07817":0,"A0782D":0,"A07D9C":7,"A08069":20,"A0821F":7,"A08527":20,"A085FC":19,"A086C6":10,"A087BE":0,"A08869":20,"A0889D":8,"A088B4":20,"A08CF8":8,"A08CFD":5,"A08D16":8,"A09169":17,"A091A2":13,"A091CA":15,"A09347":11,"A0941A":11,"A09921":37,"A0999B":0,"A09A0C":9,"A09A8E":0,"A09D22":0,"A0A0DC":8,"A0A309":0,"A0A33B":8,"A0A4C5":20,"A0A8CD":20,"A0AC69":7,"A0AD62":8,"A0AD9F":1,"A0AF12":8,"A0AFBD":20,"A0B0BD":7,"A0B15C":18,"A0B339":20,"A0B3CC":5,"A0B40F":0,"A0B4A5":7,"A0C1C5":0,"A0C20D":8,"A0C589":20,"A0C926":8,"A0C98B":15,"A0CBFD":7,"A0D05B":7,"A0D0DC":30,"A0D1B3":0,"A0D2B1":30,"A0D365":20,"A0D37A":20,"A0D3C1":5,"A0D722":7,"A0D795":0,"A0D7A0":8,"A0D7F3":7,"A0D807":8,"A0DDE5":27,"A0DE0F":8,"A0DF15":8,"A0E390":0,"A0E453":16,"A0E70B":20,"A0EDCD":0,"A0EE1A":0,"A0F3C1":31,"A0F419":15,"A0F479":8,"A0FAC8":8,"A0FB83":9,"A0FBC5":0,"A400E2":8,"A402B7":30,"A402B9":20,"A407B6":7,"A40801":30,"A40853":15,"A40987":0,"A409B3":8,"A40F98":11,"A41194":6,"A41232":11,"A416C0":0,"A416E7":8,"A4178B":8,"A41A3A":31,"A41F72":4,"A422B6":14,"A426CA":20,"A42A26":37,"A42B8C":33,"A42BB0":31,"A42DF1":14,"A4307A":7,"A43135":0,"A434D9":20,"A4373E":8,"A43828":8,"A439B3":10,"A43B0E":8,"A43D78":11,"A4423B":20,"A44280":0,"A44343":8,"A44380":8,"A44519":10,"A446B4":8,"A44BD5":10,"A44CC8":4,"A44E31":20,"A45046":10,"A4516F":19,"A45590":10,"A45D36":5,"A45E60":0,"A46706":0,"A46BB6":20,"A46C24":8,"A46CF1":7,"A46DA4":8,"A470D6":14,"A47174":8,"A475B9":7,"A47733":18,"A47760":15,"A477F3":0,"A47952":8,"A47B1A":8,"A47B2C":15,"A47CC9":8,"A481EE":15,"A483E7":0,"A48431":7,"A48CDB":6,"A490CE":12,"A492CB":15,"A4933F":8,"A493AD":8,"A493FE":8,"A49947":8,"A49A58":7,"A49B4F":8,"A49DDD":7,"A49FE7":7,"A4A46B":8,"A4A490":7,"A4A930":10,"A4AAFE":8,"A4AC0F":8,"A4B197":0,"A4B1C1":20,"A4B61E":8,"A4B805":0,"A4BA70":10,"A4BA76":8,"A4BADB":4,"A4BB6D":4,"A4BDC4":8,"A4BE2B":8,"A4BF01":20,"A4C337":0,"A4C361":0,"A4C3BE":10,"A4C3F0":20,"A4C494":20,"A4C54E":8,"A4C64F":8,"A4C69A":7,"A4C6F0":0,"A4C74B":8,"A4C788":10,"A4C939":11,"A4CAA0":8,"A4CCB3":10,"A4CF99":0,"A4D18C":0,"A4D1D2":0,"A4D23E":0,"A4D931":0,"A4D990":7,"A4DCBE":8,"A4DD58":8,"A4E287":10,"A4E31B":15,"A4E731":15,"A4E975":0,"A4EBD3":7,"A4F05E":11,"A4F1E8":0,"A4F6E6":0,"A4F6E8":0,"A4F841":0,"A4F8FF":32,"A4F921":0,"A4F933":20,"A4FC14":0,"A4FCA1":15,"A4FF95":15,"A4FF9F":10,"A80556":12,"A80600":7,"A809B1":8,"A80C63":8,"A80DE1":8,"A81306":12,"A81374":26,"A81378":15,"A8154D":31,"A8169D":38,"A816B2":17,"A816D0":7,"A81AF1":0,"A81B5A":11,"A82066":0,"A82316":15,"A823FE":17,"A824B8":15,"A82948":31,"A829DC":31,"A82BB9":7,"A82BCD":8,"A82BD5":10,"A82C89":0,"A830BC":7,"A8346A":7,"A83512":8,"A83759":8,"A83B5C":8,"A83CA5":4,"A83ED3":8,"A8407D":37,"A842A1":31,"A84481":15,"A84616":8,"A8494D":8,"A84A28":0,"A84B4D":7,"A85081":8,"A8515B":7,"A851AB":0,"A8574E":31,"A8595F":20,"A85AE0":8,"A85B78":0,"A85BB7":0,"A85C2C":0,"A85E45":1,"A85F61":0,"A860B6":0,"A864F1":20,"A8667F":0,"A86A86":10,"A86DAA":20,"A86E4E":8,"A86E84":31,"A86F36":12,"A8724D":20,"A87650":7,"A87971":8,"A8798D":7,"A87B39":15,"A87C01":7,"A87C45":8,"A87CF8":0,"A87D12":8,"A87E33":15,"A87EEA":20,"A8817E":0,"A88195":7,"A886DD":0,"A887B3":7,"A88808":0,"A888CE":11,"A88940":8,"A88C3E":19,"A88E24":0,"A88FD9":0,"A8913D":0,"A8922C":17,"A89675":14,"A8968A":0,"A89892":11,"A89969":4,"A89AD7":15,"A89C6C":32,"A89C78":0,"A89CED":10,"A89FBA":7,"A8AA7C":8,"A8ABB5":0,"A8B13B":5,"A8B271":8,"A8B2DA":24,"A8B86E":17,"A8BA69":7,"A8BB56":0,"A8BBCF":0,"A8BE27":0,"A8C092":8,"A8C252":8,"A8C407":8,"A8C56F":11,"A8C83A":8,"A8CA77":30,"A8CA7B":8,"A8CAB9":7,"A8D081":8,"A8D162":7,"A8D4E0":8,"A8DB03":7,"A8E018":15,"A8E3EE":16,"A8E544":8,"A8E5EC":15,"A8E621":30,"A8E978":8,"A8EE67":7,"A8F059":8,"A8F07C":8,"A8F266":8,"A8F274":7,"A8F5AC":8,"A8FAD8":0,"A8FB40":15,"A8FE9D":0,"A8FFBA":8,"AC007A":0,"AC05C7":20,"AC075F":8,"AC0775":0,"AC0D1B":17,"AC1203":20,"AC15A2":31,"AC15F4":0,"AC1615":0,"AC162D":5,"AC16DE":20,"AC198E":20,"AC1A3D":4,"AC1D06":0,"AC1E92":7,"AC1E9E":10,"AC1F74":0,"AC220B":1,"AC2241":8,"AC293A":0,"AC2B6E":20,"AC3184":8,"AC3328":8,"AC3613":7,"AC3870":6,"AC3AE2":21,"AC3C0B":0,"AC3DCB":20,"AC3EB1":18,"AC401E":12,"AC416A":30,"AC4500":0,"AC45CA":11,"AC45EF":20,"AC471B":8,"AC49DB":0,"AC4E91":8,"AC51AB":8,"AC5322":7,"AC5A14":7,"AC5AF0":17,"AC5AFC":20,"AC5C2C":0,"AC5E14":8,"AC5F3E":7,"AC5FEA":13,"AC606F":15,"AC6089":8,"AC6175":8,"AC61EA":0,"AC63BE":30,"AC6490":8,"AC675D":20,"AC6784":18,"AC6C90":7,"AC7289":20,"AC72DD":37,"AC7352":11,"AC74B1":20,"AC751D":8,"AC764C":11,"AC7A94":11,"AC7BA1":20,"AC7E01":8,"AC7F3E":0,"AC800A":16,"AC80FB":7,"AC81F3":15,"AC8247":20,"AC82F0":0,"AC84C6":31,"AC853D":8,"AC86A3":0,"AC87A3":0,"AC88FD":0,"AC8AC7":8,"AC8BA9":32,"AC8C46":10,"AC8D34":8,"AC8EBD":19,"AC8FA9":15,"AC8FF8":15,"AC9073":8,"AC9085":0,"AC91A1":4,"AC9232":8,"AC932F":15,"AC936A":8,"AC93C4":37,"AC9738":0,"AC9929":8,"AC9B0A":16,"AC9E17":1,"ACA7F1":31,"ACA88E":27,"ACAFB9":7,"ACB3B5":8,"ACB480":4,"ACBC32":0,"ACBCB5":0,"ACBD70":8,"ACC048":13,"ACC1EE":10,"ACC33A":7,"ACC4BD":11,"ACC5B4":8,"ACC906":0,"ACCCFC":30,"ACCF5C":0,"ACCF85":8,"ACD618":13,"ACDCCA":8,"ACDFA1":0,"ACE011":8,"ACE215":8,"ACE2D3":5,"ACE342":8,"ACE4B5":0,"ACE4D8":10,"ACE606":9,"ACE6BB":18,"ACE6FB":0,"ACE87B":8,"ACEAEA":8,"ACED5C":20,"ACEE9E":7,"ACF466":5,"ACF6F7":17,"ACF7F3":10,"ACF970":8,"ACFDCE":20,"ACFDEC":0,"ACFF6B":8,"B00875":8,"B00B22":8,"B00CD1":5,"B01656":8,"B01831":0,"B01921":31,"B019C6":0,"B0216F":8,"B0227A":5,"B02491":8,"B02628":22,"B02A43":18,"B02EE0":8,"B03366":12,"B03495":0,"B0358D":15,"B0359F":20,"B035B5":0,"B03795":17,"B03956":33,"B03ACE":8,"B03CDC":20,"B03F64":0,"B042B7":8,"B04502":8,"B04692":11,"B047BF":7,"B047E9":20,"B0481A":0,"B0487A":31,"B04A6A":7,"B04AB4":14,"B04E26":31,"B04F13":4,"B05476":7,"B05508":8,"B05A7B":8,"B05ADA":5,"B05B67":8,"B05CDA":5,"B05CE5":15,"B06088":20,"B064E0":7,"B065BD":0,"B067B5":0,"B06A41":18,"B06B11":38,"B06EBF":1,"B06FE0":7,"B0700D":15,"B0702D":0,"B07219":0,"B0735D":8,"B0739C":30,"B0754D":15,"B0761B":8,"B07839":37,"B07994":14,"B07ADF":8,"B07B25":4,"B07D64":20,"B07FB9":33,"B08101":9,"B082E2":1,"B083FE":4,"B08900":8,"B08BA8":30,"B08C75":0,"B09200":0,"B09575":31,"B0958E":31,"B096EA":37,"B098BC":8,"B09928":24,"B0995A":8,"B099D7":7,"B09C63":10,"B09CB2":18,"B09FBA":0,"B0A460":20,"B0A4F0":8,"B0A7B9":31,"B0AA36":11,"B0ACFA":24,"B0B5C3":11,"B0B98A":33,"B0BBA9":0,"B0BE76":31,"B0BE83":0,"B0C2C7":14,"B0C38E":8,"B0C4E7":7,"B0C559":7,"B0C61C":8,"B0C787":8,"B0C952":11,"B0CA68":0,"B0CAE7":8,"B0CCCED":10,"B0CCFE":8,"B0CFCB":30,"B0D09C":7,"B0D1D6":15,"B0D576":0,"B0D5FB":18,"B0D77E":8,"B0D888":26,"B0DCEF":20,"B0DE28":0,"B0DE31":7,"B0DF3A":7,"B0E17E":8,"B0E235":10,"B0E45C":7,"B0E4D5":18,"B0E5ED":8,"B0E5EF":0,"B0E5F9":0,"B0EB57":8,"B0EC69":0,"B0EC71":7,"B0ECDD":8,"B0F079":11,"B0F1D8":0,"B0F2F6":7,"B0F7C4":30,"B0FA8B":8,"B0FC0D":30,"B0FEE5":8,"B405A1":10,"B407F9":7,"B40931":8,"B40AD8":16,"B40B1D":7,"B40EDE":20,"B40FB3":12,"B4101C":10,"B4107A":30,"B41324":18,"B414E6":8,"B41513":8,"B41584":7,"B418D1":0,"B41974":0,"B41A1D":7,"B41BB0":0,"B41D62":15,"B41DC4":8,"B41F4D":16,"B4205B":11,"B423A2":18,"B42802":20,"B42BB9":8,"B42E99":39,"B43052":8,"B43836":8,"B4394C":8,"B43A28":7,"B43AE2":8,"B440A4":0,"B440DC":7,"B44326":8,"B44389":8,"B44506":4,"B44BD2":0,"B4527D":16,"B4527E":16,"B454F2":8,"B45575":0,"B456E3":0,"B457E6":11,"B45976":0,"B45BD1":31,"B460ED":10,"B46142":8,"B46293":7,"B4636F":15,"B46921":20,"B46BFC":20,"B46C47":26,"B46D83":20,"B46E08":8,"B46E10":12,"B47064":7,"B47443":7,"B476A4":8,"B479A7":7,"B47B1A":8,"B47C9C":30,"B47E9F":10,"B48351":20,"B485E1":0,"B48655":8,"B48901":8,"B48B19":0,"B490E5":37,"B49107":8,"B49691":20,"B496A5":0,"B499BA":5,"B49A7D":11,"B49CDF":0,"B49D02":7,"B49D6B":12,"B4A10A":8,"B4A5AC":11,"B4A898":8,"B4AE2B":19,"B4AEC1":0,"B4B024":31,"B4B055":8,"B4B291":17,"B4B52F":5,"B4B676":20,"B4B686":5,"B4B742":30,"B4B853":9,"B4BFF6":7,"B4C0C3":31,"B4C2F7":8,"B4C3D9":8,"B4C4FC":10,"B4CB57":11,"B4CD27":8,"B4CE40":7,"B4D1F6":8,"B4D5BD":20,"B4D5E5":7,"B4E10F":4,"B4E1C4":19,"B4E25B":5,"B4E3D0":17,"B4E454":30,"B4E5C5":8,"B4E9B8":4,"B4EF39":7,"B4F0AB":0,"B4F18C":8,"B4F1DA":17,"B4F49B":8,"B4F58E":8,"B4F61C":0,"B4F7A1":17,"B4FA48":0,"B4FBE4":32,"B4FBF9":8,"B4FF98":8,"B8005B":8,"B8011F":0,"B80305":20,"B80533":0,"B80716":12,"B808CF":20,"B808D7":8,"B8098A":0,"B80B9A":8,"B80BDA":37,"B80C68":9,"B8144D":0,"B8145C":8,"B817C2":0,"B81904":15,"B81D1F":8,"B81DAA":17,"B81E9E":8,"B8208E":26,"B8211C":0,"B8220C":0,"B827C5":8,"B827EB":28,"B82A72":4,"B82AA9":0,"B82B68":8,"B831B5":19,"B83446":14,"B8374A":0,"B83765":11,"B83BCC":10,"B83C20":8,"B83C28":0,"B841A4":0,"B844D9":0,"B845EB":0,"B8496D":0,"B84FA7":0,"B84FD5":19,"B850D8":10,"B851A9":15,"B852E0":10,"B85384":10,"B853AC":0,"B85600":8,"B857D8":7,"B85A73":7,"B85C5C":19,"B85D0A":0,"B85DC3":8,"B85E7B":7,"B85F98":30,"B85FB0":8,"B8634D":0,"B86B23":25,"B86CE8":7,"B8752E":8,"B8782E":0,"B87BC5":0,"B87BD4":18,"B87CD0":8,"B87E39":14,"B87E40":8,"B88198":20,"B881FA":0,"B8857B":8,"B88584":4,"B88788":5,"B88880":10,"B88A60":20,"B88C29":37,"B88D12":0,"B88E82":8,"B89047":0,"B89436":8,"B894E7":10,"B8977A":15,"B898AD":14,"B89A2A":20,"B89FCC":8,"B8A0B8":7,"B8A25D":14,"B8A825":7,"B8AB62":38,"B8AC6F":4,"B8AF67":5,"B8B1EA":9,"B8B2F8":0,"B8B409":7,"B8B81E":20,"B8BA66":19,"B8BBAF":7,"B8BC1B":8,"B8BC5B":7,"B8BF83":20,"B8C111":0,"B8C385":8,"B8C68E":7,"B8C74A":11,"B8C75D":0,"B8C9B5":11,"B8CA3A":4,"B8CB29":4,"B8CEED":22,"B8D43E":12,"B8D4C3":8,"B8D5AD":15,"B8D6F6":8,"B8D9CE":7,"B8DAE8":8,"B8DB38":18,"B8E28C":14,"B8E3B1":8,"B8E60C":0,"B8E856":0,"B8EA98":10,"B8F12A":0,"B8F4A4":18,"B8F6B1":0,"B8F775":20,"B8F883":31,"B8F934":16,"B8FBB3":31,"B8FE96":30,"B8FF61":0,"BC0023":9,"BC0358":20,"BC0435":37,"BC071D":31,"BC091B":20,"BC0963":0,"BC09B9":38,"BC0EAB":7,"BC0F64":20,"BC0FF3":5,"BC107B":7,"BC1485":7,"BC1541":15,"BC17B8":20,"BC1896":8,"BC1AE4":8,"BC1D89":14,"BC1E85":8,"BC20A4":7,"BC25E0":8,"BC277A":7,"BC2EF6":8,"BC2F3D":12,"BC305B":4,"BC32B2":7,"BC3329":16,"BC37D3":0,"BC3898":20,"BC3AEA":11,"BC3BAF":0,"BC3D85":8,"BC3E0B":26,"BC3ECB":12,"BC3F8F":8,"BC4486":7,"BC455B":7,"BC4699":31,"BC4760":7,"BC4C78":8,"BC4CA0":8,"BC4CC4":0,"BC4F2D":0,"BC515F":15,"BC5274":7,"BC52B4":15,"BC52B7":0,"BC542F":20,"BC5436":0,"BC5451":7,"BC5E91":9,"BC60A7":16,"BC6193":10,"BC620E":8,"BC64D9":11,"BC6778":0,"BC68C3":8,"BC69CB":26,"BC6AD1":10,"BC6B4D":15,"BC6C21":0,"BC6E64":16,"BC6EE2":20,"BC72B1":7,"BC74EA":0,"BC7574":8,"BC765E":7,"BC7670":8,"BC76C5":8,"BC7737":20,"BC79AD":7,"BC7ABF":7,"BC7B72":8,"BC7E8B":7,"BC7F7B":8,"BC7FA4":10,"BC804E":0,"BC8385":19,"BC851F":7,"BC89A7":0,"BC89C1":0,"BC89F8":37,"BC8CCD":7,"BC8D0E":15,"BC926B":0,"BC9307":7,"BC9388":15,"BC9789":8,"BC97E1":22,"BC9829":12,"BC98DF":14,"BC9930":8,"BC9A53":8,"BC9C31":8,"BC9F58":0,"BC9FEF":0,"BCA080":7,"BCA0B9":8,"BCA231":8,"BCA511":33,"BCA58B":7,"BCA5A9":0,"BCA8A6":20,"BCA920":0,"BCAEC5":1,"BCB0E7":8,"BCB181":27,"BCB1F3":7,"BCB2CC":7,"BCB863":0,"BCBB58":0,"BCBCCA":8,"BCBF2E":1,"BCC342":26,"BCC427":8,"BCC436":15,"BCC6DB":15,"BCCD7F":8,"BCCD99":20,"BCD074":0,"BCD11F":7,"BCD177":31,"BCD206":8,"BCD22C":20,"BCDF58":18,"BCE143":0,"BCE265":8,"BCE63F":7,"BCE8FA":11,"BCE92F":5,"BCEAFA":5,"BCEBE8":7,"BCEC5D":0,"BCEE7B":1,"BCF105":20,"BCF171":20,"BCF5AC":17,"BCF730":7,"BCFCE7":1,"BCFED9":0,"BCFFEB":14,"C00195":30,"C0060C":8,"C006C3":31,"C01173":7,"C014B8":15,"C0151B":16,"C01693":10,"C0174D":7,"C01754":0,"C01803":5,"C01ADA":0,"C01C6A":18,"C0238D":7,"C025A5":4,"C025E9":31,"C0280B":9,"C02C5C":0,"C02E1D":15,"C02E25":11,"C02FCD":11,"C0335E":19,"C03379":8,"C038F9":15,"C03A55":31,"C03C59":20,"C03D03":7,"C03E50":8,"C03E75":8,"C03EBA":4,"C03F0E":33,"C03FDD":8,"C04121":15,"C041F6":17,"C04442":0,"C0470E":4,"C04754":12,"C048E6":7,"C04A00":31,"C04E8A":8,"C05234":8,"C0544D":15,"C05724":9,"C05B44":10,"C05BBD":8,"C06118":31,"C06194":8,"C06394":0,"C064C6":15,"C06599":7,"C06B55":14,"C06C0C":0,"C07009":8,"C07831":8,"C07982":38,"C07AD6":7,"C083C9":8,"C0847A":0,"C084E0":8,"C084FF":37,"C087EB":7,"C08840":37,"C08997":7,"C08B05":8,"C08C71":14,"C08D51":30,"C091B9":30,"C0956D":0,"C095CF":30,"C09727":7,"C09879":2,"C09AD0":0,"C09B63":8,"C09F05":11,"C09F42":0,"C0A36D":8,"C0A53E":0,"C0A5E8":20,"C0A600":0,"C0A810":20,"C0A938":8,"C0AB2B":8,"C0B22F":0,"C0B47D":8,"C0B550":22,"C0B5CD":8,"C0B658":0,"C0B6F9":20,"C0B883":20,"C0BC9A":8,"C0BDC8":7,"C0BDD1":7,"C0BFAC":8,"C0BFC0":8,"C0C7DB":0,"C0C9E3":31,"C0CCF8":0,"C0CECD":0,"C0D012":0,"C0D026":8,"C0D193":8,"C0D2DD":7,"C0D2F3":38,"C0D3C0":7,"C0D46B":8,"C0D5E2":7,"C0D6D5":19,"C0DA5E":8,"C0DCD7":8,"C0DCDA":7,"C0E018":8,"C0E1BE":8,"C0E3FB":8,"C0E42D":31,"C0E579":8,"C0E862":0,"C0EDE5":11,"C0EEFB":13,"C0F2FB":0,"C0F4E6":8,"C0F6C2":8,"C0F6EC":8,"C0F945":25,"C0F9B0":8,"C0FFA8":8,"C0FFD4":33,"C403A8":20,"C40415":33,"C40528":8,"C40683":8,"C4072F":8,"C4084A":15,"C40B31":0,"C40BCB":10,"C40D96":8,"C40F08":20,"C41234":0,"C412EC":8,"C41411":0,"C41688":8,"C4168F":0,"C416C8":8,"C4170E":8,"C418E9":7,"C41C07":7,"C42155":8,"C42360":20,"C4278C":8,"C42AD0":0,"C42B44":8,"C42C03":0,"C4345B":8,"C4346B":5,"C435D9":0,"C43960":37,"C43ABE":16,"C43D1A":20,"C43DC7":33,"C43EAB":8,"C440F6":11,"C44202":7,"C4438F":17,"C4447D":8,"C4473F":8,"C4474E":20,"C4491B":0,"C4493E":14,"C44F5F":8,"C45006":7,"C4524F":0,"C4576E":7,"C457CD":8,"C45A86":8,"C45AB1":4,"C45BAC":0,"C45D83":7,"C45E5C":8,"C4618B":0,"C461C7":19,"C462EA":7,"C463C4":8,"C46516":5,"C46699":12,"C467D1":8,"C46940":10,"C469F0":8,"C46AB7":10,"C46DD1":8,"C46E1F":31,"C4710F":10,"C47154":31,"C4731E":7,"C475AB":20,"C475EA":8,"C47764":7,"C478A2":8,"C47BE3":8,"C47D46":24,"C47D9F":7,"C48025":8,"C48466":0,"C484C0":14,"C484FC":0,"C48508":20,"C486E9":8,"C488E5":7,"C48B66":38,"C4910C":0,"C493BB":10,"C493D9":7,"C49500":30,"C4969F":30,"C4985C":38,"C49880":0,"C49A02":17,"C49D08":8,"C49DED":19,"C49F4C":8,"C4A052":14,"C4A1AE":8,"C4A402":8,"C4AA99":8,"C4ABB2":12,"C4ACAA":0,"C4AE12":7,"C4B1D9":8,"C4B301":0,"C4B349":0,"C4B8B4":8,"C4BB03":8,"C4BDE5":20,"C4C00A":19,"C4C17D":0,"C4C36B":0,"C4CB76":19,"C4CBE1":4,"C4D0E3":20,"C4D438":8,"C4D6D3":4,"C4D738":8,"C4D8D4":8,"C4D987":20,"C4DB04":8,"C4DE7B":8,"C4E1A1":11,"C4E287":8,"C4E39F":11,"C4E984":31,"C4EF3D":7,"C4F081":8,"C4F445":0,"C4F7C1":0,"C4FBAA":8,"C4FE5B":11,"C4FF1F":8,"C4FF22":8,"C4FF99":20,"C805A4":14,"C808E9":17,"C809A8":20,"C80CC8":8,"C8102F":33,"C8120B":7,"C81451":8,"C81479":7,"C8154E":20,"C819F7":7,"C81EE7":0,"C81F66":4,"C81FBE":8,"C81FE8":0,"C82158":20,"C82832":10,"C82A14":0,"C82ADD":18,"C83049":8,"C8334B":0,"C833E5":8,"C8348E":20,"C83870":7,"C839AC":8,"C83A1B":25,"C83C85":0,"C83D97":15,"C83DDC":10,"C83E9E":8,"C83F26":19,"C8418A":7,"C84AA0":16,"C84BD6":4,"C850CE":8,"C85142":7,"C85195":8,"C85895":14,"C858B3":20,"C858C0":20,"C85ACF":5,"C85CCC":10,"C85EA9":20,"C86000":1,"C863F1":16,"C868DE":8,"C869CD":0,"C86C3D":30,"C86E08":20,"C86F1D":0,"C8727E":15,"C875DD":17,"C87E75":7,"C87F54":1,"C8806D":0,"C884CF":8,"C88550":0,"C889F3":0,"C88A9A":20,"C88D83":8,"C8908A":7,"C890F7":8,"C894BB":8,"C895CE":20,"C89665":19,"C8979F":15,"C89BAD":9,"C89D18":8,"C89E43":33,"C89F0C":14,"C89F1A":8,"C8A1DC":14,"C8A6EF":7,"C8A776":8,"C8A823":7,"C8B1CD":0,"C8B29B":20,"C8B5B7":0,"C8B6D3":8,"C8B78A":8,"C8BA94":7,"C8BB81":8,"C8BC9C":8,"C8BCC8":0,"C8BD4D":7,"C8BD69":7,"C8BF4C":10,"C8BFFE":8,"C8C2FA":8,"C8C465":8,"C8C750":14,"C8C9B2":30,"C8CA63":8,"C8CB9E":20,"C8CBB8":5,"C8D083":0,"C8D10B":15,"C8D15E":8,"C8D1A9":8,"C8D3FF":5,"C8D7B0":7,"C8D959":14,"C8D9D2":5,"C8DDC9":6,"C8DF7C":15,"C8E0EB":0,"C8E265":20,"C8E31D":8,"C8E5E0":8,"C8E600":8,"C8F230":11,"C8F319":17,"C8F650":0,"C8F733":20,"C8F750":4,"CC033D":10,"CC051B":7,"CC0577":8,"CC07AB":7,"CC07E4":6,"CC087B":8,"CC088D":0,"CC08E0":0,"CC08FA":0,"CC08FB":31,"CC0DCB":19,"CC0DF2":14,"CC115A":0,"CC1531":20,"CC1E56":8,"CC1E97":8,"CC208C":8,"CC20AC":7,"CC20E8":0,"CC2119":7,"CC2293":30,"CC22FE":0,"CC25EF":0,"CC2746":0,"CC28AA":1,"CC29F5":0,"CC2D83":11,"CC2D8C":17,"CC2DB7":0,"CC2F71":20,"CC3296":8,"CC32E5":31,"CC3429":31,"CC35D9":32,"CC38E1":8,"CC3A61":7,"CC3D82":20,"CC3DD1":8,"CC3E5F":5,"CC3F36":0,"CC40D0":33,"CC4210":10,"CC4460":8,"CC4463":0,"CC464E":7,"CC483A":4,"CC4B04":0,"CC4D75":10,"CC53B5":8,"CC5763":26,"CC58C7":15,"CC5C61":8,"CC6023":0,"CC60C8":19,"CC61E5":14,"CC6200":9,"CC64A6":8,"CC660A":0,"CC66B2":15,"CC68B6":31,"CC68E0":0,"CC69FA":0,"CC6EA4":7,"CC722A":0,"CC7645":19,"CC785F":0,"CC7EE7":26,"CC808F":0,"CC812A":12,"CC817D":0,"CC874A":15,"CC895E":8,"CC89FD":15,"CC8A84":8,"CC9096":8,"CC915B":8,"CC96A0":8,"CC96E5":4,"CC988B":16,"CC98EF":11,"CC9EA2":30,"CCA12B":38,"CCA223":8,"CCA7C1":18,"CCAA00":19,"CCAFE3":30,"CCB0A8":8,"CCB0B3":19,"CCB11A":7,"CCB182":8,"CCB3F8":24,"CCB5D1":10,"CCB775":8,"CCB7C4":8,"CCBA6F":8,"CCBABD":31,"CCBBFE":8,"CCBC2B":8,"CCBCE3":8,"CCBE61":0,"CCC3EA":14,"CCC5E5":4,"CCC760":0,"CCC95D":0,"CCCC81":8,"CCD281":0,"CCD73C":8,"CCD843":10,"CCD9AC":20,"CCDA20":10,"CCDEDE":15,"CCE686":7,"CCE9FA":7,"CCEB5E":10,"CCED21":15,"CCF411":18,"CCF735":30,"CCF826":7,"CCF9E4":20,"CCF9E8":7,"CCF9F0":7,"CCFA00":17,"CCFA66":8,"CCFA95":9,"CCFE3C":7,"CCFF90":8,"D0034B":0,"D003DF":7,"D00401":14,"D00477":8,"D004B0":7,"D005E4":8,"D00DF7":8,"D011E5":0,"D01255":38,"D013FD":17,"D016B4":8,"D0176A":7,"D017C2":1,"D01B49":7,"D020DD":11,"D021F9":32,"D022BE":7,"D023DB":0,"D02544":7,"D02598":0,"D02B20":0,"D02DB3":8,"D03169":7,"D03311":0,"D03745":31,"D039FA":7,"D03C1F":20,"D03E07":0,"D03E5C":8,"D03FAA":0,"D0431E":4,"D0460C":4,"D0484F":15,"D0497C":13,"D04D86":0,"D04E99":8,"D04F7E":0,"D05162":16,"D056FB":7,"D0577B":20,"D0577E":20,"D058A5":0,"D059E4":7,"D06158":8,"D06544":0,"D06578":20,"D065B3":38,"D065CA":8,"D0667B":7,"D067E5":4,"D069C1":8,"D06B78":0,"D06DC8":8,"D06F82":8,"D07380":8,"D07602":38,"D076E7":31,"D07714":14,"D07AB5":8,"D07D33":8,"D07E01":8,"D07E28":5,"D07E35":20,"D07FA0":7,"D0817A":0,"D087E2":7,"D0880C":0,"D08E79":4,"D09282":0,"D0929E":19,"D09466":4,"D094CF":8,"D096EA":12,"D09C7A":10,"D09CAE":12,"D0A637":0,"D0AA5F1":10,"D0AA5F7":37,"D0ABD5":20,"D0AD08":5,"D0AE05":10,"D0B128":7,"D0B324":0,"D0B45D":8,"D0BF9C":5,"D0C050":0,"D0C1B1":7,"D0C1B5":4,"D0C1BF":10,"D0C24E":7,"D0C5F3":0,"D0C637":20,"D0C65B":8,"D0C67F":8,"D0C7C0":31,"D0CDBF":17,"D0CEC0":10,"D0D003":7,"D0D04B":8,"D0D23C":0,"D0D2B0":0,"D0D49F":0,"D0D783":8,"D0D7BE":8,"D0DAD7":0,"D0DB32":15,"D0DFC7":7,"D0E140":0,"D0E581":0,"D0EFC1":8,"D0F3F5":8,"D0F4F7":8,"D0F815":8,"D0F88C":14,"D0FCCC":7,"D0FF98":8,"D40129":22,"D4016D":31,"D404E6":22,"D40F9E":0,"D411A3":7,"D4126E":0,"D41761":10,"D41A3F":11,"D4223F":6,"D4258B":20,"D42CA6":7,"D42DCC":0,"D42FCA":0,"D43538":10,"D4389C":16,"D43A2C":18,"D43B04":20,"D43D7E":3,"D440F0":8,"D4438A":10,"D44649":8,"D446E1":0,"D44D77":15,"D44F67":8,"D4503F":11,"D450EE":37,"D4532A":10,"D4548B":20,"D45763":0,"D45B51":14,"D45D42":15,"D45D64":1,"D45E89":14,"D45EEC":10,"D45F7A":8,"D4612E":8,"D4619D":0,"D461DA":0,"D462EA":8,"D463C0":0,"D463C6":14,"D463DE":12,"D467D3":11,"D468AA":0,"D46AA8":8,"D46BA6":8,"D46D6D":20,"D46E0E":31,"D46E5C":8,"D47327":8,"D47415":8,"D47954":8,"D47AE2":7,"D481D7":4,"D48457":37,"D48564":5,"D487D8":7,"D48866":8,"D48890":7,"D489C1":32,"D48A39":7,"D48F33":19,"D48FA2":8,"D4909C":0,"D4910F":30,"D49398":15,"D49400":8,"D494A9":20,"D494E8":8,"D4970B":10,"D498B9":12,"D49A20":0,"D49DC0":7,"D49FDD":8,"D4A148":8,"D4A254":8,"D4A2CD":4,"D4A33D":0,"D4A365":10,"D4A923":8,"D4AB61":20,"D4ABCD":38,"D4AE05":7,"D4AE52":4,"D4B110":8,"D4B6C9":15,"D4BAFA":11,"D4BBC8":12,"D4BBE6":8,"D4BED7":4,"D4BED9":4,"D4C1FC":15,"D4C94B":14,"D4C9EF":5,"D4CBAF":15,"D4CBCC":12,"D4CE40":0,"D4D252":20,"D4D51B":8,"D4D6DF":31,"D4D853":20,"D4D892":8,"D4DA21":10,"D4DCCD":0,"D4E33F":15,"D4E6B7":7,"D4E8B2":7,"D4E98A":20,"D4ECAB":12,"D4F0EA":10,"D4F242":8,"D4F32D":20,"D4F46F":0,"D4F547":18,"D4F7D5":16,"D4F9A1":8,"D4FB8E":0,"D4FF1A":0,"D8004D":0,"D801D0":8,"D807B6":31,"D80831":7,"D80A60":8,"D80B9A":7,"D80D17":31,"D80E29":12,"D8109F":8,"D81399":38,"D814DF":38,"D8150D":31,"D81BB5":8,"D81C79":0,"D81D72":0,"D81EDD":11,"D820A2":8,"D82918":8,"D829F8":8,"D82A7E":15,"D83062":0,"D831CF":7,"D832E3":10,"D8341C":37,"D83ADD":28,"D83BBF":20,"D84008":8,"D843AE":3,"D84489":31,"D846CE":0,"D84732":31,"D847BB":8,"D8490B":8,"D84A83":12,"D84C90":0,"D84FB8":17,"D850E6":1,"D853BC":6,"D85437":8,"D854F2":8,"D85575":7,"D857EF":7,"D85982":8,"D85B2A":7,"D85D4C":31,"D85ED3":39,"D860C5":11,"D8630D":14,"D86375":10,"D867D3":8,"D86852":8,"D868A0":7,"D868C3":7,"D86C63":18,"D86D17":8,"D87154":7,"D87157":6,"D87475":0,"D874DF":8,"D87533":15,"D876AE":8,"D880DC":8,"D88863":8,"D88ADC":8,"D88C79":18,"D88F76":0,"D890E8":7,"D89333":8,"D893D4":10,"D89695":0,"D89AC1":15,"D89B3B":8,"D89D13":7,"D89D67":5,"D89E3F":0,"D89E61":8,"D89EF3":4,"D8A25E":0,"D8A315":12,"D8A35C":7,"D8A491":8,"D8AD49":9,"D8AFF1":26,"D8B053":10,"D8B12A":26,"D8B249":8,"D8B370":32,"D8BB2C":0,"D8BBC1":3,"D8BE1F":0,"D8BE65":30,"D8BF42":20,"D8C262":32,"D8C4E9":7,"D8C771":8,"D8C960":8,"D8CB8A":3,"D8CC98":8,"D8CE3A":10,"D8CF9C":0,"D8CFBF":14,"D8D090":4,"D8D1CB":0,"D8D261":37,"D8D385":5,"D8D43C":16,"D8DAF1":8,"D8DC40":0,"D8DE3A":0,"D8E0E1":7,"D8E2DF":19,"D8E374":10,"D8E593":0,"D8EB46":18,"D8EDA8":10,"D8EF42":8,"D8EFCD":15,"D8F12E":31,"D8F2CA":20,"D8F883":20,"D8FBD6":30,"D8FC93":20,"DC0077":31,"DC080F":0,"DC094C":8,"DC0B34":17,"DC0C5C":0,"DC1057":0,"DC121D":8,"DC16B2":8,"DC1AC5":12,"DC1BA1":20,"DC2148":20,"DC215C":20,"DC21E2":8,"DC2727":8,"DC2B2A":0,"DC2B61":0,"DC2D04":12,"DC2D3C":8,"DC31D1":12,"DC333D":8,"DC3714":0,"DC3EF8":15,"DC415F":0,"DC41A9":20,"DC42C8":8,"DC44B6":7,"DC4546":20,"DC45B8":0,"DC4628":20,"DC4A3E":5,"DC5285":0,"DC5360":20,"DC5392":0,"DC54D7":30,"DC5583":11,"DC56E7":0,"DC6180":8,"DC621F":8,"DC6279":31,"DC6672":7,"DC69E2":7,"DC6AE7":10,"DC6B1B":8,"DC6DBC":0,"DC6DCD":11,"DC7144":7,"DC7196":20,"DC71D0":0,"DC7223":38,"DC729B":8,"DC7385":8,"DC74A8":7,"DC7794":8,"DC7E1D":8,"DC7EF5":8,"DC8084":0,"DC868D":8,"DC86D8":0,"DC87F8":7,"DC8983":7,"DC8B28":20,"DC8C1B":12,"DC8D8A":15,"DC8E6D":8,"DC9009":20,"DC9088":8,"DC9166":8,"DC91BF":30,"DC9396":0,"DC9566":0,"DC97BA":20,"DC9840":19,"DC9914":8,"DC9B9C":0,"DC9C99":8,"DC9DED":7,"DC9E8F":0,"DC9FA4":15,"DC9FDB":32,"DCA0D0":30,"DCA120":15,"DCA281":9,"DCA4CA":0,"DCA632":28,"DCA782":8,"DCA904":0,"DCA956":11,"DCA971":20,"DCB082":15,"DCB4C4":19,"DCB4CA":11,"DCB54F":0,"DCB72E":10,"DCBFE9":14,"DCC49C":7,"DCC64B":8,"DCC793":15,"DCCCE6":7,"DCCF96":7,"DCD2FC":8,"DCD2FD":8,"DCD3A2":0,"DCD444":8,"DCD7A0":8,"DCD916":8,"DCD9AE":15,"DCDB27":8,"DCDCE2":7,"DCE541":0,"DCE55B":18,"DCEB4D":14,"DCED83":10,"DCEE06":8,"DCEF09":33,"DCEF80":8,"DCF110":15,"DCF401":4,"DCF4CA":0,"DCF756":7,"DCFB48":20,"DCFE18":31,"E00084":8,"E001C7":38,"E0036B":7,"E005C5":31,"E00630":8,"E00CE5":8,"E00DEE":8,"E013B5":12,"E01877":24,"E0191D":8,"E01ADF":18,"E01F2B":15,"E01F6A":8,"E01F88":10,"E01FED":15,"E01FFC":14,"E0247F":8,"E02481":8,"E02611":0,"E0280A":31,"E02861":8,"E02B96":0,"E02BE9":20,"E02CB2":6,"E02E0B":20,"E02E3F":8,"E0338E":0,"E03676":8,"E03AAA":20,"E03E44":22,"E03F49":1,"E04007":8,"E04027":8,"E0426D":11,"E0469A":33,"E046EE":33,"E04BA6":8,"E04E5D":8,"E05F45":0,"E06267":10,"E063DA":32,"E063E5":16,"E06678":0,"E06CC5":8,"E06D17":0,"E070EA":5,"E07256":20,"E073E7":5,"E0757D":14,"E07726":8,"E0806B":10,"E0897E":0,"E08F4C":20,"E091F5":33,"E0925C":0,"E09467":20,"E09796":8,"E09861":14,"E09971":7,"E09D13":7,"E09D31":20,"E09E2D":15,"E0A366":14,"E0A3AC":8,"E0A670":15,"E0AA96":7,"E0ACCB":0,"E0AD9B":8,"E0AEA2":8,"E0B52D":0,"E0B55F":0,"E0B655":10,"E0B9BA":0,"E0BA78":0,"E0BDA0":0,"E0BFB2":0,"E0C250":33,"E0C264":20,"E0C377":7,"E0C3EA":0,"E0C767":0,"E0C932":20,"E0C97A":0,"E0CB19":15,"E0CB1D":30,"E0CB4E":1,"E0CBEE":7,"E0CC7A":8,"E0CCF8":10,"E0CDB8":8,"E0D045":20,"E0D083":7,"E0D362":31,"E0D462":8,"E0D464":20,"E0D4E8":20,"E0D55D":20,"E0D55E":39,"E0D848":4,"E0DA90":8,"E0DB10":7,"E0DB55":4,"E0DCFF":10,"E0DDC0":12,"E0E0FC":8,"E0E258":20,"E0E37C":8,"E0EB40":0,"E0EE1B":26,"E0F330":8,"E0F442":8,"E0F5C6":0,"E0F728":30,"E0F847":0,"E4029B":20,"E406E0":8,"E4072B":8,"E40A16":8,"E40CFD":11,"E40D36":20,"E40EEE":8,"E41088":7,"E4115B":5,"E4121D":7,"E419C1":8,"E41B43":10,"E41FD5":20,"E425E7":0,"E4268B":8,"E426D5":14,"E42761":9,"E42AAC":19,"E42B34":0,"E42B79":15,"E42F37":0,"E432CB":7,"E433AE":11,"E43493":8,"E435C8":8,"E43883":32,"E43D1A":22,"E43EC6":8,"E44097":11,"E440E2":7,"E44122":13,"E44164":15,"E441D4":12,"E442A6":20,"E4434B":4,"E44389":0,"E443CF":31,"E44519":10,"E446B0":24,"E446DA":10,"E44790":11,"E44AE0":20,"E450EB":0,"E45341":0,"E454E5":8,"E454E8":4,"E45768":12,"E458B8":7,"E458E7":7,"E45AA2":12,"E45D75":7,"E45E1B":18,"E45E37":20,"E45F01":28,"E46017":20,"E468A3":8,"E47010":0,"E470B8":20,"E4729D":15,"E472E2":8,"E47319":8,"E47684":0,"E47727":8,"E47CF9":7,"E47DBD":7,"E47E66":8,"E47FB2":24,"E48184":15,"E48210":8,"E48326":8,"E484D3":10,"E48B7F":0,"E48EC5":8,"E48F1D":8,"E4902A":8,"E4907E":14,"E490FD":0,"E49282":7,"E492FB":7,"E4936A":11,"E49652":12,"E498D1":19,"E498D6":0,"E4995F":8,"E49A79":0,"E49ADC":0,"E49C67":0,"E49F7D":7,"E4A430":7,"E4A471":20,"E4A7A0":20,"E4A7C5":8,"E4A7D0":8,"E4A8B6":8,"E4AAE4":10,"E4B021":7,"E4B107":8,"E4B16C":0,"E4B224":8,"E4B2FB":0,"E4B318":20,"E4B555":8,"E4B97A":4,"E4BCAA":10,"E4BEFB":8,"E4C0FE":0,"E4C2D1":8,"E4C32A":31,"E4C483":11,"E4C63D":0,"E4C767":20,"E4CE8F":0,"E4D332":31,"E4D373":8,"E4D398":10,"E4D436":15,"E4DB6D":10,"E4DC43":8,"E4DCCC":8,"E4E0A6":0,"E4E0C5":7,"E4E26C":11,"E4E4AB":0,"E4E749":5,"E4EC10":15,"E4ECE8":7,"E4F004":4,"E4F042":18,"E4F1D4":12,"E4F3C4":7,"E4F4C6":33,"E4F89C":20,"E4F8EF":7,"E4FAC4":31,"E4FADE":19,"E4FAED":7,"E4FAFD":20,"E4FB1E":19,"E4FB5D":8,"E4FD45":20,"E4FDA1":8,"E4FE43":10,"E4FED4":19,"E8039A":7,"E8040B":0,"E80688":0,"E8088B":8,"E81132":7,"E8136E":8,"E8150E":15,"E817FC":24,"E81CD8":0,"E81E92":8,"E82281":37,"E8288D":8,"E82AEA":20,"E82BC5":8,"E83617":0,"E83935":5,"E83A12":7,"E83A97":25,"E83F67":8,"E843B6":36,"E848B8":31,"E84A54":10,"E84A78":0,"E84C4A":30,"E84D74":8,"E84DD0":8,"E84E84":7,"E84EBA":18,"E84FA7":8,"E8508B":7,"E85497":7,"E85A8B":10,"E85B5B":17,"E85F02":0,"E85FB4":10,"E862BE":20,"E8655F":4,"E86819":8,"E868B1":11,"E86DCB":7,"E86DE9":8,"E86E3A":16,"E87865":0,"E87E1C":8,"E87EEF":10,"E87F6B":7,"E87F95":0,"E8802E":0,"E880E7":8,"E88152":0,"E884A5":20,"E884C6":8,"E8854B":0,"E886CF":15,"E88843":10,"E88D28":0,"E89120":14,"E892A4":17,"E89309":7,"E89363":15,"E894F6":31,"E89847":10,"E898EE":0,"E89C25":1,"E89D87":25,"E89F39":15,"E8A34E":8,"E8A660":8,"E8A6CA":8,"E8A72F":19,"E8A730":0,"E8AACB":7,"E8ABF3":8,"E8AC23":8,"E8B0C5":20,"E8B1FC":20,"E8B265":4,"E8B2AC":0,"E8B4C8":7,"E8B5D0":4,"E8BA17":10,"E8BBA8":11,"E8BDD1":8,"E8BFB8":20,"E8BFE1":20,"E8C386":0,"E8C3C5":8,"E8C829":20,"E8C913":7,"E8CAC8":38,"E8CBA1":15,"E8CD2D":8,"E8CF83":4,"E8D52B":18,"E8D765":8,"E8D775":8,"E8D87E":30,"E8D8D1":5,"E8DA3E":8,"E8DE27":31,"E8E0B7":25,"E8E5D6":7,"E8EA34":8,"E8EA4D":8,"E8F085":8,"E8F094":37,"E8F375":15,"E8F408":20,"E8F654":8,"E8F673":19,"E8F72F":8,"E8F791":10,"E8F8D0":15,"E8F9D4":8,"E8FA23":8,"E8FBE9":0,"E8FCAF":33,"E8FD35":8,"E8FEBE":0,"E8FF98":8,"E8FFF4":0,"EC01EE":11,"EC086B":31,"EC08E5":14,"EC0C96":15,"EC0D51":0,"EC0DE4":30,"EC1055":10,"EC107B":7,"EC172F":31,"EC1A02":8,"EC1D53":8,"EC1F72":7,"EC2125":25,"EC2150":12,"EC21E5":25,"EC233D":8,"EC2651":0,"EC26CA":31,"EC28D3":0,"EC2A72":4,"EC2BEB":30,"EC2C0D":0,"EC2C73":0,"EC2CE2":0,"EC2F90":11,"EC30B3":10,"EC315F":30,"EC3586":0,"EC388F":8,"EC3A52":8,"EC3CBB":8,"EC4118":10,"EC42CC":0,"EC4654":0,"EC4684":19,"EC4C8C":20,"EC4D3E":10,"EC4D47":8,"EC51BC":11,"EC536F":8,"EC5382":9,"EC551C":8,"EC5623":8,"EC59E7":19,"EC5AA3":8,"EC6073":31,"EC63D7":20,"EC6488":9,"EC65CC":26,"EC7379":0,"EC748C":16,"EC750C":31,"EC753E":8,"EC7949":24,"EC7C2C":8,"EC7CB6":7,"EC7D11":12,"EC8150":0,"EC8152":8,"EC819C":8,"EC8350":19,"EC852F":0,"EC888F":31,"EC8892":14,"EC8914":8,"EC89F5":6,"EC8AC4":30,"EC8C9A":8,"EC8E12":15,"EC8E77":20,"EC8EB5":5,"EC90C1":7,"EC97A2":0,"EC9A74":5,"EC9B5B":15,"EC9BF3":7,"ECA138":30,"ECA1D1":8,"ECA62F":8,"ECA907":0,"ECAA25":7,"ECAA8F":8,"ECADB8":0,"ECB1D7":5,"ECB550":7,"ECB878":8,"ECB931":31,"ECB9A5":8,"ECC01B":8,"ECC5D2":8,"ECCB30":8,"ECCED7":0,"ECD09F":10,"ECD508":0,"ECDCAA":0,"ECDF3A":12,"ECE09B":7,"ECE61D":8,"ECE7A7":20,"ECE9D2":0,"ECE9F5":8,"ECED04":20,"ECED73":14,"ECF33C":20,"ECF342":11,"ECF35B":15,"ECF4BB":4,"ECF8D0":8,"ECFA5C":10,"ECFF3A":0,"F004E1":0,"F0051B":7,"F008F1":7,"F0090D":31,"F00FEC":8,"F01898":0,"F01B6C":12,"F01C13":17,"F01DBC":19,"F01FAF":4,"F01FC7":0,"F020FF":20,"F02475":0,"F0258E":8,"F025B7":7,"F0272D":30,"F027A0":0,"F02F4B":0,"F02F74":1,"F02F9E":30,"F02FA7":8,"F02FBA":0,"F033E5":8,"F03575":38,"F037CF":8,"F03965":7,"F03F95":8,"F040AF9":28,"F040D9":0,"F0421C":20,"F042F5":8,"F04347":8,"F04DA2":4,"F04EA4":5,"F04F7C":30,"F05501":8,"F057A6":20,"F05A09":7,"F05B7B":7,"F05C0E":8,"F05C77":18,"F05CD5":0,"F063F9":8,"F065AE":7,"F06728":11,"F067B1":7,"F06BCA":7,"F06C5D":10,"F06C73":15,"F06D78":11,"F06E0B":19,"F0704F":7,"F0728C":7,"F072EA":18,"F0766F":0,"F077C3":20,"F07807":0,"F07959":1,"F07960":0,"F079E8":11,"F08173":30,"F08A76":7,"F0921C":5,"F09838":8,"F0989D":0,"F099B6":0,"F099BF":0,"F09BB8":8,"F09E4A":20,"F09FC2":32,"F09FFC":27,"F0A0B1":8,"F0A225":30,"F0A35A":0,"F0A3B2":38,"F0A4EA":8,"F0A731":31,"F0A951":8,"F0B0E7":0,"F0B11D":15,"F0B13F":8,"F0B2B9":20,"F0B3EC":0,"F0B429":10,"F0B479":0,"F0B61E":20,"F0BDEE":8,"F0BF97":16,"F0C1F1":0,"F0C371":0,"F0C42F":8,"F0C478":8,"F0C725":0,"F0C850":8,"F0C8B5":8,"F0C9D1":37,"F0CBA1":0,"F0CD31":7,"F0D1A9":0,"F0D2F1":30,"F0D31F":0,"F0D415":20,"F0D4E2":4,"F0D5BF":20,"F0D635":0,"F0D793":0,"F0D7AA":14,"F0D7EE":8,"F0DBE2":0,"F0DBF8":0,"F0DCE2":0,"F0E426":7,"F0E4A2":8,"F0E77E":7,"F0EE10":7,"F0EE7A":0,"F0EF86":18,"F0F0A4":30,"F0F336":31,"F0F564":7,"F0F61C":0,"F0F7E7":8,"F0F7FC":8,"F0F832":18,"F0FABA":21,"F0FAC7":8,"F0FEE7":8,"F400A2":7,"F40228":7,"F40270":4,"F40304":18,"F4032A":30,"F40616":0,"F40669":20,"F406A3":8,"F409D8":7,"F40E01":0,"F40E22":7,"F40F24":0,"F41A9C":10,"F41BA1":0,"F41D6B":8,"F421CA":0,"F4248B":8,"F425FE":8,"F42679":20,"F42981":12,"F42A7D":31,"F42B8C":7,"F4308B":10,"F430B9":5,"F431C3":0,"F433B7":0,"F434F0":0,"F437B7":0,"F438C1":8,"F43909":5,"F439A6":0,"F43AFAC":6,"F43BD8":20,"F4419E":8,"F4428F":7,"F44588":8,"F44637":20,"F44C7F":8,"F44EE3":20,"F45293":0,"F453E4":8,"F4559C":8,"F45B29":8,"F45C42":8,"F45C89":0,"F460E2":10,"F462DC":8,"F4631F":8,"F463FC":12,"F46412":16,"F4645D":25,"F465A6":0,"F46639":37,"F46802":8,"F46AD7":19,"F46D04":1,"F46D2F":31,"F46D3F":20,"F470AB":12,"F47190":7,"F478AC":0,"F47946":8,"F47960":8,"F47B09":20,"F47B5E":7,"F47DEF":7,"F481C4":0,"F483CD":31,"F4848D":31,"F487C5":8,"F48918":8,"F48B32":10,"F48C50":20,"F48E09":15,"F48E38":4,"F48E92":8,"F492BF":32,"F49634":20,"F49F54":7,"F49FF3":8,"F4A157":8,"F4A1A6":0,"F4A310":0,"F4A475":20,"F4A4D6":8,"F4A59D":8,"F4A9A7":9,"F4AFE7":0,"F4B24E":8,"F4B301":20,"F4B599":0,"F4B78D":8,"F4B7B3":12,"F4BBC7":12,"F4BEEC":0,"F4BF80":8,"F4C248":7,"F4C714":8,"F4C88A":20,"F4CB52":8,"F4CBE7":0,"F4CE23":20,"F4CE46":5,"F4D108":20,"F4D488":0,"F4D620":11,"F4D9FB":7,"F4DBE3":0,"F4DCF9":8,"F4DD06":7,"F4DEAF":8,"F4E2C6":32,"F4E3FB":8,"F4E451":8,"F4E5F2":8,"F4E8C7":0,"F4EC38":31,"F4EE08":4,"F4F15A":0,"F4F1E1":14,"F4F26D":31,"F4F28A":8,"F4F309":7,"F4F50B":31,"F4F524":14,"F4F5A5":15,"F4F5D8":18,"F4F5DB":10,"F4F5E8":18,"F4F951":0,"F4FBB8":8,"F4FE3E":0,"F4FEFB":7,"F800A1":8,"F80113":8,"F801B4":17,"F80377":0,"F8042E":7,"F8075D":8,"F80C9A":11,"F80CF3":17,"F80DAC":5,"F80FF9":18,"F81093":0,"F81308":15,"F81654":20,"F81A2B":18,"F81A67":31,"F81E49":0,"F81EDF":0,"F81F32":14,"F820A9":8,"F82229":15,"F823B2":8,"F82793":0,"F828C9":8,"F82A53":37,"F82AE2":0,"F82B7F":8,"F82D7C":0,"F82E3F":8,"F82F65":8,"F832E4":1,"F83441":20,"F834E5":12,"F83869":17,"F83880":0,"F83B7E":8,"F83DFF":8,"F83E95":8,"F83F51":7,"F84288":0,"F843EF":10,"F8461C":16,"F84ABF":8,"F84CDA":8,"F84D89":0,"F84E17":16,"F84E58":7,"F84E73":0,"F84FAD":38,"F85329":8,"F854B8":30,"F85971":20,"F85B6E":7,"F85C4D":15,"F85EA0":20,"F85F2A":15,"F86214":0,"F8633F":20,"F8665A":0,"F86EEE":8,"F86FB0":31,"F86FC1":0,"F8710C":10,"F871A6":0,"F87394":33,"F873DF":0,"F875287":26,"F87588":8,"F877B8":7,"F87907":8,"F87D3F":8,"F87D76":0,"F88306":10,"F884F2":7,"F887F1":0,"F88C21":31,"F88F07":7,"F88FCA":18,"F894C2":20,"F89522":8,"F895C7":17,"F895EA":0,"F89753":8,"F898B9":8,"F898EF":8,"F89A25":8,"F89A78":8,"F89B6E":15,"F89E94":20,"F8A45F":10,"F8A986":19,"F8A9D0":17,"F8AB82":10,"F8AC65":20,"F8AF05":8,"F8B132":8,"F8B156":4,"F8B1DD":0,"F8B46A":5,"F8B54D":20,"F8BAE6":15,"F8BC12":4,"F8BF09":8,"F8C39E":8,"F8C3CC":0,"F8C4AE":11,"F8C4FA":11,"F8C903":31,"F8CAB8":4,"F8CB15":0,"F8CE21":31,"F8CF52":20,"F8CFC5":14,"F8D0AC":16,"F8D0BD":7,"F8D111":31,"F8D3F0":0,"F8D554":37,"F8DB88":4,"F8DE73":8,"F8E079":14,"F8E252":0,"F8E4E3":20,"F8E5CE":0,"F8E61A":7,"F8E7A0":12,"F8E811":8,"F8E94E":0,"F8EDFC":5,"F8EF5D":14,"F8F1B6":14,"F8F1E6":7,"F8F21E":20,"F8F58C":0,"F8F7B9":8,"F8FCE1":30,"F8FE5E":20,"F8FFC2":0,"FC0012":25,"FC0296":10,"FC039F":7,"FC041C":11,"FC0736":8,"FC084A":24,"FC0F76":30,"FC0FE6":16,"FC1193":8,"FC122C":8,"FC15B4":5,"FC1803":8,"FC183C":0,"FC1910":7,"FC1999":10,"FC1A11":12,"FC1A46":7,"FC1BD1":8,"FC1CA1":15,"FC1D2A":12,"FC1D3A":8,"FC1D43":0,"FC1F19":7,"FC253F":0,"FC2640":8,"FC2A9C":0,"FC2FAA":15,"FC315D":0,"FC3497":1,"FC3F7C":8,"FC3FDB":5,"FC4116":18,"FC4203":7,"FC4345":10,"FC4482":20,"FC47D8":0,"FC48EF":8,"FC492D":30,"FC4A47":15,"FC4CEA":4,"FC4CEF":8,"FC4DA6":8,"FC4E6D":8,"FC4EA4":0,"FC50D6":8,"FC51B5":8,"FC5557":0,"FC5708":22,"FC5B8C":10,"FC643A":7,"FC64BA":10,"FC65B3":8,"FC65DE":30,"FC66CF":0,"FC6D77":20,"FC6E83":7,"FC73FB":8,"FC7774":20,"FC79DD":8,"FC8417":9,"FC862A":8,"FC8743":8,"FC8827":0,"FC8C11":19,"FC8F90":7,"FC915D":18,"FC923B":15,"FC931D":8,"FC936B":7,"FC9435":8,"FC9CA7":0,"FC9D05":3,"FC9DD2":0,"FC9E53":20,"FCA0F3":8,"FCA13E":7,"FCA183":30,"FCA27E":8,"FCA47A0":22,"FCA5C8":0,"FCA5D0":11,"FCA621":7,"FCA667":30,"FCA9F5":10,"FCAA14":39,"FCAA81":0,"FCAAB6":7,"FCAB90":8,"FCABD0":12,"FCB214":0,"FCB3AA":20,"FCB3BC":20,"FCB6D8":0,"FCB9DF":14,"FCBCD1":8,"FCBE7B":12,"FCC17D":10,"FCC233":1,"FCC734":7,"FCC766":8,"FCCA40":16,"FCD436":14,"FCD733":31,"FCD749":30,"FCD848":0,"FCD908":10,"FCDE90":7,"FCDF00":37,"FCE1A6":8,"FCE26C":0,"FCE33C":8,"FCE557":15,"FCE5F0":7,"FCE998":0,"FCE9D8":30,"FCEB7B":8,"FCECDA":32,"FCEFD7":8,"FCF136":7,"FCF152":16,"FCF738":8,"FCF77B":8,"FCF8AE":20,"FCFC48":0}} diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/vivo.svg b/luci-app-oui/htdocs/luci-static/resources/oui/vivo.svg new file mode 100644 index 000000000..0591ac425 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/vivo.svg @@ -0,0 +1 @@ +vivo \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/vmware.svg b/luci-app-oui/htdocs/luci-static/resources/oui/vmware.svg new file mode 100644 index 000000000..efc0a1a12 --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/vmware.svg @@ -0,0 +1 @@ +VMware \ No newline at end of file diff --git a/luci-app-oui/htdocs/luci-static/resources/oui/xiaomi.svg b/luci-app-oui/htdocs/luci-static/resources/oui/xiaomi.svg new file mode 100644 index 000000000..f08eae5bd --- /dev/null +++ b/luci-app-oui/htdocs/luci-static/resources/oui/xiaomi.svg @@ -0,0 +1 @@ +Xiaomi \ No newline at end of file diff --git a/luci-app-oui/root/etc/config/oui b/luci-app-oui/root/etc/config/oui new file mode 100644 index 000000000..59db98b63 --- /dev/null +++ b/luci-app-oui/root/etc/config/oui @@ -0,0 +1,4 @@ +# Optional exact device overrides for OEM network modules. +# config device +# option mac '00:11:22:33:44:55' +# option vendor 'tcl' diff --git a/luci-app-oui/root/usr/share/rpcd/acl.d/luci-app-oui.json b/luci-app-oui/root/usr/share/rpcd/acl.d/luci-app-oui.json new file mode 100644 index 000000000..5c755e5fa --- /dev/null +++ b/luci-app-oui/root/usr/share/rpcd/acl.d/luci-app-oui.json @@ -0,0 +1,10 @@ +{ + "luci-app-oui": { + "description": "Read local OUI device brand overrides", + "read": { + "uci": [ + "oui" + ] + } + } +} diff --git a/luci-app-oui/tools/.gitignore b/luci-app-oui/tools/.gitignore new file mode 100644 index 000000000..c18dd8d83 --- /dev/null +++ b/luci-app-oui/tools/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/luci-app-oui/tools/generate.py b/luci-app-oui/tools/generate.py new file mode 100644 index 000000000..d2419dce4 --- /dev/null +++ b/luci-app-oui/tools/generate.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Generate the offline subset from a local WH-2099/macdb checkout and Simple Icons 11.15.0. + +No download or generation takes place on the router or during package builds. +""" +import argparse +import csv +import hashlib +import json +from pathlib import Path +import re +import shutil + +ROOT = Path(__file__).resolve().parents[1] + + +def normalize(name): + return re.sub(r'[^a-z0-9]+', ' ', name.lower()).strip() + + +def vendor_for(name, vendors): + matches = [i for i, v in enumerate(vendors) + if any(re.search(p, normalize(name)) for p in v['patterns'])] + if len(matches) > 1: + raise ValueError('Ambiguous vendor: ' + name) + return matches[0] if matches else None + + +def generate(source, icons): + vendors = json.loads((ROOT / 'tools/vendors.json').read_text()) + prefixes, all_rows, aliases = {}, {}, {} + path = source / 'mac.csv' + for row in csv.DictReader(path.open(encoding='utf-8-sig')): + assignment = row['assignment'].upper() + length = {'MA-L': 6, 'MA-M': 7, 'MA-S': 9}.get(row['registry']) + if length is None or not re.fullmatch('[0-9A-F]{%d}' % length, assignment): + raise ValueError('Invalid assignment: ' + assignment) + index = vendor_for(row['org_name'], vendors) + all_rows[assignment] = index + if index is not None: + prefixes[assignment] = index + aliases.setdefault(vendors[index]['slug'], set()).add(row['org_name']) + # An unrecognized more-specific assignment must mask a recognized parent. + for assignment, index in all_rows.items(): + if index is None and any(assignment[:n] in prefixes for n in (6, 7) if n < len(assignment)): + prefixes[assignment] = None + out = ROOT / 'htdocs/luci-static/resources/oui' + out.mkdir(parents=True, exist_ok=True) + data = json.dumps({'vendors': [[v['slug'], v['name']] for v in vendors], + 'prefixes': dict(sorted(prefixes.items()))}, separators=(',', ':')) + '\n' + revision = hashlib.sha256(data.encode()).hexdigest()[:12] + for old in out.glob('vendors-*.json'): + old.unlink() + (out / ('vendors-' + revision + '.json')).write_text(data) + for v in vendors: + if not v.get('supplemental'): + shutil.copyfile(icons / 'icons' / (v['slug'] + '.svg'), out / (v['slug'] + '.svg')) + elif not (out / (v['slug'] + '.svg')).is_file(): + raise ValueError('Missing supplemental icon: ' + v['slug']) + shutil.copyfile(source / 'LICENSE', out / 'LICENSE.macdb') + helper = out / 'oui.js' + if helper.exists(): + helper.write_text(re.sub(r'vendors-[0-9a-f]+\.json', 'vendors-' + revision + '.json', helper.read_text())) + for name in ['LICENSE.md', 'DISCLAIMER.md']: + shutil.copyfile(icons / name, out / name) + (ROOT / 'tools/sources.json').write_text(json.dumps({ + 'simple_icons': 'https://github.com/simple-icons/simple-icons/tree/11.15.0', + 'macdb': {'url': 'https://github.com/WH-2099/macdb', + 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()}, + 'database': 'vendors-' + revision + '.json', + 'assignments': len(prefixes), + 'supplemental_icons': json.loads((ROOT / 'tools/supplemental.json').read_text()), + 'aliases': {s: sorted(a) for s, a in sorted(aliases.items())} + }, indent=2) + '\n') + print(f'{len(vendors)} vendors, {len(prefixes)} assignments, {len(data)} bytes, revision {revision}') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('source', type=Path, help='macdb checkout containing mac.csv and LICENSE') + parser.add_argument('icons', type=Path, help='Unpacked simple-icons 11.15.0 source') + args = parser.parse_args() + generate(args.source, args.icons) diff --git a/luci-app-oui/tools/sources.json b/luci-app-oui/tools/sources.json new file mode 100644 index 000000000..7f99225de --- /dev/null +++ b/luci-app-oui/tools/sources.json @@ -0,0 +1,339 @@ +{ + "simple_icons": "https://github.com/simple-icons/simple-icons/tree/11.15.0", + "macdb": { + "url": "https://github.com/WH-2099/macdb", + "sha256": "dab68f6728825b5e6e894e537ffbc82d2d387eb2ba3d8dd313962dcef8076979" + }, + "database": "vendors-451e5311befc.json", + "assignments": 8637, + "supplemental_icons": { + "midea": { + "source": "https://commons.wikimedia.org/wiki/File:Midea.svg", + "sha256": "cd91efe06bdcb95055255d16b6a908e5dfba7530f92d7531f01b9e6f79f4fa6e", + "author": "Midea Group", + "license": "Public domain simple shape/text logo; trademark retained" + }, + "tcl": { + "source": "https://commons.wikimedia.org/wiki/File:Logo_of_the_TCL_Corporation.svg", + "sha256": "949117dc99503fe4a382afed3bd7f69fbfe61a6da91236b6842d8a4991df7fa8", + "author": "TCL Electronics", + "license": "Public domain simple shape/text logo; trademark retained" + }, + "gigabyte": { + "source": "https://commons.wikimedia.org/wiki/File:Gigabyte_Technology_logo_20080107.svg", + "sha256": "49693ef6522f58af4e059afe18c1fb3ddc8e6a5b15ac724d6a7493f47d84d849", + "author": "Koman90", + "license": "Public domain simple shape/text logo; trademark retained" + } + }, + "aliases": { + "acer": [ + "ACER NETXUS INC.", + "ACER PERIPHERALS, INC.", + "ACER TECHNOLOGIES CORP.", + "Acer Computer(Shanghai) Limited.", + "Acer Connect", + "Acer Inc.", + "Acer Incorporated" + ], + "amazon": [ + "Amazon Robotics MTAC Matrix NPI", + "Amazon Technologies Inc.", + "Amazon.com, LLC" + ], + "apple": [ + "Apple, Inc." + ], + "asus": [ + "ASUSTek COMPUTER INC.", + "Asus Network Technologies, Inc." + ], + "broadcom": [ + "Broadcom", + "Broadcom Corporation", + "Broadcom Inc.", + "Broadcom Limited", + "Broadcom Technologies" + ], + "dell": [ + "Dell", + "Dell EMC", + "Dell Inc.", + "Dell Technologies" + ], + "framework": [ + "Framework Computer Inc." + ], + "fujitsu": [ + "FUJITSU DENSO LTD.", + "FUJITSU ISOTEC LIMITED", + "FUJITSU LIMITED", + "FUJITSU MICROELECTRONICS, INC.", + "FUJITSU NEXION, INC.", + "Fujitsu Client Computing Limited", + "Fujitsu I-Network Systems Limited.", + "Fujitsu Limited", + "Fujitsu Services Ltd", + "Fujitsu Siemens Computers", + "Fujitsu Softek", + "Fujitsu Technology Solutions GmbH", + "Fujitsu component limited" + ], + "gigabyte": [ + "GIGA-BYTE TECHNOLOGY CO. , Ltd.", + "GIGA-BYTE TECHNOLOGY CO., LTD.", + "GIGA-BYTE TECHNOLOGY CO.,LTD.", + "Giga-Byte Technology Co., Ltd." + ], + "google": [ + "Google, Inc." + ], + "honor": [ + "Honor Device Co., Ltd." + ], + "hp": [ + "HP Inc.", + "Hewlett Packard" + ], + "huawei": [ + "HUAWEI TECHNOLOGIES CO.,LTD", + "Huawei Device Co., Ltd.", + "Huawei Symantec Technologies Co.,Ltd.", + "Huawei Technologies Co., Ltd." + ], + "intel": [ + " Intel \u2013 GE Care Innovations LLC", + "Intel Corporate", + "Intel Corporation", + "Intel Wireless Network Group" + ], + "lenovo": [ + "LENOVO(BEIJING)CO., LTD.", + "Lenovo", + "Lenovo (Beijing) Co., Ltd.", + "Lenovo (Beijing) Limited.", + "Lenovo Data Center Group", + "Lenovo Future Communication Technology (Chongqing) Company Limited", + "Lenovo Image(Tianjin) Technology Ltd.", + "Lenovo India Private Limited", + "Lenovo Information Products (Shenzhen)Co.,Ltd", + "Lenovo Mobile Communication (Wuhan) Company Limited", + "Lenovo Mobile Communication Technology Ltd." + ], + "lg": [ + "LG ELECTRONICS", + "LG ELECTRONICS INC", + "LG Electronics", + "LG Electronics (Mobile Communications)", + "LG Electronics Inc.", + "LG Electronics NV" + ], + "linksys": [ + "Linksys Telecom Shenzhen CO., LTD", + "Linksys USA, Inc" + ], + "mediatek": [ + "MediaTek Inc" + ], + "microsoft": [ + "MICROSOFT CORP.", + "Microsoft", + "Microsoft Corp.", + "Microsoft Corporation", + "Microsoft Mobile Oy", + "Microsoft Research", + "Microsoft XCG" + ], + "midea": [ + "GD Midea Air-Conditioning Equipment Co.,Ltd.", + "GD Midea Heating & Ventilating Equipment Co., Ltd", + "Midea Group Co., Ltd." + ], + "motorola": [ + "MOTOROLA", + "MOTOROLA COMMUNICATION ISRAEL", + "MOTOROLA SOLUTIONS MALAYSIA SDN. BHD.", + "Motorola", + "Motorola (Wuhan) Mobility Technologies Communication Co., Ltd.", + "Motorola - BSG", + "Motorola Inc Business Light Radios", + "Motorola Korea", + "Motorola Mobility LLC, a Lenovo Company", + "Motorola Solutions Inc.", + "Motorola(Wuhan) Mobility Technologies Communication Co.,Ltd", + "Motorola, Broadband Solutions Group" + ], + "msi": [ + "MICRO-STAR INT'L CO., LTD.", + "MICRO-STAR INT'L CO.,LTD", + "MICRO-STAR INT'L CO.,LTD.", + "MICRO-STAR INTERNATIONAL CO., LTD.", + "MSI", + "MSI Technology GmbH", + "Micro-Star INT'L CO., LTD", + "Micro-Star INT'L CO., LTD.", + "Micro-Star INTL CO., LTD.", + "Micro-Star Int'l Co, Ltd", + "Micro-Star International" + ], + "netgear": [ + "NETGEAR" + ], + "nokia": [ + "NOKIA MULTIMEDIA TERMINALS", + "NOKIA WIRELESS BUSINESS COMMUN", + "Nokia", + "Nokia Bell Labs", + "Nokia Bell N.V.", + "Nokia Corporation", + "Nokia Danmark A/S", + "Nokia NET Product Operations", + "Nokia Shanghai Bell Co., Ltd.", + "Nokia Siemens Networks GmbH & Co. KG.", + "Nokia Solutions (Shanghai) Co.,Ltd.", + "Nokia Solutions and Networks GmbH & Co. KG", + "Nokia Solutions and Networks India Private Limited" + ], + "nvidia": [ + "NVIDIA", + "NVIDIA Corporation" + ], + "oneplus": [ + "OnePlus Electronics (Shenzhen) Co., Ltd.", + "OnePlus Tech (Shenzhen) Ltd", + "OnePlus Technology (Shenzhen) Co., Ltd" + ], + "oppo": [ + "GUANGDONG OPPO MOBILE TELECOMMUNICATIONS CORP.,LTD", + "OPPO Digital, Inc." + ], + "panasonic": [ + "MATSUSHITA COMM. IND. CO. LTD.", + "MATSUSHITA ELECTRIC IND. CO", + "MATSUSHITA ELECTRONIC INSTRUMENTS CORP.", + "Matsushita Avionics Systems Corporation", + "Matsushita Electric Industrial Co.,Ltd.", + "Matsushita Electronic Components (Europe) GmbH", + "PANASONIC AUTOMOTIVE SYSTEM MALAYSIA", + "PANASONIC LIFE SOLUTIONS ELEKTR?K SANAY? VE T?CARE", + "PANASONIC TECHNOLOGIES, INC./MIECO-US", + "Panasonic Appliances Company", + "Panasonic Appliances Marketing Asia Pacific", + "Panasonic Automotive Systems", + "Panasonic Automotive Systems Asia Pacific(Thailand)Co.,Ltd.", + "Panasonic Automotive Systems Co.,Ltd", + "Panasonic Automotive Systems Company of America", + "Panasonic Communications Co., Ltd.", + "Panasonic Connect Co., Ltd.", + "Panasonic Corporation", + "Panasonic Corporation AVC Networks Company", + "Panasonic Corporation of North America", + "Panasonic Ecology Systems", + "Panasonic Electric Works Automation Controls Techno Co.,Ltd.", + "Panasonic Electric Works Co., Ltd.", + "Panasonic Electric Works Networks Co., Ltd.", + "Panasonic Enterprise Solutions Company", + "Panasonic Entertainment & Communication Co., Ltd.", + "Panasonic Europe Ltd.", + "Panasonic India Private limited", + "Panasonic India Pvt. Ltd.", + "Panasonic Industrial Devices Europe GmbH", + "Panasonic Marketing Middle East & Africa FZE", + "Panasonic Mobile Communications Co.,Ltd.", + "Panasonic System Solutions Europe", + "Panasonic Taiwan Co.,Ltd.", + "Panasonic do Brasil Limitada" + ], + "qnap": [ + "QNAP Systems, Inc." + ], + "raspberrypi": [ + "Raspberry Pi (Trading) Ltd", + "Raspberry Pi Foundation", + "Raspberry Pi Trading Ltd" + ], + "samsung": [ + "SAMSUNG ELECTRO MECHANICS CO., LTD.", + "SAMSUNG ELECTRO-MECHANICS(THAILAND)", + "SAMSUNG ELECTRONICS", + "SAMSUNG Electronics. Co. LTD", + "SAMSUNG HEAVY INDUSTRIES CO., LTD.", + "SAMSUNG TECHWIN CO.,LTD", + "Samsung Electronics", + "Samsung Electronics Co., Ltd", + "Samsung Electronics Co., Ltd.", + "Samsung Electronics Co., Ltd. ARTIK", + "Samsung Electronics Co., Ltd., Memory Division", + "Samsung Electronics Co.,Ltd", + "Samsung Electronics.,LTD", + "Samsung Semiconductor Inc.", + "Samsung Thales" + ], + "sharp": [ + "SHARP CORPORATION", + "SHARP Corporation", + "SHARP Corporation." + ], + "sony": [ + "SONY TEKTRONIX CORP.", + "SONY Visual Products Inc.", + "Sony Computer Entertainment America", + "Sony Corporation", + "Sony Home Entertainment&Sound Products Inc", + "Sony Honda Mobility Inc.", + "Sony Imaging Products & Solutions Inc.", + "Sony Interactive Entertainment Inc.", + "Sony Video & Sound Products Inc." + ], + "synology": [ + "Synology Incorporated" + ], + "tcl": [ + "Hui Zhou Gaoshengda Technology Co.,LTD", + "Shenzhen TCL New Technology Co., Ltd", + "TCL King Electrical Appliances (Huizhou) Co., Ltd", + "TCL King Electrical Appliances(Huizhou)Co.,Ltd", + "TCL Networks Equipment Co., Ltd.", + "TCL Technoly Electronics (Huizhou) Co., Ltd." + ], + "toshiba": [ + "TOSHIBA CLIENT SOLUTIONS CO., LTD.", + "TOSHIBA CORPORATION", + "TOSHIBA MITSUBISHI-ELECTRIC INDUSTRIAL SYSTEMS CORPORATION", + "Toshiba", + "Toshiba Corp.", + "Toshiba Corporation", + "Toshiba Electron Tubes & Devices Co., Ltd.", + "Toshiba Global Commerce Solutions", + "Toshiba Samsung Storage Technolgoy Korea Corporation ", + "Toshiba TEC Corporation Inc", + "Toshiba Teli Corporation", + "Toshiba Toko Meter Systems Co., LTD.", + "Toshiba Visual Solutions Corporation Co.,Ltd" + ], + "tplink": [ + "TP-LINK TECHNOLOGIES CO.,LTD.", + "TP-Link Systems Inc", + "TP-Link Systems Inc." + ], + "ubiquiti": [ + "Ubiquiti Inc" + ], + "vivo": [ + "Vivo International Corporation Pty Ltd", + "vivo Mobile Communication Co., Ltd." + ], + "vmware": [ + "VMware, Inc." + ], + "xiaomi": [ + "Beijing Xiaomi Electronics Co., Ltd.", + "Beijing Xiaomi Electronics Co.,Ltd", + "Beijing Xiaomi Mobile Software Co., Ltd", + "Shanghai Imilab Technology Co.Ltd", + "XIAOMI Electronics,CO.,LTD", + "Xiaomi Communications Co Ltd", + "Xiaomi EV Technology Co., Ltd." + ] + } +} diff --git a/luci-app-oui/tools/supplemental.json b/luci-app-oui/tools/supplemental.json new file mode 100644 index 000000000..0cb355db8 --- /dev/null +++ b/luci-app-oui/tools/supplemental.json @@ -0,0 +1,20 @@ +{ + "midea": { + "source": "https://commons.wikimedia.org/wiki/File:Midea.svg", + "sha256": "cd91efe06bdcb95055255d16b6a908e5dfba7530f92d7531f01b9e6f79f4fa6e", + "author": "Midea Group", + "license": "Public domain simple shape/text logo; trademark retained" + }, + "tcl": { + "source": "https://commons.wikimedia.org/wiki/File:Logo_of_the_TCL_Corporation.svg", + "sha256": "949117dc99503fe4a382afed3bd7f69fbfe61a6da91236b6842d8a4991df7fa8", + "author": "TCL Electronics", + "license": "Public domain simple shape/text logo; trademark retained" + }, + "gigabyte": { + "source": "https://commons.wikimedia.org/wiki/File:Gigabyte_Technology_logo_20080107.svg", + "sha256": "49693ef6522f58af4e059afe18c1fb3ddc8e6a5b15ac724d6a7493f47d84d849", + "author": "Koman90", + "license": "Public domain simple shape/text logo; trademark retained" + } +} diff --git a/luci-app-oui/tools/test_generate.py b/luci-app-oui/tools/test_generate.py new file mode 100644 index 000000000..fa2b519a6 --- /dev/null +++ b/luci-app-oui/tools/test_generate.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import json +import unittest +from generate import ROOT, vendor_for + + +class Aliases(unittest.TestCase): + def setUp(self): + self.vendors = json.loads((ROOT / 'tools/vendors.json').read_text()) + + def brand(self, name): + i = vendor_for(name, self.vendors) + return None if i is None else self.vendors[i]['slug'] + + def test_aliases(self): + for name in ['ASUSTek COMPUTER INC.', 'ASUSTeK Computer Inc.', 'ASUS', + 'ASUS COMPUTER INTERNATIONAL']: + self.assertEqual(self.brand(name), 'asus') + for name in ["MICRO-STAR INT'L CO., LTD.", 'Micro-Star International']: + self.assertEqual(self.brand(name), 'msi') + for name in ['GIGA-BYTE TECHNOLOGY CO.,LTD.', 'GIGA-BYTE TECHNOLOGY CO. , Ltd.']: + self.assertEqual(self.brand(name), 'gigabyte') + self.assertEqual(self.brand('vivo Mobile Communication Co., Ltd.'), 'vivo') + self.assertEqual(self.brand('TP-Link Systems Inc.'), 'tplink') + self.assertEqual(self.brand('GD Midea Air-Conditioning Equipment Co.,Ltd.'), 'midea') + self.assertEqual(self.brand('Midea Group Co., Ltd.'), 'midea') + self.assertEqual(self.brand('Shanghai Imilab Technology Co.Ltd'), 'xiaomi') + self.assertEqual(self.brand('Shanghai Chuangmi Technology Co., Ltd.'), 'xiaomi') + self.assertIsNone(self.brand('Chuangming Futre Technology Co., Ltd.')) + self.assertEqual(self.brand('TCL King Electrical Appliances(Huizhou)Co.,Ltd'), 'tcl') + self.assertEqual(self.brand('Hui Zhou Gaoshengda Technology Co.,LTD'), 'tcl') + + def test_vmware(self): + self.assertEqual(self.brand('VMware, Inc.'), 'vmware') + self.assertIsNone(self.brand('VMware Unrelated Devices')) + data = json.loads(next((ROOT / 'htdocs/luci-static/resources/oui').glob('vendors-*.json')).read_text()) + for prefix in ['000569', '000C29', '001C14', '005056']: + self.assertEqual(data['vendors'][data['prefixes'][prefix]], ['vmware', 'VMware']) + + def test_unrelated_names(self): + for name in ['Pegasus Technologies Inc.', 'Asustor Inc.', 'Vivotek, Inc.', + 'Invivo Research Inc.', 'Dongguan Koppo Electronic Co.,Ltd', + 'SHENZHEN HONOR ELECTRONIC CO.,LTD', 'Limidea Concept Pte. Ltd.', + 'Gigabit Systems Inc.', 'Unrelated Module Manufacturer']: + self.assertIsNone(self.brand(name)) + + def test_assets(self): + data = json.loads(next((ROOT / 'htdocs/luci-static/resources/oui').glob('vendors-*.json')).read_text()) + for slug, _ in data['vendors']: + self.assertTrue((ROOT / 'htdocs/luci-static/resources/oui' / (slug + '.svg')).exists()) + self.assertEqual(data['vendors'][data['prefixes']['30560F']][0], 'gigabyte') + self.assertEqual(data['vendors'][data['prefixes']['E87F95']][0], 'apple') + self.assertEqual(data['vendors'][data['prefixes']['9C4782']][0], 'tplink') + self.assertEqual(data['vendors'][data['prefixes']['1841C3']][0], 'midea') + self.assertEqual(data['vendors'][data['prefixes']['607EA4']][0], 'xiaomi') + self.assertEqual(data['vendors'][data['prefixes']['0C7955']][0], 'tcl') + + +if __name__ == '__main__': + unittest.main() diff --git a/luci-app-oui/tools/test_oui.js b/luci-app-oui/tools/test_oui.js new file mode 100644 index 000000000..636db26f2 --- /dev/null +++ b/luci-app-oui/tools/test_oui.js @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +const assert = require('assert'); +const fs = require('fs'); +const vm = require('vm'); +const path = require('path'); +const source = fs.readFileSync(path.join(__dirname, '../htdocs/luci-static/resources/oui/oui.js'), 'utf8'); +function setup(fail = false) { + let requests = 0; + const document = { + currentScript: { src: 'http://router/luci-static/resources/oui/oui.js?v=1' }, + createElement() { return { style: {}, remove() { this.removed = true; } }; } + }; + const data = { vendors: [['asus', 'ASUS'], ['apple', 'Apple']], prefixes: { + '001122': 0, '0011223': 1, '001122334': 0, '0011224': null + }}; + const ctx = { window: {}, document, Promise, XMLHttpRequest: function() { + this.open = (method, url, async) => { assert(async); assert(url.startsWith('http://router/luci-static/resources/oui/vendors-')); }; + this.send = () => { requests++; this.status = fail ? 404 : 200; this.responseText = JSON.stringify(data); this.onload(); }; + }}; + vm.runInNewContext(source, ctx); + const node = () => ({ children: [], querySelector() { return this.children[0]; }, insertBefore(img) { this.children.unshift(img); } }); + return { oui: ctx.window.luciOUI, node, requests: () => requests }; +} +(async () => { + let t = setup(); + for (const mac of [null, '', '02:11:22:00:00:00', '01:11:22:00:00:00', 'FF:FF:FF:FF:FF:FF', '001122', '00:11-22:33:44:55']) + t.oui.decorate(t.node(), mac); + assert.equal(t.requests(), 0, 'invalid or private MACs must not fetch data'); + const cases = [['00:11:22:00:00:00','ASUS'],['00-11-22-35-00-00','Apple'],['001122334455','ASUS'],['00:11:22:40:00:00',null],['00:AB:CD:00:00:00',null]]; + const nodes = cases.map(([mac]) => { const n=t.node(); t.oui.decorate(n,mac); return n; }); + await Promise.resolve(); await Promise.resolve(); + cases.forEach(([mac, brand], i) => { assert.equal(nodes[i].children[0]?.title ?? null, brand || 'Unknown vendor'); t.oui.decorate(nodes[i],mac); assert(nodes[i].children.length <= 1); }); + assert.equal(t.requests(), 1, 'polls share one database request'); + nodes[0].children[0].onerror(); assert(nodes[0].children[0].src.endsWith('/computer.svg')); + nodes[0].children[0].onerror(); assert(nodes[0].children[0].removed); + t.oui.setDevices([{mac:'00:AB:CD:00:00:01',vendor:'apple'}, {mac:'00:AB:CD:00:00:03',vendor:'../../bad'}]); + const exact=t.node(), other=t.node(), unsafe=t.node(); + t.oui.decorate(exact,'00:AB:CD:00:00:01'); + t.oui.decorate(other,'00:AB:CD:00:00:02'); + t.oui.decorate(unsafe,'00:AB:CD:00:00:03'); + assert.equal(exact.children[0].title,'Apple'); + assert.equal(other.children[0].title,'Unknown vendor','OEM overrides must not affect other devices in the same OUI'); + assert.equal(unsafe.children[0].title,'Unknown vendor'); + const fnos=t.node(); + t.oui.decorate(fnos,'BA:71:BE:00:00:01',true); + assert.equal(fnos.children[0].title,'fnOS 飞牛OS'); + assert(fnos.children[0].src.endsWith('/fnos.svg')); + const overridden=t.node(); + t.oui.decorate(overridden,'00:11:22:00:00:00',true); + assert.equal(overridden.children[0].title,'fnOS 飞牛OS'); + fnos.children[0].onerror(); + assert(fnos.children[0].src.endsWith('/computer.svg')); + t=setup(true); + t.oui.decorate(t.node(),'00:11:22:00:00:00'); await Promise.resolve(); + t.oui.decorate(t.node(),'00:11:22:00:00:00'); await Promise.resolve(); + assert.equal(t.requests(),1,'failed lookups must not retry every poll'); + // Exercise the optional LuCI integration, including URL construction. + const statusSource = fs.readFileSync(path.join(__dirname, '../../../modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js'), 'utf8'); + let enabled = false, scripts = []; + const doc = { createTextNode: text => ({ text }), createElement: () => ({}), head: { appendChild: s => scripts.push(s) } }; + const L = { resolveDefault: p => Promise.resolve(p).catch(() => null), hasSystemFeature: () => enabled, resource: name => { + assert(!name.includes('?'), 'L.resource rejects query strings'); + return '/luci-static/resources/' + name; + }}; + const view = new Function('baseclass','rpc','L','_','E','document','window','uci', statusSource)( + { extend: o => o }, { declare: () => () => {} }, L, s => s, + (tag, attrs, children) => ({ tag, children }), doc, {}, { load: () => Promise.resolve(), sections: () => [] } + ); + assert.equal(view.renderHostname('host', '00:11:22:00:00:00').children[0].text, 'host'); + assert.equal(scripts.length, 0); + enabled = true; + const host = view.renderHostname('', '00:11:22:00:00:00'); + assert.equal(host.children[0].text, ''); + assert.equal(scripts[0].src, '/luci-static/resources/oui/oui.js?v=7'); + view.renderHostname('host2', '00:11:22:00:00:01'); + assert.equal(scripts.length, 1); + console.log('OUI runtime and optional integration checks passed'); +})(); diff --git a/luci-app-oui/tools/vendors.json b/luci-app-oui/tools/vendors.json new file mode 100644 index 000000000..a40b2fe59 --- /dev/null +++ b/luci-app-oui/tools/vendors.json @@ -0,0 +1,299 @@ +[ + { + "slug": "apple", + "name": "Apple", + "patterns": [ + "^apple(?: computer)?(?: inc)?$" + ] + }, + { + "slug": "asus", + "name": "ASUS", + "patterns": [ + "^(?:asustek|asus)(?: |$)" + ] + }, + { + "slug": "acer", + "name": "Acer", + "patterns": [ + "^acer(?: |$)" + ] + }, + { + "slug": "msi", + "name": "MSI", + "patterns": [ + "^micro star(?: |$)", + "^msi(?: |$)" + ] + }, + { + "slug": "dell", + "name": "Dell", + "patterns": [ + "^dell(?: |$)" + ] + }, + { + "slug": "hp", + "name": "HP", + "patterns": [ + "^hewlett packard$", + "^hp inc$", + "^hp$" + ] + }, + { + "slug": "lenovo", + "name": "Lenovo", + "patterns": [ + "^lenovo(?: |$)" + ] + }, + { + "slug": "samsung", + "name": "Samsung", + "patterns": [ + "^samsung(?: |$)" + ] + }, + { + "slug": "huawei", + "name": "Huawei", + "patterns": [ + "^huawei(?: |$)" + ] + }, + { + "slug": "honor", + "name": "HONOR", + "patterns": [ + "^honor device(?: |$)" + ] + }, + { + "slug": "xiaomi", + "name": "Xiaomi", + "patterns": [ + "^(?:beijing )?xiaomi(?: |$)", + "^(?:shanghai )?imilab(?: |$)", + "^(?:shanghai )?chuangmi(?: |$)" + ] + }, + { + "slug": "oppo", + "name": "OPPO", + "patterns": [ + "^(?:guangdong )?oppo(?: |$)" + ] + }, + { + "slug": "vivo", + "name": "vivo", + "patterns": [ + "^vivo (?:mobile communication|international corporation)(?: |$)" + ] + }, + { + "slug": "oneplus", + "name": "OnePlus", + "patterns": [ + "^oneplus(?: |$)" + ] + }, + { + "slug": "motorola", + "name": "Motorola", + "patterns": [ + "^motorola(?: |$)" + ] + }, + { + "slug": "nokia", + "name": "Nokia", + "patterns": [ + "^nokia(?: |$)" + ] + }, + { + "slug": "sony", + "name": "Sony", + "patterns": [ + "^sony(?: |$)" + ] + }, + { + "slug": "lg", + "name": "LG", + "patterns": [ + "^lg electronics(?: |$)" + ] + }, + { + "slug": "google", + "name": "Google", + "patterns": [ + "^google(?: |$)" + ] + }, + { + "slug": "microsoft", + "name": "Microsoft", + "patterns": [ + "^microsoft(?: |$)" + ] + }, + { + "slug": "intel", + "name": "Intel", + "patterns": [ + "^intel(?: |$)" + ] + }, + { + "slug": "nvidia", + "name": "NVIDIA", + "patterns": [ + "^nvidia(?: |$)" + ] + }, + { + "slug": "broadcom", + "name": "Broadcom", + "patterns": [ + "^broadcom(?: |$)" + ] + }, + { + "slug": "mediatek", + "name": "MediaTek", + "patterns": [ + "^mediatek(?: |$)" + ] + }, + { + "slug": "fujitsu", + "name": "Fujitsu", + "patterns": [ + "^fujitsu(?: |$)" + ] + }, + { + "slug": "toshiba", + "name": "Toshiba", + "patterns": [ + "^toshiba(?: |$)" + ] + }, + { + "slug": "panasonic", + "name": "Panasonic", + "patterns": [ + "^(?:panasonic|matsushita)(?: |$)" + ] + }, + { + "slug": "sharp", + "name": "Sharp", + "patterns": [ + "^sharp(?: |$)" + ] + }, + { + "slug": "raspberrypi", + "name": "Raspberry Pi", + "patterns": [ + "^raspberry pi(?: |$)" + ] + }, + { + "slug": "framework", + "name": "Framework", + "patterns": [ + "^framework computer(?: |$)" + ] + }, + { + "slug": "amazon", + "name": "Amazon", + "patterns": [ + "^amazon(?: |$)" + ] + }, + { + "slug": "tplink", + "name": "TP-Link", + "patterns": [ + "^tp link(?: |$)", + "^tplink(?: |$)" + ] + }, + { + "slug": "ubiquiti", + "name": "Ubiquiti", + "patterns": [ + "^ubiquiti(?: |$)" + ] + }, + { + "slug": "netgear", + "name": "NETGEAR", + "patterns": [ + "^netgear(?: |$)" + ] + }, + { + "slug": "linksys", + "name": "Linksys", + "patterns": [ + "^linksys(?: |$)" + ] + }, + { + "slug": "synology", + "name": "Synology", + "patterns": [ + "^synology(?: |$)" + ] + }, + { + "slug": "qnap", + "name": "QNAP", + "patterns": [ + "^qnap(?: |$)" + ] + }, + { + "slug": "midea", + "name": "Midea", + "patterns": [ + "^(?:gd |guangdong )?midea(?: |$)" + ], + "supplemental": true + }, + { + "slug": "tcl", + "name": "TCL", + "patterns": [ + "^(?:shenzhen )?tcl (?:king|technoly|networks|new technology|communication|overseas|electronic|electronics|mobile|smart|technological)(?: |$)", + "^(?:hui zhou|huizhou) gaoshengda(?: |$)" + ], + "supplemental": true + }, + { + "slug": "gigabyte", + "name": "GIGABYTE", + "patterns": [ + "^giga ?byte technology(?: |$)" + ], + "supplemental": true + }, + { + "slug": "vmware", + "name": "VMware", + "patterns": [ + "^vmware(?: inc)?$" + ] + } +] diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 73411a9c5..c1fab606c 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.9.9 -PKG_RELEASE:=270 +PKG_RELEASE:=271 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index ea98befa1..bebd42542 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -652,21 +652,46 @@ o:value("info", "Info") o:value("warn", "Warning") o:value("error", "Error") -o = s:taboption("log", Flag, "advanced_log_feature", translate("Advanced log feature"), translate("For professionals only.")) -o.default = "0" -o = s:taboption("log", Flag, "sys_log", translate("Logging to system log"), translate("Logging to the system log for more advanced functions. For example, send logs to a dedicated log server.")) -o:depends("advanced_log_feature", "1") -o.default = "0" -o = s:taboption("log", Value, "persist_log_path", translate("Persist log file directory"), translate("The path to the directory used to store persist log files, the \"/\" at the end can be omitted. Leave it blank to disable this feature.")) -o:depends({ ["advanced_log_feature"] = 1, ["sys_log"] = 0 }) -o = s:taboption("log", Value, "log_event_filter", translate("Log Event Filter"), translate("Support regular expression.")) -o:depends("advanced_log_feature", "1") -o = s:taboption("log", Value, "log_event_cmd", translate("Shell Command"), translate("Shell command to execute, replace log content with %s.")) -o:depends("advanced_log_feature", "1") +o = s:taboption("log", DummyValue, "_node_log", translate("Log File")) +o.rawhtml = true +o.cfgvalue = function(t, n) + local log_file = api.TMP_PATH .. "/acl/default/global.log" + local log_url = api.url("get_redir_log") .. "?id=default" + local s = "%s  " % log_file + if api.fs.access(log_file) then + local btn = string.format( + '', + translate("View Log"), + log_url + ) + s = s .. btn + end + return s +end +o:depends("log_node", "1") o = s:taboption("log", Flag, "log_chinadns_ng", translate("Enable") .. " ChinaDNS-NG " .. translate("Log")) o.default = "0" o.rmempty = false +o:depends("dns_shunt", "chinadns-ng") + +o = s:taboption("log", DummyValue, "_chinadns_ng_log", translate("Log File")) +o.rawhtml = true +o.cfgvalue = function(t, n) + local log_file = api.TMP_PATH .. "/acl/default/chinadns_ng.log" + local log_url = api.url("get_chinadns_log") .. "?flag=default" + local s = "%s  " % log_file + if api.fs.access(log_file) then + local btn = string.format( + '', + translate("View Log"), + log_url + ) + s = s .. btn + end + return s +end +o:depends("log_chinadns_ng", "1") o = s:taboption("log", DummyValue, "_log_tips", " ") o.rawhtml = true diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua index 1204da366..93173da55 100644 --- a/luci-app-passwall/luasrc/passwall/api.lua +++ b/luci-app-passwall/luasrc/passwall/api.lua @@ -163,12 +163,16 @@ function sh_uci_commit(config) exec_call(string.format("uci -q commit %s", config)) end +function del_cache_var(key) + sys.call(string.format('. /usr/share/passwall2/utils.sh ; del_cache_var "%s"', key)) +end + function set_cache_var(key, val) - sys.call(string.format('. /usr/share/passwall/utils.sh ; set_cache_var %s "%s"', key, val)) + sys.call(string.format('. /usr/share/passwall/utils.sh ; set_cache_var "%s" "%s"', key, val)) end function get_cache_var(key) - local val = sys.exec(string.format('. /usr/share/passwall/utils.sh ; echo -n $(get_cache_var %s)', key)) + local val = sys.exec(string.format('. /usr/share/passwall/utils.sh ; echo -n $(get_cache_var "%s")', key)) if val == "" then val = nil end return val end @@ -2072,3 +2076,11 @@ function gen_wireguard_key() } end end + +function get_socks_port_by_cache(node_id) + return get_cache_var("node_%s_socks_port" % { node_id }) +end + +function set_socks_port_to_cache(node_id, v) + set_cache_var("node_%s_socks_port" % { node_id }, v) +end diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua index ca4ee6fa7..31895d26f 100644 --- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -105,31 +105,54 @@ function gen_outbound(flag, node, tag, proxy_table) end if node.type ~= "sing-box" then - local relay_port = node.port - local new_port = api.get_new_port() - local config_file = string.format("%s_%s_%s.json", flag, tag, new_port) - if tag and node_id and not tag:find(node_id) then - config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port) - end - if run_socks_instance then - sys.call(string.format('/usr/share/passwall/app.sh run_socks "%s"> /dev/null', - string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s", - new_port, --flag - node_id, --node - "127.0.0.1", --bind - new_port, --socks port - config_file, --config file - (proxy_tag and relay_port) and tostring(relay_port) or "" --relay port + if node.type == "Socks" then + node.protocol = "socks" + proxy_tag = "socks <- " .. node_id + else + local new_port + if run_socks_instance then + local relay_port = (proxy_tag and node.port) and tostring(node.port) or "" + if relay_port == "" then + local cache = api.get_socks_port_by_cache(node_id) + if cache then + new_port = cache + run_socks_instance = nil + end + end + if run_socks_instance then + new_port = api.get_new_port() + local config_file = string.format("nodesocks_%s_%s.json", node_id, new_port) + if tag and node_id and not tag:find(node_id) then + config_file = string.format("nodesocks_%s_%s_%s.json", tag, node_id, new_port) + end + sys.call(string.format('/usr/share/passwall/app.sh run_socks "%s"> /dev/null', + string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s", + new_port, --flag + node_id, --node + "127.0.0.1", --bind + new_port, --socks port + config_file, --config file + relay_port --relay port + ) + ) ) - ) - ) + if relay_port == "" then + api.set_socks_port_to_cache(node_id, new_port) + end + end + else + TMP_PORT = TMP_PORT and TMP_PORT + 1 or 3001 -- 导出配置时作为演示 + new_port = TMP_PORT + end + if new_port then + node = { + protocol = "socks", + address = "127.0.0.1", + port = new_port + } + proxy_tag = "socks <- " .. node_id + end end - node = { - protocol = "socks", - address = "127.0.0.1", - port = new_port - } - proxy_tag = "socks <- " .. node_id else if proxy_tag then node.detour = proxy_tag diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index 73c890179..bc4e44191 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -12,7 +12,7 @@ local GLOBAL = { local xray_version = api.get_app_version("xray") -local xray_min_version = "26.3.27" +local xray_min_version = "26.7.11" local function get_domain_excluded() local path = string.format("/usr/share/%s/rules/domains_excluded", api.c_config) @@ -58,31 +58,50 @@ function gen_outbound(flag, node, tag, proxy_table) if node.type ~= "Xray" then if node.type == "Socks" then node.protocol = "socks" - node.transport = "tcp" + node.transport = "raw" else - local relay_port = node.port - local new_port = api.get_new_port() - local config_file = string.format("%s_%s_%s.json", flag, tag, new_port) - if tag and node_id and not tag:find(node_id) then - config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port) - end + local new_port if run_socks_instance then - sys.call(string.format('/usr/share/passwall/app.sh run_socks "%s"> /dev/null', - string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s", - new_port, --flag - node_id, --node - "127.0.0.1", --bind - new_port, --socks port - config_file, --config file - (proxy_tag and relay_port) and tostring(relay_port) or "" --relay port + local relay_port = (proxy_tag and node.port) and tostring(node.port) or "" + if relay_port == "" then + local cache = api.get_socks_port_by_cache(node_id) + if cache then + new_port = cache + run_socks_instance = nil + end + end + if run_socks_instance then + new_port = api.get_new_port() + local config_file = string.format("nodesocks_%s_%s.json", node_id, new_port) + if tag and node_id and not tag:find(node_id) then + config_file = string.format("nodesocks_%s_%s_%s.json", tag, node_id, new_port) + end + sys.call(string.format('/usr/share/passwall/app.sh run_socks "%s"> /dev/null', + string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s", + new_port, --flag + node_id, --node + "127.0.0.1", --bind + new_port, --socks port + config_file, --config file + relay_port --relay port + ) + ) ) - )) + if relay_port == "" then + api.set_socks_port_to_cache(node_id, new_port) + end + end + else + TMP_PORT = TMP_PORT and TMP_PORT + 1 or 3001 -- 导出配置时作为演示 + new_port = TMP_PORT + end + if new_port then + node = {} + node.protocol = "socks" + node.transport = "raw" + node.address = "127.0.0.1" + node.port = new_port end - node = {} - node.protocol = "socks" - node.transport = "tcp" - node.address = "127.0.0.1" - node.port = new_port end node.stream_security = "none" proxy_tag = "socks <- " .. node_id @@ -156,7 +175,7 @@ function gen_outbound(flag, node, tag, proxy_table) } or nil, dialerProxy = dialer_proxy_tag, }, - [(api.compare_versions(xray_version, "<", "26.7.11")) and "network" or "method"] = node.transport, -- Todo: Remove version check and "network" + method = node.transport, security = node.stream_security, tlsSettings = (node.stream_security == "tls") and { serverName = node.tls_serverName, @@ -483,7 +502,7 @@ function gen_config_server(node) local settings = nil local routing = nil local outbounds = { - { protocol = "freedom", tag = "direct", settings = { finalRules = {{ action = "allow" }}}}, { protocol = "blackhole", tag = "blocked" } + { protocol = "freedom", tag = "direct", finalRules = {{ action = "allow" }}}, { protocol = "blackhole", tag = "blocked" } } local users = node.users or {} @@ -631,9 +650,7 @@ function gen_config_server(node) interface = node.outbound_node_iface } }, - settings = { - finalRules = {{ action = "allow" }} - } + finalRules = {{ action = "allow" }} } sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.outbound_node_iface)) else @@ -675,7 +692,7 @@ function gen_config_server(node) protocol = node.protocol, settings = settings, streamSettings = { - [(api.compare_versions(xray_version, "<", "26.7.11")) and "network" or "method"] = node.transport, -- Todo: Remove version check and "network" + method = node.transport, security = "none", tlsSettings = ("1" == node.tls) and { disableSystemRoot = false, @@ -857,9 +874,6 @@ function gen_config_server(node) config.outbounds[index][k] = nil end end - if value.protocol == "freedom" and api.compare_versions(xray_version, "<", "26.5.3") then -- Todo is to remove it - value.settings = nil - end end return config @@ -1316,9 +1330,7 @@ function gen_config(var) interface = node.iface } }, - settings = (api.compare_versions(xray_version, ">", "26.4.25")) and { -- Todo: Remove version check - finalRules = {{ action = "allow" }} - } or nil + finalRules = {{ action = "allow" }} } sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface)) end @@ -1802,11 +1814,10 @@ function gen_config(var) sockopt = { dialerProxy = (dns_outbound_tag ~= "blackhole") and dns_outbound_tag or "direct" } } or nil, settings = { - address = (chn_list ~= "proxy") and "8.8.8.8" or "223.5.5.5", - port = 53, - network = "tcp", - nonIPQuery = (api.compare_versions(xray_version, "<", "26.4.25")) and "reject" or nil, -- Todo is to remove it - rules = (api.compare_versions(xray_version, ">", "26.4.17")) and {} or nil + rewriteAddress = (chn_list ~= "proxy") and "8.8.8.8" or "223.5.5.5", + rewritePort = 53, + rewriteNetwork = "tcp", + rules = {} } } @@ -2030,9 +2041,7 @@ function gen_config(var) local direct_outbound = { protocol = "freedom", tag = "direct", - settings = (api.compare_versions(xray_version, ">", "26.4.25")) and { -- Todo: Remove version check - finalRules = {{ action = "allow" }} - } or nil, + finalRules = {{ action = "allow" }}, streamSettings = { sockopt = { mark = 255, @@ -2136,7 +2145,7 @@ function gen_proto_config(var) local outbound = { protocol = server_proto, streamSettings = { - network = "tcp", + method = "raw", security = "none" }, settings = { @@ -2161,10 +2170,10 @@ function gen_proto_config(var) table.insert(outbounds, { protocol = "freedom", tag = "direct", - settings = (api.compare_versions(xray_version, ">", "26.4.25")) and { -- Todo: Remove version check - finalRules = {{ action = "allow" }} - } or nil, - sockopt = {mark = 255} + finalRules = {{ action = "allow" }}, + streamSettings = { + sockopt = {mark = 255} + } }) local config = { @@ -2176,7 +2185,10 @@ function gen_proto_config(var) -- 传出连接 outbounds = outbounds, -- 路由 - routing = routing + routing = routing, + version = { + min = xray_min_version + } } return jsonc.stringify(config, 1) end diff --git a/luci-app-passwall/po/zh-cn/passwall.po b/luci-app-passwall/po/zh-cn/passwall.po index a0e6ca45d..7b373555a 100644 --- a/luci-app-passwall/po/zh-cn/passwall.po +++ b/luci-app-passwall/po/zh-cn/passwall.po @@ -1685,36 +1685,6 @@ msgstr "启用节点日志" msgid "Log Level" msgstr "日志等级" -msgid "Advanced log feature" -msgstr "高级日志功能" - -msgid "For professionals only." -msgstr "仅限专业人士使用。" - -msgid "Persist log file directory" -msgstr "持久性日志文件目录" - -msgid "The path to the directory used to store persist log files, the \"/\" at the end can be omitted. Leave it blank to disable this feature." -msgstr "用来存储持久性日志文件的目录路径,末尾的 “/” 可以省略。留空以禁用此功能。" - -msgid "Logging to system log" -msgstr "记录到系统日志" - -msgid "Logging to the system log for more advanced functions. For example, send logs to a dedicated log server." -msgstr "将日志记录到系统日志,以实现更加高级的功能。例如,把日志发送到专门的日志服务器。" - -msgid "Log Event Filter" -msgstr "日志事件过滤器" - -msgid "Support regular expression." -msgstr "支持正则表达式。" - -msgid "Shell Command" -msgstr "Shell 命令" - -msgid "Shell command to execute, replace log content with %s." -msgstr "要执行的 Shell 命令,用 %s 代替日志内容。" - msgid "Not enabled log" msgstr "未启用日志" diff --git a/luci-app-passwall/root/etc/hotplug.d/ntp/30-passwall-resync b/luci-app-passwall/root/etc/hotplug.d/ntp/30-passwall-resync index 5f020b9a4..675b8e8d8 100644 --- a/luci-app-passwall/root/etc/hotplug.d/ntp/30-passwall-resync +++ b/luci-app-passwall/root/etc/hotplug.d/ntp/30-passwall-resync @@ -1,17 +1,12 @@ #!/bin/sh -# Devices without a hardware RTC boot with a wrong system clock: sysfixtime can -# only restore the mtime of the newest file under /etc, so the clock is usually -# hours behind after a cold boot. passwall is then started by -# /etc/hotplug.d/iface/98-passwall on ifup, which typically happens before NTP -# has corrected the time, and time-sensitive handshakes (VMess AEAD, TLS) fail. -# -# Nothing restarts passwall once the clock is corrected, so it stays broken -# until the user restarts it manually. Restart once when ntpd reports that the -# time is valid -- the same approach dnsmasq uses for DNSSEC in -# /etc/hotplug.d/ntp/25-dnsmasqsec. +[ "$ACTION" = "step" ] || exit 0 -[ "$ACTION" = "stratum" ] || exit 0 +offset=${offset#-} +offset=${offset%.*} + +# Skip service restart if the time adjustment is less than 120 seconds. +[ "$offset" -lt 120 ] && exit 0 . /usr/share/passwall/utils.sh @@ -23,5 +18,5 @@ NTP_LOCK_FILE="${LOCK_PATH}/${CONFIG}_ntp.lock" echo $$ > ${NTP_LOCK_FILE} /etc/init.d/${CONFIG} restart >/dev/null 2>&1 & - logger -p notice -t network -s "${CONFIG}: restart after NTP time became valid" + logger -p notice -t network -s "${CONFIG}: restart after NTP time step (${offset}s)" } diff --git a/luci-app-passwall/root/usr/share/passwall/haproxy.lua b/luci-app-passwall/root/usr/share/passwall/haproxy.lua index d6168f06e..a2ce23b6e 100644 --- a/luci-app-passwall/root/usr/share/passwall/haproxy.lua +++ b/luci-app-passwall/root/usr/share/passwall/haproxy.lua @@ -109,20 +109,26 @@ api.uci_foreach_c("haproxy_config", function(t) t.origin_port = server_port if health_check_type == "script_logic" then if server_node.type ~= "Socks" then - local relay_port = server_node.port - local new_port = api.get_new_port() - local config_file = string.format("%s_%s.json", t[".name"], new_port) - sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null', - appname, - string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s", - new_port, --flag - server_node[".name"], --node - "127.0.0.1", --bind - new_port, --socks port - config_file --config file + local new_port + local cache = api.get_socks_port_by_cache(server_node[".name"]) + if cache then + new_port = cache + else + new_port = api.get_new_port() + local config_file = string.format("%s_%s.json", t[".name"], new_port) + sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null', + appname, + string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s", + new_port, --flag + server_node[".name"], --node + "127.0.0.1", --bind + new_port, --socks port + config_file --config file + ) ) ) - ) + api.set_socks_port_to_cache(server_node[".name"], new_port) + end server_address = "127.0.0.1" server_port = new_port end diff --git a/luci-app-passwall/root/usr/share/passwall/nftables.sh b/luci-app-passwall/root/usr/share/passwall/nftables.sh index 26630d037..26c0c5342 100755 --- a/luci-app-passwall/root/usr/share/passwall/nftables.sh +++ b/luci-app-passwall/root/usr/share/passwall/nftables.sh @@ -469,16 +469,28 @@ load_acl() { else [ -n "${DIRECT_DNSMASQ_PORT}" ] && dns_redirect=${DIRECT_DNSMASQ_PORT} fi - if [ -n "${dns_redirect}" ]; then - nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\"" - [ "$_ipv4" != "1" ] && nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 meta l4proto udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\"" - nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\"" - [ "$_ipv4" != "1" ] && nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 meta l4proto tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\"" - #nft "add rule $NFTABLE_NAME PSW_DNS ip protocol udp ${_ipt_source} udp dport 53 counter redirect to :${dns_redirect} comment \"$remarks\"" - #nft "add rule $NFTABLE_NAME PSW_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter redirect to :${dns_redirect} comment \"$remarks\"" - nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter redirect to :${dns_redirect} comment \"$remarks\"" - nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter redirect to :${dns_redirect} comment \"$remarks\"" + + if ([ -n "$tcp_port" ] || [ -n "$udp_port" ]) && [ -n "$dns_redirect" ]; then + if [ "$PROXY_IPV6" = "1" ]; then + nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 meta l4proto udp ${_ipt_source} udp dport 53 counter accept" + nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 meta l4proto tcp ${_ipt_source} tcp dport 53 counter accept" + nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter redirect to :$dns_redirect comment \"$remarks\"" + nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter redirect to :$dns_redirect comment \"$remarks\"" + else + nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol udp ${_ipt_source} udp dport 53 counter accept" + nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol tcp ${_ipt_source} tcp dport 53 counter accept" + nft "add rule $NFTABLE_NAME PSW_DNS ip protocol udp ${_ipt_source} udp dport 53 counter redirect to :$dns_redirect comment \"$remarks\"" + nft "add rule $NFTABLE_NAME PSW_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter redirect to :$dns_redirect comment \"$remarks\"" + fi [ -z "$(get_cache_var "ACL_${sid}_default")" ] && echolog " - ${msg}节点不同于全局配置,DNS 重定向到专用服务器[${dns_redirect}]。" + else + if [ "$PROXY_IPV6" = "1" ]; then + nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\"" + nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\"" + else + nft "add rule $NFTABLE_NAME PSW_DNS ip protocol udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\"" + nft "add rule $NFTABLE_NAME PSW_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\"" + fi fi [ -n "$tcp_port" ] || [ -n "$udp_port" ] && { @@ -657,15 +669,26 @@ load_acl() { [ -n "${DIRECT_DNSMASQ_PORT}" ] && DNS_REDIRECT=${DIRECT_DNSMASQ_PORT} fi - if [ -n "${DNS_REDIRECT}" ]; then - nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol udp udp dport 53 counter return comment \"默认\"" - nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 meta l4proto udp udp dport 53 counter return comment \"默认\"" - nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol tcp tcp dport 53 counter return comment \"默认\"" - nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 meta l4proto tcp tcp dport 53 counter return comment \"默认\"" - nft "add rule $NFTABLE_NAME PSW_DNS ip protocol udp udp dport 53 counter redirect to :${DNS_REDIRECT} comment \"默认\"" - nft "add rule $NFTABLE_NAME PSW_DNS ip protocol tcp tcp dport 53 counter redirect to :${DNS_REDIRECT} comment \"默认\"" - nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto udp udp dport 53 counter redirect to :${DNS_REDIRECT} comment \"默认\"" - nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto tcp tcp dport 53 counter redirect to :${DNS_REDIRECT} comment \"默认\"" + if ([ -n "${TCP_PROXY_MODE}" ] || [ -n "${UDP_PROXY_MODE}" ]) && [ -n "$DNS_REDIRECT" ]; then + if [ "$PROXY_IPV6" = "1" ]; then + nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 meta l4proto udp udp dport 53 counter accept" + nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 meta l4proto tcp tcp dport 53 counter accept" + nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto udp udp dport 53 counter redirect to :$DNS_REDIRECT comment \"默认\"" + nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto tcp tcp dport 53 counter redirect to :$DNS_REDIRECT comment \"默认\"" + else + nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol udp udp dport 53 counter accept" + nft "add rule $NFTABLE_NAME PSW_MANGLE ip protocol tcp tcp dport 53 counter accept" + nft "add rule $NFTABLE_NAME PSW_DNS ip protocol udp udp dport 53 counter redirect to :$DNS_REDIRECT comment \"默认\"" + nft "add rule $NFTABLE_NAME PSW_DNS ip protocol tcp tcp dport 53 counter redirect to :$DNS_REDIRECT comment \"默认\"" + fi + else + if [ "$PROXY_IPV6" = "1" ]; then + nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto udp udp dport 53 counter return comment \"默认\"" + nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto tcp tcp dport 53 counter return comment \"默认\"" + else + nft "add rule $NFTABLE_NAME PSW_DNS ip protocol udp udp dport 53 counter return comment \"默认\"" + nft "add rule $NFTABLE_NAME PSW_DNS ip protocol tcp tcp dport 53 counter return comment \"默认\"" + fi fi [ -n "${TCP_PROXY_MODE}" ] || [ -n "${UDP_PROXY_MODE}" ] && { @@ -1310,10 +1333,15 @@ add_firewall_rule() { if [ -n "$NODE" ] && ([ -n "${LOCALHOST_TCP_PROXY_MODE}" ] || [ -n "${LOCALHOST_UDP_PROXY_MODE}" ]); then [ -n "$DNS_REDIRECT_PORT" ] && { - nft "add rule $NFTABLE_NAME nat_output ip protocol udp oif lo udp dport 53 counter redirect to :$DNS_REDIRECT_PORT comment \"PSW_DNS\"" - nft "add rule $NFTABLE_NAME nat_output ip protocol tcp oif lo tcp dport 53 counter redirect to :$DNS_REDIRECT_PORT comment \"PSW_DNS\"" - nft "add rule $NFTABLE_NAME nat_output meta l4proto udp oif lo udp dport 53 counter redirect to :$DNS_REDIRECT_PORT comment \"PSW_DNS\"" - nft "add rule $NFTABLE_NAME nat_output meta l4proto tcp oif lo tcp dport 53 counter redirect to :$DNS_REDIRECT_PORT comment \"PSW_DNS\"" + if [ "$PROXY_IPV6" == "1" ]; then + #nft "add rule $NFTABLE_NAME PSW_OUTPUT_MANGLE_V6 meta l4proto udp udp dport 53 counter accept" + #nft "add rule $NFTABLE_NAME PSW_OUTPUT_MANGLE_V6 meta l4proto tcp tcp dport 53 counter accept" + nft "add rule $NFTABLE_NAME nat_output oif lo meta l4proto udp udp dport 53 counter redirect to :$DNS_REDIRECT_PORT comment \"PSW_DNS\"" + nft "add rule $NFTABLE_NAME nat_output oif lo meta l4proto tcp tcp dport 53 counter redirect to :$DNS_REDIRECT_PORT comment \"PSW_DNS\"" + else + nft "add rule $NFTABLE_NAME nat_output oif lo ip protocol udp udp dport 53 counter redirect to :$DNS_REDIRECT_PORT comment \"PSW_DNS\"" + nft "add rule $NFTABLE_NAME nat_output oif lo ip protocol tcp tcp dport 53 counter redirect to :$DNS_REDIRECT_PORT comment \"PSW_DNS\"" + fi } fi diff --git a/luci-app-passwall/root/usr/share/passwall/socks_auto_switch.sh b/luci-app-passwall/root/usr/share/passwall/socks_auto_switch.sh index a77abf8e6..e062c8930 100755 --- a/luci-app-passwall/root/usr/share/passwall/socks_auto_switch.sh +++ b/luci-app-passwall/root/usr/share/passwall/socks_auto_switch.sh @@ -183,8 +183,6 @@ test_auto_switch() { start() { id=$1 LOCK_FILE=${LOCK_PATH}/${CONFIG}_socks_auto_switch_${id}.lock - LOG_EVENT_FILTER=$(uci -q get "${CONFIG}.global[0].log_event_filter" 2>/dev/null) - LOG_EVENT_CMD=$(uci -q get "${CONFIG}.global[0].log_event_cmd" 2>/dev/null) main_node=$(config_n_get $id node) socks_port=$(config_n_get $id port 0) delay=$(config_n_get $id autoswitch_testing_time 30) diff --git a/luci-app-passwall/root/usr/share/passwall/utils.sh b/luci-app-passwall/root/usr/share/passwall/utils.sh index ac91c3b10..cc8eb5c1a 100755 --- a/luci-app-passwall/root/usr/share/passwall/utils.sh +++ b/luci-app-passwall/root/usr/share/passwall/utils.sh @@ -384,15 +384,25 @@ lua_api() { echo $(lua -e "local api = require 'luci.passwall.api' print(api.${func})") } + +del_cache_var() { + local key="${1}" + [ -n "${key}" ] && [ -f "${TMP_PATH}/var" ] && { + sed -i "/${key}=/d" $TMP_PATH/var >/dev/null 2>&1 + } +} + set_cache_var() { local key="${1}" shift 1 - local val="$@" - [ -n "${key}" ] && [ -n "${val}" ] && { - [ ! -d $TMP_PATH ] && mkdir -p $TMP_PATH - sed -i "/${key}=/d" $TMP_PATH/var >/dev/null 2>&1 - echo "${key}=\"${val}\"" >> $TMP_PATH/var - eval ${key}=\"${val}\" + [ -n "${key}" ] && { + del_cache_var ${key} + local val="$@" + [ -n "${val}" ] && { + [ ! -d $TMP_PATH ] && mkdir -p $TMP_PATH + echo "${key}=\"${val}\"" >> $TMP_PATH/var + eval ${key}=\"${val}\" + } } } @@ -470,25 +480,9 @@ ln_run() { echolog " - 找不到 ${ln_name},无法启动..." return 1 } - [ "${output}" != "/dev/null" ] && [ -n "$(echo "${output}" | grep -E "default|socks_")" ] && [ "${ln_name}" != "chinadns-ng" ] && { - local persist_log_path=$(config_n_get @global[0] persist_log_path) - local sys_log=$(config_n_get @global[0] sys_log "0") - } - if [ -z "$persist_log_path" ] && [ "$sys_log" != "1" ]; then - ${file_func:-echolog " - ${ln_name}"} "$@" >${output} 2>&1 & - else - if [ -n "${persist_log_path}" ]; then - mkdir -p ${persist_log_path} - local log_file=${persist_log_path}/passwall_global_${ln_name}_$(date '+%F').log - echolog "记录到持久性日志文件:${log_file}" - ${file_func:-echolog " - ${ln_name}"} "$@" >> ${log_file} 2>&1 & - sys_log=0 - fi - if [ "${sys_log}" = "1" ]; then - echolog "记录 ${ln_name}_global 到系统日志" - ${file_func:-echolog " - ${ln_name}"} "$@" 2>&1 | logger -t PASSWALL_global_${ln_name} & - fi - fi + + ${file_func:-echolog " - ${ln_name}"} "$@" >${output} 2>&1 & + [ "$NO_REC_PROCESS" = "1" ] && return process_count=$(ls $TMP_SCRIPT_FUNC_PATH | wc -l) process_count=$((process_count + 1)) diff --git a/luci-app-ssr-plus/Makefile b/luci-app-ssr-plus/Makefile index edb32bd59..72bc961e6 100644 --- a/luci-app-ssr-plus/Makefile +++ b/luci-app-ssr-plus/Makefile @@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus LUCI_PKGARCH:=all PKG_NAME:=luci-app-ssr-plus PKG_VERSION:=196 -PKG_RELEASE:=71 +PKG_RELEASE:=72 PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ diff --git a/luci-app-ssr-plus/root/etc/init.d/shadowsocksr b/luci-app-ssr-plus/root/etc/init.d/shadowsocksr index e037f97cf..1b0503278 100755 --- a/luci-app-ssr-plus/root/etc/init.d/shadowsocksr +++ b/luci-app-ssr-plus/root/etc/init.d/shadowsocksr @@ -650,14 +650,14 @@ prepare_clash_runtime_config() { enable: false EOF + # 根据 enable_fake_ip 决定 enhanced-mode + if [ "$enable_fake_ip" = "1" ]; then + ENHANCED_MODE="fake-ip" + else + ENHANCED_MODE="redir-host" + fi # 根据 dns_mode 添加不同的 dns 配置 if [ "$dns_mode" = "7" ]; then - # 根据 enable_fake_ip 决定 enhanced-mode - if [ "$enable_fake_ip" = "1" ]; then - ENHANCED_MODE="fake-ip" - else - ENHANCED_MODE="redir-host" - fi cat >> "$overlay_file" <<-EOF dns: enable: true @@ -668,7 +668,9 @@ prepare_clash_runtime_config() { else cat >> "$overlay_file" <<-EOF dns: - enable: false + enable: true + enhanced-mode: $ENHANCED_MODE + ipv6: $( [ "$dns_ipv4_only" = "1" ] && echo "false" || echo "true" ) EOF fi @@ -681,7 +683,7 @@ prepare_clash_runtime_config() { if [ "$socks5_auth" = "password" ]; then if [ -z "$socks5_user" ] || [ -z "$socks5_pass" ]; then - echolog "警告:SOCKS5 代理未完整配置用户名或密码,已自动降级为无认证模式 (noauth)。" + echolog "警告:SOCKS5 代理未完整配置用户名或密码,已自动降级为无认证模式 (noauth)!" socks5_auth="noauth" fi fi diff --git a/luci-app-ssr-plus/root/usr/share/shadowsocksr/clash_yaml.lua b/luci-app-ssr-plus/root/usr/share/shadowsocksr/clash_yaml.lua index fcb9853da..2e7747dea 100644 --- a/luci-app-ssr-plus/root/usr/share/shadowsocksr/clash_yaml.lua +++ b/luci-app-ssr-plus/root/usr/share/shadowsocksr/clash_yaml.lua @@ -653,17 +653,14 @@ local function build_dns_upstreams() } end -local function build_dns_section(dns_mode, user_dns, is_external_dns) +local function build_dns_section(dns_mode, user_dns) local result local has_user_dns = type(user_dns) == "table" and next(user_dns) + dns_mode = tostring(dns_mode or "0") if not has_user_dns then - if is_external_dns then - return { enable = false } - end result = { - enable = true, - listen = "127.0.0.1:5335" + enable = true } else result = clone_table(user_dns) @@ -749,7 +746,7 @@ local function build_dns_section(dns_mode, user_dns, is_external_dns) for k, v in pairs(upstreams) do if k == "proxy-server-nameserver" then result[k] = v - elseif result[k] == nil then + elseif result[k] == nil and not (k == "respect-rules" and enable_fake_ip ~= "1") then result[k] = v end end @@ -862,7 +859,8 @@ local function apply_sniffer_config(doc, enable_fake_ip) ["override-destination"] = true, sniff = { HTTP = { - ports = { 80, 2052, 2082, 2086, 2095, "8080-8880" } + ports = { 80, 2052, 2082, 2086, 2095, "8080-8880" }, + ["override-destination"] = true }, TLS = { ports = { 443, 2053, 2083, 2087, 2096, 8443 } @@ -1603,8 +1601,6 @@ end local function build_single_proxy_runtime_doc(proxy, local_port, socks_port, mode) local listen_port = tonumber(local_port) local socks_listen = tonumber(socks_port) - local mode_str = tostring(dns_mode or "") - local is_ext_dns = (mode_str ~= "7") local doc = { ["allow-lan"] = true, @@ -1629,8 +1625,9 @@ local function build_single_proxy_runtime_doc(proxy, local_port, socks_port, mod ["store-selected"] = true, ["store-fake-ip"] = true }, - dns = build_dns_section(dns_mode, nil, is_ext_dns) + dns = build_dns_section(dns_mode, nil) } + apply_sniffer_config(doc, enable_fake_ip) if mode == "socks" then doc["socks-port"] = listen_port @@ -1641,6 +1638,23 @@ local function build_single_proxy_runtime_doc(proxy, local_port, socks_port, mod doc["socks-port"] = socks_listen end end + + if doc["socks-port"] and doc["socks-port"] > 0 then + local socks5_auth = uci:get_first("shadowsocksr", "socks5_proxy", "socks5_auth", "noauth") + if socks5_auth == "password" then + local socks5_user = uci:get_first("shadowsocksr", "socks5_proxy", "socks5_user", "") + local socks5_pass = uci:get_first("shadowsocksr", "socks5_proxy", "socks5_pass", "") + + if socks5_user == "" or socks5_pass == "" then + io.stderr:write("警告:SOCKS5 代理未完整配置用户名或密码,已自动降级为无认证模式 (noauth)!\n") + else + doc["authentication"] = { + string.format("%s:%s", socks5_user, socks5_pass) + } + end + end + end + return doc end @@ -1650,8 +1664,6 @@ local function build_tuic_runtime_doc(sid, local_port, socks_port, mode) local tuic_ip = get_server_field(sid, "tuic_ip", "") local tls_host = get_server_field(sid, "tls_host", "") local ipstack_prefer = get_server_field(sid, "ipstack_prefer", "") - local mode_str = tostring(dns_mode or "") - local is_ext_dns = (mode_str ~= "7") local proxy = { name = sid, @@ -1724,8 +1736,9 @@ local function build_tuic_runtime_doc(sid, local_port, socks_port, mode) ["store-selected"] = true, ["store-fake-ip"] = true }, - dns = build_dns_section(dns_mode, nil, is_ext_dns) + dns = build_dns_section(dns_mode, nil) } + apply_sniffer_config(doc, enable_fake_ip) if mode == "socks" then doc["socks-port"] = listen_port @@ -1737,6 +1750,22 @@ local function build_tuic_runtime_doc(sid, local_port, socks_port, mode) end end + if doc["socks-port"] and doc["socks-port"] > 0 then + local socks5_auth = uci:get_first("shadowsocksr", "socks5_proxy", "socks5_auth", "noauth") + if socks5_auth == "password" then + local socks5_user = uci:get_first("shadowsocksr", "socks5_proxy", "socks5_user", "") + local socks5_pass = uci:get_first("shadowsocksr", "socks5_proxy", "socks5_pass", "") + + if socks5_user == "" or socks5_pass == "" then + io.stderr:write("警告:SOCKS5 代理未完整配置用户名或密码,已自动降级为无认证模式 (noauth)!\n") + else + doc["authentication"] = { + string.format("%s:%s", socks5_user, socks5_pass) + } + end + end + end + return doc end @@ -1745,8 +1774,7 @@ local function build_shadowsocks_runtime_doc(sid, local_port, socks_port, mode) local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0 local method = get_server_field(sid, "encrypt_method_ss", "none") local password = get_server_field(sid, "password", "") - local mode_str = tostring(dns_mode or "") - local is_ext_dns = (mode_str ~= "7") + local proxy = { name = sid, type = "ss", @@ -1788,8 +1816,9 @@ local function build_shadowsocks_runtime_doc(sid, local_port, socks_port, mode) ["store-selected"] = true, ["store-fake-ip"] = true }, - dns = build_dns_section(dns_mode, nil, is_ext_dns) + dns = build_dns_section(dns_mode, nil) } + apply_sniffer_config(doc, enable_fake_ip) local listen_port = tonumber(local_port) local socks_listen = tonumber(socks_port) @@ -1803,6 +1832,22 @@ local function build_shadowsocks_runtime_doc(sid, local_port, socks_port, mode) end end + if doc["socks-port"] and doc["socks-port"] > 0 then + local socks5_auth = uci:get_first("shadowsocksr", "socks5_proxy", "socks5_auth", "noauth") + if socks5_auth == "password" then + local socks5_user = uci:get_first("shadowsocksr", "socks5_proxy", "socks5_user", "") + local socks5_pass = uci:get_first("shadowsocksr", "socks5_proxy", "socks5_pass", "") + + if socks5_user == "" or socks5_pass == "" then + io.stderr:write("警告:SOCKS5 代理未完整配置用户名或密码,已自动降级为无认证模式 (noauth)!\n") + else + doc["authentication"] = { + string.format("%s:%s", socks5_user, socks5_pass) + } + end + end + end + return doc end @@ -1987,11 +2032,8 @@ local function prepare(input_path, output_path) strip_runtime_conflicts(doc) local filled_groups = fill_empty_proxy_groups(doc) local stripped_rules = strip_incompatible_script_rules(doc) - - local dns_config = build_dns_section(dns_mode, user_dns, false) - if dns_config and next(dns_config) then - doc.dns = dns_config - end + + doc.dns = build_dns_section(dns_mode, user_dns) doc.rules = merge_rules_with_direct(doc.rules) apply_sniffer_config(doc, enable_fake_ip) @@ -2002,6 +2044,7 @@ local function prepare(input_path, output_path) if doc["tcp-concurrent"] == nil then doc["tcp-concurrent"] = true end + if doc["find-process-mode"] == nil then doc["find-process-mode"] = "off" end @@ -2035,21 +2078,13 @@ local function merge(raw_path, overlay_path, output_path) strip_runtime_conflicts(raw_doc) local filled_groups = fill_empty_proxy_groups(raw_doc) local stripped_rules = strip_incompatible_script_rules(raw_doc) - - if user_dns then - if dns_mode ~= "7" then - overlay_doc.dns = nil - end - end - local merged = deep_merge(raw_doc, overlay_doc) - if user_dns then - merged.dns = build_dns_section(dns_mode, user_dns, false) - elseif type(merged.dns) == "table" and next(merged.dns) then - merged.dns = build_dns_section(dns_mode, merged.dns, false) - else - merged.dns = build_dns_section(dns_mode, nil, false) + + local target_dns = user_dns + if not target_dns and type(merged.dns) == "table" and next(merged.dns) then + target_dns = merged.dns end + merged.dns = build_dns_section(dns_mode, target_dns) merged.rules = merge_rules_with_direct(merged.rules) apply_sniffer_config(merged, enable_fake_ip) @@ -2060,6 +2095,7 @@ local function merge(raw_path, overlay_path, output_path) if merged["tcp-concurrent"] == nil then merged["tcp-concurrent"] = true end + if merged["find-process-mode"] == nil then merged["find-process-mode"] = "off" end diff --git a/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua b/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua index 7f33052c6..4433ae401 100755 --- a/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua +++ b/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua @@ -1714,7 +1714,7 @@ local function processData(szType, content, cfgid) result.quic_security = params.quicSecurity or "none" result.quic_key = params.key elseif result.transport == "grpc" then - result.serviceName = params.serviceName + result.serviceName = params.servicename result.grpc_mode = params.mode or "gun" elseif result.transport == "tcp" or result.transport == "raw" then result.tcp_guise = params.headerType and params.headerType ~= "" and params.headerType or "none" diff --git a/luci-app-systools/Makefile b/luci-app-systools/Makefile index cc4a0ccef..0b618d113 100644 --- a/luci-app-systools/Makefile +++ b/luci-app-systools/Makefile @@ -2,12 +2,12 @@ include $(TOPDIR)/rules.mk -PKG_VERSION:=1.1.0-r1 -PKG_RELEASE:=3 +PKG_VERSION:=1.1.1-r1 +PKG_RELEASE:=4 LUCI_TITLE:=LuCI support for SysTools LUCI_PKGARCH:=all -LUCI_DEPENDS:=+lsblk +luci-lib-iform +luci-lib-taskd +speedtestcli +LUCI_DEPENDS:=+attr +lsblk +luci-lib-iform +luci-lib-taskd +speedtestcli LUCI_EXTRA_DEPENDS:=luci-lib-iform (>=1.1) include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-systools/root/usr/share/systools/reset_rom_pkgs.run b/luci-app-systools/root/usr/share/systools/reset_rom_pkgs.run index cf25497de..0ead4204b 100755 --- a/luci-app-systools/root/usr/share/systools/reset_rom_pkgs.run +++ b/luci-app-systools/root/usr/share/systools/reset_rom_pkgs.run @@ -1,5 +1,10 @@ #!/bin/sh +[ -d "/overlay/upper/etc" ] || { + echo "Only squashfs rom supported, and /overlay must be mounted." >&2 + exit 1 +} + APK=false PKG_BASE=usr/lib/opkg/info PKG_EXT=control @@ -35,7 +40,26 @@ reset_pkg() { if [ -f /rom/$PKG_BASE/${pkgname}.list ]; then nonconf_files_of_pkg "${pkgname}" | while read; do line="$REPLY" - rm -f "/overlay/upper/${line}" "/ext_overlay/upper/${line}" + if [ -e "${line}" -o -e "/overlay/upper/${line}" -o -e "/ext_overlay/upper/${line}" ]; then + rm -f "/overlay/upper/${line}" "/ext_overlay/upper/${line}" + else + local dir="$(dirname ${line})" + [ -z "$dir" -o "$dir" = "/" -o "$dir" = "." -o "$line" = "$dir" ] || echo "$dir" >> /tmp/.reset_dirs + fi + done + if [ -s /rom/$PKG_BASE/${pkgname}.conffiles ]; then + # restore config files from rom, if they are not exist in working rootfs + while read; do + line="$REPLY" + [ -e "${line}" ] && continue + rm -rf "/overlay/upper/${line}" "/ext_overlay/upper/${line}" + done < /rom/$PKG_BASE/${pkgname}.conffiles + fi + # restore auto start + grep '^/etc/init\.d/' /rom/$PKG_BASE/${pkgname}.list | while read; do + line="$REPLY" + line="$(basename ${line})" + rm -f "/overlay/upper/etc/rc.d/"*"${line}" "/ext_overlay/upper/etc/rc.d/"*"${line}" done rm -f /overlay/upper/$PKG_BASE/${pkgname}.list /ext_overlay/upper/$PKG_BASE/${pkgname}.list fi @@ -48,10 +72,32 @@ ls_all_rom_pkgs() { reset_all_rom() { local line + rm -f /tmp/.reset_dirs ls_all_rom_pkgs | while read; do line="$REPLY" reset_pkg "$line" done + if [ -s /tmp/.reset_dirs ]; then + echo "reset overlayfs opaque flags..." + sort -u /tmp/.reset_dirs | while read; do + local line="$REPLY" + echo "reset dir $line and its parents" + while :; do + dir="$line" + line="$(dirname ${dir})" + [ -z "$dir" -o "$dir" = "/" -o "$dir" = "." ] && break + if [ -e "/overlay/upper/${dir}" -o -e "/ext_overlay/upper/${dir}" ]; then + if [ -d "/overlay/upper/${dir}" -o -d "/ext_overlay/upper/${dir}" ]; then + setfattr -x "trusted.overlay.opaque" -h "/overlay/upper/${dir}" "/ext_overlay/upper/${dir}" 2>/dev/null + else + rm -f "/overlay/upper/${dir}" "/ext_overlay/upper/${dir}" + fi + fi + [ "$line" = "$dir" ] && break + done + done + fi + rm -f /tmp/.reset_dirs } reset_all_rom diff --git a/luci-base/htdocs/luci-static/resources/network.js b/luci-base/htdocs/luci-static/resources/network.js index 90d3f2001..6d7c100ac 100644 --- a/luci-base/htdocs/luci-static/resources/network.js +++ b/luci-base/htdocs/luci-static/resources/network.js @@ -362,7 +362,9 @@ function refreshWirelessState() { const wifiDevices = uci.sections('wireless', 'wifi-device'); const configOnly = wifiDevices.length > 0 && wifiDevices.every(function(device) { - return (device.type == 'mt_dbdc' || device.type == 'qcawifi' || device.type == 'qcawificfg80211'); + // Legacy MT7615 DBDC uses ra0/rax0 radio sections; MT798x uses ra/rax. + return ((device.type == 'mt_dbdc' && /^ra[xiyez]?0$/.test(device['.name'])) || + device.type == 'qcawifi' || device.type == 'qcawificfg80211'); }); if (configOnly) @@ -408,7 +410,9 @@ function refreshWirelessState() { function waitForWirelessState() { const wifiDevices = uci.sections('wireless', 'wifi-device'); const hasConfigOnlyWifi = wifiDevices.some(function(device) { - return (device.type == 'mt_dbdc' || device.type == 'qcawifi' || device.type == 'qcawificfg80211'); + // Legacy MT7615 DBDC uses ra0/rax0 radio sections; MT798x uses ra/rax. + return ((device.type == 'mt_dbdc' && /^ra[xiyez]?0$/.test(device['.name'])) || + device.type == 'qcawifi' || device.type == 'qcawificfg80211'); }); const refresh = refreshWirelessState(); diff --git a/luci-base/po/templates/base.pot b/luci-base/po/templates/base.pot index 165e9175b..9266a9e7d 100644 --- a/luci-base/po/templates/base.pot +++ b/luci-base/po/templates/base.pot @@ -9214,6 +9214,14 @@ msgctxt "nft redirect" msgid "Redirect to local system" msgstr "" +#: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js +msgid "Redirect LAN NTP requests to this device." +msgstr "" + +#: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js +msgid "Redirect NTP requests" +msgstr "" + #: modules/luci-mod-status/htdocs/luci-static/resources/view/status/iptables.js:57 msgid "References" msgstr "" @@ -13671,3 +13679,24 @@ msgstr "" #: modules/luci-compat/luasrc/view/cbi/delegator.htm:20 msgid "« Back" msgstr "" + +#: modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/30_network.js +msgid "Total download bandwidth" +msgstr "" + +#: modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/30_network.js +msgid "Total upload bandwidth" +msgstr "" + +#: modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js +msgid "Total traffic" +msgstr "" + +msgid "Online" +msgstr "" + +msgid "Offline" +msgstr "" + +msgid "Online Hosts" +msgstr "" diff --git a/luci-base/po/zh_Hans/base.po b/luci-base/po/zh_Hans/base.po index 9775a833d..587b4e8b2 100644 --- a/luci-base/po/zh_Hans/base.po +++ b/luci-base/po/zh_Hans/base.po @@ -9484,6 +9484,14 @@ msgctxt "nft redirect" msgid "Redirect to local system" msgstr "重定向到本地系统" +#: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js +msgid "Redirect LAN NTP requests to this device." +msgstr "将局域网 NTP 请求重定向到本机。" + +#: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js +msgid "Redirect NTP requests" +msgstr "重定向局域网 NTP 请求" + #: modules/luci-mod-status/htdocs/luci-static/resources/view/status/iptables.js:57 msgid "References" msgstr "引用" @@ -14138,3 +14146,24 @@ msgstr "移远蜂窝网络" msgid "Temperature" msgstr "温度" + +#: modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/30_network.js +msgid "Total download bandwidth" +msgstr "下载总带宽" + +#: modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/30_network.js +msgid "Total upload bandwidth" +msgstr "上传总带宽" + +#: modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js +msgid "Total traffic" +msgstr "总流量" + +msgid "Online" +msgstr "在线" + +msgid "Offline" +msgstr "离线" + +msgid "Online Hosts" +msgstr "在线主机" diff --git a/luci-base/po/zh_Hant/base.po b/luci-base/po/zh_Hant/base.po index 6e2145811..5d7f7c2d8 100644 --- a/luci-base/po/zh_Hant/base.po +++ b/luci-base/po/zh_Hant/base.po @@ -9393,6 +9393,14 @@ msgctxt "nft redirect" msgid "Redirect to local system" msgstr "重新導向至本地系統" +#: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js +msgid "Redirect LAN NTP requests to this device." +msgstr "將區域網路 NTP 請求重新導向至本機。" + +#: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js +msgid "Redirect NTP requests" +msgstr "重新導向區域網路 NTP 請求" + #: modules/luci-mod-status/htdocs/luci-static/resources/view/status/iptables.js:57 msgid "References" msgstr "引用" @@ -13993,3 +14001,24 @@ msgstr "{example_nx} 返回 {nxdomain}。" #: modules/luci-compat/luasrc/view/cbi/delegator.htm:20 msgid "« Back" msgstr "« 返回" + +#: modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/30_network.js +msgid "Total download bandwidth" +msgstr "下載總頻寬" + +#: modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/30_network.js +msgid "Total upload bandwidth" +msgstr "上傳總頻寬" + +#: modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js +msgid "Total traffic" +msgstr "總流量" + +msgid "Online" +msgstr "在線" + +msgid "Offline" +msgstr "離線" + +msgid "Online Hosts" +msgstr "在線主機" diff --git a/luci-base/root/usr/share/rpcd/ucode/luci b/luci-base/root/usr/share/rpcd/ucode/luci index a4d0f980a..a6e982ecb 100644 --- a/luci-base/root/usr/share/rpcd/ucode/luci +++ b/luci-base/root/usr/share/rpcd/ucode/luci @@ -260,6 +260,7 @@ const methods = { apk: access('/usr/bin/apk') == true, wifi: access('/sbin/wifi') == true, ufpd: access('/usr/sbin/ufpd') == true, + oui: access('/www/luci-static/resources/oui/oui.js') == true, vrf: access('/sys/module/vrf/refcnt') == true, // vrf.ko is loaded netifd_vrf: false, mptcp: access('/proc/sys/net/mptcp/enabled'), @@ -298,7 +299,7 @@ const methods = { let fd = popen('dnsmasq --version 2>/dev/null'); if (fd) { - const m = match(fd.read('all'), /^Compile time options: (.+)$/s); + const m = match(fd.read('all'), /Compile time options: ([^\n]+)/); for (let opt in split(m?.[1], ' ')) { let f = replace(opt, 'no-', '', 1); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js b/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js index 68f7e5edc..32e4ddeec 100644 --- a/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js @@ -89,8 +89,11 @@ function getMtDbdcStaIfname(device) { return ifname ? 'apcli0' : null; } -function isConfigOnlyWifiHwtype(hwtype) { - return (hwtype == 'mt_dbdc' || isQcaWifiHwtype(hwtype)); +function isConfigOnlyWifiDevice(device) { + const hwtype = uci.get('wireless', device, 'type'); + + // Only legacy MT7615 DBDC radios lack usable runtime wireless RPCs. + return ((hwtype == 'mt_dbdc' && /^ra[xiyez]?0$/.test(device)) || isQcaWifiHwtype(hwtype)); } function buildIwinfoDeviceLookup(devices) { @@ -107,13 +110,13 @@ function getIwinfoDevicesFromConfig() { const radios = uci.sections('wireless', 'wifi-device'); const devices = []; - if (!radios.length || !radios.every((radio) => isConfigOnlyWifiHwtype(radio.type))) + if (!radios.length || !radios.every((radio) => isConfigOnlyWifiDevice(radio['.name']))) return null; for (const iface of uci.sections('wireless', 'wifi-iface')) { const hwtype = uci.get('wireless', iface.device, 'type'); - if (isConfigWifiIfaceDisabled(iface) || !isConfigOnlyWifiHwtype(hwtype)) + if (isConfigWifiIfaceDisabled(iface) || !isConfigOnlyWifiDevice(iface.device)) continue; const fallback = isQcaWifiHwtype(hwtype) @@ -2616,7 +2619,7 @@ return view.extend({ return a.getName() > b.getName(); }); const hasConfigOnlyWifi = configuredRadios.some(function(radio) { - return isConfigOnlyWifiHwtype(uci.get('wireless', radio.getName(), 'type')); + return isConfigOnlyWifiDevice(radio.getName()); }); if (hasConfigOnlyWifi) { diff --git a/luci-mod-status/Makefile b/luci-mod-status/Makefile index dd33cb7a3..6dfd296b0 100644 --- a/luci-mod-status/Makefile +++ b/luci-mod-status/Makefile @@ -7,9 +7,9 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI Status Pages -LUCI_DEPENDS:=+luci-base +libiwinfo +rpcd-mod-iwinfo +LUCI_DEPENDS:=+luci-base +libiwinfo +rpcd-mod-iwinfo +ucode +ucode-mod-fs +ucode-mod-ubus -PKG_RELEASE:=5 +PKG_RELEASE:=6 PKG_BUILD_DEPENDS:=iwinfo PKG_LICENSE:=Apache-2.0 diff --git a/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js b/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js index d616bc5e0..9b35bf1e8 100644 --- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js +++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js @@ -1,6 +1,7 @@ 'use strict'; 'require baseclass'; 'require rpc'; +'require uci'; var callGetUnixtime = rpc.declare({ object: 'luci', @@ -33,9 +34,9 @@ var callCPUInfo = rpc.declare({ method: 'getCPUInfo' }); -var callTempInfo = rpc.declare({ +var callCPUUsage = rpc.declare({ object: 'luci', - method: 'getTempInfo' + method: 'getCPUUsage' }); return baseclass.extend({ @@ -48,9 +49,10 @@ return baseclass.extend({ L.resolveDefault(callSystemInfo(), {}), L.resolveDefault(callCPUBench(), {}), L.resolveDefault(callCPUInfo(), {}), - L.resolveDefault(callTempInfo(), {}), + L.resolveDefault(callCPUUsage(), {}), L.resolveDefault(callLuciVersion(), { revision: _('unknown version'), branch: 'LuCI' }), - L.resolveDefault(callGetUnixtime(), 0) + L.resolveDefault(callGetUnixtime(), 0), + uci.load('system') ]); }, @@ -59,7 +61,7 @@ return baseclass.extend({ systeminfo = data[1], cpubench = data[2], cpuinfo = data[3], - tempinfo = data[4], + cpuusage = data[4], luciversion = data[5], unixtime = data[6]; @@ -68,11 +70,16 @@ return baseclass.extend({ var datestr = null; if (unixtime) { - var date = new Date(unixtime * 1000); + var date = new Date(unixtime * 1000), + zn = uci.get('system', '@system[0]', 'zonename')?.replaceAll(' ', '_') || 'UTC', + ts = uci.get('system', '@system[0]', 'clock_timestyle') || 0, + hc = uci.get('system', '@system[0]', 'clock_hourcycle') || 0; datestr = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', - timeStyle: 'long' + timeStyle: (ts == 0) ? 'long' : 'full', + hourCycle: (hc == 0) ? undefined : hc, + timeZone: zn }).format(date); } @@ -81,6 +88,11 @@ return baseclass.extend({ _('Architecture'), (cpuinfo.cpuinfo || boardinfo.system) + ' ' + cpubench.cpubench, _('Target Platform'), (L.isObject(boardinfo.release) ? boardinfo.release.target : ''), _('Firmware Version'), (L.isObject(boardinfo.release) ? boardinfo.release.description + ' / ' : '') + (luciversion || ''), + ? '%s%s / '.format( + boardinfo.release.description || '', + boardinfo.release.revision ? boardinfo.release.revision : '' + ) + : '') + (luciversion || ''), _('Kernel Version'), boardinfo.kernel, _('Local Time'), datestr, _('Uptime'), systeminfo.uptime ? '%t'.format(systeminfo.uptime) : null, @@ -91,7 +103,7 @@ return baseclass.extend({ ) : null ]; - if (tempinfo.tempinfo) { + if (cpuusage.tempinfo) { fields.splice(6, 0, _('Temperature')); fields.splice(7, 0, tempinfo.tempinfo); } diff --git a/luci-mod-status/htdocs/luci-static/resources/view/status/include/20_memory.js b/luci-mod-status/htdocs/luci-static/resources/view/status/include/20_memory.js index cd2f7c635..f65006fb3 100644 --- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/20_memory.js +++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/20_memory.js @@ -31,12 +31,13 @@ return baseclass.extend({ var mem = L.isObject(systeminfo.memory) ? systeminfo.memory : {}, swap = L.isObject(systeminfo.swap) ? systeminfo.swap : {}; - var fields = [ - _('Used'), (mem.total && mem.available) ? (mem.total - mem.free - mem.buffered - mem.cached) : null, mem.total, - ]; + var available = (mem.available != null) ? mem.available : mem.free; + if (available != null) + available = Math.min(mem.total ?? Infinity, Math.max(0, available + (mem.buffered || 0))); - if (mem.buffered) - fields.push(_('Buffered'), mem.buffered, mem.total); + var fields = [ + _('Used'), (mem.total && mem.available) ? (mem.total - available): null, mem.total, + ]; if (mem.cached) fields.push(_('Cached'), mem.cached, mem.total); diff --git a/luci-mod-status/htdocs/luci-static/resources/view/status/include/30_network.js b/luci-mod-status/htdocs/luci-static/resources/view/status/include/30_network.js index f5f86e97c..b5f84a4fd 100644 --- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/30_network.js +++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/30_network.js @@ -83,6 +83,68 @@ function renderbox(ifc, ipv6, dhcpv6_stats) { return baseclass.extend({ title: _('Network'), + wanCapacity(networks) { + const lower = new Map(), ports = new Map(); + for (const net of networks) { + const l3 = net.getL3Device(), l2 = net.getL2Device(); + if (l3 && l2 && l3.getName() != l2.getName()) + lower.set(l3.getName(), l2); + } + for (const net of networks) { + let dev = net.getL2Device() || net.getL3Device(); + const seen = new Set(); + while (dev && !seen.has(dev.getName())) { + seen.add(dev.getName()); + // Follow PPPoE and VLAN layers, but retain individual DSA ports. + const parent = lower.get(dev.getName()) || (dev.getType() == 'vlan' ? dev.getParent() : null); + if (!parent || seen.has(parent.getName())) + break; + dev = parent; + } + if (dev) { + const speed = Number(dev.getSpeed()); + ports.set(dev.getName(), Number.isFinite(speed) && speed > 0 && speed < 0xffffffff ? speed : 1000); + } + } + return Array.from(ports.values()).reduce((sum, speed) => sum + speed, 0) || 1000; + }, + + bandwidthBar(value, capacity) { + if (value != null) + value = Number.isFinite(value) ? Math.max(0, Math.min(value, capacity)) : null; + const percent = value != null ? Math.max(0, value / capacity * 100) : 0; + return E('div', { + 'class': 'cbi-progressbar', + 'title': value != null + ? '%.2f Mbps / %s Gbps (%.1f%%)'.format(value, capacity / 1000, percent) + : '- / %s Gbps'.format(capacity / 1000) + }, E('div', { 'style': 'width:%.2f%%'.format(Math.min(100, percent)) })); + }, + + wanRates(networks, now) { + const previous = this.wanSamples || new Map(); + const samples = new Map(); + let download = 0, upload = 0, ready = true; + for (const net of networks) { + const dev = net.getL3Device(); + if (!dev || samples.has(dev.getName())) + continue; + const name = dev.getName(); + const sample = { rx: dev.getRXBytes(), tx: dev.getTXBytes(), time: now }; + const old = previous.get(name); + samples.set(name, sample); + if (!old || now <= old.time || sample.rx < old.rx || sample.tx < old.tx) { + ready = false; + continue; + } + // bytes / milliseconds * 8 / 1000 = decimal Mbps. + download += (sample.rx - old.rx) * 8 / (now - old.time) / 1000; + upload += (sample.tx - old.tx) * 8 / (now - old.time) / 1000; + } + this.wanSamples = samples; + return ready && samples.size ? [download, upload] : [null, null]; + }, + load() { return Promise.all([ fs.trimmed('/proc/sys/net/netfilter/nf_conntrack_count'), @@ -96,15 +158,21 @@ return baseclass.extend({ render([ct_count, ct_max, wan_nets, wan6_nets, dhcpv6_stats, onlineusers]) { + const networks = [...wan_nets, ...wan6_nets]; + const [download, upload] = this.wanRates(networks, performance.now()); + const capacity = this.wanCapacity(networks); + const fields = [ { label: _('Active Connections'), value: ct_max ? ct_count : null }, - { label: _('Online Users'), value: onlineusers ? onlineusers.onlineusers : null } + { label: _('Online Users'), value: onlineusers ? onlineusers.onlineusers : null }, + { label: _('Total download bandwidth'), value: this.bandwidthBar(download, capacity) }, + { label: _('Total upload bandwidth'), value: this.bandwidthBar(upload, capacity) } ]; const ctstatus = E('table', { 'class': 'table' }); for (const { label, value } of fields) { - if (label == _('Online Users')) { + if (label != _('Active Connections')) { ctstatus.appendChild(E('tr', { 'class': 'tr' }, [ E('td', { 'class': 'td left', 'width': '33%' }, [ label ]), E('td', { 'class': 'td left' }, [ diff --git a/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js b/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js index c07f8898d..36c6608c1 100644 --- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js +++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js @@ -1,9 +1,11 @@ 'use strict'; 'require baseclass'; 'require rpc'; +'require fs'; 'require uci'; 'require network'; 'require validation'; +'require poll'; const callLuciDHCPLeases = rpc.declare({ object: 'luci-rpc', @@ -11,32 +13,217 @@ const callLuciDHCPLeases = rpc.declare({ expect: { '': {} } }); +const callClientRates = rpc.declare({ + object: 'luci.client-rates', + method: 'get', + params: [ 'addresses' ], + expect: { '': {} } +}); + +const callClientWeb = rpc.declare({ + object: 'luci.client-web', + method: 'get', + expect: { '': {} } +}); + const callUfpList = rpc.declare({ object: 'fingerprint', method: 'fingerprint', }); return baseclass.extend({ - title: _('DHCP Leases'), + title: _('Online Hosts'), deferFirstLoad: true, disableCache: true, + // Optional enhancement: never load the helper or query OUIs without the package. + renderHostname(host, mac, fnos) { + const node = E('span', { 'style': 'display:block;text-align:left' }, [ document.createTextNode(host || '-') ]); + if (!L.hasSystemFeature('oui')) + return node; + if (!this.ouiLoader) { + this.ouiLoader = Promise.all([L.resolveDefault(uci.load('oui')), new Promise(function(resolve) { + const script = document.createElement('script'); + script.src = L.resource('oui/oui.js') + '?v=7'; + script.onload = function() { resolve(window.luciOUI); }; + script.onerror = function() { resolve(null); }; + document.head.appendChild(script); + })]).then(function(results) { + if (results[1]) + results[1].setDevices(uci.sections('oui', 'device')); + return results[1]; + }); + } + this.ouiLoader.then(function(oui) { + if (oui) + oui.decorate(node, mac, fnos); + }); + return node; + }, + isMACStatic: {}, isDUIDStatic: {}, isDUIDIAIDStatic: {}, + normalizeRateAddress(ip) { + ip = ip.replace(/\/\d+$/, ''); + const v4 = validation.parseIPv4(ip); + if (v4) + return v4.join('.'); + const v6 = validation.parseIPv6(ip); + return v6 ? v6.map(word => word.toString(16)).join(':') : null; + }, + + clientAddresses(lease, hints) { + if (lease.rateAddresses) + return lease.rateAddresses; + const host = hints.hosts?.[lease.macaddr?.toUpperCase()] || {}; + return Array.from(new Set([ + lease.ipaddr, ...L.toArray(lease.ipaddrs), lease.ip6addr, ...L.toArray(lease.ip6addrs), + ...L.toArray(host.ipaddrs || host.ipv4), ...L.toArray(host.ip6addrs || host.ipv6) + ].filter(Boolean).map(ip => this.normalizeRateAddress(ip)).filter(Boolean))); + }, + + renderRate(lease, hints, data, direction) { + return this.rateValue(this.clientAddresses(lease, hints), data, direction); + }, + + rateValue(addresses, data, direction, mac) { + if (direction == 'total') { + const value = data?.totals?.[mac?.toUpperCase()]; + return value != null ? [Number(value), '%1024.2mB'.format(value)] : [-1, '-']; + } + let total = 0; + for (const ip of addresses) { + const rate = data?.rates?.[ip]; + if (!rate?.ready || (mac && rate.mac && rate.mac != mac.toUpperCase())) + return [ -1, '-' ]; + total += Number(rate[direction] || 0); + } + return addresses.length ? [ total, '%1024.1mB/s'.format(total) ] : [ -1, '-' ]; + }, + + rateCell(lease, hints, direction) { + const addresses = this.clientAddresses(lease, hints); + const value = this.rateValue(addresses, this.rateData, direction, lease.macaddr); + return [ value[0], E('span', { + 'class': 'luci-client-rate', + 'data-addresses': JSON.stringify(addresses), + 'data-direction': direction, + 'data-mac': lease.macaddr || '' + }, value[1]) ]; + }, + + isFnosClient(client, data) { + return (client.activeAddresses || []).some(address => { + const ip = validation.parseIPv4(address)?.join('.'); + const probe = data?.clients?.[ip]; + return !!(ip && client.macaddr && probe?.ready && + probe.mac === client.macaddr.toUpperCase() && + [5666, 5667].some(port => probe.ports?.includes(port))); + }); + }, + + clientURL(lease, data) { + const ip = validation.parseIPv4(lease.ipaddr || '')?.join('.'); + const client = data?.clients?.[ip]; + if (!ip || !client?.ready || client.mac != lease.macaddr?.toUpperCase()) + return null; + const port = [80, 8080, 5666, 443, 4430, 5667].find(port => client.ports?.includes(port)); + if (!port) + return null; + const scheme = [80, 8080, 5666].includes(port) ? 'http' : 'https'; + return scheme + '://' + ip + (port == 80 || port == 443 ? '' : ':' + port) + '/'; + }, + + renderClientIP(lease, data) { + const url = this.clientURL(lease, data); + return url ? E('a', { 'href': url, 'target': '_blank', 'rel': 'noopener noreferrer', 'style': 'text-decoration:underline', 'data-value': lease.ipaddr }, lease.ipaddr) : lease.ipaddr; + }, + + initLeaseTable(table) { + const widget = new L.ui.Table(table); + const update = widget.update; + widget.update = function(...args) { + const result = update.apply(this, args); + this.node.querySelectorAll('.cbi-section-actions').forEach(cell => { + cell.style.setProperty('text-align', 'center', 'important'); + }); + return result; + }; + const derive = widget.deriveSortKey; + widget.deriveSortKey = function(value, index) { + return Array.isArray(value) ? Number(value[0]) : derive.call(this, value, index); + }; + const totalIndex = Array.from(table.querySelectorAll('th')).findIndex(th => th.dataset.totalTraffic); + if (!widget.getActiveSortState()) + widget.sortState = [totalIndex, true]; + L.dom.bindClassInstance(table, widget); + }, + + updateLeaseOrder(table) { + const widget = L.dom.findClassInstance(table); + if (!widget) + return; + // Keep the table widget's raw keys current for subsequent header clicks. + for (const row of widget.data || []) + for (const value of row) + if (Array.isArray(value) && value[1]?.matches?.('.luci-client-rate')) + value[0] = Number(value[1].closest('td')?.dataset.value ?? -1); + const sorting = widget.getActiveSortState(); + if (sorting) { + const rows = Array.from(table.querySelectorAll('tr')).filter(row => row.querySelector('.luci-client-rate')); + const key = row => { + const cell = row.children[sorting[0]]; + return cell.hasAttribute('data-value') ? Number(cell.dataset.value) : widget.deriveSortKey(cell.querySelector('a[data-value]') || cell, sorting[0]); + }; + const sorted = rows.slice().sort((a, b) => { + const av = key(a), bv = key(b); + const cmp = typeof av == 'number' && typeof bv == 'number' ? av - bv : L.naturalCompare(av, bv); + return sorting[1] ? -cmp : cmp; + }); + if (sorted.some((row, i) => row !== rows[i])) + for (const row of sorted) + row.parentElement.appendChild(row); + } + }, + + refreshRates() { + const cells = Array.from(document.querySelectorAll('.luci-client-rate')); + const addresses = Array.from(new Set(cells.flatMap(cell => JSON.parse(cell.dataset.addresses)))); + if (!cells.length) + return Promise.resolve(); + return L.resolveDefault(callClientRates(addresses.slice(0, 1024)), {}).then(data => { + this.rateData = data; + // Query again: the normal overview refresh may have replaced the rows. + document.querySelectorAll('.luci-client-rate').forEach(cell => { + const value = this.rateValue(JSON.parse(cell.dataset.addresses), data, cell.dataset.direction, cell.dataset.mac); + cell.textContent = value[1]; + cell.closest('td')?.setAttribute('data-value', value[0]); + }); + document.querySelectorAll('#status_leases').forEach(table => this.updateLeaseOrder(table)); + }); + }, + load() { return Promise.all([ callLuciDHCPLeases(), network.getHostHints(), L.hasSystemFeature('ufpd') ? callUfpList() : null, - L.resolveDefault(uci.load('dhcp')) + L.resolveDefault(uci.load('dhcp')), + L.resolveDefault(callClientWeb(), {}), + L.resolveDefault(fs.read('/proc/net/arp'), ''), + L.resolveDefault(fs.read('/tmp/luci-client-history.json').then(JSON.parse), {}) ]); }, - render([dhcp_leases, host_hints, ufp_list]) { + render([dhcp_leases, host_hints, ufp_list, dhcp_config, web, arp, history]) { + if (!this.ratePoll) { + this.ratePoll = L.bind(this.refreshRates, this); + poll.add(this.ratePoll, 2); + } if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd')) - return this.renderLeases(dhcp_leases, host_hints, ufp_list); + return this.renderLeases(dhcp_leases, host_hints, ufp_list, web, arp, history); return null; }, @@ -83,14 +270,123 @@ return baseclass.extend({ .then(L.bind(L.ui.changes.displayChanges, L.ui.changes)); }, - renderLeases(dhcp_leases, host_hints, macaddr) { - const leases = Array.isArray(dhcp_leases.dhcp_leases) ? dhcp_leases.dhcp_leases : []; - const leases6 = Array.isArray(dhcp_leases.dhcp6_leases) ? dhcp_leases.dhcp6_leases : []; + arpLeases(arp) { + const leases = []; + for (const line of String(arp || '').trim().split(/\n/)) { + const fields = line.trim().split(/\s+/); + if (fields.length != 6 || fields[5] != 'br-lan' || + !(parseInt(fields[2], 16) & 2) || !validation.parseIPv4(fields[0]) || + !/^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$/i.test(fields[3]) || + fields[3] == '00:00:00:00:00:00' || (parseInt(fields[3].slice(0, 2), 16) & 1)) + continue; + leases.push({ ipaddr: fields[0], macaddr: fields[3].toUpperCase(), interface: fields[5] }); + } + return leases; + }, + + mergeLeases(leases, leases6, hints) { + const clients = new Map(); + const owners = new Map(); + const normalizeMAC = mac => /^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$/i.test(mac || '') && + mac != '00:00:00:00:00:00' && !(parseInt(mac.slice(0, 2), 16) & 1) + ? mac.toUpperCase() : null; + const addresses = lease => [lease.ipaddr, ...L.toArray(lease.ipaddrs), + lease.ip6addr, ...L.toArray(lease.ip6addrs)].filter(Boolean); + const addOwner = (ip, mac) => { + const address = this.normalizeRateAddress(ip); + if (!address || !mac) + return; + if (!owners.has(address)) + owners.set(address, new Set()); + owners.get(address).add(mac); + }; + for (const [mac, host] of Object.entries(hints.hosts || {})) + for (const ip of [...L.toArray(host.ipaddrs || host.ipv4), ...L.toArray(host.ip6addrs || host.ipv6)]) + addOwner(ip, normalizeMAC(mac)); + for (const lease of [...leases, ...leases6]) + for (const ip of addresses(lease)) + addOwner(ip, normalizeMAC(lease.macaddr)); + + for (const [family, list] of [[4, leases], [6, leases6]]) { + for (const lease of list) { + let mac = normalizeMAC(lease.macaddr); + if (!mac) { + const matches = new Set(); + for (const ip of addresses(lease)) + for (const owner of owners.get(this.normalizeRateAddress(ip)) || []) + matches.add(owner); + if (matches.size == 1) + mac = matches.values().next().value; + } + // DUID/IAID only deduplicate the same unidentified lease and its history; + // they never join it to a different IPv4 record based on hostname. + const unknown = lease.duid ? [family, lease.duid.toLowerCase(), lease.iaid?.toLowerCase(), lease.interface] : + addresses(lease).length ? [family, ...addresses(lease).map(ip => this.normalizeRateAddress(ip))] : null; + const key = mac || (unknown ? JSON.stringify(unknown) : Symbol()); + if (!clients.has(key)) + clients.set(key, { macaddr: mac, hostname: lease.hostname, + ipaddrs: [], ip6addrs: [], interfaces: [], leases: [], leases6: [], activeAddresses: [] }); + const client = clients.get(key); + client.hostname ||= lease.hostname; + if (lease.interface && !client.interfaces.includes(lease.interface)) + client.interfaces.push(lease.interface); + const ips = family == 4 ? client.ipaddrs : client.ip6addrs; + for (const ip of addresses(lease)) { + const address = this.normalizeRateAddress(ip); + if (address && !lease._historical && !client.activeAddresses.includes(address)) + client.activeAddresses.push(address); + if (address && !ips.some(value => this.normalizeRateAddress(value) == address)) + ips.push(ip); + } + const records = family == 4 ? client.leases : client.leases6; + if (!lease._historical && !records.some(record => family == 4 ? record.ipaddr == lease.ipaddr : + record.duid?.toLowerCase() == lease.duid?.toLowerCase() && record.iaid == lease.iaid)) + records.push(Object.assign({}, lease, { macaddr: mac })); + } + } + for (const client of clients.values()) { + const current = { macaddr: client.macaddr, ipaddrs: client.activeAddresses }; + client.rateAddresses = client.activeAddresses.length ? this.clientAddresses(current, hints) : []; + } + return Array.from(clients.values()); + }, + + leaseActions(client) { + const actions = []; + for (const [family, leases] of [[4, client.leases], [6, client.leases6]]) { + for (const lease of leases) { + const mac = lease.macaddr?.toLowerCase(); + const duid = lease.duid?.toLowerCase(); + const iaid = lease.iaid?.toLowerCase(); + if (family == 4 ? !mac : !duid) + continue; + const disabled = family == 4 ? this.isMACStatic[mac] : + this.isDUIDStatic[duid] || (iaid && this.isDUIDIAIDStatic[`${duid}%${iaid}`]); + actions.push(E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': L.bind(family == 4 ? this.handleCreateStaticLease : this.handleCreateStaticLease6, this, lease), + 'data-tooltip': _('Reserve a specific IP address for this device'), + 'disabled': disabled || null + }, [_('Reserve IP') + ` (IPv${family})` + (leases.length > 1 ? ` ${family == 4 ? lease.ipaddr : lease.ip6addrs?.[0] || lease.ip6addr || ''}` : '')])); + } + } + return E('div', { 'style': 'display:flex;flex-direction:column;gap:4px;align-items:center' }, actions); + }, + + renderLeases(dhcp_leases, host_hints, macaddr, web, arp, history) { + const leases = [...(Array.isArray(dhcp_leases.dhcp_leases) ? dhcp_leases.dhcp_leases : []), + ...this.arpLeases(arp), + ...(Array.isArray(history?.dhcp_leases) ? history.dhcp_leases : []).map(lease => ({ ...lease, _historical: true }))]; + const leases6 = [...(Array.isArray(dhcp_leases.dhcp6_leases) ? dhcp_leases.dhcp6_leases : []), + ...(Array.isArray(history?.dhcp6_leases) ? history.dhcp6_leases : []).map(lease => ({ ...lease, _historical: true }))]; if (leases.length == 0 && leases6.length == 0) return E('em', _('No active leases found')); const machints = host_hints.getMACHints(false); const isReadonlyView = !L.hasViewPermission(); + this.isMACStatic = {}; + this.isDUIDStatic = {}; + this.isDUIDIAIDStatic = {}; for (const host of uci.sections('dhcp', 'host')) { for (const mac of L.toArray(host.mac).map(m => m.toLowerCase())) @@ -106,138 +402,56 @@ return baseclass.extend({ } }; + const clients = this.mergeLeases(leases, leases6, host_hints); + const onlineMACs = new Set(String(arp || '').trim().split(/\n/).map(line => line.trim().split(/\s+/)) + .filter(fields => fields.length == 6 && fields[5] == 'br-lan') + .map(fields => fields[3].toUpperCase())); const table = E('table', { 'id': 'status_leases', 'class': 'table leases' }, [ E('tr', { 'class': 'tr table-titles' }, [ - L.hasSystemFeature('odhcpd', 'dhcpv4') ? E('th', { 'class': 'th' }, _('Interface')) : E([]), - E('th', { 'class': 'th' }, _('Hostname')), + E('th', { 'class': 'th' }, _('Online')), + E('th', { 'class': 'th', 'style': 'text-align:left' }, _('Hostname')), E('th', { 'class': 'th' }, _('IPv4 address')), - E('th', { 'class': 'th' }, _('MAC address')), - E('th', { 'class': 'th' }, _('DUID')), - E('th', { 'class': 'th' }, _('IAID')), - E('th', { 'class': 'th' }, _('Remaining time')), - isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions' }, _('Static Lease')) - ]) - ]); - - cbi_update_table(table, leases.map(L.bind(function(lease) { - let exp; - let vendor; - - if (lease.expires === false) - exp = E('em', _('unlimited')); - else if (lease.expires <= 0) - exp = E('em', _('expired')); - else - exp = '%t'.format(lease.expires); - - const hint = lease.macaddr ? machints.filter(function(h) { return h[0] == lease.macaddr })[0] : null; - let host = null; - - if (hint && lease.hostname && lease.hostname != hint[1]) - host = '%s (%s)'.format(lease.hostname, hint[1]); - else if (lease.hostname) - host = lease.hostname; - - if (macaddr) - vendor = macaddr[lease.macaddr.toLowerCase()]?.vendor ?? null; - - const columns = [ - host || '-', - lease.ipaddr, - vendor ? lease.macaddr + ` (${vendor})` : lease.macaddr, - lease.duid || '-', - lease.iaid || '-', - exp, - ]; - - if (L.hasSystemFeature('odhcpd', 'dhcpv4')) - columns.unshift(lease.interface || '-'); - - if (!isReadonlyView && lease.macaddr != null) { - columns.push(E('button', { - 'class': 'cbi-button cbi-button-apply', - 'click': L.bind(this.handleCreateStaticLease, this, lease), - 'data-tooltip': _('Reserve a specific IP address for this device'), - 'disabled': this.isMACStatic[lease.macaddr.toLowerCase()] - }, [ _('Reserve IP') ])); - } - - return columns; - }, this)), E('em', _('No active leases found'))); - - const table6 = E('table', { 'id': 'status_leases6', 'class': 'table leases6' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - L.hasSystemFeature('odhcpd', 'dhcpv6') ? E('th', { 'class': 'th' }, _('Interface')) : E([]), - E('th', { 'class': 'th' }, _('Hostname')), E('th', { 'class': 'th' }, _('IPv6 addresses')), - E('th', { 'class': 'th' }, _('DUID')), - E('th', { 'class': 'th' }, _('IAID')), - E('th', { 'class': 'th' }, _('Remaining time')), - isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions' }, _('Static Lease')) + E('th', { 'class': 'th' }, _('MAC address')), + E('th', { 'class': 'th' }, _('Upload')), + E('th', { 'class': 'th' }, _('Download')), + E('th', { 'class': 'th', 'data-total-traffic': '1' }, _('Total traffic')), + isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions center' }, _('Static Lease')) ]) ]); - cbi_update_table(table6, leases6.map(L.bind(function(lease) { - let exp; - - if (lease.expires === false) - exp = E('em', _('unlimited')); - else if (lease.expires <= 0) - exp = E('em', _('expired')); - else - exp = '%t'.format(lease.expires); - - const hint = lease.macaddr ? machints.filter(function(h) { return h[0] == lease.macaddr })[0] : null; - let host = null; - - if (hint && lease.hostname && lease.hostname != hint[1] && lease.ip6addr != hint[1]) - host = '%s (%s)'.format(lease.hostname, hint[1]); - else if (lease.hostname) - host = lease.hostname; - else if (hint) - host = hint[1]; - - const duid = lease.duid?.toLowerCase(); - const iaid = lease.iaid?.toLowerCase(); - - // Note: "disabled: false" doesn't work - let disabled = null; - if (!duid) - disabled = true; - else if (duid && this.isDUIDStatic[duid]) - disabled = true; - else if (duid && iaid && this.isDUIDIAIDStatic[`${duid}%${iaid}`]) - disabled = true; - + this.initLeaseTable(table); + cbi_update_table(table, clients.map(client => { + const hint = machints.find(h => h[0].toUpperCase() == client.macaddr); + let host = client.hostname || hint?.[1]; + if (hint && client.hostname && client.hostname != hint[1]) + host = '%s (%s)'.format(client.hostname, hint[1]); + const vendor = macaddr?.[client.macaddr?.toLowerCase()]?.vendor; + const online = onlineMACs.has(client.macaddr); + const status = online ? _('Online') : _('Offline'); const columns = [ - host || '-', - lease.ip6addrs ? lease.ip6addrs.join('
') : lease.ip6addr, - duid || '-', - iaid || '-', - exp + [online ? 1 : 0, E('span', { + 'class': 'luci-client-online', + 'role': 'img', 'aria-label': status, 'title': status, + 'style': 'display:inline-block;width:10px;height:10px;border-radius:50%;background-color:' + (online ? '#28a745' : '#dc3545') + })], + this.renderHostname(host, client.macaddr, this.isFnosClient(client, web)), + client.ipaddrs.length ? E('div', {}, client.ipaddrs.map(ipaddr => + E('div', client.activeAddresses.includes(this.normalizeRateAddress(ipaddr)) ? {} : { 'style': 'opacity:.55', 'title': _('Expired') }, + client.activeAddresses.includes(this.normalizeRateAddress(ipaddr)) ? this.renderClientIP({ ipaddr, macaddr: client.macaddr }, web) : ipaddr))) : '-', + client.ip6addrs.length ? E('div', { 'style': 'overflow-wrap:anywhere' }, + client.ip6addrs.map(ip => E('div', client.activeAddresses.includes(this.normalizeRateAddress(ip)) ? {} : { 'style': 'opacity:.55', 'title': _('Expired') }, ip))) : '-', + vendor ? `${client.macaddr} (${vendor})` : client.macaddr || '-', + this.rateCell(client, host_hints, 'upload'), + this.rateCell(client, host_hints, 'download'), + this.rateCell(client, host_hints, 'total') ]; - - if (L.hasSystemFeature('odhcpd', 'dhcpv6')) - columns.unshift(lease.interface || '-'); - - if (!isReadonlyView && lease.duid) { - columns.push(E('button', { - 'class': 'cbi-button cbi-button-apply', - 'click': L.bind(this.handleCreateStaticLease6, this, lease), - 'data-tooltip': _('Reserve a specific IP address for this device'), - 'disabled': disabled - }, [ _('Reserve IP') ])); - } - + if (!isReadonlyView) + columns.push(this.leaseActions(client)); return columns; - }, this)), E('em', _('No active leases found'))); - - return E([ - E('h3', _('Active DHCPv4 Leases')), - table, - E('h3', _('Active DHCPv6 Leases')), - table6 - ]); + }), E('em', _('No active leases found'))); + this.updateLeaseOrder(table); + return table; }, }); diff --git a/luci-mod-status/htdocs/luci-static/resources/view/status/index.js b/luci-mod-status/htdocs/luci-static/resources/view/status/index.js index a377a64b8..1b27fc1b2 100644 --- a/luci-mod-status/htdocs/luci-static/resources/view/status/index.js +++ b/luci-mod-status/htdocs/luci-static/resources/view/status/index.js @@ -285,7 +285,7 @@ return view.extend({ { name: 'view.status.include.20_memory' }, { name: 'view.status.include.25_storage' }, { name: 'view.status.include.30_network' }, - { name: 'view.status.include.40_dhcp', title: _('DHCP Leases'), deferFirstLoad: true }, + { name: 'view.status.include.40_dhcp', title: _('Online Hosts'), deferFirstLoad: true }, { name: 'view.status.include.50_dsl' }, { name: 'view.status.include.60_wifi' } ]; diff --git a/luci-mod-status/root/etc/init.d/luci-client-history b/luci-mod-status/root/etc/init.d/luci-client-history new file mode 100755 index 000000000..473ed8f2e --- /dev/null +++ b/luci-mod-status/root/etc/init.d/luci-client-history @@ -0,0 +1,12 @@ +#!/bin/sh /etc/rc.common +START=95 +STOP=10 +USE_PROCD=1 + +start_service() { + procd_open_instance + procd_set_param command /bin/sh -c 'while :; do /usr/bin/ucode /usr/libexec/luci-client-history; sleep 10; done' + procd_set_param respawn + procd_set_param stderr 1 + procd_close_instance +} diff --git a/luci-mod-status/root/usr/libexec/luci-client-history b/luci-mod-status/root/usr/libexec/luci-client-history new file mode 100755 index 000000000..814329890 --- /dev/null +++ b/luci-mod-status/root/usr/libexec/luci-client-history @@ -0,0 +1,27 @@ +#!/usr/bin/ucode +// SPDX-License-Identifier: Apache-2.0 +'use strict'; +import { readfile, writefile, rename, chmod } from 'fs'; +import { connect } from 'ubus'; +import { arp_leases, update_history } from 'luci.client_history'; + +const path = '/tmp/luci-client-history.json'; +const boot_id = trim(readfile('/proc/sys/kernel/random/boot_id') || ''); +if (!length(boot_id)) + exit(1); +let previous = {}; +try { previous = json(readfile(path) || '{}'); } catch (e) {} +const bus = connect(); +let current = bus ? bus.call('luci-rpc', 'getDHCPLeases', {}) : null; +if (bus) + bus.disconnect(); +current ||= {}; +current.dhcp_leases = [ ...(current.dhcp_leases || []), ...arp_leases(readfile('/proc/net/arp')) ]; +const next = sprintf('%J', update_history(previous, current, boot_id)); +if (next != readfile(path)) { + if (writefile(path + '.new', next) != length(next)) + exit(1); + chmod(path + '.new', 0600); + if (!rename(path + '.new', path)) + exit(1); +} diff --git a/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status-index.json b/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status-index.json index a69418f9e..d4eaf44f8 100644 --- a/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status-index.json +++ b/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status-index.json @@ -4,6 +4,8 @@ "read": { "file": { "/etc/board.json": [ "read" ], + "/proc/net/arp": [ "read" ], + "/tmp/luci-client-history.json": [ "read" ], "/proc/sys/net/netfilter/nf_conntrack_count": [ "read" ], "/proc/sys/net/netfilter/nf_conntrack_max": [ "read" ], "/www/luci-static/resources/view/status/include": [ "list" ] diff --git a/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json b/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json index 8f6b96238..d023be195 100644 --- a/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json +++ b/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json @@ -8,6 +8,8 @@ "ubus": { "luci": [ "getConntrackList", "getRealtimeStats", "getCPUBench", "getCPUUsage", "getOnlineUsers" ], "luci-rpc": [ "getHostHints", "getNetworkDevices", "getDHCPLeases" ], + "luci.client-rates": [ "get" ], + "luci.client-web": [ "get" ], "network.rrdns": [ "lookup" ] } } diff --git a/luci-mod-status/root/usr/share/ucode/luci/client_history.uc b/luci-mod-status/root/usr/share/ucode/luci/client_history.uc new file mode 100644 index 000000000..e9b3c25fb --- /dev/null +++ b/luci-mod-status/root/usr/share/ucode/luci/client_history.uc @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// Retain discovered clients in RAM for this boot, independently of the browser. +export function arp_leases(text) { + let leases = []; + for (let line in split(text || '', '\n')) { + let f = split(trim(line), /\s+/); + if (length(f) != 6 || f[5] != 'br-lan' || !(int(f[2], 16) & 2) || + !match(f[0], /^\d+\.\d+\.\d+\.\d+$/) || + !match(f[3], /^([0-9a-f]{2}:){5}[0-9a-f]{2}$/i) || + f[3] == '00:00:00:00:00:00' || (int(substr(f[3], 0, 2), 16) & 1)) + continue; + push(leases, { ipaddr: f[0], macaddr: uc(f[3]), interface: f[5] }); + } + return leases; +}; + +export function update_history(previous, current, boot_id) { + if (type(previous) != 'object' || previous.version != 1 || previous.boot_id != boot_id) + previous = {}; + let result = { version: 1, boot_id, dhcp_leases: [], dhcp6_leases: [] }; + for (let family in ['dhcp_leases', 'dhcp6_leases']) { + let records = {}; + for (let source in [previous, current]) { + for (let lease in type(source[family]) == 'array' ? source[family] : []) { + if (type(lease) != 'object') + continue; + let mac = type(lease.macaddr) == 'string' ? uc(lease.macaddr) : ''; + let identity = mac || lc(lease.duid || ''); + let key = sprintf('%J', [identity, lease.interface || '', + family == 'dhcp_leases' ? lease.ipaddr : [lease.duid || '', lease.iaid || '', lease.ip6addr || '']]); + let old = records[key] || {}; + let record = { ...old, ...lease }; + if (mac) + record.macaddr = mac; + if (family == 'dhcp6_leases') { + let ips = []; + for (let item in [old, lease]) + for (let ip in type(item.ip6addrs) == 'array' ? item.ip6addrs : []) + if (index(ips, ip) < 0) + push(ips, ip); + record.ip6addrs = ips; + } + records[key] = record; + } + } + result[family] = values(records); + } + return result; +}; diff --git a/luci-mod-status/tests/README.md b/luci-mod-status/tests/README.md new file mode 100644 index 000000000..e7b01b98e --- /dev/null +++ b/luci-mod-status/tests/README.md @@ -0,0 +1,19 @@ +The overview combines DHCPv4, DHCPv6 and resolved `br-lan` ARP entries by MAC. +IPv6 leases without a MAC can join a client only when their addresses have an +unambiguous MAC mapping in host hints or another lease. Hostnames alone never +merge devices. DUID/IAID identify otherwise unknown lease records and preserve +IPv6 reservation actions. + +`luci-client-history` samples leases and ARP every ten seconds without requiring +an open browser. It atomically checkpoints discovered records in +`/tmp/luci-client-history.json`. Records are retained after lease/ARP expiry and +collector/rpcd restarts; a new boot clears them. Collection starts when the +service is installed/enabled, so earlier expired records cannot be reconstructed. +Historical addresses are dimmed and excluded from live rates and reservation +buttons. Totals remain the existing per-MAC conntrack accounting totals. + +Run `ucode client-history.uc` on a target with the installed +`luci.client_history` module. The tests cover LAN filtering, expiry, collector +restart, IP reassignment, changing IPv6 addresses and boot-ID reset without +rebooting the device. Run the existing frontend tests with +`node libs/rpcd-mod-luci/tests/traffic-ui.js` from the LuCI repository. diff --git a/luci-mod-status/tests/client-history.uc b/luci-mod-status/tests/client-history.uc new file mode 100644 index 000000000..304731dd6 --- /dev/null +++ b/luci-mod-status/tests/client-history.uc @@ -0,0 +1,29 @@ +// Run with ucode after installing luci.client_history on the test target. +import { arp_leases, update_history } from 'luci.client_history'; +function check(value, message) { + if (!value) die(message + '\n'); +}; +const arp = '192.168.0.2 0x1 0x2 aa:bb:cc:dd:ee:ff * br-lan\n' + + '192.168.0.3 0x1 0x0 00:11:22:33:44:66 * br-lan\n' + + '192.168.9.1 0x1 0x2 00:11:22:33:44:77 * eth1\n'; +let first = update_history({}, { + dhcp_leases: arp_leases(arp), + dhcp6_leases: [{ macaddr: 'aa:bb:cc:dd:ee:ff', duid: 'uuid', iaid: '1', ip6addrs: ['2001:db8::2/128'] }] +}, 'boot-a'); +check(length(first.dhcp_leases) == 1, 'filter non-LAN and unresolved ARP'); +let absent = update_history(first, {}, 'boot-a'); +check(length(absent.dhcp_leases) == 1 && length(absent.dhcp6_leases) == 1, 'retain expired DHCP and ARP'); +let restored = update_history(json(sprintf('%J', absent)), {}, 'boot-a'); +check(sprintf('%J', restored) == sprintf('%J', absent), 'restore after collector restart'); +let updated = update_history(restored, { + dhcp_leases: [{ macaddr: '00:11:22:33:44:66', ipaddr: '192.168.0.2', interface: 'br-lan' }], + dhcp6_leases: [{ macaddr: 'AA:BB:CC:DD:EE:FF', duid: 'uuid', iaid: '1', ip6addrs: ['2001:db8::3/128'] }] +}, 'boot-a'); +check(length(updated.dhcp_leases) == 2, 'preserve former owner after address reuse'); +check(length(updated.dhcp6_leases) == 1 && length(updated.dhcp6_leases[0].ip6addrs) == 2, 'retain IPv6 address changes'); +let repeat = update_history(updated, { dhcp_leases: arp_leases(arp) }, 'boot-a'); +check(length(repeat.dhcp_leases) == 2, 'deduplicate repeated discoveries'); +let rebooted = update_history(updated, {}, 'boot-b'); +check(!length(rebooted.dhcp_leases) && !length(rebooted.dhcp6_leases), 'new boot resets history'); +check(!length(update_history('invalid', {}, 'boot-a').dhcp_leases), 'recover invalid state'); +print('client history: LAN filtering, expiry, restart, reassignment, IPv6 changes and boot reset passed\n'); diff --git a/luci-mod-system/htdocs/luci-static/resources/view/system/system.js b/luci-mod-system/htdocs/luci-static/resources/view/system/system.js index 0979d0571..33365d827 100644 --- a/luci-mod-system/htdocs/luci-static/resources/view/system/system.js +++ b/luci-mod-system/htdocs/luci-static/resources/view/system/system.js @@ -60,6 +60,27 @@ function formatTime(epoch) { }).format(date); } +const ntpRedirectSection = 'ntp_redirect'; + +function removeNtpRedirect() { + if (uci.get('firewall', ntpRedirectSection) != null) + uci.remove('firewall', ntpRedirectSection); +} + +function setNtpRedirect() { + if (uci.get('firewall', ntpRedirectSection)?.['.type'] != 'redirect') { + removeNtpRedirect(); + uci.add('firewall', 'redirect', ntpRedirectSection); + } + + uci.set('firewall', ntpRedirectSection, 'name', 'Redirect NTP requests'); + uci.set('firewall', ntpRedirectSection, 'src', 'lan'); + uci.set('firewall', ntpRedirectSection, 'proto', 'udp'); + uci.set('firewall', ntpRedirectSection, 'src_dport', '123'); + uci.set('firewall', ntpRedirectSection, 'dest_port', '123'); + uci.set('firewall', ntpRedirectSection, 'target', 'DNAT'); +} + const CBILocalTime = form.DummyValue.extend({ renderWidget(section_id, option_id, cfgvalue) { return E([], [ @@ -99,13 +120,14 @@ return view.extend({ callGetUnixtime(), uci.load('luci'), uci.load('system'), + uci.load('firewall'), L.hasSystemFeature('zram') ? L.resolveDefault(fs.read('/sys/block/zram0/comp_algorithm'), '') : '' ]); }, - render([ntpd_enabled, timezones, unixtime, _luci, _system, zram_algorithms]) { + render([ntpd_enabled, timezones, unixtime, _luci, _system, _firewall, zram_algorithms]) { let m, s, o; const system_section = uci.sections('system', 'system')[0]; @@ -116,6 +138,7 @@ return view.extend({ this.systemSid = system_section?.['.name']; m.chain('luci'); + m.chain('firewall'); s = m.section(form.TypedSection, 'system', _('System Properties')); s.anonymous = true; @@ -306,8 +329,10 @@ return view.extend({ uci.set('system', 'ntp', 'server', default_servers); } - if (!ntpd_enabled) + if (!ntpd_enabled) { uci.set('system', 'ntp', 'enabled', 0); + removeNtpRedirect(); + } else uci.unset('system', 'ntp', 'enabled'); @@ -322,6 +347,12 @@ return view.extend({ o = s.taboption('timesync', form.Flag, 'enable_server', _('Provide NTP server')); o.ucisection = 'ntp'; o.depends('enabled', '1'); + o.write = function(section_id, value) { + uci.set('system', 'ntp', 'enable_server', value); + + if (value != '1') + removeNtpRedirect(); + }; o = s.taboption('timesync', widgets.NetworkSelect, 'interface', _('Bind NTP server'), @@ -332,6 +363,21 @@ return view.extend({ o.nocreate = true; o.optional = true; + o = s.taboption('timesync', form.Flag, '_redirect', _('Redirect NTP requests'), + _('Redirect LAN NTP requests to this device.')); + o.ucisection = 'ntp'; + o.depends({ enabled: '1', enable_server: '1' }); + o.cfgvalue = function(section_id) { + return uci.get('firewall', ntpRedirectSection)?.['.type'] == 'redirect' ? '1' : '0'; + }; + o.write = function(section_id, value) { + if (value == '1') + setNtpRedirect(); + else + removeNtpRedirect(); + }; + o.remove = removeNtpRedirect; + o = s.taboption('timesync', form.Flag, 'use_dhcp', _('Use DHCP advertised servers')); o.ucisection = 'ntp'; o.default = o.enabled; diff --git a/luci-mod-system/root/usr/share/rpcd/acl.d/luci-mod-system.json b/luci-mod-system/root/usr/share/rpcd/acl.d/luci-mod-system.json index f3b24f60e..5323eb9de 100644 --- a/luci-mod-system/root/usr/share/rpcd/acl.d/luci-mod-system.json +++ b/luci-mod-system/root/usr/share/rpcd/acl.d/luci-mod-system.json @@ -10,14 +10,14 @@ "luci": [ "getLEDs", "getTimezones", "getUSBDevices", "getUnixtime" ], "rc": [ "list" ] }, - "uci": [ "luci", "system" ] + "uci": [ "firewall", "luci", "system" ] }, "write": { "ubus": { "luci": [ "setLocaltime", "setPassword" ], "rc": [ "init" ] }, - "uci": [ "luci", "system" ] + "uci": [ "firewall", "luci", "system" ] } }, diff --git a/luci-theme-argon/Makefile b/luci-theme-argon/Makefile index 9c9d79c6b..70217e06d 100644 --- a/luci-theme-argon/Makefile +++ b/luci-theme-argon/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=Argon Theme LUCI_DEPENDS:=+USE_APK:wget-any +!USE_APK:wget +jsonfilter PKG_VERSION:=2.4.7 -PKG_RELEASE:=14 +PKG_RELEASE:=15 CONFIG_LUCI_CSSTIDY:= diff --git a/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css b/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css index 8bc15aa79..c7052dbc8 100644 --- a/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css +++ b/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css @@ -2060,6 +2060,12 @@ td > table > tbody > tr > td, .cbi-section-node > [id^="cbi-config-"]:last-child { padding-bottom: 1rem; } +.cbi-section-node > [id^="cbi-argon-"]:first-child { + padding-top: 1rem; +} +.cbi-section-node > [id^="cbi-argon-"]:last-child { + padding-bottom: 1rem; +} .cbi-section .cbi-section-remove:nth-of-type(2n), .container > .cbi-section .cbi-section-node:nth-of-type(2n) { background-color: #f9f9f9; diff --git a/luci-theme-argon/less/cascade.less b/luci-theme-argon/less/cascade.less index 97783abaa..8df9f6ed8 100644 --- a/luci-theme-argon/less/cascade.less +++ b/luci-theme-argon/less/cascade.less @@ -1013,6 +1013,14 @@ td>table>tbody>tr>td, padding-bottom: 1rem; } } + + >[id^="cbi-argon-"]:first-child { + padding-top: 1rem; + } + + >[id^="cbi-argon-"]:last-child { + padding-bottom: 1rem; + } } .cbi-section .cbi-section-remove:nth-of-type(2n), diff --git a/luci-theme-aurora/.claude/skills/luci-change/SKILL.md b/luci-theme-aurora/.claude/skills/luci-change/SKILL.md index 1675976c5..a21844a1d 100644 --- a/luci-theme-aurora/.claude/skills/luci-change/SKILL.md +++ b/luci-theme-aurora/.claude/skills/luci-change/SKILL.md @@ -17,7 +17,10 @@ workflow. No edit to a shipped or source path happens before gate 1, and the 1. **Research (read-only).** Load `luci-knowledge`; read `references/aurora-contract.md` and the section relevant to the files involved; `git log -8 --oneline -- `; the tests and budgets that - cover them. + cover them. If the request names a page, a package, a DOM element or + shows a page screenshot, load `luci-dom-compat` and complete its steps + 1–2 (locate the page, read `.dev/compat/pages/` for the files and + selectors about to change) before planning. 2. **Plan.** Call `EnterPlanMode`. The plan states: files to change; the approach in ≤ 10 lines; budget impact (bytes per `tests/build-performance.test.js`, per-request ubus/uci/fs calls, dispatches per navigation); floor it @@ -46,7 +49,10 @@ workflow. No edit to a shipped or source path happens before gate 1, and the out and why. Write the same report as a decision record to `.dev/decisions/-.md` (git-ignored; header lines `Request`, `Decision: implemented | reverted | deferred`, `Commit:` once - one exists) so the ledger outlives the session. `rm -f + one exists) so the ledger outlives the session. A page-scoped change also + appends its entry to `.dev/compat/pages/.md` (and a + fixture under `.dev/compat/fixtures/` when the symptom is geometric — + `luci-dom-compat` step 5) in the same commit. `rm -f .claude/.change-approved`. Commit only when the user asks: one logical change, short conventional subject, body carries the reasoning, no session trailers. Never push. diff --git a/luci-theme-aurora/.claude/skills/luci-dom-compat/SKILL.md b/luci-theme-aurora/.claude/skills/luci-dom-compat/SKILL.md new file mode 100644 index 000000000..ab9a4c2fd --- /dev/null +++ b/luci-theme-aurora/.claude/skills/luci-dom-compat/SKILL.md @@ -0,0 +1,78 @@ +--- +name: luci-dom-compat +description: Use whenever a request is about how one LuCI page renders under this theme — a cell that wraps or overflows, misaligned controls, a mobile card layout, a screenshot of a page, a data-page value, a package name (luci-app-*), or an issue about a plugin. Stock LuCI pages count as much as third-party apps. Reads the DOM compat ledger before any CSS is touched and writes the entry (and a geometry fixture) after. +argument-hint: +--- + +# luci-dom-compat $ARGUMENTS + +The ledger lives in `.dev/compat/` (format in its README). It answers "why +is this rule here" for every page-scoped rule in the theme, and +`tests/compat.test.js` runs the fixtures that guard the geometric ones. +This skill does not replace `/luci-change`: the edit itself still goes +through that gate; this is what happens before and after. + +## 1. Locate — write the quadruple down before touching CSS + +| what | where to get it | +|---|---| +| `data-page` | `body[data-page]` on the page; or the URL segments after `/cgi-bin/luci/` joined with `-` | +| package | on the device: `apk list -I \| grep -i ` or `opkg list-installed \| grep -i `; stock pages are `luci-mod-*`/`luci-app-*` from openwrt/luci | +| menu path | `/usr/share/luci/menu.d/*.json` (`grep -o '"admin/[^"]*"'` + the `title` fields) or the breadcrumb on the page | +| DOM element | the selector, plus any inline `style`/attribute the layout depends on (`table-layout:fixed`, `white-space:nowrap`, `width:`) — that is usually the cause | + +## 2. Read the ledger first + +``` +ls .dev/compat/pages/ | grep # this page +grep -rln "" .dev/compat/pages/ +grep -rln "" .dev/compat/pages/ +``` + +For every hit, state in the plan whether the change keeps, changes or +removes that entry's behaviour. A rule that looks pointless usually has an +entry; if it has none, that is the first thing to add. + +## 3. Evidence + +Device reachable (`VITE_OPENWRT_HOST` in `.dev/.env`, empty root password +over http on the dev routers): `node .claude/skills/luci-dom-compat/scripts/compat-probe.mjs`. +It logs in, opens each page in headless Chrome, serves the *before* CSS +through `Fetch` interception, measures, hot-swaps the *after* CSS into the +same document, measures again, and prints per-element computed-style +diffs on `.table` subtrees, wrap/overflow counts for `SELECTOR`, and +screenshots. Nothing on the device is written. Env: `HOST`, `PAGES` +(comma-separated `admin/...` paths), `WIDTHS`, `BEFORE`/`AFTER` (css files; +default before = the file the device serves, after = `htdocs/.../main.css`), +`SELECTOR`, `OUT`, `PRESET_LS` (a `localStorage` key=value the page needs, +e.g. the selected disk). + +Device unreachable: a `.dev/mocks/` snapshot through `pnpm dev` +(`/mocks/`), or an existing fixture. Compare against luci-theme-bootstrap +(`../luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css`) when +the question is "what does upstream do here" — serve it as `AFTER` to the +same page. + +Wrap detection that survives inline swatches and checkboxes: walk the +cell's text nodes, collect client-rect tops, and treat "text starts below +the inline indicator" as a wrap (`nowrap` in `tests/compat.test.js`). + +## 4. Fix + +Through `/luci-change`. Prefer the component partial over a `patches/` +file when the cause is a theme rule blocking what the page asked for +(#65: a font size on `td` blocked the table's own); a patch is for markup +the theme cannot reasonably absorb. + +## 5. Sink — same commit as the fix + +1. Append the entry to `.dev/compat/pages/.md` in the + README format. `verified:` carries the numbers from step 3. +2. Symptom geometric (wrap, overflow, width, height)? Trim the DOM to a + fixture in `.dev/compat/fixtures/--.html`: the block, + the page's own injected ` + + + +
+
+
+ +
+
PartitionType / FilesystemMount PointLabelSizeUsedUnusedFlags
0BOOTCONFIG--256.00 KiB---
SQUASHFS/rom-2.00 GiB110.0 MiB0-
Ext4/mnt/mmcblk0p6-113.26 GiB2.0 MiB105.2 GiBstorage
+
+
+
+
+ + diff --git a/luci-theme-aurora/.dev/compat/pages/admin-dashboard.md b/luci-theme-aurora/.dev/compat/pages/admin-dashboard.md new file mode 100644 index 000000000..041328ec3 --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-dashboard.md @@ -0,0 +1,25 @@ +# admin-dashboard + +Patch file: `.dev/src/media/patches/admin-dashboard.css` (scope `[data-page="admin-dashboard"] .Dashboard > .section-content`, line 8-10). Route: main.css `@layer luci-mod-dashboard` (7f25128) → `_patches.css` (aedf3dc, 7d9f6a1) → own on-demand file with `@reference`/`@apply` (1c924cf, PR #82) → native CSS (e14cfcd). Page package: stock luci-mod-dashboard (menu.d "admin/dashboard" "Dashboard"); it ships its own `view/dashboard/css/custom.css`. + +## dashboard-cards-and-dark — card decoration on `.dashboard-bg`, dark-mode svg invert, title rule, wifi device cell +- page: admin-dashboard · Dashboard +- package: stock luci-mod-dashboard +- dom: `.dashboard-bg` (card), `.dashboard-bg.tr` (row variant, no radius), `img[src*=".svg"]`, `.title h3`, `.router-status-wifi .wifi-info .devices-info .tr .td:nth-child(3)` +- symptom: not recorded beyond the subjects ("dark mode display issue in luci-mod-dashboard"; 56db83c: "Adapt for missing data-page="admin-dashboard" on default login redirect to dashboard", "Remove border-radius on .dashboard-bg for mobile view") +- cause: unknown for the card/wifi/h3 rules (7f25128 has subject only). For the scope: 56db83c body says the `[data-page="admin-dashboard"]` attribute was missing when the login redirect landed on the dashboard, so the scope was dropped; 1c924cf restored it when the patch became on-demand (PR #82: "Backfilled missing data-page scoping in the original patches (… dashboard)"), and header.ut now computes the page name from `ctx.path` when `request_path` is empty (DEVELOPMENT.md §On-Demand Third-Party Patches, item 1) +- fix: patches/admin-dashboard.css — `.dashboard-bg { background: var(--surface-overlay) }` + `&.tr { border-radius: 0 }` + `&:not(.tr) { border: 1px solid var(--hairline); box-shadow: var(--app-shadow-lg); &:hover {…} }` (line 11-27); `img[src*=".svg"] { [data-darkmode="true"] & { filter: invert(100%) } }` (line 38-42); `.title h3 { color: var(--text); border: 0; padding-bottom: 1rem; @media (width < 48rem) { margin-inline: 0; padding-bottom: 0.5rem } }` (line 93-102); `.router-status-wifi .wifi-info .devices-info .tr .td { &:nth-child(3) { position: relative; top: 0; width: 100%; padding-inline: 0 } @media (width < 48rem) { flex: 1 1 50% } }` (line 104-117) · 7f25128 2025-09-28 · 0.5.1_beta (first tag v0.5.5_beta); 56db83c 2025-10-12 · 0.5.9_beta (first tag v0.6.0_beta) dropped the scope and added `.tr { rounded-none }`; the original `.router-status-lan .devices-info tr th { max-md:flex-[1_1_50%] }` from 7f25128 was removed by c3fae55 2025-09-29 "fix: mobile display issue in luci-mod-dashboard th" (subject only) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active + +## dashboard-dark-text-and-inline-tables — force theme text colour in dark mode, compact assoclist cells, undecorate inline info tables +- page: admin-dashboard · Dashboard (issue #66 names the System, DHCP Devices and Wireless panels) +- package: stock luci-mod-dashboard +- dom: `.dashboard-bg span/td/th` (dark mode), `.dashboard-bg .td/.th` (assoclist cells carry `.td/.th` classes), `.dashboard-bg .table:not(.assoclist)` with its `tr` and `td` (the DNS-style inline info tables) +- symptom: issue #66: "Using luci-mod-dashboard with aurora in dark mode results in multiple UI readability and layout issues … Some elements are rendered with very low contrast (almost black on dark background), and spacing/padding in certain tables and panels (System, DHCP Devices, Wireless) appears inconsistent." (OpenWrt 25.12.4, two screenshots) +- cause: the patch's own comment: "Dark mode: override custom.css hardcoded light-mode colors on all text nodes" — luci-mod-dashboard's `custom.css` sets text colours the theme's dark palette does not reach; the spacing/inline-table rules have no recorded cause (944e7c5 has subject only) +- fix: patches/admin-dashboard.css — `& span, & td, & th { [data-darkmode="true"] & { color: var(--text) !important } }` (line 30-36); `& .td { padding-block: 0.5rem; @media (width < 48rem) { padding-block: 0.25rem } }` and `& .th { padding-block: 0.5rem }` (line 45-54); `& .table:not(.assoclist) { margin-bottom: 0; border-radius: 0; border: 0; background: transparent; box-shadow: none; & tr { &:hover { background: transparent } @media (width < 48rem) { display: table-row; border: 0; padding: 0 } } & td { border: 0; padding: 0.125rem 0; @media (width < 48rem) { display: table-cell } &:first-child { padding-right: 0.5rem; white-space: nowrap; opacity: 0.7 } } }` (line 57-91) · 944e7c5 2026-05-31 (in `_patches.css`) · 0.11.10 (first tag v0.12.0) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: #66 (still open) · mirror: pending · status: active diff --git a/luci-theme-aurora/.dev/compat/pages/admin-modem-modemdata-modempreview.md b/luci-theme-aurora/.dev/compat/pages/admin-modem-modemdata-modempreview.md new file mode 100644 index 000000000..7737b130f --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-modem-modemdata-modempreview.md @@ -0,0 +1,12 @@ +# admin-modem-modemdata-modempreview + +## modemdata-badge-width — let signal badges inside info cards size to content +- page: admin-modem-modemdata-modempreview · Modem → Modem Data → (preview) (menu path inferred from the page name; not in the local LuCI checkouts) +- package: luci-app-modemdata (third-party; named in the patch header and PR #80) +- dom: `.ifacebox-body .ifacebadge` on the "General Information" cards (PR #80 text) +- symptom: PR #80 body: "Aurora's default ifacebox styles spread modemdata rows apart, wrap signal badges vertically, and leave cards at uneven heights." +- cause: unknown beyond the PR wording — the merged diff (910c85d, 12 lines) contains only the badge width override; the row-spacing and card-height parts of the PR description have no rule in the commit +- fix: patches/admin-modem-modemdata-modempreview.css line 8-12 `[data-page="admin-modem-modemdata-modempreview"] { & .ifacebox-body .ifacebadge { width: auto !important; } }` · 910c85d 2026-06-28 (author CJay D Acopra, PR #80) · 1.0.2 (first tag v1.1.0) — added to `_patches.css` as `@apply w-auto!`, split into its own file by 1c924cf, native CSS by e14cfcd +- verified: not measured (no record) +- fixture: none (backfill) +- issue: PR #80 · mirror: pending · status: active diff --git a/luci-theme-aurora/.dev/compat/pages/admin-modem-qmodem.md b/luci-theme-aurora/.dev/compat/pages/admin-modem-qmodem.md new file mode 100644 index 000000000..3cdb02499 --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-modem-qmodem.md @@ -0,0 +1,12 @@ +# admin-modem-qmodem + +## qmodem-theming — colours, borders, hover states, tab and table styling across the QModem pages +- page: admin-modem-qmodem (prefix; covers `-overview`, `-sms`, `-sms-conversation-`, `-sms_sim`, `-config_advanced`, `-settings`, `-network_config`) · Modem → QModem → … (menu paths inferred from the page names; the app is not in the local LuCI checkouts) +- package: luci-app-qmodem (third-party; named in the patch header and PR #92) +- dom: `[data-page^="admin-modem-qmodem"]`: `.copyright-section`, `fieldset.cbi-section > legend`, `fieldset.cbi-section`, `.cbi-section-table-titles.named::before` (line 9-42); `[data-page^="admin-modem-qmodem-sms-conversation"]`: `#sms-messages-area`, `.sms-message-bubble`, `.sms-message-content`, `.sms-message-meta` (line 44-64); `[data-page="admin-modem-qmodem-config_advanced"]`: `.cbi-tabmenu li` and `.cbi-tab` (line 66-96; the file's comment: "targets
  • directly (not li > a) because qmodem's tab markup has no inside tabs"); `[data-page="admin-modem-qmodem-sms"]`: `.td > div`, `.td > span[style*="margin-left"]`, `.sms-conversations` (+ `> .cbi-section`, `fieldset.cbi-section`, `.table.cbi-section-table`), `.cbi-section-table-row.tr` (+ hover), `.sms-unread-conversation`, `.sms-unread-badge` (line 98-175); `[data-page="admin-modem-qmodem-sms_sim"]`: `.cbi-section-table-row.tr` (+ hover) (line 177-197); `[data-page="admin-modem-qmodem-settings"]`: `.cbi-section-table-descr.named::before` (line 201-206) +- symptom: not recorded per rule. PR #92 body: "Styles qmodem overview, SMS, conversation, advanced config, network config, and settings pages with consistent theming (borders, colors, hover states, table formatting, and tab styling)." +- cause: unknown (commit 7066351 has subject only; the PR body lists scope, not what each page did wrong) +- fix: patches/admin-modem-qmodem.css (whole file) · 7066351 2026-07-25 (author shiraneko, PR #92 by cjayacopra) · 1.1.2 (first tag v1.2.0) — written with `@reference`/`@apply`, rewritten natively by e14cfcd 2026-07-27 (1.1.8; radii became `calc(var(--radius-base) * n)`) +- verified: not measured (no record) +- fixture: none (backfill; DOM snapshots exist in `.dev/mocks/admin-modem-qmodem*.html` — overview, sms, sms_sim, config_advanced, settings, network_config) +- issue: PR #92 · mirror: pending · status: active diff --git a/luci-theme-aurora/.dev/compat/pages/admin-network-network.md b/luci-theme-aurora/.dev/compat/pages/admin-network-network.md new file mode 100644 index 000000000..e16eeed08 --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-network-network.md @@ -0,0 +1,47 @@ +# admin-network-network + +## bridge-vlan-descr-hidden — hide the table description row on mobile +- page: admin-network-network · Network → Interfaces → Devices → Bridge VLAN Filtering (efa07d8 body: "Network -> Interfaces -> Devices -> Bridge VLAN Filtering") +- package: stock luci-mod-network +- dom: `.table tr.cbi-section-table-descr` — global selector, added for this page +- symptom: not recorded beyond "to improve layout" (efa07d8 body) +- cause: unknown (efa07d8 body states the action, not what the descr row did wrong) +- fix: components/_table.css `&.table-titles, &.cbi-section-table-titles, &.cbi-section-table-descr { max-md:hidden }` (line 58-62) — `.cbi-section-table-descr` was added to the existing list · efa07d8 2025-09-18 · 0.3.7_alpha (first tag v0.4.0-alpha) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active + +## ifacebox-cells-full-row — interface status/box cells stack full-width on mobile +- page: admin-network-network · Network → Interfaces (menu.d title "Interfaces") +- package: stock luci-mod-network +- dom: `td[data-name="_ifacebox"]`, `td[data-name="_ifacestat"]` — DummyValue cells of the `interface` GridSection (interfaces.js:296-297 query `[data-name="_ifacestat"] > div` and `[data-name="_ifacebox"] .ifacebox-body`) +- symptom: not recorded (bec6830 subject: "feat: enhance ifacebox display in td") +- cause: unknown (commit bec6830 has subject only) +- fix: components/_table.css `[data-page="admin-network-network"] & { &[data-name="_ifacebox"], &[data-name="_ifacestat"] { … } }` nested under `& td, & .td` (line 214-219) `max-md:basis-[100%]` · bec6830 2025-09-23 · 0.4.3_alpha (first tag v0.5.0_beta). The same commit added the `td &` variants of `.ifacebox` that 397e46b later moved under `#cbi-network-interface` (next entry) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active + +## interface-cards-floor — equal-width interface cards, horizontal card layout on mobile +- page: admin-network-network · Network → Interfaces +- package: stock luci-mod-network +- dom: `#cbi-network-interface` (id of `form.Map('network')` + GridSection `'interface'`, interfaces.js:497-501) — five descendant rules on `.ifacebox`, `.ifacebox-head`, `.ifacebox-body`, `.ifacebox-body > div`, `.ifacebox-body small` +- symptom: 397e46b body: "The interfaces overview sizes every card by its content, so a bridge carrying seven device icons rendered far wider than a single-port one." +- cause: 397e46b body: "The shared `.ifacebox` min-width had been masking half of that until 1.2.7 lowered it to stop the port status grid overlapping. The floor now sits on the overview's own section instead — `#cbi-network-interface`, the scope luci-theme-bootstrap already uses for this page — at 152px, and the five `td &` rules that only ever applied there move under it too rather than reaching every `.ifacebox` inside any table cell." +- fix: components/_card.css `#cbi-network-interface &` at line 72-74 (`.ifacebox`: `max-md:flex-row md:min-w-38`), 83-85 (`.ifacebox-head`: `max-md:flex max-md:w-auto max-md:shrink-0 max-md:items-center max-md:justify-center max-md:rounded-l-3xl max-md:rounded-tr-none max-md:border-r max-md:border-b-0`), 107-109 (`.ifacebox-body`: `max-md:flex-row max-md:items-center max-md:rounded-r-3xl max-md:rounded-bl-none max-md:py-2 max-md:pr-2 max-md:pl-4`), 144-146 (`.ifacebox-body > div`: `max-md:w-auto max-md:flex-1 max-md:space-y-1`), 163-165 (`.ifacebox-body small`: `max-md:mt-0`) · 397e46b 2026-08-30 · 1.2.10 (first tag v1.3.0). The mobile rules were first written as `td &` in bec6830 (2025-09-23) +- verified: no before/after measurement of the card widths recorded; the body states the size changes it made: "icons 24px -> 20px, the gap between adjacent ones 8px -> 4px, card padding 16px -> 12px and the label line spacing 16px -> 4px", floor 152px, `.ifacebox-body > img` / badge icons "intrinsic 32px" → 20px, zone swatch 33px tall → vertical padding 2px +- fixture: none (backfill) +- issue: — · mirror: pending · status: active + +## bridge-vlan-dropdown-clip — let the Bridge VLAN table's dropdowns escape their wrapper on mobile +- page: admin-network-network · Network → Interfaces → Devices → Configure → Bridge VLAN filtering (path from efa07d8's wording of the same table; c5a2dff itself only says "bridge-vlan table") +- package: stock luci-mod-network +- dom: `[data-name="bridge-vlan"] > div` — the SectionValue option `bridge-vlan` wrapping the Bridge VLAN TableSection (tools/network.js:1354); patch rule under `@media (width < 48rem)` +- symptom: dropdown clipped on mobile (c5a2dff subject: "fix(mobile): resolve clipped dropdown in bridge-vlan table") +- cause: unknown (commit c5a2dff has subject only; the diff shows only the override, not which rule clipped the dropdown) +- fix: patches/admin-network-network.css line 8-12 `@media (width < 48rem) { [data-name="bridge-vlan"] > div { overflow: visible !important; } }` · c5a2dff 2025-10-19 · 0.6.1_beta (first tag v0.7.0_beta) — written as `@layer patches > admin-network-network { … @apply max-md:!overflow-visible }` in main.css, moved to `_patches.css` by aedf3dc, split into its own on-demand file by 1c924cf (#82, `@reference` + `@apply`), rewritten as native CSS by e14cfcd +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active + +See also: e86b70f lists "Network => (Interfaces, Wireless, Firewall)" for the `.cbi-section-actions` mobile rule — recorded in `admin-status-overview.md`. diff --git a/luci-theme-aurora/.dev/compat/pages/admin-network-wireless.md b/luci-theme-aurora/.dev/compat/pages/admin-network-wireless.md new file mode 100644 index 000000000..d5a0eec5a --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-network-wireless.md @@ -0,0 +1,14 @@ +# admin-network-wireless + +## assoclist-full-row-cells — Associated Stations cells take a full row on mobile +- page: admin-network-wireless · Network → Wireless, "Associated Stations" table (wireless.js:2667 creates `#wifi_assoclist_table`, h3 at :2680). The same id is also created by luci-mod-status 60_wifi.js:243 on Status → Overview; 1dbd95a does not say which page it was written against +- package: stock luci-mod-network (and luci-mod-status for the overview copy) +- dom: `#wifi_assoclist_table td`, `#wifi_assoclist_table .td` (class `table assoclist`); rule also moves the `data-title` ::before label (`before:max-md:mb-1.5`) +- symptom: not recorded (1dbd95a subject: "Ensure each table cell in Associated Stations takes up a full row on mobile") +- cause: unknown (commit 1dbd95a has subject only) +- fix: components/_table.css `#wifi_assoclist_table &` nested under `& td, & .td` (line 156-158) `max-md:flex-[0_0_100%] max-md:px-3 max-md:py-2 before:max-md:mb-1.5` · 1dbd95a 2025-10-17 · 0.5.14_beta (first tag v0.6.0_beta) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active + +See also: 50c2b84 lists "Network => Wireless host: Fix overflow when td text is too long" as one motivation for `.td.cbi-value-field { break-all }` — that lineage is recorded in `admin-services-dockerman.md` (the rule's current form comes from a2d110a). e86b70f lists "Network => (Interfaces, Wireless, Firewall)" for the `.cbi-section-actions` mobile rule — recorded in `admin-status-overview.md`. diff --git a/luci-theme-aurora/.dev/compat/pages/admin-services-dockerman.md b/luci-theme-aurora/.dev/compat/pages/admin-services-dockerman.md new file mode 100644 index 000000000..d77fe8f1f --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-services-dockerman.md @@ -0,0 +1,13 @@ +# admin-services-dockerman + +## info-table-entry-column — value cells break only words that do not fit + +- page: admin-services-dockerman-overview · Services → Dockerman JS → Overview +- package: luci-app-dockerman (openwrt/luci, the JS rewrite) +- dom: `.cbi-section.cbi-tblsection > table.table.cbi-section-table > tr.cbi-section-table-row > td.td.cbi-value-field[data-name=entry|value]` — `docker info` rendered as two-column TableSections; the value column holds multi-KB JSON with no spaces (Runtimes, Plugins) +- symptom: the entry column collapsed to ~42px and showed one character per line (`MemoryLimit` over 7–8 lines) beside the JSON values +- cause: `.td.cbi-value-field` carried `word-break: break-all` at every width (added in 50c2b84 as an overflow guard). `break-all` drops every cell's min-content to about one character; auto table layout then hands spare width out by max-content, so the JSON took nearly all of it. bootstrap sets no word-break on desktop cells — its labels stay whole but the JSON stretches the table to ~3.5k px and the page scrolls sideways. +- fix: `components/_table.css` `& td, & .td { &.cbi-value-field { wrap-anywhere md:max-w-prose } }` — `wrap-anywhere` keeps break-all's min-content (same overflow guard) but only breaks a word that does not fit; `md:max-w-prose` caps each cell's intrinsic contribution so one huge value cannot starve the other columns (the used width may still exceed it: 944px measured) · a2d110a · 1.3.2. Lineage: 50c2b84 2025-10-12 (0.5.8_beta) added `&.cbi-value-field { break-all }` for "System => (Software, Mount Points): Fix overflow when td text is too long" and "Network => Wireless host: Fix overflow when td text is too long", replacing the global `td max-w-94` it removed for passwall (see `admin-services-passwall.md`); 031283c 2026-07-09 added the mobile-card utilities on the same rule (`max-md:mb-1 max-md:basis-full max-md:self-start max-md:text-left` and the `[data-widget=…] max-md:basis-1/2` sub-rule) +- verified: device, Chromium, Environment table before → after: entry column 42 → 260px at 1440, 42 → 196 at 1024, 41 → 152 at 800; short words split across lines 41 → 0 / 0 / 2; mobile cards unchanged; section height 8301 → 3797px at 1440; firewall zone and rule GridSections kept identical column widths at 1440/1024/800/390; a file:// fixture gave the same result in Chromium, WebKit and Firefox. Side effect noted, not measured: on admin-system-mounts the GridSection value cells take the 65ch cap instead of the page's `max-w-104` (same specificity, later rule). +- fixture: fixtures/admin-services-dockerman-overview--info-table.html (entry column `nowrap` at 1440/1024/390 — at 800 two short words still split, so that width is not asserted) +- issue: #96 · mirror: shadcn f4d7219 · status: active diff --git a/luci-theme-aurora/.dev/compat/pages/admin-services-mini-diskmanager.md b/luci-theme-aurora/.dev/compat/pages/admin-services-mini-diskmanager.md new file mode 100644 index 000000000..73802717b --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-services-mini-diskmanager.md @@ -0,0 +1,13 @@ +# admin-services-mini-diskmanager + +## partition-table-font — the table's own font-size reaches its cells + +- page: admin-services-mini-diskmanager-minidiskmanager · Services → Disk Manager +- package: luci-app-mini-diskmanager (4IceG/luci-app-mini-diskmanager) +- dom: `table#partition-table- > tr.tr > td.td` — the `` carries inline `table-layout:fixed; width:100%; font-size:12px`; first `th` 180px, the other seven `width:12.5%; min-width:90px`; the type cell is ` NAME` +- symptom: type/filesystem and mount-point cells wrap — swatch on one line, name below; long names break mid-word (`0BOOTCONFIG` / `1`, `/mnt/mmcblk0` / `p6`) +- cause: `td, .td` set `text-base` (16px) directly, so the table's inline 12px never reached the cells; fixed columns sized for 12px got 16px text and `wrap-break-word` split what did not fit. bootstrap puts its font size on `.table` and lets cells inherit. +- fix: `components/_table.css` `table.table, .table` `text-base max-md:text-sm` (moved off `td, .td`, which now inherit) · dd7c64b · 1.3.3 +- verified: device 192.168.8.1, CDP before/after in one page load — cells 16 → 12px; wrapped cells 5 → 1 at 1280 (the remaining `0BOOTCONFIG1` wraps under bootstrap at that width too), 5 → 0 at 1920, 0 → 0 at 390; eleven stock pages (overview, routes, processes, interfaces, firewall zones/rules, DHCP, wireless, mounts, startup, SSH keys) showed zero font-size/line-height changes on any text-bearing table element at 1280 and 390 +- fixture: fixtures/admin-services-mini-diskmanager-minidiskmanager--partition-table.html +- issue: #65 · mirror: shadcn 4f1c7f4 · status: active diff --git a/luci-theme-aurora/.dev/compat/pages/admin-services-openclash-config.md b/luci-theme-aurora/.dev/compat/pages/admin-services-openclash-config.md new file mode 100644 index 000000000..cad46a720 --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-services-openclash-config.md @@ -0,0 +1,12 @@ +# admin-services-openclash-config + +## openclash-subinfo-icons — fix the subscription-info icon size +- page: admin-services-openclash-config · Services → OpenClash → Config Manage (controller openclash.lua:84 entry title "Config Manage") +- package: luci-app-openclash (vernesong; the ImmortalWrt feed copy was checked) +- dom: `.sub_div img` — `.sub_div` comes from `luasrc/view/openclash/sub_info_show.htm`, the template used by config.lua:191 +- symptom: not recorded (87522d8 subject: "feat: add openclash patch and remove btn spacing") +- cause: unknown (commit 87522d8 has subject only) +- fix: patches/admin-services-openclash-config.css line 8-13 `[data-page="admin-services-openclash-config"] { .sub_div img { height: 1.25rem; width: 1.25rem; } }` · 87522d8 2025-09-17 · 0.3.6_alpha (first tag v0.4.0-alpha) — written as `@layer patches > luci-app-openclash { [data-page=…] .sub_div img { @apply h-5 w-5 } }` in main.css; `_patches.css` via aedf3dc; own file via 1c924cf; native via e14cfcd +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active diff --git a/luci-theme-aurora/.dev/compat/pages/admin-services-openclash-log.md b/luci-theme-aurora/.dev/compat/pages/admin-services-openclash-log.md new file mode 100644 index 000000000..dedf90662 --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-services-openclash-log.md @@ -0,0 +1,12 @@ +# admin-services-openclash-log + +## openclash-log-dark-bg — dark page background on the Server Logs page +- page: admin-services-openclash-log · Services → OpenClash → Server Logs (controller openclash.lua:85 entry title "Server Logs") +- package: luci-app-openclash (vernesong) +- dom: `[data-page="admin-services-openclash-log"]` (the body element itself) +- symptom: not recorded (87522d8 subject only) +- cause: unknown (commit 87522d8 has subject only) +- fix: (historic, main.css `@layer patches > luci-app-openclash`) `[data-page="admin-services-openclash-log"] { @apply dark:bg-aurora-night-teal; }` · 87522d8 2025-09-17 · 0.3.6_alpha (first tag v0.4.0-alpha) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: n/a · status: removed 30f922d (2025-11-09 "chore: remove patch styles and adjust DOM indentation" — body: "Removed temporary patch styles … Test case: openclash → Server Logs (dark mode background)"; 0.7.9_beta, first tag v0.8.0_beta) diff --git a/luci-theme-aurora/.dev/compat/pages/admin-services-openclash-settings.md b/luci-theme-aurora/.dev/compat/pages/admin-services-openclash-settings.md new file mode 100644 index 000000000..a93392b46 --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-services-openclash-settings.md @@ -0,0 +1,23 @@ +# admin-services-openclash-settings + +## openclash-settings-select-full — selects fill the value field on mobile +- page: admin-services-openclash-settings · Services → OpenClash → Plugin Settings (controller openclash.lua:71 entry title "Plugin Settings") +- package: luci-app-openclash (vernesong) +- dom: `.cbi-value-field .cbi-input-select` under `@media (width < 48rem)` +- symptom: not recorded (a667f02 subject: "feat: add openclash cbi-input-select style patch") +- cause: unknown (commit a667f02 has subject only; its diff is a whole-file rewrite of main.css, the rule as first written was `[data-page="admin-services-openclash-settings"] .cbi-value-field .cbi-input-select { @apply max-md:!w-full }`) +- fix: patches/admin-services-openclash-settings.css line 9-13 `@media (width < 48rem) { .cbi-value-field .cbi-input-select { width: 100% !important; } }` · a667f02 2025-09-26 · 0.4.6_alpha (first tag v0.5.0_beta) — `_patches.css` via aedf3dc; own file via 1c924cf; native via e14cfcd +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active + +## openclash-diag-grid — stack the Debug Logs diagnostic controls in one column on mobile +- page: admin-services-openclash-settings · Services → OpenClash → Plugin Settings → Debug Logs tab (settings.lua:81 tab "debug", :1308 DummyValue rendering `debug.htm`) +- package: luci-app-openclash (vernesong) +- dom: `.diag-style` and `.diag-style > div` — from `luasrc/view/openclash/debug.htm` (which itself styles `.diag-style` for `[data-darkmode="true"]` at its line 12-17) +- symptom: not recorded (87522d8 subject only) +- cause: unknown (commit 87522d8 has subject only) +- fix: patches/admin-services-openclash-settings.css line 15-33 `.diag-style { & > div { display: flex; flex-direction: column; align-items: center; gap: 0.5rem } @media (width < 48rem) { display: grid; grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 1rem; & > div { width: 100% !important; gap: 0.25rem } } }` · 87522d8 2025-09-17 · 0.3.6_alpha (first tag v0.4.0-alpha) — first written unscoped under `@layer plugins > luci-app-openclash` (applied on every page); 7d9f6a1 merged `_plugins.css` into `_patches.css`; 1c924cf scoped it to this page (PR #82: "openclash `.diag-style` → settings"); native via e14cfcd +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active diff --git a/luci-theme-aurora/.dev/compat/pages/admin-services-passwall.md b/luci-theme-aurora/.dev/compat/pages/admin-services-passwall.md new file mode 100644 index 000000000..85bba9fdf --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-services-passwall.md @@ -0,0 +1,14 @@ +# admin-services-passwall + +## td-global-max-width — a global `max-w-94` on every table cell +- page: admin-services-passwall-node_list · Services → PassWall → Node List (controller passwall.lua:39 entry title "Node List") — the page that got the rule removed; the page it was added for is not recorded +- package: luci-app-passwall (xiaorouji; the ImmortalWrt feed copy was checked) +- dom: `.table td, .table .td` (global) — on Node List the `td.cbi-section-actions` cell holds the row's action buttons +- symptom: on removal — 50c2b84 subject: "Services => passwall => Node List cbi-section-actions: Fix overflow beyond td max width" (the action buttons ran past the capped cell). On addition — not recorded +- cause: addition: unknown (efa07d8's body describes two other changes — "Removed conflicting styling that prevented setting `overflow-auto`, which was necessary to maintain the sticky positioning of `th` elements" and hiding `.cbi-section-table-descr` on the Bridge VLAN page — and says nothing about `max-w-94`, which the diff adds to td alongside `max-md:max-w-full` and col caps). Removal: the fixed cap could not hold the Node List action cell (50c2b84 subject); d276fd3 later restates the lesson: "the fix avoids applying a global maximum width to table data cells (`td`) to prevent regressions in CBI section actions (e.g., in Services -> passwall -> Node List)" +- fix: (historic, main.css `& td, & .td`) `max-w-94` · efa07d8 2025-09-18 · 0.3.7_alpha (first tag v0.4.0-alpha) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: n/a · status: removed 50c2b84 (2025-10-12 "fix: table content overflow", 0.5.8_beta, first tag v0.6.0_beta). The same commit added `.td.cbi-value-field { break-all }` as the replacement overflow guard for "System => (Software, Mount Points)" and "Network => Wireless host" — see `admin-services-dockerman.md`; per-page caps came back scoped: `[data-page="admin-system-mounts"]` (d276fd3, `admin-system-mounts.md`) and `[id*="status_leases"]` (80be54c, `admin-status-overview.md`) + +See also: bbb355b 2025-10-26 "fix: resolve tooltip flicker and add footer spacing" — body: "added top margin to the footer to avoid overlap with main content in plugins that lack proper view containers (e.g., PassWall)"; a global `footer` rule (currently _layout.css line 644), not traced further here. diff --git a/luci-theme-aurora/.dev/compat/pages/admin-services-samba4.md b/luci-theme-aurora/.dev/compat/pages/admin-services-samba4.md new file mode 100644 index 000000000..d283801ca --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-services-samba4.md @@ -0,0 +1,23 @@ +# admin-services-samba4 + +## samba4-cell-padding — tighter th/td padding inside the Network Shares form +- page: admin-services-samba4 · Services → Network Shares (title from luci-app-samba4 menu.d; 1dbd95a writes it as "NAS => Network Shares => Shared Directories") +- package: stock luci-app-samba4 +- dom: `#cbi-samba4` (id of `form.Map('samba4')`, samba4.js:23) containing the `sambashare` TableSection "Shared Directories" (samba4.js:93); rule hits every `th/.th` and `td/.td` under it +- symptom: not recorded (1dbd95a subject: "Reduce cell padding in NAS => Network Shares => Shared Directories") +- cause: unknown (commit 1dbd95a has subject only; no symptom or measurement) +- fix: components/_table.css `#cbi-samba4 &` (nested under `& th, & .th`, line 114-116, and under `& td, & .td`, line 148-150) `px-0.5 py-1.5` · 1dbd95a 2025-10-17 · 0.5.14_beta (first tag v0.6.0_beta) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active + +## samba4-section-overflow — horizontal scroll on the samba4 card +- page: admin-services-samba4 · Services → Network Shares (menu path inferred from the page name) +- package: stock luci-app-samba4 +- dom: `#cbi-samba4 .cbi-section` +- symptom: not recorded (2e1d18a subject: "table overflow issue on samba4, package, and autoreboot pages") +- cause: unknown (commit 2e1d18a has subject only). The diff shows the section went from `overflow-x-auto` to `overflow-visible` globally and got `#cbi-samba4 & { @apply overflow-x-auto; }` back for this page only, alongside dropping `table-fixed` from `.table` +- fix: (historic, main.css `.cbi-section`) `#cbi-samba4 & { @apply overflow-x-auto; }` · 2e1d18a 2025-08-31 · 0.1.3_alpha (first tag v0.2.0-alpha) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: n/a · status: removed 6b9b328 (2025-09-03 "feat: make dropdown fill based on content, simplify table" — `.cbi-section` went back to global `overflow-x-auto`, so the per-page override was deleted with it) diff --git a/luci-theme-aurora/.dev/compat/pages/admin-statistics-graphs.md b/luci-theme-aurora/.dev/compat/pages/admin-statistics-graphs.md new file mode 100644 index 000000000..a2cfa2ae7 --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-statistics-graphs.md @@ -0,0 +1,12 @@ +# admin-statistics-graphs + +## rrd-graphs-dark — invert and hue-rotate the rendered graph images in dark mode +- page: admin-statistics-graphs · Statistics → Graphs (menu.d title "Graphs"; edf378d: "Affected pages: Statistics => Graphs") +- package: stock luci-app-statistics +- dom: `[data-plugin] img` — the per-plugin tab containers built in graphs.js:75 (`'data-plugin': plugin`) holding the rrdtool PNGs; dark scope is `[data-darkmode="true"]` on `` +- symptom: not recorded beyond "Adapt img background color for dark mode" (edf378d body) +- cause: edf378d body: "Adapt img background color for dark mode" — the graphs are server-rendered images whose light background does not follow the theme; nothing further recorded +- fix: patches/admin-statistics-graphs.css line 8-10 `[data-darkmode="true"] [data-page="admin-statistics-graphs"] [data-plugin] img { filter: hue-rotate(150deg) invert(100%); }` · edf378d 2025-10-17 · 0.5.16_beta (first tag v0.6.0_beta) — written as `@layer luci-app-statistics { [data-page=…] [data-plugin] img { @apply dark:hue-rotate-150 dark:invert } }` in main.css; `_plugins.css` after aedf3dc, merged into `_patches.css` by 7d9f6a1, own file via 1c924cf, native via e14cfcd (the e14cfcd body cites this file: "the two-rule statistics patch alone went 1,609 B → 110 B") +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active diff --git a/luci-theme-aurora/.dev/compat/pages/admin-status-logs.md b/luci-theme-aurora/.dev/compat/pages/admin-status-logs.md new file mode 100644 index 000000000..2b9e35cad --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-status-logs.md @@ -0,0 +1,12 @@ +# admin-status-logs + +## log-viewer — parse the read-only `#syslog` textarea into a column-aligned, colour-coded view +- page: admin-status-logs (prefix; covers `admin-status-logs-syslog`, `admin-status-logs-dmesg` and the bare `/logs` alias) · Status → System Log / Kernel Log (menu.d: "admin/status/logs" "System Log" alias → "…/syslog" "System Log", "…/dmesg" "Kernel Log") +- package: stock luci-mod-status +- dom: `textarea#syslog` inside `#content_syslog`; the patch inserts `div.syslog-view` (rows `.log-row.lv-*` with `.log-time/.log-sev/.log-tag/.log-msg`) after the textarea and adds `.log-enhanced` to the container. Styling lives in components/_syslog.css in main.css, not in a patch +- symptom: none — this is an enhancement, not a breakage fix (a5b44da: "The first payload reworks the System/Kernel Log pages") +- cause: a5b44da body: "the read-only #syslog textarea stays in place as LuCI's polled data source while a dirty-checked viewer parses each line (classic logread and the bracketed RPC format, whitelist-gated) into aligned time/level/source columns with per-severity colors, whole lines scrolling sideways, and a flash-free takeover via an insertion observer. Unknown formats trip a parse gate and the stock textarea simply shows again. One admin-status-logs name covers both tabs on every supported release — 23.05, 24.10 and master all mount the pages under admin/status/logs/*." Why the CSS is not a patch: "The viewer's CSS is core-page styling, not third-party compat, so it lives in components/_syslog.css inside main.css where the full utility pipeline applies" +- fix: src/resource/patches/admin-status-logs.js (whole file; router lifecycle `window.luciPatches["admin-status-logs"] = { mount, unmount }` at line 292-295, `mount()` at line 296) · a5b44da 2026-07-27 · 1.1.7 (first tag v1.2.0). Follow-ups: ee16953 2026-07-29 (1.1.9) "contain select-all, follow the tail, leave dmesg unsplit" — Ctrl+A grabbed the whole page, tail-follow when pinned to the bottom, dmesg rendered verbatim because "kernel lines have no uniform tag grammar"; e681ecb 2026-07-29 (1.1.10) "render every log token verbatim, copies byte-identical" — timestamps/colons were being rewritten, now "joining the rendered tokens with single spaces reproduces the source line byte for byte, and a test pins that round-trip"; 664c795 2026-08-16 (1.2.1) and e748f7c 2026-08-26 (1.2.6) wired the mount/unmount lifecycle for the same-document router (subjects only read) +- verified: a5b44da: "Node tests pin the parser against each format family and the payload's home" (`.dev/tests/log-patch.test.js`); e681ecb: round-trip test. No rendering measurement recorded +- fixture: none (backfill; DOM snapshot exists in `.dev/mocks/admin-status-logs-syslog.html`) +- issue: — · mirror: pending · status: active diff --git a/luci-theme-aurora/.dev/compat/pages/admin-status-overview.md b/luci-theme-aurora/.dev/compat/pages/admin-status-overview.md new file mode 100644 index 000000000..bf369a080 --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-status-overview.md @@ -0,0 +1,34 @@ +# admin-status-overview + +## leases-td-max-width — cap DHCP lease cells so DUID/IAID columns cannot widen the table +- page: admin-status-overview · Status → Overview, "Active DHCP Leases" / "Active DHCPv6 Leases" tables (luci-mod-status 40_dhcp.js:110 `#status_leases`, :169 `#status_leases6`; `[id*=]` matches both). The commit subject says "DHCP > Leases"; the ids exist only in this status include +- package: stock luci-mod-status +- dom: `.table[id*="status_leases"] .td` (class `table leases` / `table leases6`, div-based `.td` cells) +- symptom: table overflow from the DUID/IAID columns (80be54c subject) +- cause: unknown (commit 80be54c has subject only; no DOM or widths recorded) +- fix: components/_table.css `&[id*="status_leases"] { & .td { … } }` (line 137-141) `max-w-62 max-md:max-w-full` · 80be54c 2025-12-12 · 0.9.1_beta (first tag v0.10.0) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active + +## section-actions-mobile — action-button cell spans the mobile card, buttons share the row +- page: admin-status-overview · Status → Overview → Active DHCP Leases; also System → Mount Points and Network → Interfaces / Wireless / Firewall (all five listed in e86b70f) +- package: stock luci-mod-status / luci-mod-system / luci-mod-network (per the pages listed) +- dom: `.table td.cbi-section-actions` (`> div` wrapper, `> div > .cbi-button`) and `.table td > .cbi-button` +- symptom: not recorded (e86b70f subject: "cbi-button display on mobile for cell view") +- cause: unknown (commit e86b70f has subject only) +- fix: components/_table.css `&.cbi-section-actions` nested under `& td, & .td` (line 190-200) `max-md:flex-[0_0_100%]`, `& > div` `max-md:w-full`, `& > div > .cbi-button` `max-md:flex-1`; plus `& > .cbi-button` (line 202-204) `max-md:w-full` · e86b70f 2025-10-17 · 0.5.15_beta (first tag v0.6.0_beta) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: pending · status: active + +## section-actions-mobile-v0 — original scaffold rule for the actions cell on mobile +- page: unknown (076281f is the initial scaffold commit; no page recorded) +- package: unknown +- dom: `.table td.cbi-section-actions` (`& div`, `& .btn, & .cbi-button`) and a sibling `.td.cbi-section-actions { p-2 max-md:p-1 … }` block in the button layer +- symptom: not recorded +- cause: unknown (076281f "chore: setup luci-theme-aurora with Vite and TailwindCSS" has subject only) +- fix: (historic, main.css) `&.cbi-section-actions { @apply max-md:mt-3 max-md:border-t max-md:pt-3; & div { max-md:flex max-md:flex-wrap max-md:justify-start max-md:gap-2 } & .btn, & .cbi-button { max-md:min-w-0 max-md:flex-1 … } }` · 076281f 2025-08-25 · 0.1.0 (first tag v0.2.0-alpha) +- verified: not measured (no record) +- fixture: none (backfill) +- issue: — · mirror: n/a · status: removed 386dec5 (2025-08-30 "feat: optimize mobile display for table" rewrote the mobile table block and dropped it; the `.td.cbi-section-actions` button-layer block was dropped by 4c99ac8 2025-09-05 "chore: adjust button nesting style structure", which replaced it with the generic `.cbi-section-actions > div { flex flex-row … }`). e86b70f re-added a different rule seven weeks later — see the entry above diff --git a/luci-theme-aurora/.dev/compat/pages/admin-system-diskman.md b/luci-theme-aurora/.dev/compat/pages/admin-system-diskman.md new file mode 100644 index 000000000..496a21027 --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-system-diskman.md @@ -0,0 +1,12 @@ +# admin-system-diskman + +## diskman-partition-bar — keep the injected partition bar inside the card on small screens, clip segment labels +- page: admin-system-diskman (prefix; also `-partition-`, `-btrfs-`) · System → Disk Man (controller diskman.lua:24 entry title "Disk Man", alias to `disks`; issue #58 calls it the storage area on the home page) +- package: luci-app-diskman (lisaac; the ImmortalWrt feed copy was checked) +- dom: `td[colspan]:has(> div[title])` and `td[colspan] > div[title]`, `.tr > div[style*="nowrap"] > div[title]` — disk_info.htm:34/88 emit `
    ` and :36 `
    `, with one inline-block `div[title]` per partition sized in percent +- symptom: issue #58: "首页的存储区域会超出宽度,导致页面可以左右滑动" (the storage area exceeds the width; page scrolls sideways), Safari 26 / iOS 26, OpenWrt 24.10.5, theme 0.11; 49fff90 body: "drags the page ~300px wide" +- cause: 49fff90 body: "luci-app-diskman injects the partition bar as a full-span row whose cell carries an inline white-space:nowrap. Once .tr becomes a flex row on mobile, that cell's automatic minimum size is its min-content — the whole unwrapped bar — so it cannot shrink and drags the page ~300px wide (reported in #58 with the page DOM). … clip each segment so its label stops spilling onto the neighbouring one (visible on desktop too)". The file adds: "The segments are inline-blocks sized in percent: any whitespace between them adds a word space on top of the 100% they already sum to" (hence `font-size: 0` on the cell) +- fix: patches/admin-system-diskman.css line 9-35 `[data-page^="admin-system-diskman"] { td[colspan]:has(> div[title]) { flex: 0 0 100%; min-width: 0; overflow: hidden; border-radius: var(--radius-base); font-size: 0 } td[colspan] > div[title], .tr > div[style*="nowrap"] > div[title] { overflow: hidden; font-size: 0.75rem; line-height: 1.5rem; text-overflow: ellipsis } }` · 49fff90 2026-08-02 · 1.1.13 (first tag v1.2.0) +- verified: not measured (the body states the before width, "~300px wide", from the reporter's DOM; no after-number recorded) +- fixture: none (backfill; DOM snapshot exists in `.dev/mocks/admin-system-diskman.html`) +- issue: #58 · mirror: pending · status: active diff --git a/luci-theme-aurora/.dev/compat/pages/admin-system-filemanager.md b/luci-theme-aurora/.dev/compat/pages/admin-system-filemanager.md new file mode 100644 index 000000000..aa7dc8932 --- /dev/null +++ b/luci-theme-aurora/.dev/compat/pages/admin-system-filemanager.md @@ -0,0 +1,12 @@ +# admin-system-filemanager + +## filemanager-container-scroll — scrollable file-manager container, flat status bar +- page: admin-system-filemanager · System → File Manager (menu.d title "File Manager") +- package: luci-app-filemanager (in openwrt/luci applications) +- dom: `#file-manager-container` (the app's own `