secubox-openwrt/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/view/service-registry/publish.js
CyberMind-FR ccba39da62 feat(service-registry): Add unified service aggregation dashboard
Implement Service Registry LuCI app for unified service management:
- RPCD backend aggregating services from HAProxy, Tor, netstat, LXC
- One-click publish to clearnet (HAProxy+ACME) and/or Tor hidden service
- Static landing page generator with QR codes for all URLs
- LuCI dashboard with service grid, quick publish form
- CLI tool (secubox-registry) for command-line management
- Share buttons for X, Telegram, WhatsApp

RPCD methods: list_services, publish_service, unpublish_service,
generate_landing_page, get_qr_data, list_categories

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 05:04:26 +01:00

284 lines
8.5 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require ui';
'require form';
'require service-registry/api as api';
return view.extend({
title: _('Publish Service'),
load: function() {
return Promise.all([
api.listCategories(),
api.getUnpublishedServices()
]);
},
render: function(data) {
var self = this;
var categories = data[0].categories || [];
var unpublished = data[1] || [];
// Load CSS
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = L.resource('service-registry/registry.css');
document.head.appendChild(link);
var m, s, o;
m = new form.Map('service-registry', _('Publish New Service'),
_('Create a new published service with HAProxy reverse proxy and/or Tor hidden service.'));
s = m.section(form.NamedSection, '_new', 'service', _('Service Details'));
s.anonymous = true;
s.addremove = false;
o = s.option(form.Value, 'name', _('Service Name'));
o.placeholder = 'e.g., Gitea, Nextcloud';
o.rmempty = false;
o.validate = function(section_id, value) {
if (!value || value.trim() === '')
return _('Name is required');
if (!/^[a-zA-Z0-9\s_-]+$/.test(value))
return _('Name can only contain letters, numbers, spaces, dashes, and underscores');
return true;
};
o = s.option(form.Value, 'local_port', _('Local Port'),
_('The port where the service is listening locally'));
o.datatype = 'port';
o.placeholder = '3000';
o.rmempty = false;
o = s.option(form.ListValue, 'category', _('Category'));
o.value('services', _('Services'));
categories.forEach(function(cat) {
if (cat.id !== 'services') {
o.value(cat.id, cat.name);
}
});
o.default = 'services';
o = s.option(form.Value, 'icon', _('Icon'),
_('Icon name: server, music, shield, chart, git, blog, app, security, etc.'));
o.placeholder = 'server';
o.optional = true;
// HAProxy section
s = m.section(form.NamedSection, '_haproxy', 'haproxy', _('HAProxy (Clearnet)'),
_('Configure a public domain with automatic HTTPS certificate'));
s.anonymous = true;
o = s.option(form.Flag, 'enabled', _('Enable HAProxy Vhost'));
o.default = '0';
o = s.option(form.Value, 'domain', _('Domain'),
_('Public domain name (must point to this server)'));
o.placeholder = 'service.example.com';
o.depends('enabled', '1');
o.validate = function(section_id, value) {
if (!value) return true;
if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value))
return _('Invalid domain format');
return true;
};
o = s.option(form.Flag, 'ssl', _('Enable SSL/TLS'),
_('Request ACME certificate automatically'));
o.default = '1';
o.depends('enabled', '1');
o = s.option(form.Flag, 'ssl_redirect', _('Force HTTPS'),
_('Redirect HTTP to HTTPS'));
o.default = '1';
o.depends('ssl', '1');
// Tor section
s = m.section(form.NamedSection, '_tor', 'tor', _('Tor Hidden Service'),
_('Create a .onion address for anonymous access'));
s.anonymous = true;
o = s.option(form.Flag, 'enabled', _('Enable Tor Hidden Service'));
o.default = '0';
o = s.option(form.Value, 'virtual_port', _('Virtual Port'),
_('The port that will be exposed on the .onion address'));
o.datatype = 'port';
o.default = '80';
o.depends('enabled', '1');
return m.render().then(function(mapEl) {
// Add custom publish button
var publishBtn = E('button', {
'class': 'cbi-button cbi-button-apply',
'style': 'margin-top: 20px;',
'click': ui.createHandlerFn(self, 'handlePublish', m)
}, _('Publish Service'));
mapEl.appendChild(E('div', { 'class': 'cbi-page-actions' }, [publishBtn]));
// Add discoverable services section
if (unpublished.length > 0) {
mapEl.appendChild(E('div', { 'class': 'cbi-section', 'style': 'margin-top: 30px;' }, [
E('h3', {}, _('Discovered Services')),
E('p', {}, _('These services are running but not yet published:')),
E('div', { 'class': 'sr-grid' },
unpublished.slice(0, 10).map(function(svc) {
return self.renderDiscoveredCard(svc);
})
)
]));
}
return mapEl;
});
},
renderDiscoveredCard: function(service) {
var self = this;
return E('div', {
'class': 'sr-card',
'style': 'cursor: pointer;',
'click': function() {
self.prefillForm(service);
}
}, [
E('div', { 'class': 'sr-card-header' }, [
E('div', { 'class': 'sr-card-title' }, service.name || 'Port ' + service.local_port),
E('span', { 'class': 'sr-card-status sr-status-running' }, 'running')
]),
E('p', { 'style': 'font-size: 0.9em; color: #666;' },
_('Port: ') + service.local_port + ' | ' + _('Category: ') + (service.category || 'other'))
]);
},
prefillForm: function(service) {
var nameInput = document.querySelector('input[id*="name"]');
var portInput = document.querySelector('input[id*="local_port"]');
if (nameInput) nameInput.value = service.name || '';
if (portInput) portInput.value = service.local_port || '';
nameInput && nameInput.focus();
},
handlePublish: function(map) {
var self = this;
// Get form values
var nameEl = document.querySelector('input[id*="_new"][id*="name"]');
var portEl = document.querySelector('input[id*="_new"][id*="local_port"]');
var categoryEl = document.querySelector('select[id*="_new"][id*="category"]');
var iconEl = document.querySelector('input[id*="_new"][id*="icon"]');
var haproxyEnabledEl = document.querySelector('input[id*="_haproxy"][id*="enabled"]');
var domainEl = document.querySelector('input[id*="_haproxy"][id*="domain"]');
var torEnabledEl = document.querySelector('input[id*="_tor"][id*="enabled"]');
var name = nameEl ? nameEl.value.trim() : '';
var port = portEl ? parseInt(portEl.value) : 0;
var category = categoryEl ? categoryEl.value : 'services';
var icon = iconEl ? iconEl.value.trim() : '';
var haproxyEnabled = haproxyEnabledEl ? haproxyEnabledEl.checked : false;
var domain = domainEl ? domainEl.value.trim() : '';
var torEnabled = torEnabledEl ? torEnabledEl.checked : false;
// Validation
if (!name) {
ui.addNotification(null, E('p', _('Service name is required')), 'error');
return;
}
if (!port || port < 1 || port > 65535) {
ui.addNotification(null, E('p', _('Valid port number is required')), 'error');
return;
}
if (haproxyEnabled && !domain) {
ui.addNotification(null, E('p', _('Domain is required when HAProxy is enabled')), 'error');
return;
}
ui.showModal(_('Publishing Service'), [
E('p', { 'class': 'spinning' }, _('Creating service endpoints...')),
E('ul', {}, [
haproxyEnabled ? E('li', {}, _('Creating HAProxy vhost for ') + domain) : null,
haproxyEnabled ? E('li', {}, _('Requesting SSL certificate...')) : null,
torEnabled ? E('li', {}, _('Creating Tor hidden service...')) : null
].filter(Boolean))
]);
return api.publishService(
name,
port,
haproxyEnabled ? domain : '',
torEnabled,
category,
icon
).then(function(result) {
ui.hideModal();
if (result.success) {
self.showSuccessModal(result);
} else {
ui.addNotification(null, E('p', _('Failed to publish: ') + (result.error || 'Unknown error')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + err.message), 'error');
});
},
showSuccessModal: function(result) {
var urls = result.urls || {};
var content = [
E('div', { 'style': 'text-align: center; padding: 20px;' }, [
E('h3', { 'style': 'color: #22c55e;' }, _('Service Published!')),
E('p', {}, result.name)
])
];
var urlsDiv = E('div', { 'style': 'margin: 20px 0;' });
if (urls.local) {
urlsDiv.appendChild(E('div', { 'style': 'margin: 10px 0;' }, [
E('strong', {}, _('Local: ')),
E('code', {}, urls.local)
]));
}
if (urls.clearnet) {
urlsDiv.appendChild(E('div', { 'style': 'margin: 10px 0;' }, [
E('strong', {}, _('Clearnet: ')),
E('a', { 'href': urls.clearnet, 'target': '_blank' }, urls.clearnet)
]));
}
if (urls.onion) {
urlsDiv.appendChild(E('div', { 'style': 'margin: 10px 0;' }, [
E('strong', {}, _('Onion: ')),
E('code', { 'style': 'word-break: break-all;' }, urls.onion)
]));
}
content.push(urlsDiv);
content.push(E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button',
'click': function() {
ui.hideModal();
window.location.href = L.url('admin/services/service-registry/overview');
}
}, _('Go to Overview')),
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': function() {
ui.hideModal();
window.location.reload();
}
}, _('Publish Another'))
]));
ui.showModal(_('Success'), content);
}
});