secubox-openwrt/package/secubox/luci-app-metrics-dashboard/htdocs/luci-static/resources/view/metrics/dashboard.js
CyberMind-FR a53d2b1d63 fix(metrics): Get WAF blocked count from CrowdSec mitmproxy decisions
- WAF blocked now counts mitmproxy scenario decisions (1031 blocks)
- Removed waf_threats field (redundant with waf_blocked)
- Fixed dashboard to show 3 WAF stats: Bans, Alerts, Blocked

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-17 12:41:14 +01:00

388 lines
14 KiB
JavaScript

'use strict';
'require view';
'require poll';
'require rpc';
'require secubox/kiss-theme';
var callOverview = rpc.declare({
object: 'luci.metrics',
method: 'overview',
expect: {}
});
var callWafStats = rpc.declare({
object: 'luci.metrics',
method: 'waf_stats',
expect: {}
});
var callConnections = rpc.declare({
object: 'luci.metrics',
method: 'connections',
expect: {}
});
function formatUptime(seconds) {
var d = Math.floor(seconds / 86400);
var h = Math.floor((seconds % 86400) / 3600);
var m = Math.floor((seconds % 3600) / 60);
if (d > 0) return d + 'd ' + h + 'h';
if (h > 0) return h + 'h ' + m + 'm';
return m + 'm';
}
function formatMem(kb) {
return (kb / 1048576).toFixed(1) + ' GB';
}
return view.extend({
load: function() {
// Inject custom CSS
var style = document.createElement('style');
style.textContent = `
.mx-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--kiss-border, #2a2a40);
}
.mx-title {
font-size: 22px;
font-weight: 600;
color: #fff;
}
.mx-live {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--kiss-muted, #888);
}
.mx-dot {
width: 8px;
height: 8px;
background: #00c853;
border-radius: 50%;
animation: blink 1.5s infinite;
}
@keyframes blink {
50% { opacity: 0.4; }
}
/* Stats Grid */
.mx-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.mx-card {
background: var(--kiss-bg2, #1a1a2e);
border: 1px solid var(--kiss-border, #2a2a40);
border-radius: 8px;
padding: 16px;
text-align: center;
}
.mx-card:hover {
border-color: var(--kiss-green, #00c853);
}
.mx-icon {
font-size: 24px;
margin-bottom: 8px;
}
.mx-val {
font-size: 28px;
font-weight: 700;
color: #fff;
line-height: 1;
}
.mx-val.green { color: #00c853; }
.mx-val.cyan { color: #00bcd4; }
.mx-val.orange { color: #ff9800; }
.mx-val.red { color: #f44336; }
.mx-val.purple { color: #ab47bc; }
.mx-lbl {
font-size: 11px;
color: var(--kiss-muted, #888);
text-transform: uppercase;
margin-top: 4px;
}
.mx-sub {
font-size: 10px;
color: #555;
margin-top: 4px;
}
/* Services bar */
.mx-svc {
display: flex;
gap: 20px;
align-items: center;
padding: 12px 16px;
background: var(--kiss-bg2, #1a1a2e);
border: 1px solid var(--kiss-border, #2a2a40);
border-radius: 8px;
margin-bottom: 20px;
}
.mx-svc-title {
font-size: 11px;
color: var(--kiss-muted, #888);
text-transform: uppercase;
}
.mx-svc-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #ccc;
}
.mx-svc-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.mx-svc-dot.on {
background: #00c853;
box-shadow: 0 0 6px #00c853;
}
.mx-svc-dot.off {
background: #f44336;
}
/* Panels */
.mx-panels {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 768px) {
.mx-panels { grid-template-columns: 1fr; }
}
.mx-panel {
background: var(--kiss-bg2, #1a1a2e);
border: 1px solid var(--kiss-border, #2a2a40);
border-radius: 8px;
padding: 16px;
}
.mx-panel-title {
font-size: 13px;
font-weight: 600;
color: #fff;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.mx-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--kiss-border, #2a2a40);
}
.mx-row:last-child { border-bottom: none; }
.mx-row-label {
font-size: 12px;
color: var(--kiss-muted, #888);
}
.mx-row-val {
font-size: 16px;
font-weight: 600;
color: #00bcd4;
}
`;
document.head.appendChild(style);
return Promise.all([
callOverview().catch(function() { return {}; }),
callWafStats().catch(function() { return {}; }),
callConnections().catch(function() { return {}; })
]);
},
render: function(data) {
var o = data[0] || {};
var w = data[1] || {};
var c = data[2] || {};
var memPct = o.mem_pct || 0;
var memCls = memPct > 85 ? 'red' : (memPct > 70 ? 'orange' : 'green');
var content = [
// Header
E('div', { 'class': 'mx-header' }, [
E('div', { 'class': 'mx-title' }, 'Metrics Dashboard'),
E('div', { 'class': 'mx-live' }, [
E('span', { 'class': 'mx-dot' }),
'LIVE',
E('span', { 'id': 'mx-time' }, new Date().toLocaleTimeString())
])
]),
// Main stats grid
E('div', { 'class': 'mx-grid' }, [
E('div', { 'class': 'mx-card' }, [
E('div', { 'class': 'mx-icon' }, '⏱'),
E('div', { 'class': 'mx-val green', 'id': 's-up' }, formatUptime(o.uptime || 0)),
E('div', { 'class': 'mx-lbl' }, 'Uptime'),
E('div', { 'class': 'mx-sub', 'id': 's-load' }, 'Load: ' + (o.load || '0'))
]),
E('div', { 'class': 'mx-card' }, [
E('div', { 'class': 'mx-icon' }, '🧠'),
E('div', { 'class': 'mx-val ' + memCls, 'id': 's-mem' }, memPct + '%'),
E('div', { 'class': 'mx-lbl' }, 'Memory'),
E('div', { 'class': 'mx-sub' }, formatMem(o.mem_used_kb || 0) + ' / ' + formatMem(o.mem_total_kb || 0))
]),
E('div', { 'class': 'mx-card' }, [
E('div', { 'class': 'mx-icon' }, '🌐'),
E('div', { 'class': 'mx-val cyan', 'id': 's-vh' }, String(o.vhosts || 0)),
E('div', { 'class': 'mx-lbl' }, 'vHosts')
]),
E('div', { 'class': 'mx-card' }, [
E('div', { 'class': 'mx-icon' }, '🔒'),
E('div', { 'class': 'mx-val purple', 'id': 's-cert' }, String(o.certificates || 0)),
E('div', { 'class': 'mx-lbl' }, 'SSL Certs')
]),
E('div', { 'class': 'mx-card' }, [
E('div', { 'class': 'mx-icon' }, '📄'),
E('div', { 'class': 'mx-val cyan' }, String(o.metablogs || 0)),
E('div', { 'class': 'mx-lbl' }, 'MetaBlogs')
]),
E('div', { 'class': 'mx-card' }, [
E('div', { 'class': 'mx-icon' }, '🐍'),
E('div', { 'class': 'mx-val green' }, String(o.streamlits || 0)),
E('div', { 'class': 'mx-lbl' }, 'Streamlits')
]),
E('div', { 'class': 'mx-card' }, [
E('div', { 'class': 'mx-icon' }, '📦'),
E('div', { 'class': 'mx-val purple' }, String(o.lxc_containers || 0)),
E('div', { 'class': 'mx-lbl' }, 'LXC')
]),
E('div', { 'class': 'mx-card' }, [
E('div', { 'class': 'mx-icon' }, '🔗'),
E('div', { 'class': 'mx-val cyan', 'id': 's-tcp' }, String(c.total_tcp || 0)),
E('div', { 'class': 'mx-lbl' }, 'TCP Conns')
])
]),
// Services bar
E('div', { 'class': 'mx-svc' }, [
E('span', { 'class': 'mx-svc-title' }, 'Services'),
E('div', { 'class': 'mx-svc-item' }, [
E('span', { 'class': 'mx-svc-dot ' + (o.haproxy ? 'on' : 'off'), 'id': 'sv-ha' }),
'HAProxy'
]),
E('div', { 'class': 'mx-svc-item' }, [
E('span', { 'class': 'mx-svc-dot ' + (o.mitmproxy ? 'on' : 'off'), 'id': 'sv-waf' }),
'WAF'
]),
E('div', { 'class': 'mx-svc-item' }, [
E('span', { 'class': 'mx-svc-dot ' + (o.crowdsec ? 'on' : 'off'), 'id': 'sv-cs' }),
'CrowdSec'
])
]),
// Panels
E('div', { 'class': 'mx-panels' }, [
// WAF Panel
E('div', { 'class': 'mx-panel' }, [
E('div', { 'class': 'mx-panel-title' }, [
E('span', {}, '🛡'),
'WAF & Security'
]),
E('div', { 'class': 'mx-row' }, [
E('span', { 'class': 'mx-row-label' }, 'Active Bans'),
E('span', { 'class': 'mx-row-val', 'id': 'w-bans', 'style': (w.active_bans || 0) > 0 ? 'color:#ff9800' : '' }, String(w.active_bans || 0))
]),
E('div', { 'class': 'mx-row' }, [
E('span', { 'class': 'mx-row-label' }, 'Alerts (24h)'),
E('span', { 'class': 'mx-row-val', 'id': 'w-alerts' }, String(w.alerts_today || 0))
]),
E('div', { 'class': 'mx-row' }, [
E('span', { 'class': 'mx-row-label' }, 'WAF Blocked'),
E('span', { 'class': 'mx-row-val', 'id': 'w-blocked', 'style': (w.waf_blocked || 0) > 0 ? 'color:#ff9800' : '' }, String(w.waf_blocked || 0))
])
]),
// Connections Panel
E('div', { 'class': 'mx-panel' }, [
E('div', { 'class': 'mx-panel-title' }, [
E('span', {}, '🔗'),
'Connections'
]),
E('div', { 'class': 'mx-row' }, [
E('span', { 'class': 'mx-row-label' }, 'HTTPS (443)'),
E('span', { 'class': 'mx-row-val', 'id': 'c-https' }, String(c.https || 0))
]),
E('div', { 'class': 'mx-row' }, [
E('span', { 'class': 'mx-row-label' }, 'HTTP (80)'),
E('span', { 'class': 'mx-row-val', 'id': 'c-http' }, String(c.http || 0))
]),
E('div', { 'class': 'mx-row' }, [
E('span', { 'class': 'mx-row-label' }, 'SSH (22)'),
E('span', { 'class': 'mx-row-val', 'id': 'c-ssh' }, String(c.ssh || 0))
]),
E('div', { 'class': 'mx-row' }, [
E('span', { 'class': 'mx-row-label' }, 'Total TCP'),
E('span', { 'class': 'mx-row-val', 'id': 'c-total', 'style': 'color:#ab47bc' }, String(c.total_tcp || 0))
])
])
])
];
// Setup polling
poll.add(L.bind(this.pollMetrics, this), 5);
// Clock
setInterval(function() {
var el = document.getElementById('mx-time');
if (el) el.textContent = new Date().toLocaleTimeString();
}, 1000);
return KissTheme.wrap(content, 'admin/status/metrics');
},
pollMetrics: function() {
return Promise.all([
callOverview(),
callWafStats(),
callConnections()
]).then(function(data) {
var o = data[0] || {};
var w = data[1] || {};
var c = data[2] || {};
var upd = {
's-up': formatUptime(o.uptime || 0),
's-load': 'Load: ' + (o.load || '0'),
's-mem': (o.mem_pct || 0) + '%',
's-vh': String(o.vhosts || 0),
's-cert': String(o.certificates || 0),
's-tcp': String(c.total_tcp || 0),
'w-bans': String(w.active_bans || 0),
'w-alerts': String(w.alerts_today || 0),
'w-blocked': String(w.waf_blocked || 0),
'c-https': String(c.https || 0),
'c-http': String(c.http || 0),
'c-ssh': String(c.ssh || 0),
'c-total': String(c.total_tcp || 0)
};
for (var id in upd) {
var el = document.getElementById(id);
if (el) el.textContent = upd[id];
}
// Service dots
var ha = document.getElementById('sv-ha');
var waf = document.getElementById('sv-waf');
var cs = document.getElementById('sv-cs');
if (ha) ha.className = 'mx-svc-dot ' + (o.haproxy ? 'on' : 'off');
if (waf) waf.className = 'mx-svc-dot ' + (o.mitmproxy ? 'on' : 'off');
if (cs) cs.className = 'mx-svc-dot ' + (o.crowdsec ? 'on' : 'off');
});
}
});