feat: add Top UA column to Actions + Reasons panel

Convert actions+reasons panel from 2-col to 3-col grid and add a
Top UA bars chart as the third column, reusing the existing top_ua
stats data. Also adds responsive collapse and i18n key for all 3 locales.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 08:54:16 +02:00
parent a4464214af
commit 85a524784c

View File

@@ -203,6 +203,7 @@ main {
#chart { width: 100%; height: 72px; display: block; } #chart { width: 100%; height: 72px; display: block; }
.bars-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } .bars-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.bars-3col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
.bar-section-title { font-size: 10px; letter-spacing: 2px; color: var(--amber); margin-bottom: 6px; } .bar-section-title { font-size: 10px; letter-spacing: 2px; color: var(--amber); margin-bottom: 6px; }
.bar-list { list-style: none; } .bar-list { list-style: none; }
.bar-item { .bar-item {
@@ -320,7 +321,7 @@ footer a:hover { color: var(--cyan2); }
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.stats-row { grid-template-columns: 1fr 1fr; } .stats-row { grid-template-columns: 1fr 1fr; }
.bars-2col { grid-template-columns: 1fr; } .bars-2col, .bars-3col { grid-template-columns: 1fr; }
.bar-item { grid-template-columns: 100px 1fr 42px; } .bar-item { grid-template-columns: 100px 1fr 42px; }
} }
</style> </style>
@@ -409,7 +410,7 @@ footer a:hover { color: var(--cyan2); }
<div class="panel"> <div class="panel">
<div class="panel-hdr" data-i18n="actions_title">▶ ACTIONS + REASONS // LAST 30 DAYS</div> <div class="panel-hdr" data-i18n="actions_title">▶ ACTIONS + REASONS // LAST 30 DAYS</div>
<div class="panel-body"> <div class="panel-body">
<div class="bars-2col"> <div class="bars-3col">
<div> <div>
<div class="bar-section-title" data-i18n="actions">ACTIONS</div> <div class="bar-section-title" data-i18n="actions">ACTIONS</div>
<ul class="bar-list" id="bars-actions"></ul> <ul class="bar-list" id="bars-actions"></ul>
@@ -418,6 +419,10 @@ footer a:hover { color: var(--cyan2); }
<div class="bar-section-title" data-i18n="reasons">REASONS</div> <div class="bar-section-title" data-i18n="reasons">REASONS</div>
<ul class="bar-list" id="bars-reasons"></ul> <ul class="bar-list" id="bars-reasons"></ul>
</div> </div>
<div>
<div class="bar-section-title" data-i18n="top_ua">TOP UA</div>
<ul class="bar-list" id="bars-ua-ar"></ul>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -481,7 +486,7 @@ const I18N = {
stat_sites:'SITES REPORTING', top_target_label:'▶ MOST ACTIVE BOT TYPE (30D):', stat_sites:'SITES REPORTING', top_target_label:'▶ MOST ACTIVE BOT TYPE (30D):',
chart_title:'▶ 24H ACTIVITY TREND', breakdown_title:'▶ BOT BREAKDOWN // LAST 30 DAYS', chart_title:'▶ 24H ACTIVITY TREND', breakdown_title:'▶ BOT BREAKDOWN // LAST 30 DAYS',
bot_types:'BOT TYPES', ua_family:'UA FAMILIES', bot_types:'BOT TYPES', ua_family:'UA FAMILIES',
actions_title:'▶ ACTIONS + REASONS // LAST 30 DAYS', actions:'ACTIONS', reasons:'REASONS', actions_title:'▶ ACTIONS + REASONS // LAST 30 DAYS', actions:'ACTIONS', reasons:'REASONS', top_ua:'TOP UA',
feed_title:'▶ LIVE BOT FEED', events:'events', feed_title:'▶ LIVE BOT FEED', events:'events',
connecting:'connecting…', connected:'connected', reconnecting:'reconnecting…', connecting:'connecting…', connected:'connected', reconnecting:'reconnecting…',
attackers_title:'▶ TOP OFFENDERS // LAST 30 DAYS', attackers_title:'▶ TOP OFFENDERS // LAST 30 DAYS',
@@ -496,7 +501,7 @@ const I18N = {
stat_sites:'SITIOS REPORTANDO', top_target_label:'▶ BOT MÁS ACTIVO (30D):', stat_sites:'SITIOS REPORTANDO', top_target_label:'▶ BOT MÁS ACTIVO (30D):',
chart_title:'▶ TENDENCIA 24H', breakdown_title:'▶ DESGLOSE // ÚLTIMOS 30 DÍAS', chart_title:'▶ TENDENCIA 24H', breakdown_title:'▶ DESGLOSE // ÚLTIMOS 30 DÍAS',
bot_types:'TIPOS DE BOT', ua_family:'FAMILIAS UA', bot_types:'TIPOS DE BOT', ua_family:'FAMILIAS UA',
actions_title:'▶ ACCIONES + MOTIVOS // ÚLTIMOS 30 DÍAS', actions:'ACCIONES', reasons:'MOTIVOS', actions_title:'▶ ACCIONES + MOTIVOS // ÚLTIMOS 30 DÍAS', actions:'ACCIONES', reasons:'MOTIVOS', top_ua:'TOP UA',
feed_title:'▶ FEED EN VIVO', events:'eventos', feed_title:'▶ FEED EN VIVO', events:'eventos',
connecting:'conectando…', connected:'conectado', reconnecting:'reconectando…', connecting:'conectando…', connected:'conectado', reconnecting:'reconectando…',
attackers_title:'▶ TOP OFENSORES // ÚLTIMOS 30 DÍAS', attackers_title:'▶ TOP OFENSORES // ÚLTIMOS 30 DÍAS',
@@ -511,7 +516,7 @@ const I18N = {
stat_sites:'SITE-URI RAPORTÂND', top_target_label:'▶ BOT CEL MAI ACTIV (30Z):', stat_sites:'SITE-URI RAPORTÂND', top_target_label:'▶ BOT CEL MAI ACTIV (30Z):',
chart_title:'▶ TENDINȚĂ 24H', breakdown_title:'▶ ANALIZĂ // ULTIMELE 30 ZILE', chart_title:'▶ TENDINȚĂ 24H', breakdown_title:'▶ ANALIZĂ // ULTIMELE 30 ZILE',
bot_types:'TIPURI BOT', ua_family:'FAMILII UA', bot_types:'TIPURI BOT', ua_family:'FAMILII UA',
actions_title:'▶ ACȚIUNI + MOTIVE // ULTIMELE 30 ZILE', actions:'ACȚIUNI', reasons:'MOTIVE', actions_title:'▶ ACȚIUNI + MOTIVE // ULTIMELE 30 ZILE', actions:'ACȚIUNI', reasons:'MOTIVE', top_ua:'TOP UA',
feed_title:'▶ FLUX LIVE BOȚI', events:'evenimente', feed_title:'▶ FLUX LIVE BOȚI', events:'evenimente',
connecting:'conectare…', connected:'conectat', reconnecting:'reconectare…', connecting:'conectare…', connected:'conectat', reconnecting:'reconectare…',
attackers_title:'▶ TOP OFENSATORI // ULTIMELE 30 ZILE', attackers_title:'▶ TOP OFENSATORI // ULTIMELE 30 ZILE',
@@ -716,6 +721,7 @@ async function fetchStats() {
renderBars(document.getElementById('bars-ua'), s.top_ua, 'ua_family'); renderBars(document.getElementById('bars-ua'), s.top_ua, 'ua_family');
renderBars(document.getElementById('bars-actions'), s.top_actions, 'action', 'bar-fill-amber'); renderBars(document.getElementById('bars-actions'), s.top_actions, 'action', 'bar-fill-amber');
renderBars(document.getElementById('bars-reasons'), s.top_reasons, 'reason'); renderBars(document.getElementById('bars-reasons'), s.top_reasons, 'reason');
renderBars(document.getElementById('bars-ua-ar'), s.top_ua, 'ua_family');
renderAttackers(s.top_ips); renderAttackers(s.top_ips);
if (s.top_bot_types && s.top_bot_types.length) { if (s.top_bot_types && s.top_bot_types.length) {