secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js
CyberMind-FR e62919eec7 refactor(packages): Rename and reorganize SecuBox packages
- Rename crowdsec-firewall-bouncer to secubox-app-cs-firewall-bouncer
- Rename secubox-auth-logger to secubox-app-auth-logger
- Delete secubox-crowdsec-setup (merged into other packages)
- Fix circular dependencies in luci-app-secubox-crowdsec
- Fix dependency chain in secubox-app-crowdsec-bouncer
- Add consolidated get_overview API to crowdsec-dashboard
- Improve crowdsec-dashboard overview performance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:42:52 +01:00

1088 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require poll';
'require ui';
'require fs';
'require crowdsec-dashboard/api as api';
'require crowdsec-dashboard/nav as CsNav';
'require secubox-portal/header as SbHeader';
/**
* CrowdSec Dashboard - Overview View
* Main dashboard with stats, charts, and recent activity
* Copyright (C) 2024 CyberMind.fr - Gandalf
*/
return view.extend({
title: _('CrowdSec Dashboard'),
css: null,
data: null,
csApi: null,
load: function() {
// Load CSS
var cssLink = document.createElement('link');
cssLink.rel = 'stylesheet';
cssLink.href = L.resource('crowdsec-dashboard/dashboard.css');
document.head.appendChild(cssLink);
// Load API
this.csApi = api;
// Use consolidated API call + secondary calls for extended data
return Promise.all([
this.csApi.getOverview().catch(function() { return {}; }),
this.csApi.getCapiMetrics().catch(function() { return {}; }),
this.csApi.getCollections().catch(function() { return { collections: [] }; }),
this.csApi.getNftablesStats().catch(function() { return {}; }),
this.csApi.getHub().catch(function() { return {}; })
]).then(this.transformOverviewData.bind(this));
},
// Transform getOverview response to expected data structure
transformOverviewData: function(results) {
var overview = results[0] || {};
var capiMetrics = results[1] || {};
var collectionsData = results[2] || {};
var nftablesStats = results[3] || {};
var hubData = results[4] || {};
// Parse raw JSON strings for scenarios and countries
var topScenarios = [];
var topCountries = [];
try {
if (overview.top_scenarios_raw) {
topScenarios = JSON.parse(overview.top_scenarios_raw);
}
} catch(e) { topScenarios = []; }
try {
if (overview.top_countries_raw) {
topCountries = JSON.parse(overview.top_countries_raw);
}
} catch(e) { topCountries = []; }
// Build compatible data structure
var dashboardData = {
status: {
crowdsec: overview.crowdsec || 'unknown',
bouncer: overview.bouncer || 'unknown',
version: overview.version || 'unknown'
},
stats: {
total_decisions: overview.total_decisions || 0,
alerts_today: overview.alerts_24h || 0,
alerts_week: overview.alerts_24h || 0,
scenarios_triggered: topScenarios.length,
top_countries: topCountries.map(function(c) {
return { country: c.country, count: c.count };
})
},
decisions: overview.decisions || [],
alerts: overview.alerts || [],
error: null
};
var logsData = {
entries: overview.logs || []
};
var healthCheck = {
crowdsec_running: overview.crowdsec === 'running',
lapi_status: overview.lapi_status || 'unavailable',
capi_status: overview.capi_enrolled ? 'connected' : 'disconnected',
capi_enrolled: overview.capi_enrolled || false,
capi_subscription: 'COMMUNITY',
sharing_signals: overview.capi_enrolled || false,
pulling_blocklist: overview.capi_enrolled || false,
version: overview.version || 'N/A',
decisions_count: overview.total_decisions || 0
};
// Return array matching expected payload structure
return [
dashboardData,
logsData,
healthCheck,
capiMetrics,
collectionsData,
nftablesStats,
hubData
];
},
renderHeader: function(status) {
var header = E('div', { 'class': 'cs-header' }, [
E('div', { 'class': 'cs-logo' }, [
E('div', { 'class': 'cs-logo-icon' }, '🛡️'),
E('div', { 'class': 'cs-logo-text' }, [
'Crowd',
E('span', {}, 'Sec'),
' Dashboard'
])
]),
E('div', { 'class': 'cs-status-badges' }, [
E('div', { 'class': 'cs-badge' }, [
E('span', {
'class': 'cs-badge-dot ' + (status.crowdsec === 'running' ? 'running' : 'stopped')
}),
'Engine: ' + (status.crowdsec || 'unknown')
]),
E('div', { 'class': 'cs-badge' }, [
E('span', {
'class': 'cs-badge-dot ' + (status.bouncer === 'running' ? 'running' : 'stopped')
}),
'Bouncer: ' + (status.bouncer || 'unknown')
]),
E('div', { 'class': 'cs-badge' }, [
'v' + (status.version || 'N/A')
])
])
]);
return header;
},
renderStatsGrid: function(stats, decisions) {
var self = this;
// Count by action type
var banCount = 0;
var captchaCount = 0;
if (Array.isArray(decisions)) {
decisions.forEach(function(d) {
if (d.type === 'ban') banCount++;
else if (d.type === 'captcha') captchaCount++;
});
}
var grid = E('div', { 'class': 'cs-stats-grid' }, [
E('div', { 'class': 'cs-stat-card' }, [
E('div', { 'class': 'cs-stat-label' }, 'Active Bans'),
E('div', { 'class': 'cs-stat-value danger' }, String(stats.total_decisions || 0)),
E('div', { 'class': 'cs-stat-trend' }, 'Currently blocked IPs'),
E('div', { 'class': 'cs-stat-icon' }, '🚫')
]),
E('div', { 'class': 'cs-stat-card' }, [
E('div', { 'class': 'cs-stat-label' }, 'Alerts (24h)'),
E('div', { 'class': 'cs-stat-value warning' }, String(stats.alerts_24h || 0)),
E('div', { 'class': 'cs-stat-trend' }, 'Detected threats'),
E('div', { 'class': 'cs-stat-icon' }, '⚠️')
]),
E('div', { 'class': 'cs-stat-card' }, [
E('div', { 'class': 'cs-stat-label' }, 'Bouncers'),
E('div', { 'class': 'cs-stat-value success' }, String(stats.bouncers || 0)),
E('div', { 'class': 'cs-stat-trend' }, 'Active remediation'),
E('div', { 'class': 'cs-stat-icon' }, '🔒')
]),
E('div', { 'class': 'cs-stat-card' }, [
E('div', { 'class': 'cs-stat-label' }, 'Ban Rate'),
E('div', { 'class': 'cs-stat-value' }, banCount > 0 ? '100%' : '0%'),
E('div', { 'class': 'cs-stat-trend' }, banCount + ' bans / ' + captchaCount + ' captchas'),
E('div', { 'class': 'cs-stat-icon' }, '📊')
])
]);
return grid;
},
renderDecisionsTable: function(decisions) {
var self = this;
if (!Array.isArray(decisions) || decisions.length === 0) {
return E('div', { 'class': 'cs-empty' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('div', { 'class': 'cs-empty-icon' }, '✅'),
E('p', {}, 'No active decisions - All clear!')
]);
}
var rows = decisions.slice(0, 10).map(function(d) {
return E('tr', {}, [
E('td', {}, E('span', { 'class': 'cs-ip' }, d.value || 'N/A')),
E('td', {}, E('span', { 'class': 'cs-scenario' }, self.csApi.parseScenario(d.scenario))),
E('td', {}, E('span', { 'class': 'cs-country' }, [
E('span', { 'class': 'cs-country-flag' }, self.csApi.getCountryFlag(d.country)),
d.country || 'N/A'
])),
E('td', {}, E('span', { 'class': 'cs-action ' + (d.type || 'ban') }, d.type || 'ban')),
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatDuration(d.duration))),
E('td', {}, E('button', {
'class': 'cs-btn cs-btn-danger cs-btn-sm',
'data-ip': d.value,
'click': ui.createHandlerFn(self, 'handleUnban', d.value)
}, 'Unban'))
]);
});
return E('table', { 'class': 'cs-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, 'IP Address'),
E('th', {}, 'Scenario'),
E('th', {}, 'Country'),
E('th', {}, 'Action'),
E('th', {}, 'Expires'),
E('th', {}, 'Actions')
])),
E('tbody', {}, rows)
]);
},
renderAlertsTimeline: function(alerts) {
var self = this;
if (!Array.isArray(alerts) || alerts.length === 0) {
return E('div', { 'class': 'cs-empty' }, [
E('div', { 'class': 'cs-empty-icon' }, '📭'),
E('p', {}, 'No recent alerts')
]);
}
var items = alerts.slice(0, 8).map(function(a) {
return E('div', { 'class': 'cs-timeline-item alert' }, [
E('div', { 'class': 'cs-timeline-time' }, self.csApi.formatRelativeTime(a.created_at)),
E('div', { 'class': 'cs-timeline-content' }, [
E('strong', {}, self.csApi.parseScenario(a.scenario)),
E('br', {}),
E('span', { 'class': 'cs-ip' }, a.source?.ip || 'N/A'),
' → ',
E('span', {}, (a.events_count || 0) + ' events')
])
]);
});
return E('div', { 'class': 'cs-timeline' }, items);
},
renderTopScenarios: function(stats) {
var scenarios = [];
try {
if (stats.top_scenarios_raw) {
scenarios = JSON.parse(stats.top_scenarios_raw);
}
} catch(e) {
scenarios = [];
}
if (scenarios.length === 0) {
return E('div', { 'class': 'cs-empty' }, [
E('p', {}, 'No scenario data available')
]);
}
var maxCount = Math.max.apply(null, scenarios.map(function(s) { return s.count; }));
var bars = scenarios.map(function(s) {
var pct = maxCount > 0 ? (s.count / maxCount * 100) : 0;
return E('div', { 'class': 'cs-bar-item' }, [
E('div', { 'class': 'cs-bar-label', 'title': s.scenario }, s.scenario.split('/').pop()),
E('div', { 'class': 'cs-bar-track' }, [
E('div', { 'class': 'cs-bar-fill', 'style': 'width: ' + pct + '%' })
]),
E('div', { 'class': 'cs-bar-value' }, String(s.count))
]);
});
return E('div', { 'class': 'cs-bar-chart' }, bars);
},
renderTopCountries: function(stats) {
var self = this;
var countries = [];
try {
if (stats.top_countries_raw) {
countries = JSON.parse(stats.top_countries_raw);
}
} catch(e) {
countries = [];
}
if (countries.length === 0) {
return E('div', { 'class': 'cs-empty' }, [
E('p', {}, 'No country data available')
]);
}
var maxCount = Math.max.apply(null, countries.map(function(c) { return c.count; }));
var bars = countries.map(function(c) {
var pct = maxCount > 0 ? (c.count / maxCount * 100) : 0;
return E('div', { 'class': 'cs-bar-item' }, [
E('div', { 'class': 'cs-bar-label' }, [
self.csApi.getCountryFlag(c.country),
' ',
c.country || 'N/A'
]),
E('div', { 'class': 'cs-bar-track' }, [
E('div', { 'class': 'cs-bar-fill', 'style': 'width: ' + pct + '%' })
]),
E('div', { 'class': 'cs-bar-value' }, String(c.count))
]);
});
return E('div', { 'class': 'cs-bar-chart' }, bars);
},
renderBanModal: function() {
return E('div', { 'class': 'cs-modal-overlay', 'id': 'ban-modal', 'style': 'display: none' }, [
E('div', { 'class': 'cs-modal' }, [
E('div', { 'class': 'cs-modal-header' }, [
E('div', { 'class': 'cs-modal-title' }, 'Add IP Ban'),
E('button', {
'class': 'cs-modal-close',
'click': ui.createHandlerFn(this, 'closeBanModal')
}, '×')
]),
E('div', { 'class': 'cs-modal-body' }, [
E('div', { 'class': 'cs-form-group' }, [
E('label', { 'class': 'cs-form-label' }, 'IP Address'),
E('input', {
'class': 'cs-input',
'id': 'ban-ip',
'type': 'text',
'placeholder': '192.168.1.100 or 10.0.0.0/24'
})
]),
E('div', { 'class': 'cs-form-group' }, [
E('label', { 'class': 'cs-form-label' }, 'Duration'),
E('input', {
'class': 'cs-input',
'id': 'ban-duration',
'type': 'text',
'placeholder': '4h',
'value': '4h'
})
]),
E('div', { 'class': 'cs-form-group' }, [
E('label', { 'class': 'cs-form-label' }, 'Reason'),
E('input', {
'class': 'cs-input',
'id': 'ban-reason',
'type': 'text',
'placeholder': 'Manual ban from dashboard'
})
])
]),
E('div', { 'class': 'cs-modal-footer' }, [
E('button', {
'class': 'cs-btn',
'click': ui.createHandlerFn(this, 'closeBanModal')
}, 'Cancel'),
E('button', {
'class': 'cs-btn cs-btn-primary',
'click': ui.createHandlerFn(this, 'submitBan')
}, 'Add Ban')
])
])
]);
},
handleUnban: function(ip, ev) {
var self = this;
if (!confirm('Remove ban for ' + ip + '?')) {
return;
}
this.csApi.unbanIP(ip).then(function(result) {
if (result.success) {
self.showToast('IP ' + ip + ' unbanned successfully', 'success');
// Refresh data
return self.refreshDashboard();
} else {
self.showToast('Failed to unban: ' + (result.error || 'Unknown error'), 'error');
}
}).then(function(data) {
if (data) {
self.data = data;
self.updateView();
}
}).catch(function(err) {
self.showToast('Error: ' + err.message, 'error');
});
},
openBanModal: function(ev) {
document.getElementById('ban-modal').style.display = 'flex';
},
closeBanModal: function(ev) {
document.getElementById('ban-modal').style.display = 'none';
document.getElementById('ban-ip').value = '';
document.getElementById('ban-duration').value = '4h';
document.getElementById('ban-reason').value = '';
},
submitBan: function(ev) {
var self = this;
var ip = document.getElementById('ban-ip').value.trim();
var duration = document.getElementById('ban-duration').value.trim() || '4h';
var reason = document.getElementById('ban-reason').value.trim() || 'Manual ban from dashboard';
if (!ip) {
self.showToast('Please enter an IP address', 'error');
return;
}
if (!self.csApi.isValidIP(ip)) {
self.showToast('Invalid IP address format', 'error');
return;
}
self.csApi.banIP(ip, duration, reason).then(function(result) {
if (result.success) {
self.showToast('IP ' + ip + ' banned for ' + duration, 'success');
self.closeBanModal();
return self.refreshDashboard();
} else {
self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error');
}
}).then(function(data) {
if (data) {
self.data = data;
self.updateView();
}
}).catch(function(err) {
self.showToast('Error: ' + err.message, 'error');
});
},
showToast: function(message, type) {
var existing = document.querySelector('.cs-toast');
if (existing) existing.remove();
var toast = E('div', { 'class': 'cs-toast ' + (type || '') }, message);
document.body.appendChild(toast);
setTimeout(function() {
toast.remove();
}, 4000);
},
updateView: function() {
var container = document.getElementById('cs-dashboard-content');
if (!container || !this.data) return;
dom.content(container, this.renderContent(this.data));
},
renderContent: function(data) {
var status = data.status || {};
var stats = data.stats || {};
var decisions = data.decisions || [];
var alerts = data.alerts || [];
var logs = this.logs || [];
// Check if service is not running
var serviceWarning = null;
if (data.error && status.crowdsec !== 'running') {
serviceWarning = E('div', { 'class': 'cs-warning-banner' }, [
E('div', { 'class': 'cs-warning-icon' }, '⚠️'),
E('div', { 'class': 'cs-warning-content' }, [
E('div', { 'class': 'cs-warning-title' }, 'CrowdSec Service Not Running'),
E('div', { 'class': 'cs-warning-message' }, [
'The CrowdSec engine is currently stopped. ',
E('a', {
'href': '#',
'click': ui.createHandlerFn(this, 'startCrowdSec')
}, 'Click here to start the service'),
' or use the command: ',
E('code', {}, '/etc/init.d/crowdsec start')
])
])
]);
}
return E('div', {}, [
this.renderHeader(status),
serviceWarning || E([]),
this.renderHealthCheck(),
this.renderStatsGrid(stats, decisions),
E('div', { 'class': 'cs-charts-row' }, [
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, 'Top Scenarios'),
]),
E('div', { 'class': 'cs-card-body' }, this.renderTopScenarios(stats))
]),
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, 'Top Countries'),
]),
E('div', { 'class': 'cs-card-body' }, this.renderTopCountries(stats))
])
]),
E('div', { 'class': 'cs-charts-row' }, [
this.renderCapiBlocklist(),
this.renderCollectionsCard(),
this.renderHubStatsCard()
]),
E('div', { 'class': 'cs-charts-row' }, [
this.renderFirewallHealth(),
this.renderFirewallBlocks()
]),
E('div', { 'class': 'cs-charts-row' }, [
E('div', { 'class': 'cs-card', 'style': 'flex: 2' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, 'Active Decisions'),
E('button', {
'class': 'cs-btn cs-btn-primary cs-btn-sm',
'click': ui.createHandlerFn(this, 'openBanModal')
}, '+ Add Ban')
]),
E('div', { 'class': 'cs-card-body no-padding' }, this.renderDecisionsTable(decisions))
]),
E('div', { 'class': 'cs-card', 'style': 'flex: 1' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, 'Recent Alerts'),
]),
E('div', { 'class': 'cs-card-body' }, this.renderAlertsTimeline(alerts))
]),
this.renderLogCard(logs)
]),
this.renderBanModal()
]);
},
startCrowdSec: function(ev) {
var self = this;
ev.preventDefault();
ui.showModal(_('Start CrowdSec'), [
E('p', {}, _('Do you want to start the CrowdSec service?')),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button-positive',
'click': function() {
return fs.exec('/etc/init.d/crowdsec', ['start']).then(function() {
ui.hideModal();
self.showToast('CrowdSec service started', 'success');
setTimeout(function() {
return self.refreshDashboard();
}, 2000);
}).catch(function(err) {
ui.hideModal();
self.showToast('Failed to start service: ' + err, 'error');
});
}
}, _('Start Service'))
])
]);
},
render: function(payload) {
var self = this;
this.data = payload[0] || {};
this.logs = (payload[1] && payload[1].entries) || [];
this.healthCheck = payload[2] || {};
this.capiMetrics = payload[3] || {};
this.collections = (payload[4] && payload[4].collections) || [];
this.nftablesStats = payload[5] || {};
this.hubData = payload[6] || {};
// Main wrapper with SecuBox header
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
wrapper.appendChild(SbHeader.render());
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
CsNav.renderTabs('overview'),
E('div', { 'id': 'cs-dashboard-content' }, this.renderContent(this.data))
]);
wrapper.appendChild(view);
// Setup polling for auto-refresh (every 60 seconds)
poll.add(function() {
return self.refreshDashboard();
}, 60);
return wrapper;
},
refreshDashboard: function() {
var self = this;
// Use consolidated API call + secondary calls for extended data
return Promise.all([
self.csApi.getOverview().catch(function() { return {}; }),
self.csApi.getCapiMetrics().catch(function() { return {}; }),
self.csApi.getCollections().catch(function() { return { collections: [] }; }),
self.csApi.getNftablesStats().catch(function() { return {}; }),
self.csApi.getHub().catch(function() { return {}; })
]).then(function(results) {
var transformed = self.transformOverviewData(results);
self.data = transformed[0];
self.logs = (transformed[1] && transformed[1].entries) || [];
self.healthCheck = transformed[2] || {};
self.capiMetrics = transformed[3] || {};
self.collections = (transformed[4] && transformed[4].collections) || [];
self.nftablesStats = transformed[5] || {};
self.hubData = transformed[6] || {};
self.updateView();
});
},
// Health Check Section - Shows LAPI/CAPI/Console status
renderHealthCheck: function() {
var health = this.healthCheck || {};
var csRunning = health.crowdsec_running;
var lapiStatus = health.lapi_status || 'unavailable';
var capiStatus = health.capi_status || 'disconnected';
var capiEnrolled = health.capi_enrolled;
var capiSubscription = health.capi_subscription || '-';
var sharingSignals = health.sharing_signals;
var pullingBlocklist = health.pulling_blocklist;
var version = health.version || 'N/A';
var decisionsCount = health.decisions_count || 0;
return E('div', { 'class': 'cs-health-check', 'style': 'margin-bottom: 1.5em;' }, [
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, _('System Health'))
]),
E('div', { 'class': 'cs-card-body' }, [
E('div', { 'class': 'cs-health-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1em;' }, [
// CrowdSec Status
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, csRunning ? '✅' : '❌'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'CrowdSec'),
E('div', { 'style': 'font-size: 0.85em; color: ' + (csRunning ? '#00d4aa' : '#ff4757') + ';' }, csRunning ? 'Running' : 'Stopped'),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, version ? (version.charAt(0) === 'v' ? version : 'v' + version) : '')
]),
// LAPI Status
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, lapiStatus === 'available' ? '✅' : '❌'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'LAPI'),
E('div', { 'style': 'font-size: 0.85em; color: ' + (lapiStatus === 'available' ? '#00d4aa' : '#ff4757') + ';' }, lapiStatus === 'available' ? 'Available' : 'Unavailable'),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, ':8080')
]),
// CAPI Status
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, capiStatus === 'connected' ? '✅' : '⚠️'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'CAPI'),
E('div', { 'style': 'font-size: 0.85em; color: ' + (capiStatus === 'connected' ? '#00d4aa' : '#ffa500') + ';' }, capiStatus === 'connected' ? 'Connected' : 'Disconnected'),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, capiSubscription)
]),
// Console Status
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, capiEnrolled ? '✅' : '⚪'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'Console'),
E('div', { 'style': 'font-size: 0.85em; color: ' + (capiEnrolled ? '#00d4aa' : '#888') + ';' }, capiEnrolled ? 'Enrolled' : 'Not Enrolled'),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, sharingSignals ? 'Sharing: ON' : 'Sharing: OFF')
]),
// Blocklist Status
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, pullingBlocklist ? '🛡️' : '⚪'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'Blocklist'),
E('div', { 'style': 'font-size: 0.85em; color: ' + (pullingBlocklist ? '#00d4aa' : '#888') + ';' }, pullingBlocklist ? 'Active' : 'Inactive'),
E('div', { 'style': 'font-size: 0.75em; color: #667eea; font-weight: 600;' }, decisionsCount.toLocaleString() + ' IPs')
])
])
])
])
]);
},
// CAPI Blocklist Metrics - Shows blocked IPs by category
renderCapiBlocklist: function() {
var metrics = this.capiMetrics || {};
var totalCapi = metrics.total_capi || 0;
var totalLocal = metrics.total_local || 0;
var breakdown = metrics.breakdown || [];
if (totalCapi === 0 && totalLocal === 0) {
return E('span'); // Empty if no data
}
// Build breakdown bars
var maxCount = Math.max.apply(null, breakdown.map(function(b) { return b.count || 0; }).concat([1]));
var breakdownBars = breakdown.slice(0, 5).map(function(item) {
var scenario = item.scenario || 'unknown';
var count = item.count || 0;
var pct = Math.round((count / maxCount) * 100);
var displayName = scenario.split('/').pop().replace(/-/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
return E('div', { 'style': 'margin-bottom: 0.75em;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.25em;' }, [
E('span', { 'style': 'font-size: 0.85em;' }, displayName),
E('span', { 'style': 'font-size: 0.85em; font-weight: 600; color: #667eea;' }, count.toLocaleString())
]),
E('div', { 'style': 'height: 8px; background: rgba(102,126,234,0.2); border-radius: 4px; overflow: hidden;' }, [
E('div', { 'style': 'height: 100%; width: ' + pct + '%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px;' })
])
]);
});
return E('div', { 'class': 'cs-capi-blocklist', 'style': 'margin-bottom: 1.5em;' }, [
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, _('Community Blocklist (CAPI)'))
]),
E('div', { 'class': 'cs-card-body' }, [
E('div', { 'style': 'display: flex; gap: 2em; margin-bottom: 1em;' }, [
E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #667eea;' }, totalCapi.toLocaleString()),
E('div', { 'style': 'font-size: 0.8em; color: #888;' }, 'CAPI Blocked')
]),
E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #00d4aa;' }, totalLocal.toLocaleString()),
E('div', { 'style': 'font-size: 0.8em; color: #888;' }, 'Local Blocked')
])
]),
breakdownBars.length > 0 ? E('div', { 'style': 'margin-top: 1em;' }, [
E('div', { 'style': 'font-size: 0.85em; font-weight: 600; margin-bottom: 0.75em; color: #888;' }, _('Top Blocked Categories')),
E('div', {}, breakdownBars)
]) : E('span')
])
])
]);
},
// Collections Card - Shows installed collections with quick actions
renderCollectionsCard: function() {
var self = this;
var collections = this.collections || [];
if (!collections.length) {
return E('span'); // Empty if no collections
}
var collectionItems = collections.slice(0, 6).map(function(col) {
var name = col.name || col.Name || 'unknown';
var status = col.status || col.Status || '';
var version = col.version || col.Version || '';
var isInstalled = status.toLowerCase().indexOf('enabled') >= 0 || status.toLowerCase().indexOf('installed') >= 0;
var hasUpdate = status.toLowerCase().indexOf('update') >= 0;
return E('div', { 'style': 'display: flex; align-items: center; justify-content: space-between; padding: 0.5em 0; border-bottom: 1px solid rgba(255,255,255,0.1);' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('span', { 'style': 'font-size: 1.2em;' }, isInstalled ? '✅' : '⬜'),
E('span', { 'style': 'font-size: 0.9em;' }, name)
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('span', { 'style': 'font-size: 0.75em; color: #888;' }, 'v' + version),
hasUpdate ? E('span', { 'style': 'font-size: 0.7em; padding: 0.15em 0.4em; background: #ffa500; color: #000; border-radius: 3px;' }, 'UPDATE') : E('span')
])
]);
});
return E('div', { 'class': 'cs-collections-card' }, [
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, _('Installed Collections')),
E('button', {
'class': 'cs-btn cs-btn-secondary cs-btn-sm',
'click': ui.createHandlerFn(this, 'handleUpdateHub')
}, _('Update Hub'))
]),
E('div', { 'class': 'cs-card-body' }, collectionItems)
])
]);
},
handleUpdateHub: function() {
var self = this;
ui.showModal(_('Updating Hub'), [
E('p', {}, _('Downloading latest hub index...')),
E('div', { 'class': 'spinning' })
]);
this.csApi.updateHub().then(function(result) {
ui.hideModal();
if (result && result.success) {
self.showToast(_('Hub updated successfully'), 'success');
self.refreshDashboard();
} else {
self.showToast((result && result.error) || _('Hub update failed'), 'error');
}
}).catch(function(err) {
ui.hideModal();
self.showToast(err.message || _('Hub update failed'), 'error');
});
},
// Hub Stats Card - Shows installed parsers, scenarios, collections counts
renderHubStatsCard: function() {
var hub = this.hubData || {};
// Count installed items by type
var parsers = hub.parsers || [];
var scenarios = hub.scenarios || [];
var collections = hub.collections || [];
var postoverflows = hub.postoverflows || [];
var installedParsers = parsers.filter(function(p) {
return p.installed || (p.status && p.status.toLowerCase().indexOf('enabled') >= 0);
});
var installedScenarios = scenarios.filter(function(s) {
return s.installed || (s.status && s.status.toLowerCase().indexOf('enabled') >= 0);
});
var installedCollections = collections.filter(function(c) {
return c.installed || (c.status && c.status.toLowerCase().indexOf('enabled') >= 0);
});
// Get parser details for display
var parserList = installedParsers.slice(0, 8).map(function(p) {
var name = p.name || p.Name || 'unknown';
var shortName = name.split('/').pop();
return E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.35em 0; border-bottom: 1px solid rgba(255,255,255,0.05);' }, [
E('span', { 'style': 'color: #22c55e;' }, '✓'),
E('span', { 'style': 'font-size: 0.85em;' }, shortName)
]);
});
return E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, _('Hub Components'))
]),
E('div', { 'class': 'cs-card-body' }, [
// Mini stats row
E('div', { 'style': 'display: flex; gap: 1em; margin-bottom: 1em;' }, [
E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(34,197,94,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #22c55e;' }, String(installedParsers.length)),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Parsers'))
]),
E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(59,130,246,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #3b82f6;' }, String(installedScenarios.length)),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Scenarios'))
]),
E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(168,85,247,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #a855f7;' }, String(installedCollections.length)),
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Collections'))
])
]),
// Parser list
E('div', { 'style': 'font-size: 0.8em; font-weight: 600; color: #94a3b8; margin-bottom: 0.5em;' }, _('Installed Parsers')),
parserList.length > 0 ? E('div', {}, parserList) : E('div', { 'style': 'color: #666; font-size: 0.85em;' }, _('No parsers installed'))
])
]);
},
// Firewall Health Status Card
renderFirewallHealth: function() {
var stats = this.nftablesStats || {};
var health = stats.firewall_health || {};
var status = health.status || 'unknown';
var issues = health.issues || '';
var bouncerRunning = health.bouncer_running;
var uciEnabled = health.uci_enabled;
var apiKeyConfigured = health.api_key_configured;
var inputHooked = health.input_chain_hooked;
var forwardHooked = health.forward_chain_hooked;
var setsHaveTimeout = health.sets_have_timeout;
var decisionsSynced = health.decisions_synced;
var cscliDecisions = health.cscli_decisions_count || 0;
var nftElements = health.nft_elements_count || 0;
var statusColor = status === 'ok' ? '#00d4aa' : (status === 'warning' ? '#ffa500' : '#ff4757');
var statusIcon = status === 'ok' ? '✅' : (status === 'warning' ? '⚠️' : '❌');
var statusText = status === 'ok' ? 'Healthy' : (status === 'warning' ? 'Warning' : 'Error');
var checkItems = [
{ label: 'Bouncer Process', ok: bouncerRunning, detail: bouncerRunning ? 'Running' : 'Not running' },
{ label: 'UCI Enabled', ok: uciEnabled, detail: uciEnabled ? 'Enabled' : 'Disabled' },
{ label: 'API Key', ok: apiKeyConfigured, detail: apiKeyConfigured ? 'Configured' : 'Missing or default' },
{ label: 'Input Chain', ok: inputHooked, detail: inputHooked ? 'Hooked' : 'Not hooked' },
{ label: 'Forward Chain', ok: forwardHooked, detail: forwardHooked ? 'Hooked' : 'Not hooked' },
{ label: 'Set Timeout', ok: setsHaveTimeout, detail: setsHaveTimeout ? 'Enabled' : 'Disabled' },
{ label: 'Decisions Sync', ok: decisionsSynced, detail: decisionsSynced ? (nftElements + ' synced') : 'Out of sync' }
];
var checkRows = checkItems.map(function(item) {
return E('div', { 'style': 'display: flex; align-items: center; justify-content: space-between; padding: 0.4em 0; border-bottom: 1px solid rgba(255,255,255,0.05);' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('span', { 'style': 'font-size: 1em;' }, item.ok ? '✅' : '❌'),
E('span', { 'style': 'font-size: 0.85em;' }, item.label)
]),
E('span', { 'style': 'font-size: 0.75em; color: ' + (item.ok ? '#00d4aa' : '#ff4757') + ';' }, item.detail)
]);
});
return E('div', { 'class': 'cs-card', 'style': 'flex: 1;' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, [
_('Firewall Health'),
E('span', {
'style': 'margin-left: 0.75em; font-size: 0.8em; padding: 0.2em 0.6em; background: ' + statusColor + '; border-radius: 12px;'
}, statusIcon + ' ' + statusText)
])
]),
E('div', { 'class': 'cs-card-body' }, [
// Status summary
issues ? E('div', { 'style': 'background: rgba(255,71,87,0.1); border: 1px solid rgba(255,71,87,0.3); border-radius: 8px; padding: 0.75em; margin-bottom: 1em;' }, [
E('div', { 'style': 'font-size: 0.85em; color: #ff4757;' }, issues)
]) : E('span'),
// Sync stats
E('div', { 'style': 'display: flex; gap: 1em; margin-bottom: 1em;' }, [
E('div', { 'style': 'flex: 1; text-align: center; padding: 0.5em; background: rgba(102,126,234,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 1.25em; font-weight: 700; color: #667eea;' }, String(cscliDecisions)),
E('div', { 'style': 'font-size: 0.7em; color: #888;' }, 'Decisions')
]),
E('div', { 'style': 'flex: 1; text-align: center; padding: 0.5em; background: rgba(0,212,170,0.1); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 1.25em; font-weight: 700; color: #00d4aa;' }, String(nftElements)),
E('div', { 'style': 'font-size: 0.7em; color: #888;' }, 'In Firewall')
])
]),
// Check items
E('div', {}, checkRows)
])
]);
},
// Firewall Blocks - Shows IPs blocked in nftables
renderFirewallBlocks: function() {
var self = this;
var stats = this.nftablesStats || {};
// Check if nftables available
if (stats.error) {
return E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, _('Firewall Blocks'))
]),
E('div', { 'class': 'cs-card-body' }, [
E('div', { 'class': 'cs-empty' }, [
E('div', { 'class': 'cs-empty-icon' }, '⚠️'),
E('p', {}, stats.error)
])
])
]);
}
var ipv4Active = stats.ipv4_table_exists;
var ipv6Active = stats.ipv6_table_exists;
var ipv4List = stats.ipv4_blocked_ips || [];
var ipv6List = stats.ipv6_blocked_ips || [];
var ipv4Rules = stats.ipv4_rules_count || 0;
var ipv6Rules = stats.ipv6_rules_count || 0;
// Use total counts from API (includes all IPs, not just sample)
var ipv4Total = stats.ipv4_total_count || ipv4List.length;
var ipv6Total = stats.ipv6_total_count || ipv6List.length;
var ipv4Capi = stats.ipv4_capi_count || 0;
var ipv4Cscli = stats.ipv4_cscli_count || 0;
var ipv6Capi = stats.ipv6_capi_count || 0;
var ipv6Cscli = stats.ipv6_cscli_count || 0;
var totalBlocked = ipv4Total + ipv6Total;
// Build IP list (combine IPv4 and IPv6, limit to 20)
var allIps = [];
ipv4List.forEach(function(ip) { allIps.push({ ip: ip, type: 'IPv4' }); });
ipv6List.forEach(function(ip) { allIps.push({ ip: ip, type: 'IPv6' }); });
var displayIps = allIps.slice(0, 20);
var ipRows = displayIps.map(function(item) {
return E('div', {
'style': 'display: flex; align-items: center; justify-content: space-between; padding: 0.5em 0; border-bottom: 1px solid rgba(255,255,255,0.1);'
}, [
E('div', { 'style': 'display: flex; align-items: center; gap: 0.75em;' }, [
E('span', { 'style': 'font-size: 1.1em;' }, '🚫'),
E('code', { 'style': 'font-size: 0.85em; background: rgba(0,0,0,0.2); padding: 0.2em 0.5em; border-radius: 4px;' }, item.ip),
E('span', {
'style': 'font-size: 0.7em; padding: 0.15em 0.4em; background: ' + (item.type === 'IPv4' ? '#667eea' : '#764ba2') + '; border-radius: 3px;'
}, item.type)
]),
E('button', {
'class': 'cs-btn cs-btn-danger cs-btn-sm',
'style': 'font-size: 0.75em; padding: 0.25em 0.5em;',
'click': ui.createHandlerFn(self, 'handleUnban', item.ip)
}, _('Unban'))
]);
});
// Status indicators with breakdown by origin
var statusRow = E('div', { 'style': 'display: flex; gap: 1.5em; margin-bottom: 1em; flex-wrap: wrap;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('span', { 'style': 'font-size: 1.2em;' }, ipv4Active ? '✅' : '❌'),
E('span', { 'style': 'font-size: 0.85em;' }, 'IPv4'),
E('span', { 'style': 'font-size: 0.75em; color: #888;' }, ipv4Total.toLocaleString() + ' IPs')
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('span', { 'style': 'font-size: 1.2em;' }, ipv6Active ? '✅' : '❌'),
E('span', { 'style': 'font-size: 0.85em;' }, 'IPv6'),
E('span', { 'style': 'font-size: 0.75em; color: #888;' }, ipv6Total.toLocaleString() + ' IPs')
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding-left: 1em; border-left: 1px solid rgba(255,255,255,0.2);' }, [
E('span', { 'style': 'font-size: 0.8em; padding: 0.2em 0.5em; background: #667eea; border-radius: 4px;' }, 'CAPI'),
E('span', { 'style': 'font-size: 0.85em; color: #667eea;' }, (ipv4Capi + ipv6Capi).toLocaleString())
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('span', { 'style': 'font-size: 0.8em; padding: 0.2em 0.5em; background: #00d4aa; border-radius: 4px;' }, 'Local'),
E('span', { 'style': 'font-size: 0.85em; color: #00d4aa;' }, (ipv4Cscli + ipv6Cscli).toLocaleString())
])
]);
return E('div', { 'class': 'cs-card', 'style': 'flex: 2;' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, [
_('Firewall Blocks'),
E('span', {
'style': 'margin-left: 0.75em; font-size: 0.8em; padding: 0.2em 0.6em; background: linear-gradient(90deg, #ff4757, #ff6b81); border-radius: 12px;'
}, totalBlocked + ' blocked')
])
]),
E('div', { 'class': 'cs-card-body' }, [
statusRow,
ipRows.length > 0 ?
E('div', { 'style': 'max-height: 300px; overflow-y: auto;' }, ipRows) :
E('div', { 'class': 'cs-empty', 'style': 'padding: 1em;' }, [
E('div', { 'class': 'cs-empty-icon' }, '✅'),
E('p', {}, _('No IPs currently blocked in firewall'))
]),
allIps.length > 20 ? E('div', { 'style': 'text-align: center; padding: 0.5em; font-size: 0.8em; color: #888;' },
_('Showing 20 of ') + allIps.length + _(' blocked IPs')
) : E('span')
])
]);
},
renderLogCard: function(entries) {
return E('div', { 'class': 'cs-card cs-log-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, _('CrowdSec Logs'))
]),
entries && entries.length ?
E('pre', { 'class': 'cs-log-output' }, entries.slice(-30).join('\n')) :
E('p', { 'class': 'cs-empty' }, _('No log entries'))
]);
},
handleSnapshot: function() {
var self = this;
ui.showModal(_('Collecting snapshot'), [
E('p', {}, _('Aggregating dmesg/logread into SecuBox log…')),
E('div', { 'class': 'spinning' })
]);
this.csApi.collectDebugSnapshot().then(function(result) {
ui.hideModal();
if (result && result.success) {
self.refreshDashboard();
self.showToast(_('Snapshot appended to /var/log/seccubox.log'), 'success');
} else {
self.showToast((result && result.error) || _('Snapshot failed'), 'error');
}
}).catch(function(err) {
ui.hideModal();
self.showToast(err.message || _('Snapshot failed'), 'error');
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});