secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/metrics.js
CyberMind-FR d1bc9a9b63 feat(crowdsec-dashboard): Add system health check and CAPI metrics
- Add health_check API with LAPI/CAPI/Console status verification
- Add capi_metrics API for community blocklist statistics
- Add hub_available, install_hub_item, remove_hub_item APIs
- Add System Health panel to overview with visual status indicators
- Add CAPI Blocklist section showing community vs local decisions
- Add Installed Collections card with version display
- Fix settings.js syntax error (missing comma)
- Fix metrics.js null display in acquisition statistics
- Update ACL file with new RPC method permissions

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

589 lines
28 KiB
JavaScript

'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require poll';
'require ui';
'require crowdsec-dashboard/api as api';
'require crowdsec-dashboard/nav as CsNav';
/**
* CrowdSec Dashboard - Metrics View
* Detailed metrics from CrowdSec engine
* Copyright (C) 2024 CyberMind.fr - Gandalf
*/
return view.extend({
title: _('Metrics'),
csApi: null,
metrics: {},
bouncers: [],
machines: [],
hub: {},
acquisitionMetrics: {},
previousAcquisitionMetrics: null,
acquisitionRates: {},
load: function() {
this.csApi = api;
return Promise.all([
this.csApi.getMetrics(),
this.csApi.getBouncers(),
this.csApi.getMachines(),
this.csApi.getHub(),
this.csApi.getMetricsConfig(),
this.csApi.getAcquisitionMetrics()
]).then(function(results) {
return {
metrics: results[0],
bouncers: results[1],
machines: results[2],
hub: results[3],
metricsConfig: results[4],
acquisitionMetrics: results[5]
};
});
},
renderMetricSection: function(title, data) {
if (!data || typeof data !== 'object') {
return null;
}
var entries = Object.entries(data);
if (entries.length === 0) {
return null;
}
var items = entries.map(function(entry) {
var value = entry[1];
if (typeof value === 'object') {
value = JSON.stringify(value);
}
return E('div', { 'class': 'cyber-metric-item', 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [
E('span', { 'class': 'cyber-metric-name', 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, entry[0]),
E('span', { 'class': 'cyber-metric-value', 'style': 'color: var(--cyber-text-primary, #fff); font-weight: 500;' }, String(value))
]);
});
return E('div', { 'class': 'cyber-metric-section', 'style': 'margin-bottom: 1rem;' }, [
E('div', { 'class': 'cyber-metric-section-title', 'style': 'font-weight: 600; color: var(--cyber-accent-primary, #667eea); margin-bottom: 0.5rem; font-size: 0.9rem;' }, title),
E('div', { 'class': 'cyber-metric-list' }, items)
]);
},
renderBouncersTable: function() {
var self = this;
var bouncers = this.bouncers;
// Handle response structure: may be { bouncers: [...] } or direct array
if (bouncers && bouncers.bouncers) {
bouncers = bouncers.bouncers;
}
if (!Array.isArray(bouncers) || bouncers.length === 0) {
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('div', { 'style': 'font-size: 2rem; margin-bottom: 0.5rem;' }, '🔌'),
E('p', {}, _('No bouncers registered'))
]);
}
var rows = bouncers.map(function(b) {
var isValid = b.is_valid !== false;
return E('tr', {}, [
E('td', {}, E('strong', {}, b.name || 'N/A')),
E('td', {}, b.ip_address || 'N/A'),
E('td', {}, b.type || 'N/A'),
E('td', {}, E('span', {
'class': 'cyber-badge ' + (isValid ? 'cyber-badge--success' : 'cyber-badge--danger')
}, isValid ? _('Valid') : _('Invalid'))),
E('td', {}, E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9em;' }, self.csApi.formatRelativeTime(b.last_pull)))
]);
});
return E('table', { 'class': 'cyber-table', 'style': 'width: 100%; border-collapse: collapse;' }, [
E('thead', {}, E('tr', {}, [
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Name')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('IP Address')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Type')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Status')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Last Pull'))
])),
E('tbody', { 'style': 'color: var(--cyber-text-primary, #fff);' }, rows.map(function(row) {
row.querySelectorAll('td').forEach(function(td) {
td.style.cssText = 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));';
});
return row;
}))
]);
},
renderMachinesTable: function() {
var self = this;
var machines = this.machines;
// Handle response structure: may be { machines: [...] } or direct array
if (machines && machines.machines) {
machines = machines.machines;
}
if (!Array.isArray(machines) || machines.length === 0) {
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('div', { 'style': 'font-size: 2rem; margin-bottom: 0.5rem;' }, '🖥️'),
E('p', {}, _('No machines registered'))
]);
}
var rows = machines.map(function(m) {
var isValid = m.is_validated !== false;
return E('tr', {}, [
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, E('strong', {}, m.machineId || m.machine_id || 'N/A')),
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, m.ip_address || 'N/A'),
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, E('span', {
'class': 'cyber-badge ' + (isValid ? 'cyber-badge--success' : 'cyber-badge--warning')
}, isValid ? _('Validated') : _('Pending'))),
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05)); color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9em;' }, self.csApi.formatRelativeTime(m.last_heartbeat)),
E('td', { 'style': 'padding: 0.75rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, m.version || 'N/A')
]);
});
return E('table', { 'class': 'cyber-table', 'style': 'width: 100%; border-collapse: collapse; color: var(--cyber-text-primary, #fff);' }, [
E('thead', {}, E('tr', {}, [
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Machine ID')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('IP Address')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Status')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Last Heartbeat')),
E('th', { 'style': 'text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); color: var(--cyber-text-secondary, #a0a0b0); font-weight: 500;' }, _('Version'))
])),
E('tbody', {}, rows)
]);
},
renderHubStats: function() {
var hub = this.hub;
if (!hub || typeof hub !== 'object') {
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('p', {}, _('Hub data not available'))
]);
}
var collections = hub.collections || [];
var parsers = hub.parsers || [];
var scenarios = hub.scenarios || [];
var postoverflows = hub.postoverflows || [];
var countInstalled = function(items) {
if (!Array.isArray(items)) return 0;
// Check for status === 'enabled' or if local_version exists (means installed)
return items.filter(function(i) {
return i.status === 'enabled' || i.local_version;
}).length;
};
var statCards = [
{ label: _('Collections'), count: countInstalled(collections), icon: '📦' },
{ label: _('Parsers'), count: countInstalled(parsers), icon: '📝' },
{ label: _('Scenarios'), count: countInstalled(scenarios), icon: '🎯' },
{ label: _('Postoverflows'), count: countInstalled(postoverflows), icon: '🔄' }
];
return E('div', { 'class': 'cyber-card-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;' },
statCards.map(function(stat) {
return E('div', { 'class': 'cyber-card cyber-card--compact', 'style': 'text-align: center;' }, [
E('div', { 'class': 'cyber-card-body' }, [
E('div', { 'style': 'font-size: 1.5rem; margin-bottom: 0.5rem;' }, stat.icon),
E('div', { 'style': 'font-size: 2rem; font-weight: 700; color: var(--cyber-success, #00d4aa);' }, String(stat.count)),
E('div', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.85rem; margin-top: 0.25rem;' }, stat.label),
E('div', { 'style': 'color: var(--cyber-text-muted, #666); font-size: 0.75rem;' }, _('installed'))
])
]);
})
);
},
renderCollectionsList: function() {
var collections = this.hub && this.hub.collections ? this.hub.collections : [];
if (!Array.isArray(collections) || collections.length === 0) {
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('p', {}, _('No collections data'))
]);
}
var installed = collections.filter(function(c) {
return c.status === 'enabled' || c.local_version;
});
var items = installed.slice(0, 15).map(function(c) {
return E('div', { 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [
E('span', { 'style': 'color: var(--cyber-text-primary, #fff);' }, c.name || 'N/A'),
E('span', {
'class': 'cyber-badge ' + (c.up_to_date !== false ? 'cyber-badge--success' : 'cyber-badge--warning'),
'style': 'font-size: 0.75rem;'
}, c.up_to_date !== false ? (c.local_version || _('installed')) : _('update available'))
]);
});
return E('div', { 'class': 'cyber-collections-list' }, items);
},
renderAcquisitionMetrics: function() {
var metrics = this.metrics;
if (!metrics || !metrics.acquisition) {
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('p', {}, _('Acquisition metrics not available'))
]);
}
var acquisition = metrics.acquisition;
var items = [];
Object.entries(acquisition).forEach(function(entry) {
var source = entry[0];
var data = entry[1];
items.push(E('div', { 'style': 'padding: 0.75rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [
E('strong', { 'style': 'font-size: 0.85rem; color: var(--cyber-text-primary, #fff); display: block; margin-bottom: 0.25rem;' }, source),
E('div', { 'style': 'display: flex; gap: 1rem; flex-wrap: wrap; font-size: 0.8rem; color: var(--cyber-text-secondary, #a0a0b0);' }, [
E('span', {}, _('Read: ') + (data.lines_read || 0)),
E('span', {}, _('Parsed: ') + (data.lines_parsed || 0)),
E('span', {}, _('Unparsed: ') + (data.lines_unparsed || 0)),
E('span', {}, _('Buckets: ') + (data.lines_poured_to_bucket || 0))
])
]));
});
return E('div', { 'class': 'cyber-acquisition-list' }, items);
},
renderRealtimeAcquisitionMetrics: function() {
var self = this;
var acqMetrics = this.acquisitionMetrics;
if (!acqMetrics || !acqMetrics.available) {
return E('div', { 'class': 'cyber-empty', 'style': 'text-align: center; padding: 2rem; color: var(--cyber-text-muted, #666);' }, [
E('div', { 'style': 'font-size: 2rem; margin-bottom: 0.5rem;' }, '📊'),
E('p', {}, acqMetrics && acqMetrics.error ? acqMetrics.error : _('Realtime metrics not available'))
]);
}
var totalRead = acqMetrics.total_lines_read || 0;
var totalParsed = acqMetrics.total_lines_parsed || 0;
var totalUnparsed = acqMetrics.total_lines_unparsed || 0;
var totalBuckets = acqMetrics.total_buckets || 0;
var parseRate = acqMetrics.parse_rate || 0;
var activeFiles = acqMetrics.active_files || [];
// Calculate rates if we have previous data
var readRate = this.acquisitionRates.readRate || 0;
var parsedRate = this.acquisitionRates.parsedRate || 0;
// Create stats cards grid
var statsGrid = E('div', { 'class': 'cyber-realtime-stats', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; margin-bottom: 1.5rem;' }, [
// Lines Read Card
E('div', { 'class': 'cyber-stat-card', 'style': 'background: var(--cyber-card-bg, rgba(30,30,40,0.8)); border: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); border-radius: 8px; padding: 1rem; text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-muted, #666); margin-bottom: 0.25rem; text-transform: uppercase;' }, _('Lines Read')),
E('div', { 'class': 'cyber-stat-value', 'style': 'font-size: 1.5rem; font-weight: 700; color: var(--cyber-accent-primary, #667eea);' }, this.formatNumber(totalRead)),
readRate > 0 ? E('div', { 'style': 'font-size: 0.7rem; color: var(--cyber-success, #00d4aa); margin-top: 0.25rem;' }, '+' + readRate + '/s') : E('span')
]),
// Lines Parsed Card
E('div', { 'class': 'cyber-stat-card', 'style': 'background: var(--cyber-card-bg, rgba(30,30,40,0.8)); border: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); border-radius: 8px; padding: 1rem; text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-muted, #666); margin-bottom: 0.25rem; text-transform: uppercase;' }, _('Parsed')),
E('div', { 'class': 'cyber-stat-value', 'style': 'font-size: 1.5rem; font-weight: 700; color: var(--cyber-success, #00d4aa);' }, this.formatNumber(totalParsed)),
parsedRate > 0 ? E('div', { 'style': 'font-size: 0.7rem; color: var(--cyber-success, #00d4aa); margin-top: 0.25rem;' }, '+' + parsedRate + '/s') : E('span')
]),
// Parse Rate Card with progress bar
E('div', { 'class': 'cyber-stat-card', 'style': 'background: var(--cyber-card-bg, rgba(30,30,40,0.8)); border: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); border-radius: 8px; padding: 1rem; text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-muted, #666); margin-bottom: 0.25rem; text-transform: uppercase;' }, _('Parse Rate')),
E('div', { 'class': 'cyber-stat-value', 'style': 'font-size: 1.5rem; font-weight: 700; color: ' + (parseRate >= 80 ? 'var(--cyber-success, #00d4aa)' : parseRate >= 50 ? 'var(--cyber-warning, #ffa500)' : 'var(--cyber-danger, #ff4757)') + ';' }, parseRate + '%'),
E('div', { 'class': 'cyber-progress', 'style': 'height: 4px; background: var(--cyber-border, rgba(255,255,255,0.1)); border-radius: 2px; margin-top: 0.5rem; overflow: hidden;' }, [
E('div', { 'class': 'cyber-progress-bar', 'style': 'width: ' + parseRate + '%; height: 100%; background: ' + (parseRate >= 80 ? 'var(--cyber-success, #00d4aa)' : parseRate >= 50 ? 'var(--cyber-warning, #ffa500)' : 'var(--cyber-danger, #ff4757)') + '; transition: width 0.3s ease;' })
])
]),
// Buckets Card
E('div', { 'class': 'cyber-stat-card', 'style': 'background: var(--cyber-card-bg, rgba(30,30,40,0.8)); border: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); border-radius: 8px; padding: 1rem; text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-muted, #666); margin-bottom: 0.25rem; text-transform: uppercase;' }, _('Buckets')),
E('div', { 'class': 'cyber-stat-value', 'style': 'font-size: 1.5rem; font-weight: 700; color: var(--cyber-warning, #ffa500);' }, this.formatNumber(totalBuckets))
]),
// Unparsed Card
E('div', { 'class': 'cyber-stat-card', 'style': 'background: var(--cyber-card-bg, rgba(30,30,40,0.8)); border: 1px solid var(--cyber-border, rgba(255,255,255,0.1)); border-radius: 8px; padding: 1rem; text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-muted, #666); margin-bottom: 0.25rem; text-transform: uppercase;' }, _('Unparsed')),
E('div', { 'class': 'cyber-stat-value', 'style': 'font-size: 1.5rem; font-weight: 700; color: ' + (totalUnparsed > 0 ? 'var(--cyber-danger, #ff4757)' : 'var(--cyber-text-muted, #666)') + ';' }, this.formatNumber(totalUnparsed))
])
]);
// Active sources list
var sourcesList = E('div', { 'class': 'cyber-sources-list', 'style': 'margin-top: 1rem;' }, [
E('div', { 'style': 'font-size: 0.85rem; font-weight: 600; color: var(--cyber-text-secondary, #a0a0b0); margin-bottom: 0.5rem;' }, _('Active Acquisition Sources')),
activeFiles.length > 0 ?
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 0.5rem;' },
activeFiles.map(function(file) {
return E('span', {
'class': 'cyber-badge cyber-badge--info',
'style': 'font-size: 0.75rem; padding: 0.25rem 0.5rem; background: var(--cyber-accent-primary, #667eea); color: white; border-radius: 4px;'
}, file);
})
) :
E('span', { 'style': 'color: var(--cyber-text-muted, #666); font-size: 0.85rem;' }, _('No active sources'))
]);
// Last update timestamp
var timestamp = acqMetrics.timestamp ? new Date(acqMetrics.timestamp * 1000).toLocaleTimeString() : 'N/A';
var lastUpdate = E('div', { 'style': 'text-align: right; font-size: 0.75rem; color: var(--cyber-text-muted, #666); margin-top: 1rem;' }, [
E('span', { 'class': 'cyber-pulse', 'style': 'display: inline-block; width: 8px; height: 8px; background: var(--cyber-success, #00d4aa); border-radius: 50%; margin-right: 0.5rem; animation: pulse 2s infinite;' }),
_('Last update: ') + timestamp
]);
return E('div', { 'class': 'cyber-realtime-acquisition', 'id': 'realtime-acquisition-container' }, [
statsGrid,
sourcesList,
lastUpdate
]);
},
formatNumber: function(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return String(num);
},
updateAcquisitionRates: function(newMetrics) {
if (!this.previousAcquisitionMetrics || !newMetrics) {
this.previousAcquisitionMetrics = newMetrics;
return;
}
var prevTimestamp = this.previousAcquisitionMetrics.timestamp || 0;
var newTimestamp = newMetrics.timestamp || 0;
var timeDiff = newTimestamp - prevTimestamp;
if (timeDiff > 0) {
var readDiff = (newMetrics.total_lines_read || 0) - (this.previousAcquisitionMetrics.total_lines_read || 0);
var parsedDiff = (newMetrics.total_lines_parsed || 0) - (this.previousAcquisitionMetrics.total_lines_parsed || 0);
this.acquisitionRates.readRate = Math.round(readDiff / timeDiff);
this.acquisitionRates.parsedRate = Math.round(parsedDiff / timeDiff);
}
this.previousAcquisitionMetrics = newMetrics;
},
renderMetricsConfig: function(metricsConfig) {
var self = this;
var enabled = metricsConfig && (metricsConfig.metrics_enabled === true || metricsConfig.metrics_enabled === 1);
var prometheusEndpoint = metricsConfig && metricsConfig.prometheus_endpoint || 'http://127.0.0.1:6060/metrics';
return E('div', { 'class': 'cyber-card', 'style': 'margin-bottom: 1.5rem;' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '⚙️'),
_('Metrics Export Configuration')
]),
E('span', {
'class': 'cyber-badge ' + (enabled ? 'cyber-badge--success' : 'cyber-badge--danger')
}, enabled ? _('Enabled') : _('Disabled'))
]),
E('div', { 'class': 'cyber-card-body' }, [
E('div', { 'style': 'margin-bottom: 1rem;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [
E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, _('Metrics Export Status')),
E('span', { 'style': 'color: var(--cyber-text-primary, #fff);' }, enabled ? _('Enabled') : _('Disabled'))
]),
E('div', { 'style': 'display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.05));' }, [
E('span', { 'style': 'color: var(--cyber-text-secondary, #a0a0b0);' }, _('Prometheus Endpoint')),
E('code', { 'style': 'font-size: 0.85rem; color: var(--cyber-accent-primary, #667eea);' }, prometheusEndpoint)
])
]),
E('div', { 'style': 'display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;' }, [
E('button', {
'class': 'cyber-btn ' + (enabled ? 'cyber-btn--danger' : 'cyber-btn--success'),
'click': function() {
var newState = !enabled;
ui.showModal(_('Updating Metrics Configuration...'), [
E('p', {}, _('Changing metrics export to: %s').format(newState ? _('Enabled') : _('Disabled'))),
E('div', { 'class': 'spinning' })
]);
self.csApi.configureMetrics(newState ? '1' : '0').then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('Metrics configuration updated. Restart CrowdSec to apply changes.')), 'info');
} else {
ui.addNotification(null, E('p', {}, result.error || _('Failed to update configuration')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
}
}, enabled ? _('Disable Metrics Export') : _('Enable Metrics Export')),
E('span', { 'style': 'color: var(--cyber-text-muted, #666); font-size: 0.85rem;' },
_('Note: Changing this setting requires restarting CrowdSec'))
]),
E('div', { 'class': 'cyber-card cyber-card--info cyber-card--compact', 'style': 'margin-top: 1rem;' }, [
E('div', { 'class': 'cyber-card-body' }, [
E('p', { 'style': 'margin: 0 0 0.5rem 0; color: var(--cyber-text-primary, #fff); font-weight: 600;' }, _('About Metrics Export')),
E('p', { 'style': 'margin: 0; color: var(--cyber-text-secondary, #a0a0b0); font-size: 0.9rem;' }, [
_('When enabled, CrowdSec exports Prometheus-compatible metrics that can be scraped by monitoring tools. Access metrics at: '),
E('code', { 'style': 'color: var(--cyber-accent-primary, #667eea);' }, prometheusEndpoint)
])
])
])
])
]);
},
render: function(data) {
var self = this;
// Initialize theme
Theme.init();
this.metrics = data.metrics || {};
this.bouncers = data.bouncers || [];
this.machines = data.machines || {};
this.hub = data.hub || {};
this.acquisitionMetrics = data.acquisitionMetrics || {};
var metricsConfig = data.metricsConfig || {};
var view = E('div', { 'class': 'crowdsec-dashboard crowdsec-metrics' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
CsNav.renderTabs('metrics'),
// Page Header
E('div', { 'style': 'margin-bottom: 1.5rem;' }, [
E('h2', { 'style': 'color: var(--cs-text-primary, #e6edf3); margin: 0 0 0.5rem 0;' }, _('CrowdSec Metrics')),
E('p', { 'style': 'color: var(--cs-text-secondary, #8b949e); margin: 0;' }, _('Detailed metrics and statistics from CrowdSec engine'))
]),
// Metrics Configuration
this.renderMetricsConfig(metricsConfig),
// Hub Stats
E('div', { 'class': 'cyber-card', 'style': 'margin-bottom: 1.5rem;' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '🎯'),
_('Hub Components')
])
]),
E('div', { 'class': 'cyber-card-body' }, this.renderHubStats())
]),
// Grid of cards
E('div', { 'class': 'cyber-card-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem;' }, [
// Bouncers
E('div', { 'class': 'cyber-card' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '🔒'),
_('Registered Bouncers')
])
]),
E('div', { 'class': 'cyber-card-body' }, this.renderBouncersTable())
]),
// Machines
E('div', { 'class': 'cyber-card' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '🖥️'),
_('Registered Machines')
])
]),
E('div', { 'class': 'cyber-card-body' }, this.renderMachinesTable())
]),
// Collections
E('div', { 'class': 'cyber-card' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '📦'),
_('Installed Collections')
])
]),
E('div', { 'class': 'cyber-card-body' }, this.renderCollectionsList())
]),
// Acquisition - Per Source Details
E('div', { 'class': 'cyber-card' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '📊'),
_('Acquisition Sources')
])
]),
E('div', { 'class': 'cyber-card-body' }, this.renderAcquisitionMetrics())
])
]),
// Realtime Acquisition Statistics (full width)
E('div', { 'class': 'cyber-card', 'style': 'margin-top: 1.5rem;' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '⚡'),
_('Realtime Acquisition Statistics')
]),
E('span', { 'class': 'cyber-badge cyber-badge--info', 'style': 'font-size: 0.7rem;' }, _('Live'))
]),
E('div', { 'class': 'cyber-card-body', 'id': 'realtime-acquisition-body' }, this.renderRealtimeAcquisitionMetrics())
]),
// Raw metrics sections
E('div', { 'class': 'cyber-card', 'style': 'margin-top: 1.5rem;' }, [
E('div', { 'class': 'cyber-card-header' }, [
E('div', { 'class': 'cyber-card-title' }, [
E('span', { 'style': 'margin-right: 0.5rem;' }, '📈'),
_('Raw Prometheus Metrics')
])
]),
E('div', { 'class': 'cyber-card-body' }, [
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;' }, [
this.renderMetricSection(_('Parsers'), this.metrics.parsers),
this.renderMetricSection(_('Scenarios'), this.metrics.scenarios),
this.renderMetricSection(_('Buckets'), this.metrics.buckets),
this.renderMetricSection(_('LAPI'), this.metrics.lapi),
this.renderMetricSection(_('Decisions'), this.metrics.decisions)
].filter(Boolean))
])
])
]);
// Setup polling (every 60 seconds for general metrics)
poll.add(function() {
return Promise.all([
self.csApi.getMetrics(),
self.csApi.getBouncers(),
self.csApi.getMachines()
]).then(function(results) {
self.metrics = results[0];
self.bouncers = results[1];
self.machines = results[2];
});
}, 60);
// Fast polling for realtime acquisition metrics (every 10 seconds)
poll.add(function() {
return self.csApi.getAcquisitionMetrics().then(function(result) {
self.updateAcquisitionRates(result);
self.acquisitionMetrics = result;
// Update the realtime acquisition display
var container = document.getElementById('realtime-acquisition-body');
if (container) {
dom.content(container, self.renderRealtimeAcquisitionMetrics());
}
});
}, 10);
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});