secubox-openwrt/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js
CyberMind-FR 4e23037a22 feat: implement network mode switcher with rollback protection
Implémente un switcher de mode réseau complet avec 4 modes:
- Router: NAT, DHCP server, firewall (default)
- Access Point: Bridge mode, no NAT, DHCP client
- Repeater: WiFi client + AP relay with optimizations
- Bridge: Pure L2 bridge, DHCP client

Nouvelles méthodes RPCD:
- get_current_mode: Détails du mode actif avec statut rollback
- get_available_modes: Liste des modes avec features
- set_mode: Préparer le changement de mode
- preview_changes: Prévisualiser les modifications
- apply_mode: Appliquer avec reconfiguration réseau complète
- confirm_mode: Confirmer et annuler le timer de rollback
- rollback: Restaurer la configuration précédente

Sécurité:
- Backup automatique avant changement
- Rollback automatique après 2 minutes sans confirmation
- Timer affiché en temps réel dans l'interface
- Restauration complète de network/wireless/firewall/dhcp

Vue wizard.js:
- Cards interactives pour chaque mode avec icônes
- Preview des changements avant application
- Progress bar et instructions post-switch
- Polling du timer de rollback
- Boutons de confirmation et rollback manuel

ACL mis à jour pour toutes les nouvelles méthodes.
2025-12-24 00:45:19 +01:00

411 lines
12 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require ui';
'require dom';
'require poll';
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: { }
});
return view.extend({
load: function() {
return Promise.all([
callGetAvailableModes(),
callGetCurrentMode()
]);
},
render: function(data) {
var modes = data[0].modes || [];
var currentModeData = data[1] || {};
var container = E('div', { 'class': 'cbi-map' });
// Header
container.appendChild(E('h2', {}, _('Network Mode Switcher')));
container.appendChild(E('div', { 'class': 'cbi-section-descr' },
_('Sélectionnez et basculez entre différents modes réseau. Un rollback automatique de 2 minutes protège contre les configurations défectueuses.')
));
// Current mode status
if (currentModeData.rollback_active) {
var remaining = currentModeData.rollback_remaining || 0;
container.appendChild(this.renderRollbackBanner(remaining, currentModeData.current_mode));
} else {
container.appendChild(this.renderCurrentMode(currentModeData));
}
// Modes grid
container.appendChild(this.renderModesGrid(modes, currentModeData.current_mode));
// Instructions
container.appendChild(this.renderInstructions());
// Start polling if rollback is active
if (currentModeData.rollback_active) {
this.startRollbackPoll();
}
return container;
},
renderCurrentMode: function(data) {
var section = E('div', {
'class': 'cbi-section',
'style': 'background: #1e293b; padding: 16px; border-radius: 8px; margin-bottom: 24px;'
});
section.appendChild(E('h3', { 'style': 'margin: 0 0 8px 0; color: #f1f5f9;' }, _('Mode Actuel')));
section.appendChild(E('div', { 'style': 'color: #94a3b8; font-size: 14px;' }, [
E('strong', { 'style': 'color: #22c55e; font-size: 18px;' }, data.mode_name || data.current_mode),
E('br'),
E('span', {}, data.description || ''),
E('br'),
E('span', { 'style': 'font-size: 12px;' }, _('Dernière modification: ') + (data.last_change || 'Never'))
]));
return section;
},
renderRollbackBanner: function(remaining, mode) {
var minutes = Math.floor(remaining / 60);
var seconds = remaining % 60;
var timeStr = minutes + 'm ' + seconds + 's';
var banner = E('div', {
'class': 'alert-message warning',
'style': 'background: #f59e0b; color: #000; padding: 16px; border-radius: 8px; margin-bottom: 24px;'
}, [
E('h3', { 'style': 'margin: 0 0 8px 0;' }, '⏱️ ' + _('Rollback Automatique Actif')),
E('div', { 'id': 'rollback-timer', 'style': 'font-size: 20px; font-weight: bold; margin: 8px 0;' },
_('Temps restant: ') + timeStr
),
E('div', { 'style': 'margin: 12px 0;' },
_('Le mode ') + mode + _(' sera annulé automatiquement si vous ne confirmez pas.')
),
E('button', {
'class': 'cbi-button cbi-button-positive',
'style': 'margin-right: 8px;',
'click': ui.createHandlerFn(this, 'handleConfirmMode')
}, _('✓ Confirmer le Mode')),
E('button', {
'class': 'cbi-button cbi-button-negative',
'click': ui.createHandlerFn(this, 'handleRollbackNow')
}, _('↩ Annuler Maintenant'))
]);
return banner;
},
renderModesGrid: function(modes, currentMode) {
var grid = E('div', {
'class': 'cbi-section',
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-bottom: 24px;'
});
modes.forEach(L.bind(function(mode) {
grid.appendChild(this.renderModeCard(mode, mode.current));
}, this));
return grid;
},
renderModeCard: function(mode, isCurrent) {
var borderColor = isCurrent ? '#22c55e' : '#334155';
var bgColor = isCurrent ? '#1e293b' : '#0f172a';
var card = E('div', {
'class': 'mode-card',
'style': 'background: ' + bgColor + '; border: 2px solid ' + borderColor + '; border-radius: 8px; padding: 16px; cursor: ' + (isCurrent ? 'default' : 'pointer') + '; transition: all 0.2s;',
'data-mode': mode.id
});
// Icon and name
card.appendChild(E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, mode.icon));
card.appendChild(E('div', { 'style': 'font-size: 18px; font-weight: bold; color: #f1f5f9; margin-bottom: 4px;' },
mode.name
));
// Description
card.appendChild(E('div', { 'style': 'color: #94a3b8; font-size: 14px; margin-bottom: 12px; min-height: 40px;' },
mode.description
));
// Features list
var featuresList = E('ul', { 'style': 'color: #64748b; font-size: 13px; margin: 12px 0; padding-left: 20px;' });
(mode.features || []).forEach(function(feature) {
featuresList.appendChild(E('li', {}, feature));
});
card.appendChild(featuresList);
// Button
if (isCurrent) {
card.appendChild(E('div', {
'class': 'cbi-value-description',
'style': 'color: #22c55e; font-weight: bold; text-align: center; padding: 8px;'
}, '✓ ' + _('Mode Actuel')));
} else {
var btn = E('button', {
'class': 'cbi-button cbi-button-action',
'style': 'width: 100%;',
'click': ui.createHandlerFn(this, 'handleSwitchMode', mode)
}, _('Switch to ') + mode.name);
card.appendChild(btn);
// Hover effect
card.addEventListener('mouseenter', function() {
this.style.borderColor = '#3b82f6';
this.style.transform = 'translateY(-2px)';
});
card.addEventListener('mouseleave', function() {
this.style.borderColor = borderColor;
this.style.transform = 'translateY(0)';
});
}
return card;
},
renderInstructions: function() {
var section = E('div', { 'class': 'cbi-section' });
section.appendChild(E('h3', {}, _('Instructions')));
var steps = E('ol', { 'style': 'color: #94a3b8; line-height: 1.8;' });
steps.appendChild(E('li', {}, _('Sélectionnez le mode réseau souhaité en cliquant sur "Switch to..."')));
steps.appendChild(E('li', {}, _('Vérifiez les changements qui seront appliqués dans la prévisualisation')));
steps.appendChild(E('li', {}, _('Confirmez l\'application - la configuration réseau sera modifiée')));
steps.appendChild(E('li', {}, _('Reconnectez-vous via la nouvelle IP si nécessaire (notée dans les instructions)')));
steps.appendChild(E('li', {}, _('Confirmez le nouveau mode dans les 2 minutes, sinon rollback automatique')));
section.appendChild(steps);
return section;
},
handleSwitchMode: function(mode, ev) {
var modal = ui.showModal(_('Switch to ') + mode.name, [
E('p', { 'class': 'spinning' }, _('Préparation du changement de mode...'))
]);
return callSetMode(mode.id).then(L.bind(function(result) {
if (!result.success) {
ui.hideModal();
ui.addNotification(null, E('p', result.error || _('Erreur')), 'error');
return;
}
// Show preview
return callPreviewChanges().then(L.bind(function(preview) {
this.showPreviewModal(mode, preview);
}, this));
}, this)).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Erreur: ') + err.message), 'error');
});
},
showPreviewModal: function(mode, preview) {
if (!preview.success) {
ui.hideModal();
ui.addNotification(null, E('p', preview.error || _('Erreur')), 'error');
return;
}
var content = [
E('h4', {}, _('Changements qui seront appliqués:')),
E('div', { 'style': 'background: #1e293b; padding: 12px; border-radius: 4px; margin: 12px 0;' }, [
E('strong', {}, preview.current_mode + ' → ' + preview.target_mode)
])
];
// Changes list
var changesList = E('ul', { 'style': 'margin: 12px 0;' });
(preview.changes || []).forEach(function(change) {
changesList.appendChild(E('li', {}, [
E('strong', {}, change.file + ': '),
E('span', {}, change.change)
]));
});
content.push(changesList);
// Warnings
if (preview.warnings && preview.warnings.length > 0) {
content.push(E('div', {
'class': 'alert-message warning',
'style': 'background: #f59e0b20; border-left: 4px solid #f59e0b; padding: 12px; margin: 12px 0;'
}, [
E('h5', { 'style': 'margin: 0 0 8px 0;' }, '⚠️ ' + _('Avertissements:')),
E('ul', { 'style': 'margin: 0; padding-left: 20px;' },
preview.warnings.map(function(w) {
return E('li', {}, w);
})
)
]));
}
// Buttons
content.push(E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': ui.hideModal
}, _('Annuler')),
' ',
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': L.bind(this.handleApplyMode, this, mode)
}, _('Appliquer le Mode'))
]));
ui.showModal(_('Prévisualisation: ') + mode.name, content);
},
handleApplyMode: function(mode, ev) {
ui.showModal(_('Application en cours...'), [
E('p', { 'class': 'spinning' }, _('Application du mode ') + mode.name + '...'),
E('p', {}, _('La connexion réseau sera brièvement interrompue.'))
]);
return callApplyMode().then(function(result) {
if (!result.success) {
ui.hideModal();
ui.addNotification(null, E('p', result.error || _('Erreur')), 'error');
return;
}
ui.hideModal();
// Show success with instructions
ui.showModal(_('Mode Appliqué'), [
E('div', { 'class': 'alert-message success' }, [
E('h4', {}, '✓ ' + _('Mode ') + mode.name + _(' activé')),
E('p', {}, _('Rollback automatique dans 2 minutes si non confirmé.')),
E('p', {}, _('Si vous perdez la connexion:')),
E('ul', {}, [
E('li', {}, _('Router: http://192.168.1.1')),
E('li', {}, _('Access Point/Bridge: Utilisez DHCP')),
E('li', {}, _('Repeater: http://192.168.2.1'))
])
]),
E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': function() {
ui.hideModal();
window.location.reload();
}
}, _('Recharger la Page'))
])
]);
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Erreur: ') + err.message), 'error');
});
},
handleConfirmMode: function(ev) {
return callConfirmMode().then(function(result) {
if (result.success) {
ui.addNotification(null, E('p', '✓ ' + result.message), 'info');
setTimeout(function() {
window.location.reload();
}, 1000);
} else {
ui.addNotification(null, E('p', result.error), 'error');
}
}).catch(function(err) {
ui.addNotification(null, E('p', _('Erreur: ') + err.message), 'error');
});
},
handleRollbackNow: function(ev) {
if (!confirm(_('Annuler le changement de mode et revenir à la configuration précédente?'))) {
return;
}
ui.showModal(_('Rollback...'), [
E('p', { 'class': 'spinning' }, _('Restauration de la configuration précédente...'))
]);
return callRollback().then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', '✓ ' + result.message), 'info');
setTimeout(function() {
window.location.reload();
}, 2000);
} else {
ui.addNotification(null, E('p', result.error), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Erreur: ') + err.message), '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 = _('Temps restant: ') + minutes + 'm ' + seconds + 's';
}
}, this));
}, this), 1);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});