mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 23:53:56 +08:00
🎉 Sync 2026-10-14 03:01:37
This commit is contained in:
1 parent
57ac656612
commit
ef45574e85
78 files changed
+2683
-3911
No files matched your search
+3
-3
@@ -5,10 +5,10 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=dae
|
PKG_NAME:=dae
|
||||||
PKG_VERSION:=2026.09.24
|
PKG_VERSION:=2026.09.30
|
||||||
PKG_RELEASE:=49
|
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_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
|
||||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
|
|||||||
+3
-3
@@ -5,10 +5,10 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=daed
|
PKG_NAME:=daed
|
||||||
PKG_VERSION:=2026.09.24
|
PKG_VERSION:=2026.09.30
|
||||||
PKG_RELEASE:=60
|
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_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
|
||||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=filebrowser-q
|
PKG_NAME:=filebrowser-q
|
||||||
PKG_VERSION:=2.0.0-stable
|
PKG_VERSION:=2.0.1-stable
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=10
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=grpcurl
|
PKG_NAME:=grpcurl
|
||||||
PKG_VERSION:=1.9.4
|
PKG_VERSION:=1.9.4
|
||||||
PKG_RELEASE:=7
|
PKG_RELEASE:=8
|
||||||
|
|
||||||
PKG_SOURCE:=v$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=v$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/fullstorydev/grpcurl/archive/refs/tags/
|
PKG_SOURCE_URL:=https://github.com/fullstorydev/grpcurl/archive/refs/tags/
|
||||||
@@ -21,7 +21,7 @@ PKG_MAINTAINER:=Yannick Chabanois <ycarus@zugaina.org>
|
|||||||
|
|
||||||
PKG_BUILD_DEPENDS:=golang/host
|
PKG_BUILD_DEPENDS:=golang/host
|
||||||
PKG_BUILD_PARALLEL:=1
|
PKG_BUILD_PARALLEL:=1
|
||||||
PKG_USE_MIPS16:=0
|
PKG_BUILD_FLAGS:=no-mips16
|
||||||
|
|
||||||
GO_PKG:=github.com/fullstorydev/grpcurl
|
GO_PKG:=github.com/fullstorydev/grpcurl
|
||||||
GO_PKG_BUILD_PKG:=github.com/fullstorydev/grpcurl/cmd/grpcurl
|
GO_PKG_BUILD_PKG:=github.com/fullstorydev/grpcurl/cmd/grpcurl
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=headscale
|
PKG_NAME:=headscale
|
||||||
PKG_VERSION:=0.29.4
|
PKG_VERSION:=0.29.5
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=10
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/juanfont/headscale/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/juanfont/headscale/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
snapshot
|
both
|
||||||
@@ -105,7 +105,6 @@ querylog:
|
|||||||
- orirainbow.cc^
|
- orirainbow.cc^
|
||||||
- orirainwe.edu.kg^
|
- orirainwe.edu.kg^
|
||||||
- coriseay.edu.kg^
|
- coriseay.edu.kg^
|
||||||
- xzbzcoriseay.win^
|
|
||||||
- google.com^
|
- google.com^
|
||||||
interval: 7d
|
interval: 7d
|
||||||
size_memory: 1000
|
size_memory: 1000
|
||||||
@@ -118,7 +117,6 @@ statistics:
|
|||||||
- orirainbow.cc^
|
- orirainbow.cc^
|
||||||
- orirainwe.edu.kg^
|
- orirainwe.edu.kg^
|
||||||
- coriseay.edu.kg^
|
- coriseay.edu.kg^
|
||||||
- xzbzcoriseay.win^
|
|
||||||
- google.com^
|
- google.com^
|
||||||
interval: 7d
|
interval: 7d
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -137,6 +135,8 @@ user_rules:
|
|||||||
- '@@||ntfy.sh^'
|
- '@@||ntfy.sh^'
|
||||||
- '@@||pubnub.com^'
|
- '@@||pubnub.com^'
|
||||||
- '@@||clarity.ms^$important'
|
- '@@||clarity.ms^$important'
|
||||||
|
- '@@||sh-gsg.com^$important'
|
||||||
|
- '@@||sh-mtgc.com^$important'
|
||||||
- '@@||taobao.com^$important'
|
- '@@||taobao.com^$important'
|
||||||
- '@@||tmall.com^$important'
|
- '@@||tmall.com^$important'
|
||||||
- '@@||jd.com^important'
|
- '@@||jd.com^important'
|
||||||
@@ -164,7 +164,6 @@ user_rules:
|
|||||||
- '@@||miwifi.com^'
|
- '@@||miwifi.com^'
|
||||||
- '@@||tracker.ai.xiaomi.com^$important'
|
- '@@||tracker.ai.xiaomi.com^$important'
|
||||||
- '@@||tracking.miui.com^$important'
|
- '@@||tracking.miui.com^$important'
|
||||||
- '@@||xiaomiyoupin.com^'
|
|
||||||
- '#解除 Apple 及其所有子域名的封锁;'
|
- '#解除 Apple 及其所有子域名的封锁;'
|
||||||
- '@@||apple.com^'
|
- '@@||apple.com^'
|
||||||
- '@@||apple.com.cn^'
|
- '@@||apple.com.cn^'
|
||||||
@@ -196,6 +195,9 @@ user_rules:
|
|||||||
- '@@||app-measurement.com^$important'
|
- '@@||app-measurement.com^$important'
|
||||||
- '@@||tmdb.org^'
|
- '@@||tmdb.org^'
|
||||||
- '@@||themoviedb.org^'
|
- '@@||themoviedb.org^'
|
||||||
|
- /googleads.$~script,domain=~googleads.github.io
|
||||||
|
- /pagead/lvz?
|
||||||
|
- '||google.com/pagead/'
|
||||||
- '||static.doubleclick.net^$domain=youtube.com'
|
- '||static.doubleclick.net^$domain=youtube.com'
|
||||||
- '||youtube.com/get_midroll_'
|
- '||youtube.com/get_midroll_'
|
||||||
- '||5hz.org^'
|
- '||5hz.org^'
|
||||||
@@ -230,27 +232,7 @@ user_rules:
|
|||||||
- '@@||1ptba.com^'
|
- '@@||1ptba.com^'
|
||||||
- '@@||ptcafe.club^'
|
- '@@||ptcafe.club^'
|
||||||
- '||nbpicture.xyz^'
|
- '||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:
|
dhcp:
|
||||||
enabled: false
|
enabled: false
|
||||||
interface_name: ""
|
interface_name: ""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-daede
|
PKG_NAME:=luci-app-daede
|
||||||
PKG_VERSION:=1.15
|
PKG_VERSION:=1.15
|
||||||
PKG_RELEASE:=50
|
PKG_RELEASE:=51
|
||||||
PKG_MAINTAINER:=kenzok8
|
PKG_MAINTAINER:=kenzok8
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||||
|
|
||||||
@@ -35,7 +35,6 @@ define Package/$(PKG_NAME)
|
|||||||
TITLE:=LuCI app for daede dashboard
|
TITLE:=LuCI app for daede dashboard
|
||||||
DEPENDS:=+luci-base +PACKAGE_$(PKG_NAME)_dae:dae +PACKAGE_$(PKG_NAME)_daed:daed
|
DEPENDS:=+luci-base +PACKAGE_$(PKG_NAME)_dae:dae +PACKAGE_$(PKG_NAME)_daed:daed
|
||||||
PKGARCH:=all
|
PKGARCH:=all
|
||||||
MAINTAINER:=kenzok8
|
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/$(PKG_NAME)/description
|
define Package/$(PKG_NAME)/description
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ LUCI_DEPENDS:=+iperf3-ssl
|
|||||||
|
|
||||||
PKG_LICENSE:=GPLv3
|
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
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,12 @@ function run_test(server,proto,mode,updown,omit,parallel,transmit,bitrate)
|
|||||||
parallel = tostring(parallel or "1"):match("^%d+$") or "1"
|
parallel = tostring(parallel or "1"):match("^%d+$") or "1"
|
||||||
omit = tostring(omit or "0"):match("^%d+$") or "0"
|
omit = tostring(omit or "0"):match("^%d+$") or "0"
|
||||||
transmit = tostring(transmit or "10"):match("^%d+$") or "10"
|
transmit = tostring(transmit or "10"):match("^%d+$") or "10"
|
||||||
bitrate = tostring(bitrate or "1M")
|
-- The page asks for Mbit/s and sends a bare number, which iperf3
|
||||||
if not bitrate:match("^[0-9]+[KMG]?$") then
|
-- 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"
|
bitrate = "1M"
|
||||||
end
|
end
|
||||||
-- With credentials omr-iperf runs the test: it writes the key to its own
|
-- With credentials omr-iperf runs the test: it writes the key to its own
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ LUCI_DEPENDS:=+msmtp-mta
|
|||||||
|
|
||||||
PKG_LICENSE:=GPLv3
|
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
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-modem-manager-next
|
PKG_NAME:=luci-app-modem-manager-next
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
|
|
||||||
PKG_LICENSE:=MIT
|
PKG_LICENSE:=MIT
|
||||||
PKG_MAINTAINER:=Dante <dante@seckv.com>
|
PKG_MAINTAINER:=Dante <dante@seckv.com>
|
||||||
@@ -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.
|
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 \
|
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 +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 \
|
+kmod-usb-net-cdc-ncm +kmod-usb-net-cdc-ether +sms-tool
|
||||||
+umbim +uqmi +sms-tool
|
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|||||||
@@ -58,18 +58,25 @@ return view.extend({
|
|||||||
const self = this;
|
const self = this;
|
||||||
if (!self.selectedPort) {
|
if (!self.selectedPort) {
|
||||||
ui.addNotification(null, E('p', {}, _('Please select a valid modem port first.')), 'error');
|
ui.addNotification(null, E('p', {}, _('Please select a valid modem port first.')), 'error');
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.showModal(_('Executing Action'), [
|
ui.showModal(_('Executing Action'), [
|
||||||
E('p', { 'class': 'spinning' }, _('Sending action to cellular module, please wait...'))
|
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();
|
ui.hideModal();
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
ui.addNotification(null, E('p', {}, _('Action executed successfully.')), 'info');
|
if (action === 'reboot') {
|
||||||
self.forceRefresh();
|
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 {
|
} else {
|
||||||
ui.addNotification(null, E('p', {}, _('Action failed: ') + (res ? res.output : _('Unknown error'))), 'error');
|
ui.addNotification(null, E('p', {}, _('Action failed: ') + (res ? res.output : _('Unknown error'))), 'error');
|
||||||
}
|
}
|
||||||
@@ -81,18 +88,33 @@ return view.extend({
|
|||||||
|
|
||||||
forceRefresh: function() {
|
forceRefresh: function() {
|
||||||
const self = this;
|
const self = this;
|
||||||
if (!self.selectedPort) return;
|
return callGetDevices().then(function(devRes) {
|
||||||
return callRefreshStatus(self.selectedPort, self.selectedNet).then(function(res) {
|
const devs = Array.isArray(devRes) ? devRes : ((devRes && Array.isArray(devRes.devices)) ? devRes.devices : []);
|
||||||
self.applyStatusDOM(res);
|
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() {
|
updateStatus: function() {
|
||||||
const self = this;
|
const self = this;
|
||||||
if (!self.selectedPort)
|
|
||||||
return Promise.resolve();
|
|
||||||
|
|
||||||
return callGetStatus(self.selectedPort, self.selectedNet).then(function(res) {
|
return callGetStatus(self.selectedPort, self.selectedNet).then(function(res) {
|
||||||
|
if (res && res.port && res.port !== self.selectedPort) {
|
||||||
|
self.selectedPort = res.port;
|
||||||
|
}
|
||||||
self.applyStatusDOM(res);
|
self.applyStatusDOM(res);
|
||||||
}).catch(function(e) {});
|
}).catch(function(e) {});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ const callSaveConfig = rpc.declare({
|
|||||||
params: [ 'port', 'baud' ]
|
params: [ 'port', 'baud' ]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const callDeviceAction = rpc.declare({
|
||||||
|
object: 'luci.modem',
|
||||||
|
method: 'device_action',
|
||||||
|
params: [ 'port', 'action' ]
|
||||||
|
});
|
||||||
|
|
||||||
const callSetMode = rpc.declare({
|
const callSetMode = rpc.declare({
|
||||||
object: 'luci.modem',
|
object: 'luci.modem',
|
||||||
method: 'set_mode',
|
method: 'set_mode',
|
||||||
@@ -151,13 +157,20 @@ return view.extend({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modeChanged = (self.currentDriverMode && modeVal !== self.currentDriverMode);
|
||||||
|
|
||||||
ui.showModal(_('Applying Configuration'), [
|
ui.showModal(_('Applying Configuration'), [
|
||||||
E('p', { 'class': 'spinning' }, _('Saving port settings and sending configuration commands, please wait...'))
|
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() {
|
.then(function() {
|
||||||
return callSetMode(targetPort, modeVal);
|
if (modeChanged) {
|
||||||
|
return callSetMode(targetPort, modeVal);
|
||||||
|
}
|
||||||
|
return Promise.resolve({ success: true });
|
||||||
})
|
})
|
||||||
.then(function() {
|
.then(function() {
|
||||||
return callSetPreferences(targetPort, ratVal, bandsLte, bandsNr);
|
return callSetPreferences(targetPort, ratVal, bandsLte, bandsNr);
|
||||||
@@ -169,11 +182,22 @@ return view.extend({
|
|||||||
return callSetCellLock(targetPort, cellLockMode, cellLockEarfcn, cellLockPci);
|
return callSetCellLock(targetPort, cellLockMode, cellLockEarfcn, cellLockPci);
|
||||||
})
|
})
|
||||||
.then(function(lockRes) {
|
.then(function(lockRes) {
|
||||||
ui.hideModal();
|
if (modeChanged) {
|
||||||
if (lockRes && lockRes.success === false) {
|
ui.showModal(_('Switching Operation Mode'), [
|
||||||
ui.addNotification(null, E('p', {}, _('Settings saved, but cell lock failed: ') + (lockRes.error || lockRes.output || '')), 'warning');
|
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 {
|
} 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) {
|
.catch(function(err) {
|
||||||
@@ -314,29 +338,45 @@ return view.extend({
|
|||||||
container.appendChild(portSection);
|
container.appendChild(portSection);
|
||||||
|
|
||||||
// Protocol Mode Switching Section
|
// 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' }, [
|
const modeSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
E('legend', {}, _('Driver Protocol and Operation Mode')),
|
E('legend', {}, _('Driver Protocol and Operation Mode')),
|
||||||
E('div', { 'class': 'cbi-section-node' }, [
|
E('div', { 'class': 'cbi-section-node' }, [
|
||||||
E('div', { 'class': 'cbi-value' }, [
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
E('label', { 'class': 'cbi-value-title' }, _('Driver Mode')),
|
E('label', { 'class': 'cbi-value-title' }, _('Driver Mode')),
|
||||||
E('div', { 'class': 'cbi-value-field' }, [
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
E('label', { 'style': 'margin-right: 20px;' }, [
|
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 15px; align-items: center;' }, modeRadioNodes),
|
||||||
E('input', { 'type': 'radio', 'name': 'modem_mode', 'value': 'ncm', 'checked': 'checked' }), ' ',
|
E('div', { 'class': 'cbi-value-description', 'style': 'margin-top: 8px;' },
|
||||||
_('NCM Mode')
|
_('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.')
|
||||||
]),
|
)
|
||||||
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.'))
|
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ return view.extend({
|
|||||||
}, [
|
}, [
|
||||||
E('div', { 'style': 'display: flex; align-items: center; gap: 20px;' }, [
|
E('div', { 'style': 'display: flex; align-items: center; gap: 20px;' }, [
|
||||||
E('div', {}, [
|
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;' },
|
E('strong', { 'style': 'color: #333; font-size: 14px;' },
|
||||||
self.smsCapacity.total > 0 ? (self.smsCapacity.used + ' / ' + self.smsCapacity.total + ' ' + _('Messages')) : (self.smsList.length + ' ' + _('Messages')))
|
self.smsCapacity.total > 0 ? (self.smsCapacity.used + ' / ' + self.smsCapacity.total + ' ' + _('Messages')) : (self.smsList.length + ' ' + _('Messages')))
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ msgstr "正在向模组发送操作指令,请稍候..."
|
|||||||
msgid "Action executed successfully."
|
msgid "Action executed successfully."
|
||||||
msgstr "动作执行成功。"
|
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: "
|
msgid "Action failed: "
|
||||||
msgstr "动作执行失败: "
|
msgstr "动作执行失败: "
|
||||||
|
|
||||||
@@ -238,17 +241,20 @@ msgstr "MBIM模式"
|
|||||||
msgid "NCM Mode"
|
msgid "NCM Mode"
|
||||||
msgstr "NCM模式"
|
msgstr "NCM模式"
|
||||||
|
|
||||||
msgid "QMI Mode"
|
msgid "NCM Mode (Recommended)"
|
||||||
msgstr "QMI模式"
|
msgstr "推荐NCM模式"
|
||||||
|
|
||||||
msgid "ECM Mode"
|
msgid "ECM Mode"
|
||||||
msgstr "ECM模式"
|
msgstr "ECM模式"
|
||||||
|
|
||||||
msgid "RNDIS Mode"
|
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 "RNDIS模式"
|
msgstr "切换工作模式需要重启模组生效。NCM模式具备低CPU负载的高速聚合吞吐,ECM模式提供通用以太网兼容性。"
|
||||||
|
|
||||||
msgid "Switching the operation mode requires restarting the modem to take effect."
|
msgid "Switching Operation Mode"
|
||||||
msgstr "切换工作模式后需要重新复位模组方能使新驱动生效。"
|
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"
|
msgid "Sync OpenWrt Network"
|
||||||
msgstr "同步网络配置"
|
msgstr "同步网络配置"
|
||||||
@@ -490,6 +496,9 @@ msgstr "停用功能"
|
|||||||
msgid "SIM Card Storage Capacity: "
|
msgid "SIM Card Storage Capacity: "
|
||||||
msgstr "SIM卡存储容量: "
|
msgstr "SIM卡存储容量: "
|
||||||
|
|
||||||
|
msgid "SIM Card Storage Capacity"
|
||||||
|
msgstr "SIM卡存储容量"
|
||||||
|
|
||||||
msgid "Messages"
|
msgid "Messages"
|
||||||
msgstr "条短信"
|
msgstr "条短信"
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ msgstr "正在向模组发送操作指令,请稍候..."
|
|||||||
msgid "Action executed successfully."
|
msgid "Action executed successfully."
|
||||||
msgstr "动作执行成功。"
|
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: "
|
msgid "Action failed: "
|
||||||
msgstr "动作执行失败: "
|
msgstr "动作执行失败: "
|
||||||
|
|
||||||
@@ -238,17 +241,20 @@ msgstr "MBIM模式"
|
|||||||
msgid "NCM Mode"
|
msgid "NCM Mode"
|
||||||
msgstr "NCM模式"
|
msgstr "NCM模式"
|
||||||
|
|
||||||
msgid "QMI Mode"
|
msgid "NCM Mode (Recommended)"
|
||||||
msgstr "QMI模式"
|
msgstr "推荐NCM模式"
|
||||||
|
|
||||||
msgid "ECM Mode"
|
msgid "ECM Mode"
|
||||||
msgstr "ECM模式"
|
msgstr "ECM模式"
|
||||||
|
|
||||||
msgid "RNDIS Mode"
|
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 "RNDIS模式"
|
msgstr "切换工作模式需要重启模组生效。NCM模式具备低CPU负载的高速聚合吞吐,ECM模式提供通用以太网兼容性。"
|
||||||
|
|
||||||
msgid "Switching the operation mode requires restarting the modem to take effect."
|
msgid "Switching Operation Mode"
|
||||||
msgstr "切换工作模式后需要重新复位模组方能使新驱动生效。"
|
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"
|
msgid "Sync OpenWrt Network"
|
||||||
msgstr "同步网络配置"
|
msgstr "同步网络配置"
|
||||||
@@ -490,6 +496,9 @@ msgstr "停用功能"
|
|||||||
msgid "SIM Card Storage Capacity: "
|
msgid "SIM Card Storage Capacity: "
|
||||||
msgstr "SIM卡存储容量: "
|
msgstr "SIM卡存储容量: "
|
||||||
|
|
||||||
|
msgid "SIM Card Storage Capacity"
|
||||||
|
msgstr "SIM卡存储容量"
|
||||||
|
|
||||||
msgid "Messages"
|
msgid "Messages"
|
||||||
msgstr "条短信"
|
msgstr "条短信"
|
||||||
|
|
||||||
|
|||||||
@@ -79,13 +79,43 @@ fi
|
|||||||
|
|
||||||
# Fast path: Prioritize native sms_tool engine if installed (zero echo loop, robust termios)
|
# 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 command -v sms_tool >/dev/null 2>&1; then
|
||||||
if [ -n "$SMS_TO" ]; then
|
TMP_OUT="/tmp/at_sms_$$.tmp"
|
||||||
OUT=$(sms_tool -d "$DEVICE" -b "$BAUDRATE" send "$SMS_TO" "$SMS_MSG" 2>&1)
|
TMP_DONE="/tmp/at_sms_$$.done"
|
||||||
RC=$?
|
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
|
else
|
||||||
OUT=$(sms_tool -d "$DEVICE" -b "$BAUDRATE" at "$COMMAND" 2>&1)
|
wait "$CHILD_PID" 2>/dev/null
|
||||||
RC=$?
|
RC=$(cat "$TMP_DONE" 2>/dev/null || echo 0)
|
||||||
|
OUT=$(cat "$TMP_OUT" 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
|
rm -f "$TMP_OUT" "$TMP_DONE"
|
||||||
|
|
||||||
flock -u 9 2>/dev/null || true
|
flock -u 9 2>/dev/null || true
|
||||||
CLEAN_OUT=$(echo "$OUT" | tr -d "\r")
|
CLEAN_OUT=$(echo "$OUT" | tr -d "\r")
|
||||||
if [ $RC -eq 0 ] && ! echo "$CLEAN_OUT" | grep -qE "ERROR|\+CME ERROR|\+CMS ERROR"; then
|
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
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
else
|
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"
|
echo "$CLEAN_OUT"
|
||||||
exit ${RC:-1}
|
exit ${RC:-1}
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -39,11 +39,14 @@ collect_single() {
|
|||||||
let devs_data = plugin.get_devices.call();
|
let devs_data = plugin.get_devices.call();
|
||||||
let dev = (devs_data?.devices && length(devs_data.devices) > 0) ? devs_data.devices[0] : null;
|
let dev = (devs_data?.devices && length(devs_data.devices) > 0) ? devs_data.devices[0] : null;
|
||||||
let user_port = "'"$user_port"'";
|
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) {
|
if (target_port) {
|
||||||
let status = plugin.collect_raw_status.call({ args: { port: target_port, primary_net: dev?.primary_net || "usb0" } });
|
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";
|
let tmp = "/tmp/run/modem-manager/status.json.tmp";
|
||||||
fs.writefile(tmp, sprintf("%J\n", status));
|
fs.writefile(tmp, sprintf("%J\n", status));
|
||||||
fs.rename(tmp, "/tmp/run/modem-manager/status.json");
|
fs.rename(tmp, "/tmp/run/modem-manager/status.json");
|
||||||
|
|||||||
@@ -116,6 +116,19 @@ detect_all() {
|
|||||||
local PROD=$(get_sys_attr "$dev/product")
|
local PROD=$(get_sys_attr "$dev/product")
|
||||||
local SER=$(get_sys_attr "$dev/serial")
|
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=""
|
local PRIMARY_TTY=""
|
||||||
for t in $TTYS; do
|
for t in $TTYS; do
|
||||||
case "$t" in
|
case "$t" in
|
||||||
@@ -125,20 +138,19 @@ detect_all() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
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}')
|
[ -z "$PRIMARY_TTY" ] && PRIMARY_TTY=$(echo "$TTYS" | awk '{print $1}')
|
||||||
local PRIMARY_NET=$(echo "$NETS" | 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 -eq 1 ] || echo ","
|
||||||
FIRST=0
|
FIRST=0
|
||||||
|
|
||||||
@@ -154,6 +166,8 @@ detect_all() {
|
|||||||
NET_JSON="$NET_JSON\"$n\""
|
NET_JSON="$NET_JSON\"$n\""
|
||||||
done
|
done
|
||||||
|
|
||||||
|
local MODES_JSON="\"ncm\", \"ecm\""
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
{
|
{
|
||||||
"id": "$DEVNAME",
|
"id": "$DEVNAME",
|
||||||
@@ -165,6 +179,7 @@ detect_all() {
|
|||||||
"driver": "$DRIVER",
|
"driver": "$DRIVER",
|
||||||
"primary_port": "$PRIMARY_TTY",
|
"primary_port": "$PRIMARY_TTY",
|
||||||
"primary_net": "$PRIMARY_NET",
|
"primary_net": "$PRIMARY_NET",
|
||||||
|
"supported_modes": [ $MODES_JSON ],
|
||||||
"ports": [ $TTY_JSON ],
|
"ports": [ $TTY_JSON ],
|
||||||
"nets": [ $NET_JSON ]
|
"nets": [ $NET_JSON ]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,16 +30,54 @@ function touch_terminal_active() {
|
|||||||
fs.writefile(TERMINAL_LOCK, 'active\n');
|
fs.writefile(TERMINAL_LOCK, 'active\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function safe_exec_at(port, cmd, timeout) {
|
function is_terminal_active() {
|
||||||
if (!port || !match(port, /^\/dev\/tty(USB|ACM)[0-9]+$/))
|
let st = fs.stat(TERMINAL_LOCK);
|
||||||
return { success: false, output: 'Invalid port parameter' };
|
if (st && st.mtime) {
|
||||||
|
let now = time();
|
||||||
|
if ((now - st.mtime) <= 15) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_effective_port(specified_port) {
|
||||||
|
let dev_content = fs.readfile(CACHE_DEVICES);
|
||||||
|
let data = dev_content ? json(dev_content) : null;
|
||||||
|
let dev = (data?.devices && length(data.devices) > 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)
|
if (!cmd)
|
||||||
return { success: false, output: 'Empty command' };
|
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 to = (timeout && timeout > 0 && timeout <= 30) ? int(timeout) : 3;
|
||||||
let clean_cmd = replace(cmd, /'/g, "'\\''");
|
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)
|
if (!p)
|
||||||
return { success: false, output: 'Failed to execute command helper' };
|
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) };
|
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) {
|
function decode_ucs2_if_needed(text) {
|
||||||
if (!text || length(text) < 4 || (length(text) % 4) != 0)
|
if (!text || length(text) < 4 || (length(text) % 4) != 0)
|
||||||
return text;
|
return text;
|
||||||
@@ -206,11 +225,24 @@ function parse_serving_cell(raw_output) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function do_collect_status(port, net_dev) {
|
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);
|
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);
|
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);
|
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);
|
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);
|
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 temp_res = safe_exec_at(port, 'AT+QTEMP', 2);
|
||||||
|
|
||||||
let csq_data = parse_csq(csq_res.output);
|
let csq_data = parse_csq(csq_res.output);
|
||||||
@@ -388,7 +420,7 @@ function do_collect_status(port, net_dev) {
|
|||||||
temperature: temp,
|
temperature: temp,
|
||||||
network: net_info,
|
network: net_info,
|
||||||
raw_info: info_res.output,
|
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');
|
let user_port = c.get('modem-manager', 'global', 'port');
|
||||||
if (user_port && user_port != '' && res.devices && length(res.devices) > 0) {
|
if (user_port && user_port != '' && res.devices && length(res.devices) > 0) {
|
||||||
res.devices[0].user_configured_port = user_port;
|
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: {
|
collect_raw_status: {
|
||||||
args: { port: '', primary_net: '' },
|
args: { port: '', primary_net: '' },
|
||||||
call: function(req) {
|
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 net_dev = req.args?.primary_net || 'usb0';
|
||||||
return do_collect_status(port, net_dev);
|
return do_collect_status(port, net_dev);
|
||||||
}
|
}
|
||||||
@@ -473,7 +507,7 @@ const methods = {
|
|||||||
get_status: {
|
get_status: {
|
||||||
args: { port: '', primary_net: '' },
|
args: { port: '', primary_net: '' },
|
||||||
call: function(req) {
|
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 net_dev = req.args?.primary_net || 'usb0';
|
||||||
|
|
||||||
// Non-blocking fast path: read decoupled background status cache directly
|
// 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
|
// Cold start fallback: synchronously collect once and write cache
|
||||||
let status = do_collect_status(port, net_dev);
|
let status = do_collect_status(port, net_dev);
|
||||||
fs.mkdir(RUN_DIR);
|
if (status && status.success) {
|
||||||
fs.writefile(CACHE_STATUS, sprintf("%J\n", status));
|
fs.mkdir(RUN_DIR);
|
||||||
|
fs.writefile(CACHE_STATUS, sprintf("%J\n", status));
|
||||||
|
}
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -499,11 +535,13 @@ const methods = {
|
|||||||
refresh_status: {
|
refresh_status: {
|
||||||
args: { port: '', primary_net: '' },
|
args: { port: '', primary_net: '' },
|
||||||
call: function(req) {
|
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 net_dev = req.args?.primary_net || 'usb0';
|
||||||
let status = do_collect_status(port, net_dev);
|
let status = do_collect_status(port, net_dev);
|
||||||
fs.mkdir(RUN_DIR);
|
if (status && status.success) {
|
||||||
fs.writefile(CACHE_STATUS, sprintf("%J\n", status));
|
fs.mkdir(RUN_DIR);
|
||||||
|
fs.writefile(CACHE_STATUS, sprintf("%J\n", status));
|
||||||
|
}
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -522,10 +560,10 @@ const methods = {
|
|||||||
touch_terminal_active();
|
touch_terminal_active();
|
||||||
|
|
||||||
log_message(`[AT] >>> ${cmd}`);
|
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 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 t1 = time();
|
||||||
let duration_ms = (t1 > 0 && t0 > 0 && t1 >= t0) ? (int(t1) - int(t0)) : 0;
|
let duration_ms = (t1 >= t0) ? ((t1 - t0) * 1000) : 0;
|
||||||
log_message(`[AT] <<< ${res.output}`);
|
log_message(`[AT] <<< ${res.output}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -558,11 +596,8 @@ const methods = {
|
|||||||
touch_terminal_active();
|
touch_terminal_active();
|
||||||
|
|
||||||
let mode_map = {
|
let mode_map = {
|
||||||
'qmi': '0',
|
'ncm': '5',
|
||||||
'ecm': '1',
|
'ecm': '1'
|
||||||
'mbim': '2',
|
|
||||||
'rndis': '3',
|
|
||||||
'ncm': '5'
|
|
||||||
};
|
};
|
||||||
let val = mode_map[mode] || '5';
|
let val = mode_map[mode] || '5';
|
||||||
let cmd = `AT+QCFG="usbnet",${val}`;
|
let cmd = `AT+QCFG="usbnet",${val}`;
|
||||||
@@ -760,8 +795,10 @@ const methods = {
|
|||||||
touch_terminal_active();
|
touch_terminal_active();
|
||||||
|
|
||||||
let cmd = 'AT';
|
let cmd = 'AT';
|
||||||
|
let timeout = 5;
|
||||||
if (action === 'reboot') {
|
if (action === 'reboot') {
|
||||||
cmd = 'AT+CFUN=1,1';
|
cmd = 'AT+CFUN=1,1';
|
||||||
|
timeout = 10;
|
||||||
} else if (action === 'airplane_on') {
|
} else if (action === 'airplane_on') {
|
||||||
cmd = 'AT+CFUN=4';
|
cmd = 'AT+CFUN=4';
|
||||||
} else if (action === 'airplane_off') {
|
} else if (action === 'airplane_off') {
|
||||||
@@ -772,7 +809,14 @@ const methods = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log_message(`[ACTION] Executing ${action} via ${cmd}`);
|
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 };
|
return { success: res.success, output: res.output };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI Support for MPTCP
|
LUCI_TITLE:=LuCI Support for MPTCP
|
||||||
|
LUCI_DEPENDS:=+mptcp +ip-full +ss +nstat +tracebox +omr-sockdiag
|
||||||
PKG_LICENSE:=GPLv3
|
PKG_LICENSE:=GPLv3
|
||||||
|
|
||||||
#include $(TOPDIR)/feeds/luci/luci.mk
|
#include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"luci-app-mptcp": {
|
"luci-app-mptcp": {
|
||||||
"description": "Grant UCI access for luci-app-mptcp",
|
"description": "Grant UCI access for luci-app-mptcp",
|
||||||
"read": {
|
"read": {
|
||||||
"uci": [ "network", "mqvpn" ],
|
"uci": [ "network" ],
|
||||||
"file": {
|
"file": {
|
||||||
"/usr/lib/mptcpd": [ "list" ],
|
"/usr/lib/mptcpd": [ "list" ],
|
||||||
"/usr/share/bpf/scheduler": [ "list" ],
|
"/usr/share/bpf/scheduler": [ "list" ],
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (C) 2017 Dan Luedtke <mail@danrl.com>
|
|
||||||
#
|
|
||||||
# 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 <fe@dev.tdt.de>
|
|
||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
|
||||||
|
|
||||||
# call BuildPackage - OpenWrt buildroot signature
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#mwan3-service-status > .alert-message {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
width: 15rem;
|
|
||||||
height: 6rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
@@ -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 <code>0x</code>'));
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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.') + '<br />' +
|
|
||||||
_('Names must match the interface name found in /etc/config/network.') + '<br />' +
|
|
||||||
_('Names may contain characters A-Z, a-z, 0-9, _ and no spaces-') + '<br />' +
|
|
||||||
_('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();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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.') + '<br />' +
|
|
||||||
_('Names may contain characters A-Z, a-z, 0-9, _ and no spaces.') + '<br />' +
|
|
||||||
_('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();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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".') + '<br/>' +
|
|
||||||
_('The file is also preserved during sysupgrade.') + '<br/>' +
|
|
||||||
'<br />' +
|
|
||||||
_('Notes:') + '<br />' +
|
|
||||||
_('This file is interpreted as a shell script.') + '<br />' +
|
|
||||||
_('The first line of the script must be "#!/bin/sh" without quotes.') + '<br />' +
|
|
||||||
_('Lines beginning with # are comments and are not executed.') + '<br />' +
|
|
||||||
_('Put your custom mwan3 action here, they will be executed with each netifd hotplug interface event on interfaces for which mwan3 is enabled.') + '<br />' +
|
|
||||||
'<br />' +
|
|
||||||
_('There are three main environment variables that are passed to this script.') + '<br />' +
|
|
||||||
'<br />' +
|
|
||||||
_('%s: Name of the action that triggered this event').format('$ACTION') + '<br />' +
|
|
||||||
_('* %s: Is called by netifd and mwan3track').format('ifup') + '<br />' +
|
|
||||||
_('* %s: Is called by netifd and mwan3track').format('ifdown') + '<br />' +
|
|
||||||
_('* %s: Is only called by mwan3track if tracking was successful').format('connected') + '<br />' +
|
|
||||||
_('* %s: Is only called by mwan3track if tracking has failed').format('disonnected') + '<br />' +
|
|
||||||
_('%s: Name of the interface which went up or down (e.g. "wan" or "wwan")').format('$INTERFACE') + '<br />' +
|
|
||||||
_('%s: Name of Physical device which interface went up or down (e.g. "eth0" or "wwan0")').format('$DEVICE') + '<br />'),
|
|
||||||
E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 10, 'disabled': isReadonlyView }, [ mwan3user != null ? mwan3user : '' ]))
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSaveApply: null,
|
|
||||||
handleReset: 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.') + '<br />' +
|
|
||||||
_('Member interfaces with lower metrics are used first.') + '<br />' +
|
|
||||||
_('Member interfaces with the same metric will be load-balanced.') + '<br />' +
|
|
||||||
_('Load-balanced member interfaces distribute more traffic out those with higher weights.') + '<br />' +
|
|
||||||
_('Names may contain characters A-Z, a-z, 0-9, _ and no spaces.') + '<br />' +
|
|
||||||
_('Names must be 15 characters or less.') + '<br />' +
|
|
||||||
_('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();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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.') + '<br />' +
|
|
||||||
_('Rules are based on IP address, port or protocol.') + '<br />' +
|
|
||||||
_('Rules are matched from top to bottom.') + '<br />' +
|
|
||||||
_('Rules below a matching rule are ignored.') + '<br />' +
|
|
||||||
_('Traffic not matching any rule is routed using the main routing table.') + '<br />' +
|
|
||||||
_('Traffic destined for known (other than default) networks is handled by the main routing table.') + '<br />' +
|
|
||||||
_('Traffic matching a rule, but all WAN interfaces for that policy are down will be blackholed.') + '<br />' +
|
|
||||||
_('Names may contain characters A-Z, a-z, 0-9, _ and no spaces.') + '<br />' +
|
|
||||||
_('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();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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
|
|
||||||
})
|
|
||||||
@@ -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
|
|
||||||
})
|
|
||||||
@@ -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 '<strong>%h</strong>'.format(_('No MWAN interfaces found'));
|
|
||||||
|
|
||||||
var tableHtml = '<table class="table cbi-section-table" style="width: 100%;">';
|
|
||||||
tableHtml += '<thead><tr class="tr cbi-section-table-titles">';
|
|
||||||
tableHtml += '<th class="th left">' + _('Interface') + '</th>';
|
|
||||||
tableHtml += '<th class="th left">' + _('Status') + '</th>';
|
|
||||||
tableHtml += '<th class="th left">' + _('Uptime') + '</th>';
|
|
||||||
tableHtml += '</tr></thead><tbody>';
|
|
||||||
|
|
||||||
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 += '<tr class="tr cbi-section-table-row">';
|
|
||||||
tableHtml += '<td class="td left"><strong>' + iface + '</strong></td>';
|
|
||||||
tableHtml += '<td class="td left"><span class="' + statusClass + '">' + state + '</span></td>';
|
|
||||||
tableHtml += '<td class="td left">' + (time || '') + '</td>';
|
|
||||||
tableHtml += '</tr>';
|
|
||||||
}
|
|
||||||
|
|
||||||
tableHtml += '</tbody></table>';
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
@@ -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
|
|
||||||
})
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -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 "
|
|
||||||
"<snipersniper@users.noreply.hosted.weblate.org>\n"
|
|
||||||
"Language-Team: Russian <https://hosted.weblate.org/projects/openwrt/"
|
|
||||||
"luciapplicationsmwan3/ru/>\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 <code>0x</code>"
|
|
||||||
msgstr "Введите значение в шестнадцатеричной форме, начиная с <code>0x</code>"
|
|
||||||
|
|
||||||
#: 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 "Нет доступных интерфейсов"
|
|
||||||
@@ -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
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# Copyright (C) 2021 TDT AG <development@tdt.de>
|
|
||||||
#
|
|
||||||
# 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") <command>"
|
|
||||||
echo "command:"
|
|
||||||
echo " diag: diagnostic commands"
|
|
||||||
echo " nftset: nft set commands"
|
|
||||||
echo ""
|
|
||||||
echo "diag <command> <iface>"
|
|
||||||
echo "command:"
|
|
||||||
echo " gateway <iface>: ping interface gateway"
|
|
||||||
echo " tracking <iface>: ping interface tracking targets"
|
|
||||||
echo " rules <iface>: check interface routing rules"
|
|
||||||
echo " routes <iface>: check interface routing tables"
|
|
||||||
echo ""
|
|
||||||
echo "nftset <command>"
|
|
||||||
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 "$@"
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-open-box
|
PKG_NAME:=luci-app-open-box
|
||||||
PKG_VERSION:=0.1.320
|
PKG_VERSION:=0.1.326
|
||||||
PKG_RELEASE:=28
|
PKG_RELEASE:=29
|
||||||
|
|
||||||
PKG_MAINTAINER:=liandu2024
|
PKG_MAINTAINER:=liandu2024
|
||||||
PKG_LICENSE:=GPL-3.0-or-later
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ LUCI_DEPENDS:=+luci-mod-admin-full +tcpdump +coreutils +coreutils-timeout
|
|||||||
PKG_MAINTAINER:=Wojciech Jowsa <wojciech.jowsa@gmail.com>
|
PKG_MAINTAINER:=Wojciech Jowsa <wojciech.jowsa@gmail.com>
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
|
||||||
|
define Package/luci-app-packet-capture/conffiles
|
||||||
|
/etc/config/packet_capture
|
||||||
|
endef
|
||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
# call BuildPackage - OpenWrt buildroot signatureet
|
# call BuildPackage - OpenWrt buildroot signatureet
|
||||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall2
|
PKG_NAME:=luci-app-passwall2
|
||||||
PKG_VERSION:=26.10.1
|
PKG_VERSION:=26.10.1
|
||||||
PKG_RELEASE:=141
|
PKG_RELEASE:=142
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
@@ -80,6 +80,7 @@ config PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy
|
|||||||
select PACKAGE_iptables-nft
|
select PACKAGE_iptables-nft
|
||||||
select PACKAGE_iptables-zz-legacy
|
select PACKAGE_iptables-zz-legacy
|
||||||
select PACKAGE_iptables-mod-conntrack-extra
|
select PACKAGE_iptables-mod-conntrack-extra
|
||||||
|
select PACKAGE_iptables-mod-extra
|
||||||
select PACKAGE_iptables-mod-iprange
|
select PACKAGE_iptables-mod-iprange
|
||||||
select PACKAGE_iptables-mod-socket
|
select PACKAGE_iptables-mod-socket
|
||||||
select PACKAGE_iptables-mod-tproxy
|
select PACKAGE_iptables-mod-tproxy
|
||||||
|
|||||||
@@ -755,12 +755,14 @@ end
|
|||||||
function com_update(comname)
|
function com_update(comname)
|
||||||
local json = nil
|
local json = nil
|
||||||
local task = http.formvalue("task")
|
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"))
|
json = api.to_extract(comname, http.formvalue("file"), http.formvalue("subfix"))
|
||||||
elseif task == "move" then
|
elseif task == "move" then
|
||||||
json = api.to_move(comname, http.formvalue("file"))
|
json = api.to_move(comname, http.formvalue("file"))
|
||||||
else
|
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
|
end
|
||||||
|
|
||||||
http_write_json(json)
|
http_write_json(json)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ c_config = "passwall2"
|
|||||||
s_config = "passwall2_server"
|
s_config = "passwall2_server"
|
||||||
local com = require "luci.passwall2.com"
|
local com = require "luci.passwall2.com"
|
||||||
nixio = require "nixio"
|
nixio = require "nixio"
|
||||||
|
require "nixio.util"
|
||||||
fs = require "nixio.fs"
|
fs = require "nixio.fs"
|
||||||
sys = require "luci.sys"
|
sys = require "luci.sys"
|
||||||
uci = require"luci.model.uci".cursor()
|
uci = require"luci.model.uci".cursor()
|
||||||
@@ -17,6 +18,7 @@ i18n = require "luci.i18n"
|
|||||||
|
|
||||||
curl_args = { "-skfL", "--connect-timeout 3", "--retry 3", "-H 'Accept: */*'" }
|
curl_args = { "-skfL", "--connect-timeout 3", "--retry 3", "-H 'Accept: */*'" }
|
||||||
command_timeout = 300
|
command_timeout = 300
|
||||||
|
DIRECT_GID = 9912
|
||||||
OPENWRT_ARCH = nil
|
OPENWRT_ARCH = nil
|
||||||
DISTRIB_ARCH = nil
|
DISTRIB_ARCH = nil
|
||||||
|
|
||||||
@@ -197,7 +199,7 @@ function del_cache_var(key)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function set_cache_var(key, val)
|
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
|
end
|
||||||
|
|
||||||
function get_cache_var(key)
|
function get_cache_var(key)
|
||||||
@@ -226,6 +228,30 @@ function exec_call(cmd)
|
|||||||
return tonumber(rc), trim(out)
|
return tonumber(rc), trim(out)
|
||||||
end
|
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)
|
function base64Decode(text)
|
||||||
if type(text) ~= "string" then return "" end
|
if type(text) ~= "string" then return "" end
|
||||||
local encoded = text:gsub("%z", ""):gsub("%c", ""):gsub("_", "/"):gsub("-", "+"):gsub("=+$", "")
|
local encoded = text:gsub("%z", ""):gsub("%c", ""):gsub("_", "/"):gsub("-", "+"):gsub("=+$", "")
|
||||||
@@ -274,22 +300,38 @@ function get_domain_port_from_url(url)
|
|||||||
return domain, port
|
return domain, port
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Domain resolution
|
function resolve_domain(domain, dns)
|
||||||
function domainToIPv4(domain, dns)
|
dns = dns or get_cache_var("DIRECT_DNS") or "223.5.5.5"
|
||||||
local Dns = dns or "223.5.5.5"
|
local address, port = dns:match("^(.-)#(%d+)$")
|
||||||
local IPs = luci.sys.exec('nslookup %s %s | awk \'/^Name:/{getline; if ($1 == "Address:") print $2}\'' % { domain, Dns })
|
if not address then address, port = dns:match("^%[(.-)%]:(%d+)$") end
|
||||||
for IP in string.gmatch(IPs, "%S+") do
|
if not address then address, port = dns:match("^([^:]+):(%d+)$") end
|
||||||
if datatypes.ipaddr(IP) and not datatypes.ip6addr(IP) then return IP 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
|
end
|
||||||
return nil
|
return nil, result, rc
|
||||||
end
|
end
|
||||||
|
|
||||||
function curl_base(url, file, args)
|
function curl_base(url, file, args, gid)
|
||||||
if not args then args = {} end
|
if not args then args = {} end
|
||||||
if file then
|
if file then
|
||||||
args[#args + 1] = "-o " .. file
|
args[#args + 1] = "-o " .. file
|
||||||
end
|
end
|
||||||
local cmd = string.format('curl %s "%s"', table_join(args), url)
|
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)
|
return exec_call(cmd)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -311,36 +353,34 @@ end
|
|||||||
function curl_logic(url, file, args)
|
function curl_logic(url, file, args)
|
||||||
local return_code, result = curl_proxy(url, file, args)
|
local return_code, result = curl_proxy(url, file, args)
|
||||||
if not return_code or return_code ~= 0 then
|
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
|
end
|
||||||
return return_code, result
|
return return_code, result
|
||||||
end
|
end
|
||||||
|
|
||||||
function curl_direct(url, file, args)
|
function curl_direct(url, file, args)
|
||||||
-- Direct access
|
if get_cache_var("ENABLED_DEFAULT_ACL") ~= "1" and get_cache_var("ENABLED_ACLS") ~= "1" then
|
||||||
if not args then args = {} end
|
return curl_base(url, file, args)
|
||||||
local tmp_args = clone(args)
|
end
|
||||||
|
local tmp_args = clone(args or {})
|
||||||
|
tmp_args[#tmp_args + 1] = "--noproxy '*'"
|
||||||
local domain, port = get_domain_port_from_url(url)
|
local domain, port = get_domain_port_from_url(url)
|
||||||
if domain then
|
if domain then
|
||||||
local ip = domainToIPv4(domain)
|
local ips, err = resolve_domain(domain)
|
||||||
if ip then
|
if not ips then return 6, err end
|
||||||
tmp_args[#tmp_args + 1] = "--resolve " .. domain .. ":" .. port .. ":" .. ip
|
for i, ip in ipairs(ips) do
|
||||||
|
if datatypes.ip6addr(ip) then ips[i] = "[" .. ip .. "]" end
|
||||||
end
|
end
|
||||||
|
tmp_args[#tmp_args + 1] = "--resolve " .. util.shellquote(domain .. ":" .. port .. ":" .. table.concat(ips, ","))
|
||||||
end
|
end
|
||||||
return curl_base(url, file, tmp_args)
|
return curl_base(url, file, tmp_args, DIRECT_GID)
|
||||||
end
|
end
|
||||||
|
|
||||||
function curl_auto(url, file, args)
|
function curl_auto(url, file, args)
|
||||||
local localhost_proxy = uci_get_c("@global[0]", "localhost_proxy") or "1"
|
if (uci_get_c("@global[0]", "localhost_proxy") or "1") == "1" then
|
||||||
if localhost_proxy == "1" then
|
|
||||||
return curl_base(url, file, args)
|
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
|
end
|
||||||
|
return curl_logic(url, file, args)
|
||||||
end
|
end
|
||||||
|
|
||||||
function url(...)
|
function url(...)
|
||||||
@@ -853,19 +893,26 @@ function clone(org)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function get_bin_version_cache(file, cmd)
|
local function get_bin_version_cache(file, cmd)
|
||||||
sys.call("mkdir -p " .. CACHE_PATH)
|
if file and file ~= "" and fs.access(file) then
|
||||||
if fs.access(file) then
|
|
||||||
chmod_755(file)
|
chmod_755(file)
|
||||||
local md5 = sys.exec("echo -n $(md5sum " .. file .. " | awk '{print $1}')")
|
local stat = fs.stat(file)
|
||||||
if fs.access(CACHE_PATH .. "/" .. md5) then
|
local cache_file
|
||||||
return sys.exec("echo -n $(cat %s)" % { CACHE_PATH .. "/" .. md5 })
|
if stat and stat.dev and stat.ino and stat.size and stat.mtime and stat.ctime then
|
||||||
else
|
-- Use metadata to avoid hashing the entire binary on every lookup.
|
||||||
local version = sys.exec(string.format("echo -n $(%s %s)", file, cmd))
|
cache_file = CACHE_PATH .. "/binver_" .. table.concat({stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime}, "_")
|
||||||
if version and version ~= "" then
|
local cached = fs.readfile(cache_file)
|
||||||
sys.call("echo '%s' > %s" % { version, CACHE_PATH .. "/" .. md5})
|
if cached and trim(cached) ~= "" then
|
||||||
return version
|
return trim(cached)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
@@ -886,7 +933,7 @@ end
|
|||||||
|
|
||||||
function is_file(path)
|
function is_file(path)
|
||||||
if path and #path > 1 then
|
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
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -895,7 +942,7 @@ end
|
|||||||
|
|
||||||
function is_dir(path)
|
function is_dir(path)
|
||||||
if path and #path > 1 then
|
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
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1123,7 +1170,7 @@ local default_file_tree = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function get_api_json(url)
|
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
|
if return_code ~= 0 or content == "" then return {} end
|
||||||
return jsonc.parse(content) or {}
|
return jsonc.parse(content) or {}
|
||||||
end
|
end
|
||||||
@@ -1216,7 +1263,7 @@ function to_check(arch, app_name)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
function to_download(app_name, url, size)
|
function to_download(app_name, url, size, task_id)
|
||||||
local result = check_path(app_name)
|
local result = check_path(app_name)
|
||||||
if result.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
return result
|
return result
|
||||||
@@ -1228,7 +1275,13 @@ function to_download(app_name, url, size)
|
|||||||
|
|
||||||
sys.call("/bin/rm -f /tmp/".. app_name .."_download.*")
|
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
|
if size then
|
||||||
local kb1 = get_free_space("/tmp")
|
local kb1 = get_free_space("/tmp")
|
||||||
@@ -1240,7 +1293,7 @@ function to_download(app_name, url, size)
|
|||||||
local _curl_args = clone(curl_args)
|
local _curl_args = clone(curl_args)
|
||||||
table.insert(_curl_args, "--speed-limit 51200 --speed-time 15 --max-time 300")
|
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
|
result = return_code == 0
|
||||||
|
|
||||||
if not result then
|
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 }
|
return {code = 0, file = tmp_file, zip = com[app_name].zipped }
|
||||||
end
|
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)
|
function to_extract(app_name, file, subfix)
|
||||||
local result = check_path(app_name)
|
local result = check_path(app_name)
|
||||||
if result.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
@@ -1446,7 +1516,7 @@ function cacheFileCompareToLogic(file, str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function is_js_luci()
|
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
|
end
|
||||||
|
|
||||||
function set_default_cbi()
|
function set_default_cbi()
|
||||||
@@ -1519,6 +1589,16 @@ function set_default_cbi()
|
|||||||
return obj
|
return obj
|
||||||
end
|
end
|
||||||
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
|
if true then
|
||||||
--TextValue
|
--TextValue
|
||||||
local TextValue = cbi.TextValue
|
local TextValue = cbi.TextValue
|
||||||
|
|||||||
@@ -129,12 +129,54 @@ local com = self.com
|
|||||||
|
|
||||||
var appUpdateUrl = '<%=api.url("update_")%>' + app;
|
var appUpdateUrl = '<%=api.url("update_")%>' + app;
|
||||||
var appInfo = appInfoList[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
|
// Download file
|
||||||
XHR.get(appUpdateUrl, {
|
XHR.get(appUpdateUrl, {
|
||||||
token: tokenStr,
|
token: tokenStr,
|
||||||
url: appInfo ? appInfo.data.browser_download_url : '',
|
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) {
|
}, function (x, json) {
|
||||||
|
stopDownloadProgress();
|
||||||
if (json.code) {
|
if (json.code) {
|
||||||
removePageNotice();
|
removePageNotice();
|
||||||
onRequestError(btn, json.error);
|
onRequestError(btn, json.error);
|
||||||
@@ -233,4 +275,4 @@ local com = self.com
|
|||||||
});
|
});
|
||||||
//]]>
|
//]]>
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
-- Description: Passwall(2) UI template
|
-- Description: Passwall(2) UI template
|
||||||
|
|
||||||
local json = require "luci.jsonc"
|
local json = require "luci.jsonc"
|
||||||
|
local api = self.map.api
|
||||||
|
|
||||||
-- 读取值(keylist/vallist/group)
|
-- 读取值(keylist/vallist/group)
|
||||||
local values = {}
|
local values = {}
|
||||||
@@ -48,6 +49,30 @@ for _, item in ipairs(values) do
|
|||||||
end
|
end
|
||||||
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 not current_key then
|
||||||
if #ungrouped > 0 then
|
if #ungrouped > 0 then
|
||||||
@@ -73,19 +98,28 @@ local function find_label_by_key(k)
|
|||||||
end
|
end
|
||||||
local current_label = find_label_by_key(current_key) or ""
|
local current_label = find_label_by_key(current_key) or ""
|
||||||
|
|
||||||
-- 打包数据给 JS
|
-- 控件只携带分组引用,重复的候选数据在页面中仅输出一次
|
||||||
local dropdown_data = {
|
local dropdown_data = {
|
||||||
|
appname = api.appname,
|
||||||
current_key = current_key,
|
current_key = current_key,
|
||||||
current_label = current_label,
|
current_label = current_label,
|
||||||
ungrouped = ungrouped,
|
ungrouped = ungrouped,
|
||||||
groups = groups,
|
group_refs = group_refs,
|
||||||
group_order = group_order,
|
group_order = group_order,
|
||||||
cbid = cbid,
|
cbid = cbid,
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<script>
|
<script>
|
||||||
window.lv_dropdown_data = window.lv_dropdown_data || {};
|
(function() {
|
||||||
window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
const appname = <%=json.stringify(api.appname)%>;
|
||||||
|
window.lv_groups_pool = window.lv_groups_pool || Object.create(null);
|
||||||
|
const pool = window.lv_groups_pool[appname] || (window.lv_groups_pool[appname] = Object.create(null));
|
||||||
|
<% if next(new_groups) then %>
|
||||||
|
Object.assign(pool, <%=json.stringify(new_groups)%>);
|
||||||
|
<% end %>
|
||||||
|
window.lv_dropdown_data = window.lv_dropdown_data || {};
|
||||||
|
window.lv_dropdown_data[<%=json.stringify(cbid)%>] = <%=json.stringify(dropdown_data)%>;
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="<%=cbid%>.main" class="lv-dropdown-container">
|
<div id="<%=cbid%>.main" class="lv-dropdown-container">
|
||||||
@@ -98,7 +132,7 @@ window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
|||||||
<!-- 模拟 ListValue 控件外观 -->
|
<!-- 模拟 ListValue 控件外观 -->
|
||||||
<div class="cbi-input-value cbi-input-select lv-dropdown-display" id="<%=cbid%>.display" tabindex="0">
|
<div class="cbi-input-value cbi-input-select lv-dropdown-display" id="<%=cbid%>.display" tabindex="0">
|
||||||
<span id="<%=cbid%>.label" class="lv-dropdown-label" title="<%=pcdata(current_label)%>">
|
<span id="<%=cbid%>.label" class="lv-dropdown-label" title="<%=pcdata(current_label)%>">
|
||||||
<%=pcdata("("..translate("Not set")..")")%>
|
<%=pcdata("( "..translate("Not Set").." )")%>
|
||||||
</span>
|
</span>
|
||||||
<span class="lv-arrow-down"></span>
|
<span class="lv-arrow-down"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,7 +175,7 @@ window.lv_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
|||||||
});
|
});
|
||||||
lv_registerAdaptive(cbid);
|
lv_registerAdaptive(cbid);
|
||||||
setTimeout(() => {
|
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);
|
}, 100);
|
||||||
})();
|
})();
|
||||||
//]]>
|
//]]>
|
||||||
|
|||||||
@@ -207,7 +207,7 @@
|
|||||||
if (!rgb) return 'unknown'; // Handle invalid colors
|
if (!rgb) return 'unknown'; // Handle invalid colors
|
||||||
// Calculate YIQ brightness (human eye perception)
|
// Calculate YIQ brightness (human eye perception)
|
||||||
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
|
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
|
||||||
|
|
||||||
return brightness > 128 ? 'light' : 'dark';
|
return brightness > 128 ? 'light' : 'dark';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,7 +538,7 @@
|
|||||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||||
}[c]));
|
}[c]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function lv_change(cbid, listContainer, hiddenSelect, labelSpan, searchInput, new_key, new_text) {
|
function lv_change(cbid, listContainer, hiddenSelect, labelSpan, searchInput, new_key, new_text) {
|
||||||
//改值
|
//改值
|
||||||
hiddenSelect.options[0].value = new_key;
|
hiddenSelect.options[0].value = new_key;
|
||||||
@@ -549,7 +549,7 @@
|
|||||||
lv_highlightSelectedItem(listContainer, hiddenSelect);
|
lv_highlightSelectedItem(listContainer, hiddenSelect);
|
||||||
lv_updateGroupCounts(cbid, listContainer, hiddenSelect, searchInput);
|
lv_updateGroupCounts(cbid, listContainer, hiddenSelect, searchInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lv_render_dropdown_list(cbid, panel, listContainer, hiddenSelect, labelSpan, searchInput, display) {
|
function lv_render_dropdown_list(cbid, panel, listContainer, hiddenSelect, labelSpan, searchInput, display) {
|
||||||
if (window.lv_dropdown_rendered[cbid]) return;
|
if (window.lv_dropdown_rendered[cbid]) return;
|
||||||
const data = window.lv_dropdown_data[cbid];
|
const data = window.lv_dropdown_data[cbid];
|
||||||
@@ -577,8 +577,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分组项
|
// 分组项
|
||||||
data.group_order.forEach(gname => {
|
const groupPool = window.lv_groups_pool && window.lv_groups_pool[data.appname];
|
||||||
const items = data.groups[gname];
|
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 += `
|
html += `
|
||||||
<div class="lv-group" data-group="${lv_idSafe(gname)}" style="margin-bottom:8px;">
|
<div class="lv-group" data-group="${lv_idSafe(gname)}" style="margin-bottom:8px;">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
-- Description: Passwall(2) UI template
|
-- Description: Passwall(2) UI template
|
||||||
|
|
||||||
local json = require "luci.jsonc"
|
local json = require "luci.jsonc"
|
||||||
|
local api = self.map.api
|
||||||
|
|
||||||
-- 读取值(keylist/vallist/group)
|
-- 读取值(keylist/vallist/group)
|
||||||
local values = {{key = "", label = translate("-- Please choose --")}}
|
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})
|
table.insert(ungrouped, {key = current_key, label = current_key})
|
||||||
end
|
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 not current_key then
|
||||||
if #ungrouped > 0 then
|
if #ungrouped > 0 then
|
||||||
@@ -81,19 +106,28 @@ local function find_label_by_key(k)
|
|||||||
end
|
end
|
||||||
local current_label = find_label_by_key(current_key) or ""
|
local current_label = find_label_by_key(current_key) or ""
|
||||||
|
|
||||||
-- 打包数据给 JS
|
-- 控件只携带分组引用,重复的候选数据在页面中仅输出一次
|
||||||
local dropdown_data = {
|
local dropdown_data = {
|
||||||
|
appname = api.appname,
|
||||||
current_key = current_key,
|
current_key = current_key,
|
||||||
current_label = current_label,
|
current_label = current_label,
|
||||||
ungrouped = ungrouped,
|
ungrouped = ungrouped,
|
||||||
groups = groups,
|
group_refs = group_refs,
|
||||||
group_order = group_order,
|
group_order = group_order,
|
||||||
cbid = cbid,
|
cbid = cbid,
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<script>
|
<script>
|
||||||
window.v_dropdown_data = window.v_dropdown_data || {};
|
(function() {
|
||||||
window.v_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
const appname = <%=json.stringify(api.appname)%>;
|
||||||
|
window.v_groups_pool = window.v_groups_pool || Object.create(null);
|
||||||
|
const pool = window.v_groups_pool[appname] || (window.v_groups_pool[appname] = Object.create(null));
|
||||||
|
<% if next(new_groups) then %>
|
||||||
|
Object.assign(pool, <%=json.stringify(new_groups)%>);
|
||||||
|
<% end %>
|
||||||
|
window.v_dropdown_data = window.v_dropdown_data || {};
|
||||||
|
window.v_dropdown_data[<%=json.stringify(cbid)%>] = <%=json.stringify(dropdown_data)%>;
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="<%=cbid%>.v-main" class="v-dropdown-container">
|
<div id="<%=cbid%>.v-main" class="v-dropdown-container">
|
||||||
|
|||||||
@@ -202,7 +202,7 @@
|
|||||||
if (!rgb) return 'unknown'; // Handle invalid colors
|
if (!rgb) return 'unknown'; // Handle invalid colors
|
||||||
// Calculate YIQ brightness (human eye perception)
|
// Calculate YIQ brightness (human eye perception)
|
||||||
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
|
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
|
||||||
|
|
||||||
return brightness > 128 ? 'light' : 'dark';
|
return brightness > 128 ? 'light' : 'dark';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,8 +603,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分组项
|
// 分组项
|
||||||
data.group_order.forEach(gname => {
|
const groupPool = window.v_groups_pool && window.v_groups_pool[data.appname];
|
||||||
const items = data.groups[gname];
|
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 += `
|
html += `
|
||||||
<div class="v-group" data-group="${v_idSafe(gname)}" style="margin-bottom:8px;">
|
<div class="v-group" data-group="${v_idSafe(gname)}" style="margin-bottom:8px;">
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ check_run_environment() {
|
|||||||
[ -d "/lib/apk/packages" ] && { file_path="/lib/apk/packages"; file_ext=".list"; }
|
[ -d "/lib/apk/packages" ] && { file_path="/lib/apk/packages"; file_ext=".list"; }
|
||||||
|
|
||||||
if [ "$USE_TABLES" = "iptables" ]; then
|
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
|
else
|
||||||
dep_list="kmod-nft-socket kmod-nft-tproxy kmod-nft-nat"
|
dep_list="kmod-nft-socket kmod-nft-tproxy kmod-nft-nat"
|
||||||
nftflag=1
|
nftflag=1
|
||||||
@@ -923,6 +923,8 @@ start() {
|
|||||||
set_cache_var "USE_TABLES" "$USE_TABLES"
|
set_cache_var "USE_TABLES" "$USE_TABLES"
|
||||||
fi
|
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
|
if [ "$ENABLED_DEFAULT_ACL" == 1 ] || [ "$ENABLED_ACLS" == 1 ]; then
|
||||||
bridge_nf_ipt=$(sysctl -e -n net.bridge.bridge-nf-call-iptables)
|
bridge_nf_ipt=$(sysctl -e -n net.bridge.bridge-nf-call-iptables)
|
||||||
set_cache_var "bak_bridge_nf_ipt" "$bridge_nf_ipt"
|
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)
|
DIRECT_DNS_QUERY_STRATEGY=$(config_n_get @global[0] direct_dns_query_strategy UseIP)
|
||||||
|
|
||||||
get_direct_dns
|
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
|
DEFAULT_DNSMASQ_CONF_DIR=/tmp/dnsmasq.d
|
||||||
DNSMASQ_CONF_DIR=${DEFAULT_DNSMASQ_CONF_DIR}
|
DNSMASQ_CONF_DIR=${DEFAULT_DNSMASQ_CONF_DIR}
|
||||||
|
|||||||
@@ -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 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_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 balancingStrategy = api.uci_get_c("@global_haproxy[0]", "balancingStrategy") or "roundrobin"
|
||||||
local console_port = api.uci_get_c("@global_haproxy[0]", "console_port")
|
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"
|
local bind_local = api.uci_get_c("@global_haproxy[0]", "bind_local") or "0"
|
||||||
@@ -44,7 +44,7 @@ global
|
|||||||
nbthread {{nbthread}}
|
nbthread {{nbthread}}
|
||||||
external-check
|
external-check
|
||||||
insecure-fork-wanted
|
insecure-fork-wanted
|
||||||
spread-checks 50
|
spread-checks 20
|
||||||
tune.max-checks-per-thread 5
|
tune.max-checks-per-thread 5
|
||||||
|
|
||||||
defaults
|
defaults
|
||||||
@@ -99,7 +99,7 @@ api.uci_foreach_c("haproxy_config", function(t)
|
|||||||
local lbss = t.lbss
|
local lbss = t.lbss
|
||||||
local listen_port = tonumber(t.haproxy_port) or 0
|
local listen_port = tonumber(t.haproxy_port) or 0
|
||||||
local server_node = api.uci_get_c(lbss)
|
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
|
hop = hop and hop:gsub(":", "-") or nil
|
||||||
if server_node and server_node.address and (server_node.port or hop) then
|
if server_node and server_node.address and (server_node.port or hop) then
|
||||||
server_remark = server_node.address .. ":" .. (server_node.port or hop)
|
server_remark = server_node.address .. ":" .. (server_node.port or hop)
|
||||||
@@ -153,9 +153,12 @@ api.uci_foreach_c("haproxy_config", function(t)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local inter = tonumber((health_check_inter or "20"):match("^%d+"))
|
local inter = tonumber(health_check_inter:match("^%d+")) or 60
|
||||||
inter = (inter and inter >= 20) and inter or 20
|
inter = math.max(inter, 20)
|
||||||
local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s"
|
-- 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 = {}
|
local sortTable = {}
|
||||||
for i in pairs(listens) do
|
for i in pairs(listens) do
|
||||||
@@ -165,7 +168,28 @@ for i in pairs(listens) do
|
|||||||
end
|
end
|
||||||
table.sort(sortTable, function(a,b) return (a < b) 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))
|
log(1, api.i18n.translatef("Entrance %s:%s", bind_address, port))
|
||||||
|
|
||||||
f_out:write("\n" .. string.format([[
|
f_out:write("\n" .. string.format([[
|
||||||
@@ -184,7 +208,7 @@ listen %s
|
|||||||
|
|
||||||
local count_M, count_B = 1, 1
|
local count_M, count_B = 1, 1
|
||||||
for i, o in ipairs(listens[port]) do
|
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.
|
-- To prevent duplicate names from causing the program to fail to run.
|
||||||
if tostring(o.backup) ~= "1" then
|
if tostring(o.backup) ~= "1" then
|
||||||
remark = "M" .. count_M .. "-" .. remark
|
remark = "M" .. count_M .. "-" .. remark
|
||||||
@@ -193,8 +217,18 @@ listen %s
|
|||||||
remark = "B" .. count_B .. "-" .. remark
|
remark = "B" .. count_B .. "-" .. remark
|
||||||
count_B = count_B + 1
|
count_B = count_B + 1
|
||||||
end
|
end
|
||||||
local server = o.server_address .. ":" .. o.server_port
|
local server = server_endpoint(o)
|
||||||
local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}"
|
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("{{remark}}", remark)
|
||||||
server_conf = server_conf:gsub("{{server}}", server)
|
server_conf = server_conf:gsub("{{server}}", server)
|
||||||
server_conf = server_conf:gsub("{{weight}}", o.lbweight)
|
server_conf = server_conf:gsub("{{weight}}", o.lbweight)
|
||||||
@@ -203,7 +237,7 @@ listen %s
|
|||||||
resolvers = ""
|
resolvers = ""
|
||||||
end
|
end
|
||||||
server_conf = server_conf:gsub("{{resolvers}}", resolvers)
|
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 "")
|
server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "")
|
||||||
|
|
||||||
f_out:write(" " .. api.trim(server_conf) .. "\n")
|
f_out:write(" " .. api.trim(server_conf) .. "\n")
|
||||||
@@ -216,6 +250,20 @@ listen %s
|
|||||||
end
|
end
|
||||||
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
|
-- Console config
|
||||||
local console_user = api.uci_get_c("@global_haproxy[0]", "console_user")
|
local console_user = api.uci_get_c("@global_haproxy[0]", "console_user")
|
||||||
local console_password = api.uci_get_c("@global_haproxy[0]", "console_password")
|
local console_password = api.uci_get_c("@global_haproxy[0]", "console_password")
|
||||||
@@ -234,8 +282,7 @@ f_out:close()
|
|||||||
|
|
||||||
-- Built-in health check URL
|
-- Built-in health check URL
|
||||||
if health_check_type == "script_logic" then
|
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 probeUrl = api.trim(api.uci_get_c("@global_haproxy[0]", "health_probe_url") or "")
|
||||||
local f_url = io.open(haproxy_path .. "/Probe_URL", "w")
|
if probeUrl == "" then probeUrl = "https://www.google.com/generate_204" end
|
||||||
f_url:write(probeUrl)
|
api.set_cache_var("HAPROXY_PROBE_URL", probeUrl)
|
||||||
f_url:close()
|
|
||||||
end
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
export PATH=/usr/sbin:/usr/bin:/sbin:/bin:/root/bin
|
export PATH=/usr/sbin:/usr/bin:/sbin:/bin:/root/bin
|
||||||
CONFIG=passwall2
|
. /usr/share/passwall2/utils.sh
|
||||||
|
|
||||||
listen_address=$1
|
listen_address=$1
|
||||||
listen_port=$2
|
listen_port=$2
|
||||||
@@ -13,19 +13,16 @@ busybox pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|ipt
|
|||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
probe_file="/tmp/etc/${CONFIG}/haproxy/Probe_URL"
|
probeUrl=$(get_cache_var "HAPROXY_PROBE_URL")
|
||||||
probeUrl="https://www.google.com/generate_204"
|
probeUrl="${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
|
|
||||||
|
|
||||||
extra_params="-x socks5h://${server_address}:${server_port}"
|
case "$server_address" in
|
||||||
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
|
\[*\]) ;;
|
||||||
extra_params="${extra_params} --retry-all-errors"
|
*:*) server_address="[$server_address]" ;;
|
||||||
fi
|
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
|
case "$status" in
|
||||||
200|204)
|
200|204)
|
||||||
|
|||||||
@@ -851,6 +851,11 @@ add_firewall_rule() {
|
|||||||
done
|
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"
|
log_i18n 0 "%s firewall rules load complete!" "iptables"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,7 +901,7 @@ gen_include() {
|
|||||||
[ -z "${_ipt}" ] && return
|
[ -z "${_ipt}" ] && return
|
||||||
|
|
||||||
echo "*$2"
|
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'
|
echo 'COMMIT'
|
||||||
}
|
}
|
||||||
local __ipt=""
|
local __ipt=""
|
||||||
@@ -908,6 +913,8 @@ gen_include() {
|
|||||||
$(extract_rules 4 nat)
|
$(extract_rules 4 nat)
|
||||||
$(extract_rules 4 mangle)
|
$(extract_rules 4 mangle)
|
||||||
EOT
|
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")
|
[ "$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")
|
[ -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 nat)
|
||||||
$(extract_rules 6 mangle)
|
$(extract_rules 6 mangle)
|
||||||
EOT
|
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
|
[ "$accept_icmpv6" = "1" ] && $ip6t_n -A PREROUTING $(dst $IPSET_DIRECT6 !) -p ipv6-icmp -j PSW2
|
||||||
|
|
||||||
|
|||||||
@@ -902,6 +902,9 @@ add_firewall_rule() {
|
|||||||
|
|
||||||
filter_direct_node_list > /dev/null 2>&1 &
|
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"
|
log_i18n 0 "%s firewall rules load complete!" "nftables"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# Copyright (C) 2026 Openwrt-Passwall Organization
|
# Copyright (C) 2026 Openwrt-Passwall Organization
|
||||||
|
|
||||||
CONFIG=passwall2
|
CONFIG=passwall2
|
||||||
|
DIRECT_GID=9912
|
||||||
APP_PATH=/usr/share/${CONFIG}
|
APP_PATH=/usr/share/${CONFIG}
|
||||||
TMP_PATH=/tmp/etc/${CONFIG}
|
TMP_PATH=/tmp/etc/${CONFIG}
|
||||||
TMP_PATH2=${TMP_PATH}_tmp
|
TMP_PATH2=${TMP_PATH}_tmp
|
||||||
|
|||||||
@@ -268,14 +268,6 @@ return view.extend({
|
|||||||
o.default = false;
|
o.default = false;
|
||||||
o.depends("autorate","1");
|
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
|
// cake-autorate types these by whether the value carries a decimal
|
||||||
// point (typeof float vs integer) and refuses to start an instance
|
// point (typeof float vs integer) and refuses to start an instance
|
||||||
// whose value has the wrong type, so always persist a float here.
|
// 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);
|
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 = 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.datatype = 'ufloat';
|
||||||
o.write = writeFloat;
|
o.write = writeFloat;
|
||||||
o.depends("autorate","1");
|
o.depends("autorate","1");
|
||||||
@@ -307,8 +313,9 @@ return view.extend({
|
|||||||
o.depends("autorate","1");
|
o.depends("autorate","1");
|
||||||
|
|
||||||
o = s.taboption("tab_autorate", form.Value, "startup_wait_s",_("Number of seconds to wait on startup:"));
|
o = s.taboption("tab_autorate", form.Value, "startup_wait_s",_("Number of seconds to wait on startup:"));
|
||||||
o.default = "60.0";
|
o.default = "0.0";
|
||||||
o.datatype = 'float'
|
o.datatype = 'float';
|
||||||
|
o.write = writeFloat;
|
||||||
o.depends("autorate","1");
|
o.depends("autorate","1");
|
||||||
|
|
||||||
o = s.taboption("tab_autorate", form.Flag, "congestion_sync", _("Sync bufferbloat threshold with congestion score"),
|
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.
|
// off; without retain LuCI deletes it with the other hidden fields.
|
||||||
o.retain = true;
|
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();
|
return m.render();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-vantage
|
PKG_NAME:=luci-app-vantage
|
||||||
PKG_VERSION:=1.0.4
|
PKG_VERSION:=1.0.5
|
||||||
PKG_RELEASE:=10
|
PKG_RELEASE:=11
|
||||||
PKG_LICENSE:=GPL-3.0-or-later
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
|
|
||||||
LUCI_TITLE:=Vantage dashboard
|
LUCI_TITLE:=Vantage dashboard
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
--vt-line-2: var(--v-line-2, #cbd0d6);
|
--vt-line-2: var(--v-line-2, #cbd0d6);
|
||||||
--vt-text: var(--v-text, #15181c);
|
--vt-text: var(--v-text, #15181c);
|
||||||
--vt-text-2: var(--v-text-2, #4b525b);
|
--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: var(--v-accent, #3563e9);
|
||||||
--vt-accent-ink: var(--v-accent-ink, #ffffff);
|
--vt-accent-ink: var(--v-accent-ink, #ffffff);
|
||||||
--vt-accent-soft: var(--v-accent-soft, rgba(53, 99, 233, .12));
|
--vt-accent-soft: var(--v-accent-soft, rgba(53, 99, 233, .12));
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
--vt-b5: var(--vt-chart-3);
|
--vt-b5: var(--vt-chart-3);
|
||||||
--vt-b6: var(--vt-chart-4);
|
--vt-b6: var(--vt-chart-4);
|
||||||
--vt-b60: var(--vt-text-2);
|
--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);
|
--vt-series-up: var(--vt-chart-1);
|
||||||
/* throughput directions: download keeps the uplink colour, upload
|
/* throughput directions: download keeps the uplink colour, upload
|
||||||
takes one that no band uses */
|
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-6: var(--v-chart-6, #d9588c);
|
||||||
--vt-chart-grid: var(--v-chart-grid, color-mix(in srgb, var(--vt-text) 7%, transparent));
|
--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-fill-op: var(--v-chart-fill-opacity, .2);
|
||||||
|
--vt-band-ink: 78%;
|
||||||
--vt-av-l: 24%;
|
--vt-av-l: 24%;
|
||||||
--vt-av-s: 30%;
|
--vt-av-s: 30%;
|
||||||
--vt-av-ink-l: 76%;
|
--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;
|
border-radius: 5px; font-size: var(--vt-fs-micro); font-weight: 700; letter-spacing: .01em; white-space: nowrap;
|
||||||
--vt-bc: var(--vt-b60);
|
--vt-bc: var(--vt-b60);
|
||||||
background: color-mix(in oklab, var(--vt-bc) 14%, transparent);
|
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-2g { --vt-bc: var(--vt-b2); }
|
||||||
.vt-app .vt-band-5g { --vt-bc: var(--vt-b5); }
|
.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; }
|
.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 {
|
.vt-app.vt-editing .vt-ov {
|
||||||
display: block; position: absolute; inset: 0; z-index: 4;
|
display: block; position: absolute; inset: 0; z-index: 4;
|
||||||
border: 2px dashed var(--vt-accent-line); border-radius: var(--vt-r-lg);
|
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-subline { color: var(--vt-text-3); font-size: var(--vt-fs-sm); margin-top: 2px; }
|
||||||
|
|
||||||
.vt-app .vt-livepill {
|
.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;
|
height: 30px; padding: 0 12px 0 10px; border-radius: 999px;
|
||||||
border: 1px solid var(--vt-line); background: var(--vt-surface);
|
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;
|
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-beat { position: relative; width: 8px; height: 8px; border-radius: 50%; background: var(--vt-beat-c, var(--vt-ok)); }
|
||||||
.vt-app .vt-live-paused .vt-live-beat { background: var(--vt-text-3); animation: none; }
|
/* the beat is a ring that scales and fades (compositor only; an animated
|
||||||
.vt-app .vt-live-err .vt-live-beat { background: var(--vt-warn); }
|
box-shadow repainted every frame). It sits behind the dot (z-index -1 in
|
||||||
@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; } }
|
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 {
|
.vt-app .vt-health {
|
||||||
display: grid; gap: 10px; margin-bottom: 14px;
|
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 { 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-main { stroke-width: 2.5; }
|
||||||
.vt-app .vt-wire-on { stroke: color-mix(in oklab, var(--vt-accent-line) 40%, var(--vt-line-2)); }
|
.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 {
|
.vt-app .vt-flow {
|
||||||
fill: none; stroke: var(--vt-accent-line); stroke-width: 2.4; stroke-linecap: round;
|
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-flows, .vt-app .vt-fwire { position: absolute; left: 0; top: 0; width: 0; height: 0; z-index: 0; pointer-events: none; }
|
||||||
.vt-app .vt-flow-2.vt-flow-on { animation-duration: 1.05s; }
|
.vt-app .vt-fwire { opacity: 0; transition: opacity .4s; }
|
||||||
.vt-app .vt-flow-3.vt-flow-on { animation-duration: .6s; stroke-width: 3; }
|
.vt-app .vt-fwire-on { opacity: .95; }
|
||||||
@keyframes vt-flow { to { stroke-dashoffset: -32; } }
|
/* 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 {
|
.vt-app .vt-node {
|
||||||
position: relative; z-index: 1;
|
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-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-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 { 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-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-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-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-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 { 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-used { background: var(--vt-mem); border-radius: 6px 2px 2px 6px; }
|
||||||
.vt-app .vt-seg-cache {
|
.vt-app .vt-seg-cache {
|
||||||
background:
|
background:
|
||||||
@@ -704,7 +732,7 @@ h2, h3{
|
|||||||
.vt-app .vt-storage { display: flex; flex-direction: column; gap: 8px; }
|
.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-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 { 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-warn { background: var(--vt-warn); }
|
||||||
.vt-app .vt-bg-err { background: var(--vt-err); }
|
.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); }
|
.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-cell { display: grid; grid-template-columns: auto; gap: 3px; min-width: 96px; }
|
||||||
.vt-app .vt-live-total { font-weight: 650; }
|
.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 { 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-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-exp { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
||||||
.vt-app .vt-score {
|
.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-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-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-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-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; }
|
.vt-app .vt-ssids { display: flex; flex-direction: column; gap: 2px; }
|
||||||
@@ -1092,7 +1120,12 @@ h2, h3{
|
|||||||
reconnecting still says so in words */
|
reconnecting still says so in words */
|
||||||
.vt-app .vt-live-ok { padding: 0 11px; }
|
.vt-app .vt-live-ok { padding: 0 11px; }
|
||||||
.vt-app .vt-live-ok .vt-live-text { display: none; }
|
.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-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-health { grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||||
.vt-app .vt-chip { padding: 9px 10px; gap: 8px; }
|
.vt-app .vt-chip { padding: 9px 10px; gap: 8px; }
|
||||||
@@ -1163,5 +1196,7 @@ h2, h3{
|
|||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.vt-app *, .vt-app *::before, .vt-app *::after { animation: none !important; transition: none !important; }
|
.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; }
|
||||||
}
|
}
|
||||||
@@ -187,6 +187,19 @@ return baseclass.extend({
|
|||||||
return { h: prev.h, since: since };
|
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;
|
/* where shown tiles go. items: [{ id, c, w, h }] in order, h measured;
|
||||||
returns { rects: { id: { c, w, top, h } }, height }. A tile that
|
returns { rects: { id: { c, w, top, h } }, height }. A tile that
|
||||||
ends a little above the next one in its columns stretches to meet
|
ends a little above the next one in its columns stretches to meet
|
||||||
|
|||||||
@@ -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]); }
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
'require vantage.insight as insight';
|
'require vantage.insight as insight';
|
||||||
'require vantage.series as series';
|
'require vantage.series as series';
|
||||||
'require vantage.layout as layout';
|
'require vantage.layout as layout';
|
||||||
|
'require vantage.wires as wires';
|
||||||
|
|
||||||
/* Vantage dashboard (luci-app-vantage): a live, read-only overview of the
|
/* Vantage dashboard (luci-app-vantage): a live, read-only overview of the
|
||||||
device, its uplink, radios, SSIDs and stations. The only write is 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 */
|
/* -------------------------------------------------------------- helpers */
|
||||||
|
|
||||||
function txt(el, s) { el.textContent = (s == null ? '' : String(s)); return el; }
|
/* text writes only when the text changes: an unchanged poll leaves the
|
||||||
function setW(el, pct) { el.style.width = Math.max(0, Math.min(100, +pct || 0)).toFixed(2) + '%'; }
|
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) {
|
function hueOf(mac) {
|
||||||
var h = 0, s = String(mac || '');
|
var h = 0, s = String(mac || '');
|
||||||
for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360;
|
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) {
|
function tileHead(ic, title, extra) {
|
||||||
return E('header', { 'class': 'vt-tile-head' }, [
|
return E('header', { 'class': 'vt-tile-head' }, [
|
||||||
E('h3', { 'class': 'vt-tile-title' }, [ icon(ic), E('span', {}, [ title ]) ]),
|
E('h3', { 'class': 'vt-tile-title' }, [ icon(ic), E('span', {}, [ title ]) ]),
|
||||||
@@ -702,6 +739,10 @@ return view.extend({
|
|||||||
this.buildDrawer()
|
this.buildDrawer()
|
||||||
]);
|
]);
|
||||||
this.el.root = root;
|
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();
|
this.paint();
|
||||||
|
|
||||||
@@ -712,15 +753,23 @@ return view.extend({
|
|||||||
});
|
});
|
||||||
document.addEventListener('keydown', L.bind(this.handleKey, this));
|
document.addEventListener('keydown', L.bind(this.handleKey, this));
|
||||||
window.setInterval(function() { self.paintLive(); }, 1000);
|
window.setInterval(function() { self.paintLive(); }, 1000);
|
||||||
if (window.ResizeObserver) new ResizeObserver(function() { self.scheduleWires(); }).observe(this.el.path);
|
|
||||||
if (window.ResizeObserver) {
|
if (window.ResizeObserver) {
|
||||||
var ro = new ResizeObserver(function() { self.scheduleLayout(); });
|
var ro = new ResizeObserver(function() { self.scheduleLayout(); });
|
||||||
ro.observe(this.el.grid);
|
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('resize', function() { self.scheduleLayout(); });
|
||||||
window.addEventListener('hashchange', function() { self.openFromHash(); });
|
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;
|
return root;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -737,8 +786,11 @@ return view.extend({
|
|||||||
this.paintSsids();
|
this.paintSsids();
|
||||||
this.paintDrawer();
|
this.paintDrawer();
|
||||||
this.paintLive();
|
this.paintLive();
|
||||||
this.scheduleWires();
|
if (!this.wireRo) this.scheduleWires();
|
||||||
this.scheduleLayout();
|
/* 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 */
|
/* ------------------------------------------------------------ folds */
|
||||||
@@ -769,7 +821,7 @@ return view.extend({
|
|||||||
writeFolds(this.folds);
|
writeFolds(this.folds);
|
||||||
this.applyFold(key);
|
this.applyFold(key);
|
||||||
if (this.held) delete this.held[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();
|
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.setProperty('--w', Math.min(e.w, cols).toFixed(0));
|
||||||
t.style.minHeight = '';
|
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
|
/* live content changes hold a tile's height for a while; a new
|
||||||
width or an edit shows true sizes at once */
|
width or an edit shows true sizes at once */
|
||||||
if (this.heldWidth !== width || this.drag) { this.held = Object.create(null); this.heldWidth = width; }
|
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');
|
t.style.setProperty('--y', r.top.toFixed(1) + 'px');
|
||||||
});
|
});
|
||||||
grid.style.height = p.height.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;
|
var g = this.drag && p.rects[this.drag.id], ghost = this.el.ghost;
|
||||||
ghost.classList.toggle('vt-on', !!g);
|
ghost.classList.toggle('vt-on', !!g);
|
||||||
if (g) {
|
if (g) {
|
||||||
@@ -915,10 +976,11 @@ return view.extend({
|
|||||||
this.el.root.classList.toggle('vt-editing', this.editing);
|
this.el.root.classList.toggle('vt-editing', this.editing);
|
||||||
this.el.layoutBtn.setAttribute('aria-pressed', String(this.editing));
|
this.el.layoutBtn.setAttribute('aria-pressed', String(this.editing));
|
||||||
this.el.editBar.hidden = !this.editing;
|
this.el.editBar.hidden = !this.editing;
|
||||||
if (this.editing) {
|
/* the handle bars change the tiles' heights both ways: true sizes
|
||||||
this.doLayout();
|
at once, nothing held from the other mode */
|
||||||
this.el.editDone.focus();
|
this.held = Object.create(null);
|
||||||
}
|
this.doLayout();
|
||||||
|
if (this.editing) this.el.editDone.focus();
|
||||||
else this.el.layoutBtn.focus();
|
else this.el.layoutBtn.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -985,7 +1047,7 @@ return view.extend({
|
|||||||
self.el.editDone.focus();
|
self.el.editDone.focus();
|
||||||
});
|
});
|
||||||
o.root = E('div', { 'class': 'vt-ov vt-fold-keep', 'role': 'group', 'aria-label': name }, [
|
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,
|
o.grip,
|
||||||
E('span', { 'class': 'vt-ov-name' }, [ name ]),
|
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 ])
|
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() {
|
paintHealth: function() {
|
||||||
var self = this;
|
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) {
|
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 ic = c.level === 'ok' ? 'check' : c.level === 'err' ? 'err' : c.level === 'warn' ? 'warn' : 'info';
|
||||||
var body = [
|
var body = [
|
||||||
@@ -1164,11 +1230,15 @@ return view.extend({
|
|||||||
/* ------------------------------------------------------------- hero */
|
/* ------------------------------------------------------------- hero */
|
||||||
|
|
||||||
buildHero: function() {
|
buildHero: function() {
|
||||||
var self = this, el = this.el;
|
var el = this.el;
|
||||||
el.path = E('div', { 'class': 'vt-path' });
|
el.path = E('div', { 'class': 'vt-path' });
|
||||||
el.wires = svgRoot('vt-wires', '0 0 10 10');
|
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);
|
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' });
|
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') }, [
|
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')) ]),
|
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() {
|
/* the nodes the path shows: rebuilt only when this changes (a radio,
|
||||||
var self = this, m = this.m, st = this.st, up = m.uplink, path = this.el.path;
|
SSID or network comes or goes); a poll updates text in place */
|
||||||
var up_r = st.uplinkRate;
|
heroKey: function() {
|
||||||
|
var m = this.m;
|
||||||
/* keep the wires layer, rebuild the nodes (cheap, a dozen elements) */
|
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');
|
||||||
while (path.lastChild && path.lastChild !== this.el.wires) path.removeChild(path.lastChild);
|
return 'r\n' + m.radios.map(function(r) {
|
||||||
while (path.firstChild && path.firstChild !== this.el.wires) path.removeChild(path.firstChild);
|
return r.id + '=' + m.ssids.filter(function(s) { return s.radio === r.id; }).map(function(s) { return s.id; }).join(',');
|
||||||
|
}).join('\n');
|
||||||
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();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* wired-only: the device's other networks take the radios' place,
|
paintHero: function() {
|
||||||
wired into the path the same way (nodes.radios) */
|
var key = this.heroKey();
|
||||||
netBranches: function(branches) {
|
if (!this.hero || this.hero.key !== key) this.buildHeroNodes(key);
|
||||||
var st = this.st, self = this;
|
this.updateHero();
|
||||||
var nets = this.m.networks.filter(function(n) { return n.up; }).slice(0, 8);
|
this.paintKpis();
|
||||||
nets.forEach(function(n) {
|
this.paintWireState();
|
||||||
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(' · ') }, [
|
buildHeroNodes: function(key) {
|
||||||
E('span', { 'class': 'vt-node-body' }, [
|
var self = this, m = this.m, path = this.el.path;
|
||||||
E('span', { 'class': 'vt-node-kicker' }, [ n.proto || _('Interface') ]),
|
function span(cls) { return E('span', { 'class': cls }); }
|
||||||
E('span', { 'class': 'vt-node-title' }, [ n.name ]),
|
var h = { key: key, radios: [] };
|
||||||
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 ])
|
/* 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);
|
||||||
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 ]));
|
|
||||||
|
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
|
/* 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() {
|
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;
|
var self = this, m = this.m, st = this.st, up = st.uplinkRate;
|
||||||
if (m.wired) return this.paintNetKpis();
|
|
||||||
var byBand = {};
|
var byBand = {};
|
||||||
m.clients.forEach(function(c) { byBand[c.band] = (byBand[c.band] || 0) + 1; });
|
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 bandText = Object.keys(byBand).sort().map(function(b) { return _('%d on %s').format(byBand[b], wifi.bandLabel(b)); }).join(' · ');
|
||||||
var busiest = null;
|
var busiest = null;
|
||||||
m.radios.forEach(function(r) { var i = st.radioInsights[r.id]; if (i && i.busiest) busiest = { r: r, share: i.share }; });
|
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];
|
var weakest = m.clients.filter(function(c) { return c.signal != null; }).sort(function(a, b) { return a.signal - b.signal; })[0];
|
||||||
|
return [
|
||||||
dom.content(this.el.kpis, [
|
{ label: _('Clients'), value: String(m.clients.length), sub: bandText || _('none connected'), tone: 'plain',
|
||||||
kpi(_('Clients'), String(m.clients.length), bandText || _('none connected'), 'plain', function() { self.el.clients.scrollIntoView({ behavior: reducedMotion() ? 'auto' : 'smooth', block: 'start' }); }),
|
act: 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'); }),
|
{ 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',
|
||||||
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); })
|
act: function() { self.openDrawer('uplink', 'uplink'); } },
|
||||||
: kpi(_('Busiest radio'), fmt.DASH, _('no Wi-Fi traffic yet'), 'plain'),
|
busiest ? { label: _('Busiest radio'), value: busiest.r.bandLabel, sub: _('%s of Wi-Fi traffic').format(fmt.pct(busiest.share * 100)), tone: 'plain',
|
||||||
weakest ? kpi(_('Weakest signal'), fmt.dbm(weakest.signal), weakest.name, weakest.signal < insight.WEAK_DBM ? 'warn' : 'plain', function() { self.openDrawer('client', weakest.mac); })
|
act: function() { self.openDrawer('radio', busiest.r.id); } }
|
||||||
: kpi(_('Weakest signal'), fmt.DASH, _('no clients'), 'plain')
|
: { 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
|
/* wired-only: what flows, how long the uplink has held, which
|
||||||
networks are up and which is busiest */
|
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 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 nets = m.networks, upNets = nets.filter(function(n) { return n.up; });
|
||||||
var busiest = null;
|
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 };
|
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); }
|
function toNetwork() { window.location.href = L.url.apply(L, insight.PAGES.network); }
|
||||||
dom.content(this.el.kpis, [
|
function toUplink() { self.openDrawer('uplink', 'uplink'); }
|
||||||
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'); }),
|
return [
|
||||||
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'); }),
|
{ 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 },
|
||||||
kpi(_('Networks'), String(upNets.length), nets.length > upNets.length ? _('%d down').format(nets.length - upNets.length) : _('all up'), 'plain', toNetwork),
|
{ 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 },
|
||||||
busiest ? kpi(_('Busiest network'), busiest.n.name, '↓ ' + fmt.bits(busiest.r.tx) + ' ↑ ' + fmt.bits(busiest.r.rx), 'plain', toNetwork)
|
{ label: _('Networks'), value: String(upNets.length), sub: nets.length > upNets.length ? _('%d down').format(nets.length - upNets.length) : _('all up'), tone: 'plain', act: toNetwork },
|
||||||
: kpi(_('Busiest network'), fmt.DASH, _('no traffic yet'), 'plain')
|
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() {
|
scheduleWires: function() {
|
||||||
@@ -1332,63 +1505,141 @@ return view.extend({
|
|||||||
window.requestAnimationFrame(function() { self.wirePending = false; self.drawWires(); });
|
window.requestAnimationFrame(function() { self.wirePending = false; self.drawWires(); });
|
||||||
},
|
},
|
||||||
|
|
||||||
/* connectors between the hero nodes, measured after layout; wires are
|
/* the wires the nodes call for: key, ends, and how busy */
|
||||||
kept by key so the flow animation does not restart every poll */
|
wireList: function() {
|
||||||
drawWires: function() {
|
var h = this.hero, out = [];
|
||||||
var path = this.el && this.el.path, svg = this.el && this.el.wires, n = this.nodes;
|
if (!h) return out;
|
||||||
if (!path || !svg || !n || !path.isConnected) return;
|
out.push({ key: 'gw', from: h.gw, to: h.ap, active: h.upActive, speed: wires.speed(h.upRate), main: true });
|
||||||
var box = path.getBoundingClientRect();
|
h.radios.forEach(function(b) {
|
||||||
if (!box.width) return;
|
out.push({ key: 'r:' + b.id, from: h.ap, to: b.el, active: b.active, speed: wires.speed(b.rate) });
|
||||||
svg.setAttribute('viewBox', '0 0 ' + Math.round(box.width) + ' ' + Math.round(box.height));
|
b.ssids.forEach(function(sn) { out.push({ key: 's:' + b.id + ':' + sn.id, from: b.el, to: sn.el, active: sn.active, speed: 1 }); });
|
||||||
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 }); });
|
|
||||||
});
|
});
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
var seen = {}, map = this.wireMap;
|
/* connectors between the hero nodes, measured after layout (called
|
||||||
want.forEach(function(w) {
|
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;
|
seen[w.key] = true;
|
||||||
var g = map[w.key];
|
var g = map[w.key];
|
||||||
if (!g) {
|
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.base);
|
||||||
svg.appendChild(g.flow);
|
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) {
|
Object.keys(map).forEach(function(k) {
|
||||||
if (seen[k]) return;
|
if (seen[k]) return;
|
||||||
svg.removeChild(map[k].base);
|
var g = map[k];
|
||||||
svg.removeChild(map[k].flow);
|
self.clearDots(g);
|
||||||
|
svg.removeChild(g.base);
|
||||||
|
svg.removeChild(g.flow);
|
||||||
|
self.el.flows.removeChild(g.fw);
|
||||||
delete map[k];
|
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 */
|
/* ----------------------------------------------------------- system */
|
||||||
@@ -1434,8 +1685,8 @@ return view.extend({
|
|||||||
txt(el.cpuVal, cpu ? Math.round(cpu.pct) + '%' : fmt.DASH);
|
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' : '');
|
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(' · ')));
|
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%'; } }) ]);
|
patch(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.cpuCores, (cpu ? cpu.cores : []).map(function(p, i) {
|
||||||
var fill = E('span', { 'class': 'vt-core-fill' });
|
var fill = E('span', { 'class': 'vt-core-fill' });
|
||||||
fill.style.height = Math.max(4, Math.min(100, p || 0)).toFixed(1) + '%';
|
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 ]);
|
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 */
|
/* memory: one bar, three named segments */
|
||||||
var mem = d.memory;
|
var mem = d.memory;
|
||||||
if (mem) {
|
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-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)) ])
|
E('span', { 'class': 'vt-mem-of vt-tn' }, [ _('%s of %s').format(fmt.bytes(mem.available), fmt.bytes(mem.total)) ])
|
||||||
]);
|
]);
|
||||||
if (!el.memSegs) {
|
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; });
|
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(', '));
|
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) ]) ]);
|
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 {
|
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) {
|
if (d.swap) {
|
||||||
var sb = E('span', { 'class': 'vt-thin-fill' });
|
var sb = E('span', { 'class': 'vt-thin-fill' });
|
||||||
setW(sb, d.swap.usedPct);
|
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)) ]) ]);
|
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' : '') });
|
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));
|
setW(f, Math.max(s.usedPct, 0.8));
|
||||||
return E('div', { 'class': 'vt-mount' }, [
|
return E('div', { 'class': 'vt-mount' }, [
|
||||||
@@ -1476,7 +1732,7 @@ return view.extend({
|
|||||||
E('div', { 'class': 'vt-bar' }, [ f ])
|
E('div', { 'class': 'vt-bar' }, [ f ])
|
||||||
]);
|
]);
|
||||||
}));
|
}));
|
||||||
dom.content(el.fw, [
|
patch(el.fw, [
|
||||||
E('span', {}, [ d.firmware || '' ]),
|
E('span', {}, [ d.firmware || '' ]),
|
||||||
d.kernel ? E('span', { 'class': 'vt-hint' }, [ _('Kernel %s').format(d.kernel) ]) : ''
|
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.') ]) ]) ]);
|
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;
|
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 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) : '' });
|
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) {
|
if (sp) {
|
||||||
@@ -1742,7 +1998,7 @@ return view.extend({
|
|||||||
}
|
}
|
||||||
var hist = st.hist.net[r.id] || [];
|
var hist = st.hist.net[r.id] || [];
|
||||||
var clients = m.clients.filter(function(c) { return c.radio === 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' }, [
|
E('header', { 'class': 'vt-radio-head' }, [
|
||||||
bandBadge(r.band),
|
bandBadge(r.band),
|
||||||
E('span', { 'class': 'vt-radio-name' }, [ r.gen || r.id ]),
|
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([ '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 ]);
|
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';
|
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' : ''),
|
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(); } }, [
|
'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]) ])
|
E('span', {}, [ d[1] ]), E('span', { 'class': 'vt-fchip-n vt-tn' }, [ String(d[2]) ])
|
||||||
]);
|
]);
|
||||||
}));
|
}));
|
||||||
|
this.chipSig = chipSig;
|
||||||
el.filterClear.hidden = !ui.filter;
|
el.filterClear.hidden = !ui.filter;
|
||||||
|
|
||||||
/* header with sort buttons */
|
/* header with sort buttons */
|
||||||
var cols = [ [ 'name', _('Device') ], [ 'signal', _('Signal') ], [ 'ssid', _('Network') ], [ 'rate', _('Link rate') ], [ 'live', _('Live') ], [ 'exp', _('Experience') ], [ 'time', _('Online') ] ];
|
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];
|
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' }, [
|
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() {
|
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 ? '↑' : '↓') : '' ]) ])
|
} }, [ 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));
|
var list = m.clients.filter(L.bind(this.matches, this));
|
||||||
list.sort(function(a, b) {
|
list.sort(function(a, b) {
|
||||||
@@ -1886,7 +2147,8 @@ return view.extend({
|
|||||||
var maxRate = 0;
|
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; });
|
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) {
|
list.concat(gone).forEach(function(c) {
|
||||||
var key = (c.gone ? 'gone:' : '') + c.mac;
|
var key = (c.gone ? 'gone:' : '') + c.mac;
|
||||||
keep.add(key);
|
keep.add(key);
|
||||||
@@ -1901,13 +2163,18 @@ return view.extend({
|
|||||||
tr.classList.toggle('vt-row-gone', !!c.gone);
|
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.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));
|
tr.setAttribute('aria-label', _('Open details of %s').format(c.name));
|
||||||
dom.content(tr, self.clientCells(c, maxRate));
|
/* a row that would come out the same keeps its cells, and rows
|
||||||
el.tbody.appendChild(tr);
|
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) {
|
this.rowMap.forEach(function(tr, key) {
|
||||||
if (keep.has(key)) return;
|
if (keep.has(key)) return;
|
||||||
if (tr.parentNode) tr.parentNode.removeChild(tr);
|
if (tr.parentNode) tr.parentNode.removeChild(tr);
|
||||||
self.rowMap.delete(key);
|
self.rowMap.delete(key);
|
||||||
|
self.rowSig.delete(key);
|
||||||
});
|
});
|
||||||
window.setTimeout(function() { self.rowMap.forEach(function(tr) { tr.classList.remove('vt-row-enter'); }); }, 600);
|
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.') ]) ]);
|
dom.content(this.el.ssids, [ E('p', { 'class': 'vt-empty-inline' }, [ _('No wireless networks configured.') ]) ]);
|
||||||
return;
|
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;
|
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; } });
|
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); });
|
var bands = g.bands.map(function(b) { return bandBadge(b); });
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -22,6 +22,20 @@
|
|||||||
local current_omr_version = luci.model.uci.cursor():get("openmptcprouter","settings","version") or ""
|
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 ""
|
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
|
||||||
|
|
||||||
-%>
|
-%>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="<%=luci.i18n.context.lang%>">
|
<html lang="<%=luci.i18n.context.lang%>">
|
||||||
@@ -46,7 +60,7 @@
|
|||||||
<script src="<%=resource%>/cbi.js"></script>
|
<script src="<%=resource%>/cbi.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="lang_<%=luci.i18n.context.lang%> <% if node then %><%= striptags( node.title ) %><%- end %>" data-page="<%= table.concat(disp.context.requestpath, "-") %>">
|
<body class="lang_<%=luci.i18n.context.lang%> <% if node then %><%= striptags( node.title ) %><%- end %>" data-page="<%= pcdata(table.concat(disp.context.requestpath, "-")) %>">
|
||||||
<header>
|
<header>
|
||||||
<div class="fill">
|
<div class="fill">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -67,9 +81,9 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<%- 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 -%>
|
||||||
<div class="alert-message notice">
|
<div class="alert-message notice">
|
||||||
<p><%=translatef("You have OpenMPTCProuter version %s and version %s is now available",current_omr_version,latest_omr_version)%></p>
|
<p><%=translatef("You have OpenMPTCProuter version %s and version %s is now available",pcdata(current_omr_version),pcdata(latest_omr_version))%></p>
|
||||||
<div class="right"><a class="btn" href="https://www.openmptcprouter.com/"><%:Download latest version...%></a></div>
|
<div class="right"><a class="btn" href="https://www.openmptcprouter.com/"><%:Download latest version...%></a></div>
|
||||||
</div>
|
</div>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-theme-vantage
|
PKG_NAME:=luci-theme-vantage
|
||||||
PKG_VERSION:=1.0.4
|
PKG_VERSION:=1.0.5
|
||||||
PKG_RELEASE:=17
|
PKG_RELEASE:=18
|
||||||
PKG_LICENSE:=GPL-3.0-or-later
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
|
|
||||||
LUCI_TITLE:=Vantage theme
|
LUCI_TITLE:=Vantage theme
|
||||||
|
|||||||
@@ -186,6 +186,14 @@ function headingKey(text) {
|
|||||||
return String(text || '').replace(/\s+/g, ' ').replace(/[\s:]+$/, '').trim().toLowerCase();
|
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
|
* Stock-page enhancers, only where CSS cannot restructure LuCI's markup
|
||||||
* (Status -> Overview: fused memory bar, per-mount storage bars, radio band
|
* (Status -> Overview: fused memory bar, per-mount storage bars, radio band
|
||||||
@@ -587,7 +595,7 @@ return baseclass.extend({
|
|||||||
this.closeTimer = null;
|
this.closeTimer = null;
|
||||||
this.openTimer = null;
|
this.openTimer = null;
|
||||||
this.entries = [];
|
this.entries = [];
|
||||||
this.trailKeys = new Set();
|
this.trail = { all: new Set(), page: new Set() };
|
||||||
|
|
||||||
this.setupTheme();
|
this.setupTheme();
|
||||||
this.setupIndicators();
|
this.setupIndicators();
|
||||||
@@ -670,8 +678,11 @@ return baseclass.extend({
|
|||||||
link.lastChild.setAttribute('class', 'vt-rail-caret');
|
link.lastChild.setAttribute('class', 'vt-rail-caret');
|
||||||
|
|
||||||
link.addEventListener('click', ev => this.handleRailClick(ev, cat, li, link));
|
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 => {
|
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();
|
this.cancelClose();
|
||||||
window.clearTimeout(this.openTimer);
|
window.clearTimeout(this.openTimer);
|
||||||
this.openTimer = window.setTimeout(() => this.openFlyout(cat, li, link, false), this.fly.hidden ? 90 : 0);
|
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 => {
|
li.addEventListener('pointerleave', ev => {
|
||||||
if (ev.pointerType !== 'mouse') return;
|
if (ev.pointerType !== 'mouse') return;
|
||||||
window.clearTimeout(this.openTimer);
|
window.clearTimeout(this.openTimer);
|
||||||
|
if (this.isPinned() || this.switchOpen()) return;
|
||||||
this.scheduleClose();
|
this.scheduleClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -701,13 +713,16 @@ return baseclass.extend({
|
|||||||
list.addEventListener('keydown', ev => this.handleRailKeys(ev, list));
|
list.addEventListener('keydown', ev => this.handleRailKeys(ev, list));
|
||||||
|
|
||||||
fly.addEventListener('pointerenter', () => this.cancelClose());
|
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));
|
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));
|
if (this.scrim) this.scrim.addEventListener('click', () => this.closeFlyout(false));
|
||||||
|
|
||||||
document.addEventListener('click', ev => {
|
document.addEventListener('click', ev => {
|
||||||
if (fly.hidden) return;
|
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);
|
this.closeFlyout(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -777,7 +792,8 @@ return baseclass.extend({
|
|||||||
handleFlyKeys(ev) {
|
handleFlyKeys(ev) {
|
||||||
const links = Array.prototype.slice.call(this.fly.querySelectorAll('a'));
|
const links = Array.prototype.slice.call(this.fly.querySelectorAll('a'));
|
||||||
const i = links.indexOf(document.activeElement);
|
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();
|
ev.preventDefault();
|
||||||
this.closeFlyout(true);
|
this.closeFlyout(true);
|
||||||
}
|
}
|
||||||
@@ -787,15 +803,57 @@ return baseclass.extend({
|
|||||||
links[(i + d + links.length) % links.length].focus();
|
links[(i + d + links.length) % links.length].focus();
|
||||||
}
|
}
|
||||||
else if (ev.key === 'Tab' && links.length) {
|
else if (ev.key === 'Tab' && links.length) {
|
||||||
/* leaving the panel returns to the rail */
|
|
||||||
const edge = ev.shiftKey ? 0 : links.length - 1;
|
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) {
|
openFlyout(cat, li, link, focusFirst) {
|
||||||
const fly = this.fly;
|
const fly = this.fly;
|
||||||
this.cancelClose();
|
this.cancelClose();
|
||||||
|
this.endSwitch();
|
||||||
if (fly.getAttribute('data-cat') !== cat.name || fly.hidden) {
|
if (fly.getAttribute('data-cat') !== cat.name || fly.hidden) {
|
||||||
fly.textContent = '';
|
fly.textContent = '';
|
||||||
const total = cat.pages.length;
|
const total = cat.pages.length;
|
||||||
@@ -851,6 +909,7 @@ return baseclass.extend({
|
|||||||
this.cancelClose();
|
this.cancelClose();
|
||||||
this.fly.hidden = true;
|
this.fly.hidden = true;
|
||||||
if (this.scrim) this.scrim.hidden = true;
|
if (this.scrim) this.scrim.hidden = true;
|
||||||
|
this.endSwitch();
|
||||||
document.querySelectorAll('.vt-rail-item.is-open').forEach(n => {
|
document.querySelectorAll('.vt-rail-item.is-open').forEach(n => {
|
||||||
n.classList.remove('is-open');
|
n.classList.remove('is-open');
|
||||||
const b = n.querySelector('.vt-rail-link[aria-expanded]');
|
const b = n.querySelector('.vt-rail-link[aria-expanded]');
|
||||||
@@ -859,6 +918,54 @@ return baseclass.extend({
|
|||||||
if (refocus && this.openLink) this.openLink.focus();
|
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() {
|
scheduleClose() {
|
||||||
this.cancelClose();
|
this.cancelClose();
|
||||||
this.closeTimer = window.setTimeout(() => this.closeFlyout(false), 220);
|
this.closeTimer = window.setTimeout(() => this.closeFlyout(false), 220);
|
||||||
@@ -883,17 +990,43 @@ return baseclass.extend({
|
|||||||
}
|
}
|
||||||
if (!parts.length) return;
|
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 = '';
|
nav.textContent = '';
|
||||||
parts.forEach((p, i) => {
|
parts.forEach((p, i) => {
|
||||||
const last = i === parts.length - 1;
|
const last = i === parts.length - 1;
|
||||||
if (i) nav.appendChild(E('span', { 'class': 'vt-crumb-sep', 'aria-hidden': 'true' }, [ '›' ]));
|
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('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 ]));
|
: E('a', { 'class': 'vt-crumb', 'href': L.url.apply(L, p.segs) }, [ p.title ]));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.pageTitle = parts[parts.length - 1].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();
|
this.updateTitle();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -906,17 +1039,16 @@ return baseclass.extend({
|
|||||||
const tabs = ui.menu.getChildren(page);
|
const tabs = ui.menu.getChildren(page);
|
||||||
if (tabs.length < 2) return;
|
if (tabs.length < 2) return;
|
||||||
|
|
||||||
bar.textContent = '';
|
const seg = E('div', { 'class': 'vt-pagetabs' });
|
||||||
for (const t of tabs) {
|
for (const t of tabs) {
|
||||||
const cur = this.dp[3] === t.name;
|
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
|
'class': 'vt-tab', 'href': L.url('admin', this.dp[1], this.dp[2], t.name), 'aria-current': cur ? 'page' : null
|
||||||
}, [ _(t.title) ]));
|
}, [ _(t.title) ]));
|
||||||
}
|
}
|
||||||
|
bar.textContent = '';
|
||||||
|
bar.appendChild(seg);
|
||||||
bar.hidden = false;
|
bar.hidden = false;
|
||||||
const active = bar.querySelector('[aria-current]');
|
|
||||||
if (active && active.scrollIntoView && bar.scrollWidth > bar.clientWidth)
|
|
||||||
bar.scrollLeft = active.offsetLeft - 16;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ------------------------------------------------------ palette */
|
/* ------------------------------------------------------ palette */
|
||||||
@@ -954,8 +1086,11 @@ return baseclass.extend({
|
|||||||
|
|
||||||
openPalette() {
|
openPalette() {
|
||||||
if (this.pal) return;
|
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.closeFlyout(false);
|
||||||
this.palReturn = document.activeElement;
|
|
||||||
|
|
||||||
const input = E('input', {
|
const input = E('input', {
|
||||||
'class': 'vt-pal-input', 'type': 'text', 'role': 'combobox', 'aria-expanded': 'true',
|
'class': 'vt-pal-input', 'type': 'text', 'role': 'combobox', 'aria-expanded': 'true',
|
||||||
@@ -1238,7 +1373,7 @@ return baseclass.extend({
|
|||||||
setupForms() {
|
setupForms() {
|
||||||
this.formsQueued = false;
|
this.formsQueued = false;
|
||||||
this.helpSeq = 0;
|
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)
|
if (window.MutationObserver)
|
||||||
new MutationObserver(() => this.queueForms()).observe(document.body, { childList: true, subtree: true });
|
new MutationObserver(() => this.queueForms()).observe(document.body, { childList: true, subtree: true });
|
||||||
this.decorateForms();
|
this.decorateForms();
|
||||||
@@ -1319,14 +1454,26 @@ return baseclass.extend({
|
|||||||
not displayed (another tab, an unmet dependency) measures 0 and waits
|
not displayed (another tab, an unmet dependency) measures 0 and waits
|
||||||
for the next resize notification. */
|
for the next resize notification. */
|
||||||
fitHelp(desc) {
|
fitHelp(desc) {
|
||||||
if (!desc.isConnected) {
|
this.fitHelps([ desc ]);
|
||||||
if (this.helpSizes) this.helpSizes.unobserve(desc);
|
},
|
||||||
return;
|
|
||||||
|
/* 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
|
/* 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 => {
|
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;
|
if (h.closest('.vt-app') || h.querySelector('a, button, input, select, [data-clickable], [tabindex]')) return;
|
||||||
const key = headingKey(h.textContent);
|
const key = headingKey(h.textContent);
|
||||||
let repeat = this.trailKeys.has(key);
|
let repeat = repeatsTrail(key, h.tagName, this.trail);
|
||||||
if (!repeat && h.tagName === 'H3') {
|
if (!repeat && h.tagName === 'H3') {
|
||||||
const map = h.closest('.cbi-map');
|
const map = h.closest('.cbi-map');
|
||||||
const mapTitle = map && map.querySelector(':scope > h2');
|
const mapTitle = map && map.querySelector(':scope > h2');
|
||||||
|
|||||||
@@ -69,14 +69,17 @@
|
|||||||
--warn-soft: rgba(214, 138, 0, .15);
|
--warn-soft: rgba(214, 138, 0, .15);
|
||||||
--err: #c42b2b;
|
--err: #c42b2b;
|
||||||
--err-soft: rgba(196, 43, 43, .1);
|
--err-soft: rgba(196, 43, 43, .1);
|
||||||
|
--err-ink: #ffffff; /* text on an --err / --info fill, >= 4.5:1 */
|
||||||
--info: #1c64c4;
|
--info: #1c64c4;
|
||||||
|
--info-ink: #ffffff;
|
||||||
|
--ok-text: #0a7262; /* --ok as text on control fills (s3, s4) */
|
||||||
--info-soft: rgba(28, 100, 196, .1);
|
--info-soft: rgba(28, 100, 196, .1);
|
||||||
--scrim: rgba(20, 23, 26, .38);
|
--scrim: rgba(20, 23, 26, .38);
|
||||||
--shadow-pop: 0 12px 32px rgba(20, 23, 26, .16), 0 2px 6px rgba(20, 23, 26, .08);
|
--shadow-pop: 0 12px 32px rgba(20, 23, 26, .16), 0 2px 6px rgba(20, 23, 26, .08);
|
||||||
--zone-mix: .22;
|
--zone-mix: .22;
|
||||||
--seg-bg: var(--s3); /* segmented control: track, selected segment */
|
--seg-bg: var(--s3); /* segmented control: track, selected segment */
|
||||||
--seg-on: var(--s2);
|
--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"] {
|
html[data-darkmode="true"] {
|
||||||
@@ -107,14 +110,17 @@ html[data-darkmode="true"] {
|
|||||||
--warn-soft: rgba(245, 184, 74, .13);
|
--warn-soft: rgba(245, 184, 74, .13);
|
||||||
--err: #ff6b6b;
|
--err: #ff6b6b;
|
||||||
--err-soft: rgba(255, 107, 107, .12);
|
--err-soft: rgba(255, 107, 107, .12);
|
||||||
|
--err-ink: #1a0505; /* white on the light dark-mode red was 2.8:1 */
|
||||||
--info: #6cb4ff;
|
--info: #6cb4ff;
|
||||||
|
--info-ink: #0d0e10;
|
||||||
|
--ok-text: var(--ok);
|
||||||
--info-soft: rgba(108, 180, 255, .12);
|
--info-soft: rgba(108, 180, 255, .12);
|
||||||
--scrim: rgba(0, 0, 0, .58);
|
--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);
|
--shadow-pop: 0 16px 40px rgba(0, 0, 0, .55), 0 0 0 1px rgba(255, 255, 255, .02);
|
||||||
--zone-mix: .2;
|
--zone-mix: .2;
|
||||||
--seg-bg: var(--bg);
|
--seg-bg: var(--bg);
|
||||||
--seg-on: var(--s3);
|
--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 */
|
/* 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-r-lg: var(--r-lg);
|
||||||
--v-ctl-h: var(--ctl-h); /* every control: 36px, 44px on touch */
|
--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);
|
--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);
|
--v-stick-top: var(--stick-top);
|
||||||
|
|
||||||
/* charts (stock realtime graphs, channel analysis, the dashboard app).
|
/* 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-list > li + li { margin-top: 1px; }
|
||||||
.vt-fly-link {
|
.vt-fly-link {
|
||||||
display: flex; align-items: center; gap: var(--sp-2);
|
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);
|
border-radius: var(--r);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
@@ -475,16 +481,49 @@ html[data-rail="pinned"] .vt-frame { margin-left: var(--rail-w-pinned); }
|
|||||||
|
|
||||||
.vt-crumbs {
|
.vt-crumbs {
|
||||||
display: flex; align-items: center; gap: 6px;
|
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);
|
font-size: var(--fs-base);
|
||||||
white-space: nowrap; overflow: hidden;
|
white-space: nowrap; overflow: hidden;
|
||||||
}
|
}
|
||||||
.vt-crumb { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; text-decoration: none !important; }
|
.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); }
|
a.vt-crumb:hover { color: var(--text); }
|
||||||
.vt-crumb-current { color: var(--text); font-weight: 600; }
|
.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); }
|
.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 {
|
.vt-search {
|
||||||
display: inline-flex; align-items: center; gap: var(--sp-2);
|
display: inline-flex; align-items: center; gap: var(--sp-2);
|
||||||
@@ -522,6 +561,7 @@ a.vt-crumb:hover { color: var(--text); }
|
|||||||
.vt-host {
|
.vt-host {
|
||||||
display: inline-flex; align-items: center; gap: 6px;
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
height: 28px; max-width: 300px;
|
height: 28px; max-width: 300px;
|
||||||
|
flex: 0 1000 auto; min-width: 0; overflow: hidden;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border: 1px solid var(--line); border-radius: 999px;
|
border: 1px solid var(--line); border-radius: 999px;
|
||||||
background: var(--s1);
|
background: var(--s1);
|
||||||
@@ -547,18 +587,30 @@ a.vt-crumb:hover { color: var(--text); }
|
|||||||
}
|
}
|
||||||
#indicators > span[data-clickable] { cursor: pointer; }
|
#indicators > span[data-clickable] { cursor: pointer; }
|
||||||
#indicators > span[data-clickable]:hover { border-color: var(--text-3); color: var(--text); }
|
#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 {
|
#indicators > span[data-indicator="poll-status"][data-style="active"]::before {
|
||||||
content: ""; width: 7px; height: 7px; border-radius: 50%;
|
content: ""; width: 7px; height: 7px; border-radius: 50%;
|
||||||
background: var(--accent-text);
|
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;
|
animation: vt-live 2s ease-out infinite;
|
||||||
}
|
}
|
||||||
#indicators > span[data-indicator="poll-status"][data-style="inactive"]::before {
|
#indicators > span[data-indicator="poll-status"][data-style="inactive"]::before {
|
||||||
content: ""; width: 7px; height: 7px; border-radius: 1px; background: var(--text-3);
|
content: ""; width: 7px; height: 7px; border-radius: 1px; background: var(--text-3);
|
||||||
}
|
}
|
||||||
@keyframes vt-live {
|
@keyframes vt-live {
|
||||||
0% { box-shadow: 0 0 0 0 var(--accent-line); }
|
0% { transform: scale(1); opacity: 1; }
|
||||||
70%, 100% { box-shadow: 0 0 0 6px transparent; }
|
70%, 100% { transform: scale(2.7); opacity: 0; }
|
||||||
}
|
}
|
||||||
/* "Unsaved Changes: N" -> label + prominent count (text node stays for LuCI) */
|
/* "Unsaved Changes: N" -> label + prominent count (text node stays for LuCI) */
|
||||||
#indicators > span[data-indicator][data-count] {
|
#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); }
|
#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 {
|
.vt-tabs {
|
||||||
position: sticky; top: var(--top-h); z-index: 190;
|
width: 100%; max-width: calc(var(--content-max) + 2 * var(--sp-6));
|
||||||
display: flex; gap: var(--sp-1);
|
margin: 0 auto; padding: var(--sp-6) var(--sp-6) 0;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
.vt-tabs:not([hidden]) + .vt-main { padding-top: var(--sp-5); }
|
||||||
|
|
||||||
/* content column */
|
/* content column */
|
||||||
.vt-main {
|
.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
|
/* page = title block + a stack of cards. Cards carry a full-bleed header
|
||||||
row (h3) and full-bleed tables; nested sections lose their chrome. */
|
row (h3) and full-bleed tables; nested sections lose their chrome. */
|
||||||
:root { --card-px: 20px; --card-py: 16px; --stick-top: var(--top-h); }
|
: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 { margin-bottom: var(--sp-6); }
|
||||||
.cbi-map > h2, .vt-main > h2, #view > h2, #view > div > h2:first-child, .cbi-map > h2:first-child {
|
.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 > * { margin: 0; }
|
||||||
.cbi-section-create > .cbi-section-create-name { max-width: 240px; }
|
.cbi-section-create > .cbi-section-create-name { max-width: 240px; }
|
||||||
.cbi-section-remove { text-align: right; padding: var(--sp-2) 0; }
|
.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 */
|
/* page actions: a floating command bar docked to the bottom right */
|
||||||
.cbi-page-actions {
|
.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-save { order: 2; }
|
||||||
.cbi-page-actions .cbi-button-apply, .cbi-page-actions .cbi-dropdown { order: 3; }
|
.cbi-page-actions .cbi-button-apply, .cbi-page-actions .cbi-dropdown { order: 3; }
|
||||||
.cbi-page-actions:empty { display: none; }
|
.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 */
|
/* ------------------------------------------------------------ 6. widgets */
|
||||||
|
|
||||||
@@ -929,6 +968,8 @@ input, select, textarea, button { font: inherit; color: inherit; }
|
|||||||
transition: border-color .12s, box-shadow .12s;
|
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; }
|
.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 {
|
.vt-main select, .cbi-input-select, .modal select {
|
||||||
appearance: none; -webkit-appearance: none;
|
appearance: none; -webkit-appearance: none;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
@@ -953,6 +994,15 @@ input:focus, select:focus, textarea:focus {
|
|||||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||||
}
|
}
|
||||||
input:focus-visible, select:focus-visible, textarea:focus-visible { outline: none; }
|
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[disabled], select[disabled], textarea[disabled], input[readonly] { opacity: .6; cursor: not-allowed; }
|
||||||
input[readonly] { cursor: default; }
|
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(: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 > :not(:first-child) { border-top-left-radius: 0 !important; border-bottom-left-radius: 0 !important; }
|
||||||
.control-group > .btn, .control-group > .cbi-button { flex: none; }
|
.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 */
|
/* checkbox */
|
||||||
.cbi-checkbox { display: inline-flex; align-items: center; min-height: var(--ctl-h); gap: var(--sp-2); }
|
.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;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.cbi-dropdown:hover { border-color: var(--text-3); }
|
.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:focus-visible { outline: none; }
|
||||||
.cbi-dropdown[disabled] { opacity: .55; pointer-events: 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,
|
.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); }
|
.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) {
|
:is(.btn, .cbi-button, .cbi-dropdown):is(.cbi-button-negative, .cbi-button-remove) {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--err);
|
color: var(--err);
|
||||||
border-color: color-mix(in srgb, var(--err) 40%, var(--line-2));
|
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):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 { 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) 85%, #000); color: #fff; }
|
: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 { background: transparent; color: var(--text-2); }
|
||||||
:is(.btn, .cbi-button).cbi-button-reset:hover { color: var(--text); }
|
: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 { 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; }
|
div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0; }
|
||||||
|
|
||||||
/* Tabs inside a page (ui.tabs), one look per level:
|
/* Tabs: one segmented control for every level, the menu's page tabs
|
||||||
- tabs standing at page level (not inside a form section or another
|
(.vt-pagetabs in #vt-tabs), ui.tabs at page level, in a form or modal,
|
||||||
tab) are the page's sub-pages when the menu shows no page tabs: they
|
and tabs inside a tab. Nesting shows by where a control sits, not by
|
||||||
take the page-tab underline, like .vt-tabs (Interfaces / Devices /
|
another look. It wraps its segments instead of hiding them. Every
|
||||||
..., DHCP's Leases / dnsmasq, Channel Analysis's radios). Under
|
segment is a whole control high (--ctl-h, 36px / 44px on touch) to
|
||||||
.vt-tabs they are the first level inside the page instead;
|
click or tap: the 3px between the track's edge and the drawn segment
|
||||||
- the first level inside a page, form or modal: a segmented control;
|
is the segment's own transparent border, so the track is one control
|
||||||
- tabs inside a tab: a side list where the pane is wide enough
|
high as well. */
|
||||||
(container query on the pane, so modals and pages alike), chips
|
.cbi-tabmenu, .vt-tabs > .vt-pagetabs {
|
||||||
where it is not and at every deeper level.
|
display: flex; flex-wrap: wrap; align-items: stretch;
|
||||||
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;
|
|
||||||
width: fit-content; max-width: 100%; min-height: var(--ctl-h);
|
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;
|
list-style: none;
|
||||||
background: var(--seg-bg);
|
background: var(--seg-bg);
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-lg);
|
border-radius: var(--r-lg);
|
||||||
overflow-x: auto;
|
box-shadow: inset 0 0 0 1px var(--line);
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
}
|
||||||
.cbi-tabmenu::-webkit-scrollbar { display: none; }
|
.vt-tabs > .vt-pagetabs { margin: 0; }
|
||||||
.cbi-tabmenu > li { position: relative; flex: none; }
|
.cbi-tabmenu > li { position: relative; flex: none; display: flex; max-width: 100%; }
|
||||||
.cbi-tabmenu > li > a {
|
.cbi-tabmenu > li > a, .vt-pagetabs > .vt-tab {
|
||||||
|
flex: 1 1 auto;
|
||||||
display: flex; align-items: center; gap: 6px;
|
display: flex; align-items: center; gap: 6px;
|
||||||
height: calc(var(--ctl-h) - 6px); padding: 0 var(--sp-3);
|
min-height: var(--ctl-h); padding: 0 10px;
|
||||||
border-radius: var(--r);
|
border: 3px solid transparent; background-clip: padding-box;
|
||||||
|
border-radius: calc(var(--r) + 3px);
|
||||||
color: var(--text-2);
|
color: var(--text-2);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
.cbi-tabmenu > li > a:hover { color: var(--text); }
|
:is(.cbi-tabmenu > li > a, .vt-pagetabs > .vt-tab):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); }
|
: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); }
|
||||||
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; }
|
|
||||||
}
|
|
||||||
.cbi-tabmenu > li[data-errors] > a::after {
|
.cbi-tabmenu > li[data-errors] > a::after {
|
||||||
content: attr(data-errors);
|
content: attr(data-errors);
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
min-width: 18px; height: 18px; padding: 0 5px;
|
min-width: 18px; height: 18px; padding: 0 5px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--err); color: #fff;
|
background: var(--err); color: var(--err-ink);
|
||||||
font: 700 var(--fs-micro)/1 var(--mono);
|
font: 700 var(--fs-micro)/1 var(--mono);
|
||||||
}
|
}
|
||||||
.cbi-tabmenu > li.cbi-tab-disabled[data-errors] > a { color: var(--err); }
|
.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
|
|||||||
/* "<strong>Label:</strong> value<br>" dumps (LuCI itemlist) -> aligned
|
/* "<strong>Label:</strong> value<br>" dumps (LuCI itemlist) -> aligned
|
||||||
label/value grid. Each span.nowrap becomes a subgrid row: the <strong>
|
label/value grid. Each span.nowrap becomes a subgrid row: the <strong>
|
||||||
is the label cell, the contiguous value run the anonymous value cell. */
|
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) {
|
:is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content minmax(0, 1fr);
|
grid-template-columns: max-content minmax(0, 1fr);
|
||||||
column-gap: 14px; row-gap: 3px;
|
column-gap: 14px; row-gap: 0;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
text-align: left;
|
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) > 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 {
|
:is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) > .nowrap {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
display: grid; grid-template-columns: subgrid;
|
display: grid; grid-template-columns: subgrid;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
margin-block: 1.5px;
|
||||||
white-space: normal; overflow-wrap: anywhere;
|
white-space: normal; overflow-wrap: anywhere;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: 400 var(--fs-sm)/1.55 var(--mono);
|
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; }
|
.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*="--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; }
|
.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; }
|
.zonebadge .ifacebadge { padding: 0 4px; border: 0; background: transparent; }
|
||||||
.zone-forwards { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
.zone-forwards { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
||||||
.zone-forwards > span { color: var(--text-3); }
|
.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;
|
transition: opacity .15s;
|
||||||
pointer-events: none;
|
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.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 { position: relative; cursor: help; }
|
||||||
.cbi-tooltip-container > .cbi-tooltip { left: -10000px; top: auto; margin-top: 6px; white-space: normal; }
|
.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; }
|
.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-modal { width: min(960px, 100%); }
|
||||||
#modal_overlay > .modal .cbi-section { background: none; border: 0; padding: 0; margin: 0; box-shadow: none; }
|
#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; }
|
#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 > 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; }
|
.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);
|
color: var(--text-3);
|
||||||
font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
|
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 {
|
.vt-login-error {
|
||||||
margin: 0 0 var(--sp-4);
|
margin: 0 0 var(--sp-4);
|
||||||
padding: var(--sp-2) var(--sp-3);
|
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 { 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-label { color: var(--text-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }
|
||||||
.vt-field input {
|
.vt-field input {
|
||||||
width: 100%; height: 38px;
|
width: 100%; height: var(--ctl-h);
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
background: var(--s2);
|
background: var(--s2);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -2238,7 +2233,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
|
|||||||
font-size: var(--fs-md);
|
font-size: var(--fs-md);
|
||||||
}
|
}
|
||||||
.vt-field input[name="luci_username"] { font-family: var(--mono); }
|
.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; }
|
.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 */
|
/* ------------------------------------------------ 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. */
|
a non-scrolling ancestor, so status tables only scroll when narrow. */
|
||||||
.cbi-tblsection, .cbi-section:has(> .cbi-tblsection) { overflow-x: auto; }
|
.cbi-tblsection, .cbi-section:has(> .cbi-tblsection) { overflow-x: auto; }
|
||||||
.cbi-section:has(.cbi-dropdown[open]), .cbi-tblsection:has(.cbi-dropdown[open]) { overflow: visible; }
|
.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 { overflow-x: auto; }
|
||||||
.cbi-section:has(.cbi-dropdown[open]) { overflow: visible; }
|
.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; }
|
.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; } }
|
@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 */
|
/* ----------------------------------------------------- responsive */
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
@@ -2380,6 +2397,13 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
|
|||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.vt-host { display: none !important; }
|
.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-rail { width: var(--rail-w); }
|
||||||
html[data-rail="pinned"] .vt-frame { margin-left: 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); }
|
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; }
|
.vt-rail > .vt-brand, .vt-rail-foot { display: none; }
|
||||||
body.vt-auth .vt-shell::before { display: none; }
|
body.vt-auth .vt-shell::before { display: none; }
|
||||||
.vt-rail-list {
|
.vt-rail-list {
|
||||||
flex-direction: row; justify-content: space-around;
|
flex-direction: row; justify-content: flex-start;
|
||||||
gap: 0; padding: 0 var(--sp-1);
|
gap: 0; padding: 0 var(--sp-1);
|
||||||
overflow-x: auto; overflow-y: hidden;
|
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 {
|
.vt-rail-link, html[data-rail="pinned"] .vt-rail-link {
|
||||||
flex-direction: column; justify-content: center; gap: 3px;
|
flex-direction: column; justify-content: center; gap: 3px;
|
||||||
height: 48px; padding: 0 4px;
|
height: 48px; padding: 0 4px;
|
||||||
}
|
}
|
||||||
.vt-rail-link .vt-rail-label, html[data-rail="pinned"] .vt-rail-link .vt-rail-label {
|
.vt-rail-link .vt-rail-label, html[data-rail="pinned"] .vt-rail-link .vt-rail-label {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
flex: none; /* the rail's label grows; here it would push the icon up */
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-size: var(--fs-micro); font-weight: 500;
|
font-size: var(--fs-micro); font-weight: 500;
|
||||||
text-align: center;
|
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-frame, html[data-rail="pinned"] .vt-frame { margin-left: 0; padding-bottom: calc(var(--bar-h) + env(safe-area-inset-bottom)); }
|
||||||
|
|
||||||
.vt-flyout {
|
.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-crumbs .vt-crumb-current { font-size: var(--fs-md); }
|
||||||
.vt-top-actions { gap: 2px; }
|
.vt-top-actions { gap: 2px; }
|
||||||
.vt-search { border-color: transparent; background: none; color: var(--text-2); width: 32px; height: 32px; }
|
.vt-search { border-color: transparent; background: none; color: var(--text-2); width: 32px; height: 32px; }
|
||||||
#indicators > span[data-indicator] { height: 28px; padding: 0 8px; }
|
.vt-tabs { padding: var(--sp-4) var(--sp-4) 0; }
|
||||||
#indicators > span[data-indicator="poll-status"] { font-size: 0; gap: 0; width: 28px; justify-content: center; padding: 0; }
|
.vt-tabs:not([hidden]) + .vt-main { padding-top: var(--sp-4); }
|
||||||
#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-main { padding: var(--sp-4); }
|
.vt-main { padding: var(--sp-4); }
|
||||||
.vt-foot { padding-bottom: 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;
|
width: auto;
|
||||||
margin: var(--sp-4) 0 0;
|
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-section { overflow-x: visible; }
|
||||||
.cbi-tblsection, .cbi-section:has(> .cbi-tblsection) { 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; }
|
.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) > .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) > .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; }
|
.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 */
|
/* a segmented control spans the width and its segments share each row */
|
||||||
.cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) { flex-wrap: wrap; width: auto; }
|
.cbi-tabmenu, .vt-tabs > .vt-pagetabs { width: auto; }
|
||||||
.cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) > li { flex: 1 1 auto; }
|
.cbi-tabmenu > li, .vt-pagetabs > .vt-tab { flex: 1 1 auto; }
|
||||||
.cbi-tabmenu:not(.cbi-section-node-tabbed > [data-tab-title] *) > li > a { justify-content: center; }
|
.cbi-tabmenu > li > a, .vt-pagetabs > .vt-tab { 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; }
|
|
||||||
.cbi-dropdown, .vt-main input, .vt-main select, .control-group, .cbi-dynlist { max-width: none; }
|
.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; }
|
.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%; }
|
:is(.vt-main, .modal) .table, :is(.vt-main, .modal) .table > :is(tbody, thead) { display: block; width: 100%; }
|
||||||
.vt-main .table > .tr.table-titles, .vt-main .table > thead > .tr.table-titles, .vt-main .table > tbody > .tr.table-titles,
|
:is(.vt-main, .modal) .table :is(tr, .tr):is(.table-titles, .cbi-section-table-titles, .cbi-section-table-descr) { display: none !important; }
|
||||||
.vt-main .cbi-section-table-titles, .vt-main .cbi-section-table-descr, .modal .table-titles { display: none !important; }
|
:is(.vt-main, .modal) .table .tr {
|
||||||
.vt-main .table .tr, .modal .table .tr {
|
|
||||||
display: flex; flex-wrap: wrap;
|
display: flex; flex-wrap: wrap;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.vt-main .table .tr:last-child { border-bottom: 0; }
|
:is(.vt-main, .modal) .table .tr:last-child { border-bottom: 0; }
|
||||||
.vt-main .table .tr > .td, .modal .table .tr > .td {
|
:is(.vt-main, .modal) .table .tr > .td {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
|
height: auto;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border: 0;
|
border: 0;
|
||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
}
|
}
|
||||||
.vt-main :is(.cbi-section, .cbi-value) .table .tr > .td:nth-child(n) { padding: 4px var(--card-px); height: auto; }
|
:is(.vt-main, .modal) :is(.cbi-section, .cbi-value) .table .tr > .td:nth-child(n) { padding: 4px var(--card-px); }
|
||||||
.vt-main .table .tr > .td[data-title]::before, .modal .table .tr > .td[data-title]::before {
|
:is(.vt-main, .modal) .table .tr > .td[data-title]::before {
|
||||||
content: attr(data-title);
|
content: attr(data-title);
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
@@ -2529,18 +2562,20 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
|
|||||||
color: var(--text-3);
|
color: var(--text-3);
|
||||||
font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
|
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;
|
width: auto !important; padding-bottom: 0;
|
||||||
color: var(--text-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
|
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%; }
|
:is(.vt-main, .modal) .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%; }
|
:is(.vt-main, .modal) .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; }
|
:is(.vt-main, .modal) .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%; }
|
/* 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%; }
|
.cbi-section-table .td[data-name="_ifacebox"] .ifacebox { width: 100%; }
|
||||||
.vt-main .table .tr > .td.cbi-section-actions > div { flex-wrap: wrap; }
|
:is(.vt-main, .modal) .table .tr > .td.cbi-section-actions > div { flex-wrap: wrap; }
|
||||||
.vt-main .table .tr > .td.cbi-section-actions::before { display: none !important; }
|
:is(.vt-main, .modal) .table .tr > .td.cbi-section-actions::before { display: none !important; }
|
||||||
.vt-main .table .tr:hover > .td { background: none; }
|
:is(.vt-main, .modal) .table .tr:hover > .td { background: none; }
|
||||||
.cbi-section .table, .cbi-section .cbi-section-table { border: 0; }
|
.cbi-section .table, .cbi-section .cbi-section-table { border: 0; }
|
||||||
.cbi-progressbar { max-width: none; }
|
.cbi-progressbar { max-width: none; }
|
||||||
.network-status-table > .ifacebox { 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) {
|
@media (pointer: coarse) {
|
||||||
input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }
|
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; }
|
.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 */
|
/* ui.Dropdown moves focus on transitionend: keep a (near-instant) transition */
|
||||||
.cbi-dropdown[open] > ul.dropdown { transition-duration: .01s; }
|
.cbi-dropdown[open] > ul.dropdown { transition-duration: .01s; }
|
||||||
.cbi-progressbar > div { transition: none; }
|
.cbi-progressbar > div { transition: none; }
|
||||||
|
.vt-crumb-switch > svg, .vt-rail-caret, .vt-rail-pin svg { transition: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
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"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:947
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1079
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:948
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1080
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:949
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1081
|
||||||
msgid "Appearance"
|
msgid "Appearance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ msgstr ""
|
|||||||
msgid "Authorization Required"
|
msgid "Authorization Required"
|
||||||
msgstr ""
|
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"
|
msgid "Auto"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -23,52 +23,52 @@ msgstr ""
|
|||||||
msgid "Breadcrumb"
|
msgid "Breadcrumb"
|
||||||
msgstr ""
|
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"
|
msgid "Buffered"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:290
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:298
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:294
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:302
|
||||||
msgid "Cache"
|
msgid "Cache"
|
||||||
msgstr ""
|
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"
|
msgid "Cached"
|
||||||
msgstr ""
|
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"
|
msgid "Channel"
|
||||||
msgstr ""
|
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"
|
msgid "Collapse navigation"
|
||||||
msgstr ""
|
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
|
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:81
|
||||||
msgid "Colour theme"
|
msgid "Colour theme"
|
||||||
msgstr ""
|
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"
|
msgid "Command palette"
|
||||||
msgstr ""
|
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"
|
msgid "Dark"
|
||||||
msgstr ""
|
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
|
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:54
|
||||||
msgid "Expand navigation"
|
msgid "Expand navigation"
|
||||||
msgstr ""
|
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"
|
msgid "Follow system theme"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:281
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:295
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:303
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -76,9 +76,9 @@ msgstr ""
|
|||||||
msgid "Home"
|
msgid "Home"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:289
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:297
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:293
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:301
|
||||||
msgid "In use"
|
msgid "In use"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -86,11 +86,11 @@ msgstr ""
|
|||||||
msgid "Invalid username and/or password! Please try again."
|
msgid "Invalid username and/or password! Please try again."
|
||||||
msgstr ""
|
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…"
|
msgid "Jump to a page or action…"
|
||||||
msgstr ""
|
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"
|
msgid "Light"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ msgstr ""
|
|||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr ""
|
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
|
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:84
|
||||||
msgid "Log out"
|
msgid "Log out"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -108,15 +108,19 @@ msgstr ""
|
|||||||
msgid "Main navigation"
|
msgid "Main navigation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:264
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:272
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:279
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:287
|
||||||
msgid "Memory"
|
msgid "Memory"
|
||||||
msgstr ""
|
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"
|
msgid "No matching pages"
|
||||||
msgstr ""
|
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
|
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:90
|
||||||
msgid "Page tabs"
|
msgid "Page tabs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -125,32 +129,33 @@ msgstr ""
|
|||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
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"
|
msgid "Press"
|
||||||
msgstr ""
|
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"
|
msgid "Results"
|
||||||
msgstr ""
|
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
|
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:74
|
||||||
msgid "Search pages"
|
msgid "Search pages"
|
||||||
msgstr ""
|
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"
|
msgid "Session"
|
||||||
msgstr ""
|
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"
|
msgid "Show full description"
|
||||||
msgstr ""
|
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"
|
msgid "Show full help"
|
||||||
msgstr ""
|
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"
|
msgid "Show full help: %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -158,36 +163,36 @@ msgstr ""
|
|||||||
msgid "Skip to content"
|
msgid "Skip to content"
|
||||||
msgstr ""
|
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"
|
msgid "Storage"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:302
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:310
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:304
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:312
|
||||||
msgid "Swap"
|
msgid "Swap"
|
||||||
msgstr ""
|
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"
|
msgid "Swap free"
|
||||||
msgstr ""
|
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"
|
msgid "Total"
|
||||||
msgstr ""
|
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"
|
msgid "Total Available"
|
||||||
msgstr ""
|
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"
|
msgid "Use dark theme"
|
||||||
msgstr ""
|
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"
|
msgid "Use light theme"
|
||||||
msgstr ""
|
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"
|
msgid "Used"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -195,54 +200,56 @@ msgstr ""
|
|||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr ""
|
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"
|
msgid "Wireless"
|
||||||
msgstr ""
|
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"
|
msgid "available"
|
||||||
msgstr ""
|
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"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:331
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:339
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:334
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:342
|
||||||
msgid "free"
|
msgid "free"
|
||||||
msgstr ""
|
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"
|
msgid "items"
|
||||||
msgstr ""
|
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"
|
msgid "navigate"
|
||||||
msgstr ""
|
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"
|
msgid "open"
|
||||||
msgstr ""
|
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"
|
msgid "page"
|
||||||
msgstr ""
|
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"
|
msgid "pages"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:280
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:288
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:290
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:298
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:294
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:302
|
||||||
msgid "reclaimable"
|
msgid "reclaimable"
|
||||||
msgstr ""
|
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"
|
msgid "to search all pages"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:331
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:339
|
||||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:334
|
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:342
|
||||||
msgid "used"
|
msgid "used"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<nav class="vt-crumbs" id="vt-crumbs" aria-label="{{ entityencode(_('Breadcrumb'), true) }}"><span class="vt-crumb vt-crumb-current">{{ entityencode(page_title, true) }}</span></nav>
|
<nav class="vt-crumbs" id="vt-crumbs" aria-label="{{ entityencode(_('Breadcrumb'), true) }}"><span class="vt-crumb vt-crumb-current">{{ entityencode(page_title, true) }}</span></nav>
|
||||||
<div class="vt-top-actions">
|
<div class="vt-top-actions">
|
||||||
{% if (authed): %}
|
{% if (authed): %}
|
||||||
<button type="button" class="vt-search" id="vt-search-btn" aria-haspopup="dialog" aria-keyshortcuts="Control+K /">
|
<button type="button" class="vt-search" id="vt-search-btn" aria-haspopup="dialog" aria-keyshortcuts="Control+K /" aria-label="{{ entityencode(_('Search pages'), true) }}">
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="m16 16 4.5 4.5"/></svg>
|
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="m16 16 4.5 4.5"/></svg>
|
||||||
<span class="vt-search-label">{{ entityencode(_('Search pages'), true) }}</span>
|
<span class="vt-search-label">{{ entityencode(_('Search pages'), true) }}</span>
|
||||||
<kbd class="vt-kbd">Ctrl K</kbd>
|
<kbd class="vt-kbd">Ctrl K</kbd>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="vt-login-title">{{ hostname }}</h1>
|
<h1 class="vt-login-title">{{ hostname }}</h1>
|
||||||
<p class="vt-login-sub"><span class="vt-live-dot" aria-hidden="true"></span>{{ entityencode(_('Authorization Required'), true) }}</p>
|
<p class="vt-login-sub"><span class="vt-login-dot" aria-hidden="true"></span>{{ entityencode(_('Authorization Required'), true) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
|
|||||||
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
||||||
|
|
||||||
PKG_NAME:=mise
|
PKG_NAME:=mise
|
||||||
PKG_VERSION:=2026.10.8
|
PKG_VERSION:=2026.10.9
|
||||||
PKG_RELEASE:=33
|
PKG_RELEASE:=34
|
||||||
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
|
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_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=mptcp
|
PKG_NAME:=mptcp
|
||||||
PKG_VERSION:=6.1
|
PKG_VERSION:=6.1
|
||||||
PKG_RELEASE:=25
|
PKG_RELEASE:=26
|
||||||
|
|
||||||
PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
|
PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
|
||||||
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
|
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
|
||||||
|
|||||||
@@ -37,6 +37,21 @@ _mptcp_limit() {
|
|||||||
esac
|
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() {
|
restart_scheduler_dependent_services() {
|
||||||
local proxy vpn
|
local proxy vpn
|
||||||
proxy="$(uci -q get openmptcprouter.settings.proxy)"
|
proxy="$(uci -q get openmptcprouter.settings.proxy)"
|
||||||
@@ -145,11 +160,7 @@ global_multipath_settings() {
|
|||||||
# Global MPTCP configuration
|
# Global MPTCP configuration
|
||||||
if [ -f /proc/sys/net/mptcp/mptcp_enabled ]; then
|
if [ -f /proc/sys/net/mptcp/mptcp_enabled ]; then
|
||||||
sysctl -qew net.mptcp.mptcp_enabled="$multipath_status"
|
sysctl -qew net.mptcp.mptcp_enabled="$multipath_status"
|
||||||
if grep -q net.mptcp.mptcp_enabled /etc/sysctl.d/zzz_openmptcprouter.conf; then
|
_sysctl_conf_set net.mptcp.mptcp_enabled "$multipath_status"
|
||||||
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
|
|
||||||
[ -z "$mptcp_path_manager" ] || sysctl -qew net.mptcp.mptcp_path_manager="$mptcp_path_manager"
|
[ -z "$mptcp_path_manager" ] || sysctl -qew net.mptcp.mptcp_path_manager="$mptcp_path_manager"
|
||||||
if [ -n "$mptcp_scheduler" ]; then
|
if [ -n "$mptcp_scheduler" ]; then
|
||||||
local prev_sched="$(cat /proc/sys/net/mptcp/mptcp_scheduler 2>/dev/null)"
|
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"
|
[ -z "$mptcp_version" ] || sysctl -qew net.mptcp.mptcp_version="$mptcp_version"
|
||||||
elif [ -f /proc/sys/net/mptcp/enabled ]; then
|
elif [ -f /proc/sys/net/mptcp/enabled ]; then
|
||||||
sysctl -qew net.mptcp.enabled="$multipath_status"
|
sysctl -qew net.mptcp.enabled="$multipath_status"
|
||||||
if grep -q net.mptcp.enabled /etc/sysctl.d/zzz_openmptcprouter.conf; then
|
_sysctl_conf_set net.mptcp.enabled "$multipath_status"
|
||||||
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
|
|
||||||
# The kernel takes at most 8 for each limit (MPTCP_PM_ADDR_MAX) and
|
# 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
|
# refuses the whole command when one is above, so a single value too
|
||||||
# high left both limits at whatever they were
|
# high left both limits at whatever they were
|
||||||
@@ -282,7 +289,9 @@ interface_max_metric() {
|
|||||||
EOF
|
EOF
|
||||||
;;
|
;;
|
||||||
lan)
|
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)
|
loopback)
|
||||||
;;
|
;;
|
||||||
@@ -380,8 +389,48 @@ _prune_wan_routes() {
|
|||||||
done
|
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() {
|
interface_multipath_settings() {
|
||||||
local mode iface proto metric ip4table qdisc
|
local mode iface proto metric ip4table qdisc device_section
|
||||||
local config="$1"
|
local config="$1"
|
||||||
local intf="$2"
|
local intf="$2"
|
||||||
# DHCPv6 companion interfaces (<intf>_6) share the parent's device:
|
# DHCPv6 companion interfaces (<intf>_6) share the parent's device:
|
||||||
@@ -400,6 +449,17 @@ interface_multipath_settings() {
|
|||||||
config_get txqueuelen "$config" txqueuelen
|
config_get txqueuelen "$config" txqueuelen
|
||||||
config_get qdisc "$config" qdisc
|
config_get qdisc "$config" qdisc
|
||||||
case "$iface" in *@*) iface=$(ifstatus "$config" | jsonfilter -q -e '@["device"]') ;; esac
|
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
|
if [ "$(uci -q get openmptcprouter.${config}.metric)" = "" ] || [ "$(uci -q get openmptcprouter.${config}.metric)" = "1" ]; then
|
||||||
count=$((count+1))
|
count=$((count+1))
|
||||||
metric=$count
|
metric=$count
|
||||||
@@ -430,7 +490,8 @@ interface_multipath_settings() {
|
|||||||
[ -n "$iface" ] && {
|
[ -n "$iface" ] && {
|
||||||
local offload val
|
local offload val
|
||||||
for offload in gro gso lro ufo tso; do
|
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" = "1" ] && ethtool -K $iface $offload on >/dev/null 2>&1
|
||||||
[ "$val" = "0" ] && ethtool -K $iface $offload off >/dev/null 2>&1
|
[ "$val" = "0" ] && ethtool -K $iface $offload off >/dev/null 2>&1
|
||||||
done
|
done
|
||||||
@@ -787,7 +848,8 @@ interface_multipath_settings() {
|
|||||||
elif ([ "$mode" = "master" ] || [ "$mode" = "on" ]) && { [ -z "$(multipath $iface | grep default)" ] || ! multipath -i "$iface" "$id"; }; then
|
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)"
|
logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)"
|
||||||
multipath "$iface" "on" "$id"
|
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)"
|
logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)"
|
||||||
multipath "$iface" "$mode" "$id"
|
multipath "$iface" "$mode" "$id"
|
||||||
elif [ "$mode" = "backup" ] && { [ -z "$(multipath $iface | grep backup)" ] || ! multipath -i "$iface" "$id"; }; then
|
elif [ "$mode" = "backup" ] && { [ -z "$(multipath $iface | grep backup)" ] || ! multipath -i "$iface" "$id"; }; then
|
||||||
|
|||||||
@@ -66,10 +66,45 @@ EOF
|
|||||||
# endpoints only use a free slot, and a refused add is logged.
|
# endpoints only use a free slot, and a refused add is logged.
|
||||||
MPTCP_ENDPOINTS_MAX=8
|
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 <<EOF
|
||||||
|
$listing
|
||||||
|
EOF
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# Return 0 when a slot is free, or was freed by removing the extra endpoint
|
# Return 0 when a slot is free, or was freed by removing the extra endpoint
|
||||||
# with the highest id of another device
|
# with the highest id of another device. An id of 128 or more alone does not
|
||||||
|
# make an extra endpoint: one the kernel picked (no id given, or the WAN's id
|
||||||
|
# was held by another device) can be the only endpoint of a WAN, which was
|
||||||
|
# then taken out of MPTCP. The device must keep an endpoint with a lower id.
|
||||||
_free_endpoint_slot() {
|
_free_endpoint_slot() {
|
||||||
local line ep_addr ep_id ep_dev victim_id="" victim_addr="" victim_dev="" count=0
|
local line ep_addr ep_id ep_dev victim_id="" victim_addr="" victim_dev="" count=0 listing
|
||||||
|
listing="$(ip mptcp endpoint show)"
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
[ -n "$line" ] || continue
|
[ -n "$line" ] || continue
|
||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
@@ -86,11 +121,12 @@ _free_endpoint_slot() {
|
|||||||
done
|
done
|
||||||
[ -n "$ep_id" ] && [ "$ep_id" -ge 128 ] && [ "$ep_dev" != "$DEVICE" ] || continue
|
[ -n "$ep_id" ] && [ "$ep_id" -ge 128 ] && [ "$ep_dev" != "$DEVICE" ] || continue
|
||||||
[ -z "$victim_id" ] || [ "$ep_id" -gt "$victim_id" ] || continue
|
[ -z "$victim_id" ] || [ "$ep_id" -gt "$victim_id" ] || continue
|
||||||
|
_dev_has_lower_id "$listing" "$ep_dev" "$ep_id" || continue
|
||||||
victim_id="$ep_id"
|
victim_id="$ep_id"
|
||||||
victim_addr="$ep_addr"
|
victim_addr="$ep_addr"
|
||||||
victim_dev="$ep_dev"
|
victim_dev="$ep_dev"
|
||||||
done <<EOF
|
done <<EOF
|
||||||
$(ip mptcp endpoint show)
|
$listing
|
||||||
EOF
|
EOF
|
||||||
[ "$count" -lt "$MPTCP_ENDPOINTS_MAX" ] && return 0
|
[ "$count" -lt "$MPTCP_ENDPOINTS_MAX" ] && return 0
|
||||||
[ -n "$victim_id" ] || return 1
|
[ -n "$victim_id" ] || return 1
|
||||||
@@ -228,7 +264,7 @@ case $1 in
|
|||||||
fi
|
fi
|
||||||
if [ -n "$IP" ] && [ -n "$endpoint" ]; then
|
if [ -n "$IP" ] && [ -n "$endpoint" ]; then
|
||||||
for i in $IP; do
|
for i in $IP; do
|
||||||
ID=$(echo "${endpoint}" | grep -F "$i " | awk '{print $3}')
|
ID=$(endpoint_id_of_addr "${endpoint}" "$i")
|
||||||
[ -n "$ID" ] && break
|
[ -n "$ID" ] && break
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
@@ -364,7 +400,7 @@ EOF
|
|||||||
fi
|
fi
|
||||||
if [ -z "$ID" ] && [ -n "$IP" ] && [ -n "$endpoint" ]; then
|
if [ -z "$ID" ] && [ -n "$IP" ] && [ -n "$endpoint" ]; then
|
||||||
for i in $IP; do
|
for i in $IP; do
|
||||||
ID=$(echo "${endpoint}" | grep -F "$i " | awk '{print $3}')
|
ID=$(endpoint_id_of_addr "${endpoint}" "$i")
|
||||||
[ -n "$ID" ] && break
|
[ -n "$ID" ] && break
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=teleproxy
|
PKG_NAME:=teleproxy
|
||||||
PKG_VERSION:=4.16.1
|
PKG_VERSION:=4.16.1
|
||||||
PKG_RELEASE:=27
|
PKG_RELEASE:=28
|
||||||
|
|
||||||
PKG_MAINTAINER:=Kosntantine Shevlakov <shevlakov@132lan.ru>
|
PKG_MAINTAINER:=Kosntantine Shevlakov <shevlakov@132lan.ru>
|
||||||
PKG_LICENSE:=GPLv2
|
PKG_LICENSE:=GPLv2
|
||||||
@@ -10,7 +10,7 @@ PKG_LICENSE_FILES:=LICENSE
|
|||||||
|
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_URL:=https://github.com/teleproxy/teleproxy.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_SUBDIR:=$(PKG_NAME)
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
|
|||||||
+3
-3
@@ -50,12 +50,12 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=wwand
|
PKG_NAME:=wwand
|
||||||
PKG_VERSION:=1.7.0_pre21
|
PKG_VERSION:=1.7.0_pre23
|
||||||
PKG_RELEASE:=76
|
PKG_RELEASE:=77
|
||||||
|
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||||
PKG_SOURCE_VERSION:=b67a83e2da98e77953a9577653394c61fd910da0
|
PKG_SOURCE_VERSION:=4d745c6c61622bb81c7ab258a967ea3604a2a4e5
|
||||||
PKG_MIRROR_HASH:=skip
|
PKG_MIRROR_HASH:=skip
|
||||||
|
|
||||||
PKG_LICENSE:=GPL-2.0-only
|
PKG_LICENSE:=GPL-2.0-only
|
||||||
|
|||||||
Reference in new issue
Block a user