diff --git a/package/secubox/luci-app-gitea/Makefile b/package/secubox/luci-app-gitea/Makefile index ece89ac0..d0900294 100644 --- a/package/secubox/luci-app-gitea/Makefile +++ b/package/secubox/luci-app-gitea/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-gitea PKG_VERSION:=1.0.0 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_ARCH:=all PKG_LICENSE:=Apache-2.0 diff --git a/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea b/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea index 0b88b701..0b4598fe 100644 --- a/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea +++ b/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea @@ -362,7 +362,12 @@ list_repos() { local repo_root="$data_path/git/repositories" if [ -d "$repo_root" ]; then - find "$repo_root" -name "*.git" -type d 2>/dev/null | while read -r repo; do + # Use temp file to avoid subshell issue with piped while loop + local tmpfile="/tmp/gitea-repos.$$" + find "$repo_root" -name "*.git" -type d 2>/dev/null > "$tmpfile" + + while read -r repo; do + [ -z "$repo" ] && continue local rel_path="${repo#$repo_root/}" local name=$(basename "$repo" .git) local owner=$(dirname "$rel_path") @@ -383,7 +388,9 @@ list_repos() { json_add_string "size" "$size" [ -n "$mtime" ] && json_add_int "mtime" "$mtime" json_close_object - done + done < "$tmpfile" + + rm -f "$tmpfile" fi json_close_array @@ -528,7 +535,7 @@ list_backups() { local backup_dir="$data_path/backups" if [ -d "$backup_dir" ]; then - ls -1 "$backup_dir"/*.tar.gz 2>/dev/null | while read -r backup; do + for backup in "$backup_dir"/*.tar.gz; do [ -f "$backup" ] || continue local name=$(basename "$backup") local size=$(ls -lh "$backup" 2>/dev/null | awk '{print $5}') diff --git a/package/secubox/luci-app-haproxy/Makefile b/package/secubox/luci-app-haproxy/Makefile index 7fb070c4..9f30b180 100644 --- a/package/secubox/luci-app-haproxy/Makefile +++ b/package/secubox/luci-app-haproxy/Makefile @@ -11,7 +11,7 @@ LUCI_PKGARCH:=all PKG_NAME:=luci-app-haproxy PKG_VERSION:=1.0.0 -PKG_RELEASE:=4 +PKG_RELEASE:=6 PKG_MAINTAINER:=CyberMind PKG_LICENSE:=MIT diff --git a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/api.js b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/api.js index 1916f410..9101378d 100644 --- a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/api.js +++ b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/api.js @@ -115,14 +115,14 @@ var callCreateServer = rpc.declare({ var callUpdateServer = rpc.declare({ object: 'luci.haproxy', method: 'update_server', - params: ['id', 'backend', 'name', 'address', 'port', 'weight', 'check', 'enabled'], + params: ['id', 'backend', 'name', 'address', 'port', 'weight', 'check', 'enabled', 'inline'], expect: {} }); var callDeleteServer = rpc.declare({ object: 'luci.haproxy', method: 'delete_server', - params: ['id'], + params: ['id', 'inline'], expect: {} }); @@ -293,11 +293,16 @@ function getDashboardData() { callListBackends(), callListCertificates() ]).then(function(results) { + // Handle both array and object responses from RPC + var vhosts = Array.isArray(results[1]) ? results[1] : (results[1] && results[1].vhosts) || []; + var backends = Array.isArray(results[2]) ? results[2] : (results[2] && results[2].backends) || []; + var certificates = Array.isArray(results[3]) ? results[3] : (results[3] && results[3].certificates) || []; + return { status: results[0], - vhosts: results[1].vhosts || [], - backends: results[2].backends || [], - certificates: results[3].certificates || [] + vhosts: vhosts, + backends: backends, + certificates: certificates }; }); } diff --git a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/dashboard.css b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/dashboard.css index f3438464..ab0e9645 100644 --- a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/dashboard.css +++ b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/haproxy/dashboard.css @@ -588,6 +588,12 @@ code, gap: 8px; } +.hp-server-actions { + display: flex; + align-items: center; + gap: 6px; +} + .hp-server-weight { font-size: 12px; padding: 4px 8px; @@ -596,6 +602,21 @@ code, color: var(--hp-text-secondary); } +.hp-backend-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: var(--hp-bg-secondary); + border-top: 1px solid var(--hp-border); +} + +.hp-badge-secondary { + background: var(--hp-bg-tertiary); + color: var(--hp-text-secondary); + border: 1px solid var(--hp-border); +} + /* === Certificate List === */ .hp-cert-list { display: flex; diff --git a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/backends.js b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/backends.js index f1d64943..7bc7c5a6 100644 --- a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/backends.js +++ b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/backends.js @@ -4,9 +4,23 @@ 'require ui'; 'require haproxy.api as api'; +/** + * HAProxy Backends Management + * Copyright (C) 2025 CyberMind.fr + */ + return view.extend({ + title: _('Backends'), + load: function() { - return api.listBackends().then(function(backends) { + // Load CSS + var cssLink = document.createElement('link'); + cssLink.rel = 'stylesheet'; + cssLink.href = L.resource('haproxy/dashboard.css'); + document.head.appendChild(cssLink); + + return api.listBackends().then(function(result) { + var backends = (result && result.backends) || result || []; return Promise.all([ Promise.resolve(backends), api.listServers('') @@ -17,7 +31,8 @@ return view.extend({ render: function(data) { var self = this; var backends = data[0] || []; - var servers = data[1] || []; + var serversResult = data[1] || {}; + var servers = (serversResult && serversResult.servers) || serversResult || []; // Group servers by backend var serversByBackend = {}; @@ -28,42 +43,258 @@ return view.extend({ serversByBackend[s.backend].push(s); }); - var view = E('div', { 'class': 'cbi-map' }, [ - E('h2', {}, 'Backends'), - E('p', {}, 'Manage backend server pools and load balancing settings.'), + return E('div', { 'class': 'haproxy-dashboard' }, [ + // Page Header + E('div', { 'class': 'hp-page-header' }, [ + E('div', {}, [ + E('h1', { 'class': 'hp-page-title' }, [ + E('span', { 'class': 'hp-page-title-icon' }, '\u{1F5C4}'), + 'Backends' + ]), + E('p', { 'class': 'hp-page-subtitle' }, 'Manage backend server pools and load balancing settings') + ]), + E('a', { + 'href': L.url('admin/services/haproxy/overview'), + 'class': 'hp-btn hp-btn-secondary' + }, ['\u2190', ' Back to Overview']) + ]), - // Add backend form - E('div', { 'class': 'haproxy-form-section' }, [ - E('h3', {}, 'Add Backend'), + // Add Backend Card + E('div', { 'class': 'hp-card' }, [ + E('div', { 'class': 'hp-card-header' }, [ + E('div', { 'class': 'hp-card-title' }, [ + E('span', { 'class': 'hp-card-title-icon' }, '\u2795'), + 'Add Backend' + ]) + ]), + E('div', { 'class': 'hp-card-body' }, [ + E('div', { 'class': 'hp-grid hp-grid-2', 'style': 'gap: 16px;' }, [ + E('div', { 'class': 'hp-form-group' }, [ + E('label', { 'class': 'hp-form-label' }, 'Name'), + E('input', { + 'type': 'text', + 'id': 'new-backend-name', + 'class': 'hp-form-input', + 'placeholder': 'web-servers' + }) + ]), + E('div', { 'class': 'hp-form-group' }, [ + E('label', { 'class': 'hp-form-label' }, 'Mode'), + E('select', { 'id': 'new-backend-mode', 'class': 'hp-form-input' }, [ + E('option', { 'value': 'http', 'selected': true }, 'HTTP'), + E('option', { 'value': 'tcp' }, 'TCP') + ]) + ]), + E('div', { 'class': 'hp-form-group' }, [ + E('label', { 'class': 'hp-form-label' }, 'Balance Algorithm'), + E('select', { 'id': 'new-backend-balance', 'class': 'hp-form-input' }, [ + E('option', { 'value': 'roundrobin', 'selected': true }, 'Round Robin'), + E('option', { 'value': 'leastconn' }, 'Least Connections'), + E('option', { 'value': 'source' }, 'Source IP Hash'), + E('option', { 'value': 'uri' }, 'URI Hash'), + E('option', { 'value': 'first' }, 'First Available') + ]) + ]), + E('div', { 'class': 'hp-form-group' }, [ + E('label', { 'class': 'hp-form-label' }, 'Health Check (optional)'), + E('input', { + 'type': 'text', + 'id': 'new-backend-health', + 'class': 'hp-form-input', + 'placeholder': 'httpchk GET /health' + }) + ]) + ]), + E('button', { + 'class': 'hp-btn hp-btn-primary', + 'style': 'margin-top: 16px;', + 'click': function() { self.handleAddBackend(); } + }, ['\u2795', ' Add Backend']) + ]) + ]), + + // Backends List + E('div', { 'class': 'hp-card' }, [ + E('div', { 'class': 'hp-card-header' }, [ + E('div', { 'class': 'hp-card-title' }, [ + E('span', { 'class': 'hp-card-title-icon' }, '\u{1F4CB}'), + 'Configured Backends (' + backends.length + ')' + ]) + ]), + E('div', { 'class': 'hp-card-body' }, + backends.length === 0 ? [ + E('div', { 'class': 'hp-empty' }, [ + E('div', { 'class': 'hp-empty-icon' }, '\u{1F5C4}'), + E('div', { 'class': 'hp-empty-text' }, 'No backends configured'), + E('div', { 'class': 'hp-empty-hint' }, 'Add a backend above to create a server pool') + ]) + ] : [ + E('div', { 'class': 'hp-backends-grid' }, + backends.map(function(backend) { + return self.renderBackendCard(backend, serversByBackend[backend.id] || []); + }) + ) + ] + ) + ]) + ]); + }, + + renderBackendCard: function(backend, servers) { + var self = this; + + return E('div', { 'class': 'hp-backend-card', 'data-id': backend.id }, [ + // Header + E('div', { 'class': 'hp-backend-header' }, [ + E('div', {}, [ + E('h4', { 'style': 'margin: 0 0 4px 0;' }, backend.name), + E('small', { 'style': 'color: var(--hp-text-muted);' }, [ + backend.mode.toUpperCase(), + ' \u2022 ', + this.getBalanceLabel(backend.balance) + ]) + ]), + E('div', { 'style': 'display: flex; gap: 8px; align-items: center;' }, [ + E('span', { + 'class': 'hp-badge ' + (backend.enabled ? 'hp-badge-success' : 'hp-badge-danger') + }, backend.enabled ? 'Enabled' : 'Disabled'), + E('button', { + 'class': 'hp-btn hp-btn-sm hp-btn-primary', + 'click': function() { self.showEditBackendModal(backend); } + }, '\u270F') + ]) + ]), + + // Health check info + backend.health_check ? E('div', { 'style': 'padding: 8px 16px; background: var(--hp-bg-tertiary, #f5f5f5); font-size: 12px; color: var(--hp-text-muted);' }, [ + '\u{1F3E5} Health Check: ', + E('code', {}, backend.health_check) + ]) : null, + + // Servers + E('div', { 'class': 'hp-backend-servers' }, + servers.length === 0 ? [ + E('div', { 'style': 'padding: 20px; text-align: center; color: var(--hp-text-muted);' }, [ + E('div', {}, '\u{1F4E6} No servers configured'), + E('small', {}, 'Add a server to this backend') + ]) + ] : servers.map(function(server) { + return E('div', { 'class': 'hp-server-item' }, [ + E('div', { 'class': 'hp-server-info' }, [ + E('span', { 'class': 'hp-server-name' }, server.name), + E('span', { 'class': 'hp-server-address' }, server.address + ':' + server.port) + ]), + E('div', { 'class': 'hp-server-actions' }, [ + E('span', { 'class': 'hp-badge hp-badge-secondary', 'style': 'font-size: 11px;' }, 'W:' + server.weight), + server.check ? E('span', { 'class': 'hp-badge hp-badge-info', 'style': 'font-size: 11px;' }, '\u2713 Check') : null, + E('button', { + 'class': 'hp-btn hp-btn-sm hp-btn-secondary', + 'style': 'padding: 2px 6px;', + 'click': function() { self.showEditServerModal(server, backend); } + }, '\u270F'), + E('button', { + 'class': 'hp-btn hp-btn-sm hp-btn-danger', + 'style': 'padding: 2px 6px;', + 'click': function() { self.handleDeleteServer(server); } + }, '\u2715') + ]) + ]); + }) + ), + + // Footer Actions + E('div', { 'class': 'hp-backend-footer' }, [ + E('button', { + 'class': 'hp-btn hp-btn-sm hp-btn-primary', + 'click': function() { self.showAddServerModal(backend); } + }, ['\u2795', ' Add Server']), + E('div', { 'style': 'display: flex; gap: 8px;' }, [ + E('button', { + 'class': 'hp-btn hp-btn-sm ' + (backend.enabled ? 'hp-btn-secondary' : 'hp-btn-success'), + 'click': function() { self.handleToggleBackend(backend); } + }, backend.enabled ? 'Disable' : 'Enable'), + E('button', { + 'class': 'hp-btn hp-btn-sm hp-btn-danger', + 'click': function() { self.handleDeleteBackend(backend); } + }, 'Delete') + ]) + ]) + ]); + }, + + getBalanceLabel: function(balance) { + var labels = { + 'roundrobin': 'Round Robin', + 'leastconn': 'Least Connections', + 'source': 'Source IP', + 'uri': 'URI Hash', + 'first': 'First Available' + }; + return labels[balance] || balance; + }, + + handleAddBackend: function() { + var self = this; + var name = document.getElementById('new-backend-name').value.trim(); + var mode = document.getElementById('new-backend-mode').value; + var balance = document.getElementById('new-backend-balance').value; + var healthCheck = document.getElementById('new-backend-health').value.trim(); + + if (!name) { + self.showToast('Backend name is required', 'error'); + return; + } + + if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) { + self.showToast('Invalid backend name format', 'error'); + return; + } + + return api.createBackend(name, mode, balance, healthCheck, 1).then(function(res) { + if (res.success) { + self.showToast('Backend "' + name + '" created', 'success'); + window.location.reload(); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + }, + + showEditBackendModal: function(backend) { + var self = this; + + ui.showModal('Edit Backend: ' + backend.name, [ + E('div', { 'style': 'max-width: 500px;' }, [ E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, 'Name'), E('div', { 'class': 'cbi-value-field' }, [ E('input', { 'type': 'text', - 'id': 'new-backend-name', + 'id': 'edit-backend-name', 'class': 'cbi-input-text', - 'placeholder': 'web-servers' + 'value': backend.name, + 'style': 'width: 100%;' }) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, 'Mode'), E('div', { 'class': 'cbi-value-field' }, [ - E('select', { 'id': 'new-backend-mode', 'class': 'cbi-input-select' }, [ - E('option', { 'value': 'http', 'selected': true }, 'HTTP'), - E('option', { 'value': 'tcp' }, 'TCP') + E('select', { 'id': 'edit-backend-mode', 'class': 'cbi-input-select', 'style': 'width: 100%;' }, [ + E('option', { 'value': 'http', 'selected': backend.mode === 'http' }, 'HTTP'), + E('option', { 'value': 'tcp', 'selected': backend.mode === 'tcp' }, 'TCP') ]) ]) ]), E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Balance'), + E('label', { 'class': 'cbi-value-title' }, 'Balance Algorithm'), E('div', { 'class': 'cbi-value-field' }, [ - E('select', { 'id': 'new-backend-balance', 'class': 'cbi-input-select' }, [ - E('option', { 'value': 'roundrobin', 'selected': true }, 'Round Robin'), - E('option', { 'value': 'leastconn' }, 'Least Connections'), - E('option', { 'value': 'source' }, 'Source IP Hash'), - E('option', { 'value': 'uri' }, 'URI Hash'), - E('option', { 'value': 'first' }, 'First Available') + E('select', { 'id': 'edit-backend-balance', 'class': 'cbi-input-select', 'style': 'width: 100%;' }, [ + E('option', { 'value': 'roundrobin', 'selected': backend.balance === 'roundrobin' }, 'Round Robin'), + E('option', { 'value': 'leastconn', 'selected': backend.balance === 'leastconn' }, 'Least Connections'), + E('option', { 'value': 'source', 'selected': backend.balance === 'source' }, 'Source IP Hash'), + E('option', { 'value': 'uri', 'selected': backend.balance === 'uri' }, 'URI Hash'), + E('option', { 'value': 'first', 'selected': backend.balance === 'first' }, 'First Available') ]) ]) ]), @@ -72,135 +303,98 @@ return view.extend({ E('div', { 'class': 'cbi-value-field' }, [ E('input', { 'type': 'text', - 'id': 'new-backend-health', + 'id': 'edit-backend-health', 'class': 'cbi-input-text', - 'placeholder': 'httpchk GET /health (optional)' + 'value': backend.health_check || '', + 'placeholder': 'httpchk GET /health', + 'style': 'width: 100%;' }) ]) ]), E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, ''), + E('label', { 'class': 'cbi-value-title' }, 'Status'), E('div', { 'class': 'cbi-value-field' }, [ - E('button', { - 'class': 'cbi-button cbi-button-add', - 'click': function() { self.handleAddBackend(); } - }, 'Add Backend') + E('label', {}, [ + E('input', { 'type': 'checkbox', 'id': 'edit-backend-enabled', 'checked': backend.enabled }), + ' Enabled' + ]) ]) ]) ]), + E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px; margin-top: 16px;' }, [ + E('button', { + 'class': 'hp-btn hp-btn-secondary', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'hp-btn hp-btn-primary', + 'click': function() { + var name = document.getElementById('edit-backend-name').value.trim(); + var mode = document.getElementById('edit-backend-mode').value; + var balance = document.getElementById('edit-backend-balance').value; + var healthCheck = document.getElementById('edit-backend-health').value.trim(); + var enabled = document.getElementById('edit-backend-enabled').checked ? 1 : 0; - // Backends list - E('div', { 'class': 'haproxy-form-section' }, [ - E('h3', {}, 'Configured Backends (' + backends.length + ')'), - E('div', { 'class': 'haproxy-backends-grid' }, - backends.length === 0 - ? E('p', { 'style': 'color: var(--text-color-medium, #666)' }, 'No backends configured.') - : backends.map(function(backend) { - return self.renderBackendCard(backend, serversByBackend[backend.id] || []); - }) - ) + if (!name) { + self.showToast('Backend name is required', 'error'); + return; + } + + ui.hideModal(); + api.updateBackend(backend.id, name, mode, balance, healthCheck, enabled).then(function(res) { + if (res.success) { + self.showToast('Backend updated', 'success'); + window.location.reload(); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + } + }, 'Save Changes') ]) ]); - - // Add CSS - var style = E('style', {}, ` - @import url('/luci-static/resources/haproxy/dashboard.css'); - `); - view.insertBefore(style, view.firstChild); - - return view; }, - renderBackendCard: function(backend, servers) { + handleToggleBackend: function(backend) { var self = this; + var newEnabled = backend.enabled ? 0 : 1; + var action = newEnabled ? 'enabled' : 'disabled'; - return E('div', { 'class': 'haproxy-backend-card', 'data-id': backend.id }, [ - E('div', { 'class': 'haproxy-backend-header' }, [ - E('div', {}, [ - E('h4', {}, backend.name), - E('small', { 'style': 'color: #666' }, - backend.mode.toUpperCase() + ' / ' + backend.balance) - ]), - E('div', {}, [ - E('span', { - 'class': 'haproxy-badge ' + (backend.enabled ? 'enabled' : 'disabled') - }, backend.enabled ? 'Enabled' : 'Disabled') - ]) - ]), - E('div', { 'class': 'haproxy-backend-servers' }, - servers.length === 0 - ? E('div', { 'style': 'padding: 1rem; color: #666; text-align: center' }, 'No servers configured') - : servers.map(function(server) { - return E('div', { 'class': 'haproxy-server-item' }, [ - E('div', { 'class': 'haproxy-server-info' }, [ - E('span', { 'class': 'haproxy-server-name' }, server.name), - E('span', { 'class': 'haproxy-server-address' }, - server.address + ':' + server.port) - ]), - E('div', { 'class': 'haproxy-server-status' }, [ - E('span', { 'class': 'haproxy-server-weight' }, 'W:' + server.weight), - E('button', { - 'class': 'cbi-button cbi-button-remove', - 'style': 'padding: 2px 8px; font-size: 12px', - 'click': function() { self.handleDeleteServer(server); } - }, 'X') - ]) - ]); - }) - ), - E('div', { 'style': 'padding: 0.75rem; border-top: 1px solid #eee; display: flex; gap: 0.5rem' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'style': 'flex: 1', - 'click': function() { self.showAddServerModal(backend); } - }, 'Add Server'), - E('button', { - 'class': 'cbi-button cbi-button-remove', - 'click': function() { self.handleDeleteBackend(backend); } - }, 'Delete') - ]) - ]); - }, - - handleAddBackend: function() { - var name = document.getElementById('new-backend-name').value.trim(); - var mode = document.getElementById('new-backend-mode').value; - var balance = document.getElementById('new-backend-balance').value; - var healthCheck = document.getElementById('new-backend-health').value.trim(); - - if (!name) { - ui.addNotification(null, E('p', {}, 'Backend name is required'), 'error'); - return; - } - - return api.createBackend(name, mode, balance, healthCheck, 1).then(function(res) { + return api.updateBackend(backend.id, null, null, null, null, newEnabled).then(function(res) { if (res.success) { - ui.addNotification(null, E('p', {}, 'Backend created')); + self.showToast('Backend ' + action, 'success'); window.location.reload(); } else { - ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error'); + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); } }); }, handleDeleteBackend: function(backend) { + var self = this; + ui.showModal('Delete Backend', [ - E('p', {}, 'Are you sure you want to delete backend "' + backend.name + '" and all its servers?'), - E('div', { 'class': 'right' }, [ + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('p', { 'style': 'margin: 0;' }, 'Are you sure you want to delete this backend and all its servers?'), + E('div', { + 'style': 'margin-top: 12px; padding: 12px; background: var(--hp-bg-tertiary, #f5f5f5); border-radius: 8px; font-family: monospace;' + }, backend.name) + ]), + E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px;' }, [ E('button', { - 'class': 'cbi-button', + 'class': 'hp-btn hp-btn-secondary', 'click': ui.hideModal }, 'Cancel'), E('button', { - 'class': 'cbi-button cbi-button-negative', + 'class': 'hp-btn hp-btn-danger', 'click': function() { ui.hideModal(); api.deleteBackend(backend.id).then(function(res) { if (res.success) { - ui.addNotification(null, E('p', {}, 'Backend deleted')); + self.showToast('Backend deleted', 'success'); window.location.reload(); } else { - ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error'); + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); } }); } @@ -213,70 +407,78 @@ return view.extend({ var self = this; ui.showModal('Add Server to ' + backend.name, [ - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Server Name'), - E('div', { 'class': 'cbi-value-field' }, [ - E('input', { - 'type': 'text', - 'id': 'modal-server-name', - 'class': 'cbi-input-text', - 'placeholder': 'server1' - }) - ]) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Address'), - E('div', { 'class': 'cbi-value-field' }, [ - E('input', { - 'type': 'text', - 'id': 'modal-server-address', - 'class': 'cbi-input-text', - 'placeholder': '192.168.1.10' - }) - ]) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Port'), - E('div', { 'class': 'cbi-value-field' }, [ - E('input', { - 'type': 'number', - 'id': 'modal-server-port', - 'class': 'cbi-input-text', - 'placeholder': '8080', - 'value': '80' - }) - ]) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Weight'), - E('div', { 'class': 'cbi-value-field' }, [ - E('input', { - 'type': 'number', - 'id': 'modal-server-weight', - 'class': 'cbi-input-text', - 'placeholder': '100', - 'value': '100', - 'min': '0', - 'max': '256' - }) - ]) - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Health Check'), - E('div', { 'class': 'cbi-value-field' }, [ - E('label', {}, [ - E('input', { 'type': 'checkbox', 'id': 'modal-server-check', 'checked': true }), - ' Enable health check' + E('div', { 'style': 'max-width: 500px;' }, [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Server Name'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'text', + 'id': 'modal-server-name', + 'class': 'cbi-input-text', + 'placeholder': 'server1', + 'style': 'width: 100%;' + }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Address'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'text', + 'id': 'modal-server-address', + 'class': 'cbi-input-text', + 'placeholder': '192.168.1.10', + 'style': 'width: 100%;' + }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Port'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'number', + 'id': 'modal-server-port', + 'class': 'cbi-input-text', + 'placeholder': '8080', + 'value': '80', + 'min': '1', + 'max': '65535', + 'style': 'width: 100%;' + }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Weight'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'number', + 'id': 'modal-server-weight', + 'class': 'cbi-input-text', + 'value': '100', + 'min': '0', + 'max': '256', + 'style': 'width: 100%;' + }), + E('small', { 'style': 'color: var(--hp-text-muted);' }, 'Higher weight = more traffic (0-256)') + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Options'), + E('div', { 'class': 'cbi-value-field' }, [ + E('label', {}, [ + E('input', { 'type': 'checkbox', 'id': 'modal-server-check', 'checked': true }), + ' Enable health check' + ]) ]) ]) ]), - E('div', { 'class': 'right' }, [ + E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px; margin-top: 16px;' }, [ E('button', { - 'class': 'cbi-button', + 'class': 'hp-btn hp-btn-secondary', 'click': ui.hideModal }, 'Cancel'), E('button', { - 'class': 'cbi-button cbi-button-positive', + 'class': 'hp-btn hp-btn-primary', 'click': function() { var name = document.getElementById('modal-server-name').value.trim(); var address = document.getElementById('modal-server-address').value.trim(); @@ -285,17 +487,17 @@ return view.extend({ var check = document.getElementById('modal-server-check').checked ? 1 : 0; if (!name || !address) { - ui.addNotification(null, E('p', {}, 'Name and address are required'), 'error'); + self.showToast('Name and address are required', 'error'); return; } ui.hideModal(); api.createServer(backend.id, name, address, port, weight, check, 1).then(function(res) { if (res.success) { - ui.addNotification(null, E('p', {}, 'Server added')); + self.showToast('Server added', 'success'); window.location.reload(); } else { - ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error'); + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); } }); } @@ -304,24 +506,141 @@ return view.extend({ ]); }, - handleDeleteServer: function(server) { - ui.showModal('Delete Server', [ - E('p', {}, 'Are you sure you want to delete server "' + server.name + '"?'), - E('div', { 'class': 'right' }, [ + showEditServerModal: function(server, backend) { + var self = this; + + ui.showModal('Edit Server: ' + server.name, [ + E('div', { 'style': 'max-width: 500px;' }, [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Server Name'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'text', + 'id': 'edit-server-name', + 'class': 'cbi-input-text', + 'value': server.name, + 'style': 'width: 100%;' + }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Address'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'text', + 'id': 'edit-server-address', + 'class': 'cbi-input-text', + 'value': server.address, + 'style': 'width: 100%;' + }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Port'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'number', + 'id': 'edit-server-port', + 'class': 'cbi-input-text', + 'value': server.port, + 'min': '1', + 'max': '65535', + 'style': 'width: 100%;' + }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Weight'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'number', + 'id': 'edit-server-weight', + 'class': 'cbi-input-text', + 'value': server.weight, + 'min': '0', + 'max': '256', + 'style': 'width: 100%;' + }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Options'), + E('div', { 'class': 'cbi-value-field' }, [ + E('div', { 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ + E('label', {}, [ + E('input', { 'type': 'checkbox', 'id': 'edit-server-check', 'checked': server.check }), + ' Enable health check' + ]), + E('label', {}, [ + E('input', { 'type': 'checkbox', 'id': 'edit-server-enabled', 'checked': server.enabled }), + ' Enabled' + ]) + ]) + ]) + ]) + ]), + E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px; margin-top: 16px;' }, [ E('button', { - 'class': 'cbi-button', + 'class': 'hp-btn hp-btn-secondary', 'click': ui.hideModal }, 'Cancel'), E('button', { - 'class': 'cbi-button cbi-button-negative', + 'class': 'hp-btn hp-btn-primary', 'click': function() { + var name = document.getElementById('edit-server-name').value.trim(); + var address = document.getElementById('edit-server-address').value.trim(); + var port = parseInt(document.getElementById('edit-server-port').value) || 80; + var weight = parseInt(document.getElementById('edit-server-weight').value) || 100; + var check = document.getElementById('edit-server-check').checked ? 1 : 0; + var enabled = document.getElementById('edit-server-enabled').checked ? 1 : 0; + + if (!name || !address) { + self.showToast('Name and address are required', 'error'); + return; + } + ui.hideModal(); - api.deleteServer(server.id).then(function(res) { + var inline = server.inline ? 1 : 0; + api.updateServer(server.id, backend.id, name, address, port, weight, check, enabled, inline).then(function(res) { if (res.success) { - ui.addNotification(null, E('p', {}, 'Server deleted')); + self.showToast('Server updated', 'success'); window.location.reload(); } else { - ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error'); + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + } + }, 'Save Changes') + ]) + ]); + }, + + handleDeleteServer: function(server) { + var self = this; + + ui.showModal('Delete Server', [ + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('p', { 'style': 'margin: 0;' }, 'Are you sure you want to delete this server?'), + E('div', { + 'style': 'margin-top: 12px; padding: 12px; background: var(--hp-bg-tertiary, #f5f5f5); border-radius: 8px; font-family: monospace;' + }, server.name + ' (' + server.address + ':' + server.port + ')') + ]), + E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px;' }, [ + E('button', { + 'class': 'hp-btn hp-btn-secondary', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'hp-btn hp-btn-danger', + 'click': function() { + ui.hideModal(); + var inline = server.inline ? 1 : 0; + api.deleteServer(server.id, inline).then(function(res) { + if (res.success) { + self.showToast('Server deleted', 'success'); + window.location.reload(); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); } }); } @@ -330,6 +649,27 @@ return view.extend({ ]); }, + showToast: function(message, type) { + var existing = document.querySelector('.hp-toast'); + if (existing) existing.remove(); + + var iconMap = { + 'success': '\u2705', + 'error': '\u274C', + 'warning': '\u26A0\uFE0F' + }; + + var toast = E('div', { 'class': 'hp-toast ' + (type || '') }, [ + E('span', {}, iconMap[type] || '\u2139\uFE0F'), + message + ]); + document.body.appendChild(toast); + + setTimeout(function() { + toast.remove(); + }, 4000); + }, + handleSaveApply: null, handleSave: null, handleReset: null diff --git a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/vhosts.js b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/vhosts.js index c5067303..6f0000ed 100644 --- a/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/vhosts.js +++ b/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/vhosts.js @@ -135,7 +135,7 @@ return view.extend({ E('th', {}, 'Backend'), E('th', {}, 'SSL Configuration'), E('th', {}, 'Status'), - E('th', { 'style': 'width: 180px; text-align: right;' }, 'Actions') + E('th', { 'style': 'width: 220px; text-align: right;' }, 'Actions') ]) ]), E('tbody', {}, vhosts.map(function(vh) { @@ -152,11 +152,16 @@ return view.extend({ vh.ssl ? E('span', { 'class': 'hp-badge hp-badge-info', 'style': 'margin-right: 6px;' }, '\u{1F512} SSL') : null, vh.acme ? E('span', { 'class': 'hp-badge hp-badge-success' }, '\u{1F504} ACME') : null, !vh.ssl && !vh.acme ? E('span', { 'class': 'hp-badge hp-badge-warning' }, 'No SSL') : null - ]), + ].filter(function(e) { return e !== null; })), E('td', {}, E('span', { 'class': 'hp-badge ' + (vh.enabled ? 'hp-badge-success' : 'hp-badge-danger') }, vh.enabled ? '\u2705 Active' : '\u26D4 Disabled')), E('td', { 'style': 'text-align: right;' }, [ + E('button', { + 'class': 'hp-btn hp-btn-sm hp-btn-primary', + 'style': 'margin-right: 8px;', + 'click': function() { self.showEditVhostModal(vh, backends); } + }, '\u270F Edit'), E('button', { 'class': 'hp-btn hp-btn-sm ' + (vh.enabled ? 'hp-btn-secondary' : 'hp-btn-success'), 'style': 'margin-right: 8px;', @@ -172,6 +177,100 @@ return view.extend({ ]); }, + showEditVhostModal: function(vh, backends) { + var self = this; + + ui.showModal('Edit Virtual Host: ' + vh.domain, [ + E('div', { 'style': 'max-width: 500px;' }, [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Domain'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'text', + 'id': 'edit-domain', + 'class': 'cbi-input-text', + 'value': vh.domain, + 'style': 'width: 100%;' + }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Backend'), + E('div', { 'class': 'cbi-value-field' }, [ + E('select', { 'id': 'edit-backend', 'class': 'cbi-input-select', 'style': 'width: 100%;' }, + [E('option', { 'value': '' }, '-- Select Backend --')].concat( + backends.map(function(b) { + var selected = (vh.backend === (b.id || b.name)) ? { 'selected': true } : {}; + return E('option', Object.assign({ 'value': b.id || b.name }, selected), b.name); + }) + ) + ) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'SSL Options'), + E('div', { 'class': 'cbi-value-field' }, [ + E('div', { 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ + E('label', {}, [ + E('input', { 'type': 'checkbox', 'id': 'edit-ssl', 'checked': vh.ssl }), + ' Enable SSL/TLS' + ]), + E('label', {}, [ + E('input', { 'type': 'checkbox', 'id': 'edit-ssl-redirect', 'checked': vh.ssl_redirect }), + ' Force HTTPS redirect' + ]), + E('label', {}, [ + E('input', { 'type': 'checkbox', 'id': 'edit-acme', 'checked': vh.acme }), + ' Auto-renew with ACME (Let\'s Encrypt)' + ]) + ]) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Status'), + E('div', { 'class': 'cbi-value-field' }, [ + E('label', {}, [ + E('input', { 'type': 'checkbox', 'id': 'edit-enabled', 'checked': vh.enabled }), + ' Enabled' + ]) + ]) + ]) + ]), + E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px; margin-top: 16px;' }, [ + E('button', { + 'class': 'hp-btn hp-btn-secondary', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'hp-btn hp-btn-primary', + 'click': function() { + var domain = document.getElementById('edit-domain').value.trim(); + var backend = document.getElementById('edit-backend').value; + var ssl = document.getElementById('edit-ssl').checked ? 1 : 0; + var sslRedirect = document.getElementById('edit-ssl-redirect').checked ? 1 : 0; + var acme = document.getElementById('edit-acme').checked ? 1 : 0; + var enabled = document.getElementById('edit-enabled').checked ? 1 : 0; + + if (!domain) { + self.showToast('Domain is required', 'error'); + return; + } + + ui.hideModal(); + api.updateVhost(vh.id, domain, backend, ssl, sslRedirect, acme, enabled).then(function(res) { + if (res.success) { + self.showToast('Virtual host updated', 'success'); + window.location.reload(); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + } + }, 'Save Changes') + ]) + ]); + }, + handleAddVhost: function(backends) { var self = this; var domain = document.getElementById('new-domain').value.trim(); diff --git a/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy b/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy index fddb77fc..4e82daa3 100644 --- a/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy +++ b/package/secubox/luci-app-haproxy/root/usr/libexec/rpcd/luci.haproxy @@ -55,15 +55,15 @@ method_status() { stats_enabled=$(get_uci main stats_enabled 1) # Check container status - if lxc-info -n haproxy-lxc >/dev/null 2>&1; then - container_running=$(lxc-info -n haproxy-lxc -s 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0") + if lxc-info -n haproxy >/dev/null 2>&1; then + container_running=$(lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0") else container_running="0" fi # Check HAProxy process in container if [ "$container_running" = "1" ]; then - haproxy_running=$(lxc-attach -n haproxy-lxc -- pgrep haproxy >/dev/null 2>&1 && echo "1" || echo "0") + haproxy_running=$(lxc-attach -n haproxy -- pgrep haproxy >/dev/null 2>&1 && echo "1" || echo "0") else haproxy_running="0" fi @@ -83,7 +83,7 @@ method_status() { method_get_stats() { local stats_output - if lxc-info -n haproxy-lxc -s 2>/dev/null | grep -q "RUNNING"; then + if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then # Get stats via HAProxy socket stats_output=$(run_ctl stats 2>/dev/null) if [ -n "$stats_output" ]; then @@ -291,13 +291,14 @@ method_list_backends() { _add_backend() { local section="$1" - local name mode balance health_check enabled + local name mode balance health_check enabled server_line config_get name "$section" name "$section" config_get mode "$section" mode "http" config_get balance "$section" balance "roundrobin" config_get health_check "$section" health_check "" config_get enabled "$section" enabled "1" + config_get server_line "$section" server "" json_add_object json_add_string "id" "$section" @@ -306,6 +307,59 @@ _add_backend() { json_add_string "balance" "$balance" json_add_string "health_check" "$health_check" json_add_boolean "enabled" "$enabled" + + # Include servers array - parse inline server option if present + json_add_array "servers" + if [ -n "$server_line" ]; then + # Parse inline format: "name address:port [options]" + local srv_name srv_addr_port srv_addr srv_port srv_check + srv_name=$(echo "$server_line" | awk '{print $1}') + srv_addr_port=$(echo "$server_line" | awk '{print $2}') + srv_addr=$(echo "$srv_addr_port" | cut -d: -f1) + srv_port=$(echo "$srv_addr_port" | cut -d: -f2) + srv_check=$(echo "$server_line" | grep -q "check" && echo "1" || echo "0") + + json_add_object + json_add_string "id" "${section}_${srv_name}" + json_add_string "name" "$srv_name" + json_add_string "address" "$srv_addr" + json_add_int "port" "${srv_port:-80}" + json_add_int "weight" "100" + json_add_boolean "check" "$srv_check" + json_add_boolean "enabled" "1" + json_add_boolean "inline" "1" + json_close_object + fi + # Also check for separate server sections + config_foreach _add_server_for_backend_inline server "$section" + json_close_array + + json_close_object +} + +_add_server_for_backend_inline() { + local srv_section="$1" + local backend_filter="$2" + local backend srv_name srv_address srv_port srv_weight srv_check srv_enabled + + config_get backend "$srv_section" backend "" + [ "$backend" != "$backend_filter" ] && return + + config_get srv_name "$srv_section" name "$srv_section" + config_get srv_address "$srv_section" address "" + config_get srv_port "$srv_section" port "" + config_get srv_weight "$srv_section" weight "100" + config_get srv_check "$srv_section" check "1" + config_get srv_enabled "$srv_section" enabled "1" + + json_add_object + json_add_string "id" "$srv_section" + json_add_string "name" "$srv_name" + json_add_string "address" "$srv_address" + json_add_int "port" "${srv_port:-80}" + json_add_int "weight" "$srv_weight" + json_add_boolean "check" "$srv_check" + json_add_boolean "enabled" "$srv_enabled" json_close_object } @@ -580,7 +634,7 @@ method_create_server() { # Update server method_update_server() { - local id backend name address port weight check enabled + local id backend name address port weight check enabled inline read -r input json_load "$input" @@ -592,6 +646,7 @@ method_update_server() { json_get_var weight weight json_get_var check check json_get_var enabled enabled + json_get_var inline inline "" if [ -z "$id" ]; then json_init @@ -601,13 +656,36 @@ method_update_server() { return fi - [ -n "$backend" ] && uci set "$UCI_CONFIG.$id.backend=$backend" - [ -n "$name" ] && uci set "$UCI_CONFIG.$id.name=$name" - [ -n "$address" ] && uci set "$UCI_CONFIG.$id.address=$address" - [ -n "$port" ] && uci set "$UCI_CONFIG.$id.port=$port" - [ -n "$weight" ] && uci set "$UCI_CONFIG.$id.weight=$weight" - [ -n "$check" ] && uci set "$UCI_CONFIG.$id.check=$check" - [ -n "$enabled" ] && uci set "$UCI_CONFIG.$id.enabled=$enabled" + # Check if this is an inline server (id format: backendname_servername) + # If so, we need to convert it to a proper server section + if [ "$inline" = "1" ] || ! uci -q get "$UCI_CONFIG.$id" >/dev/null 2>&1; then + # This is an inline server - extract backend from id + local backend_id + backend_id=$(echo "$id" | sed 's/_[^_]*$//') + + # Remove inline server option from backend + uci -q delete "$UCI_CONFIG.$backend_id.server" + + # Create new server section + local section_id="${backend_id}_${name}" + uci set "$UCI_CONFIG.$section_id=server" + uci set "$UCI_CONFIG.$section_id.backend=$backend_id" + uci set "$UCI_CONFIG.$section_id.name=$name" + uci set "$UCI_CONFIG.$section_id.address=$address" + uci set "$UCI_CONFIG.$section_id.port=$port" + [ -n "$weight" ] && uci set "$UCI_CONFIG.$section_id.weight=$weight" + [ -n "$check" ] && uci set "$UCI_CONFIG.$section_id.check=$check" + [ -n "$enabled" ] && uci set "$UCI_CONFIG.$section_id.enabled=$enabled" + else + # Regular server section - update in place + [ -n "$backend" ] && uci set "$UCI_CONFIG.$id.backend=$backend" + [ -n "$name" ] && uci set "$UCI_CONFIG.$id.name=$name" + [ -n "$address" ] && uci set "$UCI_CONFIG.$id.address=$address" + [ -n "$port" ] && uci set "$UCI_CONFIG.$id.port=$port" + [ -n "$weight" ] && uci set "$UCI_CONFIG.$id.weight=$weight" + [ -n "$check" ] && uci set "$UCI_CONFIG.$id.check=$check" + [ -n "$enabled" ] && uci set "$UCI_CONFIG.$id.enabled=$enabled" + fi uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 @@ -619,11 +697,12 @@ method_update_server() { # Delete server method_delete_server() { - local id + local id inline read -r input json_load "$input" json_get_var id id + json_get_var inline inline "" if [ -z "$id" ]; then json_init @@ -633,7 +712,16 @@ method_delete_server() { return fi - uci delete "$UCI_CONFIG.$id" + # Check if this is an inline server or regular server section + if [ "$inline" = "1" ] || ! uci -q get "$UCI_CONFIG.$id" >/dev/null 2>&1; then + # Inline server - extract backend id and delete the server option + local backend_id + backend_id=$(echo "$id" | sed 's/_[^_]*$//') + uci -q delete "$UCI_CONFIG.$backend_id.server" + else + # Regular server section + uci delete "$UCI_CONFIG.$id" + fi uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 @@ -688,7 +776,7 @@ method_request_certificate() { fi local result - result=$(run_ctl cert-issue "$domain" 2>&1) + result=$(run_ctl cert add "$domain" 2>&1) local rc=$? json_init @@ -721,7 +809,7 @@ method_import_certificate() { fi local result - result=$(run_ctl cert-import "$domain" "$cert_data" "$key_data" 2>&1) + result=$(run_ctl cert import "$domain" "$cert_data" "$key_data" 2>&1) local rc=$? json_init @@ -755,7 +843,7 @@ method_delete_certificate() { domain=$(get_uci "$id" domain "") # Remove certificate files - run_ctl cert-delete "$domain" >/dev/null 2>&1 + run_ctl cert remove "$domain" >/dev/null 2>&1 uci delete "$UCI_CONFIG.$id" uci commit "$UCI_CONFIG" @@ -1216,8 +1304,8 @@ case "$1" in "delete_backend": { "id": "string" }, "list_servers": { "backend": "string" }, "create_server": { "backend": "string", "name": "string", "address": "string", "port": "integer", "weight": "integer", "check": "boolean", "enabled": "boolean" }, - "update_server": { "id": "string", "backend": "string", "name": "string", "address": "string", "port": "integer", "weight": "integer", "check": "boolean", "enabled": "boolean" }, - "delete_server": { "id": "string" }, + "update_server": { "id": "string", "backend": "string", "name": "string", "address": "string", "port": "integer", "weight": "integer", "check": "boolean", "enabled": "boolean", "inline": "boolean" }, + "delete_server": { "id": "string", "inline": "boolean" }, "list_certificates": {}, "request_certificate": { "domain": "string" }, "import_certificate": { "domain": "string", "cert": "string", "key": "string" }, diff --git a/package/secubox/secubox-app-haproxy/Makefile b/package/secubox/secubox-app-haproxy/Makefile index a6c3178b..6c3b2045 100644 --- a/package/secubox/secubox-app-haproxy/Makefile +++ b/package/secubox/secubox-app-haproxy/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=secubox-app-haproxy PKG_VERSION:=1.0.0 -PKG_RELEASE:=1 +PKG_RELEASE:=13 PKG_MAINTAINER:=CyberMind PKG_LICENSE:=MIT @@ -18,7 +18,7 @@ define Package/secubox-app-haproxy CATEGORY:=SecuBox SUBMENU:=Services TITLE:=HAProxy Load Balancer & Reverse Proxy - DEPENDS:=+lxc +lxc-common +openssl-util +wget-ssl +tar +jsonfilter +acme +socat + DEPENDS:=+lxc +lxc-common +openssl-util +wget-ssl +tar +jsonfilter +acme +acme-acmesh +socat PKGARCH:=all endef diff --git a/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy b/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy index 6c73d3e5..94cd476c 100644 --- a/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy +++ b/package/secubox/secubox-app-haproxy/files/etc/init.d/haproxy @@ -21,7 +21,7 @@ start_service() { procd_set_param respawn 3600 5 0 procd_set_param stdout 1 procd_set_param stderr 1 - procd_set_param pidfile /var/run/haproxy-lxc.pid + procd_set_param pidfile /var/run/haproxy.pid procd_close_instance } diff --git a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl index ffdc7ef5..c72ce7dd 100644 --- a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl +++ b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl @@ -2,6 +2,9 @@ # SecuBox HAProxy Controller # Copyright (C) 2025 CyberMind.fr +# Source OpenWrt functions for UCI iteration +. /lib/functions.sh + CONFIG="haproxy" LXC_NAME="haproxy" @@ -196,7 +199,7 @@ lxc.net.0.type = none # Mount points lxc.mount.auto = proc:mixed sys:ro cgroup:mixed -lxc.mount.entry = $data_path /opt/haproxy none bind,create=dir 0 0 +lxc.mount.entry = $data_path opt/haproxy none bind,create=dir 0 0 # Environment lxc.environment = HTTP_PORT=$http_port @@ -206,8 +209,8 @@ lxc.environment = STATS_PORT=$stats_port # Security lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time -# Resource limits -lxc.cgroup.memory.limit_in_bytes = $mem_bytes +# Resource limits (cgroup2) +lxc.cgroup2.memory.max = $mem_bytes # Init command lxc.init.cmd = /opt/start-haproxy.sh @@ -234,7 +237,14 @@ lxc_run() { export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CONFIG_FILE="/opt/haproxy/config/haproxy.cfg" -PID_FILE="/var/run/haproxy.pid" +LOG_FILE="/opt/haproxy/startup.log" + +# Log all output +exec >>"$LOG_FILE" 2>&1 +echo "=== HAProxy startup: $(date) ===" +echo "Config: $CONFIG_FILE" +ls -la /opt/haproxy/ +ls -la /opt/haproxy/certs/ 2>/dev/null || echo "No certs dir" # Wait for config if [ ! -f "$CONFIG_FILE" ]; then @@ -275,6 +285,16 @@ backend fallback CFGEOF fi +# Validate config first +echo "[haproxy] Validating config..." +haproxy -c -f "$CONFIG_FILE" +RC=$? +echo "[haproxy] Validation exit code: $RC" +if [ $RC -ne 0 ]; then + echo "[haproxy] Config validation failed!" + exit 1 +fi + echo "[haproxy] Starting HAProxy..." exec haproxy -f "$CONFIG_FILE" -W -db STARTEOF @@ -388,10 +408,12 @@ EOF echo "" # HTTPS Frontend (if certificates exist) + # Use container path /opt/haproxy/certs/ (not host path) + local CONTAINER_CERTS_PATH="/opt/haproxy/certs" if [ -d "$CERTS_PATH" ] && ls "$CERTS_PATH"/*.pem >/dev/null 2>&1; then cat << EOF frontend https-in - bind *:$https_port ssl crt $CERTS_PATH/ alpn h2,http/1.1 + bind *:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1 mode http http-request set-header X-Forwarded-Proto https http-request set-header X-Real-IP %[src] @@ -448,15 +470,18 @@ _add_vhost_acl() { _generate_backends() { config_load haproxy - # Generate each backend + # Generate each backend from UCI config_foreach _generate_backend backend - # Fallback backend - cat << EOF + # Only add default fallback if no "fallback" backend exists in UCI + if ! uci -q get haproxy.fallback >/dev/null 2>&1; then + cat << EOF + backend fallback mode http http-request deny deny_status 503 EOF + fi } _generate_backend() { @@ -478,7 +503,12 @@ _generate_backend() { [ -n "$health_check" ] && echo " option $health_check" - # Add servers for this backend + # Add servers defined in backend section (handles both single and list) + local server_line + config_get server_line "$section" server "" + [ -n "$server_line" ] && echo " server $server_line" + + # Add servers from separate server UCI sections config_foreach _add_server_to_backend server "$name" } @@ -538,41 +568,108 @@ cmd_cert_add() { local email=$(uci_get acme.email) local staging=$(uci_get acme.staging) - local key_type=$(uci_get acme.key_type) || key_type="ec-256" + local key_type_raw=$(uci_get acme.key_type) || key_type_raw="ec-256" - [ -z "$email" ] && { log_error "ACME email not configured"; return 1; } + # Convert key type for acme.sh (rsa-4096 → 4096, ec-256 stays ec-256) + local key_type="$key_type_raw" + case "$key_type_raw" in + rsa-*) key_type="${key_type_raw#rsa-}" ;; # rsa-4096 → 4096 + RSA-*) key_type="${key_type_raw#RSA-}" ;; + esac + + [ -z "$email" ] && { log_error "ACME email not configured. Set in LuCI > Services > HAProxy > Settings"; return 1; } log_info "Requesting certificate for $domain..." local staging_flag="" [ "$staging" = "1" ] && staging_flag="--staging" - # Use acme.sh or certbot if available - if command -v acme.sh >/dev/null 2>&1; then - acme.sh --issue -d "$domain" --standalone --httpport $http_port \ - --keylength $key_type $staging_flag \ - --cert-file "$CERTS_PATH/$domain.crt" \ - --key-file "$CERTS_PATH/$domain.key" \ - --fullchain-file "$CERTS_PATH/$domain.pem" \ - --reloadcmd "haproxyctl reload" + # Find acme.sh - check OpenWrt location first, then PATH + local ACME_SH="" + if [ -x "/usr/lib/acme/client/acme.sh" ]; then + ACME_SH="/usr/lib/acme/client/acme.sh" + elif command -v acme.sh >/dev/null 2>&1; then + ACME_SH="acme.sh" + fi + + if [ -n "$ACME_SH" ]; then + # Set acme.sh home directory + export LE_WORKING_DIR="/etc/acme" + export LE_CONFIG_HOME="/etc/acme" + ensure_dir "$LE_WORKING_DIR" + + # Register account if needed + if [ ! -f "$LE_WORKING_DIR/account.conf" ]; then + log_info "Registering ACME account..." + "$ACME_SH" --register-account -m "$email" $staging_flag --home "$LE_WORKING_DIR" || true + fi + + # Check if HAProxy is using the port + local haproxy_was_running=0 + if lxc_running; then + log_info "Temporarily stopping HAProxy for certificate issuance..." + haproxy_was_running=1 + /etc/init.d/haproxy stop 2>/dev/null || true + sleep 2 + fi + + # Issue certificate using standalone mode + log_info "Issuing certificate (standalone mode on port $http_port)..." + local acme_result=0 + "$ACME_SH" --issue -d "$domain" \ + --standalone --httpport "$http_port" \ + --keylength "$key_type" \ + $staging_flag \ + --home "$LE_WORKING_DIR" || acme_result=$? + + # acme.sh returns 0 on success, 2 on "skip/already valid" - both are OK + # Install the certificate to our certs path + if [ "$acme_result" -eq 0 ] || [ "$acme_result" -eq 2 ]; then + log_info "Installing certificate..." + "$ACME_SH" --install-cert -d "$domain" \ + --home "$LE_WORKING_DIR" \ + --cert-file "$CERTS_PATH/$domain.crt" \ + --key-file "$CERTS_PATH/$domain.key" \ + --fullchain-file "$CERTS_PATH/$domain.pem" \ + --reloadcmd "/etc/init.d/haproxy reload" 2>/dev/null || true + fi + + # Restart HAProxy if it was running + if [ "$haproxy_was_running" = "1" ]; then + log_info "Restarting HAProxy..." + /etc/init.d/haproxy start 2>/dev/null || true + fi + + # Check if certificate was created + if [ ! -f "$CERTS_PATH/$domain.pem" ]; then + log_error "Certificate issuance failed. Ensure port $http_port is accessible from internet and domain points to this IP." + return 1 + fi + log_info "Certificate ready: $CERTS_PATH/$domain.pem" elif command -v certbot >/dev/null 2>&1; then certbot certonly --standalone -d "$domain" \ --email "$email" --agree-tos -n \ - --http-01-port $http_port $staging_flag + --http-01-port "$http_port" $staging_flag || { + log_error "Certbot failed" + return 1 + } # Copy to HAProxy certs dir local le_path="/etc/letsencrypt/live/$domain" cat "$le_path/fullchain.pem" "$le_path/privkey.pem" > "$CERTS_PATH/$domain.pem" else - log_error "No ACME client found. Install acme.sh or certbot" + log_error "No ACME client found. Install: opkg install acme acme-acmesh" return 1 fi + chmod 600 "$CERTS_PATH/$domain.pem" + # Add to UCI - uci set haproxy.cert_${domain//[.-]/_}=certificate - uci set haproxy.cert_${domain//[.-]/_}.domain="$domain" - uci set haproxy.cert_${domain//[.-]/_}.type="acme" - uci set haproxy.cert_${domain//[.-]/_}.enabled="1" + local section="cert_$(echo "$domain" | tr '.-' '__')" + uci set haproxy.$section=certificate + uci set haproxy.$section.domain="$domain" + uci set haproxy.$section.type="acme" + uci set haproxy.$section.enabled="1" uci commit haproxy log_info "Certificate installed for $domain" @@ -818,8 +915,11 @@ cmd_reload() { generate_config log_info "Reloading HAProxy configuration..." - lxc_exec sh -c "echo 'reload' | socat stdio /var/run/haproxy.sock" || \ - lxc_exec killall -HUP haproxy + # HAProxy in master-worker mode (-W) reloads gracefully on SIGUSR2 + # Fallback to SIGHUP if USR2 fails + lxc_exec killall -USR2 haproxy 2>/dev/null || \ + lxc_exec killall -HUP haproxy 2>/dev/null || \ + log_error "Could not signal HAProxy for reload" log_info "Reload complete" } diff --git a/package/secubox/secubox-app-hexojs/Makefile b/package/secubox/secubox-app-hexojs/Makefile index 711c3bbd..debe0305 100644 --- a/package/secubox/secubox-app-hexojs/Makefile +++ b/package/secubox/secubox-app-hexojs/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=secubox-app-hexojs PKG_VERSION:=1.0.0 -PKG_RELEASE:=2 +PKG_RELEASE:=6 PKG_ARCH:=all PKG_MAINTAINER:=CyberMind Studio diff --git a/package/secubox/secubox-app-hexojs/files/etc/config/hexojs b/package/secubox/secubox-app-hexojs/files/etc/config/hexojs index ba0fe3ef..a29cf8fa 100644 --- a/package/secubox/secubox-app-hexojs/files/etc/config/hexojs +++ b/package/secubox/secubox-app-hexojs/files/etc/config/hexojs @@ -38,3 +38,7 @@ config theme_config 'theme' option accent_color '#f97316' option logo_symbol '>' option logo_text 'Blog_' + +config portal 'portal' + option enabled '1' + option path '/www' diff --git a/package/secubox/secubox-app-hexojs/files/usr/sbin/hexoctl b/package/secubox/secubox-app-hexojs/files/usr/sbin/hexoctl index d3efd960..7e4efb6d 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/sbin/hexoctl +++ b/package/secubox/secubox-app-hexojs/files/usr/sbin/hexoctl @@ -90,18 +90,19 @@ Site Management: site switch Switch active site Content Commands: - new post "Title" Create new blog post - new page "Title" Create new page - new draft "Title" Create new draft - publish Publish a draft - list posts List all posts - list drafts List all drafts + new post "Title" Create new blog post + new page "Title" Create new page + new draft "Title" Create new draft + publish draft Publish a draft + list posts List all posts + list drafts List all drafts Build Commands: - serve Start preview server (port $http_port) - build Generate static files - clean Clean generated files - deploy Deploy to configured target + serve Start preview server (port $http_port) + build (generate) Generate static files + clean Clean generated files + deploy Deploy to configured git target + publish Copy static files to /www/blog/ Service Commands: service-run Run in foreground (for init) @@ -293,8 +294,7 @@ lxc_run() { # Ensure start script exists in container local start_script="$LXC_ROOTFS/opt/start-hexo.sh" - if [ ! -f "$start_script" ]; then - cat > "$start_script" << 'STARTEOF' + cat > "$start_script" << 'STARTEOF' #!/bin/sh export PATH=/usr/local/bin:/usr/bin:/bin:$PATH export HOME=/root @@ -302,11 +302,11 @@ export NODE_ENV=production HEXO_PORT="${HEXO_PORT:-4000}" SITE_DIR="/opt/hexojs/site" cd "$SITE_DIR" 2>/dev/null || exec tail -f /dev/null +[ -d "node_modules" ] || npm install [ -d "$SITE_DIR/public" ] || hexo generate -exec hexo server -p "$HEXO_PORT" +exec hexo server -p "$HEXO_PORT" -i 0.0.0.0 STARTEOF - chmod +x "$start_script" - fi + chmod +x "$start_script" log_info "Starting Hexo container on port $http_port..." exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG" @@ -668,14 +668,14 @@ cmd_new_draft() { lxc_exec sh -c "cd /opt/hexojs/site && hexo new draft \"$title\"" } -cmd_publish() { +cmd_publish_draft() { require_root load_config local slug="$1" if [ -z "$slug" ]; then log_error "Slug required" - echo "Usage: hexoctl publish " + echo "Usage: hexoctl publish draft " return 1 fi @@ -762,7 +762,7 @@ cmd_serve() { fi log_info "Starting preview server on port $http_port..." - lxc_exec sh -c "cd /opt/hexojs/site && hexo server -p $http_port" + lxc_exec sh -c "cd /opt/hexojs/site && hexo server -p $http_port -i 0.0.0.0" } cmd_build() { @@ -813,6 +813,64 @@ cmd_deploy() { log_info "Deploy complete!" } +cmd_publish() { + require_root + load_config + + local public_dir="$data_path/site/public" + local portal_path="/www" + local config_file="$data_path/site/_config.yml" + + # Allow custom portal path from config + local custom_path=$(uci_get portal.path) + [ -n "$custom_path" ] && portal_path="$custom_path" + + # Calculate web root from portal path (strip /www prefix) + local web_root="${portal_path#/www}" + [ -z "$web_root" ] && web_root="/" + # Ensure trailing slash + [ "${web_root%/}" = "$web_root" ] && web_root="$web_root/" + + if ! lxc_running; then + log_error "Container not running" + return 1 + fi + + if [ ! -f "$config_file" ]; then + log_error "No Hexo config found at $config_file" + return 1 + fi + + log_info "Setting Hexo root to: $web_root" + + # Update root in _config.yml (use sed to replace existing root line) + if grep -q "^root:" "$config_file"; then + sed -i "s|^root:.*|root: $web_root|" "$config_file" + else + # Add root config if not present + echo "root: $web_root" >> "$config_file" + fi + + log_info "Regenerating static files for $web_root..." + lxc_exec sh -c "cd /opt/hexojs/site && hexo clean && hexo generate" + + if [ ! -d "$public_dir" ]; then + log_error "Build failed - no public directory" + return 1 + fi + + log_info "Publishing to $portal_path..." + + # Create portal directory + ensure_dir "$portal_path" + + # Sync files + rsync -av --delete "$public_dir/" "$portal_path/" + + log_info "Published $(find "$portal_path" -type f | wc -l) files to $portal_path" + log_info "Access at: http://$(uci -q get network.lan.ipaddr || echo 'router')$web_root" +} + cmd_logs() { load_config @@ -1074,8 +1132,6 @@ case "${1:-}" in esac ;; - publish) shift; cmd_publish "$@" ;; - list) shift case "${1:-}" in @@ -1086,9 +1142,16 @@ case "${1:-}" in ;; serve) shift; cmd_serve "$@" ;; - build) shift; cmd_build "$@" ;; + build|generate) shift; cmd_build "$@" ;; clean) shift; cmd_clean "$@" ;; deploy) shift; cmd_deploy "$@" ;; + publish) + shift + case "${1:-}" in + draft) shift; cmd_publish_draft "$@" ;; + *) cmd_publish "$@" ;; + esac + ;; logs) shift; cmd_logs "$@" ;; shell) shift; cmd_shell "$@" ;; diff --git a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/presets/portfolio.yml b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/presets/portfolio.yml index 8dd41d33..0304cd52 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/presets/portfolio.yml +++ b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/presets/portfolio.yml @@ -19,7 +19,7 @@ menu: Home: / Projects: /portfolio/ Services: /services/ - Blog: /blog/ + Blog: /categories/ Contact: /contact/ sections: diff --git a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/presets/tech.yml b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/presets/tech.yml index e4543be5..e6d769c6 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/presets/tech.yml +++ b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/presets/tech.yml @@ -18,11 +18,11 @@ branding: menu: Home: / Blog: - _path: /blog/ - Security: /blog/security/ - Linux: /blog/linux/ - Development: /blog/dev/ - Tutorials: /blog/tutorials/ + _path: /categories/ + Security: /security/ + Linux: /linux/ + Development: /dev/ + Tutorials: /tutorials/ Projects: /portfolio/ About: /about/ diff --git a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/_config.yml b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/_config.yml index 7a8a2d59..704b706b 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/_config.yml +++ b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/_config.yml @@ -33,13 +33,13 @@ branding: menu: Accueil: / Blog: - _path: /blog/ - 🛡️ Cybersécurité: /blog/cybersecurity/ - ⚙️ Embarqué: /blog/embedded/ - 🐧 Linux: /blog/linux/ - 🎨 Créativité: /blog/creative/ - 🧘 Philosophie: /blog/philosophy/ - 📖 Tutoriels: /blog/tutorials/ + _path: /categories/ + 🛡️ Cybersécurité: /cybersecurity/ + ⚙️ Embarqué: /embedded/ + 🐧 Linux: /linux/ + 🎨 Créativité: /creative/ + 🧘 Philosophie: /philosophy/ + 📖 Tutoriels: /tutorials/ Apps: /apps/ Services: /services/ Portfolio: /portfolio/ diff --git a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/apps.ejs b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/apps.ejs index bc397873..86bdfd1e 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/apps.ejs +++ b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/apps.ejs @@ -155,8 +155,8 @@ allApps.sort(function(a, b) { <% } %> diff --git a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/category.ejs b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/category.ejs index 4f229d29..3eaf0a54 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/category.ejs +++ b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/category.ejs @@ -68,7 +68,7 @@ if (typeof get_blog_categories === 'function') { <% if (catDesc) { %>

<%= catDesc %>

<% } %>

<%= postCount %> article<%= postCount > 1 ? 's' : '' %>

@@ -78,7 +78,7 @@ if (typeof get_blog_categories === 'function') {

🚀 Apps

<% page.featured_apps.forEach(function(app) { %> - <%= app %> + <%= app %> <% }); %>
<% } %> @@ -86,7 +86,7 @@ if (typeof get_blog_categories === 'function') {

🛡️ Services

<% page.featured_services.forEach(function(svc) { %> - <%= svc %> + <%= svc %> <% }); %>
<% } %> @@ -127,7 +127,7 @@ if (typeof get_blog_categories === 'function') { <% } else { %>

🚧 Aucun article dans cette catégorie pour le moment.

-

← Retour au blog

+

← Retour au blog

<% } %> diff --git a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/index.ejs b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/index.ejs index 75985150..36e0438c 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/index.ejs +++ b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/index.ejs @@ -478,7 +478,7 @@ if (typeof get_blog_categories === 'function') { <% } %> diff --git a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/post.ejs b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/post.ejs index c26bcf21..9ef6e94d 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/post.ejs +++ b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/post.ejs @@ -83,7 +83,7 @@ if (page.tags && page.tags.length > 0) {
<% if (hasCategory) { %> diff --git a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/showcase.ejs b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/showcase.ejs index 27222da5..59d48692 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/showcase.ejs +++ b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/layout/showcase.ejs @@ -90,7 +90,7 @@ var opensource = portfolio.filter(function(p) { return p.type === 'opensource'; @@ -159,7 +159,7 @@ var opensource = portfolio.filter(function(p) { return p.type === 'opensource'; @@ -225,10 +225,10 @@ var opensource = portfolio.filter(function(p) { return p.type === 'opensource'; - +
@@ -262,10 +262,10 @@ var opensource = portfolio.filter(function(p) { return p.type === 'opensource';
- +
@@ -314,30 +314,30 @@ var opensource = portfolio.filter(function(p) { return p.type === 'opensource';
- +
@@ -347,7 +347,7 @@ var opensource = portfolio.filter(function(p) { return p.type === 'opensource';

💬 Un projet en tête ?

Discutons de vos besoins et trouvons la meilleure solution ensemble.

diff --git a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/scripts/dynamic-blog.js b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/scripts/dynamic-blog.js index 9a3ef007..24fae8f9 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/scripts/dynamic-blog.js +++ b/package/secubox/secubox-app-hexojs/files/usr/share/hexojs/themes/cybermind/scripts/dynamic-blog.js @@ -126,7 +126,7 @@ function scanCategories(hexo) { color: DEFAULT_COLORS[orderIndex % DEFAULT_COLORS.length], description: '', order: 100 + orderIndex, - path: `/blog/${slug}/` + path: `/${slug}/` }; // Lire les métadonnées depuis index.md si présent @@ -360,7 +360,7 @@ hexo.extend.helper.register('get_dynamic_menu', function() { const blogMenu = { name: 'Blog', icon: '📚', - path: '/blog/', + path: '/categories/', children: categories.map(cat => ({ name: cat.name, icon: cat.icon, diff --git a/package/secubox/secubox-app-metabolizer/files/etc/config/metabolizer b/package/secubox/secubox-app-metabolizer/files/etc/config/metabolizer index f68d1974..99ee324d 100644 --- a/package/secubox/secubox-app-metabolizer/files/etc/config/metabolizer +++ b/package/secubox/secubox-app-metabolizer/files/etc/config/metabolizer @@ -18,10 +18,10 @@ config cms 'cms' config hexo 'hexo' option source_path '/srv/hexojs/site/source/_posts' option public_path '/srv/hexojs/site/public' - option portal_path '/www/blog' + option portal_path '/www' option auto_publish '1' config portal 'portal' option enabled '1' - option url_path '/blog' + option url_path '/' option title 'SecuBox Blog'