feat(metrics): Make dashboard live with 3-second polling
- Add poll.add() for continuous 3-second updates - Use data-attributes for efficient DOM targeting - Add CSS pulse animation on value changes - Add live indicator with timestamp - Implement updateValue, updateBar, updateList methods - No page rebuilds - direct element text updates - KISS and fast real-time metrics Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d8578653b4
commit
46655a2dca
@ -21,6 +21,12 @@ var callGetActiveSessions = rpc.declare({
|
||||
expect: { }
|
||||
});
|
||||
|
||||
// Helper to get country flag emoji
|
||||
function getFlag(country) {
|
||||
if (!country || country.length !== 2) return '';
|
||||
return String.fromCodePoint(...[...country.toUpperCase()].map(c => 0x1F1E6 - 65 + c.charCodeAt(0)));
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
@ -31,19 +37,7 @@ return view.extend({
|
||||
},
|
||||
|
||||
render: function(results) {
|
||||
var overview = results[0] || {};
|
||||
var visitStats = results[1] || {};
|
||||
var sessions = results[2] || {};
|
||||
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 sessionCounts = sessions.counts || {};
|
||||
var recentVisitors = sessions.recent_visitors || [];
|
||||
var topEndpoints = sessions.top_endpoints || [];
|
||||
var self = this;
|
||||
|
||||
var style = E('style', {}, `
|
||||
.metrics-container {
|
||||
@ -55,13 +49,40 @@ return view.extend({
|
||||
border: 1px solid #0ff;
|
||||
}
|
||||
.metrics-header {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border-bottom: 2px solid #0ff;
|
||||
}
|
||||
.live-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #0f0;
|
||||
}
|
||||
.live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #0f0;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.8); }
|
||||
}
|
||||
@keyframes valueChange {
|
||||
0% { background: rgba(0,255,255,0.3); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
.value-changed {
|
||||
animation: valueChange 0.5s ease-out;
|
||||
}
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
@ -72,6 +93,10 @@ return view.extend({
|
||||
border: 1px solid rgba(0,255,255,0.3);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.metrics-section:hover {
|
||||
border-color: rgba(0,255,255,0.6);
|
||||
}
|
||||
.metrics-section.security {
|
||||
background: rgba(255,0,100,0.1);
|
||||
@ -92,40 +117,24 @@ return view.extend({
|
||||
border-bottom: 1px solid rgba(0,255,255,0.3);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.metrics-section.security h3 {
|
||||
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-section.sessions h3 {
|
||||
color: #ffc800;
|
||||
border-color: rgba(255,200,0,0.4);
|
||||
}
|
||||
.metrics-section.security h3 { 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-section.sessions h3 { color: #ffc800; border-color: rgba(255,200,0,0.4); }
|
||||
.metrics-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.metrics-label {
|
||||
color: #888;
|
||||
}
|
||||
.metrics-label { color: #888; }
|
||||
.metrics-value {
|
||||
color: #0ff;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.metrics-section.security .metrics-value {
|
||||
color: #ff0064;
|
||||
}
|
||||
.metrics-section.traffic .metrics-value {
|
||||
color: #00ff88;
|
||||
}
|
||||
.metrics-section.sessions .metrics-value {
|
||||
color: #ffc800;
|
||||
}
|
||||
.metrics-section.security .metrics-value { color: #ff0064; }
|
||||
.metrics-section.traffic .metrics-value { color: #00ff88; }
|
||||
.metrics-section.sessions .metrics-value { color: #ffc800; }
|
||||
.metrics-bar {
|
||||
height: 8px;
|
||||
background: rgba(0,255,255,0.2);
|
||||
@ -137,6 +146,7 @@ return view.extend({
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #0ff, #00ff88);
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease-out;
|
||||
}
|
||||
.country-list {
|
||||
display: flex;
|
||||
@ -150,248 +160,220 @@ return view.extend({
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.country-tag .flag {
|
||||
margin-right: 4px;
|
||||
.country-tag:hover {
|
||||
background: rgba(0,255,136,0.4);
|
||||
}
|
||||
.host-list, .visitor-list, .endpoint-list {
|
||||
.list-container {
|
||||
font-size: 11px;
|
||||
margin-top: 8px;
|
||||
min-height: 60px;
|
||||
}
|
||||
.host-item, .visitor-item, .endpoint-item {
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid rgba(0,255,136,0.1);
|
||||
}
|
||||
.visitor-item {
|
||||
border-color: rgba(255,200,0,0.1);
|
||||
}
|
||||
.endpoint-item {
|
||||
border-color: rgba(255,200,0,0.1);
|
||||
border-bottom: 1px solid rgba(0,255,255,0.1);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.list-item:hover { background: rgba(0,255,255,0.1); }
|
||||
.sessions .list-item { border-color: rgba(255,200,0,0.1); }
|
||||
.sessions .list-item:hover { background: rgba(255,200,0,0.1); }
|
||||
.traffic .list-item { border-color: rgba(0,255,136,0.1); }
|
||||
.traffic .list-item:hover { background: rgba(0,255,136,0.1); }
|
||||
.no-data { color: #666; font-style: italic; }
|
||||
`);
|
||||
|
||||
// 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))
|
||||
]);
|
||||
});
|
||||
|
||||
// Build recent visitors list
|
||||
var visitorItems = recentVisitors.slice(0, 6).map(function(v) {
|
||||
var flag = v.country && v.country.length === 2 ?
|
||||
String.fromCodePoint(...[...v.country.toUpperCase()].map(c => 0x1F1E6 - 65 + c.charCodeAt(0))) : '';
|
||||
return E('div', { 'class': 'visitor-item' }, [
|
||||
E('span', {}, (v.ip || '-').substring(0, 15)),
|
||||
E('span', { 'class': 'metrics-value' }, flag + ' ' + (v.country || '??'))
|
||||
]);
|
||||
});
|
||||
|
||||
// Build top endpoints list
|
||||
var endpointItems = topEndpoints.slice(0, 5).map(function(e) {
|
||||
var path = e.path || '-';
|
||||
if (path.length > 28) path = path.substring(0, 25) + '...';
|
||||
return E('div', { 'class': 'endpoint-item' }, [
|
||||
E('span', {}, path),
|
||||
E('span', { 'class': 'metrics-value' }, String(e.count || 0))
|
||||
]);
|
||||
});
|
||||
|
||||
var container = E('div', { 'class': 'metrics-container' }, [
|
||||
E('div', { 'class': 'metrics-header' }, 'SECUBOX SYSTEM METRICS'),
|
||||
var container = E('div', { 'class': 'metrics-container', 'id': 'secubox-metrics' }, [
|
||||
E('div', { 'class': 'metrics-header' }, [
|
||||
E('span', {}, 'SECUBOX SYSTEM METRICS'),
|
||||
E('div', { 'class': 'live-indicator' }, [
|
||||
E('div', { 'class': 'live-dot' }),
|
||||
E('span', { 'id': 'last-update' }, 'LIVE')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'metrics-grid' }, [
|
||||
// System Health
|
||||
E('div', { 'class': 'metrics-section' }, [
|
||||
E('div', { 'class': 'metrics-section', 'id': 'section-system' }, [
|
||||
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')
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sys.load' }, '-')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'CPU Usage'),
|
||||
E('span', { 'class': 'metrics-value' }, (sys.cpu_used || 0) + '%')
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sys.cpu' }, '-')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-bar' }, [
|
||||
E('div', { 'class': 'metrics-bar-fill', 'style': 'width:' + (sys.cpu_used || 0) + '%' })
|
||||
E('div', { 'class': 'metrics-bar-fill', 'data-bar': 'cpu', 'style': 'width:0%' })
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Uptime'),
|
||||
E('span', { 'class': 'metrics-value' }, sys.uptime || 'N/A')
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sys.uptime' }, '-')
|
||||
])
|
||||
]),
|
||||
|
||||
// Resources
|
||||
E('div', { 'class': 'metrics-section' }, [
|
||||
E('div', { 'class': 'metrics-section', 'id': 'section-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')
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sys.mem_free' }, '-')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Memory Used'),
|
||||
E('span', { 'class': 'metrics-value' }, (sys.mem_pct || 0) + '%')
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sys.mem_pct' }, '-')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-bar' }, [
|
||||
E('div', { 'class': 'metrics-bar-fill', 'style': 'width:' + (sys.mem_pct || 0) + '%' })
|
||||
E('div', { 'class': 'metrics-bar-fill', 'data-bar': 'mem', 'style': 'width:0%' })
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Disk /'),
|
||||
E('span', { 'class': 'metrics-value' }, sys.disk_root || 'N/A')
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sys.disk_root' }, '-')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Disk /srv'),
|
||||
E('span', { 'class': 'metrics-value' }, sys.disk_srv || 'N/A')
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sys.disk_srv' }, '-')
|
||||
])
|
||||
]),
|
||||
|
||||
// Active Sessions - NEW
|
||||
E('div', { 'class': 'metrics-section sessions' }, [
|
||||
// Active Sessions
|
||||
E('div', { 'class': 'metrics-section sessions', 'id': 'section-sessions' }, [
|
||||
E('h3', {}, 'ACTIVE SESSIONS'),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Tor Circuits'),
|
||||
E('span', { 'class': 'metrics-value' }, sessionCounts.tor_circuits || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sess.tor' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'HTTPS Visitors'),
|
||||
E('span', { 'class': 'metrics-value' }, sessionCounts.https || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sess.https' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Streamlit'),
|
||||
E('span', { 'class': 'metrics-value' }, sessionCounts.streamlit || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sess.streamlit' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Mitmproxy'),
|
||||
E('span', { 'class': 'metrics-value' }, sessionCounts.mitmproxy || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sess.mitmproxy' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'SSH'),
|
||||
E('span', { 'class': 'metrics-value' }, sessionCounts.ssh || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sess.ssh' }, '0')
|
||||
])
|
||||
]),
|
||||
|
||||
// Recent Visitors - NEW
|
||||
E('div', { 'class': 'metrics-section sessions' }, [
|
||||
// Recent Visitors
|
||||
E('div', { 'class': 'metrics-section sessions', 'id': 'section-visitors' }, [
|
||||
E('h3', {}, 'RECENT VISITORS'),
|
||||
E('div', { 'class': 'visitor-list' }, visitorItems.length ? visitorItems : [
|
||||
E('div', { 'style': 'color:#666' }, 'No recent visitors')
|
||||
E('div', { 'class': 'list-container', 'data-list': 'visitors' }, [
|
||||
E('div', { 'class': 'no-data' }, 'Loading...')
|
||||
])
|
||||
]),
|
||||
|
||||
// Web Traffic
|
||||
E('div', { 'class': 'metrics-section traffic' }, [
|
||||
E('div', { 'class': 'metrics-section traffic', 'id': '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('span', { 'class': 'metrics-value', 'data-key': 'traffic.total' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Bots'),
|
||||
E('span', { 'class': 'metrics-value' }, botsHumans.bots || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'traffic.bots' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Humans'),
|
||||
E('span', { 'class': 'metrics-value' }, botsHumans.humans || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'traffic.humans' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Countries'),
|
||||
E('span', { 'class': 'metrics-value' }, byCountry.length)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'traffic.countries' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'country-list' }, countryTags)
|
||||
E('div', { 'class': 'country-list', 'data-list': 'countries' })
|
||||
]),
|
||||
|
||||
// Top Endpoints - NEW
|
||||
E('div', { 'class': 'metrics-section sessions' }, [
|
||||
E('h3', {}, 'TOP ENDPOINTS'),
|
||||
E('div', { 'class': 'endpoint-list' }, endpointItems.length ? endpointItems : [
|
||||
E('div', { 'style': 'color:#666' }, 'No data')
|
||||
// Top Hosts
|
||||
E('div', { 'class': 'metrics-section traffic', 'id': 'section-hosts' }, [
|
||||
E('h3', {}, 'TOP HOSTS'),
|
||||
E('div', { 'class': 'list-container', 'data-list': 'hosts' }, [
|
||||
E('div', { 'class': 'no-data' }, 'Loading...')
|
||||
])
|
||||
]),
|
||||
|
||||
// Services
|
||||
E('div', { 'class': 'metrics-section' }, [
|
||||
E('div', { 'class': 'metrics-section', 'id': 'section-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)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'svc.haproxy' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Virtual Hosts'),
|
||||
E('span', { 'class': 'metrics-value' }, svc.haproxy_vhosts || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'svc.vhosts' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'MetaBlogizer Sites'),
|
||||
E('span', { 'class': 'metrics-value' }, svc.metablog_sites || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'svc.metablog' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Streamlit Apps'),
|
||||
E('span', { 'class': 'metrics-value' }, svc.streamlit_apps || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'svc.streamlit' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Tor Onion Services'),
|
||||
E('span', { 'class': 'metrics-value' }, svc.tor_onions || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'svc.tor' }, '0')
|
||||
])
|
||||
]),
|
||||
|
||||
// Network
|
||||
E('div', { 'class': 'metrics-section' }, [
|
||||
E('div', { 'class': 'metrics-section', 'id': 'section-network' }, [
|
||||
E('h3', {}, 'NETWORK'),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Active Connections'),
|
||||
E('span', { 'class': 'metrics-value' }, net.connections || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'net.connections' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Tor (port 9040)'),
|
||||
E('span', { 'class': 'metrics-value' }, net.tor || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'net.tor' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'HTTPS (port 443)'),
|
||||
E('span', { 'class': 'metrics-value' }, net.https || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'net.https' }, '0')
|
||||
])
|
||||
]),
|
||||
|
||||
// Security
|
||||
E('div', { 'class': 'metrics-section security' }, [
|
||||
E('div', { 'class': 'metrics-section security', 'id': 'section-security' }, [
|
||||
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)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sec.bans' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'SSRF Attacks'),
|
||||
E('span', { 'class': 'metrics-value' }, sec.attacks_ssrf || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sec.ssrf' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Bot Scans'),
|
||||
E('span', { 'class': 'metrics-value' }, sec.attacks_botscan || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sec.botscan' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Brute Force'),
|
||||
E('span', { 'class': 'metrics-value' }, sec.attacks_brute || 0)
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sec.brute' }, '0')
|
||||
]),
|
||||
E('div', { 'class': 'metrics-row' }, [
|
||||
E('span', { 'class': 'metrics-label' }, 'Top Countries'),
|
||||
E('span', { 'class': 'metrics-value' }, sec.top_countries || 'N/A')
|
||||
E('span', { 'class': 'metrics-value', 'data-key': 'sec.countries', 'style': 'font-size:11px' }, '-')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
// Auto-refresh every 10 seconds
|
||||
// Initial update
|
||||
this.updateMetrics(container, results);
|
||||
|
||||
// Fast polling - every 3 seconds
|
||||
poll.add(L.bind(function() {
|
||||
return Promise.all([
|
||||
callGetSystemOverview(),
|
||||
@ -400,13 +382,132 @@ return view.extend({
|
||||
]).then(L.bind(function(newResults) {
|
||||
this.updateMetrics(container, newResults);
|
||||
}, this));
|
||||
}, this), 10);
|
||||
}, this), 3);
|
||||
|
||||
return E('div', {}, [style, container]);
|
||||
},
|
||||
|
||||
updateValue: function(container, key, value) {
|
||||
var el = container.querySelector('[data-key="' + key + '"]');
|
||||
if (el && el.textContent !== String(value)) {
|
||||
el.textContent = value;
|
||||
el.classList.remove('value-changed');
|
||||
void el.offsetWidth; // Trigger reflow
|
||||
el.classList.add('value-changed');
|
||||
}
|
||||
},
|
||||
|
||||
updateBar: function(container, name, percent) {
|
||||
var el = container.querySelector('[data-bar="' + name + '"]');
|
||||
if (el) {
|
||||
el.style.width = percent + '%';
|
||||
}
|
||||
},
|
||||
|
||||
updateList: function(container, name, items, renderFn) {
|
||||
var el = container.querySelector('[data-list="' + name + '"]');
|
||||
if (!el) return;
|
||||
|
||||
// Clear and rebuild
|
||||
while (el.firstChild) el.removeChild(el.firstChild);
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
el.appendChild(E('div', { 'class': 'no-data' }, 'No data'));
|
||||
return;
|
||||
}
|
||||
|
||||
items.slice(0, 6).forEach(function(item) {
|
||||
el.appendChild(renderFn(item));
|
||||
});
|
||||
},
|
||||
|
||||
updateMetrics: function(container, results) {
|
||||
// For now, poll will trigger page refresh logic
|
||||
// Full DOM update could be implemented here
|
||||
var overview = results[0] || {};
|
||||
var visitStats = results[1] || {};
|
||||
var sessions = results[2] || {};
|
||||
var sys = overview.system || {};
|
||||
var net = overview.network || {};
|
||||
var svc = overview.services || {};
|
||||
var sec = overview.security || {};
|
||||
var sessionCounts = sessions.counts || {};
|
||||
var byCountry = visitStats.by_country || [];
|
||||
var byHost = visitStats.by_host || [];
|
||||
var botsHumans = visitStats.bots_vs_humans || {};
|
||||
var recentVisitors = sessions.recent_visitors || [];
|
||||
|
||||
// Update timestamp
|
||||
var timeEl = container.querySelector('#last-update');
|
||||
if (timeEl) {
|
||||
var now = new Date();
|
||||
timeEl.textContent = 'LIVE ' + now.toLocaleTimeString();
|
||||
}
|
||||
|
||||
// System
|
||||
this.updateValue(container, 'sys.load', sys.load || 'N/A');
|
||||
this.updateValue(container, 'sys.cpu', (sys.cpu_used || 0) + '%');
|
||||
this.updateBar(container, 'cpu', sys.cpu_used || 0);
|
||||
this.updateValue(container, 'sys.uptime', sys.uptime || 'N/A');
|
||||
this.updateValue(container, 'sys.mem_free', (sys.mem_free || 0) + ' MB');
|
||||
this.updateValue(container, 'sys.mem_pct', (sys.mem_pct || 0) + '%');
|
||||
this.updateBar(container, 'mem', sys.mem_pct || 0);
|
||||
this.updateValue(container, 'sys.disk_root', sys.disk_root || 'N/A');
|
||||
this.updateValue(container, 'sys.disk_srv', sys.disk_srv || 'N/A');
|
||||
|
||||
// Sessions
|
||||
this.updateValue(container, 'sess.tor', sessionCounts.tor_circuits || 0);
|
||||
this.updateValue(container, 'sess.https', sessionCounts.https || 0);
|
||||
this.updateValue(container, 'sess.streamlit', sessionCounts.streamlit || 0);
|
||||
this.updateValue(container, 'sess.mitmproxy', sessionCounts.mitmproxy || 0);
|
||||
this.updateValue(container, 'sess.ssh', sessionCounts.ssh || 0);
|
||||
|
||||
// Traffic
|
||||
this.updateValue(container, 'traffic.total', visitStats.total_requests || 0);
|
||||
this.updateValue(container, 'traffic.bots', botsHumans.bots || 0);
|
||||
this.updateValue(container, 'traffic.humans', botsHumans.humans || 0);
|
||||
this.updateValue(container, 'traffic.countries', byCountry.length);
|
||||
|
||||
// Services
|
||||
this.updateValue(container, 'svc.haproxy', svc.haproxy_backends || 0);
|
||||
this.updateValue(container, 'svc.vhosts', svc.haproxy_vhosts || 0);
|
||||
this.updateValue(container, 'svc.metablog', svc.metablog_sites || 0);
|
||||
this.updateValue(container, 'svc.streamlit', svc.streamlit_apps || 0);
|
||||
this.updateValue(container, 'svc.tor', svc.tor_onions || 0);
|
||||
|
||||
// Network
|
||||
this.updateValue(container, 'net.connections', net.connections || 0);
|
||||
this.updateValue(container, 'net.tor', net.tor || 0);
|
||||
this.updateValue(container, 'net.https', net.https || 0);
|
||||
|
||||
// Security
|
||||
this.updateValue(container, 'sec.bans', sec.active_bans || 0);
|
||||
this.updateValue(container, 'sec.ssrf', sec.attacks_ssrf || 0);
|
||||
this.updateValue(container, 'sec.botscan', sec.attacks_botscan || 0);
|
||||
this.updateValue(container, 'sec.brute', sec.attacks_brute || 0);
|
||||
this.updateValue(container, 'sec.countries', sec.top_countries || 'N/A');
|
||||
|
||||
// Update country tags
|
||||
this.updateList(container, 'countries', byCountry, function(c) {
|
||||
return E('span', { 'class': 'country-tag' },
|
||||
getFlag(c.country) + ' ' + (c.country || '??') + ' ' + c.count
|
||||
);
|
||||
});
|
||||
|
||||
// Update hosts list
|
||||
this.updateList(container, 'hosts', byHost, function(h) {
|
||||
var host = h.host || '-';
|
||||
if (host.length > 25) host = host.substring(0, 22) + '...';
|
||||
return E('div', { 'class': 'list-item' }, [
|
||||
E('span', {}, host),
|
||||
E('span', { 'class': 'metrics-value' }, String(h.count || 0))
|
||||
]);
|
||||
});
|
||||
|
||||
// Update visitors list
|
||||
this.updateList(container, 'visitors', recentVisitors, function(v) {
|
||||
return E('div', { 'class': 'list-item' }, [
|
||||
E('span', {}, (v.ip || '-').substring(0, 15)),
|
||||
E('span', { 'class': 'metrics-value' }, getFlag(v.country) + ' ' + (v.country || '??'))
|
||||
]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user