feat(luci-app-crowdsec-dashboard): Add graceful error handling when service stopped

Enhanced dashboard UX when CrowdSec service is not running:

API module (api.js):
- Modified getDashboardData() to handle error responses gracefully
- Returns empty arrays/objects for stats when service is stopped
- Includes error flag in response data

Overview module (overview.js):
- Added 'fs' module import for service control
- Added startCrowdSec() function to start service from UI
- Display warning banner when service is stopped
- Provide actionable message with start service link

Dashboard CSS (dashboard.css):
- Added .cs-warning-banner styles for error messages
- Professional warning styling with icon and content layout

This resolves XHR timeout errors by showing friendly error messages
instead of hanging requests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-06 16:07:01 +01:00
parent 4e98c03be4
commit d1788a12ff
3 changed files with 110 additions and 5 deletions

View File

@ -198,11 +198,18 @@ return baseclass.extend({
callDecisions(),
callAlerts()
]).then(function(results) {
// Check if any result has an error (service not running)
var status = results[0] || {};
var stats = results[1] || {};
var decisions = results[2] || {};
var alerts = results[3] || {};
return {
status: results[0] || {},
stats: results[1] || {},
decisions: (results[2] && results[2].decisions) || [],
alerts: (results[3] && results[3].alerts) || []
status: status,
stats: (stats.error) ? {} : stats,
decisions: (decisions.error) ? [] : (decisions.decisions || []),
alerts: (alerts.error) ? [] : (alerts.alerts || []),
error: stats.error || decisions.error || alerts.error || null
};
});
}

View File

@ -849,3 +849,48 @@
border-radius: var(--cs-radius);
border: 1px solid var(--cs-border);
}
/* Warning banner */
.cs-warning-banner {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
margin: 1rem 0;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 0.5rem;
}
.cs-warning-icon {
font-size: 1.5rem;
line-height: 1;
}
.cs-warning-content {
flex: 1;
}
.cs-warning-title {
font-weight: 600;
color: #856404;
margin-bottom: 0.5rem;
}
.cs-warning-message {
color: #856404;
line-height: 1.5;
}
.cs-warning-message a {
color: #0056b3;
text-decoration: underline;
}
.cs-warning-message code {
background: #f8f9fa;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-family: monospace;
color: #d63384;
}

View File

@ -4,6 +4,7 @@
'require dom';
'require poll';
'require ui';
'require fs';
'require crowdsec-dashboard/api as api';
/**
@ -399,9 +400,30 @@ return view.extend({
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,
this.renderStatsGrid(stats, decisions),
E('div', { 'class': 'cs-charts-row' }, [
@ -443,6 +465,37 @@ return view.extend({
]);
},
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] || {};