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>
355 lines
11 KiB
JavaScript
355 lines
11 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require ui';
|
|
'require dom';
|
|
'require poll';
|
|
'require network-modes.helpers as helpers';
|
|
'require secubox-theme/theme as Theme';
|
|
|
|
var callGetAvailableModes = rpc.declare({
|
|
object: 'luci.network-modes',
|
|
method: 'get_available_modes',
|
|
expect: { modes: [] }
|
|
});
|
|
|
|
var callGetCurrentMode = rpc.declare({
|
|
object: 'luci.network-modes',
|
|
method: 'get_current_mode',
|
|
expect: { }
|
|
});
|
|
|
|
var callSetMode = rpc.declare({
|
|
object: 'luci.network-modes',
|
|
method: 'set_mode',
|
|
params: ['mode'],
|
|
expect: { }
|
|
});
|
|
|
|
var callPreviewChanges = rpc.declare({
|
|
object: 'luci.network-modes',
|
|
method: 'preview_changes',
|
|
expect: { }
|
|
});
|
|
|
|
var callApplyMode = rpc.declare({
|
|
object: 'luci.network-modes',
|
|
method: 'apply_mode',
|
|
expect: { }
|
|
});
|
|
|
|
var callConfirmMode = rpc.declare({
|
|
object: 'luci.network-modes',
|
|
method: 'confirm_mode',
|
|
expect: { }
|
|
});
|
|
|
|
var callRollback = rpc.declare({
|
|
object: 'luci.network-modes',
|
|
method: 'rollback',
|
|
expect: { }
|
|
});
|
|
|
|
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: _('Network Mode Wizard'),
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
callGetAvailableModes(),
|
|
callGetCurrentMode()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var modes = data[0].modes || [];
|
|
var currentModeData = data[1] || {};
|
|
|
|
var hero = helpers.createHero({
|
|
icon: '🌐',
|
|
title: _('Network Configuration Wizard'),
|
|
subtitle: _('Switch between Router, Access Point, Relay, Travel, Bridge, and Sniffer topologies with automatic preview + rollback safety.'),
|
|
gradient: 'linear-gradient(135deg,#6366f1,#9333ea)',
|
|
actions: [
|
|
currentModeData.rollback_active ? E('button', {
|
|
'class': 'nm-btn nm-btn-primary',
|
|
'click': ui.createHandlerFn(this, 'handleConfirmMode')
|
|
}, ['✅ ', _('Confirm Mode')]) : null,
|
|
currentModeData.rollback_active ? E('button', {
|
|
'class': 'nm-btn',
|
|
'click': ui.createHandlerFn(this, 'handleRollbackNow')
|
|
}, ['↩ ', _('Rollback Now')]) : null
|
|
].filter(Boolean)
|
|
});
|
|
|
|
var stepper = helpers.createStepper([
|
|
{ title: _('Select Topology'), description: _('Pick Router / AP / Relay / Travel / Bridge / Sniffer') },
|
|
{ title: _('Review Changes'), description: _('Preview network config+services impacted') },
|
|
{ title: _('Apply Mode'), description: _('Network restarts, rollback timer starts') },
|
|
{ title: _('Confirm or Rollback'), description: _('2 minute confirmation window') }
|
|
], currentModeData.rollback_active ? 2 : 1);
|
|
|
|
var container = E('div', { 'class': 'network-modes-dashboard nm-wizard' }, [
|
|
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('wizard'),
|
|
hero,
|
|
stepper,
|
|
currentModeData.rollback_active ? this.renderRollbackBanner(currentModeData) : this.renderCurrentMode(currentModeData),
|
|
this.renderModesGrid(modes, currentModeData.current_mode),
|
|
this.renderInstructions()
|
|
]);
|
|
|
|
if (currentModeData.rollback_active) {
|
|
this.startRollbackPoll();
|
|
}
|
|
|
|
return container;
|
|
},
|
|
|
|
renderCurrentMode: function(data) {
|
|
return helpers.createSection({
|
|
title: _('Current Mode'),
|
|
icon: '✅',
|
|
badge: data.current_mode || '--',
|
|
body: [
|
|
E('p', { 'style': 'color:#94a3b8;' }, [
|
|
E('strong', {}, data.mode_name || data.current_mode),
|
|
E('br'),
|
|
E('span', {}, data.description || ''),
|
|
E('br'),
|
|
E('span', { 'style': 'font-size:12px;' }, _('Last change: ') + (data.last_change || _('Never')))
|
|
])
|
|
]
|
|
});
|
|
},
|
|
|
|
renderRollbackBanner: function(data) {
|
|
var remaining = data.rollback_remaining || 0;
|
|
var minutes = Math.floor(remaining / 60);
|
|
var seconds = remaining % 60;
|
|
return E('div', {
|
|
'class': 'nm-card',
|
|
'style': 'background:rgba(245,158,11,.15);border:1px solid rgba(245,158,11,.4);'
|
|
}, [
|
|
E('div', { 'class': 'nm-card-header' }, [
|
|
E('div', { 'class': 'nm-card-title' }, [
|
|
E('span', { 'class': 'nm-card-title-icon' }, '⏱️'),
|
|
_('Auto-Rollback Active')
|
|
]),
|
|
E('div', { 'class': 'nm-card-badge' }, data.current_mode || '')
|
|
]),
|
|
E('div', { 'class': 'nm-card-body' }, [
|
|
E('div', { 'id': 'rollback-timer', 'style': 'font-size:20px;font-weight:600;margin-bottom:8px;' },
|
|
_('Time left: ') + minutes + 'm ' + seconds + 's'),
|
|
E('p', {}, _('Confirm the mode if the network is stable, otherwise rollback will trigger automatically.')),
|
|
E('div', { 'class': 'nm-btn-group' }, [
|
|
E('button', { 'class': 'nm-btn nm-btn-primary', 'click': ui.createHandlerFn(this, 'handleConfirmMode') }, '✅ ' + _('Confirm Mode')),
|
|
E('button', { 'class': 'nm-btn', 'click': ui.createHandlerFn(this, 'handleRollbackNow') }, '↩ ' + _('Rollback Now'))
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderModesGrid: function(modes, currentMode) {
|
|
var grid = E('div', { 'class': 'nm-modes-grid nm-wizard-grid' });
|
|
modes.forEach(L.bind(function(mode) {
|
|
grid.appendChild(this.renderModeCard(mode, currentMode));
|
|
}, this));
|
|
return grid;
|
|
},
|
|
|
|
renderModeCard: function(mode, currentMode) {
|
|
var isCurrent = mode.id === currentMode;
|
|
var card = E('div', {
|
|
'class': 'nm-mode-card wizard-card' + (isCurrent ? ' active' : '')
|
|
}, [
|
|
E('div', { 'class': 'nm-mode-header' }, [
|
|
E('div', { 'class': 'nm-mode-icon' }, mode.icon || '🌐'),
|
|
E('div', { 'class': 'nm-mode-title' }, [
|
|
E('h3', {}, mode.name),
|
|
E('p', {}, mode.description || '')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'nm-mode-features' }, (mode.features || []).slice(0, 4).map(function(feature) {
|
|
return E('span', { 'class': 'nm-mode-feature' }, ['✓ ', feature]);
|
|
})),
|
|
isCurrent ?
|
|
E('div', { 'class': 'nm-mode-active-indicator' }, _('Active Mode')) :
|
|
E('button', {
|
|
'class': 'nm-btn nm-btn-primary',
|
|
'type': 'button',
|
|
'click': ui.createHandlerFn(this, 'handleSwitchMode', mode)
|
|
}, _('Switch to ') + mode.name)
|
|
]);
|
|
|
|
return card;
|
|
},
|
|
|
|
renderInstructions: function() {
|
|
return helpers.createSection({
|
|
title: _('Wizard Guidance'),
|
|
icon: '🧭',
|
|
body: [
|
|
E('ol', { 'style': 'color:#94a3b8;line-height:1.8;margin-left:16px;' }, [
|
|
E('li', {}, _('Select the target mode from the cards above.')),
|
|
E('li', {}, _('Review configuration diffs + services impacted.')),
|
|
E('li', {}, _('Apply mode; device takes a snapshot and restarts network.')),
|
|
E('li', {}, _('Confirm within 2 minutes or automatic rollback restores previous state.'))
|
|
])
|
|
]
|
|
});
|
|
},
|
|
|
|
handleSwitchMode: function(mode) {
|
|
ui.showModal(_('Switch to ') + mode.name, [
|
|
E('p', { 'class': 'spinning' }, _('Preparing mode change...'))
|
|
]);
|
|
|
|
return callSetMode(mode.id).then(L.bind(function(result) {
|
|
if (!result.success) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, result.error || _('Failed to prepare mode')), 'error');
|
|
return;
|
|
}
|
|
|
|
return callPreviewChanges().then(L.bind(function(preview) {
|
|
this.showPreviewModal(mode, preview);
|
|
}, this));
|
|
}, this)).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
showPreviewModal: function(mode, preview) {
|
|
if (!preview.success) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, preview.error || _('Preview failed')), 'error');
|
|
return;
|
|
}
|
|
|
|
var content = [
|
|
E('h4', {}, _('Changes to apply')),
|
|
E('p', {}, preview.current_mode + ' → ' + preview.target_mode),
|
|
E('ul', {},
|
|
(preview.changes || []).map(function(change) {
|
|
return E('li', {}, [
|
|
E('strong', {}, change.file + ': '),
|
|
E('span', {}, change.change)
|
|
]);
|
|
})
|
|
)
|
|
];
|
|
|
|
if (preview.warnings && preview.warnings.length) {
|
|
content.push(E('div', {
|
|
'class': 'alert-message warning',
|
|
'style': 'margin-top:12px;'
|
|
}, [
|
|
E('strong', {}, _('Warnings')),
|
|
E('ul', {}, preview.warnings.map(function(w) { return E('li', {}, w); }))
|
|
]));
|
|
}
|
|
|
|
content.push(E('div', { 'class': 'right', 'style': 'margin-top:16px;' }, [
|
|
E('button', { 'class': 'nm-btn', 'click': ui.hideModal }, _('Cancel')),
|
|
' ',
|
|
E('button', { 'class': 'nm-btn nm-btn-primary', 'click': L.bind(this.handleApplyMode, this, mode) }, _('Apply Mode'))
|
|
]));
|
|
|
|
ui.showModal(_('Preview: ') + mode.name, content);
|
|
},
|
|
|
|
handleApplyMode: function(mode) {
|
|
ui.showModal(_('Applying ') + mode.name, [
|
|
E('p', { 'class': 'spinning' }, _('Updating network configuration…'))
|
|
]);
|
|
|
|
return callApplyMode().then(function(result) {
|
|
ui.hideModal();
|
|
if (!result.success) {
|
|
ui.addNotification(null, E('p', {}, result.error || _('Failed to apply mode')), 'error');
|
|
return;
|
|
}
|
|
|
|
ui.showModal(_('Mode applied'), [
|
|
E('p', {}, _('Network restarting. If you lose connectivity, reconnect to the new subnet.')),
|
|
E('p', {}, _('Confirm within 2 minutes to keep this mode.')),
|
|
E('div', { 'class': 'right', 'style': 'margin-top:12px;' }, [
|
|
E('button', {
|
|
'class': 'nm-btn nm-btn-primary',
|
|
'click': function() {
|
|
ui.hideModal();
|
|
window.location.reload();
|
|
}
|
|
}, _('Reload view'))
|
|
])
|
|
]);
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
handleConfirmMode: function() {
|
|
return callConfirmMode().then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', {}, result.message || _('Mode confirmed')), 'info');
|
|
setTimeout(function() { window.location.reload(); }, 1000);
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, result.error || _('Confirmation failed')), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
handleRollbackNow: function() {
|
|
ui.showModal(_('Rollback...'), [
|
|
E('p', { 'class': 'spinning' }, _('Restoring previous configuration...'))
|
|
]);
|
|
|
|
return callRollback().then(function(result) {
|
|
ui.hideModal();
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', {}, result.message || _('Rollback started')), 'info');
|
|
setTimeout(function() { window.location.reload(); }, 1500);
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, result.error || _('Rollback failed')), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
startRollbackPoll: function() {
|
|
poll.add(L.bind(function() {
|
|
return callGetCurrentMode().then(L.bind(function(data) {
|
|
if (!data.rollback_active) {
|
|
poll.stop();
|
|
window.location.reload();
|
|
return;
|
|
}
|
|
|
|
var timerElem = document.getElementById('rollback-timer');
|
|
if (timerElem) {
|
|
var remaining = data.rollback_remaining || 0;
|
|
var minutes = Math.floor(remaining / 60);
|
|
var seconds = remaining % 60;
|
|
timerElem.textContent = _('Time left: ') + minutes + 'm ' + seconds + 's';
|
|
}
|
|
}, this));
|
|
}, this), 1);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|