diff --git a/public/index.html b/public/index.html index 0b6c397..e272817 100644 --- a/public/index.html +++ b/public/index.html @@ -204,6 +204,11 @@ main { .bars-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } .bars-3col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; } +.ua-list { list-style: none; display: flex; flex-direction: column; gap: 5px; } +.ua-row { display: flex; align-items: center; gap: 8px; font-size: 11px; } +.ua-str { flex: 1; color: var(--dim); font-family: monospace; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; } +.ua-hits { color: var(--cyan2); font-weight: 700; width: 36px; text-align: right; flex-shrink: 0; } .bar-section-title { font-size: 10px; letter-spacing: 2px; color: var(--amber); margin-bottom: 6px; } .bar-list { list-style: none; } .bar-item { @@ -410,7 +415,7 @@ footer a:hover { color: var(--cyan2); }
▶ ACTIONS + REASONS // LAST 30 DAYS
-
+
ACTIONS
    @@ -419,14 +424,17 @@ footer a:hover { color: var(--cyan2); }
    REASONS
      -
      -
      TOP UA
      -
        -
        +
        +
        ▶ TOP USER AGENTS // LAST 30 DAYS
        +
        +
          +
          +
          +
          @@ -486,7 +494,8 @@ const I18N = { stat_sites:'SITES REPORTING', top_target_label:'▶ MOST ACTIVE BOT TYPE (30D):', chart_title:'▶ 24H ACTIVITY TREND', breakdown_title:'▶ BOT BREAKDOWN // LAST 30 DAYS', bot_types:'BOT TYPES', ua_family:'UA FAMILIES', - actions_title:'▶ ACTIONS + REASONS // LAST 30 DAYS', actions:'ACTIONS', reasons:'REASONS', top_ua:'TOP UA', + actions_title:'▶ ACTIONS + REASONS // LAST 30 DAYS', actions:'ACTIONS', reasons:'REASONS', + ua_title:'▶ TOP USER AGENTS // LAST 30 DAYS', feed_title:'▶ LIVE BOT FEED', events:'events', connecting:'connecting…', connected:'connected', reconnecting:'reconnecting…', attackers_title:'▶ TOP OFFENDERS // LAST 30 DAYS', @@ -501,7 +510,8 @@ const I18N = { stat_sites:'SITIOS REPORTANDO', top_target_label:'▶ BOT MÁS ACTIVO (30D):', chart_title:'▶ TENDENCIA 24H', breakdown_title:'▶ DESGLOSE // ÚLTIMOS 30 DÍAS', bot_types:'TIPOS DE BOT', ua_family:'FAMILIAS UA', - actions_title:'▶ ACCIONES + MOTIVOS // ÚLTIMOS 30 DÍAS', actions:'ACCIONES', reasons:'MOTIVOS', top_ua:'TOP UA', + actions_title:'▶ ACCIONES + MOTIVOS // ÚLTIMOS 30 DÍAS', actions:'ACCIONES', reasons:'MOTIVOS', + ua_title:'▶ TOP AGENTES DE USUARIO // ÚLTIMOS 30 DÍAS', feed_title:'▶ FEED EN VIVO', events:'eventos', connecting:'conectando…', connected:'conectado', reconnecting:'reconectando…', attackers_title:'▶ TOP OFENSORES // ÚLTIMOS 30 DÍAS', @@ -516,7 +526,8 @@ const I18N = { stat_sites:'SITE-URI RAPORTÂND', top_target_label:'▶ BOT CEL MAI ACTIV (30Z):', chart_title:'▶ TENDINȚĂ 24H', breakdown_title:'▶ ANALIZĂ // ULTIMELE 30 ZILE', bot_types:'TIPURI BOT', ua_family:'FAMILII UA', - actions_title:'▶ ACȚIUNI + MOTIVE // ULTIMELE 30 ZILE', actions:'ACȚIUNI', reasons:'MOTIVE', top_ua:'TOP UA', + actions_title:'▶ ACȚIUNI + MOTIVE // ULTIMELE 30 ZILE', actions:'ACȚIUNI', reasons:'MOTIVE', + ua_title:'▶ TOP USER AGENTS // ULTIMELE 30 ZILE', feed_title:'▶ FLUX LIVE BOȚI', events:'evenimente', connecting:'conectare…', connected:'conectat', reconnecting:'reconectare…', attackers_title:'▶ TOP OFENSATORI // ULTIMELE 30 ZILE', @@ -593,6 +604,19 @@ function renderBars(listEl, items, labelKey, fillClass = '') { }).join(''); } +function renderUAList(listEl, items) { + if (!items || !items.length) { + listEl.innerHTML = `
        • ${t('no_data')}
        • `; + return; + } + listEl.innerHTML = items.map(item => + `
        • + ${item.hits.toLocaleString()} + ${esc(item.ua || '(empty)')} +
        • ` + ).join(''); +} + const canvas = document.getElementById('chart'); const ctx = canvas.getContext('2d'); function drawChart(hourly) { @@ -721,7 +745,7 @@ async function fetchStats() { 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-reasons'), s.top_reasons, 'reason'); - renderBars(document.getElementById('bars-ua-ar'), s.top_ua, 'ua_family'); + renderUAList(document.getElementById('ua-list'), s.top_user_agents || []); renderAttackers(s.top_ips); if (s.top_bot_types && s.top_bot_types.length) { diff --git a/server.js b/server.js index 6c7f984..46944fd 100644 --- a/server.js +++ b/server.js @@ -44,7 +44,7 @@ DB.exec(` `); // Migrations – silently ignored if columns already exist -['country', 'asn', 'request_uri'].forEach(col => { +['country', 'asn', 'request_uri', 'user_agent'].forEach(col => { try { DB.exec(`ALTER TABLE bots ADD COLUMN ${col} TEXT NOT NULL DEFAULT ''`); } catch {} }); @@ -190,6 +190,11 @@ function getStats() { FROM bots WHERE received_at > ? GROUP BY ua_family ORDER BY hits DESC LIMIT 8 `).all(now - 2592000), + top_user_agents: DB.prepare(` + SELECT user_agent ua, COUNT(*) hits + FROM bots WHERE received_at > ? AND user_agent != '' + GROUP BY user_agent ORDER BY hits DESC LIMIT 15 + `).all(now - 2592000), recent: DB.prepare(` SELECT received_at, ip_masked ip, country, bot_type, action, reason, ua_family, site_id FROM bots ORDER BY id DESC LIMIT 40 @@ -221,8 +226,8 @@ setInterval(() => { // ── Prepared statements ─────────────────────────────────────────────────────── const stmtIns = DB.prepare(` - INSERT INTO bots (received_at, site_id, ip_masked, bot_type, action, reason, ua_family, request_uri, country, asn) - VALUES (?,?,?,?,?,?,?,?,?,?) + INSERT INTO bots (received_at, site_id, ip_masked, bot_type, action, reason, ua_family, request_uri, country, asn, user_agent) + VALUES (?,?,?,?,?,?,?,?,?,?,?) `); const stmtSite = DB.prepare(` INSERT INTO sites (site_id, first_seen, last_seen, block_count) VALUES (?,?,?,?) @@ -244,7 +249,8 @@ const insertBatch = DB.transaction((siteId, bots) => { String(b.reason || '').slice(0, 255), parseUA(b.user_agent || ''), String(b.request_uri || '').slice(0, 500), - '', '' // country/asn filled async + '', '', // country/asn filled async + String(b.user_agent || '').slice(0, 300) ); ids.push({ id: Number(r.lastInsertRowid), ip }); } @@ -274,7 +280,7 @@ function selfObserve(req, res, next) { try { const r = stmtIns.run( - now, 'self', ip, fam, 'observed', 'Direct API visitor', fam, req.path, '', '' + now, 'self', ip, fam, 'observed', 'Direct API visitor', fam, req.path, '', '', ua.slice(0, 300) ); _cache = null; setImmediate(() => enrichIP(Number(r.lastInsertRowid), ip));