From 327cc5b2853b811f26a393e0e27221e6b747007a Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Thu, 8 Jan 2026 15:02:03 +0100 Subject: [PATCH] feat: Add smart action buttons and fix CrowdSec settings display (v0.6.0-r29) - Add service control RPCD method (start/stop/restart/reload) - Add smart action buttons to CrowdSec Settings (Service Control, Register Bouncer, Hub Update) - Add CrowdSec Console quick access link button - Fix LAPI status check (use lapi_status field) - Fix collections display (handle nested response structure) - Fix System Hub Quick Status Indicators layout (label/value stacking) Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 4 +- .../resources/crowdsec-dashboard/api.js | 10 + .../view/crowdsec-dashboard/settings.js | 220 ++++++++++++++---- .../usr/libexec/rpcd/luci.crowdsec-dashboard | 46 +++- .../acl.d/luci-app-crowdsec-dashboard.json | 3 +- .../resources/system-hub/overview.css | 12 + 6 files changed, 247 insertions(+), 48 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 20c3bfe8..ca5f1b17 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -347,7 +347,9 @@ "Bash($SSH root@192.168.255.1 \"uci show network | grep -E ''=interface|\\\\.proto=''\")", "Bash($SSH root@192.168.255.1 \"uci show firewall | grep -E ''zone.*name|=forwarding''\")", "Bash(for:*)", - "Bash(do sed -i \"s/''require secubox-theme\\\\/theme as Theme'';//g\" \"$f\")" + "Bash(do sed -i \"s/''require secubox-theme\\\\/theme as Theme'';//g\" \"$f\")", + "Bash(do sleep 5)", + "Bash(break)" ] } } 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 55ef1361..1708c3a3 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 @@ -217,6 +217,13 @@ var callConsoleDisable = rpc.declare({ expect: { } }); +var callServiceControl = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'service_control', + params: ['action'], + expect: { } +}); + function formatDuration(seconds) { if (!seconds) return 'N/A'; if (seconds < 60) return seconds + 's'; @@ -343,6 +350,9 @@ return baseclass.extend({ consoleEnroll: callConsoleEnroll, consoleDisable: callConsoleDisable, + // Service Control + serviceControl: callServiceControl, + formatDuration: formatDuration, formatDate: formatDate, formatRelativeTime: formatRelativeTime, diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js index 1a3d079e..db618b36 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js @@ -16,9 +16,12 @@ return view.extend({ render: function(data) { var status = data[0] || {}; - var machines = data[1] || []; + var machinesData = data[1] || {}; + var machines = Array.isArray(machinesData) ? machinesData : (machinesData.machines || []); var hub = data[2] || {}; - var collections = Array.isArray(data[3]) ? data[3] : []; + var collectionsData = data[3] || {}; + var collections = collectionsData.collections || []; + if (collections.collections) collections = collections.collections; var view = E('div', { 'class': 'cbi-map' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), @@ -42,13 +45,13 @@ return view.extend({ ]), // LAPI Status - E('div', { 'class': 'cbi-value', 'style': 'background: ' + (status.lapi === 'running' ? '#d4edda' : '#f8d7da') + '; padding: 1em; border-radius: 4px; border-left: 4px solid ' + (status.lapi === 'running' ? '#28a745' : '#dc3545') + ';' }, [ + E('div', { 'class': 'cbi-value', 'style': 'background: ' + (status.lapi_status === 'available' ? '#d4edda' : '#f8d7da') + '; padding: 1em; border-radius: 4px; border-left: 4px solid ' + (status.lapi_status === 'available' ? '#28a745' : '#dc3545') + ';' }, [ E('label', { 'class': 'cbi-value-title' }, _('Local API (LAPI)')), E('div', { 'class': 'cbi-value-field' }, [ E('span', { 'class': 'badge', - 'style': 'background: ' + (status.lapi === 'running' ? '#28a745' : '#dc3545') + '; color: white; padding: 0.5em 1em; border-radius: 4px; font-size: 1em;' - }, status.lapi === 'running' ? _('RUNNING') : _('STOPPED')) + 'style': 'background: ' + (status.lapi_status === 'available' ? '#28a745' : '#dc3545') + '; color: white; padding: 0.5em 1em; border-radius: 4px; font-size: 1em;' + }, status.lapi_status === 'available' ? _('AVAILABLE') : _('UNAVAILABLE')) ]) ]), @@ -152,7 +155,7 @@ return view.extend({ E('tbody', {}, collections.length > 0 ? collections.map(function(collection) { - var isInstalled = collection.status === 'installed' || collection.installed === 'ok'; + var isInstalled = collection.status === 'enabled' || collection.status === 'installed' || collection.installed === 'ok'; var collectionName = collection.name || 'Unknown'; return E('tr', {}, [ E('td', {}, [ @@ -228,46 +231,173 @@ return view.extend({ // Quick Actions E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [ E('h3', {}, _('Quick Actions')), - E('div', { 'style': 'display: flex; gap: 1em; flex-wrap: wrap; margin-top: 1em;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function() { - ui.showModal(_('Service Control'), [ - E('p', {}, _('Use the following commands to control CrowdSec:')), - E('pre', { 'style': 'background: #f5f5f5; padding: 1em; border-radius: 4px; overflow-x: auto;' }, [ - '/etc/init.d/crowdsec start\n', - '/etc/init.d/crowdsec stop\n', - '/etc/init.d/crowdsec restart\n', - '/etc/init.d/crowdsec status' - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Close')) - ]) - ]); - } - }, _('Service Control')), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function() { - ui.showModal(_('Register Bouncer'), [ - E('p', {}, _('To register a new bouncer, use the following command:')), - E('pre', { 'style': 'background: #f5f5f5; padding: 1em; border-radius: 4px;' }, - 'cscli bouncers add '), - E('p', { 'style': 'margin-top: 1em;' }, - _('The command will output an API key. Use this key to configure your bouncer.')), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Close')) - ]) - ]); - } - }, _('Register Bouncer')) + // Service Control + E('div', { 'style': 'margin-top: 1em;' }, [ + E('h4', { 'style': 'margin-bottom: 0.5em; color: var(--cyber-text-secondary, #888);' }, _('Service Control')), + E('div', { 'style': 'display: flex; gap: 0.5em; flex-wrap: wrap;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'style': 'min-width: 80px;', + 'click': function(ev) { + ev.target.disabled = true; + ev.target.classList.add('spinning'); + API.serviceControl('start').then(function(result) { + ev.target.disabled = false; + ev.target.classList.remove('spinning'); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('CrowdSec started successfully')), 'info'); + window.setTimeout(function() { location.reload(); }, 1500); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to start service')), 'error'); + } + }); + } + }, _('▶ Start')), + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'min-width: 80px;', + 'click': function(ev) { + ev.target.disabled = true; + ev.target.classList.add('spinning'); + API.serviceControl('stop').then(function(result) { + ev.target.disabled = false; + ev.target.classList.remove('spinning'); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('CrowdSec stopped')), 'info'); + window.setTimeout(function() { location.reload(); }, 1500); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to stop service')), 'error'); + } + }); + } + }, _('■ Stop')), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'min-width: 80px;', + 'click': function(ev) { + ev.target.disabled = true; + ev.target.classList.add('spinning'); + API.serviceControl('restart').then(function(result) { + ev.target.disabled = false; + ev.target.classList.remove('spinning'); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('CrowdSec restarted')), 'info'); + window.setTimeout(function() { location.reload(); }, 2000); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to restart service')), 'error'); + } + }); + } + }, _('↻ Restart')), + E('button', { + 'class': 'cbi-button', + 'style': 'min-width: 80px;', + 'click': function(ev) { + ev.target.disabled = true; + ev.target.classList.add('spinning'); + API.serviceControl('reload').then(function(result) { + ev.target.disabled = false; + ev.target.classList.remove('spinning'); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Configuration reloaded')), 'info'); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to reload')), 'error'); + } + }); + } + }, _('⟳ Reload')) + ]) + ]), + + // Register Bouncer + E('div', { 'style': 'margin-top: 1.5em;' }, [ + E('h4', { 'style': 'margin-bottom: 0.5em; color: var(--cyber-text-secondary, #888);' }, _('Register New Bouncer')), + E('div', { 'style': 'display: flex; gap: 0.5em; flex-wrap: wrap; align-items: center;' }, [ + E('input', { + 'type': 'text', + 'id': 'new-bouncer-name', + 'placeholder': _('Bouncer name...'), + 'style': 'padding: 0.5em; border: 1px solid var(--cyber-border, #444); border-radius: 4px; background: var(--cyber-bg-secondary, #1a1a2e); color: var(--cyber-text-primary, #fff); min-width: 200px;' + }), + E('button', { + 'class': 'cbi-button cbi-button-add', + 'click': function(ev) { + var nameInput = document.getElementById('new-bouncer-name'); + var name = nameInput.value.trim(); + if (!name) { + ui.addNotification(null, E('p', {}, _('Please enter a bouncer name')), 'error'); + return; + } + ev.target.disabled = true; + ev.target.classList.add('spinning'); + API.registerBouncer(name).then(function(result) { + ev.target.disabled = false; + ev.target.classList.remove('spinning'); + if (result && result.success) { + nameInput.value = ''; + ui.showModal(_('Bouncer Registered'), [ + E('p', {}, _('Bouncer "%s" registered successfully!').format(name)), + E('p', { 'style': 'margin-top: 1em;' }, _('API Key:')), + E('pre', { + 'style': 'background: var(--cyber-bg-tertiary, #252538); padding: 1em; border-radius: 4px; word-break: break-all; user-select: all;' + }, result.api_key || result.key || 'Check console'), + E('p', { 'style': 'margin-top: 1em; color: #f39c12;' }, + _('Save this key! It will not be shown again.')), + E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { + ui.hideModal(); + location.reload(); + } + }, _('Close')) + ]) + ]); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to register bouncer')), 'error'); + } + }); + } + }, _('+ Register')) + ]) + ]), + + // Hub Update + E('div', { 'style': 'margin-top: 1.5em;' }, [ + E('h4', { 'style': 'margin-bottom: 0.5em; color: var(--cyber-text-secondary, #888);' }, _('Hub Management')), + E('div', { 'style': 'display: flex; gap: 0.5em; flex-wrap: wrap;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function(ev) { + ev.target.disabled = true; + ev.target.classList.add('spinning'); + API.updateHub().then(function(result) { + ev.target.disabled = false; + ev.target.classList.remove('spinning'); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Hub index updated successfully')), 'info'); + window.setTimeout(function() { location.reload(); }, 1500); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to update hub')), 'error'); + } + }); + } + }, _('⬇ Update Hub Index')) + ]) + ]), + + // CrowdSec Console + E('div', { 'style': 'margin-top: 1.5em;' }, [ + E('h4', { 'style': 'margin-bottom: 0.5em; color: var(--cyber-text-secondary, #888);' }, _('CrowdSec Console')), + E('div', { 'style': 'display: flex; gap: 0.5em; flex-wrap: wrap;' }, [ + E('a', { + 'href': 'https://app.crowdsec.net', + 'target': '_blank', + 'class': 'cbi-button cbi-button-action', + 'style': 'text-decoration: none; display: inline-flex; align-items: center; gap: 0.5em;' + }, _('🌐 Open CrowdSec Console')) + ]) ]) ]), 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 2e90cf08..a887ad4e 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 @@ -1049,10 +1049,49 @@ console_disable() { json_dump } +# Service control (start/stop/restart/reload) +service_control() { + local action="$1" + json_init + + case "$action" in + start|stop|restart|reload) + secubox_log "CrowdSec service $action requested" + local output + output=$(/etc/init.d/crowdsec "$action" 2>&1) + local result=$? + sleep 2 + + # Check if service is running after action + local running=0 + if pgrep -x crowdsec >/dev/null 2>&1; then + running=1 + fi + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "action" "$action" + json_add_boolean "running" "$running" + json_add_string "message" "Service $action completed" + else + json_add_boolean "success" 0 + json_add_string "error" "Service $action failed" + json_add_string "output" "$output" + fi + ;; + *) + json_add_boolean "success" 0 + json_add_string "error" "Invalid action. Use: start, stop, restart, reload" + ;; + esac + + 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":{},"repair_lapi":{},"console_status":{},"console_enroll":{"key":"string","name":"string"},"console_disable":{}}' + 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":{},"console_status":{},"console_enroll":{"key":"string","name":"string"},"console_disable":{},"service_control":{"action":"string"}}' ;; call) case "$2" in @@ -1178,6 +1217,11 @@ case "$1" in console_disable) console_disable ;; + service_control) + read -r input + action=$(echo "$input" | jsonfilter -e '@.action' 2>/dev/null) + service_control "$action" + ;; *) 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 feb6c038..b5bd2863 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 @@ -43,7 +43,8 @@ "update_firewall_bouncer_config", "repair_lapi", "console_enroll", - "console_disable" + "console_disable", + "service_control" ] }, "uci": [ "crowdsec-dashboard" ] diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/overview.css b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/overview.css index 579c17b2..af232f46 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/overview.css +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/overview.css @@ -232,6 +232,18 @@ font-size: 20px; } +.sh-status-body { + display: flex; + flex-direction: column; + gap: 0.25em; +} + +.sh-status-body strong { + font-size: 0.85em; + font-weight: 500; + color: var(--sh-text-secondary, #888); +} + .sh-status-card.ok { border-left: 3px solid #22c55e; }