secubox-openwrt/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/vhosts.js
CyberMind-FR f3fd676ad1 feat(haproxy): Add HAProxy load balancer packages for OpenWrt
- Add secubox-app-haproxy: LXC-containerized HAProxy service
  - Alpine Linux container with HAProxy
  - Multi-certificate SSL/TLS termination with SNI routing
  - ACME/Let's Encrypt auto-renewal
  - Virtual hosts management
  - Backend health checks and load balancing

- Add luci-app-haproxy: Full LuCI web interface
  - Overview dashboard with service status
  - Virtual hosts management with SSL options
  - Backends and servers configuration
  - SSL certificate management (ACME + import)
  - ACLs and URL-based routing rules
  - Statistics dashboard and logs
  - Settings for ports, timeouts, ACME

- Update luci-app-secubox-portal:
  - Add Services category with HAProxy, HexoJS, PicoBrew,
    Tor Shield, Jellyfin, Home Assistant, AdGuard Home, Nextcloud
  - Make portal dynamic - only shows installed apps
  - Add empty state UI for sections with no apps
  - Remove 404 errors for uninstalled apps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 20:09:32 +01:00

212 lines
6.3 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require ui';
'require form';
'require haproxy.api as api';
return view.extend({
load: function() {
return Promise.all([
api.listVhosts(),
api.listBackends()
]);
},
render: function(data) {
var self = this;
var vhosts = data[0] || [];
var backends = data[1] || [];
var view = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'Virtual Hosts'),
E('p', {}, 'Configure domain-based routing to backend servers.'),
// Add vhost form
E('div', { 'class': 'haproxy-form-section' }, [
E('h3', {}, 'Add Virtual Host'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Domain'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'new-domain',
'class': 'cbi-input-text',
'placeholder': 'example.com'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Backend'),
E('div', { 'class': 'cbi-value-field' }, [
E('select', { 'id': 'new-backend', 'class': 'cbi-input-select' },
[E('option', { 'value': '' }, '-- Select Backend --')].concat(
backends.map(function(b) {
return E('option', { 'value': b.id }, b.name);
})
)
)
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Options'),
E('div', { 'class': 'cbi-value-field' }, [
E('label', { 'style': 'margin-right: 1rem' }, [
E('input', { 'type': 'checkbox', 'id': 'new-ssl', 'checked': true }),
' Enable SSL'
]),
E('label', { 'style': 'margin-right: 1rem' }, [
E('input', { 'type': 'checkbox', 'id': 'new-ssl-redirect', 'checked': true }),
' Force HTTPS redirect'
]),
E('label', {}, [
E('input', { 'type': 'checkbox', 'id': 'new-acme', 'checked': true }),
' Auto-renew with ACME'
])
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, ''),
E('div', { 'class': 'cbi-value-field' }, [
E('button', {
'class': 'cbi-button cbi-button-add',
'click': function() { self.handleAddVhost(); }
}, 'Add Virtual Host')
])
])
]),
// Vhosts list
E('div', { 'class': 'haproxy-form-section' }, [
E('h3', {}, 'Configured Virtual Hosts (' + vhosts.length + ')'),
this.renderVhostsTable(vhosts, backends)
])
]);
// Add CSS
var style = E('style', {}, `
@import url('/luci-static/resources/haproxy/dashboard.css');
`);
view.insertBefore(style, view.firstChild);
return view;
},
renderVhostsTable: function(vhosts, backends) {
var self = this;
if (vhosts.length === 0) {
return E('p', { 'style': 'color: var(--text-color-medium, #666)' },
'No virtual hosts configured.');
}
var backendMap = {};
backends.forEach(function(b) { backendMap[b.id] = b.name; });
return E('table', { 'class': 'haproxy-vhosts-table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', {}, 'Domain'),
E('th', {}, 'Backend'),
E('th', {}, 'SSL'),
E('th', {}, 'Status'),
E('th', { 'style': 'width: 150px' }, 'Actions')
])
]),
E('tbody', {}, vhosts.map(function(vh) {
return E('tr', { 'data-id': vh.id }, [
E('td', {}, [
E('strong', {}, vh.domain),
vh.ssl_redirect ? E('small', { 'style': 'display: block; color: #666' }, 'Redirects HTTP to HTTPS') : null
]),
E('td', {}, backendMap[vh.backend] || vh.backend || '-'),
E('td', {}, [
vh.ssl ? E('span', { 'class': 'haproxy-badge ssl', 'style': 'margin-right: 4px' }, 'SSL') : null,
vh.acme ? E('span', { 'class': 'haproxy-badge acme' }, 'ACME') : null
]),
E('td', {}, E('span', {
'class': 'haproxy-badge ' + (vh.enabled ? 'enabled' : 'disabled')
}, vh.enabled ? 'Enabled' : 'Disabled')),
E('td', {}, [
E('button', {
'class': 'cbi-button cbi-button-edit',
'style': 'margin-right: 4px',
'click': function() { self.handleToggleVhost(vh); }
}, vh.enabled ? 'Disable' : 'Enable'),
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': function() { self.handleDeleteVhost(vh); }
}, 'Delete')
])
]);
}))
]);
},
handleAddVhost: function() {
var self = this;
var domain = document.getElementById('new-domain').value.trim();
var backend = document.getElementById('new-backend').value;
var ssl = document.getElementById('new-ssl').checked ? 1 : 0;
var sslRedirect = document.getElementById('new-ssl-redirect').checked ? 1 : 0;
var acme = document.getElementById('new-acme').checked ? 1 : 0;
if (!domain) {
ui.addNotification(null, E('p', {}, 'Domain is required'), 'error');
return;
}
return api.createVhost(domain, backend, ssl, sslRedirect, acme, 1).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, 'Virtual host created'));
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
handleToggleVhost: function(vh) {
var newEnabled = vh.enabled ? 0 : 1;
return api.updateVhost(vh.id, null, null, null, null, null, newEnabled).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, 'Virtual host updated'));
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
handleDeleteVhost: function(vh) {
var self = this;
ui.showModal('Delete Virtual Host', [
E('p', {}, 'Are you sure you want to delete virtual host "' + vh.domain + '"?'),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button',
'click': ui.hideModal
}, 'Cancel'),
E('button', {
'class': 'cbi-button cbi-button-negative',
'click': function() {
ui.hideModal();
api.deleteVhost(vh.id).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, 'Virtual host deleted'));
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
}
}, 'Delete')
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});