Complete LuCI app with: - Overview dashboard with stats (services, Tor, SSL counts) - Port conflict detection and warnings - Services list with quick actions - Tor hidden services management (add/list/remove) - HAProxy SSL backends management (add/list/remove) Views: overview.js, services.js, tor.js, ssl.js RPCD: luci.exposure backend Menu: admin/secubox/network/exposure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
153 lines
6.3 KiB
JavaScript
153 lines
6.3 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require ui';
|
|
'require exposure/api as api';
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
api.status(),
|
|
api.conflicts()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var status = data[0] || {};
|
|
var conflicts = data[1] || [];
|
|
|
|
var services = status.services || {};
|
|
var tor = status.tor || {};
|
|
var ssl = status.ssl || {};
|
|
|
|
// Inject CSS
|
|
var cssLink = document.querySelector('link[href*="exposure/dashboard.css"]');
|
|
if (!cssLink) {
|
|
var link = document.createElement('link');
|
|
link.rel = 'stylesheet';
|
|
link.href = L.resource('exposure/dashboard.css');
|
|
document.head.appendChild(link);
|
|
}
|
|
|
|
var view = E('div', { 'class': 'exposure-dashboard' }, [
|
|
E('h2', {}, 'Service Exposure Manager'),
|
|
E('p', { 'style': 'color: #8892b0; margin-bottom: 1.5rem;' },
|
|
'Manage port conflicts, Tor hidden services, and HAProxy SSL backends'),
|
|
|
|
// Stats cards
|
|
E('div', { 'class': 'exposure-stats' }, [
|
|
E('div', { 'class': 'exposure-stat-card' }, [
|
|
E('div', { 'class': 'exposure-stat-icon' }, '\ud83d\udd0c'),
|
|
E('div', { 'class': 'exposure-stat-value' }, String(services.total || 0)),
|
|
E('div', { 'class': 'exposure-stat-label' }, 'Total Services')
|
|
]),
|
|
E('div', { 'class': 'exposure-stat-card' }, [
|
|
E('div', { 'class': 'exposure-stat-icon' }, '\ud83c\udf10'),
|
|
E('div', { 'class': 'exposure-stat-value' }, String(services.external || 0)),
|
|
E('div', { 'class': 'exposure-stat-label' }, 'External (0.0.0.0)')
|
|
]),
|
|
E('div', { 'class': 'exposure-stat-card' }, [
|
|
E('div', { 'class': 'exposure-stat-icon' }, '\ud83e\uddc5'),
|
|
E('div', { 'class': 'exposure-stat-value' }, String(tor.count || 0)),
|
|
E('div', { 'class': 'exposure-stat-label' }, 'Tor Hidden Services')
|
|
]),
|
|
E('div', { 'class': 'exposure-stat-card' }, [
|
|
E('div', { 'class': 'exposure-stat-icon' }, '\ud83d\udd12'),
|
|
E('div', { 'class': 'exposure-stat-value' }, String(ssl.count || 0)),
|
|
E('div', { 'class': 'exposure-stat-label' }, 'SSL Backends')
|
|
])
|
|
]),
|
|
|
|
// Conflicts warning
|
|
conflicts.length > 0 ? E('div', { 'class': 'conflict-warning' }, [
|
|
E('div', { 'class': 'conflict-warning-header' }, [
|
|
'\u26a0\ufe0f Port Conflicts Detected'
|
|
]),
|
|
E('ul', {},
|
|
conflicts.map(function(c) {
|
|
return E('li', {},
|
|
'Port ' + c.port + ': ' + (c.services || []).join(', ')
|
|
);
|
|
})
|
|
)
|
|
]) : null,
|
|
|
|
// Tor Hidden Services section
|
|
E('div', { 'class': 'exposure-section' }, [
|
|
E('div', { 'class': 'exposure-section-header' }, [
|
|
E('div', { 'class': 'exposure-section-title' }, [
|
|
E('span', { 'class': 'icon' }, '\ud83e\uddc5'),
|
|
'Tor Hidden Services'
|
|
]),
|
|
E('a', {
|
|
'href': L.url('admin/secubox/network/exposure/tor'),
|
|
'class': 'btn-action btn-primary'
|
|
}, 'Manage')
|
|
]),
|
|
(tor.services && tor.services.length > 0) ?
|
|
E('table', { 'class': 'exposure-table' }, [
|
|
E('thead', {}, [
|
|
E('tr', {}, [
|
|
E('th', {}, 'Service'),
|
|
E('th', {}, 'Onion Address')
|
|
])
|
|
]),
|
|
E('tbody', {},
|
|
tor.services.map(function(svc) {
|
|
return E('tr', {}, [
|
|
E('td', {}, svc.service),
|
|
E('td', { 'class': 'onion-address' }, svc.onion)
|
|
]);
|
|
})
|
|
)
|
|
]) :
|
|
E('div', { 'class': 'exposure-empty' }, [
|
|
E('div', { 'class': 'icon' }, '\ud83e\uddc5'),
|
|
E('p', {}, 'No Tor hidden services configured')
|
|
])
|
|
]),
|
|
|
|
// SSL Backends section
|
|
E('div', { 'class': 'exposure-section' }, [
|
|
E('div', { 'class': 'exposure-section-header' }, [
|
|
E('div', { 'class': 'exposure-section-title' }, [
|
|
E('span', { 'class': 'icon' }, '\ud83d\udd12'),
|
|
'HAProxy SSL Backends'
|
|
]),
|
|
E('a', {
|
|
'href': L.url('admin/secubox/network/exposure/ssl'),
|
|
'class': 'btn-action btn-primary'
|
|
}, 'Manage')
|
|
]),
|
|
(ssl.backends && ssl.backends.length > 0) ?
|
|
E('table', { 'class': 'exposure-table' }, [
|
|
E('thead', {}, [
|
|
E('tr', {}, [
|
|
E('th', {}, 'Service'),
|
|
E('th', {}, 'Domain')
|
|
])
|
|
]),
|
|
E('tbody', {},
|
|
ssl.backends.map(function(b) {
|
|
return E('tr', {}, [
|
|
E('td', {}, b.service),
|
|
E('td', {}, b.domain)
|
|
]);
|
|
})
|
|
)
|
|
]) :
|
|
E('div', { 'class': 'exposure-empty' }, [
|
|
E('div', { 'class': 'icon' }, '\ud83d\udd12'),
|
|
E('p', {}, 'No SSL backends configured')
|
|
])
|
|
])
|
|
].filter(Boolean));
|
|
|
|
return view;
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|