feat(streamlit): Add domain column with editing in dashboard

- Show domain column with editable input for non-exposed instances
- Show clickable domain link + edit button for exposed instances
- Add editDomain modal for changing domain on exposed instances
- Domain input pre-filled with default (id.gk2.secubox.in)
- Separate Status column for SSL/WAF badges
- Update API to support domain parameter in renameInstance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-25 11:18:01 +01:00
parent 93fcefa5c3
commit 80c496b57e
2 changed files with 113 additions and 32 deletions

View File

@ -195,7 +195,7 @@ var callRenameApp = rpc.declare({
var callRenameInstance = rpc.declare({ var callRenameInstance = rpc.declare({
object: 'luci.streamlit', object: 'luci.streamlit',
method: 'rename_instance', method: 'rename_instance',
params: ['id', 'name'], params: ['id', 'name', 'domain'],
expect: { result: {} } expect: { result: {} }
}); });
@ -457,8 +457,8 @@ return baseclass.extend({
return callRenameApp(id, name); return callRenameApp(id, name);
}, },
renameInstance: function(id, name) { renameInstance: function(id, name, domain) {
return callRenameInstance(id, name); return callRenameInstance(id, name, domain || '');
}, },
getGiteaConfig: function() { getGiteaConfig: function() {

View File

@ -147,33 +147,64 @@ return view.extend({
var certValid = exp.cert_valid; var certValid = exp.cert_valid;
var authRequired = exp.auth_required; var authRequired = exp.auth_required;
var wafEnabled = exp.waf_enabled; var wafEnabled = exp.waf_enabled;
var domain = exp.domain || '';
// Status indicator
var statusBadge;
if (isExposed && certValid) {
statusBadge = E('span', { 'style': 'background:#0a0; color:#fff; padding:2px 6px; border-radius:3px; font-size:11px' },
'\u2713 ' + (exp.domain || 'Exposed'));
} else if (isExposed) {
statusBadge = E('span', { 'style': 'background:#f90; color:#fff; padding:2px 6px; border-radius:3px; font-size:11px' },
'\u26A0 Cert pending');
} else {
statusBadge = E('span', { 'style': 'color:#999' }, _('Local only'));
}
// WAF badge (shown when exposed)
var wafBadge = '';
if (isExposed && wafEnabled) {
wafBadge = E('span', {
'style': 'display:inline-block; padding:2px 6px; border-radius:4px; font-size:0.85em; background:#d1ecf1; color:#0c5460; margin-left:4px',
'title': _('Traffic inspected by WAF (mitmproxy)')
}, 'WAF');
}
// Running indicator // Running indicator
var runStatus = inst.enabled ? var runStatus = inst.enabled ?
E('span', { 'style': 'color:#0a0' }, '\u25CF') : E('span', { 'style': 'color:#0a0' }, '\u25CF') :
E('span', { 'style': 'color:#999' }, '\u25CB'); E('span', { 'style': 'color:#999' }, '\u25CB');
// Domain/Vhost column - editable input or link
var domainCell;
if (isExposed && domain) {
// Show clickable link + edit button
domainCell = E('div', { 'style': 'display:flex; align-items:center; gap:4px' }, [
E('a', {
'href': 'https://' + domain,
'target': '_blank',
'style': 'font-size:12px; color:#007bff'
}, domain),
E('button', {
'class': 'cbi-button',
'style': 'padding:2px 6px; font-size:10px',
'title': _('Edit domain'),
'click': function() { self.editDomain(inst.id, domain); }
}, '\u270E')
]);
} else {
// Show input field for setting domain before expose
var inputId = 'domain-' + inst.id;
var defaultDomain = inst.id + '.gk2.secubox.in';
domainCell = E('input', {
'type': 'text',
'id': inputId,
'value': defaultDomain,
'style': 'width:160px; font-size:11px; padding:2px 4px',
'placeholder': 'domain.example.com'
});
}
// Status badges
var badges = [];
if (isExposed) {
if (certValid) {
badges.push(E('span', {
'style': 'background:#0a0; color:#fff; padding:2px 6px; border-radius:3px; font-size:10px'
}, '\u2713 SSL'));
} else {
badges.push(E('span', {
'style': 'background:#f90; color:#fff; padding:2px 6px; border-radius:3px; font-size:10px'
}, '\u26A0 Cert'));
}
if (wafEnabled) {
badges.push(E('span', {
'style': 'background:#d1ecf1; color:#0c5460; padding:2px 6px; border-radius:3px; font-size:10px; margin-left:2px'
}, 'WAF'));
}
} else {
badges.push(E('span', { 'style': 'color:#999; font-size:11px' }, _('Local')));
}
// Action buttons // Action buttons
var actions = []; var actions = [];
@ -204,8 +235,12 @@ return view.extend({
actions.push(E('button', { actions.push(E('button', {
'class': 'cbi-button cbi-button-positive', 'class': 'cbi-button cbi-button-positive',
'style': 'margin-left:4px', 'style': 'margin-left:4px',
'title': _('Expose (one-click)'), 'title': _('Expose with domain'),
'click': function() { self.exposeInstance(inst.id); } 'click': function() {
var input = document.getElementById('domain-' + inst.id);
var customDomain = input ? input.value.trim() : '';
self.exposeInstance(inst.id, customDomain);
}
}, '\u2197')); }, '\u2197'));
} }
@ -231,7 +266,8 @@ return view.extend({
E('td', {}, [runStatus, ' ', E('strong', {}, inst.id)]), E('td', {}, [runStatus, ' ', E('strong', {}, inst.id)]),
E('td', {}, inst.app || '-'), E('td', {}, inst.app || '-'),
E('td', {}, ':' + inst.port), E('td', {}, ':' + inst.port),
E('td', {}, [statusBadge, wafBadge]), E('td', {}, domainCell),
E('td', {}, badges),
E('td', {}, actions) E('td', {}, actions)
]); ]);
}); });
@ -241,7 +277,8 @@ return view.extend({
E('th', { 'class': 'th' }, _('Instance')), E('th', { 'class': 'th' }, _('Instance')),
E('th', { 'class': 'th' }, _('App')), E('th', { 'class': 'th' }, _('App')),
E('th', { 'class': 'th' }, _('Port')), E('th', { 'class': 'th' }, _('Port')),
E('th', { 'class': 'th' }, _('Exposure')), E('th', { 'class': 'th' }, _('Domain')),
E('th', { 'class': 'th' }, _('Status')),
E('th', { 'class': 'th' }, _('Actions')) E('th', { 'class': 'th' }, _('Actions'))
]) ])
].concat(rows)); ].concat(rows));
@ -473,14 +510,15 @@ return view.extend({
]); ]);
}, },
// One-click expose // One-click expose with optional domain
exposeInstance: function(id) { exposeInstance: function(id, domain) {
var self = this; var self = this;
domain = domain || '';
ui.showModal(_('Exposing...'), [ ui.showModal(_('Exposing...'), [
E('p', { 'class': 'spinning' }, _('Creating vhost + SSL certificate...')) E('p', { 'class': 'spinning' }, _('Creating vhost + SSL certificate for ') + (domain || id + '.gk2.secubox.in') + '...')
]); ]);
api.emancipateInstance(id, '').then(function(r) { api.emancipateInstance(id, domain).then(function(r) {
ui.hideModal(); ui.hideModal();
if (r && r.success) { if (r && r.success) {
ui.addNotification(null, E('p', {}, _('Exposed at: ') + r.url), 'success'); ui.addNotification(null, E('p', {}, _('Exposed at: ') + r.url), 'success');
@ -491,6 +529,49 @@ return view.extend({
}); });
}, },
// Edit domain for existing exposed instance
editDomain: function(id, currentDomain) {
var self = this;
ui.showModal(_('Edit Domain'), [
E('p', {}, _('Change domain for instance: ') + id),
E('input', {
'type': 'text',
'id': 'edit-domain-input',
'value': currentDomain,
'style': 'width:100%; margin:8px 0'
}),
E('p', { 'style': 'color:#666; font-size:12px' },
_('Note: Changing domain will request a new SSL certificate.')),
E('div', { 'class': 'right', 'style': 'margin-top:16px' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Cancel')),
E('button', {
'class': 'cbi-button cbi-button-positive',
'style': 'margin-left:8px',
'click': function() {
var newDomain = document.getElementById('edit-domain-input').value.trim();
if (!newDomain) {
ui.addNotification(null, E('p', {}, _('Domain cannot be empty')), 'error');
return;
}
ui.hideModal();
ui.showModal(_('Updating...'), [
E('p', { 'class': 'spinning' }, _('Updating domain and certificate...'))
]);
api.renameInstance(id, id, newDomain).then(function(r) {
ui.hideModal();
if (r && r.success) {
ui.addNotification(null, E('p', {}, _('Domain updated to: ') + newDomain), 'success');
self.refresh().then(function() { self.updateStatus(); });
} else {
ui.addNotification(null, E('p', {}, r.message || _('Failed')), 'error');
}
});
}
}, _('Save'))
])
]);
},
// Unpublish // Unpublish
unpublishInstance: function(id, domain) { unpublishInstance: function(id, domain) {
var self = this; var self = this;