mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 23:12:52 +08:00
628 lines
21 KiB
JavaScript
628 lines
21 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require uci';
|
|
'require form';
|
|
'require ui';
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
uci.load('linkback'),
|
|
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 targets
|
|
if (link_sections.length < 2) {
|
|
return _('Cannot enable service: At least 2 monitored links/gateways must be configured for failover switcher.');
|
|
}
|
|
|
|
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 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: Target "%s" has no health check configured.').format(target_name);
|
|
}
|
|
|
|
// Priority uniqueness
|
|
if (priorities[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;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
render: function() {
|
|
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 与其他目标冲突。优先级必须是唯一的。' : _('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) {
|
|
var origRender = opt.render;
|
|
opt.render = function(option_index, section_id, in_table) {
|
|
return Promise.resolve(origRender.call(this, option_index, section_id, in_table)).then(function(node) {
|
|
if (in_table && node) {
|
|
if (width) {
|
|
node.style.width = width;
|
|
}
|
|
var input = node.querySelector('input, select, .cbi-dropdown');
|
|
if (input) {
|
|
input.style.width = '100%';
|
|
input.style.maxWidth = 'none';
|
|
}
|
|
}
|
|
return node;
|
|
});
|
|
};
|
|
};
|
|
|
|
m = new form.Map('linkback',
|
|
_('LinkBack 链路守护') + ' - ' + _('Settings'),
|
|
_('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;
|
|
|
|
// 1. Enable switch
|
|
o = s.option(form.Flag, 'enabled', _('Enable Service'),
|
|
_('Master switch to enable or disable the LinkBack failover daemon.'));
|
|
o.rmempty = false;
|
|
o.write = function(section_id, value) {
|
|
if (value === '1') {
|
|
var err = self._validateServiceConfig();
|
|
if (err) {
|
|
ui.addNotification(null, E('p', err), 'error');
|
|
uci.set('linkback', section_id, 'enabled', '0');
|
|
return;
|
|
}
|
|
}
|
|
uci.set('linkback', section_id, 'enabled', value);
|
|
};
|
|
|
|
var refreshTargetsSection = null;
|
|
|
|
// 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);
|
|
// 切换工作模式时直接清空旧链路配置,保持单模式纯净性
|
|
uci.sections('linkback', 'link').forEach(function(sec) {
|
|
uci.remove('linkback', sec['.name']);
|
|
});
|
|
if (typeof refreshTargetsSection === 'function') {
|
|
refreshTargetsSection();
|
|
}
|
|
};
|
|
|
|
// 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 ---
|
|
var 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;
|
|
|
|
var origSectionRender = s.render;
|
|
s.render = function() {
|
|
var mode = getActiveMode();
|
|
if (mode === 'multi_gw') {
|
|
this.title = _('Monitored Gateways');
|
|
this.description = _('Add next-hop gateways with custom probe targets and priorities (1 = primary, 2 = backup).');
|
|
} else {
|
|
this.title = _('Monitored WAN Interfaces');
|
|
this.description = _('Add WAN interfaces from firewall zone with custom probe targets and priorities (1 = primary, 2 = backup).');
|
|
}
|
|
return origSectionRender.apply(this, arguments);
|
|
};
|
|
|
|
refreshTargetsSection = function() {
|
|
var old_node = document.getElementById('cbi-linkback-link') ||
|
|
document.querySelector('.cbi-section[data-tab="link"]') ||
|
|
document.querySelector('.cbi-section[id*="link"]');
|
|
if (old_node && old_node.parentNode) {
|
|
targets_section.render().then(function(new_node) {
|
|
if (old_node.parentNode) {
|
|
old_node.parentNode.replaceChild(new_node, old_node);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// Custom dynamic Modal title
|
|
s.modaltitle = function(section_id) {
|
|
var parent_title = _('LinkBack 链路守护') + ' - ' + _('Settings');
|
|
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 {
|
|
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 + ')';
|
|
}
|
|
}
|
|
};
|
|
|
|
var origRemove = s.handleRemove;
|
|
s.handleRemove = function(section_id, ev) {
|
|
return origRemove.apply(this, arguments).then(function() {
|
|
var enabled = uci.get('linkback', '@global[0]', 'enabled');
|
|
if (enabled === '1') {
|
|
var err = self._validateServiceConfig();
|
|
if (err) {
|
|
ui.addNotification(null, E('p',
|
|
_('Service has been auto-disabled because the configuration is no longer valid: ') + err
|
|
), 'warning');
|
|
uci.set('linkback', '@global[0]', 'enabled', '0');
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
// 1. Enabled
|
|
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
|
o.default = '1';
|
|
o.rmempty = false;
|
|
makeTableColumnExpand(o, '8%');
|
|
|
|
// 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');
|
|
if (gw) {
|
|
return (name && name !== gw) ? (gw + ' (' + name + ')') : gw;
|
|
}
|
|
return name || '-';
|
|
};
|
|
makeTableColumnExpand(o, '24%');
|
|
|
|
// 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.1).'));
|
|
o_gw.datatype = 'ip4addr';
|
|
o_gw.modalonly = true;
|
|
o_gw.placeholder = '192.168.1.1';
|
|
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;
|
|
}
|
|
}
|
|
});
|
|
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');
|
|
}
|
|
};
|
|
|
|
// 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. Primary_GW, Backup_GW). If empty, Gateway IP will be used.'));
|
|
o_alias.modalonly = true;
|
|
o_alias.placeholder = 'Primary_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) {
|
|
added = true;
|
|
}
|
|
});
|
|
if (added) {
|
|
return _('This interface has already been configured.');
|
|
}
|
|
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');
|
|
}
|
|
};
|
|
|
|
// 3. Priority
|
|
o = s.option(form.Value, 'priority', _('Priority'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '1';
|
|
o.rmempty = false;
|
|
var origRenderPrio = o.render;
|
|
o.render = function(option_index, section_id, in_table) {
|
|
if (in_table) {
|
|
this.description = null;
|
|
} else {
|
|
this.description = _('Lower number indicates higher priority (e.g. 1 = Primary, 2 = Backup).');
|
|
}
|
|
return origRenderPrio.call(this, option_index, section_id, in_table);
|
|
};
|
|
o.validate = function(section_id, value) {
|
|
if (value == null || value === '') {
|
|
return t_priority_empty;
|
|
}
|
|
var self_opt = this;
|
|
var has_conflict = false;
|
|
uci.sections('linkback', 'link').forEach(function(sec) {
|
|
var sid = sec['.name'];
|
|
if (sid !== section_id) {
|
|
var other_val = self_opt.formvalue(sid);
|
|
if (other_val == null || other_val === '') {
|
|
other_val = uci.get('linkback', sid, 'priority');
|
|
}
|
|
if (other_val != null && other_val !== '' && String(other_val) === String(value)) {
|
|
has_conflict = true;
|
|
}
|
|
}
|
|
});
|
|
if (has_conflict) {
|
|
return t_priority_conflict.format(value);
|
|
}
|
|
return true;
|
|
};
|
|
makeTableColumnExpand(o, '12%');
|
|
|
|
// 4. Metric (Read-only, generated from priority * 10)
|
|
var metric_title = _('Base Metric');
|
|
if (metric_title === '默认跃点 (Metric)') {
|
|
metric_title = '默认跃点';
|
|
}
|
|
o = s.option(form.DummyValue, 'metric', metric_title);
|
|
o.cfgvalue = function(section_id) {
|
|
var prio = uci.get('linkback', section_id, 'priority');
|
|
var prio_val = parseInt(prio, 10);
|
|
if (isNaN(prio_val) || prio_val <= 0) {
|
|
prio_val = 1;
|
|
}
|
|
return prio_val * 10;
|
|
};
|
|
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'));
|
|
o.modalonly = false;
|
|
o.cfgvalue = function(section_id) {
|
|
if (uci.get('linkback', section_id, 'ping_targets'))
|
|
return _('Ping Probe');
|
|
if (uci.get('linkback', section_id, 'dns_server'))
|
|
return _('DNS Probe');
|
|
if (uci.get('linkback', section_id, 'tcp_target'))
|
|
return _('TCP Probe');
|
|
return _('-- Not Configured --');
|
|
};
|
|
makeTableColumnExpand(o, '18%');
|
|
|
|
// 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'));
|
|
o.default = 'ping';
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
o.cfgvalue = function(section_id) {
|
|
if (uci.get('linkback', section_id, 'ping_targets'))
|
|
return 'ping';
|
|
if (uci.get('linkback', section_id, 'dns_server'))
|
|
return 'dns';
|
|
if (uci.get('linkback', section_id, 'tcp_target'))
|
|
return 'tcp';
|
|
return 'ping';
|
|
};
|
|
|
|
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);
|
|
|
|
if (next === current) {
|
|
return;
|
|
}
|
|
|
|
uci.remove('linkback', section_id, 'weight_threshold');
|
|
uci.remove('linkback', section_id, 'ping_weight');
|
|
uci.remove('linkback', section_id, 'dns_weight');
|
|
uci.remove('linkback', section_id, 'tcp_weight');
|
|
|
|
if (current === 'ping' || current === '') {
|
|
uci.remove('linkback', section_id, 'ping_targets');
|
|
}
|
|
if (current === 'dns' || current === '') {
|
|
uci.remove('linkback', section_id, 'dns_server');
|
|
uci.remove('linkback', section_id, 'dns_domain');
|
|
}
|
|
if (current === 'tcp' || current === '') {
|
|
uci.remove('linkback', section_id, 'tcp_target');
|
|
uci.remove('linkback', section_id, 'tcp_port');
|
|
}
|
|
};
|
|
|
|
// 7. Custom Ping Probe Parameters
|
|
o = s.option(form.Value, 'ping_targets', _('Ping Targets'),
|
|
_('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 _('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]?)$/;
|
|
if (!ipPattern.test(ips[i])) {
|
|
return _('Invalid IP address: "%s"').format(ips[i]);
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
o.write = function(section_id, value) {
|
|
if (value != null) {
|
|
var cleaned = String(value).replace(/\s+/g, '');
|
|
uci.set('linkback', section_id, 'ping_targets', cleaned);
|
|
} else {
|
|
uci.remove('linkback', section_id, 'ping_targets');
|
|
}
|
|
};
|
|
|
|
// 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).'));
|
|
o.datatype = 'ip4addr';
|
|
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.default = 'www.baidu.com';
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
o.depends('check_type', 'dns');
|
|
|
|
// 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 = false;
|
|
o.modalonly = true;
|
|
o.depends('check_type', 'tcp');
|
|
|
|
o = s.option(form.Value, 'tcp_port', _('TCP Port'),
|
|
_('Target port for TCP handshake probe (e.g. 80 or 443).'));
|
|
o.datatype = 'port';
|
|
o.default = '80';
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
o.depends('check_type', 'tcp');
|
|
|
|
// 提示:超时与防抖延迟由全局配置统一管理,子项完全无需重复配置
|
|
return m.render();
|
|
}
|
|
});
|