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
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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
});