mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 02:35:13 +08:00
🤞 Sync 2026-03-15 21:44:24
This commit is contained in:
1 parent
a5cbeb474b
commit
ceeb3a33dc
11 files changed
+414
-1065
No files matched your search
@@ -8,6 +8,7 @@
|
||||
|
||||
const callGetStatus = rpc.declare({ object: 'tailscale', method: 'get_status' });
|
||||
const callGetSettings = rpc.declare({ object: 'tailscale', method: 'get_settings' });
|
||||
const callSetSettings = rpc.declare({ object: 'tailscale', method: 'set_settings', params: ['form_data'] });
|
||||
const callDoLogin = rpc.declare({ object: 'tailscale', method: 'do_login', params: ['form_data'] });
|
||||
const callDoLogout = rpc.declare({ object: 'tailscale', method: 'do_logout' });
|
||||
const callGetSubroutes = rpc.declare({ object: 'tailscale', method: 'get_subroutes' });
|
||||
@@ -23,8 +24,7 @@ const tailscaleSettingsConf = [
|
||||
[form.Flag, 'nosnat', _('Disable SNAT'), _('Disable Source NAT (SNAT) for traffic to advertised routes. Most users should leave this unchecked.'), { rmempty: false }],
|
||||
[form.Flag, 'shields_up', _('Shields Up'), _('When enabled, blocks all inbound connections from the Tailscale network.'), { rmempty: false }],
|
||||
[form.Flag, 'ssh', _('Enable Tailscale SSH'), _('Allow connecting to this device through the SSH function of Tailscale.'), { rmempty: false }],
|
||||
[form.Flag, 'disable_magic_dns', _('Disable MagicDNS'), _('Use system DNS instead of MagicDNS.'), { rmempty: false }],
|
||||
[form.Flag, 'enable_relay', _('Enable Peer Relay'), _('Enable this device as a Peer Relay server. Requires a public IP and an UDP port open on the router.'), { rmempty: false }]
|
||||
[form.Flag, 'disable_magic_dns', _('Disable MagicDNS'), _('Use system DNS instead of MagicDNS.'), { rmempty: false }]
|
||||
];
|
||||
|
||||
const accountConf = []; // dynamic created in render function
|
||||
@@ -235,66 +235,69 @@ function renderStatus(status) {
|
||||
E('tr', {}, statusData.map(item => E('td', { 'style': 'padding-right: 20px;' }, item.value)))
|
||||
]);
|
||||
|
||||
return statusTable;
|
||||
}
|
||||
|
||||
function renderDevices(status) {
|
||||
if (!status || !status.hasOwnProperty('status')) {
|
||||
return E('em', {}, _('Collecting data ...'));
|
||||
}
|
||||
|
||||
if (status.status != 'running') {
|
||||
return E('em', {}, _('Tailscale status error'));
|
||||
}
|
||||
|
||||
if (Object.keys(regionCodeMap).length === 0) {
|
||||
initializeRegionMap();
|
||||
}
|
||||
// --- Part 3: Render the Peers/Network Devices table ---
|
||||
|
||||
const peers = status.peers;
|
||||
let peersContent;
|
||||
|
||||
if (!peers || Object.keys(peers).length === 0) {
|
||||
return E('p', {}, _('No peer devices found.'));
|
||||
// Display a message if no peers are found.
|
||||
peersContent = E('p', {}, _('No peer devices found.'));
|
||||
} else {
|
||||
// Define headers for the peers table.
|
||||
const peerTableHeaders = [
|
||||
{ text: _('Status'), style: 'width: 80px;' },
|
||||
{ text: _('Hostname') },
|
||||
{ text: _('Tailscale IP') },
|
||||
{ text: _('OS') },
|
||||
{ text: _('Connection Info') },
|
||||
{ text: _('RX') },
|
||||
{ text: _('TX') },
|
||||
{ text: _('Last Seen') }
|
||||
];
|
||||
|
||||
// Build the peers table.
|
||||
peersContent = E('table', { 'class': 'cbi-table' }, [
|
||||
// Table Header Row
|
||||
E('tr', { 'class': 'cbi-table-header' }, peerTableHeaders.map(header => {
|
||||
let th_style = 'padding-right: 20px; text-align: left;';
|
||||
if (header.style) {
|
||||
th_style += header.style;
|
||||
}
|
||||
return E('th', { 'class': 'cbi-table-cell', 'style': th_style }, header.text);
|
||||
})),
|
||||
|
||||
// Table Body Rows (one for each peer)
|
||||
...Object.entries(peers).map(([peerid, peer]) => {
|
||||
const td_style = 'padding-right: 20px;';
|
||||
|
||||
return E('tr', { 'class': 'cbi-rowstyle-1' }, [
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style },
|
||||
E('span', {
|
||||
'style': `color:${peer.exit_node ? 'blue' : (peer.online ? 'green' : 'gray')};`,
|
||||
'title': (peer.exit_node ? _('Exit Node') + ' ' : '') + (peer.online ? _('Online') : _('Offline'))
|
||||
}, peer.online ? '●' : '○')
|
||||
),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, E('strong', {}, peer.hostname + (peer.exit_node_option ? ' (ExNode)' : ''))),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, peer.ip || 'N/A'),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, peer.ostype || 'N/A'),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatConnectionInfo(peer.linkadress || '-')),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatBytes(peer.rx)),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatBytes(peer.tx)),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatLastSeen(peer.lastseen))
|
||||
]);
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
const peerTableHeaders = [
|
||||
{ text: _('Status'), style: 'width: 80px;' },
|
||||
{ text: _('Hostname') },
|
||||
{ text: _('Tailscale IP') },
|
||||
{ text: _('OS') },
|
||||
{ text: _('Connection Info') },
|
||||
{ text: _('RX') },
|
||||
{ text: _('TX') },
|
||||
{ text: _('Last Seen') }
|
||||
];
|
||||
|
||||
return E('table', { 'class': 'cbi-table' }, [
|
||||
E('tr', { 'class': 'cbi-table-header' }, peerTableHeaders.map(header => {
|
||||
let th_style = 'padding-right: 20px; text-align: left;';
|
||||
if (header.style) {
|
||||
th_style += header.style;
|
||||
}
|
||||
return E('th', { 'class': 'cbi-table-cell', 'style': th_style }, header.text);
|
||||
})),
|
||||
|
||||
...Object.entries(peers).map(([peerid, peer]) => {
|
||||
const td_style = 'padding-right: 20px;';
|
||||
|
||||
return E('tr', { 'class': 'cbi-rowstyle-1' }, [
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style },
|
||||
E('span', {
|
||||
'style': `color:${peer.exit_node ? 'blue' : (peer.online ? 'green' : 'gray')};`,
|
||||
'title': (peer.exit_node ? _('Exit Node') + ' ' : '') + (peer.online ? _('Online') : _('Offline'))
|
||||
}, peer.online ? '●' : '○')
|
||||
),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, E('strong', {}, peer.hostname + (peer.exit_node_option ? ' (ExNode)' : ''))),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, peer.ip || 'N/A'),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, peer.ostype || 'N/A'),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatConnectionInfo(peer.linkadress || '-')),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatBytes(peer.rx)),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatBytes(peer.tx)),
|
||||
E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatLastSeen(peer.lastseen))
|
||||
]);
|
||||
})
|
||||
// Combine all parts into a single DocumentFragment.
|
||||
// Using E() without a tag name creates a fragment, which is perfect for grouping elements.
|
||||
return E([
|
||||
statusTable,
|
||||
E('div', { 'style': 'margin-top: 25px;' }, [
|
||||
E('h4', {}, _('Network Devices')),
|
||||
peersContent
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -350,11 +353,6 @@ return view.extend({
|
||||
view.replaceChildren(content);
|
||||
}
|
||||
|
||||
const devicesView = document.getElementById("tailscale_devices_display");
|
||||
if (devicesView) {
|
||||
devicesView.replaceChildren(renderDevices(res));
|
||||
}
|
||||
|
||||
// login button only available when logged out
|
||||
const login_btn=document.getElementsByClassName('cbi-button cbi-button-apply')[0];
|
||||
if(login_btn) { login_btn.disabled=(res.status != 'logout'); }
|
||||
@@ -367,7 +365,7 @@ return view.extend({
|
||||
}
|
||||
|
||||
// Bind settings to the 'settings' section of uci
|
||||
s = map.section(form.NamedSection, 'settings', 'settings', null);
|
||||
s = map.section(form.NamedSection, 'settings', 'settings', _('Settings'));
|
||||
s.dynamic = true;
|
||||
|
||||
// Create the "General Settings" tab and apply tailscaleSettingsConf
|
||||
@@ -375,14 +373,6 @@ return view.extend({
|
||||
|
||||
defTabOpts(s, 'general', tailscaleSettingsConf, { optional: false });
|
||||
|
||||
const relayPort = s.taboption('general', form.Value, 'relay_server_port', _('Peer Relay Port'),
|
||||
_('UDP port for the Peer Relay service. Open this port on your router firewall/NAT.')
|
||||
);
|
||||
relayPort.datatype = 'port';
|
||||
relayPort.placeholder = '40000';
|
||||
relayPort.rmempty = false;
|
||||
relayPort.depends('enable_relay', '1');
|
||||
|
||||
const en = s.taboption('general', form.ListValue, 'exit_node', _('Exit Node'), _('Select an exit node from the list. If enabled, Allow LAN Access is enabled implicitly.'));
|
||||
en.value('', _('None'));
|
||||
if (status.peers) {
|
||||
@@ -458,7 +448,7 @@ return view.extend({
|
||||
|
||||
const customLoginUrl = s.taboption('account', form.Value, 'custom_login_url',
|
||||
_('Custom Login Server'),
|
||||
_('Optional: Specify a custom control server URL (e.g., a Headscale instance, https://example.com).')
|
||||
_('Optional: Specify a custom control server URL (e.g., a Headscale instance, %s).'.format('https://example.com'))
|
||||
+'<br>'+_('Leave blank for default Tailscale control plane.')
|
||||
);
|
||||
customLoginUrl.placeholder = '';
|
||||
@@ -551,12 +541,6 @@ return view.extend({
|
||||
ui.showModal(_('Confirm Logout'), confirmationContent);
|
||||
};
|
||||
|
||||
s.tab('devices', _('Devices List'));
|
||||
const devicesSection = s.taboption('devices', form.DummyValue, '_devices');
|
||||
devicesSection.render = function () {
|
||||
return E('div', { 'id': 'tailscale_devices_display', 'class': 'cbi-value' }, renderDevices(status));
|
||||
};
|
||||
|
||||
// Create the "Daemon Settings" tab and apply daemonConf
|
||||
//s.tab('daemon', _('Daemon Settings'));
|
||||
//defTabOpts(s, 'daemon', daemonConf, { optional: false });
|
||||
@@ -564,12 +548,40 @@ return view.extend({
|
||||
return map.render();
|
||||
},
|
||||
|
||||
// The handleSaveApply function saves UCI changes then applies them via the
|
||||
// standard OpenWrt apply mechanism, which triggers /etc/init.d/tailscale-settings
|
||||
// and provides automatic rollback protection if the device becomes unreachable.
|
||||
handleSaveApply(ev, mode) {
|
||||
// The handleSaveApply function is executed after clicking "Save & Apply"
|
||||
handleSaveApply(ev) {
|
||||
return map.save().then(function () {
|
||||
return ui.changes.apply(mode == '0');
|
||||
const data = map.data.get('tailscale', 'settings');
|
||||
|
||||
// fix empty value issue
|
||||
if(!data.advertise_exit_node) data.advertise_exit_node = '';
|
||||
if(!data.advertise_routes) data.advertise_routes = '';
|
||||
if(!data.exit_node) data.exit_node = '';
|
||||
if(!data.custom_login_url) data.custom_login_url = '';
|
||||
if(!data.custom_login_AuthKey) data.custom_login_AuthKey = '';
|
||||
|
||||
ui.showModal(_('Applying changes...'), E('em', {}, _('Please wait.')));
|
||||
|
||||
return callSetSettings(data).then(function (response) {
|
||||
if (response.success) {
|
||||
ui.hideModal();
|
||||
setTimeout(function() {
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Tailscale settings applied successfully.')) ], 5000, 'info');
|
||||
}, 1000);
|
||||
try {
|
||||
L.ui.changes.revert();
|
||||
} catch (error) {
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Error saving settings: %s').format(error || _('Unknown error'))) ], 7000, 'error');
|
||||
}
|
||||
} else {
|
||||
ui.hideModal();
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Error applying settings: %s').format(response.error || _('Unknown error'))) ], 7000, 'error');
|
||||
}
|
||||
});
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
//console.error('Save failed:', err);
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Failed to save settings: %s').format(err.message)) ], 7000, 'error');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Reference in new issue
Block a user