secubox-openwrt/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/view/service-registry/publish.js
CyberMind-FR 1bbd345cee refactor(luci): Mass KissTheme UI rework across all LuCI apps
Convert 90+ LuCI view files from legacy cbi-button-* classes to
KissTheme kiss-btn-* classes for consistent dark theme styling.

Pattern conversions applied:
- cbi-button-positive → kiss-btn-green
- cbi-button-negative/remove → kiss-btn-red
- cbi-button-apply → kiss-btn-cyan
- cbi-button-action → kiss-btn-blue
- cbi-button (plain) → kiss-btn

Also replaced hardcoded colors (#080, #c00, #888, etc.) with
CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.)
for proper dark theme compatibility.

Apps updated include: ai-gateway, auth-guardian, bandwidth-manager,
cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure,
glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager,
mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer,
metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status,
secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats,
service-registry, simplex, streamlit, system-hub, tor-shield,
traffic-shaper, vhost-manager, vortex-dns, vortex-firewall,
webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-12 11:09:34 +01:00

379 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require dom';
'require ui';
'require form';
'require service-registry/api as api';
'require secubox/kiss-theme';
// Category icons
var catIcons = {
'proxy': '🌐', 'privacy': '🧅', 'system': '⚙️', 'app': '📱',
'media': '🎵', 'security': '🔐', 'container': '📦', 'services': '🖥️',
'monitoring': '📊', 'other': '🔗'
};
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] && data[0].categories) || [];
var unpublished = data[1] || [];
// Inject styles
var style = document.createElement('style');
style.textContent = this.getStyles();
document.head.appendChild(style);
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') {
var icon = catIcons[cat.id] || '🔗';
o.value(cat.id, icon + ' ' + 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': 'kiss-btn kiss-btn-cyan',
'style': 'margin-top: 20px; font-size: 1.1em; padding: 10px 25px;',
'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', { 'class': 'pub-section-title' }, '🔍 ' + _('Discovered Services') + ' (' + unpublished.length + ')'),
E('p', { 'class': 'pub-hint' }, _('Click a service to pre-fill the form above')),
E('div', { 'class': 'pub-list' },
unpublished.slice(0, 15).map(function(svc) {
return self.renderDiscoveredRow(svc);
})
)
]));
}
return KissTheme.wrap(mapEl, 'admin/secubox/services/service-registry/publish');
});
},
renderDiscoveredRow: function(service) {
var self = this;
var catIcon = catIcons[service.category] || '🔗';
var statusIcon = service.status === 'running' ? '🟢' : service.status === 'stopped' ? '🔴' : '🟡';
return E('div', {
'class': 'pub-row',
'click': function() {
self.prefillForm(service);
}
}, [
E('span', { 'class': 'pub-col-status', 'title': service.status }, statusIcon),
E('span', { 'class': 'pub-col-icon' }, catIcon),
E('span', { 'class': 'pub-col-name' }, [
E('strong', {}, service.name || 'Port ' + service.local_port),
service.local_port ? E('span', { 'class': 'pub-port' }, ':' + service.local_port) : null
]),
E('span', { 'class': 'pub-col-cat' }, service.category || 'other'),
E('span', { 'class': 'pub-col-action' }, [
E('button', {
'class': 'pub-btn',
'title': 'Use this service',
'click': function(ev) {
ev.stopPropagation();
self.prefillForm(service);
}
}, '➡️')
])
]);
},
prefillForm: function(service) {
// Use specific selectors matching the form section
var nameInput = document.querySelector('input[id*="_new"][id*="name"]');
var portInput = document.querySelector('input[id*="_new"][id*="local_port"]');
var categorySelect = document.querySelector('select[id*="_new"][id*="category"]');
var iconInput = document.querySelector('input[id*="_new"][id*="icon"]');
// Set values and trigger change events for LuCI bindings
if (nameInput) {
nameInput.value = service.name || '';
nameInput.dispatchEvent(new Event('input', { bubbles: true }));
nameInput.dispatchEvent(new Event('change', { bubbles: true }));
}
if (portInput) {
portInput.value = service.local_port || '';
portInput.dispatchEvent(new Event('input', { bubbles: true }));
portInput.dispatchEvent(new Event('change', { bubbles: true }));
}
if (categorySelect && service.category) {
for (var i = 0; i < categorySelect.options.length; i++) {
if (categorySelect.options[i].value === service.category) {
categorySelect.selectedIndex = i;
categorySelect.dispatchEvent(new Event('change', { bubbles: true }));
break;
}
}
}
if (iconInput && service.icon) {
iconInput.value = service.icon;
iconInput.dispatchEvent(new Event('input', { bubbles: true }));
}
// Scroll to form and focus
var formSection = document.querySelector('.cbi-section');
if (formSection) formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
setTimeout(function() { nameInput && nameInput.focus(); }, 300);
ui.addNotification(null, E('p', '✅ ' + _('Form pre-filled with ') + (service.name || 'Port ' + service.local_port)), 'info');
},
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;
}
var steps = [];
if (haproxyEnabled) steps.push('🌐 Creating HAProxy vhost for ' + domain);
if (haproxyEnabled) steps.push('🔒 Requesting SSL certificate...');
if (torEnabled) steps.push('🧅 Creating Tor hidden service...');
ui.showModal('📤 ' + _('Publishing Service'), [
E('p', { 'class': 'spinning' }, _('Creating service endpoints...')),
E('ul', { 'style': 'margin-top: 15px;' }, steps.map(function(s) { return E('li', {}, s); }))
]);
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('div', { 'style': 'font-size: 3em; margin-bottom: 10px;' }, '✅'),
E('h3', { 'style': 'color: #22c55e; margin: 0;' }, _('Service Published!')),
E('p', { 'style': 'font-size: 1.2em; margin-top: 10px;' }, result.name)
])
];
var urlsDiv = E('div', { 'class': 'pub-urls' });
if (urls.local) {
urlsDiv.appendChild(E('div', { 'class': 'pub-url-row' }, [
E('span', { 'class': 'pub-url-icon' }, '🏠'),
E('span', { 'class': 'pub-url-label' }, _('Local')),
E('code', {}, urls.local)
]));
}
if (urls.clearnet) {
urlsDiv.appendChild(E('div', { 'class': 'pub-url-row' }, [
E('span', { 'class': 'pub-url-icon' }, '🌐'),
E('span', { 'class': 'pub-url-label' }, _('Clearnet')),
E('a', { 'href': urls.clearnet, 'target': '_blank' }, urls.clearnet + ' ↗')
]));
}
if (urls.onion) {
urlsDiv.appendChild(E('div', { 'class': 'pub-url-row' }, [
E('span', { 'class': 'pub-url-icon' }, '🧅'),
E('span', { 'class': 'pub-url-label' }, _('Onion')),
E('code', { 'style': 'font-size: 0.8em; word-break: break-all;' }, urls.onion)
]));
}
content.push(urlsDiv);
content.push(E('div', { 'class': 'right', 'style': 'margin-top: 20px;' }, [
E('button', {
'class': 'kiss-btn',
'click': function() {
ui.hideModal();
window.location.href = L.url('admin/services/service-registry/overview');
}
}, '📋 ' + _('Go to Overview')),
E('button', {
'class': 'kiss-btn kiss-btn-cyan',
'style': 'margin-left: 10px;',
'click': function() {
ui.hideModal();
window.location.reload();
}
}, ' ' + _('Publish Another'))
]));
ui.showModal('✅ ' + _('Success'), content);
},
getStyles: function() {
return `
.pub-section-title { font-size: 1.1em; margin: 0 0 10px 0; padding-bottom: 8px; border-bottom: 2px solid #0ff; color: #0ff; }
.pub-hint { color: #888; font-size: 0.9em; margin-bottom: 15px; }
.pub-list { border: 1px solid #ddd; border-radius: 6px; overflow: hidden; max-height: 400px; overflow-y: auto; }
@media (prefers-color-scheme: dark) { .pub-list { border-color: #444; } }
.pub-row { display: flex; align-items: center; padding: 10px 12px; border-bottom: 1px solid #eee; gap: 10px; cursor: pointer; transition: background 0.15s; }
.pub-row:last-child { border-bottom: none; }
.pub-row:hover { background: rgba(0,255,255,0.08); }
@media (prefers-color-scheme: dark) { .pub-row { border-bottom-color: #333; } }
.pub-col-status { width: 24px; text-align: center; }
.pub-col-icon { width: 24px; text-align: center; }
.pub-col-name { flex: 1; min-width: 120px; }
.pub-col-name strong { display: inline; }
.pub-port { font-size: 0.85em; color: #888; margin-left: 4px; }
.pub-col-cat { width: 80px; font-size: 0.85em; color: #666; }
.pub-col-action { width: 36px; }
.pub-btn { border: none; background: transparent; cursor: pointer; font-size: 1.1em; padding: 4px 8px; border-radius: 4px; transition: all 0.15s; }
.pub-btn:hover { background: rgba(0,153,204,0.15); }
.pub-urls { margin: 20px 0; padding: 15px; background: #f8f8f8; border-radius: 8px; }
@media (prefers-color-scheme: dark) { .pub-urls { background: #1a1a2e; } }
.pub-url-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid #eee; }
.pub-url-row:last-child { border-bottom: none; }
@media (prefers-color-scheme: dark) { .pub-url-row { border-bottom-color: #333; } }
.pub-url-icon { font-size: 1.2em; }
.pub-url-label { font-weight: 600; min-width: 70px; }
.pub-url-row a { color: #0099cc; text-decoration: none; }
.pub-url-row a:hover { text-decoration: underline; }
`;
}
});