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:
2025-11-22 13:09:54 +01:00
parent 74dd84a5d6
commit fd76427cc5
8 changed files with 734 additions and 114 deletions

View File

@@ -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('');