feat(haproxy): Add emergency health banner and quick restart buttons

- HAProxy overview: Add prominent emergency banner showing service status
  with quick health indicators (Container/HAProxy/Config) and one-click
  Restart/Start/Stop buttons
- SecuBox dashboard: Add Critical Services Quick Restart section with
  buttons for HAProxy, CrowdSec, Tor Shield, and Gitea
- Metabolizer config: Fix portal_path to /www/blog

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-26 08:13:40 +01:00
parent 1a4096fd2e
commit 62cf871eeb
4 changed files with 170 additions and 4 deletions

View File

@ -11,7 +11,7 @@ LUCI_PKGARCH:=all
PKG_NAME:=luci-app-haproxy PKG_NAME:=luci-app-haproxy
PKG_VERSION:=1.0.0 PKG_VERSION:=1.0.0
PKG_RELEASE:=6 PKG_RELEASE:=7
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr> PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
PKG_LICENSE:=MIT PKG_LICENSE:=MIT

View File

@ -40,6 +40,7 @@ return view.extend({
// Build content array, filtering out nulls // Build content array, filtering out nulls
var content = [ var content = [
this.renderEmergencyBanner(status),
this.renderPageHeader(status), this.renderPageHeader(status),
this.renderStatsGrid(status, vhosts, backends, certificates), this.renderStatsGrid(status, vhosts, backends, certificates),
this.renderHealthGrid(status), this.renderHealthGrid(status),
@ -126,6 +127,72 @@ return view.extend({
]); ]);
}, },
renderEmergencyBanner: function(status) {
var self = this;
var haproxyRunning = status.haproxy_running;
var containerRunning = status.container_running;
var statusColor = haproxyRunning ? '#22c55e' : (containerRunning ? '#f97316' : '#ef4444');
var statusText = haproxyRunning ? 'HEALTHY' : (containerRunning ? 'DEGRADED' : 'DOWN');
var statusIcon = haproxyRunning ? '\u2705' : (containerRunning ? '\u26A0\uFE0F' : '\u274C');
return E('div', {
'class': 'hp-emergency-banner',
'style': 'background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border: 1px solid ' + statusColor + '; border-radius: 12px; padding: 20px; margin-bottom: 24px; display: flex; align-items: center; justify-content: space-between; gap: 24px;'
}, [
// Status indicator
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
E('div', {
'style': 'width: 64px; height: 64px; border-radius: 50%; background: ' + statusColor + '22; display: flex; align-items: center; justify-content: center; font-size: 32px; border: 3px solid ' + statusColor + ';'
}, statusIcon),
E('div', {}, [
E('div', { 'style': 'font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: #888; margin-bottom: 4px;' }, 'Service Status'),
E('div', { 'style': 'font-size: 24px; font-weight: 700; color: ' + statusColor + ';' }, statusText),
E('div', { 'style': 'font-size: 13px; color: #888; margin-top: 4px;' },
'Container: ' + (containerRunning ? 'Running' : 'Stopped') +
' \u2022 HAProxy: ' + (haproxyRunning ? 'Active' : 'Inactive'))
])
]),
// Quick health checks
E('div', { 'style': 'display: flex; gap: 16px;' }, [
E('div', { 'style': 'text-align: center; padding: 12px 20px; background: rgba(255,255,255,0.05); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 20px;' }, containerRunning ? '\u2705' : '\u274C'),
E('div', { 'style': 'font-size: 11px; color: #888; margin-top: 4px;' }, 'Container')
]),
E('div', { 'style': 'text-align: center; padding: 12px 20px; background: rgba(255,255,255,0.05); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 20px;' }, haproxyRunning ? '\u2705' : '\u274C'),
E('div', { 'style': 'font-size: 11px; color: #888; margin-top: 4px;' }, 'HAProxy')
]),
E('div', { 'style': 'text-align: center; padding: 12px 20px; background: rgba(255,255,255,0.05); border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 20px;' }, status.config_valid !== false ? '\u2705' : '\u26A0\uFE0F'),
E('div', { 'style': 'font-size: 11px; color: #888; margin-top: 4px;' }, 'Config')
])
]),
// Emergency actions
E('div', { 'style': 'display: flex; gap: 12px;' }, [
E('button', {
'class': 'hp-btn',
'style': 'background: #3b82f6; color: white; padding: 12px 20px; font-size: 14px; font-weight: 600; border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 8px;',
'click': function() { self.handleRestart(); },
'disabled': !containerRunning ? true : null
}, ['\u{1F504}', ' Restart']),
E('button', {
'class': 'hp-btn',
'style': 'background: ' + (haproxyRunning ? '#ef4444' : '#22c55e') + '; color: white; padding: 12px 20px; font-size: 14px; font-weight: 600; border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 8px;',
'click': function() {
if (haproxyRunning) {
self.handleStop();
} else {
self.handleStart();
}
}
}, haproxyRunning ? ['\u23F9\uFE0F', ' Stop'] : ['\u25B6\uFE0F', ' Start'])
])
]);
},
renderStatsGrid: function(status, vhosts, backends, certificates) { renderStatsGrid: function(status, vhosts, backends, certificates) {
var activeVhosts = vhosts.filter(function(v) { return v.enabled; }).length; var activeVhosts = vhosts.filter(function(v) { return v.enabled; }).length;
var activeBackends = backends.filter(function(b) { return b.enabled; }).length; var activeBackends = backends.filter(function(b) { return b.enabled; }).length;
@ -539,6 +606,19 @@ return view.extend({
}); });
}, },
handleRestart: function() {
var self = this;
self.showToast('Restarting HAProxy...', 'warning');
return api.restart().then(function(res) {
if (res.success) {
self.showToast('HAProxy service restarted', 'success');
return self.refreshDashboard();
} else {
self.showToast('Failed to restart: ' + (res.error || 'Unknown error'), 'error');
}
});
},
handleReload: function() { handleReload: function() {
var self = this; var self = this;
return api.reload().then(function(res) { return api.reload().then(function(res) {

View File

@ -3,6 +3,7 @@
'require ui'; 'require ui';
'require dom'; 'require dom';
'require poll'; 'require poll';
'require fs';
'require secubox/api as API'; 'require secubox/api as API';
'require secubox-theme/theme as Theme'; 'require secubox-theme/theme as Theme';
'require secubox/nav as SecuNav'; 'require secubox/nav as SecuNav';
@ -393,6 +394,14 @@ return view.extend({
{ id: 'export_config', label: _('Export Configuration'), icon: '📦', variant: 'green' } { id: 'export_config', label: _('Export Configuration'), icon: '📦', variant: 'green' }
]; ];
// Critical services quick restart
var criticalServices = [
{ id: 'haproxy', label: 'HAProxy', icon: '⚖️' },
{ id: 'crowdsec', label: 'CrowdSec', icon: '🛡️' },
{ id: 'tor', label: 'Tor Shield', icon: '🧅' },
{ id: 'gitea', label: 'Gitea', icon: '🦊' }
];
return E('section', { 'class': 'sb-card' }, [ return E('section', { 'class': 'sb-card' }, [
E('div', { 'class': 'sb-card-header' }, [ E('div', { 'class': 'sb-card-header' }, [
E('h2', {}, _('Quick Actions')), E('h2', {}, _('Quick Actions')),
@ -410,10 +419,87 @@ return view.extend({
E('span', { 'class': 'sb-action-icon' }, action.icon), E('span', { 'class': 'sb-action-icon' }, action.icon),
E('span', { 'class': 'sb-action-label' }, action.label) E('span', { 'class': 'sb-action-label' }, action.label)
]); ]);
})),
// Critical Services Quick Restart Section
E('div', { 'class': 'sb-card-header', 'style': 'margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.1);' }, [
E('h3', { 'style': 'font-size: 14px; margin: 0;' }, _('Critical Services Quick Restart')),
E('p', { 'class': 'sb-card-subtitle', 'style': 'font-size: 12px;' }, _('One-click restart for essential services'))
]),
E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap; padding: 0 16px 16px;' },
criticalServices.map(function(svc) {
return E('button', {
'class': 'sb-service-restart-btn',
'type': 'button',
'style': 'display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: rgba(59, 130, 246, 0.15); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 8px; color: #3b82f6; cursor: pointer; font-size: 13px; transition: all 0.2s;',
'click': function(ev) {
self.restartService(svc.id, ev.target);
},
'onmouseover': function(ev) {
ev.target.style.background = 'rgba(59, 130, 246, 0.25)';
ev.target.style.borderColor = '#3b82f6';
},
'onmouseout': function(ev) {
ev.target.style.background = 'rgba(59, 130, 246, 0.15)';
ev.target.style.borderColor = 'rgba(59, 130, 246, 0.3)';
}
}, [
E('span', {}, svc.icon),
E('span', {}, svc.label),
E('span', { 'style': 'opacity: 0.7;' }, '🔄')
]);
})) }))
]); ]);
}, },
restartService: function(serviceId, btnElement) {
var self = this;
// Visual feedback
if (btnElement) {
btnElement.style.opacity = '0.6';
btnElement.disabled = true;
}
ui.showModal(_('Restarting Service'), [
E('p', { 'class': 'spinning' }, _('Restarting ') + serviceId + '...')
]);
// Map service to init.d script
var serviceMap = {
'haproxy': 'haproxy',
'crowdsec': 'crowdsec',
'tor': 'tor',
'gitea': 'gitea'
};
var initScript = serviceMap[serviceId] || serviceId;
return L.resolveDefault(
L.Request.post(L.env.cgi_base + '/cgi-exec', 'command=/etc/init.d/' + initScript + ' restart'),
{}
).then(function() {
// Also try the standard approach via fs
return fs.exec('/etc/init.d/' + initScript, ['restart']);
}).then(function() {
ui.hideModal();
ui.addNotification(null, E('p', {}, serviceId + ' ' + _('restarted successfully')), 'info');
}).catch(function(err) {
ui.hideModal();
// Fallback: try via API if available
return API.quickAction('restart_' + serviceId).then(function() {
ui.addNotification(null, E('p', {}, serviceId + ' ' + _('restarted successfully')), 'info');
}).catch(function() {
ui.addNotification(null, E('p', {}, _('Failed to restart ') + serviceId + ': ' + (err.message || err)), 'error');
});
}).finally(function() {
if (btnElement) {
btnElement.style.opacity = '1';
btnElement.disabled = false;
}
});
},
runQuickAction: function(actionId) { runQuickAction: function(actionId) {
ui.showModal(_('Executing action...'), [ ui.showModal(_('Executing action...'), [
E('p', { 'class': 'spinning' }, _('Running ') + actionId + ' ...') E('p', { 'class': 'spinning' }, _('Running ') + actionId + ' ...')

View File

@ -18,10 +18,10 @@ config cms 'cms'
config hexo 'hexo' config hexo 'hexo'
option source_path '/srv/hexojs/site/source/_posts' option source_path '/srv/hexojs/site/source/_posts'
option public_path '/srv/hexojs/site/public' option public_path '/srv/hexojs/site/public'
option portal_path '/www' option portal_path '/www/blog'
option auto_publish '0' option auto_publish '1'
config portal 'portal' config portal 'portal'
option enabled '1' option enabled '1'
option url_path '/' option url_path '/blog'
option title 'SecuBox Blog' option title 'SecuBox Blog'