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:
parent
1a4096fd2e
commit
62cf871eeb
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 + ' ...')
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user