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>
200 lines
6.4 KiB
JavaScript
200 lines
6.4 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require ui';
|
|
'require form';
|
|
'require fs';
|
|
'require service-registry/api as api';
|
|
|
|
return view.extend({
|
|
title: _('Landing Page'),
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
api.getLandingConfig(),
|
|
api.getPublishedServices()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
var config = data[0] || {};
|
|
var services = 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', _('Landing Page Configuration'),
|
|
_('Configure the public landing page that displays all published services with QR codes.'));
|
|
|
|
s = m.section(form.NamedSection, 'main', 'settings', _('Settings'));
|
|
|
|
o = s.option(form.Flag, 'landing_auto_regen', _('Auto-regenerate'),
|
|
_('Automatically regenerate landing page when services are published or unpublished'));
|
|
o.default = '1';
|
|
|
|
o = s.option(form.Value, 'landing_path', _('Landing Page Path'),
|
|
_('File path where the landing page will be generated'));
|
|
o.default = '/www/secubox-services.html';
|
|
o.readonly = true;
|
|
|
|
return m.render().then(function(mapEl) {
|
|
// Status section
|
|
var statusSection = E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Status')),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('File exists')),
|
|
E('div', { 'class': 'cbi-value-field' },
|
|
config.exists ?
|
|
E('span', { 'style': 'color: #22c55e;' }, _('Yes')) :
|
|
E('span', { 'style': 'color: #ef4444;' }, _('No'))
|
|
)
|
|
]),
|
|
config.exists && config.modified ? E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Last modified')),
|
|
E('div', { 'class': 'cbi-value-field' },
|
|
new Date(config.modified * 1000).toLocaleString()
|
|
)
|
|
]) : null,
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Published services')),
|
|
E('div', { 'class': 'cbi-value-field' }, String(services.length))
|
|
])
|
|
].filter(Boolean));
|
|
|
|
// Actions section
|
|
var actionsSection = E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Actions')),
|
|
E('div', { 'style': 'display: flex; gap: 15px; flex-wrap: wrap;' }, [
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-apply',
|
|
'click': ui.createHandlerFn(self, 'handleRegenerate')
|
|
}, _('Regenerate Landing Page')),
|
|
config.exists ? E('a', {
|
|
'class': 'cbi-button',
|
|
'href': '/secubox-services.html',
|
|
'target': '_blank'
|
|
}, _('View Landing Page')) : null,
|
|
config.exists ? E('button', {
|
|
'class': 'cbi-button',
|
|
'click': ui.createHandlerFn(self, 'handlePreview')
|
|
}, _('Preview')) : null
|
|
].filter(Boolean))
|
|
]);
|
|
|
|
// Services preview
|
|
var previewSection = null;
|
|
if (services.length > 0) {
|
|
previewSection = E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Services on Landing Page')),
|
|
E('p', { 'style': 'color: #666; margin-bottom: 15px;' },
|
|
_('These services will be displayed on the landing page:')),
|
|
E('table', { 'class': 'table' }, [
|
|
E('tr', { 'class': 'tr table-titles' }, [
|
|
E('th', { 'class': 'th' }, _('Name')),
|
|
E('th', { 'class': 'th' }, _('Category')),
|
|
E('th', { 'class': 'th' }, _('Status')),
|
|
E('th', { 'class': 'th' }, _('Clearnet')),
|
|
E('th', { 'class': 'th' }, _('Onion'))
|
|
])
|
|
].concat(services.map(function(svc) {
|
|
var urls = svc.urls || {};
|
|
return E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, svc.name || svc.id),
|
|
E('td', { 'class': 'td' }, svc.category || '-'),
|
|
E('td', { 'class': 'td' }, [
|
|
E('span', {
|
|
'style': 'padding: 2px 8px; border-radius: 10px; font-size: 0.85em;' +
|
|
(svc.status === 'running' ? 'background: #dcfce7; color: #166534;' :
|
|
'background: #fee2e2; color: #991b1b;')
|
|
}, svc.status || 'unknown')
|
|
]),
|
|
E('td', { 'class': 'td' },
|
|
urls.clearnet ?
|
|
E('a', { 'href': urls.clearnet, 'target': '_blank' }, urls.clearnet) :
|
|
'-'
|
|
),
|
|
E('td', { 'class': 'td' },
|
|
urls.onion ?
|
|
E('span', { 'style': 'font-size: 0.85em; word-break: break-all;' },
|
|
urls.onion.substring(0, 30) + '...') :
|
|
'-'
|
|
)
|
|
]);
|
|
})))
|
|
]);
|
|
}
|
|
|
|
// Customization info
|
|
var customSection = E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Customization')),
|
|
E('p', {}, _('The landing page includes:')),
|
|
E('ul', {}, [
|
|
E('li', {}, _('Responsive grid layout with service cards')),
|
|
E('li', {}, _('QR codes for clearnet and onion URLs')),
|
|
E('li', {}, _('Copy-to-clipboard functionality')),
|
|
E('li', {}, _('Real-time service status')),
|
|
E('li', {}, _('Dark mode support')),
|
|
E('li', {}, _('Share buttons for social media'))
|
|
]),
|
|
E('p', { 'style': 'margin-top: 15px; color: #666;' },
|
|
_('To customize the appearance, edit the template at:') +
|
|
' /usr/sbin/secubox-landing-gen')
|
|
]);
|
|
|
|
mapEl.appendChild(statusSection);
|
|
mapEl.appendChild(actionsSection);
|
|
if (previewSection) mapEl.appendChild(previewSection);
|
|
mapEl.appendChild(customSection);
|
|
|
|
return mapEl;
|
|
});
|
|
},
|
|
|
|
handleRegenerate: function() {
|
|
ui.showModal(_('Regenerating'), [
|
|
E('p', { 'class': 'spinning' }, _('Regenerating landing page...'))
|
|
]);
|
|
|
|
return api.generateLandingPage().then(function(result) {
|
|
ui.hideModal();
|
|
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', _('Landing page regenerated successfully')), 'info');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', _('Failed to regenerate: ') + (result.error || '')), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', _('Error: ') + err.message), 'error');
|
|
});
|
|
},
|
|
|
|
handlePreview: function() {
|
|
var self = this;
|
|
|
|
ui.showModal(_('Landing Page Preview'), [
|
|
E('div', { 'style': 'text-align: center;' }, [
|
|
E('iframe', {
|
|
'src': '/secubox-services.html',
|
|
'style': 'width: 100%; height: 500px; border: 1px solid #ddd; border-radius: 8px;'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'right', 'style': 'margin-top: 15px;' }, [
|
|
E('a', {
|
|
'class': 'cbi-button',
|
|
'href': '/secubox-services.html',
|
|
'target': '_blank'
|
|
}, _('Open in New Tab')),
|
|
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Close'))
|
|
])
|
|
], 'wide');
|
|
}
|
|
});
|