diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js index ad09e321..088e1762 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js @@ -191,6 +191,12 @@ var callWizardState = rpc.declare({ expect: { } }); +var callRepairLapi = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'repair_lapi', + expect: { } +}); + function formatDuration(seconds) { if (!seconds) return 'N/A'; if (seconds < 60) return seconds + 's'; @@ -310,6 +316,7 @@ return baseclass.extend({ // Wizard Methods checkWizardNeeded: callCheckWizardNeeded, getWizardState: callWizardState, + repairLapi: callRepairLapi, formatDuration: formatDuration, formatDate: formatDate, diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js index 76c57e91..ba4019df 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js @@ -14,7 +14,7 @@ return view.extend({ title: _('Metrics'), - + csApi: null, metrics: {}, bouncers: [], @@ -22,11 +22,6 @@ return view.extend({ hub: {}, load: function() { - var cssLink = document.createElement('link'); - cssLink.rel = 'stylesheet'; - cssLink.href = L.resource('crowdsec-dashboard/dashboard.css'); - document.head.appendChild(cssLink); - this.csApi = api; return Promise.all([ @@ -50,97 +45,111 @@ return view.extend({ if (!data || typeof data !== 'object') { return null; } - + var entries = Object.entries(data); if (entries.length === 0) { return null; } - + var items = entries.map(function(entry) { var value = entry[1]; if (typeof value === 'object') { value = JSON.stringify(value); } - return E('div', { 'class': 'cs-metric-item' }, [ - E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('span', { 'class': 'cs-metric-name' }, entry[0]), - E('span', { 'class': 'cs-metric-value' }, String(value)) + return E('div', { 'class': 'cyber-metric-item', 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [ + E('span', { 'class': 'cyber-metric-name', 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, entry[0]), + E('span', { 'class': 'cyber-metric-value', 'style': 'color: var(--cyber-text-primary, #fff); font-weight: 500;' }, String(value)) ]); }); - - return E('div', { 'class': 'cs-metric-section' }, [ - E('div', { 'class': 'cs-metric-section-title' }, title), - E('div', { 'class': 'cs-metric-list' }, items) + + return E('div', { 'class': 'cyber-metric-section', 'style': 'margin-bottom: 1rem;' }, [ + E('div', { 'class': 'cyber-metric-section-title', 'style': 'font-weight: 600; color: var(--cyber-accent-primary, #667eea); margin-bottom: 0.5rem; font-size: 0.9rem;' }, title), + E('div', { 'class': 'cyber-metric-list' }, items) ]); }, renderBouncersTable: function() { var self = this; - - if (!Array.isArray(this.bouncers) || this.bouncers.length === 0) { - return E('div', { 'class': 'cs-empty' }, [ - E('div', { 'class': 'cs-empty-icon' }, '🔌'), - E('p', {}, 'No bouncers registered') + var bouncers = this.bouncers; + + // Handle response structure: may be { bouncers: [...] } or direct array + if (bouncers && bouncers.bouncers) { + bouncers = bouncers.bouncers; + } + + if (!Array.isArray(bouncers) || bouncers.length === 0) { + return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [ + E('div', { 'style': 'font-size: 2rem; margin-bottom: 0.5rem;' }, '🔌'), + E('p', {}, _('No bouncers registered')) ]); } - - var rows = this.bouncers.map(function(b) { + + var rows = bouncers.map(function(b) { var isValid = b.is_valid !== false; return E('tr', {}, [ E('td', {}, E('strong', {}, b.name || 'N/A')), E('td', {}, b.ip_address || 'N/A'), E('td', {}, b.type || 'N/A'), - E('td', {}, E('span', { - 'class': 'cs-action ' + (isValid ? 'ban' : ''), - 'style': isValid ? 'background: rgba(0,212,170,0.15); color: var(--cs-accent-green)' : '' - }, isValid ? 'Valid' : 'Invalid')), - E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatRelativeTime(b.last_pull))) + E('td', {}, E('span', { + 'class': 'cyber-badge ' + (isValid ? 'cyber-badge--success' : 'cyber-badge--danger') + }, isValid ? _('Valid') : _('Invalid'))), + E('td', {}, E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9em;' }, self.csApi.formatRelativeTime(b.last_pull))) ]); }); - - return E('table', { 'class': 'cs-table' }, [ + + return E('table', { 'class': 'cyber-table', 'style': 'width: 100%; border-collapse: collapse;' }, [ E('thead', {}, E('tr', {}, [ - E('th', {}, 'Name'), - E('th', {}, 'IP Address'), - E('th', {}, 'Type'), - E('th', {}, 'Status'), - E('th', {}, 'Last Pull') + E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Name')), + E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('IP Address')), + E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Type')), + E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Status')), + E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Last Pull')) ])), - E('tbody', {}, rows) + E('tbody', { 'style': 'color: var(--cyber-text-primary, #fff);' }, rows.map(function(row) { + row.querySelectorAll('td').forEach(function(td) { + td.style.cssText = 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));'; + }); + return row; + })) ]); }, renderMachinesTable: function() { var self = this; - - if (!Array.isArray(this.machines) || this.machines.length === 0) { - return E('div', { 'class': 'cs-empty' }, [ - E('div', { 'class': 'cs-empty-icon' }, '🖥️'), - E('p', {}, 'No machines registered') + var machines = this.machines; + + // Handle response structure: may be { machines: [...] } or direct array + if (machines && machines.machines) { + machines = machines.machines; + } + + if (!Array.isArray(machines) || machines.length === 0) { + return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [ + E('div', { 'style': 'font-size: 2rem; margin-bottom: 0.5rem;' }, '🖥️'), + E('p', {}, _('No machines registered')) ]); } - - var rows = this.machines.map(function(m) { + + var rows = machines.map(function(m) { var isValid = m.is_validated !== false; return E('tr', {}, [ - E('td', {}, E('strong', {}, m.machineId || 'N/A')), - E('td', {}, m.ip_address || 'N/A'), - E('td', {}, E('span', { - 'class': 'cs-action', - 'style': isValid ? 'background: rgba(0,212,170,0.15); color: var(--cs-accent-green)' : 'background: rgba(255,107,107,0.15); color: var(--cs-accent-red)' - }, isValid ? 'Validated' : 'Pending')), - E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatRelativeTime(m.last_heartbeat))), - E('td', {}, m.version || 'N/A') + E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, E('strong', {}, m.machineId || m.machine_id || 'N/A')), + E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, m.ip_address || 'N/A'), + E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, E('span', { + 'class': 'cyber-badge ' + (isValid ? 'cyber-badge--success' : 'cyber-badge--warning') + }, isValid ? _('Validated') : _('Pending'))), + E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05)); color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9em;' }, self.csApi.formatRelativeTime(m.last_heartbeat)), + E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, m.version || 'N/A') ]); }); - - return E('table', { 'class': 'cs-table' }, [ + + return E('table', { 'class': 'cyber-table', 'style': 'width: 100%; border-collapse: collapse; color: var(--cyber-text-primary, #fff);' }, [ E('thead', {}, E('tr', {}, [ - E('th', {}, 'Machine ID'), - E('th', {}, 'IP Address'), - E('th', {}, 'Status'), - E('th', {}, 'Last Heartbeat'), - E('th', {}, 'Version') + E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Machine ID')), + E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('IP Address')), + E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Status')), + E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Last Heartbeat')), + E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Version')) ])), E('tbody', {}, rows) ]); @@ -148,18 +157,18 @@ return view.extend({ renderHubStats: function() { var hub = this.hub; - + if (!hub || typeof hub !== 'object') { - return E('div', { 'class': 'cs-empty' }, [ - E('p', {}, 'Hub data not available') + return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [ + E('p', {}, _('Hub data not available')) ]); } - + var collections = hub.collections || []; var parsers = hub.parsers || []; var scenarios = hub.scenarios || []; var postoverflows = hub.postoverflows || []; - + var countInstalled = function(items) { if (!Array.isArray(items)) return 0; // Check for status === 'enabled' or if local_version exists (means installed) @@ -167,85 +176,82 @@ return view.extend({ return i.status === 'enabled' || i.local_version; }).length; }; - - return E('div', { 'class': 'cs-stats-grid' }, [ - E('div', { 'class': 'cs-stat-card' }, [ - E('div', { 'class': 'cs-stat-label' }, 'Collections'), - E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(collections))), - E('div', { 'class': 'cs-stat-trend' }, 'installed') - ]), - E('div', { 'class': 'cs-stat-card' }, [ - E('div', { 'class': 'cs-stat-label' }, 'Parsers'), - E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(parsers))), - E('div', { 'class': 'cs-stat-trend' }, 'installed') - ]), - E('div', { 'class': 'cs-stat-card' }, [ - E('div', { 'class': 'cs-stat-label' }, 'Scenarios'), - E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(scenarios))), - E('div', { 'class': 'cs-stat-trend' }, 'installed') - ]), - E('div', { 'class': 'cs-stat-card' }, [ - E('div', { 'class': 'cs-stat-label' }, 'Postoverflows'), - E('div', { 'class': 'cs-stat-value success' }, String(countInstalled(postoverflows))), - E('div', { 'class': 'cs-stat-trend' }, 'installed') - ]) - ]); + + var statCards = [ + { label: _('Collections'), count: countInstalled(collections), icon: '📦' }, + { label: _('Parsers'), count: countInstalled(parsers), icon: '📝' }, + { label: _('Scenarios'), count: countInstalled(scenarios), icon: '🎯' }, + { label: _('Postoverflows'), count: countInstalled(postoverflows), icon: '🔄' } + ]; + + return E('div', { 'class': 'cyber-card-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;' }, + statCards.map(function(stat) { + return E('div', { 'class': 'cyber-card cyber-card--compact', 'style': 'text-align: center;' }, [ + E('div', { 'class': 'cyber-card-body' }, [ + E('div', { 'style': 'font-size: 1.5rem; margin-bottom: 0.5rem;' }, stat.icon), + E('div', { 'style': 'font-size: 2rem; font-weight: 700; color: var(--cyber-success, #00d4aa);' }, String(stat.count)), + E('div', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.85rem; margin-top: 0.25rem;' }, stat.label), + E('div', { 'style': 'color: var(--cyber-text-muted, #666); font-size: 0.75rem;' }, _('installed')) + ]) + ]); + }) + ); }, renderCollectionsList: function() { - var collections = this.hub?.collections || []; - + var collections = this.hub && this.hub.collections ? this.hub.collections : []; + if (!Array.isArray(collections) || collections.length === 0) { - return E('div', { 'class': 'cs-empty' }, [ - E('p', {}, 'No collections data') + return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [ + E('p', {}, _('No collections data')) ]); } - + var installed = collections.filter(function(c) { return c.status === 'enabled' || c.local_version; }); - + var items = installed.slice(0, 15).map(function(c) { - return E('div', { 'class': 'cs-metric-item' }, [ - E('span', { 'class': 'cs-metric-name' }, c.name || 'N/A'), - E('span', { - 'class': 'cs-scenario', - 'style': c.up_to_date ? '' : 'background: rgba(255,169,77,0.15); color: var(--cs-accent-orange)' - }, c.up_to_date ? c.local_version || 'installed' : 'update available') + return E('div', { 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [ + E('span', { 'style': 'color: var(--cyber-text-primary, #fff);' }, c.name || 'N/A'), + E('span', { + 'class': 'cyber-badge ' + (c.up_to_date !== false ? 'cyber-badge--success' : 'cyber-badge--warning'), + 'style': 'font-size: 0.75rem;' + }, c.up_to_date !== false ? (c.local_version || _('installed')) : _('update available')) ]); }); - - return E('div', { 'class': 'cs-metric-list' }, items); + + return E('div', { 'class': 'cyber-collections-list' }, items); }, renderAcquisitionMetrics: function() { var metrics = this.metrics; - + if (!metrics || !metrics.acquisition) { - return E('div', { 'class': 'cs-empty' }, [ - E('p', {}, 'Acquisition metrics not available') + return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [ + E('p', {}, _('Acquisition metrics not available')) ]); } - + var acquisition = metrics.acquisition; var items = []; - + Object.entries(acquisition).forEach(function(entry) { var source = entry[0]; var data = entry[1]; - - items.push(E('div', { 'class': 'cs-metric-item', 'style': 'flex-direction: column; align-items: flex-start; gap: 8px' }, [ - E('strong', { 'style': 'font-size: 12px' }, source), - E('div', { 'style': 'display: flex; gap: 16px; font-size: 11px; color: var(--cs-text-muted)' }, [ - E('span', {}, 'Read: ' + (data.lines_read || 0)), - E('span', {}, 'Parsed: ' + (data.lines_parsed || 0)), - E('span', {}, 'Unparsed: ' + (data.lines_unparsed || 0)), - E('span', {}, 'Buckets: ' + (data.lines_poured_to_bucket || 0)) + + items.push(E('div', { 'style': 'padding: 0.75rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [ + E('strong', { 'style': 'font-size: 0.85rem; color: var(--cyber-text-primary, #fff); display: block; margin-bottom: 0.25rem;' }, source), + E('div', { 'style': 'display: flex; gap: 1rem; flex-wrap: wrap; font-size: 0.8rem; color: var(--cyber-text-secondary, #a0a0b0);' }, [ + E('span', {}, _('Read: ') + (data.lines_read || 0)), + E('span', {}, _('Parsed: ') + (data.lines_parsed || 0)), + E('span', {}, _('Unparsed: ') + (data.lines_unparsed || 0)), + E('span', {}, _('Buckets: ') + (data.lines_poured_to_bucket || 0)) ]) ])); }); - - return E('div', { 'class': 'cs-metric-list' }, items); + + return E('div', { 'class': 'cyber-acquisition-list' }, items); }, renderMetricsConfig: function(metricsConfig) { @@ -253,30 +259,30 @@ return view.extend({ var enabled = metricsConfig && (metricsConfig.metrics_enabled === true || metricsConfig.metrics_enabled === 1); var prometheusEndpoint = metricsConfig && metricsConfig.prometheus_endpoint || 'http://127.0.0.1:6060/metrics'; - return E('div', { 'class': 'cs-card', 'style': 'margin-bottom: 24px;' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, '⚙️ Metrics Export Configuration'), + return E('div', { 'class': 'cyber-card', 'style': 'margin-bottom: 1.5rem;' }, [ + E('div', { 'class': 'cyber-card-header' }, [ + E('div', { 'class': 'cyber-card-title' }, [ + E('span', { 'style': 'margin-right: 0.5rem;' }, '⚙️'), + _('Metrics Export Configuration') + ]), E('span', { - 'class': 'cs-action', - 'style': enabled ? - 'background: rgba(0,212,170,0.15); color: var(--cs-accent-green); padding: 6px 12px; border-radius: 6px; font-weight: 600; margin-left: auto;' : - 'background: rgba(255,107,107,0.15); color: var(--cs-accent-red); padding: 6px 12px; border-radius: 6px; font-weight: 600; margin-left: auto;' + 'class': 'cyber-badge ' + (enabled ? 'cyber-badge--success' : 'cyber-badge--danger') }, enabled ? _('Enabled') : _('Disabled')) ]), - E('div', { 'class': 'cs-card-body' }, [ - E('div', { 'class': 'cs-metric-list' }, [ - E('div', { 'class': 'cs-metric-item' }, [ - E('span', { 'class': 'cs-metric-name' }, _('Metrics Export Status')), - E('span', { 'class': 'cs-metric-value' }, enabled ? _('Enabled') : _('Disabled')) + E('div', { 'class': 'cyber-card-body' }, [ + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [ + E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, _('Metrics Export Status')), + E('span', { 'style': 'color: var(--cyber-text-primary, #fff);' }, enabled ? _('Enabled') : _('Disabled')) ]), - E('div', { 'class': 'cs-metric-item' }, [ - E('span', { 'class': 'cs-metric-name' }, _('Prometheus Endpoint')), - E('code', { 'class': 'cs-metric-value', 'style': 'font-size: 13px;' }, prometheusEndpoint) + E('div', { 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [ + E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, _('Prometheus Endpoint')), + E('code', { 'style': 'font-size: 0.85rem; color: var(--cyber-accent-primary, #667eea);' }, prometheusEndpoint) ]) ]), - E('div', { 'style': 'margin-top: 16px; display: flex; gap: 12px; align-items: center;' }, [ + E('div', { 'style': 'display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;' }, [ E('button', { - 'class': 'cbi-button ' + (enabled ? 'cbi-button-negative' : 'cbi-button-positive'), + 'class': 'cyber-btn ' + (enabled ? 'cyber-btn--danger' : 'cyber-btn--success'), 'click': function() { var newState = !enabled; ui.showModal(_('Updating Metrics Configuration...'), [ @@ -296,14 +302,16 @@ return view.extend({ }); } }, enabled ? _('Disable Metrics Export') : _('Enable Metrics Export')), - E('span', { 'style': 'color: var(--cs-text-muted); font-size: 13px;' }, + E('span', { 'style': 'color: var(--cyber-text-muted, #666); font-size: 0.85rem;' }, _('Note: Changing this setting requires restarting CrowdSec')) ]), - E('div', { 'class': 'cs-info-box', 'style': 'margin-top: 16px; padding: 12px; background: rgba(0,150,255,0.1); border-left: 4px solid var(--cs-accent-cyan); border-radius: 4px;' }, [ - E('p', { 'style': 'margin: 0 0 8px 0; color: var(--cs-text-primary); font-weight: 600;' }, _('About Metrics Export')), - E('p', { 'style': 'margin: 0; color: var(--cs-text-secondary); font-size: 14px;' }, [ - _('When enabled, CrowdSec exports Prometheus-compatible metrics that can be scraped by monitoring tools. Access metrics at: '), - E('code', {}, prometheusEndpoint) + E('div', { 'class': 'cyber-card cyber-card--info cyber-card--compact', 'style': 'margin-top: 1rem;' }, [ + E('div', { 'class': 'cyber-card-body' }, [ + E('p', { 'style': 'margin: 0 0 0.5rem 0; color: var(--cyber-text-primary, #fff); font-weight: 600;' }, _('About Metrics Export')), + E('p', { 'style': 'margin: 0; color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9rem;' }, [ + _('When enabled, CrowdSec exports Prometheus-compatible metrics that can be scraped by monitoring tools. Access metrics at: '), + E('code', { 'style': 'color: var(--cyber-accent-primary, #667eea);' }, prometheusEndpoint) + ]) ]) ]) ]) @@ -313,75 +321,105 @@ return view.extend({ render: function(data) { var self = this; + // Initialize theme + Theme.init(); + this.metrics = data.metrics || {}; this.bouncers = data.bouncers || []; this.machines = data.machines || {}; this.hub = data.hub || {}; var metricsConfig = data.metricsConfig || {}; - var view = E('div', { 'class': 'crowdsec-dashboard' }, [ + var view = E('div', { 'class': 'cyber-container crowdsec-metrics' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + + // Page Header + E('div', { 'style': 'margin-bottom: 1.5rem;' }, [ + E('h2', { 'style': 'color: var(--cyber-text-primary, #fff); margin: 0 0 0.5rem 0;' }, _('CrowdSec Metrics')), + E('p', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0); margin: 0;' }, _('Detailed metrics and statistics from CrowdSec engine')) + ]), + // Metrics Configuration this.renderMetricsConfig(metricsConfig), // Hub Stats - E('div', { 'style': 'margin-bottom: 24px' }, [ - E('h3', { 'style': 'color: var(--cs-text-primary); margin-bottom: 16px; font-size: 16px' }, - '🎯 Hub Components'), - this.renderHubStats() + E('div', { 'class': 'cyber-card', 'style': 'margin-bottom: 1.5rem;' }, [ + E('div', { 'class': 'cyber-card-header' }, [ + E('div', { 'class': 'cyber-card-title' }, [ + E('span', { 'style': 'margin-right: 0.5rem;' }, '🎯'), + _('Hub Components') + ]) + ]), + E('div', { 'class': 'cyber-card-body' }, this.renderHubStats()) ]), - + // Grid of cards - E('div', { 'class': 'cs-metrics-grid' }, [ + E('div', { 'class': 'cyber-card-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem;' }, [ // Bouncers - E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, '🔒 Registered Bouncers') + E('div', { 'class': 'cyber-card' }, [ + E('div', { 'class': 'cyber-card-header' }, [ + E('div', { 'class': 'cyber-card-title' }, [ + E('span', { 'style': 'margin-right: 0.5rem;' }, '🔒'), + _('Registered Bouncers') + ]) ]), - E('div', { 'class': 'cs-card-body no-padding' }, this.renderBouncersTable()) + E('div', { 'class': 'cyber-card-body' }, this.renderBouncersTable()) ]), - + // Machines - E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, '🖥️ Registered Machines') + E('div', { 'class': 'cyber-card' }, [ + E('div', { 'class': 'cyber-card-header' }, [ + E('div', { 'class': 'cyber-card-title' }, [ + E('span', { 'style': 'margin-right: 0.5rem;' }, '🖥️'), + _('Registered Machines') + ]) ]), - E('div', { 'class': 'cs-card-body no-padding' }, this.renderMachinesTable()) + E('div', { 'class': 'cyber-card-body' }, this.renderMachinesTable()) ]), - + // Collections - E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, '📦 Installed Collections') + E('div', { 'class': 'cyber-card' }, [ + E('div', { 'class': 'cyber-card-header' }, [ + E('div', { 'class': 'cyber-card-title' }, [ + E('span', { 'style': 'margin-right: 0.5rem;' }, '📦'), + _('Installed Collections') + ]) ]), - E('div', { 'class': 'cs-card-body' }, this.renderCollectionsList()) + E('div', { 'class': 'cyber-card-body' }, this.renderCollectionsList()) ]), - + // Acquisition - E('div', { 'class': 'cs-card' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, '📊 Acquisition Sources') + E('div', { 'class': 'cyber-card' }, [ + E('div', { 'class': 'cyber-card-header' }, [ + E('div', { 'class': 'cyber-card-title' }, [ + E('span', { 'style': 'margin-right: 0.5rem;' }, '📊'), + _('Acquisition Sources') + ]) ]), - E('div', { 'class': 'cs-card-body' }, this.renderAcquisitionMetrics()) + E('div', { 'class': 'cyber-card-body' }, this.renderAcquisitionMetrics()) ]) ]), - + // Raw metrics sections - E('div', { 'class': 'cs-card', 'style': 'margin-top: 24px' }, [ - E('div', { 'class': 'cs-card-header' }, [ - E('div', { 'class': 'cs-card-title' }, '📈 Raw Prometheus Metrics') + E('div', { 'class': 'cyber-card', 'style': 'margin-top: 1.5rem;' }, [ + E('div', { 'class': 'cyber-card-header' }, [ + E('div', { 'class': 'cyber-card-title' }, [ + E('span', { 'style': 'margin-right: 0.5rem;' }, '📈'), + _('Raw Prometheus Metrics') + ]) ]), - E('div', { 'class': 'cs-card-body' }, [ - E('div', { 'class': 'cs-metrics-grid' }, [ - this.renderMetricSection('Parsers', this.metrics.parsers), - this.renderMetricSection('Scenarios', this.metrics.scenarios), - this.renderMetricSection('Buckets', this.metrics.buckets), - this.renderMetricSection('LAPI', this.metrics.lapi), - this.renderMetricSection('Decisions', this.metrics.decisions) + E('div', { 'class': 'cyber-card-body' }, [ + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;' }, [ + this.renderMetricSection(_('Parsers'), this.metrics.parsers), + this.renderMetricSection(_('Scenarios'), this.metrics.scenarios), + this.renderMetricSection(_('Buckets'), this.metrics.buckets), + this.renderMetricSection(_('LAPI'), this.metrics.lapi), + this.renderMetricSection(_('Decisions'), this.metrics.decisions) ].filter(Boolean)) ]) ]) ]); - + // Setup polling (every 60 seconds for metrics) poll.add(function() { return Promise.all([ @@ -392,10 +430,9 @@ return view.extend({ self.metrics = results[0]; self.bouncers = results[1]; self.machines = results[2]; - // Note: Could update view here if needed }); }, 60); - + return view; }, diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js index a38ad8df..31ac5554 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js @@ -1,5 +1,6 @@ 'use strict'; 'require view'; +'require secubox-theme/theme as Theme'; 'require ui'; 'require form'; 'require rpc'; @@ -14,6 +15,8 @@ return view.extend({ // Step 1 data crowdsecRunning: false, lapiAvailable: false, + lapiRepairing: false, + lapiRepairAttempted: false, // Step 2 data hubUpdating: false, @@ -56,6 +59,39 @@ return view.extend({ this.wizardData.crowdsecRunning = status && status.crowdsec === 'running'; this.wizardData.lapiAvailable = status && status.lapi_status === 'available'; + // Auto-repair LAPI if CrowdSec is running but LAPI is not available + if (this.wizardData.crowdsecRunning && !this.wizardData.lapiAvailable && !this.wizardData.lapiRepairAttempted) { + console.log('[Wizard] LAPI unavailable, triggering auto-repair...'); + this.wizardData.lapiRepairing = true; + this.wizardData.lapiRepairAttempted = true; + + return API.repairLapi().then(L.bind(function(repairResult) { + console.log('[Wizard] LAPI repair result:', repairResult); + this.wizardData.lapiRepairing = false; + + if (repairResult && repairResult.success) { + ui.addNotification(null, E('p', _('LAPI auto-repaired successfully')), 'success'); + // Re-fetch status after repair + return API.getStatus().then(L.bind(function(newStatus) { + this.wizardData.crowdsecRunning = newStatus && newStatus.crowdsec === 'running'; + this.wizardData.lapiAvailable = newStatus && newStatus.lapi_status === 'available'; + return { + status: newStatus, + wizardNeeded: wizardNeeded, + repaired: true + }; + }, this)); + } else { + console.log('[Wizard] LAPI repair failed:', repairResult); + return { + status: status, + wizardNeeded: wizardNeeded, + repairFailed: true + }; + } + }, this)); + } + return { status: status, wizardNeeded: wizardNeeded @@ -142,7 +178,16 @@ return view.extend({ console.log('[Wizard] status:', status); var crowdsecRunning = status && status.crowdsec === 'running'; var lapiAvailable = status && status.lapi_status === 'available'; - console.log('[Wizard] crowdsecRunning:', crowdsecRunning, 'lapiAvailable:', lapiAvailable); + var lapiRepairing = this.wizardData.lapiRepairing; + var repaired = data && data.repaired; + var repairFailed = data && data.repairFailed; + console.log('[Wizard] crowdsecRunning:', crowdsecRunning, 'lapiAvailable:', lapiAvailable, 'repairing:', lapiRepairing); + + // Determine LAPI status display + var lapiStatusText = lapiAvailable ? _('AVAILABLE') : (lapiRepairing ? _('REPAIRING...') : _('UNAVAILABLE')); + var lapiStatusClass = lapiAvailable ? 'success' : (lapiRepairing ? 'warning' : 'error'); + var lapiIconClass = lapiAvailable ? ' success' : (lapiRepairing ? ' warning' : ' error'); + var lapiIcon = lapiAvailable ? '✓' : (lapiRepairing ? '⟳' : '✗'); return E('div', { 'class': 'wizard-step' }, [ E('h2', {}, _('Welcome to CrowdSec Setup')), @@ -158,14 +203,31 @@ return view.extend({ crowdsecRunning ? _('RUNNING') : _('STOPPED')) ]), E('div', { 'class': 'check-item' }, [ - E('span', { 'class': 'check-icon' + (lapiAvailable ? ' success' : ' error') }, - lapiAvailable ? '✓' : '✗'), + E('span', { 'class': 'check-icon' + lapiIconClass + (lapiRepairing ? ' spinning' : '') }, + lapiIcon), E('span', {}, _('Local API (LAPI)')), - E('span', { 'class': 'badge badge-' + (lapiAvailable ? 'success' : 'error') }, - lapiAvailable ? _('AVAILABLE') : _('UNAVAILABLE')) + E('span', { 'class': 'badge badge-' + lapiStatusClass }, + lapiStatusText) ]) ]), + // Repair status message + repaired ? E('div', { 'class': 'success-message', 'style': 'margin: 16px 0; padding: 12px; background: rgba(34, 197, 94, 0.15); border-radius: 8px; color: #16a34a;' }, [ + E('span', { 'style': 'margin-right: 8px;' }, '✓'), + _('LAPI was automatically repaired!') + ]) : E([]), + + // Manual repair button if auto-repair failed + (repairFailed || (!lapiAvailable && !lapiRepairing && this.wizardData.lapiRepairAttempted)) ? + E('div', { 'style': 'margin: 16px 0; padding: 16px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; border: 1px solid rgba(239, 68, 68, 0.3);' }, [ + E('p', { 'style': 'margin: 0 0 12px 0; color: #dc2626;' }, + _('LAPI auto-repair failed. You can try manual repair or check the CrowdSec logs.')), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': L.bind(this.handleManualRepair, this) + }, _('🔧 Retry Repair')) + ]) : E([]), + // Info box E('div', { 'class': 'info-box' }, [ E('h4', {}, _('What will be configured:')), @@ -188,7 +250,7 @@ return view.extend({ }, _('Cancel')), E('button', { 'class': 'cbi-button cbi-button-positive', - 'disabled': (!crowdsecRunning || !lapiAvailable) ? true : null, + 'disabled': (!crowdsecRunning || !lapiAvailable || lapiRepairing) ? true : null, 'click': L.bind(function(ev) { console.log('[Wizard] Next button clicked!'); ev.preventDefault(); @@ -780,6 +842,37 @@ return view.extend({ }, this)); }, + handleManualRepair: function() { + console.log('[Wizard] Manual repair triggered'); + this.wizardData.lapiRepairing = true; + this.wizardData.lapiRepairAttempted = false; // Reset to allow retry + this.refreshView(); + + return API.repairLapi().then(L.bind(function(result) { + console.log('[Wizard] Manual repair result:', result); + this.wizardData.lapiRepairing = false; + this.wizardData.lapiRepairAttempted = true; + + if (result && result.success) { + ui.addNotification(null, E('p', _('LAPI repaired successfully: ') + (result.steps || '')), 'success'); + // Re-check status + return API.getStatus().then(L.bind(function(status) { + this.wizardData.crowdsecRunning = status && status.crowdsec === 'running'; + this.wizardData.lapiAvailable = status && status.lapi_status === 'available'; + this.refreshView(); + }, this)); + } else { + ui.addNotification(null, E('p', _('LAPI repair failed: ') + (result.error || result.errors || 'Unknown error')), 'error'); + this.refreshView(); + } + }, this)).catch(L.bind(function(err) { + console.error('[Wizard] Manual repair error:', err); + this.wizardData.lapiRepairing = false; + ui.addNotification(null, E('p', _('LAPI repair failed: ') + err.message), 'error'); + this.refreshView(); + }, this)); + }, + handleSaveAndApply: null, handleSave: null, handleReset: null diff --git a/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard b/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard index c224b72c..b90fe2f4 100755 --- a/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard +++ b/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard @@ -430,18 +430,30 @@ register_bouncer() { return fi - # Generate API key + # Check if bouncer already exists + local exists=0 + if $CSCLI bouncers list -o json 2>/dev/null | grep -q "\"name\":\"$bouncer_name\""; then + exists=1 + fi + local api_key + if [ "$exists" = "1" ]; then + # Delete existing bouncer and re-register to get new API key + $CSCLI bouncers delete "$bouncer_name" >/dev/null 2>&1 + secubox_log "Deleted existing bouncer: $bouncer_name" + fi + + # Generate API key api_key=$($CSCLI bouncers add "$bouncer_name" -o raw 2>&1) - if [ -n "$api_key" ] && [ "${#api_key}" -gt 10 ]; then + if [ -n "$api_key" ] && [ "${#api_key}" -gt 10 ] && ! echo "$api_key" | grep -qi "error"; then json_add_boolean "success" 1 json_add_string "api_key" "$api_key" json_add_string "message" "Bouncer '$bouncer_name' registered successfully" secubox_log "Registered bouncer: $bouncer_name" else json_add_boolean "success" 0 - json_add_string "error" "Failed to register bouncer '$bouncer_name'" + json_add_string "error" "Failed to register bouncer '$bouncer_name': $api_key" fi json_dump @@ -681,9 +693,9 @@ update_firewall_bouncer_config() { json_dump return fi - uci set "crowdsec.bouncer.$key=$value" + uci set "crowdsec.@bouncer[0].$key=$value" ;; - api_url|update_frequency|deny_action|log_level) + api_url|update_frequency|deny_action|log_level|api_key) # String values if [ -z "$value" ]; then json_add_boolean "success" 0 @@ -691,7 +703,7 @@ update_firewall_bouncer_config() { json_dump return fi - uci set "crowdsec.bouncer.$key=$value" + uci set "crowdsec.@bouncer[0].$key=$value" ;; *) json_add_boolean "success" 0 @@ -830,10 +842,99 @@ get_wizard_state() { json_dump } +# Repair LAPI - auto-fix common configuration issues +repair_lapi() { + json_init + local steps_done="" + local errors="" + + secubox_log "Starting LAPI repair..." + + # Step 1: Create data directory + if [ ! -d "/srv/crowdsec/data" ]; then + mkdir -p /srv/crowdsec/data 2>/dev/null + if [ -d "/srv/crowdsec/data" ]; then + steps_done="${steps_done}Created /srv/crowdsec/data; " + else + errors="${errors}Failed to create data directory; " + fi + fi + + # Step 2: Fix config.yaml - ensure data_dir and db_path are set + local config_file="/etc/crowdsec/config.yaml" + if [ -f "$config_file" ]; then + # Check if data_dir is empty or missing + local current_data_dir=$(grep "^ data_dir:" "$config_file" | awk '{print $2}') + if [ -z "$current_data_dir" ] || [ "$current_data_dir" = "" ]; then + sed -i 's|^ data_dir:.*| data_dir: /srv/crowdsec/data/|' "$config_file" + steps_done="${steps_done}Fixed data_dir in config; " + fi + + # Check if db_path is empty or missing + local current_db_path=$(grep "^ db_path:" "$config_file" | awk '{print $2}') + if [ -z "$current_db_path" ] || [ "$current_db_path" = "" ]; then + sed -i 's|^ db_path:.*| db_path: /srv/crowdsec/data/crowdsec.db|' "$config_file" + steps_done="${steps_done}Fixed db_path in config; " + fi + else + errors="${errors}Config file not found; " + fi + + # Step 3: Restart CrowdSec to apply config changes + if /etc/init.d/crowdsec restart >/dev/null 2>&1; then + steps_done="${steps_done}Restarted CrowdSec; " + sleep 2 + else + errors="${errors}Failed to restart CrowdSec; " + fi + + # Step 4: Re-register local machine if needed + if [ -x "$CSCLI" ]; then + # Check if machine is registered and working + if ! $CSCLI machines list >/dev/null 2>&1; then + # Force re-register + if $CSCLI machines add localhost --auto --force >/dev/null 2>&1; then + steps_done="${steps_done}Re-registered localhost machine; " + # Restart again to apply new credentials + /etc/init.d/crowdsec restart >/dev/null 2>&1 + sleep 2 + else + errors="${errors}Failed to register machine; " + fi + fi + fi + + # Step 5: Verify LAPI is now working + local lapi_ok=0 + if [ -x "$CSCLI" ]; then + if $CSCLI lapi status >/dev/null 2>&1; then + lapi_ok=1 + steps_done="${steps_done}LAPI verified working" + else + errors="${errors}LAPI still not responding" + fi + fi + + if [ "$lapi_ok" = "1" ]; then + json_add_boolean "success" 1 + json_add_string "message" "LAPI repaired successfully" + json_add_string "steps" "$steps_done" + secubox_log "LAPI repair completed: $steps_done" + else + json_add_boolean "success" 0 + json_add_string "error" "LAPI repair failed" + json_add_string "steps" "$steps_done" + json_add_string "errors" "$errors" + secubox_log "LAPI repair failed: $errors" + fi + + json_dump +} + # Main dispatcher case "$1" in list) - echo '{"decisions":{},"alerts":{"limit":"number"},"metrics":{},"bouncers":{},"machines":{},"hub":{},"status":{},"ban":{"ip":"string","duration":"string","reason":"string"},"unban":{"ip":"string"},"stats":{},"seccubox_logs":{},"collect_debug":{},"waf_status":{},"metrics_config":{},"configure_metrics":{"enable":"string"},"collections":{},"install_collection":{"collection":"string"},"remove_collection":{"collection":"string"},"update_hub":{},"register_bouncer":{"bouncer_name":"string"},"delete_bouncer":{"bouncer_name":"string"},"firewall_bouncer_status":{},"control_firewall_bouncer":{"action":"string"},"firewall_bouncer_config":{},"update_firewall_bouncer_config":{"key":"string","value":"string"},"nftables_stats":{},"check_wizard_needed":{},"wizard_state":{}}' + echo '{"decisions":{},"alerts":{"limit":"number"},"metrics":{},"bouncers":{},"machines":{},"hub":{},"status":{},"ban":{"ip":"string","duration":"string","reason":"string"},"unban":{"ip":"string"},"stats":{},"seccubox_logs":{},"collect_debug":{},"waf_status":{},"metrics_config":{},"configure_metrics":{"enable":"string"},"collections":{},"install_collection":{"collection":"string"},"remove_collection":{"collection":"string"},"update_hub":{},"register_bouncer":{"bouncer_name":"string"},"delete_bouncer":{"bouncer_name":"string"},"firewall_bouncer_status":{},"control_firewall_bouncer":{"action":"string"},"firewall_bouncer_config":{},"update_firewall_bouncer_config":{"key":"string","value":"string"},"nftables_stats":{},"check_wizard_needed":{},"wizard_state":{},"repair_lapi":{}}' ;; call) case "$2" in @@ -944,6 +1045,9 @@ case "$1" in wizard_state) get_wizard_state ;; + repair_lapi) + repair_lapi + ;; *) echo '{"error": "Unknown method"}' ;; diff --git a/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json b/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json index 53246b36..4aa17d04 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json +++ b/package/secubox/luci-app-crowdsec-dashboard/root/usr/share/rpcd/acl.d/luci-app-crowdsec-dashboard.json @@ -39,7 +39,8 @@ "register_bouncer", "delete_bouncer", "control_firewall_bouncer", - "update_firewall_bouncer_config" + "update_firewall_bouncer_config", + "repair_lapi" ] }, "uci": [ "crowdsec-dashboard" ]