diff --git a/dae/Makefile b/dae/Makefile index bc735203b..571b0a8ea 100644 --- a/dae/Makefile +++ b/dae/Makefile @@ -5,10 +5,10 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dae -PKG_VERSION:=2026.09.24 -PKG_RELEASE:=49 +PKG_VERSION:=2026.09.30 +PKG_RELEASE:=50 -PKG_SOURCE:=dae-src-2026.09.24-17135da7c881.tar.gz +PKG_SOURCE:=dae-src-2026.09.30-30538527b146.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_HASH:=skip diff --git a/daed/Makefile b/daed/Makefile index d738e3a41..46167289d 100644 --- a/daed/Makefile +++ b/daed/Makefile @@ -5,10 +5,10 @@ include $(TOPDIR)/rules.mk PKG_NAME:=daed -PKG_VERSION:=2026.09.24 -PKG_RELEASE:=60 +PKG_VERSION:=2026.09.30 +PKG_RELEASE:=61 -PKG_SOURCE:=daed-src-2026.09.24-527bb166b1ae.tar.gz +PKG_SOURCE:=daed-src-2026.09.30-84f171fef875.tar.gz PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_HASH:=skip diff --git a/filebrowser-q/Makefile b/filebrowser-q/Makefile index 6ba1016cd..84a8cceba 100644 --- a/filebrowser-q/Makefile +++ b/filebrowser-q/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=filebrowser-q -PKG_VERSION:=2.0.0-stable -PKG_RELEASE:=9 +PKG_VERSION:=2.0.1-stable +PKG_RELEASE:=10 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)? diff --git a/grpcurl/Makefile b/grpcurl/Makefile index 659716716..75d7d3e65 100644 --- a/grpcurl/Makefile +++ b/grpcurl/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=grpcurl PKG_VERSION:=1.9.4 -PKG_RELEASE:=7 +PKG_RELEASE:=8 PKG_SOURCE:=v$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://github.com/fullstorydev/grpcurl/archive/refs/tags/ @@ -21,7 +21,7 @@ PKG_MAINTAINER:=Yannick Chabanois PKG_BUILD_DEPENDS:=golang/host PKG_BUILD_PARALLEL:=1 -PKG_USE_MIPS16:=0 +PKG_BUILD_FLAGS:=no-mips16 GO_PKG:=github.com/fullstorydev/grpcurl GO_PKG_BUILD_PKG:=github.com/fullstorydev/grpcurl/cmd/grpcurl diff --git a/headscale/Makefile b/headscale/Makefile index 8dc96de30..8e9b7ad3d 100644 --- a/headscale/Makefile +++ b/headscale/Makefile @@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=headscale -PKG_VERSION:=0.29.4 -PKG_RELEASE:=9 +PKG_VERSION:=0.29.5 +PKG_RELEASE:=10 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/juanfont/headscale/tar.gz/v$(PKG_VERSION)? diff --git a/luci-app-adguardhome/.official_channel b/luci-app-adguardhome/.official_channel index 8ab67f2e7..49f33a8c6 100644 --- a/luci-app-adguardhome/.official_channel +++ b/luci-app-adguardhome/.official_channel @@ -1 +1 @@ -snapshot +both diff --git a/luci-app-adguardhome/root/etc/adguardhome/adguardhome.yaml b/luci-app-adguardhome/root/etc/adguardhome/adguardhome.yaml index 7d78aeb87..a32a4d07a 100644 --- a/luci-app-adguardhome/root/etc/adguardhome/adguardhome.yaml +++ b/luci-app-adguardhome/root/etc/adguardhome/adguardhome.yaml @@ -105,7 +105,6 @@ querylog: - orirainbow.cc^ - orirainwe.edu.kg^ - coriseay.edu.kg^ - - xzbzcoriseay.win^ - google.com^ interval: 7d size_memory: 1000 @@ -118,7 +117,6 @@ statistics: - orirainbow.cc^ - orirainwe.edu.kg^ - coriseay.edu.kg^ - - xzbzcoriseay.win^ - google.com^ interval: 7d enabled: true @@ -137,6 +135,8 @@ user_rules: - '@@||ntfy.sh^' - '@@||pubnub.com^' - '@@||clarity.ms^$important' + - '@@||sh-gsg.com^$important' + - '@@||sh-mtgc.com^$important' - '@@||taobao.com^$important' - '@@||tmall.com^$important' - '@@||jd.com^important' @@ -164,7 +164,6 @@ user_rules: - '@@||miwifi.com^' - '@@||tracker.ai.xiaomi.com^$important' - '@@||tracking.miui.com^$important' - - '@@||xiaomiyoupin.com^' - '#解除 Apple 及其所有子域名的封锁;' - '@@||apple.com^' - '@@||apple.com.cn^' @@ -196,6 +195,9 @@ user_rules: - '@@||app-measurement.com^$important' - '@@||tmdb.org^' - '@@||themoviedb.org^' + - /googleads.$~script,domain=~googleads.github.io + - /pagead/lvz? + - '||google.com/pagead/' - '||static.doubleclick.net^$domain=youtube.com' - '||youtube.com/get_midroll_' - '||5hz.org^' @@ -230,27 +232,7 @@ user_rules: - '@@||1ptba.com^' - '@@||ptcafe.club^' - '||nbpicture.xyz^' - - '! ===== Gemini & NotebookLM & Google Labs 核心 API 放行 =====' - - '@@||notebooklm.google.com^' - - '@@||labs.google.com^' - - '@@||gemini.google.com^' - - '@@||alkalimakersuite-pa.googleapis.com^' - - '@@||generativelanguage.googleapis.com^' - - '! ===== Google 基础前端脚本与 API 通讯保障 =====' - - '@@||googleapis.com^' - - '@@||gstatic.com^' - - '@@||googleusercontent.com^' - - '@@||apis.google.com^' - - '@@||clients1.google.com^' - - '@@||clients2.google.com^' - - '@@||clients3.google.com^' - - '@@||clients4.google.com^' - - '@@||clients5.google.com^' - - '@@||clients6.google.com^' - - '! ===== 仅对 Gemini 域名排除以下广告拦截/过滤规则的影响 =====' - - /googleads.$~script,domain=~googleads.github.io&~gemini.google.com - - /pagead/lvz?$domain=~gemini.google.com - - '||google.com/pagead/^$domain=~gemini.google.com' + - "" dhcp: enabled: false interface_name: "" diff --git a/luci-app-daede/Makefile b/luci-app-daede/Makefile index b8e465c26..b3d60bc5c 100644 --- a/luci-app-daede/Makefile +++ b/luci-app-daede/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-daede PKG_VERSION:=1.15 -PKG_RELEASE:=50 +PKG_RELEASE:=51 PKG_MAINTAINER:=kenzok8 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) @@ -35,7 +35,6 @@ define Package/$(PKG_NAME) TITLE:=LuCI app for daede dashboard DEPENDS:=+luci-base +PACKAGE_$(PKG_NAME)_dae:dae +PACKAGE_$(PKG_NAME)_daed:daed PKGARCH:=all - MAINTAINER:=kenzok8 endef define Package/$(PKG_NAME)/description diff --git a/luci-app-iperf/Makefile b/luci-app-iperf/Makefile index 10069911a..f2eb12770 100644 --- a/luci-app-iperf/Makefile +++ b/luci-app-iperf/Makefile @@ -10,6 +10,10 @@ LUCI_DEPENDS:=+iperf3-ssl PKG_LICENSE:=GPLv3 +define Package/luci-app-iperf/conffiles +/etc/config/iperf +endef + #include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-iperf/luasrc/controller/iperf.lua b/luci-app-iperf/luasrc/controller/iperf.lua index 407d61430..ddb2386b8 100644 --- a/luci-app-iperf/luasrc/controller/iperf.lua +++ b/luci-app-iperf/luasrc/controller/iperf.lua @@ -28,8 +28,12 @@ function run_test(server,proto,mode,updown,omit,parallel,transmit,bitrate) parallel = tostring(parallel or "1"):match("^%d+$") or "1" omit = tostring(omit or "0"):match("^%d+$") or "0" transmit = tostring(transmit or "10"):match("^%d+$") or "10" - bitrate = tostring(bitrate or "1M") - if not bitrate:match("^[0-9]+[KMG]?$") then + -- The page asks for Mbit/s and sends a bare number, which iperf3 + -- reads as bit/s + bitrate = tostring(bitrate or "") + if bitrate:match("^%d+$") or bitrate:match("^%d+%.%d+$") then + bitrate = bitrate .. "M" + elseif not bitrate:match("^%d+[KMG]$") then bitrate = "1M" end -- With credentials omr-iperf runs the test: it writes the key to its own diff --git a/luci-app-mail/Makefile b/luci-app-mail/Makefile index 675424768..11ba04d76 100644 --- a/luci-app-mail/Makefile +++ b/luci-app-mail/Makefile @@ -10,6 +10,10 @@ LUCI_DEPENDS:=+msmtp-mta PKG_LICENSE:=GPLv3 +define Package/luci-app-mail/conffiles +/etc/config/mail +endef + #include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-modem-manager-next/Makefile b/luci-app-modem-manager-next/Makefile index 9b7431b6d..b37b3353e 100644 --- a/luci-app-modem-manager-next/Makefile +++ b/luci-app-modem-manager-next/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-modem-manager-next PKG_VERSION:=1.0.0 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_LICENSE:=MIT PKG_MAINTAINER:=Dante @@ -12,8 +12,7 @@ LUCI_DESCRIPTION:=Provides a modern LuCI Web management interface for 4G/5G cell PKG_DESCRIPTION:=Provides a modern LuCI Web management interface for 4G/5G cellular modems. LUCI_DEPENDS:=+ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-uci \ +kmod-usb-net +kmod-usb-serial +kmod-usb-serial-option \ - +kmod-usb-net-cdc-ncm +kmod-usb-net-cdc-mbim +kmod-usb-net-cdc-ether +kmod-usb-net-qmi-wwan \ - +umbim +uqmi +sms-tool + +kmod-usb-net-cdc-ncm +kmod-usb-net-cdc-ether +sms-tool LUCI_PKGARCH:=all include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/overview.js b/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/overview.js index e3bb1108a..224940160 100644 --- a/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/overview.js +++ b/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/overview.js @@ -58,18 +58,25 @@ return view.extend({ const self = this; if (!self.selectedPort) { ui.addNotification(null, E('p', {}, _('Please select a valid modem port first.')), 'error'); - return; + return Promise.resolve(); } ui.showModal(_('Executing Action'), [ E('p', { 'class': 'spinning' }, _('Sending action to cellular module, please wait...')) ]); - callDeviceAction(self.selectedPort, action).then(function(res) { + return callDeviceAction(self.selectedPort, action).then(function(res) { ui.hideModal(); if (res && res.success) { - ui.addNotification(null, E('p', {}, _('Action executed successfully.')), 'info'); - self.forceRefresh(); + if (action === 'reboot') { + ui.addNotification(null, E('p', {}, _('Modem reboot signal sent successfully. The module is rebooting, cellular network will reinitialize in 15-30 seconds.')), 'info'); + window.setTimeout(function() { + self.forceRefresh(); + }, 20000); + } else { + ui.addNotification(null, E('p', {}, _('Action executed successfully.')), 'info'); + self.forceRefresh(); + } } else { ui.addNotification(null, E('p', {}, _('Action failed: ') + (res ? res.output : _('Unknown error'))), 'error'); } @@ -81,18 +88,33 @@ return view.extend({ forceRefresh: function() { const self = this; - if (!self.selectedPort) return; - return callRefreshStatus(self.selectedPort, self.selectedNet).then(function(res) { - self.applyStatusDOM(res); + return callGetDevices().then(function(devRes) { + const devs = Array.isArray(devRes) ? devRes : ((devRes && Array.isArray(devRes.devices)) ? devRes.devices : []); + if (devs.length > 0) { + self.selectedPort = devs[0].primary_port; + self.selectedNet = devs[0].primary_net; + } + if (!self.selectedPort) return; + return callRefreshStatus(self.selectedPort, self.selectedNet).then(function(res) { + if (res && res.port && res.port !== self.selectedPort) { + self.selectedPort = res.port; + } + self.applyStatusDOM(res); + }); + }).catch(function(err) { + if (!self.selectedPort) return; + return callRefreshStatus(self.selectedPort, self.selectedNet).then(function(res) { + self.applyStatusDOM(res); + }); }); }, updateStatus: function() { const self = this; - if (!self.selectedPort) - return Promise.resolve(); - return callGetStatus(self.selectedPort, self.selectedNet).then(function(res) { + if (res && res.port && res.port !== self.selectedPort) { + self.selectedPort = res.port; + } self.applyStatusDOM(res); }).catch(function(e) {}); }, diff --git a/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/settings.js b/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/settings.js index afd899e42..1e36061c7 100644 --- a/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/settings.js +++ b/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/settings.js @@ -21,6 +21,12 @@ const callSaveConfig = rpc.declare({ params: [ 'port', 'baud' ] }); +const callDeviceAction = rpc.declare({ + object: 'luci.modem', + method: 'device_action', + params: [ 'port', 'action' ] +}); + const callSetMode = rpc.declare({ object: 'luci.modem', method: 'set_mode', @@ -151,13 +157,20 @@ return view.extend({ } } + const modeChanged = (self.currentDriverMode && modeVal !== self.currentDriverMode); + ui.showModal(_('Applying Configuration'), [ E('p', { 'class': 'spinning' }, _('Saving port settings and sending configuration commands, please wait...')) ]); - callSaveConfig(targetPort, baudVal) + const portToSave = modeChanged ? '' : targetPort; + + callSaveConfig(portToSave, baudVal) .then(function() { - return callSetMode(targetPort, modeVal); + if (modeChanged) { + return callSetMode(targetPort, modeVal); + } + return Promise.resolve({ success: true }); }) .then(function() { return callSetPreferences(targetPort, ratVal, bandsLte, bandsNr); @@ -169,11 +182,22 @@ return view.extend({ return callSetCellLock(targetPort, cellLockMode, cellLockEarfcn, cellLockPci); }) .then(function(lockRes) { - ui.hideModal(); - if (lockRes && lockRes.success === false) { - ui.addNotification(null, E('p', {}, _('Settings saved, but cell lock failed: ') + (lockRes.error || lockRes.output || '')), 'warning'); + if (modeChanged) { + ui.showModal(_('Switching Operation Mode'), [ + E('p', { 'class': 'spinning' }, _('Operation mode updated. The module is rebooting, please wait 20 seconds for bus re-enumeration and port re-scan...')) + ]); + callDeviceAction(targetPort, 'reboot').finally(function() { + window.setTimeout(function() { + location.reload(); + }, 20000); + }); } else { - ui.addNotification(null, E('p', {}, _('Modem settings successfully applied.')), 'info'); + ui.hideModal(); + if (lockRes && lockRes.success === false) { + ui.addNotification(null, E('p', {}, _('Settings saved, but cell lock failed: ') + (lockRes.error || lockRes.output || '')), 'warning'); + } else { + ui.addNotification(null, E('p', {}, _('Modem settings successfully applied.')), 'info'); + } } }) .catch(function(err) { @@ -314,29 +338,45 @@ return view.extend({ container.appendChild(portSection); // Protocol Mode Switching Section + let currentDriverMode = 'ncm'; + if (dev.driver === 'cdc_mbim') currentDriverMode = 'mbim'; + else if (dev.driver === 'qmi_wwan') currentDriverMode = 'qmi'; + else if (dev.driver === 'cdc_ether') currentDriverMode = 'ecm'; + else if (dev.driver === 'rndis_host') currentDriverMode = 'rndis'; + else if (dev.driver === 'cdc_ncm') currentDriverMode = 'ncm'; + self.currentDriverMode = currentDriverMode; + + const supportedModes = ['ncm', 'ecm']; + + const modeLabels = { + 'ncm': _('NCM Mode (Recommended)'), + 'ecm': _('ECM Mode') + }; + + const modeRadioNodes = []; + supportedModes.forEach(function(m) { + const isChecked = (currentDriverMode === m); + modeRadioNodes.push(E('label', { 'style': 'margin-right: 25px; font-weight: ' + (m === 'ncm' ? 'bold' : 'normal') + ';' }, [ + E('input', { + 'type': 'radio', + 'name': 'modem_mode', + 'value': m, + 'checked': isChecked ? 'checked' : null + }), ' ', + modeLabels[m] || m.toUpperCase() + ])); + }); + const modeSection = E('fieldset', { 'class': 'cbi-section' }, [ E('legend', {}, _('Driver Protocol and Operation Mode')), E('div', { 'class': 'cbi-section-node' }, [ E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Driver Mode')), E('div', { 'class': 'cbi-value-field' }, [ - E('label', { 'style': 'margin-right: 20px;' }, [ - E('input', { 'type': 'radio', 'name': 'modem_mode', 'value': 'ncm', 'checked': 'checked' }), ' ', - _('NCM Mode') - ]), - E('label', { 'style': 'margin-right: 20px;' }, [ - E('input', { 'type': 'radio', 'name': 'modem_mode', 'value': 'mbim' }), ' ', - _('MBIM Mode') - ]), - E('label', { 'style': 'margin-right: 20px;' }, [ - E('input', { 'type': 'radio', 'name': 'modem_mode', 'value': 'qmi' }), ' ', - _('QMI Mode') - ]), - E('label', {}, [ - E('input', { 'type': 'radio', 'name': 'modem_mode', 'value': 'ecm' }), ' ', - _('ECM Mode') - ]), - E('div', { 'class': 'cbi-value-description' }, _('Switching the operation mode requires restarting the modem to take effect.')) + E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 15px; align-items: center;' }, modeRadioNodes), + E('div', { 'class': 'cbi-value-description', 'style': 'margin-top: 8px;' }, + _('Switching the operation mode requires restarting the modem to take effect. NCM provides high-speed throughput with low CPU overhead, while ECM provides standard compatibility.') + ) ]) ]) ]) diff --git a/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/sms.js b/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/sms.js index 8942834ff..0d2d03559 100644 --- a/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/sms.js +++ b/luci-app-modem-manager-next/htdocs/luci-static/resources/view/modem/sms.js @@ -156,7 +156,7 @@ return view.extend({ }, [ E('div', { 'style': 'display: flex; align-items: center; gap: 20px;' }, [ E('div', {}, [ - E('span', { 'style': 'color: #333; font-size: 13px;' }, _('SIM Card Storage Capacity: ')), + E('span', { 'style': 'color: #333; font-size: 13px;' }, _('SIM Card Storage Capacity') + ': '), E('strong', { 'style': 'color: #333; font-size: 14px;' }, self.smsCapacity.total > 0 ? (self.smsCapacity.used + ' / ' + self.smsCapacity.total + ' ' + _('Messages')) : (self.smsList.length + ' ' + _('Messages'))) ]), diff --git a/luci-app-modem-manager-next/po/zh_Hans/modem-manager.po b/luci-app-modem-manager-next/po/zh_Hans/modem-manager.po index aee0f92c4..45c5c69b8 100644 --- a/luci-app-modem-manager-next/po/zh_Hans/modem-manager.po +++ b/luci-app-modem-manager-next/po/zh_Hans/modem-manager.po @@ -193,6 +193,9 @@ msgstr "正在向模组发送操作指令,请稍候..." msgid "Action executed successfully." msgstr "动作执行成功。" +msgid "Modem reboot signal sent successfully. The module is rebooting, cellular network will reinitialize in 15-30 seconds." +msgstr "模组重启指令已成功下发。模组正在重启,蜂窝网络将在 15-30 秒内重新初始化。" + msgid "Action failed: " msgstr "动作执行失败: " @@ -238,17 +241,20 @@ msgstr "MBIM模式" msgid "NCM Mode" msgstr "NCM模式" -msgid "QMI Mode" -msgstr "QMI模式" +msgid "NCM Mode (Recommended)" +msgstr "推荐NCM模式" msgid "ECM Mode" msgstr "ECM模式" -msgid "RNDIS Mode" -msgstr "RNDIS模式" +msgid "Switching the operation mode requires restarting the modem to take effect. NCM provides high-speed throughput with low CPU overhead, while ECM provides standard compatibility." +msgstr "切换工作模式需要重启模组生效。NCM模式具备低CPU负载的高速聚合吞吐,ECM模式提供通用以太网兼容性。" -msgid "Switching the operation mode requires restarting the modem to take effect." -msgstr "切换工作模式后需要重新复位模组方能使新驱动生效。" +msgid "Switching Operation Mode" +msgstr "切换工作模式" + +msgid "Operation mode updated. The module is rebooting, please wait 20 seconds for bus re-enumeration and port re-scan..." +msgstr "工作模式已更新。模组正在重启,请等待20秒以完成总线重枚举与端口重新扫描..." msgid "Sync OpenWrt Network" msgstr "同步网络配置" @@ -490,6 +496,9 @@ msgstr "停用功能" msgid "SIM Card Storage Capacity: " msgstr "SIM卡存储容量: " +msgid "SIM Card Storage Capacity" +msgstr "SIM卡存储容量" + msgid "Messages" msgstr "条短信" diff --git a/luci-app-modem-manager-next/po/zh_Hans/modem.po b/luci-app-modem-manager-next/po/zh_Hans/modem.po index aee0f92c4..45c5c69b8 100644 --- a/luci-app-modem-manager-next/po/zh_Hans/modem.po +++ b/luci-app-modem-manager-next/po/zh_Hans/modem.po @@ -193,6 +193,9 @@ msgstr "正在向模组发送操作指令,请稍候..." msgid "Action executed successfully." msgstr "动作执行成功。" +msgid "Modem reboot signal sent successfully. The module is rebooting, cellular network will reinitialize in 15-30 seconds." +msgstr "模组重启指令已成功下发。模组正在重启,蜂窝网络将在 15-30 秒内重新初始化。" + msgid "Action failed: " msgstr "动作执行失败: " @@ -238,17 +241,20 @@ msgstr "MBIM模式" msgid "NCM Mode" msgstr "NCM模式" -msgid "QMI Mode" -msgstr "QMI模式" +msgid "NCM Mode (Recommended)" +msgstr "推荐NCM模式" msgid "ECM Mode" msgstr "ECM模式" -msgid "RNDIS Mode" -msgstr "RNDIS模式" +msgid "Switching the operation mode requires restarting the modem to take effect. NCM provides high-speed throughput with low CPU overhead, while ECM provides standard compatibility." +msgstr "切换工作模式需要重启模组生效。NCM模式具备低CPU负载的高速聚合吞吐,ECM模式提供通用以太网兼容性。" -msgid "Switching the operation mode requires restarting the modem to take effect." -msgstr "切换工作模式后需要重新复位模组方能使新驱动生效。" +msgid "Switching Operation Mode" +msgstr "切换工作模式" + +msgid "Operation mode updated. The module is rebooting, please wait 20 seconds for bus re-enumeration and port re-scan..." +msgstr "工作模式已更新。模组正在重启,请等待20秒以完成总线重枚举与端口重新扫描..." msgid "Sync OpenWrt Network" msgstr "同步网络配置" @@ -490,6 +496,9 @@ msgstr "停用功能" msgid "SIM Card Storage Capacity: " msgstr "SIM卡存储容量: " +msgid "SIM Card Storage Capacity" +msgstr "SIM卡存储容量" + msgid "Messages" msgstr "条短信" diff --git a/luci-app-modem-manager-next/root/usr/libexec/modem-at-executor b/luci-app-modem-manager-next/root/usr/libexec/modem-at-executor index b799e9cfa..b1b38baee 100755 --- a/luci-app-modem-manager-next/root/usr/libexec/modem-at-executor +++ b/luci-app-modem-manager-next/root/usr/libexec/modem-at-executor @@ -79,13 +79,43 @@ fi # Fast path: Prioritize native sms_tool engine if installed (zero echo loop, robust termios) if command -v sms_tool >/dev/null 2>&1; then - if [ -n "$SMS_TO" ]; then - OUT=$(sms_tool -d "$DEVICE" -b "$BAUDRATE" send "$SMS_TO" "$SMS_MSG" 2>&1) - RC=$? + TMP_OUT="/tmp/at_sms_$$.tmp" + TMP_DONE="/tmp/at_sms_$$.done" + rm -f "$TMP_OUT" "$TMP_DONE" + + ( + if [ -n "$SMS_TO" ]; then + sms_tool -d "$DEVICE" -b "$BAUDRATE" send "$SMS_TO" "$SMS_MSG" > "$TMP_OUT" 2>&1 + else + sms_tool -d "$DEVICE" -b "$BAUDRATE" at "$COMMAND" > "$TMP_OUT" 2>&1 + fi + echo $? > "$TMP_DONE" + ) & + CHILD_PID=$! + + elapsed=0 + while [ $elapsed -lt "$TIMEOUT" ]; do + if [ -f "$TMP_DONE" ] || ! kill -0 "$CHILD_PID" 2>/dev/null; then + break + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + + if [ ! -f "$TMP_DONE" ] && kill -0 "$CHILD_PID" 2>/dev/null; then + kill -9 "$CHILD_PID" 2>/dev/null || true + for p in $(pgrep -f "sms_tool.*$DEVICE" 2>/dev/null); do + kill -9 "$p" 2>/dev/null || true + done + RC=124 + OUT="ERROR: Command timed out after ${TIMEOUT}s" else - OUT=$(sms_tool -d "$DEVICE" -b "$BAUDRATE" at "$COMMAND" 2>&1) - RC=$? + wait "$CHILD_PID" 2>/dev/null + RC=$(cat "$TMP_DONE" 2>/dev/null || echo 0) + OUT=$(cat "$TMP_OUT" 2>/dev/null) fi + rm -f "$TMP_OUT" "$TMP_DONE" + flock -u 9 2>/dev/null || true CLEAN_OUT=$(echo "$OUT" | tr -d "\r") if [ $RC -eq 0 ] && ! echo "$CLEAN_OUT" | grep -qE "ERROR|\+CME ERROR|\+CMS ERROR"; then @@ -100,6 +130,14 @@ if command -v sms_tool >/dev/null 2>&1; then fi exit 0 else + case "$COMMAND" in + *CFUN=1,1*|*CFUN=16*|*QPOWD*) + if ! echo "$CLEAN_OUT" | grep -qE "ERROR|\+CME ERROR|\+CMS ERROR"; then + [ -z "$CLEAN_OUT" ] && echo "OK" || echo "$CLEAN_OUT" + exit 0 + fi + ;; + esac echo "$CLEAN_OUT" exit ${RC:-1} fi diff --git a/luci-app-modem-manager-next/root/usr/libexec/modem-collector b/luci-app-modem-manager-next/root/usr/libexec/modem-collector index 6f1d139b9..03207e184 100755 --- a/luci-app-modem-manager-next/root/usr/libexec/modem-collector +++ b/luci-app-modem-manager-next/root/usr/libexec/modem-collector @@ -39,11 +39,14 @@ collect_single() { let devs_data = plugin.get_devices.call(); let dev = (devs_data?.devices && length(devs_data.devices) > 0) ? devs_data.devices[0] : null; let user_port = "'"$user_port"'"; - let target_port = (user_port && user_port != "") ? user_port : (dev?.primary_port || null); + let target_port = dev?.primary_port || user_port || null; + if (dev?.driver !== "cdc_mbim" && user_port && user_port != "") { + target_port = user_port; + } if (target_port) { let status = plugin.collect_raw_status.call({ args: { port: target_port, primary_net: dev?.primary_net || "usb0" } }); - if (status) { + if (status && status.success) { let tmp = "/tmp/run/modem-manager/status.json.tmp"; fs.writefile(tmp, sprintf("%J\n", status)); fs.rename(tmp, "/tmp/run/modem-manager/status.json"); diff --git a/luci-app-modem-manager-next/root/usr/libexec/modem-detect b/luci-app-modem-manager-next/root/usr/libexec/modem-detect index 77d7ddc4f..6a3244aa4 100755 --- a/luci-app-modem-manager-next/root/usr/libexec/modem-detect +++ b/luci-app-modem-manager-next/root/usr/libexec/modem-detect @@ -116,6 +116,19 @@ detect_all() { local PROD=$(get_sys_attr "$dev/product") local SER=$(get_sys_attr "$dev/serial") + local DRIVER="option" + for net_iface in $NETS; do + if [ -e "/sys/class/net/$net_iface/device/driver" ]; then + local drv_target=$(basename "$(readlink -f "/sys/class/net/$net_iface/device/driver" 2>/dev/null)") + case "$drv_target" in + cdc_ncm|cdc_ether|cdc_mbim|qmi_wwan) + DRIVER="$drv_target" + break + ;; + esac + fi + done + local PRIMARY_TTY="" for t in $TTYS; do case "$t" in @@ -125,20 +138,19 @@ detect_all() { ;; esac done + if [ -z "$PRIMARY_TTY" ]; then + for t in $TTYS; do + case "$t" in + *ttyUSB3) + PRIMARY_TTY="$t" + break + ;; + esac + done + fi [ -z "$PRIMARY_TTY" ] && PRIMARY_TTY=$(echo "$TTYS" | awk '{print $1}') local PRIMARY_NET=$(echo "$NETS" | awk '{print $1}') - local DRIVER="option" - for drv in $(find "$dev" -name "driver" 2>/dev/null); do - local drv_target=$(basename "$(readlink -f "$drv" 2>/dev/null)") - case "$drv_target" in - cdc_mbim|cdc_ncm|qmi_wwan|cdc_ether|rndis_host) - DRIVER="$drv_target" - break - ;; - esac - done - [ $FIRST -eq 1 ] || echo "," FIRST=0 @@ -154,6 +166,8 @@ detect_all() { NET_JSON="$NET_JSON\"$n\"" done + local MODES_JSON="\"ncm\", \"ecm\"" + cat < 0) ? data.devices[0] : null; + + if (specified_port && match(specified_port, /^\/dev\/tty(USB|ACM)[0-9]+$/)) { + if (!dev || !dev.ports || index(dev.ports, specified_port) >= 0) + return specified_port; + } + + let c = cursor(); + if (c) { + c.load('modem-manager'); + let p = c.get('modem-manager', 'global', 'port'); + if (p && match(p, /^\/dev\/tty(USB|ACM)[0-9]+$/)) { + if (!dev || !dev.ports || index(dev.ports, p) >= 0) + return p; + } + } + + if (dev && dev.primary_port) + return dev.primary_port; + + return specified_port || '/dev/ttyUSB2'; +} + +function safe_exec_at(port, cmd, timeout) { if (!cmd) return { success: false, output: 'Empty command' }; + let effective_port = get_effective_port(port); + if (!effective_port || !match(effective_port, /^\/dev\/tty(USB|ACM)[0-9]+$/)) + return { success: false, output: 'Invalid port parameter' }; + let to = (timeout && timeout > 0 && timeout <= 30) ? int(timeout) : 3; let clean_cmd = replace(cmd, /'/g, "'\\''"); - let p = fs.popen(`${EXECUTOR_BIN} -d '${port}' -c '${clean_cmd}' -t ${to} 2>&1`, 'r'); + let p = fs.popen(`${EXECUTOR_BIN} -d '${effective_port}' -c '${clean_cmd}' -t ${to} 2>&1`, 'r'); if (!p) return { success: false, output: 'Failed to execute command helper' }; @@ -51,25 +89,6 @@ function safe_exec_at(port, cmd, timeout) { return { success: success, output: trim(output) }; } -function get_effective_port(specified_port) { - if (specified_port && match(specified_port, /^\/dev\/tty(USB|ACM)[0-9]+$/)) - return specified_port; - let c = cursor(); - if (c) { - c.load('modem-manager'); - let p = c.get('modem-manager', 'global', 'port'); - if (p && match(p, /^\/dev\/tty(USB|ACM)[0-9]+$/)) - return p; - } - let dev_content = fs.readfile(CACHE_DEVICES); - if (dev_content) { - let data = json(dev_content); - if (data?.devices && length(data.devices) > 0 && data.devices[0].primary_port) - return data.devices[0].primary_port; - } - return null; -} - function decode_ucs2_if_needed(text) { if (!text || length(text) < 4 || (length(text) % 4) != 0) return text; @@ -206,11 +225,24 @@ function parse_serving_cell(raw_output) { } function do_collect_status(port, net_dev) { + let get_fallback_or_yield = function() { + let cached = fs.readfile(CACHE_STATUS); + return cached ? json(cached) : { success: false, yielded: true }; + }; + + if (is_terminal_active()) + return get_fallback_or_yield(); + let info_res = safe_exec_at(port, 'ATI', 2); + if (is_terminal_active()) return get_fallback_or_yield(); let cpin_res = safe_exec_at(port, 'AT+CPIN?', 2); + if (is_terminal_active()) return get_fallback_or_yield(); let csq_res = safe_exec_at(port, 'AT+CSQ', 2); + if (is_terminal_active()) return get_fallback_or_yield(); let cops_res = safe_exec_at(port, 'AT+COPS?', 2); + if (is_terminal_active()) return get_fallback_or_yield(); let qeng_res = safe_exec_at(port, 'AT+QENG="servingcell"', 3); + if (is_terminal_active()) return get_fallback_or_yield(); let temp_res = safe_exec_at(port, 'AT+QTEMP', 2); let csq_data = parse_csq(csq_res.output); @@ -388,7 +420,7 @@ function do_collect_status(port, net_dev) { temperature: temp, network: net_info, raw_info: info_res.output, - updated_at: fs.popen("date +%s", 'r')?.read('all')?.trim() || 0 + updated_at: time() }; } @@ -434,7 +466,9 @@ const methods = { let user_port = c.get('modem-manager', 'global', 'port'); if (user_port && user_port != '' && res.devices && length(res.devices) > 0) { res.devices[0].user_configured_port = user_port; - res.devices[0].primary_port = user_port; + if (res.devices[0].driver !== 'cdc_mbim' || user_port === res.devices[0].primary_port) { + res.devices[0].primary_port = user_port; + } } } @@ -464,7 +498,7 @@ const methods = { collect_raw_status: { args: { port: '', primary_net: '' }, call: function(req) { - let port = req.args?.port || '/dev/ttyUSB2'; + let port = get_effective_port(req.args?.port); let net_dev = req.args?.primary_net || 'usb0'; return do_collect_status(port, net_dev); } @@ -473,7 +507,7 @@ const methods = { get_status: { args: { port: '', primary_net: '' }, call: function(req) { - let port = req.args?.port || '/dev/ttyUSB2'; + let port = get_effective_port(req.args?.port); let net_dev = req.args?.primary_net || 'usb0'; // Non-blocking fast path: read decoupled background status cache directly @@ -490,8 +524,10 @@ const methods = { // Cold start fallback: synchronously collect once and write cache let status = do_collect_status(port, net_dev); - fs.mkdir(RUN_DIR); - fs.writefile(CACHE_STATUS, sprintf("%J\n", status)); + if (status && status.success) { + fs.mkdir(RUN_DIR); + fs.writefile(CACHE_STATUS, sprintf("%J\n", status)); + } return status; } }, @@ -499,11 +535,13 @@ const methods = { refresh_status: { args: { port: '', primary_net: '' }, call: function(req) { - let port = req.args?.port || '/dev/ttyUSB2'; + let port = get_effective_port(req.args?.port); let net_dev = req.args?.primary_net || 'usb0'; let status = do_collect_status(port, net_dev); - fs.mkdir(RUN_DIR); - fs.writefile(CACHE_STATUS, sprintf("%J\n", status)); + if (status && status.success) { + fs.mkdir(RUN_DIR); + fs.writefile(CACHE_STATUS, sprintf("%J\n", status)); + } return status; } }, @@ -522,10 +560,10 @@ const methods = { touch_terminal_active(); log_message(`[AT] >>> ${cmd}`); - let t0 = fs.popen("date +%s%3N 2>/dev/null || date +%s", 'r')?.read('all')?.trim() || 0; + let t0 = time(); let res = safe_exec_at(port, cmd, timeout); - let t1 = fs.popen("date +%s%3N 2>/dev/null || date +%s", 'r')?.read('all')?.trim() || 0; - let duration_ms = (t1 > 0 && t0 > 0 && t1 >= t0) ? (int(t1) - int(t0)) : 0; + let t1 = time(); + let duration_ms = (t1 >= t0) ? ((t1 - t0) * 1000) : 0; log_message(`[AT] <<< ${res.output}`); return { @@ -558,11 +596,8 @@ const methods = { touch_terminal_active(); let mode_map = { - 'qmi': '0', - 'ecm': '1', - 'mbim': '2', - 'rndis': '3', - 'ncm': '5' + 'ncm': '5', + 'ecm': '1' }; let val = mode_map[mode] || '5'; let cmd = `AT+QCFG="usbnet",${val}`; @@ -760,8 +795,10 @@ const methods = { touch_terminal_active(); let cmd = 'AT'; + let timeout = 5; if (action === 'reboot') { cmd = 'AT+CFUN=1,1'; + timeout = 10; } else if (action === 'airplane_on') { cmd = 'AT+CFUN=4'; } else if (action === 'airplane_off') { @@ -772,7 +809,14 @@ const methods = { } log_message(`[ACTION] Executing ${action} via ${cmd}`); - let res = safe_exec_at(port, cmd, 5); + let res = safe_exec_at(port, cmd, timeout); + + if (action === 'reboot') { + let is_err = (res.output && match(res.output, /\bERROR\b|\+CME ERROR|\+CMS ERROR/i) != null); + if (!is_err) { + return { success: true, output: res.output || 'OK' }; + } + } return { success: res.success, output: res.output }; } diff --git a/luci-app-mptcp/Makefile b/luci-app-mptcp/Makefile index 6b3b16203..536a91ef5 100644 --- a/luci-app-mptcp/Makefile +++ b/luci-app-mptcp/Makefile @@ -6,6 +6,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI Support for MPTCP +LUCI_DEPENDS:=+mptcp +ip-full +ss +nstat +tracebox +omr-sockdiag PKG_LICENSE:=GPLv3 #include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-mptcp/root/usr/share/rpcd/acl.d/luci-app-mptcp.json b/luci-app-mptcp/root/usr/share/rpcd/acl.d/luci-app-mptcp.json index dc1de0133..fb883e34a 100644 --- a/luci-app-mptcp/root/usr/share/rpcd/acl.d/luci-app-mptcp.json +++ b/luci-app-mptcp/root/usr/share/rpcd/acl.d/luci-app-mptcp.json @@ -2,7 +2,7 @@ "luci-app-mptcp": { "description": "Grant UCI access for luci-app-mptcp", "read": { - "uci": [ "network", "mqvpn" ], + "uci": [ "network" ], "file": { "/usr/lib/mptcpd": [ "list" ], "/usr/share/bpf/scheduler": [ "list" ], diff --git a/luci-app-mwan3-nft/Makefile b/luci-app-mwan3-nft/Makefile deleted file mode 100644 index c2d69c1f4..000000000 --- a/luci-app-mwan3-nft/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright (C) 2017 Dan Luedtke -# -# This is free software, licensed under the GNU General Public License v2. - -include $(TOPDIR)/rules.mk - -LUCI_TITLE:=LuCI support for the MWAN3 MultiWAN Manager -LUCI_DEPENDS:=+luci-base +mwan3-nft -PKG_LICENSE:=GPL-2.0 - -PKG_MAINTAINER:=Florian Eckert - -include $(TOPDIR)/feeds/luci/luci.mk - -# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/mwan3.css b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/mwan3.css deleted file mode 100644 index ebe7764c8..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/mwan3.css +++ /dev/null @@ -1,8 +0,0 @@ -#mwan3-service-status > .alert-message { - display: inline-block; - margin: 1rem; - padding: 1rem; - width: 15rem; - height: 6rem; - vertical-align: middle; -} diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/globals.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/globals.js deleted file mode 100644 index bcf58138c..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/globals.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; -'require form'; -'require view'; - -return view.extend({ - - render: function () { - let m, s, o; - - m = new form.Map('mwan3', _('MultiWAN Manager - Globals')); - - s = m.section(form.NamedSection, 'globals', 'globals'); - - o = s.option(form.Value, 'mmx_mask', _('Firewall mask'), - _('Enter value in hex, starting with 0x')); - o.datatype = 'hex(4)'; - o.default = '0x3F00'; - - o = s.option(form.Flag, 'logging', _('Logging'), - _('Enables global firewall logging')); - - o = s.option(form.ListValue, 'loglevel', _('Loglevel'), - _('Firewall loglevel')); - o.default = 'notice'; - o.value('emerg', _('Emergency')); - o.value('alert', _('Alert')); - o.value('crit', _('Critical')); - o.value('error', _('Error')); - o.value('warning', _('Warning')); - o.value('notice', _('Notice')); - o.value('info', _('Info')); - o.value('debug', _('Debug')); - o.depends('logging', '1'); - - o = s.option(form.DynamicList, 'rt_table_lookup', - _('Routing table lookup'), - _('Also scan this Routing table for connected networks')); - o.datatype = 'uinteger'; - o.value('220', _('Routing table %d').format('220')); - - o = s.option(form.Flag, 'show_overview', _('Show on overview'), - _('Show MultiWAN status section on the overview page')); - o.default = o.enabled; - o.rmempty = false; - - return m.render(); - } -}) diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/interface.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/interface.js deleted file mode 100644 index 2761a8674..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/interface.js +++ /dev/null @@ -1,324 +0,0 @@ -'use strict'; -'require form'; -'require fs'; -'require view'; -'require uci'; -'require ui'; - -return view.extend({ - load: function() { - return Promise.all([ - L.resolveDefault(fs.stat('/usr/bin/httping'), {}), - L.resolveDefault(fs.stat('/usr/bin/nping'), {}), - L.resolveDefault(fs.stat('/usr/bin/arping'), {}), - uci.load('network') - ]); - }, - - render: function (stats) { - let m, s, o; - - m = new form.Map('mwan3', _('MultiWAN Manager - Interfaces'), - _('Mwan3 requires that all interfaces have a unique metric configured in /etc/config/network.') + '
' + - _('Names must match the interface name found in /etc/config/network.') + '
' + - _('Names may contain characters A-Z, a-z, 0-9, _ and no spaces-') + '
' + - _('Interfaces may not share the same name as configured members, policies or rules.')); - - s = m.section(form.GridSection, 'interface'); - s.addremove = true; - s.anonymous = false; - s.nodescriptions = true; - - s.renderSectionAdd = function(extra_class) { - var usedNames = [ - ...uci.sections('mwan3', 'interface'), - ...uci.sections('mwan3', 'member'), - ...uci.sections('mwan3', 'policy'), - ...uci.sections('mwan3', 'rule') - ].map(function(sec) { return sec['.name']; }); - - var available = uci.sections('network', 'interface') - .map(function(sec) { return sec['.name']; }) - .filter(function(name) { - return name !== 'loopback' && - name.length <= 15 && - usedNames.indexOf(name) === -1; - }); - - var el = E('div', { 'class': 'cbi-section-create' + (extra_class ? ' ' + extra_class : '') }, [ - E('div', { 'class': 'cbi-section-create-name-container' }, [ - E('select', { - 'class': 'cbi-section-create-name', - 'style': 'min-width: 12em' - }, available.length - ? available.map(function(name) { - return E('option', { 'value': name }, name); - }) - : [E('option', { 'value': '', 'disabled': true, 'selected': true }, - _('No available interfaces'))] - ), - E('button', { - 'class': 'cbi-button cbi-button-add', - 'title': _('Add'), - 'disabled': available.length === 0 || null, - 'click': ui.createHandlerFn(this, function(ev) { - var select = ev.target.closest('.cbi-section-create') - .querySelector('select.cbi-section-create-name'); - var name = select ? select.value : ''; - if (!name) return; - return this.handleAdd(ev, name); - }) - }, _('Add')) - ]) - ]); - - return el; - }; - - o = s.option(form.Flag, 'enabled', _('Enabled')); - o.default = false; - - o = s.option(form.ListValue, 'initial_state', _('Initial state'), - _('Expect interface state on up event')); - o.default = 'online'; - o.value('online', _('Online')); - o.value('offline', _('Offline')); - o.modalonly = true; - - o = s.option(form.ListValue, 'family', _('Internet Protocol')); - o.default = 'ipv4'; - o.value('ipv4', _('IPv4')); - o.value('ipv6', _('IPv6')); - o.modalonly = true; - - o = s.option(form.DynamicList, 'track_ip', _('Tracking hostname or IP address'), - _('This hostname or IP address will be pinged to determine if the link is up or down. Leave blank to assume interface is always online')); - o.datatype = 'host'; - o.modalonly = true; - - o = s.option(form.ListValue, 'track_method', _('Tracking method')); - o.default = 'ping'; - o.value('ping'); - if (stats[0].type === 'file') { - o.value('httping'); - } - if (stats[1].type === 'file') { - o.value('nping-tcp'); - o.value('nping-udp'); - o.value('nping-icmp'); - o.value('nping-arp'); - } - if (stats[2].type === 'file') { - o.value('arping'); - } - - o = s.option(form.Flag, 'httping_ssl', _('Enable ssl tracking'), - _('Enables https tracking on ssl port 443')); - o.depends('track_method', 'httping'); - o.rmempty = false; - o.modalonly = true; - - o = s.option(form.Value, 'reliability', _('Tracking reliability'), - _('Acceptable values: 1-100. This many Tracking IP addresses must respond for the link to be deemed up')); - o.datatype = 'range(1, 100)'; - o.default = '1'; - - o = s.option(form.ListValue, 'count', _('Ping count')); - o.default = '1'; - o.value('1'); - o.value('2'); - o.value('3'); - o.value('4'); - o.value('5'); - o.modalonly = true; - - o = s.option(form.Value, 'size', _('Ping size')); - o.default = '56'; - o.depends('track_method', 'ping'); - o.value('8'); - o.value('24'); - o.value('56'); - o.value('120'); - o.value('248'); - o.value('504'); - o.value('1016'); - o.value('1472'); - o.value('2040'); - o.datatype = 'range(1, 65507)'; - o.modalonly = true; - - o =s.option(form.Value, 'max_ttl', _('Max TTL')); - o.default = '60'; - o.depends('track_method', 'ping'); - o.value('10'); - o.value('20'); - o.value('30'); - o.value('40'); - o.value('50'); - o.value('60'); - o.value('70'); - o.datatype = 'range(1, 255)'; - o.modalonly = true; - - o = s.option(form.Flag, 'check_quality', _('Check link quality')); - o.depends('track_method', 'ping'); - o.default = false; - o.modalonly = true; - - o = s.option(form.Value, 'failure_latency', _('Failure latency [ms]')); - o.depends('check_quality', '1'); - o.default = '1000'; - o.value('25'); - o.value('50'); - o.value('75'); - o.value('100'); - o.value('150'); - o.value('200'); - o.value('250'); - o.value('300'); - o.modalonly = true; - - o = s.option(form.Value, 'failure_loss', _('Failure packet loss [%]')); - o.depends('check_quality', '1'); - o.default = '40'; - o.value('2'); - o.value('5'); - o.value('10'); - o.value('20'); - o.value('25'); - o.modalonly = true; - - o = s.option(form.Value, 'recovery_latency', _('Recovery latency [ms]')); - o.depends('check_quality', '1'); - o.default = '500'; - o.value('25'); - o.value('50'); - o.value('75'); - o.value('100'); - o.value('150'); - o.value('200'); - o.value('250'); - o.value('300'); - o.modalonly = true; - - o = s.option(form.Value, 'recovery_loss', _('Recovery packet loss [%]')); - o.depends('check_quality', '1'); - o.default = '10'; - o.value('2'); - o.value('5'); - o.value('10'); - o.value('20'); - o.value('25'); - o.modalonly = true; - - o = s.option(form.ListValue, "timeout", _("Ping timeout")); - o.default = '4'; - o.value('1', _('%d second').format('1')); - for (var i = 2; i <= 10; i++) - o.value(String(i), _('%d seconds').format(i)); - o.modalonly = true; - - o = s.option(form.ListValue, 'interval', _('Ping interval')); - o.default = '10'; - o.value('1', _('%d second').format('1')); - o.value('3', _('%d seconds').format('3')); - o.value('5', _('%d seconds').format('5')); - o.value('10', _('%d seconds').format('10')); - o.value('20', _('%d seconds').format('20')); - o.value('30', _('%d seconds').format('30')); - o.value('60', _('%d minute').format('1')); - o.value('300', _('%d minutes').format('5')); - o.value('600', _('%d minutes').format('10')); - o.value('900', _('%d minutes').format('15')); - o.value('1800', _('%d minutes').format('30')); - o.value('3600', _('%d hour').format('1')); - - o = s.option(form.Value, 'failure_interval', _('Failure interval'), - _('Ping interval during failure detection')); - o.default = '5'; - o.value('1', _('%d second').format('1')); - o.value('3', _('%d seconds').format('3')); - o.value('5', _('%d seconds').format('5')); - o.value('10', _('%d seconds').format('10')); - o.value('20', _('%d seconds').format('20')); - o.value('30', _('%d seconds').format('30')); - o.value('60', _('%d minute').format('1')); - o.value('300', _('%d minutes').format('5')); - o.value('600', _('%d minutes').format('10')); - o.value('900', _('%d minutes').format('15')); - o.value('1800', _('%d minutes').format('30')); - o.value('3600', _('%d hour').format('1')); - o.modalonly = true; - - o = s.option(form.Flag, 'keep_failure_interval', _('Keep failure interval'), - _('Keep ping failure interval during failure state')); - o.default = false; - o.modalonly = true; - - o = s.option(form.Value, 'recovery_interval', _('Recovery interval'), - _('Ping interval during failure recovering')); - o.default = '5'; - o.value('1', _('%d second').format('1')); - o.value('3', _('%d seconds').format('3')); - o.value('5', _('%d seconds').format('5')); - o.value('10', _('%d seconds').format('10')); - o.value('20', _('%d seconds').format('20')); - o.value('30', _('%d seconds').format('30')); - o.value('60', _('%d minute').format('1')); - o.value('300', _('%d minutes').format('5')); - o.value('600', _('%d minutes').format('10')); - o.value('900', _('%d minutes').format('15')); - o.value('1800', _('%d minutes').format('30')); - o.value('3600', _('%d hour').format('1')); - o.modalonly = true; - - o = s.option(form.ListValue, 'down', _('Interface down'), - _('Interface will be deemed down after this many failed ping tests')); - o.default = '5'; - o.value('1'); - o.value('2'); - o.value('3'); - o.value('4'); - o.value('5'); - o.value('6'); - o.value('7'); - o.value('8'); - o.value('9'); - o.value('10'); - - o = s.option(form.ListValue, 'up', _('Interface up'), - _('Downed interface will be deemed up after this many successful ping tests')); - o.default = "5"; - o.value('1'); - o.value('2'); - o.value('3'); - o.value('4'); - o.value('5'); - o.value('6'); - o.value('7'); - o.value('8'); - o.value('9'); - o.value('10'); - - o = s.option(form.DynamicList, 'flush_conntrack', _('Flush conntrack table'), - _('Flush global firewall conntrack table on interface events')); - o.value('ifup', _('ifup (netifd)')); - o.value('ifdown', _('ifdown (netifd)')); - o.value('connected', _('connected (mwan3)')); - o.value('disconnected', _('disconnected (mwan3)')); - o.modalonly = true; - - o = s.option(form.DummyValue, 'metric', _('Metric'), - _('This displays the metric assigned to this interface in /etc/config/network')); - o.rawhtml = true; - o.cfgvalue = function(s) { - var metric = uci.get('network', s, 'metric') - if (metric) - return metric; - else - return _('No interface metric set!'); - } - - return m.render(); - } -}) diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/member.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/member.js deleted file mode 100644 index 8a4195243..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/member.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; -'require form'; -'require view'; -'require uci'; -'require ui'; - -return view.extend({ - load: function() { - return Promise.all([ - uci.load('mwan3') - ]); - }, - - render: function () { - let m, s, o; - - m = new form.Map('mwan3', _('MultiWAN Manager - Members'), - _('Members are profiles attaching a metric and weight to an MWAN interface.') + '
' + - _('Names may contain characters A-Z, a-z, 0-9, _ and no spaces.') + '
' + - _('Members may not share the same name as configured interfaces, policies or rules.')); - - s = m.section(form.GridSection, 'member'); - s.addremove = true; - s.anonymous = false; - s.nodescriptions = true; - - /* This name length error check can likely be removed when mwan3 migrates to nftables */ - s.renderSectionAdd = function(extra_class) { - var el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments), - nameEl = el.querySelector('.cbi-section-create-name'); - ui.addValidator(nameEl, 'uciname', true, function(v) { - let sections = [ - ...uci.sections('mwan3', 'interface'), - ...uci.sections('mwan3', 'member'), - ...uci.sections('mwan3', 'policy'), - ...uci.sections('mwan3', 'rule') - ]; - - for (let j = 0; j < sections.length; j++) { - if (sections[j]['.name'] == v) { - return _('Members may not share the same name as configured interfaces, policies or rules.'); - } - } - if (v.length > 15) return _('Name length shall not exceed 15 characters'); - return true; - }, 'blur', 'keyup'); - return el; - }; - - o = s.option(form.ListValue, 'interface', _('Interface')); - var options = uci.sections('mwan3', 'interface') - for (var i = 0; i < options.length; i++) { - var value = options[i]['.name']; - o.value(value); - } - - o = s.option(form.Value, 'metric', _('Metric'), - _('Acceptable values: 1-256. Defaults to 1 if not set')); - o.datatype = 'range(1, 256)'; - - o = s.option(form.Value, 'weight', _('Weight'), - _('Acceptable values: 1-1000. Defaults to 1 if not set')); - o.datatype = 'range(1, 1000)'; - - return m.render(); - } -}) diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/notify.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/notify.js deleted file mode 100644 index 31dbdb2be..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/notify.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; -'require view'; -'require fs'; -'require ui'; - -const isReadonlyView = !L.hasViewPermission() || null; - -return view.extend({ - load() { - return L.resolveDefault(fs.read('/etc/mwan3.user'), ''); - }, - - handleSave(ev) { - const value = (document.querySelector('textarea').value || '').trim().replace(/\r\n/g, '\n') + '\n'; - - return fs.write('/etc/mwan3.user', value).then(function(rc) { - document.querySelector('textarea').value = value; - ui.addNotification(null, E('p', _('Contents have been saved.')), 'info'); - }).catch(function(e) { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - }, - - render(mwan3user) { - return E([ - E('h2', _('MultiWAN Manager - Notify')), - E('p', { 'class': 'cbi-section-descr' }, - _('This section allows you to modify the content of "/etc/mwan3.user".') + '
' + - _('The file is also preserved during sysupgrade.') + '
' + - '
' + - _('Notes:') + '
' + - _('This file is interpreted as a shell script.') + '
' + - _('The first line of the script must be "#!/bin/sh" without quotes.') + '
' + - _('Lines beginning with # are comments and are not executed.') + '
' + - _('Put your custom mwan3 action here, they will be executed with each netifd hotplug interface event on interfaces for which mwan3 is enabled.') + '
' + - '
' + - _('There are three main environment variables that are passed to this script.') + '
' + - '
' + - _('%s: Name of the action that triggered this event').format('$ACTION') + '
' + - _('* %s: Is called by netifd and mwan3track').format('ifup') + '
' + - _('* %s: Is called by netifd and mwan3track').format('ifdown') + '
' + - _('* %s: Is only called by mwan3track if tracking was successful').format('connected') + '
' + - _('* %s: Is only called by mwan3track if tracking has failed').format('disonnected') + '
' + - _('%s: Name of the interface which went up or down (e.g. "wan" or "wwan")').format('$INTERFACE') + '
' + - _('%s: Name of Physical device which interface went up or down (e.g. "eth0" or "wwan0")').format('$DEVICE') + '
'), - E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 10, 'disabled': isReadonlyView }, [ mwan3user != null ? mwan3user : '' ])) - ]); - }, - - handleSaveApply: null, - handleReset: null -}); diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/policy.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/policy.js deleted file mode 100644 index 73ab7b746..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/policy.js +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; -'require form'; -'require view'; -'require uci'; -'require ui'; - -return view.extend({ - load: function() { - return Promise.all([ - uci.load('mwan3') - ]); - }, - - render: function () { - let m, s, o; - - m = new form.Map('mwan3', _('MultiWAN Manager - Policies'), - _('Policies are profiles grouping one or more members controlling how Mwan3 distributes traffic.') + '
' + - _('Member interfaces with lower metrics are used first.') + '
' + - _('Member interfaces with the same metric will be load-balanced.') + '
' + - _('Load-balanced member interfaces distribute more traffic out those with higher weights.') + '
' + - _('Names may contain characters A-Z, a-z, 0-9, _ and no spaces.') + '
' + - _('Names must be 15 characters or less.') + '
' + - _('Policies may not share the same name as configured interfaces, members or rules')); - - s = m.section(form.GridSection, 'policy'); - s.addremove = true; - s.anonymous = false; - s.nodescriptions = true; - - /* This name length error check can likely be removed when mwan3 migrates to nftables */ - s.renderSectionAdd = function(extra_class) { - var el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments), - nameEl = el.querySelector('.cbi-section-create-name'); - ui.addValidator(nameEl, 'uciname', true, function(v) { - let sections = [ - ...uci.sections('mwan3', 'interface'), - ...uci.sections('mwan3', 'member'), - ...uci.sections('mwan3', 'policy'), - ...uci.sections('mwan3', 'rule') - ]; - - for (let j = 0; j < sections.length; j++) { - if (sections[j]['.name'] == v) { - return _('Policies may not share the same name as configured interfaces, members or rules'); - } - } - if (v.length > 15) return _('Name length shall not exceed 15 characters'); - return true; - }, 'blur', 'keyup'); - return el; - }; - - o = s.option(form.DynamicList, 'use_member', _('Member used')); - var options = uci.sections('mwan3', 'member') - for (var i = 0; i < options.length; i++) { - var value = options[i]['.name']; - o.value(value); - } - - o = s.option(form.ListValue, 'last_resort', _('Last resort'), - _('When all policy members are offline use this behavior for matched traffic')); - o.default = 'unreachable'; - o.value('unreachable', _('unreachable (reject)')); - o.value('blackhole', _('blackhole (drop)')); - o.value('default', _('default (use main routing table)')); - - return m.render(); - } -}) diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/rule.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/rule.js deleted file mode 100644 index be2394ba0..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/network/rule.js +++ /dev/null @@ -1,132 +0,0 @@ -'use strict'; -'require form'; -'require fs'; -'require view'; -'require uci'; -'require ui'; - -return view.extend({ - load: function() { - return Promise.all([ - fs.exec_direct('/usr/libexec/luci-mwan3', ['nftset', 'dump']), - uci.load('mwan3') - ]); - }, - - render: function (data) { - let m, s, o; - - m = new form.Map('mwan3', _('MultiWAN Manager - Rules'), - _('Rules specify which traffic will use a particular MWAN policy.') + '
' + - _('Rules are based on IP address, port or protocol.') + '
' + - _('Rules are matched from top to bottom.') + '
' + - _('Rules below a matching rule are ignored.') + '
' + - _('Traffic not matching any rule is routed using the main routing table.') + '
' + - _('Traffic destined for known (other than default) networks is handled by the main routing table.') + '
' + - _('Traffic matching a rule, but all WAN interfaces for that policy are down will be blackholed.') + '
' + - _('Names may contain characters A-Z, a-z, 0-9, _ and no spaces.') + '
' + - _('Rules may not share the same name as configured interfaces, members or policies.')); - - s = m.section(form.GridSection, 'rule'); - s.addremove = true; - s.anonymous = false; - s.nodescriptions = true; - s.sortable = true; - - /* This name length error check can likely be removed when mwan3 migrates to nftables */ - s.renderSectionAdd = function(extra_class) { - const el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments); - const nameEl = el.querySelector('.cbi-section-create-name'); - ui.addValidator(nameEl, 'uciname', true, function(v) { - let sections = [ - ...uci.sections('mwan3', 'interface'), - ...uci.sections('mwan3', 'member'), - ...uci.sections('mwan3', 'policy'), - ...uci.sections('mwan3', 'rule') - ]; - - for (let j = 0; j < sections.length; j++) { - if (sections[j]['.name'] == v) { - return _('Rules may not share the same name as configured interfaces, members or policies.'); - } - } - if (v.length > 15) return _('Name length shall not exceed 15 characters'); - return true; - }, 'blur', 'keyup'); - return el; - }; - - o = s.option(form.ListValue, 'family', _('Internet Protocol')); - o.default = ''; - o.value('', _('IPv4 and IPv6')); - o.value('ipv4', _('IPv4 only')); - o.value('ipv6', _('IPv6 only')); - o.modalonly = true; - - o = s.option(form.Value, 'proto', _('Protocol'), - _('View the content of /etc/protocols for protocol description')); - o.default = 'all'; - o.rmempty = false; - o.value('all'); - o.value('tcp'); - o.value('udp'); - o.value('icmp'); - o.value('esp'); - - o = s.option(form.Value, 'src_ip', _('Source address'), - _('Supports CIDR notation (eg "192.168.100.0/24") without quotes')); - o.datatype = 'ipaddr'; - - o = s.option(form.Value, 'src_port', _('Source port'), - _('May be entered as a single or multiple port(s) (eg "22" or "80,443") or as a portrange (eg "1024:2048") without quotes')); - o.depends('proto', 'tcp'); - o.depends('proto', 'udp'); - - o = s.option(form.Value, 'dest_ip', _('Destination address'), - _('Supports CIDR notation (eg "192.168.100.0/24") without quotes')); - o.datatype = 'ipaddr'; - - o = s.option(form.Value, 'dest_port', _('Destination port'), - _('May be entered as a single or multiple port(s) (eg "22" or "80,443") or as a portrange (eg "1024:2048") without quotes')); - o.depends('proto', 'tcp'); - o.depends('proto', 'udp'); - - o = s.option(form.ListValue, 'sticky', _('Sticky'), - _('Traffic from the same source IP address that previously matched this rule within the sticky timeout period will use the same WAN interface')); - o.default = '0'; - o.value('1', _('Yes')); - o.value('0', _('No')); - o.modalonly = true; - - o = s.option(form.Value, 'timeout', _('Sticky timeout'), - _('Seconds. Acceptable values: 1-1000000. Defaults to 600 if not set')); - o.datatype = 'range(1, 1000000)'; - o.modalonly = true; - o.depends('sticky', '1'); - - o = s.option(form.Value, 'ipset', _('IPset'), - _('Name of IPset rule. Requires IPset rule in /etc/dnsmasq.conf (eg "ipset=/youtube.com/youtube")')); - o.value('', _('-- Please choose --')); - let ipsets = data[0].split(/\n/); - for (let ips of ipsets) { - if (ips.length > 0) - o.value(ips); - } - o.modalonly = true; - - o = s.option(form.Flag, 'logging', _('Logging'), - _('Enables firewall rule logging (global mwan3 logging must also be enabled)')); - o.modalonly = true; - - o = s.option(form.ListValue, 'use_policy', _('Policy assigned')); - let options = uci.sections('mwan3', 'policy') - for (let opt of options) { - o.value(opt['.name']); - } - o.value('unreachable', _('unreachable (reject)')); - o.value('blackhole', _('blackhole (drop)')); - o.value('default', _('default (use main routing table)')); - - return m.render(); - } -}) diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/detail.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/detail.js deleted file mode 100644 index 552b1321f..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/detail.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; -'require fs'; -'require view'; - -return view.extend({ - load: function() { - return L.resolveDefault(fs.exec_direct('/usr/sbin/mwan3', [ 'status' ]),''); - }, - - render: function (report) { - return E('div', { 'class': 'cbi-map', 'id': 'map' }, [ - E('h2', _('MultiWAN Manager - Status')), - E('div', { 'class': 'cbi-section' }, [ - E('pre', [ report ]) - ]), - ]) - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}) diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js deleted file mode 100644 index 71bee68f8..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; -'require fs'; -'require uci'; -'require dom'; -'require ui'; -'require view'; - -return view.extend({ - handleCommand: function(exec, args) { - var buttons = document.querySelectorAll('.cbi-button'); - - for (var i = 0; i < buttons.length; i++) - buttons[i].setAttribute('disabled', 'true'); - - return fs.exec(exec, args).then(function(res) { - var out = document.querySelector('.command-output'); - out.style.display = ''; - - dom.content(out, [ res.stdout || '', res.stderr || '' ]); - }).catch(function(err) { - ui.addNotification(null, E('p', [ err ])) - }).finally(function() { - for (var i = 0; i < buttons.length; i++) - buttons[i].removeAttribute('disabled'); - }); - }, - - handleAction: function(ev) { - var iface = document.getElementById('iface'); - var task = document.getElementById('task'); - - switch (task.value) { - case 'gateway': - return this.handleCommand('/usr/libexec/luci-mwan3', - [ 'diag', 'gateway', iface.value ]); - case 'tracking': - return this.handleCommand('/usr/libexec/luci-mwan3', - [ 'diag', 'tracking', iface.value ]); - case 'rules': - return this.handleCommand('/usr/libexec/luci-mwan3', - [ 'diag', 'rules', iface.value ]); - case 'routes': - return this.handleCommand('/usr/libexec/luci-mwan3', - [ 'diag', 'routes', iface.value ]); - case 'ifup': - return this.handleCommand('/usr/sbin/mwan3', - [ 'ifup', iface.value]); - case 'ifdown': - return this.handleCommand('/usr/sbin/mwan3', - [ 'ifdown', iface.value]); - } - }, - - load: function() { - return Promise.all([ - uci.load('mwan3') - ]); - }, - - render: function () { - - var taskSel = [ - E('option', { 'value': 'gateway' }, [ _('Ping default gateway') ]), - E('option', { 'value': 'tracking' }, [ _('Ping tracking IP') ]), - E('option', { 'value': 'rules' }, [ _('Check IP rules') ]), - E('option', { 'value': 'routes' }, [ _('Check routing table') ]), - E('option', { 'value': 'ifup' }, [ _('Hotplug ifup') ]), - E('option', { 'value': 'ifdown' }, [ _('Hotplug ifdown') ]) - ]; - - var ifaceSel = [E('option', { value: '' }, [_('-- Interface Selection --')])]; - - var options = uci.sections('mwan3', 'interface') - for (var i = 0; i < options.length; i++) { - ifaceSel.push(E('option', { 'value': options[i]['.name'] }, options[i]['.name'])); - } - - return E('div', { 'class': 'cbi-map', 'id': 'map' }, [ - E('h2', {}, [ _('MultiWAN Manager - Diagnostics') ]), - E('div', { 'class': 'cbi-section' }, [ - E('div', { 'class': 'cbi-section-node' }, [ - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, [ _('Interface') ]), - E('div', { 'class': 'cbi-value-field' }, [ - E('select', {'class': 'cbi-input-select', 'id': 'iface'}, - ifaceSel - ) - ]) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, [ _('Task') ]), - E('div', { 'class': 'cbi-value-field' }, [ - E('select', { 'class': 'cbi-input-select', 'id': 'task' }, - taskSel - ) - ]) - ]) - ]) - ]), - '\xa0', - E('pre', { 'class': 'command-output', 'style': 'display:none' }), - '\xa0', - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'cbi-button cbi-button-apply', - 'id': 'execute', - 'click': ui.createHandlerFn(this, 'handleAction') - }, [ _('Execute') ]), - ]), - ]); - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}) diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/overview.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/overview.js deleted file mode 100644 index d63fb6540..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/overview.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict'; -'require poll'; -'require view'; -'require rpc'; - -const callMwan3Status = rpc.declare({ - object: 'mwan3', - method: 'status', - params: ['section'], - expect: { }, -}); - -function getStatusBackgroundClass(status) { - switch (status) { - case 'online': - return 'label success'; - case 'offline': - return 'label warning'; - case 'disabled': - return 'label'; - case 'notracking': - return 'label'; - default: - return 'label'; - } -} - -function renderMwan3Status(status) { - if (!status.interfaces) - return '%h'.format(_('No MWAN interfaces found')); - - var tableHtml = ''; - tableHtml += ''; - tableHtml += ''; - tableHtml += ''; - tableHtml += ''; - tableHtml += ''; - - for (var iface in status.interfaces) { - var state = ''; - var time = ''; - var statusType = status.interfaces[iface].status; - - switch (statusType) { - case 'online': - state = _('Online'); - time = '%t'.format(status.interfaces[iface].online); - break; - case 'offline': - state = _('Offline'); - time = '%t'.format(status.interfaces[iface].offline); - break; - case 'notracking': - state = _('No Tracking'); - if ((status.interfaces[iface].uptime) > 0) { - time = '%t'.format(status.interfaces[iface].uptime); - } else { - time = ''; - } - break; - default: - state = _('Disabled'); - time = ''; - break; - } - - var statusClass = getStatusBackgroundClass(statusType); - - tableHtml += ''; - tableHtml += ''; - tableHtml += ''; - tableHtml += ''; - tableHtml += ''; - } - - tableHtml += '
' + _('Interface') + '' + _('Status') + '' + _('Uptime') + '
' + iface + '' + state + '' + (time || '') + '
'; - - return tableHtml; -} - -return view.extend({ - load: function() { - return Promise.all([ - callMwan3Status("interfaces"), - ]); - }, - - render: function (data) { - poll.add(function() { - return callMwan3Status("interfaces").then(function(result) { - var view = document.getElementById('mwan3-service-status'); - view.innerHTML = renderMwan3Status(result); - }); - }); - - return E('div', { class: 'cbi-map' }, [ - E('h2', [ _('MultiWAN Manager - Overview') ]), - E('div', { class: 'cbi-section' }, [ - E('div', { 'id': 'mwan3-service-status' }, [ - E('em', { 'class': 'spinning' }, [ _('Collecting data ...') ]) - ]) - ]) - ]); - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}); diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/troubleshooting.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/troubleshooting.js deleted file mode 100644 index 644612517..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/mwan3/status/troubleshooting.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; -'require fs'; -'require view'; - -return view.extend({ - load: function() { - return L.resolveDefault(fs.exec_direct('/usr/sbin/mwan3', [ 'internal', 'ipv4' ]),''); - }, - - render: function (report) { - return E('div', { 'class': 'cbi-map', 'id': 'map' }, [ - E('h2', _('MultiWAN Manager - Troubleshooting')), - E('div', { 'class': 'cbi-section' }, [ - E('pre', [ report ]) - ]), - ]) - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}) diff --git a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/status/include/90_mwan3.js b/luci-app-mwan3-nft/htdocs/luci-static/resources/view/status/include/90_mwan3.js deleted file mode 100644 index 20cef8463..000000000 --- a/luci-app-mwan3-nft/htdocs/luci-static/resources/view/status/include/90_mwan3.js +++ /dev/null @@ -1,117 +0,0 @@ -'use strict'; -'require baseclass'; -'require rpc'; -'require uci'; - -const callMwan3Status = rpc.declare({ - object: 'mwan3', - method: 'status', - params: ['section'], - expect: { }, -}); - -function getStatusBackgroundClass(status) { - switch (status) { - case 'online': - return 'label success'; - case 'offline': - return 'label warning'; - case 'disabled': - return 'label'; - case 'notracking': - return 'label'; - default: - return 'label'; - } -} - -return baseclass.extend({ - title: _('MultiWAN Manager'), - - load: function() { - return uci.load('mwan3').then(function() { - var globals = uci.sections('mwan3', 'globals'); - if (!globals || !globals[0] || globals[0].show_overview !== '1') - return null; - return Promise.all([ - callMwan3Status("interfaces"), - ]); - }); - }, - - render: function (result) { - if (!result) - return null; - - if (!result[0]?.interfaces || Object.keys(result[0].interfaces).length === 0) - return null; - - // Основной контейнер - var container = E('div', { 'class': 'cbi-section' }); - - // Таблица с системными классами LuCI - var table = E('table', { 'class': 'table cbi-section-table' }); - - // Заголовок - var thead = E('tr', { 'class': 'tr cbi-section-table-titles' }); - thead.appendChild(E('th', { 'class': 'th left' }, [ _('Interface') ])); - thead.appendChild(E('th', { 'class': 'th left' }, [ _('Status') ])); - thead.appendChild(E('th', { 'class': 'th left' }, [ _('Uptime') ])); - table.appendChild(thead); - - // Тело таблицы - var tbody = E('tbody'); - - for (var iface in result[0].interfaces) { - var state = ''; - var time = ''; - var statusType = result[0].interfaces[iface].status; - - switch (statusType) { - case 'online': - state = _('Online'); - time = '%t'.format(result[0].interfaces[iface].online); - break; - case 'offline': - state = _('Offline'); - time = '%t'.format(result[0].interfaces[iface].offline); - break; - case 'notracking': - state = _('No Tracking'); - if ((result[0].interfaces[iface].uptime) > 0) { - time = '%t'.format(result[0].interfaces[iface].uptime); - } else { - time = ''; - } - break; - default: - state = _('Disabled'); - time = ''; - break; - } - - var tr = E('tr', { 'class': 'tr cbi-section-table-row' }); - - // Ячейка интерфейса - tr.appendChild(E('td', { 'class': 'td left' }, [ - E('strong', {}, [ iface ]) - ])); - - // Ячейка статуса с цветным фоном через системные классы label - var statusClass = getStatusBackgroundClass(statusType); - tr.appendChild(E('td', { 'class': 'td left' }, [ - E('span', { 'class': statusClass }, [ state ]) - ])); - - // Ячейка времени - tr.appendChild(E('td', { 'class': 'td left' }, [ time || '' ])); - - tbody.appendChild(tr); - } - - table.appendChild(tbody); - container.appendChild(table); - - return container; - } -}); diff --git a/luci-app-mwan3-nft/po/ru/mwan3.po b/luci-app-mwan3-nft/po/ru/mwan3.po deleted file mode 100644 index 8beacc4ef..000000000 --- a/luci-app-mwan3-nft/po/ru/mwan3.po +++ /dev/null @@ -1,886 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: LuCI: mwan3\n" -"POT-Creation-Date: 2017-09-14 16:00+0300\n" -"PO-Revision-Date: 2026-05-02 12:03+0000\n" -"Last-Translator: SnIPeRSnIPeR " -"\n" -"Language-Team: Russian \n" -"Language: ru\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -"X-Generator: Weblate 5.17.1\n" -"Project-Info: Это технический перевод, не дословный. Главное-удобный русский " -"интерфейс, все проверялось в графическом режиме, совместим с другими apps\n" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:211 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:227 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:249 -msgid "%d hour" -msgstr "%d час" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:206 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:222 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:244 -msgid "%d minute" -msgstr "%d минута" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:207 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:208 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:209 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:210 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:223 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:224 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:225 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:226 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:245 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:246 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:247 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:248 -msgid "%d minutes" -msgstr "%d минут" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:193 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:200 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:216 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:238 -msgid "%d second" -msgstr "%d секунда" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:195 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:201 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:202 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:203 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:204 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:205 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:217 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:218 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:219 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:220 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:221 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:239 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:240 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:241 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:242 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:243 -msgid "%d seconds" -msgstr "%d секунд" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:45 -msgid "" -"%s: Name of Physical device which interface went up or down (e.g. \"eth0\" " -"or \"wwan0\")" -msgstr "" -"%s: Название физического устройства, интерфейс которого поднялся или упал " -"(прим. \"eth0\" или \"wwan0\")" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:39 -msgid "%s: Name of the action that triggered this event" -msgstr "%s: Название действия вызвавшего это событие" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:44 -msgid "" -"%s: Name of the interface which went up or down (e.g. \"wan\" or \"wwan\")" -msgstr "" -"%s: Название интерфейса который поднялся или упал (прим. \"wan\" или " -"\"wwan\")" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:40 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:41 -msgid "* %s: Is called by netifd and mwan3track" -msgstr "* %s: Вызывается netifd и mwan3track" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:43 -msgid "* %s: Is only called by mwan3track if tracking has failed" -msgstr "" -"* %s: Вызывается mwan3track только в том случае, если отслеживание не удалось" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:42 -msgid "* %s: Is only called by mwan3track if tracking was successful" -msgstr "* %s: Вызывается mwan3track только в случае успешного отслеживания" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:71 -msgid "-- Interface Selection --" -msgstr "-- Выбор интерфейса --" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:109 -msgid "-- Please choose --" -msgstr "-- Выберите --" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:99 -msgid "" -"Acceptable values: 1-100. This many Tracking IP addresses must respond for " -"the link to be deemed up" -msgstr "" -"Допустимые значения: 1–100. Количество IP-адресов, которое должно ответить " -"чтобы соединение было признано активным" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:62 -msgid "Acceptable values: 1-1000. Defaults to 1 if not set" -msgstr "Допустимые значения: 1–1000. По умолчанию 1, если значение не задано" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:58 -msgid "Acceptable values: 1-256. Defaults to 1 if not set" -msgstr "Допустимые значения: 1–256 По умолчанию 1, если значение не задано" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:26 -msgid "Alert" -msgstr "Тревога" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:37 -msgid "Also scan this Routing table for connected networks" -msgstr "" -"Также сканировать эту таблицу маршрутизации на предмет подключённых сетей" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:65 -msgid "Check IP rules" -msgstr "Проверить правила IP" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:140 -msgid "Check link quality" -msgstr "Проверка качества подключения" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:66 -msgid "Check routing table" -msgstr "Проверить таблицу маршрутизации" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:96 -msgid "Collecting data ..." -msgstr "Сбор данных ..." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:18 -msgid "Contents have been saved." -msgstr "Содержимое сохранено." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:27 -msgid "Critical" -msgstr "Критический" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:32 -msgid "Debug" -msgstr "Отладка" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:85 -msgid "Destination address" -msgstr "Адрес назначения" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:89 -msgid "Destination port" -msgstr "Порт назначения" - -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:31 -msgid "Diagnostics" -msgstr "Диагностика" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:57 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:66 -msgid "Disabled" -msgstr "Отключено" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:267 -msgid "" -"Downed interface will be deemed up after this many successful ping tests" -msgstr "" -"Отключённый интерфейс будет считаться активным после указанного количество " -"успешных ping проверок" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:41 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:49 -msgid "Downtime" -msgstr "Время простоя" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:25 -msgid "Emergency" -msgstr "Аварийный" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:92 -msgid "Enable ssl tracking" -msgstr "Включить отслеживание SSL" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:55 -msgid "Enabled" -msgstr "Включено" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:118 -msgid "" -"Enables firewall rule logging (global mwan3 logging must also be enabled)" -msgstr "" -"Включает журналирование правил брандмауэра (также должно быть включено " -"глобальное журналирование mwan3)" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:20 -msgid "Enables global firewall logging" -msgstr "Включает глобальное журналирование правил брандмауэера" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:93 -msgid "Enables https tracking on ssl port 443" -msgstr "Включает отслеживание HTTPS на SSL порту 443" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:15 -msgid "Enter value in hex, starting with 0x" -msgstr "Введите значение в шестнадцатеричной форме, начиная с 0x" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:28 -msgid "Error" -msgstr "Ошибка" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:108 -msgid "Execute" -msgstr "Выполнить" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:59 -msgid "Expect interface state on up event" -msgstr "Ожидать состояние интерфейса при событии подключения интерейса" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:213 -msgid "Failure interval" -msgstr "Интервал отказа" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:145 -msgid "Failure latency [ms]" -msgstr "Задержка при отказе [мс]" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:158 -msgid "Failure packet loss [%]" -msgstr "Потери пакетов при отказе [%]" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:23 -msgid "Firewall loglevel" -msgstr "Уровень журналирования брандмауэра" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:14 -msgid "Firewall mask" -msgstr "Маска межсетевого экрана" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:280 -msgid "Flush conntrack table" -msgstr "Сбрасывать conntrack таблицу" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:281 -msgid "Flush global firewall conntrack table on interface events" -msgstr "" -"Сбрасывать глобальную таблицу conntrack межсетевого экрана при событиях " -"интерфейса" - -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:59 -msgid "Globals" -msgstr "Глобальные" - -#: applications/luci-app-mwan3/root/usr/share/rpcd/acl.d/luci-app-mwan3.json:51 -msgid "Grant access for luci-app-mwan3 configuration" -msgstr "Предоставить доступ для настройки luci-app-mwan3" - -#: applications/luci-app-mwan3/root/usr/share/rpcd/acl.d/luci-app-mwan3.json:3 -msgid "Grant access for luci-app-mwan3 status information" -msgstr "Предоставить доступ к информации о состоянии luci-app-mwan3" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:68 -msgid "Hotplug ifdown" -msgstr "Hotplug ifdown" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:67 -msgid "Hotplug ifup" -msgstr "Hotplug ifup" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:107 -msgid "IPset" -msgstr "IPset" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:67 -msgid "IPv4" -msgstr "IPv4" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:61 -msgid "IPv4 and IPv6" -msgstr "IPv4 и IPv6" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:62 -msgid "IPv4 only" -msgstr "Только IPv4" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:68 -msgid "IPv6" -msgstr "IPv6" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:63 -msgid "IPv6 only" -msgstr "Только IPv6" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:31 -msgid "Info" -msgstr "Информация" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:58 -msgid "Initial state" -msgstr "Исходное состояние" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:50 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:83 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:65 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:77 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:101 -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:67 -msgid "Interface" -msgstr "Интерфейс" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:252 -msgid "Interface down" -msgstr "Отключение интерфейса" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:266 -msgid "Interface up" -msgstr "Включение интерфейса" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:253 -msgid "Interface will be deemed down after this many failed ping tests" -msgstr "" -"Интерфейс будет считаться неработающим после заданного количество неудачных " -"пинг-запросов" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:25 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:46 -msgid "" -"Interfaces may not share the same name as configured members, policies or " -"rules." -msgstr "" -"Интерфейсы не могут иметь то же имя, что и настроенные элементы, политики " -"или правила." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:65 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:59 -msgid "Internet Protocol" -msgstr "Протокол Интернета (IP)" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:230 -msgid "Keep failure interval" -msgstr "Сохранять интервал отказа" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:231 -msgid "Keep ping failure interval during failure state" -msgstr "Сохранять интервал отказа пинг-запроса во время состояния отказа" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:61 -msgid "Last resort" -msgstr "Крайнее средство" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:34 -msgid "Lines beginning with # are comments and are not executed." -msgstr "Строки, начинающиеся с #, являются комментариями и не выполняются." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:21 -msgid "" -"Load-balanced member interfaces distribute more traffic out those with " -"higher weights." -msgstr "" -"Интерфейсы с балансировкой нагрузки распределяют больше трафика между " -"интерфейсами с большим весом." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:19 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:117 -msgid "Logging" -msgstr "Журналирование" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:22 -msgid "Loglevel" -msgstr "Уровень журналирования" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:127 -msgid "Max TTL" -msgstr "Максимальное TTL" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:81 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:90 -msgid "" -"May be entered as a single or multiple port(s) (eg \"22\" or \"80,443\") or " -"as a portrange (eg \"1024:2048\") without quotes" -msgstr "" -"Может быть введено как один или несколько портов (например, «22» или " -"«80,443») или как диапазон портов (например, «1024:2048») без кавычек" - -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:75 -msgid "Member" -msgstr "Узел" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:19 -msgid "Member interfaces with lower metrics are used first." -msgstr "Интерфейсы с более низкими метриками используются в первую очередь." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:20 -msgid "Member interfaces with the same metric will be load-balanced." -msgstr "Интерфейсы с одинаковой метрикой будут сбалансированы по нагрузке." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:54 -msgid "Member used" -msgstr "Используемый узел" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:18 -msgid "" -"Members are profiles attaching a metric and weight to an MWAN interface." -msgstr "Элементы - это профили, прикрепляющие метрику и вес к интерфейсу MWAN." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:20 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:41 -msgid "" -"Members may not share the same name as configured interfaces, policies or " -"rules." -msgstr "" -"Элементы не могут иметь одинаковые имена с настроенными интерфейсами, " -"политиками или правилами." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:288 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:57 -msgid "Metric" -msgstr "Метрика" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:19 -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:3 -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:47 -msgid "MultiWAN Manager" -msgstr "Менеджер MultiWAN" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:79 -msgid "MultiWAN Manager - Diagnostics" -msgstr "Менеджер MultiWAN - Диагностика" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:10 -msgid "MultiWAN Manager - Globals" -msgstr "Менеджер MultiWAN - Глобальные" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:21 -msgid "MultiWAN Manager - Interfaces" -msgstr "Менеджер MultiWAN - Интерфейсы" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:17 -msgid "MultiWAN Manager - Members" -msgstr "Менеджер MultiWAN - Элементы" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:26 -msgid "MultiWAN Manager - Notify" -msgstr "Менеджер MultiWAN - Уведомления" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:93 -msgid "MultiWAN Manager - Overview" -msgstr "Менеджер MultiWAN - Обзор" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:17 -msgid "MultiWAN Manager - Policies" -msgstr "Менеджер MultiWAN - Политики" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:19 -msgid "MultiWAN Manager - Rules" -msgstr "Менеджер MultiWAN - Правила" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/detail.js:12 -msgid "MultiWAN Manager - Status" -msgstr "Менеджер MultiWAN - Статус" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/troubleshooting.js:12 -msgid "MultiWAN Manager - Troubleshooting" -msgstr "Менеджер MultiWAN - Устранение неполадок" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:22 -msgid "" -"Mwan3 requires that all interfaces have a unique metric configured in /etc/" -"config/network." -msgstr "" -"Mwan3 требует, чтобы все интерфейсы имели уникальную метрику, настроенную в /" -"etc/config/network." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:49 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:44 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:48 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:53 -msgid "Name length shall not exceed 15 characters" -msgstr "Длина имени не должна превышать 15 символов" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:108 -msgid "" -"Name of IPset rule. Requires IPset rule in /etc/dnsmasq.conf (eg \"ipset=/" -"youtube.com/youtube\")" -msgstr "" -"Имя IPset правила. Требуется правило IPset в /etc/dnsmasq.conf (например " -"«ipset=/youtube.com/youtube»)" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:24 -msgid "Names may contain characters A-Z, a-z, 0-9, _ and no spaces-" -msgstr "Имена могут содержать символы A-Z, a-z, 0-9, _ и без пробелов и -" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:19 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:22 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:27 -msgid "Names may contain characters A-Z, a-z, 0-9, _ and no spaces." -msgstr "Имена могут содержать символы A-Z, a-z, 0-9, _ и без пробелов." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:23 -msgid "Names must be 15 characters or less." -msgstr "Имена должны состоять из 15 символов или менее." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:23 -msgid "Names must match the interface name found in /etc/config/network." -msgstr "Имена должны совпадать с именем интерфейса из /etc/config/network." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:98 -msgid "No" -msgstr "Нет" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:21 -msgid "No MWAN interfaces found" -msgstr "Интерфейсы MWAN не найдены" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:44 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:52 -msgid "No Tracking" -msgstr "Нет отслеживания" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:296 -msgid "No interface metric set!" -msgstr "Не задана метрика интерфейса!" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:31 -msgid "Notes:" -msgstr "Примечания:" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:30 -msgid "Notice" -msgstr "Уведомление" - -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:99 -msgid "Notify" -msgstr "Уведомить" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:62 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:38 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:46 -msgid "Offline" -msgstr "Отключён" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:61 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:31 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:40 -msgid "Online" -msgstr "Онлайн" - -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:15 -msgid "Overview" -msgstr "Обзор" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:103 -msgid "Ping count" -msgstr "Количество пинг-запросов" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:63 -msgid "Ping default gateway" -msgstr "Пинг-запрос шлюза по умолчанию" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:198 -msgid "Ping interval" -msgstr "Интервал пинг-запроса" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:214 -msgid "Ping interval during failure detection" -msgstr "Интервал пинг-запросов во время обнаруженного отказа" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:236 -msgid "Ping interval during failure recovering" -msgstr "Интервал пинг-запросов при восстановлении отказа" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:112 -msgid "Ping size" -msgstr "Размер пакета пинг-запроса" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:191 -msgid "Ping timeout" -msgstr "Время ожидания ответа пинг-запроса" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:64 -msgid "Ping tracking IP" -msgstr "Пинг-запрос отслеживаемого IP" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:18 -msgid "" -"Policies are profiles grouping one or more members controlling how Mwan3 " -"distributes traffic." -msgstr "" -"Политики - это профили, объединяющие один или несколько элементов, " -"управляющих распределением трафика в Mwan3." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:24 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:45 -msgid "" -"Policies may not share the same name as configured interfaces, members or " -"rules" -msgstr "" -"Политики не могут иметь одинаковые имена с настроенными интерфейсами, " -"элементами или правилами" - -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:83 -msgid "Policy" -msgstr "Политика" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:121 -msgid "Policy assigned" -msgstr "Назначенная политика" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:66 -msgid "Protocol" -msgstr "Протокол" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:35 -msgid "" -"Put your custom mwan3 action here, they will be executed with each netifd " -"hotplug interface event on interfaces for which mwan3 is enabled." -msgstr "" -"Поместите сюда ваши пользовательские действия mwan3, они будут выполняться " -"при каждом событии интерфейса netifd hotplug на интерфейсах, для которых " -"включён mwan3." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:235 -msgid "Recovery interval" -msgstr "Интервал восстановления" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:168 -msgid "Recovery latency [ms]" -msgstr "Задержка восстановления [мс]" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:181 -msgid "Recovery packet loss [%]" -msgstr "Потери пакетов для восстановления [%]" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:39 -msgid "Routing table %d" -msgstr "Таблица маршрутизации %d" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:36 -msgid "Routing table lookup" -msgstr "Поиск таблицы маршрутизации" - -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:91 -msgid "Rule" -msgstr "Правило" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:21 -msgid "Rules are based on IP address, port or protocol." -msgstr "Правила основаны на IP-адресе, порте или протоколе." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:22 -msgid "Rules are matched from top to bottom." -msgstr "Правила сопоставляются сверху вниз." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:23 -msgid "Rules below a matching rule are ignored." -msgstr "Правила, находящиеся ниже соответствующего правила, игнорируются." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:28 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:50 -msgid "" -"Rules may not share the same name as configured interfaces, members or " -"policies." -msgstr "" -"Правила не могут иметь одинаковые имена с настроенными интерфейсами, " -"элементами или политиками." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:20 -msgid "Rules specify which traffic will use a particular MWAN policy." -msgstr "" -"Правила определяют, какой трафик будет использовать определённую политику " -"MWAN." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:102 -msgid "Seconds. Acceptable values: 1-1000000. Defaults to 600 if not set" -msgstr "" -"Секунды. Допустимые значения: 1–1000000. По умолчанию 600, если значение не " -"установлено" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:76 -msgid "Source address" -msgstr "Адрес источника" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:80 -msgid "Source port" -msgstr "Порт источника" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:66 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:83 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:107 -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:23 -msgid "Status" -msgstr "Статус" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:94 -msgid "Sticky" -msgstr "Режим Sticky" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:101 -msgid "Sticky timeout" -msgstr "Время ожидания режима Sticky" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:77 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:86 -msgid "Supports CIDR notation (eg \"192.168.100.0/24\") without quotes" -msgstr "Поддерживает CIDR нотацию (например «192.168.100.0/24») без кавычек" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/diagnostics.js:91 -msgid "Task" -msgstr "Задача" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:29 -msgid "The file is also preserved during sysupgrade." -msgstr "Этот файл также сохраняется при обновлении системы." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:33 -msgid "" -"The first line of the script must be "#!/bin/sh" without quotes." -msgstr "Первая строка скрипта должна быть "#!/bin/sh" без кавычек." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:37 -msgid "" -"There are three main environment variables that are passed to this script." -msgstr "В этот сценарий передаются три основные переменные среды окружения." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:289 -msgid "" -"This displays the metric assigned to this interface in /etc/config/network" -msgstr "" -"Здесь отображается метрика, назначенная этому интерфейсу в файле /etc/config/" -"network" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:32 -msgid "This file is interpreted as a shell script." -msgstr "Этот файл интерпретируется как shell-скрипт." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:72 -msgid "" -"This hostname or IP address will be pinged to determine if the link is up or " -"down. Leave blank to assume interface is always online" -msgstr "" -"Имя хоста или IP-адрес для пинг-запроса, чтобы определить активно соединение " -"или нет. Оставьте пустым, чтобы указать, что интерфейс всегда в сети" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:28 -msgid "This section allows you to modify the content of \"/etc/mwan3.user\"." -msgstr "Этот раздел позволяет изменять содержимое файла \"/etc/mwan3.user\"." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:71 -msgid "Tracking hostname or IP address" -msgstr "Отслеживамое имя хоста или IP-адрес" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:76 -msgid "Tracking method" -msgstr "Метод отслеживания" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:98 -msgid "Tracking reliability" -msgstr "Надёжность отслеживания" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:25 -msgid "" -"Traffic destined for known (other than default) networks is handled by the " -"main routing table." -msgstr "" -"Трафик, предназначенный для известных (кроме стандартных) сетей, " -"обрабатывается основной таблицей маршрутизации." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:95 -msgid "" -"Traffic from the same source IP address that previously matched this rule " -"within the sticky timeout period will use the same WAN interface" -msgstr "" -"Трафик с того же IP-адреса источника, который ранее соответствовал этому " -"правилу в период времени ожидания режима Sticky, будет использовать тот же " -"интерфейс WAN" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:26 -msgid "" -"Traffic matching a rule, but all WAN interfaces for that policy are down " -"will be blackholed." -msgstr "" -"Трафик, соответствующий правилу, но все интерфейсы WAN для этой политики не " -"работают, будет заблокирован." - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:24 -msgid "Traffic not matching any rule is routed using the main routing table." -msgstr "" -"Трафик, не соответствующий какому-либо правилу, направляется по основной " -"таблице маршрутизации." - -#: applications/luci-app-mwan3/root/usr/share/luci/menu.d/luci-app-mwan3.json:39 -msgid "Troubleshooting" -msgstr "Устранение неполадок" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/notify.js:20 -msgid "Unable to save contents: %s" -msgstr "Не удалось сохранить содержимое: %s" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:34 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/status/overview.js:48 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:43 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/status/include/90_mwan3.js:56 -msgid "Uptime" -msgstr "Время работы" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:67 -msgid "View the content of /etc/protocols for protocol description" -msgstr "Просмотр содержимого файла /etc/protocols для описания протокола" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/globals.js:29 -msgid "Warning" -msgstr "Предупреждение" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/member.js:61 -msgid "Weight" -msgstr "Вес" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:62 -msgid "" -"When all policy members are offline use this behavior for matched traffic" -msgstr "" -"Когда все члены политики находятся в автономном режиме, используйте это " -"поведение для сопоставляемого трафика" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:97 -msgid "Yes" -msgstr "Да" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:65 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:127 -msgid "blackhole (drop)" -msgstr "blackhole (отбросить)" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:284 -msgid "connected (mwan3)" -msgstr "подключено (connected, mwan3)" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:66 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:128 -msgid "default (use main routing table)" -msgstr "default (использовать основную таблицу маршрутизации)" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:285 -msgid "disconnected (mwan3)" -msgstr "отключено (disconnected, mwan3)" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:283 -msgid "ifdown (netifd)" -msgstr "отключено (ifdown, netifd)" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/interface.js:282 -msgid "ifup (netifd)" -msgstr "подключено (ifup, netifd)" - -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/policy.js:64 -#: applications/luci-app-mwan3/htdocs/luci-static/resources/view/mwan3/network/rule.js:126 -msgid "unreachable (reject)" -msgstr "unreachable (отклонить)" - -msgid "Show on overview" -msgstr "Показать на главной" - -msgid "Show MultiWAN status section on the overview page" -msgstr "Показывать секцию состояния MultiWan на главной странице" - -msgid "No available interfaces" -msgstr "Нет доступных интерфейсов" diff --git a/luci-app-mwan3-nft/root/etc/uci-defaults/60_luci-mwan3 b/luci-app-mwan3-nft/root/etc/uci-defaults/60_luci-mwan3 deleted file mode 100755 index 414146b5e..000000000 --- a/luci-app-mwan3-nft/root/etc/uci-defaults/60_luci-mwan3 +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -uci -q get mwan3.globals >/dev/null || { - uci -q add mwan3 globals >/dev/null - uci -q rename mwan3.@globals[-1]="globals" >/dev/null - uci -q set mwan3.globals.local_source="none" >/dev/null - uci commit mwan3 -} - -exit 0 diff --git a/luci-app-mwan3-nft/root/usr/libexec/luci-mwan3 b/luci-app-mwan3-nft/root/usr/libexec/luci-mwan3 deleted file mode 100755 index 5a38634fe..000000000 --- a/luci-app-mwan3-nft/root/usr/libexec/luci-mwan3 +++ /dev/null @@ -1,199 +0,0 @@ -#!/bin/sh -# -# Copyright (C) 2021 TDT AG -# -# This is free software, licensed under the GNU General Public License v2. -# See https://www.gnu.org/licenses/gpl-2.0.txt for more information. -# - -. /lib/functions.sh -. /lib/functions/network.sh -. /usr/share/libubox/jshn.sh - -IIF=1000 -FWMARK=2000 -ID=0 - -usage() { - local status="$1" - local msg="$2" - if [ -n "$msg" ]; then - echo "$msg" - echo "" - fi - echo "Usage: $(basename "$0") " - echo "command:" - echo " diag: diagnostic commands" - echo " nftset: nft set commands" - echo "" - echo "diag " - echo "command:" - echo " gateway : ping interface gateway" - echo " tracking : ping interface tracking targets" - echo " rules : check interface routing rules" - echo " routes : check interface routing tables" - echo "" - echo "nftset " - echo "command:" - echo " dump: show all configured nft set names" - - exit "$status" -} - -diag_gateway() { - local iface="$1" - - local gw - - network_get_gateway gw "${iface}" - [ -z "$gw" ] && network_get_gateway gw "${iface}_4" - - [ -z "$gw" ] && { - echo "No gateway for interface ${iface} found." - exit 2 - } - - mwan3 use "$iface" "ping" "-c" "5" "-W" "1" "$gw" -} - -diag_tracking() { - local iface="$1" - - checkips() { - local ip="$1" - local iface="$2" - - mwan3 use "$iface" "ping" "-c" "5" "-W" "1" "$ip" - } - - config_load mwan3 - config_list_foreach "$iface" "track_ip" checkips "$iface" -} - -iface_number() { - local cfg="$1" - local iface="$2" - - let number++ - - [ "$cfg" = "$iface" ] && { - ID="$number" - } -} - -diag_rules() { - local iface="$1" - - local number=0 - local iif=0 - local fwmark=0 - - local iif_rule iif_result - local fwmark_rule fwmark_result - - config_load mwan3 - config_foreach iface_number 'interface' "$iface" - - [ "$ID" = "0" ] && { - echo "Unable to get mwan3 interface number for \"$iface\"." - exit 2 - } - - let "iif=$IIF+$ID" - let "fwmark=$FWMARK+$ID" - - iif_rule="$(ip rule | grep ${iif})" - iif_result="$?" - - fwmark_rule="$(ip rule | grep ${fwmark})" - fwmark_result="$?" - - if [ "$fwmark_result" = 0 ] && [ "$iif_result" = 0 ]; then - echo "All required IP rules for interface \"$iface\" found" - echo "$fwmark_rule" - echo "$iif_rule" - elif [ "$fwmark_result" = 1 ] && [ "$iif_result" = 0 ]; then - echo "Only iif IP rule for interface \"$iface\" found" - echo "$iif_rule" - elif [ "$fwmark_result" = 0 ] && [ "$iif_result" = 1 ]; then - echo "Only fwmark IP rule for interface \"$iface\" found" - echo "$fwmark_rule" - else - echo "Missing fwmark and iif IP rule for interface \"$iface\"" - fi -} - -diag_routes() { - local iface="$1" - - local table table_result - - config_load mwan3 - config_foreach iface_number 'interface' "$iface" - - [ "$ID" = "0" ] && { - echo "Unable to get mwan3 interface number for \"$iface\"." - exit 2 - } - - table="$(ip route list table $ID)" - table_result="$?" - - if [ "$table_result" = 0 ]; then - echo "Routing table \"$ID\" for interface \"$iface\" found" - echo "$table" - else - echo "Routing table \"$ID\" for interface \"$iface\" not found" - fi -} - -diag_cmd() { - case "$1" in - gateway) - diag_gateway "$2" - ;; - tracking) - diag_tracking "$2" - ;; - rules) - diag_rules "$2" - ;; - routes) - diag_routes "$2" - ;; - *) - usage "1" "Command not supported" - ;; - esac -} - -nftset_dump() { - nft -a list table inet mwan3 2>/dev/null | awk '/^\tset / && !/^\tset mwan3_/ {print $2}' | sort -u -} - -nftset_cmd() { - case "$1" in - dump) - nftset_dump - ;; - *) - usage "1" "Command not supported" - ;; - esac -} - -main () { - case "$1" in - diag) - diag_cmd "$2" "$3" - ;; - nftset) - nftset_cmd "$2" - ;; - *) - usage "1" "Command not supported" - ;; - esac -} - -main "$@" diff --git a/luci-app-mwan3-nft/root/usr/share/luci/menu.d/luci-app-mwan3.json b/luci-app-mwan3-nft/root/usr/share/luci/menu.d/luci-app-mwan3.json deleted file mode 100644 index ee142df41..000000000 --- a/luci-app-mwan3-nft/root/usr/share/luci/menu.d/luci-app-mwan3.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "admin/status/mwan3": { - "title": "MultiWAN Manager", - "order": "600", - "action": { - "type": "firstchild" - }, - "depends": { - "acl": [ - "luci-app-mwan3-status" - ] - } - }, - "admin/status/mwan3/overview": { - "title": "Overview", - "order": 10, - "action": { - "type": "view", - "path": "mwan3/status/overview" - } - }, - "admin/status/mwan3/detail": { - "title": "Status", - "order": 20, - "action": { - "type": "view", - "path": "mwan3/status/detail" - } - }, - "admin/status/mwan3/diagnostics": { - "title": "Diagnostics", - "order": 30, - "action": { - "type": "view", - "path": "mwan3/status/diagnostics" - } - }, - "admin/status/mwan3/troubleshooting": { - "title": "Troubleshooting", - "order": 40, - "action": { - "type": "view", - "path": "mwan3/status/troubleshooting" - } - }, - "admin/network/mwan3": { - "title": "MultiWAN Manager", - "order": "600", - "action": { - "type": "firstchild" - }, - "depends": { - "acl": [ - "luci-app-mwan3" - ] - } - }, - "admin/network/mwan3/globals": { - "title": "Globals", - "order": 10, - "action": { - "type": "view", - "path": "mwan3/network/globals" - } - }, - "admin/network/mwan3/interface": { - "title": "Interface", - "order": 20, - "action": { - "type": "view", - "path": "mwan3/network/interface" - } - }, - "admin/network/mwan3/member": { - "title": "Member", - "order": 30, - "action": { - "type": "view", - "path": "mwan3/network/member" - } - }, - "admin/network/mwan3/policy": { - "title": "Policy", - "order": 40, - "action": { - "type": "view", - "path": "mwan3/network/policy" - } - }, - "admin/network/mwan3/rule": { - "title": "Rule", - "order": 50, - "action": { - "type": "view", - "path": "mwan3/network/rule" - } - }, - "admin/network/mwan3/notify": { - "title": "Notify", - "order": 60, - "action": { - "type": "view", - "path": "mwan3/network/notify" - } - } -} diff --git a/luci-app-mwan3-nft/root/usr/share/rpcd/acl.d/luci-app-mwan3.json b/luci-app-mwan3-nft/root/usr/share/rpcd/acl.d/luci-app-mwan3.json deleted file mode 100644 index 809558cc1..000000000 --- a/luci-app-mwan3-nft/root/usr/share/rpcd/acl.d/luci-app-mwan3.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "luci-app-mwan3-status": { - "description": "Grant access for luci-app-mwan3 status information", - "read": { - "cgi-io": [ - "exec" - ], - "file": { - "/usr/sbin/mwan3 status": [ - "exec" - ] - }, - "ubus": { - "mwan3": [ - "status" - ] - } - }, - "write": { - "file": { - "/usr/libexec/luci-mwan3 diag gateway *": [ - "exec" - ], - "/usr/libexec/luci-mwan3 diag tracking *": [ - "exec" - ], - "/usr/libexec/luci-mwan3 diag rules *": [ - "exec" - ], - "/usr/libexec/luci-mwan3 diag routes *": [ - "exec" - ], - "/usr/sbin/mwan3 internal ipv4": [ - "exec" - ], - "/usr/sbin/mwan3 ifup *": [ - "exec" - ], - "/usr/sbin/mwan3 ifdown *": [ - "exec" - ] - }, - "ubus": { - "file": [ - "exec" - ] - } - } - }, - "luci-app-mwan3": { - "description": "Grant access for luci-app-mwan3 configuration", - "read": { - "cgi-io": [ - "exec" - ], - "file": { - "/etc/mwan3.user": [ - "read" - ], - "/usr/bin/httping": [ - "list" - ], - "/usr/bin/nping": [ - "list" - ], - "/usr/bin/arping": [ - "list" - ], - "/usr/libexec/luci-mwan3 nftset dump": [ - "exec" - ] - }, - "ubus": { - "mwan3": [ - "status" - ] - }, - "uci": [ - "mwan3", - "network" - ] - }, - "write": { - "file": { - "/etc/mwan3.user": [ - "write" - ] - }, - "uci": [ - "mwan3" - ] - } - } -} diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile index dfd98c5f8..2a8fee2e6 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.320 -PKG_RELEASE:=28 +PKG_VERSION:=0.1.326 +PKG_RELEASE:=29 PKG_MAINTAINER:=liandu2024 PKG_LICENSE:=GPL-3.0-or-later diff --git a/luci-app-packet-capture/Makefile b/luci-app-packet-capture/Makefile index aa9bcf5a2..30e9a21e4 100644 --- a/luci-app-packet-capture/Makefile +++ b/luci-app-packet-capture/Makefile @@ -9,6 +9,10 @@ LUCI_DEPENDS:=+luci-mod-admin-full +tcpdump +coreutils +coreutils-timeout PKG_MAINTAINER:=Wojciech Jowsa PKG_LICENSE:=Apache-2.0 +define Package/luci-app-packet-capture/conffiles +/etc/config/packet_capture +endef + include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signatureet diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 3fb9f8b6a..cbdf2a7f5 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.1 -PKG_RELEASE:=141 +PKG_RELEASE:=142 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ @@ -80,6 +80,7 @@ config PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy select PACKAGE_iptables-nft select PACKAGE_iptables-zz-legacy select PACKAGE_iptables-mod-conntrack-extra + select PACKAGE_iptables-mod-extra select PACKAGE_iptables-mod-iprange select PACKAGE_iptables-mod-socket select PACKAGE_iptables-mod-tproxy diff --git a/luci-app-passwall2/luasrc/controller/passwall2.lua b/luci-app-passwall2/luasrc/controller/passwall2.lua index 099ceee8e..831e23a10 100644 --- a/luci-app-passwall2/luasrc/controller/passwall2.lua +++ b/luci-app-passwall2/luasrc/controller/passwall2.lua @@ -755,12 +755,14 @@ end function com_update(comname) local json = nil local task = http.formvalue("task") - if task == "extract" then + if task == "progress" then + json = api.to_download_progress(comname, http.formvalue("id"), http.formvalue("total_size")) + elseif task == "extract" then json = api.to_extract(comname, http.formvalue("file"), http.formvalue("subfix")) elseif task == "move" then json = api.to_move(comname, http.formvalue("file")) else - json = api.to_download(comname, http.formvalue("url"), http.formvalue("size")) + json = api.to_download(comname, http.formvalue("url"), http.formvalue("size"), http.formvalue("id")) end http_write_json(json) diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua index 7d8d4484c..63afb968a 100644 --- a/luci-app-passwall2/luasrc/passwall2/api.lua +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -7,6 +7,7 @@ c_config = "passwall2" s_config = "passwall2_server" local com = require "luci.passwall2.com" nixio = require "nixio" +require "nixio.util" fs = require "nixio.fs" sys = require "luci.sys" uci = require"luci.model.uci".cursor() @@ -17,6 +18,7 @@ i18n = require "luci.i18n" curl_args = { "-skfL", "--connect-timeout 3", "--retry 3", "-H 'Accept: */*'" } command_timeout = 300 +DIRECT_GID = 9912 OPENWRT_ARCH = nil DISTRIB_ARCH = nil @@ -197,7 +199,7 @@ function del_cache_var(key) end function set_cache_var(key, val) - sys.call(string.format('. /usr/share/passwall2/utils.sh ; set_cache_var "%s" "%s"', key, val)) + sys.call(string.format('. /usr/share/passwall2/utils.sh ; set_cache_var %s %s', util.shellquote(tostring(key)), util.shellquote(tostring(val)))) end function get_cache_var(key) @@ -226,6 +228,30 @@ function exec_call(cmd) return tonumber(rc), trim(out) end +function exec_call_as_gid(cmd, gid) + local reader, writer = nixio.pipe() + if not reader then return 255, "Unable to create command pipe" end + local pid = nixio.fork() + if not pid then + reader:close() + writer:close() + return 255, "Unable to fork command" + elseif pid == 0 then + reader:close() + nixio.dup(writer, nixio.stdout) + nixio.dup(writer, nixio.stderr) + writer:close() + if not nixio.setgid(gid) then os.exit(1) end + nixio.exec("/bin/sh", "-c", cmd) + os.exit(127) + end + writer:close() + local out = reader:readall() or "" + reader:close() + local _, status, code = nixio.waitpid(pid) + return status == "exited" and code or 255, trim(out) +end + function base64Decode(text) if type(text) ~= "string" then return "" end local encoded = text:gsub("%z", ""):gsub("%c", ""):gsub("_", "/"):gsub("-", "+"):gsub("=+$", "") @@ -274,22 +300,38 @@ function get_domain_port_from_url(url) return domain, port end --- Domain resolution -function domainToIPv4(domain, dns) - local Dns = dns or "223.5.5.5" - local IPs = luci.sys.exec('nslookup %s %s | awk \'/^Name:/{getline; if ($1 == "Address:") print $2}\'' % { domain, Dns }) - for IP in string.gmatch(IPs, "%S+") do - if datatypes.ipaddr(IP) and not datatypes.ip6addr(IP) then return IP end +function resolve_domain(domain, dns) + dns = dns or get_cache_var("DIRECT_DNS") or "223.5.5.5" + local address, port = dns:match("^(.-)#(%d+)$") + if not address then address, port = dns:match("^%[(.-)%]:(%d+)$") end + if not address then address, port = dns:match("^([^:]+):(%d+)$") end + address = (address or dns):gsub("^%[(.-)%]$", "%1") + port = port or "53" + local server = datatypes.ip6addr(address) and ("[" .. address .. "]") or address + local rc, result + for _, dns_server in ipairs({server .. ":" .. port, address .. "#" .. port, "223.5.5.5"}) do + rc, result = exec_call_as_gid("LC_ALL=C nslookup " .. util.shellquote(domain) .. " " .. util.shellquote(dns_server), DIRECT_GID) + local ips, seen, answer = {}, {}, false + for line in result:gmatch("[^\r\n]+") do + if line:match("^%s*Name:") then answer = true end + local ip = answer and line:match("^%s*Address%s*%d*:%s*(%S+)") + if ip and (datatypes.ip4addr(ip) or datatypes.ip6addr(ip)) and not seen[ip] then + ips[#ips + 1] = ip + seen[ip] = true + end + end + if #ips > 0 then return ips end end - return nil + return nil, result, rc end -function curl_base(url, file, args) +function curl_base(url, file, args, gid) if not args then args = {} end if file then args[#args + 1] = "-o " .. file end local cmd = string.format('curl %s "%s"', table_join(args), url) + if gid then return exec_call_as_gid(cmd, gid) end return exec_call(cmd) end @@ -311,36 +353,34 @@ end function curl_logic(url, file, args) local return_code, result = curl_proxy(url, file, args) if not return_code or return_code ~= 0 then - return_code, result = curl_base(url, file, args) + return_code, result = curl_direct(url, file, args) end return return_code, result end function curl_direct(url, file, args) - -- Direct access - if not args then args = {} end - local tmp_args = clone(args) + if get_cache_var("ENABLED_DEFAULT_ACL") ~= "1" and get_cache_var("ENABLED_ACLS") ~= "1" then + return curl_base(url, file, args) + end + local tmp_args = clone(args or {}) + tmp_args[#tmp_args + 1] = "--noproxy '*'" local domain, port = get_domain_port_from_url(url) if domain then - local ip = domainToIPv4(domain) - if ip then - tmp_args[#tmp_args + 1] = "--resolve " .. domain .. ":" .. port .. ":" .. ip + local ips, err = resolve_domain(domain) + if not ips then return 6, err end + for i, ip in ipairs(ips) do + if datatypes.ip6addr(ip) then ips[i] = "[" .. ip .. "]" end end + tmp_args[#tmp_args + 1] = "--resolve " .. util.shellquote(domain .. ":" .. port .. ":" .. table.concat(ips, ",")) end - return curl_base(url, file, tmp_args) + return curl_base(url, file, tmp_args, DIRECT_GID) end function curl_auto(url, file, args) - local localhost_proxy = uci_get_c("@global[0]", "localhost_proxy") or "1" - if localhost_proxy == "1" then + if (uci_get_c("@global[0]", "localhost_proxy") or "1") == "1" then return curl_base(url, file, args) - else - local return_code, result = curl_proxy(url, file, args) - if not return_code or return_code ~= 0 then - return_code, result = curl_direct(url, file, args) - end - return return_code, result end + return curl_logic(url, file, args) end function url(...) @@ -853,19 +893,26 @@ function clone(org) end local function get_bin_version_cache(file, cmd) - sys.call("mkdir -p " .. CACHE_PATH) - if fs.access(file) then + if file and file ~= "" and fs.access(file) then chmod_755(file) - local md5 = sys.exec("echo -n $(md5sum " .. file .. " | awk '{print $1}')") - if fs.access(CACHE_PATH .. "/" .. md5) then - return sys.exec("echo -n $(cat %s)" % { CACHE_PATH .. "/" .. md5 }) - else - local version = sys.exec(string.format("echo -n $(%s %s)", file, cmd)) - if version and version ~= "" then - sys.call("echo '%s' > %s" % { version, CACHE_PATH .. "/" .. md5}) - return version + local stat = fs.stat(file) + local cache_file + if stat and stat.dev and stat.ino and stat.size and stat.mtime and stat.ctime then + -- Use metadata to avoid hashing the entire binary on every lookup. + cache_file = CACHE_PATH .. "/binver_" .. table.concat({stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime}, "_") + local cached = fs.readfile(cache_file) + if cached and trim(cached) ~= "" then + return trim(cached) end end + local version = sys.exec(string.format("echo -n $(%s %s)", util.shellquote(file), cmd)) + if version and version ~= "" then + if cache_file then + fs.mkdirr(CACHE_PATH) + fs.writefile(cache_file, version) + end + return version + end end return "" end @@ -886,7 +933,7 @@ end function is_file(path) if path and #path > 1 then - if sys.exec('[ -f "%s" ] && echo -n 1' % path) == "1" then + if fs.stat(path, "type") == "reg" then return true end end @@ -895,7 +942,7 @@ end function is_dir(path) if path and #path > 1 then - if sys.exec('[ -d "%s" ] && echo -n 1' % path) == "1" then + if fs.stat(path, "type") == "dir" then return true end end @@ -1123,7 +1170,7 @@ local default_file_tree = { } function get_api_json(url) - local return_code, content = curl_logic(url, nil, curl_args) + local return_code, content = curl_auto(url, nil, curl_args) if return_code ~= 0 or content == "" then return {} end return jsonc.parse(content) or {} end @@ -1216,7 +1263,7 @@ function to_check(arch, app_name) } end -function to_download(app_name, url, size) +function to_download(app_name, url, size, task_id) local result = check_path(app_name) if result.code ~= 0 then return result @@ -1228,7 +1275,13 @@ function to_download(app_name, url, size) sys.call("/bin/rm -f /tmp/".. app_name .."_download.*") - local tmp_file = trim(util.exec("mktemp -u -t ".. app_name .."_download.XXXXXX")) + local tmp_file + if type(task_id) == "string" and task_id:match("^[%w_-]+$") then + tmp_file = "/tmp/" .. app_name .. "_download." .. task_id + exec("/bin/rm", {"-f", tmp_file}) + else + tmp_file = trim(util.exec("mktemp -u -t " .. app_name .. "_download.XXXXXX")) + end if size then local kb1 = get_free_space("/tmp") @@ -1240,7 +1293,7 @@ function to_download(app_name, url, size) local _curl_args = clone(curl_args) table.insert(_curl_args, "--speed-limit 51200 --speed-time 15 --max-time 300") - local return_code, result = curl_logic(url, tmp_file, _curl_args) + local return_code, result = curl_auto(url, tmp_file, _curl_args) result = return_code == 0 if not result then @@ -1254,6 +1307,23 @@ function to_download(app_name, url, size) return {code = 0, file = tmp_file, zip = com[app_name].zipped } end +function to_download_progress(app_name, task_id, total_size) + if not com[app_name] or type(task_id) ~= "string" or not task_id:match("^[%w_-]+$") then + return {code = 1, error = i18n.translate("Invalid download task.")} + end + + total_size = tonumber(total_size) or 0 + local tmp_file = "/tmp/" .. app_name .. "_download." .. task_id + local downloaded = tonumber(fs.stat(tmp_file, "size")) or 0 + local percent + if total_size > 0 then + -- Leave 100% for the download request's success callback. + percent = math.min(99, math.floor(downloaded * 100 / total_size)) + end + + return {code = 0, downloaded = downloaded, total = total_size, percent = percent} +end + function to_extract(app_name, file, subfix) local result = check_path(app_name) if result.code ~= 0 then @@ -1446,7 +1516,7 @@ function cacheFileCompareToLogic(file, str) end function is_js_luci() - return sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 + return fs.stat("/www/luci-static/resources/uci.js", "type") == "reg" end function set_default_cbi() @@ -1519,6 +1589,16 @@ function set_default_cbi() return obj end end + if true then + --Table + local Table = cbi.Table + local default_init = Table.__init__ + function Table.__init__(self, form, ...) + default_init(self, form, ...) + -- The Table's map is an independent data source and needs to receive the API from its parent form. + self.map.api = form.api + end + end if true then --TextValue local TextValue = cbi.TextValue diff --git a/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm b/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm index 2742c923f..0720e935b 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm @@ -129,12 +129,54 @@ local com = self.com var appUpdateUrl = '<%=api.url("update_")%>' + app; var appInfo = appInfoList[app]; + var downloadId = Date.now().toString(36) + Math.random().toString(36).substring(2, 8); + var totalSize = appInfo && appInfo.data ? Number(appInfo.data.size) : 0; + if (!isFinite(totalSize) || totalSize <= 0) { + totalSize = 0; + } + var downloadProgressActive = true; + var downloadProgressPending = false; + var progressTimer = null; + + function stopDownloadProgress() { + downloadProgressActive = false; + if (progressTimer) { + clearInterval(progressTimer); + progressTimer = null; + } + } + + function updateDownloadProgress() { + if (!downloadProgressActive || downloadProgressPending || totalSize <= 0) { + return; + } + downloadProgressPending = true; + XHR.get(appUpdateUrl, { + token: tokenStr, + task: 'progress', + id: downloadId, + total_size: totalSize + }, function (x, json) { + downloadProgressPending = false; + if (downloadProgressActive && json && !json.code && typeof json.percent === 'number') { + btn.value = downloadingText + ' ' + json.percent + '%'; + } + }, 300); + } + + if (totalSize > 0) { + updateDownloadProgress(); + progressTimer = setInterval(updateDownloadProgress, 1000); + } + // Download file XHR.get(appUpdateUrl, { token: tokenStr, url: appInfo ? appInfo.data.browser_download_url : '', - size: appInfo ? appInfo.data.size / 1024 : null + size: appInfo ? appInfo.data.size / 1024 : null, + id: downloadId }, function (x, json) { + stopDownloadProgress(); if (json.code) { removePageNotice(); onRequestError(btn, json.error); @@ -233,4 +275,4 @@ local com = self.com }); //]]> - \ No newline at end of file + diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_listvalue.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_listvalue.htm index 8749d5485..295f9ad11 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_listvalue.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_listvalue.htm @@ -7,6 +7,7 @@ -- Description: Passwall(2) UI template local json = require "luci.jsonc" +local api = self.map.api -- 读取值(keylist/vallist/group) local values = {} @@ -48,6 +49,30 @@ for _, item in ipairs(values) do end end +-- 仅共享组名、节点 key/label 及顺序完全相同的分组,保留各控件的候选限制 +local group_cache = self.map._lv_group_cache +if not group_cache then + group_cache = {} + self.map._lv_group_cache = group_cache +end +local group_refs = {} +local new_groups = {} +for i, g in ipairs(group_order) do + -- 使用有序数组生成完整签名,不依赖 JSON 对象的字段顺序或哈希值 + local signature = {g} + for _, item in ipairs(groups[g]) do + signature[#signature + 1] = {item.key, item.label} + end + local cache_key = json.stringify(signature) + local ref = group_cache[cache_key] + if not ref then + ref = cbid .. ".group." .. i + group_cache[cache_key] = ref + new_groups[ref] = groups[g] + end + group_refs[i] = ref +end + -- 如果没有配置值,默认取第一个(优先无组第一项,否则第一组第一项) if not current_key then if #ungrouped > 0 then @@ -73,19 +98,28 @@ local function find_label_by_key(k) end local current_label = find_label_by_key(current_key) or "" --- 打包数据给 JS +-- 控件只携带分组引用,重复的候选数据在页面中仅输出一次 local dropdown_data = { + appname = api.appname, current_key = current_key, current_label = current_label, ungrouped = ungrouped, - groups = groups, + group_refs = group_refs, group_order = group_order, cbid = cbid, } %>
@@ -98,7 +132,7 @@ window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
- <%=pcdata("("..translate("Not set")..")")%> + <%=pcdata("( "..translate("Not Set").." )")%>
@@ -141,7 +175,7 @@ window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>; }); lv_registerAdaptive(cbid); setTimeout(() => { - labelSpan.textContent = lv_ellipsisByWidth(cbid, '<%=pcdata(current_label ~= "" and current_label or ("("..translate("Not set")..")"))%>'); + labelSpan.textContent = lv_ellipsisByWidth(cbid, '<%=pcdata(current_label ~= "" and current_label or ("( "..translate("Not Set").." )"))%>'); }, 100); })(); //]]> diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_listvalue_com.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_listvalue_com.htm index 0e8940eff..2a066287b 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_listvalue_com.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_listvalue_com.htm @@ -207,7 +207,7 @@ if (!rgb) return 'unknown'; // Handle invalid colors // Calculate YIQ brightness (human eye perception) const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000; - + return brightness > 128 ? 'light' : 'dark'; } @@ -538,7 +538,7 @@ "&":"&", "<":"<", ">":">", '"':""", "'":"'" }[c])); } - + function lv_change(cbid, listContainer, hiddenSelect, labelSpan, searchInput, new_key, new_text) { //改值 hiddenSelect.options[0].value = new_key; @@ -549,7 +549,7 @@ lv_highlightSelectedItem(listContainer, hiddenSelect); lv_updateGroupCounts(cbid, listContainer, hiddenSelect, searchInput); } - + function lv_render_dropdown_list(cbid, panel, listContainer, hiddenSelect, labelSpan, searchInput, display) { if (window.lv_dropdown_rendered[cbid]) return; const data = window.lv_dropdown_data[cbid]; @@ -577,8 +577,10 @@ } // 分组项 - data.group_order.forEach(gname => { - const items = data.groups[gname]; + const groupPool = window.lv_groups_pool && window.lv_groups_pool[data.appname]; + data.group_order.forEach((gname, index) => { + const items = data.groups ? data.groups[gname] : groupPool && groupPool[data.group_refs[index]]; + if (!Array.isArray(items)) return; html += `
diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value.htm index ccaaedc12..7fe732073 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value.htm @@ -6,6 +6,7 @@ -- Description: Passwall(2) UI template local json = require "luci.jsonc" +local api = self.map.api -- 读取值(keylist/vallist/group) local values = {{key = "", label = translate("-- Please choose --")}} @@ -56,6 +57,30 @@ if not current_key_added and current_key and current_key ~= "" then table.insert(ungrouped, {key = current_key, label = current_key}) end +-- 仅共享候选分组,各控件的自定义地址仍保留在自己的 ungrouped 中 +local group_cache = self.map._v_group_cache +if not group_cache then + group_cache = {} + self.map._v_group_cache = group_cache +end +local group_refs = {} +local new_groups = {} +for i, g in ipairs(group_order) do + -- 组名、节点 key/label 及顺序完全相同才复用,不与 ListValue 缓存混用 + local signature = {g} + for _, item in ipairs(groups[g]) do + signature[#signature + 1] = {item.key, item.label} + end + local cache_key = json.stringify(signature) + local ref = group_cache[cache_key] + if not ref then + ref = cbid .. ".v-group." .. i + group_cache[cache_key] = ref + new_groups[ref] = groups[g] + end + group_refs[i] = ref +end + -- 如果没有配置值,默认取第一个(优先无组第一项,否则第一组第一项) if not current_key then if #ungrouped > 0 then @@ -81,19 +106,28 @@ local function find_label_by_key(k) end local current_label = find_label_by_key(current_key) or "" --- 打包数据给 JS +-- 控件只携带分组引用,重复的候选数据在页面中仅输出一次 local dropdown_data = { + appname = api.appname, current_key = current_key, current_label = current_label, ungrouped = ungrouped, - groups = groups, + group_refs = group_refs, group_order = group_order, cbid = cbid, } %>
diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value_com.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value_com.htm index 3aa8f39f7..0c5f5c02c 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value_com.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value_com.htm @@ -202,7 +202,7 @@ if (!rgb) return 'unknown'; // Handle invalid colors // Calculate YIQ brightness (human eye perception) const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000; - + return brightness > 128 ? 'light' : 'dark'; } @@ -603,8 +603,10 @@ } // 分组项 - data.group_order.forEach(gname => { - const items = data.groups[gname]; + const groupPool = window.v_groups_pool && window.v_groups_pool[data.appname]; + data.group_order.forEach((gname, index) => { + const items = data.groups ? data.groups[gname] : groupPool && groupPool[data.group_refs[index]]; + if (!Array.isArray(items)) return; html += `
diff --git a/luci-app-passwall2/root/usr/share/passwall2/app.sh b/luci-app-passwall2/root/usr/share/passwall2/app.sh index f11e87065..8a5e79ed0 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/app.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/app.sh @@ -47,7 +47,7 @@ check_run_environment() { [ -d "/lib/apk/packages" ] && { file_path="/lib/apk/packages"; file_ext=".list"; } if [ "$USE_TABLES" = "iptables" ]; then - dep_list="iptables-mod-tproxy iptables-mod-socket iptables-mod-iprange iptables-mod-conntrack-extra kmod-ipt-nat" + dep_list="iptables-mod-tproxy iptables-mod-socket iptables-mod-iprange iptables-mod-conntrack-extra iptables-mod-extra kmod-ipt-nat" else dep_list="kmod-nft-socket kmod-nft-tproxy kmod-nft-nat" nftflag=1 @@ -923,6 +923,8 @@ start() { set_cache_var "USE_TABLES" "$USE_TABLES" fi } + set_cache_var "ENABLED_DEFAULT_ACL" "$ENABLED_DEFAULT_ACL" + set_cache_var "ENABLED_ACLS" "$ENABLED_ACLS" if [ "$ENABLED_DEFAULT_ACL" == 1 ] || [ "$ENABLED_ACLS" == 1 ]; then bridge_nf_ipt=$(sysctl -e -n net.bridge.bridge-nf-call-iptables) set_cache_var "bak_bridge_nf_ipt" "$bridge_nf_ipt" @@ -1059,6 +1061,12 @@ get_config() { DIRECT_DNS_QUERY_STRATEGY=$(config_n_get @global[0] direct_dns_query_strategy UseIP) get_direct_dns + # nslookup uses UDP; TCP direct DNS remains configured for the proxy cores. + if [ "$DIRECT_DNS_PROTO" = "udp" ]; then + set_cache_var "DIRECT_DNS" "${DIRECT_DNS_SERVER}#${DIRECT_DNS_PORT}" + else + set_cache_var "DIRECT_DNS" "${AUTO_DNS%%,*}" + fi DEFAULT_DNSMASQ_CONF_DIR=/tmp/dnsmasq.d DNSMASQ_CONF_DIR=${DEFAULT_DNSMASQ_CONF_DIR} diff --git a/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua b/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua index 47276ce10..1f2468ca6 100644 --- a/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua @@ -21,7 +21,7 @@ local haproxy_dns = "127.0.0.1" local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1" local health_check_type = api.uci_get_c("@global_haproxy[0]", "health_check_type") or "tcp" -local health_check_inter = api.uci_get_c("@global_haproxy[0]", "health_check_inter") or "20" +local health_check_inter = api.uci_get_c("@global_haproxy[0]", "health_check_inter") or "60" local balancingStrategy = api.uci_get_c("@global_haproxy[0]", "balancingStrategy") or "roundrobin" local console_port = api.uci_get_c("@global_haproxy[0]", "console_port") local bind_local = api.uci_get_c("@global_haproxy[0]", "bind_local") or "0" @@ -44,7 +44,7 @@ global nbthread {{nbthread}} external-check insecure-fork-wanted - spread-checks 50 + spread-checks 20 tune.max-checks-per-thread 5 defaults @@ -99,7 +99,7 @@ api.uci_foreach_c("haproxy_config", function(t) local lbss = t.lbss local listen_port = tonumber(t.haproxy_port) or 0 local server_node = api.uci_get_c(lbss) - local hop = (health_check_type == "script_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil + local hop = (health_check_type == "script_logic") and server_node and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil hop = hop and hop:gsub(":", "-") or nil if server_node and server_node.address and (server_node.port or hop) then server_remark = server_node.address .. ":" .. (server_node.port or hop) @@ -153,9 +153,12 @@ api.uci_foreach_c("haproxy_config", function(t) end end) -local inter = tonumber((health_check_inter or "20"):match("^%d+")) -inter = (inter and inter >= 20) and inter or 20 -local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s" +local inter = tonumber(health_check_inter:match("^%d+")) or 60 +inter = math.max(inter, 20) +-- Bound transition checks so long normal intervals do not delay removal or recovery. +local fastinter = math.max(10, math.min(15, math.floor(inter / 2))) +local downinter = math.ceil(inter * 1.5) +local inter_arg = string.format("inter %ds fastinter %ds downinter %ds", inter, fastinter, downinter) local sortTable = {} for i in pairs(listens) do @@ -165,7 +168,28 @@ for i in pairs(listens) do end table.sort(sortTable, function(a,b) return (a < b) end) -for i, port in pairs(sortTable) do +local function server_endpoint(o) + local address = o.server_address:gsub("^%[(.*)%]$", "%1") + if address:find(":", 1, true) then address = "[" .. address .. "]" end + return address .. ":" .. o.server_port +end + +local check_counts = {} +if health_check_type == "script_logic" then + for _, port in ipairs(sortTable) do + for _, o in ipairs(listens[port]) do + -- Domains can resolve to different IPs; share checks only for actual IP endpoints. + if api.is_ip(o.server_address) then + local endpoint = server_endpoint(o) + check_counts[endpoint] = (check_counts[endpoint] or 0) + 1 + end + end + end +end +local shared_checks = {} +local shared_check_names = {} + +for i, port in ipairs(sortTable) do log(1, api.i18n.translatef("Entrance %s:%s", bind_address, port)) f_out:write("\n" .. string.format([[ @@ -184,7 +208,7 @@ listen %s local count_M, count_B = 1, 1 for i, o in ipairs(listens[port]) do - local remark = o.server_remark or "" + local remark = (o.server_remark or ""):gsub("[%[%]]", "") -- To prevent duplicate names from causing the program to fail to run. if tostring(o.backup) ~= "1" then remark = "M" .. count_M .. "-" .. remark @@ -193,8 +217,18 @@ listen %s remark = "B" .. count_B .. "-" .. remark count_B = count_B + 1 end - local server = o.server_address .. ":" .. o.server_port - local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}" + local server = server_endpoint(o) + local check_arg = "check " .. inter_arg .. " rise 2 fall 2" + if (check_counts[server] or 0) > 1 then + local name = shared_check_names[server] + if not name then + name = "check_" .. (#shared_checks + 1) + shared_check_names[server] = name + table.insert(shared_checks, {name = name, server = server}) + end + check_arg = "track " .. appname .. "_health_checks/" .. name + end + local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} {{check_arg}} {{backup}}" server_conf = server_conf:gsub("{{remark}}", remark) server_conf = server_conf:gsub("{{server}}", server) server_conf = server_conf:gsub("{{weight}}", o.lbweight) @@ -203,7 +237,7 @@ listen %s resolvers = "" end server_conf = server_conf:gsub("{{resolvers}}", resolvers) - server_conf = server_conf:gsub("{{inter_arg}}", inter_arg) + server_conf = server_conf:gsub("{{check_arg}}", check_arg) server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "") f_out:write(" " .. api.trim(server_conf) .. "\n") @@ -216,6 +250,20 @@ listen %s end end +-- A separate backend keeps manual maintenance on one listener from affecting others. +if #shared_checks > 0 then + f_out:write(string.format([[ + +backend %s_health_checks + mode tcp + option external-check + external-check command "/usr/share/%s/haproxy_check.sh" +]], appname, appname)) + for _, check in ipairs(shared_checks) do + f_out:write(string.format("\tserver %s %s check %s rise 2 fall 2\n", check.name, check.server, inter_arg)) + end +end + -- Console config local console_user = api.uci_get_c("@global_haproxy[0]", "console_user") local console_password = api.uci_get_c("@global_haproxy[0]", "console_password") @@ -234,8 +282,7 @@ f_out:close() -- Built-in health check URL if health_check_type == "script_logic" then - local probeUrl = api.uci_get_c("@global_haproxy[0]", "health_probe_url") or "https://www.google.com/generate_204" - local f_url = io.open(haproxy_path .. "/Probe_URL", "w") - f_url:write(probeUrl) - f_url:close() + local probeUrl = api.trim(api.uci_get_c("@global_haproxy[0]", "health_probe_url") or "") + if probeUrl == "" then probeUrl = "https://www.google.com/generate_204" end + api.set_cache_var("HAPROXY_PROBE_URL", probeUrl) end diff --git a/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh b/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh index 106b4cf01..8fc30434a 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh @@ -1,7 +1,7 @@ #!/bin/sh export PATH=/usr/sbin:/usr/bin:/sbin:/bin:/root/bin -CONFIG=passwall2 +. /usr/share/passwall2/utils.sh listen_address=$1 listen_port=$2 @@ -13,19 +13,16 @@ busybox pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|ipt exit 0 } -probe_file="/tmp/etc/${CONFIG}/haproxy/Probe_URL" -probeUrl="https://www.google.com/generate_204" -if [ -f "$probe_file" ]; then - firstLine=$(head -n 1 "$probe_file" | tr -d ' \t\n') - [ -n "$firstLine" ] && probeUrl="$firstLine" -fi +probeUrl=$(get_cache_var "HAPROXY_PROBE_URL") +probeUrl="${probeUrl:-https://www.google.com/generate_204}" -extra_params="-x socks5h://${server_address}:${server_port}" -if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then - extra_params="${extra_params} --retry-all-errors" -fi +case "$server_address" in + \[*\]) ;; + *:*) server_address="[$server_address]" ;; +esac -status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 --max-time 6 -w "%{http_code}" "${probeUrl}") +# Probe once per round; HAProxy rise/fall handles consecutive successes and failures. +status=$(/usr/bin/curl -q -I -o /dev/null -sk --noproxy "" -x "socks5h://${server_address}:${server_port}" --connect-timeout 3 --max-time 6 -w "%{http_code}" "$probeUrl") || exit 1 case "$status" in 200|204) diff --git a/luci-app-passwall2/root/usr/share/passwall2/iptables.sh b/luci-app-passwall2/root/usr/share/passwall2/iptables.sh index aad5cae38..071f1e832 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/iptables.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/iptables.sh @@ -851,6 +851,11 @@ add_firewall_rule() { done } + local direct_ipt + for direct_ipt in "$ipt_n" "$ipt_m" "$ip6t_n" "$ip6t_m"; do + $direct_ipt -I OUTPUT $(comment "PSW2_DIRECT") -m owner --gid-owner $DIRECT_GID -j RETURN + done + log_i18n 0 "%s firewall rules load complete!" "iptables" } @@ -896,7 +901,7 @@ gen_include() { [ -z "${_ipt}" ] && return echo "*$2" - ${_ipt}-save -t $2 | grep "PSW2" | grep -v "\-j PSW2$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\)/-I \1 1/" + ${_ipt}-save -t $2 | grep "PSW2" | grep -v "PSW2_DIRECT" | grep -v "\-j PSW2$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\)/-I \1 1/" echo 'COMMIT' } local __ipt="" @@ -908,6 +913,8 @@ gen_include() { $(extract_rules 4 nat) $(extract_rules 4 mangle) EOT + $ipt_n -I OUTPUT $(comment "PSW2_DIRECT") -m owner --gid-owner $DIRECT_GID -j RETURN + $ipt_m -I OUTPUT $(comment "PSW2_DIRECT") -m owner --gid-owner $DIRECT_GID -j RETURN [ "$accept_icmp" = "1" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "$(dst $IPSET_DIRECT !) -p icmp -j PSW2") [ -z "${is_tproxy}" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "$(dst $IPSET_DIRECT !) -p tcp -j PSW2") @@ -925,6 +932,8 @@ gen_include() { $(extract_rules 6 nat) $(extract_rules 6 mangle) EOT + $ip6t_n -I OUTPUT $(comment "PSW2_DIRECT") -m owner --gid-owner $DIRECT_GID -j RETURN + $ip6t_m -I OUTPUT $(comment "PSW2_DIRECT") -m owner --gid-owner $DIRECT_GID -j RETURN [ "$accept_icmpv6" = "1" ] && $ip6t_n -A PREROUTING $(dst $IPSET_DIRECT6 !) -p ipv6-icmp -j PSW2 diff --git a/luci-app-passwall2/root/usr/share/passwall2/nftables.sh b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh index 1a243b9bb..1b2c295a4 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/nftables.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh @@ -902,6 +902,9 @@ add_firewall_rule() { filter_direct_node_list > /dev/null 2>&1 & + nft "insert rule $NFTABLE_NAME mangle_output meta skgid $DIRECT_GID counter return comment \"PSW2_DIRECT\"" + nft "insert rule $NFTABLE_NAME nat_output meta skgid $DIRECT_GID counter return comment \"PSW2_DIRECT\"" + log_i18n 0 "%s firewall rules load complete!" "nftables" } diff --git a/luci-app-passwall2/root/usr/share/passwall2/utils.sh b/luci-app-passwall2/root/usr/share/passwall2/utils.sh index 87eaf2c7b..d159826d4 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/utils.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/utils.sh @@ -3,6 +3,7 @@ # Copyright (C) 2026 Openwrt-Passwall Organization CONFIG=passwall2 +DIRECT_GID=9912 APP_PATH=/usr/share/${CONFIG} TMP_PATH=/tmp/etc/${CONFIG} TMP_PATH2=${TMP_PATH}_tmp diff --git a/luci-app-sqm-autorate/htdocs/luci-static/resources/view/network/sqm.js b/luci-app-sqm-autorate/htdocs/luci-static/resources/view/network/sqm.js index 81891e231..10497c504 100644 --- a/luci-app-sqm-autorate/htdocs/luci-static/resources/view/network/sqm.js +++ b/luci-app-sqm-autorate/htdocs/luci-static/resources/view/network/sqm.js @@ -268,14 +268,6 @@ return view.extend({ o.default = false; o.depends("autorate","1"); - o = s.taboption("tab_autorate", form.Value, "reflector_ping_interval_s", _("Reflector ping interval in seconds:")); - o.default = "0.2"; - o.depends("autorate","1"); - - o = s.taboption("tab_autorate", form.Value, "no_pingers", _("Pingers numbers:")); - o.default = "4"; - o.depends("autorate","1"); - // cake-autorate types these by whether the value carries a decimal // point (typeof float vs integer) and refuses to start an instance // whose value has the wrong type, so always persist a float here. @@ -285,8 +277,22 @@ return view.extend({ return form.Value.prototype.write.call(this, section_id, value); }; + // The defaults below are the ones config_template.sh applies to an + // unset option. The page showed others (25.0 ms against 250.0 ms + // for the delay threshold): a value typed in equal to the page's + // default was removed on save, and the backend's ran instead. + o = s.taboption("tab_autorate", form.Value, "reflector_ping_interval_s", _("Reflector ping interval in seconds:")); + o.default = "0.5"; + o.datatype = 'ufloat'; + o.write = writeFloat; + o.depends("autorate","1"); + + o = s.taboption("tab_autorate", form.Value, "no_pingers", _("Pingers numbers:")); + o.default = "4"; + o.depends("autorate","1"); + o = s.taboption("tab_autorate", form.Value, "delay_thr_ms",_("delay threshold in ms:")); - o.default = "25.0"; + o.default = "250.0"; o.datatype = 'ufloat'; o.write = writeFloat; o.depends("autorate","1"); @@ -307,8 +313,9 @@ return view.extend({ o.depends("autorate","1"); o = s.taboption("tab_autorate", form.Value, "startup_wait_s",_("Number of seconds to wait on startup:")); - o.default = "60.0"; - o.datatype = 'float' + o.default = "0.0"; + o.datatype = 'float'; + o.write = writeFloat; o.depends("autorate","1"); o = s.taboption("tab_autorate", form.Flag, "congestion_sync", _("Sync bufferbloat threshold with congestion score"), @@ -351,6 +358,14 @@ return view.extend({ // off; without retain LuCI deletes it with the other hidden fields. o.retain = true; + // Every option hidden by its dependency keeps its stored value: a + // save with autorate, the advanced qdisc options or the link layer + // off deleted delay_thr_ms (omrvpn's 300 ms from 2040-omr-sqm), the + // qdisc option strings and the overhead of every queue + s.children.forEach(function(opt) { + if (opt.deps && opt.deps.length) + opt.retain = true; + }); return m.render(); } diff --git a/luci-app-vantage/Makefile b/luci-app-vantage/Makefile index 94d96d987..b38a384ce 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.4 -PKG_RELEASE:=10 +PKG_VERSION:=1.0.5 +PKG_RELEASE:=11 PKG_LICENSE:=GPL-3.0-or-later LUCI_TITLE:=Vantage dashboard 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 ca0bbd347..3fd22d14d 100644 --- a/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css @@ -23,7 +23,7 @@ --vt-line-2: var(--v-line-2, #cbd0d6); --vt-text: var(--v-text, #15181c); --vt-text-2: var(--v-text-2, #4b525b); - --vt-text-3: var(--v-text-3, #6a717a); + --vt-text-3: var(--v-text-3, #5f666f); /* 4.5:1 on --vt-surface-3 (tags) */ --vt-accent: var(--v-accent, #3563e9); --vt-accent-ink: var(--v-accent-ink, #ffffff); --vt-accent-soft: var(--v-accent-soft, rgba(53, 99, 233, .12)); @@ -71,6 +71,7 @@ --vt-b5: var(--vt-chart-3); --vt-b6: var(--vt-chart-4); --vt-b60: var(--vt-text-2); + --vt-band-ink: 60%; /* band colour in a chip's text: 4.5:1 on light surfaces */ --vt-series-up: var(--vt-chart-1); /* throughput directions: download keeps the uplink colour, upload takes one that no band uses */ @@ -123,6 +124,7 @@ html[data-darkmode="true"] .vt-app { --vt-chart-6: var(--v-chart-6, #d9588c); --vt-chart-grid: var(--v-chart-grid, color-mix(in srgb, var(--vt-text) 7%, transparent)); --vt-fill-op: var(--v-chart-fill-opacity, .2); + --vt-band-ink: 78%; --vt-av-l: 24%; --vt-av-s: 30%; --vt-av-ink-l: 76%; @@ -224,7 +226,7 @@ html[data-darkmode="true"] .vt-app { border-radius: 5px; font-size: var(--vt-fs-micro); font-weight: 700; letter-spacing: .01em; white-space: nowrap; --vt-bc: var(--vt-b60); background: color-mix(in oklab, var(--vt-bc) 14%, transparent); - color: color-mix(in oklab, var(--vt-bc) 78%, var(--vt-text)); + color: color-mix(in oklab, var(--vt-bc) var(--vt-band-ink), var(--vt-text)); } .vt-app .vt-band-2g { --vt-bc: var(--vt-b2); } .vt-app .vt-band-5g { --vt-bc: var(--vt-b5); } @@ -339,6 +341,11 @@ html[data-darkmode="true"] .vt-app { } .vt-app .vt-ov { display: none; } +/* arranging: the handle bar takes the place of the tile's head (it names + the tile), and the content starts below it (--vt-ovh: the bar's height, + measured by the view) instead of under it */ +.vt-app.vt-editing .vt-cell { padding-top: calc(var(--vt-ovh, 50px) + 16px); } +.vt-app.vt-editing .vt-cell > .vt-tile-head { display: none; } .vt-app.vt-editing .vt-ov { display: block; position: absolute; inset: 0; z-index: 4; border: 2px dashed var(--vt-accent-line); border-radius: var(--vt-r-lg); @@ -479,15 +486,26 @@ h2, h3{ .vt-app .vt-subline { color: var(--vt-text-3); font-size: var(--vt-fs-sm); margin-top: 2px; } .vt-app .vt-livepill { - flex: none; display: inline-flex; align-items: center; gap: 8px; + flex: none; display: inline-flex; align-items: center; gap: 8px; isolation: isolate; height: 30px; padding: 0 12px 0 10px; border-radius: 999px; border: 1px solid var(--vt-line); background: var(--vt-surface); font-size: var(--vt-fs-sm); font-weight: 560; color: var(--vt-text-2); white-space: nowrap; } -.vt-app .vt-live-beat { width: 8px; height: 8px; border-radius: 50%; background: var(--vt-ok); box-shadow: 0 0 0 0 color-mix(in oklab, var(--vt-ok) 50%, transparent); animation: vt-beat 2s infinite; } -.vt-app .vt-live-paused .vt-live-beat { background: var(--vt-text-3); animation: none; } -.vt-app .vt-live-err .vt-live-beat { background: var(--vt-warn); } -@keyframes vt-beat { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--vt-ok) 45%, transparent); } 70% { box-shadow: 0 0 0 7px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } } +.vt-app .vt-live-beat { position: relative; width: 8px; height: 8px; border-radius: 50%; background: var(--vt-beat-c, var(--vt-ok)); } +/* the beat is a ring that scales and fades (compositor only; an animated + box-shadow repainted every frame). It sits behind the dot (z-index -1 in + the pill's stacking context) so the dot keeps its colour, and it is + transparent unless the animation runs: nothing shows without motion. */ +.vt-app .vt-live-beat::after { + content: ""; position: absolute; inset: 0; z-index: -1; border-radius: 50%; + background: color-mix(in oklab, var(--vt-beat-c, var(--vt-ok)) 45%, transparent); + opacity: 0; + animation: vt-beat 2s ease-out infinite; +} +.vt-app .vt-live-paused .vt-live-beat { --vt-beat-c: var(--vt-text-3); } +.vt-app .vt-live-paused .vt-live-beat::after { animation: none; } +.vt-app .vt-live-err .vt-live-beat { --vt-beat-c: var(--vt-warn); } +@keyframes vt-beat { 0% { transform: scale(1); opacity: 1; } 70%, 100% { transform: scale(2.75); opacity: 0; } } .vt-app .vt-health { display: grid; gap: 10px; margin-bottom: 14px; @@ -571,14 +589,20 @@ h2, h3{ .vt-app .vt-wire { fill: none; stroke: var(--vt-line-2); stroke-width: 1.5; transition: stroke .4s; } .vt-app .vt-wire-main { stroke-width: 2.5; } .vt-app .vt-wire-on { stroke: color-mix(in oklab, var(--vt-accent-line) 40%, var(--vt-line-2)); } +/* traffic on a wire: dots about 16 px apart moving along it (1.6, 1.05 + or 0.6 s per 32 px by load), each a small element the view animates with + transform, so the compositor moves them without repainting the wires. + The SVG flow path is only the static dotted look under reduced motion. */ .vt-app .vt-flow { fill: none; stroke: var(--vt-accent-line); stroke-width: 2.4; stroke-linecap: round; - stroke-dasharray: 1.5 14.5; opacity: 0; transition: opacity .4s; + stroke-dasharray: 1.5 6; opacity: 0; } -.vt-app .vt-flow-on { opacity: .95; animation: vt-flow 1.6s linear infinite; } -.vt-app .vt-flow-2.vt-flow-on { animation-duration: 1.05s; } -.vt-app .vt-flow-3.vt-flow-on { animation-duration: .6s; stroke-width: 3; } -@keyframes vt-flow { to { stroke-dashoffset: -32; } } +.vt-app .vt-flows, .vt-app .vt-fwire { position: absolute; left: 0; top: 0; width: 0; height: 0; z-index: 0; pointer-events: none; } +.vt-app .vt-fwire { opacity: 0; transition: opacity .4s; } +.vt-app .vt-fwire-on { opacity: .95; } +/* a 1.5 px dash with round caps of the 2.4 px stroke, centred on its point */ +.vt-app .vt-fdot { position: absolute; left: -1.95px; top: -1.2px; width: 3.9px; height: 2.4px; border-radius: 1.2px; background: var(--vt-accent-line); } +.vt-app .vt-fwire-3 .vt-fdot { left: -2.25px; top: -1.5px; width: 4.5px; height: 3px; border-radius: 1.5px; } .vt-app .vt-node { position: relative; z-index: 1; @@ -675,14 +699,18 @@ h2, h3{ .vt-app .vt-cores-box { display: flex; flex-direction: column; align-items: center; flex: none; cursor: help; } .vt-app .vt-cores { display: flex; gap: 3px; height: 38px; align-items: flex-end; } .vt-app .vt-core { position: relative; width: 6px; height: 100%; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; } -.vt-app .vt-core-fill { position: absolute; left: 0; right: 0; bottom: 0; border-radius: 3px; background: var(--vt-chart-5); transition: height .6s ease; } +.vt-app .vt-core-fill { position: absolute; left: 0; right: 0; bottom: 0; border-radius: 3px; background: var(--vt-chart-5); } .vt-app .vt-sys-mem { display: flex; flex-direction: column; gap: 7px; } .vt-app .vt-mem-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; } .vt-app .vt-mem-avail { font-size: var(--vt-fs-lg); font-weight: 680; } .vt-app .vt-mem-of { font-size: var(--vt-fs-sm); color: var(--vt-text-3); } .vt-app .vt-seg { display: flex; height: 12px; border-radius: 6px; overflow: hidden; background: var(--vt-surface-3); gap: 2px; } -.vt-app .vt-seg-part { height: 100%; min-width: 0; transition: width .7s cubic-bezier(.2, .7, .2, 1); } +/* width animates only for a visible change (the view sets .vt-seg-move): + a width transition lays out every frame, and memory moves by fractions + of a percent on most polls */ +.vt-app .vt-seg-part { height: 100%; min-width: 0; } +.vt-app .vt-seg-move .vt-seg-part { transition: width .7s cubic-bezier(.2, .7, .2, 1); } .vt-app .vt-seg-used { background: var(--vt-mem); border-radius: 6px 2px 2px 6px; } .vt-app .vt-seg-cache { background: @@ -704,7 +732,7 @@ h2, h3{ .vt-app .vt-storage { display: flex; flex-direction: column; gap: 8px; } .vt-app .vt-mount-top { display: flex; justify-content: space-between; gap: 8px; font-size: var(--vt-fs-sm); margin-bottom: 4px; } .vt-app .vt-bar { height: 6px; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; } -.vt-app .vt-bar-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-mem); transition: width .7s ease; } +.vt-app .vt-bar-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-mem); } .vt-app .vt-bg-warn { background: var(--vt-warn); } .vt-app .vt-bg-err { background: var(--vt-err); } .vt-app .vt-fw { display: flex; flex-direction: column; gap: 1px; margin-top: auto; padding-top: 10px; border-top: 1px dashed var(--vt-line); font-size: var(--vt-fs-sm); color: var(--vt-text-2); } @@ -919,7 +947,7 @@ h2, h3{ .vt-app .vt-live-cell { display: grid; grid-template-columns: auto; gap: 3px; min-width: 96px; } .vt-app .vt-live-total { font-weight: 650; } .vt-app .vt-mini { display: block; height: 4px; border-radius: 2px; background: var(--vt-surface-3); overflow: hidden; } -.vt-app .vt-mini-fill { display: block; height: 100%; background: var(--vt-info); border-radius: 2px; transition: width .6s ease; } +.vt-app .vt-mini-fill { display: block; height: 100%; background: var(--vt-info); border-radius: 2px; } .vt-app .vt-live-split { font-size: var(--vt-fs-micro); color: var(--vt-text-3); white-space: nowrap; } .vt-app .vt-exp { display: flex; align-items: center; gap: 9px; min-width: 0; } .vt-app .vt-score { @@ -957,7 +985,7 @@ h2, h3{ .vt-app .vt-talk-share { font-size: var(--vt-fs-sm); font-weight: 650; color: var(--vt-text-2); white-space: nowrap; } .vt-app .vt-talk-sub b { color: var(--vt-text); font-weight: 650; } .vt-app .vt-talk-bar { height: 5px; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; } -.vt-app .vt-talk-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-info); transition: width .6s ease; } +.vt-app .vt-talk-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-info); } .vt-app .vt-talk-sub { display: flex; gap: 8px; align-items: baseline; font-size: var(--vt-fs-micro); color: var(--vt-text-3); white-space: nowrap; overflow: hidden; } .vt-app .vt-ssids { display: flex; flex-direction: column; gap: 2px; } @@ -1092,7 +1120,12 @@ h2, h3{ reconnecting still says so in words */ .vt-app .vt-live-ok { padding: 0 11px; } .vt-app .vt-live-ok .vt-live-text { display: none; } - .vt-app .vt-livepill .vt-live-text { max-width: 110px; overflow: hidden; text-overflow: ellipsis; } + /* "Paused while hidden" and the like get a row of their own under + the headline instead of being cut (the orb is 32 px + the 10 px gap) */ + .vt-app .vt-head { flex-wrap: wrap; row-gap: 8px; } + .vt-app .vt-head-text { flex-basis: 160px; } + .vt-app .vt-livepill:not(.vt-live-ok) { order: 1; margin-left: 42px; min-width: 0; max-width: calc(100% - 42px); } + .vt-app .vt-livepill .vt-live-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; } .vt-app .vt-chart .vt-tile-extra { flex-basis: 100%; justify-content: space-between; } .vt-app .vt-health { grid-template-columns: 1fr 1fr; gap: 8px; } .vt-app .vt-chip { padding: 9px 10px; gap: 8px; } @@ -1163,5 +1196,7 @@ h2, h3{ @media (prefers-reduced-motion: reduce) { .vt-app *, .vt-app *::before, .vt-app *::after { animation: none !important; transition: none !important; } - .vt-app .vt-flow-on { opacity: .8; stroke-dasharray: 1.5 6; } + .vt-app .vt-flow-on { opacity: .8; } + .vt-app .vt-flow-3.vt-flow-on { stroke-width: 3; } + .vt-app .vt-flows { display: none; } } 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 c3dac3906..3fce498b4 100644 --- a/luci-app-vantage/htdocs/luci-static/resources/vantage/layout.js +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/layout.js @@ -187,6 +187,19 @@ return baseclass.extend({ return { h: prev.h, since: since }; }, + /* ms until the earliest held height may be released (0 when one is + already due), or null when nothing is held */ + nextRelease: function(held, now) { + var next = null, self = this; + Object.keys(held || {}).forEach(function(id) { + var s = held[id]; + if (!s || s.since == null) return; + var left = Math.max(0, s.since + self.HOLD_MS - now); + if (next == null || left < next) next = left; + }); + return next; + }, + /* where shown tiles go. items: [{ id, c, w, h }] in order, h measured; returns { rects: { id: { c, w, top, h } }, height }. A tile that ends a little above the next one in its columns stretches to meet diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/wires.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/wires.js new file mode 100644 index 000000000..d4175a9b3 --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/wires.js @@ -0,0 +1,95 @@ +'use strict'; +'require baseclass'; + +/* Geometry of the network path's wires and of the dots that flow along + them. The view measures the nodes; this module turns two boxes into a + path (`d` for the SVG wire) plus the same curve as points, and the + points into evenly spaced keyframes, so the dots can move with + transform (compositor only) instead of an animated stroke-dashoffset, + which repainted the whole wire layer every frame. Pure functions. + + A box is { l, r, t, b, cy } relative to the path element. */ + +var STEPS = 32; /* points per curve before resampling */ +var GAP = 16; /* px between dots, as the old dash pattern */ +var STRIDE = 32; /* px a dot travels per period */ +var PERIOD = { 1: 1600, 2: 1050, 3: 600 }; /* ms per STRIDE by speed */ + +function f1(v) { return v.toFixed(1); } + +return baseclass.extend({ + GAP: GAP, + + /* bits per second -> 1 (idle trickle), 2, 3 (busy) */ + speed: function(bps) { return bps > 100e6 ? 3 : bps > 5e6 ? 2 : 1; }, + + /* from box a to box b: side by side an S-curve between their facing + edges, stacked an elbow down a's left side into b */ + curve: function(a, b) { + var x1, y1, x2, y2, pts = [], i; + if (b.l >= a.r - 2) { + x1 = a.r; y1 = a.cy; x2 = b.l; y2 = b.cy; + var mx = (x1 + x2) / 2; + for (i = 0; i <= STEPS; i++) { + var t = i / STEPS, u = 1 - t; + /* cubic Bézier with both control points at mx */ + pts.push([ u * u * u * x1 + 3 * u * t * mx + t * t * t * x2, (u * u * u + 3 * u * u * t) * y1 + (3 * u * t * t + t * t * t) * y2 ]); + } + return { d: 'M' + f1(x1) + ' ' + f1(y1) + 'C' + f1(mx) + ' ' + f1(y1) + ' ' + f1(mx) + ' ' + f1(y2) + ' ' + f1(x2) + ' ' + f1(y2), pts: pts }; + } + x1 = a.l + 18; y1 = a.b; x2 = b.l; y2 = b.cy; + if (x2 <= x1 + 6) return { d: 'M' + f1(x1) + ' ' + f1(y1) + 'V' + f1(b.t), pts: [ [ x1, y1 ], [ x1, b.t ] ] }; + var rad = Math.min(8, Math.max(0, y2 - y1)); + pts.push([ x1, y1 ]); + for (i = 0; i <= 8; i++) { + /* quadratic corner from (x1, y2 - rad) via (x1, y2) to (x1 + rad, y2) */ + var s = i / 8, v = 1 - s; + pts.push([ v * v * x1 + 2 * v * s * x1 + s * s * (x1 + rad), v * v * (y2 - rad) + 2 * v * s * y2 + s * s * y2 ]); + } + pts.push([ x2, y2 ]); + return { d: 'M' + f1(x1) + ' ' + f1(y1) + 'V' + f1(y2 - rad) + 'Q' + f1(x1) + ' ' + f1(y2) + ' ' + f1(x1 + rad) + ' ' + f1(y2) + 'H' + f1(x2), pts: pts }; + }, + + /* points at equal distances along a polyline, about `step` px apart, + each with the direction of travel in degrees (unwrapped, so the + keyframes never turn the long way round); { pts: [[x, y, deg]], len } */ + resample: function(pts, step) { + var cum = [ 0 ], i, out = []; + for (i = 1; i < pts.length; i++) + cum.push(cum[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1])); + var len = cum[cum.length - 1]; + if (!(len > 0)) return { pts: [], len: 0 }; + var n = Math.max(1, Math.ceil(len / step)), j = 1, prev = null; + for (i = 0; i <= n; i++) { + var s = len * i / n; + while (j < pts.length - 1 && cum[j] < s) j++; + /* the segment [j-1, j] holds s; skip zero-length segments for the angle */ + var k = j; + while (k < pts.length - 1 && cum[k] === cum[k - 1]) k++; + var seg = cum[j] - cum[j - 1], f = seg > 0 ? (s - cum[j - 1]) / seg : 0; + var dx = pts[k][0] - pts[k - 1][0], dy = pts[k][1] - pts[k - 1][1]; + var deg = Math.atan2(dy, dx) * 180 / Math.PI; + if (prev != null) { while (deg - prev > 180) deg -= 360; while (deg - prev < -180) deg += 360; } + prev = deg; + out.push([ pts[j - 1][0] + (pts[j][0] - pts[j - 1][0]) * f, pts[j - 1][1] + (pts[j][1] - pts[j - 1][1]) * f, deg ]); + } + return { pts: out, len: len }; + }, + + /* the Web Animations keyframes for one trip along the samples */ + keyframes: function(samples) { + var frames = []; + samples.forEach(function(p) { + frames.push({ transform: 'translate(' + p[0].toFixed(2) + 'px, ' + p[1].toFixed(2) + 'px) rotate(' + p[2].toFixed(1) + 'deg)' }); + }); + return frames; + }, + + /* how many dots a wire of this length carries (about GAP apart) */ + dots: function(len) { return Math.max(1, Math.round(len / GAP)); }, + + /* one trip along a wire at speed 1, in ms; faster speeds play it at + rate(speed), so a speed change keeps every dot where it is */ + trip: function(len) { return len / STRIDE * PERIOD[1]; }, + rate: function(speed) { return PERIOD[1] / (PERIOD[speed] || PERIOD[1]); } +}); 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 d4fb12afb..58e2211e1 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 @@ -11,6 +11,7 @@ 'require vantage.insight as insight'; 'require vantage.series as series'; 'require vantage.layout as layout'; +'require vantage.wires as wires'; /* Vantage dashboard (luci-app-vantage): a live, read-only overview of the device, its uplink, radios, SSIDs and stations. The only write is the @@ -245,8 +246,52 @@ function sparkline(data, cols, vmax, cls, opts) { /* -------------------------------------------------------------- helpers */ -function txt(el, s) { el.textContent = (s == null ? '' : String(s)); return el; } -function setW(el, pct) { el.style.width = Math.max(0, Math.min(100, +pct || 0)).toFixed(2) + '%'; } +/* text writes only when the text changes: an unchanged poll leaves the + node, and its layout, alone */ +function txt(el, s) { s = (s == null ? '' : String(s)); if (el.textContent !== s) el.textContent = s; return el; } +function setClass(el, c) { if (el.getAttribute('class') !== c) el.setAttribute('class', c); } +function setTitle(el, v) { if (el.getAttribute('title') !== v) el.setAttribute('title', v); } + +/* an element's own tag and attributes, or a text node's text */ +function nodeSig(n) { + if (n.nodeType !== 1) return n.nodeType + '"' + n.data; + return '<' + n.tagName + n.getAttributeNames().map(function(a) { return ' ' + a + '=' + n.getAttribute(a); }).join(''); +} + +/* what a freshly built subtree would show (tags, attributes, text), so a + poll can keep the nodes it would only rebuild identically */ +function treeSig(nodes) { + var out = []; + (function walk(list) { + Array.prototype.forEach.call(list, function(n) { + if (n == null || typeof n !== 'object') return; + out.push(nodeSig(n)); + if (n.nodeType === 1) { walk(n.childNodes); out.push('>'); } + }); + })(nodes); + return out.join('\n'); +} + +/* put freshly built nodes (Nodes only) into parent, keeping every old + subtree that would come out the same and descending into elements whose + own tag and attributes did not change: a poll that moves one sparkline + replaces that sparkline, not the card around it. Old elements keep their + listeners, so whatever a handler acts on must show in an attribute. */ +function patch(parent, nodes) { + nodes = nodes.filter(function(n) { return n != null && n !== ''; }).map(function(n) { return typeof n === 'object' ? n : document.createTextNode(String(n)); }); + var old = parent.childNodes; + if (old.length !== nodes.length) { + while (parent.firstChild) parent.removeChild(parent.firstChild); + nodes.forEach(function(n) { parent.appendChild(n); }); + return; + } + nodes.slice().forEach(function(n, i) { + var o = old[i]; + if (nodeSig(o) !== nodeSig(n)) parent.replaceChild(n, o); + else if (o.nodeType === 1) patch(o, Array.prototype.slice.call(n.childNodes)); + }); +} +function setW(el, pct) { var w = Math.max(0, Math.min(100, +pct || 0)).toFixed(2) + '%'; if (el.style.width !== w) el.style.width = w; } function hueOf(mac) { var h = 0, s = String(mac || ''); for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360; @@ -283,14 +328,6 @@ function kv(label, value, mono) { ]); } -/* one of the four numbers under the network path */ -function kpi(label, value, sub, tone, onclick) { - var body = [ E('span', { 'class': 'vt-kpi-label' }, [ label ]), E('span', { 'class': 'vt-kpi-value vt-tn' }, [ value ]), E('span', { 'class': 'vt-kpi-sub' }, [ sub ]) ]; - return onclick - ? E('button', { 'type': 'button', 'class': 'vt-kpi vt-kpi-' + (tone || 'plain'), 'click': onclick }, body) - : E('div', { 'class': 'vt-kpi vt-kpi-' + (tone || 'plain') }, body); -} - function tileHead(ic, title, extra) { return E('header', { 'class': 'vt-tile-head' }, [ E('h3', { 'class': 'vt-tile-title' }, [ icon(ic), E('span', {}, [ title ]) ]), @@ -702,6 +739,10 @@ return view.extend({ this.buildDrawer() ]); this.el.root = root; + /* wires follow the nodes' geometry: redrawn when the path or a + node changes size (observer callbacks run after layout, so the + reads there force nothing), never on a poll that moves nothing */ + if (window.ResizeObserver) this.wireRo = new ResizeObserver(function() { self.drawWires(); }); this.paint(); @@ -712,15 +753,23 @@ return view.extend({ }); document.addEventListener('keydown', L.bind(this.handleKey, this)); window.setInterval(function() { self.paintLive(); }, 1000); - if (window.ResizeObserver) new ResizeObserver(function() { self.scheduleWires(); }).observe(this.el.path); if (window.ResizeObserver) { var ro = new ResizeObserver(function() { self.scheduleLayout(); }); ro.observe(this.el.grid); - layout.TILES.forEach(function(id) { ro.observe(self.tiles[id]); }); + layout.TILES.forEach(function(id) { + ro.observe(self.tiles[id]); + /* a held min-height keeps the tile's box still while its + content changes: watch the content as well */ + Array.prototype.forEach.call(self.tiles[id].children, function(c) { if (!c.classList.contains('vt-ov')) ro.observe(c); }); + }); + } + if (window.matchMedia) { + var motion = window.matchMedia('(prefers-reduced-motion: reduce)'); + if (motion.addEventListener) motion.addEventListener('change', function() { self.paintWireState(); }); } window.addEventListener('resize', function() { self.scheduleLayout(); }); window.addEventListener('hashchange', function() { self.openFromHash(); }); - window.requestAnimationFrame(function() { self.drawWires(); self.openFromHash(); }); + window.requestAnimationFrame(function() { if (!self.wireRo) self.drawWires(); self.openFromHash(); }); return root; }, @@ -737,8 +786,11 @@ return view.extend({ this.paintSsids(); this.paintDrawer(); this.paintLive(); - this.scheduleWires(); - this.scheduleLayout(); + if (!this.wireRo) this.scheduleWires(); + /* the tiles' ResizeObserver lays out again when a tile changes + size, so a poll lays out only when the tile set may change (the + first one, wired or not); one that changes nothing measures nothing */ + if (this.laidWired !== !!this.m.wired) { this.laidWired = !!this.m.wired; this.scheduleLayout(); } }, /* ------------------------------------------------------------ folds */ @@ -769,7 +821,7 @@ return view.extend({ writeFolds(this.folds); this.applyFold(key); if (this.held) delete this.held[key]; - if (key === 'hero') this.scheduleWires(); + if (key === 'hero' && !this.wireRo) this.scheduleWires(); if (key === 'chart') this.paintChart(); }, @@ -839,6 +891,11 @@ return view.extend({ t.style.setProperty('--w', Math.min(e.w, cols).toFixed(0)); t.style.minHeight = ''; }); + /* arranging: each tile's content starts below its handle bar */ + if (this.editing) { + var bars = shown.map(function(e) { return self.ov[e.id].bar.offsetHeight; }); + shown.forEach(function(e, k) { tiles[e.id].style.setProperty('--vt-ovh', bars[k].toFixed(0) + 'px'); }); + } /* live content changes hold a tile's height for a while; a new width or an edit shows true sizes at once */ if (this.heldWidth !== width || this.drag) { this.held = Object.create(null); this.heldWidth = width; } @@ -855,6 +912,10 @@ return view.extend({ t.style.setProperty('--y', r.top.toFixed(1) + 'px'); }); grid.style.height = p.height.toFixed(1) + 'px'; + /* polls no longer lay out, so the end of a hold needs its own wake-up */ + var wait = layout.nextRelease(held, now); + if (this.holdTimer) { window.clearTimeout(this.holdTimer); this.holdTimer = null; } + if (wait != null) this.holdTimer = window.setTimeout(function() { self.holdTimer = null; self.scheduleLayout(); }, wait + 50); var g = this.drag && p.rects[this.drag.id], ghost = this.el.ghost; ghost.classList.toggle('vt-on', !!g); if (g) { @@ -915,10 +976,11 @@ return view.extend({ this.el.root.classList.toggle('vt-editing', this.editing); this.el.layoutBtn.setAttribute('aria-pressed', String(this.editing)); this.el.editBar.hidden = !this.editing; - if (this.editing) { - this.doLayout(); - this.el.editDone.focus(); - } + /* the handle bars change the tiles' heights both ways: true sizes + at once, nothing held from the other mode */ + this.held = Object.create(null); + this.doLayout(); + if (this.editing) this.el.editDone.focus(); else this.el.layoutBtn.focus(); }, @@ -985,7 +1047,7 @@ return view.extend({ self.el.editDone.focus(); }); o.root = E('div', { 'class': 'vt-ov vt-fold-keep', 'role': 'group', 'aria-label': name }, [ - E('div', { 'class': 'vt-ov-bar' }, [ + o.bar = E('div', { 'class': 'vt-ov-bar' }, [ o.grip, E('span', { 'class': 'vt-ov-name' }, [ name ]), E('span', { 'class': 'vt-ov-btns' }, [ o.up, o.down, o.left, o.right, o.narrow, o.size, o.wide, o.hide ]) @@ -1145,6 +1207,10 @@ return view.extend({ paintHealth: function() { var self = this; + /* the same checks as last poll: leave the chips alone */ + var sig = JSON.stringify(this.st.health.map(function(c) { return [ c.level, c.title, c.value, c.reason, c.detail, c.client, c.page ]; })); + if (sig === this.healthSig) return; + this.healthSig = sig; dom.content(this.el.health, this.st.health.map(function(c) { var ic = c.level === 'ok' ? 'check' : c.level === 'err' ? 'err' : c.level === 'warn' ? 'warn' : 'info'; var body = [ @@ -1164,11 +1230,15 @@ return view.extend({ /* ------------------------------------------------------------- hero */ buildHero: function() { - var self = this, el = this.el; + var el = this.el; el.path = E('div', { 'class': 'vt-path' }); el.wires = svgRoot('vt-wires', '0 0 10 10'); + /* the flowing dots: small elements moved with transform */ + el.flows = E('div', { 'class': 'vt-flows', 'aria-hidden': 'true' }); el.path.appendChild(el.wires); - this.wireMap = {}; + el.path.appendChild(el.flows); + 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') }, [ 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')) ]), @@ -1177,137 +1247,239 @@ return view.extend({ ])); }, - paintHero: function() { - var self = this, m = this.m, st = this.st, up = m.uplink, path = this.el.path; - var up_r = st.uplinkRate; - - /* keep the wires layer, rebuild the nodes (cheap, a dozen elements) */ - while (path.lastChild && path.lastChild !== this.el.wires) path.removeChild(path.lastChild); - while (path.firstChild && path.firstChild !== this.el.wires) path.removeChild(path.firstChild); - - this.nodes = {}; - var gw = E('button', { 'type': 'button', 'class': 'vt-node vt-node-gw', 'click': function() { self.openDrawer('uplink', 'uplink'); } }, [ - E('span', { 'class': 'vt-node-ico' }, [ icon('globe') ]), - E('span', { 'class': 'vt-node-body' }, [ - E('span', { 'class': 'vt-node-kicker' }, [ up && up.isWan ? _('Internet') : _('Gateway') ]), - E('span', { 'class': 'vt-node-title' }, [ up && up.gateway ? up.gateway : _('No default route') ]), - E('span', { 'class': 'vt-node-sub' }, [ up && up.dns.length ? _('DNS %s').format(up.dns[0]) : _('Reachability not probed') ]) - ]) - ]); - this.nodes.gw = gw; - - var link = E('div', { 'class': 'vt-link-label' }, [ - E('span', { 'class': 'vt-link-rate vt-link-down' }, [ icon('down'), E('b', {}, [ up_r ? fmt.bitsParts(up_r.rx).v : fmt.DASH ]), E('small', {}, [ up_r ? fmt.bitsParts(up_r.rx).u : '' ]) ]), - E('span', { 'class': 'vt-link-below' }, [ - E('span', { 'class': 'vt-link-rate vt-link-up' }, [ icon('up'), E('b', {}, [ up_r ? fmt.bitsParts(up_r.tx).v : fmt.DASH ]), E('small', {}, [ up_r ? fmt.bitsParts(up_r.tx).u : '' ]) ]), - E('span', { 'class': 'vt-link-meta' }, [ up ? [ up.dev, up.speed ? (up.speed.mbps >= 1000 ? (up.speed.mbps / 1000) + 'G' : up.speed.mbps + 'M') + (up.speed.duplex === 'full' ? ' FD' : up.speed.duplex === 'half' ? ' HD' : '') : null ].filter(Boolean).join(' · ') : _('no uplink') ]) - ]) - ]); - - var d = m.device; - var ap = E('button', { 'type': 'button', 'class': 'vt-node vt-node-ap', 'click': function() { self.openDrawer('device', 'device'); } }, [ - E('span', { 'class': 'vt-node-ico' }, [ icon('ap') ]), - E('span', { 'class': 'vt-node-body' }, [ - E('span', { 'class': 'vt-node-kicker' }, [ m.wired ? _('This device') : _('This access point') ]), - E('span', { 'class': 'vt-node-title' }, [ d.model ]), - E('span', { 'class': 'vt-node-sub vt-mono' }, [ up && up.ipv4[0] ? up.ipv4[0].split('/')[0] : fmt.DASH ]), - E('span', { 'class': 'vt-node-sub' }, [ d.uptime != null ? _('up %s').format(fmt.duration(d.uptime)) : '' ]) - ]) - ]); - this.nodes.ap = ap; - - var branches = E('div', { 'class': 'vt-branches' }); - this.nodes.radios = []; - m.radios.forEach(function(r) { - var rr = st.radioRates[r.id]; - var rnode = E('button', { 'type': 'button', 'class': 'vt-node vt-node-radio' + (r.up ? '' : ' vt-node-off'), 'click': function() { self.openDrawer('radio', r.id); } }, [ - E('span', { 'class': 'vt-node-body' }, [ - E('span', { 'class': 'vt-node-kicker' }, [ bandBadge(r.band), E('span', { 'class': 'vt-node-gen' }, [ r.gen || '' ]) ]), - E('span', { 'class': 'vt-node-title vt-tn' }, [ r.channel ? _('Ch %d · %d MHz').format(r.channel, r.width || 20) : _('Channel —') ]), - E('span', { 'class': 'vt-node-sub vt-tn' }, [ r.disabled ? _('Disabled') : !r.up ? _('Down') : rr ? '↓ ' + fmt.bits(rr.tx) : fmt.DASH ]) - ]) - ]); - var ssidCol = E('div', { 'class': 'vt-branch-ssids' }); - var ssNodes = []; - m.ssids.filter(function(s) { return s.radio === r.id; }).forEach(function(s) { - var dots = E('span', { 'class': 'vt-dots' }); - m.clients.filter(function(c) { return c.ssidId === s.id; }).slice(0, 8).forEach(function(c) { - var dot = E('span', { 'class': 'vt-dot vt-lvl-' + c.level.level, 'title': c.name + ' · ' + fmt.dbm(c.signal) }); - dots.appendChild(dot); - }); - var sn = E('button', { 'type': 'button', 'class': 'vt-node vt-node-ssid' + (s.clients ? '' : ' vt-node-idle'), 'title': _('Show the clients of %s').format(s.ssid), - 'click': function() { self.filterSsid(s.ssid, s.band); } }, [ - E('span', { 'class': 'vt-node-body' }, [ - E('span', { 'class': 'vt-node-title vt-ssid-name' }, [ icon(s.security.tone === 'open' ? 'unlock' : 'lock', 'vt-ico-sm'), E('span', {}, [ s.ssid || _('(hidden)') ]) ]), - E('span', { 'class': 'vt-node-sub' }, [ dots, E('span', { 'class': 'vt-tn' }, [ s.clients === 1 ? _('1 client') : _('%d clients').format(s.clients) ]) ]) - ]) - ]); - ssNodes.push({ el: sn, active: !!(st.ssidRates[s.id] && st.ssidRates[s.id].rx + st.ssidRates[s.id].tx > 50e3) }); - ssidCol.appendChild(sn); - }); - if (!ssNodes.length) ssidCol.appendChild(E('span', { 'class': 'vt-empty-inline' }, [ _('No SSIDs') ])); - self.nodes.radios.push({ el: rnode, ssids: ssNodes, active: !!(rr && rr.rx + rr.tx > 50e3), rate: rr ? rr.rx + rr.tx : 0 }); - branches.appendChild(E('div', { 'class': 'vt-branch' }, [ rnode, ssidCol ])); - }); - if (m.wired) this.netBranches(branches); - else if (!m.radios.length) branches.appendChild(E('div', { 'class': 'vt-empty-inline' }, [ _('No radios reported') ])); - - this.nodes.upActive = !!(up_r && up_r.rx + up_r.tx > 50e3); - this.nodes.upRate = up_r ? up_r.rx + up_r.tx : 0; - path.insertBefore(gw, this.el.wires); - path.insertBefore(link, this.el.wires); - path.insertBefore(ap, this.el.wires); - path.insertBefore(branches, this.el.wires); - this.paintKpis(); + /* the nodes the path shows: rebuilt only when this changes (a radio, + SSID or network comes or goes); a poll updates text in place */ + heroKey: function() { + var m = this.m; + 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'); }, - /* wired-only: the device's other networks take the radios' place, - wired into the path the same way (nodes.radios) */ - netBranches: function(branches) { - var st = this.st, self = this; - var nets = this.m.networks.filter(function(n) { return n.up; }).slice(0, 8); - nets.forEach(function(n) { - var r = n.dev ? st.devRates[n.dev] : null; - var node = E('a', { 'class': 'vt-node vt-node-radio vt-node-net', 'href': L.url.apply(L, insight.PAGES.network), - 'title': [ n.name, n.dev, n.ipv4 ].filter(Boolean).join(' · ') }, [ - E('span', { 'class': 'vt-node-body' }, [ - E('span', { 'class': 'vt-node-kicker' }, [ n.proto || _('Interface') ]), - E('span', { 'class': 'vt-node-title' }, [ n.name ]), - E('span', { 'class': 'vt-node-sub vt-mono' }, [ n.ipv4 ? n.ipv4.split('/')[0] : (n.dev || fmt.DASH) ]), - E('span', { 'class': 'vt-node-sub vt-tn' }, [ r ? '↓ ' + fmt.bitsShort(r.tx) + ' ↑ ' + fmt.bitsShort(r.rx) : fmt.DASH ]) - ]) - ]); - self.nodes.radios.push({ el: node, ssids: [], active: !!(r && r.rx + r.tx > 50e3), rate: r ? r.rx + r.tx : 0 }); - branches.appendChild(E('div', { 'class': 'vt-branch' }, [ node ])); + paintHero: function() { + var key = this.heroKey(); + if (!this.hero || this.hero.key !== key) this.buildHeroNodes(key); + this.updateHero(); + this.paintKpis(); + this.paintWireState(); + }, + + buildHeroNodes: function(key) { + var self = this, m = this.m, path = this.el.path; + function span(cls) { return E('span', { 'class': cls }); } + var h = { key: key, radios: [] }; + + /* everything but the wires and the dots goes */ + Array.prototype.slice.call(path.childNodes).forEach(function(c) { + if (c !== self.el.wires && c !== self.el.flows) path.removeChild(c); + }); + + h.gwKicker = span('vt-node-kicker'); h.gwTitle = span('vt-node-title'); h.gwSub = span('vt-node-sub'); + h.gw = E('button', { 'type': 'button', 'class': 'vt-node vt-node-gw', 'click': function() { self.openDrawer('uplink', 'uplink'); } }, [ + E('span', { 'class': 'vt-node-ico' }, [ icon('globe') ]), + E('span', { 'class': 'vt-node-body' }, [ h.gwKicker, h.gwTitle, h.gwSub ]) + ]); + + h.dnVal = E('b'); h.dnUnit = E('small'); h.upVal = E('b'); h.upUnit = E('small'); h.linkMeta = span('vt-link-meta'); + h.link = E('div', { 'class': 'vt-link-label' }, [ + E('span', { 'class': 'vt-link-rate vt-link-down' }, [ icon('down'), h.dnVal, h.dnUnit ]), + E('span', { 'class': 'vt-link-below' }, [ + E('span', { 'class': 'vt-link-rate vt-link-up' }, [ icon('up'), h.upVal, h.upUnit ]), + h.linkMeta + ]) + ]); + + h.apKicker = span('vt-node-kicker'); h.apTitle = span('vt-node-title'); h.apAddr = span('vt-node-sub vt-mono'); h.apUp = span('vt-node-sub'); + h.ap = E('button', { 'type': 'button', 'class': 'vt-node vt-node-ap', 'click': function() { self.openDrawer('device', 'device'); } }, [ + E('span', { 'class': 'vt-node-ico' }, [ icon('ap') ]), + E('span', { 'class': 'vt-node-body' }, [ h.apKicker, h.apTitle, h.apAddr, h.apUp ]) + ]); + + h.branches = E('div', { 'class': 'vt-branches' }); + h.watch = [ path, h.gw, h.link, h.ap, h.branches ]; + if (m.wired) { + m.networks.filter(function(n) { return n.up; }).slice(0, 8).forEach(function(n) { + var b = { id: n.name, ssids: [], kicker: span('vt-node-kicker'), title: span('vt-node-title'), addr: span('vt-node-sub vt-mono'), sub: span('vt-node-sub vt-tn') }; + b.el = E('a', { 'class': 'vt-node vt-node-radio vt-node-net', 'href': L.url.apply(L, insight.PAGES.network) }, [ + E('span', { 'class': 'vt-node-body' }, [ b.kicker, b.title, b.addr, b.sub ]) + ]); + b.wrap = E('div', { 'class': 'vt-branch' }, [ b.el ]); + h.radios.push(b); + }); + if (!h.radios.length) h.branches.appendChild(E('div', { 'class': 'vt-empty-inline' }, [ _('No other interfaces are up') ])); + } + else { + m.radios.forEach(function(r) { + var b = { id: r.id, ssids: [], badge: span('vt-band'), gen: span('vt-node-gen'), title: span('vt-node-title vt-tn'), sub: span('vt-node-sub vt-tn') }; + b.el = E('button', { 'type': 'button', 'class': 'vt-node vt-node-radio', 'click': function() { self.openDrawer('radio', b.id); } }, [ + E('span', { 'class': 'vt-node-body' }, [ E('span', { 'class': 'vt-node-kicker' }, [ b.badge, b.gen ]), b.title, b.sub ]) + ]); + var col = E('div', { 'class': 'vt-branch-ssids' }); + m.ssids.filter(function(s) { return s.radio === r.id; }).forEach(function(s) { + var sn = { id: s.id, data: s, tone: null, ico: null, name: E('span'), dots: span('vt-dots'), count: span('vt-tn'), sig: null }; + sn.title = E('span', { 'class': 'vt-node-title vt-ssid-name' }, [ sn.name ]); + sn.el = E('button', { 'type': 'button', 'class': 'vt-node vt-node-ssid', + 'click': function() { self.filterSsid(sn.data.ssid, sn.data.band); } }, [ + E('span', { 'class': 'vt-node-body' }, [ sn.title, E('span', { 'class': 'vt-node-sub' }, [ sn.dots, sn.count ]) ]) + ]); + b.ssids.push(sn); + col.appendChild(sn.el); + }); + if (!b.ssids.length) col.appendChild(E('span', { 'class': 'vt-empty-inline' }, [ _('No SSIDs') ])); + b.wrap = E('div', { 'class': 'vt-branch' }, [ b.el, col ]); + h.radios.push(b); + }); + if (!h.radios.length) h.branches.appendChild(E('div', { 'class': 'vt-empty-inline' }, [ _('No radios reported') ])); + } + h.radios.forEach(function(b) { + h.branches.appendChild(b.wrap); + h.watch.push(b.wrap, b.el); + b.ssids.forEach(function(sn) { h.watch.push(sn.el); }); + }); + + path.insertBefore(h.gw, this.el.wires); + path.insertBefore(h.link, this.el.wires); + path.insertBefore(h.ap, this.el.wires); + path.insertBefore(h.branches, this.el.wires); + this.hero = h; + if (this.wireRo) { + var ro = this.wireRo; + ro.disconnect(); + h.watch.forEach(function(el) { ro.observe(el); }); + } + }, + + /* 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; + txt(h.gwKicker, up && up.isWan ? _('Internet') : _('Gateway')); + txt(h.gwTitle, up && up.gateway ? up.gateway : _('No default route')); + txt(h.gwSub, 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 : ''); + txt(h.upVal, upR ? fmt.bitsParts(upR.tx).v : fmt.DASH); + txt(h.upUnit, upR ? fmt.bitsParts(upR.tx).u : ''); + txt(h.linkMeta, up ? [ up.dev, up.speed ? (up.speed.mbps >= 1000 ? (up.speed.mbps / 1000) + 'G' : up.speed.mbps + 'M') + (up.speed.duplex === 'full' ? ' FD' : up.speed.duplex === 'half' ? ' HD' : '') : null ].filter(Boolean).join(' · ') : _('no uplink')); + + txt(h.apKicker, m.wired ? _('This device') : _('This access point')); + txt(h.apTitle, d.model); + txt(h.apAddr, up && up.ipv4[0] ? up.ipv4[0].split('/')[0] : fmt.DASH); + 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 (m.wired) { + var nets = Object.create(null); + m.networks.forEach(function(n) { if (n.up && !nets[n.name]) nets[n.name] = n; }); + h.radios.forEach(function(b) { + var n = nets[b.id]; + if (!n) return; + var r = n.dev ? st.devRates[n.dev] : null; + setTitle(b.el, [ n.name, n.dev, n.ipv4 ].filter(Boolean).join(' · ')); + txt(b.kicker, n.proto || _('Interface')); + txt(b.title, n.name); + txt(b.addr, n.ipv4 ? n.ipv4.split('/')[0] : (n.dev || fmt.DASH)); + txt(b.sub, r ? '↓ ' + fmt.bitsShort(r.tx) + ' ↑ ' + fmt.bitsShort(r.rx) : fmt.DASH); + b.active = !!(r && r.rx + r.tx > 50e3); + b.rate = r ? r.rx + r.tx : 0; + }); + return; + } + + var radios = Object.create(null), ssids = Object.create(null); + m.radios.forEach(function(r) { radios[r.id] = r; }); + m.ssids.forEach(function(s) { ssids[s.id] = s; }); + h.radios.forEach(function(b) { + 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')); + setClass(b.badge, 'vt-band vt-band-' + (r.band || 'x')); + txt(b.badge, wifi.bandLabel(r.band)); + txt(b.gen, r.gen || ''); + txt(b.title, r.channel ? _('Ch %d · %d MHz').format(r.channel, r.width || 20) : _('Channel —')); + txt(b.sub, r.disabled ? _('Disabled') : !r.up ? _('Down') : rr ? '↓ ' + fmt.bits(rr.tx) : fmt.DASH); + b.active = !!(rr && rr.rx + rr.tx > 50e3); + b.rate = rr ? rr.rx + rr.tx : 0; + b.ssids.forEach(function(sn) { + var s = ssids[sn.id]; + if (!s) return; + sn.data = s; + var tone = s.security.tone === 'open' ? 'unlock' : 'lock'; + if (sn.tone !== tone) { + var ico = icon(tone, 'vt-ico-sm'); + if (sn.ico) sn.title.replaceChild(ico, sn.ico); else sn.title.insertBefore(ico, sn.name); + sn.ico = ico; + sn.tone = tone; + } + setClass(sn.el, 'vt-node vt-node-ssid' + (s.clients ? '' : ' vt-node-idle')); + setTitle(sn.el, _('Show the clients of %s').format(s.ssid)); + txt(sn.name, s.ssid || _('(hidden)')); + txt(sn.count, s.clients === 1 ? _('1 client') : _('%d clients').format(s.clients)); + var dots = m.clients.filter(function(c) { return c.ssidId === s.id; }).slice(0, 8); + var sig = dots.map(function(c) { return c.level.level + ' ' + c.name + ' · ' + fmt.dbm(c.signal); }).join('\n'); + if (sn.sig !== sig) { + sn.sig = sig; + dom.content(sn.dots, dots.map(function(c) { return E('span', { 'class': 'vt-dot vt-lvl-' + c.level.level, 'title': c.name + ' · ' + fmt.dbm(c.signal) }); })); + } + var sr = st.ssidRates[s.id]; + sn.active = !!(sr && sr.rx + sr.tx > 50e3); + }); }); - if (!nets.length) branches.appendChild(E('div', { 'class': 'vt-empty-inline' }, [ _('No other interfaces are up') ])); }, /* four numbers under the path: who is here, what flows, where, and - who is worst off */ + who is worst off. The cells stay; only a change of shape (tone, or + whether a cell links somewhere) rebuilds them. */ paintKpis: function() { + var self = this, specs = this.m.wired ? this.netKpiSpecs() : this.kpiSpecs(); + var shape = specs.map(function(k) { return k.tone + (k.act ? '+' : '-'); }).join(' '); + this.kpiActs = specs.map(function(k) { return k.act; }); + if (this.kpiShape !== shape) { + this.kpiShape = shape; + this.kpiCells = specs.map(function(k, i) { + var c = { label: E('span', { 'class': 'vt-kpi-label' }), value: E('span', { 'class': 'vt-kpi-value vt-tn' }), sub: E('span', { 'class': 'vt-kpi-sub' }) }; + var body = [ c.label, c.value, c.sub ]; + c.el = k.act + ? E('button', { 'type': 'button', 'class': 'vt-kpi vt-kpi-' + k.tone, 'click': function() { var f = self.kpiActs[i]; if (f) f(); } }, body) + : E('div', { 'class': 'vt-kpi vt-kpi-' + k.tone }, body); + return c; + }); + dom.content(this.el.kpis, this.kpiCells.map(function(c) { return c.el; })); + } + this.kpiCells.forEach(function(c, i) { + txt(c.label, specs[i].label); + txt(c.value, specs[i].value); + txt(c.sub, specs[i].sub); + }); + }, + + kpiSpecs: function() { var self = this, m = this.m, st = this.st, up = st.uplinkRate; - if (m.wired) return this.paintNetKpis(); 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(' · '); 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]; - - dom.content(this.el.kpis, [ - kpi(_('Clients'), String(m.clients.length), bandText || _('none connected'), 'plain', function() { self.el.clients.scrollIntoView({ behavior: reducedMotion() ? 'auto' : 'smooth', block: 'start' }); }), - kpi(_('Traffic now'), up ? '↓ ' + fmt.bits(up.rx) : fmt.DASH, up ? _('↑ %s · via %s').format(fmt.bits(up.tx), m.uplink ? m.uplink.dev : '') : _('measuring'), 'plain', function() { self.openDrawer('uplink', 'uplink'); }), - busiest ? kpi(_('Busiest radio'), busiest.r.bandLabel, _('%s of Wi-Fi traffic').format(fmt.pct(busiest.share * 100)), 'plain', function() { self.openDrawer('radio', busiest.r.id); }) - : kpi(_('Busiest radio'), fmt.DASH, _('no Wi-Fi traffic yet'), 'plain'), - weakest ? kpi(_('Weakest signal'), fmt.dbm(weakest.signal), weakest.name, weakest.signal < insight.WEAK_DBM ? 'warn' : 'plain', function() { self.openDrawer('client', weakest.mac); }) - : kpi(_('Weakest signal'), fmt.DASH, _('no clients'), 'plain') - ]); + return [ + { label: _('Clients'), value: String(m.clients.length), 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'); } }, + busiest ? { label: _('Busiest radio'), value: busiest.r.bandLabel, sub: _('%s of Wi-Fi traffic').format(fmt.pct(busiest.share * 100)), tone: 'plain', + act: function() { self.openDrawer('radio', busiest.r.id); } } + : { label: _('Busiest radio'), value: fmt.DASH, sub: _('no Wi-Fi traffic yet'), tone: 'plain', act: null }, + weakest ? { label: _('Weakest signal'), value: fmt.dbm(weakest.signal), sub: weakest.name, tone: weakest.signal < insight.WEAK_DBM ? 'warn' : 'plain', + act: function() { self.openDrawer('client', weakest.mac); } } + : { label: _('Weakest signal'), value: fmt.DASH, sub: _('no clients'), tone: 'plain', act: null } + ]; }, /* wired-only: what flows, how long the uplink has held, which networks are up and which is busiest */ - paintNetKpis: function() { + netKpiSpecs: function() { var self = this, m = this.m, st = this.st, up = st.uplinkRate, link = m.uplink; var nets = m.networks, upNets = nets.filter(function(n) { return n.up; }); var busiest = null; @@ -1316,13 +1488,14 @@ return view.extend({ if (r && r.rx + r.tx > 0 && (!busiest || r.rx + r.tx > busiest.r.rx + busiest.r.tx)) busiest = { n: n, r: r }; }); function toNetwork() { window.location.href = L.url.apply(L, insight.PAGES.network); } - dom.content(this.el.kpis, [ - kpi(_('Traffic now'), up ? '↓ ' + fmt.bits(up.rx) : fmt.DASH, up ? _('↑ %s · via %s').format(fmt.bits(up.tx), link ? link.dev : '') : _('measuring'), 'plain', function() { self.openDrawer('uplink', 'uplink'); }), - kpi(_('Connected for'), link && link.uptime != null ? fmt.duration(link.uptime) : fmt.DASH, link ? [ link.iface, link.proto ].filter(Boolean).join(' · ') : _('no uplink'), 'plain', function() { self.openDrawer('uplink', 'uplink'); }), - kpi(_('Networks'), String(upNets.length), nets.length > upNets.length ? _('%d down').format(nets.length - upNets.length) : _('all up'), 'plain', toNetwork), - busiest ? kpi(_('Busiest network'), busiest.n.name, '↓ ' + fmt.bits(busiest.r.tx) + ' ↑ ' + fmt.bits(busiest.r.rx), 'plain', toNetwork) - : kpi(_('Busiest network'), fmt.DASH, _('no traffic yet'), 'plain') - ]); + function toUplink() { self.openDrawer('uplink', 'uplink'); } + return [ + { label: _('Traffic now'), value: up ? '↓ ' + fmt.bits(up.rx) : fmt.DASH, sub: up ? _('↑ %s · via %s').format(fmt.bits(up.tx), link ? link.dev : '') : _('measuring'), tone: 'plain', act: toUplink }, + { label: _('Connected for'), value: link && link.uptime != null ? fmt.duration(link.uptime) : fmt.DASH, sub: link ? [ link.iface, link.proto ].filter(Boolean).join(' · ') : _('no uplink'), tone: 'plain', act: toUplink }, + { label: _('Networks'), value: String(upNets.length), sub: nets.length > upNets.length ? _('%d down').format(nets.length - upNets.length) : _('all up'), tone: 'plain', act: toNetwork }, + busiest ? { label: _('Busiest network'), value: busiest.n.name, sub: '↓ ' + fmt.bits(busiest.r.tx) + ' ↑ ' + fmt.bits(busiest.r.rx), tone: 'plain', act: toNetwork } + : { label: _('Busiest network'), value: fmt.DASH, sub: _('no traffic yet'), tone: 'plain', act: toNetwork } + ]; }, scheduleWires: function() { @@ -1332,63 +1505,141 @@ return view.extend({ window.requestAnimationFrame(function() { self.wirePending = false; self.drawWires(); }); }, - /* connectors between the hero nodes, measured after layout; wires are - kept by key so the flow animation does not restart every poll */ - drawWires: function() { - var path = this.el && this.el.path, svg = this.el && this.el.wires, n = this.nodes; - if (!path || !svg || !n || !path.isConnected) return; - var box = path.getBoundingClientRect(); - if (!box.width) return; - svg.setAttribute('viewBox', '0 0 ' + Math.round(box.width) + ' ' + Math.round(box.height)); - svg.setAttribute('width', String(Math.round(box.width))); - svg.setAttribute('height', String(Math.round(box.height))); - - function rect(el) { - var r = el.getBoundingClientRect(); - return { l: r.left - box.left, r: r.right - box.left, t: r.top - box.top, b: r.bottom - box.top, cy: (r.top + r.bottom) / 2 - box.top }; - } - function curve(a, b) { - var x1, y1, x2, y2; - if (b.l >= a.r - 2) { - x1 = a.r; y1 = a.cy; x2 = b.l; y2 = b.cy; - var mx = (x1 + x2) / 2; - return 'M' + x1.toFixed(1) + ' ' + y1.toFixed(1) + 'C' + mx.toFixed(1) + ' ' + y1.toFixed(1) + ' ' + mx.toFixed(1) + ' ' + y2.toFixed(1) + ' ' + x2.toFixed(1) + ' ' + y2.toFixed(1); - } - /* stacked layout: an elbow down the left edge */ - x1 = a.l + 18; y1 = a.b; x2 = b.l; y2 = b.cy; - if (x2 <= x1 + 6) return 'M' + x1.toFixed(1) + ' ' + y1.toFixed(1) + 'V' + b.t.toFixed(1); - var rad = Math.min(8, Math.max(0, y2 - y1)); - return 'M' + x1.toFixed(1) + ' ' + y1.toFixed(1) + 'V' + (y2 - rad).toFixed(1) + 'Q' + x1.toFixed(1) + ' ' + y2.toFixed(1) + ' ' + (x1 + rad).toFixed(1) + ' ' + y2.toFixed(1) + 'H' + x2.toFixed(1); - } - function speed(bps) { return bps > 100e6 ? 3 : bps > 5e6 ? 2 : 1; } - - var want = []; - want.push({ key: 'gw', d: curve(rect(n.gw), rect(n.ap)), active: n.upActive, speed: speed(n.upRate), main: true }); - n.radios.forEach(function(r, i) { - var rr = rect(r.el); - want.push({ key: 'r' + i, d: curve(rect(n.ap), rr), active: r.active, speed: speed(r.rate) }); - r.ssids.forEach(function(s, j) { want.push({ key: 'r' + i + 's' + j, d: curve(rr, rect(s.el)), active: s.active, speed: 1 }); }); + /* the wires the nodes call for: key, ends, and how busy */ + wireList: function() { + var h = this.hero, out = []; + if (!h) return out; + out.push({ key: 'gw', from: h.gw, to: h.ap, active: h.upActive, speed: wires.speed(h.upRate), main: true }); + h.radios.forEach(function(b) { + out.push({ key: 'r:' + b.id, from: h.ap, to: b.el, active: b.active, speed: wires.speed(b.rate) }); + b.ssids.forEach(function(sn) { out.push({ key: 's:' + b.id + ':' + sn.id, from: b.el, to: sn.el, active: sn.active, speed: 1 }); }); }); + return out; + }, - var seen = {}, map = this.wireMap; - want.forEach(function(w) { + /* connectors between the hero nodes, measured after layout (called + from the ResizeObserver); wires are kept by key so the flow does not + restart when nothing moved */ + drawWires: function() { + var self = this, path = this.el && this.el.path, svg = this.el && this.el.wires; + if (!path || !svg || !this.hero || !path.isConnected) return; + var box = path.getBoundingClientRect(); + /* folded or hidden: no dots running where nobody sees them, and + none rebuilt by a poll until the path has a size again */ + if (!box.width) { this.stopFlows(); return; } + this.flowsHidden = false; + var vb = '0 0 ' + Math.round(box.width) + ' ' + Math.round(box.height); + if (svg.getAttribute('viewBox') !== vb) { + svg.setAttribute('viewBox', vb); + svg.setAttribute('width', String(Math.round(box.width))); + svg.setAttribute('height', String(Math.round(box.height))); + } + var rects = new Map(); + function rect(el) { + var r = rects.get(el); + if (!r) { + var b = el.getBoundingClientRect(); + r = { l: b.left - box.left, r: b.right - box.left, t: b.top - box.top, b: b.bottom - box.top, cy: (b.top + b.bottom) / 2 - box.top }; + rects.set(el, r); + } + return r; + } + + var list = this.wireList(), seen = Object.create(null), map = this.wireMap; + list.forEach(function(w) { rect(w.from); rect(w.to); }); + list.forEach(function(w) { seen[w.key] = true; var g = map[w.key]; if (!g) { - g = map[w.key] = { base: svgPath('', 'vt-wire'), flow: svgPath('', 'vt-flow') }; + g = map[w.key] = { base: svgPath('', 'vt-wire'), flow: svgPath('', 'vt-flow'), fw: E('div', { 'class': 'vt-fwire' }), dots: [], anims: [], d: null }; svg.appendChild(g.base); svg.appendChild(g.flow); + self.el.flows.appendChild(g.fw); + } + var c = wires.curve(rect(w.from), rect(w.to)); + if (g.d !== c.d) { + g.base.setAttribute('d', c.d); + g.flow.setAttribute('d', c.d); + g.d = c.d; + var rs = wires.resample(c.pts, 8); + g.samples = rs.pts; + g.len = rs.len; } - if (g.d !== w.d) { g.base.setAttribute('d', w.d); g.flow.setAttribute('d', w.d); g.d = w.d; } - g.base.setAttribute('class', 'vt-wire' + (w.main ? ' vt-wire-main' : '') + (w.active ? ' vt-wire-on' : '')); - g.flow.setAttribute('class', 'vt-flow vt-flow-' + w.speed + (w.active ? ' vt-flow-on' : '')); }); Object.keys(map).forEach(function(k) { if (seen[k]) return; - svg.removeChild(map[k].base); - svg.removeChild(map[k].flow); + var g = map[k]; + self.clearDots(g); + svg.removeChild(g.base); + svg.removeChild(g.flow); + self.el.flows.removeChild(g.fw); delete map[k]; }); + this.paintWireState(); + }, + + /* which wires carry traffic and how fast: classes only, no geometry */ + paintWireState: function() { + var self = this, map = this.wireMap, still = reducedMotion() || !!this.flowsHidden; + if (!map) return; + this.wireList().forEach(function(w) { + var g = map[w.key]; + if (!g) return; + setClass(g.base, 'vt-wire' + (w.main ? ' vt-wire-main' : '') + (w.active ? ' vt-wire-on' : '')); + setClass(g.flow, 'vt-flow vt-flow-' + w.speed + (w.active ? ' vt-flow-on' : '')); + self.syncFlow(g, !!(w.active && !still && g.d), w.speed); + }); + }, + + /* The flow: about one dot every 16 px moving along the wire, each + animated with transform only (the compositor runs it; the old + stroke-dashoffset animation repainted the wire layer every frame). + Dots exist only while a wire is busy; reduced motion shows the static + dotted path (app.css) instead. */ + syncFlow: function(g, on, speed) { + if (on) { + if (g.offTimer) { window.clearTimeout(g.offTimer); g.offTimer = null; } + if (g.dotsFor !== g.d) this.buildDots(g, speed); + else if (g.speed !== speed) g.anims.forEach(function(a) { a.playbackRate = wires.rate(speed); }); + g.speed = speed; + } + else if (g.dots.length && !g.offTimer) { + /* fade out (app.css), then stop */ + var self = this; + g.offTimer = window.setTimeout(function() { g.offTimer = null; self.clearDots(g); }, 450); + } + setClass(g.fw, 'vt-fwire' + (g.speed === 3 ? ' vt-fwire-3' : '') + (on ? ' vt-fwire-on' : '')); + }, + + buildDots: function(g, speed) { + this.clearDots(g); + g.dotsFor = g.d; + if (!g.samples || g.samples.length < 2 || !(g.len >= 8) || typeof g.fw.animate !== 'function') return; + var n = wires.dots(g.len), dur = wires.trip(g.len), rate = wires.rate(speed); + for (var i = 0; i < n; i++) { + var dot = E('span', { 'class': 'vt-fdot' }); + g.fw.appendChild(dot); + /* every dot takes the whole trip, started i/n of the way along */ + var a = dot.animate(wires.keyframes(g.samples), { duration: dur, delay: -dur * i / n, iterations: Infinity, easing: 'linear' }); + a.playbackRate = rate; + g.dots.push(dot); + g.anims.push(a); + } + }, + + clearDots: function(g) { + if (g.offTimer) { window.clearTimeout(g.offTimer); g.offTimer = null; } + g.anims.forEach(function(a) { a.cancel(); }); + g.dots.forEach(function(d) { if (d.parentNode) d.parentNode.removeChild(d); }); + g.anims = []; + g.dots = []; + g.dotsFor = null; + }, + + stopFlows: function() { + var self = this, map = this.wireMap || {}; + this.flowsHidden = true; + Object.keys(map).forEach(function(k) { self.clearDots(map[k]); setClass(map[k].fw, 'vt-fwire'); }); }, /* ----------------------------------------------------------- system */ @@ -1434,8 +1685,8 @@ return view.extend({ txt(el.cpuVal, cpu ? Math.round(cpu.pct) + '%' : fmt.DASH); el.cpuVal.className = 'vt-big vt-tn' + (cpu && cpu.pct >= 90 ? ' vt-t-err' : cpu && cpu.pct >= 70 ? ' vt-t-warn' : ''); txt(el.cpuLoad, _('Load avg %s').format(d.load.map(fmt.load).join(' · '))); - dom.content(el.cpuSpark, [ sparkline(this.st.hist.cpu, [ 1 ], 100, 'vt-spark-cpu', { scale: function() { return '100%'; } }) ]); - dom.content(el.cpuCores, (cpu ? cpu.cores : []).map(function(p, i) { + patch(el.cpuSpark, [ sparkline(this.st.hist.cpu, [ 1 ], 100, 'vt-spark-cpu', { scale: function() { return '100%'; } }) ]); + patch(el.cpuCores, (cpu ? cpu.cores : []).map(function(p, i) { var fill = E('span', { 'class': 'vt-core-fill' }); fill.style.height = Math.max(4, Math.min(100, p || 0)).toFixed(1) + '%'; return E('span', { 'class': 'vt-core', 'title': _('Core %d: %s').format(i, fmt.pct(p)) }, [ fill ]); @@ -1444,31 +1695,36 @@ return view.extend({ /* memory: one bar, three named segments */ var mem = d.memory; if (mem) { - dom.content(el.memHead, [ + patch(el.memHead, [ E('span', { 'class': 'vt-mem-avail vt-tn' }, [ _('%s available').format(fmt.pct(mem.availPct)) ]), E('span', { 'class': 'vt-mem-of vt-tn' }, [ _('%s of %s').format(fmt.bytes(mem.available), fmt.bytes(mem.total)) ]) ]); if (!el.memSegs) { el.memSegs = mem.segments.map(function(s) { var x = E('span', { 'class': 'vt-seg-part vt-seg-' + s.key }); el.memBar.appendChild(x); return x; }); } - mem.segments.forEach(function(s, i) { setW(el.memSegs[i], s.pct); el.memSegs[i].setAttribute('title', s.label + ': ' + fmt.bytes(s.bytes)); }); + /* animate a visible change only (app.css): a width transition + lays out every frame for most of a second */ + var was = el.memPct || []; + el.memBar.classList.toggle('vt-seg-move', mem.segments.some(function(s, i) { return was[i] != null && Math.abs(s.pct - was[i]) >= 2; })); + el.memPct = mem.segments.map(function(s) { return s.pct; }); + mem.segments.forEach(function(s, i) { setW(el.memSegs[i], s.pct); setTitle(el.memSegs[i], s.label + ': ' + fmt.bytes(s.bytes)); }); el.memBar.setAttribute('aria-label', mem.segments.map(function(s) { return s.label + ' ' + fmt.bytes(s.bytes); }).join(', ')); - dom.content(el.memLegend, mem.segments.map(function(s) { + patch(el.memLegend, mem.segments.map(function(s) { return E('span', { 'class': 'vt-legend-item' }, [ E('span', { 'class': 'vt-key vt-seg-' + s.key }), E('span', {}, [ s.label ]), E('b', { 'class': 'vt-tn' }, [ fmt.bytes(s.bytes) ]) ]); })); } else { - dom.content(el.memHead, [ E('span', { 'class': 'vt-muted' }, [ _('Not reported') ]) ]); + patch(el.memHead, [ E('span', { 'class': 'vt-muted' }, [ _('Not reported') ]) ]); } if (d.swap) { var sb = E('span', { 'class': 'vt-thin-fill' }); setW(sb, d.swap.usedPct); - dom.content(el.swap, [ E('span', { 'class': 'vt-swap-label' }, [ _('Swap') ]), E('span', { 'class': 'vt-thin' }, [ sb ]), + patch(el.swap, [ E('span', { 'class': 'vt-swap-label' }, [ _('Swap') ]), E('span', { 'class': 'vt-thin' }, [ sb ]), E('span', { 'class': 'vt-tn vt-hint' }, [ _('%s of %s').format(fmt.bytes(d.swap.used), fmt.bytes(d.swap.total)) ]) ]); } - else dom.content(el.swap, []); + else patch(el.swap, []); - dom.content(el.storage, d.storage.map(function(s) { + patch(el.storage, d.storage.map(function(s) { var f = E('span', { 'class': 'vt-bar-fill' + (s.usedPct >= 95 ? ' vt-bg-err' : s.usedPct >= 85 ? ' vt-bg-warn' : '') }); setW(f, Math.max(s.usedPct, 0.8)); return E('div', { 'class': 'vt-mount' }, [ @@ -1476,7 +1732,7 @@ return view.extend({ E('div', { 'class': 'vt-bar' }, [ f ]) ]); })); - dom.content(el.fw, [ + patch(el.fw, [ E('span', {}, [ d.firmware || '' ]), d.kernel ? E('span', { 'class': 'vt-hint' }, [ _('Kernel %s').format(d.kernel) ]) : '' ]); @@ -1729,7 +1985,7 @@ return view.extend({ dom.content(this.el.radioList, [ E('div', { 'class': 'vt-tile vt-empty' }, [ icon('radio'), E('p', {}, [ _('No wireless radios are reported by this device.') ]) ]) ]); return; } - dom.content(this.el.radioList, m.radios.map(function(r) { + patch(this.el.radioList, m.radios.map(function(r) { var rr = st.radioRates[r.id], ins = st.radioInsights[r.id] || { notes: [] }; var sp = r.spectrum, spec = E('div', { 'class': 'vt-spectrum', 'title': sp ? _('%d–%d MHz of the %d–%d MHz band').format(sp.lo, sp.hi, sp.bandLo, sp.bandHi) : '' }); if (sp) { @@ -1742,7 +1998,7 @@ return view.extend({ } var hist = st.hist.net[r.id] || []; var clients = m.clients.filter(function(c) { return c.radio === r.id; }); - return E('article', { 'class': 'vt-tile vt-radio vt-radio-' + (r.band || 'x') + (r.up ? '' : ' vt-radio-off') + (ins.busiest ? ' vt-radio-busiest' : '') }, [ + return E('article', { 'class': 'vt-tile vt-radio vt-radio-' + (r.band || 'x') + (r.up ? '' : ' vt-radio-off') + (ins.busiest ? ' vt-radio-busiest' : ''), 'data-radio': r.id }, [ E('header', { 'class': 'vt-radio-head' }, [ bandBadge(r.band), E('span', { 'class': 'vt-radio-name' }, [ r.gen || r.id ]), @@ -1851,17 +2107,21 @@ return view.extend({ chipDefs.push([ 'weak', _('Weak signal'), m.clients.filter(function(c) { return c.signal != null && c.signal < insight.WEAK_DBM; }).length ]); chipDefs.push([ 'new', _('New'), m.clients.filter(function(c) { return c.isNew; }).length ]); if (!chipDefs.some(function(d) { return d[0] === ui.chip; })) ui.chip = 'all'; - dom.content(el.chips, chipDefs.map(function(d) { + /* 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; 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') ] ]; - dom.content(el.thead, [ E('tr', {}, cols.map(function(c) { + var headSig = ui.sort + ' ' + ui.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() { @@ -1870,6 +2130,7 @@ return view.extend({ } }, [ E('span', {}, [ c[1] ]), E('span', { 'class': 'vt-sort-ind', 'aria-hidden': 'true' }, [ on ? (ui.dir > 0 ? '↑' : '↓') : '' ]) ]) ]); })) ]); + this.headSig = headSig; var list = m.clients.filter(L.bind(this.matches, this)); list.sort(function(a, b) { @@ -1886,7 +2147,8 @@ return view.extend({ 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; }); - var keep = new Set(); + 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; keep.add(key); @@ -1901,13 +2163,18 @@ return view.extend({ tr.classList.toggle('vt-row-gone', !!c.gone); tr.classList.toggle('vt-row-sel', !!(ui.drawer && ui.drawer.kind === 'client' && ui.drawer.id === c.mac)); tr.setAttribute('aria-label', _('Open details of %s').format(c.name)); - dom.content(tr, self.clientCells(c, maxRate)); - el.tbody.appendChild(tr); + /* 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); + 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++; }); this.rowMap.forEach(function(tr, key) { if (keep.has(key)) return; if (tr.parentNode) tr.parentNode.removeChild(tr); self.rowMap.delete(key); + self.rowSig.delete(key); }); window.setTimeout(function() { self.rowMap.forEach(function(tr) { tr.classList.remove('vt-row-enter'); }); }, 600); @@ -2027,7 +2294,7 @@ return view.extend({ dom.content(this.el.ssids, [ E('p', { 'class': 'vt-empty-inline' }, [ _('No wireless networks configured.') ]) ]); return; } - dom.content(this.el.ssids, groups.map(function(g) { + patch(this.el.ssids, groups.map(function(g) { var rx = 0, tx = 0, has = false; g.ids.forEach(function(id) { var r = st.ssidRates[id]; if (r) { rx += r.rx; tx += r.tx; has = true; } }); var bands = g.bands.map(function(b) { return bandBadge(b); }); diff --git a/luci-app-vantage/po/templates/vantage.pot b/luci-app-vantage/po/templates/vantage.pot index 37fcc0873..3eef5c320 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:2347 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2614 msgid "%d MHz (%d–%d MHz)" msgstr "" @@ -21,23 +21,23 @@ msgstr "" msgid "%d SSIDs · %d idle" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1130 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1242 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1192 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1979 msgid "%d clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2182 msgid "%d connected" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:101 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1322 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 msgid "%d down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2391 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2392 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2658 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2659 msgid "%d errors" msgstr "" @@ -45,24 +45,24 @@ msgstr "" msgid "%d h ago" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2393 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2660 msgid "%d in · %d out" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:357 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:314 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:351 msgid "%d min ago" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2182 msgid "%d of %d" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1017 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1079 msgid "%d of %d columns" msgstr "" @@ -74,24 +74,24 @@ msgstr "" msgid "%d older devices (Wi-Fi 4 or earlier) slow airtime" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1293 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1462 msgid "%d on %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1715 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1971 msgid "%d radios" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:357 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:314 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:351 msgid "%d s ago" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2205 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2472 msgid "%d sent · %s retried · %s failed" msgstr "" @@ -107,7 +107,7 @@ msgstr "" msgid "%d weak" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1734 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1990 msgid "%d–%d MHz of the %d–%d MHz band" msgstr "" @@ -119,7 +119,7 @@ msgstr "" msgid "%s (%s) is enabled but not up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2200 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2467 msgid "%s (new this session)" msgstr "" @@ -130,10 +130,10 @@ msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:131 #: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:103 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1741 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1755 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1959 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2340 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1997 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2011 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2226 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2607 msgid "%s MHz" msgstr "" @@ -143,7 +143,7 @@ msgstr "" msgid "%s Mbit/s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2199 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2466 msgid "%s ago" msgstr "" @@ -152,12 +152,12 @@ msgid "%s at %s; below %s roaming and throughput suffer" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:78 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1448 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1699 msgid "%s available" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1760 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2348 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2016 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2615 msgid "%s dBm" msgstr "" @@ -169,7 +169,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:2167 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2434 msgid "%s experience" msgstr "" @@ -185,7 +185,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:984 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1046 msgid "%s hidden" msgstr "" @@ -193,7 +193,7 @@ msgstr "" msgid "%s is disabled" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:977 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1039 msgid "%s is narrower" msgstr "" @@ -201,7 +201,7 @@ msgstr "" msgid "%s is not up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:980 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1042 msgid "%s is wider" msgstr "" @@ -209,23 +209,23 @@ msgstr "" msgid "%s link" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1098 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1160 msgid "%s moved" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:974 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1036 msgid "%s moved down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:965 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1027 msgid "%s moved left" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:968 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1030 msgid "%s moved right" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:971 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1033 msgid "%s moved up" msgstr "" @@ -238,9 +238,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:79 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1449 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1467 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1700 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2686 msgid "%s of %s" msgstr "" @@ -248,7 +248,7 @@ msgstr "" msgid "%s of %s available (%s in use)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1301 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1471 msgid "%s of Wi-Fi traffic" msgstr "" @@ -265,15 +265,15 @@ msgstr "" msgid "%s of frames to the device fail; interference likely" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2417 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2684 msgid "%s over %d cores" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1530 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1786 msgid "%s radio" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1023 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1085 msgid "%s shown" msgstr "" @@ -289,7 +289,7 @@ msgstr "" msgid "%s used" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1475 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1731 msgid "%s used of %s" msgstr "" @@ -297,7 +297,7 @@ msgstr "" msgid "%s · %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2197 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2464 msgid "%s · channel %d · %d MHz" msgstr "" @@ -313,22 +313,22 @@ msgstr "" msgid "%s: no carrier" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2348 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2615 msgid "(configured %d dBm, limited by regulatory rules)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1241 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2038 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1418 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2305 msgid "(hidden)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1130 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1242 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1192 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1979 msgid "1 client" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2182 msgid "1 connected" msgstr "" @@ -336,7 +336,7 @@ msgstr "" msgid "1 older device (Wi-Fi 4 or earlier) slows airtime" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1715 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1971 msgid "1 radio" msgstr "" @@ -360,19 +360,19 @@ msgstr "" msgid "60 GHz" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1850 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2106 msgid "Active" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2209 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2476 msgid "Address type" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2384 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2651 msgid "Addresses" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1762 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2018 msgid "Airtime" msgstr "" @@ -380,7 +380,7 @@ msgstr "" msgid "Alias" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1846 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2102 msgid "All" msgstr "" @@ -396,9 +396,9 @@ msgstr "" msgid "All good" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:930 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:931 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1118 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:992 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:993 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1180 msgid "Arrange the dashboard" msgstr "" @@ -406,13 +406,13 @@ msgstr "" msgid "B" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1323 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1324 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1496 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1497 msgid "Busiest network" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1301 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1302 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1471 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1473 msgid "Busiest radio" msgstr "" @@ -420,7 +420,7 @@ msgstr "" msgid "Busiest radio: %s of Wi-Fi traffic" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1670 msgid "Busy share of each CPU core" msgstr "" @@ -434,8 +434,8 @@ msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:68 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:69 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1413 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2417 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1664 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2684 msgid "CPU" msgstr "" @@ -443,93 +443,93 @@ msgstr "" msgid "Cache (reclaimable)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2285 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2552 msgid "Cancel" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2192 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2459 msgid "Capabilities" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2379 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2646 msgid "Carrier" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2347 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2614 msgid "Centre frequency" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1754 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2010 msgid "Ch %d" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1226 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1722 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1401 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1978 msgid "Ch %d · %d MHz" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2340 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2607 msgid "Channel" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1226 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1401 msgid "Channel —" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1786 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2042 msgid "Clear filter" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1174 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1244 msgid "Click any node for details" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:786 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1299 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1759 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1794 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1795 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:838 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1467 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2015 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2050 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2051 msgid "Clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2352 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2619 msgid "Clients on this radio" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2055 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2322 msgid "Close" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1974 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2241 msgid "Connected" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1936 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2203 msgid "Connected after you opened this page" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1321 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2198 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1494 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2465 msgid "Connected for" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2195 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2462 msgid "Connection" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1441 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1692 msgid "Core %d: %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2268 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2535 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:2390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2657 msgid "Counters since boot" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2350 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2617 msgid "Country" msgstr "" @@ -537,16 +537,16 @@ msgstr "" msgid "DHCP" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2223 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2490 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:2388 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2655 msgid "DNS" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1194 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1358 msgid "DNS %s" msgstr "" @@ -555,84 +555,84 @@ 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:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1941 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2408 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2208 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2675 msgid "Device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2252 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2519 msgid "Device name" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2281 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2548 msgid "Device type" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1227 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1724 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1749 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2345 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1402 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1980 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2005 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2612 msgid "Disabled" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2161 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2428 msgid "Disconnected %s; shown from the last sample." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:943 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1005 msgid "Done" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:45 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1227 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1724 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1749 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2345 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1402 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1980 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2005 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2612 msgid "Down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1508 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1583 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1657 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1764 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1839 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1913 msgid "Download" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:274 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:319 msgid "Download (to clients)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2203 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2470 msgid "Downloaded" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1004 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1066 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:1005 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1067 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:1003 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1065 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:956 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1018 msgid "Drag to move" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2393 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2660 msgid "Dropped" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2018 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2285 msgid "Edit" msgstr "" @@ -649,8 +649,8 @@ msgstr "" msgid "Excellent" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1968 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2235 msgid "Experience" msgstr "" @@ -667,19 +667,19 @@ msgstr "" msgid "Fair signal %s; lower rates at range" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1783 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2039 msgid "Filter by name, address, SSID…" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1784 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2040 msgid "Filter clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2411 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2678 msgid "Firmware" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2205 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2472 msgid "Frames to device" msgstr "" @@ -687,20 +687,20 @@ msgstr "" msgid "Free" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2190 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2457 msgid "From device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1957 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2224 msgid "From device: %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2374 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2641 msgid "From network" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1192 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2387 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1356 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2654 msgid "Gateway" msgstr "" @@ -708,7 +708,7 @@ msgstr "" msgid "Gbit/s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2191 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2458 msgid "Generation" msgstr "" @@ -716,11 +716,11 @@ msgstr "" msgid "GiB" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2209 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2476 msgid "Globally unique" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2209 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2476 msgid "Globally unique · %s" msgstr "" @@ -741,32 +741,32 @@ msgstr "" msgid "Half duplex" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:698 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:735 msgid "Health checks" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1020 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2038 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1082 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2305 msgid "Hidden" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:983 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1045 msgid "Hide %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2210 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2477 msgid "IP addresses" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2385 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2652 msgid "IPv4" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2386 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2653 msgid "IPv6" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2207 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2474 msgid "Identity" msgstr "" @@ -774,27 +774,27 @@ msgstr "" msgid "In use" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1274 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1381 msgid "Interface" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2382 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2649 msgid "Interface up for" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2372 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2639 msgid "Interfaces" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1192 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1356 msgid "Internet" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2412 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2679 msgid "Kernel" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1481 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1737 msgid "Kernel %s" msgstr "" @@ -802,44 +802,44 @@ msgstr "" msgid "KiB" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2199 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2466 msgid "Last activity" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1119 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1181 msgid "Layout" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:941 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1003 msgid "Layout reset" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1935 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2202 msgid "Left %s" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:159 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1937 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2204 msgid "Legacy" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2188 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2378 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2455 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2645 msgid "Link" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1956 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2223 msgid "Link rate" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1138 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1961 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1200 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2228 msgid "Live" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2416 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2683 msgid "Load" msgstr "" @@ -851,19 +851,19 @@ msgstr "" msgid "Load %s · %d cores" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2418 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2685 msgid "Load average" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1402 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1653 msgid "Load average over 1, 5 and 15 minutes" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1436 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1687 msgid "Load avg %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2414 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2681 msgid "Local time" msgstr "" @@ -879,7 +879,7 @@ msgstr "" msgid "MAC" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2208 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2475 msgid "MAC address" msgstr "" @@ -887,15 +887,15 @@ msgstr "" msgid "MCS %d" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:976 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1038 msgid "Make %s narrower" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:979 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1041 msgid "Make %s wider" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2225 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2492 msgid "Manufacturer of the network adapter" msgstr "" @@ -912,11 +912,11 @@ msgid "Measuring…" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:78 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1423 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1674 msgid "Memory" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2686 msgid "Memory in use" msgstr "" @@ -924,54 +924,54 @@ msgstr "" msgid "MiB" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2346 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2613 msgid "Mode" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2409 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2676 msgid "Model" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:973 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1035 msgid "Move %s down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:964 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1026 msgid "Move %s left" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:967 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1029 msgid "Move %s right" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:970 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1032 msgid "Move %s up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:956 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1018 msgid "Move %s: drag, or use the arrow keys" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2280 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2547 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:2318 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2585 msgid "Name contains invisible or control characters" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2211 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2478 msgid "Name from" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1946 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2213 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:2319 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2586 msgid "Name is longer than %d characters" msgstr "" @@ -979,32 +979,32 @@ msgstr "" msgid "Name must be text" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2152 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2419 msgid "Name this device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1953 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2196 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2220 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2463 msgid "Network" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:786 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1173 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1174 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:838 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1243 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1244 msgid "Network path" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1322 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 msgid "Networks" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1852 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1936 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2108 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2203 msgid "New" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1248 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1329 msgid "No SSIDs" msgstr "" @@ -1012,23 +1012,23 @@ msgstr "" msgid "No clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1920 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2187 msgid "No clients connected" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1991 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2258 msgid "No clients connected." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1923 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2190 msgid "No clients in this view" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1923 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2190 msgid "No clients match “%s”" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2357 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2624 msgid "No clients; an idle radio is fine." msgstr "" @@ -1036,7 +1036,7 @@ msgstr "" msgid "No clients; idle radios are fine" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1193 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1357 msgid "No default route" msgstr "" @@ -1044,12 +1044,12 @@ msgstr "" msgid "No interface with a route" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2364 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2631 msgid "No interface with a route is up." msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:47 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2379 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2646 msgid "No link" msgstr "" @@ -1057,7 +1057,7 @@ msgstr "" msgid "No network interface with a route is up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1283 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1310 msgid "No other interfaces are up" msgstr "" @@ -1067,7 +1067,7 @@ msgid "No problems detected" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:100 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1253 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1333 msgid "No radios reported" msgstr "" @@ -1079,11 +1079,11 @@ msgstr "" msgid "No stations are associated right now" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2027 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2294 msgid "No wireless networks configured." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1729 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1985 msgid "No wireless radios are reported by this device." msgstr "" @@ -1091,19 +1091,19 @@ msgstr "" msgid "Nobody associated" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1991 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2258 msgid "Nobody is moving data right now." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1761 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2017 msgid "Noise" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2193 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2460 msgid "Noise / SNR" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2349 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2616 msgid "Noise floor" msgstr "" @@ -1116,23 +1116,23 @@ 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:2317 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2584 msgid "Not a station MAC address" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2134 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2401 msgid "Not connected" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1461 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1717 msgid "Not reported" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2228 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2495 msgid "Nothing better known" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2226 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2493 msgid "Nothing better known; the address is randomised" msgstr "" @@ -1144,11 +1144,11 @@ msgstr "" msgid "Older %s device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1937 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2204 msgid "Older Wi-Fi generation than the radio; uses more airtime" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 msgid "Online" msgstr "" @@ -1156,7 +1156,7 @@ msgstr "" msgid "Open" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1903 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2165 msgid "Open details of %s" msgstr "" @@ -1168,11 +1168,11 @@ msgstr "" msgid "OpenWrt device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2192 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2459 msgid "PMF" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1136 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1198 msgid "Paused while hidden" msgstr "" @@ -1180,7 +1180,7 @@ msgstr "" msgid "PiB" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2410 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2677 msgid "Platform" msgstr "" @@ -1192,7 +1192,7 @@ msgstr "" msgid "Private" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2209 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2476 msgid "Private Wi-Fi address (randomised by the device)" msgstr "" @@ -1200,24 +1200,24 @@ msgstr "" msgid "Private device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2407 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2674 msgid "Processes" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2381 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2648 msgid "Protocol" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1788 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2044 msgid "Quick filters" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2197 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2344 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2464 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2611 msgid "Radio" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1750 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2006 msgid "Radio details" msgstr "" @@ -1229,46 +1229,46 @@ msgstr "" msgid "Radio is enabled but not up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1773 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2338 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2029 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2605 msgid "Radio settings" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:786 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1702 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1704 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:838 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1958 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1960 msgid "Radios" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1194 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1358 msgid "Reachability not probed" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2372 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2639 msgid "Realtime graphs" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2391 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2658 msgid "Received" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1137 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1199 msgid "Reconnecting…" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2286 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2553 msgid "Remove name" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2152 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2419 msgid "Rename" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:942 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1004 msgid "Reset" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2221 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2488 msgid "Reverse DNS of its address" msgstr "" @@ -1276,57 +1276,57 @@ msgstr "" msgid "Root (overlay)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2278 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2545 msgid "Save" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2200 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2467 msgid "Seen since" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2392 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2659 msgid "Sent" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1022 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1084 msgid "Show %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1924 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2191 msgid "Show all clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1410 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1661 msgid "Show or hide system details" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1174 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1244 msgid "Show or hide the network path" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1705 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1961 msgid "Show or hide the radio details" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1511 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1767 msgid "Show or hide the throughput chart" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2018 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2285 msgid "Show or hide the wireless networks" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1238 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2034 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1417 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2301 msgid "Show the clients of %s" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:113 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:114 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:117 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1950 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2176 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2217 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2443 msgid "Signal" msgstr "" @@ -1334,24 +1334,24 @@ msgstr "" msgid "Signal not reported" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2380 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2647 msgid "Speed" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1921 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2188 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:2345 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2612 msgid "Status" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:88 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1426 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1677 msgid "Storage" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2287 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2554 msgid "Stored on this device in /etc/config/vantage" msgstr "" @@ -1367,17 +1367,17 @@ msgstr "" msgid "Strong signal, %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1466 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1722 msgid "Swap" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:787 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1409 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1410 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:839 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1660 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1661 msgid "System" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2407 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2674 msgid "System settings" msgstr "" @@ -1389,16 +1389,16 @@ msgstr "" msgid "Temporary (RAM)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2322 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2589 msgid "The device could not write /etc/config/vantage." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1761 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2017 msgid "" "The driver reports a placeholder value, so no noise floor or SNR is shown" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2224 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2491 msgid "The name the device announces (WPS)" msgstr "" @@ -1406,15 +1406,15 @@ msgstr "" msgid "The stored name has an invalid section id" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1211 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1366 msgid "This access point" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1211 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1366 msgid "This device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2321 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2588 msgid "This device already stores %d names; remove some first." msgstr "" @@ -1422,22 +1422,22 @@ msgstr "" msgid "This device reports no radios" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2329 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2596 msgid "This radio is not reported any more." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2134 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2401 msgid "This station is not associated right now." msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:786 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1503 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1505 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2182 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:838 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1759 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1761 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2449 msgid "Throughput" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1496 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1752 msgid "" "Throughput per link, download above the line and upload below; arrow keys " "read the values over time" @@ -1447,64 +1447,64 @@ msgstr "" msgid "TiB" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2189 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2456 msgid "To device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1965 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2006 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2232 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2273 msgid "To device %s, from device %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:2224 msgid "To device: %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2375 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2642 msgid "To network" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:787 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1982 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1983 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:839 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2249 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2250 msgid "Top talkers" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2202 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2341 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2469 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2608 msgid "Traffic" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1300 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1469 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1493 msgid "Traffic now" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1760 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2348 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2016 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2615 msgid "Tx power" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2133 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2400 msgid "Unknown device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2587 msgid "Unknown device type" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2329 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2596 msgid "Unknown radio" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:57 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1749 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2345 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2379 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2005 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2612 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2646 msgid "Up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1433 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1684 msgid "Up %s" msgstr "" @@ -1513,34 +1513,34 @@ msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:49 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:51 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:57 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1529 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2364 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1785 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2631 msgid "Uplink" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1529 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1785 msgid "Uplink %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2368 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2635 msgid "Uplink · %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1509 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1585 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1658 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1765 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1841 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1914 msgid "Upload" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:275 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:320 msgid "Upload (from clients)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2204 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2471 msgid "Uploaded" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2413 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2680 msgid "Uptime" msgstr "" @@ -1666,7 +1666,7 @@ msgstr "" msgid "WPS" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2212 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2479 msgid "WPS device name" msgstr "" @@ -1675,7 +1675,7 @@ msgid "Weak" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:142 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1851 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2107 msgid "Weak signal" msgstr "" @@ -1683,8 +1683,8 @@ msgstr "" msgid "Weak signal %s; may roam poorly" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1303 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1304 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1474 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1476 msgid "Weakest signal" msgstr "" @@ -1695,29 +1695,29 @@ msgstr "" msgid "Wi-Fi" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:787 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2017 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2018 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:839 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2284 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2285 msgid "Wireless networks" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2162 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2429 msgid "Wireless settings" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2220 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2487 msgid "Your name for this device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1322 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 msgid "all up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1755 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2011 msgid "auto" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2200 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2467 msgid "before this page opened" msgstr "" @@ -1725,16 +1725,16 @@ msgstr "" msgid "bit/s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1615 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1871 msgid "collecting · %s of data" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1615 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1871 msgid "collecting · first samples" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1595 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:216 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1851 msgid "collecting…" msgstr "" @@ -1742,7 +1742,7 @@ msgstr "" msgid "dBm" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2252 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2519 msgid "e.g. Living room TV" msgstr "" @@ -1758,7 +1758,7 @@ msgstr "" msgid "half duplex" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2043 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2310 msgid "idle" msgstr "" @@ -1770,14 +1770,14 @@ msgstr "" msgid "kbit/s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1418 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1614 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1669 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1870 msgid "last 5 min" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:160 #: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:104 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1959 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2226 msgid "legacy" msgstr "" @@ -1785,61 +1785,61 @@ msgstr "" msgid "mDNS" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2222 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2489 msgid "mDNS announcement" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1300 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2184 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1469 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1493 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 msgid "measuring" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1589 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1845 msgid "measuring…" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1302 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1473 msgid "no Wi-Fi traffic yet" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1304 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1476 msgid "no clients" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1324 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1497 msgid "no traffic yet" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1203 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1321 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1364 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1494 msgid "no uplink" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1299 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1467 msgid "none connected" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2210 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2477 msgid "not known to this AP" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1761 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1762 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2349 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2017 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2018 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2616 msgid "not reported" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2349 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2616 msgid "not reported (driver placeholder %s)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2193 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2460 msgid "not reported by the driver" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1611 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2199 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2466 msgid "now" msgstr "" @@ -1847,27 +1847,27 @@ msgstr "" msgid "now %s down, %s up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2191 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2458 msgid "older than the radio (%s)" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1588 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1844 msgid "peak" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1670 msgid "per core" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2178 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2445 msgid "range %s … %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2374 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2641 msgid "received on %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2375 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2642 msgid "sent on %s" msgstr "" @@ -1875,19 +1875,19 @@ msgstr "" msgid "short GI" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1214 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1369 msgid "up %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1983 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2250 msgid "who uses the bandwidth" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1300 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1469 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1493 msgid "↑ %s · via %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1608 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1864 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 e6f9382c0..e8d07e26a 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:2347 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2614 msgid "%d MHz (%d–%d MHz)" msgstr "%d MHz(%d–%d MHz)" @@ -32,23 +32,23 @@ msgstr "%d 个 SSID 正在广播" msgid "%d SSIDs · %d idle" msgstr "%d 个 SSID · %d 个空闲" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1130 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1242 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1192 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1979 msgid "%d clients" msgstr "%d 个客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2182 msgid "%d connected" msgstr "已连接 %d 个" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:101 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1322 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 msgid "%d down" msgstr "%d 个已关闭" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2391 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2392 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2658 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2659 msgid "%d errors" msgstr "%d 个错误" @@ -56,24 +56,24 @@ msgstr "%d 个错误" msgid "%d h ago" msgstr "%d 小时前" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2393 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2660 msgid "%d in · %d out" msgstr "%d 入 · %d 出" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:357 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:314 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:351 msgid "%d min ago" msgstr "%d 分钟前" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2182 msgid "%d of %d" msgstr "%d/%d" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1017 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1079 msgid "%d of %d columns" msgstr "" @@ -85,24 +85,24 @@ 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:1293 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1462 msgid "%d on %s" msgstr "%d 个在 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1715 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1971 msgid "%d radios" msgstr "%d 个射频" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:357 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:314 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:351 msgid "%d s ago" msgstr "%d 秒前" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2205 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2472 msgid "%d sent · %s retried · %s failed" msgstr "发送 %d · 重传 %s · 失败 %s" @@ -118,7 +118,7 @@ msgstr "%d 个已启用" msgid "%d weak" msgstr "%d 个信号弱" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1734 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1990 msgid "%d–%d MHz of the %d–%d MHz band" msgstr "%d–%d MHz 属于 %d–%d MHz 频段" @@ -130,7 +130,7 @@ msgstr "%s(%s)已禁用" msgid "%s (%s) is enabled but not up" msgstr "%s(%s)已启用但未上线" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2200 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2467 msgid "%s (new this session)" msgstr "%s(本次新增)" @@ -141,10 +141,10 @@ msgstr "%s Gbit/s" #: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:131 #: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:103 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1741 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1755 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1959 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2340 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1997 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2011 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2226 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2607 msgid "%s MHz" msgstr "%s MHz" @@ -154,7 +154,7 @@ msgstr "%s MHz" msgid "%s Mbit/s" msgstr "%s Mbit/s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2199 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2466 msgid "%s ago" msgstr "%s 前" @@ -163,12 +163,12 @@ msgid "%s at %s; below %s roaming and throughput suffer" msgstr "%s(%s);低于 %s 时漫游和吞吐量会受影响" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:78 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1448 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1699 msgid "%s available" msgstr "可用 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1760 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2348 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2016 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2615 msgid "%s dBm" msgstr "%s dBm" @@ -180,7 +180,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:2167 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2434 msgid "%s experience" msgstr "%s 体验" @@ -196,7 +196,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:984 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1046 msgid "%s hidden" msgstr "" @@ -204,7 +204,7 @@ msgstr "" msgid "%s is disabled" msgstr "%s 已禁用" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:977 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1039 msgid "%s is narrower" msgstr "" @@ -212,7 +212,7 @@ msgstr "" msgid "%s is not up" msgstr "%s 未上线" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:980 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1042 msgid "%s is wider" msgstr "" @@ -220,23 +220,23 @@ msgstr "" msgid "%s link" msgstr "%s 链路" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1098 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1160 msgid "%s moved" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:974 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1036 msgid "%s moved down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:965 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1027 msgid "%s moved left" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:968 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1030 msgid "%s moved right" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:971 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1033 msgid "%s moved up" msgstr "" @@ -249,9 +249,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:79 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1449 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1467 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1700 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2686 msgid "%s of %s" msgstr "%s / %s" @@ -259,7 +259,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:1301 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1471 msgid "%s of Wi-Fi traffic" msgstr "占 Wi-Fi 流量的 %s" @@ -276,15 +276,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:2417 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2684 msgid "%s over %d cores" msgstr "%s / %d 核" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1530 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1786 msgid "%s radio" msgstr "%s 射频" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1023 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1085 msgid "%s shown" msgstr "" @@ -300,7 +300,7 @@ msgstr "%s 协商速率为 %s" msgid "%s used" msgstr "已用 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1475 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1731 msgid "%s used of %s" msgstr "已用 %s / 共 %s" @@ -308,7 +308,7 @@ msgstr "已用 %s / 共 %s" msgid "%s · %s" msgstr "%s · %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2197 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2464 msgid "%s · channel %d · %d MHz" msgstr "%s · 信道 %d · %d MHz" @@ -324,22 +324,22 @@ msgstr "%s · 链路较慢" msgid "%s: no carrier" msgstr "%s:无载波" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2348 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2615 msgid "(configured %d dBm, limited by regulatory rules)" msgstr "(配置 %d dBm,受监管规则限制)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1241 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2038 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1418 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2305 msgid "(hidden)" msgstr "(隐藏)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1130 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1242 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1723 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1192 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1979 msgid "1 client" msgstr "1 个客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1915 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2182 msgid "1 connected" msgstr "已连接 1 个" @@ -347,7 +347,7 @@ msgstr "已连接 1 个" 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:1715 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1971 msgid "1 radio" msgstr "1 个射频" @@ -371,19 +371,19 @@ msgstr "6 GHz" msgid "60 GHz" msgstr "60 GHz" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1850 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2106 msgid "Active" msgstr "活跃" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2209 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2476 msgid "Address type" msgstr "地址类型" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2384 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2651 msgid "Addresses" msgstr "地址" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1762 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2018 msgid "Airtime" msgstr "空口时间" @@ -391,7 +391,7 @@ msgstr "空口时间" msgid "Alias" msgstr "别名" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1846 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2102 msgid "All" msgstr "全部" @@ -407,9 +407,9 @@ msgstr "全部高于 %s" msgid "All good" msgstr "全部正常" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:930 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:931 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1118 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:992 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:993 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1180 msgid "Arrange the dashboard" msgstr "" @@ -417,13 +417,13 @@ msgstr "" msgid "B" msgstr "B" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1323 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1324 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1496 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1497 msgid "Busiest network" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1301 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1302 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1471 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1473 msgid "Busiest radio" msgstr "最繁忙射频" @@ -431,7 +431,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:1419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1670 msgid "Busy share of each CPU core" msgstr "每个 CPU 核心的繁忙占比" @@ -445,8 +445,8 @@ msgstr "繁忙;负载 %s(%d 核);查看最繁忙进程" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:68 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:69 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1413 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2417 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1664 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2684 msgid "CPU" msgstr "CPU" @@ -454,93 +454,93 @@ msgstr "CPU" msgid "Cache (reclaimable)" msgstr "缓存(可回收)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2285 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2552 msgid "Cancel" msgstr "取消" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2192 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2459 msgid "Capabilities" msgstr "能力" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2379 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2646 msgid "Carrier" msgstr "链路状态" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2347 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2614 msgid "Centre frequency" msgstr "中心频率" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1754 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2010 msgid "Ch %d" msgstr "信道 %d" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1226 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1722 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1401 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1978 msgid "Ch %d · %d MHz" msgstr "信道 %d · %d MHz" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2340 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2607 msgid "Channel" msgstr "信道" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1226 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1401 msgid "Channel —" msgstr "信道 —" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1786 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2042 msgid "Clear filter" msgstr "清除筛选" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1174 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1244 msgid "Click any node for details" msgstr "点击任意节点查看详情" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:786 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1299 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1759 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1794 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1795 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:838 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1467 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2015 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2050 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2051 msgid "Clients" msgstr "客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2352 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2619 msgid "Clients on this radio" msgstr "此射频上的客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2055 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2322 msgid "Close" msgstr "关闭" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1974 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2241 msgid "Connected" msgstr "已连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1936 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2203 msgid "Connected after you opened this page" msgstr "打开本页面后连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1321 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2198 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1494 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2465 msgid "Connected for" msgstr "已连接时长" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2195 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2462 msgid "Connection" msgstr "连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1441 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1692 msgid "Core %d: %s" msgstr "核心 %d:%s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2268 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2535 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:2390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2657 msgid "Counters since boot" msgstr "自启动以来的计数器" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2350 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2617 msgid "Country" msgstr "国家" @@ -548,16 +548,16 @@ msgstr "国家" msgid "DHCP" msgstr "DHCP" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2223 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2490 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:2388 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2655 msgid "DNS" msgstr "DNS" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1194 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1358 msgid "DNS %s" msgstr "DNS %s" @@ -566,84 +566,84 @@ 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:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1941 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2408 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2208 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2675 msgid "Device" msgstr "设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2252 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2519 msgid "Device name" msgstr "设备名" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2281 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2548 msgid "Device type" msgstr "设备类型" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1227 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1724 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1749 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2345 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1402 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1980 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2005 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2612 msgid "Disabled" msgstr "已禁用" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2161 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2428 msgid "Disconnected %s; shown from the last sample." msgstr "断开于 %s;显示来自最后一次采样。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:943 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1005 msgid "Done" msgstr "" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:45 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1227 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1724 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1749 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2345 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1402 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1980 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2005 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2612 msgid "Down" msgstr "断开" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1508 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1583 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1657 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1764 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1839 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1913 msgid "Download" msgstr "下载" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:274 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:319 msgid "Download (to clients)" msgstr "下载(到客户端)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2203 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2470 msgid "Downloaded" msgstr "已下载" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1004 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1066 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:1005 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1067 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:1003 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1065 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:956 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1018 msgid "Drag to move" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2393 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2660 msgid "Dropped" msgstr "已丢弃" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2018 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2285 msgid "Edit" msgstr "编辑" @@ -660,8 +660,8 @@ msgstr "一切看起来正常" msgid "Excellent" msgstr "极好" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1968 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2235 msgid "Experience" msgstr "体验" @@ -678,19 +678,19 @@ msgstr "信号一般" msgid "Fair signal %s; lower rates at range" msgstr "信号一般 %s;远距离速率较低" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1783 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2039 msgid "Filter by name, address, SSID…" msgstr "按名称、地址、SSID…筛选" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1784 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2040 msgid "Filter clients" msgstr "筛选客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2411 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2678 msgid "Firmware" msgstr "固件" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2205 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2472 msgid "Frames to device" msgstr "发往设备的帧" @@ -698,20 +698,20 @@ msgstr "发往设备的帧" msgid "Free" msgstr "空闲" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2190 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2457 msgid "From device" msgstr "来自设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1957 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2224 msgid "From device: %s" msgstr "来自设备:%s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2374 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2641 msgid "From network" msgstr "来自网络" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1192 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2387 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1356 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2654 msgid "Gateway" msgstr "网关" @@ -719,7 +719,7 @@ msgstr "网关" msgid "Gbit/s" msgstr "Gbit/s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2191 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2458 msgid "Generation" msgstr "代际" @@ -727,11 +727,11 @@ msgstr "代际" msgid "GiB" msgstr "GiB" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2209 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2476 msgid "Globally unique" msgstr "全局唯一" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2209 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2476 msgid "Globally unique · %s" msgstr "全局唯一 · %s" @@ -752,32 +752,32 @@ msgstr "信号良好 %s" msgid "Half duplex" msgstr "半双工" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:698 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:735 msgid "Health checks" msgstr "健康检查" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1020 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2038 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1082 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2305 msgid "Hidden" msgstr "隐藏" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:983 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1045 msgid "Hide %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2210 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2477 msgid "IP addresses" msgstr "IP 地址" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2385 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2652 msgid "IPv4" msgstr "IPv4" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2386 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2653 msgid "IPv6" msgstr "IPv6" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2207 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2474 msgid "Identity" msgstr "身份" @@ -785,27 +785,27 @@ msgstr "身份" msgid "In use" msgstr "使用中" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1274 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1381 msgid "Interface" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2382 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2649 msgid "Interface up for" msgstr "接口已启用时长" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2372 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2639 msgid "Interfaces" msgstr "接口" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1192 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1356 msgid "Internet" msgstr "互联网" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2412 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2679 msgid "Kernel" msgstr "内核" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1481 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1737 msgid "Kernel %s" msgstr "内核 %s" @@ -813,44 +813,44 @@ msgstr "内核 %s" msgid "KiB" msgstr "KiB" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2199 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2466 msgid "Last activity" msgstr "最后活动" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1119 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1181 msgid "Layout" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:941 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1003 msgid "Layout reset" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1935 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2202 msgid "Left %s" msgstr "离开于 %s" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:159 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1937 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2204 msgid "Legacy" msgstr "旧版" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2188 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2378 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2455 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2645 msgid "Link" msgstr "链路" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1956 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2223 msgid "Link rate" msgstr "链路速率" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1138 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1961 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1200 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2228 msgid "Live" msgstr "实时" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2416 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2683 msgid "Load" msgstr "负载" @@ -862,19 +862,19 @@ msgstr "负载 %s(%d 核)" msgid "Load %s · %d cores" msgstr "负载 %s · %d 核" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2418 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2685 msgid "Load average" msgstr "平均负载" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1402 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1653 msgid "Load average over 1, 5 and 15 minutes" msgstr "1、5、15 分钟平均负载" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1436 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1687 msgid "Load avg %s" msgstr "平均负载 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2414 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2681 msgid "Local time" msgstr "本地时间" @@ -890,7 +890,7 @@ msgstr "相对其信号链路速率偏低" msgid "MAC" msgstr "MAC" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2208 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2475 msgid "MAC address" msgstr "MAC 地址" @@ -898,15 +898,15 @@ msgstr "MAC 地址" msgid "MCS %d" msgstr "MCS %d" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:976 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1038 msgid "Make %s narrower" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:979 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1041 msgid "Make %s wider" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2225 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2492 msgid "Manufacturer of the network adapter" msgstr "网卡制造商" @@ -923,11 +923,11 @@ msgid "Measuring…" msgstr "测量中…" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:78 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1423 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1674 msgid "Memory" msgstr "内存" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2686 msgid "Memory in use" msgstr "已用内存" @@ -935,54 +935,54 @@ msgstr "已用内存" msgid "MiB" msgstr "MiB" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2346 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2613 msgid "Mode" msgstr "模式" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2409 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2676 msgid "Model" msgstr "型号" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:973 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1035 msgid "Move %s down" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:964 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1026 msgid "Move %s left" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:967 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1029 msgid "Move %s right" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:970 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1032 msgid "Move %s up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:956 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1018 msgid "Move %s: drag, or use the arrow keys" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2280 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2547 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:2318 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2585 msgid "Name contains invisible or control characters" msgstr "名称包含不可见或控制字符" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2211 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2478 msgid "Name from" msgstr "名称来源" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1946 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2213 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:2319 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2586 msgid "Name is longer than %d characters" msgstr "名称超过 %d 个字符" @@ -990,32 +990,32 @@ msgstr "名称超过 %d 个字符" msgid "Name must be text" msgstr "名称必须是文本" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2152 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2419 msgid "Name this device" msgstr "命名此设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1953 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2196 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2220 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2463 msgid "Network" msgstr "网络" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:786 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1173 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1174 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:838 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1243 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1244 msgid "Network path" msgstr "网络路径" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1322 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 msgid "Networks" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1852 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1936 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2108 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2203 msgid "New" msgstr "新增" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1248 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1329 msgid "No SSIDs" msgstr "无 SSID" @@ -1023,23 +1023,23 @@ msgstr "无 SSID" msgid "No clients" msgstr "无客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1920 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2187 msgid "No clients connected" msgstr "无客户端连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1991 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2258 msgid "No clients connected." msgstr "没有客户端连接。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1923 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2190 msgid "No clients in this view" msgstr "此视图中没有客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1923 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2190 msgid "No clients match “%s”" msgstr "没有匹配“%s”的客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2357 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2624 msgid "No clients; an idle radio is fine." msgstr "无客户端;空闲的射频是正常的。" @@ -1047,7 +1047,7 @@ msgstr "无客户端;空闲的射频是正常的。" msgid "No clients; idle radios are fine" msgstr "无客户端;空闲的射频是正常的" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1193 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1357 msgid "No default route" msgstr "无默认路由" @@ -1055,12 +1055,12 @@ msgstr "无默认路由" msgid "No interface with a route" msgstr "无带路由的接口" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2364 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2631 msgid "No interface with a route is up." msgstr "没有带路由的接口处于启用状态。" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:47 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2379 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2646 msgid "No link" msgstr "无链路" @@ -1068,7 +1068,7 @@ msgstr "无链路" msgid "No network interface with a route is up" msgstr "没有带路由的网络接口处于启用状态" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1283 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1310 msgid "No other interfaces are up" msgstr "" @@ -1078,7 +1078,7 @@ msgid "No problems detected" msgstr "未检测到问题" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:100 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1253 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1333 msgid "No radios reported" msgstr "未报告射频" @@ -1090,11 +1090,11 @@ msgstr "无信号" msgid "No stations are associated right now" msgstr "当前没有站点关联" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2027 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2294 msgid "No wireless networks configured." msgstr "未配置无线网络。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1729 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1985 msgid "No wireless radios are reported by this device." msgstr "此设备未报告任何无线射频。" @@ -1102,19 +1102,19 @@ msgstr "此设备未报告任何无线射频。" msgid "Nobody associated" msgstr "无关联站点" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1991 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2258 msgid "Nobody is moving data right now." msgstr "当前没有设备在传输数据。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1761 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2017 msgid "Noise" msgstr "噪声" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2193 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2460 msgid "Noise / SNR" msgstr "噪声 / 信噪比" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2349 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2616 msgid "Noise floor" msgstr "噪声底" @@ -1127,23 +1127,23 @@ 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:2317 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2584 msgid "Not a station MAC address" msgstr "不是站点 MAC 地址" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2134 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2401 msgid "Not connected" msgstr "未连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1461 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1717 msgid "Not reported" msgstr "未报告" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2228 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2495 msgid "Nothing better known" msgstr "无更多信息" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2226 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2493 msgid "Nothing better known; the address is randomised" msgstr "无更多信息;地址已随机化" @@ -1155,11 +1155,11 @@ msgstr "OWE" msgid "Older %s device" msgstr "较旧的 %s 设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1937 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2204 msgid "Older Wi-Fi generation than the radio; uses more airtime" msgstr "比射频支持的 Wi-Fi 代际更旧;占用更多空口时间" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 msgid "Online" msgstr "在线" @@ -1167,7 +1167,7 @@ msgstr "在线" msgid "Open" msgstr "开放" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1903 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2165 msgid "Open details of %s" msgstr "打开 %s 的详情" @@ -1179,11 +1179,11 @@ msgstr "开放网络" msgid "OpenWrt device" msgstr "OpenWrt 设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2192 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2459 msgid "PMF" msgstr "PMF" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1136 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1198 msgid "Paused while hidden" msgstr "隐藏时暂停" @@ -1191,7 +1191,7 @@ msgstr "隐藏时暂停" msgid "PiB" msgstr "PiB" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2410 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2677 msgid "Platform" msgstr "平台" @@ -1203,7 +1203,7 @@ msgstr "较差" msgid "Private" msgstr "私有" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2209 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2476 msgid "Private Wi-Fi address (randomised by the device)" msgstr "私有 Wi-Fi 地址(设备随机化)" @@ -1211,24 +1211,24 @@ msgstr "私有 Wi-Fi 地址(设备随机化)" msgid "Private device" msgstr "私有设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2407 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2674 msgid "Processes" msgstr "进程" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2381 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2648 msgid "Protocol" msgstr "协议" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1788 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2044 msgid "Quick filters" msgstr "快速筛选" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2197 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2344 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2464 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2611 msgid "Radio" msgstr "射频" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1750 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2006 msgid "Radio details" msgstr "射频详情" @@ -1240,46 +1240,46 @@ msgstr "射频已禁用" msgid "Radio is enabled but not up" msgstr "射频已启用但未上线" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1773 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2338 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2029 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2605 msgid "Radio settings" msgstr "射频设置" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:786 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1702 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1704 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:838 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1958 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1960 msgid "Radios" msgstr "射频" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1194 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1358 msgid "Reachability not probed" msgstr "未探测可达性" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2372 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2639 msgid "Realtime graphs" msgstr "实时图表" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2391 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2658 msgid "Received" msgstr "已接收" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1137 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1199 msgid "Reconnecting…" msgstr "重新连接中…" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2286 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2553 msgid "Remove name" msgstr "移除名称" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2152 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2419 msgid "Rename" msgstr "重命名" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:942 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1004 msgid "Reset" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2221 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2488 msgid "Reverse DNS of its address" msgstr "其地址的反向 DNS" @@ -1287,57 +1287,57 @@ msgstr "其地址的反向 DNS" msgid "Root (overlay)" msgstr "Root(overlay)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2278 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2545 msgid "Save" msgstr "保存" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2200 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2467 msgid "Seen since" msgstr "首次出现" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2392 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2659 msgid "Sent" msgstr "已发送" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1022 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1084 msgid "Show %s" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1924 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2191 msgid "Show all clients" msgstr "显示全部客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1410 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1661 msgid "Show or hide system details" msgstr "显示/隐藏系统详情" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1174 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1244 msgid "Show or hide the network path" msgstr "显示/隐藏网络路径" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1705 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1961 msgid "Show or hide the radio details" msgstr "显示/隐藏射频详情" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1511 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1767 msgid "Show or hide the throughput chart" msgstr "显示/隐藏吞吐量图表" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2018 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2285 msgid "Show or hide the wireless networks" msgstr "显示/隐藏无线网络" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1238 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2034 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1417 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2301 msgid "Show the clients of %s" msgstr "显示 %s 的客户端" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:113 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:114 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:117 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1863 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1950 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2176 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2122 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2217 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2443 msgid "Signal" msgstr "信号强度" @@ -1345,24 +1345,24 @@ msgstr "信号强度" msgid "Signal not reported" msgstr "未报告信号" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2380 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2647 msgid "Speed" msgstr "速率" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1921 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2188 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:2345 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2612 msgid "Status" msgstr "状态" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:88 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1426 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1677 msgid "Storage" msgstr "存储" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2287 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2554 msgid "Stored on this device in /etc/config/vantage" msgstr "保存在本设备 /etc/config/vantage 中" @@ -1378,17 +1378,17 @@ msgstr "信号强 %s,链路 %s" msgid "Strong signal, %s" msgstr "信号强,%s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1466 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1722 msgid "Swap" msgstr "交换分区" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:787 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1409 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1410 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:839 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1660 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1661 msgid "System" msgstr "系统" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2407 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2674 msgid "System settings" msgstr "系统设置" @@ -1400,16 +1400,16 @@ msgstr "Tbit/s" msgid "Temporary (RAM)" msgstr "临时(RAM)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2322 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2589 msgid "The device could not write /etc/config/vantage." msgstr "设备无法写入 /etc/config/vantage。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1761 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2017 msgid "" "The driver reports a placeholder value, so no noise floor or SNR is shown" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2224 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2491 msgid "The name the device announces (WPS)" msgstr "设备广播的名称(WPS)" @@ -1417,15 +1417,15 @@ msgstr "设备广播的名称(WPS)" msgid "The stored name has an invalid section id" msgstr "保存的名称包含无效的节 ID" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1211 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1366 msgid "This access point" msgstr "此接入点" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1211 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1366 msgid "This device" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2321 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2588 msgid "This device already stores %d names; remove some first." msgstr "此设备已存储 %d 个名称;请先移除部分。" @@ -1433,22 +1433,22 @@ msgstr "此设备已存储 %d 个名称;请先移除部分。" msgid "This device reports no radios" msgstr "此设备未报告任何射频" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2329 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2596 msgid "This radio is not reported any more." msgstr "此射频不再被报告。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2134 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2401 msgid "This station is not associated right now." msgstr "此站点当前未关联。" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:786 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1503 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1505 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2182 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:838 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1759 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1761 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2449 msgid "Throughput" msgstr "吞吐量" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1496 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1752 msgid "" "Throughput per link, download above the line and upload below; arrow keys " "read the values over time" @@ -1458,64 +1458,64 @@ msgstr "" msgid "TiB" msgstr "TiB" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2189 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2456 msgid "To device" msgstr "到设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1965 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2006 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2232 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2273 msgid "To device %s, from device %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:2224 msgid "To device: %s" msgstr "到设备:%s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2375 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2642 msgid "To network" msgstr "发往网络" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:787 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1982 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1983 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:839 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2249 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2250 msgid "Top talkers" msgstr "流量大户" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2202 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2341 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2469 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2608 msgid "Traffic" msgstr "流量" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1300 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1469 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1493 msgid "Traffic now" msgstr "当前流量" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1760 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2348 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2016 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2615 msgid "Tx power" msgstr "发射功率" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2133 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2400 msgid "Unknown device" msgstr "未知设备" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2587 msgid "Unknown device type" msgstr "未知设备类型" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2329 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2596 msgid "Unknown radio" msgstr "未知射频" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:57 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1749 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2345 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2379 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2005 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2612 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2646 msgid "Up" msgstr "正常" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1433 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1684 msgid "Up %s" msgstr "已运行 %s" @@ -1524,34 +1524,34 @@ msgstr "已运行 %s" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:49 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:51 #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:57 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1529 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2364 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1785 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2631 msgid "Uplink" msgstr "上行链路" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1529 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1785 msgid "Uplink %s" msgstr "上行链路 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2368 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2635 msgid "Uplink · %s" msgstr "上行链路 · %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1509 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1585 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1658 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1765 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1841 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1914 msgid "Upload" msgstr "上传" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:275 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:320 msgid "Upload (from clients)" msgstr "上传(来自客户端)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2204 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2471 msgid "Uploaded" msgstr "已上传" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2413 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2680 msgid "Uptime" msgstr "运行时间" @@ -1677,7 +1677,7 @@ msgstr "WPA3 个人版" msgid "WPS" msgstr "WPS" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2212 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2479 msgid "WPS device name" msgstr "WPS 设备名称" @@ -1686,7 +1686,7 @@ msgid "Weak" msgstr "弱" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:142 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1851 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2107 msgid "Weak signal" msgstr "信号弱" @@ -1694,8 +1694,8 @@ msgstr "信号弱" msgid "Weak signal %s; may roam poorly" msgstr "信号弱 %s;漫游可能不佳" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1303 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1304 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1474 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1476 msgid "Weakest signal" msgstr "最弱信号" @@ -1706,29 +1706,29 @@ msgstr "最弱信号" msgid "Wi-Fi" msgstr "Wi-Fi" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:787 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2017 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2018 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:839 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2284 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2285 msgid "Wireless networks" msgstr "无线网络" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2162 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2429 msgid "Wireless settings" msgstr "无线设置" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2220 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2487 msgid "Your name for this device" msgstr "你为此设备设置的名称" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1322 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 msgid "all up" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1755 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2011 msgid "auto" msgstr "自动" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2200 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2467 msgid "before this page opened" msgstr "打开本页之前" @@ -1736,16 +1736,16 @@ msgstr "打开本页之前" msgid "bit/s" msgstr "bit/s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1615 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1871 msgid "collecting · %s of data" msgstr "采集中 · 已采集 %s 数据" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1615 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1871 msgid "collecting · first samples" msgstr "采集中 · 首个样本" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:215 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1595 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:216 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1851 msgid "collecting…" msgstr "采集中…" @@ -1753,7 +1753,7 @@ msgstr "采集中…" msgid "dBm" msgstr "dBm" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2252 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2519 msgid "e.g. Living room TV" msgstr "例如:客厅电视" @@ -1769,7 +1769,7 @@ msgstr "网关 %s" msgid "half duplex" msgstr "半双工" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2043 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2310 msgid "idle" msgstr "空闲" @@ -1781,14 +1781,14 @@ msgstr "刚刚" msgid "kbit/s" msgstr "kbit/s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1418 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1614 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1669 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1870 msgid "last 5 min" msgstr "最近 5 分钟" #: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:160 #: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:104 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1959 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2226 msgid "legacy" msgstr "旧版" @@ -1796,61 +1796,61 @@ msgstr "旧版" msgid "mDNS" msgstr "mDNS" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2222 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2489 msgid "mDNS announcement" msgstr "mDNS 广播" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1300 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2184 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1469 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1493 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2451 msgid "measuring" msgstr "测量中" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1589 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1845 msgid "measuring…" msgstr "测量中…" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1302 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1473 msgid "no Wi-Fi traffic yet" msgstr "尚无 Wi-Fi 流量" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1304 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1476 msgid "no clients" msgstr "无客户端" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1324 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1497 msgid "no traffic yet" msgstr "" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1203 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1321 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1364 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1494 msgid "no uplink" msgstr "无上行链路" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1299 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1467 msgid "none connected" msgstr "无连接" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2210 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2477 msgid "not known to this AP" msgstr "此 AP 未知" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1761 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1762 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2349 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2017 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2018 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2616 msgid "not reported" msgstr "未报告" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2349 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2616 msgid "not reported (driver placeholder %s)" msgstr "未报告(驱动占位 %s)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2193 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2460 msgid "not reported by the driver" msgstr "驱动未报告" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1611 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2199 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2466 msgid "now" msgstr "现在" @@ -1858,27 +1858,27 @@ msgstr "现在" msgid "now %s down, %s up" msgstr "当前下行 %s,上行 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2191 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2458 msgid "older than the radio (%s)" msgstr "比射频支持的标准更旧(%s)" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1588 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1844 msgid "peak" msgstr "峰值" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1419 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1670 msgid "per core" msgstr "每核" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2178 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2445 msgid "range %s … %s" msgstr "范围 %s … %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2374 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2641 msgid "received on %s" msgstr "在 %s 上接收" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2375 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2642 msgid "sent on %s" msgstr "在 %s 上发送" @@ -1886,20 +1886,20 @@ msgstr "在 %s 上发送" msgid "short GI" msgstr "短 GI" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1214 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1369 msgid "up %s" msgstr "已运行 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1983 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:2250 msgid "who uses the bandwidth" msgstr "带宽使用者" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1300 -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1320 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1469 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1493 msgid "↑ %s · via %s" msgstr "↑ %s · 经 %s" -#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1608 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1864 msgid "−%d min" msgstr "−%d 分钟" diff --git a/luci-theme-openmptcprouter/luasrc/view/themes/openmptcprouter/header.htm b/luci-theme-openmptcprouter/luasrc/view/themes/openmptcprouter/header.htm index 01d93220b..2dc8c0c64 100644 --- a/luci-theme-openmptcprouter/luasrc/view/themes/openmptcprouter/header.htm +++ b/luci-theme-openmptcprouter/luasrc/view/themes/openmptcprouter/header.htm @@ -22,6 +22,20 @@ local current_omr_version = luci.model.uci.cursor():get("openmptcprouter","settings","version") or "" local latest_omr_version = luci.model.uci.cursor():get("openmptcprouter","latest_versions","omr") or "" + -- Numeric per dotted component of the release part ("0.64.10" of + -- "0.64.10-6.18"): a string compare put 0.64.10 before 0.64.9. + local function omr_version_newer(cur, new) + local a, b = {}, {} + for n in (cur:match("^v?([%d%.]+)") or ""):gmatch("%d+") do a[#a + 1] = tonumber(n) end + for n in (new:match("^v?([%d%.]+)") or ""):gmatch("%d+") do b[#b + 1] = tonumber(n) end + if #a == 0 or #b == 0 then return false end + for i = 1, math.max(#a, #b) do + local x, y = a[i] or 0, b[i] or 0 + if x ~= y then return y > x end + end + return false + end + -%> @@ -46,7 +60,7 @@ - "> + ">
@@ -67,9 +81,9 @@ <% end %>
<%- end -%> - <%- if current_omr_version ~= "" and latest_omr_version ~= "" and current_omr_version < latest_omr_version then -%> + <%- if omr_version_newer(current_omr_version, latest_omr_version) then -%>
-

<%=translatef("You have OpenMPTCProuter version %s and version %s is now available",current_omr_version,latest_omr_version)%>

+

<%=translatef("You have OpenMPTCProuter version %s and version %s is now available",pcdata(current_omr_version),pcdata(latest_omr_version))%>

<%- end -%> diff --git a/luci-theme-vantage/Makefile b/luci-theme-vantage/Makefile index b07dfa44c..2d00e2da3 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.4 -PKG_RELEASE:=17 +PKG_VERSION:=1.0.5 +PKG_RELEASE:=18 PKG_LICENSE:=GPL-3.0-or-later LUCI_TITLE:=Vantage theme diff --git a/luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js b/luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js index f0b651ad9..91228c494 100644 --- a/luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js +++ b/luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js @@ -186,6 +186,14 @@ function headingKey(text) { return String(text || '').replace(/\s+/g, ' ').replace(/[\s:]+$/, '').trim().toLowerCase(); } +/* Does a heading only repeat the breadcrumb? A page title (h2) may repeat + any crumb; a card heading (h3) only the page's or the tab's, never the + category's: on Services > Dynamic DNS the "Services" table is content. */ +function repeatsTrail(key, tag, trail) { + if (!key) return false; + return (tag === 'H2' ? trail.all : trail.page).has(key); +} + /* --------------------------------------------------------------------------- * Stock-page enhancers, only where CSS cannot restructure LuCI's markup * (Status -> Overview: fused memory bar, per-mount storage bars, radio band @@ -587,7 +595,7 @@ return baseclass.extend({ this.closeTimer = null; this.openTimer = null; this.entries = []; - this.trailKeys = new Set(); + this.trail = { all: new Set(), page: new Set() }; this.setupTheme(); this.setupIndicators(); @@ -670,8 +678,11 @@ return baseclass.extend({ link.lastChild.setAttribute('class', 'vt-rail-caret'); link.addEventListener('click', ev => this.handleRailClick(ev, cat, li, link)); + /* hover opens a category; an open page switcher was opened on + purpose, so passing over the rail leaves it be (a click on a + category still swaps it) */ li.addEventListener('pointerenter', ev => { - if (ev.pointerType !== 'mouse' || this.isPinned() || this.mqMobile.matches) return; + if (ev.pointerType !== 'mouse' || this.isPinned() || this.mqMobile.matches || this.switchOpen()) return; this.cancelClose(); window.clearTimeout(this.openTimer); this.openTimer = window.setTimeout(() => this.openFlyout(cat, li, link, false), this.fly.hidden ? 90 : 0); @@ -679,6 +690,7 @@ return baseclass.extend({ li.addEventListener('pointerleave', ev => { if (ev.pointerType !== 'mouse') return; window.clearTimeout(this.openTimer); + if (this.isPinned() || this.switchOpen()) return; this.scheduleClose(); }); @@ -701,13 +713,16 @@ return baseclass.extend({ list.addEventListener('keydown', ev => this.handleRailKeys(ev, list)); fly.addEventListener('pointerenter', () => this.cancelClose()); - fly.addEventListener('pointerleave', ev => { if (ev.pointerType === 'mouse') this.scheduleClose(); }); + fly.addEventListener('pointerleave', ev => { if (ev.pointerType === 'mouse' && !fly.classList.contains('is-switch')) this.scheduleClose(); }); fly.addEventListener('keydown', ev => this.handleFlyKeys(ev)); + /* the switcher closes once focus moves on past it (a null target is + a click on nothing focusable, which the document click handles) */ + fly.addEventListener('focusout', ev => this.leaveSwitch(ev.relatedTarget)); if (this.scrim) this.scrim.addEventListener('click', () => this.closeFlyout(false)); document.addEventListener('click', ev => { if (fly.hidden) return; - if (ev.target.closest('#vt-flyout') || ev.target.closest('#vt-rail')) return; + if (ev.target.closest('#vt-flyout') || ev.target.closest('#vt-rail') || ev.target.closest('.vt-crumb-switch')) return; this.closeFlyout(false); }); @@ -777,7 +792,8 @@ return baseclass.extend({ handleFlyKeys(ev) { const links = Array.prototype.slice.call(this.fly.querySelectorAll('a')); const i = links.indexOf(document.activeElement); - if (ev.key === 'Escape' || (ev.key === 'ArrowLeft' && !this.mqMobile.matches)) { + const isSwitch = this.switchOpen(); + if (ev.key === 'Escape' || (ev.key === 'ArrowLeft' && !isSwitch && !this.mqMobile.matches)) { ev.preventDefault(); this.closeFlyout(true); } @@ -787,15 +803,57 @@ return baseclass.extend({ links[(i + d + links.length) % links.length].focus(); } else if (ev.key === 'Tab' && links.length) { - /* leaving the panel returns to the rail */ const edge = ev.shiftKey ? 0 : links.length - 1; - if (i === edge) { ev.preventDefault(); this.closeFlyout(true); } + if (i !== edge) return; + ev.preventDefault(); + /* the switcher reads as part of the top bar, after its crumb: + Tab goes on to the next control there, Shift+Tab back to the + crumb. The rail's flyout returns to its rail button. */ + if (isSwitch && !ev.shiftKey) { + const btn = this.switchBtn; + this.closeFlyout(false); + this.focusAfter(btn); + } + else this.closeFlyout(true); } }, + /* keys on the crumb switch: arrows open it (or enter it) at the current + page; Tab from the open switch goes into its list */ + handleSwitchKeys(ev, cat, page, btn) { + const down = ev.key === 'ArrowDown', up = ev.key === 'ArrowUp'; + if (!down && !up && !(ev.key === 'Tab' && !ev.shiftKey && this.switchOpen())) return; + ev.preventDefault(); + if (!this.switchOpen()) { this.openSwitch(cat, page, btn, true); return; } + const links = this.fly.querySelectorAll('a'); + const target = (ev.key === 'Tab' ? null : this.fly.querySelector('a[aria-current]')) || + (up ? links[links.length - 1] : links[0]); + if (target) target.focus(); + }, + + switchOpen() { + return !!(this.fly && !this.fly.hidden && this.fly.classList.contains('is-switch')); + }, + + /* focus moved from the switch or its list to `to` */ + leaveSwitch(to) { + if (!to || !this.switchOpen()) return; + if (this.fly.contains(to) || to === this.switchBtn) return; + this.closeFlyout(false); + }, + + /* the control after `el` in tab order, outside the flyout */ + focusAfter(el) { + const all = Array.prototype.filter.call(document.querySelectorAll('a[href], button, input, select, textarea, [tabindex]'), n => + n.tabIndex >= 0 && !n.disabled && n.type !== 'hidden' && !this.fly.contains(n) && n.getClientRects().length > 0); + const next = all[all.indexOf(el) + 1]; + (next || el).focus(); + }, + openFlyout(cat, li, link, focusFirst) { const fly = this.fly; this.cancelClose(); + this.endSwitch(); if (fly.getAttribute('data-cat') !== cat.name || fly.hidden) { fly.textContent = ''; const total = cat.pages.length; @@ -851,6 +909,7 @@ return baseclass.extend({ this.cancelClose(); this.fly.hidden = true; if (this.scrim) this.scrim.hidden = true; + this.endSwitch(); document.querySelectorAll('.vt-rail-item.is-open').forEach(n => { n.classList.remove('is-open'); const b = n.querySelector('.vt-rail-link[aria-expanded]'); @@ -859,6 +918,54 @@ return baseclass.extend({ if (refocus && this.openLink) this.openLink.focus(); }, + /* the flyout, filled with a page's tabs: a dropdown under the crumb, the + same bottom sheet as the rail's on phones */ + openSwitch(cat, page, btn, focusFirst) { + const fly = this.fly; + if (!fly) return; + /* a rail hover may have left a timer behind; closeFlyout() skips + cancelling it when the panel is hidden, so cancel both here */ + this.cancelClose(); + window.clearTimeout(this.openTimer); + this.openTimer = null; + this.closeFlyout(false); + fly.textContent = ''; + const total = page.subs.length; + fly.appendChild(E('div', { 'class': 'vt-fly-head' }, [ + svg(cat.icon, 18), + E('span', { 'class': 'vt-fly-title', 'id': 'vt-fly-title' }, [ page.title ]), + E('span', { 'class': 'vt-fly-count' }, [ '%d %s'.format(total, total === 1 ? _('page') : _('pages')) ]) + ])); + const ul = E('ul', { 'class': 'vt-fly-list', 'role': 'list' }); + for (const s of page.subs) + ul.appendChild(E('li', {}, E('a', { + 'class': 'vt-fly-link', 'href': L.url('admin', cat.name, page.name, s.name), 'aria-current': this.dp[3] === s.name ? 'page' : null + }, [ E('span', {}, [ s.title ]) ]))); + fly.appendChild(ul); + fly.setAttribute('data-cat', ''); + fly.setAttribute('role', 'region'); + fly.setAttribute('aria-labelledby', 'vt-fly-title'); + + const x = Math.max(8, Math.min(btn.getBoundingClientRect().left - 8, window.innerWidth - 328)); + fly.style.setProperty('--x', x.toFixed(0) + 'px'); + fly.classList.add('is-switch'); + btn.setAttribute('aria-expanded', 'true'); + this.switchBtn = btn; + this.openLink = btn; + fly.hidden = false; + if (this.scrim) this.scrim.hidden = !this.mqMobile.matches; + + if (focusFirst) { + const first = fly.querySelector('a[aria-current]') || fly.querySelector('a'); + if (first) first.focus(); + } + }, + + endSwitch() { + if (this.fly) this.fly.classList.remove('is-switch'); + if (this.switchBtn) this.switchBtn.setAttribute('aria-expanded', 'false'); + }, + scheduleClose() { this.cancelClose(); this.closeTimer = window.setTimeout(() => this.closeFlyout(false), 220); @@ -883,17 +990,43 @@ return baseclass.extend({ } if (!parts.length) return; + /* a page with page tabs: its current crumb switches between them, + from the top bar, which stays in view while the tabs scroll away */ + const cat = this.cats.find(c => c.name === this.dp[1]); + const page = cat && this.dp.length > 3 ? cat.pages.find(p => p.name === this.dp[2]) : null; + const siblings = page && page.subs.length > 1 ? page : null; + nav.textContent = ''; parts.forEach((p, i) => { const last = i === parts.length - 1; if (i) nav.appendChild(E('span', { 'class': 'vt-crumb-sep', 'aria-hidden': 'true' }, [ '›' ])); - nav.appendChild(last + if (last && siblings && parts.length === 3) { + /* a disclosure (aria-expanded + aria-controls): the panel is a + region of links, not a menu, so no aria-haspopup */ + const btn = E('button', { + 'type': 'button', 'class': 'vt-crumb vt-crumb-current vt-crumb-switch', 'aria-current': 'page', + 'aria-expanded': 'false', 'aria-controls': 'vt-flyout', + 'title': _('Other pages in %s').format(siblings.title) + }, [ E('span', {}, [ p.title ]), svg('caret', 14) ]); + btn.addEventListener('click', ev => { + if (this.switchOpen()) this.closeFlyout(true); + else this.openSwitch(cat, siblings, btn, ev.detail === 0); + }); + btn.addEventListener('keydown', ev => this.handleSwitchKeys(ev, cat, siblings, btn)); + btn.addEventListener('focusout', ev => this.leaveSwitch(ev.relatedTarget)); + nav.appendChild(btn); + } + else nav.appendChild(last ? E('span', { 'class': 'vt-crumb vt-crumb-current', 'aria-current': 'page' }, [ p.title ]) : E('a', { 'class': 'vt-crumb', 'href': L.url.apply(L, p.segs) }, [ p.title ])); }); this.pageTitle = parts[parts.length - 1].title; - parts.forEach(p => this.trailKeys.add(headingKey(p.title))); + parts.forEach((p, i) => { + this.trail.all.add(headingKey(p.title)); + /* the category, unless it is the page itself */ + if (i || parts.length === 1) this.trail.page.add(headingKey(p.title)); + }); this.updateTitle(); }, @@ -906,17 +1039,16 @@ return baseclass.extend({ const tabs = ui.menu.getChildren(page); if (tabs.length < 2) return; - bar.textContent = ''; + const seg = E('div', { 'class': 'vt-pagetabs' }); for (const t of tabs) { const cur = this.dp[3] === t.name; - bar.appendChild(E('a', { + seg.appendChild(E('a', { 'class': 'vt-tab', 'href': L.url('admin', this.dp[1], this.dp[2], t.name), 'aria-current': cur ? 'page' : null }, [ _(t.title) ])); } + bar.textContent = ''; + bar.appendChild(seg); bar.hidden = false; - const active = bar.querySelector('[aria-current]'); - if (active && active.scrollIntoView && bar.scrollWidth > bar.clientWidth) - bar.scrollLeft = active.offsetLeft - 16; }, /* ------------------------------------------------------ palette */ @@ -954,8 +1086,11 @@ return baseclass.extend({ openPalette() { if (this.pal) return; + /* focus inside the flyout goes back to what opened the flyout, since + closing it hides the element that had focus */ + const from = document.activeElement; + this.palReturn = (this.fly && this.fly.contains(from)) ? this.openLink : from; this.closeFlyout(false); - this.palReturn = document.activeElement; const input = E('input', { 'class': 'vt-pal-input', 'type': 'text', 'role': 'combobox', 'aria-expanded': 'true', @@ -1238,7 +1373,7 @@ return baseclass.extend({ setupForms() { this.formsQueued = false; this.helpSeq = 0; - this.helpSizes = window.ResizeObserver ? new ResizeObserver(entries => entries.forEach(e => this.fitHelp(e.target))) : null; + this.helpSizes = window.ResizeObserver ? new ResizeObserver(entries => this.fitHelps(entries.map(e => e.target))) : null; if (window.MutationObserver) new MutationObserver(() => this.queueForms()).observe(document.body, { childList: true, subtree: true }); this.decorateForms(); @@ -1319,14 +1454,26 @@ return baseclass.extend({ not displayed (another tab, an unmet dependency) measures 0 and waits for the next resize notification. */ fitHelp(desc) { - if (!desc.isConnected) { - if (this.helpSizes) this.helpSizes.unobserve(desc); - return; + this.fitHelps([ desc ]); + }, + + /* all sizes are read before any button changes: a write between two + reads forced a layout per field (66 when the Wireless modal opened) */ + fitHelps(descs) { + const fits = []; + for (const desc of descs) { + if (!desc.isConnected) { + if (this.helpSizes) this.helpSizes.unobserve(desc); + continue; + } + const h = desc._vtHelp; + if (!h || !h.btn.isConnected || !desc.clientHeight) continue; + fits.push([ h, desc.scrollHeight > desc.clientHeight + 1 ]); + } + for (const [ h, cut ] of fits) { + const hide = !cut && h.holder.getAttribute('data-v-help') !== 'open'; + if (h.btn.hidden !== hide) h.btn.hidden = hide; } - const h = desc._vtHelp; - if (!h || !h.btn.isConnected || !desc.clientHeight) return; - const cut = desc.scrollHeight > desc.clientHeight + 1; - h.btn.hidden = !cut && h.holder.getAttribute('data-v-help') !== 'open'; }, /* A page title or card heading that only repeats the breadcrumb, the @@ -1338,7 +1485,7 @@ return baseclass.extend({ document.querySelectorAll('#maincontent > h2, #view h2, #view .cbi-section > h3:first-child').forEach(h => { if (h.closest('.vt-app') || h.querySelector('a, button, input, select, [data-clickable], [tabindex]')) return; const key = headingKey(h.textContent); - let repeat = this.trailKeys.has(key); + let repeat = repeatsTrail(key, h.tagName, this.trail); if (!repeat && h.tagName === 'H3') { const map = h.closest('.cbi-map'); const mapTitle = map && map.querySelector(':scope > h2'); diff --git a/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css b/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css index dd700a1e7..9e0663ebd 100644 --- a/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css +++ b/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css @@ -69,14 +69,17 @@ --warn-soft: rgba(214, 138, 0, .15); --err: #c42b2b; --err-soft: rgba(196, 43, 43, .1); + --err-ink: #ffffff; /* text on an --err / --info fill, >= 4.5:1 */ --info: #1c64c4; + --info-ink: #ffffff; + --ok-text: #0a7262; /* --ok as text on control fills (s3, s4) */ --info-soft: rgba(28, 100, 196, .1); --scrim: rgba(20, 23, 26, .38); --shadow-pop: 0 12px 32px rgba(20, 23, 26, .16), 0 2px 6px rgba(20, 23, 26, .08); --zone-mix: .22; --seg-bg: var(--s3); /* segmented control: track, selected segment */ --seg-on: var(--s2); - --seg-on-ring: 0 0 0 1px var(--line), var(--v-shadow); + --seg-on-ring: inset 0 0 0 1px var(--line); /* inset: drawn at the segment's padding edge */ } html[data-darkmode="true"] { @@ -107,14 +110,17 @@ html[data-darkmode="true"] { --warn-soft: rgba(245, 184, 74, .13); --err: #ff6b6b; --err-soft: rgba(255, 107, 107, .12); + --err-ink: #1a0505; /* white on the light dark-mode red was 2.8:1 */ --info: #6cb4ff; + --info-ink: #0d0e10; + --ok-text: var(--ok); --info-soft: rgba(108, 180, 255, .12); --scrim: rgba(0, 0, 0, .58); --shadow-pop: 0 16px 40px rgba(0, 0, 0, .55), 0 0 0 1px rgba(255, 255, 255, .02); --zone-mix: .2; --seg-bg: var(--bg); --seg-on: var(--s3); - --seg-on-ring: 0 0 0 1px var(--line-2); + --seg-on-ring: inset 0 0 0 1px var(--line-2); } /* touch: controls are sized by the pointer, not the screen width */ @@ -161,7 +167,7 @@ html[data-darkmode="true"] { --v-r-lg: var(--r-lg); --v-ctl-h: var(--ctl-h); /* every control: 36px, 44px on touch */ --v-shadow: 0 1px 2px rgba(20, 23, 26, .06), 0 1px 0 rgba(20, 23, 26, .03); - /* where sticky content may start: below the top bar and page tabs */ + /* where sticky content may start: below the top bar (page tabs scroll) */ --v-stick-top: var(--stick-top); /* charts (stock realtime graphs, channel analysis, the dashboard app). @@ -429,7 +435,7 @@ html[data-rail="pinned"] .vt-frame { margin-left: var(--rail-w-pinned); } .vt-fly-list > li + li { margin-top: 1px; } .vt-fly-link { display: flex; align-items: center; gap: var(--sp-2); - min-height: 34px; padding: 6px 10px; + min-height: var(--ctl-h); padding: 6px 10px; border-radius: var(--r); color: var(--text); text-decoration: none !important; @@ -475,16 +481,49 @@ html[data-rail="pinned"] .vt-frame { margin-left: var(--rail-w-pinned); } .vt-crumbs { display: flex; align-items: center; gap: 6px; - min-width: 0; flex: 1 1 auto; + min-width: 0; flex: 1 .001 auto; /* gives way last: see .vt-top-actions */ + align-self: stretch; /* the bar's height: room for focus rings */ + margin-inline: -8px; padding-inline: 8px; /* room for the switch's ring (it sits 6px out) */ font-size: var(--fs-base); white-space: nowrap; overflow: hidden; } .vt-crumb { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; text-decoration: none !important; } +/* a link crumb is a control high to click; the padding adds no height to the bar */ +a.vt-crumb { padding-block: calc((var(--ctl-h) - 1.5em) / 2); } a.vt-crumb:hover { color: var(--text); } .vt-crumb-current { color: var(--text); font-weight: 600; } .vt-crumb-sep { color: var(--text-3); flex: none; font-family: var(--mono); font-size: var(--fs-sm); } +/* the current crumb of a page with page tabs: opens its sibling pages. + A control high; the ring sits on its edge, inside the crumbs' clip. */ +.vt-crumb-switch { + display: inline-flex; align-items: center; gap: 4px; + min-width: 0; min-height: var(--ctl-h); margin: 0 -6px; padding: 0 6px; + border: 0; border-radius: var(--r); + background: none; + font: inherit; font-weight: 600; color: var(--text); + cursor: pointer; +} +.vt-crumb-switch > span { overflow: hidden; text-overflow: ellipsis; } +.vt-crumb-switch > svg { flex: none; width: 14px; height: 14px; color: var(--text-3); transition: transform .15s; } +.vt-crumb-switch:hover, .vt-crumb-switch[aria-expanded="true"] { background: var(--s3); } +.vt-crumb-switch:focus-visible { outline-offset: 0; } +.vt-crumb-switch[aria-expanded="true"] > svg { transform: rotate(90deg); } +@media (width > 720px) { + /* the flyout as a dropdown under the crumb (left: set from the crumb) */ + .vt-flyout.is-switch { + top: calc(var(--top-h) - 4px); bottom: auto; left: var(--x, var(--rail-w)); + width: auto; min-width: 240px; max-width: 320px; + max-height: calc(100vh - var(--top-h) - var(--sp-4)); + border: 1px solid var(--line-2); border-radius: var(--r-lg); + animation: vt-drop-in .12s ease-out; + } +} +@keyframes vt-drop-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } } -.vt-top-actions { display: flex; align-items: center; gap: var(--sp-2); flex: none; } +/* when the bar is short of room, the host chip gives way first, then the + search field's label, and only then the crumbs */ +.vt-top-actions { display: flex; align-items: center; gap: var(--sp-2); flex: 0 1 auto; min-width: 0; } +.vt-top-actions > :not(.vt-host, .vt-search) { flex: none; } .vt-search { display: inline-flex; align-items: center; gap: var(--sp-2); @@ -522,6 +561,7 @@ a.vt-crumb:hover { color: var(--text); } .vt-host { display: inline-flex; align-items: center; gap: 6px; height: 28px; max-width: 300px; + flex: 0 1000 auto; min-width: 0; overflow: hidden; padding: 0 10px; border: 1px solid var(--line); border-radius: 999px; background: var(--s1); @@ -547,18 +587,30 @@ a.vt-crumb:hover { color: var(--text); } } #indicators > span[data-clickable] { cursor: pointer; } #indicators > span[data-clickable]:hover { border-color: var(--text-3); color: var(--text); } +#indicators > span[data-indicator="poll-status"][data-style="active"] { position: relative; isolation: isolate; } #indicators > span[data-indicator="poll-status"][data-style="active"]::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-text); - box-shadow: 0 0 0 0 var(--accent-line); +} +/* the pulse is a ring of its own that scales and fades: the compositor + runs it, where an animated box-shadow repainted the page every frame. + It sits behind the dot (z-index -1 in the pill's isolated stack), so + the dot never lightens, and is invisible unless animated: with reduced + motion the one near-instant run ends on opacity 0. */ +#indicators > span[data-indicator="poll-status"][data-style="active"]::after { + content: ""; position: absolute; z-index: -1; left: 10px; top: calc(50% - 3.5px); + width: 7px; height: 7px; border-radius: 50%; + background: var(--accent-line); + opacity: 0; + pointer-events: none; animation: vt-live 2s ease-out infinite; } #indicators > span[data-indicator="poll-status"][data-style="inactive"]::before { content: ""; width: 7px; height: 7px; border-radius: 1px; background: var(--text-3); } @keyframes vt-live { - 0% { box-shadow: 0 0 0 0 var(--accent-line); } - 70%, 100% { box-shadow: 0 0 0 6px transparent; } + 0% { transform: scale(1); opacity: 1; } + 70%, 100% { transform: scale(2.7); opacity: 0; } } /* "Unsaved Changes: N" -> label + prominent count (text node stays for LuCI) */ #indicators > span[data-indicator][data-count] { @@ -580,35 +632,14 @@ a.vt-crumb:hover { color: var(--text); } } #indicators > span[data-indicator="media_error"] { color: var(--err); border-color: var(--err); background: var(--err-soft); } -/* level-3 page tabs */ +/* level-3 page tabs: on the content column, drawn as every other tab + level (.vt-pagetabs shares the .cbi-tabmenu rules below). They scroll with + the page; the current crumb in the top bar switches between them. */ .vt-tabs { - position: sticky; top: var(--top-h); z-index: 190; - display: flex; gap: var(--sp-1); - padding: 0 var(--sp-5); - background: color-mix(in srgb, var(--bg) 92%, transparent); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border-bottom: 1px solid var(--line); - overflow-x: auto; - scrollbar-width: none; -} -.vt-tabs::-webkit-scrollbar { display: none; } -.vt-tab { - position: relative; - display: inline-flex; align-items: center; - height: var(--ctl-h); padding: 0 var(--sp-3); - color: var(--text-2); - font-weight: 500; - white-space: nowrap; - text-decoration: none !important; -} -.vt-tab:hover { color: var(--text); } -.vt-tab[aria-current] { color: var(--text); font-weight: 600; } -.vt-tab[aria-current]::after { - content: ""; position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: -1px; - height: 2px; border-radius: 2px 2px 0 0; - background: var(--accent-text); + width: 100%; max-width: calc(var(--content-max) + 2 * var(--sp-6)); + margin: 0 auto; padding: var(--sp-6) var(--sp-6) 0; } +.vt-tabs:not([hidden]) + .vt-main { padding-top: var(--sp-5); } /* content column */ .vt-main { @@ -712,7 +743,6 @@ a.vt-crumb:hover { color: var(--text); } /* page = title block + a stack of cards. Cards carry a full-bleed header row (h3) and full-bleed tables; nested sections lose their chrome. */ :root { --card-px: 20px; --card-py: 16px; --stick-top: var(--top-h); } -html:has(.vt-tabs:not([hidden])) { --stick-top: calc(var(--top-h) + var(--ctl-h) + 1px); } .cbi-map { margin-bottom: var(--sp-6); } .cbi-map > h2, .vt-main > h2, #view > h2, #view > div > h2:first-child, .cbi-map > h2:first-child { @@ -884,6 +914,8 @@ legend { font-size: var(--fs-md); font-weight: 600; padding: 0; margin-bottom: v .cbi-section-create > * { margin: 0; } .cbi-section-create > .cbi-section-create-name { max-width: 240px; } .cbi-section-remove { text-align: right; padding: var(--sp-2) 0; } +/* several nodes of one typed section in a card: a rule between them */ +.cbi-section-node + :is(.cbi-section-node, .cbi-section-remove) { margin-top: var(--sp-5); padding-top: var(--sp-5); border-top: 1px solid var(--line); } /* page actions: a floating command bar docked to the bottom right */ .cbi-page-actions { @@ -905,6 +937,13 @@ legend { font-size: var(--fs-md); font-weight: 600; padding: 0; margin-bottom: v .cbi-page-actions .cbi-button-save { order: 2; } .cbi-page-actions .cbi-button-apply, .cbi-page-actions .cbi-dropdown { order: 3; } .cbi-page-actions:empty { display: none; } +/* the bar sits at the bottom of the view (docked, or the page's end), so + its Save & Apply menu opens upwards; LuCI's touch branch would open it + down, under the phone's bottom bar and past the screen */ +.cbi-page-actions .cbi-dropdown[open] > ul.dropdown { + top: auto !important; bottom: 100% !important; + max-height: 50vh !important; +} /* ------------------------------------------------------------ 6. widgets */ @@ -929,6 +968,8 @@ input, select, textarea, button { font: inherit; color: inherit; } transition: border-color .12s, box-shadow .12s; } .vt-main textarea, .cbi-input-textarea, .modal textarea { max-width: calc(2 * var(--field-max)); min-height: 88px; font-family: var(--mono); font-size: var(--fs-sm); resize: vertical; } +/* read-only text (System and Kernel log) is a viewer, not a field: full width */ +.vt-main textarea[readonly] { max-width: none; } .vt-main select, .cbi-input-select, .modal select { appearance: none; -webkit-appearance: none; padding-right: 30px; @@ -953,6 +994,15 @@ input:focus, select:focus, textarea:focus { box-shadow: 0 0 0 3px var(--accent-soft); } input:focus-visible, select:focus-visible, textarea:focus-visible { outline: none; } +/* fields in pages, modals and the login: the rules above lose to the field + rules' (0,2,1), so hover and focus are restated at their weight. Focus: + a --focus border plus a 1px ring of it (>= 3:1 against the field and + the card in both modes) and the soft halo. The dashboard styles its own. */ +:is(input, select, textarea):not([type="checkbox"], [type="radio"], [type="range"], .vt-pal-input, .vt-app *):hover { border-color: var(--text-3); } +:is(input, select, textarea):not([type="checkbox"], [type="radio"], [type="range"], .vt-pal-input, .vt-app *):focus { + border-color: var(--focus); + box-shadow: 0 0 0 1px var(--focus), 0 0 0 4px var(--accent-soft); +} input[disabled], select[disabled], textarea[disabled], input[readonly] { opacity: .6; cursor: not-allowed; } input[readonly] { cursor: default; } @@ -975,6 +1025,9 @@ input[readonly] { cursor: default; } .control-group > :not(:last-child) { border-top-right-radius: 0 !important; border-bottom-right-radius: 0 !important; } .control-group > :not(:first-child) { border-top-left-radius: 0 !important; border-bottom-left-radius: 0 !important; } .control-group > .btn, .control-group > .cbi-button { flex: none; } +/* in LuCI's unclassed field wrapper (a flex row) the group sized to its + content, 254px for a password with its reveal button: as wide as a field */ +.cbi-value-field > div:not([class]) > .control-group { flex: 1 1 auto; } /* checkbox */ .cbi-checkbox { display: inline-flex; align-items: center; min-height: var(--ctl-h); gap: var(--sp-2); } @@ -1054,7 +1107,7 @@ input[type="button"], input[type="reset"], input[type="submit"]) { vertical-align: middle; } .cbi-dropdown:hover { border-color: var(--text-3); } -.cbi-dropdown:focus, .cbi-dropdown[open] { border-color: var(--accent-line); box-shadow: 0 0 0 3px var(--accent-soft); } +.cbi-dropdown:focus, .cbi-dropdown[open] { border-color: var(--focus); box-shadow: 0 0 0 1px var(--focus), 0 0 0 4px var(--accent-soft); } .cbi-dropdown:focus-visible { outline: none; } .cbi-dropdown[disabled] { opacity: .55; pointer-events: none; } @@ -1229,15 +1282,15 @@ input[type="button"], input[type="reset"], input[type="submit"]) { } .cbi-value-field :is(.btn, .cbi-button).cbi-button-apply:hover, .td :is(.btn, .cbi-button).cbi-button-apply:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); color: var(--accent-text); } -:is(.btn, .cbi-button).cbi-button-positive:not(.important) { color: var(--ok); } +:is(.btn, .cbi-button).cbi-button-positive:not(.important) { color: var(--ok-text); } :is(.btn, .cbi-button, .cbi-dropdown):is(.cbi-button-negative, .cbi-button-remove) { background: transparent; color: var(--err); border-color: color-mix(in srgb, var(--err) 40%, var(--line-2)); } :is(.btn, .cbi-button, .cbi-dropdown):is(.cbi-button-negative, .cbi-button-remove):hover { background: var(--err-soft); border-color: var(--err); } -:is(.btn, .cbi-button, .cbi-dropdown).cbi-button-negative.important { background: var(--err); border-color: var(--err); color: #fff; } -:is(.btn, .cbi-button, .cbi-dropdown).cbi-button-negative.important:hover { background: color-mix(in srgb, var(--err) 85%, #000); color: #fff; } +:is(.btn, .cbi-button, .cbi-dropdown).cbi-button-negative.important { background: var(--err); border-color: var(--err); color: var(--err-ink); } +:is(.btn, .cbi-button, .cbi-dropdown).cbi-button-negative.important:hover { background: color-mix(in srgb, var(--err) 88%, var(--err-ink)); color: var(--err-ink); } :is(.btn, .cbi-button).cbi-button-reset { background: transparent; color: var(--text-2); } :is(.btn, .cbi-button).cbi-button-reset:hover { color: var(--text); } @@ -1397,119 +1450,45 @@ 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; } -/* Tabs inside a page (ui.tabs), one look per level: - - tabs standing at page level (not inside a form section or another - tab) are the page's sub-pages when the menu shows no page tabs: they - take the page-tab underline, like .vt-tabs (Interfaces / Devices / - ..., DHCP's Leases / dnsmasq, Channel Analysis's radios). Under - .vt-tabs they are the first level inside the page instead; - - the first level inside a page, form or modal: a segmented control; - - tabs inside a tab: a side list where the pane is wide enough - (container query on the pane, so modals and pages alike), chips - where it is not and at every deeper level. - A level is a pane of a section's option tabs (.cbi-section-node-tabbed); - map-level panes are not counted, so a page's sub-pages add no level. */ -.cbi-tabmenu { - display: flex; flex-wrap: nowrap; align-items: center; gap: 2px; +/* 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, + and tabs inside a tab. Nesting shows by where a control sits, not by + another look. It wraps its segments instead of hiding them. Every + segment is a whole control high (--ctl-h, 36px / 44px on touch) to + click or tap: the 3px between the track's edge and the drawn segment + is the segment's own transparent border, so the track is one control + high as well. */ +.cbi-tabmenu, .vt-tabs > .vt-pagetabs { + display: flex; flex-wrap: wrap; align-items: stretch; width: fit-content; max-width: 100%; min-height: var(--ctl-h); - margin: 0 0 var(--sp-5); padding: 2px; + margin: 0 0 var(--sp-5); padding: 0; list-style: none; background: var(--seg-bg); - border: 1px solid var(--line); border-radius: var(--r-lg); - overflow-x: auto; - scrollbar-width: none; + box-shadow: inset 0 0 0 1px var(--line); } -.cbi-tabmenu::-webkit-scrollbar { display: none; } -.cbi-tabmenu > li { position: relative; flex: none; } -.cbi-tabmenu > li > a { +.vt-tabs > .vt-pagetabs { margin: 0; } +.cbi-tabmenu > li { position: relative; flex: none; display: flex; max-width: 100%; } +.cbi-tabmenu > li > a, .vt-pagetabs > .vt-tab { + flex: 1 1 auto; display: flex; align-items: center; gap: 6px; - height: calc(var(--ctl-h) - 6px); padding: 0 var(--sp-3); - border-radius: var(--r); + min-height: var(--ctl-h); padding: 0 10px; + border: 3px solid transparent; background-clip: padding-box; + border-radius: calc(var(--r) + 3px); color: var(--text-2); font-weight: 500; white-space: nowrap; text-decoration: none !important; } -.cbi-tabmenu > li > a:hover { color: var(--text); } -.cbi-tabmenu > li.cbi-tab > a { background: var(--seg-on); color: var(--text); font-weight: 600; box-shadow: var(--seg-on-ring); } - -html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) { - gap: var(--sp-1); - width: auto; padding: 0; - background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; -} -html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li > a { height: var(--ctl-h); border-radius: 0; } -html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li.cbi-tab > a { background: none; box-shadow: none; } -html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li.cbi-tab::after { - content: ""; position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: 0; - height: 2px; background: var(--accent-text); border-radius: 2px 2px 0 0; -} - -.cbi-section-node-tabbed > [data-tab-title] { container-type: inline-size; } -.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu { - flex-wrap: wrap; gap: var(--sp-2); - width: auto; margin-bottom: var(--sp-4); padding: 0; - background: none; border: 0; border-radius: 0; - overflow: visible; -} -.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a { - height: var(--ctl-h); padding: 0 14px; - border: 1px solid var(--line-2); border-radius: 999px; -} -.cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li.cbi-tab > a { - background: var(--accent-soft); border-color: var(--accent-line); - color: var(--accent-text); box-shadow: none; -} -@container (min-width: 640px) { - .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) { - display: grid; - grid-template-columns: 168px minmax(0, 1fr); - column-gap: var(--sp-6); - align-items: start; - } - .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > * { grid-column: 1 / -1; } - .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * { grid-column: 2; } - .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu { - grid-column: 1; - flex-direction: column; flex-wrap: nowrap; align-items: stretch; gap: 2px; - margin: 0; - } - .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a { - justify-content: space-between; - padding: 0 var(--sp-3); - border: 0; border-radius: var(--r); - white-space: normal; line-height: 1.25; - } - .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a:hover { background: var(--s3); } - .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li.cbi-tab > a { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent-line); } - /* beside the list there is no card edge to bleed to: table sections and - tables fit the column */ - .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .cbi-tblsection { margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; } - .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .table { width: 100% !important; margin-left: 0; margin-right: 0; } - .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .table .tr > :is(.th, .td):first-child { padding-left: 0; } - .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) > .cbi-tabmenu + * .table .tr > :is(.th, .td):last-child { padding-right: 0; } - /* deeper levels: back to the chips above */ - .cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] :has(> .cbi-tabmenu) { display: block; } - .cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu { - flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--sp-2); - margin: 0 0 var(--sp-4); - } - .cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a { - justify-content: normal; - padding: 0 14px; - border: 1px solid var(--line-2); border-radius: 999px; - white-space: nowrap; line-height: inherit; - } - .cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li > a:hover { background: none; } - .cbi-section-node-tabbed > [data-tab-title] .cbi-section-node-tabbed > [data-tab-title] .cbi-tabmenu > li.cbi-tab > a { border-color: var(--accent-line); box-shadow: none; } -} +:is(.cbi-tabmenu > li > a, .vt-pagetabs > .vt-tab):hover { color: var(--text); } +:is(.cbi-tabmenu > li > a, .vt-pagetabs > .vt-tab):focus-visible { outline-offset: -1px; border-radius: calc(var(--r) + 3px); } +.cbi-tabmenu > li.cbi-tab > a, .vt-pagetabs > .vt-tab[aria-current] { background-color: var(--seg-on); color: var(--text); font-weight: 600; box-shadow: var(--seg-on-ring); } .cbi-tabmenu > li[data-errors] > a::after { content: attr(data-errors); display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px; - background: var(--err); color: #fff; + background: var(--err); color: var(--err-ink); font: 700 var(--fs-micro)/1 var(--mono); } .cbi-tabmenu > li.cbi-tab-disabled[data-errors] > a { color: var(--err); } @@ -1554,21 +1533,27 @@ html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [d /* "Label: value
" dumps (LuCI itemlist) -> aligned label/value grid. Each span.nowrap becomes a subgrid row: the is the label cell, the contiguous value run the anonymous value cell. */ +/* LuCI's " | " separators between the items are bare text, so each would + be a grid row of its own: no text size, line or row gap for them; the + items keep their own size and space */ :is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) { display: grid; grid-template-columns: max-content minmax(0, 1fr); - column-gap: 14px; row-gap: 3px; + column-gap: 14px; row-gap: 0; align-items: baseline; min-width: 0; white-space: normal; text-align: left; + font-size: 0; line-height: 0; } :is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) > br { display: none; } +:is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) > :not(.nowrap) { font-size: var(--fs-sm); line-height: 1.55; } :is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) > .nowrap { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; align-items: baseline; min-width: 0; + margin-block: 1.5px; white-space: normal; overflow-wrap: anywhere; color: var(--text); font: 400 var(--fs-sm)/1.55 var(--mono); @@ -1680,7 +1665,8 @@ html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [d .zonebadge[style*="--zone-color-rgb"] { box-shadow: inset 3px 0 0 rgb(var(--zone-color-rgb)); padding-left: 10px; } .ifacebox-head[style*="--zone-color-rgb"] { box-shadow: inset 0 3px 0 rgb(var(--zone-color-rgb)); border-bottom-color: rgba(var(--zone-color-rgb), .4) !important; } .ifacebox-head[style*="background-color:#"], .ifacebox-head[style*="background-color: #"] { color: #111 !important; } -.zonebadge > em { font-style: normal; color: var(--text-3); } +/* muted, but on the zone tint: --text-3 fell to 3.1:1 there in dark mode */ +.zonebadge > em { font-style: normal; color: color-mix(in srgb, var(--text) 78%, transparent); } .zonebadge .ifacebadge { padding: 0 4px; border: 0; background: transparent; } .zone-forwards { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; } .zone-forwards > span { color: var(--text-3); } @@ -1774,9 +1760,9 @@ html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [d transition: opacity .15s; pointer-events: none; } -.cbi-tooltip.error { background: var(--err); border-color: var(--err); color: #fff; } +.cbi-tooltip.error { background: var(--err); border-color: var(--err); color: var(--err-ink); } .cbi-tooltip.warning { background: var(--warn); border-color: var(--warn); color: #1a1200; } -.cbi-tooltip.info { background: var(--info); border-color: var(--info); color: #fff; } +.cbi-tooltip.info { background: var(--info); border-color: var(--info); color: var(--info-ink); } .cbi-tooltip-container { position: relative; cursor: help; } .cbi-tooltip-container > .cbi-tooltip { left: -10000px; top: auto; margin-top: 6px; white-space: normal; } .cbi-tooltip-container:hover > .cbi-tooltip { left: 0; top: 100%; opacity: 1; } @@ -1813,6 +1799,10 @@ body.modal-overlay-active #modal_overlay { display: block; } } #modal_overlay > .modal.cbi-modal { width: min(960px, 100%); } #modal_overlay > .modal .cbi-section { background: none; border: 0; padding: 0; margin: 0; box-shadow: none; } +/* a modal's sections have no card padding, so nothing in them bleeds out + by it: tables keep the modal's width (a bleed scrolled the section + sideways) and their first and last cells line up with the fields */ +#modal_overlay > .modal .cbi-section { --card-px: 0px; --card-py: 0px; } #modal_overlay > .modal .cbi-section > h3:first-child { margin: 0 0 var(--sp-3); padding: 0; min-height: 0; border: 0; } .uci-dialog > .cbi-section > br, .uci-change-list > br:last-child { display: none; } .uci-dialog > .cbi-section > strong { display: block; margin-bottom: var(--sp-2); color: var(--text-3); font-size: var(--fs-micro); letter-spacing: .08em; text-transform: uppercase; } @@ -2215,7 +2205,12 @@ html:not([data-darkmode="true"]) .vt-login-page::before { color: var(--text-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; } -.vt-live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-text); animation: vt-live 2s ease-out infinite; } +.vt-login-sub { isolation: isolate; } +/* the ring as on the poll pill: behind the dot (the dot itself makes no + stacking context, so z-index -1 lands under its background), opacity 0 + unless animated */ +.vt-login-dot { position: relative; width: 6px; height: 6px; border-radius: 50%; background: var(--accent-text); } +.vt-login-dot::after { content: ""; position: absolute; z-index: -1; inset: 0; border-radius: 50%; background: var(--accent-line); opacity: 0; animation: vt-live 2s ease-out infinite; } .vt-login-error { margin: 0 0 var(--sp-4); padding: var(--sp-2) var(--sp-3); @@ -2229,7 +2224,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before { .vt-field { display: flex; flex-direction: column; gap: 6px; } .vt-field-label { color: var(--text-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; } .vt-field input { - width: 100%; height: 38px; + width: 100%; height: var(--ctl-h); padding: 0 12px; background: var(--s2); color: var(--text); @@ -2238,7 +2233,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before { font-size: var(--fs-md); } .vt-field input[name="luci_username"] { font-family: var(--mono); } -.vt-login-submit { height: 38px; margin-top: var(--sp-2); font-size: var(--fs-md) !important; width: 100%; } +.vt-login-submit { height: var(--ctl-h); margin-top: var(--sp-2); font-size: var(--fs-md) !important; width: 100%; } .vt-login-foot { margin: var(--sp-4) 0 0; text-align: center; color: var(--text-3); font: var(--fs-micro)/1.4 var(--mono); letter-spacing: .03em; } /* ------------------------------------------------ 9. stock page layouts */ @@ -2359,9 +2354,19 @@ html:not([data-darkmode="true"]) .vt-login-page::before { a non-scrolling ancestor, so status tables only scroll when narrow. */ .cbi-tblsection, .cbi-section:has(> .cbi-tblsection) { overflow-x: auto; } .cbi-section:has(.cbi-dropdown[open]), .cbi-tblsection:has(.cbi-dropdown[open]) { overflow: visible; } -@media (max-width: 1180px) and (min-width: 721px) { +@media (720px < width <= 1180px) { .cbi-section { overflow-x: auto; } .cbi-section:has(.cbi-dropdown[open]) { overflow: visible; } + /* a free-standing table card (Processes, Wireless's stations) has no + section to scroll in: the card itself scrolls, its rows held in one + table box (LuCI puts every row, titles too, in the tbody, so the + columns stay aligned), its header static as in sections */ + :is(#view, #view > div, .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:not(:has(> thead)) { + display: block; overflow-x: auto; + } + :is(#view, #view > div, .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:not(:has(> thead)) > tbody { display: table; width: 100%; } + :is(#view, #view > div, .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:has(.cbi-dropdown[open]) { overflow: visible; } + :is(#view, #view > div, .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table .tr > .th.th { position: static; } } @@ -2370,6 +2375,18 @@ html:not([data-darkmode="true"]) .vt-login-page::before { .cbi-tblsection .table .tr > .th.th, .cbi-section:has(> .cbi-tblsection) .table .tr > .th.th { position: static; } @media (max-width: 1180px) { .cbi-section .table .tr > .th.th { position: static; } } +/* add-ons: adblock and banIP inject status palettes keyed on the OS + colour scheme (@media prefers-color-scheme), so with the theme toggle + set the other way their text fell to 1.7-3:1. Their own two palettes, + picked by the toggle instead (html[data-darkmode] outweighs their #id + rules); nothing else of theirs is touched. */ +html[data-darkmode="false"] #adb-status { --adb-ok: #1f8a5f; --adb-warn: #a8760a; --adb-err: #c0392b; --adb-info: #2f6fb0; } +html[data-darkmode="true"] #adb-status { --adb-ok: #63c79b; --adb-warn: #d9ab4e; --adb-err: #e8897e; --adb-info: #7fb3e8; } +html[data-darkmode="false"] #adb-dash { --adb-blocked: #c0392b; --adb-allowed: #1f8a5f; --adb-neutral: #2f6fb0; --adb-warn: #a8760a; } +html[data-darkmode="true"] #adb-dash { --adb-blocked: #e8897e; --adb-allowed: #63c79b; --adb-neutral: #7fb3e8; --adb-warn: #d9ab4e; } +html[data-darkmode="false"] :is(#ban-status, #ban-report) { --ban-ok: #1f8a5f; --ban-err: #c0392b; --ban-info: #2f6fb0; } +html[data-darkmode="true"] :is(#ban-status, #ban-report) { --ban-ok: #63c79b; --ban-err: #e8897e; --ban-info: #7fb3e8; } + /* ----------------------------------------------------- responsive */ @media (max-width: 1100px) { @@ -2380,6 +2397,13 @@ html:not([data-darkmode="true"]) .vt-login-page::before { @media (max-width: 900px) { .vt-host { display: none !important; } + /* indicators go compact (dot, count) before the crumbs get cut; the + label stays as text for screen readers and as the title */ + #indicators > span[data-indicator] { height: 28px; padding: 0 8px; } + #indicators > span[data-indicator="poll-status"] { font-size: 0; gap: 0; width: 28px; justify-content: center; padding: 0; } + #indicators > span[data-indicator="poll-status"][data-style="active"]::after { left: calc(50% - 3.5px); } + #indicators > span[data-indicator][data-count] { padding: 0 3px; gap: 0; } + #indicators > span[data-indicator][data-count]::before { content: none; } html[data-rail="pinned"] .vt-rail { width: var(--rail-w); } html[data-rail="pinned"] .vt-frame { margin-left: var(--rail-w); } html[data-rail="pinned"] body.vt-auth .vt-shell::before { width: var(--rail-w); } @@ -2405,22 +2429,29 @@ html:not([data-darkmode="true"]) .vt-login-page::before { .vt-rail > .vt-brand, .vt-rail-foot { display: none; } body.vt-auth .vt-shell::before { display: none; } .vt-rail-list { - flex-direction: row; justify-content: space-around; + flex-direction: row; justify-content: flex-start; gap: 0; padding: 0 var(--sp-1); overflow-x: auto; overflow-y: hidden; } - .vt-rail-item { flex: 1 0 60px; padding: 6px 2px; } + /* the buttons stay a centred group on wider screens. Centred with auto + margins, not justify-content: center, which would push an overflowing + list's first items past the scroll start where nothing reaches them; + auto margins drop to 0 when the list overflows. */ + .vt-rail-item { flex: 1 0 60px; max-width: 104px; padding: 6px 2px; } + .vt-rail-item:first-child { margin-inline-start: auto; } + .vt-rail-item:last-child { margin-inline-end: auto; } .vt-rail-link, html[data-rail="pinned"] .vt-rail-link { flex-direction: column; justify-content: center; gap: 3px; height: 48px; padding: 0 4px; } .vt-rail-link .vt-rail-label, html[data-rail="pinned"] .vt-rail-link .vt-rail-label { display: block !important; + flex: none; /* the rail's label grows; here it would push the icon up */ max-width: 100%; font-size: var(--fs-micro); font-weight: 500; text-align: center; } - .vt-rail-item.is-active > .vt-rail-link::before { left: 14px; right: 14px; top: -6px; bottom: auto; width: auto; height: 2px; border-radius: 0 0 2px 2px; } + .vt-rail-item.is-active > .vt-rail-link::before { left: calc(50% - 10px); top: -6px; bottom: auto; width: 20px; height: 3px; border-radius: 0 0 3px 3px; } .vt-frame, html[data-rail="pinned"] .vt-frame { margin-left: 0; padding-bottom: calc(var(--bar-h) + env(safe-area-inset-bottom)); } .vt-flyout { @@ -2447,11 +2478,8 @@ html:not([data-darkmode="true"]) .vt-login-page::before { .vt-crumbs .vt-crumb-current { font-size: var(--fs-md); } .vt-top-actions { gap: 2px; } .vt-search { border-color: transparent; background: none; color: var(--text-2); width: 32px; height: 32px; } - #indicators > span[data-indicator] { height: 28px; padding: 0 8px; } - #indicators > span[data-indicator="poll-status"] { font-size: 0; gap: 0; width: 28px; justify-content: center; padding: 0; } - #indicators > span[data-indicator][data-count] { padding: 0 3px; gap: 0; } - #indicators > span[data-indicator][data-count]::before { content: none; } - .vt-tabs { padding: 0 var(--sp-2); } + .vt-tabs { padding: var(--sp-4) var(--sp-4) 0; } + .vt-tabs:not([hidden]) + .vt-main { padding-top: var(--sp-4); } .vt-main { padding: var(--sp-4); } .vt-foot { padding-bottom: var(--sp-4); } @@ -2462,7 +2490,14 @@ html:not([data-darkmode="true"]) .vt-login-page::before { width: auto; margin: var(--sp-4) 0 0; } - .cbi-page-actions > * { flex: 1 1 auto; } + /* one row: the buttons share the bar from a zero basis, so the row only + breaks when their longest words no longer fit, and a long label + wraps inside its button instead of taking the bar to a second row */ + .cbi-page-actions > *, .cbi-page-actions > form > * { flex: 1 1 0; } + .cbi-page-actions :is(.btn, .cbi-button, button, input[type="button"], input[type="submit"]):not(.cbi-dropdown) { + padding: 4px 8px; + white-space: normal; text-align: center; line-height: 1.15; + } .cbi-section { overflow-x: visible; } .cbi-tblsection, .cbi-section:has(> .cbi-tblsection) { overflow-x: visible; } .network-status-table .ifacebox-head, .network-status-table .ifacebox-body { padding-left: 12px; padding-right: 12px; } @@ -2490,38 +2525,36 @@ html:not([data-darkmode="true"]) .vt-login-page::before { .cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-title { grid-row: 1; grid-column: 2; padding: 0; } .cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .vt-help { grid-row: 1; grid-column: 3; } .cbi-value:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field > :not(.cbi-checkbox) { grid-column: 1 / -1; } - /* a segmented control wraps its segments instead of hiding them */ - .cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) { flex-wrap: wrap; width: auto; } - .cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) > li { flex: 1 1 auto; } - .cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) > li > a { justify-content: center; } - /* ... while an underline strip scrolls, like the page tabs */ - html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) { flex-wrap: nowrap; } - html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li { flex: none; } - html:not(:has(.vt-tabs:not([hidden]))) #view .cbi-tabmenu:not(.cbi-section *, [data-tab-title] *) > li > a { justify-content: normal; } + /* a segmented control spans the width and its segments share each row */ + .cbi-tabmenu, .vt-tabs > .vt-pagetabs { width: auto; } + .cbi-tabmenu > li, .vt-pagetabs > .vt-tab { flex: 1 1 auto; } + .cbi-tabmenu > li > a, .vt-pagetabs > .vt-tab { justify-content: center; } .cbi-dropdown, .vt-main input, .vt-main select, .control-group, .cbi-dynlist { max-width: none; } - /* tables become cards */ + /* tables become cards, in pages and in modals alike. Header rows go + (LuCI also puts .cbi-section-table-titles on each row's name cell, + which stays); every cell drops the fixed row height. */ .hide-xs { display: none !important; } - .vt-main .table, .vt-main .table > tbody, .vt-main .table > thead, .modal .table, .modal .table > tbody { display: block; width: 100%; } - .vt-main .table > .tr.table-titles, .vt-main .table > thead > .tr.table-titles, .vt-main .table > tbody > .tr.table-titles, - .vt-main .cbi-section-table-titles, .vt-main .cbi-section-table-descr, .modal .table-titles { display: none !important; } - .vt-main .table .tr, .modal .table .tr { + :is(.vt-main, .modal) .table, :is(.vt-main, .modal) .table > :is(tbody, thead) { display: block; width: 100%; } + :is(.vt-main, .modal) .table :is(tr, .tr):is(.table-titles, .cbi-section-table-titles, .cbi-section-table-descr) { display: none !important; } + :is(.vt-main, .modal) .table .tr { display: flex; flex-wrap: wrap; padding: 6px 0; border-bottom: 1px solid var(--line); } - .vt-main .table .tr:last-child { border-bottom: 0; } - .vt-main .table .tr > .td, .modal .table .tr > .td { + :is(.vt-main, .modal) .table .tr:last-child { border-bottom: 0; } + :is(.vt-main, .modal) .table .tr > .td { display: block; flex: 1 1 100%; min-width: 0; width: auto !important; + height: auto; padding: 4px 10px; border: 0; text-align: left !important; } - .vt-main :is(.cbi-section, .cbi-value) .table .tr > .td:nth-child(n) { padding: 4px var(--card-px); height: auto; } - .vt-main .table .tr > .td[data-title]::before, .modal .table .tr > .td[data-title]::before { + :is(.vt-main, .modal) :is(.cbi-section, .cbi-value) .table .tr > .td:nth-child(n) { padding: 4px var(--card-px); } + :is(.vt-main, .modal) .table .tr > .td[data-title]::before { content: attr(data-title); display: block; margin-bottom: 2px; @@ -2529,18 +2562,20 @@ html:not([data-darkmode="true"]) .vt-login-page::before { color: var(--text-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; } - .vt-main .table:not(:has(.table-titles)) .tr > .td:first-child:nth-last-child(2) { + :is(.vt-main, .modal) .table:not(:has(.table-titles)) .tr > .td:first-child:nth-last-child(2) { width: auto !important; padding-bottom: 0; color: var(--text-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; } - .vt-main .table .tr > .td[data-title]:not(:only-child) { flex-basis: 50%; } - .vt-main .table .tr > .td[data-title]:last-child:nth-child(odd) { flex-basis: 100%; } - .vt-main .table .tr > .td.cbi-section-actions { flex-basis: 100%; text-align: left !important; } - .vt-main .table .tr > .td[data-name="_ifacebox"], .vt-main .table .tr > .td[data-name="_ifacestat"] { flex-basis: 100%; } + :is(.vt-main, .modal) .table .tr > .td[data-title]:not(:only-child) { flex-basis: 50%; } + :is(.vt-main, .modal) .table .tr > .td[data-title]:last-child:nth-child(odd) { flex-basis: 100%; } + :is(.vt-main, .modal) .table .tr > .td.cbi-section-actions { flex-basis: 100%; text-align: left !important; } + /* a named grid section's name cell heads its card */ + :is(.vt-main, .modal) .table .tr > .td.cbi-section-table-titles { flex-basis: 100%; color: var(--text); font-weight: 600; } + :is(.vt-main, .modal) .table .tr > .td:is([data-name="_ifacebox"], [data-name="_ifacestat"]) { flex-basis: 100%; } .cbi-section-table .td[data-name="_ifacebox"] .ifacebox { width: 100%; } - .vt-main .table .tr > .td.cbi-section-actions > div { flex-wrap: wrap; } - .vt-main .table .tr > .td.cbi-section-actions::before { display: none !important; } - .vt-main .table .tr:hover > .td { background: none; } + :is(.vt-main, .modal) .table .tr > .td.cbi-section-actions > div { flex-wrap: wrap; } + :is(.vt-main, .modal) .table .tr > .td.cbi-section-actions::before { display: none !important; } + :is(.vt-main, .modal) .table .tr:hover > .td { background: none; } .cbi-section .table, .cbi-section .cbi-section-table { border: 0; } .cbi-progressbar { max-width: none; } .network-status-table > .ifacebox { max-width: none; } @@ -2563,6 +2598,10 @@ html:not([data-darkmode="true"]) .vt-login-page::before { } @media (pointer: coarse) { input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; } + /* top-bar buttons: a touch control high, at every width */ + .vt-icon-btn, .vt-search { min-width: var(--ctl-h); height: var(--ctl-h); } + /* dropdown choices (Save & Apply's too) are touch targets */ + .cbi-dropdown[open] > ul.dropdown > li { min-height: var(--ctl-h); } .cbi-value { padding: 8px 0; } } @@ -2592,6 +2631,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before { /* ui.Dropdown moves focus on transitionend: keep a (near-instant) transition */ .cbi-dropdown[open] > ul.dropdown { transition-duration: .01s; } .cbi-progressbar > div { transition: none; } + .vt-crumb-switch > svg, .vt-rail-caret, .vt-rail-pin svg { transition: none; } } @media print { diff --git a/luci-theme-vantage/po/templates/vantage-theme.pot b/luci-theme-vantage/po/templates/vantage-theme.pot index 2f3b41052..c3d91ef8e 100644 --- a/luci-theme-vantage/po/templates/vantage-theme.pot +++ b/luci-theme-vantage/po/templates/vantage-theme.pot @@ -1,13 +1,13 @@ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:951 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1083 msgid "Actions" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:947 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:948 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:949 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1079 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1080 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1081 msgid "Appearance" msgstr "" @@ -15,7 +15,7 @@ msgstr "" msgid "Authorization Required" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1125 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1260 msgid "Auto" msgstr "" @@ -23,52 +23,52 @@ msgstr "" msgid "Breadcrumb" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:269 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:277 msgid "Buffered" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:290 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:294 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:298 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:302 msgid "Cache" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:269 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:277 msgid "Cached" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:350 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:358 msgid "Channel" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:719 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:734 msgid "Collapse navigation" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1126 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1261 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:81 msgid "Colour theme" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:967 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1102 msgid "Command palette" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1125 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1260 msgid "Dark" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:719 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:734 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:54 msgid "Expand navigation" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:949 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1081 msgid "Follow system theme" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:281 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:295 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:303 msgid "Free" msgstr "" @@ -76,9 +76,9 @@ msgstr "" msgid "Home" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:293 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:297 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:301 msgid "In use" msgstr "" @@ -86,11 +86,11 @@ msgstr "" msgid "Invalid username and/or password! Please try again." msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:963 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1098 msgid "Jump to a page or action…" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1125 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1260 msgid "Light" msgstr "" @@ -99,7 +99,7 @@ msgstr "" msgid "Log in" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1082 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:84 msgid "Log out" msgstr "" @@ -108,15 +108,19 @@ msgstr "" msgid "Main navigation" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:264 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:279 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:272 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:287 msgid "Memory" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1038 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1173 msgid "No matching pages" msgstr "" +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1009 +msgid "Other pages in %s" +msgstr "" + #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:90 msgid "Page tabs" msgstr "" @@ -125,32 +129,33 @@ msgstr "" msgid "Password" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:825 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:883 msgid "Press" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:965 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1100 msgid "Results" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:963 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1098 +#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:72 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:74 msgid "Search pages" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1082 msgid "Session" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1283 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1418 msgid "Show full description" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1272 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1407 msgid "Show full help" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1272 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1407 msgid "Show full help: %s" msgstr "" @@ -158,36 +163,36 @@ msgstr "" msgid "Skip to content" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:316 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:324 msgid "Storage" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:302 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:304 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:310 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:312 msgid "Swap" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:269 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:277 msgid "Swap free" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:281 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289 msgid "Total" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:268 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:276 msgid "Total Available" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:947 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1079 msgid "Use dark theme" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:948 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1080 msgid "Use light theme" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:268 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:276 msgid "Used" msgstr "" @@ -195,54 +200,56 @@ msgstr "" msgid "Username" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:343 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:351 msgid "Wireless" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:285 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:293 msgid "available" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:973 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1108 msgid "close" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:331 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:334 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:339 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:342 msgid "free" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1040 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1175 msgid "items" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:971 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1106 msgid "navigate" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:972 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1107 msgid "open" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:805 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:863 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:937 msgid "page" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:805 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:863 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:937 msgid "pages" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:290 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:294 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:298 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:302 msgid "reclaimable" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:825 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:883 msgid "to search all pages" msgstr "" -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:331 -#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:334 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:339 +#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:342 msgid "used" msgstr "" diff --git a/luci-theme-vantage/ucode/template/themes/vantage/header.ut b/luci-theme-vantage/ucode/template/themes/vantage/header.ut index 4b74ca971..52f5f14c5 100644 --- a/luci-theme-vantage/ucode/template/themes/vantage/header.ut +++ b/luci-theme-vantage/ucode/template/themes/vantage/header.ut @@ -78,7 +78,7 @@
{% if (authed): %} -
diff --git a/mise/Makefile b/mise/Makefile index 0efdfd9de..89c6934cc 100644 --- a/mise/Makefile +++ b/mise/Makefile @@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264 MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437 PKG_NAME:=mise -PKG_VERSION:=2026.10.8 -PKG_RELEASE:=33 +PKG_VERSION:=2026.10.9 +PKG_RELEASE:=34 PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/ PKG_HASH:=skip diff --git a/mptcp/Makefile b/mptcp/Makefile index 1d8ff3eea..76d90ecca 100644 --- a/mptcp/Makefile +++ b/mptcp/Makefile @@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mptcp PKG_VERSION:=6.1 -PKG_RELEASE:=25 +PKG_RELEASE:=26 PKG_MAINTAINER:=Ycarus (Yannick Chabanois) PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME) diff --git a/mptcp/files/etc/init.d/mptcp b/mptcp/files/etc/init.d/mptcp index 9a4251eec..7bca7e414 100755 --- a/mptcp/files/etc/init.d/mptcp +++ b/mptcp/files/etc/init.d/mptcp @@ -37,6 +37,21 @@ _mptcp_limit() { esac } +# Persist the sysctl $1=$2 in zzz_openmptcprouter.conf. procd reloads mptcp on +# every interface event, and "sed -i" rewrote the file on flash each time, +# the value unchanged: it is only edited when a line of that key differs. +_sysctl_conf_set() { + local conf=/etc/sysctl.d/zzz_openmptcprouter.conf + if grep -q "^$1=" "$conf" 2>/dev/null && ! grep "^$1=" "$conf" | grep -qvx "$1=$2"; then + return 0 + fi + if grep -q "$1" "$conf" 2>/dev/null; then + sed -i "s:^$1=[0-1]*:$1=$2:" "$conf" + else + echo "$1=$2" >> "$conf" + fi +} + restart_scheduler_dependent_services() { local proxy vpn proxy="$(uci -q get openmptcprouter.settings.proxy)" @@ -145,11 +160,7 @@ global_multipath_settings() { # Global MPTCP configuration if [ -f /proc/sys/net/mptcp/mptcp_enabled ]; then sysctl -qew net.mptcp.mptcp_enabled="$multipath_status" - if grep -q net.mptcp.mptcp_enabled /etc/sysctl.d/zzz_openmptcprouter.conf; then - sed -i "s:^net.mptcp.mptcp_enabled=[0-1]*:net.mptcp.mptcp_enabled=${multipath_status}:" /etc/sysctl.d/zzz_openmptcprouter.conf - else - echo "net.mptcp.mptcp_enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf - fi + _sysctl_conf_set net.mptcp.mptcp_enabled "$multipath_status" [ -z "$mptcp_path_manager" ] || sysctl -qew net.mptcp.mptcp_path_manager="$mptcp_path_manager" if [ -n "$mptcp_scheduler" ]; then local prev_sched="$(cat /proc/sys/net/mptcp/mptcp_scheduler 2>/dev/null)" @@ -170,11 +181,7 @@ global_multipath_settings() { [ -z "$mptcp_version" ] || sysctl -qew net.mptcp.mptcp_version="$mptcp_version" elif [ -f /proc/sys/net/mptcp/enabled ]; then sysctl -qew net.mptcp.enabled="$multipath_status" - if grep -q net.mptcp.enabled /etc/sysctl.d/zzz_openmptcprouter.conf; then - sed -i "s:^net.mptcp.enabled=[0-1]*:net.mptcp.enabled=${multipath_status}:" /etc/sysctl.d/zzz_openmptcprouter.conf - else - echo "net.mptcp.enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf - fi + _sysctl_conf_set net.mptcp.enabled "$multipath_status" # The kernel takes at most 8 for each limit (MPTCP_PM_ADDR_MAX) and # refuses the whole command when one is above, so a single value too # high left both limits at whatever they were @@ -282,7 +289,9 @@ interface_max_metric() { EOF ;; lan) - sed -i 's:50 lan:9999 lan:g' /etc/iproute2/rt_tables + # only when needed: this runs on every reload, sed -i writes flash + grep -q '50 lan' /etc/iproute2/rt_tables 2>/dev/null && \ + sed -i 's:50 lan:9999 lan:g' /etc/iproute2/rt_tables ;; loopback) ;; @@ -380,8 +389,48 @@ _prune_wan_routes() { done } +# config_foreach callback: device_section is the "config device" section of +# the device $2 +_find_device_section() { + local name + config_get name "$1" name + [ -n "$name" ] && [ "$name" = "$2" ] && device_section="$1" +} + +# config_foreach callback: _multipath_dev_found is set when the interface $1, +# not $3, is in MPTCP (on, master or backup) on the device $2 +_check_multipath_device() { + local sec="$1" dev="$2" self="$3" mode disabled sdev + [ "$sec" = "$self" ] && return 0 + [ -n "$_multipath_dev_found" ] && return 0 + case "$sec" in *_6) return 0 ;; esac + config_get disabled "$sec" disabled + [ "$disabled" = "1" ] && return 0 + config_get mode "$sec" multipath + [ -n "$mode" ] || mode="$(uci -q get openmptcprouter.${sec}.multipath)" + case "$mode" in on|master|backup) ;; *) return 0 ;; esac + network_get_device sdev "$sec" + [ -z "$sdev" ] && network_get_physdev sdev "$sec" + [ -z "$sdev" ] && sdev=$(ifstatus "$sec" | jsonfilter -q -e '@["l3_device"]') + [ -z "$sdev" ] && config_get sdev "$sec" ifname + case "$sdev" in *@*) sdev=$(ifstatus "$sec" | jsonfilter -q -e '@["device"]') ;; esac + [ -n "$sdev" ] && [ "$sdev" = "$dev" ] && _multipath_dev_found=1 + return 0 +} + +# Whether another interface than $2 has MPTCP on the device $1. A second +# interface on a WAN's device (a wan6 on @wan2, a static alias) is "off" +# itself, and turning MPTCP off for it removed the WAN's endpoints on every +# reload: until the WAN's own turn put them back, or for good when the WAN +# came first. +_device_used_by_multipath() { + _multipath_dev_found="" + config_foreach _check_multipath_device interface "$1" "$2" + [ -n "$_multipath_dev_found" ] +} + interface_multipath_settings() { - local mode iface proto metric ip4table qdisc + local mode iface proto metric ip4table qdisc device_section local config="$1" local intf="$2" # DHCPv6 companion interfaces (_6) share the parent's device: @@ -400,6 +449,17 @@ interface_multipath_settings() { config_get txqueuelen "$config" txqueuelen config_get qdisc "$config" qdisc case "$iface" in *@*) iface=$(ifstatus "$config" | jsonfilter -q -e '@["device"]') ;; esac + # LuCI's device dialog writes the qdisc, the TX queue length and the + # offloads to the "config device" section of the device: only the + # interface's own options were read, so those were never applied and the + # queue length was put back to 1000 over the one netifd had set. The + # device's value wins, the interface's is the fallback. + device_section="" + [ -n "$iface" ] && config_foreach _find_device_section device "$iface" + [ -n "$device_section" ] && { + config_get txqueuelen "$device_section" txqueuelen "$txqueuelen" + config_get qdisc "$device_section" qdisc "$qdisc" + } if [ "$(uci -q get openmptcprouter.${config}.metric)" = "" ] || [ "$(uci -q get openmptcprouter.${config}.metric)" = "1" ]; then count=$((count+1)) metric=$count @@ -430,7 +490,8 @@ interface_multipath_settings() { [ -n "$iface" ] && { local offload val for offload in gro gso lro ufo tso; do - val=$(uci -q get network.${config}.${offload}) + config_get val "$config" "$offload" + [ -n "$device_section" ] && config_get val "$device_section" "$offload" "$val" [ "$val" = "1" ] && ethtool -K $iface $offload on >/dev/null 2>&1 [ "$val" = "0" ] && ethtool -K $iface $offload off >/dev/null 2>&1 done @@ -787,7 +848,8 @@ interface_multipath_settings() { elif ([ "$mode" = "master" ] || [ "$mode" = "on" ]) && { [ -z "$(multipath $iface | grep default)" ] || ! multipath -i "$iface" "$id"; }; then logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)" multipath "$iface" "on" "$id" - elif ([ "$mode" = "off" ] || [ -z "$mode" ]) && [ -z "$(multipath $iface | grep deactivated)" ]; then + elif ([ "$mode" = "off" ] || [ -z "$mode" ]) && [ -z "$(multipath $iface | grep deactivated)" ] && \ + ! _device_used_by_multipath "$iface" "$config"; then logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)" multipath "$iface" "$mode" "$id" elif [ "$mode" = "backup" ] && { [ -z "$(multipath $iface | grep backup)" ] || ! multipath -i "$iface" "$id"; }; then diff --git a/mptcp/files/usr/bin/multipath b/mptcp/files/usr/bin/multipath index 5a6c18a1b..309618598 100755 --- a/mptcp/files/usr/bin/multipath +++ b/mptcp/files/usr/bin/multipath @@ -66,10 +66,45 @@ EOF # endpoints only use a free slot, and a refused add is logged. MPTCP_ENDPOINTS_MAX=8 +# The id of the endpoint whose address is $2 in the listing $1. The address +# is compared as the whole first field: "grep -F '1.2.3.4 '" also matched +# 11.2.3.4, the endpoint of another WAN, which was then deleted. +endpoint_id_of_addr() { + printf '%s\n' "$1" | awk -v a="$2" '$1 == a { + for (i = 2; i < NF; i++) if ($i == "id") { print $(i + 1); exit } + }' +} + +# Whether the device $2 has an endpoint with an id below $3 in the listing $1 +_dev_has_lower_id() { + local listing="$1" dev="$2" below="$3" ep_line ep_id ep_dev + [ -n "$dev" ] || return 1 + while IFS= read -r ep_line; do + ep_id="" + ep_dev="" + set -- $ep_line + while [ "$#" -gt 1 ]; do + case "$1" in + id) ep_id="$2" ;; + dev) ep_dev="$2" ;; + esac + shift + done + [ "$ep_dev" = "$dev" ] && [ -n "$ep_id" ] && [ "$ep_id" -lt "$below" ] && return 0 + done < PKG_LICENSE:=GPLv2 @@ -10,7 +10,7 @@ PKG_LICENSE_FILES:=LICENSE PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/teleproxy/teleproxy.git -PKG_SOURCE_VERSION:=2deeb7a9c870940f5835ab3979cc5c5158172a4c +PKG_SOURCE_VERSION:=9d70b228161585d008c88de630470a75a4d5828b PKG_SOURCE_SUBDIR:=$(PKG_NAME) PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz diff --git a/wwand/Makefile b/wwand/Makefile index 4788f681a..2eebe60db 100644 --- a/wwand/Makefile +++ b/wwand/Makefile @@ -50,12 +50,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=wwand -PKG_VERSION:=1.7.0_pre21 -PKG_RELEASE:=76 +PKG_VERSION:=1.7.0_pre23 +PKG_RELEASE:=77 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git -PKG_SOURCE_VERSION:=b67a83e2da98e77953a9577653394c61fd910da0 +PKG_SOURCE_VERSION:=4d745c6c61622bb81c7ab258a967ea3604a2a4e5 PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only