mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 05:45:13 +08:00
🐤 Sync 2026-10-03 00:12:36
This commit is contained in:
1 parent
ce3df55b24
commit
cb1d978497
58 files changed
+1627
-1018
No files matched your search
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-linkback
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
PKG_MAINTAINER:=Dante
|
||||
|
||||
LUCI_TITLE:=LuCI support for Multi-WAN LinkBack Failover/Failback
|
||||
@@ -17,7 +17,7 @@ LUCI_DEPENDS:=+luci-base +ucode +ucode-mod-fs +ucode-mod-uci +ucode-mod-ubus +li
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/sh
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
/etc/init.d/rpcd restart
|
||||
}
|
||||
|
||||
@@ -49,10 +49,10 @@ return view.extend({
|
||||
|
||||
var container = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('LinkBack 链路守护') + ' - ' + _('Overview')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Real-time health state, priority mapping, and service control for Multi-WAN default routing metrics.'))
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Real-time health state, priority mapping, and service control for Multi-WAN / Multi-Gateway failover.'))
|
||||
]);
|
||||
|
||||
// --- Service Status Section (Aligned with lucky style) ---
|
||||
// --- Service Status Section ---
|
||||
var stateText = _('Collecting...');
|
||||
var stateColor = '#999';
|
||||
|
||||
@@ -84,15 +84,19 @@ return view.extend({
|
||||
click: ui.createHandlerFn(this, 'handleServiceAction', 'restart')
|
||||
}, _('Restart'))
|
||||
])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, _('Working Mode')),
|
||||
E('td', { 'class': 'td left', 'id': 'linkback-service-mode' }, '-')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
container.appendChild(statusSection);
|
||||
|
||||
// --- Link Status Section (Aligned with lucky style) ---
|
||||
// --- Link Status Section ---
|
||||
var linkSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||
E('legend', {}, _('Link Status')),
|
||||
E('legend', {}, _('Link / Gateway Status')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('div', { id: 'linkback-status-box' }, [
|
||||
E('p', { 'class': 'spinning' }, _('Loading real-time link status...'))
|
||||
@@ -131,6 +135,16 @@ return view.extend({
|
||||
}
|
||||
}
|
||||
|
||||
// Update mode indicator
|
||||
var modeEl = document.getElementById('linkback-service-mode');
|
||||
if (modeEl && status) {
|
||||
if (status.mode === 'multi_gw') {
|
||||
modeEl.textContent = _('单wan多网关模式') + ' (' + _('wan网络接口') + ': ' + (status.interface || 'wan') + ')';
|
||||
} else {
|
||||
modeEl.textContent = _('多wan口模式');
|
||||
}
|
||||
}
|
||||
|
||||
var restartBtn = document.getElementById('linkback-restart-btn');
|
||||
if (restartBtn) {
|
||||
if (status && status.links && status.links.length > 0) {
|
||||
@@ -143,7 +157,7 @@ return view.extend({
|
||||
if (!status || !status.links || status.links.length === 0) {
|
||||
dom.content(box, [
|
||||
E('p', { style: 'color:#f44336; font-weight:bold; padding:20px;' },
|
||||
_('LinkBack daemon is not running or no links are configured. Please check Settings page.'))
|
||||
_('LinkBack daemon is not running or no targets are configured. Please check Settings page.'))
|
||||
]);
|
||||
return;
|
||||
}
|
||||
@@ -154,7 +168,7 @@ return view.extend({
|
||||
var table = E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th', 'style': 'text-align:center; width:5%;' }, '#'),
|
||||
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Link Name')),
|
||||
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Target / Interface')),
|
||||
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Status')),
|
||||
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Routing Attributes')),
|
||||
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Latency Details'))
|
||||
|
||||
@@ -8,42 +8,70 @@ return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('linkback'),
|
||||
uci.load('network')
|
||||
uci.load('network'),
|
||||
uci.load('firewall').catch(function() { return {}; })
|
||||
]);
|
||||
},
|
||||
|
||||
// Comprehensive validation: check all conditions required for the service
|
||||
// to be safely enabled. Returns an error message string, or null if OK.
|
||||
_validateServiceConfig: function() {
|
||||
var global_sec = uci.sections('linkback', 'global')[0] || {};
|
||||
var mode = global_sec.mode || 'multi_wan';
|
||||
var link_sections = uci.sections('linkback', 'link') || [];
|
||||
|
||||
// Rule 1: At least 2 interfaces
|
||||
// Rule 1: At least 2 targets
|
||||
if (link_sections.length < 2) {
|
||||
return _('Cannot enable service: At least 2 monitored WAN interfaces must be configured for failover switcher.');
|
||||
return _('Cannot enable service: At least 2 monitored links/gateways must be configured for failover switcher.');
|
||||
}
|
||||
|
||||
// Rule 2: ALL interfaces must have a health check configured
|
||||
if (mode === 'multi_gw') {
|
||||
var iface = global_sec.interface || 'wan';
|
||||
if (!iface) {
|
||||
return _('Cannot enable service: WAN interface must be configured in Multi-Gateway mode.');
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 2: ALL targets must have a health check configured
|
||||
var priorities = {};
|
||||
var gateways = {};
|
||||
var names = {};
|
||||
|
||||
for (var i = 0; i < link_sections.length; i++) {
|
||||
var s_id = link_sections[i]['.name'];
|
||||
var iface_name = uci.get('linkback', s_id, 'name') || s_id;
|
||||
var target_name = uci.get('linkback', s_id, 'name') || s_id;
|
||||
var gw = uci.get('linkback', s_id, 'gateway');
|
||||
var prio = uci.get('linkback', s_id, 'priority') || '1';
|
||||
|
||||
var has_check = uci.get('linkback', s_id, 'ping_targets') ||
|
||||
uci.get('linkback', s_id, 'dns_server') ||
|
||||
uci.get('linkback', s_id, 'tcp_target');
|
||||
if (!has_check) {
|
||||
return _('Cannot enable service: Interface "%s" has no health check configured.').format(iface_name);
|
||||
return _('Cannot enable service: Target "%s" has no health check configured.').format(target_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 3: No duplicate priorities
|
||||
var priorities = {};
|
||||
for (var i = 0; i < link_sections.length; i++) {
|
||||
var s_id = link_sections[i]['.name'];
|
||||
var iface_name = uci.get('linkback', s_id, 'name') || s_id;
|
||||
var prio = uci.get('linkback', s_id, 'priority') || '1';
|
||||
// Priority uniqueness
|
||||
if (priorities[prio]) {
|
||||
return _('Cannot enable service: Interfaces "%s" and "%s" have the same priority %s.').format(priorities[prio], iface_name, prio);
|
||||
return _('Cannot enable service: Targets "%s" and "%s" have the same priority %s.').format(priorities[prio], target_name, prio);
|
||||
}
|
||||
priorities[prio] = target_name;
|
||||
|
||||
// Mode specific checks
|
||||
if (mode === 'multi_gw') {
|
||||
var ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
if (!gw || !ipPattern.test(gw)) {
|
||||
return _('Cannot enable service: Target "%s" has an invalid or missing Gateway IP.').format(target_name);
|
||||
}
|
||||
if (gateways[gw]) {
|
||||
return _('Cannot enable service: Targets "%s" and "%s" have the same Gateway IP %s.').format(gateways[gw], target_name, gw);
|
||||
}
|
||||
gateways[gw] = target_name;
|
||||
} else {
|
||||
if (names[target_name]) {
|
||||
return _('Cannot enable service: Interface "%s" is configured multiple times.').format(target_name);
|
||||
}
|
||||
names[target_name] = true;
|
||||
}
|
||||
priorities[prio] = iface_name;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -53,11 +81,48 @@ return view.extend({
|
||||
var m, s, o;
|
||||
var self = this;
|
||||
|
||||
var getActiveMode = function() {
|
||||
var global_sec = uci.sections('linkback', 'global')[0] || {};
|
||||
var sid = global_sec['.name'] || '@global[0]';
|
||||
var sel = document.querySelector('[name="cbid.linkback.' + sid + '.mode"]') ||
|
||||
document.querySelector('select[id*="mode"], select[name*="mode"]');
|
||||
if (sel && sel.value) {
|
||||
return sel.value;
|
||||
}
|
||||
return uci.get('linkback', sid, 'mode') || 'multi_wan';
|
||||
};
|
||||
|
||||
var current_mode = getActiveMode();
|
||||
|
||||
// 智能提取 firewall 中 wan 区域关联的所有网络接口
|
||||
var wan_interfaces = {};
|
||||
var has_wan_zone = false;
|
||||
uci.sections('firewall', 'zone').forEach(function(sec) {
|
||||
if (sec.name === 'wan') {
|
||||
has_wan_zone = true;
|
||||
var nets = sec.network;
|
||||
if (Array.isArray(nets)) {
|
||||
nets.forEach(function(n) { if (n) wan_interfaces[n] = true; });
|
||||
} else if (typeof(nets) === 'string') {
|
||||
nets.split(/\s+/).forEach(function(n) { if (n) wan_interfaces[n] = true; });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 若未配置 wan 区域,则安全回退至系统非回环非 lan 接口
|
||||
if (!has_wan_zone || Object.keys(wan_interfaces).length === 0) {
|
||||
uci.sections('network', 'interface').forEach(function(sec) {
|
||||
var n = sec['.name'];
|
||||
if (n !== 'loopback' && n !== 'lan') {
|
||||
wan_interfaces[n] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 智能判断当前是否为中文环境,并提供实时校验的翻译 fallback
|
||||
var is_zh = (_('Base Metric') === '默认跃点' || _('Base Metric') === '默认跃点 (Metric)');
|
||||
var t_priority_empty = is_zh ? '优先级不能为空。' : _('Priority must not be empty.');
|
||||
var t_priority_conflict = is_zh ? '优先级 %s 与接口 "%s" 冲突。优先级必须是唯一的。' : _('Priority %s conflicts with interface "%s". Priorities must be unique.');
|
||||
|
||||
var t_priority_conflict = is_zh ? '优先级 %s 与其他目标冲突。优先级必须是唯一的。' : _('Priority %s conflicts with another target. Priorities must be unique.');
|
||||
|
||||
// Helper function to expand table column controls and eliminate right empty space
|
||||
var makeTableColumnExpand = function(opt, width) {
|
||||
@@ -81,17 +146,16 @@ return view.extend({
|
||||
|
||||
m = new form.Map('linkback',
|
||||
_('LinkBack 链路守护') + ' - ' + _('Settings'),
|
||||
_('Configure Multi-WAN failover service, health check parameters, and monitored link interfaces.'));
|
||||
_('Configure Multi-WAN / Multi-Gateway failover service, shared health check parameters, and monitored targets.'));
|
||||
|
||||
// --- Global Settings Section ---
|
||||
s = m.section(form.TypedSection, 'global', _('Global Settings'));
|
||||
s.anonymous = true;
|
||||
|
||||
// Enable switch (总开关) - must be the first option
|
||||
// 1. Enable switch
|
||||
o = s.option(form.Flag, 'enabled', _('Enable Service'),
|
||||
_('Master switch to enable or disable the LinkBack failover daemon.'));
|
||||
o.rmempty = false;
|
||||
// Intercept at UCI write level to validate before enabling.
|
||||
o.write = function(section_id, value) {
|
||||
if (value === '1') {
|
||||
var err = self._validateServiceConfig();
|
||||
@@ -104,27 +168,94 @@ return view.extend({
|
||||
uci.set('linkback', section_id, 'enabled', value);
|
||||
};
|
||||
|
||||
// --- Monitored Links Section ---
|
||||
s = m.section(form.GridSection, 'link', _('Monitored WAN Interfaces'),
|
||||
_('Add and prioritize your WAN interfaces. Lower priority number means higher preference (e.g., 1 = primary, 2 = backup).'));
|
||||
// 2. Working Mode
|
||||
o = s.option(form.ListValue, 'mode', _('Working Mode'),
|
||||
_('Choose failover mode: Multi-WAN interface failover or Single-WAN multi-gateway redundancy.'));
|
||||
o.value('multi_wan', _('多wan口模式'));
|
||||
o.value('multi_gw', _('单wan多网关模式'));
|
||||
o.default = 'multi_wan';
|
||||
o.rmempty = false;
|
||||
o.onchange = function(ev, section_id, value) {
|
||||
uci.set('linkback', section_id, 'mode', value);
|
||||
var links = uci.sections('linkback', 'link') || [];
|
||||
if (links.length > 0) {
|
||||
if (value === 'multi_gw') {
|
||||
ui.addNotification(null, E('p',
|
||||
_('已切换为单wan多网关模式。请确保监控目标列表中配置了有效的网关 IP。')
|
||||
), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p',
|
||||
_('已切换为多wan口模式。请确保监控目标列表中选择了正确的 WAN 接口。')
|
||||
), 'info');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 3. WAN Interface in Multi-Gateway mode
|
||||
o = s.option(form.ListValue, 'interface', _('wan网络接口'),
|
||||
_('选择存在多个wan网关的接口'));
|
||||
o.default = 'wan';
|
||||
o.rmempty = false;
|
||||
o.depends('mode', 'multi_gw');
|
||||
|
||||
Object.keys(wan_interfaces).forEach(function(iface) {
|
||||
o.value(iface);
|
||||
});
|
||||
|
||||
// 4. Global Shared Health Check & Failover Parameters (共用调度超时)
|
||||
o = s.option(form.Value, 'check_interval', _('Check Interval (s)'),
|
||||
_('Shared interval in seconds between health check cycles for all targets.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'check_timeout', _('Check Timeout (s)'),
|
||||
_('Shared maximum wait time in seconds for each probe (1s recommended to prevent blocking).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'recovery_delay', _('Recovery Delay'),
|
||||
_('Number of consecutive successful checks required before marking link as recovered (failback anti-flap).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '3';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'failover_delay', _('Failover Delay'),
|
||||
_('Number of consecutive failed checks required before marking link as faulted (failover anti-flap).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
// --- Monitored Targets Section ---
|
||||
s = m.section(form.GridSection, 'link', _('Monitored Targets'),
|
||||
_('Configure monitored WAN interfaces or next-hop gateways with custom probe targets and priorities (1 = primary, 2 = backup).'));
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
|
||||
// Custom dynamic Modal title for gorgeous UX
|
||||
// Custom dynamic Modal title
|
||||
s.modaltitle = function(section_id) {
|
||||
var parent_title = _('LinkBack 链路守护') + ' - ' + _('Settings');
|
||||
var is_new = (this.map.addedSection === section_id) || !uci.get('linkback', section_id, 'name');
|
||||
if (is_new) {
|
||||
return parent_title + ' - ' + _('Add Monitored Interface');
|
||||
var mode = getActiveMode();
|
||||
var is_new = (this.map.addedSection === section_id) ||
|
||||
(mode === 'multi_gw' ? !uci.get('linkback', section_id, 'gateway') : !uci.get('linkback', section_id, 'name'));
|
||||
if (mode === 'multi_gw') {
|
||||
if (is_new) {
|
||||
return parent_title + ' - ' + _('Add Monitored Gateway');
|
||||
} else {
|
||||
var gw = uci.get('linkback', section_id, 'gateway') || uci.get('linkback', section_id, 'name') || section_id;
|
||||
return parent_title + ' - ' + _('Edit Monitored Gateway') + ' (' + gw + ')';
|
||||
}
|
||||
} else {
|
||||
var name = uci.get('linkback', section_id, 'name') || section_id;
|
||||
return parent_title + ' - ' + _('Edit Monitored Interface') + ' (' + name + ')';
|
||||
if (is_new) {
|
||||
return parent_title + ' - ' + _('Add Monitored WAN Interface');
|
||||
} else {
|
||||
var iface = uci.get('linkback', section_id, 'name') || section_id;
|
||||
return parent_title + ' - ' + _('Edit Monitored WAN Interface') + ' (' + iface + ')';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// When service is already enabled and user modifies/deletes links,
|
||||
// re-validate on the GridSection level. If the resulting config is invalid,
|
||||
// show a warning and auto-disable the service to maintain safety.
|
||||
var origRemove = s.handleRemove;
|
||||
s.handleRemove = function(section_id, ev) {
|
||||
return origRemove.apply(this, arguments).then(function() {
|
||||
@@ -141,78 +272,147 @@ return view.extend({
|
||||
});
|
||||
};
|
||||
|
||||
// 1. Enabled (Enabled as the first column)
|
||||
// 1. Enabled
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
makeTableColumnExpand(o, '8%');
|
||||
|
||||
// 2. Interface name - dynamic dropdown (No description for main column)
|
||||
o = s.option(form.ListValue, 'name', _('Interface'));
|
||||
o.rmempty = false;
|
||||
|
||||
// Once-off load of all network interfaces safely avoiding race conditions
|
||||
var network_interfaces = {};
|
||||
uci.sections('network', 'interface').forEach(function(sec) {
|
||||
var n = sec['.name'];
|
||||
if (n !== 'loopback' && n !== 'lan') {
|
||||
network_interfaces[n] = true;
|
||||
o.value(n);
|
||||
}
|
||||
});
|
||||
|
||||
// Safeguard to show already configured interfaces even if they were deleted from network config
|
||||
uci.sections('linkback', 'link').forEach(function(sec) {
|
||||
if (sec.name && !network_interfaces[sec.name]) {
|
||||
o.value(sec.name, _('%s (configured)').format(sec.name));
|
||||
}
|
||||
});
|
||||
|
||||
// 动态隐藏已被配置的接口:通过在 renderWidget 中进行局部变量过滤与实例化 ui.Select,
|
||||
// 完全不修改全局的 this.keylist 和 this.keyvalues,保证 100% 线程安全和无副作用
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
// 找出已经被其他 Section 选中的网卡(已配置接口名)
|
||||
var used_names = {};
|
||||
uci.sections('linkback', 'link').forEach(function(sec) {
|
||||
if (sec['.name'] !== section_id && sec.name) {
|
||||
used_names[sec.name] = true;
|
||||
// 2. Monitored Target Display (Table only)
|
||||
o = s.option(form.DummyValue, '_target_disp', _('Monitored Target'));
|
||||
o.modalonly = false;
|
||||
o.cfgvalue = function(section_id) {
|
||||
var gw = uci.get('linkback', section_id, 'gateway');
|
||||
var name = uci.get('linkback', section_id, 'name');
|
||||
var mode = getActiveMode();
|
||||
if (mode === 'multi_gw' || gw) {
|
||||
if (gw) {
|
||||
if (name && name !== gw) {
|
||||
return gw + ' (' + name + ')';
|
||||
}
|
||||
return gw;
|
||||
}
|
||||
});
|
||||
return name ? (_('Missing Gateway IP: %s').format(name)) : _('Unset');
|
||||
} else {
|
||||
return name || _('Unselected');
|
||||
}
|
||||
};
|
||||
makeTableColumnExpand(o, '24%');
|
||||
|
||||
// 动态生成仅针对当前渲染 section_id 的 choices 和 keylist
|
||||
var filtered_choices = {};
|
||||
var filtered_keylist = [];
|
||||
if (Array.isArray(this.keylist)) {
|
||||
for (var i = 0; i < this.keylist.length; i++) {
|
||||
var key = this.keylist[i];
|
||||
if (!used_names[key]) {
|
||||
filtered_keylist.push(key);
|
||||
filtered_choices[key] = this.vallist[i];
|
||||
// 3a. Gateway IP (Multi-GW mode only, Modal only)
|
||||
var o_gw = s.option(form.Value, 'gateway', _('Gateway IP'),
|
||||
_('Next-hop IPv4 address of this gateway (e.g. 192.168.1.254).'));
|
||||
o_gw.datatype = 'ip4addr';
|
||||
o_gw.modalonly = true;
|
||||
o_gw.placeholder = '192.168.1.254';
|
||||
var origRenderGw = o_gw.render;
|
||||
o_gw.render = function(option_index, section_id, in_table) {
|
||||
if (getActiveMode() !== 'multi_gw') {
|
||||
return Promise.resolve(E('div', { 'style': 'display: none !important;' }));
|
||||
}
|
||||
return origRenderGw.call(this, option_index, section_id, in_table);
|
||||
};
|
||||
o_gw.validate = function(section_id, value) {
|
||||
if (getActiveMode() !== 'multi_gw') {
|
||||
return true;
|
||||
}
|
||||
if (!value || value.trim() === '') {
|
||||
return _('Gateway IP is required.');
|
||||
}
|
||||
var val = value.trim();
|
||||
var self_opt = this;
|
||||
var conflict = false;
|
||||
uci.sections('linkback', 'link').forEach(function(sec) {
|
||||
var sid = sec['.name'];
|
||||
if (sid !== section_id) {
|
||||
var other_gw = self_opt.formvalue(sid) || uci.get('linkback', sid, 'gateway');
|
||||
if (other_gw && other_gw === val) {
|
||||
conflict = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断当前是编辑已有配置还是添加新配置
|
||||
// 如果在 UCI 中已经保存了 name,则是编辑状态,此时网卡不可编辑
|
||||
var is_edit = !!uci.get('linkback', section_id, 'name');
|
||||
|
||||
// 直接在局部变量中实例化 ui.Select,避免任何时序与引用问题
|
||||
var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, filtered_choices, {
|
||||
id: this.cbid(section_id),
|
||||
size: this.size,
|
||||
sort: filtered_keylist,
|
||||
widget: this.widget,
|
||||
optional: this.optional,
|
||||
orientation: this.orientation,
|
||||
placeholder: this.placeholder,
|
||||
validate: (typeof(this.getValidator) === 'function') ? this.getValidator(section_id) : (this.validate ? this.validate.bind(this, section_id) : null),
|
||||
disabled: is_edit ? true : ((this.readonly != null) ? this.readonly : this.map.readonly)
|
||||
});
|
||||
|
||||
return widget.render();
|
||||
if (conflict) {
|
||||
return _('Gateway IP %s is already used by another link.').format(val);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
o_gw.write = function(section_id, value) {
|
||||
if (getActiveMode() === 'multi_gw') {
|
||||
if (value != null && value.trim() !== '') {
|
||||
uci.set('linkback', section_id, 'gateway', value.trim());
|
||||
} else {
|
||||
uci.remove('linkback', section_id, 'gateway');
|
||||
}
|
||||
} else {
|
||||
uci.remove('linkback', section_id, 'gateway');
|
||||
}
|
||||
};
|
||||
|
||||
o.validate = function(section_id, value) {
|
||||
// 3b. Gateway Alias (Multi-GW mode only, Modal only)
|
||||
var o_alias = s.option(form.Value, 'name_alias', _('Gateway Alias (Optional)'),
|
||||
_('Descriptive alias for this gateway (e.g. Bypass_GW, Main_Router). If empty, Gateway IP will be used.'));
|
||||
o_alias.modalonly = true;
|
||||
o_alias.placeholder = 'Bypass_GW';
|
||||
var origRenderAlias = o_alias.render;
|
||||
o_alias.render = function(option_index, section_id, in_table) {
|
||||
if (getActiveMode() !== 'multi_gw') {
|
||||
return Promise.resolve(E('div', { 'style': 'display: none !important;' }));
|
||||
}
|
||||
return origRenderAlias.call(this, option_index, section_id, in_table);
|
||||
};
|
||||
o_alias.cfgvalue = function(section_id) {
|
||||
if (getActiveMode() === 'multi_gw') {
|
||||
var gw = uci.get('linkback', section_id, 'gateway');
|
||||
var name = uci.get('linkback', section_id, 'name');
|
||||
if (name && name !== gw) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
o_alias.write = function(section_id, value) {
|
||||
if (getActiveMode() === 'multi_gw') {
|
||||
var gw = uci.get('linkback', section_id, 'gateway');
|
||||
if (!value || value.trim() === '') {
|
||||
uci.set('linkback', section_id, 'name', gw || section_id);
|
||||
} else {
|
||||
uci.set('linkback', section_id, 'name', value.trim());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 3c. WAN Interface (Multi-WAN mode only, Modal only)
|
||||
var o_iface = s.option(form.ListValue, 'name_iface', _('WAN Interface'),
|
||||
_('Logical interface from firewall WAN zone.'));
|
||||
o_iface.modalonly = true;
|
||||
Object.keys(wan_interfaces).forEach(function(iface) {
|
||||
o_iface.value(iface);
|
||||
});
|
||||
uci.sections('linkback', 'link').forEach(function(sec) {
|
||||
if (sec.name && !wan_interfaces[sec.name]) {
|
||||
o_iface.value(sec.name, _('%s (configured)').format(sec.name));
|
||||
}
|
||||
});
|
||||
var origRenderIface = o_iface.render;
|
||||
o_iface.render = function(option_index, section_id, in_table) {
|
||||
if (getActiveMode() !== 'multi_wan') {
|
||||
return Promise.resolve(E('div', { 'style': 'display: none !important;' }));
|
||||
}
|
||||
return origRenderIface.call(this, option_index, section_id, in_table);
|
||||
};
|
||||
o_iface.cfgvalue = function(section_id) {
|
||||
if (getActiveMode() === 'multi_wan') {
|
||||
return uci.get('linkback', section_id, 'name');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
o_iface.validate = function(section_id, value) {
|
||||
if (getActiveMode() !== 'multi_wan') {
|
||||
return true;
|
||||
}
|
||||
if (!value) {
|
||||
return _('WAN Interface is required.');
|
||||
}
|
||||
var added = false;
|
||||
uci.sections('linkback', 'link').forEach(function(sec) {
|
||||
if (sec['.name'] !== section_id && sec.name === value) {
|
||||
@@ -224,11 +424,16 @@ return view.extend({
|
||||
}
|
||||
return true;
|
||||
};
|
||||
o_iface.write = function(section_id, value) {
|
||||
if (getActiveMode() === 'multi_wan') {
|
||||
uci.set('linkback', section_id, 'name', value);
|
||||
uci.remove('linkback', section_id, 'gateway');
|
||||
}
|
||||
};
|
||||
|
||||
makeTableColumnExpand(o, '25%');
|
||||
|
||||
// 3. Priority (No description for main column)
|
||||
o = s.option(form.Value, 'priority', _('Priority'));
|
||||
// 3. Priority
|
||||
o = s.option(form.Value, 'priority', _('Priority'),
|
||||
_('Lower number indicates higher priority (e.g. 1 = Primary, 2 = Backup).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
@@ -238,7 +443,6 @@ return view.extend({
|
||||
}
|
||||
var self_opt = this;
|
||||
var has_conflict = false;
|
||||
var conflicting_iface = '';
|
||||
uci.sections('linkback', 'link').forEach(function(sec) {
|
||||
var sid = sec['.name'];
|
||||
if (sid !== section_id) {
|
||||
@@ -248,16 +452,15 @@ return view.extend({
|
||||
}
|
||||
if (other_val != null && other_val !== '' && String(other_val) === String(value)) {
|
||||
has_conflict = true;
|
||||
conflicting_iface = uci.get('linkback', sid, 'name') || sid;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (has_conflict) {
|
||||
return t_priority_conflict.format(value, conflicting_iface);
|
||||
return t_priority_conflict.format(value);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
makeTableColumnExpand(o, '15%');
|
||||
makeTableColumnExpand(o, '12%');
|
||||
|
||||
// 4. Metric (Read-only, generated from priority * 10)
|
||||
var metric_title = _('Base Metric');
|
||||
@@ -273,7 +476,7 @@ return view.extend({
|
||||
}
|
||||
return prio_val * 10;
|
||||
};
|
||||
makeTableColumnExpand(o, '15%');
|
||||
makeTableColumnExpand(o, '12%');
|
||||
|
||||
// 5. Dummy display option for Check Type in main Grid table (read-only)
|
||||
o = s.option(form.DummyValue, 'check_type_disp', _('Check Type'));
|
||||
@@ -287,10 +490,11 @@ return view.extend({
|
||||
return _('TCP Probe');
|
||||
return _('-- Not Configured --');
|
||||
};
|
||||
makeTableColumnExpand(o, '20%');
|
||||
makeTableColumnExpand(o, '18%');
|
||||
|
||||
// 6. Check Type Dropdown (Virtual field, Modal only to avoid Save & Apply side-effects)
|
||||
o = s.option(form.ListValue, 'check_type', _('Check Type'));
|
||||
// 6. Check Type Dropdown (Virtual field, Modal only)
|
||||
o = s.option(form.ListValue, 'check_type', _('Check Type'),
|
||||
_('Health probe method for this specific target.'));
|
||||
o.value('ping', _('Ping Probe'));
|
||||
o.value('dns', _('DNS Probe'));
|
||||
o.value('tcp', _('TCP Probe'));
|
||||
@@ -298,7 +502,6 @@ return view.extend({
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
// Derive the check type from which probe target is configured in UCI.
|
||||
o.cfgvalue = function(section_id) {
|
||||
if (uci.get('linkback', section_id, 'ping_targets'))
|
||||
return 'ping';
|
||||
@@ -306,22 +509,19 @@ return view.extend({
|
||||
return 'dns';
|
||||
if (uci.get('linkback', section_id, 'tcp_target'))
|
||||
return 'tcp';
|
||||
return 'ping'; // Default to ping if not configured
|
||||
return 'ping';
|
||||
};
|
||||
|
||||
// Only write when the check type actually CHANGES from the current state.
|
||||
o.write = function(section_id, value) {
|
||||
var current = uci.get('linkback', section_id, 'ping_targets') ? 'ping' :
|
||||
(uci.get('linkback', section_id, 'dns_server') ? 'dns' :
|
||||
(uci.get('linkback', section_id, 'tcp_target') ? 'tcp' : ''));
|
||||
var next = (value == null) ? 'ping' : String(value);
|
||||
|
||||
// No change - do nothing
|
||||
if (next === current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear old type's fields and any legacy weights/thresholds
|
||||
uci.remove('linkback', section_id, 'weight_threshold');
|
||||
uci.remove('linkback', section_id, 'ping_weight');
|
||||
uci.remove('linkback', section_id, 'dns_weight');
|
||||
@@ -340,14 +540,15 @@ return view.extend({
|
||||
}
|
||||
};
|
||||
|
||||
// 7. Ping Probe Parameters
|
||||
// 7. Custom Ping Probe Parameters
|
||||
o = s.option(form.Value, 'ping_targets', _('Ping Targets'),
|
||||
_('Comma-separated list of IPs to ping (e.g., 223.5.5.5,8.8.8.8).'));
|
||||
o.rmempty = true;
|
||||
_('Custom IP list to ping through this link/gateway (comma-separated, e.g. 223.5.5.5,119.29.29.29).'));
|
||||
o.default = '223.5.5.5,119.29.29.29';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('check_type', 'ping');
|
||||
o.validate = function(section_id, value) {
|
||||
if (!value) return true;
|
||||
if (!value) return _('Ping Targets is required.');
|
||||
var ips = value.replace(/\s+/g, '').split(',');
|
||||
for (var i = 0; i < ips.length; i++) {
|
||||
var ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
@@ -366,64 +567,38 @@ return view.extend({
|
||||
}
|
||||
};
|
||||
|
||||
// 7. DNS Probe Parameters
|
||||
// 8. Custom DNS Probe Parameters
|
||||
o = s.option(form.Value, 'dns_server', _('DNS Server'),
|
||||
_('DNS server IP for UDP query probe (e.g., 119.29.29.29).'));
|
||||
_('DNS server IP for UDP query probe (e.g. 119.29.29.29).'));
|
||||
o.datatype = 'ip4addr';
|
||||
o.rmempty = true;
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('check_type', 'dns');
|
||||
|
||||
o = s.option(form.Value, 'dns_domain', _('DNS Domain'),
|
||||
_('Domain name to resolve for DNS probe (e.g., www.baidu.com).'));
|
||||
o.rmempty = true;
|
||||
_('Domain name to resolve for DNS probe (e.g. www.baidu.com).'));
|
||||
o.default = 'www.baidu.com';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('check_type', 'dns');
|
||||
|
||||
// 8. TCP Probe Parameters
|
||||
// 9. Custom TCP Probe Parameters
|
||||
o = s.option(form.Value, 'tcp_target', _('TCP Target'),
|
||||
_('Target IP for TCP handshake probe.'));
|
||||
o.datatype = 'ip4addr';
|
||||
o.rmempty = true;
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('check_type', 'tcp');
|
||||
|
||||
o = s.option(form.Value, 'tcp_port', _('TCP Port'),
|
||||
_('Target port for TCP handshake probe.'));
|
||||
_('Target port for TCP handshake probe (e.g. 80 or 443).'));
|
||||
o.datatype = 'port';
|
||||
o.rmempty = true;
|
||||
o.default = '80';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('check_type', 'tcp');
|
||||
|
||||
// 9. Individual health check options (Modal only)
|
||||
o = s.option(form.Value, 'check_interval', _('Check Interval (s)'),
|
||||
_('Time in seconds between each health check cycle for this link.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'check_timeout', _('Check Timeout (s)'),
|
||||
_('Maximum wait time in seconds for each individual check probe for this link.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '3';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'recovery_delay', _('Recovery Delay'),
|
||||
_('Number of consecutive successful checks required before marking this link as healthy (failback anti-flap).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '3';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'failover_delay', _('Failover Delay'),
|
||||
_('Number of consecutive failed checks required before marking this link as faulted (failover anti-flap).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
// 提示:超时与防抖延迟由全局配置统一管理,子项完全无需重复配置
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
msgid "LinkBack 链路守护"
|
||||
msgstr "网关切换"
|
||||
@@ -16,6 +16,12 @@ msgstr "日志"
|
||||
msgid "Real-time health state, priority mapping, and service control for Multi-WAN default routing metrics."
|
||||
msgstr "用于 Multi-WAN 默认路由 Metric 的实时健康状态、优先级映射及服务控制。"
|
||||
|
||||
msgid "Real-time health state, priority mapping, and service control for Multi-WAN / Multi-Gateway failover."
|
||||
msgstr "用于多 WAN / 多网关容灾切换的实时健康状态、优先级映射与服务控制。"
|
||||
|
||||
msgid "Configure Multi-WAN / Multi-Gateway failover service, shared health check parameters, and monitored targets."
|
||||
msgstr "配置多 WAN / 多网关容灾切换服务、全局共用健康检测参数与监控目标。"
|
||||
|
||||
msgid "Service Status"
|
||||
msgstr "服务状态"
|
||||
|
||||
@@ -37,6 +43,9 @@ msgstr "停止"
|
||||
msgid "Link Status"
|
||||
msgstr "链路状态"
|
||||
|
||||
msgid "Link / Gateway Status"
|
||||
msgstr "链路 / 网关状态"
|
||||
|
||||
msgid "Loading real-time link status..."
|
||||
msgstr "正在加载实时链路状态..."
|
||||
|
||||
@@ -49,18 +58,93 @@ msgstr "已停止"
|
||||
msgid "LinkBack daemon is not running or no links are configured. Please check Settings page."
|
||||
msgstr "网关切换守护进程未运行或未配置任何链路。请检查设置页面。"
|
||||
|
||||
msgid "主选链路 / Active"
|
||||
msgstr "主选网关 / 活跃中"
|
||||
msgid "LinkBack daemon is not running or no targets are configured. Please check Settings page."
|
||||
msgstr "网关切换守护进程未运行或未配置任何监控目标。请检查设置页面。"
|
||||
|
||||
msgid "次选链路 / Standby"
|
||||
msgstr "备选网关 / 待命"
|
||||
msgid "Working Mode"
|
||||
msgstr "工作模式"
|
||||
|
||||
msgid "备选链路 / Faulted"
|
||||
msgstr "故障网关 / 已断开"
|
||||
msgid "多wan口模式"
|
||||
msgstr "多wan口模式"
|
||||
|
||||
msgid "单wan多网关模式"
|
||||
msgstr "单wan多网关模式"
|
||||
|
||||
msgid "wan网络接口"
|
||||
msgstr "wan网络接口"
|
||||
|
||||
msgid "选择存在多个wan网关的接口"
|
||||
msgstr "选择存在多个wan网关的接口"
|
||||
|
||||
msgid "Choose failover mode: Multi-WAN interface failover or Single-WAN multi-gateway redundancy."
|
||||
msgstr "选择容灾模式:多 WAN 接口链路容灾,或单 WAN 多网关主备容灾。"
|
||||
|
||||
msgid "Monitored Gateways"
|
||||
msgstr "监控网关列表"
|
||||
|
||||
msgid "Monitored WAN Interfaces"
|
||||
msgstr "监控 WAN 接口列表"
|
||||
|
||||
msgid "Add next-hop gateways with custom probe targets and priorities (1 = primary, 2 = backup)."
|
||||
msgstr "添加下一跳网关并配置自定义探测目标与优先级 (1 = 主选,2 = 备选)。"
|
||||
|
||||
msgid "Add WAN interfaces from firewall zone with custom probe targets and priorities (1 = primary, 2 = backup)."
|
||||
msgstr "添加来自防火墙区域的 WAN 接口并配置自定义探测目标与优先级 (1 = 主选,2 = 备选)。"
|
||||
|
||||
msgid "WAN Interface"
|
||||
msgstr "WAN 接口"
|
||||
|
||||
msgid "Logical interface from firewall WAN zone."
|
||||
msgstr "来自防火墙 WAN 区域的逻辑接口。"
|
||||
|
||||
msgid "Gateway Alias (Optional)"
|
||||
msgstr "网关别名 (可选)"
|
||||
|
||||
msgid "Descriptive alias for this gateway (e.g. Bypass_GW, Main_Router). If empty, Gateway IP will be used."
|
||||
msgstr "该网关的自定义别名 (例如:旁路由主网关、光猫备用网关)。若留空则自动使用网关 IP。"
|
||||
|
||||
msgid "Gateway IP"
|
||||
msgstr "网关 IP"
|
||||
|
||||
msgid "Next-hop IPv4 address of this gateway (e.g. 192.168.1.254)."
|
||||
msgstr "该网关的下一跳 IPv4 地址 (例如:192.168.1.254)。"
|
||||
|
||||
msgid "Gateway IP is required."
|
||||
msgstr "网关 IP 不能为空。"
|
||||
|
||||
msgid "Gateway IP %s is already used by another link."
|
||||
msgstr "网关 IP %s 已被其他目标使用,必须唯一。"
|
||||
|
||||
msgid "Add Monitored Gateway"
|
||||
msgstr "添加监控网关"
|
||||
|
||||
msgid "Edit Monitored Gateway"
|
||||
msgstr "编辑监控网关"
|
||||
|
||||
msgid "Add Monitored WAN Interface"
|
||||
msgstr "添加监控 WAN 接口"
|
||||
|
||||
msgid "Edit Monitored WAN Interface"
|
||||
msgstr "编辑监控 WAN 接口"
|
||||
|
||||
msgid "Target / Interface"
|
||||
msgstr "监控目标 / 接口"
|
||||
|
||||
msgid "Running State"
|
||||
msgstr "运行状态"
|
||||
|
||||
msgid "Routing Attributes"
|
||||
msgstr "路由属性"
|
||||
|
||||
msgid "Latency Details"
|
||||
msgstr "时延明细"
|
||||
|
||||
msgid "Priority"
|
||||
msgstr "优先级"
|
||||
|
||||
msgid "Lower number indicates higher priority (e.g. 1 = Primary, 2 = Backup)."
|
||||
msgstr "数值越小代表优先级越高 (例如:1 = 主选,2 = 备选)。"
|
||||
|
||||
msgid "Metric"
|
||||
msgstr "路由跃点 (Metric)"
|
||||
|
||||
@@ -85,9 +169,6 @@ msgstr "服务操作执行失败。"
|
||||
msgid "RPC error: %s"
|
||||
msgstr "RPC 错误: %s"
|
||||
|
||||
msgid "Configure Multi-WAN failover service, health check parameters, and monitored link interfaces."
|
||||
msgstr "配置多 WAN 故障切换服务、健康度检查参数及被监控的网关接口。"
|
||||
|
||||
msgid "Global Settings"
|
||||
msgstr "全局设置"
|
||||
|
||||
@@ -100,39 +181,30 @@ msgstr "启用或禁用网关切换服务的全局总开关。"
|
||||
msgid "Check Interval (s)"
|
||||
msgstr "检测间隔 (秒)"
|
||||
|
||||
msgid "Time in seconds between each health check cycle."
|
||||
msgstr "每个健康度检测周期之间的时间间隔。"
|
||||
msgid "Shared interval in seconds between health check cycles for all targets."
|
||||
msgstr "所有链路与网关共用的健康检测周期时间 (秒)。"
|
||||
|
||||
msgid "Check Timeout (s)"
|
||||
msgstr "检测超时 (秒)"
|
||||
|
||||
msgid "Maximum wait time in seconds for each individual check probe."
|
||||
msgstr "每个检测探针的最大等待超时时间。"
|
||||
msgid "Shared maximum wait time in seconds for each probe (1s recommended to prevent blocking)."
|
||||
msgstr "所有探针共用的最大等待超时时间 (推荐 1 秒以杜绝网络阻塞)。"
|
||||
|
||||
msgid "Recovery Delay"
|
||||
msgstr "恢复判定延迟"
|
||||
|
||||
msgid "Number of consecutive successful checks required before marking a faulted link as healthy again (failback anti-flap)."
|
||||
msgid "Number of consecutive successful checks required before marking link as recovered (failback anti-flap)."
|
||||
msgstr "故障网关恢复健康所需连续检测成功的次数 (防抖动回切)。"
|
||||
|
||||
msgid "Failover Delay"
|
||||
msgstr "故障判定延迟"
|
||||
|
||||
msgid "Number of consecutive failed checks required before marking a healthy link as faulted (failover anti-flap)."
|
||||
msgid "Number of consecutive failed checks required before marking link as faulted (failover anti-flap)."
|
||||
msgstr "健康网关判定为故障所需连续检测失败的次数 (防抖动切换)。"
|
||||
|
||||
msgid "Monitored WAN Interfaces"
|
||||
msgstr "监控网关接口"
|
||||
|
||||
msgid "Add and prioritize your WAN interfaces. Lower priority number means higher preference (e.g., 1 = primary, 2 = backup)."
|
||||
msgstr "添加并配置您的广域网网关接口优先级。数值越小代表优先级越高 (例如:1 = 主网关,2 = 备用网关)。"
|
||||
|
||||
msgid "Interface"
|
||||
msgstr "网络接口"
|
||||
|
||||
msgid "Select a logical WAN interface from Network settings."
|
||||
msgstr "从系统网络配置中选择一个逻辑 WAN 接口。"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "已启用"
|
||||
|
||||
@@ -143,29 +215,29 @@ msgid "Default route metric when this link is healthy (e.g., wan=10, wan2=20)."
|
||||
msgstr "当该接口健康时应用的默认路由 Metric 跃点数值 (例如:wan=10, wan2=20)。"
|
||||
|
||||
msgid "Ping Targets"
|
||||
msgstr "Ping 检测目标"
|
||||
msgstr "Ping 探测目标"
|
||||
|
||||
msgid "Space-separated list of IPs to ping (e.g., 223.5.5.5 8.8.8.8)."
|
||||
msgstr "通过空格分隔的 Ping 目标 IP 地址列表 (例如:223.5.5.5 8.8.8.8)。"
|
||||
msgid "Custom IP list to ping through this link/gateway (comma-separated, e.g. 223.5.5.5,119.29.29.29)."
|
||||
msgstr "通过该链路/网关探测的目标 IP 列表 (逗号分隔,例如:223.5.5.5,119.29.29.29)。"
|
||||
|
||||
msgid "Ping Weight"
|
||||
msgstr "Ping 权重"
|
||||
msgid "Ping Targets is required."
|
||||
msgstr "Ping 探测目标 IP 不能为空。"
|
||||
|
||||
msgid "Invalid IP address: \"%s\""
|
||||
msgstr "无效的 IP 地址: \"%s\""
|
||||
|
||||
msgid "DNS Server"
|
||||
msgstr "DNS 服务器"
|
||||
|
||||
msgid "DNS server IP for UDP query probe (e.g., 119.29.29.29)."
|
||||
msgid "DNS server IP for UDP query probe (e.g. 119.29.29.29)."
|
||||
msgstr "用于 UDP 解析探测的 DNS 服务器 IP (例如:119.29.29.29)。"
|
||||
|
||||
msgid "DNS Domain"
|
||||
msgstr "DNS 解析域名"
|
||||
|
||||
msgid "Domain name to resolve for DNS probe (e.g., www.baidu.com)."
|
||||
msgid "Domain name to resolve for DNS probe (e.g. www.baidu.com)."
|
||||
msgstr "用于 DNS 探针解析的测试目标域名 (例如:www.baidu.com)。"
|
||||
|
||||
msgid "DNS Weight"
|
||||
msgstr "DNS 权重"
|
||||
|
||||
msgid "TCP Target"
|
||||
msgstr "TCP 检测目标"
|
||||
|
||||
@@ -175,17 +247,8 @@ msgstr "用于 TCP 三次握手探测的目标主机 IP 地址。"
|
||||
msgid "TCP Port"
|
||||
msgstr "TCP 检测端口"
|
||||
|
||||
msgid "Target port for TCP handshake probe."
|
||||
msgstr "用于 TCP 探测的测试目标端口号。"
|
||||
|
||||
msgid "TCP Weight"
|
||||
msgstr "TCP 权重"
|
||||
|
||||
msgid "Health Threshold"
|
||||
msgstr "健康门槛"
|
||||
|
||||
msgid "Combined weight score required to mark this link healthy (e.g., if threshold is 2, any 2 successful checks suffice)."
|
||||
msgstr "判定该接口健康所需的各指标合并通过总分值 (例如:门槛为 2,任意两项检测成功即可)。"
|
||||
msgid "Target port for TCP handshake probe (e.g. 80 or 443)."
|
||||
msgstr "用于 TCP 探测的测试目标端口号 (例如:80 或 443)。"
|
||||
|
||||
msgid "Loading logs..."
|
||||
msgstr "正在加载运行日志..."
|
||||
@@ -199,71 +262,32 @@ msgstr "系统日志"
|
||||
msgid "No logs available."
|
||||
msgstr "暂无可用运行日志。"
|
||||
|
||||
msgid "Enable Ping Probe"
|
||||
msgstr "启用 Ping 检测"
|
||||
|
||||
msgid "Enable DNS Probe"
|
||||
msgstr "启用 DNS 检测"
|
||||
|
||||
msgid "Enable TCP Probe"
|
||||
msgstr "启用 TCP 检测"
|
||||
|
||||
msgid "Select a logical WAN interface."
|
||||
msgstr "选择逻辑 WAN 接口。"
|
||||
|
||||
msgid "Routing priority (lower = higher preference)."
|
||||
msgstr "路由优先级(数值越小优先级越高)。"
|
||||
|
||||
msgid "Default route metric when healthy."
|
||||
msgstr "接口健康时的默认路由跃点 (Metric) 数值。"
|
||||
|
||||
msgid "Combined weight score required to mark this link healthy."
|
||||
msgstr "判定该接口健康所需的各检测指标成功权重分之和。"
|
||||
|
||||
msgid "Collecting..."
|
||||
msgstr "正在收集数据..."
|
||||
|
||||
msgid "Running State"
|
||||
msgstr "运行状态"
|
||||
|
||||
msgid "Check Type"
|
||||
msgstr "检测类型"
|
||||
|
||||
msgid "Time in seconds between each health check cycle for this link."
|
||||
msgstr "该链路每次健康度检测周期之间的时间间隔 (秒)。"
|
||||
msgid "Health probe method for this specific target."
|
||||
msgstr "该目标采用的健康探测方式。"
|
||||
|
||||
msgid "Maximum wait time in seconds for each individual check probe for this link."
|
||||
msgstr "该链路每个检测探针的最大等待超时时间 (秒)。"
|
||||
msgid "Cannot enable service: At least 2 monitored links/gateways must be configured for failover switcher."
|
||||
msgstr "无法启用服务: 容灾切换至少需要配置 2 个监控链路或网关。"
|
||||
|
||||
msgid "Number of consecutive successful checks required before marking this link as healthy (failback anti-flap)."
|
||||
msgstr "判定该链路恢复健康所需连续检测成功的次数 (防抖动回切)。"
|
||||
msgid "Cannot enable service: WAN interface must be configured in Multi-Gateway mode."
|
||||
msgstr "无法启用服务: 单wan多网关模式下必须指定wan网络接口。"
|
||||
|
||||
msgid "Number of consecutive failed checks required before marking this link as faulted (failover anti-flap)."
|
||||
msgstr "判定该链路发生故障所需连续检测失败的次数 (防抖动切换)。"
|
||||
msgid "Cannot enable service: Target \"%s\" has no health check configured."
|
||||
msgstr "无法启用服务: 监控目标「%s」未配置健康检测策略。"
|
||||
|
||||
msgid "Add Monitored Interface"
|
||||
msgstr "添加监控接口"
|
||||
msgid "Cannot enable service: Targets \"%s\" and \"%s\" have the same priority %s."
|
||||
msgstr "无法启用服务: 目标「%s」和「%s」的路由优先级均为 %s,不允许重复。"
|
||||
|
||||
msgid "Edit Monitored Interface"
|
||||
msgstr "编辑监控接口"
|
||||
msgid "Cannot enable service: Target \"%s\" has an invalid or missing Gateway IP."
|
||||
msgstr "无法启用服务: 目标「%s」缺少有效的网关 IP。"
|
||||
|
||||
msgid "Cannot enable service: No monitored WAN interfaces are configured."
|
||||
msgstr "无法启用服务: 未配置任何监控的 WAN 接口。"
|
||||
msgid "Cannot enable service: Targets \"%s\" and \"%s\" have the same Gateway IP %s."
|
||||
msgstr "无法启用服务: 目标「%s」和「%s」具有相同的网关 IP %s,不允许重复。"
|
||||
|
||||
msgid "Cannot enable service: At least one interface must have a configured check type (Ping, DNS, or TCP)."
|
||||
msgstr "无法启用服务: 至少需要为一个接口配置有效的检测类型 (Ping、DNS 或 TCP)。"
|
||||
|
||||
msgid "-- Not Configured --"
|
||||
msgstr "-- 未配置 --"
|
||||
|
||||
msgid "Cannot enable service: At least 2 monitored WAN interfaces must be configured for failover switcher."
|
||||
msgstr "无法启用服务: 链路备份切换至少需要配置 2 个监控的 WAN 接口。"
|
||||
|
||||
msgid "Cannot enable service: Interface \"%s\" has no health check configured."
|
||||
msgstr "无法启用服务: 接口「%s」未配置健康检测策略。"
|
||||
|
||||
msgid "Cannot enable service: Interfaces \"%s\" and \"%s\" have the same priority %s."
|
||||
msgstr "无法启用服务: 接口「%s」和「%s」的路由优先级均为 %s,不允许重复。"
|
||||
msgid "Cannot enable service: Interface \"%s\" is configured multiple times."
|
||||
msgstr "无法启用服务: 接口「%s」被重复配置。"
|
||||
|
||||
msgid "Service has been auto-disabled because the configuration is no longer valid: "
|
||||
msgstr "由于配置不再合法,服务已被自动禁用: "
|
||||
@@ -286,37 +310,45 @@ msgstr "该接口已被配置,请选择其他接口。"
|
||||
msgid "Priority must not be empty."
|
||||
msgstr "优先级不能为空。"
|
||||
|
||||
msgid "Priority %s conflicts with interface \"%s\". Priorities must be unique."
|
||||
msgstr "优先级 %s 与接口 \"%s\" 冲突。优先级必须是唯一的。"
|
||||
|
||||
msgid "Metric must not be empty."
|
||||
msgstr "默认跃点不能为空。"
|
||||
|
||||
msgid "Metric %s conflicts with interface \"%s\". Metrics must be unique."
|
||||
msgstr "跃点数 %s 与接口 \"%s\" 冲突。跃点数必须是唯一的。"
|
||||
|
||||
msgid "Link Name"
|
||||
msgstr "链路名称"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
msgid "Routing Attributes"
|
||||
msgstr "路由属性"
|
||||
|
||||
msgid "Latency Details"
|
||||
msgstr "时延详情"
|
||||
msgid "Priority %s conflicts with another target. Priorities must be unique."
|
||||
msgstr "优先级 %s 与其他目标冲突。优先级必须是唯一的。"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "活跃"
|
||||
msgstr "主选活跃"
|
||||
|
||||
msgid "Standby"
|
||||
msgstr "备选"
|
||||
msgstr "备选待命"
|
||||
|
||||
msgid "Faulted"
|
||||
msgstr "故障"
|
||||
msgstr "故障离线"
|
||||
|
||||
msgid "Comma-separated list of IPs to ping (e.g., 223.5.5.5,8.8.8.8)."
|
||||
msgstr "通过逗号分隔的 Ping 目标 IP 地址列表 (例如:223.5.5.5,8.8.8.8)。"
|
||||
msgid "-- Not Configured --"
|
||||
msgstr "-- 未配置 --"
|
||||
|
||||
msgid "Monitored Targets"
|
||||
msgstr "监控目标列表"
|
||||
|
||||
msgid "Configure monitored WAN interfaces or next-hop gateways with custom probe targets and priorities (1 = primary, 2 = backup)."
|
||||
msgstr "配置监控的 WAN 接口或下一跳网关,并设置自定义探测目标与优先级 (1 = 主选,2 = 备选)。"
|
||||
|
||||
msgid "Monitored Target"
|
||||
msgstr "监控目标"
|
||||
|
||||
msgid "Missing Gateway IP: %s"
|
||||
msgstr "缺少网关 IP: %s"
|
||||
|
||||
msgid "Unset"
|
||||
msgstr "未设置"
|
||||
|
||||
msgid "Unselected"
|
||||
msgstr "未选择"
|
||||
|
||||
msgid "WAN Interface is required."
|
||||
msgstr "WAN 网络接口不能为空。"
|
||||
|
||||
msgid "已切换为单wan多网关模式。请确保监控目标列表中配置了有效的网关 IP。"
|
||||
msgstr "已切换为单wan多网关模式。请确保监控目标列表中配置了有效的网关 IP。"
|
||||
|
||||
msgid "已切换为多wan口模式。请确保监控目标列表中选择了正确的 WAN 接口。"
|
||||
msgstr "已切换为多wan口模式。请确保监控目标列表中选择了正确的 WAN 接口。"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"luci-app-linkback": {
|
||||
"description": "ACL for LinkBack Multi-WAN Failover application",
|
||||
"read": {
|
||||
"uci": [ "linkback", "network" ],
|
||||
"uci": [ "linkback", "network", "firewall" ],
|
||||
"ubus": {
|
||||
"luci.linkback": [ "get_status", "get_log" ],
|
||||
"network.interface": [ "dump" ],
|
||||
|
||||
@@ -22,6 +22,8 @@ const methods = {
|
||||
if (!status) {
|
||||
status = {
|
||||
enabled: false,
|
||||
mode: "multi_wan",
|
||||
interface: "wan",
|
||||
check_interval: 5,
|
||||
active_link: "none",
|
||||
links: []
|
||||
|
||||
Reference in new issue
Block a user