secubox-openwrt/package/secubox/luci-app-routes-status/htdocs/luci-static/resources/view/routes-status/overview.js
CyberMind-FR 686fe113c5 feat(vhosts-checker): Add KISS UI dashboard for HAProxy vhosts status
Renamed from routes-status to vhosts-checker to avoid conflict with
OpenWrt's default network routes page.

- KISS UI theme with header chips and status cards
- Shows HAProxy vhosts with mitmproxy route status (OUT/IN)
- SSL certificate status indicators
- WAF bypass detection
- Sync routes and add missing route actions
- Accessible at Status → VHosts Checker and KISS UI Network → VHosts Checker

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 11:07:22 +01:00

255 lines
8.8 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require ui';
'require rpc';
'require secubox/kiss-theme';
var callStatus = rpc.declare({
object: 'luci.routes-status',
method: 'status',
expect: { }
});
var callSyncRoutes = rpc.declare({
object: 'luci.routes-status',
method: 'sync_routes',
expect: { }
});
var callAddRoute = rpc.declare({
object: 'luci.routes-status',
method: 'add_route',
params: ['domain', 'port'],
expect: { }
});
return view.extend({
load: function() {
return callStatus();
},
renderHeaderChip: function(icon, label, value, tone) {
var display = (value == null ? '—' : value).toString();
return E('div', { 'class': 'sh-header-chip' + (tone ? ' ' + tone : '') }, [
E('span', { 'class': 'sh-chip-icon' }, icon),
E('div', { 'class': 'sh-chip-text' }, [
E('span', { 'class': 'sh-chip-label' }, label),
E('strong', {}, display)
])
]);
},
renderPill: function(text, type) {
var colors = {
success: '#4CAF50',
warning: '#ff9800',
danger: '#f44336',
info: '#2196F3',
muted: '#9e9e9e'
};
return E('span', {
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:4px;color:#fff;background:' + (colors[type] || colors.muted) + ';font-size:0.8em;font-weight:500;'
}, text);
},
handleSync: function() {
ui.showModal(_('Syncing Routes...'), [
E('p', { 'class': 'spinning' }, _('Please wait...'))
]);
callSyncRoutes().then(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', {}, _('Routes synchronized successfully')), 'success');
location.reload();
} else {
ui.addNotification(null, E('p', {}, _('Error: ') + (res.error || 'Unknown error')), 'error');
}
});
},
handleAddRoute: function(domain, port) {
var self = this;
ui.showModal(_('Add Route'), [
E('div', { 'class': 'cbi-section' }, [
E('p', {}, _('Add mitmproxy route for: %s').format(domain)),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Backend Port')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', { 'type': 'number', 'id': 'route-port', 'value': port || '443', 'style': 'width:100px;' })
])
])
]),
E('div', { 'class': 'right' }, [
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
E('button', {
'class': 'btn cbi-button-positive',
'click': function() {
var p = parseInt(document.getElementById('route-port').value, 10);
if (p > 0) {
ui.hideModal();
self.doAddRoute(domain, p);
}
},
'style': 'margin-left:10px;'
}, _('Add Route'))
])
]);
},
doAddRoute: function(domain, port) {
ui.showModal(_('Adding Route...'), [
E('p', { 'class': 'spinning' }, _('Please wait...'))
]);
callAddRoute(domain, port).then(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', {}, _('Route added successfully')), 'success');
location.reload();
} else {
ui.addNotification(null, E('p', {}, _('Error: ') + (res.error || 'Unknown error')), 'error');
}
});
},
renderVhostRow: function(vhost) {
var self = this;
var missingRoutes = !vhost.has_route_out || !vhost.has_route_in;
return E('tr', {}, [
E('td', {}, [
E('a', {
'href': 'https://' + vhost.domain,
'target': '_blank',
'style': 'color:#1976D2;text-decoration:none;font-weight:500;'
}, vhost.domain)
]),
E('td', {}, vhost.backend || '-'),
E('td', {}, [
vhost.has_route_out ? this.renderPill('OUT', 'success') : this.renderPill('OUT', 'warning'),
vhost.has_route_in ? this.renderPill('IN', 'success') : this.renderPill('IN', 'warning')
]),
E('td', {}, [
vhost.ssl_status === 'valid' ? this.renderPill('SSL', 'success') :
vhost.ssl_status === 'expiring' ? this.renderPill('Expiring', 'warning') :
vhost.ssl_status === 'expired' ? this.renderPill('Expired', 'danger') :
this.renderPill('No SSL', 'muted')
]),
E('td', {}, [
vhost.waf_bypass ? this.renderPill('Bypass', 'danger') : this.renderPill('WAF', 'info')
]),
E('td', {}, [
vhost.active ? this.renderPill('Active', 'success') : this.renderPill('Inactive', 'muted'),
missingRoutes ? E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() { self.handleAddRoute(vhost.domain, vhost.backend_port); },
'style': 'margin-left:8px;padding:2px 8px;font-size:0.8em;'
}, _('+ Route')) : null
])
]);
},
render: function(data) {
var self = this;
var vhosts = data.vhosts || [];
// Sort by domain
vhosts.sort(function(a, b) {
return a.domain.localeCompare(b.domain);
});
// Stats
var totalVhosts = vhosts.length;
var activeVhosts = vhosts.filter(function(v) { return v.active; }).length;
var missingRoutes = vhosts.filter(function(v) { return !v.has_route_out || !v.has_route_in; }).length;
var wafBypassed = vhosts.filter(function(v) { return v.waf_bypass; }).length;
var sslValid = vhosts.filter(function(v) { return v.ssl_status === 'valid'; }).length;
var vhostRows = vhosts.map(function(v) { return self.renderVhostRow(v); });
var content = E('div', { 'class': 'routes-status-page' }, [
// KISS Header
E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🔀'),
_('Routes Status')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('HAProxy vhosts and mitmproxy route configuration overview.'))
]),
E('div', { 'class': 'sh-header-meta' }, [
this.renderHeaderChip('🌐', _('Vhosts'), totalVhosts),
this.renderHeaderChip('✅', _('Active'), activeVhosts),
this.renderHeaderChip('⚠️', _('Missing Routes'), missingRoutes, missingRoutes > 0 ? 'warn' : ''),
this.renderHeaderChip('🛡️', _('WAF Bypass'), wafBypassed, wafBypassed > 0 ? 'warn' : ''),
this.renderHeaderChip('🔒', _('SSL Valid'), sslValid)
])
]),
// Service Status Cards
E('div', { 'class': 'sh-card-grid', 'style': 'display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin:20px 0;' }, [
E('div', { 'class': 'sh-card', 'style': 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,0.1);' }, [
E('div', { 'style': 'display:flex;align-items:center;gap:8px;margin-bottom:8px;' }, [
E('span', { 'style': 'font-size:1.5em;' }, '⚖️'),
E('strong', {}, 'HAProxy')
]),
data.haproxy_running ?
this.renderPill('Running', 'success') :
this.renderPill('Stopped', 'danger')
]),
E('div', { 'class': 'sh-card', 'style': 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,0.1);' }, [
E('div', { 'style': 'display:flex;align-items:center;gap:8px;margin-bottom:8px;' }, [
E('span', { 'style': 'font-size:1.5em;' }, '🔍'),
E('strong', {}, 'mitmproxy')
]),
data.mitmproxy_running ?
this.renderPill('Running', 'success') :
this.renderPill('Stopped', 'danger')
]),
E('div', { 'class': 'sh-card', 'style': 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,0.1);' }, [
E('div', { 'style': 'display:flex;align-items:center;gap:8px;margin-bottom:8px;' }, [
E('span', { 'style': 'font-size:1.5em;' }, '🖥️'),
E('strong', {}, _('Host IP'))
]),
E('code', { 'style': 'background:#f5f5f5;padding:4px 8px;border-radius:4px;' }, data.host_ip || '192.168.255.1')
])
]),
// Actions
E('div', { 'style': 'margin:20px 0;' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() { self.handleSync(); },
'style': 'margin-right:10px;'
}, '🔄 ' + _('Sync Routes from HAProxy'))
]),
// Vhosts Table
E('div', { 'class': 'sh-card', 'style': 'background:#fff;border-radius:8px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,0.1);overflow-x:auto;' }, [
E('h3', { 'style': 'margin:0 0 16px 0;' }, '🌐 ' + _('Virtual Hosts (%d)').format(totalVhosts)),
vhosts.length > 0 ?
E('table', { 'class': 'table', 'style': 'width:100%;border-collapse:collapse;' }, [
E('thead', {}, [
E('tr', { 'style': 'background:#f5f5f5;' }, [
E('th', { 'style': 'padding:10px;text-align:left;' }, _('Domain')),
E('th', { 'style': 'padding:10px;text-align:left;' }, _('Backend')),
E('th', { 'style': 'padding:10px;text-align:left;' }, _('Routes')),
E('th', { 'style': 'padding:10px;text-align:left;' }, _('SSL')),
E('th', { 'style': 'padding:10px;text-align:left;' }, _('WAF')),
E('th', { 'style': 'padding:10px;text-align:left;' }, _('Status'))
])
]),
E('tbody', {}, vhostRows)
]) :
E('p', { 'style': 'color:#666;text-align:center;padding:20px;' }, _('No virtual hosts configured.'))
])
]);
return KissTheme.wrap([content], 'admin/status/vhosts-checker');
}
});