style(haproxy): Migrate acls.js and settings.js to KISS theme

ACLs & Routing (acls.js):
- Removed CSS import, replaced cbi- classes with kiss- classes
- Add ACL form with name, type, pattern, backend selector
- Add Redirect form with match host, target, code options
- KISS-styled tables for ACL and redirect rules
- Delete confirmation modals and toast notifications

Settings (settings.js):
- Removed CSS import, replaced cbi- classes with kiss- classes
- Service settings: enable, ports, max connections, memory, log level
- Statistics dashboard: enable, port, username, password
- Timeouts: connect, client, server, HTTP request, keep-alive, retries
- ACME/Let's Encrypt: enable, email, staging, key type, renew days
- KISS-styled form inputs with grid layout

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-12 12:06:23 +01:00
parent 1ff116e93d
commit 04325c789f
2 changed files with 394 additions and 499 deletions

View File

@ -5,6 +5,11 @@
'require haproxy.api as api'; 'require haproxy.api as api';
'require secubox/kiss-theme'; 'require secubox/kiss-theme';
/**
* HAProxy ACLs & Routing - KISS Style
* Copyright (C) 2025 CyberMind.fr
*/
return view.extend({ return view.extend({
load: function() { load: function() {
return Promise.all([ return Promise.all([
@ -19,195 +24,182 @@ return view.extend({
var acls = data[0] || []; var acls = data[0] || [];
var redirects = data[1] || []; var redirects = data[1] || [];
var backends = data[2] || []; var backends = data[2] || [];
var K = KissTheme;
var view = E('div', { 'class': 'cbi-map' }, [ var content = K.E('div', {}, [
E('h2', {}, 'ACLs & Routing'), // Page Header
E('p', {}, 'Configure URL-based routing rules and redirections.'), K.E('div', { 'style': 'margin-bottom: 20px;' }, [
K.E('h2', { 'style': 'margin: 0; font-size: 24px; display: flex; align-items: center; gap: 10px;' }, [
// ACL Rules section K.E('span', {}, '🔀'),
E('div', { 'class': 'haproxy-form-section' }, [ 'ACLs & Routing'
E('h3', {}, 'Add ACL Rule'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Name'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'acl-name',
'class': 'cbi-input-text',
'placeholder': 'is_api'
})
])
]), ]),
E('div', { 'class': 'cbi-value' }, [ K.E('p', { 'style': 'margin: 4px 0 0; color: var(--kiss-muted, #94a3b8); font-size: 14px;' },
E('label', { 'class': 'cbi-value-title' }, 'Match Type'), 'Configure URL-based routing rules and redirections')
E('div', { 'class': 'cbi-value-field' }, [
E('select', { 'id': 'acl-type', 'class': 'cbi-input-select' }, [
E('option', { 'value': 'path_beg' }, 'Path begins with'),
E('option', { 'value': 'path_end' }, 'Path ends with'),
E('option', { 'value': 'path_reg' }, 'Path regex'),
E('option', { 'value': 'hdr(host)' }, 'Host header'),
E('option', { 'value': 'hdr_beg(host)' }, 'Host begins with'),
E('option', { 'value': 'src' }, 'Source IP'),
E('option', { 'value': 'url_param' }, 'URL parameter')
])
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Pattern'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'acl-pattern',
'class': 'cbi-input-text',
'placeholder': '/api/'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Route to Backend'),
E('div', { 'class': 'cbi-value-field' }, [
E('select', { 'id': 'acl-backend', 'class': 'cbi-input-select' },
[E('option', { 'value': '' }, '-- No routing (ACL only) --')].concat(
backends.map(function(b) {
return E('option', { 'value': b.id }, b.name);
})
)
)
])
]),
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.handleAddAcl(); }
}, 'Add ACL Rule')
])
])
]), ]),
// ACL list // Add ACL Rule Card
E('div', { 'class': 'haproxy-form-section' }, [ K.E('div', { 'class': 'kiss-card' }, [
E('h3', {}, 'ACL Rules (' + acls.length + ')'), K.E('div', { 'class': 'kiss-card-title' }, [' ', 'Add ACL Rule']),
K.E('div', { 'class': 'kiss-grid kiss-grid-2', 'style': 'gap: 16px; margin-bottom: 16px;' }, [
K.E('div', {}, [
K.E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; display: block; margin-bottom: 6px;' }, 'Name'),
K.E('input', {
'type': 'text',
'id': 'acl-name',
'placeholder': 'is_api',
'style': 'width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px;'
})
]),
K.E('div', {}, [
K.E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; display: block; margin-bottom: 6px;' }, 'Match Type'),
K.E('select', {
'id': 'acl-type',
'style': 'width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px;'
}, [
K.E('option', { 'value': 'path_beg' }, 'Path begins with'),
K.E('option', { 'value': 'path_end' }, 'Path ends with'),
K.E('option', { 'value': 'path_reg' }, 'Path regex'),
K.E('option', { 'value': 'hdr(host)' }, 'Host header'),
K.E('option', { 'value': 'hdr_beg(host)' }, 'Host begins with'),
K.E('option', { 'value': 'src' }, 'Source IP'),
K.E('option', { 'value': 'url_param' }, 'URL parameter')
])
]),
K.E('div', {}, [
K.E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; display: block; margin-bottom: 6px;' }, 'Pattern'),
K.E('input', {
'type': 'text',
'id': 'acl-pattern',
'placeholder': '/api/',
'style': 'width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px;'
})
]),
K.E('div', {}, [
K.E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; display: block; margin-bottom: 6px;' }, 'Route to Backend'),
K.E('select', {
'id': 'acl-backend',
'style': 'width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px;'
}, [K.E('option', { 'value': '' }, '-- No routing (ACL only) --')].concat(
backends.map(function(b) {
return K.E('option', { 'value': b.id }, b.name);
})
))
])
]),
K.E('button', {
'class': 'kiss-btn kiss-btn-green',
'click': function() { self.handleAddAcl(); }
}, ' Add ACL Rule')
]),
// ACL Rules Table
K.E('div', { 'class': 'kiss-card' }, [
K.E('div', { 'class': 'kiss-card-title' }, ['📋 ', 'ACL Rules (', String(acls.length), ')']),
this.renderAclsTable(acls, backends) this.renderAclsTable(acls, backends)
]), ]),
// Redirects section // Add Redirect Rule Card
E('div', { 'class': 'haproxy-form-section' }, [ K.E('div', { 'class': 'kiss-card' }, [
E('h3', {}, 'Add Redirect Rule'), K.E('div', { 'class': 'kiss-card-title' }, ['↪️ ', 'Add Redirect Rule']),
E('div', { 'class': 'cbi-value' }, [ K.E('div', { 'class': 'kiss-grid kiss-grid-2', 'style': 'gap: 16px; margin-bottom: 16px;' }, [
E('label', { 'class': 'cbi-value-title' }, 'Name'), K.E('div', {}, [
E('div', { 'class': 'cbi-value-field' }, [ K.E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; display: block; margin-bottom: 6px;' }, 'Name'),
E('input', { K.E('input', {
'type': 'text', 'type': 'text',
'id': 'redirect-name', 'id': 'redirect-name',
'class': 'cbi-input-text', 'placeholder': 'www-redirect',
'placeholder': 'www-redirect' 'style': 'width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px;'
}) })
]) ]),
]), K.E('div', {}, [
E('div', { 'class': 'cbi-value' }, [ K.E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; display: block; margin-bottom: 6px;' }, 'Match Host (Regex)'),
E('label', { 'class': 'cbi-value-title' }, 'Match Host'), K.E('input', {
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text', 'type': 'text',
'id': 'redirect-match', 'id': 'redirect-match',
'class': 'cbi-input-text', 'placeholder': '^www\\.',
'placeholder': '^www\\.' 'style': 'width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px;'
}), })
E('p', { 'class': 'cbi-value-description' }, 'Regex pattern to match against host header') ]),
]) K.E('div', {}, [
]), K.E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; display: block; margin-bottom: 6px;' }, 'Target Host'),
E('div', { 'class': 'cbi-value' }, [ K.E('input', {
E('label', { 'class': 'cbi-value-title' }, 'Target Host'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text', 'type': 'text',
'id': 'redirect-target', 'id': 'redirect-target',
'class': 'cbi-input-text', 'placeholder': 'Leave empty to strip matched portion',
'placeholder': 'Leave empty to strip matched portion' 'style': 'width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px;'
}) })
]) ]),
]), K.E('div', {}, [
E('div', { 'class': 'cbi-value' }, [ K.E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; display: block; margin-bottom: 6px;' }, 'Redirect Code'),
E('label', { 'class': 'cbi-value-title' }, 'Options'), K.E('select', {
E('div', { 'class': 'cbi-value-field' }, [ 'id': 'redirect-code',
E('label', { 'style': 'margin-right: 1rem' }, [ 'style': 'width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px;'
E('input', { 'type': 'checkbox', 'id': 'redirect-strip-www' }), }, [
' Strip www prefix' K.E('option', { 'value': '301' }, '301 Permanent'),
]), K.E('option', { 'value': '302' }, '302 Temporary'),
E('select', { 'id': 'redirect-code', 'class': 'cbi-input-select', 'style': 'width: auto' }, [ K.E('option', { 'value': '307' }, '307 Temporary Redirect'),
E('option', { 'value': '301' }, '301 Permanent'), K.E('option', { 'value': '308' }, '308 Permanent Redirect')
E('option', { 'value': '302' }, '302 Temporary'), ])
E('option', { 'value': '303' }, '303 See Other'), ]),
E('option', { 'value': '307' }, '307 Temporary Redirect'), K.E('div', { 'style': 'grid-column: span 2;' }, [
E('option', { 'value': '308' }, '308 Permanent Redirect') K.E('label', { 'style': 'display: flex; align-items: center; gap: 8px; cursor: pointer;' }, [
K.E('input', { 'type': 'checkbox', 'id': 'redirect-strip-www' }),
'Strip www prefix automatically'
]) ])
]) ])
]), ]),
E('div', { 'class': 'cbi-value' }, [ K.E('button', {
E('label', { 'class': 'cbi-value-title' }, ''), 'class': 'kiss-btn kiss-btn-green',
E('div', { 'class': 'cbi-value-field' }, [ 'click': function() { self.handleAddRedirect(); }
E('button', { }, '↪️ Add Redirect')
'class': 'cbi-button cbi-button-add',
'click': function() { self.handleAddRedirect(); }
}, 'Add Redirect')
])
])
]), ]),
// Redirect list // Redirects Table
E('div', { 'class': 'haproxy-form-section' }, [ K.E('div', { 'class': 'kiss-card' }, [
E('h3', {}, 'Redirect Rules (' + redirects.length + ')'), K.E('div', { 'class': 'kiss-card-title' }, ['↪️ ', 'Redirect Rules (', String(redirects.length), ')']),
this.renderRedirectsTable(redirects) this.renderRedirectsTable(redirects)
]) ])
]); ]);
// Add CSS return KissTheme.wrap(content, 'admin/services/haproxy/acls');
var style = E('style', {}, `
@import url('/luci-static/resources/haproxy/dashboard.css');
`);
view.insertBefore(style, view.firstChild);
return KissTheme.wrap([view], 'admin/services/haproxy/acls');
}, },
renderAclsTable: function(acls, backends) { renderAclsTable: function(acls, backends) {
var self = this; var self = this;
var K = KissTheme;
if (acls.length === 0) { if (acls.length === 0) {
return E('p', { 'style': 'color: var(--text-color-medium, #666)' }, return K.E('div', { 'style': 'text-align: center; padding: 30px; color: var(--kiss-muted);' },
'No ACL rules configured.'); 'No ACL rules configured');
} }
var backendMap = {}; var backendMap = {};
backends.forEach(function(b) { backendMap[b.id] = b.name; }); backends.forEach(function(b) { backendMap[b.id] = b.name; });
return E('table', { 'class': 'haproxy-vhosts-table' }, [ return K.E('table', { 'style': 'width: 100%; border-collapse: collapse;' }, [
E('thead', {}, [ K.E('thead', {}, [
E('tr', {}, [ K.E('tr', { 'style': 'border-bottom: 1px solid var(--kiss-line, #1e293b);' }, [
E('th', {}, 'Name'), K.E('th', { 'style': 'padding: 10px 12px; text-align: left; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Name'),
E('th', {}, 'Type'), K.E('th', { 'style': 'padding: 10px 12px; text-align: left; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Type'),
E('th', {}, 'Pattern'), K.E('th', { 'style': 'padding: 10px 12px; text-align: left; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Pattern'),
E('th', {}, 'Backend'), K.E('th', { 'style': 'padding: 10px 12px; text-align: left; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Backend'),
E('th', {}, 'Status'), K.E('th', { 'style': 'padding: 10px 12px; text-align: left; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Status'),
E('th', { 'style': 'width: 100px' }, 'Actions') K.E('th', { 'style': 'padding: 10px 12px; text-align: right; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; width: 80px;' }, 'Actions')
]) ])
]), ]),
E('tbody', {}, acls.map(function(acl) { K.E('tbody', {}, acls.map(function(acl) {
return E('tr', { 'data-id': acl.id }, [ return K.E('tr', { 'data-id': acl.id, 'style': 'border-bottom: 1px solid var(--kiss-line, #1e293b);' }, [
E('td', {}, E('strong', {}, acl.name)), K.E('td', { 'style': 'padding: 12px;' }, K.E('strong', {}, acl.name)),
E('td', {}, E('code', {}, acl.type)), K.E('td', { 'style': 'padding: 12px; font-family: monospace; font-size: 12px;' }, acl.type),
E('td', {}, E('code', {}, acl.pattern)), K.E('td', { 'style': 'padding: 12px; font-family: monospace; font-size: 12px;' }, acl.pattern),
E('td', {}, backendMap[acl.backend] || acl.backend || '-'), K.E('td', { 'style': 'padding: 12px;' }, backendMap[acl.backend] || acl.backend || '-'),
E('td', {}, E('span', { K.E('td', { 'style': 'padding: 12px;' }, K.badge(acl.enabled ? 'Enabled' : 'Disabled', acl.enabled ? 'green' : 'red')),
'class': 'haproxy-badge ' + (acl.enabled ? 'enabled' : 'disabled') K.E('td', { 'style': 'padding: 12px; text-align: right;' }, [
}, acl.enabled ? 'Enabled' : 'Disabled')), K.E('button', {
E('td', {}, [ 'class': 'kiss-btn kiss-btn-red',
E('button', { 'style': 'padding: 4px 10px; font-size: 12px;',
'class': 'cbi-button cbi-button-remove',
'click': function() { self.handleDeleteAcl(acl); } 'click': function() { self.handleDeleteAcl(acl); }
}, 'Delete') }, '🗑️')
]) ])
]); ]);
})) }))
@ -216,37 +208,37 @@ return view.extend({
renderRedirectsTable: function(redirects) { renderRedirectsTable: function(redirects) {
var self = this; var self = this;
var K = KissTheme;
if (redirects.length === 0) { if (redirects.length === 0) {
return E('p', { 'style': 'color: var(--text-color-medium, #666)' }, return K.E('div', { 'style': 'text-align: center; padding: 30px; color: var(--kiss-muted);' },
'No redirect rules configured.'); 'No redirect rules configured');
} }
return E('table', { 'class': 'haproxy-vhosts-table' }, [ return K.E('table', { 'style': 'width: 100%; border-collapse: collapse;' }, [
E('thead', {}, [ K.E('thead', {}, [
E('tr', {}, [ K.E('tr', { 'style': 'border-bottom: 1px solid var(--kiss-line, #1e293b);' }, [
E('th', {}, 'Name'), K.E('th', { 'style': 'padding: 10px 12px; text-align: left; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Name'),
E('th', {}, 'Match Host'), K.E('th', { 'style': 'padding: 10px 12px; text-align: left; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Match Host'),
E('th', {}, 'Target'), K.E('th', { 'style': 'padding: 10px 12px; text-align: left; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Target'),
E('th', {}, 'Code'), K.E('th', { 'style': 'padding: 10px 12px; text-align: left; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Code'),
E('th', {}, 'Status'), K.E('th', { 'style': 'padding: 10px 12px; text-align: left; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Status'),
E('th', { 'style': 'width: 100px' }, 'Actions') K.E('th', { 'style': 'padding: 10px 12px; text-align: right; font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; width: 80px;' }, 'Actions')
]) ])
]), ]),
E('tbody', {}, redirects.map(function(r) { K.E('tbody', {}, redirects.map(function(r) {
return E('tr', { 'data-id': r.id }, [ return K.E('tr', { 'data-id': r.id, 'style': 'border-bottom: 1px solid var(--kiss-line, #1e293b);' }, [
E('td', {}, E('strong', {}, r.name)), K.E('td', { 'style': 'padding: 12px;' }, K.E('strong', {}, r.name)),
E('td', {}, E('code', {}, r.match_host)), K.E('td', { 'style': 'padding: 12px; font-family: monospace; font-size: 12px;' }, r.match_host),
E('td', {}, r.strip_www ? 'Strip www' : (r.target_host || '-')), K.E('td', { 'style': 'padding: 12px;' }, r.strip_www ? 'Strip www' : (r.target_host || '-')),
E('td', {}, r.code), K.E('td', { 'style': 'padding: 12px;' }, K.badge(r.code, 'blue')),
E('td', {}, E('span', { K.E('td', { 'style': 'padding: 12px;' }, K.badge(r.enabled ? 'Enabled' : 'Disabled', r.enabled ? 'green' : 'red')),
'class': 'haproxy-badge ' + (r.enabled ? 'enabled' : 'disabled') K.E('td', { 'style': 'padding: 12px; text-align: right;' }, [
}, r.enabled ? 'Enabled' : 'Disabled')), K.E('button', {
E('td', {}, [ 'class': 'kiss-btn kiss-btn-red',
E('button', { 'style': 'padding: 4px 10px; font-size: 12px;',
'class': 'cbi-button cbi-button-remove',
'click': function() { self.handleDeleteRedirect(r); } 'click': function() { self.handleDeleteRedirect(r); }
}, 'Delete') }, '🗑️')
]) ])
]); ]);
})) }))
@ -254,50 +246,60 @@ return view.extend({
}, },
handleAddAcl: function() { handleAddAcl: function() {
var self = this;
var name = document.getElementById('acl-name').value.trim(); var name = document.getElementById('acl-name').value.trim();
var type = document.getElementById('acl-type').value; var type = document.getElementById('acl-type').value;
var pattern = document.getElementById('acl-pattern').value.trim(); var pattern = document.getElementById('acl-pattern').value.trim();
var backend = document.getElementById('acl-backend').value; var backend = document.getElementById('acl-backend').value;
if (!name || !type || !pattern) { if (!name || !type || !pattern) {
ui.addNotification(null, E('p', {}, 'Name, type and pattern are required'), 'error'); self.showToast('Name, type and pattern are required', 'error');
return; return;
} }
return api.createAcl(name, type, pattern, backend, 1).then(function(res) { return api.createAcl(name, type, pattern, backend, 1).then(function(res) {
if (res.success) { if (res.success) {
ui.addNotification(null, E('p', {}, 'ACL rule created')); self.showToast('ACL rule created', 'success');
window.location.reload(); window.location.reload();
} else { } else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error'); self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error');
} }
}); });
}, },
handleDeleteAcl: function(acl) { handleDeleteAcl: function(acl) {
ui.showModal('Delete ACL', [ var self = this;
E('p', {}, 'Are you sure you want to delete ACL rule "' + acl.name + '"?'), var K = KissTheme;
E('div', { 'class': 'right' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), var modalContent = K.E('div', {}, [
E('button', { K.E('p', { 'style': 'margin: 0 0 12px;' }, 'Are you sure you want to delete this ACL rule?'),
'class': 'cbi-button cbi-button-negative', K.E('div', {
'style': 'padding: 12px 16px; background: var(--kiss-bg2, #111827); border-radius: 8px; font-family: monospace; margin-bottom: 20px;'
}, acl.name),
K.E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px;' }, [
K.E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'),
K.E('button', {
'class': 'kiss-btn kiss-btn-red',
'click': function() { 'click': function() {
ui.hideModal(); ui.hideModal();
api.deleteAcl(acl.id).then(function(res) { api.deleteAcl(acl.id).then(function(res) {
if (res.success) { if (res.success) {
ui.addNotification(null, E('p', {}, 'ACL deleted')); self.showToast('ACL deleted', 'success');
window.location.reload(); window.location.reload();
} else { } else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error'); self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error');
} }
}); });
} }
}, 'Delete') }, '🗑️ Delete')
]) ])
]); ]);
ui.showModal('Delete ACL', [modalContent]);
}, },
handleAddRedirect: function() { handleAddRedirect: function() {
var self = this;
var name = document.getElementById('redirect-name').value.trim(); var name = document.getElementById('redirect-name').value.trim();
var matchHost = document.getElementById('redirect-match').value.trim(); var matchHost = document.getElementById('redirect-match').value.trim();
var targetHost = document.getElementById('redirect-target').value.trim(); var targetHost = document.getElementById('redirect-target').value.trim();
@ -305,41 +307,69 @@ return view.extend({
var code = parseInt(document.getElementById('redirect-code').value) || 301; var code = parseInt(document.getElementById('redirect-code').value) || 301;
if (!name || !matchHost) { if (!name || !matchHost) {
ui.addNotification(null, E('p', {}, 'Name and match host pattern are required'), 'error'); self.showToast('Name and match host pattern are required', 'error');
return; return;
} }
return api.createRedirect(name, matchHost, targetHost, stripWww, code, 1).then(function(res) { return api.createRedirect(name, matchHost, targetHost, stripWww, code, 1).then(function(res) {
if (res.success) { if (res.success) {
ui.addNotification(null, E('p', {}, 'Redirect rule created')); self.showToast('Redirect rule created', 'success');
window.location.reload(); window.location.reload();
} else { } else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error'); self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error');
} }
}); });
}, },
handleDeleteRedirect: function(r) { handleDeleteRedirect: function(r) {
ui.showModal('Delete Redirect', [ var self = this;
E('p', {}, 'Are you sure you want to delete redirect rule "' + r.name + '"?'), var K = KissTheme;
E('div', { 'class': 'right' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), var modalContent = K.E('div', {}, [
E('button', { K.E('p', { 'style': 'margin: 0 0 12px;' }, 'Are you sure you want to delete this redirect rule?'),
'class': 'cbi-button cbi-button-negative', K.E('div', {
'style': 'padding: 12px 16px; background: var(--kiss-bg2, #111827); border-radius: 8px; font-family: monospace; margin-bottom: 20px;'
}, r.name),
K.E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px;' }, [
K.E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'),
K.E('button', {
'class': 'kiss-btn kiss-btn-red',
'click': function() { 'click': function() {
ui.hideModal(); ui.hideModal();
api.deleteRedirect(r.id).then(function(res) { api.deleteRedirect(r.id).then(function(res) {
if (res.success) { if (res.success) {
ui.addNotification(null, E('p', {}, 'Redirect deleted')); self.showToast('Redirect deleted', 'success');
window.location.reload(); window.location.reload();
} else { } else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error'); self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error');
} }
}); });
} }
}, 'Delete') }, '🗑️ Delete')
]) ])
]); ]);
ui.showModal('Delete Redirect', [modalContent]);
},
showToast: function(message, type) {
var existing = document.querySelector('.kiss-toast');
if (existing) existing.remove();
var icons = { success: '✅', error: '❌', warning: '⚠️' };
var colors = {
success: 'var(--kiss-green, #00C853)',
error: 'var(--kiss-red, #FF1744)',
warning: 'var(--kiss-yellow, #fbbf24)'
};
var toast = document.createElement('div');
toast.className = 'kiss-toast';
toast.style.cssText = 'position: fixed; bottom: 80px; right: 20px; padding: 12px 20px; border-radius: 8px; background: var(--kiss-card, #161e2e); border: 1px solid ' + (colors[type] || 'var(--kiss-line)') + '; color: var(--kiss-text, #e2e8f0); font-size: 14px; display: flex; align-items: center; gap: 10px; z-index: 9999; box-shadow: 0 4px 20px rgba(0,0,0,0.3);';
toast.innerHTML = (icons[type] || '') + ' ' + message;
document.body.appendChild(toast);
setTimeout(function() { toast.remove(); }, 4000);
}, },
handleSaveApply: null, handleSaveApply: null,

View File

@ -5,6 +5,11 @@
'require haproxy.api as api'; 'require haproxy.api as api';
'require secubox/kiss-theme'; 'require secubox/kiss-theme';
/**
* HAProxy Settings - KISS Style
* Copyright (C) 2025 CyberMind.fr
*/
return view.extend({ return view.extend({
load: function() { load: function() {
return api.getSettings(); return api.getSettings();
@ -12,339 +17,179 @@ return view.extend({
render: function(settings) { render: function(settings) {
var self = this; var self = this;
var K = KissTheme;
settings = settings || {}; settings = settings || {};
var main = settings.main || {}; var main = settings.main || {};
var defaults = settings.defaults || {}; var defaults = settings.defaults || {};
var acme = settings.acme || {}; var acme = settings.acme || {};
var view = E('div', { 'class': 'cbi-map' }, [ var inputStyle = 'width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px;';
E('h2', {}, 'Settings'), var labelStyle = 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase; display: block; margin-bottom: 6px;';
E('p', {}, 'Configure HAProxy service settings.'),
// Main settings var content = K.E('div', {}, [
E('div', { 'class': 'haproxy-form-section' }, [ // Page Header
E('h3', {}, 'Service Settings'), K.E('div', { 'style': 'margin-bottom: 20px;' }, [
K.E('h2', { 'style': 'margin: 0; font-size: 24px; display: flex; align-items: center; gap: 10px;' }, [
E('div', { 'class': 'cbi-value' }, [ K.E('span', {}, '⚙️'),
E('label', { 'class': 'cbi-value-title' }, 'Enable Service'), 'Settings'
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'checkbox',
'id': 'main-enabled',
'checked': main.enabled
}),
E('label', { 'for': 'main-enabled' }, ' Start HAProxy on boot')
])
]), ]),
K.E('p', { 'style': 'margin: 4px 0 0; color: var(--kiss-muted, #94a3b8); font-size: 14px;' },
'Configure HAProxy service settings')
]),
E('div', { 'class': 'cbi-value' }, [ // Service Settings
E('label', { 'class': 'cbi-value-title' }, 'HTTP Port'), K.E('div', { 'class': 'kiss-card' }, [
E('div', { 'class': 'cbi-value-field' }, [ K.E('div', { 'class': 'kiss-card-title' }, ['🔧 ', 'Service Settings']),
E('input', { K.E('div', { 'class': 'kiss-grid kiss-grid-2', 'style': 'gap: 16px;' }, [
'type': 'number', K.E('div', { 'style': 'grid-column: span 2;' }, [
'id': 'main-http-port', K.E('label', { 'style': 'display: flex; align-items: center; gap: 8px; cursor: pointer;' }, [
'class': 'cbi-input-text', K.E('input', { 'type': 'checkbox', 'id': 'main-enabled', 'checked': main.enabled }),
'value': main.http_port || 80, '✅ Start HAProxy on boot'
'min': '1', ])
'max': '65535' ]),
}) K.E('div', {}, [
]) K.E('label', { 'style': labelStyle }, 'HTTP Port'),
]), K.E('input', { 'type': 'number', 'id': 'main-http-port', 'value': main.http_port || 80, 'min': '1', 'max': '65535', 'style': inputStyle })
]),
E('div', { 'class': 'cbi-value' }, [ K.E('div', {}, [
E('label', { 'class': 'cbi-value-title' }, 'HTTPS Port'), K.E('label', { 'style': labelStyle }, 'HTTPS Port'),
E('div', { 'class': 'cbi-value-field' }, [ K.E('input', { 'type': 'number', 'id': 'main-https-port', 'value': main.https_port || 443, 'min': '1', 'max': '65535', 'style': inputStyle })
E('input', { ]),
'type': 'number', K.E('div', {}, [
'id': 'main-https-port', K.E('label', { 'style': labelStyle }, 'Max Connections'),
'class': 'cbi-input-text', K.E('input', { 'type': 'number', 'id': 'main-maxconn', 'value': main.maxconn || 4096, 'min': '100', 'max': '100000', 'style': inputStyle })
'value': main.https_port || 443, ]),
'min': '1', K.E('div', {}, [
'max': '65535' K.E('label', { 'style': labelStyle }, 'Memory Limit'),
}) K.E('input', { 'type': 'text', 'id': 'main-memory', 'value': main.memory_limit || '256M', 'placeholder': '256M', 'style': inputStyle })
]) ]),
]), K.E('div', { 'style': 'grid-column: span 2;' }, [
K.E('label', { 'style': labelStyle }, 'Log Level'),
E('div', { 'class': 'cbi-value' }, [ K.E('select', { 'id': 'main-log-level', 'style': inputStyle }, [
E('label', { 'class': 'cbi-value-title' }, 'Max Connections'), K.E('option', { 'value': 'emerg', 'selected': main.log_level === 'emerg' }, 'Emergency'),
E('div', { 'class': 'cbi-value-field' }, [ K.E('option', { 'value': 'alert', 'selected': main.log_level === 'alert' }, 'Alert'),
E('input', { K.E('option', { 'value': 'crit', 'selected': main.log_level === 'crit' }, 'Critical'),
'type': 'number', K.E('option', { 'value': 'err', 'selected': main.log_level === 'err' }, 'Error'),
'id': 'main-maxconn', K.E('option', { 'value': 'warning', 'selected': main.log_level === 'warning' || !main.log_level }, 'Warning'),
'class': 'cbi-input-text', K.E('option', { 'value': 'notice', 'selected': main.log_level === 'notice' }, 'Notice'),
'value': main.maxconn || 4096, K.E('option', { 'value': 'info', 'selected': main.log_level === 'info' }, 'Info'),
'min': '100', K.E('option', { 'value': 'debug', 'selected': main.log_level === 'debug' }, 'Debug')
'max': '100000'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Memory Limit'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'main-memory',
'class': 'cbi-input-text',
'value': main.memory_limit || '256M',
'placeholder': '256M'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Log Level'),
E('div', { 'class': 'cbi-value-field' }, [
E('select', {
'id': 'main-log-level',
'class': 'cbi-input-select'
}, [
E('option', { 'value': 'emerg', 'selected': main.log_level === 'emerg' }, 'Emergency'),
E('option', { 'value': 'alert', 'selected': main.log_level === 'alert' }, 'Alert'),
E('option', { 'value': 'crit', 'selected': main.log_level === 'crit' }, 'Critical'),
E('option', { 'value': 'err', 'selected': main.log_level === 'err' }, 'Error'),
E('option', { 'value': 'warning', 'selected': main.log_level === 'warning' || !main.log_level }, 'Warning'),
E('option', { 'value': 'notice', 'selected': main.log_level === 'notice' }, 'Notice'),
E('option', { 'value': 'info', 'selected': main.log_level === 'info' }, 'Info'),
E('option', { 'value': 'debug', 'selected': main.log_level === 'debug' }, 'Debug')
]) ])
]) ])
]) ])
]), ]),
// Stats settings // Statistics Dashboard
E('div', { 'class': 'haproxy-form-section' }, [ K.E('div', { 'class': 'kiss-card' }, [
E('h3', {}, 'Statistics Dashboard'), K.E('div', { 'class': 'kiss-card-title' }, ['📊 ', 'Statistics Dashboard']),
K.E('div', { 'class': 'kiss-grid kiss-grid-2', 'style': 'gap: 16px;' }, [
E('div', { 'class': 'cbi-value' }, [ K.E('div', { 'style': 'grid-column: span 2;' }, [
E('label', { 'class': 'cbi-value-title' }, 'Enable Stats'), K.E('label', { 'style': 'display: flex; align-items: center; gap: 8px; cursor: pointer;' }, [
E('div', { 'class': 'cbi-value-field' }, [ K.E('input', { 'type': 'checkbox', 'id': 'main-stats-enabled', 'checked': main.stats_enabled }),
E('input', { '📊 Enable statistics dashboard'
'type': 'checkbox', ])
'id': 'main-stats-enabled', ]),
'checked': main.stats_enabled K.E('div', {}, [
}), K.E('label', { 'style': labelStyle }, 'Stats Port'),
E('label', { 'for': 'main-stats-enabled' }, ' Enable statistics dashboard') K.E('input', { 'type': 'number', 'id': 'main-stats-port', 'value': main.stats_port || 8404, 'min': '1', 'max': '65535', 'style': inputStyle })
]) ]),
]), K.E('div', {}, [
K.E('label', { 'style': labelStyle }, 'Stats Username'),
E('div', { 'class': 'cbi-value' }, [ K.E('input', { 'type': 'text', 'id': 'main-stats-user', 'value': main.stats_user || 'admin', 'style': inputStyle })
E('label', { 'class': 'cbi-value-title' }, 'Stats Port'), ]),
E('div', { 'class': 'cbi-value-field' }, [ K.E('div', { 'style': 'grid-column: span 2;' }, [
E('input', { K.E('label', { 'style': labelStyle }, 'Stats Password'),
'type': 'number', K.E('input', { 'type': 'password', 'id': 'main-stats-password', 'value': main.stats_password || '', 'style': inputStyle })
'id': 'main-stats-port',
'class': 'cbi-input-text',
'value': main.stats_port || 8404,
'min': '1',
'max': '65535'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Stats Username'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'main-stats-user',
'class': 'cbi-input-text',
'value': main.stats_user || 'admin'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Stats Password'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'password',
'id': 'main-stats-password',
'class': 'cbi-input-text',
'value': main.stats_password || ''
})
]) ])
]) ])
]), ]),
// Timeouts // Timeouts
E('div', { 'class': 'haproxy-form-section' }, [ K.E('div', { 'class': 'kiss-card' }, [
E('h3', {}, 'Timeouts'), K.E('div', { 'class': 'kiss-card-title' }, ['⏱️ ', 'Timeouts']),
K.E('div', { 'class': 'kiss-grid kiss-grid-3', 'style': 'gap: 16px;' }, [
E('div', { 'class': 'cbi-value' }, [ K.E('div', {}, [
E('label', { 'class': 'cbi-value-title' }, 'Connect Timeout'), K.E('label', { 'style': labelStyle }, 'Connect Timeout'),
E('div', { 'class': 'cbi-value-field' }, [ K.E('input', { 'type': 'text', 'id': 'defaults-timeout-connect', 'value': defaults.timeout_connect || '5s', 'placeholder': '5s', 'style': inputStyle })
E('input', { ]),
'type': 'text', K.E('div', {}, [
'id': 'defaults-timeout-connect', K.E('label', { 'style': labelStyle }, 'Client Timeout'),
'class': 'cbi-input-text', K.E('input', { 'type': 'text', 'id': 'defaults-timeout-client', 'value': defaults.timeout_client || '30s', 'placeholder': '30s', 'style': inputStyle })
'value': defaults.timeout_connect || '5s', ]),
'placeholder': '5s' K.E('div', {}, [
}) K.E('label', { 'style': labelStyle }, 'Server Timeout'),
]) K.E('input', { 'type': 'text', 'id': 'defaults-timeout-server', 'value': defaults.timeout_server || '30s', 'placeholder': '30s', 'style': inputStyle })
]), ]),
K.E('div', {}, [
E('div', { 'class': 'cbi-value' }, [ K.E('label', { 'style': labelStyle }, 'HTTP Request Timeout'),
E('label', { 'class': 'cbi-value-title' }, 'Client Timeout'), K.E('input', { 'type': 'text', 'id': 'defaults-timeout-http-request', 'value': defaults.timeout_http_request || '10s', 'placeholder': '10s', 'style': inputStyle })
E('div', { 'class': 'cbi-value-field' }, [ ]),
E('input', { K.E('div', {}, [
'type': 'text', K.E('label', { 'style': labelStyle }, 'HTTP Keep-Alive'),
'id': 'defaults-timeout-client', K.E('input', { 'type': 'text', 'id': 'defaults-timeout-http-keep-alive', 'value': defaults.timeout_http_keep_alive || '10s', 'placeholder': '10s', 'style': inputStyle })
'class': 'cbi-input-text', ]),
'value': defaults.timeout_client || '30s', K.E('div', {}, [
'placeholder': '30s' K.E('label', { 'style': labelStyle }, 'Retries'),
}) K.E('input', { 'type': 'number', 'id': 'defaults-retries', 'value': defaults.retries || 3, 'min': '0', 'max': '10', 'style': inputStyle })
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Server Timeout'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'defaults-timeout-server',
'class': 'cbi-input-text',
'value': defaults.timeout_server || '30s',
'placeholder': '30s'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'HTTP Request Timeout'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'defaults-timeout-http-request',
'class': 'cbi-input-text',
'value': defaults.timeout_http_request || '10s',
'placeholder': '10s'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'HTTP Keep-Alive'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'defaults-timeout-http-keep-alive',
'class': 'cbi-input-text',
'value': defaults.timeout_http_keep_alive || '10s',
'placeholder': '10s'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Retries'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'number',
'id': 'defaults-retries',
'class': 'cbi-input-text',
'value': defaults.retries || 3,
'min': '0',
'max': '10'
})
]) ])
]) ])
]), ]),
// ACME settings // ACME / Let's Encrypt
E('div', { 'class': 'haproxy-form-section' }, [ K.E('div', { 'class': 'kiss-card' }, [
E('h3', {}, 'ACME / Let\'s Encrypt'), K.E('div', { 'class': 'kiss-card-title' }, ['🔐 ', "ACME / Let's Encrypt"]),
K.E('div', { 'class': 'kiss-grid kiss-grid-2', 'style': 'gap: 16px;' }, [
E('div', { 'class': 'cbi-value' }, [ K.E('div', { 'style': 'grid-column: span 2;' }, [
E('label', { 'class': 'cbi-value-title' }, 'Enable ACME'), K.E('label', { 'style': 'display: flex; align-items: center; gap: 8px; cursor: pointer;' }, [
E('div', { 'class': 'cbi-value-field' }, [ K.E('input', { 'type': 'checkbox', 'id': 'acme-enabled', 'checked': acme.enabled }),
E('input', { '🔐 Enable automatic certificate management'
'type': 'checkbox',
'id': 'acme-enabled',
'checked': acme.enabled
}),
E('label', { 'for': 'acme-enabled' }, ' Enable automatic certificate management')
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Email'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'email',
'id': 'acme-email',
'class': 'cbi-input-text',
'value': acme.email || '',
'placeholder': 'admin@example.com'
}),
E('p', { 'class': 'cbi-value-description' },
'Required for Let\'s Encrypt certificate registration')
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Staging Mode'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'checkbox',
'id': 'acme-staging',
'checked': acme.staging
}),
E('label', { 'for': 'acme-staging' }, ' Use Let\'s Encrypt staging server (for testing)')
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Key Type'),
E('div', { 'class': 'cbi-value-field' }, [
E('select', {
'id': 'acme-key-type',
'class': 'cbi-input-select'
}, [
E('option', { 'value': 'ec-256', 'selected': acme.key_type === 'ec-256' || !acme.key_type }, 'EC-256 (recommended)'),
E('option', { 'value': 'ec-384', 'selected': acme.key_type === 'ec-384' }, 'EC-384'),
E('option', { 'value': 'rsa-2048', 'selected': acme.key_type === 'rsa-2048' }, 'RSA-2048'),
E('option', { 'value': 'rsa-4096', 'selected': acme.key_type === 'rsa-4096' }, 'RSA-4096')
]) ])
]) ]),
]), K.E('div', { 'style': 'grid-column: span 2;' }, [
K.E('label', { 'style': labelStyle }, 'Email'),
E('div', { 'class': 'cbi-value' }, [ K.E('input', { 'type': 'email', 'id': 'acme-email', 'value': acme.email || '', 'placeholder': 'admin@example.com', 'style': inputStyle }),
E('label', { 'class': 'cbi-value-title' }, 'Renew Before (days)'), K.E('small', { 'style': 'color: var(--kiss-muted); font-size: 11px; margin-top: 4px; display: block;' },
E('div', { 'class': 'cbi-value-field' }, [ "Required for Let's Encrypt certificate registration")
E('input', { ]),
'type': 'number', K.E('div', { 'style': 'grid-column: span 2;' }, [
'id': 'acme-renew-days', K.E('label', { 'style': 'display: flex; align-items: center; gap: 8px; cursor: pointer;' }, [
'class': 'cbi-input-text', K.E('input', { 'type': 'checkbox', 'id': 'acme-staging', 'checked': acme.staging }),
'value': acme.renew_days || 30, "🧪 Use Let's Encrypt staging server (for testing)"
'min': '1', ])
'max': '60' ]),
}), K.E('div', {}, [
E('p', { 'class': 'cbi-value-description' }, K.E('label', { 'style': labelStyle }, 'Key Type'),
K.E('select', { 'id': 'acme-key-type', 'style': inputStyle }, [
K.E('option', { 'value': 'ec-256', 'selected': acme.key_type === 'ec-256' || !acme.key_type }, 'EC-256 (recommended)'),
K.E('option', { 'value': 'ec-384', 'selected': acme.key_type === 'ec-384' }, 'EC-384'),
K.E('option', { 'value': 'rsa-2048', 'selected': acme.key_type === 'rsa-2048' }, 'RSA-2048'),
K.E('option', { 'value': 'rsa-4096', 'selected': acme.key_type === 'rsa-4096' }, 'RSA-4096')
])
]),
K.E('div', {}, [
K.E('label', { 'style': labelStyle }, 'Renew Before (days)'),
K.E('input', { 'type': 'number', 'id': 'acme-renew-days', 'value': acme.renew_days || 30, 'min': '1', 'max': '60', 'style': inputStyle }),
K.E('small', { 'style': 'color: var(--kiss-muted); font-size: 11px; margin-top: 4px; display: block;' },
'Renew certificate this many days before expiry') 'Renew certificate this many days before expiry')
]) ])
]) ])
]), ]),
// Save button // Save Button
E('div', { 'class': 'cbi-page-actions' }, [ K.E('div', { 'style': 'margin-top: 20px; text-align: right;' }, [
E('button', { K.E('button', {
'class': 'cbi-button cbi-button-apply', 'class': 'kiss-btn kiss-btn-green',
'style': 'padding: 12px 24px; font-size: 14px;',
'click': function() { self.handleSave(); } 'click': function() { self.handleSave(); }
}, 'Save & Apply') }, '💾 Save & Apply')
]) ])
]); ]);
// Add CSS return KissTheme.wrap(content, 'admin/services/haproxy/settings');
var style = E('style', {}, `
@import url('/luci-static/resources/haproxy/dashboard.css');
`);
view.insertBefore(style, view.firstChild);
return KissTheme.wrap([view], 'admin/services/haproxy/settings');
}, },
handleSave: function() { handleSave: function() {
var self = this;
var mainSettings = { var mainSettings = {
enabled: document.getElementById('main-enabled').checked ? 1 : 0, enabled: document.getElementById('main-enabled').checked ? 1 : 0,
http_port: parseInt(document.getElementById('main-http-port').value) || 80, http_port: parseInt(document.getElementById('main-http-port').value) || 80,
@ -377,13 +222,33 @@ return view.extend({
return api.saveSettings(mainSettings, defaultsSettings, acmeSettings).then(function(res) { return api.saveSettings(mainSettings, defaultsSettings, acmeSettings).then(function(res) {
if (res.success) { if (res.success) {
ui.addNotification(null, E('p', {}, 'Settings saved successfully')); self.showToast('Settings saved successfully', 'success');
} else { } else {
ui.addNotification(null, E('p', {}, 'Failed to save: ' + (res.error || 'Unknown error')), 'error'); self.showToast('Failed to save: ' + (res.error || 'Unknown error'), 'error');
} }
}); });
}, },
showToast: function(message, type) {
var existing = document.querySelector('.kiss-toast');
if (existing) existing.remove();
var icons = { success: '✅', error: '❌', warning: '⚠️' };
var colors = {
success: 'var(--kiss-green, #00C853)',
error: 'var(--kiss-red, #FF1744)',
warning: 'var(--kiss-yellow, #fbbf24)'
};
var toast = document.createElement('div');
toast.className = 'kiss-toast';
toast.style.cssText = 'position: fixed; bottom: 80px; right: 20px; padding: 12px 20px; border-radius: 8px; background: var(--kiss-card, #161e2e); border: 1px solid ' + (colors[type] || 'var(--kiss-line)') + '; color: var(--kiss-text, #e2e8f0); font-size: 14px; display: flex; align-items: center; gap: 10px; z-index: 9999; box-shadow: 0 4px 20px rgba(0,0,0,0.3);';
toast.innerHTML = (icons[type] || '') + ' ' + message;
document.body.appendChild(toast);
setTimeout(function() { toast.remove(); }, 4000);
},
handleSaveApply: null, handleSaveApply: null,
handleReset: null handleReset: null
}); });