From d1788a12ff3488478cd30adbf1438874cf68cdb2 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 6 Jan 2026 16:07:01 +0100 Subject: [PATCH] feat(luci-app-crowdsec-dashboard): Add graceful error handling when service stopped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../resources/crowdsec-dashboard/api.js | 15 +++-- .../crowdsec-dashboard/dashboard.css | 45 +++++++++++++++ .../view/crowdsec-dashboard/overview.js | 55 ++++++++++++++++++- 3 files changed, 110 insertions(+), 5 deletions(-) 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 e18665a6..fe5ed5a2 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 @@ -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 }; }); } diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/dashboard.css b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/dashboard.css index a7d7e00b..cff95d04 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/dashboard.css +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/dashboard.css @@ -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; +} diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js index 25b13a77..6af4ae81 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js @@ -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] || {};