This release adds dual menu access for Network Modes (both SecuBox and LuCI Network menus) and significantly expands RPCD permissions for all mode configuration operations. ## Network Modes - Dual Menu Access (2 files) - Added Network Modes to standard LuCI Network menu (admin/network/modes) - Maintains existing SecuBox menu location (admin/secubox/network/modes) - Users can now access Network Modes from both locations - Menu order: 60 in Network menu, 10 in SecuBox Network category ## Network Modes - Enhanced Permissions (1 file) Added 13+ new RPCD methods to ACL for complete mode management: Read permissions: - preview_changes - sniffer_config, ap_config, relay_config, router_config - travel_config, doublenat_config, multiwan_config, vpnrelay_config - travel_scan_networks Write permissions: - apply_mode, confirm_mode, rollback - update_settings - generate_wireguard_keys, apply_wireguard_config - apply_mtu_clamping, enable_tcp_bbr - add_vhost, generate_config ## Network Modes - View Updates (11 files) Updated all mode views for consistency: - helpers.js: 28 lines refactored - overview.js: Enhanced view structure - All mode views: wizard, router, multiwan, doublenat, accesspoint, relay, vpnrelay, travel, sniffer ## Theme Enhancements (1 file) - theme.js: 89 lines added - Enhanced theme initialization and configuration - Improved component styling support ## SecuBox Dashboard (2 files) - Updated dashboard.js and modules.js - Improved view rendering and integration ## System Hub (3 files) - Enhanced logs.js, overview.js, services.js - Better view consistency and functionality Summary: - 19 files changed (+282, -36) - Dual menu access for Network Modes - 13+ new RPCD permission methods - All network mode views updated - Theme significantly enhanced 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
243 lines
11 KiB
JavaScript
243 lines
11 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require ui';
|
|
'require network-modes.api as api';
|
|
'require network-modes.helpers as helpers';
|
|
'require secubox/help as Help';
|
|
'require secubox-theme/theme as Theme';
|
|
|
|
var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
|
Theme.init({ language: nmLang });
|
|
|
|
return view.extend({
|
|
title: _('Travel Router Mode'),
|
|
|
|
load: function() {
|
|
return api.getTravelConfig();
|
|
},
|
|
|
|
render: function(data) {
|
|
var config = data || {};
|
|
var client = config.client || {};
|
|
var hotspot = config.hotspot || {};
|
|
var lan = config.lan || {};
|
|
var interfaces = config.available_interfaces || ['wlan0', 'wlan1'];
|
|
var radios = config.available_radios || ['radio0', 'radio1'];
|
|
|
|
var hero = helpers.createHero({
|
|
icon: '✈️',
|
|
title: _('Travel Router'),
|
|
subtitle: _('Clone the hotel uplink, stay behind your own encrypted hotspot, and keep a clean sandboxed LAN for all devices.'),
|
|
gradient: 'linear-gradient(135deg,#fbbf24,#f97316)',
|
|
actions: [
|
|
Help.createHelpButton('network-modes', 'header', { icon: '📘', label: _('Travel guide'), modal: true }),
|
|
E('button', { 'class': 'nm-btn', 'type': 'button', 'data-action': 'travel-scan' }, ['🔍 ', _('Scan Uplink')]),
|
|
E('button', { 'class': 'nm-btn nm-btn-primary', 'type': 'button', 'data-action': 'travel-save' }, ['💾 ', _('Save Settings')]),
|
|
E('button', { 'class': 'nm-btn', 'type': 'button', 'data-action': 'travel-preview' }, ['📝 ', _('Preview Config')])
|
|
]
|
|
});
|
|
|
|
var stats = E('div', { 'style': 'display:flex;flex-wrap:wrap;gap:12px;margin-bottom:24px;' }, [
|
|
helpers.createStatBadge({ label: _('Client SSID'), value: client.ssid || _('Not set') }),
|
|
helpers.createStatBadge({ label: _('Hotspot SSID'), value: hotspot.ssid || 'SecuBox-Travel' }),
|
|
helpers.createStatBadge({ label: _('LAN Gateway'), value: lan.subnet || '10.77.0.1' }),
|
|
helpers.createStatBadge({ label: _('MAC Clone'), value: client.clone_mac ? _('Active') : _('Off') })
|
|
]);
|
|
|
|
var uplinkSection = helpers.createSection({
|
|
title: _('Client WiFi Uplink'),
|
|
icon: '📡',
|
|
badge: client.ssid ? _('Connected') : _('Pending'),
|
|
body: [
|
|
E('div', { 'class': 'nm-form-grid' }, [
|
|
this.renderSelectField(_('Client interface'), 'travel-client-iface', interfaces, client.interface || 'wlan1'),
|
|
this.renderSelectField(_('Client radio'), 'travel-client-radio', radios, client.radio || 'radio1'),
|
|
this.renderSelectField(_('Encryption'), 'travel-encryption', ['sae-mixed', 'sae', 'psk2', 'psk-mixed', 'none'], client.encryption || 'sae-mixed')
|
|
]),
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('SSID / BSSID')),
|
|
E('input', { 'class': 'nm-input', 'id': 'travel-client-ssid', 'value': client.ssid || '', 'placeholder': _('Hotel WiFi name') }),
|
|
E('div', { 'class': 'nm-form-hint' }, _('Pick from scan results or enter manually'))
|
|
]),
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('Password / Captive token')),
|
|
E('input', { 'class': 'nm-input', 'type': 'password', 'id': 'travel-client-password', 'value': client.password || '' }),
|
|
E('div', { 'class': 'nm-form-hint' }, _('Leave empty if using captive portal login'))
|
|
]),
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('WAN MAC clone')),
|
|
E('input', { 'class': 'nm-input', 'id': 'travel-mac-clone', 'value': client.clone_mac || '', 'placeholder': 'AA:BB:CC:DD:EE:FF' }),
|
|
E('div', { 'class': 'nm-form-hint' }, _('Copy laptop/room-authorized MAC to bypass hotel locks'))
|
|
]),
|
|
E('div', { 'class': 'nm-btn-group' }, [
|
|
E('button', { 'class': 'nm-btn', 'type': 'button', 'data-action': 'travel-scan' }, ['🔍 ', _('Scan networks')]),
|
|
E('span', { 'id': 'travel-scan-status', 'class': 'nm-text-muted' }, _('Last scan: never'))
|
|
]),
|
|
E('div', { 'class': 'nm-scan-results', 'id': 'travel-scan-results' }, [
|
|
E('div', { 'class': 'nm-empty' }, _('No scan results yet'))
|
|
])
|
|
]
|
|
});
|
|
|
|
var hotspotSection = helpers.createSection({
|
|
title: _('Personal Hotspot'),
|
|
icon: '🔥',
|
|
badge: _('WPA3/WPA2'),
|
|
body: [
|
|
E('div', { 'class': 'nm-form-grid' }, [
|
|
this.renderSelectField(_('Hotspot radio'), 'travel-hotspot-radio', radios, hotspot.radio || 'radio0')
|
|
]),
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('Hotspot SSID')),
|
|
E('input', { 'class': 'nm-input', 'id': 'travel-hotspot-ssid', 'value': hotspot.ssid || 'SecuBox-Travel' })
|
|
]),
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('Hotspot password')),
|
|
E('input', { 'class': 'nm-input', 'id': 'travel-hotspot-password', 'value': hotspot.password || 'TravelSafe123!' })
|
|
]),
|
|
helpers.createList([
|
|
{ title: _('Private WPA3 bubble'), description: _('Keep laptops/phones on isolated SSID'), suffix: E('span', { 'class': 'nm-badge' }, _('Secure')) },
|
|
{ title: _('Dual-band'), description: _('Broadcast both 2.4/5 GHz when hardware supports it'), suffix: E('span', { 'class': 'nm-badge' }, _('Auto')) }
|
|
])
|
|
]
|
|
});
|
|
|
|
var lanSection = helpers.createSection({
|
|
title: _('LAN & DHCP Sandbox'),
|
|
icon: '🛡️',
|
|
body: [
|
|
E('div', { 'class': 'nm-form-grid' }, [
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('LAN Gateway IP')),
|
|
E('input', { 'class': 'nm-input', 'id': 'travel-lan-ip', 'value': lan.subnet || '10.77.0.1' })
|
|
]),
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('LAN Netmask')),
|
|
E('input', { 'class': 'nm-input', 'id': 'travel-lan-mask', 'value': lan.netmask || '255.255.255.0' })
|
|
])
|
|
]),
|
|
E('div', { 'class': 'nm-form-hint' }, _('Each trip receives an isolated /24 network with firewall & DNS-hardening.'))
|
|
]
|
|
});
|
|
|
|
var container = E('div', { 'class': 'network-modes-dashboard travel-mode' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }),
|
|
helpers.createNavigationTabs('travel'),
|
|
hero,
|
|
stats,
|
|
uplinkSection,
|
|
hotspotSection,
|
|
lanSection
|
|
]);
|
|
|
|
this.bindTravelActions(container);
|
|
return container;
|
|
},
|
|
|
|
renderSelectField: function(label, id, options, selected) {
|
|
return E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, label),
|
|
E('select', { 'class': 'nm-select', 'id': id },
|
|
options.map(function(opt) {
|
|
return E('option', { 'value': opt, 'selected': opt === selected }, opt);
|
|
})
|
|
)
|
|
]);
|
|
},
|
|
|
|
bindTravelActions: function(container) {
|
|
var scanButtons = container.querySelectorAll('[data-action="travel-scan"]');
|
|
Array.prototype.forEach.call(scanButtons, function(btn) {
|
|
btn.addEventListener('click', ui.createHandlerFn(this, 'scanNetworks', container));
|
|
}, this);
|
|
|
|
var saveBtn = container.querySelector('[data-action="travel-save"]');
|
|
if (saveBtn)
|
|
saveBtn.addEventListener('click', ui.createHandlerFn(this, 'saveTravelSettings', container));
|
|
|
|
var previewBtn = container.querySelector('[data-action="travel-preview"]');
|
|
if (previewBtn)
|
|
previewBtn.addEventListener('click', ui.createHandlerFn(helpers, helpers.showGeneratedConfig, 'travel'));
|
|
},
|
|
|
|
scanNetworks: function(container) {
|
|
var statusEl = container.querySelector('#travel-scan-status');
|
|
if (statusEl)
|
|
statusEl.textContent = _('Scanning...');
|
|
|
|
return api.scanTravelNetworks().then(L.bind(function(result) {
|
|
if (statusEl)
|
|
statusEl.textContent = _('Last scan: ') + new Date().toLocaleTimeString();
|
|
this.populateScanResults(container, (result && result.networks) || []);
|
|
}, this)).catch(function(err) {
|
|
if (statusEl)
|
|
statusEl.textContent = _('Scan failed');
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
populateScanResults: function(container, networks) {
|
|
var list = container.querySelector('#travel-scan-results');
|
|
if (!list)
|
|
return;
|
|
|
|
list.innerHTML = '';
|
|
|
|
if (!networks.length) {
|
|
list.appendChild(E('div', { 'class': 'nm-empty' }, _('No networks detected')));
|
|
return;
|
|
}
|
|
|
|
networks.slice(0, 8).forEach(L.bind(function(net) {
|
|
var card = E('button', {
|
|
'class': 'nm-scan-card',
|
|
'type': 'button'
|
|
}, [
|
|
E('div', { 'class': 'nm-scan-ssid' }, net.ssid || _('Hidden SSID')),
|
|
E('div', { 'class': 'nm-scan-meta' }, [
|
|
E('span', {}, net.channel ? _('Ch. ') + net.channel : ''),
|
|
E('span', {}, net.signal || ''),
|
|
E('span', {}, net.encryption || '')
|
|
])
|
|
]);
|
|
|
|
card.addEventListener('click', ui.createHandlerFn(this, 'selectScannedNetwork', container, net));
|
|
list.appendChild(card);
|
|
}, this));
|
|
},
|
|
|
|
selectScannedNetwork: function(container, network) {
|
|
var ssidInput = container.querySelector('#travel-client-ssid');
|
|
if (ssidInput)
|
|
ssidInput.value = network.ssid || '';
|
|
|
|
if (network.encryption) {
|
|
var encSelect = container.querySelector('#travel-encryption');
|
|
if (encSelect && Array.prototype.some.call(encSelect.options, function(opt) { return opt.value === network.encryption; }))
|
|
encSelect.value = network.encryption;
|
|
}
|
|
},
|
|
|
|
saveTravelSettings: function(container) {
|
|
var payload = {
|
|
client_interface: container.querySelector('#travel-client-iface') ? container.querySelector('#travel-client-iface').value : '',
|
|
client_radio: container.querySelector('#travel-client-radio') ? container.querySelector('#travel-client-radio').value : '',
|
|
hotspot_radio: container.querySelector('#travel-hotspot-radio') ? container.querySelector('#travel-hotspot-radio').value : '',
|
|
ssid: container.querySelector('#travel-client-ssid') ? container.querySelector('#travel-client-ssid').value : '',
|
|
password: container.querySelector('#travel-client-password') ? container.querySelector('#travel-client-password').value : '',
|
|
encryption: container.querySelector('#travel-encryption') ? container.querySelector('#travel-encryption').value : 'sae-mixed',
|
|
hotspot_ssid: container.querySelector('#travel-hotspot-ssid') ? container.querySelector('#travel-hotspot-ssid').value : '',
|
|
hotspot_password: container.querySelector('#travel-hotspot-password') ? container.querySelector('#travel-hotspot-password').value : '',
|
|
clone_mac: container.querySelector('#travel-mac-clone') ? container.querySelector('#travel-mac-clone').value : '',
|
|
lan_subnet: container.querySelector('#travel-lan-ip') ? container.querySelector('#travel-lan-ip').value : '',
|
|
lan_netmask: container.querySelector('#travel-lan-mask') ? container.querySelector('#travel-lan-mask').value : ''
|
|
};
|
|
|
|
return helpers.persistSettings('travel', payload);
|
|
}
|
|
});
|