'use strict'; 'require view'; 'require secubox-theme/theme as Theme'; 'require ui'; 'require form'; 'require rpc'; 'require uci'; 'require crowdsec-dashboard/api as API'; 'require crowdsec-dashboard/nav as CsNav'; 'require secubox-portal/header as SbHeader'; // Hide LuCI tabs immediately (function() { if (typeof document === 'undefined') return; var style = document.getElementById('cs-hide-tabs') || document.createElement('style'); style.id = 'cs-hide-tabs'; style.textContent = 'ul.tabs, .cbi-tabmenu { display: none !important; }'; if (!style.parentNode) (document.head || document.documentElement).appendChild(style); })(); return view.extend({ // Health check results health: { crowdsecRunning: false, lapiAvailable: false, capiConnected: false, capiEnrolled: false, bouncerRegistered: false, bouncerRunning: false, nftablesActive: false, hubUpToDate: false, acquisitionConfigured: false, collectionsInstalled: 0, checking: true, error: null }, // Configuration options (user selections) config: { // What needs to be fixed (auto-detected) needsLapiRepair: false, needsCapiRegister: false, needsHubUpdate: false, needsAcquisition: false, needsCollections: false, needsBouncer: false, needsServices: false, // User inputs enrollmentKey: '', machineName: '', // Acquisition options syslogEnabled: true, firewallEnabled: true, sshEnabled: true, httpEnabled: false, // Collections to install selectedCollections: [], selectedParsers: [], // Bouncer options ipv4Enabled: true, ipv6Enabled: true, updateFrequency: '10s', // Apply state applying: false, applyStep: '', applyProgress: 0, applyComplete: false, applyErrors: [] }, load: function() { var self = this; // Load saved settings first, then run health check return API.getSettings().then(function(settings) { if (settings && settings.enrollment_key) { self.config.enrollmentKey = settings.enrollment_key; } if (settings && settings.machine_name) { self.config.machineName = settings.machine_name; } return self.runHealthCheck(); }).catch(function() { // Settings not available, continue with health check return self.runHealthCheck(); }); }, runHealthCheck: function() { var self = this; this.health.checking = true; this.health.error = null; return Promise.all([ API.getStatus(), API.getConsoleStatus(), API.getBouncers(), API.getFirewallBouncerStatus(), API.getCollections() ]).then(function(results) { var status = results[0] || {}; var consoleStatus = results[1] || {}; var bouncers = results[2] || {}; var bouncerStatus = results[3] || {}; var collections = results[4] || []; // Update health status self.health.crowdsecRunning = status.crowdsec === 'running'; self.health.lapiAvailable = status.lapi_status === 'available'; self.health.capiConnected = status.capi_status === 'connected' || status.capi_status === 'ok'; self.health.capiEnrolled = consoleStatus.enrolled === true; // Check bouncer registration var bouncerList = bouncers.bouncers || bouncers || []; var firewallBouncer = bouncerList.find(function(b) { return b.name === 'crowdsec-firewall-bouncer' || b.name === 'firewall-bouncer'; }); self.health.bouncerRegistered = !!firewallBouncer; self.health.bouncerRunning = bouncerStatus.running === true; self.health.nftablesActive = bouncerStatus.nftables_ipv4 || bouncerStatus.nftables_ipv6; // Count installed collections self.health.collectionsInstalled = Array.isArray(collections) ? collections.filter(function(c) { return c.installed; }).length : 0; self.health.checking = false; // Determine what needs to be fixed self.config.needsLapiRepair = self.health.crowdsecRunning && !self.health.lapiAvailable; self.config.needsCapiRegister = !self.health.capiConnected && !self.health.capiEnrolled; self.config.needsHubUpdate = self.health.capiConnected; // Only if CAPI works self.config.needsCollections = self.health.collectionsInstalled < 3; self.config.needsBouncer = !self.health.bouncerRegistered; self.config.needsServices = !self.health.bouncerRunning || !self.health.nftablesActive; return self.health; }).catch(function(err) { console.error('[Wizard] Health check error:', err); self.health.checking = false; self.health.error = err.message; return self.health; }); }, render: function(data) { // Initialize theme Theme.init(); // Load CSS var head = document.head || document.getElementsByTagName('head')[0]; var cssLink = E('link', { 'rel': 'stylesheet', 'type': 'text/css', 'href': L.resource('crowdsec-dashboard/wizard.css') }); head.appendChild(cssLink); var themeLink = E('link', { 'rel': 'stylesheet', 'type': 'text/css', 'href': L.resource('secubox-theme/secubox-theme.css') }); head.appendChild(themeLink); // Main wrapper var wrapper = E('div', { 'class': 'secubox-page-wrapper' }); wrapper.appendChild(SbHeader.render()); var container = E('div', { 'class': 'crowdsec-dashboard wizard-container' }); container.appendChild(CsNav.renderTabs('wizard')); // Render content based on state if (this.config.applyComplete) { container.appendChild(this.renderComplete()); } else if (this.config.applying) { container.appendChild(this.renderApplying()); } else { container.appendChild(this.renderHealthAndConfig()); } wrapper.appendChild(container); return wrapper; }, renderHealthAndConfig: function() { var self = this; return E('div', { 'class': 'wizard-step' }, [ // Header E('h2', {}, _('CrowdSec Setup Wizard')), E('p', {}, _('Health check and configuration in one step.')), // Health Check Section this.renderHealthCheck(), // Separator E('hr', { 'style': 'margin: 24px 0; border-color: rgba(255,255,255,0.1);' }), // Configuration Section (only if health check passed basic requirements) this.health.crowdsecRunning ? this.renderConfigOptions() : E([]), // Apply Button E('div', { 'class': 'wizard-nav', 'style': 'margin-top: 32px;' }, [ E('button', { 'class': 'cbi-button', 'click': function() { window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview'); } }, _('Cancel')), E('button', { 'class': 'cbi-button cbi-button-positive', 'style': 'font-size: 1.1em; padding: 12px 32px;', 'disabled': !this.health.crowdsecRunning || this.health.checking ? true : null, 'click': L.bind(this.handleApplyAll, this) }, _('Apply Configuration')) ]) ]); }, renderHealthCheck: function() { var self = this; // Determine protection mode var lapiOnly = this.health.lapiAvailable && !this.health.capiConnected; var fullProtection = this.health.lapiAvailable && this.health.capiConnected; var checks = [ { id: 'crowdsec', label: _('CrowdSec Service'), ok: this.health.crowdsecRunning, status: this.health.crowdsecRunning ? _('Running') : _('Stopped'), critical: true }, { id: 'lapi', label: _('Local API (LAPI)'), ok: this.health.lapiAvailable, status: this.health.lapiAvailable ? _('Available') : _('Unavailable'), critical: true, // LAPI is critical for protection action: !this.health.lapiAvailable && this.health.crowdsecRunning ? _('Will repair') : null }, { id: 'capi', label: _('Central API (CAPI)'), ok: this.health.capiConnected, status: this.health.capiConnected ? _('Connected') : (this.health.capiEnrolled ? _('Enrolled but not connected') : _('Not registered')), // CAPI is NOT critical - local protection works without it critical: false, warning: !this.health.capiConnected && this.health.lapiAvailable, action: !this.health.capiConnected ? _('Optional - enroll for community blocklists') : null }, { id: 'bouncer', label: _('Firewall Bouncer'), ok: this.health.bouncerRegistered && this.health.bouncerRunning, status: this.health.bouncerRegistered ? (this.health.bouncerRunning ? _('Running') : _('Registered but stopped')) : _('Not registered'), critical: true, // Bouncer is critical for enforcement action: !this.health.bouncerRegistered ? _('Will register') : (!this.health.bouncerRunning ? _('Will start') : null) }, { id: 'nftables', label: _('nftables Rules'), ok: this.health.nftablesActive, status: this.health.nftablesActive ? _('Active') : _('Not loaded'), critical: true, action: !this.health.nftablesActive ? _('Will configure') : null }, { id: 'collections', label: _('Security Collections'), ok: this.health.collectionsInstalled >= 3, status: _('%d installed').format(this.health.collectionsInstalled), action: this.health.collectionsInstalled < 3 ? _('Will install') : null } ]; return E('div', { 'class': 'health-check-section' }, [ E('h3', { 'style': 'margin-bottom: 16px; display: flex; align-items: center;' }, [ E('span', { 'style': 'margin-right: 8px;' }, '🔍'), _('Health Check'), this.health.checking ? E('span', { 'class': 'spinning', 'style': 'margin-left: 12px; font-size: 14px;' }, '') : E([]) ]), // Protection Mode Banner this.health.lapiAvailable ? E('div', { 'style': 'margin-bottom: 16px; padding: 12px 16px; border-radius: 8px; display: flex; align-items: center; ' + (fullProtection ? 'background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3);' : 'background: rgba(234, 179, 8, 0.1); border: 1px solid rgba(234, 179, 8, 0.3);') }, [ E('span', { 'style': 'font-size: 24px; margin-right: 12px;' }, fullProtection ? '🛡️' : '⚡'), E('div', {}, [ E('strong', { 'style': 'color: ' + (fullProtection ? '#22c55e' : '#eab308') + ';' }, fullProtection ? _('Full Protection Mode') : _('Local Protection Mode')), E('div', { 'style': 'font-size: 0.85em; color: #cbd5e1; margin-top: 2px;' }, fullProtection ? _('Community blocklists + local detection active') : _('Local auto-ban active. Enroll CAPI for community blocklists.')) ]) ]) : E([]), E('div', { 'class': 'status-checks', 'style': 'display: grid; gap: 8px;' }, checks.map(function(check) { // Determine icon and color based on ok/warning/critical status var iconColor = check.ok ? '#22c55e' : (check.warning ? '#eab308' : (check.critical ? '#ef4444' : '#eab308')); var icon = check.ok ? '✓' : (check.warning ? '⚠' : (check.critical ? '✗' : '⚠')); return E('div', { 'class': 'check-item', 'style': 'display: flex; align-items: center; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;' }, [ E('span', { 'class': 'check-icon', 'style': 'font-size: 20px; margin-right: 12px; color: ' + iconColor + ';' }, icon), E('div', { 'style': 'flex: 1;' }, [ E('strong', {}, check.label), E('div', { 'style': 'font-size: 0.85em; color: ' + (check.ok ? '#22c55e' : '#94a3b8') + ';' }, check.status) ]), check.action ? E('span', { 'class': 'badge', 'style': 'background: rgba(102, 126, 234, 0.2); color: #818cf8; padding: 4px 8px; border-radius: 4px; font-size: 0.8em;' }, check.action) : E([]) ]); }) ), // Error message if any this.health.error ? E('div', { 'style': 'margin-top: 16px; padding: 12px; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 8px; color: #ef4444;' }, this.health.error) : E([]), // Refresh button E('div', { 'style': 'margin-top: 12px;' }, [ E('button', { 'class': 'cbi-button', 'click': L.bind(function() { this.runHealthCheck().then(L.bind(function() { this.refreshView(); }, this)); }, this) }, _('Refresh')) ]) ]); }, renderConfigOptions: function() { var self = this; return E('div', { 'class': 'config-section' }, [ E('h3', { 'style': 'margin-bottom: 16px; display: flex; align-items: center;' }, [ E('span', { 'style': 'margin-right: 8px;' }, '⚙️'), _('Configuration Options') ]), // CAPI Enrollment (if needed) !this.health.capiConnected ? E('div', { 'class': 'config-group', 'style': 'margin-bottom: 20px; padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;' }, [ E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Console Enrollment (Optional)')), E('p', { 'style': 'margin: 0 0 12px 0; font-size: 0.9em; color: #cbd5e1;' }, _('Enroll to receive community blocklists. Leave empty to skip.')), E('input', { 'type': 'text', 'id': 'enrollment-key', 'class': 'cbi-input-text', 'placeholder': _('Enrollment key from app.crowdsec.net'), 'style': 'width: 100%; padding: 10px; margin-bottom: 8px;', 'value': this.config.enrollmentKey, 'input': function(ev) { self.config.enrollmentKey = ev.target.value; } }), E('input', { 'type': 'text', 'id': 'machine-name', 'class': 'cbi-input-text', 'placeholder': _('Machine name (optional)'), 'style': 'width: 100%; padding: 10px;', 'value': this.config.machineName, 'input': function(ev) { self.config.machineName = ev.target.value; } }), E('p', { 'style': 'margin: 8px 0 0 0; font-size: 0.85em; color: #94a3b8;' }, [ _('After enrollment, validate on '), E('a', { 'href': 'https://app.crowdsec.net', 'target': '_blank', 'style': 'color: #818cf8;' }, 'app.crowdsec.net'), _('. Service will restart automatically.') ]) ]) : E([]), // Log Acquisition E('div', { 'class': 'config-group', 'style': 'margin-bottom: 20px; padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;' }, [ E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Log Sources to Monitor')), this.renderToggle('syslog', _('System Syslog'), _('/var/log/messages'), this.config.syslogEnabled), this.renderToggle('firewall', _('Firewall Logs'), _('Port scan detection'), this.config.firewallEnabled), this.renderToggle('ssh', _('SSH/Dropbear'), _('Brute force detection'), this.config.sshEnabled), this.renderToggle('http', _('HTTP Server'), _('Web attacks (if running)'), this.config.httpEnabled) ]), // Collections E('div', { 'class': 'config-group', 'style': 'margin-bottom: 20px; padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;' }, [ E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Security Collections to Install')), this.renderCollectionToggle('crowdsecurity/linux', _('Base Linux scenarios'), true), this.renderCollectionToggle('crowdsecurity/sshd', _('SSH protection'), true), this.renderCollectionToggle('crowdsecurity/iptables', _('Firewall log parser'), this.config.firewallEnabled), this.renderCollectionToggle('crowdsecurity/http-cve', _('Web CVE protection'), this.config.httpEnabled), E('div', { 'style': 'margin-top: 12px;' }, [ E('strong', { 'style': 'font-size: 0.9em; color: #cbd5e1;' }, _('OpenWrt Parsers:')), ]), this.renderCollectionToggle('crowdsecurity/syslog-logs', _('Syslog parser'), true, 'parser'), this.renderCollectionToggle('crowdsecurity/dropbear-logs', _('Dropbear SSH parser'), this.config.sshEnabled, 'parser') ]), // Bouncer Options E('div', { 'class': 'config-group', 'style': 'padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;' }, [ E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Firewall Bouncer Options')), this.renderToggle('ipv4', _('Block IPv4'), _('Enable IPv4 blocking'), this.config.ipv4Enabled), this.renderToggle('ipv6', _('Block IPv6'), _('Enable IPv6 blocking'), this.config.ipv6Enabled), E('div', { 'style': 'margin-top: 12px;' }, [ E('label', { 'style': 'display: block; margin-bottom: 4px; font-size: 0.9em;' }, _('Update Frequency:')), E('select', { 'id': 'update-frequency', 'class': 'cbi-input-select', 'style': 'width: 100%; padding: 8px;', 'change': function(ev) { self.config.updateFrequency = ev.target.value; } }, [ E('option', { 'value': '10s', 'selected': this.config.updateFrequency === '10s' }, _('10 seconds (recommended)')), E('option', { 'value': '30s', 'selected': this.config.updateFrequency === '30s' }, _('30 seconds')), E('option', { 'value': '1m', 'selected': this.config.updateFrequency === '1m' }, _('1 minute')) ]) ]) ]) ]); }, renderToggle: function(id, label, description, checked) { var self = this; return E('div', { 'class': 'toggle-item', 'data-id': id, 'data-checked': checked ? '1' : '0', 'style': 'display: flex; align-items: center; padding: 8px 0; cursor: pointer;', 'click': function(ev) { var el = ev.currentTarget; var current = el.getAttribute('data-checked') === '1'; var newState = !current; el.setAttribute('data-checked', newState ? '1' : '0'); var checkbox = el.querySelector('.toggle-check'); if (checkbox) { checkbox.textContent = newState ? '☑' : '☐'; checkbox.style.color = newState ? '#22c55e' : '#64748b'; } // Update config var configId = el.getAttribute('data-id'); if (configId === 'syslog') self.config.syslogEnabled = newState; else if (configId === 'firewall') self.config.firewallEnabled = newState; else if (configId === 'ssh') self.config.sshEnabled = newState; else if (configId === 'http') self.config.httpEnabled = newState; else if (configId === 'ipv4') self.config.ipv4Enabled = newState; else if (configId === 'ipv6') self.config.ipv6Enabled = newState; } }, [ E('span', { 'class': 'toggle-check', 'style': 'font-size: 22px; margin-right: 12px; color: ' + (checked ? '#22c55e' : '#64748b') + ';' }, checked ? '☑' : '☐'), E('div', { 'style': 'flex: 1;' }, [ E('strong', { 'style': 'display: block;' }, label), E('span', { 'style': 'font-size: 0.85em; color: #94a3b8;' }, description) ]) ]); }, renderCollectionToggle: function(name, description, checked, type) { var self = this; type = type || 'collection'; return E('div', { 'class': 'collection-toggle', 'data-name': name, 'data-type': type, 'data-checked': checked ? '1' : '0', 'style': 'display: flex; align-items: center; padding: 6px 0; cursor: pointer;', 'click': function(ev) { var el = ev.currentTarget; var current = el.getAttribute('data-checked') === '1'; var newState = !current; el.setAttribute('data-checked', newState ? '1' : '0'); var checkbox = el.querySelector('.toggle-check'); if (checkbox) { checkbox.textContent = newState ? '☑' : '☐'; checkbox.style.color = newState ? '#22c55e' : '#64748b'; } } }, [ E('span', { 'class': 'toggle-check', 'style': 'font-size: 20px; margin-right: 10px; color: ' + (checked ? '#22c55e' : '#64748b') + ';' }, checked ? '☑' : '☐'), E('div', { 'style': 'flex: 1;' }, [ E('code', { 'style': 'font-size: 0.9em;' }, name), E('span', { 'style': 'margin-left: 8px; font-size: 0.85em; color: #94a3b8;' }, '— ' + description) ]) ]); }, renderApplying: function() { var progressPercent = Math.round(this.config.applyProgress); return E('div', { 'class': 'wizard-step', 'style': 'text-align: center; padding: 48px 24px;' }, [ E('div', { 'class': 'spinning', 'style': 'font-size: 48px; margin-bottom: 24px;' }, '⚙️'), E('h2', {}, _('Applying Configuration...')), E('p', { 'style': 'color: #cbd5e1; margin-bottom: 24px;' }, this.config.applyStep), // Progress bar E('div', { 'style': 'max-width: 400px; margin: 0 auto 24px;' }, [ E('div', { 'style': 'height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden;' }, [ E('div', { 'style': 'height: 100%; width: ' + progressPercent + '%; background: linear-gradient(90deg, #667eea, #764ba2); transition: width 0.3s;' }) ]), E('div', { 'style': 'margin-top: 8px; font-size: 0.9em; color: #94a3b8;' }, progressPercent + '%') ]), // Errors if any this.config.applyErrors.length > 0 ? E('div', { 'style': 'max-width: 500px; margin: 0 auto; text-align: left; padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px;' }, [ E('strong', { 'style': 'color: #ef4444;' }, _('Warnings:')), E('ul', { 'style': 'margin: 8px 0 0 0; padding-left: 20px; color: #f87171;' }, this.config.applyErrors.map(function(err) { return E('li', {}, err); }) ) ]) : E([]) ]); }, renderComplete: function() { var fullProtection = this.health.lapiAvailable && this.health.capiConnected; var localOnly = this.health.lapiAvailable && !this.health.capiConnected; return E('div', { 'class': 'wizard-step wizard-complete', 'style': 'text-align: center;' }, [ E('div', { 'class': 'success-hero', 'style': 'margin-bottom: 32px;' }, [ E('div', { 'style': 'font-size: 64px; margin-bottom: 16px;' }, fullProtection ? '🛡️' : '⚡'), E('h2', {}, _('Setup Complete!')) ]), // Protection Mode Banner E('div', { 'style': 'max-width: 400px; margin: 0 auto 24px; padding: 16px; border-radius: 8px; ' + (fullProtection ? 'background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3);' : 'background: rgba(234, 179, 8, 0.1); border: 1px solid rgba(234, 179, 8, 0.3);') }, [ E('strong', { 'style': 'color: ' + (fullProtection ? '#22c55e' : '#eab308') + ';' }, fullProtection ? _('Full Protection Mode') : _('Local Protection Mode')), E('div', { 'style': 'font-size: 0.85em; color: #cbd5e1; margin-top: 4px;' }, fullProtection ? _('Community blocklists + local detection active') : _('Local auto-ban protects against attacks detected on your network')) ]), E('p', { 'style': 'color: #cbd5e1; margin-bottom: 32px;' }, _('CrowdSec is now protecting your network.')), // Summary of what was done E('div', { 'style': 'max-width: 400px; margin: 0 auto 32px; text-align: left;' }, [ this.health.lapiAvailable ? this.renderCompletedItem(_('LAPI available (local detection)')) : E([]), this.health.capiConnected ? this.renderCompletedItem(_('CAPI connected (community blocklists)')) : this.renderWarningItem(_('CAPI not connected (local protection only)')), this.health.bouncerRegistered ? this.renderCompletedItem(_('Bouncer registered')) : E([]), this.health.nftablesActive ? this.renderCompletedItem(_('nftables rules active')) : E([]), this.health.collectionsInstalled > 0 ? this.renderCompletedItem(_('%d collections installed').format(this.health.collectionsInstalled)) : E([]) ]), // Errors/warnings if any this.config.applyErrors.length > 0 ? E('div', { 'style': 'max-width: 500px; margin: 0 auto 24px; padding: 16px; background: rgba(234, 179, 8, 0.1); border: 1px solid rgba(234, 179, 8, 0.3); border-radius: 8px; text-align: left;' }, [ E('strong', { 'style': 'color: #eab308;' }, _('Some steps had issues:')), E('ul', { 'style': 'margin: 8px 0 0 0; padding-left: 20px; color: #fbbf24;' }, this.config.applyErrors.map(function(err) { return E('li', {}, err); }) ) ]) : E([]), // Navigation E('div', { 'class': 'wizard-nav' }, [ E('button', { 'class': 'cbi-button cbi-button-positive', 'style': 'font-size: 1.1em; padding: 12px 32px;', 'click': function() { window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview'); } }, _('Go to Dashboard')) ]) ]); }, renderCompletedItem: function(text) { return E('div', { 'style': 'display: flex; align-items: center; padding: 8px 0;' }, [ E('span', { 'style': 'color: #22c55e; margin-right: 12px; font-size: 18px;' }, '✓'), E('span', {}, text) ]); }, renderWarningItem: function(text) { return E('div', { 'style': 'display: flex; align-items: center; padding: 8px 0;' }, [ E('span', { 'style': 'color: #eab308; margin-right: 12px; font-size: 18px;' }, '⚠'), E('span', { 'style': 'color: #cbd5e1;' }, text) ]); }, handleApplyAll: function() { var self = this; this.config.applying = true; this.config.applyProgress = 0; this.config.applyErrors = []; this.refreshView(); // Gather selected collections and parsers from DOM var collectionToggles = document.querySelectorAll('.collection-toggle[data-type="collection"][data-checked="1"]'); var parserToggles = document.querySelectorAll('.collection-toggle[data-type="parser"][data-checked="1"]'); this.config.selectedCollections = Array.from(collectionToggles).map(function(el) { return el.getAttribute('data-name'); }); this.config.selectedParsers = Array.from(parserToggles).map(function(el) { return el.getAttribute('data-name'); }); // Get enrollment key var keyInput = document.getElementById('enrollment-key'); var nameInput = document.getElementById('machine-name'); this.config.enrollmentKey = keyInput ? keyInput.value.trim() : ''; this.config.machineName = nameInput ? nameInput.value.trim() : ''; // Save enrollment key for future repairs (if provided) if (this.config.enrollmentKey) { API.saveSettings(this.config.enrollmentKey, this.config.machineName, '1').catch(function(err) { console.log('[Wizard] Failed to save enrollment key:', err); }); } // Define steps var steps = []; var stepWeight = 0; // Step 1: Repair LAPI if needed if (this.config.needsLapiRepair) { steps.push({ name: 'lapi', label: _('Repairing LAPI...'), weight: 15 }); stepWeight += 15; } // Step 2: Enroll CAPI if key provided if (this.config.enrollmentKey) { steps.push({ name: 'capi', label: _('Enrolling in CrowdSec Console...'), weight: 10 }); stepWeight += 10; } // Step 3: Update hub if CAPI available if (this.health.capiConnected || this.config.enrollmentKey) { steps.push({ name: 'hub', label: _('Updating hub...'), weight: 10 }); stepWeight += 10; } // Step 4: Configure acquisition steps.push({ name: 'acquisition', label: _('Configuring log acquisition...'), weight: 15 }); stepWeight += 15; // Step 5: Install collections if (this.config.selectedCollections.length > 0 || this.config.selectedParsers.length > 0) { steps.push({ name: 'collections', label: _('Installing collections...'), weight: 20 }); stepWeight += 20; } // Step 6: Configure bouncer if (this.config.needsBouncer) { steps.push({ name: 'bouncer', label: _('Registering bouncer...'), weight: 15 }); stepWeight += 15; } // Step 7: Start services steps.push({ name: 'services', label: _('Starting services...'), weight: 15 }); stepWeight += 15; // Normalize weights var totalWeight = stepWeight; steps.forEach(function(s) { s.weight = (s.weight / totalWeight) * 100; }); // Execute steps var currentProgress = 0; return steps.reduce(function(promise, step) { return promise.then(function() { self.config.applyStep = step.label; self.refreshView(); return self.executeStep(step.name).then(function(result) { currentProgress += step.weight; self.config.applyProgress = currentProgress; if (result && result.error) { self.config.applyErrors.push(step.label + ': ' + result.error); } }).catch(function(err) { currentProgress += step.weight; self.config.applyProgress = currentProgress; self.config.applyErrors.push(step.label + ': ' + err.message); }); }); }, Promise.resolve()).then(function() { // Final health check return self.runHealthCheck(); }).then(function() { self.config.applying = false; self.config.applyComplete = true; self.refreshView(); }); }, executeStep: function(stepName) { var self = this; switch (stepName) { case 'lapi': return API.repairLapi(); case 'capi': return API.consoleEnroll(this.config.enrollmentKey, this.config.machineName); case 'hub': return API.updateHub().catch(function(err) { // Hub update failure is not critical return { success: false, error: err.message }; }); case 'acquisition': return API.configureAcquisition( this.config.syslogEnabled ? '1' : '0', this.config.firewallEnabled ? '1' : '0', this.config.sshEnabled ? '1' : '0', this.config.httpEnabled ? '1' : '0', '/var/log/messages' ).catch(function(err) { // XHR abort during restart is OK if (err.message && err.message.indexOf('abort') !== -1) { return { success: true }; } throw err; }); case 'collections': // Install collections sequentially var items = this.config.selectedCollections.map(function(c) { return { type: 'collection', name: c }; }).concat(this.config.selectedParsers.map(function(p) { return { type: 'parser', name: p }; })); return items.reduce(function(promise, item) { return promise.then(function() { if (item.type === 'collection') { return API.installCollection(item.name).catch(function() { return {}; }); } else { return API.installHubItem('parser', item.name).catch(function() { return {}; }); } }); }, Promise.resolve()).then(function() { return { success: true }; }); case 'bouncer': return API.registerBouncer('crowdsec-firewall-bouncer').then(function(result) { if (!result.success) { return result; } // Configure bouncer settings return Promise.all([ API.updateFirewallBouncerConfig('enabled', '1'), API.updateFirewallBouncerConfig('ipv4', self.config.ipv4Enabled ? '1' : '0'), API.updateFirewallBouncerConfig('ipv6', self.config.ipv6Enabled ? '1' : '0'), API.updateFirewallBouncerConfig('update_frequency', self.config.updateFrequency), API.updateFirewallBouncerConfig('api_key', result.api_key) ]).then(function() { return { success: true }; }); }).catch(function(err) { // XHR abort during service restart is OK if (err.message && err.message.indexOf('abort') !== -1) { return { success: true }; } throw err; }); case 'services': return API.controlFirewallBouncer('enable').then(function() { return API.controlFirewallBouncer('start'); }).then(function() { // Wait for service to start return new Promise(function(resolve) { setTimeout(resolve, 2000); }); }).then(function() { return { success: true }; }).catch(function(err) { // XHR abort during service restart is OK if (err.message && err.message.indexOf('abort') !== -1) { return { success: true }; } throw err; }); default: return Promise.resolve({ success: true }); } }, refreshView: function() { var container = document.querySelector('.wizard-container'); if (!container) return; // Remove old content (keep nav tabs) var tabs = container.querySelector('.cs-nav-tabs'); while (container.lastChild && container.lastChild !== tabs) { container.removeChild(container.lastChild); } // Render new content if (this.config.applyComplete) { container.appendChild(this.renderComplete()); } else if (this.config.applying) { container.appendChild(this.renderApplying()); } else { container.appendChild(this.renderHealthAndConfig()); } }, goToStep: function() {}, handleSaveAndApply: null, handleSave: null, handleReset: null });