feat(metrics): Add web traffic country stats to SecuBox Metrics
- Add callGetVisitStats RPC from security-threats API - Add WEB TRAFFIC section with total requests, bots/humans counts - Display country flags and visit counts for top 8 countries - Add TOP HOSTS section showing top 5 visited hosts - Green color theme for traffic sections Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bda567ed98
commit
947182ae54
@ -9,17 +9,30 @@ var callGetSystemOverview = rpc.declare({
|
|||||||
expect: { }
|
expect: { }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var callGetVisitStats = rpc.declare({
|
||||||
|
object: 'luci.secubox-security-threats',
|
||||||
|
method: 'get_visit_stats',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
load: function() {
|
load: function() {
|
||||||
return callGetSystemOverview();
|
return Promise.all([
|
||||||
|
callGetSystemOverview(),
|
||||||
|
callGetVisitStats().catch(function() { return {}; })
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(results) {
|
||||||
var overview = data || {};
|
var overview = results[0] || {};
|
||||||
|
var visitStats = results[1] || {};
|
||||||
var sys = overview.system || {};
|
var sys = overview.system || {};
|
||||||
var net = overview.network || {};
|
var net = overview.network || {};
|
||||||
var svc = overview.services || {};
|
var svc = overview.services || {};
|
||||||
var sec = overview.security || {};
|
var sec = overview.security || {};
|
||||||
|
var byCountry = visitStats.by_country || [];
|
||||||
|
var byHost = visitStats.by_host || [];
|
||||||
|
var botsHumans = visitStats.bots_vs_humans || {};
|
||||||
|
|
||||||
var style = E('style', {}, `
|
var style = E('style', {}, `
|
||||||
.metrics-container {
|
.metrics-container {
|
||||||
@ -53,6 +66,10 @@ return view.extend({
|
|||||||
background: rgba(255,0,100,0.1);
|
background: rgba(255,0,100,0.1);
|
||||||
border-color: rgba(255,0,100,0.4);
|
border-color: rgba(255,0,100,0.4);
|
||||||
}
|
}
|
||||||
|
.metrics-section.traffic {
|
||||||
|
background: rgba(0,255,136,0.1);
|
||||||
|
border-color: rgba(0,255,136,0.4);
|
||||||
|
}
|
||||||
.metrics-section h3 {
|
.metrics-section h3 {
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -64,6 +81,10 @@ return view.extend({
|
|||||||
color: #ff0064;
|
color: #ff0064;
|
||||||
border-color: rgba(255,0,100,0.4);
|
border-color: rgba(255,0,100,0.4);
|
||||||
}
|
}
|
||||||
|
.metrics-section.traffic h3 {
|
||||||
|
color: #00ff88;
|
||||||
|
border-color: rgba(0,255,136,0.4);
|
||||||
|
}
|
||||||
.metrics-row {
|
.metrics-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -80,6 +101,9 @@ return view.extend({
|
|||||||
.metrics-section.security .metrics-value {
|
.metrics-section.security .metrics-value {
|
||||||
color: #ff0064;
|
color: #ff0064;
|
||||||
}
|
}
|
||||||
|
.metrics-section.traffic .metrics-value {
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
.metrics-bar {
|
.metrics-bar {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: rgba(0,255,255,0.2);
|
background: rgba(0,255,255,0.2);
|
||||||
@ -92,14 +116,60 @@ return view.extend({
|
|||||||
background: linear-gradient(90deg, #0ff, #00ff88);
|
background: linear-gradient(90deg, #0ff, #00ff88);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
.country-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.country-tag {
|
||||||
|
background: rgba(0,255,136,0.2);
|
||||||
|
border: 1px solid rgba(0,255,136,0.4);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.country-tag .flag {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.host-list {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.host-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 3px 0;
|
||||||
|
border-bottom: 1px solid rgba(0,255,136,0.1);
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Build country tags
|
||||||
|
var countryTags = byCountry.slice(0, 8).map(function(c) {
|
||||||
|
var flag = c.country && c.country.length === 2 ?
|
||||||
|
String.fromCodePoint(...[...c.country.toUpperCase()].map(c => 0x1F1E6 - 65 + c.charCodeAt(0))) : '';
|
||||||
|
return E('span', { 'class': 'country-tag' }, [
|
||||||
|
E('span', { 'class': 'flag' }, flag),
|
||||||
|
(c.country || '??') + ' ' + c.count
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build host list
|
||||||
|
var hostItems = byHost.slice(0, 5).map(function(h) {
|
||||||
|
var host = h.host || '-';
|
||||||
|
if (host.length > 25) host = host.substring(0, 22) + '...';
|
||||||
|
return E('div', { 'class': 'host-item' }, [
|
||||||
|
E('span', {}, host),
|
||||||
|
E('span', { 'class': 'metrics-value' }, String(h.count || 0))
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
var container = E('div', { 'class': 'metrics-container' }, [
|
var container = E('div', { 'class': 'metrics-container' }, [
|
||||||
E('div', { 'class': 'metrics-header' }, '📊 SECUBOX SYSTEM METRICS'),
|
E('div', { 'class': 'metrics-header' }, 'SECUBOX SYSTEM METRICS'),
|
||||||
E('div', { 'class': 'metrics-grid' }, [
|
E('div', { 'class': 'metrics-grid' }, [
|
||||||
// System Health
|
// System Health
|
||||||
E('div', { 'class': 'metrics-section' }, [
|
E('div', { 'class': 'metrics-section' }, [
|
||||||
E('h3', {}, '⚡ SYSTEM HEALTH'),
|
E('h3', {}, 'SYSTEM HEALTH'),
|
||||||
E('div', { 'class': 'metrics-row' }, [
|
E('div', { 'class': 'metrics-row' }, [
|
||||||
E('span', { 'class': 'metrics-label' }, 'Load Average'),
|
E('span', { 'class': 'metrics-label' }, 'Load Average'),
|
||||||
E('span', { 'class': 'metrics-value' }, sys.load || 'N/A')
|
E('span', { 'class': 'metrics-value' }, sys.load || 'N/A')
|
||||||
@ -119,7 +189,7 @@ return view.extend({
|
|||||||
|
|
||||||
// Resources
|
// Resources
|
||||||
E('div', { 'class': 'metrics-section' }, [
|
E('div', { 'class': 'metrics-section' }, [
|
||||||
E('h3', {}, '💾 RESOURCES'),
|
E('h3', {}, 'RESOURCES'),
|
||||||
E('div', { 'class': 'metrics-row' }, [
|
E('div', { 'class': 'metrics-row' }, [
|
||||||
E('span', { 'class': 'metrics-label' }, 'Memory Free'),
|
E('span', { 'class': 'metrics-label' }, 'Memory Free'),
|
||||||
E('span', { 'class': 'metrics-value' }, (sys.mem_free || 0) + ' MB')
|
E('span', { 'class': 'metrics-value' }, (sys.mem_free || 0) + ' MB')
|
||||||
@ -141,9 +211,39 @@ return view.extend({
|
|||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
// Web Traffic - NEW
|
||||||
|
E('div', { 'class': 'metrics-section traffic' }, [
|
||||||
|
E('h3', {}, 'WEB TRAFFIC'),
|
||||||
|
E('div', { 'class': 'metrics-row' }, [
|
||||||
|
E('span', { 'class': 'metrics-label' }, 'Total Requests'),
|
||||||
|
E('span', { 'class': 'metrics-value' }, visitStats.total_requests || 0)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'metrics-row' }, [
|
||||||
|
E('span', { 'class': 'metrics-label' }, 'Bots'),
|
||||||
|
E('span', { 'class': 'metrics-value' }, botsHumans.bots || 0)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'metrics-row' }, [
|
||||||
|
E('span', { 'class': 'metrics-label' }, 'Humans'),
|
||||||
|
E('span', { 'class': 'metrics-value' }, botsHumans.humans || 0)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'metrics-row' }, [
|
||||||
|
E('span', { 'class': 'metrics-label' }, 'Countries'),
|
||||||
|
E('span', { 'class': 'metrics-value' }, byCountry.length)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'country-list' }, countryTags)
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Top Hosts - NEW
|
||||||
|
E('div', { 'class': 'metrics-section traffic' }, [
|
||||||
|
E('h3', {}, 'TOP HOSTS'),
|
||||||
|
E('div', { 'class': 'host-list' }, hostItems.length ? hostItems : [
|
||||||
|
E('div', { 'style': 'color:#666' }, 'No data')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
E('div', { 'class': 'metrics-section' }, [
|
E('div', { 'class': 'metrics-section' }, [
|
||||||
E('h3', {}, '🔧 SERVICES'),
|
E('h3', {}, 'SERVICES'),
|
||||||
E('div', { 'class': 'metrics-row' }, [
|
E('div', { 'class': 'metrics-row' }, [
|
||||||
E('span', { 'class': 'metrics-label' }, 'HAProxy Backends'),
|
E('span', { 'class': 'metrics-label' }, 'HAProxy Backends'),
|
||||||
E('span', { 'class': 'metrics-value' }, svc.haproxy_backends || 0)
|
E('span', { 'class': 'metrics-value' }, svc.haproxy_backends || 0)
|
||||||
@ -168,7 +268,7 @@ return view.extend({
|
|||||||
|
|
||||||
// Network
|
// Network
|
||||||
E('div', { 'class': 'metrics-section' }, [
|
E('div', { 'class': 'metrics-section' }, [
|
||||||
E('h3', {}, '🌐 NETWORK'),
|
E('h3', {}, 'NETWORK'),
|
||||||
E('div', { 'class': 'metrics-row' }, [
|
E('div', { 'class': 'metrics-row' }, [
|
||||||
E('span', { 'class': 'metrics-label' }, 'Active Connections'),
|
E('span', { 'class': 'metrics-label' }, 'Active Connections'),
|
||||||
E('span', { 'class': 'metrics-value' }, net.connections || 0)
|
E('span', { 'class': 'metrics-value' }, net.connections || 0)
|
||||||
@ -185,7 +285,7 @@ return view.extend({
|
|||||||
|
|
||||||
// Security
|
// Security
|
||||||
E('div', { 'class': 'metrics-section security' }, [
|
E('div', { 'class': 'metrics-section security' }, [
|
||||||
E('h3', {}, '🛡️ SECURITY (CrowdSec)'),
|
E('h3', {}, 'SECURITY (CrowdSec)'),
|
||||||
E('div', { 'class': 'metrics-row' }, [
|
E('div', { 'class': 'metrics-row' }, [
|
||||||
E('span', { 'class': 'metrics-label' }, 'Active Bans'),
|
E('span', { 'class': 'metrics-label' }, 'Active Bans'),
|
||||||
E('span', { 'class': 'metrics-value' }, sec.active_bans || 0)
|
E('span', { 'class': 'metrics-value' }, sec.active_bans || 0)
|
||||||
@ -212,26 +312,19 @@ return view.extend({
|
|||||||
|
|
||||||
// Auto-refresh every 10 seconds
|
// Auto-refresh every 10 seconds
|
||||||
poll.add(L.bind(function() {
|
poll.add(L.bind(function() {
|
||||||
return callGetSystemOverview().then(L.bind(function(newData) {
|
return Promise.all([
|
||||||
this.updateMetrics(container, newData);
|
callGetSystemOverview(),
|
||||||
|
callGetVisitStats().catch(function() { return {}; })
|
||||||
|
]).then(L.bind(function(newResults) {
|
||||||
|
this.updateMetrics(container, newResults);
|
||||||
}, this));
|
}, this));
|
||||||
}, this), 10);
|
}, this), 10);
|
||||||
|
|
||||||
return E('div', {}, [style, container]);
|
return E('div', {}, [style, container]);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMetrics: function(container, data) {
|
updateMetrics: function(container, results) {
|
||||||
var overview = data || {};
|
// For now, poll will trigger page refresh logic
|
||||||
var sys = overview.system || {};
|
// Full DOM update could be implemented here
|
||||||
var net = overview.network || {};
|
|
||||||
var svc = overview.services || {};
|
|
||||||
var sec = overview.security || {};
|
|
||||||
|
|
||||||
// Update values
|
|
||||||
var values = container.querySelectorAll('.metrics-value');
|
|
||||||
var bars = container.querySelectorAll('.metrics-bar-fill');
|
|
||||||
|
|
||||||
// This is a simplified update - in production you'd want to update specific elements
|
|
||||||
// For now, the poll will reload the view
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user