From 947182ae54ab2df7f23ef8fb4f9da89782554c06 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 10 Feb 2026 10:46:26 +0100 Subject: [PATCH] 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 --- .../resources/view/status/secubox-metrics.js | 141 +++++++++++++++--- 1 file changed, 117 insertions(+), 24 deletions(-) diff --git a/package/secubox/luci-app-secubox/htdocs/luci-static/resources/view/status/secubox-metrics.js b/package/secubox/luci-app-secubox/htdocs/luci-static/resources/view/status/secubox-metrics.js index ee634a50..3a2d4949 100644 --- a/package/secubox/luci-app-secubox/htdocs/luci-static/resources/view/status/secubox-metrics.js +++ b/package/secubox/luci-app-secubox/htdocs/luci-static/resources/view/status/secubox-metrics.js @@ -9,17 +9,30 @@ var callGetSystemOverview = rpc.declare({ expect: { } }); +var callGetVisitStats = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'get_visit_stats', + expect: { } +}); + return view.extend({ load: function() { - return callGetSystemOverview(); + return Promise.all([ + callGetSystemOverview(), + callGetVisitStats().catch(function() { return {}; }) + ]); }, - render: function(data) { - var overview = data || {}; + render: function(results) { + var overview = results[0] || {}; + var visitStats = results[1] || {}; var sys = overview.system || {}; var net = overview.network || {}; var svc = overview.services || {}; var sec = overview.security || {}; + var byCountry = visitStats.by_country || []; + var byHost = visitStats.by_host || []; + var botsHumans = visitStats.bots_vs_humans || {}; var style = E('style', {}, ` .metrics-container { @@ -53,6 +66,10 @@ return view.extend({ background: rgba(255,0,100,0.1); 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 { margin: 0 0 12px 0; font-size: 14px; @@ -64,6 +81,10 @@ return view.extend({ color: #ff0064; border-color: rgba(255,0,100,0.4); } + .metrics-section.traffic h3 { + color: #00ff88; + border-color: rgba(0,255,136,0.4); + } .metrics-row { display: flex; justify-content: space-between; @@ -80,6 +101,9 @@ return view.extend({ .metrics-section.security .metrics-value { color: #ff0064; } + .metrics-section.traffic .metrics-value { + color: #00ff88; + } .metrics-bar { height: 8px; background: rgba(0,255,255,0.2); @@ -92,14 +116,60 @@ return view.extend({ background: linear-gradient(90deg, #0ff, #00ff88); 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' }, [ - E('div', { 'class': 'metrics-header' }, '📊 SECUBOX SYSTEM METRICS'), + E('div', { 'class': 'metrics-header' }, 'SECUBOX SYSTEM METRICS'), E('div', { 'class': 'metrics-grid' }, [ // System Health E('div', { 'class': 'metrics-section' }, [ - E('h3', {}, '⚡ SYSTEM HEALTH'), + E('h3', {}, 'SYSTEM HEALTH'), E('div', { 'class': 'metrics-row' }, [ E('span', { 'class': 'metrics-label' }, 'Load Average'), E('span', { 'class': 'metrics-value' }, sys.load || 'N/A') @@ -119,7 +189,7 @@ return view.extend({ // Resources E('div', { 'class': 'metrics-section' }, [ - E('h3', {}, '💾 RESOURCES'), + E('h3', {}, 'RESOURCES'), E('div', { 'class': 'metrics-row' }, [ E('span', { 'class': 'metrics-label' }, 'Memory Free'), 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 E('div', { 'class': 'metrics-section' }, [ - E('h3', {}, '🔧 SERVICES'), + E('h3', {}, 'SERVICES'), E('div', { 'class': 'metrics-row' }, [ E('span', { 'class': 'metrics-label' }, 'HAProxy Backends'), E('span', { 'class': 'metrics-value' }, svc.haproxy_backends || 0) @@ -168,7 +268,7 @@ return view.extend({ // Network E('div', { 'class': 'metrics-section' }, [ - E('h3', {}, '🌐 NETWORK'), + E('h3', {}, 'NETWORK'), E('div', { 'class': 'metrics-row' }, [ E('span', { 'class': 'metrics-label' }, 'Active Connections'), E('span', { 'class': 'metrics-value' }, net.connections || 0) @@ -185,7 +285,7 @@ return view.extend({ // Security E('div', { 'class': 'metrics-section security' }, [ - E('h3', {}, '🛡️ SECURITY (CrowdSec)'), + E('h3', {}, 'SECURITY (CrowdSec)'), E('div', { 'class': 'metrics-row' }, [ E('span', { 'class': 'metrics-label' }, 'Active Bans'), E('span', { 'class': 'metrics-value' }, sec.active_bans || 0) @@ -212,26 +312,19 @@ return view.extend({ // Auto-refresh every 10 seconds poll.add(L.bind(function() { - return callGetSystemOverview().then(L.bind(function(newData) { - this.updateMetrics(container, newData); + return Promise.all([ + callGetSystemOverview(), + callGetVisitStats().catch(function() { return {}; }) + ]).then(L.bind(function(newResults) { + this.updateMetrics(container, newResults); }, this)); }, this), 10); return E('div', {}, [style, container]); }, - updateMetrics: function(container, data) { - var overview = data || {}; - var sys = overview.system || {}; - 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 + updateMetrics: function(container, results) { + // For now, poll will trigger page refresh logic + // Full DOM update could be implemented here } });