'use strict'; 'require view'; 'require rpc'; 'require ui'; 'require dom'; 'require poll'; 'require network-modes.helpers as helpers'; 'require network-modes/api as API'; '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(), API.getStatus() ]); }, render: function(data) { var modes = data[0].modes || []; var currentModeData = data[1] || {}; var status = data[2] || {}; 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/common.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }), helpers.createNavigationTabs('wizard'), this.renderStatusBadges(status, currentModeData), 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; }, renderStatusBadges: function(status, currentMode) { var chips = [ { label: _('Version'), value: status.version || _('unknown') }, { label: _('Mode'), value: currentMode.mode_name || currentMode.current_mode || _('–') }, { label: _('WAN IP'), value: this.lookupInterfaceIp(status, 'wan') || _('Unknown') }, { label: _('LAN IP'), value: this.lookupInterfaceIp(status, 'lan') || _('Unknown') } ]; return E('div', { 'class': 'nm-hero-meta', 'style': 'margin-bottom:12px;' }, chips.map(function(chip) { return E('div', { 'class': 'nm-hero-chip' }, [ E('span', { 'class': 'nm-hero-chip-label' }, chip.label), E('strong', {}, chip.value) ]); })); }, renderPendingModeCard: function(data, modes) { if (!data.pending_mode) return null; var info = modes.find(function(mode) { return mode.id === data.pending_mode; }) || { id: data.pending_mode, name: data.pending_mode_name || data.pending_mode, description: '' }; return helpers.createSection({ title: _('Planned Mode'), icon: '🗂️', badge: info.name, body: [ E('p', { 'style': 'color:#94a3b8;' }, info.description || _('Ready to apply when you need to test this template.')), E('div', { 'class': 'nm-btn-group' }, [ E('button', { 'class': 'nm-btn', 'click': ui.createHandlerFn(this, 'handlePreviewPending', info) }, '📝 ' + _('Review Changes')), E('button', { 'class': 'nm-btn nm-btn-primary', 'click': ui.createHandlerFn(this, 'handleApplyPending', info) }, '✅ ' + _('Apply Planned Mode')) ]) ] }); }, renderBackupCard: function(data) { var lastBackup = data.last_backup_time || _('Not yet created'); if (!data.last_backup_time && !data.pending_mode) return null; return helpers.createSection({ title: _('Safety & Backups'), icon: '💾', body: [ E('p', {}, _('Latest snapshot: ') + (data.last_backup_time || _('Not available'))), data.pending_mode ? E('p', { 'style': 'color:#94a3b8;' }, _('Applying the planned mode will create a fresh backup before changes.')) : null, E('div', { 'style': 'margin-top:10px;font-family:monospace;font-size:12px;' }, data.last_backup || '') ] }); }, 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': function() { ui.hideModal(); window.location.reload(); } }, _('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, handlePreviewPending: function(modeInfo) { ui.showModal(_('Previewing pending mode…'), [ E('p', { 'class': 'spinning' }, _('Loading diff preview')) ]); return callPreviewChanges().then(L.bind(function(preview) { this.showPreviewModal(modeInfo, preview); }, this)).catch(function(err) { ui.hideModal(); ui.addNotification(null, E('p', {}, err.message || err), 'error'); }); }, handleApplyPending: function(modeInfo) { return this.handleApplyMode(modeInfo); }, lookupInterfaceIp: function(status, match) { var ifaces = (status && status.interfaces) || []; var iface = ifaces.find(function(item) { return item.name === match || item.name === (match === 'lan' ? 'br-lan' : match); }); return iface && iface.ip ? iface.ip : ''; } });