diff --git a/.github/diy/packages/dockerd/Makefile b/.github/diy/packages/dockerd/Makefile.k similarity index 100% rename from .github/diy/packages/dockerd/Makefile rename to .github/diy/packages/dockerd/Makefile.k diff --git a/dockerd/Makefile b/dockerd/Makefile index 9827df6c7..f3c31741f 100644 --- a/dockerd/Makefile +++ b/dockerd/Makefile @@ -1,30 +1,33 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dockerd -PKG_FLAGS:=hold -PKG_VERSION:=27.3.1 -PKG_RELEASE:=8 +PKG_VERSION:=29.6.1 +PKG_RELEASE:=9 PKG_LICENSE:=Apache-2.0 PKG_LICENSE_FILES:=LICENSE +PKG_CPE_ID:=cpe:/a:mobyproject:moby PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_GIT_URL:=github.com/moby/moby -PKG_GIT_REF:=v$(PKG_VERSION) +PKG_GIT_REF:=docker-v$(PKG_VERSION) PKG_SOURCE_URL:=https://codeload.$(PKG_GIT_URL)/tar.gz/$(PKG_GIT_REF)? PKG_HASH:=skip -PKG_GIT_SHORT_COMMIT:=41ca978 # SHA1 used within the docker executables +PKG_GIT_SHORT_COMMIT:=8ec5ab3 # SHA1 used within the docker executables PKG_MAINTAINER:=Gerard Ryan PKG_BUILD_DEPENDS:=golang/host PKG_BUILD_PARALLEL:=1 -PKG_BUILD_FLAGS:=no-mips16 GO_PKG:=github.com/docker/docker include $(INCLUDE_DIR)/package.mk include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk +EMPTY:= +SPACE:=$(EMPTY) $(EMPTY) +ARCH_DEPENDS:=@($(subst $(SPACE),||,$(strip $(filter-out mips mips64 mipsel mips64el,$(subst ||,$(SPACE),$(patsubst @(%),%,$(GO_ARCH_DEPENDS))))))) + define Package/dockerd/config source "$(SOURCE)/Config.in" endef @@ -34,8 +37,7 @@ define Package/dockerd CATEGORY:=Utilities TITLE:=Docker Community Edition Daemon URL:=https://www.docker.com/ - DEPENDS:=$(GO_ARCH_DEPENDS) \ - +jsonfilter \ + DEPENDS:=$(ARCH_DEPENDS) \ +ca-certificates \ +containerd \ +iptables \ @@ -48,10 +50,9 @@ define Package/dockerd +kmod-nf-ipvs \ +kmod-veth \ +tini \ - +uci-firewall \ +fuse-overlayfs \ +kmod-vxlan +kmod-macvlan +kmod-dummy +kmod-crypto-gcm +kmod-crypto-seqiv +kmod-crypto-ghash +kmod-ipsec +kmod-nf-nathelper +kmod-nf-nathelper-extra +btrfs-progs \ - @!(mips||mips64||mipsel) + +uci-firewall USERID:=docker:docker MENU:=1 endef @@ -61,65 +62,45 @@ define Package/dockerd/conffiles endef define Package/dockerd/description -The Docker CE Engine. + The Docker CE Engine. endef TAR_OPTIONS:=--strip-components 1 $(TAR_OPTIONS) TAR_CMD=$(HOST_TAR) -C $(1) $(TAR_OPTIONS) TARGET_LDFLAGS += $(if $(CONFIG_USE_GLIBC),-lc -lgcc_eh) -# $(1) = path to dependent package 'Makefile' -# $(2) = relevant dependency '.installer' file +# $(1) = OpenWrt dependent PKG_NAME define EnsureVendoredVersion - ( \ - DEP_VER=$$$$( grep --only-matching --perl-regexp '(?<=PKG_VERSION:=)(.*)' "$(1)" ); \ - VEN_VER=$$$$( grep --only-matching --perl-regexp '(?<=_VERSION:=v)(.*)(?=})' "$(PKG_BUILD_DIR)/hack/dockerfile/install/$(2)" ); \ - if [ "$$$${VEN_VER}" != "$$$${DEP_VER}" ]; then \ - echo "ERROR: $(PKG_NAME) Expected 'PKG_VERSION:=$$$${VEN_VER}' in '$(1)', found 'PKG_VERSION:=$$$${DEP_VER}'"; \ - exit 1; \ - fi \ - ) -endef - -# $(1) = path to dependent package 'Makefile' -# $(2) = relevant dependency '.installer' file -define EnsureVendoredCommit - ( \ - DEP_VER=$$$$( grep --only-matching --perl-regexp '(?<=PKG_SOURCE_VERSION:=)(.*)' "$(1)" ); \ - VEN_VER=$$$$( grep --only-matching --perl-regexp '(?<=_COMMIT:=)(.*)(?=})' "$(PKG_BUILD_DIR)/hack/dockerfile/install/$(2)" ); \ - if [ "$$$${VEN_VER}" != "$$$${DEP_VER}" ]; then \ - echo "ERROR: Expected 'PKG_SOURCE_VERSION:=$$$${VEN_VER}' in '$(1)', found 'PKG_SOURCE_VERSION:=$$$${DEP_VER}'"; \ - exit 1; \ - fi \ - ) + DEP_VER=$$$$( grep --only-matching --perl-regexp '(?<=PKG_VERSION:=)(.*)' "../../packages/utils/$(1)/Makefile" ); \ + VEN_VER=$$$$( grep --only-matching --perl-regexp '(?<=$(call toupper,$(1))_VERSION=v)(.*)' "$(PKG_BUILD_DIR)/Dockerfile" ); \ + if [ "$$$${VEN_VER}" != "$$$${DEP_VER}" ]; then \ + echo "ERROR: $(PKG_NAME) Expected 'PKG_VERSION:=$$$${VEN_VER}' in '$(1)', found 'PKG_VERSION:=$$$${DEP_VER}'"; \ + exit 1; \ + fi endef define Build/Prepare $(Build/Prepare/Default) # Verify dependencies are the vendored version - $(call EnsureVendoredVersion,../../packages/utils/containerd/Makefile,containerd.installer) - $(call EnsureVendoredVersion,../../packages/utils/runc/Makefile,runc.installer) - $(call EnsureVendoredVersion,../../packages/utils/tini/Makefile,tini.installer) + $(call EnsureVendoredVersion,containerd) + $(call EnsureVendoredVersion,runc) + $(call EnsureVendoredVersion,tini) # Verify CLI is the same version - ( \ - CLI_MAKEFILE="../../packages/utils/docker/Makefile"; \ - CLI_VERSION=$$$$( grep --only-matching --perl-regexp '(?<=PKG_VERSION:=)(.*)' "$$$${CLI_MAKEFILE}" ); \ - if [ "$$$${CLI_VERSION}" != "$(PKG_VERSION)" ]; then \ - echo "ERROR: Expected 'PKG_VERSION:=$(PKG_VERSION)' in '$$$${CLI_MAKEFILE}', found 'PKG_VERSION:=$$$${CLI_VERSION}'"; \ - exit 1; \ - fi \ - ) + CLI_MAKEFILE="../../packages/utils/docker/Makefile"; \ + CLI_VERSION=$$$$( grep --only-matching --perl-regexp '(?<=PKG_VERSION:=)(.*)' "$$$${CLI_MAKEFILE}" ); \ + if [ "$$$${CLI_VERSION}" != "$(PKG_VERSION)" ]; then \ + echo "ERROR: Expected 'PKG_VERSION:=$(PKG_VERSION)' in '$$$${CLI_MAKEFILE}', found 'PKG_VERSION:=$$$${CLI_VERSION}'"; \ + exit 1; \ + fi # Verify PKG_GIT_SHORT_COMMIT - ( \ - EXPECTED_PKG_GIT_SHORT_COMMIT=$$$$( $(CURDIR)/git-short-commit.sh '$(PKG_GIT_URL)' '$(PKG_GIT_REF)' '$(TMP_DIR)/git-short-commit/$(PKG_NAME)-$(PKG_VERSION)' ); \ - if [ "$$$${EXPECTED_PKG_GIT_SHORT_COMMIT}" != "$(strip $(PKG_GIT_SHORT_COMMIT))" ]; then \ - echo "ERROR: Expected 'PKG_GIT_SHORT_COMMIT:=$$$${EXPECTED_PKG_GIT_SHORT_COMMIT}', found 'PKG_GIT_SHORT_COMMIT:=$(strip $(PKG_GIT_SHORT_COMMIT))'"; \ - exit 1; \ - fi \ - ) + EXPECTED_PKG_GIT_SHORT_COMMIT=$$$$( $(CURDIR)/git-short-commit.sh '$(PKG_GIT_URL)' '$(PKG_GIT_REF)' '$(TMP_DIR)/git-short-commit/$(PKG_NAME)-$(PKG_VERSION)' ); \ + if [ "$$$${EXPECTED_PKG_GIT_SHORT_COMMIT}" != "$(strip $(PKG_GIT_SHORT_COMMIT))" ]; then \ + echo "ERROR: Expected 'PKG_GIT_SHORT_COMMIT:=$$$${EXPECTED_PKG_GIT_SHORT_COMMIT}', found 'PKG_GIT_SHORT_COMMIT:=$(strip $(PKG_GIT_SHORT_COMMIT))'"; \ + exit 1; \ + fi endef BUILDTAGS:= @@ -131,14 +112,12 @@ BUILDTAGS += selinux endif define Build/Compile - ( \ - cd $(PKG_BUILD_DIR); \ - $(GO_PKG_VARS) \ - DOCKER_GITCOMMIT=$(PKG_GIT_SHORT_COMMIT) \ - DOCKER_BUILDTAGS='$(BUILDTAGS)' \ - VERSION=$(PKG_VERSION) \ - ./hack/make.sh binary; \ - ) + cd $(PKG_BUILD_DIR); \ + $(GO_PKG_VARS) \ + DOCKER_GITCOMMIT=$(PKG_GIT_SHORT_COMMIT) \ + DOCKER_BUILDTAGS='$(BUILDTAGS)' \ + VERSION=$(PKG_VERSION) \ + ./hack/make.sh binary endef define Package/dockerd/install @@ -146,11 +125,11 @@ define Package/dockerd/install $(INSTALL_BIN) $(PKG_BUILD_DIR)/bundles/binary-daemon/dockerd $(1)/usr/bin/ $(INSTALL_BIN) $(PKG_BUILD_DIR)/bundles/binary-daemon/docker-proxy $(1)/usr/bin/ + $(INSTALL_DIR) $(1)/opt/docker/ + $(INSTALL_DIR) $(1)/etc/uci-defaults $(INSTALL_DATA) ./files/dockerd.uci-defaults $(1)/etc/uci-defaults/dockerd - $(INSTALL_DIR) $(1)/opt/docker/ - ifeq ($(CONFIG_DOCKER_CHECK_CONFIG),y) $(INSTALL_DIR) $(1)/usr/share/docker/ $(INSTALL_BIN) $(PKG_BUILD_DIR)/contrib/check-config.sh $(1)/usr/share/docker/ @@ -162,19 +141,10 @@ define Package/dockerd/install $(INSTALL_DIR) $(1)/etc/config $(INSTALL_CONF) ./files/etc/config/dockerd $(1)/etc/config/dockerd - $(INSTALL_DIR) $(1)/etc/hotplug.d/net - $(INSTALL_DATA) ./files/etc/hotplug.d/net/br-netfilter \ - $(1)/etc/hotplug.d/net/10-docker-br-netfilter - - $(INSTALL_DIR) $(1)/etc/uci-defaults - $(INSTALL_DATA) ./files/etc/uci-defaults/16_docker-netifd-migrate2 \ - $(1)/etc/uci-defaults/16_docker-netifd-migrate2 - $(INSTALL_DATA) ./files/etc/uci-defaults/17_docker-fw4 \ - $(1)/etc/uci-defaults/17_docker-fw4 - - $(INSTALL_DIR) $(1)/lib/fstab.d - $(INSTALL_DATA) ./files/lib/fstab.d/dockerd.sh $(1)/lib/fstab.d/dockerd.sh - + # Must be after systcl 11-br-netfilter.conf from kmod-br-netfilter + $(INSTALL_DIR) $(1)/etc/sysctl.d + $(INSTALL_DATA) ./files/etc/sysctl.d/sysctl-br-netfilter-ip.conf \ + $(1)/etc/sysctl.d/12-br-netfilter-ip.conf endef define Package/dockerd/postinst diff --git a/gately/Makefile b/gately/Makefile index ddd971c8e..d77cb4f6f 100644 --- a/gately/Makefile +++ b/gately/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=gately -PKG_VERSION:=0.6.2 -PKG_RELEASE:=20 +PKG_VERSION:=0.6.2.2 +PKG_RELEASE:=21 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)? diff --git a/honk/Makefile b/honk/Makefile index 6330e4559..c9c90a6e1 100644 --- a/honk/Makefile +++ b/honk/Makefile @@ -3,17 +3,17 @@ include $(TOPDIR)/rules.mk PKG_NAME:=honk -PKG_VERSION:=0.1.0_beta4 -PKG_RELEASE:=25 +PKG_VERSION:=415.3_pre1 +PKG_RELEASE:=26 # Raw upstream release tag. It keeps the dots in qualifiers (e.g. v0.0.1.beta.49) # because that is what the GitHub release asset name uses. -HONK_RELEASE_TAG:=v0.1.0-beta.4 -HONK_ASSET_TAG:=v0.1.0-beta.4 +HONK_RELEASE_TAG:=debug-diag.issue-415.3 +HONK_ASSET_TAG:=debug # SHA256 of each release tarball, kept in sync by scripts/update_honk_version.sh. -HONK_HASH_X86_64:=48f694b13b9c5200e8e884ee19431035bd67b0ef01d04caef007047ad11ba02c -HONK_HASH_AARCH64:=6cc92f862a1684ba9c49e9f9c08fb108f53444f338f1e9be84013e2f7fb7423d +HONK_HASH_X86_64:=a40ff27b4a1d9b5aa8a76ed05c9def01f2e5612dde506f912dc9da6df8ccc1ca +HONK_HASH_AARCH64:=8ffd3327d7039a430e0370a917fc4eaa75d85bb256f3d23753da0e1d06e57d72 PKG_LICENSE:=AGPL-3.0-only PKG_LICENSE_FILE:=LICENSE diff --git a/luci-app-adguardhome/Makefile b/luci-app-adguardhome/Makefile index 59c1bf77a..d02c9b03f 100644 --- a/luci-app-adguardhome/Makefile +++ b/luci-app-adguardhome/Makefile @@ -1,6 +1,5 @@ -# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-License-Identifier: GPL-2.0-only -PKG_LICENSE:=GPL-3.0-or-later include $(TOPDIR)/rules.mk LUCI_NAME:=luci-app-adguardhome diff --git a/luci-app-cf-optimizer/Makefile b/luci-app-cf-optimizer/Makefile new file mode 100644 index 000000000..45025935d --- /dev/null +++ b/luci-app-cf-optimizer/Makefile @@ -0,0 +1,50 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-cf-optimizer +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=Dante + +LUCI_TITLE:=LuCI support for Cloudflare Proxy and Tunnel Host Optimizer +LUCI_DEPENDS:=+luci-base +curl +ucode +ucode-mod-fs +ucode-mod-uci +ucode-mod-ubus +LUCI_PKGARCH:=all + +define Package/$(PKG_NAME)/conffiles +/etc/config/cf-optimizer +endef + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + rm -f /tmp/luci-indexcache.* /tmp/luci-modulecache/* 2>/dev/null + chmod 755 /etc/init.d/cf-optimizer /usr/sbin/cf-optimizer 2>/dev/null + /etc/init.d/cf-optimizer enable 2>/dev/null || true + /etc/init.d/rpcd restart +} +exit 0 +endef + +define Package/$(PKG_NAME)/prerm +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + /etc/init.d/cf-optimizer stop + /etc/init.d/cf-optimizer disable + /usr/sbin/cf-optimizer clean +fi +exit 0 +endef + +define Package/$(PKG_NAME)/postrm +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + rm -rf /tmp/run/cf-optimizer /tmp/cf-optimizer + rm -f /tmp/hosts/cf-optimizer /var/log/cf-optimizer.log* + killall -HUP dnsmasq 2>/dev/null +fi +exit 0 +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-cf-optimizer/htdocs/luci-static/resources/view/cf-optimizer/log.js b/luci-app-cf-optimizer/htdocs/luci-static/resources/view/cf-optimizer/log.js new file mode 100644 index 000000000..7c770b5bc --- /dev/null +++ b/luci-app-cf-optimizer/htdocs/luci-static/resources/view/cf-optimizer/log.js @@ -0,0 +1,101 @@ +'use strict'; +'require view'; +'require rpc'; +'require poll'; +'require ui'; +'require dom'; + +var callGetLogs = rpc.declare({ + object: 'luci.cf-optimizer', + method: 'get_logs' +}); + +var callClearLogs = rpc.declare({ + object: 'luci.cf-optimizer', + method: 'clear_logs' +}); + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + return callGetLogs(); + }, + + handleClearLogs: function(ev) { + return callClearLogs().then(function() { + var textarea = document.getElementById('cf-log-textarea'); + if (textarea) textarea.value = ''; + ui.addNotification(null, E('p', {}, _('日志已清空')), 'info'); + }).catch(function(e) { + ui.addNotification(null, E('p', {}, e.message)); + }); + }, + + render: function(data) { + data = data || {}; + var lines = Array.isArray(data.logs) ? data.logs : []; + var logText = lines.join('\n'); + + var textarea = E('textarea', { + 'id': 'cf-log-textarea', + 'class': 'cbi-input-textarea', + 'style': 'width: 100%; height: 480px; 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); + + // 默认滚动到底部 + setTimeout(function() { + textarea.scrollTop = textarea.scrollHeight; + }, 100); + + var autoRefreshCheckbox = E('input', { + 'type': 'checkbox', + 'id': 'cf-auto-refresh', + 'checked': 'checked', + 'style': 'margin-right: 6px; vertical-align: middle;' + }); + + var controls = E('div', { + 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;' + }, [ + E('label', { 'style': 'cursor: pointer; user-select: none; font-size: 13px;' }, [ + autoRefreshCheckbox, + _('自动刷新 (每 4 秒)') + ]), + E('button', { + 'class': 'btn cbi-button cbi-button-remove', + 'click': ui.createHandlerFn(this, 'handleClearLogs') + }, _('清空日志')) + ]); + + poll.add(function() { + var chk = document.getElementById('cf-auto-refresh'); + if (!chk || !chk.checked) return; + + return callGetLogs().then(function(res) { + res = res || {}; + var ta = document.getElementById('cf-log-textarea'); + if (ta && Array.isArray(res.logs)) { + var wasAtBottom = (ta.scrollHeight - ta.clientHeight <= ta.scrollTop + 50); + ta.value = res.logs.join('\n'); + if (wasAtBottom) { + ta.scrollTop = ta.scrollHeight; + } + } + }); + }, 4); + + return E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('Cloudflare 优选加速') + ' - ' + _('运行日志')), + E('div', { 'class': 'cbi-map-descr' }, _('查看后台优选测速与 hosts 重载的实时运行记录。')), + E('fieldset', { 'class': 'cbi-section' }, [ + controls, + textarea + ]) + ]); + } +}); diff --git a/luci-app-cf-optimizer/htdocs/luci-static/resources/view/cf-optimizer/overview.js b/luci-app-cf-optimizer/htdocs/luci-static/resources/view/cf-optimizer/overview.js new file mode 100644 index 000000000..8bc55eeb1 --- /dev/null +++ b/luci-app-cf-optimizer/htdocs/luci-static/resources/view/cf-optimizer/overview.js @@ -0,0 +1,244 @@ +'use strict'; +'require view'; +'require rpc'; +'require poll'; +'require ui'; +'require dom'; + +var callGetStatus = rpc.declare({ + object: 'luci.cf-optimizer', + method: 'get_status' +}); + +var callRunOnce = rpc.declare({ + object: 'luci.cf-optimizer', + method: 'run_once' +}); + +var callServiceAction = rpc.declare({ + object: 'luci.cf-optimizer', + method: 'service_action', + params: [ 'action' ] +}); + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + return callGetStatus(); + }, + + handleRunOnce: function(ev) { + ui.showModal(_('执行优选'), [ + E('p', { 'class': 'spinning' }, _('正在后台启动优选探测测速任务,请稍候...')) + ]); + return callRunOnce().then(function() { + setTimeout(function() { + ui.hideModal(); + window.location.reload(); + }, 3000); + }).catch(function(e) { + ui.addNotification(null, E('p', {}, e.message)); + ui.hideModal(); + }); + }, + + handleServiceAction: function(action, ev) { + ui.showModal(_('服务操作'), [ + E('p', { 'class': 'spinning' }, _('正在执行操作,请稍候...')) + ]); + return callServiceAction(action).then(function() { + setTimeout(function() { + ui.hideModal(); + window.location.reload(); + }, 1500); + }).catch(function(e) { + ui.addNotification(null, E('p', {}, e.message)); + ui.hideModal(); + }); + }, + + render: function(data) { + data = data || {}; + + var container = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('Cloudflare 优选加速') + ' - ' + _('状态概览')), + E('div', { 'class': 'cbi-map-descr' }, _('监控当前生效的 Cloudflare 优选节点状态,展示各代理域名定向情况与最新测速排行榜。')) + ]); + + // 服务状态卡片 + var isRunning = data.service_running === true; + var statusText = isRunning ? (_('运行中') + (data.service_pid ? ' [PID: ' + data.service_pid + ']' : '')) : _('已停止'); + var statusColor = isRunning ? '#059669' : '#64748b'; + + var statusSection = E('fieldset', { 'class': 'cbi-section' }, [ + E('legend', {}, _('服务状态')), + E('div', { 'class': 'cbi-section-node' }, [ + E('table', { 'class': 'table cbi-section-table' }, [ + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left', 'width': '25%' }, _('服务状态')), + E('td', { 'class': 'td left' }, [ + E('span', { 'id': 'cf-service-status', 'style': 'font-weight:bold; color:' + statusColor + '; margin-right:15px;' }, statusText), + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'style': 'margin-right:10px;', + 'click': ui.createHandlerFn(this, 'handleRunOnce') + }, _('立即执行优选')), + E('button', { + 'class': 'btn cbi-button cbi-button-reload', + 'click': ui.createHandlerFn(this, 'handleServiceAction', 'restart') + }, _('重启服务')) + ]) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left' }, _('上次更新时间')), + E('td', { 'class': 'td left', 'id': 'cf-last-update' }, data.last_update || '-') + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left' }, _('生效优选 IP')), + E('td', { 'class': 'td left', 'id': 'cf-best-ip', 'style': 'font-weight:bold;' }, data.best_ip || '-') + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left' }, _('次优备选 IP')), + E('td', { 'class': 'td left', 'id': 'cf-standby-ip' }, data.standby_ip ? (data.standby_ip + ' (' + (data.standby_colo || '-') + ')') : _('暂无备选')) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left' }, _('健康看门狗')), + E('td', { 'class': 'td left', 'id': 'cf-watchdog-status', 'style': 'font-weight:bold; color:' + ((data.watchdog_status && data.watchdog_status.indexOf('倒换') !== -1) ? '#d97706' : '#059669') }, data.watchdog_status || _('未运行')) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left' }, _('数据中心')), + E('td', { 'class': 'td left', 'id': 'cf-best-colo' }, data.best_colo || '-') + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left' }, _('测速延迟')), + E('td', { 'class': 'td left', 'id': 'cf-best-latency' }, [ + E('span', { 'style': 'color:#2563eb; font-weight:bold;' }, data.best_tcp_lat ? 'TCP: ' + data.best_tcp_lat : '-'), + E('span', { 'style': 'margin:0 8px; color:#cbd5e1;' }, '|'), + E('span', { 'style': 'color:#059669; font-weight:bold;' }, data.best_tls_lat ? 'TLS: ' + data.best_tls_lat : '-') + ]) + ]) + ]) + ]) + ]); + + // 域名定向列表 + var domainRows = []; + if (Array.isArray(data.domains) && data.domains.length > 0) { + for (var i = 0; i < data.domains.length; i++) { + var d = data.domains[i]; + domainRows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, E('strong', {}, d.domain)), + E('td', { 'class': 'td' }, d.ip || '-'), + E('td', { 'class': 'td' }, d.colo || '-'), + E('td', { 'class': 'td' }, d.tcp_lat || '-'), + E('td', { 'class': 'td' }, d.tls_lat || '-'), + E('td', { 'class': 'td' }, E('span', { 'style': 'color:#059669; font-weight:bold;' }, d.status || _('正常'))) + ])); + } + } else { + domainRows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'colspan': '6', 'style': 'text-align:center; color:#94a3b8;' }, _('暂无生效的域名定向记录,请在参数配置中添加域名并启动服务。')) + ])); + } + + var domainSection = E('fieldset', { 'class': 'cbi-section' }, [ + E('legend', {}, _('域名定向列表')), + E('div', { 'class': 'cbi-section-node' }, [ + E('table', { 'class': 'table cbi-section-table', 'id': 'cf-domain-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('目标域名')), + E('th', { 'class': 'th' }, _('生效优选 IP')), + E('th', { 'class': 'th' }, _('数据中心')), + E('th', { 'class': 'th' }, _('TCP 延迟')), + E('th', { 'class': 'th' }, _('TLS 延迟')), + E('th', { 'class': 'th' }, _('状态')) + ]) + ].concat(domainRows)) + ]) + ]); + + // 优选候选排行榜 + var rankingRows = []; + if (Array.isArray(data.rankings) && data.rankings.length > 0) { + for (var j = 0; j < data.rankings.length; j++) { + var r = data.rankings[j]; + rankingRows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, r.rank), + E('td', { 'class': 'td' }, E('strong', {}, r.ip)), + E('td', { 'class': 'td' }, r.colo || '-'), + E('td', { 'class': 'td', 'style': 'color:#059669; font-weight:bold;' }, r.avg_lat), + E('td', { 'class': 'td' }, r.lat_range || '-'), + E('td', { 'class': 'td' }, r.loss_rate || '0.0%') + ])); + } + } else { + rankingRows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'colspan': '6', 'style': 'text-align:center; color:#94a3b8;' }, _('暂无排行榜数据')) + ])); + } + + var rankingSection = E('fieldset', { 'class': 'cbi-section' }, [ + E('legend', {}, _('优选候选节点排行榜')), + E('div', { 'class': 'cbi-section-node' }, [ + E('table', { 'class': 'table cbi-section-table', 'id': 'cf-ranking-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('排名')), + E('th', { 'class': 'th' }, _('候选 IP')), + E('th', { 'class': 'th' }, _('数据中心')), + E('th', { 'class': 'th' }, _('平均延迟')), + E('th', { 'class': 'th' }, _('时延范围')), + E('th', { 'class': 'th' }, _('丢包率')) + ]) + ].concat(rankingRows)) + ]) + ]); + + container.appendChild(statusSection); + container.appendChild(domainSection); + container.appendChild(rankingSection); + + // 定时轮询更新 + poll.add(function() { + return callGetStatus().then(function(res) { + res = res || {}; + var running = res.service_running === true; + var stEl = document.getElementById('cf-service-status'); + if (stEl) { + stEl.textContent = running ? (_('运行中') + (res.service_pid ? ' [PID: ' + res.service_pid + ']' : '')) : _('已停止'); + stEl.style.color = running ? '#059669' : '#64748b'; + } + var upEl = document.getElementById('cf-last-update'); + if (upEl) upEl.textContent = res.last_update || '-'; + + var ipEl = document.getElementById('cf-best-ip'); + if (ipEl) ipEl.textContent = res.best_ip || '-'; + + var sbEl = document.getElementById('cf-standby-ip'); + if (sbEl) sbEl.textContent = res.standby_ip ? (res.standby_ip + ' (' + (res.standby_colo || '-') + ')') : _('暂无备选'); + + var wdEl = document.getElementById('cf-watchdog-status'); + if (wdEl) { + wdEl.textContent = res.watchdog_status || _('未运行'); + wdEl.style.color = (res.watchdog_status && res.watchdog_status.indexOf('倒换') !== -1) ? '#d97706' : '#059669'; + } + + var coloEl = document.getElementById('cf-best-colo'); + if (coloEl) coloEl.textContent = res.best_colo || '-'; + + var latEl = document.getElementById('cf-best-latency'); + if (latEl) { + dom.content(latEl, [ + E('span', { 'style': 'color:#2563eb; font-weight:bold;' }, res.best_tcp_lat ? 'TCP: ' + res.best_tcp_lat : '-'), + E('span', { 'style': 'margin:0 8px; color:#cbd5e1;' }, '|'), + E('span', { 'style': 'color:#059669; font-weight:bold;' }, res.best_tls_lat ? 'TLS: ' + res.best_tls_lat : '-') + ]); + } + }); + }, 3); + + return container; + } +}); diff --git a/luci-app-cf-optimizer/htdocs/luci-static/resources/view/cf-optimizer/settings.js b/luci-app-cf-optimizer/htdocs/luci-static/resources/view/cf-optimizer/settings.js new file mode 100644 index 000000000..336226fe1 --- /dev/null +++ b/luci-app-cf-optimizer/htdocs/luci-static/resources/view/cf-optimizer/settings.js @@ -0,0 +1,153 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; +'require fs'; +'require ui'; + +return view.extend({ + handleSaveApply: function(ev, mode) { + return this.handleSave(ev).then(function() { + ui.showModal(_('应用配置'), [ + E('p', { 'class': 'spinning' }, _('正在下发配置并同步 hosts 域名定向,请稍候...')) + ]); + return fs.exec_direct('/etc/init.d/cf-optimizer', ['reload']).catch(function() {}).then(function() { + setTimeout(function() { + ui.hideModal(); + window.location.reload(); + }, 1500); + }); + }); + }, + + render: function() { + var m, s, o; + + m = new form.Map('cf-optimizer', _('Cloudflare 优选加速') + ' - ' + _('参数配置'), + _('配置待优选的 Cloudflare 代理域名、测速周期及采样算法参数。')); + + s = m.section(form.NamedSection, 'settings', 'cf-optimizer', _('基础设置')); + s.anonymous = true; + + o = s.option(form.Flag, 'enabled', _('启用服务'), + _('开启后守护进程将按设定周期自动执行优选测速,并通过 dnsmasq 内存级重载生效。')); + o.rmempty = false; + + o = s.option(form.DynamicList, 'domains', _('加速域名列表'), + _('填入需要优选加速的域名,支持 Cloudflare Tunnel 穿透域名和小黄云代理域名。')); + o.datatype = 'host'; + o.rmempty = false; + + o = s.option(form.ListValue, 'interval', _('自动优选周期'), + _('两次自动测速优选之间的休眠间隔时间。')); + o.value('30', _('30 分钟')); + o.value('60', _('1 小时')); + o.value('120', _('2 小时')); + o.value('240', _('4 小时')); + o.value('360', _('6 小时')); + o.value('720', _('12 小时')); + o.value('1440', _('24 小时')); + o.default = '120'; + + s = m.section(form.NamedSection, 'settings', 'cf-optimizer', _('高级探测参数')); + s.anonymous = true; + + o = s.option(form.Value, 'sample_count', _('采样探测次数'), + _('初筛后对候选节点进行连续采样的次数,用于计算平均时延与丢包率,推荐范围 3-10 次。')); + o.datatype = 'range(2, 20)'; + o.default = '4'; + + o = s.option(form.ListValue, 'hysteresis_ms', _('防震荡迟滞阈值'), + _('新节点平均延迟相比在用节点必须降低超过此绝对时延(毫秒)才触发切换,避免网络微小波动导致局域网长连接频繁重连。')); + o.value('0', _('关闭 (任意优化均切换)')); + o.value('10', '10 ms'); + o.value('20', '20 ms'); + o.value('30', '30 ms'); + o.value('50', '50 ms'); + o.value('80', '80 ms'); + o.default = '30'; + + o = s.option(form.Value, 'concurrency', _('初筛并发数'), + _('初筛阶段并发探测的进程数量,推荐范围 5-20。')); + o.datatype = 'range(2, 30)'; + o.default = '10'; + + o = s.option(form.ListValue, 'ip_source', _('候选 IP 来源'), + _('选择优选测速的候选节点 IP 来源。')); + o.value('builtin', _('内置优选 IP 池')); + o.value('custom', _('自定义 IP 或网段')); + o.value('mixed', _('混合模式')); + o.default = 'builtin'; + + o = s.option(form.DynamicList, 'custom_ips', _('自定义候选 IP 列表'), + _('填入自定义的 Cloudflare Anycast IP 地址或网段。')); + o.datatype = 'or(ip4addr, ip4prefix)'; + o.depends('ip_source', 'custom'); + o.depends('ip_source', 'mixed'); + + s = m.section(form.NamedSection, 'settings', 'cf-optimizer', _('故障快速倒换')); + s.anonymous = true; + + o = s.option(form.Flag, 'failover_enabled', _('启用快速故障倒换'), + _('开启后短周期看门狗将持续监控在用节点健康度,发生严重丢包或阻断时自动秒级倒换至次优备选节点。')); + o.rmempty = false; + o.default = '1'; + + o = s.option(form.ListValue, 'health_interval', _('健康巡检间隔'), + _('看门狗对在用节点执行极轻量 HTTP 探活的间隔时间。')); + o.value('30', _('30 秒')); + o.value('60', _('60 秒')); + o.value('120', _('120 秒')); + o.default = '60'; + o.depends('failover_enabled', '1'); + + o = s.option(form.Value, 'fail_threshold', _('连续故障判定阈值'), + _('探活连续失败达到指定次数后触发次优节点核验与自动倒换,推荐设置为 2 次以避免偶发网络抖动误切换。')); + o.datatype = 'range(1, 10)'; + o.default = '2'; + o.depends('failover_enabled', '1'); + + o = s.option(form.ListValue, 'latency_multiplier', _('延迟劣化容忍倍数'), + _('当在用节点实测延迟超过基线延迟指定倍数且绝对恶化超过 50ms 时判定为信道拥塞,连续触发将倒换至次优节点。')); + o.value('1.5', _('1.5 倍')); + o.value('2.0', _('2.0 倍')); + o.value('3.0', _('3.0 倍')); + o.value('0', _('禁用')); + o.default = '2.0'; + o.depends('failover_enabled', '1'); + + s = m.section(form.NamedSection, 'settings', 'cf-optimizer', _('机房区域过滤')); + s.anonymous = true; + + o = s.option(form.ListValue, 'colo_filter_mode', _('区域过滤模式'), + _('设置对 Cloudflare 数据中心机房的筛选策略。')); + o.value('none', _('不限制')); + o.value('exclude', _('排除指定机房')); + o.value('include', _('仅限指定机房')); + o.default = 'none'; + + o = s.option(form.MultiValue, 'colo_list', _('目标机房列表'), + _('选择需要排除或仅保留的 Cloudflare 常用数据中心机房。')); + o.value('SIN', _('新加坡 SIN')); + o.value('HKG', _('香港 HKG')); + o.value('TPE', _('台北 TPE')); + o.value('NRT', _('东京 NRT')); + o.value('KIX', _('大阪 KIX')); + o.value('ICN', _('首尔 ICN')); + o.value('KUL', _('吉隆坡 KUL')); + o.value('BKK', _('曼谷 BKK')); + o.value('SJC', _('圣何塞 SJC')); + o.value('LAX', _('洛杉矶 LAX')); + o.value('FRA', _('法兰克福 FRA')); + o.value('LHR', _('伦敦 LHR')); + o.depends('colo_filter_mode', 'exclude'); + o.depends('colo_filter_mode', 'include'); + + o = s.option(form.DynamicList, 'custom_colos', _('自定义机房代码'), + _('填入其他三位大写机场代码,例如 SFO、SEA 等。')); + o.depends('colo_filter_mode', 'exclude'); + o.depends('colo_filter_mode', 'include'); + + return m.render(); + } +}); diff --git a/luci-app-cf-optimizer/po/templates/cf-optimizer.pot b/luci-app-cf-optimizer/po/templates/cf-optimizer.pot new file mode 100644 index 000000000..e89acbe41 --- /dev/null +++ b/luci-app-cf-optimizer/po/templates/cf-optimizer.pot @@ -0,0 +1,290 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8\n" + +msgid "Cloudflare 优选加速" +msgstr "Cloudflare 优选加速" + +msgid "状态概览" +msgstr "状态概览" + +msgid "参数配置" +msgstr "参数配置" + +msgid "运行日志" +msgstr "运行日志" + +msgid "监控当前生效的 Cloudflare 优选节点状态,展示各代理域名定向情况与最新测速排行榜。" +msgstr "监控当前生效的 Cloudflare 优选节点状态,展示各代理域名定向情况与最新测速排行榜。" + +msgid "配置待优选的 Cloudflare 代理域名、测速周期及采样算法参数。" +msgstr "配置待优选的 Cloudflare 代理域名、测速周期及采样算法参数。" + +msgid "查看后台优选测速与 hosts 重载的实时运行记录。" +msgstr "查看后台优选测速与 hosts 重载的实时运行记录。" + +msgid "服务状态" +msgstr "服务状态" + +msgid "运行中" +msgstr "运行中" + +msgid "已停止" +msgstr "已停止" + +msgid "立即执行优选" +msgstr "立即执行优选" + +msgid "重启服务" +msgstr "重启服务" + +msgid "上次更新时间" +msgstr "上次更新时间" + +msgid "生效优选 IP" +msgstr "生效优选 IP" + +msgid "数据中心" +msgstr "数据中心" + +msgid "测速延迟" +msgstr "测速延迟" + +msgid "域名定向列表" +msgstr "域名定向列表" + +msgid "目标域名" +msgstr "目标域名" + +msgid "TCP 延迟" +msgstr "TCP 延迟" + +msgid "TLS 延迟" +msgstr "TLS 延迟" + +msgid "状态" +msgstr "状态" + +msgid "正常" +msgstr "正常" + +msgid "当前优选候选排行榜 (Top 5)" +msgstr "当前优选候选排行榜 (Top 5)" + +msgid "排名" +msgstr "排名" + +msgid "候选 IP" +msgstr "候选 IP" + +msgid "平均延迟" +msgstr "平均延迟" + +msgid "时延范围" +msgstr "时延范围" + +msgid "丢包率" +msgstr "丢包率" + +msgid "基础设置" +msgstr "基础设置" + +msgid "启用服务" +msgstr "启用服务" + +msgid "开启后守护进程将按设定周期自动执行优选测速,并通过 dnsmasq 内存级重载生效。" +msgstr "开启后守护进程将按设定周期自动执行优选测速,并通过 dnsmasq 内存级重载生效。" + +msgid "加速域名列表" +msgstr "加速域名列表" + +msgid "填入需要优选加速的域名,支持 Cloudflare Tunnel 穿透域名和小黄云代理域名。" +msgstr "填入需要优选加速的域名,支持 Cloudflare Tunnel 穿透域名和小黄云代理域名。" + +msgid "自动优选周期" +msgstr "自动优选周期" + +msgid "两次自动测速优选之间的休眠间隔时间。" +msgstr "两次自动测速优选之间的休眠间隔时间。" + +msgid "高级探测参数" +msgstr "高级探测参数" + +msgid "采样探测次数" +msgstr "采样探测次数" + +msgid "初筛后对候选节点进行连续采样的次数,用于计算平均时延与丢包率,推荐范围 3-10 次。" +msgstr "初筛后对候选节点进行连续采样的次数,用于计算平均时延与丢包率,推荐范围 3-10 次。" + +msgid "防震荡迟滞阈值" +msgstr "防震荡迟滞阈值" + +msgid "新节点平均延迟相比在用节点必须降低超过此绝对时延(毫秒)才触发切换,避免网络微小波动导致局域网长连接频繁重连。" +msgstr "" + +msgid "关闭 (任意优化均切换)" +msgstr "" + +msgid "正在下发配置并同步 hosts 域名定向,请稍候..." +msgstr "" + +msgid "初筛并发数" +msgstr "初筛并发数" + +msgid "初筛阶段并发探测的进程数量,推荐范围 5-20。" +msgstr "初筛阶段并发探测的进程数量,推荐范围 5-20。" + +msgid "候选 IP 来源" +msgstr "候选 IP 来源" + +msgid "选择优选测速的候选节点 IP 来源。" +msgstr "选择优选测速的候选节点 IP 来源。" + +msgid "内置优选 IP 池" +msgstr "内置优选 IP 池" + +msgid "自定义 IP 或网段" +msgstr "自定义 IP 或网段" + +msgid "混合模式" +msgstr "混合模式" + +msgid "自定义候选 IP 列表" +msgstr "自定义候选 IP 列表" + +msgid "填入自定义的 Cloudflare Anycast IP 地址或网段。" +msgstr "填入自定义的 Cloudflare Anycast IP 地址或网段。" + +msgid "清空日志" +msgstr "清空日志" + +msgid "日志已清空" +msgstr "日志已清空" + +msgid "次优备选 IP" +msgstr "" + +msgid "暂无备选" +msgstr "" + +msgid "健康看门狗" +msgstr "" + +msgid "未运行" +msgstr "" + +msgid "故障快速倒换" +msgstr "" + +msgid "启用快速故障倒换" +msgstr "" + +msgid "开启后短周期看门狗将持续监控在用节点健康度,发生严重丢包或阻断时自动秒级倒换至次优备选节点。" +msgstr "" + +msgid "健康巡检间隔" +msgstr "" + +msgid "看门狗对在用节点执行极轻量 HTTP 探活的间隔时间。" +msgstr "" + +msgid "30 秒" +msgstr "" + +msgid "60 秒" +msgstr "" + +msgid "120 秒" +msgstr "" + +msgid "连续故障判定阈值" +msgstr "" + +msgid "探活连续失败达到指定次数后触发次优节点核验与自动倒换,推荐设置为 2 次以避免偶发网络抖动误切换。" +msgstr "" + +msgid "已倒换" +msgstr "" + +msgid "延迟劣化容忍倍数" +msgstr "" + +msgid "当在用节点实测延迟超过基线延迟指定倍数且绝对恶化超过 50ms 时判定为信道拥塞,连续触发将倒换至次优节点。" +msgstr "" + +msgid "1.5 倍" +msgstr "" + +msgid "2.0 倍" +msgstr "" + +msgid "3.0 倍" +msgstr "" + +msgid "禁用" +msgstr "" + +msgid "机房区域过滤" +msgstr "" + +msgid "区域过滤模式" +msgstr "" + +msgid "设置对 Cloudflare 数据中心机房的筛选策略。" +msgstr "" + +msgid "不限制" +msgstr "" + +msgid "排除指定机房" +msgstr "" + +msgid "仅限指定机房" +msgstr "" + +msgid "目标机房列表" +msgstr "" + +msgid "选择需要排除或仅保留的 Cloudflare 常用数据中心机房。" +msgstr "" + +msgid "新加坡 SIN" +msgstr "" + +msgid "香港 HKG" +msgstr "" + +msgid "台北 TPE" +msgstr "" + +msgid "东京 NRT" +msgstr "" + +msgid "大阪 KIX" +msgstr "" + +msgid "首尔 ICN" +msgstr "" + +msgid "吉隆坡 KUL" +msgstr "" + +msgid "曼谷 BKK" +msgstr "" + +msgid "圣何塞 SJC" +msgstr "" + +msgid "洛杉矶 LAX" +msgstr "" + +msgid "法兰克福 FRA" +msgstr "" + +msgid "伦敦 LHR" +msgstr "" + +msgid "自定义机房代码" +msgstr "" + +msgid "填入其他三位大写机场代码,例如 SFO、SEA 等。" +msgstr "" diff --git a/luci-app-cf-optimizer/po/zh-cn b/luci-app-cf-optimizer/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-cf-optimizer/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-cf-optimizer/po/zh_Hans/cf-optimizer.po b/luci-app-cf-optimizer/po/zh_Hans/cf-optimizer.po new file mode 100644 index 000000000..5cf9db82a --- /dev/null +++ b/luci-app-cf-optimizer/po/zh_Hans/cf-optimizer.po @@ -0,0 +1,290 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8\n" + +msgid "Cloudflare 优选加速" +msgstr "Cloudflare 优选加速" + +msgid "状态概览" +msgstr "状态概览" + +msgid "参数配置" +msgstr "参数配置" + +msgid "运行日志" +msgstr "运行日志" + +msgid "监控当前生效的 Cloudflare 优选节点状态,展示各代理域名定向情况与最新测速排行榜。" +msgstr "监控当前生效的 Cloudflare 优选节点状态,展示各代理域名定向情况与最新测速排行榜。" + +msgid "配置待优选的 Cloudflare 代理域名、测速周期及采样算法参数。" +msgstr "配置待优选的 Cloudflare 代理域名、测速周期及采样算法参数。" + +msgid "查看后台优选测速与 hosts 重载的实时运行记录。" +msgstr "查看后台优选测速与 hosts 重载的实时运行记录。" + +msgid "服务状态" +msgstr "服务状态" + +msgid "运行中" +msgstr "运行中" + +msgid "已停止" +msgstr "已停止" + +msgid "立即执行优选" +msgstr "立即执行优选" + +msgid "重启服务" +msgstr "重启服务" + +msgid "上次更新时间" +msgstr "上次更新时间" + +msgid "生效优选 IP" +msgstr "生效优选 IP" + +msgid "数据中心" +msgstr "数据中心" + +msgid "测速延迟" +msgstr "测速延迟" + +msgid "域名定向列表" +msgstr "域名定向列表" + +msgid "目标域名" +msgstr "目标域名" + +msgid "TCP 延迟" +msgstr "TCP 延迟" + +msgid "TLS 延迟" +msgstr "TLS 延迟" + +msgid "状态" +msgstr "状态" + +msgid "正常" +msgstr "正常" + +msgid "当前优选候选排行榜 (Top 5)" +msgstr "当前优选候选排行榜 (Top 5)" + +msgid "排名" +msgstr "排名" + +msgid "候选 IP" +msgstr "候选 IP" + +msgid "平均延迟" +msgstr "平均延迟" + +msgid "时延范围" +msgstr "时延范围" + +msgid "丢包率" +msgstr "丢包率" + +msgid "基础设置" +msgstr "基础设置" + +msgid "启用服务" +msgstr "启用服务" + +msgid "开启后守护进程将按设定周期自动执行优选测速,并通过 dnsmasq 内存级重载生效。" +msgstr "开启后守护进程将按设定周期自动执行优选测速,并通过 dnsmasq 内存级重载生效。" + +msgid "加速域名列表" +msgstr "加速域名列表" + +msgid "填入需要优选加速的域名,支持 Cloudflare Tunnel 穿透域名和小黄云代理域名。" +msgstr "填入需要优选加速的域名,支持 Cloudflare Tunnel 穿透域名和小黄云代理域名。" + +msgid "自动优选周期" +msgstr "自动优选周期" + +msgid "两次自动测速优选之间的休眠间隔时间。" +msgstr "两次自动测速优选之间的休眠间隔时间。" + +msgid "高级探测参数" +msgstr "高级探测参数" + +msgid "采样探测次数" +msgstr "采样探测次数" + +msgid "初筛后对候选节点进行连续采样的次数,用于计算平均时延与丢包率,推荐范围 3-10 次。" +msgstr "初筛后对候选节点进行连续采样的次数,用于计算平均时延与丢包率,推荐范围 3-10 次。" + +msgid "防震荡迟滞阈值" +msgstr "防震荡迟滞阈值" + +msgid "新节点平均延迟相比在用节点必须降低超过此绝对时延(毫秒)才触发切换,避免网络微小波动导致局域网长连接频繁重连。" +msgstr "新节点平均延迟相比在用节点必须降低超过此绝对时延(毫秒)才触发切换,避免网络微小波动导致局域网长连接频繁重连。" + +msgid "关闭 (任意优化均切换)" +msgstr "关闭 (任意优化均切换)" + +msgid "正在下发配置并同步 hosts 域名定向,请稍候..." +msgstr "正在下发配置并同步 hosts 域名定向,请稍候..." + +msgid "初筛并发数" +msgstr "初筛并发数" + +msgid "初筛阶段并发探测的进程数量,推荐范围 5-20。" +msgstr "初筛阶段并发探测的进程数量,推荐范围 5-20。" + +msgid "候选 IP 来源" +msgstr "候选 IP 来源" + +msgid "选择优选测速的候选节点 IP 来源。" +msgstr "选择优选测速的候选节点 IP 来源。" + +msgid "内置优选 IP 池" +msgstr "内置优选 IP 池" + +msgid "自定义 IP 或网段" +msgstr "自定义 IP 或网段" + +msgid "混合模式" +msgstr "混合模式" + +msgid "自定义候选 IP 列表" +msgstr "自定义候选 IP 列表" + +msgid "填入自定义的 Cloudflare Anycast IP 地址或网段。" +msgstr "填入自定义的 Cloudflare Anycast IP 地址或网段。" + +msgid "清空日志" +msgstr "清空日志" + +msgid "日志已清空" +msgstr "日志已清空" + +msgid "次优备选 IP" +msgstr "次优备选 IP" + +msgid "暂无备选" +msgstr "暂无备选" + +msgid "健康看门狗" +msgstr "健康看门狗" + +msgid "未运行" +msgstr "未运行" + +msgid "故障快速倒换" +msgstr "故障快速倒换" + +msgid "启用快速故障倒换" +msgstr "启用快速故障倒换" + +msgid "开启后短周期看门狗将持续监控在用节点健康度,发生严重丢包或阻断时自动秒级倒换至次优备选节点。" +msgstr "开启后短周期看门狗将持续监控在用节点健康度,发生严重丢包或阻断时自动秒级倒换至次优备选节点。" + +msgid "健康巡检间隔" +msgstr "健康巡检间隔" + +msgid "看门狗对在用节点执行极轻量 HTTP 探活的间隔时间。" +msgstr "看门狗对在用节点执行极轻量 HTTP 探活的间隔时间。" + +msgid "30 秒" +msgstr "30 秒" + +msgid "60 秒" +msgstr "60 秒" + +msgid "120 秒" +msgstr "120 秒" + +msgid "连续故障判定阈值" +msgstr "连续故障判定阈值" + +msgid "探活连续失败达到指定次数后触发次优节点核验与自动倒换,推荐设置为 2 次以避免偶发网络抖动误切换。" +msgstr "探活连续失败达到指定次数后触发次优节点核验与自动倒换,推荐设置为 2 次以避免偶发网络抖动误切换。" + +msgid "已倒换" +msgstr "已倒换" + +msgid "延迟劣化容忍倍数" +msgstr "延迟劣化容忍倍数" + +msgid "当在用节点实测延迟超过基线延迟指定倍数且绝对恶化超过 50ms 时判定为信道拥塞,连续触发将倒换至次优节点。" +msgstr "当在用节点实测延迟超过基线延迟指定倍数且绝对恶化超过 50ms 时判定为信道拥塞,连续触发将倒换至次优节点。" + +msgid "1.5 倍" +msgstr "1.5 倍" + +msgid "2.0 倍" +msgstr "2.0 倍" + +msgid "3.0 倍" +msgstr "3.0 倍" + +msgid "禁用" +msgstr "禁用" + +msgid "机房区域过滤" +msgstr "机房区域过滤" + +msgid "区域过滤模式" +msgstr "区域过滤模式" + +msgid "设置对 Cloudflare 数据中心机房的筛选策略。" +msgstr "设置对 Cloudflare 数据中心机房的筛选策略。" + +msgid "不限制" +msgstr "不限制" + +msgid "排除指定机房" +msgstr "排除指定机房" + +msgid "仅限指定机房" +msgstr "仅限指定机房" + +msgid "目标机房列表" +msgstr "目标机房列表" + +msgid "选择需要排除或仅保留的 Cloudflare 常用数据中心机房。" +msgstr "选择需要排除或仅保留的 Cloudflare 常用数据中心机房。" + +msgid "新加坡 SIN" +msgstr "新加坡 SIN" + +msgid "香港 HKG" +msgstr "香港 HKG" + +msgid "台北 TPE" +msgstr "台北 TPE" + +msgid "东京 NRT" +msgstr "东京 NRT" + +msgid "大阪 KIX" +msgstr "大阪 KIX" + +msgid "首尔 ICN" +msgstr "首尔 ICN" + +msgid "吉隆坡 KUL" +msgstr "吉隆坡 KUL" + +msgid "曼谷 BKK" +msgstr "曼谷 BKK" + +msgid "圣何塞 SJC" +msgstr "圣何塞 SJC" + +msgid "洛杉矶 LAX" +msgstr "洛杉矶 LAX" + +msgid "法兰克福 FRA" +msgstr "法兰克福 FRA" + +msgid "伦敦 LHR" +msgstr "伦敦 LHR" + +msgid "自定义机房代码" +msgstr "自定义机房代码" + +msgid "填入其他三位大写机场代码,例如 SFO、SEA 等。" +msgstr "填入其他三位大写机场代码,例如 SFO、SEA 等。" diff --git a/luci-app-cf-optimizer/root/etc/config/cf-optimizer b/luci-app-cf-optimizer/root/etc/config/cf-optimizer new file mode 100644 index 000000000..816ed6e37 --- /dev/null +++ b/luci-app-cf-optimizer/root/etc/config/cf-optimizer @@ -0,0 +1,12 @@ +config cf-optimizer 'settings' + option enabled '0' + option interval '120' + option sample_count '4' + option hysteresis_ms '30' + option concurrency '10' + option ip_source 'builtin' + option failover_enabled '1' + option health_interval '60' + option fail_threshold '2' + option latency_multiplier '2.0' + option colo_filter_mode 'none' diff --git a/luci-app-cf-optimizer/root/etc/init.d/cf-optimizer b/luci-app-cf-optimizer/root/etc/init.d/cf-optimizer new file mode 100755 index 000000000..7e1cf574d --- /dev/null +++ b/luci-app-cf-optimizer/root/etc/init.d/cf-optimizer @@ -0,0 +1,50 @@ +#!/bin/sh /etc/rc.common + +START=95 +STOP=10 +USE_PROCD=1 + +PROG="/usr/sbin/cf-optimizer" +CONF="cf-optimizer" + +start_service() { + config_load "$CONF" + local enabled + config_get_bool enabled settings enabled 0 + [ "$enabled" -eq 1 ] || return 0 + + mkdir -p /var/run/cf-optimizer + "$PROG" sync-hosts >/dev/null 2>&1 + + procd_open_instance + procd_set_param command "$PROG" daemon + procd_set_param respawn 3600 5 0 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param pidfile /var/run/cf-optimizer/cf-optimizer.pid + procd_close_instance +} + +stop_service() { + rm -f /var/run/cf-optimizer/cf-optimizer.pid +} + +service_stopped() { + config_load "$CONF" + local enabled + config_get_bool enabled settings enabled 0 + if [ "$enabled" != "1" ]; then + "$PROG" clean + fi +} + +reload_service() { + "$PROG" sync-hosts >/dev/null 2>&1 + stop + start + "$PROG" run >/dev/null 2>&1 & +} + +service_triggers() { + procd_add_reload_trigger "$CONF" +} diff --git a/luci-app-cf-optimizer/root/etc/uci-defaults/99-luci-app-cf-optimizer b/luci-app-cf-optimizer/root/etc/uci-defaults/99-luci-app-cf-optimizer new file mode 100644 index 000000000..5f610bd16 --- /dev/null +++ b/luci-app-cf-optimizer/root/etc/uci-defaults/99-luci-app-cf-optimizer @@ -0,0 +1,22 @@ +#!/bin/sh +[ ! -f "/usr/share/ucitrack/luci-app-ucitrack.json" ] && { + cat > /usr/share/ucitrack/luci-app-ucitrack.json << EEOF +{ + "config": "ucitrack", + "init": "ucitrack" +} +EEOF +} + +/etc/init.d/cf-optimizer enable >/dev/null 2>&1 || true + +uci -q get ucitrack.@cf-optimizer[0] >/dev/null || { + uci add ucitrack cf-optimizer >/dev/null + uci set ucitrack.@cf-optimizer[-1].init='cf-optimizer' + uci commit ucitrack +} + +rm -f /tmp/luci-indexcache* 2>/dev/null +rm -f /tmp/luci-modulecache/* 2>/dev/null + +exit 0 diff --git a/luci-app-cf-optimizer/root/usr/sbin/cf-optimizer b/luci-app-cf-optimizer/root/usr/sbin/cf-optimizer new file mode 100755 index 000000000..0f3de3086 --- /dev/null +++ b/luci-app-cf-optimizer/root/usr/sbin/cf-optimizer @@ -0,0 +1,835 @@ +#!/bin/sh + +CONF="cf-optimizer" +RUN_DIR="/tmp/run/cf-optimizer" +TMP_DIR="/tmp/cf-optimizer" +HOSTS_FILE="/tmp/hosts/cf-optimizer" +LOG_FILE="/var/log/cf-optimizer.log" +DEFAULT_IPS_FILE="/usr/share/cf-optimizer/default_ips.txt" +COLO_MAP_FILE="/usr/share/cf-optimizer/colo_map.json" +PID_FILE="$RUN_DIR/cf-optimizer.pid" +COLO_CACHE_FILE="$RUN_DIR/colo_cache.txt" +STATUS_JSON="$RUN_DIR/status.json" +CURRENT_IP_FILE="$RUN_DIR/current_ip.txt" +PRIMARY_IP_FILE="$RUN_DIR/primary_ip.txt" +STANDBY_IP_FILE="$RUN_DIR/standby_ip.txt" +BASELINE_TCP_FILE="$RUN_DIR/baseline_tcp.txt" +FAIL_STREAK_FILE="$RUN_DIR/fail_streak.txt" + +log() { + local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1" + echo "$msg" + mkdir -p "$(dirname "$LOG_FILE")" + echo "$msg" >> "$LOG_FILE" + # 限制日志大小不超过 1000 行 + if [ -f "$LOG_FILE" ] && [ $(wc -l < "$LOG_FILE") -gt 1500 ]; then + tail -n 800 "$LOG_FILE" > "$LOG_FILE.tmp" && mv -f "$LOG_FILE.tmp" "$LOG_FILE" + fi +} + +get_colo_name() { + local code="$1" + if [ -f "$COLO_MAP_FILE" ] && [ -n "$code" ]; then + local name + name=$(grep -E "\"$code\"[[:space:]]*:" "$COLO_MAP_FILE" 2>/dev/null | head -n 1 | sed -E 's/.*:[[:space:]]*"([^"]+)".*/\1/') + if [ -n "$name" ]; then + echo "$name" + return + fi + fi + echo "${code:-未知}" +} + +update_colo_cache() { + local ip="$1" + local colo="$2" + [ -z "$ip" ] || [ -z "$colo" ] && return + mkdir -p "$RUN_DIR" + touch "$COLO_CACHE_FILE" + grep -v "^$ip[[:space:]]" "$COLO_CACHE_FILE" > "$COLO_CACHE_FILE.tmp" 2>/dev/null + echo "$ip $colo" >> "$COLO_CACHE_FILE.tmp" + mv -f "$COLO_CACHE_FILE.tmp" "$COLO_CACHE_FILE" +} + +get_cached_colo() { + local ip="$1" + [ ! -f "$COLO_CACHE_FILE" ] && return + grep "^$ip[[:space:]]" "$COLO_CACHE_FILE" 2>/dev/null | tail -n 1 | awk '{print $2}' +} + +clean_hosts() { + mkdir -p "$RUN_DIR" + if [ -f "$HOSTS_FILE" ]; then + rm -f "$HOSTS_FILE" + killall -HUP dnsmasq 2>/dev/null + log "已清空优选 hosts 映射并重载 dnsmasq" + fi + rm -f "$CURRENT_IP_FILE" "$PRIMARY_IP_FILE" "$STANDBY_IP_FILE" "$BASELINE_TCP_FILE" "$FAIL_STREAK_FILE" + rm -rf "$RUN_DIR/task.lock" + cat < "$STATUS_JSON" +{ + "running": false, + "last_update": "", + "best_ip": "", + "best_colo": "", + "best_tcp_lat": 0, + "best_tls_lat": 0, + "standby_ip": "", + "standby_colo": "", + "standby_tcp_lat": 0, + "watchdog_status": "未运行", + "domains": [], + "rankings": [] +} +EOF +} + +sync_hosts() { + . /lib/functions.sh + config_load "$CONF" + local enabled + config_get_bool enabled settings enabled 0 + if [ "$enabled" != "1" ]; then + clean_hosts + return 0 + fi + + local domains="" + append_sync_domain() { + local d="$1" + [ -n "$d" ] && domains="$domains $d" + } + config_list_foreach settings domains append_sync_domain + + if [ -z "$domains" ]; then + clean_hosts + return 0 + fi + + local current_ip="" + [ -f "$CURRENT_IP_FILE" ] && current_ip=$(cat "$CURRENT_IP_FILE" 2>/dev/null) + + if [ -z "$current_ip" ]; then + local cand_tmp="$TMP_DIR/cand_init.txt" + mkdir -p "$TMP_DIR" + load_candidate_ips "$cand_tmp" + current_ip=$(head -n 1 "$cand_tmp" 2>/dev/null) + rm -f "$cand_tmp" + fi + + [ -z "$current_ip" ] && return 0 + + local hosts_tmp="$HOSTS_FILE.tmp" + mkdir -p "$(dirname "$HOSTS_FILE")" + > "$hosts_tmp" + echo "# Auto-generated by cf-optimizer sync on $(date '+%Y-%m-%d %H:%M:%S')" >> "$hosts_tmp" + for d in $domains; do + echo "$current_ip $d" >> "$hosts_tmp" + done + mv -f "$hosts_tmp" "$HOSTS_FILE" + killall -HUP dnsmasq 2>/dev/null + log "已即时同步域名配置至 hosts 并重载 dnsmasq (定向至: $current_ip)" +} + +load_candidate_ips() { + local out_file="$1" + > "$out_file" + + local ip_source + config_get ip_source settings ip_source "builtin" + + if [ "$ip_source" = "builtin" ] || [ "$ip_source" = "mixed" ]; then + if [ -f "$DEFAULT_IPS_FILE" ]; then + grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' "$DEFAULT_IPS_FILE" >> "$out_file" + fi + fi + + if [ "$ip_source" = "custom" ] || [ "$ip_source" = "mixed" ]; then + append_custom_ip() { + local item="$1" + [ -z "$item" ] && return + # 若为 CIDR 简单网段 (如 /24),取前 3 个 IP + case "$item" in + */*) + local base_prefix=$(echo "$item" | cut -d'/' -f1 | cut -d'.' -f1-3) + for host in 1 2 3; do + echo "$base_prefix.$host" >> "$out_file" + done + ;; + *) + echo "$item" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' >> "$out_file" + ;; + esac + } + config_list_foreach settings custom_ips append_custom_ip + fi + + # 兜底:若文件为空,填入稳定官方公共 IP + if [ ! -s "$out_file" ]; then + echo "104.16.0.1" >> "$out_file" + echo "104.17.0.1" >> "$out_file" + echo "104.18.0.1" >> "$out_file" + echo "172.64.0.1" >> "$out_file" + fi + + # 排序去重 + sort -u "$out_file" > "$out_file.tmp" && mv -f "$out_file.tmp" "$out_file" +} + +# 粗筛单节点探测 +probe_fast() { + local ip="$1" + local res_dir="$2" + # 仅测 TCP 握手 + local connect_time + connect_time=$(curl -s -k -o /dev/null --connect-timeout 1 -m 2 -w "%{time_connect}\n" \ + --resolve "speed.cloudflare.com:443:$ip" "https://speed.cloudflare.com/__down?bytes=0" 2>/dev/null) + + if [ $? -eq 0 ] && [ -n "$connect_time" ] && [ "$connect_time" != "0.000000" ]; then + local ms=$(awk "BEGIN {printf \"%08.2f\", $connect_time * 1000}") + echo "$ms $ip" > "$res_dir/$ip" + fi +} + +run_optimization() { + . /lib/functions.sh + config_load "$CONF" + + local enabled + config_get_bool enabled settings enabled 0 + if [ "$enabled" != "1" ]; then + log "服务未启用,退出任务" + clean_hosts + return 0 + fi + + local domains="" + append_domain() { + local d="$1" + [ -n "$d" ] && domains="$domains $d" + } + config_list_foreach settings domains append_domain + + if [ -z "$domains" ]; then + log "未配置目标加速域名,跳过优选" + return 0 + fi + + local concurrency sample_count hysteresis_ms + config_get concurrency settings concurrency 10 + config_get sample_count settings sample_count 4 + config_get hysteresis_ms settings hysteresis_ms 30 + + local colo_filter_mode + config_get colo_filter_mode settings colo_filter_mode "none" + + local target_colos="" + append_target_colo() { + local item="$1" + [ -z "$item" ] && return + for c in $item; do + local clean_c=$(echo "$c" | tr 'a-z' 'A-Z' | tr -d ' \t\r\n') + if [ -n "$clean_c" ]; then + case " $target_colos " in + *" $clean_c "*) ;; + *) target_colos="$target_colos $clean_c" ;; + esac + fi + done + } + if [ "$colo_filter_mode" != "none" ]; then + local multi_colos="" + config_get multi_colos settings colo_list "" + [ -n "$multi_colos" ] && append_target_colo "$multi_colos" + config_list_foreach settings colo_list append_target_colo + config_list_foreach settings custom_colos append_target_colo + fi + + check_colo_allowed() { + local colo="$1" + [ -z "$colo" ] && return 0 + [ "$colo_filter_mode" = "none" ] && return 0 + + local match=0 + for c in $target_colos; do + if [ "$c" = "$colo" ]; then + match=1 + break + fi + done + + if [ "$colo_filter_mode" = "exclude" ]; then + [ "$match" -eq 1 ] && return 1 || return 0 + elif [ "$colo_filter_mode" = "include" ]; then + [ "$match" -eq 1 ] && return 0 || return 1 + fi + return 0 + } + + log "开始执行优选任务,加速域名数: $(echo $domains | wc -w),机房策略: $colo_filter_mode${target_colos:+ ($target_colos)}" + + local lock_dir="$RUN_DIR/task.lock" + if ! mkdir "$lock_dir" 2>/dev/null; then + local lock_pid="" + [ -f "$lock_dir/pid" ] && lock_pid=$(cat "$lock_dir/pid" 2>/dev/null) + if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then + log "检测到残留任务锁 (PID: $lock_pid 已退出),自动释放死锁" + rm -rf "$lock_dir" + mkdir "$lock_dir" 2>/dev/null || return 0 + else + log "已有优选任务正在运行中,跳过本次执行" + return 0 + fi + fi + echo $$ > "$lock_dir/pid" + + mkdir -p "$TMP_DIR/fast" "$TMP_DIR/deep" "$RUN_DIR" + rm -rf "$TMP_DIR/fast"/* "$TMP_DIR/deep"/* + + local cand_file="$TMP_DIR/candidates.txt" + load_candidate_ips "$cand_file" + local total_cands=$(wc -l < "$cand_file") + log "阶段一:开始并发初筛,候选节点总数: $total_cands,并发度: $concurrency" + + # 并发初筛(采用 POSIX 批次等待机制,确保在任何非交互式 Shell 环境下严格限流) + local running=0 + while read -r ip; do + [ -z "$ip" ] && continue + probe_fast "$ip" "$TMP_DIR/fast" & + running=$((running + 1)) + if [ "$running" -ge "$concurrency" ]; then + wait + running=0 + fi + done < "$cand_file" + wait + + # 汇总初筛候选,结合机房缓存预先剔除黑名单机房 + local fast_sorted_file="$TMP_DIR/fast_sorted.txt" + cat "$TMP_DIR/fast"/* 2>/dev/null | sort -n | head -n 30 > "$fast_sorted_file" + + local top_selected_file="$TMP_DIR/top_selected_ips.txt" + > "$top_selected_file" + local picked_count=0 + while read -r _lat ip; do + [ -z "$ip" ] && continue + local cached_colo=$(get_cached_colo "$ip") + if [ -n "$cached_colo" ] && ! check_colo_allowed "$cached_colo"; then + log "初筛预过滤: 节点 $ip ($cached_colo) 命中机房排除策略,自动略过" + continue + fi + echo "$ip" >> "$top_selected_file" + picked_count=$((picked_count + 1)) + [ "$picked_count" -ge 10 ] && break + done < "$fast_sorted_file" + + local top_count=$(wc -l < "$top_selected_file") + if [ "$top_count" -eq 0 ]; then + log "阶段一初筛无可用候选节点(或全部被机房区域策略过滤),请检查配置" + rm -rf "$TMP_DIR" + rm -rf "$lock_dir" + return 1 + fi + + local probe_domain=$(echo "$domains" | awk '{print $1}') + [ -z "$probe_domain" ] && probe_domain="speed.cloudflare.com" + + log "阶段一初筛完成,选出 $top_count 个候选节点进入精测采样,探活域名: $probe_domain(每节点轮询交叉采样 $sample_count 轮)" + + # 阶段二:轮询交叉时序采样(跨越完整测试时段,精准捕获阶段性丢包与网络毛刺) + mkdir -p "$TMP_DIR/samples" + + for seq in $(seq 1 "$sample_count"); do + while read -r ip; do + [ -z "$ip" ] && continue + [ -f "$TMP_DIR/blocked_${ip}" ] && continue + [ -f "$TMP_DIR/colo_excluded_${ip}" ] && continue + + local raw_header="$TMP_DIR/header_${ip}_${seq}.txt" + local metrics + metrics=$(curl -s -k -I -o /dev/null --connect-timeout 2 -m 3 \ + -A "Mozilla/5.0 (compatible; Cloudflare-Optimizer/1.0)" \ + --resolve "$probe_domain:443:$ip" "https://$probe_domain/" \ + -D "$raw_header" \ + -w "%{time_connect} %{time_appconnect} %{http_code}\n" 2>/dev/null) + + local tcp_sec=$(echo "$metrics" | awk '{print $1}') + local tls_sec=$(echo "$metrics" | awk '{print $2}') + local http_code=$(echo "$metrics" | awk '{print $3}') + + if [ "$http_code" = "403" ]; then + touch "$TMP_DIR/blocked_${ip}" + rm -f "$raw_header" + continue + fi + + local last_colo="" + if [ -f "$raw_header" ]; then + local cf_ray=$(grep -i '^cf-ray:' "$raw_header" 2>/dev/null | tr -d '\r\n') + if [ -n "$cf_ray" ]; then + last_colo=$(echo "$cf_ray" | sed -E 's/.*-([A-Za-z0-9]+).*/\1/' | tr 'a-z' 'A-Z') + fi + rm -f "$raw_header" + fi + + if [ -n "$last_colo" ]; then + update_colo_cache "$ip" "$last_colo" + if ! check_colo_allowed "$last_colo"; then + log "节点 $ip 机房 ($last_colo) 不符合区域过滤策略 ($colo_filter_mode),已终止采样并排除" + touch "$TMP_DIR/colo_excluded_${ip}" + continue + fi + fi + + if [ -n "$http_code" ] && [ "$http_code" != "000" ] && [ -n "$tcp_sec" ] && [ "$tcp_sec" != "0.000000" ]; then + local tcp_ms=$(awk "BEGIN {printf \"%.2f\", $tcp_sec * 1000}") + local tls_ms=$(awk "BEGIN {printf \"%.2f\", $tls_sec * 1000}") + echo "1|$tcp_ms|$tls_ms|$last_colo" >> "$TMP_DIR/samples/$ip" + else + echo "0|0|0|$last_colo" >> "$TMP_DIR/samples/$ip" + fi + done < "$top_selected_file" + done + + # 汇总统计各节点采样指标 + while read -r ip; do + [ -z "$ip" ] && continue + if [ -f "$TMP_DIR/blocked_${ip}" ]; then + log "节点 $ip 触发权限阻断 (HTTP 403/企业限制),已自动排除" + continue + fi + if [ -f "$TMP_DIR/colo_excluded_${ip}" ]; then + continue + fi + + local sample_file="$TMP_DIR/samples/$ip" + [ ! -f "$sample_file" ] && continue + + local success_count=0 + local total_tcp=0 + local total_tls=0 + local min_tcp=99999 + local max_tcp=0 + local last_colo="" + + while IFS='|' read -r ok tcp_ms tls_ms colo; do + [ -n "$colo" ] && last_colo="$colo" + if [ "$ok" = "1" ]; then + success_count=$((success_count + 1)) + total_tcp=$(awk "BEGIN {printf \"%.2f\", $total_tcp + $tcp_ms}") + total_tls=$(awk "BEGIN {printf \"%.2f\", $total_tls + $tls_ms}") + min_tcp=$(awk "BEGIN {if ($tcp_ms < $min_tcp) print $tcp_ms; else print $min_tcp}") + max_tcp=$(awk "BEGIN {if ($tcp_ms > $max_tcp) print $tcp_ms; else print $max_tcp}") + fi + done < "$sample_file" + + if [ -z "$last_colo" ]; then + last_colo=$(get_cached_colo "$ip") + fi + + if ! check_colo_allowed "$last_colo"; then + log "节点 $ip 机房 ($last_colo) 最终区域校验未通过,舍弃" + continue + fi + + local loss_rate=$(awk "BEGIN {printf \"%.1f\", (1 - $success_count / $sample_count) * 100}") + if [ "$success_count" -gt 0 ]; then + local avg_tcp=$(awk "BEGIN {printf \"%.2f\", $total_tcp / $success_count}") + local avg_tls=$(awk "BEGIN {printf \"%.2f\", $total_tls / $success_count}") + local jitter=$(awk "BEGIN {printf \"%.2f\", $max_tcp - $min_tcp}") + # 综合评分:平均时延 + 抖动*0.5 + 丢包率*10(采用固定宽度 0 填充) + local score=$(awk "BEGIN {printf \"%08.2f\", $avg_tcp + $jitter * 0.5 + $loss_rate * 10}") + + local colo_disp=$(get_colo_name "$last_colo") + echo "$score|$ip|$avg_tcp|$min_tcp|$max_tcp|$avg_tls|$loss_rate%|$colo_disp|$last_colo" >> "$TMP_DIR/deep_results.txt" + fi + done < "$top_selected_file" + + if [ ! -s "$TMP_DIR/deep_results.txt" ]; then + log "精测采样后无可用健康节点" + rm -rf "$TMP_DIR" + rm -rf "$lock_dir" + return 1 + fi + + # 排序:按 score 从低到高数值排序 + sort -n "$TMP_DIR/deep_results.txt" > "$TMP_DIR/deep_sorted.txt" + + local best_line=$(head -n 1 "$TMP_DIR/deep_sorted.txt") + local best_score=$(echo "$best_line" | cut -d'|' -f1) + local best_ip=$(echo "$best_line" | cut -d'|' -f2) + local best_avg_tcp=$(echo "$best_line" | cut -d'|' -f3) + local best_min_tcp=$(echo "$best_line" | cut -d'|' -f4) + local best_max_tcp=$(echo "$best_line" | cut -d'|' -f5) + local best_avg_tls=$(echo "$best_line" | cut -d'|' -f6) + local best_loss=$(echo "$best_line" | cut -d'|' -f7) + local best_colo=$(echo "$best_line" | cut -d'|' -f8) + + log "测速完成,最优候选: $best_ip ($best_colo), 平均延迟: ${best_avg_tcp}ms, 丢包率: $best_loss" + + # 迟滞逻辑判断 + local current_ip="" + [ -f "$CURRENT_IP_FILE" ] && current_ip=$(cat "$CURRENT_IP_FILE") + + local final_ip="$best_ip" + local final_colo="$best_colo" + local final_tcp="$best_avg_tcp" + local final_tls="$best_avg_tls" + + if [ -n "$current_ip" ] && [ "$current_ip" != "$best_ip" ]; then + local cur_info=$(grep "|$current_ip|" "$TMP_DIR/deep_sorted.txt" 2>/dev/null) + local cur_colo_allowed=1 + local cur_cached_colo=$(get_cached_colo "$current_ip") + if [ -n "$cur_cached_colo" ] && ! check_colo_allowed "$cur_cached_colo"; then + cur_colo_allowed=0 + fi + + if [ -n "$cur_info" ] && [ "$cur_colo_allowed" -eq 1 ]; then + local cur_tcp=$(echo "$cur_info" | cut -d'|' -f3) + local cur_loss=$(echo "$cur_info" | cut -d'|' -f7) + local cur_colo=$(echo "$cur_info" | cut -d'|' -f8) + + if [ "$cur_loss" = "0.0%" ]; then + # 测算相比当前在用 IP 的绝对时延差 (ms) + local diff_ms=$(awk "BEGIN {if ($cur_tcp > $best_avg_tcp) printf \"%.2f\", $cur_tcp - $best_avg_tcp; else print 0}") + local is_better=$(awk "BEGIN {print ($diff_ms >= $hysteresis_ms) ? 1 : 0}") + + if [ "$is_better" -eq 0 ]; then + log "新优选 IP 优化幅度 (${diff_ms}ms) 未达迟滞阈值 (${hysteresis_ms}ms),保持当前在用 IP: $current_ip" + final_ip="$current_ip" + final_colo="$cur_colo" + final_tcp="$cur_tcp" + final_tls=$(echo "$cur_info" | cut -d'|' -f6) + else + log "新优选 IP 性能显著提升 (时延改善 ${diff_ms}ms >= ${hysteresis_ms}ms),触发切换" + fi + else + log "当前在用 IP 发生丢包 ($cur_loss),强制切换至新最优 IP: $best_ip" + fi + else + if [ "$cur_colo_allowed" -eq 0 ]; then + log "当前在用 IP ($current_ip / $cur_cached_colo) 已被机房过滤策略排除,强制切换至新最优 IP: $best_ip" + fi + fi + fi + + # 原子写入 /tmp/hosts/cf-optimizer + local hosts_tmp="$HOSTS_FILE.tmp" + mkdir -p "$(dirname "$HOSTS_FILE")" + > "$hosts_tmp" + echo "# Auto-generated by cf-optimizer on $(date '+%Y-%m-%d %H:%M:%S')" >> "$hosts_tmp" + for d in $domains; do + echo "$final_ip $d" >> "$hosts_tmp" + done + mv -f "$hosts_tmp" "$HOSTS_FILE" + echo "$final_ip" > "$CURRENT_IP_FILE" + echo "$final_ip" > "$PRIMARY_IP_FILE" + echo "0" > "$FAIL_STREAK_FILE" + + # 选拔次优备选节点(排名紧随其后且 IP 不同的健康节点) + local standby_ip="" + local standby_colo="无备选" + local standby_tcp="-" + while IFS='|' read -r _sc _ip _atip _minip _maxip _attls _lrate _cdisp _craw; do + if [ -n "$_ip" ] && [ "$_ip" != "$final_ip" ]; then + standby_ip="$_ip" + standby_colo="$_cdisp" + standby_tcp="$_atip" + break + fi + done < "$TMP_DIR/deep_sorted.txt" + echo "$standby_ip|$standby_colo|$standby_tcp" > "$STANDBY_IP_FILE" + echo "$final_tcp" > "$BASELINE_TCP_FILE" + + # 发送 HUP 信号通知 dnsmasq 毫秒级重载 + killall -HUP dnsmasq 2>/dev/null + log "已更新 $HOSTS_FILE 并热重载 dnsmasq,在用主选: $final_ip ($final_colo),次优备选: ${standby_ip:-无} ($standby_colo)" + + # 组装状态 JSON 给 LuCI 前端消费 + local now_str=$(date '+%Y-%m-%d %H:%M:%S') + local domain_json="[" + local first_d=1 + for d in $domains; do + [ "$first_d" -eq 0 ] && domain_json="$domain_json," + domain_json="$domain_json{\"domain\":\"$d\",\"ip\":\"$final_ip\",\"colo\":\"$final_colo\",\"tcp_lat\":\"$final_tcp ms\",\"tls_lat\":\"$final_tls ms\",\"status\":\"正常\"}" + first_d=0 + done + domain_json="$domain_json]" + + local rankings_json="[" + local first_r=1 + local rank_idx=1 + while IFS='|' read -r sc ip atip minip maxip attls lrate cdisp craw; do + [ -z "$ip" ] && continue + [ "$first_r" -eq 0 ] && rankings_json="$rankings_json," + rankings_json="$rankings_json{\"rank\":$rank_idx,\"ip\":\"$ip\",\"colo\":\"$cdisp\",\"avg_lat\":\"$atip ms\",\"lat_range\":\"$minip / $maxip ms\",\"loss_rate\":\"$lrate\"}" + first_r=0 + rank_idx=$((rank_idx + 1)) + [ "$rank_idx" -gt 5 ] && break + done < "$TMP_DIR/deep_sorted.txt" + rankings_json="$rankings_json]" + + cat < "$STATUS_JSON.tmp" +{ + "running": true, + "last_update": "$now_str", + "best_ip": "$final_ip", + "best_colo": "$final_colo", + "best_tcp_lat": "$final_tcp ms", + "best_tls_lat": "$final_tls ms", + "standby_ip": "$standby_ip", + "standby_colo": "$standby_colo", + "standby_tcp_lat": "$standby_tcp ms", + "watchdog_status": "正常监控中", + "domains": $domain_json, + "rankings": $rankings_json +} +EOF + mv -f "$STATUS_JSON.tmp" "$STATUS_JSON" + rm -rf "$TMP_DIR" + rm -rf "$lock_dir" + return 0 +} + +apply_fast_failover() { + local target_ip="$1" + local target_colo="$2" + local target_tcp="${3:--}" + + . /lib/functions.sh + config_load "$CONF" + local domains="" + append_failover_domain() { + local d="$1" + [ -n "$d" ] && domains="$domains $d" + } + config_list_foreach settings domains append_failover_domain + + local hosts_tmp="$HOSTS_FILE.tmp" + mkdir -p "$(dirname "$HOSTS_FILE")" + > "$hosts_tmp" + echo "# Auto-generated by cf-optimizer failover on $(date '+%Y-%m-%d %H:%M:%S')" >> "$hosts_tmp" + for d in $domains; do + echo "$target_ip $d" >> "$hosts_tmp" + done + mv -f "$hosts_tmp" "$HOSTS_FILE" + echo "$target_ip" > "$CURRENT_IP_FILE" + echo "" > "$STANDBY_IP_FILE" + if [ -n "$target_tcp" ] && [ "$target_tcp" != "-" ]; then + echo "$target_tcp" > "$BASELINE_TCP_FILE" + else + rm -f "$BASELINE_TCP_FILE" + fi + + killall -HUP dnsmasq 2>/dev/null + log "已将 hosts 紧急倒换至次优节点: $target_ip ($target_colo) 并重载 dnsmasq" + + if [ -f "$STATUS_JSON" ]; then + local now_str=$(date '+%Y-%m-%d %H:%M:%S') + local domain_json="[" + local first_d=1 + for d in $domains; do + [ "$first_d" -eq 0 ] && domain_json="$domain_json," + domain_json="$domain_json{\"domain\":\"$d\",\"ip\":\"$target_ip\",\"colo\":\"$target_colo\",\"tcp_lat\":\"$target_tcp ms\",\"tls_lat\":\"-\",\"status\":\"已倒换\"}" + first_d=0 + done + domain_json="$domain_json]" + + local tmp_status="$STATUS_JSON.tmp" + sed -E "s/\"best_ip\":[[:space:]]*\"[^\"]*\"/\"best_ip\": \"$target_ip\"/" "$STATUS_JSON" | \ + sed -E "s/\"best_colo\":[[:space:]]*\"[^\"]*\"/\"best_colo\": \"$target_colo\"/" | \ + sed -E "s/\"standby_ip\":[[:space:]]*\"[^\"]*\"/\"standby_ip\": \"\"/" | \ + sed -E "s/\"standby_colo\":[[:space:]]*\"[^\"]*\"/\"standby_colo\": \"已倒换转正\"/" | \ + sed -E "s/\"last_update\":[[:space:]]*\"[^\"]*\"/\"last_update\": \"$now_str (已触发倒换)\"/" | \ + sed -E "s/\"watchdog_status\":[[:space:]]*\"[^\"]*\"/\"watchdog_status\": \"已触发故障倒换\"/" | \ + sed -E "s/\"domains\":[[:space:]]*\[[^]]*\]/\"domains\": $domain_json/" > "$tmp_status" 2>/dev/null + [ -s "$tmp_status" ] && mv -f "$tmp_status" "$STATUS_JSON" + fi +} + +check_health_and_failover() { + [ ! -f "$CURRENT_IP_FILE" ] && return 0 + local current_ip=$(cat "$CURRENT_IP_FILE" 2>/dev/null) + [ -z "$current_ip" ] && return 0 + + . /lib/functions.sh + config_load "$CONF" + + local failover_enabled fail_threshold latency_multiplier + config_get_bool failover_enabled settings failover_enabled 1 + [ "$failover_enabled" != "1" ] && return 0 + config_get fail_threshold settings fail_threshold 2 + config_get latency_multiplier settings latency_multiplier "2.0" + + local domains="" + append_check_domain() { + local d="$1" + [ -n "$d" ] && domains="$domains $d" + } + config_list_foreach settings domains append_check_domain + local probe_domain=$(echo "$domains" | awk '{print $1}') + [ -z "$probe_domain" ] && probe_domain="speed.cloudflare.com" + + local metrics + metrics=$(curl -s -k -I -o /dev/null --connect-timeout 2 -m 3 \ + -A "Mozilla/5.0 (compatible; Cloudflare-Optimizer/1.0)" \ + --resolve "$probe_domain:443:$current_ip" "https://$probe_domain/" \ + -w "%{time_connect} %{http_code}\n" 2>/dev/null) + + local tcp_sec=$(echo "$metrics" | awk '{print $1}') + local http_code=$(echo "$metrics" | awk '{print $2}') + local cur_tcp_ms=0 + if [ -n "$tcp_sec" ] && [ "$tcp_sec" != "0.000000" ]; then + cur_tcp_ms=$(awk "BEGIN {printf \"%.2f\", $tcp_sec * 1000}") + fi + + local baseline_tcp=0 + [ -f "$BASELINE_TCP_FILE" ] && baseline_tcp=$(cat "$BASELINE_TCP_FILE" 2>/dev/null) + baseline_tcp=${baseline_tcp:-0} + + local streak=0 + [ -f "$FAIL_STREAK_FILE" ] && streak=$(cat "$FAIL_STREAK_FILE" 2>/dev/null) + streak=${streak:-0} + + local is_abnormal=0 + local abnormal_reason="" + + if [ -z "$http_code" ] || [ "$http_code" = "000" ] || [ "$http_code" = "403" ]; then + is_abnormal=1 + abnormal_reason="HTTP ${http_code:-000} 阻断或超时" + elif [ "$latency_multiplier" != "0" ] && [ "$latency_multiplier" != "0.0" ]; then + local is_degraded + is_degraded=$(awk "BEGIN { + if ($baseline_tcp > 0 && $cur_tcp_ms >= ($baseline_tcp * $latency_multiplier) && ($cur_tcp_ms - $baseline_tcp) >= 50) + print 1; + else + print 0; + }") + if [ "$is_degraded" -eq 1 ]; then + is_abnormal=1 + abnormal_reason="延迟严重劣化 (${cur_tcp_ms}ms >= 基线 ${baseline_tcp}ms x ${latency_multiplier})" + fi + fi + + if [ "$is_abnormal" -eq 0 ]; then + if [ "$streak" -gt 0 ]; then + log "主用优选节点 $current_ip 探活恢复正常 (HTTP $http_code, TCP ${cur_tcp_ms}ms)" + echo "0" > "$FAIL_STREAK_FILE" + fi + return 0 + fi + + streak=$((streak + 1)) + echo "$streak" > "$FAIL_STREAK_FILE" + log "在用优选节点 $current_ip 探活异常 [$abnormal_reason],连续异常: $streak/$fail_threshold" + + if [ "$streak" -ge "$fail_threshold" ]; then + echo "0" > "$FAIL_STREAK_FILE" + local standby_info="" + [ -f "$STANDBY_IP_FILE" ] && standby_info=$(cat "$STANDBY_IP_FILE" 2>/dev/null) + local standby_ip=$(echo "$standby_info" | cut -d'|' -f1) + local standby_colo=$(echo "$standby_info" | cut -d'|' -f2) + + if [ -n "$standby_ip" ] && [ "$standby_ip" != "$current_ip" ]; then + log "正在核验次优备选节点健康度: $standby_ip ($standby_colo)" + local s_metrics + s_metrics=$(curl -s -k -I -o /dev/null --connect-timeout 2 -m 3 \ + -A "Mozilla/5.0 (compatible; Cloudflare-Optimizer/1.0)" \ + --resolve "$probe_domain:443:$standby_ip" "https://$probe_domain/" \ + -w "%{time_connect} %{http_code}\n" 2>/dev/null) + local s_tcp_sec=$(echo "$s_metrics" | awk '{print $1}') + local s_code=$(echo "$s_metrics" | awk '{print $2}') + local s_tcp_ms="-" + [ -n "$s_tcp_sec" ] && [ "$s_tcp_sec" != "0.000000" ] && s_tcp_ms=$(awk "BEGIN {printf \"%.2f\", $s_tcp_sec * 1000}") + + if [ -n "$s_code" ] && [ "$s_code" != "000" ] && [ "$s_code" != "403" ]; then + log "主用节点异常,次优备选节点验证健康 (HTTP $s_code, TCP ${s_tcp_ms}ms),秒级倒换至次优节点" + apply_fast_failover "$standby_ip" "$standby_colo" "$s_tcp_ms" + return 0 + else + log "次优备选节点同样不可用 (HTTP ${s_code:-000}),提前拉起全网节点紧急重测" + fi + else + log "未配置次优备选节点,提前拉起全网节点紧急重测" + fi + + run_optimization + fi +} + +daemon_loop() { + echo $$ > "$PID_FILE" + trap 'rm -f "$PID_FILE"; exit 0' INT TERM EXIT + + log "守护进程启动 [PID: $$]" + + local last_full_run=0 + + while true; do + . /lib/functions.sh + config_load "$CONF" + + local enabled + config_get_bool enabled settings enabled 0 + if [ "$enabled" != "1" ]; then + log "配置已禁用,清理并退出守护进程" + clean_hosts + break + fi + + local interval failover_enabled health_interval + config_get interval settings interval 120 + [ "$interval" -lt 10 ] && interval=10 + local full_interval_sec=$((interval * 60)) + + config_get_bool failover_enabled settings failover_enabled 1 + config_get health_interval settings health_interval 60 + [ "$health_interval" -lt 15 ] && health_interval=15 + + local now=$(date +%s) + if [ "$last_full_run" -eq 0 ] || [ $((now - last_full_run)) -ge "$full_interval_sec" ]; then + run_optimization + last_full_run=$(date +%s) + elif [ "$failover_enabled" = "1" ]; then + check_health_and_failover + fi + + local sleep_sec="$health_interval" + if [ "$failover_enabled" != "1" ]; then + sleep_sec=30 + fi + + local elapsed=0 + while [ "$elapsed" -lt "$sleep_sec" ]; do + sleep 5 & + wait $! + elapsed=$((elapsed + 5)) + done + done +} + +case "$1" in + run) + run_optimization + ;; + sync-hosts) + sync_hosts + ;; + daemon) + daemon_loop + ;; + clean) + clean_hosts + ;; + status) + if [ -f "$STATUS_JSON" ]; then + cat "$STATUS_JSON" + else + echo '{"running":false,"domains":[],"rankings":[]}' + fi + ;; + *) + echo "用法: $0 {run|sync-hosts|daemon|clean|status}" + exit 1 + ;; +esac diff --git a/luci-app-cf-optimizer/root/usr/share/cf-optimizer/colo_map.json b/luci-app-cf-optimizer/root/usr/share/cf-optimizer/colo_map.json new file mode 100644 index 000000000..e195de042 --- /dev/null +++ b/luci-app-cf-optimizer/root/usr/share/cf-optimizer/colo_map.json @@ -0,0 +1,58 @@ +{ + "HKG": "香港 HKG", + "TPE": "台北 TPE", + "KHH": "高雄 KHH", + "NRT": "东京 NRT", + "HND": "东京羽田 HND", + "KIX": "大阪 KIX", + "FUK": "福冈 FUK", + "ICN": "首尔 ICN", + "SIN": "新加坡 SIN", + "BKK": "曼谷 BKK", + "KUL": "吉隆坡 KUL", + "JHB": "新山 JHB", + "MNL": "马尼拉 MNL", + "SGN": "胡志明市 SGN", + "HAN": "河内 HAN", + "CGK": "雅加达 CGK", + "DPS": "巴厘岛 DPS", + "SJC": "圣何塞 SJC", + "SFO": "旧金山 SFO", + "LAX": "洛杉矶 LAX", + "SEA": "西雅图 SEA", + "PDX": "波特兰 PDX", + "DEN": "丹佛 DEN", + "PHX": "菲尼克斯 PHX", + "DFW": "达拉斯 DFW", + "IAH": "休斯顿 IAH", + "ORD": "芝加哥 ORD", + "ATL": "亚特兰大 ATL", + "MIA": "迈阿密 MIA", + "IAD": "华盛顿 IAD", + "EWR": "纽瓦克 EWR", + "JFK": "纽约 JFK", + "BOS": "波士顿 BOS", + "YVR": "温哥华 YVR", + "YYZ": "多伦多 YYZ", + "YUL": "蒙特利尔 YUL", + "FRA": "法兰克福 FRA", + "LHR": "伦敦 LHR", + "AMS": "阿姆斯特丹 AMS", + "CDG": "巴黎 CDG", + "MAD": "马德里 MAD", + "MXP": "米兰 MXP", + "ZRH": "苏黎世 ZRH", + "VIE": "维也纳 VIE", + "ARN": "斯德哥尔摩 ARN", + "HEL": "赫尔辛基 HEL", + "CPH": "哥本哈根 CPH", + "WAW": "华沙 WAW", + "PRG": "布拉格 PRG", + "SYD": "悉尼 SYD", + "MEL": "墨尔本 MEL", + "BNE": "布里斯班 BNE", + "PER": "珀斯 PER", + "AKL": "奥克兰 AKL", + "DXB": "迪拜 DXB", + "DOH": "多哈 DOH" +} diff --git a/luci-app-cf-optimizer/root/usr/share/cf-optimizer/default_ips.txt b/luci-app-cf-optimizer/root/usr/share/cf-optimizer/default_ips.txt new file mode 100644 index 000000000..38a455880 --- /dev/null +++ b/luci-app-cf-optimizer/root/usr/share/cf-optimizer/default_ips.txt @@ -0,0 +1,102 @@ +104.16.0.1 +104.16.1.1 +104.16.8.1 +104.16.16.1 +104.16.24.1 +104.16.32.1 +104.16.48.1 +104.16.64.1 +104.16.80.1 +104.16.96.1 +104.16.112.1 +104.16.128.1 +104.16.144.1 +104.16.160.1 +104.16.176.1 +104.16.192.1 +104.16.208.1 +104.16.224.1 +104.16.240.1 +104.17.0.1 +104.17.16.1 +104.17.32.1 +104.17.64.1 +104.17.96.1 +104.17.128.1 +104.17.160.1 +104.17.192.1 +104.17.224.1 +104.18.0.1 +104.18.16.1 +104.18.32.1 +104.18.64.1 +104.18.96.1 +104.18.128.1 +104.18.160.1 +104.18.192.1 +104.18.224.1 +104.19.0.1 +104.19.16.1 +104.19.32.1 +104.19.64.1 +104.19.96.1 +104.19.128.1 +104.19.160.1 +104.19.192.1 +104.19.224.1 +104.20.0.1 +104.20.32.1 +104.20.64.1 +104.20.96.1 +104.20.128.1 +104.20.160.1 +104.20.192.1 +104.21.0.1 +104.21.16.1 +104.21.32.1 +104.21.64.1 +104.21.96.1 +104.22.0.1 +104.22.32.1 +104.22.64.1 +104.24.0.1 +104.24.32.1 +104.25.0.1 +104.25.32.1 +104.26.0.1 +104.26.32.1 +104.27.0.1 +104.27.32.1 +172.64.0.1 +172.64.16.1 +172.64.32.1 +172.64.64.1 +172.64.96.1 +172.64.128.1 +172.64.160.1 +172.64.192.1 +172.65.0.1 +172.65.32.1 +172.66.0.1 +172.66.32.1 +172.67.0.1 +172.67.32.1 +172.67.64.1 +172.67.128.1 +162.158.0.1 +162.158.16.1 +162.158.32.1 +162.158.64.1 +162.158.128.1 +108.162.192.1 +108.162.208.1 +108.162.224.1 +141.101.64.1 +141.101.96.1 +141.101.120.1 +190.93.240.1 +197.234.240.1 +198.41.128.1 +198.41.160.1 +198.41.192.1 +198.41.224.1 diff --git a/luci-app-cf-optimizer/root/usr/share/luci/menu.d/luci-app-cf-optimizer.json b/luci-app-cf-optimizer/root/usr/share/luci/menu.d/luci-app-cf-optimizer.json new file mode 100644 index 000000000..eac77a2ce --- /dev/null +++ b/luci-app-cf-optimizer/root/usr/share/luci/menu.d/luci-app-cf-optimizer.json @@ -0,0 +1,37 @@ +{ + "admin/services/cf-optimizer": { + "title": "Cloudflare 优选加速", + "order": 65, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": [ "luci-app-cf-optimizer" ], + "uci": { "cf-optimizer": true } + } + }, + "admin/services/cf-optimizer/overview": { + "title": "状态概览", + "order": 10, + "action": { + "type": "view", + "path": "cf-optimizer/overview" + } + }, + "admin/services/cf-optimizer/settings": { + "title": "参数配置", + "order": 20, + "action": { + "type": "view", + "path": "cf-optimizer/settings" + } + }, + "admin/services/cf-optimizer/log": { + "title": "运行日志", + "order": 30, + "action": { + "type": "view", + "path": "cf-optimizer/log" + } + } +} diff --git a/luci-app-cf-optimizer/root/usr/share/rpcd/acl.d/luci-app-cf-optimizer.json b/luci-app-cf-optimizer/root/usr/share/rpcd/acl.d/luci-app-cf-optimizer.json new file mode 100644 index 000000000..230eb44df --- /dev/null +++ b/luci-app-cf-optimizer/root/usr/share/rpcd/acl.d/luci-app-cf-optimizer.json @@ -0,0 +1,28 @@ +{ + "luci-app-cf-optimizer": { + "description": "Grant access to cf-optimizer LuCI procedures", + "read": { + "ubus": { + "luci.cf-optimizer": [ + "get_status", + "get_logs" + ] + }, + "uci": [ + "cf-optimizer" + ] + }, + "write": { + "ubus": { + "luci.cf-optimizer": [ + "run_once", + "clear_logs", + "service_action" + ] + }, + "uci": [ + "cf-optimizer" + ] + } + } +} diff --git a/luci-app-cf-optimizer/root/usr/share/rpcd/ucode/cf-optimizer.uc b/luci-app-cf-optimizer/root/usr/share/rpcd/ucode/cf-optimizer.uc new file mode 100644 index 000000000..dd95bf815 --- /dev/null +++ b/luci-app-cf-optimizer/root/usr/share/rpcd/ucode/cf-optimizer.uc @@ -0,0 +1,130 @@ +'use strict'; + +import { connect } from 'ubus'; +import { access, readfile, writefile, stat, popen } from 'fs'; + +function exec(cmd) { + let p = popen(cmd, 'r'); + if (!p) return { code: -1, stdout: [] }; + let out = []; + while (true) { + let line = p.read('line'); + if (line == null) break; + push(out, rtrim(line, '\r\n')); + } + let code = p.close(); + return { code: code, stdout: out }; +} + +const _trim = trim; +function trim_str(s) { + if (s == null) return ''; + return _trim('' + s); +} + +function get_procd_status() { + let u = connect(); + if (!u) return { running: false }; + + let res = u.call('service', 'list', { name: 'cf-optimizer' }); + if (res && res['cf-optimizer'] && res['cf-optimizer'].instances) { + let instances = res['cf-optimizer'].instances; + for (let name in instances) { + let inst = instances[name]; + if (inst && inst.running) + return { running: true, pid: inst.pid }; + } + } + + let pid_res = exec('pidof cf-optimizer 2>/dev/null'); + if (pid_res.code == 0 && length(pid_res.stdout) > 0) { + let pids = split(trim_str(join(' ', pid_res.stdout)), /\s+/); + if (length(pids) > 0 && pids[0] != '') + return { running: true, pid: +pids[0] }; + } + + return { running: false }; +} + +const methods = {}; + +methods.get_status = { + call: function() { + let proc_st = get_procd_status(); + let status_data = null; + + const status_file = '/tmp/run/cf-optimizer/status.json'; + if (access(status_file)) { + let content = readfile(status_file); + if (content) { + try { + status_data = json(content); + } catch(e) {} + } + } + + if (!status_data) { + status_data = { + running: proc_st.running, + last_update: '', + best_ip: '', + best_colo: '', + best_tcp_lat: '', + best_tls_lat: '', + domains: [], + rankings: [] + }; + } + + status_data.service_running = proc_st.running; + status_data.service_pid = proc_st.pid || null; + + return status_data; + } +}; + +methods.get_logs = { + call: function() { + const log_file = '/var/log/cf-optimizer.log'; + let logs = []; + if (access(log_file)) { + let res = exec('tail -n 120 ' + log_file + ' 2>/dev/null'); + if (res.code == 0) logs = res.stdout; + } + return { logs: logs }; + } +}; + +methods.clear_logs = { + call: function() { + const log_file = '/var/log/cf-optimizer.log'; + writefile(log_file, ''); + return { success: true }; + } +}; + +methods.run_once = { + call: function() { + exec('/usr/sbin/cf-optimizer run >/dev/null 2>&1 &'); + return { success: true }; + } +}; + +methods.service_action = { + args: { action: 'action' }, + call: function(req) { + let action = req.args?.action; + if (!action) return { success: false, error: 'Missing action parameter' }; + + if (action == 'restart') { + exec('/etc/init.d/cf-optimizer restart >/dev/null 2>&1'); + } else if (action == 'start') { + exec('/etc/init.d/cf-optimizer start >/dev/null 2>&1'); + } else if (action == 'stop') { + exec('/etc/init.d/cf-optimizer stop >/dev/null 2>&1'); + } + return { success: true }; + } +}; + +return { 'luci.cf-optimizer': methods }; diff --git a/luci-app-easytier-next/root/usr/share/rpcd/ucode/easytier.uc b/luci-app-easytier-next/root/usr/share/rpcd/ucode/easytier.uc index 0b0000da7..41c4116fc 100644 --- a/luci-app-easytier-next/root/usr/share/rpcd/ucode/easytier.uc +++ b/luci-app-easytier-next/root/usr/share/rpcd/ucode/easytier.uc @@ -16,9 +16,11 @@ function exec(cmd) { return { code: code, stdout: out }; } +const _trim = trim; + function trim(s) { if (s == null) return ''; - return replace('' + s, /^[\s\r\n]+|[\s\r\n]+$/g, ''); + return _trim('' + s); } function get_procd_status(instance_name) { diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile index 8ed6c2bef..94507d2cd 100644 --- a/luci-app-open-box/Makefile +++ b/luci-app-open-box/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-open-box -PKG_VERSION:=0.1.385 -PKG_RELEASE:=56 +PKG_VERSION:=0.1.388 +PKG_RELEASE:=57 PKG_MAINTAINER:=liandu2024 PKG_LICENSE:=GPL-3.0-or-later diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 2dc42472f..c3d8ae333 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.10.29 -PKG_RELEASE:=361 +PKG_RELEASE:=362 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/root/usr/share/passwall/luci_update.sh b/luci-app-passwall/root/usr/share/passwall/luci_update.sh index cb21cecf1..8c61b1f39 100755 --- a/luci-app-passwall/root/usr/share/passwall/luci_update.sh +++ b/luci-app-passwall/root/usr/share/passwall/luci_update.sh @@ -5,6 +5,9 @@ status="$2" manager="$3" force="$4" +acl_file="/usr/share/rpcd/acl.d/luci-app-${CONFIG}.json" +acl_md5_before=$(md5sum "$acl_file" 2>/dev/null | awk '{print $1}') + if [ "$manager" = apk ]; then extra="" [ "$force" = 1 ] && extra="--force-reinstall --force-overwrite" @@ -20,7 +23,10 @@ fi rm -rf "$dir" if [ "$result" = 0 ]; then rm -f "/tmp/etc/${CONFIG}_tmp/${CONFIG}_version" /tmp/luci-indexcache /tmp/luci-indexcache.* - if [ -x /etc/init.d/rpcd ]; then + acl_md5_after=$(md5sum "$acl_file" 2>/dev/null | awk '{print $1}') + # rpcd only reads ACL definitions at startup. Restart it only when the ACL + # file actually changed, so in-place updates no longer drop the web session. + if [ "$acl_md5_before" != "$acl_md5_after" ] && [ -x /etc/init.d/rpcd ]; then /etc/init.d/rpcd restart >/dev/null 2>&1 fi if [ -x /etc/init.d/uhttpd ]; then diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index d58cd5c9a..482967ba2 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.10.29 -PKG_RELEASE:=162 +PKG_RELEASE:=163 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/root/usr/share/passwall2/luci_update.sh b/luci-app-passwall2/root/usr/share/passwall2/luci_update.sh index 1b2b818b5..1545b1bfa 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/luci_update.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/luci_update.sh @@ -5,6 +5,9 @@ status="$2" manager="$3" force="$4" +acl_file="/usr/share/rpcd/acl.d/luci-app-${CONFIG}.json" +acl_md5_before=$(md5sum "$acl_file" 2>/dev/null | awk '{print $1}') + [ "$manager" = apk ] && extension=apk || extension=ipk set -- "$dir/luci-app-${CONFIG}.${extension}" for package in "$dir"/luci-i18n-${CONFIG}-*.${extension}; do @@ -26,7 +29,10 @@ fi rm -rf "$dir" if [ "$result" = 0 ]; then rm -f "/tmp/etc/${CONFIG}_tmp/${CONFIG}_version" /tmp/luci-indexcache /tmp/luci-indexcache.* - if [ -x /etc/init.d/rpcd ]; then + acl_md5_after=$(md5sum "$acl_file" 2>/dev/null | awk '{print $1}') + # rpcd only reads ACL definitions at startup. Restart it only when the ACL + # file actually changed, so in-place updates no longer drop the web session. + if [ "$acl_md5_before" != "$acl_md5_after" ] && [ -x /etc/init.d/rpcd ]; then /etc/init.d/rpcd restart >/dev/null 2>&1 fi if [ -x /etc/init.d/uhttpd ]; then diff --git a/luci-app-trafficctl/Makefile b/luci-app-trafficctl/Makefile index 4fc03dc71..1be785c2b 100644 --- a/luci-app-trafficctl/Makefile +++ b/luci-app-trafficctl/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-trafficctl -PKG_VERSION:=1.25.1 -PKG_RELEASE:=17 +PKG_VERSION:=1.25.2 +PKG_RELEASE:=18 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Denis Iusupov diff --git a/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.css b/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.css index a379735ff..f6a9dfd4f 100644 --- a/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.css +++ b/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.css @@ -738,6 +738,27 @@ vertical-align: middle; margin-right: 6px; } +/* The poll indicator (#106). It must never take part in layout: a fixed-size + slot that is always present, faded in rather than shown, because the banner + it replaces reflowed the page every couple of seconds — which is what made + the rate chips below it hard to hit. */ +.tc-poll-spinner { + display: inline-block; flex: 0 0 12px; width: 12px; height: 12px; + border: 2px solid var(--tc-border); + border-top-color: var(--tc-faint); + border-radius: 50%; + animation: tc-spin .7s linear infinite; + opacity: 0; transition: opacity .25s ease; +} +.tc-poll-spinner.is-busy { opacity: 1; } +/* A refresh that failed is the one thing the poll has to say. Same slot, so + saying it still cannot move anything; the message is in the title. */ +.tc-poll-spinner.is-error { + opacity: 1; animation: none; + border-color: var(--tc-err); background: var(--tc-err); + cursor: help; +} + /* ── Link style ──────────────────────────────────────────────────── */ .tc-link { color: inherit; text-decoration: none; border-bottom: 1px dotted var(--tc-faint); } .tc-link:hover { border-bottom-style: solid; } diff --git a/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js b/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js index b3c0d9c68..983bb21f4 100644 --- a/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js +++ b/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js @@ -1397,6 +1397,13 @@ function setStatus(el, type, msg) { el.innerHTML = type === 'loading' ? ''+escHtml(msg) : escHtml(msg); } +// setStatus rewrites className wholesale, which drops the tc-hidden the status +// element starts with — so putting it back has to be explicit. +function clearStatus(el) { + el.className = 'tc-hidden'; + el.innerHTML = ''; +} + function updateUrlParams(opts) { var params = new URLSearchParams(); if (opts.lastIp && opts.lastIp !== '__all__') params.set('ip', opts.lastIp); @@ -2091,6 +2098,39 @@ return view.extend({ }); var statusDiv = E('div', { 'class': 'tc-hidden' }); + + // #106: a refresh that runs on a timer must not announce itself on the + // status line. It gets its own indicator, which cannot disturb the + // page: a fixed-size slot that is always in the layout and is only + // faded in, never displayed or hidden (tc-hidden is display:none, which + // is exactly the reflow being fixed). It waits before showing, so on a + // router that answers quickly it never appears at all — a spinner that + // blinks for 80ms every two seconds is still a flash. + // + // A refresh reports its own health HERE and nowhere else. The status + // line belongs to what the operator did: every other poller in this + // view discards its errors outright, and these two were the only ones + // writing to a shared element, which is the whole of #106. + var pollSpinner = E('span', { 'class': 'tc-poll-spinner' }); + var _pollBusy = 0, _pollShowTimer = null; + function pollStart() { + _pollBusy++; + if (_pollShowTimer === null) { + _pollShowTimer = setTimeout(function() { + _pollShowTimer = null; + if (_pollBusy > 0) pollSpinner.classList.add('is-busy'); + }, 250); + } + } + // err: the message when this refresh failed, nothing when it worked. + function pollEnd(err) { + if (_pollBusy > 0) _pollBusy--; + if (_pollBusy > 0) return; + if (_pollShowTimer !== null) { clearTimeout(_pollShowTimer); _pollShowTimer = null; } + pollSpinner.classList.remove('is-busy'); + pollSpinner.classList.toggle('is-error', !!err); + pollSpinner.title = err || ''; + } var statsDiv = E('div', { 'style': 'margin:8px 0', 'class': opts.showStats === false ? 'tc-hidden' : '' }); var connsDiv = E('div', { 'class': opts.showConns === false ? 'tc-hidden' : '' }); @@ -3043,11 +3083,11 @@ return view.extend({ self._queryGen++; var gen = self._queryGen; - setStatus(statusDiv, 'loading', _('Running…')); + pollStart(); callDevice(ip, proto).then(function(data) { if (!data || data.error) { - setStatus(statusDiv, 'error', (data && data.error) || _('Unknown error')); + pollEnd((data && data.error) || _('Unknown error')); return; } @@ -3188,9 +3228,9 @@ return view.extend({ } } } - setStatus(statusDiv, 'ok', '✓ ' + _('Done')); + pollEnd(); }) - .catch(function(e) { setStatus(statusDiv, 'error', '✗ '+e.message); }); + .catch(function(e) { pollEnd(e.message); }); } self._tableFilter = null; @@ -3368,7 +3408,7 @@ return view.extend({ } function runAll() { - setStatus(statusDiv, 'loading', _('Scanning all devices…')); + pollStart(); callTrafficctl().then(function(rows) { if (!Array.isArray(rows)) rows = []; @@ -3385,10 +3425,10 @@ return view.extend({ netifyMap = m; }).catch(function() {}); } - setStatus(statusDiv, 'ok', '✓ ' + _('Done')); + pollEnd(); self._startBytesPoll(); }) - .catch(function(e) { setStatus(statusDiv, 'error', '✗ '+e.message); }); + .catch(function(e) { pollEnd(e.message); }); } function updateExtendedStats() { @@ -3405,6 +3445,11 @@ return view.extend({ function runQuery() { var ip = searchSelect.getValue(); + // The poll used to wipe the status line on every tick, which also + // disposed of a result belonging to the device just left. Now that + // it does not, switching devices has to do it — otherwise + // "Limiting 192.168.0.50…" lingers over another device's page. + if (self._statusIp !== ip) { clearStatus(statusDiv); self._statusIp = ip; } var o = loadOpts(); o.lastIp = ip; saveOpts(o); updateUrlParams(o); updateModeUI(); @@ -3464,12 +3509,21 @@ return view.extend({ var action = inetBtn._action; var fn = action === 'block' ? callBlock : callUnblock; fn(ip, '').then(function(res) { - // A block without a known MAC covers IPv4 only, and the backend - // says so in msg. Swallowing it here would put the UI back to - // reporting a block that does not block (issue #67). - if (res && res.ipv6 === false) { - setStatus(statusDiv, 'error', (res && res.msg) || '?'); + // The result is shown whatever it says, like every other action. + // Reporting it only when res.ipv6 === false discarded the rest + // of what the backend qualifies: "NOT PERSISTENT" (#105) and the + // NSS note (#81) are appended to msg on a SUCCESSFUL block, so + // blocking a device — the one path that mattered to the reporter + // of #105 — said nothing at all. + var state; + if (!res || !res.ok || res.ipv6 === false) { + // A failure, or a block that does not block over v6 (#67). + state = 'error'; + } else { + // The device is now cut off, so not green. + state = (action === 'block') ? 'action' : 'ok'; } + setStatus(statusDiv, state, (res && res.msg) || '?'); runQuery(); }).catch(function(e) { setStatus(statusDiv, 'error', e.message); @@ -4113,7 +4167,7 @@ return view.extend({ offloadBanner, cutPanel, E('div', {'style':'margin-bottom:10px'}, [ - E('div', {'style':'display:flex;align-items:center;gap:10px;flex-wrap:wrap'}, [searchSelect.el, actionRow]), + E('div', {'style':'display:flex;align-items:center;gap:10px;flex-wrap:wrap'}, [searchSelect.el, actionRow, pollSpinner]), quickBar ]), statusDiv, diff --git a/luci-app-trafficctl/po/templates/luci-app-trafficctl.pot b/luci-app-trafficctl/po/templates/luci-app-trafficctl.pot index c0f277159..c480f7c8b 100644 --- a/luci-app-trafficctl/po/templates/luci-app-trafficctl.pot +++ b/luci-app-trafficctl/po/templates/luci-app-trafficctl.pot @@ -304,9 +304,6 @@ msgstr "" msgid "Distinct remote clients" msgstr "" -msgid "Done" -msgstr "" - msgid "Download" msgstr "" @@ -835,9 +832,6 @@ msgstr "" msgid "Routed via %s" msgstr "" -msgid "Running…" -msgstr "" - msgid "Sampled. Byte counts come from conntrack, which only reports flows that are alive when it is read — a connection that both starts and finishes between two samples is not counted. Shorten the sampling interval to narrow that gap." msgstr "" @@ -850,9 +844,6 @@ msgstr "" msgid "Saving…" msgstr "" -msgid "Scanning all devices…" -msgstr "" - msgid "Search device (name, IP, MAC)…" msgstr "" diff --git a/luci-app-vantage/Makefile b/luci-app-vantage/Makefile index 140af9f48..a064bdcff 100644 --- a/luci-app-vantage/Makefile +++ b/luci-app-vantage/Makefile @@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-vantage -PKG_VERSION:=1.0.11 -PKG_RELEASE:=18 +PKG_VERSION:=1.0.12 +PKG_RELEASE:=19 PKG_LICENSE:=GPL-3.0-or-later LUCI_TITLE:=Vantage dashboard @@ -26,7 +26,7 @@ LUCI_DESCRIPTION:=Live network dashboard (landing page under admin/dashboard): d # Optional at runtime, degrade when absent: rpcd-mod-rrdns (reverse DNS # names), umdns (mDNS names), hostapd ubus objects (WPS names, client # capabilities). -LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-iwinfo +rpcd-mod-ucode +ucode-mod-uci +ucode-mod-ubus +ucode-mod-fs +LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-iwinfo +rpcd-mod-ucode +ucode-mod-uci +ucode-mod-ubus +ucode-mod-fs +ucode-mod-rtnl LUCI_PKGARCH:=all LUCI_MAINTAINER:=anothaDev <10740588+anothaDev@users.noreply.github.com> LUCI_URL:=https://github.com/anothaDev/luci-theme-vantage diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css b/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css index 70d62c132..c3ae7800d 100644 --- a/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css @@ -338,12 +338,17 @@ html[data-darkmode="true"] .vt-app { .vt-app .vt-edit-hidden:empty { display: none; } .vt-app .vt-edit-hidden .vt-fchip .vt-ico { width: 13px; height: 13px; } .vt-app .vt-editbar-actions { display: flex; gap: 8px; margin-left: auto; } +/* the chart and network path picks wrap as one group */ +.vt-app .vt-edit-picks { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 10px 16px; min-width: 0; } .vt-app .vt-edit-field { display: flex; align-items: center; gap: 8px; min-width: 0; } .vt-app .vt-edit-field label { font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; } -/* narrow: the label above, the select across the bar, so a long choice - ("Mirrored, scale per direction") is not cut */ +/* narrow: the two picks side by side, each label above its select, so + the sticky bar stays short */ @container vt (max-width: 699px) { - .vt-app .vt-edit-field { flex: 1 1 100%; flex-direction: column; align-items: stretch; gap: 4px; } + .vt-app .vt-edit-picks { flex: 1 1 100%; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } + .vt-app .vt-edit-field { flex-direction: column; align-items: stretch; gap: 4px; } + .vt-app .vt-edit-field label { white-space: normal; } + .vt-app .vt-edit-field > .vt-select { width: 100%; min-width: 0; } } .vt-app .vt-select { appearance: none; -webkit-appearance: none; min-width: 0; max-width: 100%; @@ -622,7 +627,7 @@ h2, h3{ .vt-app .vt-kpi + .vt-kpi { box-shadow: -1px 0 0 var(--vt-line); border-radius: 0 var(--vt-r-sm) var(--vt-r-sm) 0; } .vt-app .vt-kpi-label { font-size: var(--vt-fs-micro); font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; } .vt-app .vt-kpi-value { font-size: var(--vt-fs-xl); font-weight: 700; letter-spacing: -.02em; line-height: 1.2; white-space: nowrap; } -.vt-app .vt-kpi-sub { font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.vt-app .vt-kpi-sub { max-width: 100%; font-size: var(--vt-fs-sm); color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .vt-app .vt-kpi-warn .vt-kpi-value { color: var(--vt-warn); } @container vthero (max-width: 700px) { .vt-app .vt-kpis { grid-template-columns: 1fr 1fr; } @@ -635,6 +640,10 @@ h2, h3{ -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,.55), transparent 85%); mask-image: linear-gradient(180deg, rgba(0,0,0,.55), transparent 85%); } +/* the backdrop (vantage/backdrop.js): a canvas over the dot grid and + under the wires and nodes, drawing over the path's height only */ +.vt-app .vt-backdrop { position: absolute; left: 0; top: 0; pointer-events: none; border-radius: inherit; } +.vt-app .vt-backdrop[hidden] { display: none; } .vt-app .vt-path { position: relative; display: grid; @@ -725,7 +734,8 @@ h2, h3{ .vt-app .vt-link-up { font-size: var(--vt-fs-sm); } .vt-app .vt-link-up b { font-size: var(--vt-fs-base); font-weight: 650; color: var(--vt-text-2); } .vt-app .vt-link-up .vt-ico { color: var(--vt-text-3); } -.vt-app .vt-link-meta { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-family: var(--vt-mono); white-space: nowrap; margin-top: 2px; } +.vt-app .vt-link-meta { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-family: var(--vt-mono); white-space: nowrap; margin-top: 2px; + padding: 0 6px; border-radius: 6px; background: var(--vt-surface); } /* the branches spread over the height the tile gets from its row, so a tall neighbour leaves no empty band above or below the diagram */ @@ -933,6 +943,9 @@ h2, h3{ /* ----------------------------------------------------------- 9. clients */ .vt-app .vt-clients { padding-bottom: 8px; } +/* a jump to a tile (the path's SSID, Wired and port nodes) stops below + the sticky top bar */ +.vt-app .vt-tile { scroll-margin-top: calc(var(--v-stick-top, 48px) + 12px); } .vt-app .vt-count { font-size: var(--vt-fs-sm); color: var(--vt-text-3); font-weight: 560; } .vt-app .vt-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 10px; } .vt-app .vt-filter { @@ -957,17 +970,60 @@ h2, h3{ } .vt-app .vt-filter-clear:hover { background: var(--vt-surface-3); color: var(--vt-text); } .vt-app .vt-filter-clear .vt-ico { width: 14px; height: 14px; } -.vt-app .vt-fchips { display: flex; flex-wrap: wrap; gap: 6px; } +/* Wi-Fi or wired, in the tile head */ +.vt-app .vt-kinds { display: inline-flex; padding: 2px; gap: 2px; border: 1px solid var(--vt-line); border-radius: 999px; background: var(--vt-surface-2); } +.vt-app .vt-kind { + display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 11px; margin: 0; min-width: 0; + border: 0; border-radius: 999px; background: transparent; box-shadow: none; text-transform: none; + color: var(--vt-text-2); font-size: var(--vt-fs-sm); font-weight: 600; cursor: pointer; +} +.vt-app .vt-kind:hover { color: var(--vt-text); } +.vt-app .vt-kind.vt-kind-on { background: var(--vt-surface); color: var(--vt-text); box-shadow: var(--vt-shadow); } +.vt-app .vt-kind-n { font-size: var(--vt-fs-micro); color: var(--vt-text-3); } +/* quick filters: a button at the end of the search box, a menu under it, + and the filter in force as a tag in the box */ +.vt-app .vt-filter-wrap { position: relative; display: flex; flex: 1 1 260px; max-width: 380px; min-width: 0; } +.vt-app .vt-filter-wrap > .vt-filter { flex: 1 1 auto; max-width: none; min-width: 0; } +.vt-app .vt-filter-btn { + position: relative; display: inline-flex; align-items: center; justify-content: center; flex: none; + width: calc(var(--vt-ctl) - 10px); height: calc(var(--vt-ctl) - 10px); padding: 0; margin: 0 -2px 0 0; min-width: 0; + border: 0; border-radius: var(--vt-r-ctl); background: transparent; box-shadow: none; color: var(--vt-text-3); cursor: pointer; +} +.vt-app .vt-filter-btn:hover, .vt-app .vt-filter-btn[aria-expanded="true"] { background: var(--vt-surface-3); color: var(--vt-text); } +.vt-app .vt-filter-btn.vt-filter-btn-on { color: var(--vt-accent); } +.vt-app .vt-filter-btn .vt-ico { width: 16px; height: 16px; } +.vt-app .vt-ftag { + display: inline-flex; align-items: center; gap: 4px; flex: none; max-width: 45%; height: 24px; padding: 0 6px 0 9px; margin: 0; min-width: 0; + border: 0; border-radius: 999px; background: var(--vt-accent-soft); box-shadow: none; text-transform: none; + color: var(--vt-text); font-size: var(--vt-fs-sm); font-weight: 600; cursor: pointer; +} +.vt-app .vt-ftag span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.vt-app .vt-ftag .vt-ico { width: 12px; height: 12px; flex: none; color: var(--vt-text-3); } +.vt-app .vt-ftag:hover .vt-ico { color: var(--vt-text); } +.vt-app .vt-fmenu { + position: absolute; top: calc(100% + 6px); right: 0; z-index: 6; + display: flex; flex-direction: column; min-width: 210px; padding: 5px; + border: 1px solid var(--vt-line-2); border-radius: var(--vt-r); background: var(--vt-surface); box-shadow: var(--vt-shadow-pop); +} +.vt-app .vt-fmenu[hidden] { display: none; } +.vt-app .vt-fmenu-item { + display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: center; gap: 8px; + height: 34px; padding: 0 10px 0 8px; margin: 0; min-width: 0; width: 100%; + border: 0; border-radius: var(--vt-r-ctl); background: transparent; box-shadow: none; text-align: left; text-transform: none; + color: var(--vt-text); font-size: var(--vt-fs-sm); font-weight: 560; cursor: pointer; +} +.vt-app .vt-fmenu-item:hover, .vt-app .vt-fmenu-item:focus-visible { background: var(--vt-surface-2); outline: none; } +.vt-app .vt-fmenu-check { display: inline-flex; color: var(--vt-accent); } +.vt-app .vt-fmenu-check .vt-ico { width: 14px; height: 14px; } +.vt-app .vt-fmenu-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.vt-app .vt-fmenu-n { color: var(--vt-text-3); font-size: var(--vt-fs-micro); font-weight: 600; } +.vt-app .vt-fmenu-warn .vt-fmenu-n { color: var(--vt-warn); } .vt-app .vt-fchip { display: inline-flex; align-items: center; gap: 6px; height: var(--vt-ctl); padding: 0 12px; margin: 0; border: 1px solid var(--vt-line); border-radius: 999px; background: var(--vt-surface); color: var(--vt-text-2); font-size: var(--vt-fs-sm); font-weight: 600; cursor: pointer; box-shadow: none; min-width: 0; text-transform: none; } .vt-app .vt-fchip:hover { border-color: var(--vt-line-2); color: var(--vt-text); background: var(--vt-surface-2); } -.vt-app .vt-fchip-n { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-weight: 600; } -.vt-app .vt-fchip-warn .vt-fchip-n { color: var(--vt-warn); } -.vt-app .vt-fchip.vt-fchip-on { background: var(--vt-text); border-color: var(--vt-text); color: var(--vt-surface); } -.vt-app .vt-fchip.vt-fchip-on .vt-fchip-n { color: color-mix(in oklab, var(--vt-surface) 70%, transparent); } .vt-app .vt-table-wrap { overflow-x: auto; margin: 0 -18px; padding: 0 18px; } .vt-app .vt-ctable { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: auto; min-width: 800px; } @@ -1038,7 +1094,9 @@ h2, h3{ font-size: var(--vt-fs-micro); line-height: 1.35; color: var(--vt-text-3); max-width: 170px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } -.vt-app .vt-td-time { white-space: nowrap; color: var(--vt-text-2); } +.vt-app .vt-td-time, .vt-app .vt-td-seen { white-space: nowrap; color: var(--vt-text-2); } +.vt-app .vt-td-link { white-space: nowrap; } +.vt-app .vt-td-link .vt-phy-up { padding-left: 0; } /* the Graphite shell tags numeric-looking cells with .vt-num (monospace); the dashboard sets its own type */ .vt-app td.vt-num { font-family: inherit; font-size: inherit; letter-spacing: inherit; } @@ -1253,8 +1311,7 @@ h2, h3{ .vt-app .vt-tile { padding: 14px 14px 16px; } .vt-app .vt-filter { max-width: none; flex-basis: 100%; } .vt-app .vt-key-hint { display: none; } - .vt-app .vt-fchips { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin: 0 -14px; padding: 0 14px; } - .vt-app .vt-fchip { flex: none; } + .vt-app .vt-filter-wrap { max-width: none; flex-basis: 100%; } .vt-app .vt-head { gap: 10px; } .vt-app .vt-headline { font-size: var(--vt-fs-lg); } .vt-app .vt-orb { width: 32px; height: 32px; } @@ -1297,13 +1354,14 @@ h2, h3{ .vt-app .vt-row:hover { background: var(--vt-surface-2); } .vt-app .vt-ctable tr.vt-row-sel { background: var(--vt-accent-soft); border-color: var(--vt-accent-line); } .vt-app .vt-ctable td.vt-td-name { grid-column: 1; grid-row: 1; min-width: 0; box-shadow: none !important; } - .vt-app .vt-ctable td.vt-td-signal { grid-column: 2; grid-row: 1; justify-self: end; align-self: start; padding-top: 2px; } + .vt-app .vt-ctable td.vt-td-signal, .vt-app .vt-ctable td.vt-td-link { grid-column: 2; grid-row: 1; justify-self: end; align-self: start; padding-top: 2px; text-align: right; } .vt-app .vt-ctable td.vt-td-net { grid-column: 1; grid-row: 2; padding-left: 44px; } .vt-app .vt-net { flex-direction: row; align-items: center; gap: 6px; flex-wrap: wrap; } .vt-app .vt-ctable td.vt-td-live { grid-column: 2; grid-row: 2; justify-self: end; text-align: right; } .vt-app .vt-live-cell { min-width: 0; } .vt-app .vt-live-cell .vt-mini, .vt-app .vt-live-split { display: none; } - .vt-app .vt-ctable td.vt-td-exp { grid-column: 1 / -1; grid-row: 3; padding-left: 44px; } + .vt-app .vt-ctable td.vt-td-exp, .vt-app .vt-ctable td.vt-td-seen { grid-column: 1 / -1; grid-row: 3; padding-left: 44px; } + .vt-app .vt-ctable td.vt-td-seen { font-size: var(--vt-fs-sm); color: var(--vt-text-3); } .vt-app .vt-exp-text span { max-width: none; -webkit-line-clamp: 1; } .vt-app .vt-exp { gap: 8px; } .vt-app .vt-exp-text { flex-direction: row; align-items: baseline; gap: 6px; min-width: 0; } diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/backdrop.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/backdrop.js new file mode 100644 index 000000000..0acea2092 --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/backdrop.js @@ -0,0 +1,438 @@ +'use strict'; +'require baseclass'; + +/* The network path's backdrop: one canvas behind the nodes and wires, in + the style the viewer picked under Customize. The two styles are the + login page's (luci-theme-vantage login.js), here driven by the live + uplink rate: + grid packets on the tile's dot grid: downloads enter from the + gateway's side and run right, uploads come back, now and then + a packet steps a row up or down; how many and how fast + follows the rate + stars a starfield streaming out of this device's node, faster with + more traffic + none the dot grid alone + It draws over the path's height only (not under the numbers below), + 20 frames a second while the traffic is light and up to 30 as it + grows (it is the frames, not the drawing, that cost), and stops + while another window has the focus (as the login page does), the + tab is hidden, or the tile is scrolled out of view or folded. With + reduced motion it draws one still frame. No libraries; the pure + parts are exported for tests. */ + +var STYLES = [ 'grid', 'stars', 'none' ]; +var GRID = 18, DOT = 1; /* app.css .vt-hero::before: 18 px tiles, a dot at 1 px */ + +function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; } + +/* bit/s -> 0 (10 kbit/s or less) .. 1 (1 Gbit/s or more), one fifth per + decade */ +function level(bps) { + return bps > 1e4 ? clamp(Math.log(bps / 1e4) / Math.LN10 / 5, 0, 1) : 0; +} + +/* the share of packets going right (downloads), kept between a sixth and + nine tenths so either direction stays visible; 0.7 before any rate */ +function downShare(down, up) { + var d = +down || 0, u = +up || 0; + return d + u > 0 ? clamp(d / (d + u), 0.16, 0.9) : 0.7; +} + +/* packets in a w x h area at a level: a couple when idle, about one per + 14,000 px² at the top (six to 36) */ +function packetCount(w, h, lvl) { + var top = clamp(Math.round(w * h / 14000), 6, 36); + return Math.round(2 + (top - 2) * clamp(lvl, 0, 1)); +} + +/* px per second along the grid */ +function packetSpeed(lvl) { return 45 + 175 * clamp(lvl, 0, 1); } + +/* depth per second of the starfield */ +function starSpeed(lvl) { return 0.03 + 0.32 * clamp(lvl, 0, 1); } + +/* ms between frames: 50 (20 a second) up to 100 kbit/s, where a packet + moves a few px a frame, down to 33 (30 a second) from 10 Mbit/s */ +function frameMs(lvl) { return Math.round(50 - 17 * clamp((lvl - .2) / .4, 0, 1)); } + +/* ---------------------------------------------------------------- grid */ + +function packets(env) { + var SHADES = 5, ALPHA = [ .6, .36, .2, .1, .04 ]; + var DIRS = [ [ 1, 0 ], [ 0, 1 ], [ -1, 0 ], [ 0, -1 ] ]; + var list = []; + + /* lattice lines inside the band: x = DOT + GRID * i */ + function line(v) { return DOT + GRID * v; } + function firstIn(lo) { return Math.ceil((lo - DOT) / GRID); } + function lastIn(hi) { return Math.floor((hi - DOT) / GRID); } + + function spawn(p, anywhere) { + var b = env.box, r0 = firstIn(b.t + 4), r1 = lastIn(b.b - 4), c0 = firstIn(0), c1 = lastIn(env.w); + if (r1 < r0) r1 = r0; + var main = Math.random() < env.share ? 0 : 2; + var row = r0 + Math.floor(Math.random() * (r1 - r0 + 1)); + var col = anywhere ? c0 + Math.floor(Math.random() * (c1 - c0 + 1)) : main === 0 ? c0 - 2 : c1 + 2; + p.main = main; p.d = main; p.x = line(col); p.y = line(row); p.pts = [ [ p.x, p.y ] ]; + p.v = env.speed * (0.7 + Math.random() * 0.6); + p.len = clamp(p.v * 0.45, 28, 120); + p.next = GRID; p.retire = false; + return p; + } + + /* drops the corners behind the end of the trail; returns its tail */ + function trim(p) { + var left = p.len, hx = p.x, hy = p.y; + for (var k = p.pts.length - 1; k >= 0; k--) { + var q = p.pts[k], seg = Math.abs(hx - q[0]) + Math.abs(hy - q[1]); + if (seg >= left) { + if (k > 0) p.pts.splice(0, k); + return [ hx + (q[0] - hx) * left / (seg || 1), hy + (q[1] - hy) * left / (seg || 1) ]; + } + left -= seg; hx = q[0]; hy = q[1]; + } + return [ hx, hy ]; + } + + /* at a crossing: off the main line now and then, back onto it soon; + never out of the band */ + function turn(p) { + var b = env.box; + if (p.d === 0 || p.d === 2) { + if (Math.random() < 0.16) { + var d = Math.random() < 0.5 ? 1 : 3, ny = p.y + DIRS[d][1] * GRID; + if (ny < b.t + 4 || ny > b.b - 4) d = 4 - d; + p.pts.push([ p.x, p.y ]); p.d = d; + } + } + else { + var ny2 = p.y + DIRS[p.d][1] * GRID; + if (Math.random() < 0.55 || ny2 < b.t + 4 || ny2 > b.b - 4) { p.pts.push([ p.x, p.y ]); p.d = p.main; } + } + } + + return { + /* the count follows the level: new packets enter at an edge, + extra ones finish their run; a finishing one is called back + before a new one is made, so the list never outgrows the + largest count, also while nothing moves */ + resize: function(initial) { + var n = env.count, live = 0, i; + list.forEach(function(p) { if (!p.retire) live++; }); + if (initial) { list.length = 0; live = 0; } + for (i = 0; i < list.length && live > n; i++) if (!list[i].retire) { list[i].retire = true; live--; } + for (i = 0; i < list.length && live < n; i++) if (list[i].retire) { list[i].retire = false; live++; } + while (live < n) { list.push(spawn({}, !!initial)); live++; } + }, + step: function(dt) { + var b = env.box, m = 3 * GRID; + for (var i = 0; i < list.length; i++) { + var p = list[i], go = p.v * dt; + while (go > 0) { + var s = Math.min(go, p.next); + p.x += DIRS[p.d][0] * s; p.y += DIRS[p.d][1] * s; p.next -= s; go -= s; + if (p.next <= 0) { p.next = GRID; turn(p); } + } + var tail = trim(p); + if ((p.x < -m || p.x > env.w + m) && (tail[0] < -m || tail[0] > env.w + m)) { + if (p.retire) { list.splice(i, 1); i--; } + else spawn(p, false); + } + /* a band that shrank under a packet */ + else if (p.y < b.t - GRID || p.y > b.b + GRID) spawn(p, false); + } + }, + draw: function(ctx) { + var shade = [], heads = new Path2D(), glow = new Path2D(); + for (var k = 0; k < SHADES; k++) shade.push(new Path2D()); + for (var i = 0; i < list.length; i++) { + var p = list[i], hx = p.x, hy = p.y, run = 0, piece = p.len / SHADES; + for (var j = p.pts.length - 1, n = 0; j >= 0 && n < SHADES; j--) { + var q = p.pts[j], seg = Math.abs(hx - q[0]) + Math.abs(hy - q[1]), at = 0; + while (at < seg && n < SHADES) { + var take = Math.min(seg - at, piece * (n + 1) - run), t0 = at / (seg || 1), t1 = (at + take) / (seg || 1); + shade[n].moveTo(hx + (q[0] - hx) * t0, hy + (q[1] - hy) * t0); + shade[n].lineTo(hx + (q[0] - hx) * t1, hy + (q[1] - hy) * t1); + at += take; run += take; + if (run >= piece * (n + 1) - .01) n++; + } + hx = q[0]; hy = q[1]; + } + heads.rect(p.x - 1.25, p.y - 1.25, 2.5, 2.5); + glow.rect(p.x - 3.5, p.y - 3.5, 7, 7); + } + ctx.strokeStyle = ctx.fillStyle = env.colour; + ctx.lineWidth = 1.25; + for (var s = 0; s < SHADES; s++) { ctx.globalAlpha = ALPHA[s]; ctx.stroke(shade[s]); } + ctx.globalAlpha = .12; ctx.fill(glow); + ctx.globalAlpha = .8; ctx.fill(heads); + ctx.globalAlpha = 1; + }, + size: function() { return list.length; }, + /* for tests */ + items: function() { return list; } + }; +} + +/* --------------------------------------------------------------- stars */ + +function starfield(env) { + var TRAIL = .1, WIDTH = [ .8, 1, 1.3 ], ALPHA = [ .2, .32, .45 ]; + /* it starts fast and settles to the rate's speed, as on the login page */ + var list = [], speed = 0.45, f = 1; + + function place(s, z) { + var a = Math.random() * 2 * Math.PI, r = .03 + Math.sqrt(Math.random()) * .97; + s.x = Math.cos(a) * r; s.y = Math.sin(a) * r; s.z = z; s.c = Math.random() < .25 ? 1 : 0; + return s; + } + + return { + resize: function() { + f = Math.max(env.w, env.box.b - env.box.t) * .12; + var n = clamp(Math.round(env.w * (env.box.b - env.box.t) / 7000), 24, 160); + while (list.length < n) list.push(place({}, .05 + Math.random() * .95)); + list.length = n; + }, + step: function(dt) { + speed += (env.speed - speed) * Math.min(1, dt * 1.5); + var o = env.origin, b = env.box; + for (var i = 0; i < list.length; i++) { + var s = list[i]; + s.z -= speed * dt; + var px = o.x + s.x * f / s.z, py = o.y + s.y * f / s.z; + if (s.z <= .02 || px < -20 || px > env.w + 20 || py < b.t - 20 || py > b.b + 20) place(s, .8 + Math.random() * .2); + } + }, + draw: function(ctx) { + var o = env.origin, paths = [ new Path2D(), new Path2D(), new Path2D(), new Path2D(), new Path2D(), new Path2D() ]; + for (var i = 0; i < list.length; i++) { + var s = list[i], z0 = Math.min(1, s.z + speed * TRAIL + .004); + var p = paths[(s.z > .5 ? 0 : s.z > .2 ? 1 : 2) * 2 + s.c]; + p.moveTo(o.x + s.x * f / z0, o.y + s.y * f / z0); + p.lineTo(o.x + s.x * f / s.z, o.y + s.y * f / s.z); + } + ctx.lineCap = 'round'; + for (var k = 0; k < 6; k++) { + ctx.globalAlpha = ALPHA[k >> 1]; + ctx.lineWidth = WIDTH[k >> 1]; + ctx.strokeStyle = (k & 1) ? env.colour : env.muted; + ctx.stroke(paths[k]); + } + ctx.globalAlpha = 1; + }, + size: function() { return list.length; }, + /* for tests */ + items: function() { return list; } + }; +} + +/* ---------------------------------------------------------- the canvas */ + +/* host: the tile (the canvas covers its padding box, as the dot grid); + band: the element whose height it draws over; style: one of STYLES */ +function mount(host, band, style) { + var cv = document.createElement('canvas'), ctx = cv.getContext && cv.getContext('2d'); + var env = { w: 0, h: 0, box: { t: 0, b: 0 }, origin: { x: 0, y: 0 }, count: 2, speed: packetSpeed(0), share: 0.7, frame: frameMs(0), colour: '#7a9a3a', muted: '#8a9099' }; + var down = null, up = null, originEl = null; + var scene = null, cur = 'none', raf = 0, timer = 0, last = 0, dpr = 1, inView = true, hidden = false, still = false, mq = null; + var focused = typeof document.hasFocus === 'function' ? document.hasFocus() : true; + var ro = null, io = null, onMq = null, stillRated = false, mo = null, scheme = null, onScheme = null; + + cv.className = 'vt-backdrop'; + cv.setAttribute('aria-hidden', 'true'); + if (!ctx || typeof Path2D !== 'function') return { style: function() {}, update: function() {}, stop: function() {}, canvas: null }; + host.insertBefore(cv, host.firstChild); + + if (window.matchMedia) { + mq = window.matchMedia('(prefers-reduced-motion: reduce)'); + still = !!mq.matches; + onMq = function() { still = !!mq.matches; run(); paint(); }; + if (mq.addEventListener) mq.addEventListener('change', onMq); + } + + /* a colour token of the app (set on .vt-app, often a color-mix()) + as the canvas takes it: the computed colour of the canvas element, + kept only when the context parses it */ + function token(name, fallback) { + var v = ''; + try { cv.style.color = 'var(' + name + ')'; v = getComputedStyle(cv).color; } catch (e) { v = ''; } + if (!v || !cv.isConnected) return fallback; + ctx.fillStyle = '#010203'; + ctx.fillStyle = v; + return ctx.fillStyle !== '#010203' ? v : fallback; + } + + function colours() { + env.colour = token('--vt-accent-line', env.colour); + env.muted = token('--vt-text-3', env.muted); + } + + function measure() { + var hr = host.getBoundingClientRect(), br = band.getBoundingClientRect(); + var ox = hr.left + host.clientLeft, oy = hr.top + host.clientTop; + env.w = host.clientWidth; env.h = host.clientHeight; + env.box = { t: Math.max(0, br.top - oy), b: Math.min(env.h, br.bottom - oy) }; + if (originEl) { + var r = originEl.getBoundingClientRect(); + env.origin = { x: r.left + r.width / 2 - ox, y: r.top + r.height / 2 - oy }; + } + else env.origin = { x: env.w / 2, y: (env.box.t + env.box.b) / 2 }; + } + + function load() { + var lvl = level((+down || 0) + (+up || 0)); + env.count = packetCount(env.w, env.box.b - env.box.t, lvl); + env.speed = cur === 'stars' ? starSpeed(lvl) : packetSpeed(lvl); + env.share = downShare(down, up); + env.frame = frameMs(lvl); + } + + function usable() { return cur !== 'none' && env.w > 40 && env.box.b - env.box.t > 40; } + + /* the scene to the size and level: built across the band the first + time it has room and run for a moment, so the trails are there; + adjusted after that. A still frame is built once more when the + first rate is in, and then left alone. */ + function fit(initial) { + if (!scene || !usable()) return; + var rated = down != null || up != null; + if (initial || !scene.size() || (still && rated && !stillRated)) { + stillRated = still && rated; + scene.resize(true); + for (var i = 0; i < 12; i++) scene.step(.05); + } + else if (!still) scene.resize(false); + } + + function size(initial) { + measure(); + dpr = Math.min(window.devicePixelRatio || 1, 1.5); + cv.width = Math.round(env.w * dpr); cv.height = Math.round(env.h * dpr); + cv.style.width = env.w + 'px'; cv.style.height = env.h + 'px'; + load(); + fit(initial); + paint(); + } + + function paint() { + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, env.w, env.h); + if (!scene || !usable()) return; + ctx.save(); + ctx.beginPath(); ctx.rect(0, env.box.t, env.w, env.box.b - env.box.t); ctx.clip(); + scene.draw(ctx); + ctx.restore(); + } + + /* one frame, then a wait: the browser is asked only for the frames + that are drawn (not one per display refresh), and they stay on its + frame clock; the clock starts when the loop does */ + function tick(t) { + raf = 0; + if (!running()) return; + scene.step(clamp((t - last) / 1000, 0, .1)); + last = t; + paint(); + /* the wait ends a little early, the browser's next frame is the one */ + timer = setTimeout(again, Math.max(0, env.frame - 12)); + } + function again() { timer = 0; if (running() && !raf) raf = requestAnimationFrame(tick); } + + function running() { return !!scene && !still && !hidden && focused && inView && usable(); } + function halt() { + if (raf) cancelAnimationFrame(raf); + if (timer) clearTimeout(timer); + raf = timer = 0; + } + function run() { + if (running()) { if (!raf && !timer) { last = performance.now(); raf = requestAnimationFrame(tick); } } + else halt(); + } + + function setStyle(s) { + s = STYLES.indexOf(s) >= 0 ? s : 'grid'; + if (s === cur && scene) return; + cur = s; + scene = s === 'grid' ? packets(env) : s === 'stars' ? starfield(env) : null; + cv.hidden = !scene; + colours(); + size(true); + run(); + } + + if (typeof ResizeObserver === 'function') { + ro = new ResizeObserver(function() { size(false); run(); }); + ro.observe(host); + ro.observe(band); + } + if (typeof IntersectionObserver === 'function') { + io = new IntersectionObserver(function(es) { inView = es.some(function(e) { return e.isIntersecting; }); run(); }); + io.observe(host); + } + /* a theme switch (the theme's data-darkmode / data-theme, or the + system's scheme): the new colours at once, not at the next poll */ + onScheme = function() { colours(); if (!running()) paint(); }; + if (typeof MutationObserver === 'function') { + mo = new MutationObserver(onScheme); + mo.observe(document.documentElement, { attributes: true, attributeFilter: [ 'data-darkmode', 'data-theme' ] }); + } + if (window.matchMedia) { + scheme = window.matchMedia('(prefers-color-scheme: dark)'); + if (scheme.addEventListener) scheme.addEventListener('change', onScheme); + } + var onVis = function() { hidden = !!document.hidden; run(); }; + document.addEventListener('visibilitychange', onVis); + /* another window has the focus: the last frame stays */ + var onBlur = function() { focused = false; run(); }, onFocus = function() { focused = true; run(); }; + window.addEventListener('blur', onBlur); + window.addEventListener('focus', onFocus); + + setStyle(style); + + return { + canvas: cv, + style: setStyle, + /* each poll: the uplink's rate (bit/s each way) and the node the + stars stream from; the colours follow a theme switch */ + update: function(o) { + o = o || {}; + down = o.down; up = o.up; + if (o.origin !== undefined) originEl = o.origin; + colours(); + measure(); + load(); + fit(false); + if (still) paint(); + run(); + }, + running: running, + stop: function() { + halt(); scene = null; + if (ro) ro.disconnect(); + if (io) io.disconnect(); + document.removeEventListener('visibilitychange', onVis); + window.removeEventListener('blur', onBlur); + window.removeEventListener('focus', onFocus); + if (mq && onMq && mq.removeEventListener) mq.removeEventListener('change', onMq); + if (scheme && scheme.removeEventListener) scheme.removeEventListener('change', onScheme); + if (mo) mo.disconnect(); + if (cv.parentNode) cv.parentNode.removeChild(cv); + } + }; +} + +return baseclass.extend({ + STYLES: STYLES, + GRID: GRID, + DOT: DOT, + level: level, + downShare: downShare, + packetCount: packetCount, + packetSpeed: packetSpeed, + starSpeed: starSpeed, + frameMs: frameMs, + packets: packets, + starfield: starfield, + mount: mount +}); diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/layout.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/layout.js index 3fce498b4..2b6fb6d2b 100644 --- a/luci-app-vantage/htdocs/luci-static/resources/vantage/layout.js +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/layout.js @@ -13,8 +13,8 @@ var TILES = [ 'hero', 'chart', 'radios', 'clients', 'sys', 'talk', 'ssids' ]; -/* tiles that only exist with Wi-Fi */ -var WIFI = { radios: true, clients: true, talk: true, ssids: true }; +/* tiles that only exist with Wi-Fi (Clients lists wired hosts too) */ +var WIFI = { radios: true, talk: true, ssids: true }; var COLS = { wide: 4, mid: 2, narrow: 1 }; @@ -37,9 +37,9 @@ var DEFAULTS = { narrow: [ [ 'clients', 0, 1 ], [ 'talk', 0, 1 ], [ 'radios', 0, 1 ], [ 'hero', 0, 1 ], [ 'ssids', 0, 1 ], [ 'chart', 0, 1 ], [ 'sys', 0, 1 ] ] }; var DEFAULTS_WIRED = { - wide: [ [ 'hero', 0, 4 ], [ 'chart', 0, 3 ], [ 'sys', 3, 1 ] ], - mid: [ [ 'hero', 0, 2 ], [ 'chart', 0, 2 ], [ 'sys', 0, 2 ] ], - narrow: [ [ 'hero', 0, 1 ], [ 'chart', 0, 1 ], [ 'sys', 0, 1 ] ] + wide: [ [ 'hero', 0, 4 ], [ 'chart', 0, 3 ], [ 'sys', 3, 1 ], [ 'clients', 0, 4 ] ], + mid: [ [ 'hero', 0, 2 ], [ 'chart', 0, 2 ], [ 'sys', 0, 2 ], [ 'clients', 0, 2 ] ], + narrow: [ [ 'hero', 0, 1 ], [ 'clients', 0, 1 ], [ 'chart', 0, 1 ], [ 'sys', 0, 1 ] ] }; function own(o, k) { return o != null && Object.prototype.hasOwnProperty.call(o, k); } diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/model.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/model.js index 865b23ab0..09b53773b 100644 --- a/luci-app-vantage/htdocs/luci-static/resources/vantage/model.js +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/model.js @@ -193,10 +193,14 @@ return baseclass.extend({ wan/wan6/*wan* priority only on a tie. A softwire (DS-Lite, MAP-E, 464XLAT) carries IPv4 over the IPv6 uplink, so it ranks after IPv6: the uplink shown is the line, not the tunnel. Fall back to WAN names, then - the first up interface. A bridge is followed to its wired member - (wireless members skipped; netifd 'ethernet' preferred); DSA with - hardware offload uses the conduit's counters. */ - resolveUplink: function(ifaces, devs, wirelessNames) { + the first up interface. A bridge, or a VLAN on one (br-lan.10), is + followed to its wired member (wireless members skipped): the port + given as `via` (where the bridge learned the gateway), else one with + a link, netifd 'ethernet' preferred. `port` is the member known to lead to the + gateway: `via`, or the bridge's only wired member; null when it has + several and the gateway's port is unknown. DSA with hardware + offload uses the conduit's counters. */ + resolveUplink: function(ifaces, devs, wirelessNames, via) { var self = this; ifaces = arr(ifaces); devs = obj(devs); @@ -243,13 +247,19 @@ return baseclass.extend({ var pick = routed.length ? routed[0].i : preferredWan(up) || up[0]; if (!pick) return null; - var l3 = pick.l3_device || pick.device, dev = l3, d = devs[dev]; - if (d && (d.type === 'bridge' || d.devtype === 'bridge') && Array.isArray(d['bridge-members'])) { - var members = d['bridge-members']; + var l3 = pick.l3_device || pick.device, dev = l3, d = devs[dev], port = dev; + var isBridge = function(x) { return !!x && (x.type === 'bridge' || x.devtype === 'bridge') && Array.isArray(x['bridge-members']); }; + var vlanOf = /^(.+)\.\d{1,4}$/.exec(dev); + var br = isBridge(d) ? d : (vlanOf && isBridge(devs[vlanOf[1]])) ? devs[vlanOf[1]] : null; + if (br) { + var members = br['bridge-members']; var wired = members.filter(function(m) { return !self.isWireless(m, devs[m], wirelessNames); }); var eth = wired.filter(function(m) { return devs[m] && devs[m].devtype === 'ethernet'; }); - var next = eth[0] || wired[0] || members[0]; + var lit = function(m) { return devs[m] && devs[m].carrier === true; }; + var known = typeof via === 'string' && wired.indexOf(via) >= 0 ? via : null; + var next = known || eth.filter(lit)[0] || wired.filter(lit)[0] || eth[0] || wired[0] || members[0]; if (next) { dev = next; d = devs[dev] || null; } + port = known || (wired.length === 1 ? wired[0] : null); } var statsDev = dev; @@ -261,6 +271,7 @@ return baseclass.extend({ iface: pick.interface, l3dev: l3, dev: dev, + port: port, statsDev: statsDev, isWan: /wan/i.test(pick.interface), proto: pick.proto || null, @@ -368,6 +379,7 @@ return baseclass.extend({ /* raw: { board, info, ifaces, devs, wifi, iwinfo: { ifname: info }, assoc: { ifname: [ station ] }, hapd: { ifname: { clients } }, hapdStatus: { ifname: status }, hints, aliases, rdns, mdns, + fdb: [ luci.vantage wired host ] (-> wiredHosts), vendor: fn(mac) -> name|null } */ build: function(raw) { raw = raw || {}; @@ -382,13 +394,20 @@ return baseclass.extend({ var firstIf = ifs.filter(function(i) { return i.ifname && own(iw, i.ifname); })[0]; var info0 = firstIf ? obj(iw[firstIf.ifname]) : {}; var band = wifi.bandKey(cfg.band, info0.frequency || rIw.frequency); - var ht = wifi.htmode(info0.htmode || cfg.htmode); + /* a radio read from the config (luci.vantage wireless without + netifd: vendor drivers such as mtkwifi) has no run state, and + its driver may report less than it runs: mtkwifi's iwinfo + stops at HE with EHT configured. Elsewhere iwinfo wins. */ + var fromCfg = r.source === 'config', cfgHt = wifi.htmode(cfg.htmode); + var htmode = fromCfg && cfgHt.std === 'EHT' ? cfg.htmode : (info0.htmode || cfg.htmode || null); + var ht = wifi.htmode(htmode); var chan = num(info0.channel || rIw.channel || cfg.channel) || null; + var live = ifs.some(function(i) { return typeof i.ifname === 'string' && own(iw, i.ifname) && num(obj(iw[i.ifname]).channel) > 0; }); var radio = { id: rname, band: band, bandLabel: wifi.bandLabel(band), - up: r.up === true, + up: r.up === true || (fromCfg && live), disabled: r.disabled === true || cfg.disabled === true || cfg.disabled === '1', pending: r.pending === true, channel: chan, @@ -398,7 +417,7 @@ return baseclass.extend({ width: ht.width, std: ht.std, gen: wifi.genName(ht.std, band), - htmode: info0.htmode || cfg.htmode || null, + htmode: htmode, txpower: finite(info0.txpower) ? info0.txpower : (finite(rIw.txpower) ? rIw.txpower : null), txpowerCfg: cfg.txpower != null && isFinite(+cfg.txpower) ? +cfg.txpower : null, noise: wifi.noise(finite(info0.noise) ? info0.noise : rIw.noise), @@ -458,6 +477,41 @@ return baseclass.extend({ if (mac) everyone.push({ mac: mac, ips: self.hintAddrs(hintOf(mac), subnets) }); }); }); + /* wired hosts: what the bridges learned on ports that are not + Wi-Fi, once per MAC (the entry heard last, a learned one before + a static one), stations left out */ + var staMacs = Object.create(null), fdb = Object.create(null), wiredMacs = []; + everyone.forEach(function(e) { staMacs[e.mac] = true; }); + arr(raw.fdb).forEach(function(h) { + var mac = h && names.normMac(h.mac); + if (!mac || staMacs[mac] || typeof h.port !== 'string' || self.isWireless(h.port, devs[h.port], wlNames)) return; + var seen = finite(h.seen) && h.seen >= 0 ? h.seen : null, prev = fdb[mac]; + if (prev && (seen == null || (prev.seen != null && prev.seen <= seen))) return; + if (!prev) wiredMacs.push(mac); + fdb[mac] = { port: h.port, bridge: typeof h.bridge === 'string' ? h.bridge : null, vlan: finite(h.vlan) ? h.vlan : null, seen: seen }; + }); + /* the gateway, by the address the host hints give it; on a bridge + with several ports the uplink is the port it was learned on */ + var gwMac = null; + if (uplink && uplink.gateway) Object.keys(hints).forEach(function(k) { + var mac = names.normMac(k); + if (mac && arr(obj(hints[k]).ipaddrs).concat(arr(obj(hints[k]).ip6addrs)).indexOf(uplink.gateway) >= 0) gwMac = mac; + }); + if (uplink && gwMac && fdb[gwMac] && uplink.port !== fdb[gwMac].port) { + var via = this.resolveUplink(raw.ifaces, devs, wlNames, fdb[gwMac].port); + if (via && via.port === fdb[gwMac].port) uplink = via; + } + /* the network on each bridge (or bridge VLAN: br-lan.10), and its + IPv4 subnets for the hosts' addresses */ + var lans = Object.create(null); + arr(raw.ifaces).forEach(function(i) { + var d = i && (i.l3_device || i.device); + if (typeof d === 'string' && i.interface && !lans[d]) + lans[d] = { name: String(i.interface), ipv4: arr(i['ipv4-address']).map(function(a) { return a && a.address ? a.address + '/' + a.mask : null; }).filter(Boolean) }; + }); + function lanOf(h) { return h.bridge ? (h.vlan != null && lans[h.bridge + '.' + h.vlan]) || lans[h.bridge] || null : null; } + function wiredSubnets(mac) { var l = lanOf(fdb[mac]); return l && l.ipv4.length ? l.ipv4 : subnets; } + wiredMacs.forEach(function(mac) { everyone.push({ mac: mac, ips: self.hintAddrs(hintOf(mac), wiredSubnets(mac)) }); }); var mdns = names.mdnsForStations(raw.mdns, everyone); ssids.forEach(function(ss) { @@ -525,6 +579,43 @@ return baseclass.extend({ }); }); + /* A host alone on its port has the port's link and traffic to + itself; hosts sharing one (behind a switch or another access + point) only share the port's. The gateway is found by the + address the host hints give it (above). */ + var perPort = Object.create(null); + wiredMacs.forEach(function(mac) { var p = fdb[mac].port; perPort[p] = (perPort[p] || 0) + 1; }); + var wired = wiredMacs.map(function(mac) { + var h = fdb[mac], d = devs[h.port], hint = hintOf(mac), ips = self.hintAddrs(hint, wiredSubnets(mac)); + var alias = raw.aliases && raw.aliases[mac]; + var vendor = typeof raw.vendor === 'function' ? raw.vendor(names.ouiKey(mac)) : null; + var nm = names.resolve({ mac: mac, alias: alias, ips: ips, rdns: raw.rdns, mdns: mdns, hint: hint, wps: null, vendor: vendor }); + return { + mac: mac, + macLabel: fmt.mac(mac), + name: nm.name, + nameSource: nm.source, + nameSourceLabel: nm.sourceLabel, + icon: nm.icon, + secondary: nm.secondary, + isPrivate: nm.isPrivate, + alias: alias ? alias.name : null, + aliasSid: alias ? alias.sid : null, + vendor: nm.source === 'vendor' ? vendor : null, + ips: ips, + port: h.port, + bridge: h.bridge, + network: lanOf(h) ? lanOf(h).name : null, + vlan: h.vlan, + seen: h.seen, + peers: perPort[h.port], + shared: perPort[h.port] > 1, + speed: d ? self.parseSpeed(d.speed) : null, + carrier: d ? d.carrier !== false : null, + gateway: mac === gwMac + }; + }); + var mem = this.memory(info.memory); var release = obj(board.release); return { @@ -547,7 +638,8 @@ return baseclass.extend({ networks: this.networks(raw.ifaces, uplink), radios: radios, ssids: ssids, - clients: clients + clients: clients, + wiredHosts: wired }; }, diff --git a/luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js b/luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js index 827099053..dde80f3ca 100644 --- a/luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js +++ b/luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js @@ -12,6 +12,7 @@ 'require vantage.series as series'; 'require vantage.layout as layout'; 'require vantage.wires as wires'; +'require vantage.backdrop as backdrop'; /* Vantage dashboard (luci-app-vantage): a live, read-only overview of the device, its uplink, radios, SSIDs and stations. The only write is the @@ -36,6 +37,8 @@ var callDevs = rpc.declare({ object: 'network.device', method: 'status', expect: without keys or RADIUS secrets, and the validated alias write */ var callWifi = rpc.declare({ object: 'luci.vantage', method: 'wireless', expect: { '': {} } }); var callThermal = rpc.declare({ object: 'luci.vantage', method: 'thermal', expect: { zones: [] } }); +/* { hosts, crowded }: crowded when a bridge holds too many entries to list */ +var callWired = rpc.declare({ object: 'luci.vantage', method: 'wired', expect: { '': {} } }); var callHints = rpc.declare({ object: 'luci-rpc', method: 'getHostHints', expect: { '': {} } }); var callIwInfo = rpc.declare({ object: 'iwinfo', method: 'info', params: [ 'device' ], expect: { '': {} } }); var callAssoc = rpc.declare({ object: 'iwinfo', method: 'assoclist', params: [ 'device' ], expect: { results: [] } }); @@ -75,6 +78,7 @@ function reducedMotion() { /* ------------------------------------------------------------- SVG kit */ var ICONS = { + filter: [ 'M4 5.5h16l-6.2 7.3v5.2l-3.6 1.8v-7Z' ], globe: [ 'M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18Z', 'M3.4 9.2h17.2M3.4 14.8h17.2', 'M12 3c2.4 2.5 3.6 5.5 3.6 9s-1.2 6.5-3.6 9c-2.4-2.5-3.6-5.5-3.6-9s1.2-6.5 3.6-9Z' ], ap: [ 'M4.5 15h15a1.5 1.5 0 0 1 1.5 1.5v1.5a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 18v-1.5A1.5 1.5 0 0 1 4.5 15Z', 'M7 17.25h.01', 'M8.6 10.6a4.8 4.8 0 0 1 6.8 0', 'M6.1 8.1a8.4 8.4 0 0 1 11.8 0', 'M11.3 12.9a1 1 0 0 1 1.4 0' ], radio: [ 'M12 12.5v8', 'M8.5 20.5h7', 'M9.2 8.7a4 4 0 0 0 0 5.6', 'M14.8 8.7a4 4 0 0 1 0 5.6', 'M6.4 5.9a8 8 0 0 0 0 11.2', 'M17.6 5.9a8 8 0 0 1 0 11.2', 'M12 10.3a1.2 1.2 0 1 1 0 2.4a1.2 1.2 0 1 1 0-2.4Z' ], @@ -438,6 +442,23 @@ function writeChartStyle(v) { } catch (e) {} } +/* The network path's backdrop the viewer picked (backdrop.STYLES), in + this browser */ +var BACKDROP_KEY = 'vantage.backdrop.v1'; + +function readBackdrop() { + var v = null; + try { v = window.localStorage.getItem(BACKDROP_KEY); } catch (e) { v = null; } + return backdrop.STYLES.indexOf(v) >= 0 ? v : backdrop.STYLES[0]; +} + +function writeBackdrop(v) { + try { + if (v === backdrop.STYLES[0]) window.localStorage.removeItem(BACKDROP_KEY); + else window.localStorage.setItem(BACKDROP_KEY, v); + } catch (e) {} +} + /* ================================================================= view */ return view.extend({ @@ -484,9 +505,11 @@ return view.extend({ /* null when the call fails: no answer is not "no radios" */ L.resolveDefault(callWifi(), null), L.resolveDefault(callHints(), {}), - L.resolveDefault(callThermal(), []) + L.resolveDefault(callThermal(), []), + L.resolveDefault(callWired(), {}) ]).then(function(r) { var wifiDevs = (r[4] && typeof r[4] === 'object') ? r[4] : null; + var wiredReply = (r[7] && typeof r[7] === 'object') ? r[7] : {}; var ifnames = []; Object.keys(wifiDevs || {}).forEach(function(rn) { var ifs = (wifiDevs[rn] && Array.isArray(wifiDevs[rn].interfaces)) ? wifiDevs[rn].interfaces : []; @@ -513,6 +536,7 @@ return view.extend({ else iwinfo[p[1]] = p[2]; }); return { info: r[0] || {}, stat: r[1] || '', ifaces: r[2] || [], devs: r[3] || {}, wifi: wifiDevs, hints: r[5] || {}, thermal: r[6] || [], + fdb: Array.isArray(wiredReply.hosts) ? wiredReply.hosts : [], fdbCrowded: wiredReply.crowded === true, assoc: assoc, hapd: hapd, hapdStatus: hapdStatus, iwinfo: iwinfo, at: Date.now(), started: started }; }); }); @@ -548,12 +572,13 @@ return view.extend({ wireless: null, lastOk: 0, lastErr: null, paused: false }; - this.ui = { sort: 'live', dir: -1, filter: '', chip: 'all', drawer: null, editing: null, hover: null }; + this.ui = { sorts: { wifi: { key: 'live', dir: -1 }, wired: { key: 'seen', dir: 1 } }, kind: 'wifi', filter: '', chip: 'all', drawer: null, editing: null, hover: null }; this.folds = readFolds(); this.foldEls = {}; this.foldBtns = {}; this.layouts = readLayouts(); this.chartStyle = readChartStyle(); + this.backdropStyle = readBackdrop(); this.editing = false; this.drag = null; this.applyMdns(data.mdns); @@ -585,7 +610,7 @@ return view.extend({ var m = model.build({ board: this.board, info: raw.info, ifaces: raw.ifaces, devs: raw.devs, wifi: raw.wifi, iwinfo: raw.iwinfo, - assoc: raw.assoc, hapd: raw.hapd, hapdStatus: raw.hapdStatus, hints: raw.hints, + assoc: raw.assoc, hapd: raw.hapd, hapdStatus: raw.hapdStatus, hints: raw.hints, fdb: raw.fdb, aliases: this.aliases, rdns: st.rdns, mdns: st.mdns, vendor: st.oui ? function(k) { return k ? st.oui.lookup(k) : null; } : null }); @@ -692,18 +717,19 @@ return view.extend({ }, /* names beyond what one poll carries: reverse DNS once per new - address, mDNS every minute when present, the vendor table once */ + address, mDNS every minute when present, the vendor table once; + for Wi-Fi clients and wired hosts alike */ resolveMore: function(m) { var st = this.st, self = this; - var ips = []; + var ips = [], all = m.clients.concat(m.wiredHosts || []); /* answers are kept for the session, but only for addresses still in use once there are many */ if (Object.keys(st.rdns).length > 1024) { var keep = Object.create(null); - m.clients.forEach(function(c) { c.ips.forEach(function(ip) { if (ip in st.rdns) keep[ip] = st.rdns[ip]; }); }); + all.forEach(function(c) { c.ips.forEach(function(ip) { if (ip in st.rdns) keep[ip] = st.rdns[ip]; }); }); st.rdns = keep; } - m.clients.forEach(function(c) { + all.forEach(function(c) { c.ips.forEach(function(ip) { if (!(ip in st.rdns) && ips.indexOf(ip) < 0 && ips.length < 64) ips.push(ip); }); }); if (ips.length && !st.rdnsBusy && !st.rdnsOff) { @@ -726,7 +752,7 @@ return view.extend({ st.mdnsAt = Date.now(); L.resolveDefault(callMdns(), null).then(function(r) { self.applyMdns(r); }); } - var wantVendor = m.clients.some(function(c) { return c.nameSource === 'mac'; }); + var wantVendor = all.some(function(c) { return c.nameSource === 'mac'; }); if (wantVendor && !st.oui && !st.ouiLoading) { st.ouiLoading = true; L.require('vantage.oui').then(function(mod) { st.oui = mod; self.rerender(); }).catch(function() {}); @@ -739,7 +765,7 @@ return view.extend({ var st = this.st, raw = this.lastRaw; var m = model.build({ board: this.board, info: raw.info, ifaces: raw.ifaces, devs: raw.devs, wifi: raw.wifi, iwinfo: raw.iwinfo, - assoc: raw.assoc, hapd: raw.hapd, hapdStatus: raw.hapdStatus, hints: raw.hints, + assoc: raw.assoc, hapd: raw.hapd, hapdStatus: raw.hapdStatus, hints: raw.hints, fdb: raw.fdb, aliases: this.aliases, rdns: st.rdns, mdns: st.mdns, vendor: st.oui ? function(k) { return k ? st.oui.lookup(k) : null; } : null }); @@ -1049,6 +1075,22 @@ return view.extend({ return E('div', { 'class': 'vt-edit-field' }, [ E('label', { 'for': 'vt-chart-style' }, [ _('Throughput chart') ]), pick ]); }, + /* the network path's backdrop, picked under Customize */ + buildBackdropPick: function() { + var self = this, names = { 'grid': _('Grid packets'), 'stars': _('Starfield'), 'none': _('None') }; + var pick = E('select', { 'class': 'vt-select', 'id': 'vt-backdrop-style', 'change': function(ev) { self.setBackdrop(ev.target.value); } }, + backdrop.STYLES.map(function(k) { return E('option', { 'value': k }, [ names[k] ]); })); + pick.value = this.backdropStyle; + return E('div', { 'class': 'vt-edit-field' }, [ E('label', { 'for': 'vt-backdrop-style' }, [ _('Network path background') ]), pick ]); + }, + + setBackdrop: function(v) { + if (backdrop.STYLES.indexOf(v) < 0 || v === this.backdropStyle) return; + this.backdropStyle = v; + writeBackdrop(v); + if (this.backdrop) this.backdrop.style(v); + }, + setChartStyle: function(v) { if (series.CHART_STYLES.indexOf(v) < 0 || v === this.chartStyle) return; this.chartStyle = v; @@ -1065,7 +1107,7 @@ return view.extend({ el.editBar = E('div', { 'class': 'vt-editbar', 'role': 'region', 'aria-label': _('Customize the dashboard'), 'hidden': 'hidden' }, [ E('div', { 'class': 'vt-editbar-text' }, [ E('strong', {}, [ _('Customize the dashboard') ]), el.editHint ]), el.editHidden, - this.buildChartPick(), + E('div', { 'class': 'vt-edit-picks' }, [ this.buildChartPick(), this.buildBackdropPick() ]), E('div', { 'class': 'vt-editbar-actions' }, [ E('button', { 'type': 'button', 'class': 'vt-btn', 'click': function() { var l = self.lay; @@ -1316,11 +1358,14 @@ return view.extend({ this.wireMap = Object.create(null); this.hero = null; el.kpis = E('div', { 'class': 'vt-kpis vt-fold-keep' }); - return this.foldable('hero', E('section', { 'class': 'vt-tile vt-hero', 'aria-label': _('Network path') }, [ + var tile = this.foldable('hero', E('section', { 'class': 'vt-tile vt-hero', 'aria-label': _('Network path') }, [ tileHead('pulse', _('Network path'), [ E('span', { 'class': 'vt-hint vt-hint-wide' }, [ _('Click any node for details') ]), this.foldButton('hero', _('Show or hide the network path')) ]), el.path, el.kpis ])); + /* packets or stars behind the path, moved by the uplink's rate */ + this.backdrop = backdrop.mount(tile, el.path, this.backdropStyle); + return tile; }, /* the nodes the path shows: rebuilt only when this changes (a radio, @@ -1330,7 +1375,28 @@ return view.extend({ if (m.wired) return 'w\n' + m.networks.filter(function(n) { return n.up; }).slice(0, 8).map(function(n) { return n.name; }).join('\n'); return 'r\n' + m.radios.map(function(r) { return r.id + '=' + m.ssids.filter(function(s) { return s.radio === r.id; }).map(function(s) { return s.id; }).join(','); - }).join('\n'); + }).join('\n') + '\nwired=' + this.wiredPorts().join(','); + }, + + /* the ports with wired hosts downstream of this device, at most 8: + hosts on the uplink's port are the way to the gateway, not clients + (on a bridge with several ports whose gateway port is unknown, only + the gateway is left out) */ + wiredPorts: function() { + var m = this.m, up = m.uplink, out = []; + (m.wiredHosts || []).forEach(function(c) { + if (c.gateway || (up && up.port && c.port === up.port)) return; + if (out.indexOf(c.port) < 0) out.push(c.port); + }); + return out.sort().slice(0, 8); + }, + + /* the Clients tile on its wired list, on one port's hosts or all */ + showWired: function(port) { + this.ui.kind = 'wired'; + this.ui.chip = port ? 'port:' + port : 'all'; + this.setFilter(''); + this.el.clients.scrollIntoView({ behavior: reducedMotion() ? 'auto' : 'smooth', block: 'start' }); }, paintHero: function() { @@ -1407,6 +1473,26 @@ return view.extend({ h.radios.push(b); }); if (!h.radios.length) h.branches.appendChild(E('div', { 'class': 'vt-empty-inline' }, [ _('No radios reported') ])); + /* wired hosts downstream: one branch beside the radios, a node + per port */ + var ports = this.wiredPorts(); + if (ports.length) { + var wb = { id: 'wired', wired: true, ssids: [], title: span('vt-node-title vt-tn'), sub: span('vt-node-sub vt-tn') }; + wb.el = E('button', { 'type': 'button', 'class': 'vt-node vt-node-radio', 'click': function() { self.showWired(null); } }, [ + E('span', { 'class': 'vt-node-body' }, [ E('span', { 'class': 'vt-node-kicker' }, [ _('Wired') ]), wb.title, wb.sub ]) + ]); + var wcol = E('div', { 'class': 'vt-branch-ssids' }); + ports.forEach(function(p) { + var sn = { id: 'port:' + p, port: p, name: E('span'), count: span('vt-tn') }; + sn.el = E('button', { 'type': 'button', 'class': 'vt-node vt-node-ssid', 'click': function() { self.showWired(p); } }, [ + E('span', { 'class': 'vt-node-body' }, [ E('span', { 'class': 'vt-node-title vt-ssid-name' }, [ sn.name ]), E('span', { 'class': 'vt-node-sub' }, [ sn.count ]) ]) + ]); + wb.ssids.push(sn); + wcol.appendChild(sn.el); + }); + wb.wrap = E('div', { 'class': 'vt-branch' }, [ wb.el, wcol ]); + h.radios.push(wb); + } } h.radios.forEach(function(b) { h.branches.appendChild(b.wrap); @@ -1429,9 +1515,13 @@ return view.extend({ /* this poll's text, classes and flow state on the nodes in place */ updateHero: function() { var self = this, m = this.m, st = this.st, h = this.hero, up = m.uplink, upR = st.uplinkRate, d = m.device; + /* a gateway the bridge hears (an access point's router) shows its + name once one is known beyond its vendor */ + var gwHost = (m.wiredHosts || []).filter(function(c) { return c.gateway; })[0]; + var gwName = gwHost && /^(alias|dhcp|mdns|dns)$/.test(gwHost.nameSource) ? gwHost.name : null; txt(h.gwKicker, up && up.isWan ? _('Internet') : _('Gateway')); - txt(h.gwTitle, up && up.gateway ? up.gateway : up && up.defaultRoute ? up.l3dev : _('No default route', 'vantage')); - txt(h.gwSub, up && up.dns.length ? _('DNS %s').format(up.dns[0]) : _('Reachability not probed')); + txt(h.gwTitle, gwName || (up && up.gateway ? up.gateway : up && up.defaultRoute ? up.l3dev : _('No default route', 'vantage'))); + txt(h.gwSub, gwName ? up.gateway : up && up.dns.length ? _('DNS %s').format(up.dns[0]) : _('Reachability not probed')); txt(h.dnVal, upR ? fmt.bitsParts(upR.rx).v : fmt.DASH); txt(h.dnUnit, upR ? fmt.bitsParts(upR.rx).u : ''); @@ -1445,6 +1535,7 @@ return view.extend({ txt(h.apUp, d.uptime != null ? _('up %s').format(fmt.duration(d.uptime)) : ''); h.upActive = !!(upR && upR.rx + upR.tx > 50e3); h.upRate = upR ? upR.rx + upR.tx : 0; + if (this.backdrop) this.backdrop.update({ down: upR ? upR.rx : null, up: upR ? upR.tx : null, origin: h.ap }); if (m.wired) { var nets = Object.create(null); @@ -1468,6 +1559,7 @@ return view.extend({ m.radios.forEach(function(r) { radios[r.id] = r; }); m.ssids.forEach(function(s) { ssids[s.id] = s; }); h.radios.forEach(function(b) { + if (b.wired) { self.updateWiredBranch(b); return; } var r = radios[b.id], rr = st.radioRates[b.id]; if (!r) return; setClass(b.el, 'vt-node vt-node-radio' + (r.up ? '' : ' vt-node-off')); @@ -1505,6 +1597,26 @@ return view.extend({ }); }, + /* the Wired branch of the path: its hosts, and per port the one host + with its link speed or how many share the port */ + updateWiredBranch: function(b) { + var st = this.st, hosts = this.m.wiredHosts || [], n = 0, rx = 0, tx = 0, seen = false; + b.ssids.forEach(function(sn) { + var on = hosts.filter(function(c) { return c.port === sn.port; }), r = st.devRates[sn.port], one = on.length === 1 ? on[0] : null; + n += on.length; + if (r) { rx += r.rx; tx += r.tx; seen = true; } + setClass(sn.el, 'vt-node vt-node-ssid' + (on.length ? '' : ' vt-node-idle')); + setTitle(sn.el, _('Show the devices on %s').format(sn.port)); + txt(sn.name, one ? sn.port + ' · ' + one.name : sn.port); + txt(sn.count, one ? (one.speed ? fmt.phyRate(one.speed.mbps * 1000) : fmt.DASH) : on.length === 1 ? _('1 device') : _('%d devices').format(on.length)); + sn.active = !!(r && r.rx + r.tx > 50e3); + }); + txt(b.title, n === 1 ? _('1 device') : _('%d devices').format(n)); + txt(b.sub, seen ? '↓ ' + fmt.bits(tx) : fmt.DASH); + b.active = !!(seen && rx + tx > 50e3); + b.rate = seen ? rx + tx : 0; + }, + /* four numbers under the path: who is here, what flows, where, and who is worst off. The cells stay; only a change of shape (tone, or whether a cell links somewhere) rebuilds them. */ @@ -1536,11 +1648,14 @@ return view.extend({ var byBand = {}; m.clients.forEach(function(c) { byBand[c.band] = (byBand[c.band] || 0) + 1; }); var bandText = Object.keys(byBand).sort().map(function(b) { return _('%d on %s').format(byBand[b], wifi.bandLabel(b)); }).join(' · '); + /* wired hosts downstream count as clients too */ + var ports = this.wiredPorts(), wiredN = (m.wiredHosts || []).filter(function(c) { return ports.indexOf(c.port) >= 0; }).length; + if (wiredN) bandText = [ bandText, _('%d wired').format(wiredN) ].filter(Boolean).join(' · '); var busiest = null; m.radios.forEach(function(r) { var i = st.radioInsights[r.id]; if (i && i.busiest) busiest = { r: r, share: i.share }; }); var weakest = m.clients.filter(function(c) { return c.signal != null; }).sort(function(a, b) { return a.signal - b.signal; })[0]; return [ - { label: _('Clients'), value: String(m.clients.length), sub: bandText || _('none connected'), tone: 'plain', + { label: _('Clients'), value: String(m.clients.length + wiredN), sub: bandText || _('none connected'), tone: 'plain', act: function() { self.el.clients.scrollIntoView({ behavior: reducedMotion() ? 'auto' : 'smooth', block: 'start' }); } }, { label: _('Traffic now'), value: up ? '↓ ' + fmt.bits(up.rx) : fmt.DASH, sub: up ? _('↑ %s · via %s').format(fmt.bits(up.tx), m.uplink ? m.uplink.dev : '') : _('measuring'), tone: 'plain', act: function() { self.openDrawer('uplink', 'uplink'); } }, @@ -2128,30 +2243,69 @@ return view.extend({ /* ----------------------------------------------------------- clients */ buildClients: function() { - var self = this, el = this.el; + var self = this, el = this.el, ui = this.ui; el.filter = E('input', { 'type': 'text', 'class': 'vt-filter-input', 'placeholder': _('Filter by name, address, SSID…'), 'aria-label': _('Filter clients'), 'autocomplete': 'off', 'spellcheck': 'false', 'input': function(ev) { self.ui.filter = ev.target.value; self.paintClients(); } }); el.filterClear = E('button', { 'type': 'button', 'class': 'vt-filter-clear', 'aria-label': _('Clear filter', 'vantage'), 'hidden': 'hidden', 'click': function() { self.setFilter(''); el.filter.focus(); } }, [ icon('close') ]); - el.chips = E('div', { 'class': 'vt-fchips', 'role': 'group', 'aria-label': _('Quick filters') }); + /* the quick filters sit in a menu at the end of the search box; the + one in force shows as a tag in the box, so none is ever hidden */ + el.filterTag = E('button', { 'type': 'button', 'class': 'vt-ftag', 'hidden': 'hidden', + 'click': function(ev) { ev.preventDefault(); ui.chip = 'all'; self.paintClients(); el.filter.focus(); } }); + el.filterBtn = E('button', { 'type': 'button', 'class': 'vt-filter-btn', 'aria-haspopup': 'true', 'aria-expanded': 'false', + 'aria-label': _('Quick filters'), 'title': _('Quick filters'), + 'click': function(ev) { ev.preventDefault(); self.filterMenu(); } }, [ icon('filter') ]); + el.filterMenu = E('div', { 'class': 'vt-fmenu', 'role': 'menu', 'aria-label': _('Quick filters'), 'hidden': 'hidden', + 'keydown': function(ev) { self.filterMenuKey(ev); } }); + /* Wi-Fi or wired clients, on a device that has both */ + el.kinds = E('div', { 'class': 'vt-kinds', 'role': 'group', 'aria-label': _('Kind of client'), 'hidden': 'hidden' }); el.count = E('span', { 'class': 'vt-count vt-tn' }); el.thead = E('thead'); el.tbody = E('tbody'); el.clientsEmpty = E('div', { 'class': 'vt-empty', 'hidden': 'hidden' }); this.rowMap = new Map(); el.clients = E('section', { 'class': 'vt-tile vt-clients', 'id': 'vt-clients', 'aria-label': _('Clients') }, [ - tileHead('users', _('Clients'), [ el.count ]), + tileHead('users', _('Clients'), [ el.kinds, el.count ]), E('div', { 'class': 'vt-toolbar' }, [ - E('label', { 'class': 'vt-filter' }, [ icon('search'), el.filter, E('kbd', { 'class': 'vt-key-hint' }, [ 'F' ]), el.filterClear ]), - el.chips + E('div', { 'class': 'vt-filter-wrap' }, [ + E('label', { 'class': 'vt-filter' }, [ icon('search'), el.filterTag, el.filter, E('kbd', { 'class': 'vt-key-hint' }, [ 'F' ]), el.filterClear, el.filterBtn ]), + el.filterMenu + ]) ]), E('div', { 'class': 'vt-table-wrap' }, [ E('table', { 'class': 'vt-ctable' }, [ el.thead, el.tbody ]) ]), el.clientsEmpty ]); + document.addEventListener('click', function(ev) { + if (!el.filterMenu.hidden && !el.filterMenu.contains(ev.target) && !el.filterBtn.contains(ev.target)) self.filterMenu(false); + }); return el.clients; }, + /* open (true), close (false) or toggle the quick filter menu */ + filterMenu: function(open) { + var el = this.el; + if (open == null) open = el.filterMenu.hidden; + el.filterMenu.hidden = !open; + el.filterBtn.setAttribute('aria-expanded', String(!!open)); + if (open) { + var on = el.filterMenu.querySelector('[aria-checked="true"]') || el.filterMenu.querySelector('button'); + if (on) on.focus(); + } + }, + + filterMenuKey: function(ev) { + var items = Array.prototype.slice.call(this.el.filterMenu.querySelectorAll('button')), n = items.length, i = items.indexOf(document.activeElement); + if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); this.filterMenu(false); this.el.filterBtn.focus(); } + else if ((ev.key === 'ArrowDown' || ev.key === 'ArrowUp') && n) { + ev.preventDefault(); + if (i < 0) i = ev.key === 'ArrowDown' ? n - 1 : 0; + items[(i + (ev.key === 'ArrowDown' ? 1 : n - 1)) % n].focus(); + } + else if ((ev.key === 'Home' || ev.key === 'End') && n) { ev.preventDefault(); items[ev.key === 'Home' ? 0 : n - 1].focus(); } + else if (ev.key === 'Tab') this.filterMenu(false); + }, + setFilter: function(v) { this.ui.filter = v; this.el.filter.value = v; @@ -2159,13 +2313,19 @@ return view.extend({ }, filterSsid: function(ssid, band) { + this.ui.kind = 'wifi'; this.ui.chip = 'all'; this.setFilter(ssid); this.el.clients.scrollIntoView({ behavior: reducedMotion() ? 'auto' : 'smooth', block: 'start' }); }, + /* a wired host's traffic: its port's, when it has the port to itself */ + portRate: function(c) { + return c.shared ? null : this.st.devRates[c.port] || null; + }, + sortValue: function(c, key) { - var r = this.st.staRates[c.mac]; + var r = typeof c.port === 'string' ? this.portRate(c) : this.st.staRates[c.mac]; switch (key) { case 'name': return String(c.name).toLowerCase(); case 'signal': return c.signal == null ? -999 : c.signal; @@ -2173,77 +2333,154 @@ return view.extend({ case 'rate': return Math.max(c.tx && c.tx.rate || 0, c.rx && c.rx.rate || 0); case 'exp': return c.exp ? c.exp.score : -1; case 'time': return c.connected || 0; + case 'link': return c.shared || !c.speed ? -1 : c.speed.mbps; + case 'net': return ((c.network || c.bridge || '') + ' ' + (c.vlan == null ? '' : c.vlan)).toLowerCase(); + case 'seen': return c.seen == null ? Infinity : c.seen; default: return r ? r.rx + r.tx : -1; } }, + /* a client (Wi-Fi station or wired host, which has a port) in the + list as filtered */ matches: function(c) { - var chip = this.ui.chip, q = String(this.ui.filter || '').trim().toLowerCase(); - if (chip === 'weak' && !(c.signal != null && c.signal < insight.WEAK_DBM)) return false; - if (chip === 'new' && !c.isNew) return false; - if (chip === 'active' && !(this.st.staRates[c.mac] && this.st.staRates[c.mac].rx + this.st.staRates[c.mac].tx > 8000)) return false; - if (/^band:/.test(chip) && c.band !== chip.slice(5)) return false; + var chip = this.ui.chip, q = String(this.ui.filter || '').trim().toLowerCase(), hay; + if (typeof c.port === 'string') { + if (/^port:/.test(chip) && c.port !== chip.slice(5)) return false; + hay = [ c.name, c.macLabel, c.mac, c.vendor, c.port, c.network, c.nameSourceLabel ]; + } + else { + if (chip === 'weak' && !(c.signal != null && c.signal < insight.WEAK_DBM)) return false; + if (chip === 'new' && !c.isNew) return false; + if (chip === 'active' && !(this.st.staRates[c.mac] && this.st.staRates[c.mac].rx + this.st.staRates[c.mac].tx > 8000)) return false; + if (/^band:/.test(chip) && c.band !== chip.slice(5)) return false; + hay = [ c.name, c.macLabel, c.mac, c.ssid, c.bandLabel, c.gen, c.vendor, c.wps, c.nameSourceLabel ]; + } if (!q) return true; - var hay = [ c.name, c.macLabel, c.mac, c.ssid, c.bandLabel, c.gen, c.vendor, c.wps, c.nameSourceLabel ].concat(c.ips || []).join(' ').toLowerCase(); + hay = hay.concat(c.ips || []).join(' ').toLowerCase(); return q.split(/\s+/).every(function(w) { return hay.indexOf(w) >= 0; }); }, + /* the quick filters of the list shown: [ id, label, count, warn ] */ + filterDefs: function() { + var m = this.m, st = this.st, out; + if (this.ui.kind === 'wired') { + var hosts = m.wiredHosts, ports = []; + out = [ [ 'all', _('All'), hosts.length ] ]; + hosts.forEach(function(h) { if (ports.indexOf(h.port) < 0) ports.push(h.port); }); + if (ports.length > 1) ports.sort().forEach(function(p) { out.push([ 'port:' + p, p, hosts.filter(function(h) { return h.port === p; }).length ]); }); + return out; + } + var bands = []; + out = [ [ 'all', _('All'), m.clients.length ] ]; + m.clients.forEach(function(c) { if (c.band && bands.indexOf(c.band) < 0) bands.push(c.band); }); + bands.sort().forEach(function(b) { out.push([ 'band:' + b, wifi.bandLabel(b), m.clients.filter(function(c) { return c.band === b; }).length ]); }); + out.push([ 'active', _('Active'), m.clients.filter(function(c) { var r = st.staRates[c.mac]; return r && r.rx + r.tx > 8000; }).length ]); + out.push([ 'weak', _('Weak signal'), m.clients.filter(function(c) { return c.signal != null && c.signal < insight.WEAK_DBM; }).length, true ]); + out.push([ 'new', _('New', 'vantage'), m.clients.filter(function(c) { return c.isNew; }).length ]); + return out; + }, + + /* the quick filter menu, its button and the tag of the one in force */ + paintFilters: function(defs) { + var self = this, el = this.el, ui = this.ui; + var on = defs.filter(function(d) { return d[0] === ui.chip; })[0]; + el.filterBtn.hidden = defs.length < 2; + if (el.filterBtn.hidden && !el.filterMenu.hidden) this.filterMenu(false); + el.filterTag.hidden = !on || on[0] === 'all'; + el.filterBtn.classList.toggle('vt-filter-btn-on', !el.filterTag.hidden); + var tag = el.filterTag.hidden ? '' : on[1]; + if (this.tagSig !== tag) { + this.tagSig = tag; + dom.content(el.filterTag, tag ? [ E('span', {}, [ tag ]), icon('close') ] : []); + el.filterTag.setAttribute('aria-label', tag ? _('Remove filter: %s').format(tag) : ''); + } + /* new counts go into the open menu in place: rebuilding it would + take the focus away */ + var ids = defs.map(function(d) { return d[0]; }).concat(ui.chip).join('\n'), items = el.filterMenu.children; + if (this.filterIds === ids) { + defs.forEach(function(d, i) { + if (!items[i]) return; + txt(items[i].querySelector('.vt-fmenu-n'), d[2]); + items[i].classList.toggle('vt-fmenu-warn', !!(d[3] && d[2])); + }); + return; + } + this.filterIds = ids; + dom.content(el.filterMenu, defs.map(function(d) { + var sel = ui.chip === d[0]; + return E('button', { 'type': 'button', 'role': 'menuitemradio', 'aria-checked': String(sel), 'class': 'vt-fmenu-item' + (d[3] && d[2] ? ' vt-fmenu-warn' : ''), + 'click': function() { ui.chip = d[0]; self.filterMenu(false); self.paintClients(); el.filterBtn.focus(); } }, [ + E('span', { 'class': 'vt-fmenu-check', 'aria-hidden': 'true' }, [ sel ? icon('check') : '' ]), + E('span', { 'class': 'vt-fmenu-label' }, [ d[1] ]), + E('span', { 'class': 'vt-fmenu-n vt-tn' }, [ String(d[2]) ]) + ]); + })); + }, + paintClients: function() { var self = this, m = this.m, st = this.st, el = this.el, ui = this.ui; - /* quick filter chips: all / bands present / weak / new / active */ - var chipDefs = [ [ 'all', _('All'), m.clients.length ] ]; - var bands = []; - m.clients.forEach(function(c) { if (c.band && bands.indexOf(c.band) < 0) bands.push(c.band); }); - bands.sort().forEach(function(b) { chipDefs.push([ 'band:' + b, wifi.bandLabel(b), m.clients.filter(function(c) { return c.band === b; }).length ]); }); - chipDefs.push([ 'active', _('Active'), m.clients.filter(function(c) { var r = st.staRates[c.mac]; return r && r.rx + r.tx > 8000; }).length ]); - chipDefs.push([ 'weak', _('Weak signal'), m.clients.filter(function(c) { return c.signal != null && c.signal < insight.WEAK_DBM; }).length ]); - chipDefs.push([ 'new', _('New', 'vantage'), m.clients.filter(function(c) { return c.isNew; }).length ]); - if (!chipDefs.some(function(d) { return d[0] === ui.chip; })) ui.chip = 'all'; - /* chips and headings change with the counts or the sort, not with every poll */ - var chipSig = JSON.stringify([ ui.chip, chipDefs ]); - if (this.chipSig !== chipSig) dom.content(el.chips, chipDefs.map(function(d) { - return E('button', { 'type': 'button', 'class': 'vt-fchip' + (ui.chip === d[0] ? ' vt-fchip-on' : '') + (d[0] === 'weak' && d[2] ? ' vt-fchip-warn' : ''), - 'aria-pressed': String(ui.chip === d[0]), 'click': function() { ui.chip = d[0]; self.paintClients(); } }, [ - E('span', {}, [ d[1] ]), E('span', { 'class': 'vt-fchip-n vt-tn' }, [ String(d[2]) ]) - ]); - })); - this.chipSig = chipSig; + /* Wi-Fi or wired: the switch shows where there are both; a device + without radios lists its wired hosts only */ + var hosts = m.wiredHosts || [], both = !!(hosts.length && !m.wired); + if (!both) ui.kind = m.wired ? 'wired' : 'wifi'; + var wired = ui.kind === 'wired', all = wired ? hosts : m.clients; + el.kinds.hidden = !both; + var kindSig = both ? [ ui.kind, m.clients.length, hosts.length ].join(' ') : ''; + if (this.kindSig !== kindSig) { + this.kindSig = kindSig; + dom.content(el.kinds, !both ? [] : [ [ 'wifi', _('Wi-Fi'), m.clients.length ], [ 'wired', _('Wired'), hosts.length ] ].map(function(k) { + return E('button', { 'type': 'button', 'class': 'vt-kind' + (ui.kind === k[0] ? ' vt-kind-on' : ''), 'aria-pressed': String(ui.kind === k[0]), + 'click': function() { if (ui.kind !== k[0]) { ui.kind = k[0]; ui.chip = 'all'; self.paintClients(); } } }, [ + E('span', {}, [ k[1] ]), E('span', { 'class': 'vt-kind-n vt-tn' }, [ String(k[2]) ]) + ]); + })); + } + var hint = wired ? _('Filter by name, address, port…') : _('Filter by name, address, SSID…'); + if (el.filter.getAttribute('placeholder') !== hint) el.filter.setAttribute('placeholder', hint); + + var defs = this.filterDefs(); + if (!defs.some(function(d) { return d[0] === ui.chip; })) ui.chip = 'all'; + this.paintFilters(defs); el.filterClear.hidden = !ui.filter; - /* header with sort buttons */ - var cols = [ [ 'name', _('Device') ], [ 'signal', _('Signal') ], [ 'ssid', _('Network') ], [ 'rate', _('Link rate') ], [ 'live', _('Live') ], [ 'exp', _('Experience') ], [ 'time', _('Online', 'vantage') ] ]; - var headSig = ui.sort + ' ' + ui.dir; + /* header with sort buttons; wired hosts have a link and when the + bridge last heard from them instead of radio figures */ + var cols = wired ? [ [ 'name', _('Device') ], [ 'link', _('Link', 'vantage') ], [ 'net', _('Network') ], [ 'live', _('Live') ], + [ 'seen', _('Last heard'), _('The bridge forgets a device after about five minutes without traffic') ] ] + : [ [ 'name', _('Device') ], [ 'signal', _('Signal') ], [ 'ssid', _('Network') ], [ 'rate', _('Link rate') ], [ 'live', _('Live') ], [ 'exp', _('Experience') ], [ 'time', _('Online', 'vantage') ] ]; + var so = ui.sorts[ui.kind]; + var headSig = ui.kind + ' ' + so.key + ' ' + so.dir; if (this.headSig !== headSig) dom.content(el.thead, [ E('tr', {}, cols.map(function(c) { - var on = ui.sort === c[0]; - return E('th', { 'class': 'vt-th-' + c[0], 'scope': 'col', 'aria-sort': on ? (ui.dir > 0 ? 'ascending' : 'descending') : 'none' }, [ - E('button', { 'type': 'button', 'class': 'vt-sort' + (on ? ' vt-sort-on' : ''), 'click': function() { - if (ui.sort === c[0]) ui.dir = -ui.dir; else { ui.sort = c[0]; ui.dir = (c[0] === 'name' || c[0] === 'ssid') ? 1 : -1; } - self.paintClients(); - } }, [ E('span', {}, [ c[1] ]), E('span', { 'class': 'vt-sort-ind', 'aria-hidden': 'true' }, [ on ? (ui.dir > 0 ? '↑' : '↓') : '' ]) ]) - ]); + var on = so.key === c[0]; + var btn = E('button', { 'type': 'button', 'class': 'vt-sort' + (on ? ' vt-sort-on' : ''), 'click': function() { + if (so.key === c[0]) so.dir = -so.dir; else { so.key = c[0]; so.dir = (c[0] === 'name' || c[0] === 'ssid' || c[0] === 'net' || c[0] === 'seen') ? 1 : -1; } + self.paintClients(); + } }, [ E('span', {}, [ c[1] ]), E('span', { 'class': 'vt-sort-ind', 'aria-hidden': 'true' }, [ on ? (so.dir > 0 ? '↑' : '↓') : '' ]) ]); + if (c[2]) btn.setAttribute('title', c[2]); + return E('th', { 'class': 'vt-th-' + c[0], 'scope': 'col', 'aria-sort': on ? (so.dir > 0 ? 'ascending' : 'descending') : 'none' }, [ btn ]); })) ]); this.headSig = headSig; - var list = m.clients.filter(L.bind(this.matches, this)); + var list = all.filter(L.bind(this.matches, this)); list.sort(function(a, b) { - var va = self.sortValue(a, ui.sort), vb = self.sortValue(b, ui.sort); - if (va < vb) return -ui.dir; - if (va > vb) return ui.dir; + var va = self.sortValue(a, so.key), vb = self.sortValue(b, so.key); + if (va < vb) return -so.dir; + if (va > vb) return so.dir; return a.mac < b.mac ? -1 : 1; }); - var gone = (ui.chip === 'all' || ui.chip === 'new') ? st.gone.map(function(g) { return Object.assign({}, g.client, { gone: true, goneAt: g.at }); }).filter(function(c) { + var gone = !wired && (ui.chip === 'all' || ui.chip === 'new') ? st.gone.map(function(g) { return Object.assign({}, g.client, { gone: true, goneAt: g.at }); }).filter(function(c) { return !ui.filter || self.matches(Object.assign({}, c, { isNew: false })); }) : []; if (ui.chip === 'new') gone = []; var maxRate = 0; - m.clients.forEach(function(c) { var r = st.staRates[c.mac]; if (r && r.rx + r.tx > maxRate) maxRate = r.rx + r.tx; }); + all.forEach(function(c) { var r = wired ? self.portRate(c) : st.staRates[c.mac]; if (r && r.rx + r.tx > maxRate) maxRate = r.rx + r.tx; }); var keep = new Set(), at = 0; this.rowSig = this.rowSig || new Map(); list.concat(gone).forEach(function(c) { - var key = (c.gone ? 'gone:' : '') + c.mac; + var key = (wired ? 'wired:' : '') + (c.gone ? 'gone:' : '') + c.mac; keep.add(key); var tr = self.rowMap.get(key); if (!tr) { @@ -2258,7 +2495,7 @@ return view.extend({ tr.setAttribute('aria-label', _('Open details of %s').format(c.name)); /* a row that would come out the same keeps its cells, and rows already in order stay where they are */ - var cells = self.clientCells(c, maxRate), sig = treeSig(cells); + var cells = wired ? self.wiredCells(c, maxRate) : self.clientCells(c, maxRate), sig = treeSig(cells); if (self.rowSig.get(key) !== sig) { dom.content(tr, cells); self.rowSig.set(key, sig); } if (el.tbody.children[at] !== tr) el.tbody.insertBefore(tr, el.tbody.children[at] || null); at++; @@ -2271,21 +2508,79 @@ return view.extend({ }); window.setTimeout(function() { self.rowMap.forEach(function(tr) { tr.classList.remove('vt-row-enter'); }); }, 600); - var total = m.clients.length; - txt(el.count, list.length === total ? (total === 1 ? _('1 connected') : _('%d connected').format(total)) : _('%d of %d').format(list.length, total)); + var total = all.length; + txt(el.count, list.length !== total ? _('%d of %d').format(list.length, total) + : wired ? (total === 1 ? _('1 device') : _('%d devices').format(total)) + : (total === 1 ? _('1 connected') : _('%d connected').format(total))); + el.count.hidden = both && list.length === total; el.clientsEmpty.hidden = list.length + gone.length > 0; if (!el.clientsEmpty.hidden) { - dom.content(el.clientsEmpty, !total ? [ - icon('wifi'), E('p', { 'class': 'vt-empty-title' }, [ _('No clients connected') ]), - E('p', {}, [ _('Stations appear here as soon as they associate with one of the SSIDs.') ]) - ] : [ + dom.content(el.clientsEmpty, total ? [ icon('search'), E('p', { 'class': 'vt-empty-title' }, [ ui.filter ? _('No clients match “%s”').format(fmt.safeText(ui.filter, 40)) : _('No clients in this view') ]), E('button', { 'type': 'button', 'class': 'vt-btn', 'click': function() { ui.chip = 'all'; self.setFilter(''); } }, [ _('Show all clients') ]) + ] : wired && this.lastRaw && this.lastRaw.fdbCrowded ? [ + icon('router'), E('p', { 'class': 'vt-empty-title' }, [ _('Too many devices to list') ]), + E('p', {}, [ _('The bridges have learned so many addresses that listing them here would strain this device.') ]) + ] : wired ? [ + icon('router'), E('p', { 'class': 'vt-empty-title' }, [ _('No wired devices heard') ]), + E('p', {}, [ _('Devices on the Ethernet ports appear here once they send traffic; the bridge forgets quiet ones after about five minutes.') ]) + ] : [ + icon('wifi'), E('p', { 'class': 'vt-empty-title' }, [ _('No clients connected') ]), + E('p', {}, [ _('Stations appear here as soon as they associate with one of the SSIDs.') ]) ]); } }, + /* the device cell of a client row: avatar, name, where the name comes + from, tags and the address */ + whoCell: function(c, tags) { + return E('td', { 'class': 'vt-td-name', 'data-title': _('Device') }, [ + E('div', { 'class': 'vt-who' }, [ + avatar(c), + E('div', { 'class': 'vt-who-text' }, [ + E('div', { 'class': 'vt-who-label', 'title': c.name }, [ c.name ]), + E('div', { 'class': 'vt-who-sub' }, [ E('span', { 'class': 'vt-src vt-src-' + c.nameSource, 'title': _('Name from: %s').format(this.sourceText(c)) }, [ c.nameSourceLabel ]) ].concat(tags, [ E('span', { 'class': 'vt-mono vt-who-id' }, [ c.secondary ]) ])) + ]) + ]) + ]); + }, + + /* a wired host's row. Its link is its port's when it has the port to + itself; on a shared port (a switch or another access point behind + it) neither the speed nor the traffic is its own */ + wiredCells: function(c, maxRate) { + var r = this.portRate(c), total = r ? r.rx + r.tx : 0; + var bar = E('span', { 'class': 'vt-mini-fill' }); + setW(bar, maxRate > 0 ? total / maxRate * 100 : 0); + var tags = []; + var up = this.m && this.m.uplink; + if (c.gateway) tags.push(pill(_('Gateway'), 'accent', _('The router this device sends its traffic to'))); + else if (up && up.port && c.port === up.port) tags.push(pill(_('Upstream'), 'muted', _('Reached through the uplink port %s, not connected to this device').format(c.port))); + if (c.vlan != null) tags.push(pill(_('VLAN %d').format(c.vlan), 'muted')); + var speed = c.speed ? fmt.phyRate(c.speed.mbps * 1000) : fmt.DASH; + var link = E('td', { 'class': 'vt-td-link', 'data-title': _('Link', 'vantage') }, [ + E('div', { 'class': 'vt-phy vt-tn' }, [ E('b', { 'class': c.carrier === false ? 'vt-t-warn' : '' }, [ c.carrier === false ? _('down') : c.shared ? _('Shared port') : speed ]) ]), + E('div', { 'class': 'vt-phy vt-phy-up vt-tn' }, [ c.shared ? _('%s · %d devices').format(c.port, c.peers) : c.port ]) + ]); + link.setAttribute('title', c.shared ? _('%s is shared by %d devices (a switch or another access point behind it); its %s link is not theirs').format(c.port, c.peers, speed) : _('Port %s').format(c.port)); + return [ + this.whoCell(c, tags), + link, + E('td', { 'class': 'vt-td-net', 'data-title': _('Network') }, [ E('div', { 'class': 'vt-net' }, [ E('span', { 'class': 'vt-net-ssid' }, [ c.network || c.bridge || fmt.DASH ]) ]) ]), + E('td', { 'class': 'vt-td-live', 'data-title': _('Live') }, c.shared ? [ + E('span', { 'class': 'vt-muted', 'title': _('Not counted per device on a shared port') }, [ fmt.DASH ]) + ] : [ + E('div', { 'class': 'vt-live-cell' }, [ + E('span', { 'class': 'vt-tn vt-live-total' }, [ r ? fmt.bits(total) : fmt.DASH ]), + E('span', { 'class': 'vt-mini' }, [ bar ]), + E('span', { 'class': 'vt-live-split vt-tn', 'title': r ? _('To device %s, from device %s').format(fmt.bits(r.tx), fmt.bits(r.rx)) : '' }, [ r ? '↓ ' + fmt.bitsShort(r.tx) + ' ↑ ' + fmt.bitsShort(r.rx) : '' ]) + ]) + ]), + E('td', { 'class': 'vt-td-seen vt-tn', 'data-title': _('Last heard') }, [ c.seen == null ? _('static entry') : fmt.ago(c.seen * 1000) ]) + ]; + }, + clientCells: function(c, maxRate) { var st = this.st, r = c.gone ? null : st.staRates[c.mac]; var total = r ? r.rx + r.tx : 0; @@ -2298,15 +2593,7 @@ return view.extend({ var exp = c.exp || { score: 0, level: 'muted', grade: fmt.DASH, reason: '' }; return [ - E('td', { 'class': 'vt-td-name', 'data-title': _('Device') }, [ - E('div', { 'class': 'vt-who' }, [ - avatar(c), - E('div', { 'class': 'vt-who-text' }, [ - E('div', { 'class': 'vt-who-label', 'title': c.name }, [ c.name ]), - E('div', { 'class': 'vt-who-sub' }, [ E('span', { 'class': 'vt-src vt-src-' + c.nameSource, 'title': _('Name from: %s').format(this.sourceText(c)) }, [ c.nameSourceLabel ]) ].concat(tags, [ E('span', { 'class': 'vt-mono vt-who-id' }, [ c.secondary ]) ])) - ]) - ]) - ]), + this.whoCell(c, tags), E('td', { 'class': 'vt-td-signal', 'data-title': _('Signal') }, c.gone ? [ E('span', { 'class': 'vt-muted' }, [ fmt.DASH ]) ] : [ E('span', { 'class': 'vt-signal', 'title': c.level.word }, [ signalGlyph(c.level), E('span', { 'class': 'vt-tn' }, [ fmt.dbm(c.signal) ]) ]) ]), @@ -2501,6 +2788,8 @@ return view.extend({ this.m.clients.forEach(function(x) { if (x.mac === mac) c = x; }); if (!c) st.gone.forEach(function(g) { if (g.client.mac === mac) gone = g; }); if (!c && gone) c = Object.assign({}, gone.client, { gone: true, goneAt: gone.at }); + if (!c) (this.m.wiredHosts || []).forEach(function(x) { if (x.mac === mac) c = x; }); + if (c && typeof c.port === 'string') return this.drawerWired(c); if (!c) return { title: [ E('span', { 'class': 'vt-drawer-name' }, [ names.normMac(mac) ? fmt.mac(mac) : _('Unknown device', 'vantage') ]) ], body: [ E('div', { 'class': 'vt-empty' }, [ icon('wifi'), E('p', { 'class': 'vt-empty-title' }, [ _('Not connected') ]), E('p', {}, [ _('This station is not associated right now.') ]) ]) ] @@ -2587,6 +2876,41 @@ return view.extend({ return { title: title, body: body }; }, + drawerWired: function(c) { + var self = this, r = this.portRate(c); + var title = [ + avatar(c), + E('div', { 'class': 'vt-drawer-titles' }, [ + E('div', { 'class': 'vt-drawer-name', 'id': 'vt-drawer-name' }, [ c.name ]), + E('div', { 'class': 'vt-drawer-sub' }, [ + E('span', { 'class': 'vt-src vt-src-' + c.nameSource }, [ c.nameSourceLabel ]), + E('span', { 'class': 'vt-mono' }, [ c.secondary ]) + ]) + ]) + ]; + var rename = this.canWrite ? E('button', { 'type': 'button', 'class': 'vt-btn vt-btn-ghost', 'click': function() { self.startRename(c); } }, [ icon('pencil'), E('span', {}, [ c.alias ? _('Rename', 'vantage') : _('Name this device') ]) ]) : ''; + var speed = c.speed ? model.speedLabel(c.speed) : fmt.DASH; + var body = [ + E('div', { 'class': 'vt-dactions' }, [ rename, pageLink(insight.PAGES.network, _('Interfaces')) ]), + this.el.renameSlot = E('div', { 'class': 'vt-rename-slot' }), + this.section(_('Connection'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('Port'), c.port, true), + kv(_('Link', 'vantage'), c.carrier === false ? _('down') : c.shared ? [ _('Shared by %d devices').format(c.peers), E('small', {}, [ _('the port: %s').format(speed) ]) ] : speed), + kv(_('Traffic'), c.shared ? E('span', { 'class': 'vt-muted' }, [ _('Not counted per device on a shared port') ]) + : r ? '↓ ' + fmt.bits(r.tx) + ' · ↑ ' + fmt.bits(r.rx) : _('measuring')), + kv(_('Network'), [ c.network || c.bridge || fmt.DASH, c.vlan != null ? E('small', {}, [ _('VLAN %d').format(c.vlan) ]) : '' ]), + kv(_('Last heard'), c.seen == null ? _('static entry') : fmt.ago(c.seen * 1000)) + ]) ]), + this.section(_('Identity'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('MAC address'), c.macLabel, true), + kv(_('Address type'), c.isPrivate ? _('Private address (randomised by the device)') : (c.vendor ? _('Globally unique · %s').format(c.vendor) : _('Globally unique'))), + kv(_('IP addresses'), c.ips && c.ips.length ? c.ips.join('\n') : E('span', { 'class': 'vt-muted' }, [ _('not known to this device') ]), true), + kv(_('Name from'), this.sourceText(c)) + ]) ]) + ]; + return { title: title, body: body }; + }, + sourceText: function(c) { switch (c.nameSource) { case 'alias': return _('Your name for this device'); @@ -2812,7 +3136,7 @@ return view.extend({ return; } /* 'f' focuses the client filter; '/' and Ctrl+K belong to the theme */ - if ((ev.key === 'f' || ev.key === 'F') && !typing && !ev.ctrlKey && !ev.metaKey && !ev.altKey && !this.ui.drawer && !this.m.wired) { + if ((ev.key === 'f' || ev.key === 'F') && !typing && !ev.ctrlKey && !ev.metaKey && !ev.altKey && !this.ui.drawer) { ev.preventDefault(); this.el.filter.focus(); this.el.filter.select(); diff --git a/luci-app-vantage/po/templates/vantage.pot b/luci-app-vantage/po/templates/vantage.pot index 72031b9a3..f836ee321 100644 --- a/luci-app-vantage/po/templates/vantage.pot +++ b/luci-app-vantage/po/templates/vantage.pot @@ -1,7 +1,7 @@ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2721 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3045 msgid "%d MHz (%d–%d MHz)" msgstr "" @@ -21,23 +21,29 @@ msgstr "" msgid "%d SSIDs · %d idle" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1266 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2072 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1308 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1587 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2187 msgid "%d clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2275 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2514 msgid "%d connected" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1611 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1614 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2513 +msgid "%d devices" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:105 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1686 msgid "%d down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2765 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2766 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3089 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3090 msgid "%d errors" msgstr "" @@ -46,26 +52,26 @@ msgctxt "vantage" msgid "%d h ago" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2767 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3091 msgid "%d in · %d out" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:375 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:379 msgctxt "vantage" msgid "%d min" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:90 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:369 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:373 msgctxt "vantage" msgid "%d min ago" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2275 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2512 msgid "%d of %d" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1153 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1195 msgid "%d of %d columns" msgstr "" @@ -77,7 +83,7 @@ msgstr "" msgid "%d older devices (Wi-Fi 4 or earlier) slow airtime" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1538 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1650 msgid "%d on %s" msgstr "" @@ -85,22 +91,22 @@ msgstr "" msgid "%d on this router, %d into your network" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2064 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2179 msgid "%d radios" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:375 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:379 msgctxt "vantage" msgid "%d s" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:89 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:369 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:373 msgctxt "vantage" msgid "%d s ago" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2577 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2866 msgid "%d sent · %s retried · %s failed" msgstr "" @@ -117,7 +123,11 @@ msgstr "" msgid "%d weak" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2083 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1653 +msgid "%d wired" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2198 msgid "%d–%d MHz of the %d–%d MHz band" msgstr "" @@ -135,7 +145,7 @@ msgstr "" msgid "%s (IPv%d)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2572 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2861 msgid "%s (new this session)" msgstr "" @@ -146,10 +156,10 @@ msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:137 #: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:103 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2090 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2104 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2319 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2714 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2205 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2219 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2606 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3038 msgid "%s MHz" msgstr "" @@ -163,7 +173,7 @@ msgstr "" msgid "%s accepts every connection" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2860 msgctxt "vantage" msgid "%s ago" msgstr "" @@ -173,13 +183,13 @@ msgid "%s at %s; below %s roaming and throughput suffer" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:82 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1782 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1897 msgctxt "vantage" msgid "%s available" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2109 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2722 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2224 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3046 msgid "%s dBm" msgstr "" @@ -191,7 +201,7 @@ msgstr "" msgid "%s device on a newer radio; uses more airtime per byte" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2539 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2828 msgid "%s experience" msgstr "" @@ -207,7 +217,7 @@ msgstr "" msgid "%s has no carrier: cable unplugged or switch port down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1120 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1162 msgid "%s hidden" msgstr "" @@ -223,7 +233,7 @@ msgstr "" msgid "%s is in no zone and accepts every connection" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1113 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1155 msgid "%s is narrower" msgstr "" @@ -231,7 +241,13 @@ msgstr "" msgid "%s is not up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1116 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2566 +msgid "" +"%s is shared by %d devices (a switch or another access point behind it); its " +"%s link is not theirs" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1158 msgid "%s is wider" msgstr "" @@ -247,23 +263,23 @@ msgstr "" msgid "%s may open the router's admin" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1234 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1276 msgid "%s moved" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1110 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1152 msgid "%s moved down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1101 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1143 msgid "%s moved left" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1104 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1146 msgid "%s moved right" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1107 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1149 msgid "%s moved up" msgstr "" @@ -276,9 +292,9 @@ msgid "%s negotiated only %d Mbit/s; a cable pair or port may be faulty" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:83 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1783 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1806 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2793 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1898 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1921 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3117 msgid "%s of %s" msgstr "" @@ -286,7 +302,7 @@ msgstr "" msgid "%s of %s available (%s in use)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1547 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1662 msgid "%s of Wi-Fi traffic" msgstr "" @@ -303,15 +319,15 @@ msgstr "" msgid "%s of frames to the device fail; interference likely" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2791 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3115 msgid "%s over %d cores" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1868 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1983 msgid "%s radio" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1159 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1201 msgid "%s shown" msgstr "" @@ -327,15 +343,19 @@ msgstr "" msgid "%s used" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1814 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1929 msgid "%s used of %s" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2564 +msgid "%s · %d devices" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:119 msgid "%s · %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2569 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2858 msgid "%s · channel %d · %d MHz" msgstr "" @@ -351,30 +371,36 @@ msgstr "" msgid "%s: no carrier" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2722 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3046 msgid "(configured %d dBm, limited by regulatory rules)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1494 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2410 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1586 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2697 msgid "(hidden)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1266 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2072 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1308 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1587 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2187 msgid "1 client" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2275 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2514 msgid "1 connected" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1611 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1614 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2513 +msgid "1 device" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:247 msgid "1 older device (Wi-Fi 4 or earlier) slows airtime" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2064 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2179 msgid "1 radio" msgstr "" @@ -402,19 +428,20 @@ msgstr "" msgid "60 GHz" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2199 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2377 msgid "Active" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2581 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2870 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2906 msgid "Address type" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2758 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3082 msgid "Addresses" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2111 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2226 msgid "Airtime" msgstr "" @@ -423,7 +450,8 @@ msgctxt "vantage" msgid "Alias" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2195 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2368 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2374 msgid "All" msgstr "" @@ -460,13 +488,13 @@ msgctxt "vantage" msgid "B" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1572 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1573 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1687 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1688 msgid "Busiest network" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1547 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1549 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1662 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1664 msgid "Busiest radio" msgstr "" @@ -474,7 +502,7 @@ msgstr "" msgid "Busiest radio: %s of Wi-Fi traffic" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1748 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 msgid "Busy share of each CPU core" msgstr "" @@ -488,8 +516,8 @@ msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:71 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:72 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1741 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2791 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1856 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3115 msgctxt "vantage" msgid "CPU" msgstr "" @@ -498,83 +526,84 @@ msgstr "" msgid "Cache (reclaimable)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2657 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2981 msgid "Cancel" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2564 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2853 msgctxt "vantage" msgid "Capabilities" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2753 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3077 msgid "Carrier" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2721 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3045 msgid "Centre frequency" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2103 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2218 msgid "Ch %d" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1477 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2071 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1569 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2186 msgid "Ch %d · %d MHz" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2714 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3038 msgid "Channel" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1477 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1569 msgid "Channel —" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2135 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2250 msgctxt "vantage" msgid "Clear filter" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1362 msgid "Click any node for details" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:888 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1543 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2108 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2143 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2144 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:914 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1658 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2223 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2268 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2269 msgid "Clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2726 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3050 msgid "Clients on this radio" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2427 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2714 msgid "Close" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2334 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2621 msgid "Connected" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2296 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2591 msgid "Connected after you opened this page" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1570 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2570 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1685 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2859 msgid "Connected for" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2567 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2856 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2896 msgid "Connection" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1775 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1890 msgid "Core %d: %s" msgstr "" @@ -582,15 +611,15 @@ msgstr "" msgid "Could not read the firewall configuration" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2640 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2964 msgid "Could not save the name (no permission or the device refused it)." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2764 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3088 msgid "Counters since boot" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2724 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3048 msgid "Country" msgstr "" @@ -599,13 +628,13 @@ msgid "" "Custom rule files are configured (%s); rules in them are not listed here." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1255 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1297 msgid "Customize" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1065 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1066 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1254 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1107 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1108 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1296 msgid "Customize the dashboard" msgstr "" @@ -613,16 +642,16 @@ msgstr "" msgid "DHCP" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2595 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2919 msgid "DHCP hostname" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2762 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3086 msgid "DNS" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1434 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1524 msgid "DNS %s" msgstr "" @@ -631,88 +660,95 @@ msgid "Dashboard" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:268 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2301 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2782 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2449 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2538 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3106 msgid "Device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2624 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2948 msgid "Device name" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2653 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2977 msgid "Device type" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1478 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2073 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2098 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2719 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2527 +msgid "" +"Devices on the Ethernet ports appear here once they send traffic; the bridge " +"forgets quiet ones after about five minutes." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1570 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2188 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2213 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3043 msgid "Disabled" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2533 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2822 msgid "Disconnected %s; shown from the last sample." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1079 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1121 msgctxt "vantage" msgid "Done" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:46 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1478 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2073 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2098 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2719 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1570 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2188 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2213 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3043 msgctxt "vantage" msgid "Down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1847 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1933 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2006 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1962 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2048 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2121 msgid "Download" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:321 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:325 msgid "Download (to clients)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2575 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2864 msgctxt "vantage" msgid "Downloaded" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1140 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1182 msgid "" "Drag a tile by its handle or use its buttons. Saved in this browser for " "medium screens." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1141 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1183 msgid "" "Drag a tile by its handle or use its buttons. Saved in this browser for " "narrow screens." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1139 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1181 msgid "" "Drag a tile by its handle or use its buttons. Saved in this browser for wide " "screens." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1092 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1134 msgid "Drag to move" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2767 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3091 msgctxt "vantage" msgid "Dropped" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2677 msgid "Edit" msgstr "" @@ -730,8 +766,8 @@ msgctxt "vantage" msgid "Excellent" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2328 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2615 msgid "Experience" msgstr "" @@ -749,15 +785,20 @@ msgstr "" msgid "Fair signal %s; lower rates at range" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2132 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2247 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2439 msgid "Filter by name, address, SSID…" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2133 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2439 +msgid "Filter by name, address, port…" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2248 msgid "Filter clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2785 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3109 msgctxt "vantage" msgid "Firmware" msgstr "" @@ -770,7 +811,7 @@ msgstr "" msgid "Forwarding is accepted by default" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2577 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2866 msgid "Frames to device" msgstr "" @@ -779,15 +820,15 @@ msgctxt "vantage" msgid "Free" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2562 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2851 msgid "From device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2317 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2604 msgid "From device: %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2748 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3072 msgid "From network" msgstr "" @@ -797,8 +838,9 @@ msgid "" "files are not shown." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1432 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2761 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1522 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2558 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3085 msgid "Gateway" msgstr "" @@ -811,7 +853,7 @@ msgstr "" msgid "General Settings" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2563 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2852 msgctxt "vantage" msgid "Generation" msgstr "" @@ -821,11 +863,13 @@ msgctxt "vantage" msgid "GiB" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2581 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2870 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2906 msgid "Globally unique" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2581 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2870 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2906 msgid "Globally unique · %s" msgstr "" @@ -843,38 +887,44 @@ msgstr "" msgid "Good signal %s" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1080 +msgid "Grid packets" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:50 msgid "Half duplex" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:785 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:811 msgctxt "vantage" msgid "Health checks" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1156 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2410 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1198 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2697 msgctxt "vantage" msgid "Hidden" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1119 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1161 msgid "Hide %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2582 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2871 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2907 msgid "IP addresses" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2759 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3083 msgid "IPv4" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2760 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3084 msgid "IPv6" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2579 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2868 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2904 msgid "Identity" msgstr "" @@ -892,19 +942,20 @@ msgctxt "vantage" msgid "Input" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1457 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1548 msgid "Interface" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2756 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3080 msgid "Interface up for" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2746 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2894 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3070 msgid "Interfaces" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1432 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1522 msgid "Internet" msgstr "" @@ -930,11 +981,11 @@ msgid "" "the router itself." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2786 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3110 msgid "Kernel" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1820 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1935 msgid "Kernel %s" msgstr "" @@ -943,43 +994,58 @@ msgctxt "vantage" msgid "KiB" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2262 +msgid "Kind of client" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2860 msgctxt "vantage" msgid "Last activity" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1077 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2450 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2580 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2902 +msgid "Last heard" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1119 msgid "Layout reset" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2295 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2590 msgid "Left %s" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:163 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2297 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2592 msgctxt "vantage" msgid "Legacy" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2560 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2752 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2449 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2562 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2849 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2898 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3076 msgctxt "vantage" msgid "Link" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2316 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2603 msgid "Link rate" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1274 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2321 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1316 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2449 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2608 msgid "Live" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2790 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3114 msgid "Load" msgstr "" @@ -991,20 +1057,20 @@ msgstr "" msgid "Load %s · %d cores" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2792 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3116 msgctxt "vantage" msgid "Load average" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1729 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1844 msgid "Load average over 1, 5 and 15 minutes" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1765 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1880 msgid "Load avg %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2788 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3112 msgid "Local time" msgstr "" @@ -1020,7 +1086,8 @@ msgstr "" msgid "MAC" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2580 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2869 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2905 msgid "MAC address" msgstr "" @@ -1028,15 +1095,15 @@ msgstr "" msgid "MCS %d" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1112 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1154 msgid "Make %s narrower" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1115 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1157 msgid "Make %s wider" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2597 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2921 msgid "Manufacturer of the network adapter" msgstr "" @@ -1053,11 +1120,11 @@ msgid "Measuring…" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:82 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1752 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 msgid "Memory" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2793 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3117 msgid "Memory in use" msgstr "" @@ -1066,62 +1133,63 @@ msgctxt "vantage" msgid "MiB" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1042 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1068 msgid "Mirrored" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1043 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1069 msgid "Mirrored, scale per direction" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2720 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3044 msgid "Mode" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2783 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3107 msgid "Model" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1109 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1151 msgid "Move %s down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1100 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1142 msgid "Move %s left" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1103 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1145 msgid "Move %s right" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1106 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1148 msgid "Move %s up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1092 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1134 msgid "Move %s: drag, or use the arrow keys" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2652 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2976 msgid "Name" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:134 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2692 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3016 msgid "Name contains invisible or control characters" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2583 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2872 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2908 msgid "Name from" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2306 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2543 msgid "Name from: %s" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:132 #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:137 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2693 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3017 msgid "Name is longer than %d characters" msgstr "" @@ -1129,23 +1197,31 @@ msgstr "" msgid "Name must be text" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2524 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2813 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2891 msgid "Name this device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2313 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2568 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2449 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2570 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2600 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2857 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2901 msgid "Network" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:888 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1319 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:914 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1361 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1362 msgid "Network path" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1084 +msgid "Network path background" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1686 msgid "Networks" msgstr "" @@ -1153,13 +1229,13 @@ msgstr "" msgid "Networks: %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2201 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2296 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2379 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2591 msgctxt "vantage" msgid "New" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1405 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1471 msgid "No SSIDs" msgstr "" @@ -1167,23 +1243,23 @@ msgstr "" msgid "No clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2280 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2529 msgid "No clients connected" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2365 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2652 msgid "No clients connected." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2283 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2520 msgid "No clients in this view" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2283 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2520 msgid "No clients match “%s”" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2731 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3055 msgid "No clients; an idle radio is fine." msgstr "" @@ -1191,7 +1267,7 @@ msgstr "" msgid "No clients; idle radios are fine" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1433 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1523 msgctxt "vantage" msgid "No default route" msgstr "" @@ -1200,7 +1276,7 @@ msgstr "" msgid "No interface with a route" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2738 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3062 msgid "No interface with a route is up." msgstr "" @@ -1209,7 +1285,7 @@ msgid "No internet-facing zone found" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:48 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2753 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3077 msgid "No link" msgstr "" @@ -1217,7 +1293,7 @@ msgstr "" msgid "No network interface with a route is up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1386 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1452 msgid "No other interfaces are up" msgstr "" @@ -1227,7 +1303,7 @@ msgid "No problems detected" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:104 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1409 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1475 msgid "No radios reported" msgstr "" @@ -1244,12 +1320,16 @@ msgstr "" msgid "No stations are associated right now" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2399 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2526 +msgid "No wired devices heard" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2686 msgctxt "vantage" msgid "No wireless networks configured." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2078 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2193 msgid "No wireless radios are reported by this device." msgstr "" @@ -1268,19 +1348,19 @@ msgstr "" msgid "Nobody associated" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2365 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2652 msgid "Nobody is moving data right now." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2110 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2225 msgid "Noise" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2565 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2854 msgid "Noise / SNR" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3047 msgid "Noise floor" msgstr "" @@ -1289,31 +1369,37 @@ msgid "Noise floor not reported by the driver" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:104 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1080 msgid "None" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:150 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2691 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3015 msgid "Not a station MAC address" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2506 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2795 msgid "Not connected" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2572 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2899 +msgid "Not counted per device on a shared port" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:383 msgid "Not in effect" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1800 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1915 msgid "Not reported" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2600 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2924 msgid "Nothing better known" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2598 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2922 msgid "Nothing better known; the address is randomised" msgstr "" @@ -1334,7 +1420,7 @@ msgstr "" msgid "Older %s device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2297 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2592 msgid "Older Wi-Fi generation than the radio; uses more airtime" msgstr "" @@ -1342,7 +1428,7 @@ msgstr "" msgid "On this router" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 msgctxt "vantage" msgid "Online" msgstr "" @@ -1359,7 +1445,7 @@ msgctxt "vantage" msgid "Open" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2258 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2495 msgid "Open details of %s" msgstr "" @@ -1375,7 +1461,7 @@ msgstr "" msgid "Open network" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:532 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:623 msgid "OpenWrt device" msgstr "" @@ -1385,11 +1471,11 @@ msgid "" "new rules: what is open is unknown." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1040 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1066 msgid "Overlaid" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1041 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1067 msgid "Overlaid, log scale" msgstr "" @@ -1397,11 +1483,11 @@ msgstr "" msgid "Overview" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2564 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2853 msgid "PMF" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1272 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1314 msgid "Paused while hidden" msgstr "" @@ -1415,7 +1501,7 @@ msgid "" "on to your network: what every router lets in." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2784 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3108 msgctxt "vantage" msgid "Platform" msgstr "" @@ -1425,37 +1511,50 @@ msgctxt "vantage" msgid "Poor" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2897 +msgid "Port" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2566 +msgid "Port %s" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:43 msgid "Private" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2581 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2870 msgid "Private Wi-Fi address (randomised by the device)" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2906 +msgid "Private address (randomised by the device)" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:266 msgid "Private device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2781 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3105 msgid "Processes" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2755 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3079 msgid "Protocol" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2137 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2257 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2259 msgid "Quick filters" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2569 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2718 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2858 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3042 msgctxt "vantage" msgid "Radio" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2099 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2214 msgid "Radio details" msgstr "" @@ -1467,53 +1566,62 @@ msgstr "" msgid "Radio is enabled but not up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2712 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2237 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3036 msgid "Radio settings" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:888 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2051 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2053 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:914 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2166 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2168 msgid "Radios" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1434 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1524 msgid "Reachability not probed" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2746 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2559 +msgid "Reached through the uplink port %s, not connected to this device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3070 msgid "Realtime graphs" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2765 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3089 msgctxt "vantage" msgid "Received" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1273 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1315 msgctxt "vantage" msgid "Reconnecting…" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2658 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2395 +msgid "Remove filter: %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2982 msgid "Remove name" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2524 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2813 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2891 msgctxt "vantage" msgid "Rename" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1078 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1120 msgid "Reset" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2593 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2917 msgid "Reverse DNS of its address" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:544 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:635 msgid "Root (overlay)" msgstr "" @@ -1527,20 +1635,20 @@ msgid "" "to the addresses you use." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2650 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2974 msgid "Save" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2572 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2861 msgid "Seen since" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2766 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3090 msgctxt "vantage" msgid "Sent" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1044 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1070 msgid "Separate lanes" msgstr "" @@ -1548,45 +1656,57 @@ msgstr "" msgid "Settings to check" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1158 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2898 +msgid "Shared by %d devices" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2563 +msgid "Shared port" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1200 msgid "Show %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2284 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2521 msgid "Show all clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1738 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1853 msgid "Show or hide system details" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1362 msgid "Show or hide the network path" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2054 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2169 msgid "Show or hide the radio details" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1850 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1965 msgid "Show or hide the throughput chart" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2677 msgid "Show or hide the wireless networks" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1493 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2406 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1585 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2693 msgid "Show the clients of %s" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1609 +msgid "Show the devices on %s" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:117 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:118 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:121 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2310 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2548 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2597 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2837 msgid "Signal" msgstr "" @@ -1594,7 +1714,7 @@ msgstr "" msgid "Signal not reported" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2754 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3078 msgctxt "vantage" msgid "Speed" msgstr "" @@ -1605,20 +1725,24 @@ msgid_plural "Standard housekeeping: %d rules" msgstr[0] "" msgstr[1] "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2281 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1080 +msgid "Starfield" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2530 msgid "Stations appear here as soon as they associate with one of the SSIDs." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2719 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3043 msgid "Status" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:92 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1755 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1870 msgid "Storage" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2659 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2983 msgid "Stored on this device in /etc/config/vantage" msgstr "" @@ -1634,7 +1758,7 @@ msgstr "" msgid "Strong signal, %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1805 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1920 msgctxt "vantage" msgid "Swap" msgstr "" @@ -1643,13 +1767,13 @@ msgstr "" msgid "Switched off" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:889 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1737 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1738 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1852 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1853 msgid "System" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2781 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3105 msgid "System settings" msgstr "" @@ -1658,14 +1782,24 @@ msgid "Tbit/s" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:75 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1769 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1884 msgid "Temperature %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:544 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:635 msgid "Temporary (RAM)" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2450 +msgid "The bridge forgets a device after about five minutes without traffic" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2524 +msgid "" +"The bridges have learned so many addresses that listing them here would " +"strain this device." +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:243 msgid "" "The default Input policy applies there and accepts, so anything on the " @@ -1673,11 +1807,11 @@ msgid "" "reject in General Settings." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2696 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3020 msgid "The device could not write /etc/config/vantage." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2110 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2225 msgid "" "The driver reports a placeholder value, so no noise floor or SNR is shown" msgstr "" @@ -1692,10 +1826,14 @@ msgid "" "an uplink in no zone would be open. Set Input to reject in General Settings." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2596 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2920 msgid "The name the device announces (WPS)" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2558 +msgid "The router this device sends its traffic to" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:206 msgid "The settings below apply if traffic reaches them as configured." msgstr "" @@ -1708,7 +1846,7 @@ msgstr "" msgid "The uplinks could not be checked" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1442 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1532 msgid "This access point" msgstr "" @@ -1716,11 +1854,11 @@ msgstr "" msgid "This address family is in no zone: the default policies apply." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1442 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1532 msgid "This device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2695 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3019 msgid "This device already stores %d names; remove some first." msgstr "" @@ -1734,7 +1872,7 @@ msgid "" "the firewall pages." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2703 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3027 msgid "This radio is not reported any more." msgstr "" @@ -1742,23 +1880,23 @@ msgstr "" msgid "This setup is beyond what this overview checks" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2506 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2795 msgid "This station is not associated right now." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:888 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1842 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1844 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2554 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:914 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1957 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1959 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2843 msgctxt "vantage" msgid "Throughput" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1049 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1075 msgid "Throughput chart" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1835 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1950 msgid "Throughput per link; arrow keys read the values over time" msgstr "" @@ -1767,32 +1905,38 @@ msgctxt "vantage" msgid "TiB" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2561 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2850 msgid "To device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2325 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2378 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2577 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2612 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2665 msgid "To device %s, from device %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2317 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2604 msgid "To device: %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2749 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3073 msgid "To network" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:889 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2342 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2343 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2523 +msgid "Too many devices to list" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2629 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2630 msgctxt "vantage" msgid "Top talkers" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2574 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2715 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2863 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2899 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3039 msgid "Traffic" msgstr "" @@ -1808,13 +1952,13 @@ msgid "" "networks. Set Forward to reject in General Settings." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1569 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1660 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1684 msgid "Traffic now" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2109 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2722 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2224 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3046 msgid "Tx power" msgstr "" @@ -1824,16 +1968,16 @@ msgid "" "it runs, and those are not listed here." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2505 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2794 msgctxt "vantage" msgid "Unknown device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2694 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3018 msgid "Unknown device type" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2703 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3027 msgid "Unknown radio" msgstr "" @@ -1848,14 +1992,14 @@ msgid "Unnamed rule" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:58 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2098 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2719 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2753 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2213 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3043 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3077 msgctxt "vantage" msgid "Up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1762 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1877 msgid "Up %s" msgstr "" @@ -1864,39 +2008,48 @@ msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:50 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:52 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:58 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2738 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1982 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3062 msgctxt "vantage" msgid "Uplink" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1982 msgid "Uplink %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2742 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3066 msgid "Uplink · %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1848 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1935 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2007 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1963 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2050 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 msgid "Upload" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:322 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:326 msgid "Upload (from clients)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2576 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2865 msgctxt "vantage" msgid "Uploaded" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2787 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2559 +msgid "Upstream" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3111 msgid "Uptime" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2560 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2901 +msgid "VLAN %d" +msgstr "" + #: luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json:24 msgid "" "Vantage dashboard: client names from reverse DNS (lets the holder make the " @@ -1913,7 +2066,9 @@ msgstr "" msgid "" "Vantage dashboard: read-only status of the device, uplink, radios and " "stations (wireless configuration without keys, through luci.vantage " -"wireless) and its temperature sensors (luci.vantage thermal)" +"wireless), its temperature sensors (luci.vantage thermal) and the wired " +"hosts its bridges have learned (luci.vantage wired: MAC, port and when last " +"heard)" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:43 @@ -2024,7 +2179,7 @@ msgstr "" msgid "WPS" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2584 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2873 msgid "WPS device name" msgstr "" @@ -2038,7 +2193,7 @@ msgid "Weak" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:146 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2200 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2378 msgid "Weak signal" msgstr "" @@ -2046,8 +2201,8 @@ msgstr "" msgid "Weak signal %s; may roam poorly" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1550 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1552 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1665 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1667 msgid "Weakest signal" msgstr "" @@ -2061,21 +2216,27 @@ msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:105 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:107 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:109 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2432 msgid "Wi-Fi" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:889 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2389 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1482 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2432 +msgid "Wired" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2676 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2677 msgctxt "vantage" msgid "Wireless networks" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2534 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2823 msgid "Wireless settings" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2592 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2916 msgid "Your name for this device" msgstr "" @@ -2100,7 +2261,7 @@ msgstr "" msgid "all protocols" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1686 msgid "all up" msgstr "" @@ -2117,11 +2278,11 @@ msgstr "" msgid "any zone (default policy)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2104 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2219 msgid "auto" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2572 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2861 msgid "before this page opened" msgstr "" @@ -2130,16 +2291,16 @@ msgctxt "vantage" msgid "bit/s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1964 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2079 msgid "collecting · %s of data" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1964 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2079 msgid "collecting · first samples" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:218 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1944 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:222 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2059 msgid "collecting…" msgstr "" @@ -2151,12 +2312,17 @@ msgstr "" msgid "dBm" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2563 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2898 +msgid "down" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:121 msgctxt "vantage" msgid "drop" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2624 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2948 msgctxt "vantage" msgid "e.g. Living room TV" msgstr "" @@ -2194,7 +2360,7 @@ msgstr "" msgid "half duplex" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2415 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2702 msgid "idle" msgstr "" @@ -2236,14 +2402,14 @@ msgctxt "vantage" msgid "kbit/s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1747 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1963 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1862 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2078 msgid "last 5 min" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:164 #: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:104 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2319 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2606 msgid "legacy" msgstr "" @@ -2251,66 +2417,71 @@ msgstr "" msgid "mDNS" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2594 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2918 msgid "mDNS announcement" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1569 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2556 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1660 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1684 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2845 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2900 msgid "measuring" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1939 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2054 msgid "measuring…" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1549 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1664 msgid "no Wi-Fi traffic yet" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1552 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1667 msgid "no clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1923 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2038 msgctxt "vantage" msgid "no data" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1573 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1688 msgid "no traffic yet" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1440 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1570 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1530 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1685 msgid "no uplink" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1543 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1658 msgid "none connected" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2582 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2871 msgid "not known to this AP" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2110 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2111 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2907 +msgid "not known to this device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2225 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2226 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3047 msgid "not reported" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3047 msgid "not reported (driver placeholder %s)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2565 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2854 msgid "not reported by the driver" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1960 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2075 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2860 msgctxt "vantage" msgid "now" msgstr "" @@ -2319,16 +2490,16 @@ msgstr "" msgid "now %s down, %s up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2563 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2852 msgid "older than the radio (%s)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1938 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2053 msgctxt "vantage" msgid "peak" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1748 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 msgid "per core" msgstr "" @@ -2337,11 +2508,11 @@ msgctxt "vantage" msgid "port forward" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2550 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2839 msgid "range %s … %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2748 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3072 msgid "received on %s" msgstr "" @@ -2354,7 +2525,7 @@ msgstr "" msgid "router port %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2749 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3073 msgid "sent on %s" msgstr "" @@ -2362,6 +2533,11 @@ msgstr "" msgid "short GI" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2580 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2902 +msgid "static entry" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:373 msgid "switched off" msgstr "" @@ -2370,6 +2546,10 @@ msgstr "" msgid "the covered subnets of zone %s" msgstr "" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2898 +msgid "the port: %s" +msgstr "" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:128 msgid "the set in %s" msgstr "" @@ -2382,11 +2562,11 @@ msgstr "" msgid "traffic rule" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1445 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1535 msgid "up %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2343 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2630 msgid "who uses the bandwidth" msgstr "" @@ -2403,11 +2583,11 @@ msgstr "" msgid "zones %s and %s if they share a device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1569 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1660 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1684 msgid "↑ %s · via %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1957 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2072 msgid "−%d min" msgstr "" diff --git a/luci-app-vantage/po/zh_Hans/vantage.po b/luci-app-vantage/po/zh_Hans/vantage.po index 0e71954c3..ab78a2f55 100644 --- a/luci-app-vantage/po/zh_Hans/vantage.po +++ b/luci-app-vantage/po/zh_Hans/vantage.po @@ -12,7 +12,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2721 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3045 msgid "%d MHz (%d–%d MHz)" msgstr "%d MHz(%d–%d MHz)" @@ -32,23 +32,29 @@ msgstr "%d 个 SSID 正在广播" msgid "%d SSIDs · %d idle" msgstr "%d 个 SSID · %d 个空闲" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1266 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2072 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1308 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1587 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2187 msgid "%d clients" msgstr "%d 个客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2275 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2514 msgid "%d connected" msgstr "已连接 %d 个" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1611 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1614 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2513 +msgid "%d devices" +msgstr "%d 台设备" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:105 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1686 msgid "%d down" msgstr "%d 个已关闭" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2765 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2766 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3089 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3090 msgid "%d errors" msgstr "%d 个错误" @@ -57,26 +63,26 @@ msgctxt "vantage" msgid "%d h ago" msgstr "%d 小时前" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2767 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3091 msgid "%d in · %d out" msgstr "%d 入 · %d 出" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:375 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:379 msgctxt "vantage" msgid "%d min" msgstr "%d 分钟" #: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:90 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:369 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:373 msgctxt "vantage" msgid "%d min ago" msgstr "%d 分钟前" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2275 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2512 msgid "%d of %d" msgstr "%d/%d" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1153 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1195 msgid "%d of %d columns" msgstr "%d/%d 列" @@ -88,7 +94,7 @@ msgstr "%d 个关闭" msgid "%d older devices (Wi-Fi 4 or earlier) slow airtime" msgstr "%d 台旧设备(Wi-Fi 4 或更早)占用更多空口时间" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1538 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1650 msgid "%d on %s" msgstr "%d 个在 %s" @@ -96,22 +102,22 @@ msgstr "%d 个在 %s" msgid "%d on this router, %d into your network" msgstr "路由器上 %d 项,进入你的网络 %d 项" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2064 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2179 msgid "%d radios" msgstr "%d 个射频" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:375 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:379 msgctxt "vantage" msgid "%d s" msgstr "%d 秒" #: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:89 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:369 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:373 msgctxt "vantage" msgid "%d s ago" msgstr "%d 秒前" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2577 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2866 msgid "%d sent · %s retried · %s failed" msgstr "发送 %d · 重传 %s · 失败 %s" @@ -128,7 +134,11 @@ msgstr "%d 个已启用" msgid "%d weak" msgstr "%d 个信号弱" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2083 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1653 +msgid "%d wired" +msgstr "%d 个有线" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2198 msgid "%d–%d MHz of the %d–%d MHz band" msgstr "%d–%d MHz 属于 %d–%d MHz 频段" @@ -146,7 +156,7 @@ msgstr "%s(%s)已启用但未上线" msgid "%s (IPv%d)" msgstr "%s(IPv%d)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2572 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2861 msgid "%s (new this session)" msgstr "%s(本次新增)" @@ -157,10 +167,10 @@ msgstr "%s Gbit/s" #: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:137 #: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:103 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2090 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2104 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2319 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2714 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2205 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2219 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2606 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3038 msgid "%s MHz" msgstr "%s MHz" @@ -174,7 +184,7 @@ msgstr "%s Mbit/s" msgid "%s accepts every connection" msgstr "%s 接受所有连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2860 msgctxt "vantage" msgid "%s ago" msgstr "%s 前" @@ -184,13 +194,13 @@ msgid "%s at %s; below %s roaming and throughput suffer" msgstr "%s(%s);低于 %s 时漫游和吞吐量会受影响" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:82 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1782 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1897 msgctxt "vantage" msgid "%s available" msgstr "可用 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2109 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2722 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2224 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3046 msgid "%s dBm" msgstr "%s dBm" @@ -202,7 +212,7 @@ msgstr "%s 设备" msgid "%s device on a newer radio; uses more airtime per byte" msgstr "%s 设备连接在较新的射频上,每字节占用更多空口时间" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2539 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2828 msgid "%s experience" msgstr "%s 体验" @@ -218,7 +228,7 @@ msgstr "overlay 空闲 %s(已用 %s)" msgid "%s has no carrier: cable unplugged or switch port down" msgstr "%s 无载波:网线未插或交换机端口关闭" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1120 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1162 msgid "%s hidden" msgstr "%s 已隐藏" @@ -234,7 +244,7 @@ msgstr "%s 被转发到 %s" msgid "%s is in no zone and accepts every connection" msgstr "%s 不属于任何区域,接受所有连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1113 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1155 msgid "%s is narrower" msgstr "%s 已变窄" @@ -242,7 +252,14 @@ msgstr "%s 已变窄" msgid "%s is not up" msgstr "%s 未上线" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1116 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2566 +msgid "" +"%s is shared by %d devices (a switch or another access point behind it); its " +"%s link is not theirs" +msgstr "" +"%s 由 %d 台设备共用(其后接有交换机或另一个接入点);%s 的链路不属于这些设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1158 msgid "%s is wider" msgstr "%s 已加宽" @@ -258,23 +275,23 @@ msgstr "%s 链路" msgid "%s may open the router's admin" msgstr "%s 可能暴露了路由器管理界面" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1234 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1276 msgid "%s moved" msgstr "%s 已移动" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1110 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1152 msgid "%s moved down" msgstr "%s 已下移" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1101 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1143 msgid "%s moved left" msgstr "%s 已左移" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1104 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1146 msgid "%s moved right" msgstr "%s 已右移" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1107 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1149 msgid "%s moved up" msgstr "%s 已上移" @@ -287,9 +304,9 @@ msgid "%s negotiated only %d Mbit/s; a cable pair or port may be faulty" msgstr "%s 仅协商到 %d Mbit/s;网线线对或端口可能有故障" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:83 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1783 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1806 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2793 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1898 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1921 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3117 msgid "%s of %s" msgstr "%s / %s" @@ -297,7 +314,7 @@ msgstr "%s / %s" msgid "%s of %s available (%s in use)" msgstr "可用 %s / 共 %s(已用 %s)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1547 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1662 msgid "%s of Wi-Fi traffic" msgstr "占 Wi-Fi 流量的 %s" @@ -314,15 +331,15 @@ msgstr "发往设备的帧失败率:%s" msgid "%s of frames to the device fail; interference likely" msgstr "发往设备的帧失败率:%s,可能存在干扰" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2791 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3115 msgid "%s over %d cores" msgstr "%s / %d 核" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1868 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1983 msgid "%s radio" msgstr "%s 射频" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1159 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1201 msgid "%s shown" msgstr "%s 已显示" @@ -338,15 +355,19 @@ msgstr "%s 已连接,速率 %s" msgid "%s used" msgstr "已用 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1814 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1929 msgid "%s used of %s" msgstr "已用 %s / 共 %s" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2564 +msgid "%s · %d devices" +msgstr "%s · %d 台设备" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:119 msgid "%s · %s" msgstr "%s · %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2569 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2858 msgid "%s · channel %d · %d MHz" msgstr "%s · 信道 %d · %d MHz" @@ -362,30 +383,36 @@ msgstr "%s · 链路较慢" msgid "%s: no carrier" msgstr "%s:无载波" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2722 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3046 msgid "(configured %d dBm, limited by regulatory rules)" msgstr "(配置 %d dBm,受管制规则限制)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1494 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2410 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1586 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2697 msgid "(hidden)" msgstr "(隐藏)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1266 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2072 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1308 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1587 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2187 msgid "1 client" msgstr "1 个客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2275 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2514 msgid "1 connected" msgstr "已连接 1 个" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1611 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1614 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2513 +msgid "1 device" +msgstr "1 台设备" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:247 msgid "1 older device (Wi-Fi 4 or earlier) slows airtime" msgstr "1 台旧设备(Wi-Fi 4 或更早)占用更多空口时间" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2064 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2179 msgid "1 radio" msgstr "1 个射频" @@ -413,19 +440,20 @@ msgstr "6 GHz" msgid "60 GHz" msgstr "60 GHz" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2199 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2377 msgid "Active" msgstr "活跃" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2581 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2870 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2906 msgid "Address type" msgstr "地址类型" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2758 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3082 msgid "Addresses" msgstr "地址" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2111 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2226 msgid "Airtime" msgstr "空口时间" @@ -434,7 +462,8 @@ msgctxt "vantage" msgid "Alias" msgstr "别名" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2195 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2368 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2374 msgid "All" msgstr "全部" @@ -471,13 +500,13 @@ msgctxt "vantage" msgid "B" msgstr "B" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1572 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1573 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1687 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1688 msgid "Busiest network" msgstr "最繁忙网络" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1547 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1549 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1662 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1664 msgid "Busiest radio" msgstr "最繁忙射频" @@ -485,7 +514,7 @@ msgstr "最繁忙射频" msgid "Busiest radio: %s of Wi-Fi traffic" msgstr "最繁忙射频:占 Wi-Fi 流量的 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1748 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 msgid "Busy share of each CPU core" msgstr "每个 CPU 核心的繁忙占比" @@ -499,8 +528,8 @@ msgstr "繁忙;负载 %s(%d 核);查看最繁忙进程" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:71 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:72 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1741 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2791 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1856 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3115 msgctxt "vantage" msgid "CPU" msgstr "CPU" @@ -509,83 +538,84 @@ msgstr "CPU" msgid "Cache (reclaimable)" msgstr "缓存(可回收)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2657 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2981 msgid "Cancel" msgstr "取消" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2564 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2853 msgctxt "vantage" msgid "Capabilities" msgstr "能力" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2753 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3077 msgid "Carrier" msgstr "链路状态" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2721 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3045 msgid "Centre frequency" msgstr "中心频率" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2103 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2218 msgid "Ch %d" msgstr "信道 %d" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1477 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2071 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1569 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2186 msgid "Ch %d · %d MHz" msgstr "信道 %d · %d MHz" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2714 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3038 msgid "Channel" msgstr "信道" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1477 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1569 msgid "Channel —" msgstr "信道 —" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2135 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2250 msgctxt "vantage" msgid "Clear filter" msgstr "清除筛选" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1362 msgid "Click any node for details" msgstr "点击任意节点查看详情" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:888 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1543 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2108 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2143 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2144 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:914 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1658 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2223 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2268 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2269 msgid "Clients" msgstr "客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2726 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3050 msgid "Clients on this radio" msgstr "此射频上的客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2427 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2714 msgid "Close" msgstr "关闭" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2334 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2621 msgid "Connected" msgstr "已连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2296 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2591 msgid "Connected after you opened this page" msgstr "打开本页面后连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1570 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2570 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1685 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2859 msgid "Connected for" msgstr "已连接时长" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2567 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2856 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2896 msgid "Connection" msgstr "连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1775 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1890 msgid "Core %d: %s" msgstr "核心 %d:%s" @@ -593,15 +623,15 @@ msgstr "核心 %d:%s" msgid "Could not read the firewall configuration" msgstr "无法读取防火墙配置" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2640 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2964 msgid "Could not save the name (no permission or the device refused it)." msgstr "无法保存名称(无权限或设备拒绝)。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2764 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3088 msgid "Counters since boot" msgstr "自启动以来的计数器" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2724 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3048 msgid "Country" msgstr "国家" @@ -610,13 +640,13 @@ msgid "" "Custom rule files are configured (%s); rules in them are not listed here." msgstr "已配置自定义规则文件(%s);其中的规则不会在此列出。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1255 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1297 msgid "Customize" msgstr "自定义" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1065 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1066 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1254 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1107 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1108 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1296 msgid "Customize the dashboard" msgstr "自定义仪表盘" @@ -624,16 +654,16 @@ msgstr "自定义仪表盘" msgid "DHCP" msgstr "DHCP" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2595 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2919 msgid "DHCP hostname" msgstr "DHCP 主机名" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2762 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3086 msgid "DNS" msgstr "DNS" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1434 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1524 msgid "DNS %s" msgstr "DNS %s" @@ -642,88 +672,96 @@ msgid "Dashboard" msgstr "仪表盘" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:268 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2301 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2782 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2449 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2538 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3106 msgid "Device" msgstr "设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2624 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2948 msgid "Device name" msgstr "设备名" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2653 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2977 msgid "Device type" msgstr "设备类型" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1478 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2073 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2098 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2719 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2527 +msgid "" +"Devices on the Ethernet ports appear here once they send traffic; the bridge " +"forgets quiet ones after about five minutes." +msgstr "" +"以太网端口上的设备发送流量后会显示在这里;网桥会在约五分钟无流量后忘记它们。" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1570 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2188 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2213 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3043 msgid "Disabled" msgstr "已禁用" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2533 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2822 msgid "Disconnected %s; shown from the last sample." msgstr "断开于 %s;显示最后一次采样的数据。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1079 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1121 msgctxt "vantage" msgid "Done" msgstr "完成" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:46 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1478 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2073 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2098 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2719 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1570 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2188 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2213 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3043 msgctxt "vantage" msgid "Down" msgstr "已下线" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1847 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1933 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2006 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1962 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2048 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2121 msgid "Download" msgstr "下载" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:321 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:325 msgid "Download (to clients)" msgstr "下载(到客户端)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2575 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2864 msgctxt "vantage" msgid "Downloaded" msgstr "已下载" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1140 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1182 msgid "" "Drag a tile by its handle or use its buttons. Saved in this browser for " "medium screens." msgstr "拖动卡片手柄或使用其按钮移动。中屏布局保存在此浏览器中。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1141 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1183 msgid "" "Drag a tile by its handle or use its buttons. Saved in this browser for " "narrow screens." msgstr "拖动卡片手柄或使用其按钮移动。窄屏布局保存在此浏览器中。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1139 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1181 msgid "" "Drag a tile by its handle or use its buttons. Saved in this browser for wide " "screens." msgstr "拖动卡片手柄或使用其按钮移动。宽屏布局保存在此浏览器中。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1092 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1134 msgid "Drag to move" msgstr "拖动以移动" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2767 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3091 msgctxt "vantage" msgid "Dropped" msgstr "已丢弃" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2677 msgid "Edit" msgstr "编辑" @@ -741,8 +779,8 @@ msgctxt "vantage" msgid "Excellent" msgstr "极好" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2328 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2615 msgid "Experience" msgstr "体验" @@ -760,15 +798,20 @@ msgstr "信号一般" msgid "Fair signal %s; lower rates at range" msgstr "信号一般 %s;远距离速率较低" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2132 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2247 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2439 msgid "Filter by name, address, SSID…" msgstr "按名称、地址、SSID…筛选" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2133 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2439 +msgid "Filter by name, address, port…" +msgstr "按名称、地址、端口…筛选" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2248 msgid "Filter clients" msgstr "筛选客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2785 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3109 msgctxt "vantage" msgid "Firmware" msgstr "固件" @@ -781,7 +824,7 @@ msgstr "从 %s 转发到 %s" msgid "Forwarding is accepted by default" msgstr "默认接受转发" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2577 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2866 msgid "Frames to device" msgstr "发往设备的帧" @@ -790,15 +833,15 @@ msgctxt "vantage" msgid "Free" msgstr "空闲" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2562 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2851 msgid "From device" msgstr "来自设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2317 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2604 msgid "From device: %s" msgstr "来自设备:%s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2748 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3072 msgid "From network" msgstr "来自网络" @@ -808,8 +851,9 @@ msgid "" "files are not shown." msgstr "内容来自防火墙配置。由 UPnP 或自定义规则文件开放的端口不会显示。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1432 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2761 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1522 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2558 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3085 msgid "Gateway" msgstr "网关" @@ -822,7 +866,7 @@ msgstr "Gbit/s" msgid "General Settings" msgstr "基本设置" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2563 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2852 msgctxt "vantage" msgid "Generation" msgstr "代际" @@ -832,11 +876,13 @@ msgctxt "vantage" msgid "GiB" msgstr "GiB" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2581 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2870 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2906 msgid "Globally unique" msgstr "全局唯一" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2581 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2870 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2906 msgid "Globally unique · %s" msgstr "全局唯一 · %s" @@ -854,38 +900,44 @@ msgstr "信号良好" msgid "Good signal %s" msgstr "信号良好 %s" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1080 +msgid "Grid packets" +msgstr "网格数据包" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:50 msgid "Half duplex" msgstr "半双工" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:785 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:811 msgctxt "vantage" msgid "Health checks" msgstr "健康检查" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1156 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2410 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1198 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2697 msgctxt "vantage" msgid "Hidden" msgstr "隐藏" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1119 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1161 msgid "Hide %s" msgstr "隐藏 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2582 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2871 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2907 msgid "IP addresses" msgstr "IP 地址" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2759 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3083 msgid "IPv4" msgstr "IPv4" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2760 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3084 msgid "IPv6" msgstr "IPv6" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2579 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2868 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2904 msgid "Identity" msgstr "身份" @@ -903,19 +955,20 @@ msgctxt "vantage" msgid "Input" msgstr "入站数据" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1457 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1548 msgid "Interface" msgstr "接口" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2756 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3080 msgid "Interface up for" msgstr "接口已启用时长" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2746 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2894 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3070 msgid "Interfaces" msgstr "接口" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1432 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1522 msgid "Internet" msgstr "互联网" @@ -945,11 +998,11 @@ msgstr "" "它将来自互联网上任何地址的 SSH 或网页端口转发到 %s。无法读取接口列表,因此本" "页无法判断该地址是否为路由器本身。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2786 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3110 msgid "Kernel" msgstr "内核" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1820 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1935 msgid "Kernel %s" msgstr "内核 %s" @@ -958,43 +1011,58 @@ msgctxt "vantage" msgid "KiB" msgstr "KiB" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2262 +msgid "Kind of client" +msgstr "客户端类型" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2860 msgctxt "vantage" msgid "Last activity" msgstr "最后活动" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1077 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2450 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2580 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2902 +msgid "Last heard" +msgstr "最后活动" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1119 msgid "Layout reset" msgstr "布局已重置" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2295 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2590 msgid "Left %s" msgstr "离开于 %s" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:163 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2297 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2592 msgctxt "vantage" msgid "Legacy" msgstr "旧版" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2560 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2752 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2449 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2562 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2849 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2898 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3076 msgctxt "vantage" msgid "Link" msgstr "链路" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2316 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2603 msgid "Link rate" msgstr "链路速率" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1274 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2321 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1316 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2449 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2608 msgid "Live" msgstr "实时" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2790 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3114 msgid "Load" msgstr "负载" @@ -1006,20 +1074,20 @@ msgstr "负载 %s(%d 核)" msgid "Load %s · %d cores" msgstr "负载 %s · %d 核" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2792 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3116 msgctxt "vantage" msgid "Load average" msgstr "平均负载" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1729 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1844 msgid "Load average over 1, 5 and 15 minutes" msgstr "1、5、15 分钟平均负载" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1765 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1880 msgid "Load avg %s" msgstr "平均负载 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2788 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3112 msgid "Local time" msgstr "本地时间" @@ -1035,7 +1103,8 @@ msgstr "链路速率低于信号预期" msgid "MAC" msgstr "MAC" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2580 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2869 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2905 msgid "MAC address" msgstr "MAC 地址" @@ -1043,15 +1112,15 @@ msgstr "MAC 地址" msgid "MCS %d" msgstr "MCS %d" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1112 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1154 msgid "Make %s narrower" msgstr "使 %s 变窄" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1115 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1157 msgid "Make %s wider" msgstr "使 %s 加宽" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2597 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2921 msgid "Manufacturer of the network adapter" msgstr "网卡制造商" @@ -1068,11 +1137,11 @@ msgid "Measuring…" msgstr "测量中…" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:82 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1752 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 msgid "Memory" msgstr "内存" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2793 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3117 msgid "Memory in use" msgstr "已用内存" @@ -1081,62 +1150,63 @@ msgctxt "vantage" msgid "MiB" msgstr "MiB" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1042 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1068 msgid "Mirrored" msgstr "镜像" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1043 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1069 msgid "Mirrored, scale per direction" msgstr "镜像,每个方向单独刻度" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2720 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3044 msgid "Mode" msgstr "模式" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2783 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3107 msgid "Model" msgstr "型号" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1109 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1151 msgid "Move %s down" msgstr "下移 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1100 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1142 msgid "Move %s left" msgstr "左移 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1103 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1145 msgid "Move %s right" msgstr "右移 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1106 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1148 msgid "Move %s up" msgstr "上移 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1092 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1134 msgid "Move %s: drag, or use the arrow keys" msgstr "移动 %s:拖动或使用方向键" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2652 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2976 msgid "Name" msgstr "名称" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:134 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2692 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3016 msgid "Name contains invisible or control characters" msgstr "名称包含不可见或控制字符" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2583 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2872 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2908 msgid "Name from" msgstr "名称来源" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2306 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2543 msgid "Name from: %s" msgstr "名称来源:%s" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:132 #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:137 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2693 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3017 msgid "Name is longer than %d characters" msgstr "名称超过 %d 个字符" @@ -1144,23 +1214,31 @@ msgstr "名称超过 %d 个字符" msgid "Name must be text" msgstr "名称必须是文本" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2524 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2813 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2891 msgid "Name this device" msgstr "命名此设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2313 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2568 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2449 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2570 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2600 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2857 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2901 msgid "Network" msgstr "网络" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:888 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1319 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:914 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1361 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1362 msgid "Network path" msgstr "网络路径" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1084 +msgid "Network path background" +msgstr "网络路径背景" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1686 msgid "Networks" msgstr "网络" @@ -1168,13 +1246,13 @@ msgstr "网络" msgid "Networks: %s" msgstr "网络:%s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2201 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2296 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2379 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2591 msgctxt "vantage" msgid "New" msgstr "新增" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1405 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1471 msgid "No SSIDs" msgstr "无 SSID" @@ -1182,23 +1260,23 @@ msgstr "无 SSID" msgid "No clients" msgstr "无客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2280 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2529 msgid "No clients connected" msgstr "无客户端连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2365 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2652 msgid "No clients connected." msgstr "没有客户端连接。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2283 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2520 msgid "No clients in this view" msgstr "此视图中没有客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2283 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2520 msgid "No clients match “%s”" msgstr "没有匹配“%s”的客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2731 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3055 msgid "No clients; an idle radio is fine." msgstr "无客户端;射频空闲属正常。" @@ -1206,7 +1284,7 @@ msgstr "无客户端;射频空闲属正常。" msgid "No clients; idle radios are fine" msgstr "无客户端;射频空闲属正常" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1433 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1523 msgctxt "vantage" msgid "No default route" msgstr "无默认路由" @@ -1215,7 +1293,7 @@ msgstr "无默认路由" msgid "No interface with a route" msgstr "没有带路由的接口" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2738 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3062 msgid "No interface with a route is up." msgstr "没有带路由的接口处于启用状态。" @@ -1224,7 +1302,7 @@ msgid "No internet-facing zone found" msgstr "未找到面向互联网的区域" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:48 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2753 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3077 msgid "No link" msgstr "无链路" @@ -1232,7 +1310,7 @@ msgstr "无链路" msgid "No network interface with a route is up" msgstr "没有带路由的网络接口处于启用状态" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1386 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1452 msgid "No other interfaces are up" msgstr "没有其他接口处于启用状态" @@ -1242,7 +1320,7 @@ msgid "No problems detected" msgstr "未检测到问题" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:104 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1409 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1475 msgid "No radios reported" msgstr "未报告射频" @@ -1259,12 +1337,16 @@ msgstr "无信号" msgid "No stations are associated right now" msgstr "当前没有站点关联" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2399 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2526 +msgid "No wired devices heard" +msgstr "未发现有线设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2686 msgctxt "vantage" msgid "No wireless networks configured." msgstr "未配置无线网络。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2078 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2193 msgid "No wireless radios are reported by this device." msgstr "此设备未报告任何无线射频。" @@ -1285,19 +1367,19 @@ msgstr "尚未配置区域。" msgid "Nobody associated" msgstr "无关联站点" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2365 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2652 msgid "Nobody is moving data right now." msgstr "当前没有设备在传输数据。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2110 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2225 msgid "Noise" msgstr "噪声" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2565 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2854 msgid "Noise / SNR" msgstr "噪声 / 信噪比" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3047 msgid "Noise floor" msgstr "底噪" @@ -1306,31 +1388,37 @@ msgid "Noise floor not reported by the driver" msgstr "驱动未报告底噪" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:104 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1080 msgid "None" msgstr "无" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:150 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2691 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3015 msgid "Not a station MAC address" msgstr "不是站点 MAC 地址" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2506 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2795 msgid "Not connected" msgstr "未连接" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2572 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2899 +msgid "Not counted per device on a shared port" +msgstr "共用端口不按设备统计" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:383 msgid "Not in effect" msgstr "未生效" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1800 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1915 msgid "Not reported" msgstr "未报告" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2600 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2924 msgid "Nothing better known" msgstr "无更多信息" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2598 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2922 msgid "Nothing better known; the address is randomised" msgstr "无更多信息;地址已随机化" @@ -1351,7 +1439,7 @@ msgstr "OWE" msgid "Older %s device" msgstr "较旧的 %s 设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2297 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2592 msgid "Older Wi-Fi generation than the radio; uses more airtime" msgstr "比射频支持的 Wi-Fi 代际更旧;占用更多空口时间" @@ -1359,7 +1447,7 @@ msgstr "比射频支持的 Wi-Fi 代际更旧;占用更多空口时间" msgid "On this router" msgstr "在此路由器上" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 msgctxt "vantage" msgid "Online" msgstr "在线" @@ -1375,7 +1463,7 @@ msgctxt "vantage" msgid "Open" msgstr "开放" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2258 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2495 msgid "Open details of %s" msgstr "打开 %s 的详情" @@ -1391,7 +1479,7 @@ msgstr "对互联网开放:%d" msgid "Open network" msgstr "开放网络" -#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:532 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:623 msgid "OpenWrt device" msgstr "OpenWrt 设备" @@ -1403,11 +1491,11 @@ msgstr "" "基本设置中的选项 %s 的值被 firewall4 拒绝,因此不会加载新规则:无法确定开放了" "哪些内容。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1040 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1066 msgid "Overlaid" msgstr "叠加" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1041 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1067 msgid "Overlaid, log scale" msgstr "叠加,对数刻度" @@ -1415,11 +1503,11 @@ msgstr "叠加,对数刻度" msgid "Overview" msgstr "概览" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2564 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2853 msgid "PMF" msgstr "PMF" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1272 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1314 msgid "Paused while hidden" msgstr "页面隐藏时暂停" @@ -1435,7 +1523,7 @@ msgstr "" "Ping 和其他控制消息、组播、DHCP 续租以及转发到你网络的 IPsec:每台路由器都会" "放行的流量。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2784 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3108 msgctxt "vantage" msgid "Platform" msgstr "平台" @@ -1445,37 +1533,50 @@ msgctxt "vantage" msgid "Poor" msgstr "较差" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2897 +msgid "Port" +msgstr "端口" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2566 +msgid "Port %s" +msgstr "端口 %s" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:43 msgid "Private" msgstr "私有" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2581 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2870 msgid "Private Wi-Fi address (randomised by the device)" msgstr "私有 Wi-Fi 地址(设备随机化)" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2906 +msgid "Private address (randomised by the device)" +msgstr "私有地址(设备随机化)" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:266 msgid "Private device" msgstr "私有设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2781 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3105 msgid "Processes" msgstr "进程" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2755 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3079 msgid "Protocol" msgstr "协议" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2137 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2257 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2259 msgid "Quick filters" msgstr "快速筛选" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2569 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2718 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2858 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3042 msgctxt "vantage" msgid "Radio" msgstr "射频" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2099 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2214 msgid "Radio details" msgstr "射频详情" @@ -1487,53 +1588,62 @@ msgstr "射频已禁用" msgid "Radio is enabled but not up" msgstr "射频已启用但未上线" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2712 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2237 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3036 msgid "Radio settings" msgstr "射频设置" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:888 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2051 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2053 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:914 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2166 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2168 msgid "Radios" msgstr "射频" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1434 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1524 msgid "Reachability not probed" msgstr "未探测可达性" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2746 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2559 +msgid "Reached through the uplink port %s, not connected to this device" +msgstr "经由上行端口 %s 到达,并未连接到本设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3070 msgid "Realtime graphs" msgstr "实时图表" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2765 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3089 msgctxt "vantage" msgid "Received" msgstr "已接收" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1273 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1315 msgctxt "vantage" msgid "Reconnecting…" msgstr "重新连接中…" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2658 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2395 +msgid "Remove filter: %s" +msgstr "移除筛选:%s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2982 msgid "Remove name" msgstr "移除名称" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2524 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2813 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2891 msgctxt "vantage" msgid "Rename" msgstr "重命名" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1078 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1120 msgid "Reset" msgstr "重置" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2593 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2917 msgid "Reverse DNS of its address" msgstr "其地址的反向 DNS" -#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:544 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:635 msgid "Root (overlay)" msgstr "Root(overlay)" @@ -1548,20 +1658,20 @@ msgid "" msgstr "" "SSH 或网页管理界面会响应互联网上的任何地址。请将此规则限制为你使用的地址。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2650 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2974 msgid "Save" msgstr "保存" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2572 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2861 msgid "Seen since" msgstr "首次出现" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2766 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3090 msgctxt "vantage" msgid "Sent" msgstr "已发送" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1044 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1070 msgid "Separate lanes" msgstr "分开显示" @@ -1569,45 +1679,57 @@ msgstr "分开显示" msgid "Settings to check" msgstr "待检查的设置" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1158 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2898 +msgid "Shared by %d devices" +msgstr "%d 台设备共用" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2563 +msgid "Shared port" +msgstr "共用端口" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1200 msgid "Show %s" msgstr "显示 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2284 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2521 msgid "Show all clients" msgstr "显示全部客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1738 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1853 msgid "Show or hide system details" msgstr "显示/隐藏系统详情" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1362 msgid "Show or hide the network path" msgstr "显示/隐藏网络路径" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2054 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2169 msgid "Show or hide the radio details" msgstr "显示/隐藏射频详情" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1850 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1965 msgid "Show or hide the throughput chart" msgstr "显示/隐藏吞吐量图表" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2677 msgid "Show or hide the wireless networks" msgstr "显示/隐藏无线网络" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1493 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2406 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1585 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2693 msgid "Show the clients of %s" msgstr "显示 %s 的客户端" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1609 +msgid "Show the devices on %s" +msgstr "显示 %s 上的设备" + #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:117 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:118 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:121 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2310 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2548 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2597 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2837 msgid "Signal" msgstr "信号强度" @@ -1615,7 +1737,7 @@ msgstr "信号强度" msgid "Signal not reported" msgstr "未报告信号" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2754 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3078 msgctxt "vantage" msgid "Speed" msgstr "速率" @@ -1625,20 +1747,24 @@ msgid "Standard housekeeping: %d rule" msgid_plural "Standard housekeeping: %d rules" msgstr[0] "标准常规规则:%d 条" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2281 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1080 +msgid "Starfield" +msgstr "星空" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2530 msgid "Stations appear here as soon as they associate with one of the SSIDs." msgstr "站点一旦关联到任一 SSID 便会显示在这里。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2719 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3043 msgid "Status" msgstr "状态" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:92 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1755 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1870 msgid "Storage" msgstr "存储" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2659 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2983 msgid "Stored on this device in /etc/config/vantage" msgstr "保存在本设备 /etc/config/vantage 中" @@ -1654,7 +1780,7 @@ msgstr "信号强 %s,链路 %s" msgid "Strong signal, %s" msgstr "信号强,%s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1805 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1920 msgctxt "vantage" msgid "Swap" msgstr "交换分区" @@ -1663,13 +1789,13 @@ msgstr "交换分区" msgid "Switched off" msgstr "已关闭" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:889 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1737 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1738 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1852 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1853 msgid "System" msgstr "系统" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2781 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3105 msgid "System settings" msgstr "系统设置" @@ -1678,14 +1804,24 @@ msgid "Tbit/s" msgstr "Tbit/s" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:75 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1769 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1884 msgid "Temperature %s" msgstr "温度 %s" -#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:544 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:635 msgid "Temporary (RAM)" msgstr "临时(RAM)" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2450 +msgid "The bridge forgets a device after about five minutes without traffic" +msgstr "设备约五分钟无流量后,网桥会将其遗忘" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2524 +msgid "" +"The bridges have learned so many addresses that listing them here would " +"strain this device." +msgstr "网桥学习到的地址过多,在此列出会加重本设备的负担。" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:243 msgid "" "The default Input policy applies there and accepts, so anything on the " @@ -1695,11 +1831,11 @@ msgstr "" "该处适用默认入站数据策略且为接受,因此互联网上的任何主机都能访问路由器本身。" "请将其加入 wan 区域,或在基本设置中将入站数据设为拒绝。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2696 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3020 msgid "The device could not write /etc/config/vantage." msgstr "设备无法写入 /etc/config/vantage。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2110 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2225 msgid "" "The driver reports a placeholder value, so no noise floor or SNR is shown" msgstr "驱动报告的是占位值,不显示底噪与信噪比" @@ -1716,10 +1852,14 @@ msgstr "" "无法读取接口列表,且默认入站数据策略为接受:不属于任何区域的上行接口将会开" "放。请在基本设置中将入站数据设为拒绝。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2596 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2920 msgid "The name the device announces (WPS)" msgstr "设备广播的名称(WPS)" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2558 +msgid "The router this device sends its traffic to" +msgstr "此设备发送流量所经的路由器" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:206 msgid "The settings below apply if traffic reaches them as configured." msgstr "以下设置在流量按配置到达时生效。" @@ -1732,7 +1872,7 @@ msgstr "保存的名称包含无效的配置节 ID" msgid "The uplinks could not be checked" msgstr "无法检查上行接口" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1442 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1532 msgid "This access point" msgstr "此接入点" @@ -1740,11 +1880,11 @@ msgstr "此接入点" msgid "This address family is in no zone: the default policies apply." msgstr "此地址族不属于任何区域:适用默认策略。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1442 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1532 msgid "This device" msgstr "此设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2695 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3019 msgid "This device already stores %d names; remove some first." msgstr "此设备已存储 %d 个名称;请先移除部分。" @@ -1758,7 +1898,7 @@ msgid "" "the firewall pages." msgstr "本页无法判断开放了哪些内容。请重新加载;如果仍无效,请检查防火墙页面。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2703 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3027 msgid "This radio is not reported any more." msgstr "驱动不再报告此射频。" @@ -1766,23 +1906,23 @@ msgstr "驱动不再报告此射频。" msgid "This setup is beyond what this overview checks" msgstr "此配置超出本概览的检查范围" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2506 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2795 msgid "This station is not associated right now." msgstr "此站点当前未关联。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:888 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1842 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1844 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2554 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:914 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1957 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1959 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2843 msgctxt "vantage" msgid "Throughput" msgstr "吞吐量" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1049 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1075 msgid "Throughput chart" msgstr "吞吐量图表" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1835 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1950 msgid "Throughput per link; arrow keys read the values over time" msgstr "每条链路的吞吐量;方向键可查看历史值" @@ -1791,32 +1931,38 @@ msgctxt "vantage" msgid "TiB" msgstr "TiB" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2561 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2850 msgid "To device" msgstr "到设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2325 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2378 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2577 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2612 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2665 msgid "To device %s, from device %s" msgstr "到设备 %s,来自设备 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2317 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2604 msgid "To device: %s" msgstr "到设备:%s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2749 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3073 msgid "To network" msgstr "发往网络" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:889 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2342 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2343 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2523 +msgid "Too many devices to list" +msgstr "设备过多,无法列出" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2629 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2630 msgctxt "vantage" msgid "Top talkers" msgstr "流量大户" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2574 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2715 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2863 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2899 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3039 msgid "Traffic" msgstr "流量" @@ -1834,13 +1980,13 @@ msgstr "" "未被任何转发覆盖的互联网流量仍会到达你的其他网络。请在基本设置中将转发设为拒" "绝。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1569 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1660 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1684 msgid "Traffic now" msgstr "当前流量" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2109 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2722 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2224 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3046 msgid "Tx power" msgstr "发射功率" @@ -1851,16 +1997,16 @@ msgid "" msgstr "" "已安装 UPnP:它运行时,你网络中的设备可以自行开放端口,这些端口不会在此列出。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2505 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2794 msgctxt "vantage" msgid "Unknown device" msgstr "未知设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2694 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3018 msgid "Unknown device type" msgstr "未知设备类型" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2703 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3027 msgid "Unknown radio" msgstr "未知射频" @@ -1875,14 +2021,14 @@ msgid "Unnamed rule" msgstr "未命名的规则" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:58 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2098 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2719 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2753 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2213 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3043 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3077 msgctxt "vantage" msgid "Up" msgstr "已上线" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1762 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1877 msgid "Up %s" msgstr "已运行 %s" @@ -1891,39 +2037,48 @@ msgstr "已运行 %s" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:50 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:52 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:58 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2738 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1982 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3062 msgctxt "vantage" msgid "Uplink" msgstr "上行链路" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1982 msgid "Uplink %s" msgstr "上行链路 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2742 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3066 msgid "Uplink · %s" msgstr "上行链路 · %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1848 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1935 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2007 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1963 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2050 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 msgid "Upload" msgstr "上传" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:322 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:326 msgid "Upload (from clients)" msgstr "上传(来自客户端)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2576 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2865 msgctxt "vantage" msgid "Uploaded" msgstr "已上传" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2787 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2559 +msgid "Upstream" +msgstr "上游" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3111 msgid "Uptime" msgstr "运行时间" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2560 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2901 +msgid "VLAN %d" +msgstr "VLAN %d" + #: luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json:24 msgid "" "Vantage dashboard: client names from reverse DNS (lets the holder make the " @@ -1944,7 +2099,9 @@ msgstr "" msgid "" "Vantage dashboard: read-only status of the device, uplink, radios and " "stations (wireless configuration without keys, through luci.vantage " -"wireless) and its temperature sensors (luci.vantage thermal)" +"wireless), its temperature sensors (luci.vantage thermal) and the wired " +"hosts its bridges have learned (luci.vantage wired: MAC, port and when last " +"heard)" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:43 @@ -2055,7 +2212,7 @@ msgstr "WPA3 个人版" msgid "WPS" msgstr "WPS" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2584 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2873 msgid "WPS device name" msgstr "WPS 设备名称" @@ -2069,7 +2226,7 @@ msgid "Weak" msgstr "弱" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:146 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2200 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2378 msgid "Weak signal" msgstr "信号弱" @@ -2077,8 +2234,8 @@ msgstr "信号弱" msgid "Weak signal %s; may roam poorly" msgstr "信号弱 %s;漫游可能不佳" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1550 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1552 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1665 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1667 msgid "Weakest signal" msgstr "最弱信号" @@ -2092,21 +2249,27 @@ msgstr "开放内容取决于 firewall4 如何处理 %s,本页无法跟踪。 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:105 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:107 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:109 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2432 msgid "Wi-Fi" msgstr "Wi-Fi" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:889 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2389 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1482 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2432 +msgid "Wired" +msgstr "有线" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2676 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2677 msgctxt "vantage" msgid "Wireless networks" msgstr "无线网络" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2534 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2823 msgid "Wireless settings" msgstr "无线设置" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2592 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2916 msgid "Your name for this device" msgstr "你为此设备设置的名称" @@ -2131,7 +2294,7 @@ msgstr "除 %s 以外的所有端口" msgid "all protocols" msgstr "所有协议" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1686 msgid "all up" msgstr "全部启用" @@ -2148,11 +2311,11 @@ msgstr "任意区域" msgid "any zone (default policy)" msgstr "任意区域(默认策略)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2104 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2219 msgid "auto" msgstr "自动" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2572 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2861 msgid "before this page opened" msgstr "打开本页之前" @@ -2161,16 +2324,16 @@ msgctxt "vantage" msgid "bit/s" msgstr "bit/s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1964 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2079 msgid "collecting · %s of data" msgstr "采集中 · 已采集 %s 数据" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1964 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2079 msgid "collecting · first samples" msgstr "采集中 · 首个样本" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:218 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1944 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:222 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2059 msgid "collecting…" msgstr "采集中…" @@ -2182,12 +2345,17 @@ msgstr "自定义规则文件" msgid "dBm" msgstr "dBm" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2563 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2898 +msgid "down" +msgstr "已断开" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:121 msgctxt "vantage" msgid "drop" msgstr "丢弃" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2624 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2948 msgctxt "vantage" msgid "e.g. Living room TV" msgstr "例如:客厅电视" @@ -2225,7 +2393,7 @@ msgstr "网关 %s" msgid "half duplex" msgstr "半双工" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2415 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2702 msgid "idle" msgstr "空闲" @@ -2267,14 +2435,14 @@ msgctxt "vantage" msgid "kbit/s" msgstr "kbit/s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1747 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1963 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1862 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2078 msgid "last 5 min" msgstr "最近 5 分钟" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:164 #: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:104 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2319 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2606 msgid "legacy" msgstr "旧版" @@ -2282,66 +2450,71 @@ msgstr "旧版" msgid "mDNS" msgstr "mDNS" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2594 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2918 msgid "mDNS announcement" msgstr "mDNS 广播" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1569 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2556 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1660 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1684 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2845 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2900 msgid "measuring" msgstr "测量中" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1939 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2054 msgid "measuring…" msgstr "测量中…" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1549 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1664 msgid "no Wi-Fi traffic yet" msgstr "尚无 Wi-Fi 流量" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1552 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1667 msgid "no clients" msgstr "无客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1923 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2038 msgctxt "vantage" msgid "no data" msgstr "无数据" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1573 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1688 msgid "no traffic yet" msgstr "尚无流量" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1440 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1570 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1530 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1685 msgid "no uplink" msgstr "无上行链路" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1543 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1658 msgid "none connected" msgstr "无连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2582 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2871 msgid "not known to this AP" msgstr "此 AP 未识别" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2110 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2111 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2907 +msgid "not known to this device" +msgstr "此设备未识别" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2225 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2226 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3047 msgid "not reported" msgstr "未报告" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3047 msgid "not reported (driver placeholder %s)" msgstr "未报告(驱动占位值 %s)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2565 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2854 msgid "not reported by the driver" msgstr "驱动未报告" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1960 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2571 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2075 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2860 msgctxt "vantage" msgid "now" msgstr "现在" @@ -2350,16 +2523,16 @@ msgstr "现在" msgid "now %s down, %s up" msgstr "当前下行 %s,上行 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2563 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2852 msgid "older than the radio (%s)" msgstr "比射频支持的标准更旧(%s)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1938 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2053 msgctxt "vantage" msgid "peak" msgstr "峰值" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1748 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 msgid "per core" msgstr "每核" @@ -2368,11 +2541,11 @@ msgctxt "vantage" msgid "port forward" msgstr "端口转发" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2550 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2839 msgid "range %s … %s" msgstr "范围 %s … %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2748 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3072 msgid "received on %s" msgstr "在 %s 上接收" @@ -2385,7 +2558,7 @@ msgstr "拒绝" msgid "router port %s" msgstr "路由器端口 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2749 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:3073 msgid "sent on %s" msgstr "在 %s 上发送" @@ -2393,6 +2566,11 @@ msgstr "在 %s 上发送" msgid "short GI" msgstr "短 GI" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2580 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2902 +msgid "static entry" +msgstr "静态条目" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:373 msgid "switched off" msgstr "已关闭" @@ -2401,6 +2579,10 @@ msgstr "已关闭" msgid "the covered subnets of zone %s" msgstr "区域 %s 覆盖的子网" +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2898 +msgid "the port: %s" +msgstr "端口:%s" + #: luci-app-vantage/htdocs/luci-static/resources/view/vantage/firewall.js:128 msgid "the set in %s" msgstr "%s 中的地址集合" @@ -2413,11 +2595,11 @@ msgstr "此路由器" msgid "traffic rule" msgstr "通信规则" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1445 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1535 msgid "up %s" msgstr "已运行 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2343 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2630 msgid "who uses the bandwidth" msgstr "带宽使用者" @@ -2434,12 +2616,12 @@ msgstr "区域转发" msgid "zones %s and %s if they share a device" msgstr "区域 %s 和 %s(若两者共用设备)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1569 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1660 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1684 msgid "↑ %s · via %s" msgstr "↑ %s · 经 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1957 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2072 msgid "−%d min" msgstr "−%d 分钟" diff --git a/luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json b/luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json index 16c069e5a..ac9464f63 100644 --- a/luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json +++ b/luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json @@ -1,6 +1,6 @@ { "luci-app-vantage": { - "description": "Vantage dashboard: read-only status of the device, uplink, radios and stations (wireless configuration without keys, through luci.vantage wireless) and its temperature sensors (luci.vantage thermal)", + "description": "Vantage dashboard: read-only status of the device, uplink, radios and stations (wireless configuration without keys, through luci.vantage wireless), its temperature sensors (luci.vantage thermal) and the wired hosts its bridges have learned (luci.vantage wired: MAC, port and when last heard)", "read": { "file": { "/proc/stat": [ "read" ] @@ -11,7 +11,7 @@ "network.interface": [ "dump" ], "network.device": [ "status" ], "luci-rpc": [ "getHostHints" ], - "luci.vantage": [ "thermal", "wireless" ], + "luci.vantage": [ "thermal", "wired", "wireless" ], "iwinfo": [ "assoclist", "info" ], "hostapd.*": [ "get_clients", "get_status" ], "umdns": [ "hosts" ], diff --git a/luci-app-vantage/root/usr/share/rpcd/ucode/luci.vantage b/luci-app-vantage/root/usr/share/rpcd/ucode/luci.vantage index 9cb5388ce..644ad8771 100644 --- a/luci-app-vantage/root/usr/share/rpcd/ucode/luci.vantage +++ b/luci-app-vantage/root/usr/share/rpcd/ucode/luci.vantage @@ -5,10 +5,21 @@ // // wireless network.wireless status reduced to the fields the dashboard // reads: no key, sae_password, RADIUS or 802.11r secrets, no -// EAP credentials. Read group luci-app-vantage. +// EAP credentials. Where netifd has no network.wireless object +// (vendor drivers that bypass it, such as MediaTek's mtkwifi), +// the same fields from /etc/config/wireless instead. Read group +// luci-app-vantage. // thermal the temperature of each thermal zone in /sys/class/thermal // (millidegrees Celsius) with its type; nothing else is read. // Read group luci-app-vantage. +// wired the hosts the Linux bridges have learned on their ports: +// MAC, port, bridge, VLAN and seconds since last heard, from a +// read-only netlink dump of the forwarding database; at most +// 128 per port and 512 in all. Not dumped (crowded: true) +// when the bridges hold FDB_LIMIT entries or more between +// them, counted first through sysfs without reading further: a flood of +// made-up addresses must not make rpcd build the whole +// table. Read group luci-app-vantage. // set_alias { mac, name, icon }: name one station, or remove its name // with name ''. Validated here, whatever the browser did, and // written to /etc/config/vantage only. Write group @@ -23,14 +34,21 @@ import { cursor } from 'uci'; import { connect } from 'ubus'; import { lsdir, readfile } from 'fs'; +import * as rtnl from 'rtnl'; const CONFIG = 'vantage'; +const WIRELESS = 'wireless'; // read only, by wireless_from_config() const NAME_MAX = 48; // code points, after collapsing spaces const NAME_MAX_UNITS = NAME_MAX * 8; // raw input cap, in UTF-16 code units (as names.js) const MAX_ALIASES = 512; // client sections; bounds /etc/config/vantage const ICONS = [ 'phone', 'laptop', 'tablet', 'desktop', 'tv', 'speaker', 'printer', 'camera', 'console', 'watch', 'iot', 'router', 'device' ]; const THERMAL = '/sys/class/thermal'; const MAX_ZONES = 16; +const MAX_HOSTS = 512; // wired hosts per reply +const MAX_PER_PORT = 128; // of them on one port (the uplink's would take them all) +const NET = '/sys/class/net'; +const FDB_LIMIT = 4096; // forwarding entries, all bridges together, before wired() lists none +const FDB_ENTRY = 16; // sizeof(struct __fdb_entry), brforward's record /* ------------------------------------------------------------ helpers */ @@ -170,6 +188,12 @@ function short_string(v, max) { return (type(v) == 'string' && length(v) <= max) ? v : null; } +// a boolean option that is on, read as the init scripts read it +// (config_get_bool; netifd takes '1' and 'true') +function uci_true(v) { + return v in [ '1', 'on', 'true', 'yes', 'enabled' ]; +} + /* ----------------------------------------------------------- wireless */ // the encryption mode ('sae-mixed+ccmp', 'psk2', 'none', ...): its leading @@ -229,6 +253,46 @@ function reduce_radio(r) { }; } +// The same reply from /etc/config/wireless, for firmware whose driver +// bypasses netifd. Only enabled interfaces that name their ifname, since +// the view can look nothing else up; the first section to claim a name +// wins. Run state stays unknown here: iwinfo is served by rpcd itself, so +// asking it from a plugin would wait on rpcd. No txpower either: vendor +// drivers do not configure it in dBm (mtkwifi: per cent). +function wireless_from_config() { + let wl = cursor(); + let radios = {}, taken = {}, out = {}; + + if (!wl.load(WIRELESS)) + return out; + + wl.foreach(WIRELESS, 'wifi-device', (s) => { + if (match(s['.name'], /^[A-Za-z0-9_.-]{1,64}$/)) + radios[s['.name']] = { disabled: uci_true(s.disabled), config: { ...s, txpower: null }, interfaces: [] }; + }); + + wl.foreach(WIRELESS, 'wifi-iface', (s) => { + let r = (type(s.device) == 'string') ? radios[s.device] : null; + let ifname = s.ifname; + + if (type(r) != 'object' || uci_true(s.disabled) || type(ifname) != 'string' || + !match(ifname, /^[A-Za-z0-9_.-]{1,64}$/) || taken[ifname]) + return; + + taken[ifname] = true; + push(r.interfaces, { + ifname, + section: s['.name'], + config: { ...s, network: (type(s.network) == 'string') ? filter(split(s.network, /\s+/), (n) => n != '') : s.network } + }); + }); + + for (let name, r in radios) + out[name] = { ...reduce_radio(r), source: 'config' }; + + return out; +} + /* ---------------------------------------------------------- set_alias */ function set_alias(args) { @@ -338,6 +402,72 @@ function thermal() { return out; } +/* -------------------------------------------------------------- wired */ + +function netdev(v) { + return type(v) == 'string' && match(v, /^[A-Za-z0-9_.-]{1,15}$/) ? v : null; +} + +// true when the bridges hold FDB_LIMIT forwarding entries or more +// between them (one dump returns them all): each sysfs brforward lists +// its bridge's as FDB_ENTRY-byte records, and the kernel fills only as +// many as asked for, so no more than the budget left is read +function crowded() { + let left = FDB_LIMIT * FDB_ENTRY; + + for (let n in (lsdir(NET) ?? [])) { + let b = netdev(n) ? readfile(NET + '/' + n + '/brforward', left) : null; + + if (type(b) == 'string') + left -= length(b); + + if (left <= 0) + return true; + } + + return false; +} + +// [ { mac, port, bridge, vlan, seen } ] in dump order, at most MAX_HOSTS +// and MAX_PER_PORT a port: what the bridges learned from their ports. +// Left out: the bridges' own addresses (permanent), the switch drivers' +// copies (self, no master) and group addresses. seen: seconds since the +// bridge last heard from the host (the kernel counts in 1/100 s), null +// for a static entry. null when a bridge is crowded. +function wired() { + if (crowded()) + return null; + + let fdb = rtnl.request(rtnl.const.RTM_GETNEIGH, rtnl.const.NLM_F_DUMP, { family: rtnl.const.AF_BRIDGE }); + let out = [], per = {}; + + for (let n in ((type(fdb) == 'array') ? fdb : [])) { + if (length(out) >= MAX_HOSTS) + break; + + let mac = (type(n) == 'object') ? norm_mac(n.lladdr) : null; + + if (!mac || is_multicast(mac) || type(n.state) != 'int' || (n.state & rtnl.const.NUD_PERMANENT) || + (type(n.flags) == 'int' && (n.flags & rtnl.const.NTF_SELF)) || !netdev(n.master) || !netdev(n.dev) || + (per[n.dev] ?? 0) >= MAX_PER_PORT) + continue; + + per[n.dev] = (per[n.dev] ?? 0) + 1; + + let upd = n.cacheinfo?.updated; + + push(out, { + mac, + port: n.dev, + bridge: n.master, + vlan: (type(n.vlan) == 'int' && n.vlan > 0 && n.vlan < 4095) ? n.vlan : null, + seen: (!(n.state & rtnl.const.NUD_NOARP) && type(upd) == 'int' && upd >= 0) ? int(upd / 100) : null + }); + } + + return out; +} + /* ------------------------------------------------------------ methods */ const methods = { @@ -346,8 +476,10 @@ const methods = { let reply = connect()?.call('network.wireless', 'status'); let out = {}; - if (type(reply) != 'object') - return out; + // no object, or one that skipped every radio (netifd has no + // handler for the vendor's driver type): the config instead + if (type(reply) != 'object' || !length(reply)) + return wireless_from_config(); for (let name, r in reply) if (type(r) == 'object' && match(name, /^[A-Za-z0-9_.-]{1,64}$/)) @@ -363,6 +495,14 @@ const methods = { } }, + wired: { + call: function() { + let hosts = wired(); + + return (hosts == null) ? { hosts: [], crowded: true } : { hosts }; + } + }, + set_alias: { args: { mac: 'mac', name: 'name', icon: 'icon' }, call: function(request) { diff --git a/luci-theme-shadcn/.dev/src/media/components/_card.css b/luci-theme-shadcn/.dev/src/media/components/_card.css index fad8b1a04..454ee9c51 100644 --- a/luci-theme-shadcn/.dev/src/media/components/_card.css +++ b/luci-theme-shadcn/.dev/src/media/components/_card.css @@ -51,8 +51,22 @@ @apply mb-3 flex flex-wrap justify-around gap-4 md:flex-nowrap; } + /* luci-mod-status (status/include/29_ports.js) lays the port map out as an + inline grid with `repeat(auto-fit, minmax(70px, 1fr))` tracks and no gap, + and every card carries an inline `margin:.25em;min-width:70px;max-width:100px`. + On a narrow screen auto-fit packs 4+ tracks of ~71px, which no longer fit + the 70px card once its horizontal margin is subtracted: the margin + collapses to a few pixels and the cards read as glued together. Give the + tracks a comfortable minimum there -- auto-fit then lands on two columns -- + let the cards fill them, and move the spacing into the grid gap. */ div[style*="display:grid"]:has(> .ifacebox) { - @apply place-items-center; + @apply place-items-center max-md:place-items-stretch max-md:gap-3; + @apply max-md:[grid-template-columns:repeat(auto-fit,minmax(140px,1fr))]!; + + /* Track and card width are inline styles written by LuCI. */ + > .ifacebox { + @apply max-md:m-0! max-md:max-w-none!; + } } } diff --git a/luci-theme-shadcn/.dev/tests/build-output.test.js b/luci-theme-shadcn/.dev/tests/build-output.test.js index e854e97bd..adaaea51f 100644 --- a/luci-theme-shadcn/.dev/tests/build-output.test.js +++ b/luci-theme-shadcn/.dev/tests/build-output.test.js @@ -50,8 +50,9 @@ test("page content appears without decorative entry fades", () => { }); // uhttpd serves identity bytes, so raw size is what every cold load pays. -// Set 2026-09 from main 129213, login 10521, menu 15859, router 15771, -// sidebar 6107, Inter latin 48256, logo 258. +// Set 2026-11 from main 130197 (+346 for the narrow-screen port grid in +// _card.css), login 10521, menu 15859, router 15771, sidebar 6107, +// Inter latin 48256, logo 258. test("production assets stay within raw-transfer budgets", () => { const main = bytes("shadcn/main.css"); const login = bytes("shadcn/login.css"); @@ -61,7 +62,7 @@ test("production assets stay within raw-transfer budgets", () => { const font = bytes("shadcn/inter-latin-wght-normal.woff2"); const logo = bytes("shadcn/images/logo.svg"); - assert.ok(main <= 130_000, `main.css ${main} B exceeds 130 KB`); + assert.ok(main <= 130_500, `main.css ${main} B exceeds 130.5 KB`); assert.ok(login <= 11_000, `login.css ${login} B exceeds 11 KB`); // 17.5K: palette tabs (third-level nodes, redirect-parent folding, legacy // recents mapping, parent-aware scoring, per-segment path words) added diff --git a/luci-theme-shadcn/Makefile b/luci-theme-shadcn/Makefile index 341f5c225..865659a53 100644 --- a/luci-theme-shadcn/Makefile +++ b/luci-theme-shadcn/Makefile @@ -9,7 +9,7 @@ LUCI_TITLE:=A modern sidebar LuCI theme for OpenWrt, built with shadcn/ui design LUCI_DEPENDS:=+luci-base PKG_VERSION:=0.6.1 -PKG_RELEASE:=8 +PKG_RELEASE:=9 PKG_LICENSE:=Apache-2.0 LUCI_MINIFY_CSS:= diff --git a/luci-theme-shadcn/htdocs/luci-static/shadcn/main.css b/luci-theme-shadcn/htdocs/luci-static/shadcn/main.css index 7b9a9f5f6..e7f8517d7 100644 --- a/luci-theme-shadcn/htdocs/luci-static/shadcn/main.css +++ b/luci-theme-shadcn/htdocs/luci-static/shadcn/main.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-content:"";--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-space-y-reverse:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:.25rem;--breakpoint-md:48rem;--container-xs:20rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base) * .5);--radius-md:calc(var(--radius-base) * .75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base) * 1.5);--shadow-sm:var(--shadow-sm);--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base) * .5);--color-sidebar-fg:var(--sidebar-foreground);--color-terminal-fg:var(--terminal-foreground)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.fixed{position:fixed}.relative{position:relative}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.block{display:block}.hidden{display:none}.inline{display:inline}.overflow-y-auto{overflow-y:auto}.border{border-style:var(--tw-border-style);border-width:1px}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.select-all{-webkit-user-select:all;user-select:all}}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-cyrillic-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-cyrillic-wght-normal.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-greek-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+1F??}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-greek-wght-normal.woff2)format("woff2-variations");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-vietnamese-wght-normal.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-latin-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-latin-wght-normal.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--bg:oklch(98.5% .004 275);--surface:oklch(100% 0 0);--text:oklch(18% .02 275);--brand:oklch(56.7% .158 275);--on-brand:oklch(100% 0 0);--success:oklch(58% .15 150);--warning:oklch(70% .13 75);--danger:oklch(55% .2 25);--info:oklch(58% .13 250);--overlay-base:oklch(0% 0 0);--background:oklch(98.5% .004 275);--foreground:oklch(18% .02 275);--primary:oklch(56.7% .158 275);--primary-foreground:oklch(100% 0 0);--panel-bg:oklch(100% 0 0);--destructive:oklch(55% .2 25);--ring:oklch(56.7% .158 275);--border:oklch(89.65% .0058 275);--input:oklch(89.65% .0058 275);--sidebar-bg:oklch(98.5% .004 275);--sidebar-foreground:oklch(18% .02 275);--sidebar-accent-fg:oklch(18% .02 275);--sidebar-accent:oklch(94.5% .004 275);--sidebar-hover-bg:oklch(94.5% .004 275);--login-path-stroke:oklch(56.7% .158 275);--progress-bar-end:oklch(56.7% .158 275);--destructive-fg:oklch(100% 0 0);--success-fg:oklch(100% 0 0);--info-fg:oklch(100% 0 0);--warning-fg:oklch(20% .05 75);--terminal-foreground:oklch(85% .08 155);--terminal-hover:oklch(85% .08 155/.06);--muted-foreground:oklch(52.62% .0131 275);--secondary-foreground:oklch(30.08% .0176 275);--sidebar-muted:oklch(52.62% .0131 275);--focus-ring:oklch(56.7% .158 275/.5);--muted:oklch(95% .004 275);--secondary:oklch(93.5% .004 275);--label-surface:oklch(96.3% .004 275);--panel-border:oklch(89.65% .0058 275);--primary-hover:oklch(62.7% .158 275);--sidebar-active-bg:oklch(56.7% .158 275/.12);--sidebar-active-fg:oklch(48.7% .158 275);--sidebar-border:oklch(89.65% .0058 275);--terminal-bg:oklch(14% .02 264);--warning-soft-fg:oklch(42% .12 75);--progress-bar-start:oklch(74.02% .0948 275);--login-left-bg:oklch(93.5% .004 275);--primary-a5:oklch(56.7% .158 275/.05);--primary-a10:oklch(56.7% .158 275/.1);--primary-a15:oklch(56.7% .158 275/.15);--primary-a20:oklch(56.7% .158 275/.2);--primary-a25:oklch(56.7% .158 275/.25);--primary-a60:oklch(56.7% .158 275/.6);--primary-a70:oklch(56.7% .158 275/.7);--primary-a90:oklch(56.7% .158 275/.9);--destructive-a8:oklch(55% .2 25/.08);--destructive-a10:oklch(55% .2 25/.1);--destructive-a20:oklch(55% .2 25/.2);--destructive-a30:oklch(55% .2 25/.3);--destructive-a40:oklch(55% .2 25/.4);--destructive-a50:oklch(55% .2 25/.5);--destructive-a60:oklch(55% .2 25/.6);--destructive-a70:oklch(55% .2 25/.7);--destructive-a80:oklch(55% .2 25/.8);--destructive-a90:oklch(55% .2 25/.9);--foreground-a5:oklch(18% .02 275/.05);--foreground-a30:oklch(18% .02 275/.3);--secondary-a80:oklch(93.5% .004 275/.8);--muted-a30:oklch(95% .004 275/.3);--muted-a80:oklch(95% .004 275/.8);--muted-foreground-a70:oklch(52.62% .0131 275/.7);--success-a10:oklch(58% .15 150/.1);--success-a15:oklch(58% .15 150/.15);--success-a30:oklch(58% .15 150/.3);--success-a40:oklch(58% .15 150/.4);--success-a80:oklch(58% .15 150/.8);--warning-a10:oklch(70% .13 75/.1);--warning-a15:oklch(70% .13 75/.15);--warning-a30:oklch(70% .13 75/.3);--warning-a40:oklch(70% .13 75/.4);--info-a10:oklch(58% .13 250/.1);--info-a15:oklch(58% .13 250/.15);--info-a30:oklch(58% .13 250/.3);--info-a40:oklch(58% .13 250/.4);--info-a80:oklch(58% .13 250/.8);--border-a60:oklch(89.65% .0058 275/.6);--border-a80:oklch(89.65% .0058 275/.8);--input-a30:oklch(89.65% .0058 275/.3);--label-surface-a50:oklch(96.3% .004 275/.5);--overlay-base-a50:oklch(0% 0 0/.5);--overlay-base-a60:oklch(0% 0 0/.6);--background-a60:oklch(98.5% .004 275/.6);--ring-a50:oklch(56.7% .158 275/.5);--font-sans:"Inter Variable", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;--radius-base:.5rem;--shadow-sm:0 1px 3px 0 oklch(0% 0 0/.1), 0 1px 2px -1px oklch(0% 0 0/.1)}[data-darkmode=true]{--bg:oklch(15.9% .005 264);--surface:oklch(20.5% .004 264);--text:oklch(90% .006 264);--brand:oklch(56.7% .158 275);--on-brand:oklch(100% 0 0);--success:oklch(63.7% .175 147);--warning:oklch(80% .12 80);--danger:oklch(63% .16 25);--info:oklch(70% .1 250);--overlay-base:oklch(0% 0 0);--background:oklch(15.9% .005 264);--foreground:oklch(90% .006 264);--primary:oklch(56.7% .158 275);--primary-foreground:oklch(100% 0 0);--panel-bg:oklch(20.5% .004 264);--destructive:oklch(63% .16 25);--ring:oklch(56.7% .158 275);--border:oklch(26.27% .0051 264);--input:oklch(26.27% .0051 264);--sidebar-bg:oklch(15.9% .005 264);--sidebar-foreground:oklch(90% .006 264);--sidebar-accent-fg:oklch(90% .006 264);--sidebar-accent:oklch(23% .004 264);--sidebar-hover-bg:oklch(23% .004 264);--login-path-stroke:oklch(56.7% .158 275);--progress-bar-end:oklch(56.7% .158 275);--destructive-fg:oklch(100% 0 0);--success-fg:oklch(100% 0 0);--info-fg:oklch(100% 0 0);--warning-fg:oklch(20% .05 75);--terminal-foreground:oklch(85% .08 155);--terminal-hover:oklch(85% .08 155/.06);--muted-foreground:oklch(58.14% .0056 264);--secondary-foreground:oklch(78.89% .0059 264);--sidebar-muted:oklch(58.14% .0056 264);--focus-ring:oklch(56.7% .158 275/.5);--muted:oklch(18.9% .005 264);--secondary:oklch(22.5% .004 264);--label-surface:oklch(19.5% .004 264);--panel-border:oklch(90% .006 264/.08);--primary-hover:oklch(62.7% .158 275);--sidebar-active-bg:oklch(56.7% .158 275/.14);--sidebar-active-fg:oklch(78.7% .158 275);--sidebar-border:oklch(90% .006 264/.08);--terminal-bg:oklch(12% .02 264);--warning-soft-fg:oklch(86% .1 85);--progress-bar-start:oklch(42.22% .0964 274.8);--login-left-bg:oklch(13.4% .005 264);--primary-a5:oklch(56.7% .158 275/.05);--primary-a10:oklch(56.7% .158 275/.1);--primary-a15:oklch(56.7% .158 275/.15);--primary-a20:oklch(56.7% .158 275/.2);--primary-a25:oklch(56.7% .158 275/.25);--primary-a60:oklch(56.7% .158 275/.6);--primary-a70:oklch(56.7% .158 275/.7);--primary-a90:oklch(56.7% .158 275/.9);--destructive-a8:oklch(63% .16 25/.08);--destructive-a10:oklch(63% .16 25/.1);--destructive-a20:oklch(63% .16 25/.2);--destructive-a30:oklch(63% .16 25/.3);--destructive-a40:oklch(63% .16 25/.4);--destructive-a50:oklch(63% .16 25/.5);--destructive-a60:oklch(63% .16 25/.6);--destructive-a70:oklch(63% .16 25/.7);--destructive-a80:oklch(63% .16 25/.8);--destructive-a90:oklch(63% .16 25/.9);--foreground-a5:oklch(90% .006 264/.05);--foreground-a30:oklch(90% .006 264/.3);--secondary-a80:oklch(22.5% .004 264/.8);--muted-a30:oklch(18.9% .005 264/.3);--muted-a80:oklch(18.9% .005 264/.8);--muted-foreground-a70:oklch(58.14% .0056 264/.7);--success-a10:oklch(63.7% .175 147/.1);--success-a15:oklch(63.7% .175 147/.15);--success-a30:oklch(63.7% .175 147/.3);--success-a40:oklch(63.7% .175 147/.4);--success-a80:oklch(63.7% .175 147/.8);--warning-a10:oklch(80% .12 80/.1);--warning-a15:oklch(80% .12 80/.15);--warning-a30:oklch(80% .12 80/.3);--warning-a40:oklch(80% .12 80/.4);--info-a10:oklch(70% .1 250/.1);--info-a15:oklch(70% .1 250/.15);--info-a30:oklch(70% .1 250/.3);--info-a40:oklch(70% .1 250/.4);--info-a80:oklch(70% .1 250/.8);--border-a60:oklch(26.27% .0051 264/.6);--border-a80:oklch(26.27% .0051 264/.8);--input-a30:oklch(26.27% .0051 264/.3);--label-surface-a50:oklch(19.5% .004 264/.5);--overlay-base-a50:oklch(0% 0 0/.5);--overlay-base-a60:oklch(0% 0 0/.6);--background-a60:oklch(15.9% .005 264/.6);--ring-a50:oklch(56.7% .158 275/.5)}:where(html,.sidebar-nav,#maincontent,.cmdk-list,.cbi-dropdown>ul.dropdown,#modal_overlay,#modal_overlay>.modal>ul,#modal_overlay>.modal pre,.cbi-tabmenu,.tabs,textarea){scrollbar-color:var(--foreground-a30) transparent;scrollbar-width:thin}:where(html,.sidebar-nav,#maincontent,.cmdk-list,.cbi-dropdown>ul.dropdown,#modal_overlay,#modal_overlay>.modal>ul,#modal_overlay>.modal pre,.cbi-tabmenu,.tabs,textarea)::-webkit-scrollbar{width:8px;height:8px}:where(html,.sidebar-nav,#maincontent,.cmdk-list,.cbi-dropdown>ul.dropdown,#modal_overlay,#modal_overlay>.modal>ul,#modal_overlay>.modal pre,.cbi-tabmenu,.tabs,textarea)::-webkit-scrollbar-track{background-color:#0000}:where(html,.sidebar-nav,#maincontent,.cmdk-list,.cbi-dropdown>ul.dropdown,#modal_overlay,#modal_overlay>.modal>ul,#modal_overlay>.modal pre,.cbi-tabmenu,.tabs,textarea)::-webkit-scrollbar-corner{background-color:#0000}:where(html,.sidebar-nav,#maincontent,.cmdk-list,.cbi-dropdown>ul.dropdown,#modal_overlay,#modal_overlay>.modal>ul,#modal_overlay>.modal pre,.cbi-tabmenu,.tabs,textarea)::-webkit-scrollbar-thumb{background-color:var(--foreground-a30);border-radius:9999px}html{background-color:var(--background);font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground)}html body{flex-wrap:wrap;height:100dvh;display:flex}html body.modal-overlay-active{overflow:hidden}h1,h2,h3,h4,h5,h6{font-family:var(--font-sans);--tw-leading:var(--leading-tight);line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--foreground)}h1{margin-bottom:calc(var(--spacing) * 4);font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}h2{margin-bottom:calc(var(--spacing) * 3);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}h3{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}h4{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}h5{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}h6{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}hr{margin-block:calc(var(--spacing) * 4);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border)}strong{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}abbr{cursor:help}p{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--foreground)}pre{border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--muted);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-family:var(--font-mono);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);overflow-wrap:break-word;white-space:pre-wrap;color:var(--muted-foreground);display:block}code{border-radius:calc(var(--radius-base) * .5);background-color:var(--muted);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5);font-family:var(--font-mono);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted-foreground)}a{color:var(--primary);text-decoration-line:none}@media (hover:hover){a:hover{text-decoration-line:underline}}var{font-family:var(--font-mono);color:var(--primary)}small{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--muted-foreground)}em{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted-foreground);font-style:italic}#sidebar{background-color:var(--sidebar-bg);flex-direction:column;flex-shrink:0;width:220px;height:100%;display:flex;overflow:hidden}@media not all and (width>=48rem){#sidebar{top:calc(var(--spacing) * 0);left:calc(var(--spacing) * 0);z-index:40;--tw-translate-x:-100%;width:240px;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);height:100dvh;transition:translate .2s;position:fixed}#sidebar[data-drawer-open=true]{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}}#sidebar .icon-expand{display:none}@media (width>=48rem){html[data-sidebar-collapsed=true] #sidebar{width:calc(var(--spacing) * 12)}html[data-sidebar-collapsed=true] #sidebar .sidebar-brand-text,html[data-sidebar-collapsed=true] #sidebar .sidebar-label,html[data-sidebar-collapsed=true] #sidebar .sidebar-chevron,html[data-sidebar-collapsed=true] #sidebar .icon-collapse{display:none}html[data-sidebar-collapsed=true] #sidebar .icon-expand{display:block}html[data-sidebar-collapsed=true] #sidebar .sidebar-accordion-sub{display:none!important}}.sidebar-header{height:calc(var(--spacing) * 14);padding-inline:calc(var(--spacing) * 3);flex-shrink:0;justify-content:space-between;align-items:center;display:flex}.sidebar-brand{min-width:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * 2);text-overflow:ellipsis;white-space:nowrap;color:var(--sidebar-foreground);flex:1;text-decoration-line:none;display:flex;overflow:hidden}.sidebar-logo{flex-shrink:0}.sidebar-brand-text{text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);overflow:hidden}.sidebar-toggle{height:calc(var(--spacing) * 7);width:calc(var(--spacing) * 7);cursor:pointer;border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:var(--sidebar-muted);background-color:#0000;border-width:0;flex-shrink:0;justify-content:center;align-items:center;transition:color .15s,background-color .15s,border-color .15s;display:flex}.sidebar-toggle:hover{background-color:var(--sidebar-accent);color:var(--sidebar-accent-fg)}.sidebar-toggle svg,.sidebar-toggle img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4)}.sidebar-nav{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 2);flex:1;overflow:hidden auto}.sidebar-nav-item,.sidebar-accordion-item{margin-bottom:calc(var(--spacing) * .5)}:is(.sidebar-nav-item,.sidebar-accordion-item).active>.sidebar-nav-parent{color:var(--sidebar-active-fg)}.sidebar-nav-parent{cursor:pointer;appearance:none;justify-content:flex-start;align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);width:100%;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--sidebar-muted);background-color:#0000;border-width:0;transition:color .15s,background-color .15s,border-color .15s;display:flex}.sidebar-nav-parent:hover{background-color:var(--sidebar-accent);color:var(--sidebar-accent-fg)}.sidebar-nav-parent .sidebar-icon{flex-shrink:0;justify-content:center;align-items:center;display:flex}.sidebar-nav-parent .sidebar-icon svg,.sidebar-nav-parent .sidebar-icon img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4)}.sidebar-nav-parent .sidebar-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.sidebar-nav-parent .sidebar-chevron{flex-shrink:0;margin-left:auto;transition:rotate .2s}.sidebar-nav-parent .sidebar-chevron svg,.sidebar-nav-parent .sidebar-chevron img{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5)}.sidebar-accordion-item[data-open=true]>.sidebar-nav-parent{background-color:var(--sidebar-active-bg);color:var(--sidebar-active-fg)}.sidebar-accordion-item[data-open=true] .sidebar-chevron{rotate:180deg}.sidebar-accordion-sub{visibility:hidden;opacity:0;transition:grid-template-rows .2s var(--ease-out),opacity .2s var(--ease-out),visibility .2s var(--ease-out);grid-template-rows:0fr;display:grid;overflow:hidden}.sidebar-accordion-sub>*{min-height:calc(var(--spacing) * 0);align-self:flex-start}.sidebar-accordion-item[data-open=true]>.sidebar-accordion-sub{visibility:visible;opacity:1;grid-template-rows:1fr}.sidebar-sub-list{margin-left:calc(var(--spacing) * 4);border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--sidebar-border);padding-left:calc(var(--spacing) * 2);list-style-type:none}.sidebar-sub-item{margin-bottom:calc(var(--spacing) * .5)}.sidebar-sub-item.active .sidebar-sub-link{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--sidebar-active-fg)}.sidebar-sub-link{border-radius:calc(var(--radius-base) * .75);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--sidebar-muted);text-decoration-line:none;transition:color .15s,background-color .15s,border-color .15s;display:block}.sidebar-sub-link:hover{background-color:var(--sidebar-accent);color:var(--sidebar-accent-fg)}.sidebar-collapsed-popover{z-index:50;min-width:calc(var(--spacing) * 44);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--panel-bg);padding-block:calc(var(--spacing) * 1);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);position:fixed;overflow:hidden}.sidebar-collapsed-popover-title{padding-inline:calc(var(--spacing) * 3);padding-top:calc(var(--spacing) * 1.5);padding-bottom:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground)}.sidebar-collapsed-popover-list{margin:calc(var(--spacing) * 0);padding:calc(var(--spacing) * 0);list-style-type:none}.sidebar-collapsed-popover-link{margin-inline:calc(var(--spacing) * 1);border-radius:calc(var(--radius-base) * .5);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--sidebar-muted);text-decoration-line:none;transition:color .15s,background-color .15s,border-color .15s;display:block}.sidebar-collapsed-popover-link:hover{background-color:var(--sidebar-accent);color:var(--sidebar-accent-fg)}.sidebar-collapsed-popover-link.active{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--sidebar-active-fg)}.sidebar-footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--sidebar-border);padding:calc(var(--spacing) * 2);padding-bottom:max(.5rem, env(safe-area-inset-bottom));flex-shrink:0}.sidebar-logout{align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:calc(var(--radius-base) * .75);width:100%;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--sidebar-muted);text-decoration-line:none;transition:color .15s,background-color .15s,border-color .15s;display:flex}.sidebar-logout:hover{background-color:var(--sidebar-accent);color:var(--sidebar-accent-fg)}.sidebar-logout.active{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--sidebar-active-fg)}.sidebar-logout .sidebar-icon{flex-shrink:0;justify-content:center;align-items:center;display:flex}.sidebar-logout .sidebar-icon svg,.sidebar-logout .sidebar-icon img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4)}.sidebar-logout .sidebar-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}#main-wrapper{background-color:var(--background);height:100%;padding-block:calc(var(--spacing) * 2);padding-right:calc(var(--spacing) * 2);padding-left:calc(var(--spacing) * 0);flex-direction:column;flex:1;display:flex;overflow:hidden}@media not all and (width>=48rem){#main-wrapper{padding:calc(var(--spacing) * 0);flex-basis:100%}}.content-card{border-radius:calc(var(--radius-base) * 1.5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--panel-border);background-color:var(--panel-bg);--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);flex-direction:column;flex:1;display:flex;overflow:hidden}@media not all and (width>=48rem){.content-card{--tw-border-style:none;--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);border-style:none;border-radius:0}}@media (width>=48rem){html[data-sidebar-anim] #sidebar{transition:width .2s var(--ease-out)}}.card-header{height:calc(var(--spacing) * 12);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--panel-border);background-color:var(--panel-bg);padding-inline:calc(var(--spacing) * 4);flex-shrink:0;justify-content:space-between;align-items:center;display:flex}.card-header-left,.card-header-right{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.topbar-hamburger{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);cursor:pointer;border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:var(--muted-foreground);background-color:#0000;border-width:0;justify-content:center;align-items:center;transition:color .15s,background-color .15s,border-color .15s;display:none}@media not all and (width>=48rem){.topbar-hamburger{display:flex}}.topbar-hamburger:hover{background-color:var(--muted)}.topbar-hamburger svg,.topbar-hamburger img{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5)}.topbar-breadcrumb{align-items:center;gap:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));display:flex}.topbar-breadcrumb .breadcrumb-item{color:var(--muted-foreground)}.topbar-breadcrumb .breadcrumb-item.active{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground)}.topbar-breadcrumb .breadcrumb-sep{color:var(--muted-foreground)}.topbar-breadcrumb .breadcrumb-sep svg,.topbar-breadcrumb .breadcrumb-sep img{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5)}button.topbar-icon-btn{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);cursor:pointer;border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:var(--muted-foreground);background-color:#0000;border-width:0;flex-shrink:0;justify-content:center;align-items:center;transition:color .15s,background-color .15s,border-color .15s;display:inline-flex}button.topbar-icon-btn:hover{background-color:var(--muted);color:var(--foreground)}button.topbar-icon-btn img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);flex-shrink:0}.topbar-indicators{align-items:center;display:flex}.topbar-indicators span[data-indicator]>img{display:none}.topbar-indicators span[data-indicator]{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);cursor:pointer;border-radius:calc(var(--radius-base) * .75);color:var(--muted-foreground);flex-shrink:0;justify-content:center;align-items:center;font-size:0;transition:color .15s,background-color .15s,border-color .15s,width .15s ease-out,opacity .15s ease-out,margin-left .15s ease-out;display:inline-flex;position:relative}@starting-style{.topbar-indicators span[data-indicator]{width:calc(var(--spacing) * 0)}}@starting-style{.topbar-indicators span[data-indicator]{opacity:0}}.topbar-indicators span[data-indicator]:before{content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);background-color:currentColor;flex-shrink:0;display:block;position:relative;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.topbar-indicators span[data-indicator]+span[data-indicator]{margin-left:calc(var(--spacing) * 2)}@starting-style{.topbar-indicators span[data-indicator]+span[data-indicator]{margin-left:calc(var(--spacing) * 0)}}.topbar-indicators span[data-indicator]:hover{background-color:var(--muted);color:var(--foreground)}.topbar-indicators span[data-indicator][data-indicator=poll-status][data-style=active]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-refresh-cw'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M3%2012a9%209%200%200%201%209-9%209.75%209.75%200%200%201%206.74%202.74L21%208'/%3e%3cpath%20d='M21%203v5h-5'/%3e%3cpath%20d='M21%2012a9%209%200%200%201-9%209%209.75%209.75%200%200%201-6.74-2.74L3%2016'/%3e%3cpath%20d='M8%2016H3v5'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-refresh-cw'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M3%2012a9%209%200%200%201%209-9%209.75%209.75%200%200%201%206.74%202.74L21%208'/%3e%3cpath%20d='M21%203v5h-5'/%3e%3cpath%20d='M21%2012a9%209%200%200%201-9%209%209.75%209.75%200%200%201-6.74-2.74L3%2016'/%3e%3cpath%20d='M8%2016H3v5'/%3e%3c/svg%3e")}.topbar-indicators span[data-indicator][data-indicator=poll-status][data-style=inactive]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-refresh-cw-off'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M21%208L18.74%205.74A9.75%209.75%200%200%200%2012%203C11%203%2010.03%203.16%209.13%203.47'/%3e%3cpath%20d='M8%2016H3v5'/%3e%3cpath%20d='M3%2012C3%209.51%204%207.26%205.64%205.64'/%3e%3cpath%20d='m3%2016%202.26%202.26A9.75%209.75%200%200%200%2012%2021c2.49%200%204.74-1%206.36-2.64'/%3e%3cpath%20d='M21%2012c0%201-.16%201.97-.47%202.87'/%3e%3cpath%20d='M21%203v5h-5'/%3e%3cpath%20d='M22%2022%202%202'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-refresh-cw-off'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M21%208L18.74%205.74A9.75%209.75%200%200%200%2012%203C11%203%2010.03%203.16%209.13%203.47'/%3e%3cpath%20d='M8%2016H3v5'/%3e%3cpath%20d='M3%2012C3%209.51%204%207.26%205.64%205.64'/%3e%3cpath%20d='m3%2016%202.26%202.26A9.75%209.75%200%200%200%2012%2021c2.49%200%204.74-1%206.36-2.64'/%3e%3cpath%20d='M21%2012c0%201-.16%201.97-.47%202.87'/%3e%3cpath%20d='M21%203v5h-5'/%3e%3cpath%20d='M22%2022%202%202'/%3e%3c/svg%3e")}.topbar-indicators span[data-indicator][data-indicator=media_error]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-alert-circle'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3ccircle%20cx='12'%20cy='12'%20r='10'/%3e%3cline%20x1='12'%20x2='12'%20y1='8'%20y2='12'/%3e%3cline%20x1='12'%20x2='12.01'%20y1='16'%20y2='16'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-alert-circle'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3ccircle%20cx='12'%20cy='12'%20r='10'/%3e%3cline%20x1='12'%20x2='12'%20y1='8'%20y2='12'/%3e%3cline%20x1='12'%20x2='12.01'%20y1='16'%20y2='16'/%3e%3c/svg%3e")}.topbar-indicators span[data-indicator][data-indicator=uci-changes]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-settings'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='M9.671%204.136a2.34%202.34%200%200%201%204.659%200%202.34%202.34%200%200%200%203.319%201.915%202.34%202.34%200%200%201%202.33%204.033%202.34%202.34%200%200%200%200%203.831%202.34%202.34%200%200%201-2.33%204.033%202.34%202.34%200%200%200-3.319%201.915%202.34%202.34%200%200%201-4.659%200%202.34%202.34%200%200%200-3.32-1.915%202.34%202.34%200%200%201-2.33-4.033%202.34%202.34%200%200%200%200-3.831A2.34%202.34%200%200%201%206.35%206.051a2.34%202.34%200%200%200%203.319-1.915'%20/%3e%3ccircle%20cx='12'%20cy='12'%20r='3'%20/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-settings'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='M9.671%204.136a2.34%202.34%200%200%201%204.659%200%202.34%202.34%200%200%200%203.319%201.915%202.34%202.34%200%200%201%202.33%204.033%202.34%202.34%200%200%200%200%203.831%202.34%202.34%200%200%201-2.33%204.033%202.34%202.34%200%200%200-3.319%201.915%202.34%202.34%200%200%201-4.659%200%202.34%202.34%200%200%200-3.32-1.915%202.34%202.34%200%200%201-2.33-4.033%202.34%202.34%200%200%200%200-3.831A2.34%202.34%200%200%201%206.35%206.051a2.34%202.34%200%200%200%203.319-1.915'%20/%3e%3ccircle%20cx='12'%20cy='12'%20r='3'%20/%3e%3c/svg%3e")}.topbar-indicators span[data-indicator][data-indicator=uci-changes][data-count]:not([data-count="0"]):after{content:var(--tw-content);content:var(--tw-content);top:calc(var(--spacing) * -.5);content:var(--tw-content);right:calc(var(--spacing) * -.5);content:var(--tw-content);z-index:10;content:var(--tw-content);content:var(--tw-content);min-height:calc(var(--spacing) * 3);content:var(--tw-content);min-width:calc(var(--spacing) * 3);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);background-color:var(--primary);content:var(--tw-content);padding-inline:calc(var(--spacing) * .5);content:var(--tw-content);content:var(--tw-content);--tw-leading:1;content:var(--tw-content);--tw-font-weight:var(--font-weight-bold);font-size:8px;line-height:1;font-weight:var(--font-weight-bold);content:var(--tw-content);color:var(--primary-foreground);--tw-content:attr(data-count);content:var(--tw-content);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex;position:absolute}#maincontent{scrollbar-gutter:stable;padding:calc(var(--spacing) * 6);flex:1;overflow-y:auto}@media not all and (width>=48rem){#maincontent{padding:calc(var(--spacing) * 4)}}#maincontent h1{margin-bottom:calc(var(--spacing) * 4);border-style:var(--tw-border-style);padding-left:calc(var(--spacing) * 0);font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);border-width:0}#maincontent h2{margin-bottom:calc(var(--spacing) * 3);border-style:var(--tw-border-style);padding-left:calc(var(--spacing) * 0);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);border-width:0}#maincontent h3{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}#maincontent .cbi-map-descr,#maincontent .cbi-section-descr{margin-bottom:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--muted-foreground)}#maincontent .cbi-page-actions{margin-top:calc(var(--spacing) * 4);justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 2);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding-top:calc(var(--spacing) * 4);flex-wrap:wrap;display:flex}#maincontent div[style]>svg:where([data-darkmode=true],[data-darkmode=true] *){background-color:var(--surface)!important}#maincontent div[style]>svg line[style]:where([data-darkmode=true],[data-darkmode=true] *){stroke:var(--text)!important}#maincontent .zone-forwards{align-items:stretch;gap:calc(var(--spacing) * 2);--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);flex-wrap:nowrap;display:flex}#maincontent .zone-forwards>span{flex-shrink:0;flex-basis:calc(var(--spacing) * 8);color:var(--muted-foreground);justify-content:center;align-self:center;align-items:center;display:flex}#maincontent .zone-forwards .zone-src,#maincontent .zone-forwards .zone-dest{gap:calc(var(--spacing) * 1.5);flex-direction:column;flex:1;display:flex}#maincontent .controls{align-items:center;gap:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex}#maincontent .controls label input[type=radio],#maincontent .controls label input[type=checkbox]{margin-right:calc(var(--spacing) * 1);vertical-align:middle}#maincontent .cbi-title-section{margin-bottom:calc(var(--spacing) * 4);--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}#maincontent .control-group{margin-top:calc(var(--spacing) * 1)}#maincontent .control-group,#maincontent .cbi-page-actions>div,#maincontent .cbi-section-actions>div{align-items:center;gap:calc(var(--spacing) * 2);flex-flow:row;display:flex}#sidebar-overlay{pointer-events:none;inset:calc(var(--spacing) * 0);z-index:30;background-color:var(--overlay-base-a50);opacity:0;transition:opacity .2s;position:absolute}@media (width>=48rem){#sidebar-overlay{display:none}}#sidebar-overlay.active{pointer-events:auto;opacity:1}button,input[type=button],input[type=submit],input[type=reset],.btn,.cbi-button{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-width:1px;transition:color .2s,background-color .2s,border-color .2s;display:inline-flex}:is(button,input[type=button],input[type=submit],input[type=reset],.btn,.cbi-button)[disabled]{cursor:not-allowed;opacity:.4}.td :is(button,input[type=button],input[type=submit],input[type=reset],.btn,.cbi-button){white-space:nowrap}.btn,.cbi-button,.cbi-button-default,.cbi-button-download,.cbi-button-find,.cbi-button-link,.cbi-button-up,.cbi-button-down{border-color:var(--secondary);background-color:var(--secondary);color:var(--secondary-foreground)}@media (hover:hover){:is(.btn,.cbi-button,.cbi-button-default,.cbi-button-download,.cbi-button-find,.cbi-button-link,.cbi-button-up,.cbi-button-down):hover{background-color:var(--secondary-a80)}}.drag-handle{border-color:var(--muted);background-color:var(--muted);color:var(--muted-foreground)}@media (hover:hover){.drag-handle:hover{background-color:var(--muted-a80)}}.btn.primary,.cbi-button-action,.cbi-button-apply,.cbi-button-reload,.cbi-button-add,.cbi-button-edit{border-color:var(--primary);background-color:var(--primary);color:var(--primary-foreground)}@media (hover:hover){:is(.btn.primary,.cbi-button-action,.cbi-button-apply,.cbi-button-reload,.cbi-button-add,.cbi-button-edit):hover{background-color:var(--primary-a90)}}.cbi-button-positive,.cbi-button-fieldadd,.cbi-button-save{border-color:var(--primary-a25);background-color:var(--primary-a15);color:var(--primary)}@media (hover:hover){:is(.cbi-button-positive,.cbi-button-fieldadd,.cbi-button-save):hover{background-color:var(--primary-a25)}}.cbi-button-negative,.cbi-section-remove .cbi-button,.cbi-button-reset,.cbi-button-remove{border-color:var(--destructive);background-color:var(--destructive);color:var(--destructive-fg)}@media (hover:hover){:is(.cbi-button-negative,.cbi-section-remove .cbi-button,.cbi-button-reset,.cbi-button-remove):hover{background-color:var(--destructive-a90)}}input:disabled{cursor:not-allowed;opacity:.4}input[type=text],input[type=password],input[type=number],input[type=email],.cbi-input-text,.cbi-input{width:calc(var(--spacing) * 52.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);background-color:var(--background);max-width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition:color .2s,background-color .2s,border-color .2s;position:relative}:is(input[type=text],input[type=password],input[type=number],input[type=email],.cbi-input-text,.cbi-input)::placeholder{color:var(--muted-foreground-a70)}:is(input[type=text],input[type=password],input[type=number],input[type=email],.cbi-input-text,.cbi-input):focus{border-color:var(--primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring);--tw-outline-style:none;outline-style:none}.table.cbi-section-table :is(input[type=text],input[type=password],input[type=number],input[type=email],.cbi-input-text,.cbi-input){width:100%}.cbi-input-text{min-width:calc(var(--spacing) * 20)}#localtime{min-width:calc(var(--spacing) * 64)}textarea{min-height:calc(var(--spacing) * 24);resize:vertical;border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);background-color:var(--background);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);transition:color .2s,background-color .2s,border-color .2s}textarea::placeholder{color:var(--muted-foreground-a70)}textarea:focus{border-color:var(--primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring);--tw-outline-style:none;outline-style:none}textarea[disabled]{cursor:not-allowed;opacity:.4}input[type=radio],input[type=checkbox]{margin-right:calc(var(--spacing) * 3);height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);cursor:pointer;appearance:none;vertical-align:middle;display:inline-block;position:relative}:is(input[type=radio],input[type=checkbox]):disabled{cursor:not-allowed}:is(input[type=radio],input[type=checkbox]):before{content:var(--tw-content);content:var(--tw-content);inset:calc(var(--spacing) * 0);content:var(--tw-content);border-radius:calc(var(--radius-base) * .5);content:var(--tw-content);border-style:var(--tw-border-style);content:var(--tw-content);border-width:1px;border-color:var(--input);content:var(--tw-content);background-color:var(--background);content:var(--tw-content);transition:background-color .2s,border-color .2s;position:absolute}:is(input[type=radio],input[type=checkbox]):after{content:var(--tw-content);content:var(--tw-content);inset:calc(var(--spacing) * 0);content:var(--tw-content);background-color:var(--primary-foreground);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);opacity:0;content:var(--tw-content);transition:opacity .2s;position:absolute;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}:is(input[type=radio],input[type=checkbox]):checked:before{content:var(--tw-content);border-color:var(--primary);content:var(--tw-content);background-color:var(--primary)}:is(input[type=radio],input[type=checkbox]):checked:after{content:var(--tw-content);opacity:1}:is(input[type=radio],input[type=checkbox]):focus:before{content:var(--tw-content);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);content:var(--tw-content);--tw-ring-color:var(--ring);content:var(--tw-content);--tw-outline-style:none;outline-style:none}input[type=radio]:before{content:var(--tw-content);border-radius:3.40282e38px}input[type=radio]:after{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='currentColor'%3e%3ccircle%20cx='12'%20cy='12'%20r='6'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='currentColor'%3e%3ccircle%20cx='12'%20cy='12'%20r='6'/%3e%3c/svg%3e")}input[type=checkbox]:after{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-check'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M20%206%209%2017l-5-5'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-check'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M20%206%209%2017l-5-5'/%3e%3c/svg%3e")}.cbi-checkbox{vertical-align:middle;align-items:center;display:inline-flex;position:relative}select{max-width:100%;min-width:calc(var(--spacing) * 52.5);appearance:none;border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);background-color:var(--background);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);padding-right:calc(var(--spacing) * 9);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);transition:color .2s,background-color .2s,border-color .2s}select:focus{border-color:var(--primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring);--tw-outline-style:none;outline-style:none}select:disabled{cursor:not-allowed;opacity:.4}@media (width>=48rem){select{max-width:calc(var(--spacing) * 100)}}select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238888a0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-position:right .6rem center;background-repeat:no-repeat;background-size:16px}[data-darkmode=true] select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238c8ca8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")}.cbi-dropdown{min-width:fit-content;max-width:100%;height:fit-content;padding:calc(var(--spacing) * 0);white-space:nowrap;flex-shrink:0;align-items:center;transition:color .2s,background-color .2s,border-color .2s;display:inline-flex;position:relative}.cbi-dropdown[disabled]{pointer-events:none;opacity:.4}.cbi-dropdown:not(.btn):not(.cbi-button){max-width:100%;min-width:calc(var(--spacing) * 52.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);background-color:var(--background)}@media (width>=48rem){.cbi-dropdown:not(.btn):not(.cbi-button){max-width:calc(var(--spacing) * 100)}}.cbi-dropdown:not(.btn):not(.cbi-button)>ul>li[placeholder]{display:none}.cbi-dropdown>ul{margin:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * .5);opacity:.99;flex-wrap:nowrap;flex:1;list-style-type:none;transition:opacity .12s ease-out;display:flex;overflow:hidden}.cbi-dropdown>ul>li{margin:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * 1);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);list-style-type:none;display:none;overflow:hidden}.cbi-dropdown>ul>li[display]{display:flex!important}.cbi-dropdown>ul>li .hide-open{display:block}.cbi-dropdown>ul>li .hide-close{display:none}.cbi-dropdown>ul>li>form{pointer-events:none;display:none}.cbi-dropdown>ul>li>label{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.cbi-dropdown>ul>li img{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);vertical-align:middle;flex:none}.cbi-dropdown>ul>li span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.cbi-dropdown>ul.dropdown{left:calc(var(--spacing) * 0);z-index:50;border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--panel-bg);opacity:1;--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);width:fit-content;min-width:100%;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);position:absolute;overflow-y:auto}.cbi-dropdown>ul.dropdown>li{min-height:calc(var(--spacing) * 9);cursor:pointer;width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted-foreground)}@media (hover:hover){.cbi-dropdown>ul.dropdown>li:hover{background-color:var(--muted)}}.cbi-dropdown>ul.preview{display:none}.cbi-dropdown[empty]>ul{max-width:1px;max-height:1px}.cbi-dropdown[empty]>ul>li,.cbi-dropdown[optional][open]>ul.dropdown>li[placeholder]{display:block}.cbi-dropdown[open]{border-color:var(--primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring)}.cbi-dropdown[open]>ul.dropdown{width:auto;max-width:none;display:block}.cbi-dropdown[open]>ul.dropdown>li{display:flex}.cbi-dropdown[open]>ul.dropdown>li[selected]{background-color:var(--primary-a15);color:var(--primary)}.cbi-dropdown[open]>ul.dropdown>li[unselectable]{opacity:.4}.cbi-dropdown[open]>ul.dropdown>li .hide-open{display:none}.cbi-dropdown[open]>ul.dropdown>li .hide-close{display:block}.cbi-dropdown[open]>ul.preview{display:flex}:is(.cbi-dropdown[multiple][more],.cbi-dropdown[multiple][empty])>.more{padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1.5);flex:none;display:flex}.cbi-dropdown[multiple][empty]>.more{flex:1}.cbi-dropdown[multiple][open]>ul.dropdown>li>form{align-items:center;display:flex}.cbi-dropdown[multiple]>ul>li>label{display:flex}.cbi-dropdown>.open{cursor:pointer;border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--input);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * .5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:currentColor;flex:none;justify-content:center;align-items:center;display:flex}.cbi-dropdown>.more{display:none}.cbi-tooltip{z-index:50;width:max-content;max-width:var(--container-xs);--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border-a60);background-color:var(--panel-bg);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));overflow-wrap:normal;word-break:normal;white-space:normal;color:var(--foreground);opacity:0;--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition:visibility .2s var(--ease-out),opacity .2s var(--ease-out),scale .2s var(--ease-out);position:absolute;inset-inline-start:-10000px}.cbi-tooltip.error{border-color:var(--destructive-a40);background-color:var(--destructive);color:var(--destructive-fg)}.cbi-tooltip.success{border-color:var(--success-a40);background-color:var(--success);color:var(--success-fg)}.cbi-tooltip.info{border-color:var(--info-a40);background-color:var(--info);color:var(--info-fg)}.cbi-tooltip.notice{border-color:var(--warning-a40);background-color:var(--warning);color:var(--warning-fg)}.cbi-tooltip-container,[data-tooltip]{cursor:help;white-space:nowrap;text-underline-offset:2px;text-decoration-line:underline;text-decoration-style:dotted;text-decoration-color:currentColor;display:inline-block;position:relative}:is(.cbi-tooltip-container,[data-tooltip]) .cbi-tooltip{visibility:hidden;--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:0;transition:visibility .2s var(--ease-out),opacity .2s var(--ease-out),scale .2s var(--ease-out)}:is(.cbi-tooltip-container,[data-tooltip]):hover .cbi-tooltip{visibility:visible;--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:1;inset-inline-start:auto}.alert-message{margin-bottom:calc(var(--spacing) * 4);border-radius:calc(var(--radius-base) * 1.5);border-style:var(--tw-border-style);background-color:var(--muted);padding:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);border-width:1px}.alert-message.success{border-color:var(--success-a30);background-color:var(--success-a10);color:var(--success)}.alert-message.info{border-color:var(--info-a30);background-color:var(--info-a10);color:var(--info)}.alert-message.warning,.alert-message.notice{border-color:var(--warning-a30);background-color:var(--warning-a10);color:var(--warning-soft-fg)}.alert-message.error,.alert-message.danger{border-color:var(--destructive-a30);background-color:var(--destructive-a10);color:var(--destructive)}.alert-message h4,.alert-message h5,.alert-message pre,.alert-message ul,.alert-message li,.alert-message p{border-style:var(--tw-border-style);color:inherit;background-color:#0000;border-width:0}.alert-message h4{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.alert-message p{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.alert-message .right{margin-top:calc(var(--spacing) * 3)}.cbi-progressbar{height:calc(var(--spacing) * 2.5);cursor:help;background-color:var(--secondary);border-radius:3.40282e38px;width:100%;position:relative;overflow:hidden}.cbi-progressbar:before{content:var(--tw-content);content:var(--tw-content);inset-inline:calc(var(--spacing) * 0);content:var(--tw-content);content:var(--tw-content);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y);content:var(--tw-content);text-align:center;content:var(--tw-content);content:var(--tw-content);--tw-leading:1;content:var(--tw-content);white-space:nowrap;content:var(--tw-content);color:var(--foreground);--tw-content:attr(title);content:var(--tw-content);font-size:10px;line-height:1;position:absolute;top:50%}.cbi-progressbar>div{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops));--tw-gradient-from:var(--progress-bar-start);--tw-gradient-to:var(--progress-bar-end);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));height:100%;transition:width .3s}.label{background-color:var(--muted);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--muted-foreground);border-radius:3.40282e38px;align-items:center;display:inline-flex}.label.important{background-color:var(--info-a15);color:var(--info)}.label.warning{background-color:var(--warning-a15);color:var(--warning-soft-fg)}.label.success{background-color:var(--success-a15);color:var(--success)}.label.notice{background-color:var(--warning-a15);color:var(--warning-soft-fg)}.zonebadge{align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);border-width:1px;border-color:var(--secondary);background-color:var(--secondary);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1);vertical-align:middle;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-normal);line-height:var(--leading-normal);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--secondary-foreground);transition:color .2s,background-color .2s,border-color .2s;display:inline-flex;overflow:visible}.zonebadge[style*=--zone-color-rgb]{border-color:#0000;background-color:rgb(var(--zone-color-rgb),.6)!important}.zonebadge.zonebadge-empty{--tw-border-style:dashed;border-style:dashed;border-color:var(--border);--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--muted-foreground);background-color:#0000;font-style:italic}.ifacebadge>.zonebadge{padding-inline:calc(var(--spacing) * 1);padding-block:calc(var(--spacing) * .5);border-radius:3.40282e38px}.cbi-dropdown .zonebadge{border-radius:calc(var(--radius-base) * .75);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5)}.ifacebadge{cursor:default;align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--label-surface);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--foreground);flex-wrap:nowrap;transition:color .2s,background-color .2s,border-color .2s;display:inline-flex}.zonebadge>.ifacebadge{border-radius:calc(var(--radius-base) * .75);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * 0)}.zonebadge>.ifacebadge img{margin-right:calc(var(--spacing) * 1);width:calc(var(--spacing) * 4)}.cbi-dropdown .ifacebadge{gap:calc(var(--spacing) * 1);border-radius:calc(var(--radius-base) * .75);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5)}.cbi-dropdown .ifacebadge img{margin-right:calc(var(--spacing) * 1);width:calc(var(--spacing) * 4)}.ifacebadge>img,.ifacebadge>.cbi-tooltip-container>img{width:calc(var(--spacing) * 5);flex-shrink:0;align-self:center}.ifacebadge>span .nowrap{margin-bottom:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));display:inline-block}.ifacebadge.ifacebadge-active{border-color:var(--primary);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.ifacebadge.large{min-width:calc(var(--spacing) * 48);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);flex:1}.cbi-section{margin-bottom:calc(var(--spacing) * 4);border-radius:calc(var(--radius-base) * 1.5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--panel-border);background-color:var(--panel-bg);padding:calc(var(--spacing) * 4);overflow:visible}.cbi-section[data-tab-title]{margin:calc(var(--spacing) * 0);padding:calc(var(--spacing) * 0)}.cbi-section[data-tab-active=true]{margin-bottom:calc(var(--spacing) * 4);padding:calc(var(--spacing) * 4)}.cbi-section>.cbi-title{justify-content:space-between;align-items:flex-start;display:flex}.cbi-section>.cbi-title>h3{flex:1}.cbi-section>.cbi-title>h3 .label[data-clickable]{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6);cursor:pointer;border-radius:calc(var(--radius-base) * .5);background-color:var(--label-surface);padding:calc(var(--spacing) * 0);color:var(--foreground);justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.cbi-section>.cbi-title>h3 .label[data-clickable]:hover{background-color:var(--muted)}}.cbi-section>.cbi-title>h3 .label[data-clickable]:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.cbi-section>.cbi-title>h3 span[data-indicator=poll-status]{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);cursor:pointer;font-size:0;position:relative}.cbi-section>.cbi-title>h3 span[data-indicator=poll-status]:before{content:var(--tw-content);content:var(--tw-content);width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);background-color:currentColor;position:absolute;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.cbi-section>.cbi-title>h3 span[data-indicator=poll-status][data-style=active]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-chevron-down'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='m6%209%206%206%206-6'%20/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-chevron-down'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='m6%209%206%206%206-6'%20/%3e%3c/svg%3e")}.cbi-section>.cbi-title>h3 span[data-indicator=poll-status][data-style=inactive]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-chevron-up'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='m18%2015-6-6-6%206'%20/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-chevron-up'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='m18%2015-6-6-6%206'%20/%3e%3c/svg%3e")}.cbi-section legend{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.cbi-section h3{margin-bottom:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-bottom:calc(var(--spacing) * 3);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground)}.cbi-section .cbi-section-create{margin-top:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.cbi-section .cbi-section-create>div{flex:none}.cbi-section .network-status-table{margin-bottom:calc(var(--spacing) * 3);justify-content:space-around;gap:calc(var(--spacing) * 4);flex-wrap:wrap;display:flex}@media (width>=48rem){.cbi-section .network-status-table{flex-wrap:nowrap}}.cbi-section div[style*=display\:grid]:has(>.ifacebox){place-items:center}.ifacebox{min-width:calc(var(--spacing) * 32);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--panel-bg);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);flex-direction:column;align-items:stretch;display:inline-flex;position:relative;overflow:visible}@media (width>=48rem){#cbi-network-interface .ifacebox{min-width:calc(var(--spacing) * 38)}}.ifacebox .ifacebox-head{border-top-left-radius:var(--radius-base);border-top-right-radius:var(--radius-base);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);background-color:var(--label-surface);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground);transition:color .2s,background-color .2s,border-color .2s}.ifacebox .ifacebox-head[style*=--zone-color-rgb]{background-color:rgb(var(--zone-color-rgb),.65)!important}.ifacebox .ifacebox-head.cbi-tooltip-container{background-color:#0000;position:static}.ifacebox .ifacebox-head.cbi-tooltip-container .cbi-tooltip{bottom:calc(var(--spacing) * 0);left:calc(var(--spacing) * 0)}.ifacebox .ifacebox-head.active{border-color:var(--primary);background-color:var(--primary);color:var(--primary-foreground)}.ifacebox .ifacebox-body{justify-content:center;align-items:stretch;gap:calc(var(--spacing) * 1);border-bottom-right-radius:var(--radius-base);border-bottom-left-radius:var(--radius-base);width:100%;padding:calc(var(--spacing) * 3);text-align:center;color:var(--foreground);flex-direction:column;flex:1;display:flex}.ifacebox .ifacebox-body>img{width:calc(var(--spacing) * 5);margin-inline:auto}:where(.ifacebox .ifacebox-body>span>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}.ifacebox .ifacebox-body>span .cbi-tooltip{transition-delay:.2s}.ifacebox .ifacebox-body>span .nowrap{display:inline-block}.ifacebox .ifacebox-body>span img{width:calc(var(--spacing) * 5);flex-shrink:0}.ifacebox .ifacebox-body>div{width:100%;display:block}:where(.ifacebox .ifacebox-body>div>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}.ifacebox .ifacebox-body .cbi-tooltip-container{cursor:help;color:var(--foreground);justify-content:center;align-items:center;display:inline-flex;position:static}.ifacebox .ifacebox-body .cbi-tooltip-container .cbi-tooltip{bottom:calc(var(--spacing) * 0)}.ifacebox .ifacebox-body .cbi-tooltip-container .cbi-tooltip .nowrap{white-space:nowrap}.ifacebox .ifacebox-body small{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4);display:block}.network-status-table .ifacebox-body{justify-content:space-around;align-items:center;gap:calc(var(--spacing) * 2);flex-direction:column;flex:1;height:100%;display:flex}.network-status-table .ifacebox-body>div{gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.network-status-table .ifacebox-body .ifacebadge{min-width:calc(var(--spacing) * 55);flex:1}#modal_overlay{pointer-events:none;top:calc(var(--spacing) * 0);bottom:calc(var(--spacing) * 0);background-color:#0000;display:none;position:fixed;overflow:auto}.modal-overlay-active #modal_overlay{pointer-events:auto;inset:calc(var(--spacing) * 0);z-index:50;background-color:var(--overlay-base-a60);--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);place-items:center;display:grid}#modal_overlay>.modal{width:100%;max-width:var(--container-2xl);gap:calc(var(--spacing) * 4);border-radius:calc(var(--radius-base) * 1.5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--panel-bg);padding:calc(var(--spacing) * 6);overflow-wrap:break-word;white-space:normal;--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);flex-direction:column;display:flex}#modal_overlay>.modal h4{margin-bottom:calc(var(--spacing) * 0);text-align:center}#modal_overlay>.modal h4 em{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--primary)}#modal_overlay>.modal h5{margin-block:calc(var(--spacing) * 3);color:var(--foreground)}#modal_overlay>.modal p{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--foreground)}#modal_overlay>.modal>ul{border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--background-a60);padding:calc(var(--spacing) * 3);overflow:auto}#modal_overlay>.modal label.btn{border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--background-a60);color:var(--foreground)}@media (hover:hover){#modal_overlay>.modal label.btn:hover{border-color:var(--border)}}#modal_overlay>.modal pre{overflow:auto}#modal_overlay>.modal pre.errors{border-color:var(--destructive-a60);background-color:var(--destructive-a20);color:var(--destructive)}#modal_overlay>.modal pre+pre{margin-top:calc(var(--spacing) * 3)}#modal_overlay>.modal .button-row,#modal_overlay>.modal div.left,#modal_overlay>.modal div.right{gap:calc(var(--spacing) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding:calc(var(--spacing) * 4);flex-shrink:0;display:flex}#modal_overlay>.modal div.left{justify-content:flex-start}#modal_overlay>.modal .button-row,#modal_overlay>.modal div.right{justify-content:flex-end}#modal_overlay>.modal.uci-dialog ins,#modal_overlay>.modal.uci-dialog del,#modal_overlay>.modal.uci-dialog var{font-family:var(--font-mono);--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);font-style:normal;text-decoration-line:none}#modal_overlay>.modal.uci-dialog ins{border-radius:calc(var(--radius-base) * .5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--success);background-color:var(--success-a80);color:var(--success-fg)}#modal_overlay>.modal.uci-dialog del{border-radius:calc(var(--radius-base) * .5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--destructive);background-color:var(--destructive-a80);color:var(--destructive-fg)}#modal_overlay>.modal.uci-dialog var{border-radius:calc(var(--radius-base) * .5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--info);background-color:var(--info-a80);color:var(--info-fg)}#modal_overlay>.modal.uci-dialog .uci-change-legend{margin-top:calc(var(--spacing) * 4);gap:calc(var(--spacing) * 3);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--background-a60);padding:calc(var(--spacing) * 4);grid-template-columns:repeat(2,minmax(0,1fr));display:grid}#modal_overlay>.modal.uci-dialog .uci-change-legend .uci-change-legend-label{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground);display:flex}#modal_overlay>.modal.uci-dialog .uci-change-legend .uci-change-legend-label ins,#modal_overlay>.modal.uci-dialog .uci-change-legend .uci-change-legend-label del,#modal_overlay>.modal.uci-dialog .uci-change-legend .uci-change-legend-label var{height:calc(var(--spacing) * 6);min-width:calc(var(--spacing) * 6);border-radius:calc(var(--radius-base) * .5);padding-inline:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);justify-content:center;align-items:center;display:flex}#modal_overlay>.modal.uci-dialog .uci-change-list{margin-top:calc(var(--spacing) * 4)}:where(#modal_overlay>.modal.uci-dialog .uci-change-list>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}#modal_overlay>.modal.uci-dialog .uci-change-list h5{margin-top:calc(var(--spacing) * 4);margin-bottom:calc(var(--spacing) * 2);border-radius:var(--radius-base);background-color:var(--muted);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground)}#modal_overlay>.modal.uci-dialog .uci-change-list h5:first-child{margin-top:calc(var(--spacing) * 0)}#modal_overlay>.modal.uci-dialog .uci-change-list ins,#modal_overlay>.modal.uci-dialog .uci-change-list del{border-radius:var(--radius-base);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));display:block}:is(#modal_overlay>.modal.uci-dialog .uci-change-list ins,#modal_overlay>.modal.uci-dialog .uci-change-list del) strong{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}#modal_overlay>.modal.uci-dialog .uci-change-list var{border-radius:calc(var(--radius-base) * .5);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));display:inline-block}#modal_overlay>.modal.uci-dialog .uci-change-list var ins,#modal_overlay>.modal.uci-dialog .uci-change-list var del{margin-left:calc(var(--spacing) * 1);padding-inline:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));padding-block:1px;display:inline-block}table.table,.table{margin-bottom:calc(var(--spacing) * 4);border-collapse:separate;--tw-border-spacing-x:calc(var(--spacing) * 0);--tw-border-spacing-y:calc(var(--spacing) * 0);width:100%;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));overflow:visible}@media not all and (width>=48rem){table.table,.table{border-style:var(--tw-border-style);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);border-width:0;border-radius:0;display:block}}:is(table.table,.table)[width="100%"]{width:100%}:is(table.table,.table)[width="33%"]{width:33.3333%}:is(table.table,.table) .cbi-section-table-titles{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}:is(:is(table.table,.table) .cbi-section-table-titles.named,:is(table.table,.table) .cbi-section-table-descr.named,:is(table.table,.table) .cbi-section-table-row[data-title]):before{content:var(--tw-content);content:var(--tw-content);border-bottom-style:var(--tw-border-style);content:var(--tw-content);border-bottom-width:1px;border-color:var(--border);content:var(--tw-content);padding-inline:calc(var(--spacing) * 3);content:var(--tw-content);padding-block:calc(var(--spacing) * 2.5);content:var(--tw-content);vertical-align:middle;content:var(--tw-content);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));content:var(--tw-content);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-content:attr(data-title);content:var(--tw-content);display:table-cell}:is(:is(:is(table.table,.table) .cbi-section-table-titles.named,:is(table.table,.table) .cbi-section-table-descr.named,:is(table.table,.table) .cbi-section-table-row[data-title]):not([data-title]),:is(:is(table.table,.table) .cbi-section-table-titles.named,:is(table.table,.table) .cbi-section-table-descr.named,:is(table.table,.table) .cbi-section-table-row[data-title]):has(td.cbi-section-table-titles)):before{content:var(--tw-content);--tw-content:none;content:none}:is(table.table,.table) .cbi-section-table-titles.named:before{content:var(--tw-content);border-top-left-radius:var(--radius-base);content:var(--tw-content);background-color:var(--label-surface)}@media not all and (width>=48rem){:is(table.table,.table) tbody,:is(table.table,.table) .tbody{display:block}}:is(table.table,.table) tr,:is(table.table,.table) .tr{transition:background-color .2s}@media (hover:hover){:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):hover{background-color:var(--label-surface)}}@media not all and (width>=48rem){:is(table.table,.table) tr,:is(table.table,.table) .tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}}@media (width>=48rem){:is(:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):first-child:not([data-title]) .th,:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):first-child:not([data-title]) .td):first-child{border-top-left-radius:var(--radius-base)}:is(:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):first-child .th,:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):first-child .td):last-child{border-top-right-radius:var(--radius-base)}}:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):last-child[data-title]:before{content:var(--tw-content);border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (width>=48rem){:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):last-child[data-title]:before{content:var(--tw-content);border-bottom-left-radius:var(--radius-base)}}:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):last-child .td{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (width>=48rem){:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):last-child .td:first-child{border-bottom-left-radius:var(--radius-base)}:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):last-child .td:last-child{border-bottom-right-radius:var(--radius-base)}}@media not all and (width>=48rem){:is(:is(table.table,.table) tr,:is(table.table,.table) .tr).table-titles,:is(:is(table.table,.table) tr,:is(table.table,.table) .tr).cbi-section-table-titles,:is(:is(table.table,.table) tr,:is(table.table,.table) .tr).cbi-section-table-descr{display:none}}:is(:is(table.table,.table) tr,:is(table.table,.table) .tr).cbi-rowstyle-1{background-color:var(--panel-bg)}:is(:is(table.table,.table) tr,:is(table.table,.table) .tr).cbi-rowstyle-2{background-color:var(--label-surface-a50)}:is(table.table,.table) th,:is(table.table,.table) .th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);background-color:var(--label-surface);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}@media not all and (width>=48rem){:is(table.table,.table) th,:is(table.table,.table) .th{display:none}}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[width="100%"]{width:100%}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[width="33%"]{width:33.3333%}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[data-sortable-row=true]{cursor:pointer;-webkit-user-select:none;user-select:none}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[data-sortable-row=true]:after{content:var(--tw-content);margin-left:calc(var(--spacing) * .5);content:var(--tw-content);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}@media (hover:hover){:is(:is(table.table,.table) th,:is(table.table,.table) .th)[data-sortable-row=true]:hover{background-color:var(--label-surface)}}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[data-sortable-row=true][data-sort-direction=asc]:after{--tw-content:"▲";content:var(--tw-content)}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[data-sortable-row=true][data-sort-direction=desc]:after{--tw-content:"▼";content:var(--tw-content)}:is(table.table,.table) td,:is(table.table,.table) .td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);vertical-align:middle;overflow-wrap:break-word;white-space:normal;display:table-cell}@media not all and (width>=48rem){:is(table.table,.table) td,:is(table.table,.table) .td{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 1);padding-block:calc(var(--spacing) * 1);border-width:0;flex:50%}}:is(:is(table.table,.table) td,:is(table.table,.table) .td):before{content:var(--tw-content);--tw-content:attr(data-title);content:var(--tw-content);content:var(--tw-content);display:none}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td):before{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));display:block}}:is(:is(table.table,.table) td,:is(table.table,.table) .td):before{content:var(--tw-content)}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td):before{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}}:is(:is(table.table,.table) td,:is(table.table,.table) .td)>input[type=text],:is(:is(table.table,.table) td,:is(table.table,.table) .td)>input[type=password],:is(:is(table.table,.table) td,:is(table.table,.table) .td) select,:is(:is(table.table,.table) td,:is(table.table,.table) .td) .cbi-dropdown:not(.btn):not(.cbi-button){width:100%}:is(:is(table.table,.table) td,:is(table.table,.table) .td) select,:is(:is(table.table,.table) td,:is(table.table,.table) .td) .cbi-dropdown:not(.btn):not(.cbi-button){min-width:fit-content}:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field{overflow-wrap:anywhere}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field{margin-bottom:calc(var(--spacing) * 1);text-align:left;flex-basis:100%;align-self:flex-start}}@media (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field{max-width:65ch}}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=CBI\.DummyValue],:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=CBI\.Button],:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=CBI\.FlagValue],:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=dvalue],:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=button],:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=fvalue]{flex-basis:50%}:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-section-actions{flex:0 0 100%}}:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-section-actions>div>.cbi-button{flex:1}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td)>.cbi-button{width:100%}}:is(:is(table.table,.table) td,:is(table.table,.table) .td)[width="100%"]{width:100%}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td)[width="100%"]{flex:0 0 100%}}:is(:is(table.table,.table) td,:is(table.table,.table) .td)[width="33%"]{width:33.3333%}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td)[width="33%"]{flex:0 0 33.333%}}.cbi-section-table .cbi-section-table-titles .cbi-section-table-cell{width:auto!important}#content_syslog>div{align-items:center;column-gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}#content_syslog>div>select,#content_syslog>div>input{margin-block:calc(var(--spacing) * 0)!important}#content_syslog>.syslog-view{display:block}#syslog,#log_textarea{width:100%;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}#syslog{margin-bottom:calc(var(--spacing) * 5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--terminal-bg);padding:calc(var(--spacing) * 4);color:var(--terminal-foreground)}#content_syslog.log-enhanced>#syslog{display:none}.syslog-view{margin-bottom:calc(var(--spacing) * 5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--terminal-bg);width:100%;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 4);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:1.7;color:var(--terminal-foreground);--tw-outline-style:none;outline-style:none;line-height:1.7;overflow-x:auto}.syslog-view:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring-a50)}.syslog-view .log-row{border-radius:calc(var(--radius-base) * .5);width:max-content;min-width:100%;padding-inline:calc(var(--spacing) * 2);white-space:pre}.syslog-view .log-row:hover{background-color:var(--terminal-hover)}.syslog-view ::selection{background-color:var(--info-a40)}.syslog-view .log-time{opacity:.55}.syslog-view .log-sev{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.syslog-view .log-tag{opacity:.8}@media (width>=48rem){.syslog-view .log-time,.syslog-view .log-sev,.syslog-view .log-tag{vertical-align:bottom;display:inline-block}.syslog-view .log-time{min-width:24ch}.syslog-view .log-sev{min-width:16ch}.syslog-view .log-tag{min-width:18ch}}.syslog-view .lv-info .log-sev{color:var(--info)}.syslog-view .lv-notice .log-sev{color:var(--success)}.syslog-view .lv-warn .log-sev{color:var(--warning)}.syslog-view .lv-debug{opacity:.5}.syslog-view .lv-err,.syslog-view .lv-crit{background-color:var(--destructive-a20)}:is(.syslog-view .lv-err,.syslog-view .lv-crit):hover{background-color:var(--destructive-a30)}:is(.syslog-view .lv-err,.syslog-view .lv-crit) .log-sev{color:var(--destructive)}.cbi-input-invalid,.cbi-value-error{border-color:var(--destructive-a70)!important}:is(.cbi-input-invalid,.cbi-value-error):focus{--tw-ring-color:var(--destructive-a40)!important}.cbi-value{gap:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 1.5);flex-flow:wrap;display:flex}@media not all and (width>=48rem){.cbi-value{gap:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * 1);flex-direction:column}}.cbi-value.hidden{display:none}.cbi-value+.cbi-value{margin-top:calc(var(--spacing) * 3)}.cbi-value>.cbi-value-title{padding-top:calc(var(--spacing) * 1);text-align:right;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground)}@media not all and (width>=48rem){.cbi-value>.cbi-value-title{text-align:left}}@media (width>=48rem){.cbi-value>.cbi-value-title{flex:0 0 10rem}}.cbi-value>.cbi-value-field{flex:1;max-width:100%}.cbi-value>.cbi-value-field:has(>.cbi-dropdown[disabled]){cursor:not-allowed}.cbi-value>.cbi-value-field .cbi-value-description{margin-top:calc(var(--spacing) * 1);padding-left:calc(var(--spacing) * 5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1;overflow-wrap:break-word;color:var(--muted-foreground);line-height:1;position:relative}.cbi-value>.cbi-value-field .cbi-value-description:not(:empty):before{content:var(--tw-content);content:var(--tw-content);left:calc(var(--spacing) * 0);content:var(--tw-content);content:var(--tw-content);width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);content:var(--tw-content);background-color:var(--primary);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);display:inline-block;position:absolute;-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-info'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3ccircle%20cx='12'%20cy='12'%20r='10'/%3e%3cpath%20d='M12%2016v-4'/%3e%3cpath%20d='M12%208h.01'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-info'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3ccircle%20cx='12'%20cy='12'%20r='10'/%3e%3cpath%20d='M12%2016v-4'/%3e%3cpath%20d='M12%208h.01'/%3e%3c/svg%3e");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.cbi-value>.cbi-section,.cbi-value>.cbi-tblsection{width:100%}.cbi-dynlist{width:max-content;max-width:calc(var(--spacing) * 96);align-items:flex-start;gap:calc(var(--spacing) * 3);flex-direction:column;display:inline-flex}@media not all and (width>=48rem){.cbi-dynlist{max-width:100%}}.cbi-dynlist .item{pointer-events:auto;cursor:move;align-items:flex-start;gap:calc(var(--spacing) * 2);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--panel-bg);padding-block:calc(var(--spacing) * 3);padding-right:calc(var(--spacing) * 9);padding-left:calc(var(--spacing) * 4);word-break:break-all;--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);-webkit-user-select:text;user-select:text;flex-direction:column;align-self:stretch;transition:scale .2s,opacity .2s,box-shadow .2s,border-color .2s,background-color .2s;display:inline-flex;position:relative;overflow:hidden}.cbi-dynlist .item:after{content:"×";top:calc(var(--spacing) * 2);right:calc(var(--spacing) * 2);height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);cursor:pointer;border-radius:calc(var(--radius-base) * .5);background-color:var(--muted);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted-foreground);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex;position:absolute}.cbi-dynlist .item.dragging{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y);cursor:grabbing;opacity:.5;--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.cbi-dynlist .item.drag-over{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y);border-color:var(--primary-a60);background-color:var(--primary-a5);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring)}.cbi-dynlist .item>span,.cbi-dynlist .item>code{pointer-events:none;border-radius:calc(var(--radius-base) * .5);width:100%;font-family:var(--font-mono);--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.cbi-dynlist .placeholder{word-break:break-all}.cbi-dynlist .add-item{align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 1);padding-block:calc(var(--spacing) * 1);background-color:#0000;align-self:stretch;transition:color .2s,background-color .2s,border-color .2s;display:inline-flex}@media (hover:hover){.cbi-dynlist .add-item:hover{border-color:var(--border-a80);background-color:var(--muted-a30)}}.cbi-dynlist .add-item>input{min-width:calc(var(--spacing) * 56);flex:1}.cbi-dynlist .add-item>.cbi-button{min-height:calc(var(--spacing) * 6);min-width:calc(var(--spacing) * 6);cursor:pointer;border-radius:calc(var(--radius-base) * .5);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 0);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}.cbi-dynlist .add-item>.cbi-button:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.cbi-tabmenu{margin-bottom:calc(var(--spacing) * 2);min-height:calc(var(--spacing) * 9);align-items:center;gap:calc(var(--spacing) * 0);border-radius:var(--radius-base);background-color:var(--muted);width:fit-content;max-width:100%;color:var(--muted-foreground);padding:3px;display:inline-flex;overflow-x:auto}.cbi-tabmenu li{margin:calc(var(--spacing) * 0);flex-shrink:0;min-width:fit-content;list-style-type:none;position:relative}.cbi-tabmenu li[data-errors]:after{content:var(--tw-content);content:var(--tw-content);top:calc(var(--spacing) * -.5);content:var(--tw-content);right:calc(var(--spacing) * -1.5);content:var(--tw-content);z-index:20;content:var(--tw-content);content:var(--tw-content);min-height:calc(var(--spacing) * 4);content:var(--tw-content);min-width:calc(var(--spacing) * 4);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);background-color:var(--destructive);content:var(--tw-content);padding-inline:calc(var(--spacing) * 1);content:var(--tw-content);padding-block:calc(var(--spacing) * .5);content:var(--tw-content);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));content:var(--tw-content);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);content:var(--tw-content);color:var(--destructive-fg);--tw-content:attr(data-errors);content:var(--tw-content);border-radius:3.40282e38px;justify-content:center;align-items:center;display:inline-flex;position:absolute}.cbi-tabmenu li[data-errors]>a{border-style:var(--tw-border-style);border-width:1px;border-color:var(--destructive-a50)}.cbi-tabmenu li>a{border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;color:var(--muted-foreground);border-width:1px;border-color:#0000;justify-content:center;align-items:center;text-decoration-line:none;transition:color .15s,background-color .15s,border-color .15s,box-shadow .15s;display:inline-flex}@media (hover:hover){.cbi-tabmenu li>a:hover{color:var(--foreground)}}.cbi-tabmenu li>a:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring-a50);--tw-outline-style:none;outline-style:none}.cbi-tabmenu li.cbi-tab>a{background-color:var(--background);color:var(--foreground);--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.cbi-tabmenu li.cbi-tab>a:where([data-darkmode=true],[data-darkmode=true] *){border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);background-color:var(--input-a30)}[data-tab-title]{visibility:hidden;height:calc(var(--spacing) * 0);overflow:hidden}[data-tab-active=true]{visibility:visible;height:auto;overflow:visible}#tabmenu{margin-bottom:calc(var(--spacing) * 6);width:100%}.tabs{align-items:center;gap:calc(var(--spacing) * 0);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);display:flex;position:relative;overflow-x:auto}.tabs a{padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 3);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;color:var(--muted-foreground);transition:color .2s,background-color .2s,border-color .2s;display:block}@media (hover:hover){.tabs a:hover{color:var(--foreground);text-decoration-line:none}}.tabs>li.active{border-bottom-style:var(--tw-border-style);border-bottom-width:2px;border-color:var(--foreground)}.tabs>li.active>a{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground)}.cbi-section-node-tabbed{margin-top:calc(var(--spacing) * 4)}button.cmdk-trigger{height:calc(var(--spacing) * 8);cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);padding-right:calc(var(--spacing) * 1.5);padding-left:calc(var(--spacing) * 7.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted-foreground);background-color:#0000;display:flex;position:relative}@media not all and (width>=48rem){button.cmdk-trigger{width:calc(var(--spacing) * 8);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);border-width:0;justify-content:center}}button.cmdk-trigger:before{pointer-events:none;left:calc(var(--spacing) * 2.5);width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);--tw-content:"";content:var(--tw-content);background-color:currentColor;position:absolute;-webkit-mask:url("data:image/svg+xml,%3csvg%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M10%2010m-7%200a7%207%200%201%200%2014%200a7%207%200%201%200%20-14%200'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cpath%20d='M21%2021l-6%20-6'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3csvg%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M10%2010m-7%200a7%207%200%201%200%2014%200a7%207%200%201%200%20-14%200'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cpath%20d='M21%2021l-6%20-6'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3c/svg%3e") 50%/contain no-repeat}@media not all and (width>=48rem){button.cmdk-trigger:before{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);position:static}}button.cmdk-trigger:hover{border-color:var(--muted-foreground)}@media not all and (width>=48rem){button.cmdk-trigger:hover{background-color:var(--muted)}}.cmdk-trigger-label{text-overflow:ellipsis;white-space:nowrap;text-align:left;overflow:hidden}@media not all and (width>=48rem){.cmdk-trigger-label{display:none}}.cmdk-trigger-key{border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--label-surface);padding-inline:calc(var(--spacing) * 1.5);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted-foreground);padding-block:1px}@media not all and (width>=48rem){.cmdk-trigger-key{display:none}}#cmdk-overlay{inset:calc(var(--spacing) * 0);z-index:50;background-color:var(--overlay-base-a60);--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);justify-content:center;align-items:flex-start;display:flex;position:fixed}@media not all and (width>=48rem){#cmdk-overlay{--tw-backdrop-blur: ;-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);background-color:#0000}}#cmdk-overlay[hidden]{display:none}.cmdk-panel{border-radius:calc(var(--radius-base) * 1.5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--panel-border);background-color:var(--panel-bg);--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);width:min(560px,100vw - 32px);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition:opacity .15s var(--ease-out),translate .15s var(--ease-out);flex-direction:column;margin-top:15vh;display:flex;overflow:hidden}@starting-style{.cmdk-panel{--tw-translate-y:calc(var(--spacing) * -1.5);translate:var(--tw-translate-x) var(--tw-translate-y)}}@starting-style{.cmdk-panel{opacity:0}}@media not all and (width>=48rem){.cmdk-panel{margin-top:calc(var(--spacing) * 0);border-style:var(--tw-border-style);width:100%;height:100dvh;padding-top:env(safe-area-inset-top);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);border-width:0;border-radius:0;transition-property:none}}.cmdk-inputrow{height:calc(var(--spacing) * 12);align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--panel-border);padding-right:calc(var(--spacing) * 3);padding-left:calc(var(--spacing) * 10);flex-shrink:0;display:flex;position:relative}@media not all and (width>=48rem){.cmdk-inputrow{height:calc(var(--spacing) * 14);padding-right:calc(var(--spacing) * 1.5)}}.cmdk-inputrow:before{pointer-events:none;left:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);color:var(--muted-foreground);--tw-content:"";content:var(--tw-content);background-color:currentColor;position:absolute;-webkit-mask:url("data:image/svg+xml,%3csvg%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M10%2010m-7%200a7%207%200%201%200%2014%200a7%207%200%201%200%20-14%200'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cpath%20d='M21%2021l-6%20-6'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3csvg%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M10%2010m-7%200a7%207%200%201%200%2014%200a7%207%200%201%200%20-14%200'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cpath%20d='M21%2021l-6%20-6'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3c/svg%3e") 50%/contain no-repeat}.cmdk-inputrow:has(input.cmdk-input:placeholder-shown) button.cmdk-clear{display:none}input.cmdk-input{height:100%;min-width:calc(var(--spacing) * 0);border-style:var(--tw-border-style);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--text);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-outline-style:none;background-color:#0000;border-width:0;outline-style:none;flex:1}@media not all and (width>=48rem){input.cmdk-input{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}}input.cmdk-input:focus{border-style:var(--tw-border-style);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-outline-style:none;border-width:0;outline-style:none}button.cmdk-clear{width:calc(var(--spacing) * 5.5);height:calc(var(--spacing) * 5.5);cursor:pointer;border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:var(--muted-foreground);background-color:#0000;border-width:0;border-radius:3.40282e38px;flex-shrink:0;justify-content:center;align-items:center;display:flex}button.cmdk-clear:before{width:calc(var(--spacing) * 3.25);height:calc(var(--spacing) * 3.25);--tw-content:"";content:var(--tw-content);background-color:currentColor;-webkit-mask:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-x'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='M18%206%206%2018'%20/%3e%3cpath%20d='m6%206%2012%2012'%20/%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-x'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='M18%206%206%2018'%20/%3e%3cpath%20d='m6%206%2012%2012'%20/%3e%3c/svg%3e") 50%/contain no-repeat}button.cmdk-clear:hover{background-color:var(--muted);color:var(--foreground)}button.cmdk-cancel{cursor:pointer;border-style:var(--tw-border-style);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--primary);background-color:#0000;border-width:0;flex-shrink:0;display:none}@media not all and (width>=48rem){button.cmdk-cancel{padding-block:calc(var(--spacing) * 1.5);padding-right:calc(var(--spacing) * 4);padding-left:calc(var(--spacing) * 1.5);display:block}}.cmdk-list{overscroll-behavior:contain;max-height:342px;padding:calc(var(--spacing) * 1);overflow-y:auto}@media not all and (width>=48rem){.cmdk-list{max-height:none;padding:calc(var(--spacing) * 2);padding-bottom:calc(.75rem + env(safe-area-inset-bottom));flex:1}}.cmdk-group{padding-inline:calc(var(--spacing) * 2);padding-top:calc(var(--spacing) * 2.5);padding-bottom:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);color:var(--muted-foreground)}.cmdk-row{cursor:pointer;justify-content:flex-start;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-base);border-style:var(--tw-border-style);width:100%;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--text);background-color:#0000;border-width:0;text-decoration-line:none;display:flex}@media not all and (width>=48rem){.cmdk-row{min-height:calc(var(--spacing) * 11);padding-inline:calc(var(--spacing) * 2.5)}}.cmdk-row:hover{text-decoration-line:none}.cmdk-row img.shadcn-icon{width:calc(var(--spacing) * 3.75);height:calc(var(--spacing) * 3.75);flex-shrink:0}.cmdk-row mark{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--primary);background-color:#0000}.cmdk-row .cmdk-title{min-width:calc(var(--spacing) * 0);text-overflow:ellipsis;white-space:nowrap;--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground);overflow:hidden}.cmdk-row .cmdk-group-name{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted-foreground);flex-shrink:0;margin-left:auto}.cmdk-row .cmdk-check{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);color:var(--primary);background-color:currentColor;flex-shrink:0;margin-left:auto;display:block;-webkit-mask:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-check'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M20%206%209%2017l-5-5'/%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-check'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M20%206%209%2017l-5-5'/%3e%3c/svg%3e") 50%/contain no-repeat}.cmdk-row.is-selected{background-color:var(--muted)}.cmdk-title:has(>.cmdk-parent){align-items:baseline;display:flex}.cmdk-parent{text-overflow:ellipsis;white-space:nowrap;--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--muted-foreground);overflow:hidden}.cmdk-label{text-overflow:ellipsis;white-space:nowrap;flex-shrink:0;max-width:calc(100% - 1.5rem);overflow:hidden}.cmdk-label:before{padding-inline:calc(var(--spacing) * 1.5);--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--muted-foreground);--tw-content:"›";content:var(--tw-content)}.cmdk-empty{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 7);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted-foreground)}.cmdk-footer{align-items:center;gap:calc(var(--spacing) * 3.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--panel-border);background-color:var(--background-a60);padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted-foreground);flex-shrink:0;display:flex}@media not all and (width>=48rem){.cmdk-footer{display:none}}.cmdk-footer kbd{border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--label-surface);padding-inline:calc(var(--spacing) * 1.5);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));padding-block:1px}.cmdk-footer .cmdk-hint{align-items:center;gap:calc(var(--spacing) * 1.5);display:flex}.cmdk-footer .cmdk-hint-close{margin-left:auto}:root{view-transition-name:none}#luci-nav-progress{pointer-events:none;top:calc(var(--spacing) * 0);left:calc(var(--spacing) * 0);z-index:80;height:calc(var(--spacing) * .5);width:calc(var(--spacing) * 0);--tw-gradient-position:to right;position:fixed}@supports (background-image:linear-gradient(in lab, red, red)){#luci-nav-progress{--tw-gradient-position:to right in oklab}}#luci-nav-progress{background-image:linear-gradient(var(--tw-gradient-stops));--tw-gradient-from:var(--progress-bar-start);--tw-gradient-to:var(--progress-bar-end);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));transition:width .3s var(--ease-out)}#luci-nav-progress[data-state=done]{opacity:0;transition:width .3s ease-out,opacity .15s ease-in .15s}#luci-nav-status{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}#maincontent{--tw-outline-style:none;outline-style:none}#maincontent .view-staging{visibility:hidden;height:calc(var(--spacing) * 0);overflow:hidden}#maincontent #view.view-leaving{cursor:progress;opacity:.6;transition:opacity .15s}.hidden{display:none!important}.left{text-align:left!important}@media (width>=48rem){.left:is(td){text-align:left!important}}.right{text-align:right!important}@media (width>=48rem){.right:is(td){text-align:right!important}}.center{text-align:center!important}@media (width>=48rem){.center:is(td){text-align:center!important}}.top{vertical-align:top!important}@media (width>=48rem){.top:is(td){vertical-align:top!important}}.bottom{vertical-align:bottom!important}@media (width>=48rem){.bottom:is(td){vertical-align:bottom!important}}.middle{vertical-align:middle!important}@media (width>=48rem){.middle:is(td){vertical-align:middle!important}}.nowrap:not(span){white-space:nowrap}.fade-in,.fade-out{transition:opacity .25s ease-in-out}@starting-style{.fade-in{opacity:0}}.fade-out{pointer-events:none;opacity:0}.spinning{position:relative;padding-left:calc(var(--spacing) * 7)!important}.spinning:before{content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);top:50%;left:calc(var(--spacing) * 1.5);content:var(--tw-content);width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);content:var(--tw-content);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y);content:var(--tw-content);animation:var(--animate-spin);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);background-color:currentColor;position:absolute;-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-loader-2'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M21%2012a9%209%200%201%201-6.219-8.56'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-loader-2'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M21%2012a9%209%200%201%201-6.219-8.56'/%3e%3c/svg%3e");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}#view>.spinning:first-child{min-height:calc(var(--spacing) * 48);place-items:center;width:fit-content;margin-inline:auto;display:grid}html{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}html[data-darkmode=true]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}.card-header img,.login-input-icon img,.login-pw-toggle img,img.shadcn-icon{filter:brightness(0)saturate()}html[data-darkmode=true] .card-header img,html[data-darkmode=true] .login-input-icon img,html[data-darkmode=true] .login-pw-toggle img,html[data-darkmode=true] img.shadcn-icon{filter:brightness(0)saturate()invert()}.card-header img,img.shadcn-icon,#sidebar img.shadcn-icon{opacity:.65}html[data-darkmode=true] #sidebar img.shadcn-icon{opacity:.7}.card-header button:hover img,#sidebar .sidebar-nav-item.active img,#sidebar .sidebar-accordion-item.active img,#sidebar .sidebar-nav-parent:hover img{opacity:1}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:1ms!important;transition-delay:0s!important;animation-duration:1ms!important;animation-delay:0s!important}.spinning:before{animation-duration:1s!important}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-border-spacing-x{syntax:"";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}} +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-content:"";--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-space-y-reverse:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:.25rem;--breakpoint-md:48rem;--container-xs:20rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base) * .5);--radius-md:calc(var(--radius-base) * .75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base) * 1.5);--shadow-sm:var(--shadow-sm);--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base) * .5);--color-sidebar-fg:var(--sidebar-foreground);--color-terminal-fg:var(--terminal-foreground)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.fixed{position:fixed}.relative{position:relative}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.block{display:block}.hidden{display:none}.inline{display:inline}.overflow-y-auto{overflow-y:auto}.border{border-style:var(--tw-border-style);border-width:1px}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.select-all{-webkit-user-select:all;user-select:all}}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-cyrillic-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-cyrillic-wght-normal.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-greek-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+1F??}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-greek-wght-normal.woff2)format("woff2-variations");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-vietnamese-wght-normal.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-latin-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-latin-wght-normal.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--bg:oklch(98.5% .004 275);--surface:oklch(100% 0 0);--text:oklch(18% .02 275);--brand:oklch(56.7% .158 275);--on-brand:oklch(100% 0 0);--success:oklch(58% .15 150);--warning:oklch(70% .13 75);--danger:oklch(55% .2 25);--info:oklch(58% .13 250);--overlay-base:oklch(0% 0 0);--background:oklch(98.5% .004 275);--foreground:oklch(18% .02 275);--primary:oklch(56.7% .158 275);--primary-foreground:oklch(100% 0 0);--panel-bg:oklch(100% 0 0);--destructive:oklch(55% .2 25);--ring:oklch(56.7% .158 275);--border:oklch(89.65% .0058 275);--input:oklch(89.65% .0058 275);--sidebar-bg:oklch(98.5% .004 275);--sidebar-foreground:oklch(18% .02 275);--sidebar-accent-fg:oklch(18% .02 275);--sidebar-accent:oklch(94.5% .004 275);--sidebar-hover-bg:oklch(94.5% .004 275);--login-path-stroke:oklch(56.7% .158 275);--progress-bar-end:oklch(56.7% .158 275);--destructive-fg:oklch(100% 0 0);--success-fg:oklch(100% 0 0);--info-fg:oklch(100% 0 0);--warning-fg:oklch(20% .05 75);--terminal-foreground:oklch(85% .08 155);--terminal-hover:oklch(85% .08 155/.06);--muted-foreground:oklch(52.62% .0131 275);--secondary-foreground:oklch(30.08% .0176 275);--sidebar-muted:oklch(52.62% .0131 275);--focus-ring:oklch(56.7% .158 275/.5);--muted:oklch(95% .004 275);--secondary:oklch(93.5% .004 275);--label-surface:oklch(96.3% .004 275);--panel-border:oklch(89.65% .0058 275);--primary-hover:oklch(62.7% .158 275);--sidebar-active-bg:oklch(56.7% .158 275/.12);--sidebar-active-fg:oklch(48.7% .158 275);--sidebar-border:oklch(89.65% .0058 275);--terminal-bg:oklch(14% .02 264);--warning-soft-fg:oklch(42% .12 75);--progress-bar-start:oklch(74.02% .0948 275);--login-left-bg:oklch(93.5% .004 275);--primary-a5:oklch(56.7% .158 275/.05);--primary-a10:oklch(56.7% .158 275/.1);--primary-a15:oklch(56.7% .158 275/.15);--primary-a20:oklch(56.7% .158 275/.2);--primary-a25:oklch(56.7% .158 275/.25);--primary-a60:oklch(56.7% .158 275/.6);--primary-a70:oklch(56.7% .158 275/.7);--primary-a90:oklch(56.7% .158 275/.9);--destructive-a8:oklch(55% .2 25/.08);--destructive-a10:oklch(55% .2 25/.1);--destructive-a20:oklch(55% .2 25/.2);--destructive-a30:oklch(55% .2 25/.3);--destructive-a40:oklch(55% .2 25/.4);--destructive-a50:oklch(55% .2 25/.5);--destructive-a60:oklch(55% .2 25/.6);--destructive-a70:oklch(55% .2 25/.7);--destructive-a80:oklch(55% .2 25/.8);--destructive-a90:oklch(55% .2 25/.9);--foreground-a5:oklch(18% .02 275/.05);--foreground-a30:oklch(18% .02 275/.3);--secondary-a80:oklch(93.5% .004 275/.8);--muted-a30:oklch(95% .004 275/.3);--muted-a80:oklch(95% .004 275/.8);--muted-foreground-a70:oklch(52.62% .0131 275/.7);--success-a10:oklch(58% .15 150/.1);--success-a15:oklch(58% .15 150/.15);--success-a30:oklch(58% .15 150/.3);--success-a40:oklch(58% .15 150/.4);--success-a80:oklch(58% .15 150/.8);--warning-a10:oklch(70% .13 75/.1);--warning-a15:oklch(70% .13 75/.15);--warning-a30:oklch(70% .13 75/.3);--warning-a40:oklch(70% .13 75/.4);--info-a10:oklch(58% .13 250/.1);--info-a15:oklch(58% .13 250/.15);--info-a30:oklch(58% .13 250/.3);--info-a40:oklch(58% .13 250/.4);--info-a80:oklch(58% .13 250/.8);--border-a60:oklch(89.65% .0058 275/.6);--border-a80:oklch(89.65% .0058 275/.8);--input-a30:oklch(89.65% .0058 275/.3);--label-surface-a50:oklch(96.3% .004 275/.5);--overlay-base-a50:oklch(0% 0 0/.5);--overlay-base-a60:oklch(0% 0 0/.6);--background-a60:oklch(98.5% .004 275/.6);--ring-a50:oklch(56.7% .158 275/.5);--font-sans:"Inter Variable", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;--radius-base:.5rem;--shadow-sm:0 1px 3px 0 oklch(0% 0 0/.1), 0 1px 2px -1px oklch(0% 0 0/.1)}[data-darkmode=true]{--bg:oklch(15.9% .005 264);--surface:oklch(20.5% .004 264);--text:oklch(90% .006 264);--brand:oklch(56.7% .158 275);--on-brand:oklch(100% 0 0);--success:oklch(63.7% .175 147);--warning:oklch(80% .12 80);--danger:oklch(63% .16 25);--info:oklch(70% .1 250);--overlay-base:oklch(0% 0 0);--background:oklch(15.9% .005 264);--foreground:oklch(90% .006 264);--primary:oklch(56.7% .158 275);--primary-foreground:oklch(100% 0 0);--panel-bg:oklch(20.5% .004 264);--destructive:oklch(63% .16 25);--ring:oklch(56.7% .158 275);--border:oklch(26.27% .0051 264);--input:oklch(26.27% .0051 264);--sidebar-bg:oklch(15.9% .005 264);--sidebar-foreground:oklch(90% .006 264);--sidebar-accent-fg:oklch(90% .006 264);--sidebar-accent:oklch(23% .004 264);--sidebar-hover-bg:oklch(23% .004 264);--login-path-stroke:oklch(56.7% .158 275);--progress-bar-end:oklch(56.7% .158 275);--destructive-fg:oklch(100% 0 0);--success-fg:oklch(100% 0 0);--info-fg:oklch(100% 0 0);--warning-fg:oklch(20% .05 75);--terminal-foreground:oklch(85% .08 155);--terminal-hover:oklch(85% .08 155/.06);--muted-foreground:oklch(58.14% .0056 264);--secondary-foreground:oklch(78.89% .0059 264);--sidebar-muted:oklch(58.14% .0056 264);--focus-ring:oklch(56.7% .158 275/.5);--muted:oklch(18.9% .005 264);--secondary:oklch(22.5% .004 264);--label-surface:oklch(19.5% .004 264);--panel-border:oklch(90% .006 264/.08);--primary-hover:oklch(62.7% .158 275);--sidebar-active-bg:oklch(56.7% .158 275/.14);--sidebar-active-fg:oklch(78.7% .158 275);--sidebar-border:oklch(90% .006 264/.08);--terminal-bg:oklch(12% .02 264);--warning-soft-fg:oklch(86% .1 85);--progress-bar-start:oklch(42.22% .0964 274.8);--login-left-bg:oklch(13.4% .005 264);--primary-a5:oklch(56.7% .158 275/.05);--primary-a10:oklch(56.7% .158 275/.1);--primary-a15:oklch(56.7% .158 275/.15);--primary-a20:oklch(56.7% .158 275/.2);--primary-a25:oklch(56.7% .158 275/.25);--primary-a60:oklch(56.7% .158 275/.6);--primary-a70:oklch(56.7% .158 275/.7);--primary-a90:oklch(56.7% .158 275/.9);--destructive-a8:oklch(63% .16 25/.08);--destructive-a10:oklch(63% .16 25/.1);--destructive-a20:oklch(63% .16 25/.2);--destructive-a30:oklch(63% .16 25/.3);--destructive-a40:oklch(63% .16 25/.4);--destructive-a50:oklch(63% .16 25/.5);--destructive-a60:oklch(63% .16 25/.6);--destructive-a70:oklch(63% .16 25/.7);--destructive-a80:oklch(63% .16 25/.8);--destructive-a90:oklch(63% .16 25/.9);--foreground-a5:oklch(90% .006 264/.05);--foreground-a30:oklch(90% .006 264/.3);--secondary-a80:oklch(22.5% .004 264/.8);--muted-a30:oklch(18.9% .005 264/.3);--muted-a80:oklch(18.9% .005 264/.8);--muted-foreground-a70:oklch(58.14% .0056 264/.7);--success-a10:oklch(63.7% .175 147/.1);--success-a15:oklch(63.7% .175 147/.15);--success-a30:oklch(63.7% .175 147/.3);--success-a40:oklch(63.7% .175 147/.4);--success-a80:oklch(63.7% .175 147/.8);--warning-a10:oklch(80% .12 80/.1);--warning-a15:oklch(80% .12 80/.15);--warning-a30:oklch(80% .12 80/.3);--warning-a40:oklch(80% .12 80/.4);--info-a10:oklch(70% .1 250/.1);--info-a15:oklch(70% .1 250/.15);--info-a30:oklch(70% .1 250/.3);--info-a40:oklch(70% .1 250/.4);--info-a80:oklch(70% .1 250/.8);--border-a60:oklch(26.27% .0051 264/.6);--border-a80:oklch(26.27% .0051 264/.8);--input-a30:oklch(26.27% .0051 264/.3);--label-surface-a50:oklch(19.5% .004 264/.5);--overlay-base-a50:oklch(0% 0 0/.5);--overlay-base-a60:oklch(0% 0 0/.6);--background-a60:oklch(15.9% .005 264/.6);--ring-a50:oklch(56.7% .158 275/.5)}:where(html,.sidebar-nav,#maincontent,.cmdk-list,.cbi-dropdown>ul.dropdown,#modal_overlay,#modal_overlay>.modal>ul,#modal_overlay>.modal pre,.cbi-tabmenu,.tabs,textarea){scrollbar-color:var(--foreground-a30) transparent;scrollbar-width:thin}:where(html,.sidebar-nav,#maincontent,.cmdk-list,.cbi-dropdown>ul.dropdown,#modal_overlay,#modal_overlay>.modal>ul,#modal_overlay>.modal pre,.cbi-tabmenu,.tabs,textarea)::-webkit-scrollbar{width:8px;height:8px}:where(html,.sidebar-nav,#maincontent,.cmdk-list,.cbi-dropdown>ul.dropdown,#modal_overlay,#modal_overlay>.modal>ul,#modal_overlay>.modal pre,.cbi-tabmenu,.tabs,textarea)::-webkit-scrollbar-track{background-color:#0000}:where(html,.sidebar-nav,#maincontent,.cmdk-list,.cbi-dropdown>ul.dropdown,#modal_overlay,#modal_overlay>.modal>ul,#modal_overlay>.modal pre,.cbi-tabmenu,.tabs,textarea)::-webkit-scrollbar-corner{background-color:#0000}:where(html,.sidebar-nav,#maincontent,.cmdk-list,.cbi-dropdown>ul.dropdown,#modal_overlay,#modal_overlay>.modal>ul,#modal_overlay>.modal pre,.cbi-tabmenu,.tabs,textarea)::-webkit-scrollbar-thumb{background-color:var(--foreground-a30);border-radius:9999px}html{background-color:var(--background);font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground)}html body{flex-wrap:wrap;height:100dvh;display:flex}html body.modal-overlay-active{overflow:hidden}h1,h2,h3,h4,h5,h6{font-family:var(--font-sans);--tw-leading:var(--leading-tight);line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--foreground)}h1{margin-bottom:calc(var(--spacing) * 4);font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}h2{margin-bottom:calc(var(--spacing) * 3);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}h3{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}h4{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}h5{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}h6{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}hr{margin-block:calc(var(--spacing) * 4);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border)}strong{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}abbr{cursor:help}p{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--foreground)}pre{border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--muted);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-family:var(--font-mono);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);overflow-wrap:break-word;white-space:pre-wrap;color:var(--muted-foreground);display:block}code{border-radius:calc(var(--radius-base) * .5);background-color:var(--muted);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5);font-family:var(--font-mono);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted-foreground)}a{color:var(--primary);text-decoration-line:none}@media (hover:hover){a:hover{text-decoration-line:underline}}var{font-family:var(--font-mono);color:var(--primary)}small{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--muted-foreground)}em{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted-foreground);font-style:italic}#sidebar{background-color:var(--sidebar-bg);flex-direction:column;flex-shrink:0;width:220px;height:100%;display:flex;overflow:hidden}@media not all and (width>=48rem){#sidebar{top:calc(var(--spacing) * 0);left:calc(var(--spacing) * 0);z-index:40;--tw-translate-x:-100%;width:240px;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);height:100dvh;transition:translate .2s;position:fixed}#sidebar[data-drawer-open=true]{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}}#sidebar .icon-expand{display:none}@media (width>=48rem){html[data-sidebar-collapsed=true] #sidebar{width:calc(var(--spacing) * 12)}html[data-sidebar-collapsed=true] #sidebar .sidebar-brand-text,html[data-sidebar-collapsed=true] #sidebar .sidebar-label,html[data-sidebar-collapsed=true] #sidebar .sidebar-chevron,html[data-sidebar-collapsed=true] #sidebar .icon-collapse{display:none}html[data-sidebar-collapsed=true] #sidebar .icon-expand{display:block}html[data-sidebar-collapsed=true] #sidebar .sidebar-accordion-sub{display:none!important}}.sidebar-header{height:calc(var(--spacing) * 14);padding-inline:calc(var(--spacing) * 3);flex-shrink:0;justify-content:space-between;align-items:center;display:flex}.sidebar-brand{min-width:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * 2);text-overflow:ellipsis;white-space:nowrap;color:var(--sidebar-foreground);flex:1;text-decoration-line:none;display:flex;overflow:hidden}.sidebar-logo{flex-shrink:0}.sidebar-brand-text{text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);overflow:hidden}.sidebar-toggle{height:calc(var(--spacing) * 7);width:calc(var(--spacing) * 7);cursor:pointer;border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:var(--sidebar-muted);background-color:#0000;border-width:0;flex-shrink:0;justify-content:center;align-items:center;transition:color .15s,background-color .15s,border-color .15s;display:flex}.sidebar-toggle:hover{background-color:var(--sidebar-accent);color:var(--sidebar-accent-fg)}.sidebar-toggle svg,.sidebar-toggle img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4)}.sidebar-nav{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 2);flex:1;overflow:hidden auto}.sidebar-nav-item,.sidebar-accordion-item{margin-bottom:calc(var(--spacing) * .5)}:is(.sidebar-nav-item,.sidebar-accordion-item).active>.sidebar-nav-parent{color:var(--sidebar-active-fg)}.sidebar-nav-parent{cursor:pointer;appearance:none;justify-content:flex-start;align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);width:100%;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--sidebar-muted);background-color:#0000;border-width:0;transition:color .15s,background-color .15s,border-color .15s;display:flex}.sidebar-nav-parent:hover{background-color:var(--sidebar-accent);color:var(--sidebar-accent-fg)}.sidebar-nav-parent .sidebar-icon{flex-shrink:0;justify-content:center;align-items:center;display:flex}.sidebar-nav-parent .sidebar-icon svg,.sidebar-nav-parent .sidebar-icon img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4)}.sidebar-nav-parent .sidebar-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.sidebar-nav-parent .sidebar-chevron{flex-shrink:0;margin-left:auto;transition:rotate .2s}.sidebar-nav-parent .sidebar-chevron svg,.sidebar-nav-parent .sidebar-chevron img{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5)}.sidebar-accordion-item[data-open=true]>.sidebar-nav-parent{background-color:var(--sidebar-active-bg);color:var(--sidebar-active-fg)}.sidebar-accordion-item[data-open=true] .sidebar-chevron{rotate:180deg}.sidebar-accordion-sub{visibility:hidden;opacity:0;transition:grid-template-rows .2s var(--ease-out),opacity .2s var(--ease-out),visibility .2s var(--ease-out);grid-template-rows:0fr;display:grid;overflow:hidden}.sidebar-accordion-sub>*{min-height:calc(var(--spacing) * 0);align-self:flex-start}.sidebar-accordion-item[data-open=true]>.sidebar-accordion-sub{visibility:visible;opacity:1;grid-template-rows:1fr}.sidebar-sub-list{margin-left:calc(var(--spacing) * 4);border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--sidebar-border);padding-left:calc(var(--spacing) * 2);list-style-type:none}.sidebar-sub-item{margin-bottom:calc(var(--spacing) * .5)}.sidebar-sub-item.active .sidebar-sub-link{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--sidebar-active-fg)}.sidebar-sub-link{border-radius:calc(var(--radius-base) * .75);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--sidebar-muted);text-decoration-line:none;transition:color .15s,background-color .15s,border-color .15s;display:block}.sidebar-sub-link:hover{background-color:var(--sidebar-accent);color:var(--sidebar-accent-fg)}.sidebar-collapsed-popover{z-index:50;min-width:calc(var(--spacing) * 44);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--panel-bg);padding-block:calc(var(--spacing) * 1);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);position:fixed;overflow:hidden}.sidebar-collapsed-popover-title{padding-inline:calc(var(--spacing) * 3);padding-top:calc(var(--spacing) * 1.5);padding-bottom:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground)}.sidebar-collapsed-popover-list{margin:calc(var(--spacing) * 0);padding:calc(var(--spacing) * 0);list-style-type:none}.sidebar-collapsed-popover-link{margin-inline:calc(var(--spacing) * 1);border-radius:calc(var(--radius-base) * .5);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--sidebar-muted);text-decoration-line:none;transition:color .15s,background-color .15s,border-color .15s;display:block}.sidebar-collapsed-popover-link:hover{background-color:var(--sidebar-accent);color:var(--sidebar-accent-fg)}.sidebar-collapsed-popover-link.active{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--sidebar-active-fg)}.sidebar-footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--sidebar-border);padding:calc(var(--spacing) * 2);padding-bottom:max(.5rem, env(safe-area-inset-bottom));flex-shrink:0}.sidebar-logout{align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:calc(var(--radius-base) * .75);width:100%;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--sidebar-muted);text-decoration-line:none;transition:color .15s,background-color .15s,border-color .15s;display:flex}.sidebar-logout:hover{background-color:var(--sidebar-accent);color:var(--sidebar-accent-fg)}.sidebar-logout.active{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--sidebar-active-fg)}.sidebar-logout .sidebar-icon{flex-shrink:0;justify-content:center;align-items:center;display:flex}.sidebar-logout .sidebar-icon svg,.sidebar-logout .sidebar-icon img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4)}.sidebar-logout .sidebar-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}#main-wrapper{background-color:var(--background);height:100%;padding-block:calc(var(--spacing) * 2);padding-right:calc(var(--spacing) * 2);padding-left:calc(var(--spacing) * 0);flex-direction:column;flex:1;display:flex;overflow:hidden}@media not all and (width>=48rem){#main-wrapper{padding:calc(var(--spacing) * 0);flex-basis:100%}}.content-card{border-radius:calc(var(--radius-base) * 1.5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--panel-border);background-color:var(--panel-bg);--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);flex-direction:column;flex:1;display:flex;overflow:hidden}@media not all and (width>=48rem){.content-card{--tw-border-style:none;--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);border-style:none;border-radius:0}}@media (width>=48rem){html[data-sidebar-anim] #sidebar{transition:width .2s var(--ease-out)}}.card-header{height:calc(var(--spacing) * 12);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--panel-border);background-color:var(--panel-bg);padding-inline:calc(var(--spacing) * 4);flex-shrink:0;justify-content:space-between;align-items:center;display:flex}.card-header-left,.card-header-right{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.topbar-hamburger{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);cursor:pointer;border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:var(--muted-foreground);background-color:#0000;border-width:0;justify-content:center;align-items:center;transition:color .15s,background-color .15s,border-color .15s;display:none}@media not all and (width>=48rem){.topbar-hamburger{display:flex}}.topbar-hamburger:hover{background-color:var(--muted)}.topbar-hamburger svg,.topbar-hamburger img{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5)}.topbar-breadcrumb{align-items:center;gap:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));display:flex}.topbar-breadcrumb .breadcrumb-item{color:var(--muted-foreground)}.topbar-breadcrumb .breadcrumb-item.active{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground)}.topbar-breadcrumb .breadcrumb-sep{color:var(--muted-foreground)}.topbar-breadcrumb .breadcrumb-sep svg,.topbar-breadcrumb .breadcrumb-sep img{height:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5)}button.topbar-icon-btn{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);cursor:pointer;border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:var(--muted-foreground);background-color:#0000;border-width:0;flex-shrink:0;justify-content:center;align-items:center;transition:color .15s,background-color .15s,border-color .15s;display:inline-flex}button.topbar-icon-btn:hover{background-color:var(--muted);color:var(--foreground)}button.topbar-icon-btn img{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);flex-shrink:0}.topbar-indicators{align-items:center;display:flex}.topbar-indicators span[data-indicator]>img{display:none}.topbar-indicators span[data-indicator]{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);cursor:pointer;border-radius:calc(var(--radius-base) * .75);color:var(--muted-foreground);flex-shrink:0;justify-content:center;align-items:center;font-size:0;transition:color .15s,background-color .15s,border-color .15s,width .15s ease-out,opacity .15s ease-out,margin-left .15s ease-out;display:inline-flex;position:relative}@starting-style{.topbar-indicators span[data-indicator]{width:calc(var(--spacing) * 0)}}@starting-style{.topbar-indicators span[data-indicator]{opacity:0}}.topbar-indicators span[data-indicator]:before{content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);background-color:currentColor;flex-shrink:0;display:block;position:relative;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.topbar-indicators span[data-indicator]+span[data-indicator]{margin-left:calc(var(--spacing) * 2)}@starting-style{.topbar-indicators span[data-indicator]+span[data-indicator]{margin-left:calc(var(--spacing) * 0)}}.topbar-indicators span[data-indicator]:hover{background-color:var(--muted);color:var(--foreground)}.topbar-indicators span[data-indicator][data-indicator=poll-status][data-style=active]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-refresh-cw'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M3%2012a9%209%200%200%201%209-9%209.75%209.75%200%200%201%206.74%202.74L21%208'/%3e%3cpath%20d='M21%203v5h-5'/%3e%3cpath%20d='M21%2012a9%209%200%200%201-9%209%209.75%209.75%200%200%201-6.74-2.74L3%2016'/%3e%3cpath%20d='M8%2016H3v5'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-refresh-cw'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M3%2012a9%209%200%200%201%209-9%209.75%209.75%200%200%201%206.74%202.74L21%208'/%3e%3cpath%20d='M21%203v5h-5'/%3e%3cpath%20d='M21%2012a9%209%200%200%201-9%209%209.75%209.75%200%200%201-6.74-2.74L3%2016'/%3e%3cpath%20d='M8%2016H3v5'/%3e%3c/svg%3e")}.topbar-indicators span[data-indicator][data-indicator=poll-status][data-style=inactive]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-refresh-cw-off'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M21%208L18.74%205.74A9.75%209.75%200%200%200%2012%203C11%203%2010.03%203.16%209.13%203.47'/%3e%3cpath%20d='M8%2016H3v5'/%3e%3cpath%20d='M3%2012C3%209.51%204%207.26%205.64%205.64'/%3e%3cpath%20d='m3%2016%202.26%202.26A9.75%209.75%200%200%200%2012%2021c2.49%200%204.74-1%206.36-2.64'/%3e%3cpath%20d='M21%2012c0%201-.16%201.97-.47%202.87'/%3e%3cpath%20d='M21%203v5h-5'/%3e%3cpath%20d='M22%2022%202%202'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-refresh-cw-off'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M21%208L18.74%205.74A9.75%209.75%200%200%200%2012%203C11%203%2010.03%203.16%209.13%203.47'/%3e%3cpath%20d='M8%2016H3v5'/%3e%3cpath%20d='M3%2012C3%209.51%204%207.26%205.64%205.64'/%3e%3cpath%20d='m3%2016%202.26%202.26A9.75%209.75%200%200%200%2012%2021c2.49%200%204.74-1%206.36-2.64'/%3e%3cpath%20d='M21%2012c0%201-.16%201.97-.47%202.87'/%3e%3cpath%20d='M21%203v5h-5'/%3e%3cpath%20d='M22%2022%202%202'/%3e%3c/svg%3e")}.topbar-indicators span[data-indicator][data-indicator=media_error]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-alert-circle'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3ccircle%20cx='12'%20cy='12'%20r='10'/%3e%3cline%20x1='12'%20x2='12'%20y1='8'%20y2='12'/%3e%3cline%20x1='12'%20x2='12.01'%20y1='16'%20y2='16'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-alert-circle'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3ccircle%20cx='12'%20cy='12'%20r='10'/%3e%3cline%20x1='12'%20x2='12'%20y1='8'%20y2='12'/%3e%3cline%20x1='12'%20x2='12.01'%20y1='16'%20y2='16'/%3e%3c/svg%3e")}.topbar-indicators span[data-indicator][data-indicator=uci-changes]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-settings'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='M9.671%204.136a2.34%202.34%200%200%201%204.659%200%202.34%202.34%200%200%200%203.319%201.915%202.34%202.34%200%200%201%202.33%204.033%202.34%202.34%200%200%200%200%203.831%202.34%202.34%200%200%201-2.33%204.033%202.34%202.34%200%200%200-3.319%201.915%202.34%202.34%200%200%201-4.659%200%202.34%202.34%200%200%200-3.32-1.915%202.34%202.34%200%200%201-2.33-4.033%202.34%202.34%200%200%200%200-3.831A2.34%202.34%200%200%201%206.35%206.051a2.34%202.34%200%200%200%203.319-1.915'%20/%3e%3ccircle%20cx='12'%20cy='12'%20r='3'%20/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-settings'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='M9.671%204.136a2.34%202.34%200%200%201%204.659%200%202.34%202.34%200%200%200%203.319%201.915%202.34%202.34%200%200%201%202.33%204.033%202.34%202.34%200%200%200%200%203.831%202.34%202.34%200%200%201-2.33%204.033%202.34%202.34%200%200%200-3.319%201.915%202.34%202.34%200%200%201-4.659%200%202.34%202.34%200%200%200-3.32-1.915%202.34%202.34%200%200%201-2.33-4.033%202.34%202.34%200%200%200%200-3.831A2.34%202.34%200%200%201%206.35%206.051a2.34%202.34%200%200%200%203.319-1.915'%20/%3e%3ccircle%20cx='12'%20cy='12'%20r='3'%20/%3e%3c/svg%3e")}.topbar-indicators span[data-indicator][data-indicator=uci-changes][data-count]:not([data-count="0"]):after{content:var(--tw-content);content:var(--tw-content);top:calc(var(--spacing) * -.5);content:var(--tw-content);right:calc(var(--spacing) * -.5);content:var(--tw-content);z-index:10;content:var(--tw-content);content:var(--tw-content);min-height:calc(var(--spacing) * 3);content:var(--tw-content);min-width:calc(var(--spacing) * 3);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);background-color:var(--primary);content:var(--tw-content);padding-inline:calc(var(--spacing) * .5);content:var(--tw-content);content:var(--tw-content);--tw-leading:1;content:var(--tw-content);--tw-font-weight:var(--font-weight-bold);font-size:8px;line-height:1;font-weight:var(--font-weight-bold);content:var(--tw-content);color:var(--primary-foreground);--tw-content:attr(data-count);content:var(--tw-content);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex;position:absolute}#maincontent{scrollbar-gutter:stable;padding:calc(var(--spacing) * 6);flex:1;overflow-y:auto}@media not all and (width>=48rem){#maincontent{padding:calc(var(--spacing) * 4)}}#maincontent h1{margin-bottom:calc(var(--spacing) * 4);border-style:var(--tw-border-style);padding-left:calc(var(--spacing) * 0);font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);border-width:0}#maincontent h2{margin-bottom:calc(var(--spacing) * 3);border-style:var(--tw-border-style);padding-left:calc(var(--spacing) * 0);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);border-width:0}#maincontent h3{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}#maincontent .cbi-map-descr,#maincontent .cbi-section-descr{margin-bottom:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--muted-foreground)}#maincontent .cbi-page-actions{margin-top:calc(var(--spacing) * 4);justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 2);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding-top:calc(var(--spacing) * 4);flex-wrap:wrap;display:flex}#maincontent div[style]>svg:where([data-darkmode=true],[data-darkmode=true] *){background-color:var(--surface)!important}#maincontent div[style]>svg line[style]:where([data-darkmode=true],[data-darkmode=true] *){stroke:var(--text)!important}#maincontent .zone-forwards{align-items:stretch;gap:calc(var(--spacing) * 2);--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);flex-wrap:nowrap;display:flex}#maincontent .zone-forwards>span{flex-shrink:0;flex-basis:calc(var(--spacing) * 8);color:var(--muted-foreground);justify-content:center;align-self:center;align-items:center;display:flex}#maincontent .zone-forwards .zone-src,#maincontent .zone-forwards .zone-dest{gap:calc(var(--spacing) * 1.5);flex-direction:column;flex:1;display:flex}#maincontent .controls{align-items:center;gap:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex}#maincontent .controls label input[type=radio],#maincontent .controls label input[type=checkbox]{margin-right:calc(var(--spacing) * 1);vertical-align:middle}#maincontent .cbi-title-section{margin-bottom:calc(var(--spacing) * 4);--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}#maincontent .control-group{margin-top:calc(var(--spacing) * 1)}#maincontent .control-group,#maincontent .cbi-page-actions>div,#maincontent .cbi-section-actions>div{align-items:center;gap:calc(var(--spacing) * 2);flex-flow:row;display:flex}#sidebar-overlay{pointer-events:none;inset:calc(var(--spacing) * 0);z-index:30;background-color:var(--overlay-base-a50);opacity:0;transition:opacity .2s;position:absolute}@media (width>=48rem){#sidebar-overlay{display:none}}#sidebar-overlay.active{pointer-events:auto;opacity:1}button,input[type=button],input[type=submit],input[type=reset],.btn,.cbi-button{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-width:1px;transition:color .2s,background-color .2s,border-color .2s;display:inline-flex}:is(button,input[type=button],input[type=submit],input[type=reset],.btn,.cbi-button)[disabled]{cursor:not-allowed;opacity:.4}.td :is(button,input[type=button],input[type=submit],input[type=reset],.btn,.cbi-button){white-space:nowrap}.btn,.cbi-button,.cbi-button-default,.cbi-button-download,.cbi-button-find,.cbi-button-link,.cbi-button-up,.cbi-button-down{border-color:var(--secondary);background-color:var(--secondary);color:var(--secondary-foreground)}@media (hover:hover){:is(.btn,.cbi-button,.cbi-button-default,.cbi-button-download,.cbi-button-find,.cbi-button-link,.cbi-button-up,.cbi-button-down):hover{background-color:var(--secondary-a80)}}.drag-handle{border-color:var(--muted);background-color:var(--muted);color:var(--muted-foreground)}@media (hover:hover){.drag-handle:hover{background-color:var(--muted-a80)}}.btn.primary,.cbi-button-action,.cbi-button-apply,.cbi-button-reload,.cbi-button-add,.cbi-button-edit{border-color:var(--primary);background-color:var(--primary);color:var(--primary-foreground)}@media (hover:hover){:is(.btn.primary,.cbi-button-action,.cbi-button-apply,.cbi-button-reload,.cbi-button-add,.cbi-button-edit):hover{background-color:var(--primary-a90)}}.cbi-button-positive,.cbi-button-fieldadd,.cbi-button-save{border-color:var(--primary-a25);background-color:var(--primary-a15);color:var(--primary)}@media (hover:hover){:is(.cbi-button-positive,.cbi-button-fieldadd,.cbi-button-save):hover{background-color:var(--primary-a25)}}.cbi-button-negative,.cbi-section-remove .cbi-button,.cbi-button-reset,.cbi-button-remove{border-color:var(--destructive);background-color:var(--destructive);color:var(--destructive-fg)}@media (hover:hover){:is(.cbi-button-negative,.cbi-section-remove .cbi-button,.cbi-button-reset,.cbi-button-remove):hover{background-color:var(--destructive-a90)}}input:disabled{cursor:not-allowed;opacity:.4}input[type=text],input[type=password],input[type=number],input[type=email],.cbi-input-text,.cbi-input{width:calc(var(--spacing) * 52.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);background-color:var(--background);max-width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition:color .2s,background-color .2s,border-color .2s;position:relative}:is(input[type=text],input[type=password],input[type=number],input[type=email],.cbi-input-text,.cbi-input)::placeholder{color:var(--muted-foreground-a70)}:is(input[type=text],input[type=password],input[type=number],input[type=email],.cbi-input-text,.cbi-input):focus{border-color:var(--primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring);--tw-outline-style:none;outline-style:none}.table.cbi-section-table :is(input[type=text],input[type=password],input[type=number],input[type=email],.cbi-input-text,.cbi-input){width:100%}.cbi-input-text{min-width:calc(var(--spacing) * 20)}#localtime{min-width:calc(var(--spacing) * 64)}textarea{min-height:calc(var(--spacing) * 24);resize:vertical;border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);background-color:var(--background);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);transition:color .2s,background-color .2s,border-color .2s}textarea::placeholder{color:var(--muted-foreground-a70)}textarea:focus{border-color:var(--primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring);--tw-outline-style:none;outline-style:none}textarea[disabled]{cursor:not-allowed;opacity:.4}input[type=radio],input[type=checkbox]{margin-right:calc(var(--spacing) * 3);height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);cursor:pointer;appearance:none;vertical-align:middle;display:inline-block;position:relative}:is(input[type=radio],input[type=checkbox]):disabled{cursor:not-allowed}:is(input[type=radio],input[type=checkbox]):before{content:var(--tw-content);content:var(--tw-content);inset:calc(var(--spacing) * 0);content:var(--tw-content);border-radius:calc(var(--radius-base) * .5);content:var(--tw-content);border-style:var(--tw-border-style);content:var(--tw-content);border-width:1px;border-color:var(--input);content:var(--tw-content);background-color:var(--background);content:var(--tw-content);transition:background-color .2s,border-color .2s;position:absolute}:is(input[type=radio],input[type=checkbox]):after{content:var(--tw-content);content:var(--tw-content);inset:calc(var(--spacing) * 0);content:var(--tw-content);background-color:var(--primary-foreground);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);opacity:0;content:var(--tw-content);transition:opacity .2s;position:absolute;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}:is(input[type=radio],input[type=checkbox]):checked:before{content:var(--tw-content);border-color:var(--primary);content:var(--tw-content);background-color:var(--primary)}:is(input[type=radio],input[type=checkbox]):checked:after{content:var(--tw-content);opacity:1}:is(input[type=radio],input[type=checkbox]):focus:before{content:var(--tw-content);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);content:var(--tw-content);--tw-ring-color:var(--ring);content:var(--tw-content);--tw-outline-style:none;outline-style:none}input[type=radio]:before{content:var(--tw-content);border-radius:3.40282e38px}input[type=radio]:after{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='currentColor'%3e%3ccircle%20cx='12'%20cy='12'%20r='6'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='currentColor'%3e%3ccircle%20cx='12'%20cy='12'%20r='6'/%3e%3c/svg%3e")}input[type=checkbox]:after{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-check'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M20%206%209%2017l-5-5'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-check'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M20%206%209%2017l-5-5'/%3e%3c/svg%3e")}.cbi-checkbox{vertical-align:middle;align-items:center;display:inline-flex;position:relative}select{max-width:100%;min-width:calc(var(--spacing) * 52.5);appearance:none;border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);background-color:var(--background);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);padding-right:calc(var(--spacing) * 9);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);transition:color .2s,background-color .2s,border-color .2s}select:focus{border-color:var(--primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring);--tw-outline-style:none;outline-style:none}select:disabled{cursor:not-allowed;opacity:.4}@media (width>=48rem){select{max-width:calc(var(--spacing) * 100)}}select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238888a0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-position:right .6rem center;background-repeat:no-repeat;background-size:16px}[data-darkmode=true] select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238c8ca8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")}.cbi-dropdown{min-width:fit-content;max-width:100%;height:fit-content;padding:calc(var(--spacing) * 0);white-space:nowrap;flex-shrink:0;align-items:center;transition:color .2s,background-color .2s,border-color .2s;display:inline-flex;position:relative}.cbi-dropdown[disabled]{pointer-events:none;opacity:.4}.cbi-dropdown:not(.btn):not(.cbi-button){max-width:100%;min-width:calc(var(--spacing) * 52.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);background-color:var(--background)}@media (width>=48rem){.cbi-dropdown:not(.btn):not(.cbi-button){max-width:calc(var(--spacing) * 100)}}.cbi-dropdown:not(.btn):not(.cbi-button)>ul>li[placeholder]{display:none}.cbi-dropdown>ul{margin:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * .5);opacity:.99;flex-wrap:nowrap;flex:1;list-style-type:none;transition:opacity .12s ease-out;display:flex;overflow:hidden}.cbi-dropdown>ul>li{margin:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * 1);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);list-style-type:none;display:none;overflow:hidden}.cbi-dropdown>ul>li[display]{display:flex!important}.cbi-dropdown>ul>li .hide-open{display:block}.cbi-dropdown>ul>li .hide-close{display:none}.cbi-dropdown>ul>li>form{pointer-events:none;display:none}.cbi-dropdown>ul>li>label{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.cbi-dropdown>ul>li img{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);vertical-align:middle;flex:none}.cbi-dropdown>ul>li span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.cbi-dropdown>ul.dropdown{left:calc(var(--spacing) * 0);z-index:50;border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--panel-bg);opacity:1;--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);width:fit-content;min-width:100%;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);position:absolute;overflow-y:auto}.cbi-dropdown>ul.dropdown>li{min-height:calc(var(--spacing) * 9);cursor:pointer;width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted-foreground)}@media (hover:hover){.cbi-dropdown>ul.dropdown>li:hover{background-color:var(--muted)}}.cbi-dropdown>ul.preview{display:none}.cbi-dropdown[empty]>ul{max-width:1px;max-height:1px}.cbi-dropdown[empty]>ul>li,.cbi-dropdown[optional][open]>ul.dropdown>li[placeholder]{display:block}.cbi-dropdown[open]{border-color:var(--primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring)}.cbi-dropdown[open]>ul.dropdown{width:auto;max-width:none;display:block}.cbi-dropdown[open]>ul.dropdown>li{display:flex}.cbi-dropdown[open]>ul.dropdown>li[selected]{background-color:var(--primary-a15);color:var(--primary)}.cbi-dropdown[open]>ul.dropdown>li[unselectable]{opacity:.4}.cbi-dropdown[open]>ul.dropdown>li .hide-open{display:none}.cbi-dropdown[open]>ul.dropdown>li .hide-close{display:block}.cbi-dropdown[open]>ul.preview{display:flex}:is(.cbi-dropdown[multiple][more],.cbi-dropdown[multiple][empty])>.more{padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1.5);flex:none;display:flex}.cbi-dropdown[multiple][empty]>.more{flex:1}.cbi-dropdown[multiple][open]>ul.dropdown>li>form{align-items:center;display:flex}.cbi-dropdown[multiple]>ul>li>label{display:flex}.cbi-dropdown>.open{cursor:pointer;border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--input);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * .5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:currentColor;flex:none;justify-content:center;align-items:center;display:flex}.cbi-dropdown>.more{display:none}.cbi-tooltip{z-index:50;width:max-content;max-width:var(--container-xs);--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border-a60);background-color:var(--panel-bg);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));overflow-wrap:normal;word-break:normal;white-space:normal;color:var(--foreground);opacity:0;--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition:visibility .2s var(--ease-out),opacity .2s var(--ease-out),scale .2s var(--ease-out);position:absolute;inset-inline-start:-10000px}.cbi-tooltip.error{border-color:var(--destructive-a40);background-color:var(--destructive);color:var(--destructive-fg)}.cbi-tooltip.success{border-color:var(--success-a40);background-color:var(--success);color:var(--success-fg)}.cbi-tooltip.info{border-color:var(--info-a40);background-color:var(--info);color:var(--info-fg)}.cbi-tooltip.notice{border-color:var(--warning-a40);background-color:var(--warning);color:var(--warning-fg)}.cbi-tooltip-container,[data-tooltip]{cursor:help;white-space:nowrap;text-underline-offset:2px;text-decoration-line:underline;text-decoration-style:dotted;text-decoration-color:currentColor;display:inline-block;position:relative}:is(.cbi-tooltip-container,[data-tooltip]) .cbi-tooltip{visibility:hidden;--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:0;transition:visibility .2s var(--ease-out),opacity .2s var(--ease-out),scale .2s var(--ease-out)}:is(.cbi-tooltip-container,[data-tooltip]):hover .cbi-tooltip{visibility:visible;--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:1;inset-inline-start:auto}.alert-message{margin-bottom:calc(var(--spacing) * 4);border-radius:calc(var(--radius-base) * 1.5);border-style:var(--tw-border-style);background-color:var(--muted);padding:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground);border-width:1px}.alert-message.success{border-color:var(--success-a30);background-color:var(--success-a10);color:var(--success)}.alert-message.info{border-color:var(--info-a30);background-color:var(--info-a10);color:var(--info)}.alert-message.warning,.alert-message.notice{border-color:var(--warning-a30);background-color:var(--warning-a10);color:var(--warning-soft-fg)}.alert-message.error,.alert-message.danger{border-color:var(--destructive-a30);background-color:var(--destructive-a10);color:var(--destructive)}.alert-message h4,.alert-message h5,.alert-message pre,.alert-message ul,.alert-message li,.alert-message p{border-style:var(--tw-border-style);color:inherit;background-color:#0000;border-width:0}.alert-message h4{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.alert-message p{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.alert-message .right{margin-top:calc(var(--spacing) * 3)}.cbi-progressbar{height:calc(var(--spacing) * 2.5);cursor:help;background-color:var(--secondary);border-radius:3.40282e38px;width:100%;position:relative;overflow:hidden}.cbi-progressbar:before{content:var(--tw-content);content:var(--tw-content);inset-inline:calc(var(--spacing) * 0);content:var(--tw-content);content:var(--tw-content);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y);content:var(--tw-content);text-align:center;content:var(--tw-content);content:var(--tw-content);--tw-leading:1;content:var(--tw-content);white-space:nowrap;content:var(--tw-content);color:var(--foreground);--tw-content:attr(title);content:var(--tw-content);font-size:10px;line-height:1;position:absolute;top:50%}.cbi-progressbar>div{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops));--tw-gradient-from:var(--progress-bar-start);--tw-gradient-to:var(--progress-bar-end);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));height:100%;transition:width .3s}.label{background-color:var(--muted);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--muted-foreground);border-radius:3.40282e38px;align-items:center;display:inline-flex}.label.important{background-color:var(--info-a15);color:var(--info)}.label.warning{background-color:var(--warning-a15);color:var(--warning-soft-fg)}.label.success{background-color:var(--success-a15);color:var(--success)}.label.notice{background-color:var(--warning-a15);color:var(--warning-soft-fg)}.zonebadge{align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);border-width:1px;border-color:var(--secondary);background-color:var(--secondary);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1);vertical-align:middle;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-normal);line-height:var(--leading-normal);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--secondary-foreground);transition:color .2s,background-color .2s,border-color .2s;display:inline-flex;overflow:visible}.zonebadge[style*=--zone-color-rgb]{border-color:#0000;background-color:rgb(var(--zone-color-rgb),.6)!important}.zonebadge.zonebadge-empty{--tw-border-style:dashed;border-style:dashed;border-color:var(--border);--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--muted-foreground);background-color:#0000;font-style:italic}.ifacebadge>.zonebadge{padding-inline:calc(var(--spacing) * 1);padding-block:calc(var(--spacing) * .5);border-radius:3.40282e38px}.cbi-dropdown .zonebadge{border-radius:calc(var(--radius-base) * .75);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5)}.ifacebadge{cursor:default;align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--label-surface);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--foreground);flex-wrap:nowrap;transition:color .2s,background-color .2s,border-color .2s;display:inline-flex}.zonebadge>.ifacebadge{border-radius:calc(var(--radius-base) * .75);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * 0)}.zonebadge>.ifacebadge img{margin-right:calc(var(--spacing) * 1);width:calc(var(--spacing) * 4)}.cbi-dropdown .ifacebadge{gap:calc(var(--spacing) * 1);border-radius:calc(var(--radius-base) * .75);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5)}.cbi-dropdown .ifacebadge img{margin-right:calc(var(--spacing) * 1);width:calc(var(--spacing) * 4)}.ifacebadge>img,.ifacebadge>.cbi-tooltip-container>img{width:calc(var(--spacing) * 5);flex-shrink:0;align-self:center}.ifacebadge>span .nowrap{margin-bottom:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));display:inline-block}.ifacebadge.ifacebadge-active{border-color:var(--primary);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.ifacebadge.large{min-width:calc(var(--spacing) * 48);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);flex:1}.cbi-section{margin-bottom:calc(var(--spacing) * 4);border-radius:calc(var(--radius-base) * 1.5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--panel-border);background-color:var(--panel-bg);padding:calc(var(--spacing) * 4);overflow:visible}.cbi-section[data-tab-title]{margin:calc(var(--spacing) * 0);padding:calc(var(--spacing) * 0)}.cbi-section[data-tab-active=true]{margin-bottom:calc(var(--spacing) * 4);padding:calc(var(--spacing) * 4)}.cbi-section>.cbi-title{justify-content:space-between;align-items:flex-start;display:flex}.cbi-section>.cbi-title>h3{flex:1}.cbi-section>.cbi-title>h3 .label[data-clickable]{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6);cursor:pointer;border-radius:calc(var(--radius-base) * .5);background-color:var(--label-surface);padding:calc(var(--spacing) * 0);color:var(--foreground);justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.cbi-section>.cbi-title>h3 .label[data-clickable]:hover{background-color:var(--muted)}}.cbi-section>.cbi-title>h3 .label[data-clickable]:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.cbi-section>.cbi-title>h3 span[data-indicator=poll-status]{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);cursor:pointer;font-size:0;position:relative}.cbi-section>.cbi-title>h3 span[data-indicator=poll-status]:before{content:var(--tw-content);content:var(--tw-content);width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);background-color:currentColor;position:absolute;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.cbi-section>.cbi-title>h3 span[data-indicator=poll-status][data-style=active]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-chevron-down'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='m6%209%206%206%206-6'%20/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-chevron-down'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='m6%209%206%206%206-6'%20/%3e%3c/svg%3e")}.cbi-section>.cbi-title>h3 span[data-indicator=poll-status][data-style=inactive]:before{content:var(--tw-content);-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-chevron-up'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='m18%2015-6-6-6%206'%20/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-chevron-up'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='m18%2015-6-6-6%206'%20/%3e%3c/svg%3e")}.cbi-section legend{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.cbi-section h3{margin-bottom:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-bottom:calc(var(--spacing) * 3);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground)}.cbi-section .cbi-section-create{margin-top:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.cbi-section .cbi-section-create>div{flex:none}.cbi-section .network-status-table{margin-bottom:calc(var(--spacing) * 3);justify-content:space-around;gap:calc(var(--spacing) * 4);flex-wrap:wrap;display:flex}@media (width>=48rem){.cbi-section .network-status-table{flex-wrap:nowrap}}.cbi-section div[style*=display\:grid]:has(>.ifacebox){place-items:center}@media not all and (width>=48rem){.cbi-section div[style*=display\:grid]:has(>.ifacebox){place-items:stretch stretch;gap:calc(var(--spacing) * 3);grid-template-columns:repeat(auto-fit,minmax(140px,1fr))!important}.cbi-section div[style*=display\:grid]:has(>.ifacebox)>.ifacebox{margin:calc(var(--spacing) * 0)!important;max-width:none!important}}.ifacebox{min-width:calc(var(--spacing) * 32);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--panel-bg);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);flex-direction:column;align-items:stretch;display:inline-flex;position:relative;overflow:visible}@media (width>=48rem){#cbi-network-interface .ifacebox{min-width:calc(var(--spacing) * 38)}}.ifacebox .ifacebox-head{border-top-left-radius:var(--radius-base);border-top-right-radius:var(--radius-base);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);background-color:var(--label-surface);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground);transition:color .2s,background-color .2s,border-color .2s}.ifacebox .ifacebox-head[style*=--zone-color-rgb]{background-color:rgb(var(--zone-color-rgb),.65)!important}.ifacebox .ifacebox-head.cbi-tooltip-container{background-color:#0000;position:static}.ifacebox .ifacebox-head.cbi-tooltip-container .cbi-tooltip{bottom:calc(var(--spacing) * 0);left:calc(var(--spacing) * 0)}.ifacebox .ifacebox-head.active{border-color:var(--primary);background-color:var(--primary);color:var(--primary-foreground)}.ifacebox .ifacebox-body{justify-content:center;align-items:stretch;gap:calc(var(--spacing) * 1);border-bottom-right-radius:var(--radius-base);border-bottom-left-radius:var(--radius-base);width:100%;padding:calc(var(--spacing) * 3);text-align:center;color:var(--foreground);flex-direction:column;flex:1;display:flex}.ifacebox .ifacebox-body>img{width:calc(var(--spacing) * 5);margin-inline:auto}:where(.ifacebox .ifacebox-body>span>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}.ifacebox .ifacebox-body>span .cbi-tooltip{transition-delay:.2s}.ifacebox .ifacebox-body>span .nowrap{display:inline-block}.ifacebox .ifacebox-body>span img{width:calc(var(--spacing) * 5);flex-shrink:0}.ifacebox .ifacebox-body>div{width:100%;display:block}:where(.ifacebox .ifacebox-body>div>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}.ifacebox .ifacebox-body .cbi-tooltip-container{cursor:help;color:var(--foreground);justify-content:center;align-items:center;display:inline-flex;position:static}.ifacebox .ifacebox-body .cbi-tooltip-container .cbi-tooltip{bottom:calc(var(--spacing) * 0)}.ifacebox .ifacebox-body .cbi-tooltip-container .cbi-tooltip .nowrap{white-space:nowrap}.ifacebox .ifacebox-body small{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4);display:block}.network-status-table .ifacebox-body{justify-content:space-around;align-items:center;gap:calc(var(--spacing) * 2);flex-direction:column;flex:1;height:100%;display:flex}.network-status-table .ifacebox-body>div{gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.network-status-table .ifacebox-body .ifacebadge{min-width:calc(var(--spacing) * 55);flex:1}#modal_overlay{pointer-events:none;top:calc(var(--spacing) * 0);bottom:calc(var(--spacing) * 0);background-color:#0000;display:none;position:fixed;overflow:auto}.modal-overlay-active #modal_overlay{pointer-events:auto;inset:calc(var(--spacing) * 0);z-index:50;background-color:var(--overlay-base-a60);--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);place-items:center;display:grid}#modal_overlay>.modal{width:100%;max-width:var(--container-2xl);gap:calc(var(--spacing) * 4);border-radius:calc(var(--radius-base) * 1.5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--panel-bg);padding:calc(var(--spacing) * 6);overflow-wrap:break-word;white-space:normal;--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);flex-direction:column;display:flex}#modal_overlay>.modal h4{margin-bottom:calc(var(--spacing) * 0);text-align:center}#modal_overlay>.modal h4 em{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--primary)}#modal_overlay>.modal h5{margin-block:calc(var(--spacing) * 3);color:var(--foreground)}#modal_overlay>.modal p{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--foreground)}#modal_overlay>.modal>ul{border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--background-a60);padding:calc(var(--spacing) * 3);overflow:auto}#modal_overlay>.modal label.btn{border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--background-a60);color:var(--foreground)}@media (hover:hover){#modal_overlay>.modal label.btn:hover{border-color:var(--border)}}#modal_overlay>.modal pre{overflow:auto}#modal_overlay>.modal pre.errors{border-color:var(--destructive-a60);background-color:var(--destructive-a20);color:var(--destructive)}#modal_overlay>.modal pre+pre{margin-top:calc(var(--spacing) * 3)}#modal_overlay>.modal .button-row,#modal_overlay>.modal div.left,#modal_overlay>.modal div.right{gap:calc(var(--spacing) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding:calc(var(--spacing) * 4);flex-shrink:0;display:flex}#modal_overlay>.modal div.left{justify-content:flex-start}#modal_overlay>.modal .button-row,#modal_overlay>.modal div.right{justify-content:flex-end}#modal_overlay>.modal.uci-dialog ins,#modal_overlay>.modal.uci-dialog del,#modal_overlay>.modal.uci-dialog var{font-family:var(--font-mono);--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);font-style:normal;text-decoration-line:none}#modal_overlay>.modal.uci-dialog ins{border-radius:calc(var(--radius-base) * .5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--success);background-color:var(--success-a80);color:var(--success-fg)}#modal_overlay>.modal.uci-dialog del{border-radius:calc(var(--radius-base) * .5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--destructive);background-color:var(--destructive-a80);color:var(--destructive-fg)}#modal_overlay>.modal.uci-dialog var{border-radius:calc(var(--radius-base) * .5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--info);background-color:var(--info-a80);color:var(--info-fg)}#modal_overlay>.modal.uci-dialog .uci-change-legend{margin-top:calc(var(--spacing) * 4);gap:calc(var(--spacing) * 3);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--background-a60);padding:calc(var(--spacing) * 4);grid-template-columns:repeat(2,minmax(0,1fr));display:grid}#modal_overlay>.modal.uci-dialog .uci-change-legend .uci-change-legend-label{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground);display:flex}#modal_overlay>.modal.uci-dialog .uci-change-legend .uci-change-legend-label ins,#modal_overlay>.modal.uci-dialog .uci-change-legend .uci-change-legend-label del,#modal_overlay>.modal.uci-dialog .uci-change-legend .uci-change-legend-label var{height:calc(var(--spacing) * 6);min-width:calc(var(--spacing) * 6);border-radius:calc(var(--radius-base) * .5);padding-inline:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);justify-content:center;align-items:center;display:flex}#modal_overlay>.modal.uci-dialog .uci-change-list{margin-top:calc(var(--spacing) * 4)}:where(#modal_overlay>.modal.uci-dialog .uci-change-list>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}#modal_overlay>.modal.uci-dialog .uci-change-list h5{margin-top:calc(var(--spacing) * 4);margin-bottom:calc(var(--spacing) * 2);border-radius:var(--radius-base);background-color:var(--muted);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground)}#modal_overlay>.modal.uci-dialog .uci-change-list h5:first-child{margin-top:calc(var(--spacing) * 0)}#modal_overlay>.modal.uci-dialog .uci-change-list ins,#modal_overlay>.modal.uci-dialog .uci-change-list del{border-radius:var(--radius-base);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));display:block}:is(#modal_overlay>.modal.uci-dialog .uci-change-list ins,#modal_overlay>.modal.uci-dialog .uci-change-list del) strong{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}#modal_overlay>.modal.uci-dialog .uci-change-list var{border-radius:calc(var(--radius-base) * .5);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));display:inline-block}#modal_overlay>.modal.uci-dialog .uci-change-list var ins,#modal_overlay>.modal.uci-dialog .uci-change-list var del{margin-left:calc(var(--spacing) * 1);padding-inline:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));padding-block:1px;display:inline-block}table.table,.table{margin-bottom:calc(var(--spacing) * 4);border-collapse:separate;--tw-border-spacing-x:calc(var(--spacing) * 0);--tw-border-spacing-y:calc(var(--spacing) * 0);width:100%;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));overflow:visible}@media not all and (width>=48rem){table.table,.table{border-style:var(--tw-border-style);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);border-width:0;border-radius:0;display:block}}:is(table.table,.table)[width="100%"]{width:100%}:is(table.table,.table)[width="33%"]{width:33.3333%}:is(table.table,.table) .cbi-section-table-titles{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}:is(:is(table.table,.table) .cbi-section-table-titles.named,:is(table.table,.table) .cbi-section-table-descr.named,:is(table.table,.table) .cbi-section-table-row[data-title]):before{content:var(--tw-content);content:var(--tw-content);border-bottom-style:var(--tw-border-style);content:var(--tw-content);border-bottom-width:1px;border-color:var(--border);content:var(--tw-content);padding-inline:calc(var(--spacing) * 3);content:var(--tw-content);padding-block:calc(var(--spacing) * 2.5);content:var(--tw-content);vertical-align:middle;content:var(--tw-content);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));content:var(--tw-content);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-content:attr(data-title);content:var(--tw-content);display:table-cell}:is(:is(:is(table.table,.table) .cbi-section-table-titles.named,:is(table.table,.table) .cbi-section-table-descr.named,:is(table.table,.table) .cbi-section-table-row[data-title]):not([data-title]),:is(:is(table.table,.table) .cbi-section-table-titles.named,:is(table.table,.table) .cbi-section-table-descr.named,:is(table.table,.table) .cbi-section-table-row[data-title]):has(td.cbi-section-table-titles)):before{content:var(--tw-content);--tw-content:none;content:none}:is(table.table,.table) .cbi-section-table-titles.named:before{content:var(--tw-content);border-top-left-radius:var(--radius-base);content:var(--tw-content);background-color:var(--label-surface)}@media not all and (width>=48rem){:is(table.table,.table) tbody,:is(table.table,.table) .tbody{display:block}}:is(table.table,.table) tr,:is(table.table,.table) .tr{transition:background-color .2s}@media (hover:hover){:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):hover{background-color:var(--label-surface)}}@media not all and (width>=48rem){:is(table.table,.table) tr,:is(table.table,.table) .tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}}@media (width>=48rem){:is(:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):first-child:not([data-title]) .th,:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):first-child:not([data-title]) .td):first-child{border-top-left-radius:var(--radius-base)}:is(:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):first-child .th,:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):first-child .td):last-child{border-top-right-radius:var(--radius-base)}}:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):last-child[data-title]:before{content:var(--tw-content);border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (width>=48rem){:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):last-child[data-title]:before{content:var(--tw-content);border-bottom-left-radius:var(--radius-base)}}:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):last-child .td{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (width>=48rem){:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):last-child .td:first-child{border-bottom-left-radius:var(--radius-base)}:is(:is(table.table,.table) tr,:is(table.table,.table) .tr):last-child .td:last-child{border-bottom-right-radius:var(--radius-base)}}@media not all and (width>=48rem){:is(:is(table.table,.table) tr,:is(table.table,.table) .tr).table-titles,:is(:is(table.table,.table) tr,:is(table.table,.table) .tr).cbi-section-table-titles,:is(:is(table.table,.table) tr,:is(table.table,.table) .tr).cbi-section-table-descr{display:none}}:is(:is(table.table,.table) tr,:is(table.table,.table) .tr).cbi-rowstyle-1{background-color:var(--panel-bg)}:is(:is(table.table,.table) tr,:is(table.table,.table) .tr).cbi-rowstyle-2{background-color:var(--label-surface-a50)}:is(table.table,.table) th,:is(table.table,.table) .th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);background-color:var(--label-surface);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}@media not all and (width>=48rem){:is(table.table,.table) th,:is(table.table,.table) .th{display:none}}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[width="100%"]{width:100%}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[width="33%"]{width:33.3333%}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[data-sortable-row=true]{cursor:pointer;-webkit-user-select:none;user-select:none}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[data-sortable-row=true]:after{content:var(--tw-content);margin-left:calc(var(--spacing) * .5);content:var(--tw-content);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}@media (hover:hover){:is(:is(table.table,.table) th,:is(table.table,.table) .th)[data-sortable-row=true]:hover{background-color:var(--label-surface)}}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[data-sortable-row=true][data-sort-direction=asc]:after{--tw-content:"▲";content:var(--tw-content)}:is(:is(table.table,.table) th,:is(table.table,.table) .th)[data-sortable-row=true][data-sort-direction=desc]:after{--tw-content:"▼";content:var(--tw-content)}:is(table.table,.table) td,:is(table.table,.table) .td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);vertical-align:middle;overflow-wrap:break-word;white-space:normal;display:table-cell}@media not all and (width>=48rem){:is(table.table,.table) td,:is(table.table,.table) .td{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 1);padding-block:calc(var(--spacing) * 1);border-width:0;flex:50%}}:is(:is(table.table,.table) td,:is(table.table,.table) .td):before{content:var(--tw-content);--tw-content:attr(data-title);content:var(--tw-content);content:var(--tw-content);display:none}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td):before{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));display:block}}:is(:is(table.table,.table) td,:is(table.table,.table) .td):before{content:var(--tw-content)}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td):before{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}}:is(:is(table.table,.table) td,:is(table.table,.table) .td)>input[type=text],:is(:is(table.table,.table) td,:is(table.table,.table) .td)>input[type=password],:is(:is(table.table,.table) td,:is(table.table,.table) .td) select,:is(:is(table.table,.table) td,:is(table.table,.table) .td) .cbi-dropdown:not(.btn):not(.cbi-button){width:100%}:is(:is(table.table,.table) td,:is(table.table,.table) .td) select,:is(:is(table.table,.table) td,:is(table.table,.table) .td) .cbi-dropdown:not(.btn):not(.cbi-button){min-width:fit-content}:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field{overflow-wrap:anywhere}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field{margin-bottom:calc(var(--spacing) * 1);text-align:left;flex-basis:100%;align-self:flex-start}}@media (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field{max-width:65ch}}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=CBI\.DummyValue],:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=CBI\.Button],:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=CBI\.FlagValue],:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=dvalue],:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=button],:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-value-field[data-widget=fvalue]{flex-basis:50%}:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-section-actions{flex:0 0 100%}}:is(:is(table.table,.table) td,:is(table.table,.table) .td).cbi-section-actions>div>.cbi-button{flex:1}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td)>.cbi-button{width:100%}}:is(:is(table.table,.table) td,:is(table.table,.table) .td)[width="100%"]{width:100%}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td)[width="100%"]{flex:0 0 100%}}:is(:is(table.table,.table) td,:is(table.table,.table) .td)[width="33%"]{width:33.3333%}@media not all and (width>=48rem){:is(:is(table.table,.table) td,:is(table.table,.table) .td)[width="33%"]{flex:0 0 33.333%}}.cbi-section-table .cbi-section-table-titles .cbi-section-table-cell{width:auto!important}#content_syslog>div{align-items:center;column-gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}#content_syslog>div>select,#content_syslog>div>input{margin-block:calc(var(--spacing) * 0)!important}#content_syslog>.syslog-view{display:block}#syslog,#log_textarea{width:100%;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}#syslog{margin-bottom:calc(var(--spacing) * 5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--terminal-bg);padding:calc(var(--spacing) * 4);color:var(--terminal-foreground)}#content_syslog.log-enhanced>#syslog{display:none}.syslog-view{margin-bottom:calc(var(--spacing) * 5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--terminal-bg);width:100%;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 4);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:1.7;color:var(--terminal-foreground);--tw-outline-style:none;outline-style:none;line-height:1.7;overflow-x:auto}.syslog-view:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring-a50)}.syslog-view .log-row{border-radius:calc(var(--radius-base) * .5);width:max-content;min-width:100%;padding-inline:calc(var(--spacing) * 2);white-space:pre}.syslog-view .log-row:hover{background-color:var(--terminal-hover)}.syslog-view ::selection{background-color:var(--info-a40)}.syslog-view .log-time{opacity:.55}.syslog-view .log-sev{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.syslog-view .log-tag{opacity:.8}@media (width>=48rem){.syslog-view .log-time,.syslog-view .log-sev,.syslog-view .log-tag{vertical-align:bottom;display:inline-block}.syslog-view .log-time{min-width:24ch}.syslog-view .log-sev{min-width:16ch}.syslog-view .log-tag{min-width:18ch}}.syslog-view .lv-info .log-sev{color:var(--info)}.syslog-view .lv-notice .log-sev{color:var(--success)}.syslog-view .lv-warn .log-sev{color:var(--warning)}.syslog-view .lv-debug{opacity:.5}.syslog-view .lv-err,.syslog-view .lv-crit{background-color:var(--destructive-a20)}:is(.syslog-view .lv-err,.syslog-view .lv-crit):hover{background-color:var(--destructive-a30)}:is(.syslog-view .lv-err,.syslog-view .lv-crit) .log-sev{color:var(--destructive)}.cbi-input-invalid,.cbi-value-error{border-color:var(--destructive-a70)!important}:is(.cbi-input-invalid,.cbi-value-error):focus{--tw-ring-color:var(--destructive-a40)!important}.cbi-value{gap:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 1.5);flex-flow:wrap;display:flex}@media not all and (width>=48rem){.cbi-value{gap:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * 1);flex-direction:column}}.cbi-value.hidden{display:none}.cbi-value+.cbi-value{margin-top:calc(var(--spacing) * 3)}.cbi-value>.cbi-value-title{padding-top:calc(var(--spacing) * 1);text-align:right;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground)}@media not all and (width>=48rem){.cbi-value>.cbi-value-title{text-align:left}}@media (width>=48rem){.cbi-value>.cbi-value-title{flex:0 0 10rem}}.cbi-value>.cbi-value-field{flex:1;max-width:100%}.cbi-value>.cbi-value-field:has(>.cbi-dropdown[disabled]){cursor:not-allowed}.cbi-value>.cbi-value-field .cbi-value-description{margin-top:calc(var(--spacing) * 1);padding-left:calc(var(--spacing) * 5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1;overflow-wrap:break-word;color:var(--muted-foreground);line-height:1;position:relative}.cbi-value>.cbi-value-field .cbi-value-description:not(:empty):before{content:var(--tw-content);content:var(--tw-content);left:calc(var(--spacing) * 0);content:var(--tw-content);content:var(--tw-content);width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);content:var(--tw-content);background-color:var(--primary);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);display:inline-block;position:absolute;-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-info'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3ccircle%20cx='12'%20cy='12'%20r='10'/%3e%3cpath%20d='M12%2016v-4'/%3e%3cpath%20d='M12%208h.01'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-info'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3ccircle%20cx='12'%20cy='12'%20r='10'/%3e%3cpath%20d='M12%2016v-4'/%3e%3cpath%20d='M12%208h.01'/%3e%3c/svg%3e");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.cbi-value>.cbi-section,.cbi-value>.cbi-tblsection{width:100%}.cbi-dynlist{width:max-content;max-width:calc(var(--spacing) * 96);align-items:flex-start;gap:calc(var(--spacing) * 3);flex-direction:column;display:inline-flex}@media not all and (width>=48rem){.cbi-dynlist{max-width:100%}}.cbi-dynlist .item{pointer-events:auto;cursor:move;align-items:flex-start;gap:calc(var(--spacing) * 2);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--panel-bg);padding-block:calc(var(--spacing) * 3);padding-right:calc(var(--spacing) * 9);padding-left:calc(var(--spacing) * 4);word-break:break-all;--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);-webkit-user-select:text;user-select:text;flex-direction:column;align-self:stretch;transition:scale .2s,opacity .2s,box-shadow .2s,border-color .2s,background-color .2s;display:inline-flex;position:relative;overflow:hidden}.cbi-dynlist .item:after{content:"×";top:calc(var(--spacing) * 2);right:calc(var(--spacing) * 2);height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);cursor:pointer;border-radius:calc(var(--radius-base) * .5);background-color:var(--muted);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--muted-foreground);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex;position:absolute}.cbi-dynlist .item.dragging{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y);cursor:grabbing;opacity:.5;--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.cbi-dynlist .item.drag-over{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y);border-color:var(--primary-a60);background-color:var(--primary-a5);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring)}.cbi-dynlist .item>span,.cbi-dynlist .item>code{pointer-events:none;border-radius:calc(var(--radius-base) * .5);width:100%;font-family:var(--font-mono);--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.cbi-dynlist .placeholder{word-break:break-all}.cbi-dynlist .add-item{align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 1);padding-block:calc(var(--spacing) * 1);background-color:#0000;align-self:stretch;transition:color .2s,background-color .2s,border-color .2s;display:inline-flex}@media (hover:hover){.cbi-dynlist .add-item:hover{border-color:var(--border-a80);background-color:var(--muted-a30)}}.cbi-dynlist .add-item>input{min-width:calc(var(--spacing) * 56);flex:1}.cbi-dynlist .add-item>.cbi-button{min-height:calc(var(--spacing) * 6);min-width:calc(var(--spacing) * 6);cursor:pointer;border-radius:calc(var(--radius-base) * .5);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 0);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}.cbi-dynlist .add-item>.cbi-button:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.cbi-tabmenu{margin-bottom:calc(var(--spacing) * 2);min-height:calc(var(--spacing) * 9);align-items:center;gap:calc(var(--spacing) * 0);border-radius:var(--radius-base);background-color:var(--muted);width:fit-content;max-width:100%;color:var(--muted-foreground);padding:3px;display:inline-flex;overflow-x:auto}.cbi-tabmenu li{margin:calc(var(--spacing) * 0);flex-shrink:0;min-width:fit-content;list-style-type:none;position:relative}.cbi-tabmenu li[data-errors]:after{content:var(--tw-content);content:var(--tw-content);top:calc(var(--spacing) * -.5);content:var(--tw-content);right:calc(var(--spacing) * -1.5);content:var(--tw-content);z-index:20;content:var(--tw-content);content:var(--tw-content);min-height:calc(var(--spacing) * 4);content:var(--tw-content);min-width:calc(var(--spacing) * 4);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);background-color:var(--destructive);content:var(--tw-content);padding-inline:calc(var(--spacing) * 1);content:var(--tw-content);padding-block:calc(var(--spacing) * .5);content:var(--tw-content);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));content:var(--tw-content);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);content:var(--tw-content);color:var(--destructive-fg);--tw-content:attr(data-errors);content:var(--tw-content);border-radius:3.40282e38px;justify-content:center;align-items:center;display:inline-flex;position:absolute}.cbi-tabmenu li[data-errors]>a{border-style:var(--tw-border-style);border-width:1px;border-color:var(--destructive-a50)}.cbi-tabmenu li>a{border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;color:var(--muted-foreground);border-width:1px;border-color:#0000;justify-content:center;align-items:center;text-decoration-line:none;transition:color .15s,background-color .15s,border-color .15s,box-shadow .15s;display:inline-flex}@media (hover:hover){.cbi-tabmenu li>a:hover{color:var(--foreground)}}.cbi-tabmenu li>a:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--ring-a50);--tw-outline-style:none;outline-style:none}.cbi-tabmenu li.cbi-tab>a{background-color:var(--background);color:var(--foreground);--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.cbi-tabmenu li.cbi-tab>a:where([data-darkmode=true],[data-darkmode=true] *){border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);background-color:var(--input-a30)}[data-tab-title]{visibility:hidden;height:calc(var(--spacing) * 0);overflow:hidden}[data-tab-active=true]{visibility:visible;height:auto;overflow:visible}#tabmenu{margin-bottom:calc(var(--spacing) * 6);width:100%}.tabs{align-items:center;gap:calc(var(--spacing) * 0);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);display:flex;position:relative;overflow-x:auto}.tabs a{padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 3);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;color:var(--muted-foreground);transition:color .2s,background-color .2s,border-color .2s;display:block}@media (hover:hover){.tabs a:hover{color:var(--foreground);text-decoration-line:none}}.tabs>li.active{border-bottom-style:var(--tw-border-style);border-bottom-width:2px;border-color:var(--foreground)}.tabs>li.active>a{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--foreground)}.cbi-section-node-tabbed{margin-top:calc(var(--spacing) * 4)}button.cmdk-trigger{height:calc(var(--spacing) * 8);cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:var(--radius-base);border-style:var(--tw-border-style);border-width:1px;border-color:var(--input);padding-right:calc(var(--spacing) * 1.5);padding-left:calc(var(--spacing) * 7.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted-foreground);background-color:#0000;display:flex;position:relative}@media not all and (width>=48rem){button.cmdk-trigger{width:calc(var(--spacing) * 8);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);border-width:0;justify-content:center}}button.cmdk-trigger:before{pointer-events:none;left:calc(var(--spacing) * 2.5);width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);--tw-content:"";content:var(--tw-content);background-color:currentColor;position:absolute;-webkit-mask:url("data:image/svg+xml,%3csvg%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M10%2010m-7%200a7%207%200%201%200%2014%200a7%207%200%201%200%20-14%200'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cpath%20d='M21%2021l-6%20-6'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3csvg%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M10%2010m-7%200a7%207%200%201%200%2014%200a7%207%200%201%200%20-14%200'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cpath%20d='M21%2021l-6%20-6'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3c/svg%3e") 50%/contain no-repeat}@media not all and (width>=48rem){button.cmdk-trigger:before{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);position:static}}button.cmdk-trigger:hover{border-color:var(--muted-foreground)}@media not all and (width>=48rem){button.cmdk-trigger:hover{background-color:var(--muted)}}.cmdk-trigger-label{text-overflow:ellipsis;white-space:nowrap;text-align:left;overflow:hidden}@media not all and (width>=48rem){.cmdk-trigger-label{display:none}}.cmdk-trigger-key{border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--label-surface);padding-inline:calc(var(--spacing) * 1.5);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted-foreground);padding-block:1px}@media not all and (width>=48rem){.cmdk-trigger-key{display:none}}#cmdk-overlay{inset:calc(var(--spacing) * 0);z-index:50;background-color:var(--overlay-base-a60);--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);justify-content:center;align-items:flex-start;display:flex;position:fixed}@media not all and (width>=48rem){#cmdk-overlay{--tw-backdrop-blur: ;-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);background-color:#0000}}#cmdk-overlay[hidden]{display:none}.cmdk-panel{border-radius:calc(var(--radius-base) * 1.5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--panel-border);background-color:var(--panel-bg);--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);width:min(560px,100vw - 32px);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition:opacity .15s var(--ease-out),translate .15s var(--ease-out);flex-direction:column;margin-top:15vh;display:flex;overflow:hidden}@starting-style{.cmdk-panel{--tw-translate-y:calc(var(--spacing) * -1.5);translate:var(--tw-translate-x) var(--tw-translate-y)}}@starting-style{.cmdk-panel{opacity:0}}@media not all and (width>=48rem){.cmdk-panel{margin-top:calc(var(--spacing) * 0);border-style:var(--tw-border-style);width:100%;height:100dvh;padding-top:env(safe-area-inset-top);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);border-width:0;border-radius:0;transition-property:none}}.cmdk-inputrow{height:calc(var(--spacing) * 12);align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--panel-border);padding-right:calc(var(--spacing) * 3);padding-left:calc(var(--spacing) * 10);flex-shrink:0;display:flex;position:relative}@media not all and (width>=48rem){.cmdk-inputrow{height:calc(var(--spacing) * 14);padding-right:calc(var(--spacing) * 1.5)}}.cmdk-inputrow:before{pointer-events:none;left:calc(var(--spacing) * 3.5);width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);color:var(--muted-foreground);--tw-content:"";content:var(--tw-content);background-color:currentColor;position:absolute;-webkit-mask:url("data:image/svg+xml,%3csvg%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M10%2010m-7%200a7%207%200%201%200%2014%200a7%207%200%201%200%20-14%200'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cpath%20d='M21%2021l-6%20-6'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3csvg%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M10%2010m-7%200a7%207%200%201%200%2014%200a7%207%200%201%200%20-14%200'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cpath%20d='M21%2021l-6%20-6'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3c/svg%3e") 50%/contain no-repeat}.cmdk-inputrow:has(input.cmdk-input:placeholder-shown) button.cmdk-clear{display:none}input.cmdk-input{height:100%;min-width:calc(var(--spacing) * 0);border-style:var(--tw-border-style);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--text);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-outline-style:none;background-color:#0000;border-width:0;outline-style:none;flex:1}@media not all and (width>=48rem){input.cmdk-input{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}}input.cmdk-input:focus{border-style:var(--tw-border-style);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-outline-style:none;border-width:0;outline-style:none}button.cmdk-clear{width:calc(var(--spacing) * 5.5);height:calc(var(--spacing) * 5.5);cursor:pointer;border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:var(--muted-foreground);background-color:#0000;border-width:0;border-radius:3.40282e38px;flex-shrink:0;justify-content:center;align-items:center;display:flex}button.cmdk-clear:before{width:calc(var(--spacing) * 3.25);height:calc(var(--spacing) * 3.25);--tw-content:"";content:var(--tw-content);background-color:currentColor;-webkit-mask:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-x'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='M18%206%206%2018'%20/%3e%3cpath%20d='m6%206%2012%2012'%20/%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-x'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%20%3e%3cpath%20d='M18%206%206%2018'%20/%3e%3cpath%20d='m6%206%2012%2012'%20/%3e%3c/svg%3e") 50%/contain no-repeat}button.cmdk-clear:hover{background-color:var(--muted);color:var(--foreground)}button.cmdk-cancel{cursor:pointer;border-style:var(--tw-border-style);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--primary);background-color:#0000;border-width:0;flex-shrink:0;display:none}@media not all and (width>=48rem){button.cmdk-cancel{padding-block:calc(var(--spacing) * 1.5);padding-right:calc(var(--spacing) * 4);padding-left:calc(var(--spacing) * 1.5);display:block}}.cmdk-list{overscroll-behavior:contain;max-height:342px;padding:calc(var(--spacing) * 1);overflow-y:auto}@media not all and (width>=48rem){.cmdk-list{max-height:none;padding:calc(var(--spacing) * 2);padding-bottom:calc(.75rem + env(safe-area-inset-bottom));flex:1}}.cmdk-group{padding-inline:calc(var(--spacing) * 2);padding-top:calc(var(--spacing) * 2.5);padding-bottom:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);color:var(--muted-foreground)}.cmdk-row{cursor:pointer;justify-content:flex-start;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-base);border-style:var(--tw-border-style);width:100%;padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--text);background-color:#0000;border-width:0;text-decoration-line:none;display:flex}@media not all and (width>=48rem){.cmdk-row{min-height:calc(var(--spacing) * 11);padding-inline:calc(var(--spacing) * 2.5)}}.cmdk-row:hover{text-decoration-line:none}.cmdk-row img.shadcn-icon{width:calc(var(--spacing) * 3.75);height:calc(var(--spacing) * 3.75);flex-shrink:0}.cmdk-row mark{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--primary);background-color:#0000}.cmdk-row .cmdk-title{min-width:calc(var(--spacing) * 0);text-overflow:ellipsis;white-space:nowrap;--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--foreground);overflow:hidden}.cmdk-row .cmdk-group-name{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted-foreground);flex-shrink:0;margin-left:auto}.cmdk-row .cmdk-check{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);color:var(--primary);background-color:currentColor;flex-shrink:0;margin-left:auto;display:block;-webkit-mask:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-check'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M20%206%209%2017l-5-5'/%3e%3c/svg%3e") 50%/contain no-repeat;mask:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-check'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M20%206%209%2017l-5-5'/%3e%3c/svg%3e") 50%/contain no-repeat}.cmdk-row.is-selected{background-color:var(--muted)}.cmdk-title:has(>.cmdk-parent){align-items:baseline;display:flex}.cmdk-parent{text-overflow:ellipsis;white-space:nowrap;--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--muted-foreground);overflow:hidden}.cmdk-label{text-overflow:ellipsis;white-space:nowrap;flex-shrink:0;max-width:calc(100% - 1.5rem);overflow:hidden}.cmdk-label:before{padding-inline:calc(var(--spacing) * 1.5);--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--muted-foreground);--tw-content:"›";content:var(--tw-content)}.cmdk-empty{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 7);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted-foreground)}.cmdk-footer{align-items:center;gap:calc(var(--spacing) * 3.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--panel-border);background-color:var(--background-a60);padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted-foreground);flex-shrink:0;display:flex}@media not all and (width>=48rem){.cmdk-footer{display:none}}.cmdk-footer kbd{border-radius:calc(var(--radius-base) * .75);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--label-surface);padding-inline:calc(var(--spacing) * 1.5);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));padding-block:1px}.cmdk-footer .cmdk-hint{align-items:center;gap:calc(var(--spacing) * 1.5);display:flex}.cmdk-footer .cmdk-hint-close{margin-left:auto}:root{view-transition-name:none}#luci-nav-progress{pointer-events:none;top:calc(var(--spacing) * 0);left:calc(var(--spacing) * 0);z-index:80;height:calc(var(--spacing) * .5);width:calc(var(--spacing) * 0);--tw-gradient-position:to right;position:fixed}@supports (background-image:linear-gradient(in lab, red, red)){#luci-nav-progress{--tw-gradient-position:to right in oklab}}#luci-nav-progress{background-image:linear-gradient(var(--tw-gradient-stops));--tw-gradient-from:var(--progress-bar-start);--tw-gradient-to:var(--progress-bar-end);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));transition:width .3s var(--ease-out)}#luci-nav-progress[data-state=done]{opacity:0;transition:width .3s ease-out,opacity .15s ease-in .15s}#luci-nav-status{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}#maincontent{--tw-outline-style:none;outline-style:none}#maincontent .view-staging{visibility:hidden;height:calc(var(--spacing) * 0);overflow:hidden}#maincontent #view.view-leaving{cursor:progress;opacity:.6;transition:opacity .15s}.hidden{display:none!important}.left{text-align:left!important}@media (width>=48rem){.left:is(td){text-align:left!important}}.right{text-align:right!important}@media (width>=48rem){.right:is(td){text-align:right!important}}.center{text-align:center!important}@media (width>=48rem){.center:is(td){text-align:center!important}}.top{vertical-align:top!important}@media (width>=48rem){.top:is(td){vertical-align:top!important}}.bottom{vertical-align:bottom!important}@media (width>=48rem){.bottom:is(td){vertical-align:bottom!important}}.middle{vertical-align:middle!important}@media (width>=48rem){.middle:is(td){vertical-align:middle!important}}.nowrap:not(span){white-space:nowrap}.fade-in,.fade-out{transition:opacity .25s ease-in-out}@starting-style{.fade-in{opacity:0}}.fade-out{pointer-events:none;opacity:0}.spinning{position:relative;padding-left:calc(var(--spacing) * 7)!important}.spinning:before{content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);top:50%;left:calc(var(--spacing) * 1.5);content:var(--tw-content);width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4);content:var(--tw-content);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y);content:var(--tw-content);animation:var(--animate-spin);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);content:var(--tw-content);background-color:currentColor;position:absolute;-webkit-mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-loader-2'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M21%2012a9%209%200%201%201-6.219-8.56'/%3e%3c/svg%3e");mask-image:url("data:image/svg+xml,%3c!--%20@license%20lucide-static%20v1.16.0%20-%20ISC%20--%3e%3csvg%20class='lucide%20lucide-loader-2'%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3cpath%20d='M21%2012a9%209%200%201%201-6.219-8.56'/%3e%3c/svg%3e");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}#view>.spinning:first-child{min-height:calc(var(--spacing) * 48);place-items:center;width:fit-content;margin-inline:auto;display:grid}html{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}html[data-darkmode=true]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}.card-header img,.login-input-icon img,.login-pw-toggle img,img.shadcn-icon{filter:brightness(0)saturate()}html[data-darkmode=true] .card-header img,html[data-darkmode=true] .login-input-icon img,html[data-darkmode=true] .login-pw-toggle img,html[data-darkmode=true] img.shadcn-icon{filter:brightness(0)saturate()invert()}.card-header img,img.shadcn-icon,#sidebar img.shadcn-icon{opacity:.65}html[data-darkmode=true] #sidebar img.shadcn-icon{opacity:.7}.card-header button:hover img,#sidebar .sidebar-nav-item.active img,#sidebar .sidebar-accordion-item.active img,#sidebar .sidebar-nav-parent:hover img{opacity:1}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:1ms!important;transition-delay:0s!important;animation-duration:1ms!important;animation-delay:0s!important}.spinning:before{animation-duration:1s!important}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-border-spacing-x{syntax:"";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}} diff --git a/luci-theme-vantage/Makefile b/luci-theme-vantage/Makefile index 234c1814c..4922fe052 100644 --- a/luci-theme-vantage/Makefile +++ b/luci-theme-vantage/Makefile @@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-theme-vantage -PKG_VERSION:=1.0.11 -PKG_RELEASE:=26 +PKG_VERSION:=1.0.12 +PKG_RELEASE:=27 PKG_LICENSE:=GPL-3.0-or-later LUCI_TITLE:=Vantage theme diff --git a/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css b/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css index e47007807..01684d33e 100644 --- a/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css +++ b/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css @@ -1743,6 +1743,9 @@ table { border-collapse: separate; border-spacing: 0; width: 100%; } div.right, div.left { display: block; } div.right { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-4); } div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0; } +/* a .left and .right pair on one line (the upload dialog's Browse… and + Cancel/Upload): the right group keeps to the line */ +#modal_overlay .modal div.left + div.right { margin-top: 0; } /* Tabs: one segmented control for every level, the menu's page tabs (.vt-pagetabs in #vt-tabs), ui.tabs at page level, in a form or modal, @@ -2509,11 +2512,13 @@ body[data-page^="admin-status-channel_analysis"] #view [data-tab-title]:not([dat #view .tr.v-ca-own > .td:nth-child(2) { font-weight: 600; } @media (max-width: 720px) { + /* on a phone: the name, the current value, then average and peak on a + line each, so wide values (332.59 Mibit/s) never run into the labels */ body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr { - grid-template-columns: minmax(0, 1fr) max-content minmax(0, 1fr) max-content minmax(0, 1fr); + grid-template-columns: max-content minmax(0, 1fr); row-gap: 6px; padding: 10px 0; } - body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:nth-child(1) { grid-column: 1 / -1; } + body[data-page^="admin-status-realtime"] #view .table:has(strong[style*="border-bottom"]) > .tr > .td:is(:nth-child(1), :nth-child(2)) { grid-column: 1 / -1; } body[data-page^="admin-status-realtime"] #view #settings-row { flex-wrap: wrap; } body[data-page^="admin-status-realtime"] #view #settings-row > * { flex: 1 1 100% !important; } body[data-page^="admin-status-realtime"] #view #settings-row > .right > .btn { margin-left: 0; } diff --git a/vless-core-cli/Makefile b/vless-core-cli/Makefile index e6dcf7f9d..6f299ce1a 100644 --- a/vless-core-cli/Makefile +++ b/vless-core-cli/Makefile @@ -2,7 +2,9 @@ include $(TOPDIR)/rules.mk PKG_NAME:=vless-core-cli PKG_VERSION:=2.0.0 -PKG_RELEASE:=2 +PKG_RELEASE:=3 +PKG_GIT_DIR:=$(dir $(firstword $(MAKEFILE_LIST))) +PKG_GIT_COMMIT:=$(shell git -C $(PKG_GIT_DIR) rev-parse --short HEAD 2>/dev/null || echo unknown) PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/notfence/vless-core-cli/tar.gz/v$(PKG_VERSION)? @@ -37,6 +39,7 @@ endef define Build/Compile $(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_CPPFLAGS) -std=c11 -Wall \ -I$(PKG_BUILD_DIR)/include $(PKG_BUILD_DIR)/src/*.c \ + -DVLESS_CORE_COMMIT='"git~$(PKG_GIT_COMMIT)"'\ -o $(PKG_BUILD_DIR)/vless-core \ $(TARGET_LDFLAGS) -lssl -lcrypto -lpthread endef