mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Make alertmail as well multilingual, implement a new more modern mailtemplate. Preserve the old as classig, as option over env
This commit is contained in:
@@ -1033,21 +1033,41 @@
|
||||
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
|
||||
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
|
||||
|
||||
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 800px;">
|
||||
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 1200px;">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" data-i18n="logs.modal.insights_title">Ban Insights</h3>
|
||||
<p class="mt-1 text-sm text-gray-500" data-i18n="logs.modal.insights_description">Country distribution and recurring offenders.</p>
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-2" data-i18n="logs.modal.insights_title">Ban Insights</h3>
|
||||
<p class="text-sm text-gray-600 mb-4" data-i18n="logs.modal.insights_description">Country distribution and recurring offenders.</p>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div id="insightsSummary" class="grid gap-4 sm:grid-cols-3 mb-6"></div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<!-- Country Statistics -->
|
||||
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h4 class="text-base font-semibold text-gray-900" data-i18n="logs.modal.insights_countries">Bans by country</h4>
|
||||
<p class="text-xs text-gray-500 mt-1" data-i18n="logs.modal.insights_countries_hint">Top origins for the selected time range.</p>
|
||||
</div>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-700">Geo</span>
|
||||
</div>
|
||||
<div id="countryStatsContainer" class="space-y-4 max-h-96 overflow-y-auto"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<h4 class="text-md font-semibold text-gray-800 mb-2" data-i18n="logs.modal.insights_countries">Bans by country</h4>
|
||||
<div id="countryStatsContainer" class="max-h-64 overflow-y-auto divide-y divide-gray-200"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<h4 class="text-md font-semibold text-gray-800 mb-2" data-i18n="logs.modal.insights_recurring">Recurring IPs</h4>
|
||||
<div id="recurringIPsContainer" class="max-h-64 overflow-y-auto divide-y divide-gray-200"></div>
|
||||
<!-- Recurring IPs -->
|
||||
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h4 class="text-base font-semibold text-gray-900" data-i18n="logs.modal.insights_recurring">Recurring IPs</h4>
|
||||
<p class="text-xs text-gray-500 mt-1" data-i18n="logs.modal.insights_recurring_hint">IP addresses repeatedly triggering Fail2ban.</p>
|
||||
</div>
|
||||
<span class="inline-flex items-center rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-amber-700">Watchlist</span>
|
||||
</div>
|
||||
<div id="recurringIPsContainer" class="space-y-4 max-h-96 overflow-y-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1309,6 +1329,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
var num = Number(value);
|
||||
if (!isFinite(num)) {
|
||||
return '0';
|
||||
}
|
||||
try {
|
||||
return num.toLocaleString();
|
||||
} catch (e) {
|
||||
return String(num);
|
||||
}
|
||||
}
|
||||
|
||||
function withServerParam(url) {
|
||||
if (!currentServerId) {
|
||||
return url;
|
||||
@@ -2911,17 +2943,57 @@
|
||||
function openBanInsightsModal() {
|
||||
var countriesContainer = document.getElementById('countryStatsContainer');
|
||||
var recurringContainer = document.getElementById('recurringIPsContainer');
|
||||
var summaryContainer = document.getElementById('insightsSummary');
|
||||
|
||||
var totals = (latestBanInsights && latestBanInsights.totals) || { overall: 0, today: 0, week: 0 };
|
||||
if (summaryContainer) {
|
||||
var summaryCards = [
|
||||
{
|
||||
label: t('logs.overview.total_events', 'Total stored events'),
|
||||
value: formatNumber(totals.overall || 0),
|
||||
sub: t('logs.modal.total_overall_note', 'Lifetime bans recorded')
|
||||
},
|
||||
{
|
||||
label: t('logs.overview.total_today', 'Today'),
|
||||
value: formatNumber(totals.today || 0),
|
||||
sub: t('logs.modal.total_today_note', 'Last 24 hours')
|
||||
},
|
||||
{
|
||||
label: t('logs.overview.total_week', 'Last 7 days'),
|
||||
value: formatNumber(totals.week || 0),
|
||||
sub: t('logs.modal.total_week_note', 'Weekly activity')
|
||||
}
|
||||
];
|
||||
summaryContainer.innerHTML = summaryCards.map(function(card) {
|
||||
return ''
|
||||
+ '<div class="border border-gray-200 rounded-lg p-4 bg-white">'
|
||||
+ ' <p class="text-xs uppercase tracking-wide text-gray-500">' + escapeHtml(card.label) + '</p>'
|
||||
+ ' <p class="text-3xl font-semibold text-gray-900 mt-1">' + escapeHtml(card.value) + '</p>'
|
||||
+ ' <p class="text-xs text-gray-500 mt-1">' + escapeHtml(card.sub) + '</p>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
var countries = (latestBanInsights && latestBanInsights.countries) || [];
|
||||
if (!countries.length) {
|
||||
countriesContainer.innerHTML = '<p class="text-sm text-gray-500" data-i18n="logs.modal.insights_countries_empty">No bans recorded for this period.</p>';
|
||||
} else {
|
||||
var totalCountries = countries.reduce(function(sum, stat) {
|
||||
return sum + (stat.count || 0);
|
||||
}, 0) || 1;
|
||||
var countryHTML = countries.map(function(stat) {
|
||||
var label = stat.country || t('logs.overview.country_unknown', 'Unknown');
|
||||
var percent = Math.round(((stat.count || 0) / totalCountries) * 100);
|
||||
percent = Math.min(Math.max(percent, 3), 100);
|
||||
return ''
|
||||
+ '<div class="flex items-center justify-between py-2">'
|
||||
+ ' <span class="font-medium">' + escapeHtml(label) + '</span>'
|
||||
+ ' <span class="text-sm text-gray-600">' + (stat.count || 0) + '</span>'
|
||||
+ '<div class="space-y-2">'
|
||||
+ ' <div class="flex items-center justify-between text-sm font-medium text-gray-800">'
|
||||
+ ' <span>' + escapeHtml(label) + '</span>'
|
||||
+ ' <span>' + formatNumber(stat.count || 0) + '</span>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="w-full bg-gray-200 rounded-full h-2">'
|
||||
+ ' <div class="h-2 rounded-full bg-gradient-to-r from-blue-500 to-indigo-600" style="width:' + percent + '%;"></div>'
|
||||
+ ' </div>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
countriesContainer.innerHTML = countryHTML;
|
||||
@@ -2935,16 +3007,17 @@
|
||||
var countryLabel = stat.country || t('logs.overview.country_unknown', 'Unknown');
|
||||
var lastSeenLabel = stat.lastSeen ? formatDateTime(stat.lastSeen) : '—';
|
||||
return ''
|
||||
+ '<div class="py-2">'
|
||||
+ '<div class="rounded-lg bg-white border border-gray-200 shadow-sm p-4">'
|
||||
+ ' <div class="flex items-center justify-between">'
|
||||
+ ' <div>'
|
||||
+ ' <p class="font-mono text-sm text-gray-900">' + escapeHtml(stat.ip || '—') + '</p>'
|
||||
+ ' <p class="text-xs text-gray-500">' + escapeHtml(countryLabel) + '</p>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="text-right">'
|
||||
+ ' <p class="text-sm font-semibold">' + (stat.count || 0) + '×</p>'
|
||||
+ ' <p class="text-xs text-gray-500">' + t('logs.overview.last_seen', 'Last seen') + ': ' + escapeHtml(lastSeenLabel) + '</p>'
|
||||
+ ' <p class="font-mono text-base text-gray-900">' + escapeHtml(stat.ip || '—') + '</p>'
|
||||
+ ' <p class="text-xs text-gray-500 mt-1">' + escapeHtml(countryLabel) + '</p>'
|
||||
+ ' </div>'
|
||||
+ ' <span class="inline-flex items-center rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-700">' + formatNumber(stat.count || 0) + '×</span>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="mt-3 flex justify-between text-xs text-gray-500">'
|
||||
+ ' <span>' + t('logs.overview.last_seen', 'Last seen') + '</span>'
|
||||
+ ' <span>' + escapeHtml(lastSeenLabel) + '</span>'
|
||||
+ ' </div>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
|
||||
Reference in New Issue
Block a user