New luci-app-metrics-dashboard with real-time system overview: - System uptime, memory, load stats - Core services status (HAProxy, mitmproxy, CrowdSec) - vHosts, MetaBlog sites, Streamlit apps counts - WAF alerts, bans, threats statistics - Active connections (HTTP, HTTPS, SSH, TCP total) - SSL certificates list - Auto-refresh every 5 seconds WAF Filters page: - Changed stats display to single-line compact format - Shows "17 Categories · 17 Active · 150 Rules" inline Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
327 lines
15 KiB
JavaScript
327 lines
15 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require poll';
|
|
'require rpc';
|
|
'require ui';
|
|
|
|
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: {}
|
|
});
|
|
|
|
var callFirewallStats = rpc.declare({
|
|
object: 'luci.metrics',
|
|
method: 'firewall_stats',
|
|
expect: {}
|
|
});
|
|
|
|
var callCerts = rpc.declare({
|
|
object: 'luci.metrics',
|
|
method: 'certs',
|
|
expect: {}
|
|
});
|
|
|
|
var callVhosts = rpc.declare({
|
|
object: 'luci.metrics',
|
|
method: 'vhosts',
|
|
expect: {}
|
|
});
|
|
|
|
var callMetablogs = rpc.declare({
|
|
object: 'luci.metrics',
|
|
method: 'metablogs',
|
|
expect: {}
|
|
});
|
|
|
|
var callStreamlits = rpc.declare({
|
|
object: 'luci.metrics',
|
|
method: 'streamlits',
|
|
expect: {}
|
|
});
|
|
|
|
function formatUptime(seconds) {
|
|
var days = Math.floor(seconds / 86400);
|
|
var hours = Math.floor((seconds % 86400) / 3600);
|
|
var mins = Math.floor((seconds % 3600) / 60);
|
|
if (days > 0) return days + 'd ' + hours + 'h ' + mins + 'm';
|
|
if (hours > 0) return hours + 'h ' + mins + 'm';
|
|
return mins + 'm';
|
|
}
|
|
|
|
function formatMemory(kb) {
|
|
if (kb > 1048576) return (kb / 1048576).toFixed(1) + ' GB';
|
|
if (kb > 1024) return (kb / 1024).toFixed(0) + ' MB';
|
|
return kb + ' KB';
|
|
}
|
|
|
|
function createStatusBadge(active, label) {
|
|
var cls = active ? 'badge-success' : 'badge-danger';
|
|
var icon = active ? '●' : '○';
|
|
return E('span', { 'class': 'metrics-badge ' + cls }, icon + ' ' + label);
|
|
}
|
|
|
|
function createMetricCard(title, value, subtitle, icon, color) {
|
|
return E('div', { 'class': 'metrics-card metrics-card-' + (color || 'default') }, [
|
|
E('div', { 'class': 'metrics-card-icon' }, icon || '📊'),
|
|
E('div', { 'class': 'metrics-card-content' }, [
|
|
E('div', { 'class': 'metrics-card-value' }, String(value)),
|
|
E('div', { 'class': 'metrics-card-title' }, title),
|
|
subtitle ? E('div', { 'class': 'metrics-card-subtitle' }, subtitle) : null
|
|
])
|
|
]);
|
|
}
|
|
|
|
function createServiceRow(name, domain, running, enabled) {
|
|
var statusCls = running ? 'status-running' : (enabled ? 'status-stopped' : 'status-disabled');
|
|
var statusText = running ? 'Running' : (enabled ? 'Stopped' : 'Disabled');
|
|
return E('tr', {}, [
|
|
E('td', {}, name),
|
|
E('td', {}, domain ? E('a', { href: 'https://' + domain, target: '_blank' }, domain) : '-'),
|
|
E('td', { 'class': statusCls }, statusText)
|
|
]);
|
|
}
|
|
|
|
function createCertRow(cert) {
|
|
var statusCls = 'cert-' + cert.status;
|
|
return E('tr', { 'class': statusCls }, [
|
|
E('td', {}, cert.name),
|
|
E('td', {}, cert.expiry || 'Unknown'),
|
|
E('td', {}, cert.days_left + ' days'),
|
|
E('td', { 'class': 'cert-status-' + cert.status }, cert.status.toUpperCase())
|
|
]);
|
|
}
|
|
|
|
return view.extend({
|
|
css: `
|
|
.metrics-dashboard { padding: 10px; }
|
|
.metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }
|
|
.metrics-card { background: var(--card-bg, #1a1a2e); border-radius: 8px; padding: 15px; display: flex; align-items: center; gap: 15px; border: 1px solid var(--border-color, #333); }
|
|
.metrics-card-icon { font-size: 2em; }
|
|
.metrics-card-value { font-size: 1.8em; font-weight: bold; color: var(--primary-color, #00ffb2); }
|
|
.metrics-card-title { font-size: 0.9em; color: var(--text-muted, #888); }
|
|
.metrics-card-subtitle { font-size: 0.75em; color: var(--text-dim, #666); }
|
|
.metrics-card-success .metrics-card-value { color: #39ff14; }
|
|
.metrics-card-warning .metrics-card-value { color: #ffb300; }
|
|
.metrics-card-danger .metrics-card-value { color: #ff3535; }
|
|
.metrics-card-info .metrics-card-value { color: #00c3ff; }
|
|
|
|
.metrics-section { background: var(--card-bg, #1a1a2e); border-radius: 8px; padding: 15px; margin-bottom: 15px; border: 1px solid var(--border-color, #333); }
|
|
.metrics-section h3 { margin: 0 0 15px 0; padding-bottom: 10px; border-bottom: 1px solid var(--border-color, #333); color: var(--heading-color, #fff); display: flex; align-items: center; gap: 10px; }
|
|
.metrics-section h3 .icon { font-size: 1.2em; }
|
|
|
|
.metrics-badge { padding: 3px 8px; border-radius: 4px; font-size: 0.85em; margin-right: 8px; }
|
|
.badge-success { background: rgba(57, 255, 20, 0.2); color: #39ff14; }
|
|
.badge-danger { background: rgba(255, 53, 53, 0.2); color: #ff3535; }
|
|
.badge-warning { background: rgba(255, 179, 0, 0.2); color: #ffb300; }
|
|
.badge-info { background: rgba(0, 195, 255, 0.2); color: #00c3ff; }
|
|
|
|
.metrics-table { width: 100%; border-collapse: collapse; font-size: 0.9em; }
|
|
.metrics-table th, .metrics-table td { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--border-color, #333); }
|
|
.metrics-table th { color: var(--text-muted, #888); font-weight: normal; }
|
|
.metrics-table tr:hover { background: rgba(255,255,255,0.03); }
|
|
|
|
.status-running { color: #39ff14; }
|
|
.status-stopped { color: #ffb300; }
|
|
.status-disabled { color: #666; }
|
|
|
|
.cert-valid { }
|
|
.cert-expiring { background: rgba(255, 179, 0, 0.1); }
|
|
.cert-critical { background: rgba(255, 53, 53, 0.15); }
|
|
.cert-expired { background: rgba(255, 53, 53, 0.25); }
|
|
.cert-status-valid { color: #39ff14; }
|
|
.cert-status-expiring { color: #ffb300; }
|
|
.cert-status-critical { color: #ff6b35; }
|
|
.cert-status-expired { color: #ff3535; }
|
|
|
|
.services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 15px; }
|
|
|
|
.refresh-info { text-align: right; color: var(--text-dim, #666); font-size: 0.8em; margin-bottom: 10px; }
|
|
|
|
.progress-bar { height: 8px; background: var(--border-color, #333); border-radius: 4px; overflow: hidden; margin-top: 5px; }
|
|
.progress-fill { height: 100%; transition: width 0.3s; }
|
|
.progress-success { background: linear-gradient(90deg, #39ff14, #00ffb2); }
|
|
.progress-warning { background: linear-gradient(90deg, #ffb300, #ff6b35); }
|
|
.progress-danger { background: linear-gradient(90deg, #ff6b35, #ff3535); }
|
|
|
|
.live-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #39ff14; margin-right: 5px; animation: pulse 2s infinite; }
|
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
`,
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
callOverview().catch(function() { return {}; }),
|
|
callWafStats().catch(function() { return {}; }),
|
|
callConnections().catch(function() { return {}; }),
|
|
callFirewallStats().catch(function() { return {}; }),
|
|
callCerts().catch(function() { return { certs: [] }; }),
|
|
callVhosts().catch(function() { return { vhosts: [] }; }),
|
|
callMetablogs().catch(function() { return { sites: [] }; }),
|
|
callStreamlits().catch(function() { return { apps: [] }; })
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var overview = data[0] || {};
|
|
var waf = data[1] || {};
|
|
var conns = data[2] || {};
|
|
var fw = data[3] || {};
|
|
var certs = (data[4] || {}).certs || [];
|
|
var vhosts = (data[5] || {}).vhosts || [];
|
|
var metablogs = (data[6] || {}).sites || [];
|
|
var streamlits = (data[7] || {}).apps || [];
|
|
|
|
var memPct = overview.mem_pct || 0;
|
|
var memClass = memPct > 90 ? 'danger' : (memPct > 70 ? 'warning' : 'success');
|
|
|
|
var view = E('div', { 'class': 'metrics-dashboard' }, [
|
|
E('div', { 'class': 'refresh-info' }, [
|
|
E('span', { 'class': 'live-indicator' }),
|
|
'Live refresh: 5s | Last: ',
|
|
E('span', { 'id': 'last-refresh' }, new Date().toLocaleTimeString())
|
|
]),
|
|
|
|
// Overview Cards
|
|
E('div', { 'class': 'metrics-grid', 'id': 'overview-grid' }, [
|
|
createMetricCard('Uptime', formatUptime(overview.uptime || 0), 'Load: ' + (overview.load || '0.00'), '⏱️'),
|
|
createMetricCard('Memory', memPct + '%', formatMemory(overview.mem_used_kb || 0) + ' / ' + formatMemory(overview.mem_total_kb || 0), '🧠', memClass),
|
|
createMetricCard('vHosts', overview.vhosts || 0, 'Active virtual hosts', '🌐', 'info'),
|
|
createMetricCard('Certificates', overview.certificates || 0, 'SSL/TLS certificates', '🔒', 'success'),
|
|
createMetricCard('MetaBlogs', overview.metablogs || 0, 'Static sites', '📄'),
|
|
createMetricCard('Streamlits', overview.streamlits || 0, 'Python apps', '🐍'),
|
|
createMetricCard('LXC Containers', overview.lxc_containers || 0, 'Running containers', '📦', 'info')
|
|
]),
|
|
|
|
// Services Status
|
|
E('div', { 'class': 'metrics-section', 'id': 'services-section' }, [
|
|
E('h3', {}, [E('span', { 'class': 'icon' }, '🔧'), 'Core Services']),
|
|
E('div', { 'style': 'display: flex; gap: 15px; flex-wrap: wrap;' }, [
|
|
createStatusBadge(overview.haproxy, 'HAProxy'),
|
|
createStatusBadge(overview.mitmproxy, 'mitmproxy WAF'),
|
|
createStatusBadge(overview.crowdsec, 'CrowdSec')
|
|
])
|
|
]),
|
|
|
|
// WAF & Security
|
|
E('div', { 'class': 'metrics-section', 'id': 'waf-section' }, [
|
|
E('h3', {}, [E('span', { 'class': 'icon' }, '🛡️'), 'WAF & Security']),
|
|
E('div', { 'class': 'metrics-grid' }, [
|
|
createMetricCard('Active Bans', waf.active_bans || 0, 'CrowdSec decisions', '🚫', (waf.active_bans || 0) > 0 ? 'warning' : 'success'),
|
|
createMetricCard('Alerts (24h)', waf.alerts_today || 0, 'Security alerts', '⚠️', (waf.alerts_today || 0) > 10 ? 'danger' : 'info'),
|
|
createMetricCard('WAF Threats', waf.waf_threats || 0, 'Detected today', '🎯', (waf.waf_threats || 0) > 0 ? 'warning' : 'success'),
|
|
createMetricCard('WAF Blocked', waf.waf_blocked || 0, 'Blocked requests', '✋', 'danger')
|
|
])
|
|
]),
|
|
|
|
// Connections
|
|
E('div', { 'class': 'metrics-section', 'id': 'connections-section' }, [
|
|
E('h3', {}, [E('span', { 'class': 'icon' }, '🔗'), 'Active Connections']),
|
|
E('div', { 'class': 'metrics-grid' }, [
|
|
createMetricCard('HTTPS', conns.https || 0, 'Port 443', '🔐', 'success'),
|
|
createMetricCard('HTTP', conns.http || 0, 'Port 80', '🌍'),
|
|
createMetricCard('SSH', conns.ssh || 0, 'Port 22', '💻', 'info'),
|
|
createMetricCard('Total TCP', conns.total_tcp || 0, 'All connections', '📡')
|
|
])
|
|
]),
|
|
|
|
// Firewall
|
|
E('div', { 'class': 'metrics-section', 'id': 'firewall-section' }, [
|
|
E('h3', {}, [E('span', { 'class': 'icon' }, '🔥'), 'Firewall Stats']),
|
|
E('div', { 'class': 'metrics-grid' }, [
|
|
createMetricCard('Bouncer Blocks', fw.bouncer_blocks || 0, 'CrowdSec bouncer', '🛑', (fw.bouncer_blocks || 0) > 0 ? 'danger' : 'success')
|
|
])
|
|
]),
|
|
|
|
// Services Grid
|
|
E('div', { 'class': 'services-grid' }, [
|
|
// Certificates
|
|
E('div', { 'class': 'metrics-section', 'id': 'certs-section' }, [
|
|
E('h3', {}, [E('span', { 'class': 'icon' }, '🔒'), 'SSL Certificates']),
|
|
certs.length > 0 ?
|
|
E('table', { 'class': 'metrics-table' }, [
|
|
E('thead', {}, E('tr', {}, [
|
|
E('th', {}, 'Domain'),
|
|
E('th', {}, 'Expiry'),
|
|
E('th', {}, 'Days Left'),
|
|
E('th', {}, 'Status')
|
|
])),
|
|
E('tbody', {}, certs.slice(0, 10).map(createCertRow))
|
|
]) :
|
|
E('p', { 'class': 'text-muted' }, 'No certificates found')
|
|
]),
|
|
|
|
// MetaBlog Sites
|
|
E('div', { 'class': 'metrics-section', 'id': 'metablogs-section' }, [
|
|
E('h3', {}, [E('span', { 'class': 'icon' }, '📄'), 'MetaBlog Sites']),
|
|
metablogs.length > 0 ?
|
|
E('table', { 'class': 'metrics-table' }, [
|
|
E('thead', {}, E('tr', {}, [
|
|
E('th', {}, 'Name'),
|
|
E('th', {}, 'Domain'),
|
|
E('th', {}, 'Status')
|
|
])),
|
|
E('tbody', {}, metablogs.slice(0, 10).map(function(s) {
|
|
return createServiceRow(s.name, s.domain, s.running, s.enabled);
|
|
}))
|
|
]) :
|
|
E('p', { 'class': 'text-muted' }, 'No MetaBlog sites')
|
|
])
|
|
]),
|
|
|
|
// Streamlit Apps
|
|
streamlits.length > 0 ?
|
|
E('div', { 'class': 'metrics-section', 'id': 'streamlits-section' }, [
|
|
E('h3', {}, [E('span', { 'class': 'icon' }, '🐍'), 'Streamlit Apps']),
|
|
E('table', { 'class': 'metrics-table' }, [
|
|
E('thead', {}, E('tr', {}, [
|
|
E('th', {}, 'Name'),
|
|
E('th', {}, 'Domain'),
|
|
E('th', {}, 'Status')
|
|
])),
|
|
E('tbody', {}, streamlits.map(function(s) {
|
|
return createServiceRow(s.name, s.domain, s.running, s.enabled);
|
|
}))
|
|
])
|
|
]) : null
|
|
]);
|
|
|
|
// Setup polling for real-time updates
|
|
poll.add(L.bind(this.pollMetrics, this), 5);
|
|
|
|
return view;
|
|
},
|
|
|
|
pollMetrics: function() {
|
|
var self = this;
|
|
return Promise.all([
|
|
callOverview(),
|
|
callWafStats(),
|
|
callConnections()
|
|
]).then(function(data) {
|
|
var overview = data[0] || {};
|
|
var waf = data[1] || {};
|
|
var conns = data[2] || {};
|
|
|
|
// Update last refresh time
|
|
var refreshEl = document.getElementById('last-refresh');
|
|
if (refreshEl) refreshEl.textContent = new Date().toLocaleTimeString();
|
|
|
|
// Update could be more granular, but for now just log
|
|
// Full DOM update would require more complex diffing
|
|
});
|
|
}
|
|
});
|