🍓 Sync 2026-09-15 02:13:06

This commit is contained in:
github-actions[bot] committed 2026-09-15 02:13:06 +08:00
1 parent 0e5393594c
commit 1dd96ac2bb
146 files changed
+26541 -508

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2026 Dante <dante@seckv.com>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-linkback
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=Dante
LUCI_TITLE:=LuCI support for Multi-WAN LinkBack Failover/Failback
LUCI_DEPENDS:=+luci-base +ucode +ucode-mod-fs +ucode-mod-uci +ucode-mod-ubus +linkback
LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/postinst
#!/sh
[ -n "$${IPKG_INSTROOT}" ] || {
/etc/init.d/rpcd restart
}
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,44 @@
'use strict';
'require view';
'require rpc';
'require poll';
var callGetLog = rpc.declare({
object: 'luci.linkback',
method: 'get_log'
});
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
render: function() {
var area = E('textarea', {
id: 'linkback_log_content',
style: 'width:100%; height:500px; background:#f4f4f4; color:#333; padding:10px; border:1px solid #ccc; border-radius:3px; font-family:monospace; font-size:12px; overflow-y:auto; white-space:pre-wrap; word-break:break-all; margin-top:10px;',
readonly: 'readonly'
}, _('Loading logs...'));
poll.add(L.bind(this.updateLogs, this), 5);
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('LinkBack 链路守护') + ' - ' + _('Log')),
E('div', { 'class': 'cbi-map-descr' }, _('Real-time routing adjustments and health check state transitions from the linkbackd daemon.')),
E('fieldset', { 'class': 'cbi-section' }, [
E('legend', {}, _('System Logs')),
E('div', { 'class': 'cbi-section-node' }, area)
])
]);
},
updateLogs: function() {
return callGetLog().then(function(res) {
var area = document.getElementById('linkback_log_content');
if (area) {
area.value = res && res.log ? res.log : _('No logs available.');
area.scrollTop = area.scrollHeight;
}
});
}
});
@@ -0,0 +1,261 @@
'use strict';
'require view';
'require rpc';
'require poll';
'require ui';
'require dom';
var callGetStatus = rpc.declare({
object: 'luci.linkback',
method: 'get_status'
});
var callManageService = rpc.declare({
object: 'luci.linkback',
method: 'manage_service',
params: [ 'action' ]
});
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
injectStyle: function() {
if (document.getElementById('linkback_status_style'))
return;
var css = [
'.linkback-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px;vertical-align:middle}',
'.linkback-dot-green{background-color:#4CAF50;box-shadow:0 0 6px #4CAF50}',
'.linkback-dot-red{background-color:#f44336;box-shadow:0 0 6px #f44336}',
'.linkback-dot-grey{background-color:#9e9e9e}',
'.linkback-indicator{display:inline-flex;align-items:center;padding:4px 10px;border-radius:12px;font-size:11px;font-weight:bold;background-color:rgba(0,0,0,.05)}',
'.linkback-badge{display:inline-block;padding:4px 8px;border-radius:4px;font-size:11px;font-weight:bold;color:#fff;text-align:center}',
'.linkback-badge-active{background-color:#4CAF50}',
'.linkback-badge-standby{background-color:#2196F3}',
'.linkback-badge-faulted{background-color:#f44336}'
].join('');
document.head.appendChild(E('style', { id: 'linkback_status_style' }, css));
},
load: function() {
return callGetStatus();
},
render: function(status) {
this.injectStyle();
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.'))
]);
// --- Service Status Section (Aligned with lucky style) ---
var stateText = _('Collecting...');
var stateColor = '#999';
if (status && status.links && status.links.length > 0) {
stateText = _('Running');
stateColor = 'green';
} else {
stateText = _('Stopped');
stateColor = 'red';
}
var isRunning = (status && status.links && status.links.length > 0);
var statusSection = E('fieldset', { 'class': 'cbi-section' }, [
E('legend', {}, _('Service Status')),
E('div', { 'class': 'cbi-section-node' }, [
E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, _('Running State')),
E('td', { 'class': 'td left' }, [
E('span', {
id: 'linkback-service-state',
style: 'font-weight:bold; color:' + stateColor + '; margin-right:15px;'
}, stateText),
E('button', {
id: 'linkback-restart-btn',
'class': 'btn cbi-button cbi-button-apply',
style: 'display:' + (isRunning ? 'inline-block' : 'none') + ';',
click: ui.createHandlerFn(this, 'handleServiceAction', 'restart')
}, _('Restart'))
])
])
])
])
]);
container.appendChild(statusSection);
// --- Link Status Section (Aligned with lucky style) ---
var linkSection = E('fieldset', { 'class': 'cbi-section' }, [
E('legend', {}, _('Link Status')),
E('div', { 'class': 'cbi-section-node' }, [
E('div', { id: 'linkback-status-box' }, [
E('p', { 'class': 'spinning' }, _('Loading real-time link status...'))
])
])
]);
container.appendChild(linkSection);
// Initial render
this.updateStatusView(status);
// Start polling every 3 seconds
poll.add(L.bind(function() {
return callGetStatus().then(L.bind(function(res) {
this.updateStatusView(res);
}, this));
}, this), 3);
return container;
},
updateStatusView: function(status) {
var box = document.getElementById('linkback-status-box');
if (!box)
return;
// Update service state indicator
var stateEl = document.getElementById('linkback-service-state');
if (stateEl) {
if (status && status.links && status.links.length > 0) {
stateEl.textContent = _('Running');
stateEl.style.color = 'green';
} else {
stateEl.textContent = _('Stopped');
stateEl.style.color = 'red';
}
}
var restartBtn = document.getElementById('linkback-restart-btn');
if (restartBtn) {
if (status && status.links && status.links.length > 0) {
restartBtn.style.display = 'inline-block';
} else {
restartBtn.style.display = 'none';
}
}
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.'))
]);
return;
}
var activeLink = status.active_link || 'none';
// Create standard LuCI section table
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;' }, _('Status')),
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Routing Attributes')),
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Latency Details'))
])
]);
status.links.forEach(function(link, index) {
var roleTitle = '';
var roleClass = '';
if (link.is_up && link.healthy) {
if (link.name === activeLink) {
roleTitle = _('Active');
roleClass = 'linkback-badge-active';
} else {
roleTitle = _('Standby');
roleClass = 'linkback-badge-standby';
}
} else {
roleTitle = _('Faulted');
roleClass = 'linkback-badge-faulted';
}
// Health check indicators
var checkBadges = [];
if (link.check_type === 'ping' && link.ping) {
var dotClass = link.ping.ok ? 'linkback-dot-green' : 'linkback-dot-red';
checkBadges.push(E('span', { 'class': 'linkback-indicator', 'style': 'margin-bottom:4px; margin-right:6px;' }, [
E('span', { 'class': 'linkback-dot ' + dotClass }),
'Ping: ' + (link.ping.ok ? link.ping.rtt + 'ms' : _('Failed'))
]));
}
else if (link.check_type === 'dns' && link.dns) {
var dotClass = link.dns.ok ? 'linkback-dot-green' : 'linkback-dot-red';
checkBadges.push(E('span', { 'class': 'linkback-indicator', 'style': 'margin-bottom:4px; margin-right:6px;' }, [
E('span', { 'class': 'linkback-dot ' + dotClass }),
'DNS: ' + (link.dns.ok ? link.dns.rtt + 'ms' : _('Failed'))
]));
}
else if (link.check_type === 'tcp' && link.tcp) {
var dotClass = link.tcp.ok ? 'linkback-dot-green' : 'linkback-dot-red';
checkBadges.push(E('span', { 'class': 'linkback-indicator', 'style': 'margin-bottom:4px; margin-right:6px;' }, [
E('span', { 'class': 'linkback-dot ' + dotClass }),
'TCP: ' + (link.tcp.ok ? link.tcp.rtt + 'ms' : _('Failed'))
]));
}
if (checkBadges.length === 0) {
checkBadges.push(E('span', { 'style': 'color:#999;' }, '-'));
}
// Route and metric attributes
var routeDetails = E('div', { 'style': 'font-size:12px; line-height:1.6;' }, [
E('div', {}, [
E('strong', {}, _('Gateway') + ': '),
link.gateway || '-'
]),
E('div', {}, [
E('strong', {}, _('Priority') + ': '),
String(link.priority),
' | ',
E('strong', {}, _('Metric') + ': '),
link.metric + ' → ' + link.current_metric
])
]);
var row = E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'style': 'text-align:center; font-weight:bold; vertical-align:middle;' }, String(index + 1)),
E('td', { 'class': 'td', 'style': 'font-weight:bold; vertical-align:middle;' }, [
E('span', { 'style': 'font-size:14px;' }, link.name),
link.device ? E('span', { 'style': 'font-size:11px; color:#666; margin-left:6px; background:#eee; padding:2px 4px; border-radius:3px;' }, link.device) : ''
]),
E('td', { 'class': 'td', 'style': 'vertical-align:middle;' }, [
E('span', { 'class': 'linkback-badge ' + roleClass }, roleTitle)
]),
E('td', { 'class': 'td', 'style': 'vertical-align:middle;' }, routeDetails),
E('td', { 'class': 'td', 'style': 'vertical-align:middle;' }, E('div', { 'style': 'display:flex; flex-wrap:wrap; align-items:center;' }, checkBadges))
]);
table.appendChild(row);
});
dom.content(box, [ table ]);
},
handleServiceAction: function(action) {
ui.showModal(null, [
E('p', { 'class': 'spinning' }, _('Executing service action: %s ...').format(action))
]);
return callManageService(action).then(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', _('Service action completed successfully.')), 'info');
} else {
ui.addNotification(null, E('p', _('Failed to execute service action.')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('RPC error: %s').format(err.message)), 'error');
});
}
});
@@ -0,0 +1,429 @@
'use strict';
'require view';
'require uci';
'require form';
'require ui';
return view.extend({
load: function() {
return Promise.all([
uci.load('linkback'),
uci.load('network')
]);
},
// 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 link_sections = uci.sections('linkback', 'link') || [];
// Rule 1: At least 2 interfaces
if (link_sections.length < 2) {
return _('Cannot enable service: At least 2 monitored WAN interfaces must be configured for failover switcher.');
}
// Rule 2: ALL interfaces must have a health check configured
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 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);
}
}
// 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';
if (priorities[prio]) {
return _('Cannot enable service: Interfaces "%s" and "%s" have the same priority %s.').format(priorities[prio], iface_name, prio);
}
priorities[prio] = iface_name;
}
return null;
},
render: function() {
var m, s, o;
var self = this;
// 智能判断当前是否为中文环境,并提供实时校验的翻译 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.');
// 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 failover service, health check parameters, and monitored link interfaces.'));
// --- Global Settings Section ---
s = m.section(form.TypedSection, 'global', _('Global Settings'));
s.anonymous = true;
// Enable switch (总开关) - must be the first option
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();
if (err) {
ui.addNotification(null, E('p', err), 'error');
uci.set('linkback', section_id, 'enabled', '0');
return;
}
}
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).'));
s.anonymous = true;
s.addremove = true;
// Custom dynamic Modal title for gorgeous UX
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');
} else {
var name = uci.get('linkback', section_id, 'name') || section_id;
return parent_title + ' - ' + _('Edit Monitored Interface') + ' (' + name + ')';
}
};
// 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() {
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 (Enabled as the first column)
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;
}
});
// 动态生成仅针对当前渲染 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];
}
}
}
// 判断当前是编辑已有配置还是添加新配置
// 如果在 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();
};
o.validate = function(section_id, value) {
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;
};
makeTableColumnExpand(o, '25%');
// 3. Priority (No description for main column)
o = s.option(form.Value, 'priority', _('Priority'));
o.datatype = 'uinteger';
o.default = '1';
o.rmempty = false;
o.validate = function(section_id, value) {
if (value == null || value === '') {
return t_priority_empty;
}
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) {
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;
conflicting_iface = uci.get('linkback', sid, 'name') || sid;
}
}
});
if (has_conflict) {
return t_priority_conflict.format(value, conflicting_iface);
}
return true;
};
makeTableColumnExpand(o, '15%');
// 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, '15%');
// 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, '20%');
// 6. Check Type Dropdown (Virtual field, Modal only to avoid Save & Apply side-effects)
o = s.option(form.ListValue, 'check_type', _('Check Type'));
o.value('ping', _('Ping Probe'));
o.value('dns', _('DNS Probe'));
o.value('tcp', _('TCP Probe'));
o.default = 'ping';
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';
if (uci.get('linkback', section_id, 'dns_server'))
return 'dns';
if (uci.get('linkback', section_id, 'tcp_target'))
return 'tcp';
return 'ping'; // Default to ping if not configured
};
// 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');
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. 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;
o.modalonly = true;
o.depends('check_type', 'ping');
o.validate = function(section_id, value) {
if (!value) return true;
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');
}
};
// 7. 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 = true;
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;
o.modalonly = true;
o.depends('check_type', 'dns');
// 8. 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.modalonly = true;
o.depends('check_type', 'tcp');
o = s.option(form.Value, 'tcp_port', _('TCP Port'),
_('Target port for TCP handshake probe.'));
o.datatype = 'port';
o.rmempty = true;
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
View File
@@ -0,0 +1 @@
zh_Hans
+322
View File
@@ -0,0 +1,322 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "LinkBack 链路守护"
msgstr "网关切换"
msgid "Overview"
msgstr "概览"
msgid "Settings"
msgstr "设置"
msgid "Log"
msgstr "日志"
msgid "Real-time health state, priority mapping, and service control for Multi-WAN default routing metrics."
msgstr "用于 Multi-WAN 默认路由 Metric 的实时健康状态、优先级映射及服务控制。"
msgid "Service Status"
msgstr "服务状态"
msgid "Current Status"
msgstr "当前状态"
msgid "Service Control"
msgstr "服务控制"
msgid "Restart"
msgstr "重启"
msgid "Start"
msgstr "启动"
msgid "Stop"
msgstr "停止"
msgid "Link Status"
msgstr "链路状态"
msgid "Loading real-time link status..."
msgstr "正在加载实时链路状态..."
msgid "Running"
msgstr "运行中"
msgid "Stopped"
msgstr "已停止"
msgid "LinkBack daemon is not running or no links are configured. Please check Settings page."
msgstr "网关切换守护进程未运行或未配置任何链路。请检查设置页面。"
msgid "主选链路 / Active"
msgstr "主选网关 / 活跃中"
msgid "次选链路 / Standby"
msgstr "备选网关 / 待命"
msgid "备选链路 / Faulted"
msgstr "故障网关 / 已断开"
msgid "Priority"
msgstr "优先级"
msgid "Metric"
msgstr "路由跃点 (Metric)"
msgid "Gateway"
msgstr "网关地址"
msgid "Score"
msgstr "得分"
msgid "Failed"
msgstr "失败"
msgid "Executing service action: %s ..."
msgstr "正在执行服务操作: %s ..."
msgid "Service action completed successfully."
msgstr "服务操作执行成功。"
msgid "Failed to execute service action."
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 "全局设置"
msgid "Enable Service"
msgstr "启用服务"
msgid "Master switch to enable or disable the LinkBack failover daemon."
msgstr "启用或禁用网关切换服务的全局总开关。"
msgid "Check Interval (s)"
msgstr "检测间隔 (秒)"
msgid "Time in seconds between each health check cycle."
msgstr "每个健康度检测周期之间的时间间隔。"
msgid "Check Timeout (s)"
msgstr "检测超时 (秒)"
msgid "Maximum wait time in seconds for each individual check probe."
msgstr "每个检测探针的最大等待超时时间。"
msgid "Recovery Delay"
msgstr "恢复判定延迟"
msgid "Number of consecutive successful checks required before marking a faulted link as healthy again (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)."
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 "已启用"
msgid "Base Metric"
msgstr "默认跃点"
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 检测目标"
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 "Ping Weight"
msgstr "Ping 权重"
msgid "DNS Server"
msgstr "DNS 服务器"
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)."
msgstr "用于 DNS 探针解析的测试目标域名 (例如:www.baidu.com)。"
msgid "DNS Weight"
msgstr "DNS 权重"
msgid "TCP Target"
msgstr "TCP 检测目标"
msgid "Target IP for TCP handshake probe."
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 "Loading logs..."
msgstr "正在加载运行日志..."
msgid "Real-time routing adjustments and health check state transitions from the linkbackd daemon."
msgstr "来自后台 linkbackd 守护程序的实时路由调整和网关健康状态转换详情。"
msgid "System Logs"
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 "Maximum wait time in seconds for each individual check probe for this link."
msgstr "该链路每个检测探针的最大等待超时时间 (秒)。"
msgid "Number of consecutive successful checks required before marking this link as healthy (failback anti-flap)."
msgstr "判定该链路恢复健康所需连续检测成功的次数 (防抖动回切)。"
msgid "Number of consecutive failed checks required before marking this link as faulted (failover anti-flap)."
msgstr "判定该链路发生故障所需连续检测失败的次数 (防抖动切换)。"
msgid "Add Monitored Interface"
msgstr "添加监控接口"
msgid "Edit Monitored Interface"
msgstr "编辑监控接口"
msgid "Cannot enable service: No monitored WAN interfaces are configured."
msgstr "无法启用服务: 未配置任何监控的 WAN 接口。"
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 "Service has been auto-disabled because the configuration is no longer valid: "
msgstr "由于配置不再合法,服务已被自动禁用: "
msgid "Ping Probe"
msgstr "Ping 探测"
msgid "DNS Probe"
msgstr "DNS 探测"
msgid "TCP Probe"
msgstr "TCP 探测"
msgid "%s (configured)"
msgstr "%s (已配置)"
msgid "This interface has already been configured."
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 "Active"
msgstr "活跃"
msgid "Standby"
msgstr "备选"
msgid "Faulted"
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)。"
@@ -0,0 +1,37 @@
{
"admin/services/linkback": {
"title": "LinkBack 链路守护",
"order": 90,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-linkback" ],
"uci": { "linkback": true }
}
},
"admin/services/linkback/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "linkback/overview"
}
},
"admin/services/linkback/settings": {
"title": "Settings",
"order": 20,
"action": {
"type": "view",
"path": "linkback/settings"
}
},
"admin/services/linkback/log": {
"title": "Log",
"order": 30,
"action": {
"type": "view",
"path": "linkback/log"
}
}
}
@@ -0,0 +1,19 @@
{
"luci-app-linkback": {
"description": "ACL for LinkBack Multi-WAN Failover application",
"read": {
"uci": [ "linkback", "network" ],
"ubus": {
"luci.linkback": [ "get_status", "get_log" ],
"network.interface": [ "dump" ],
"network.device": [ "status" ]
}
},
"write": {
"uci": [ "linkback", "network" ],
"ubus": {
"luci.linkback": [ "manage_service" ]
}
}
}
}
@@ -0,0 +1,64 @@
'use strict';
import { readfile } from 'fs';
import { popen } from 'fs';
const STATUS_FILE = '/var/run/linkback.json';
const methods = {
get_status: {
call: function() {
let content = readfile(STATUS_FILE);
let status = null;
if (content) {
try {
status = json(content);
} catch (err) {
// Handle JSON parse error gracefully
}
}
if (!status) {
status = {
enabled: false,
check_interval: 5,
active_link: "none",
links: []
};
}
return status;
}
},
get_log: {
call: function() {
// Extract last 100 lines of linkbackd logs from syslog
let p = popen('logread -e linkbackd | tail -n 100', 'r');
let log_content = '';
if (p) {
log_content = p.read('all');
p.close();
}
return { log: log_content || 'No logs available.' };
}
},
manage_service: {
args: { action: 'string' },
call: function(request) {
let action = request.args?.action;
if (action !== 'start' && action !== 'stop' && action !== 'restart') {
return { success: false, error: 'Invalid action' };
}
let cmd = `/etc/init.d/linkback ${action} >/dev/null 2>&1`;
let rc = system(cmd);
return { success: (rc === 0) };
}
}
};
return { 'luci.linkback': methods };