mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 23:02:51 +08:00
276 lines
9.3 KiB
JavaScript
276 lines
9.3 KiB
JavaScript
'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 / Multi-Gateway failover.'))
|
|
]);
|
|
|
|
// --- Service Status Section ---
|
|
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'))
|
|
])
|
|
]),
|
|
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 ---
|
|
var linkSection = E('fieldset', { 'class': 'cbi-section' }, [
|
|
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...'))
|
|
])
|
|
])
|
|
]);
|
|
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';
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
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 targets 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;' }, _('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'))
|
|
])
|
|
]);
|
|
|
|
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');
|
|
});
|
|
}
|
|
});
|