feat(routes-status): Add KISS UI theme integration

- Rewrite overview.js with KissTheme.wrap() for consistent SecuBox styling
- Add header chips for stats (vhosts, active, missing routes, WAF bypass, SSL)
- Add service status cards (HAProxy, mitmproxy, host IP)
- Add to KISS navigation under Network → Routes Status

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-04 11:02:33 +01:00
parent eb9adfd06a
commit 5b8c7f498a
2 changed files with 140 additions and 156 deletions

View File

@ -3,6 +3,7 @@
'require dom'; 'require dom';
'require ui'; 'require ui';
'require rpc'; 'require rpc';
'require secubox/kiss-theme';
var callStatus = rpc.declare({ var callStatus = rpc.declare({
object: 'luci.routes-status', object: 'luci.routes-status',
@ -28,66 +29,31 @@ return view.extend({
return callStatus(); return callStatus();
}, },
renderStatusBadge: function(running, label) { renderHeaderChip: function(icon, label, value, tone) {
var color = running ? '#4CAF50' : '#f44336'; var display = (value == null ? '—' : value).toString();
return E('span', { return E('div', { 'class': 'sh-header-chip' + (tone ? ' ' + tone : '') }, [
'style': 'display:inline-block;padding:4px 12px;margin:4px;border-radius:4px;color:#fff;background:' + color + ';font-size:0.9em;font-weight:500;' E('span', { 'class': 'sh-chip-icon' }, icon),
}, label + ': ' + (running ? 'Running' : 'Stopped')); E('div', { 'class': 'sh-chip-text' }, [
E('span', { 'class': 'sh-chip-label' }, label),
E('strong', {}, display)
])
]);
}, },
renderRouteBadge: function(hasRoute, type) { renderPill: function(text, type) {
if (hasRoute) { var colors = {
return E('span', { success: '#4CAF50',
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:#4CAF50;font-size:0.8em;' warning: '#ff9800',
}, type); danger: '#f44336',
} else { info: '#2196F3',
return E('span', { muted: '#9e9e9e'
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:#ff9800;font-size:0.8em;' };
}, type + ' (missing)');
}
},
renderSslBadge: function(status) {
var color, text;
if (status === 'missing') {
color = '#9e9e9e';
text = 'No SSL';
} else if (status === 'expired') {
color = '#f44336';
text = 'Expired';
} else if (status && status.indexOf('expiring:') === 0) {
var days = status.split(':')[1];
color = '#ff9800';
text = 'Expires in ' + days + 'd';
} else if (status && status.indexOf('valid:') === 0) {
var days = status.split(':')[1];
color = '#4CAF50';
text = 'Valid (' + days + 'd)';
} else {
color = '#4CAF50';
text = 'Valid';
}
return E('span', { return E('span', {
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:' + color + ';font-size:0.8em;' '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); }, text);
}, },
renderWafBadge: function(bypass) {
if (bypass) {
return E('span', {
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:#f44336;font-size:0.8em;'
}, 'WAF Bypass');
} else {
return E('span', {
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:#2196F3;font-size:0.8em;'
}, 'WAF Protected');
}
},
handleSync: function() { handleSync: function() {
var self = this;
ui.showModal(_('Syncing Routes...'), [ ui.showModal(_('Syncing Routes...'), [
E('p', { 'class': 'spinning' }, _('Please wait...')) E('p', { 'class': 'spinning' }, _('Please wait...'))
]); ]);
@ -106,36 +72,31 @@ return view.extend({
handleAddRoute: function(domain, port) { handleAddRoute: function(domain, port) {
var self = this; var self = this;
if (!port) { ui.showModal(_('Add Route'), [
// Ask for port E('div', { 'class': 'cbi-section' }, [
ui.showModal(_('Add Route'), [ E('p', {}, _('Add mitmproxy route for: %s').format(domain)),
E('div', { 'class': 'cbi-section' }, [ E('div', { 'class': 'cbi-value' }, [
E('p', {}, _('Add mitmproxy route for: %s').format(domain)), E('label', { 'class': 'cbi-value-title' }, _('Backend Port')),
E('div', { 'class': 'cbi-value' }, [ E('div', { 'class': 'cbi-value-field' }, [
E('label', { 'class': 'cbi-value-title' }, _('Backend Port')), E('input', { 'type': 'number', 'id': 'route-port', 'value': port || '443', 'style': 'width:100px;' })
E('div', { 'class': 'cbi-value-field' }, [
E('input', { 'type': 'number', 'id': 'route-port', 'value': '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 port = parseInt(document.getElementById('route-port').value, 10);
if (port > 0) {
ui.hideModal();
self.doAddRoute(domain, port);
}
},
'style': 'margin-left:10px;'
}, _('Add Route'))
]) ])
]); ]),
} else { E('div', { 'class': 'right' }, [
this.doAddRoute(domain, parseInt(port, 10)); 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) { doAddRoute: function(domain, port) {
@ -158,31 +119,35 @@ return view.extend({
var self = this; var self = this;
var missingRoutes = !vhost.has_route_out || !vhost.has_route_in; var missingRoutes = !vhost.has_route_out || !vhost.has_route_in;
return E('tr', { 'class': vhost.active ? '' : 'inactive' }, [ return E('tr', {}, [
E('td', {}, [ E('td', {}, [
E('a', { E('a', {
'href': 'https://' + vhost.domain, 'href': 'https://' + vhost.domain,
'target': '_blank', 'target': '_blank',
'style': 'color:#1976D2;text-decoration:none;' 'style': 'color:#1976D2;text-decoration:none;font-weight:500;'
}, vhost.domain) }, vhost.domain)
]), ]),
E('td', {}, vhost.backend || '-'), E('td', {}, vhost.backend || '-'),
E('td', { 'style': 'text-align:center;' }, vhost.backend_port || '-'),
E('td', {}, [ E('td', {}, [
this.renderRouteBadge(vhost.has_route_out, 'OUT'), vhost.has_route_out ? this.renderPill('OUT', 'success') : this.renderPill('OUT', 'warning'),
this.renderRouteBadge(vhost.has_route_in, 'IN') vhost.has_route_in ? this.renderPill('IN', 'success') : this.renderPill('IN', 'warning')
]), ]),
E('td', {}, this.renderSslBadge(vhost.ssl_status)),
E('td', {}, this.renderWafBadge(vhost.waf_bypass)),
E('td', {}, [ E('td', {}, [
vhost.active ? vhost.ssl_status === 'valid' ? this.renderPill('SSL', 'success') :
E('span', { 'style': 'color:#4CAF50;font-weight:bold;' }, 'Active') : vhost.ssl_status === 'expiring' ? this.renderPill('Expiring', 'warning') :
E('span', { 'style': 'color:#9e9e9e;' }, 'Inactive'), 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', { missingRoutes ? E('button', {
'class': 'btn cbi-button', 'class': 'cbi-button cbi-button-action',
'click': function() { self.handleAddRoute(vhost.domain, vhost.backend_port); }, 'click': function() { self.handleAddRoute(vhost.domain, vhost.backend_port); },
'style': 'margin-left:10px;font-size:0.8em;padding:2px 8px;' 'style': 'margin-left:8px;padding:2px 8px;font-size:0.8em;'
}, _('Add Route')) : null }, _('+ Route')) : null
]) ])
]); ]);
}, },
@ -196,76 +161,94 @@ return view.extend({
return a.domain.localeCompare(b.domain); return a.domain.localeCompare(b.domain);
}); });
// Count stats // Stats
var totalVhosts = vhosts.length; var totalVhosts = vhosts.length;
var activeVhosts = vhosts.filter(function(v) { return v.active; }).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 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 wafBypassed = vhosts.filter(function(v) { return v.waf_bypass; }).length;
var sslValid = vhosts.filter(function(v) { return v.ssl_status === 'valid'; }).length;
var content = [];
// Header
content.push(E('h2', {}, _('Routes Status Dashboard')));
// Service Status
content.push(E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Service Status')),
E('div', { 'style': 'margin:10px 0;' }, [
this.renderStatusBadge(data.haproxy_running, 'HAProxy'),
this.renderStatusBadge(data.mitmproxy_running, 'mitmproxy')
]),
E('p', { 'style': 'color:#666;' }, _('Host IP: %s').format(data.host_ip || '192.168.255.1'))
]));
// Statistics
content.push(E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Statistics')),
E('div', { 'style': 'display:flex;gap:30px;margin:15px 0;' }, [
E('div', { 'style': 'text-align:center;' }, [
E('div', { 'style': 'font-size:2em;font-weight:bold;color:#1976D2;' }, String(totalVhosts)),
E('div', { 'style': 'color:#666;' }, _('Total Vhosts'))
]),
E('div', { 'style': 'text-align:center;' }, [
E('div', { 'style': 'font-size:2em;font-weight:bold;color:#4CAF50;' }, String(activeVhosts)),
E('div', { 'style': 'color:#666;' }, _('Active'))
]),
E('div', { 'style': 'text-align:center;' }, [
E('div', { 'style': 'font-size:2em;font-weight:bold;color:' + (missingRoutes > 0 ? '#ff9800' : '#4CAF50') + ';' }, String(missingRoutes)),
E('div', { 'style': 'color:#666;' }, _('Missing Routes'))
]),
E('div', { 'style': 'text-align:center;' }, [
E('div', { 'style': 'font-size:2em;font-weight:bold;color:' + (wafBypassed > 0 ? '#f44336' : '#4CAF50') + ';' }, String(wafBypassed)),
E('div', { 'style': 'color:#666;' }, _('WAF Bypassed'))
])
])
]));
// Vhosts Table
var vhostRows = vhosts.map(function(v) { return self.renderVhostRow(v); }); var vhostRows = vhosts.map(function(v) { return self.renderVhostRow(v); });
content.push(E('div', { 'class': 'cbi-section' }, [ var content = E('div', { 'class': 'routes-status-page' }, [
E('h3', {}, _('Virtual Hosts')), // KISS Header
E('div', { 'class': 'cbi-page-actions', 'style': 'margin-bottom:15px;' }, [ E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [
E('button', { E('div', {}, [
'class': 'btn cbi-button-action', E('h2', { 'class': 'sh-page-title' }, [
'click': function() { self.handleSync(); } E('span', { 'class': 'sh-page-title-icon' }, '🔀'),
}, _('Sync Routes from HAProxy')) _('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)
])
]), ]),
vhosts.length > 0 ?
E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Domain')),
E('th', { 'class': 'th' }, _('Backend')),
E('th', { 'class': 'th', 'style': 'text-align:center;' }, _('Port')),
E('th', { 'class': 'th' }, _('Routes')),
E('th', { 'class': 'th' }, _('SSL')),
E('th', { 'class': 'th' }, _('WAF')),
E('th', { 'class': 'th' }, _('Status'))
])
].concat(vhostRows)) :
E('p', { 'style': 'color:#666;' }, _('No virtual hosts configured.'))
]));
return E('div', { 'class': 'cbi-map' }, content); // 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/routes');
} }
}); });

View File

@ -68,6 +68,7 @@ var KissThemeClass = baseclass.extend({
{ name: 'Stats', path: 'admin/services/haproxy/stats' }, { name: 'Stats', path: 'admin/services/haproxy/stats' },
{ name: 'Settings', path: 'admin/services/haproxy/settings' } { name: 'Settings', path: 'admin/services/haproxy/settings' }
]}, ]},
{ icon: '🔀', name: 'Routes Status', path: 'admin/status/routes' },
{ icon: '🔒', name: 'WireGuard', path: 'admin/services/wireguard', tabs: [ { icon: '🔒', name: 'WireGuard', path: 'admin/services/wireguard', tabs: [
{ name: 'Wizard', path: 'admin/services/wireguard/wizard' }, { name: 'Wizard', path: 'admin/services/wireguard/wizard' },
{ name: 'Overview', path: 'admin/services/wireguard/overview' }, { name: 'Overview', path: 'admin/services/wireguard/overview' },