feat: i18n (EN/ES/RO), HP_API_TOKEN constant, EU footer v2.3.0

- SmartHoneypotI18n class with EN/ES/RO string tables and flag switcher
- All WP admin UI strings translated (logs tab, settings tab, notices)
- Language preference stored per-user in user meta (hp_lang)
- Browser language auto-detection with localStorage persistence
- HP_API_TOKEN constant support: define in wp-config.php to keep
  token out of the database; UI shows read-only note when active
- resolve_token() checks constant first, falls back to DB setting
- API dashboard: EN/ES/RO language switcher with flag buttons
- API dashboard: data-i18n attributes on all static UI elements
- API dashboard: EU footer "Made & hosted in the EU by Cloud Host"
  with link to cloudhost.es
- Bump version to 2.3.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 20:34:35 +01:00
parent 92e0522a03
commit f1c32e5060
2 changed files with 584 additions and 132 deletions

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -70,7 +70,7 @@ header {
.header-right {
display: flex;
align-items: center;
gap: 20px;
gap: 16px;
font-size: 11px;
color: var(--green2);
letter-spacing: 1px;
@@ -89,6 +89,19 @@ header {
@keyframes blink { 50% { opacity: 0; } }
/* ── Lang switcher ───────────────────────────────────────────────────── */
.lang-switcher { display: flex; gap: 4px; align-items: center; }
.lang-btn {
background: none;
border: none;
cursor: pointer;
padding: 1px 2px;
line-height: 1;
transition: opacity .15s, font-size .15s;
}
.lang-btn.active { font-size: 20px; opacity: 1; }
.lang-btn.inactive { font-size: 14px; opacity: 0.4; }
/* ── Main ───────────────────────────────────────────────────────────── */
main { padding: 14px 16px; max-width: 1700px; margin: 0 auto; }
@@ -267,10 +280,14 @@ footer {
color: var(--dim);
display: flex;
justify-content: space-between;
align-items: center;
letter-spacing: 1px;
flex-wrap: wrap;
gap: 6px;
}
footer a { color: var(--dim); text-decoration: none; }
footer a:hover { color: var(--green2); }
.footer-eu { display: flex; align-items: center; gap: 6px; }
/* ── Top target banner ──────────────────────────────────────────────── */
#top-target {
@@ -296,14 +313,8 @@ footer {
text-shadow: 0 0 10px var(--red);
letter-spacing: 1px;
}
#top-target .tt-hits {
font-size: 11px;
color: var(--amber);
}
#top-target .tt-pct {
font-size: 11px;
color: var(--dim);
}
#top-target .tt-hits { font-size: 11px; color: var(--amber); }
#top-target .tt-pct { font-size: 11px; color: var(--dim); }
/* ── Responsive ─────────────────────────────────────────────────────── */
@media (max-width: 1100px) {
@@ -324,7 +335,12 @@ footer {
<div class="logo glow">[HONEYPOT<em>]</em> // NETWORK THREAT INTELLIGENCE</div>
<div class="header-right">
<span id="clock">--:--:--</span>
<span><span class="live-dot"></span>LIVE FEED</span>
<span><span class="live-dot"></span><span data-i18n="live_feed">LIVE FEED</span></span>
<div class="lang-switcher">
<button class="lang-btn" data-lang="en" onclick="setLang('en')" title="English">🇬🇧</button>
<button class="lang-btn" data-lang="es" onclick="setLang('es')" title="Español">🇪🇸</button>
<button class="lang-btn" data-lang="ro" onclick="setLang('ro')" title="Română">🇷🇴</button>
</div>
</div>
</header>
@@ -334,32 +350,32 @@ footer {
<div class="stats-row">
<div class="stat-card">
<div class="stat-num glow" id="s-total"></div>
<div class="stat-lbl">Total Blocked</div>
<div class="stat-lbl" data-i18n="stat_total">TOTAL BLOCKED</div>
</div>
<div class="stat-card">
<div class="stat-num glow" id="s-today"></div>
<div class="stat-lbl">Today</div>
<div class="stat-lbl" data-i18n="stat_today">TODAY</div>
</div>
<div class="stat-card">
<div class="stat-num" id="s-7d"></div>
<div class="stat-lbl">Last 7 Days</div>
<div class="stat-lbl" data-i18n="stat_7d">LAST 7 DAYS</div>
</div>
<div class="stat-card">
<div class="stat-num" id="s-30d"></div>
<div class="stat-lbl">Last 30 Days</div>
<div class="stat-lbl" data-i18n="stat_30d">LAST 30 DAYS</div>
</div>
<div class="stat-card">
<div class="stat-num glow" id="s-sites"></div>
<div class="stat-lbl">Sites Reporting</div>
<div class="stat-lbl" data-i18n="stat_sites">SITES REPORTING</div>
</div>
</div>
<!-- ── Most attacked form ────────────────────────────────────────── -->
<div id="top-target">
<span class="tt-label">▶ MOST ATTACKED FORM (30D):</span>
<span class="tt-label" id="tt-label" data-i18n="top_target_label">▶ MOST ATTACKED FORM (30D):</span>
<span class="tt-form" id="tt-form"></span>
<span class="tt-hits" id="tt-hits"></span>
<span class="tt-pct" id="tt-pct"></span>
<span class="tt-pct" id="tt-pct"></span>
</div>
<!-- ── Content grid ───────────────────────────────────────────────── -->
@@ -370,7 +386,7 @@ footer {
<!-- 24h chart -->
<div class="panel">
<div class="panel-hdr">
<span>▶ 24H ACTIVITY TREND</span>
<span data-i18n="chart_title">▶ 24H ACTIVITY TREND</span>
<span id="chart-peak" style="color:var(--dim);font-size:11px"></span>
</div>
<div class="panel-body" style="padding:10px 12px">
@@ -380,15 +396,15 @@ footer {
<!-- Bar charts -->
<div class="panel">
<div class="panel-hdr">▶ ATTACK BREAKDOWN // LAST 30 DAYS</div>
<div class="panel-hdr" data-i18n="breakdown_title">▶ ATTACK BREAKDOWN // LAST 30 DAYS</div>
<div class="panel-body">
<div class="bars-2col">
<div>
<div class="bar-section-title">FORM TYPES</div>
<div class="bar-section-title" data-i18n="form_types">FORM TYPES</div>
<ul class="bar-list" id="bars-forms"></ul>
</div>
<div>
<div class="bar-section-title">BOT TOOLKIT</div>
<div class="bar-section-title" data-i18n="bot_toolkit">BOT TOOLKIT</div>
<ul class="bar-list" id="bars-ua"></ul>
</div>
</div>
@@ -397,7 +413,7 @@ footer {
<!-- Block reasons -->
<div class="panel">
<div class="panel-hdr">▶ BLOCK REASONS // LAST 30 DAYS</div>
<div class="panel-hdr" data-i18n="reasons_title">▶ BLOCK REASONS // LAST 30 DAYS</div>
<div class="panel-body">
<ul class="bar-list" id="bars-reasons"></ul>
</div>
@@ -408,13 +424,13 @@ footer {
<!-- Live feed -->
<div class="panel feed-panel">
<div class="panel-hdr">
<span>▶ LIVE THREAT FEED</span>
<span id="feed-count" style="color:var(--dim);font-size:11px">0 events</span>
<span data-i18n="feed_title">▶ LIVE THREAT FEED</span>
<span id="feed-count" style="color:var(--dim);font-size:11px">0 <span data-i18n="events">events</span></span>
</div>
<div id="feed"></div>
<div class="feed-footer">
<span class="cursor"></span>
<span id="feed-status" style="color:var(--dim)">connecting…</span>
<span id="feed-status" style="color:var(--dim)" data-i18n="connecting">connecting…</span>
</div>
</div>
@@ -422,19 +438,19 @@ footer {
<!-- ── Top Attackers ──────────────────────────────────────────────── -->
<div class="panel" style="margin-bottom:10px">
<div class="panel-hdr">▶ TOP ATTACKERS // LAST 30 DAYS</div>
<div class="panel-hdr" data-i18n="attackers_title">▶ TOP ATTACKERS // LAST 30 DAYS</div>
<div style="overflow-x:auto">
<table class="atk-table">
<thead>
<tr>
<th style="width:50px">RANK</th>
<th>IP ADDRESS</th>
<th style="width:110px">TOTAL HITS</th>
<th>FREQUENCY</th>
<th style="width:50px" data-i18n="col_rank">RANK</th>
<th data-i18n="col_ip">IP ADDRESS</th>
<th style="width:110px" data-i18n="col_hits">TOTAL HITS</th>
<th data-i18n="col_freq">FREQUENCY</th>
</tr>
</thead>
<tbody id="atk-body">
<tr><td colspan="4" style="text-align:center;padding:20px;color:var(--dim)">Loading…</td></tr>
<tr><td colspan="4" style="text-align:center;padding:20px;color:var(--dim)" data-i18n="loading">Loading…</td></tr>
</tbody>
</table>
</div>
@@ -443,11 +459,145 @@ footer {
</main>
<footer>
<span>HONEYPOT NETWORK MONITOR // CENTRALIZED THREAT INTELLIGENCE</span>
<span>REFRESHED: <span id="last-update">--</span></span>
<span data-i18n="footer_copy">HONEYPOT NETWORK MONITOR // CENTRALIZED THREAT INTELLIGENCE</span>
<div class="footer-eu">
<span data-i18n="made_in_eu">🇪🇺 Made &amp; hosted in the EU by</span>
<a href="https://cloudhost.es" target="_blank" rel="noopener">Cloud Host</a>
&nbsp;|&nbsp;
<span data-i18n="refreshed">REFRESHED:</span> <span id="last-update">--</span>
</div>
</footer>
<script>
// ── i18n ──────────────────────────────────────────────────────────────────────
const I18N = {
en: {
live_feed: 'LIVE FEED',
stat_total: 'TOTAL BLOCKED',
stat_today: 'TODAY',
stat_7d: 'LAST 7 DAYS',
stat_30d: 'LAST 30 DAYS',
stat_sites: 'SITES REPORTING',
top_target_label: '▶ MOST ATTACKED FORM (30D):',
chart_title: '▶ 24H ACTIVITY TREND',
breakdown_title: '▶ ATTACK BREAKDOWN // LAST 30 DAYS',
form_types: 'FORM TYPES',
bot_toolkit: 'BOT TOOLKIT',
reasons_title: '▶ BLOCK REASONS // LAST 30 DAYS',
feed_title: '▶ LIVE THREAT FEED',
events: 'events',
connecting: 'connecting…',
connected: 'connected',
reconnecting: 'reconnecting…',
attackers_title: '▶ TOP ATTACKERS // LAST 30 DAYS',
col_rank: 'RANK',
col_ip: 'IP ADDRESS',
col_hits: 'TOTAL HITS',
col_freq: 'FREQUENCY',
loading: 'Loading…',
no_data: 'No data yet',
footer_copy: 'HONEYPOT NETWORK MONITOR // CENTRALIZED THREAT INTELLIGENCE',
refreshed: 'REFRESHED:',
made_in_eu: '🇪🇺 Made & hosted in the EU by',
},
es: {
live_feed: 'EN VIVO',
stat_total: 'TOTAL BLOQUEADOS',
stat_today: 'HOY',
stat_7d: 'ÚLTIMOS 7 DÍAS',
stat_30d: 'ÚLTIMOS 30 DÍAS',
stat_sites: 'SITIOS REPORTANDO',
top_target_label: '▶ FORMULARIO MÁS ATACADO (30D):',
chart_title: '▶ TENDENCIA DE ACTIVIDAD 24H',
breakdown_title: '▶ DESGLOSE DE ATAQUES // ÚLTIMOS 30 DÍAS',
form_types: 'TIPOS DE FORMULARIO',
bot_toolkit: 'HERRAMIENTAS BOT',
reasons_title: '▶ MOTIVOS DE BLOQUEO // ÚLTIMOS 30 DÍAS',
feed_title: '▶ FEED DE AMENAZAS EN VIVO',
events: 'eventos',
connecting: 'conectando…',
connected: 'conectado',
reconnecting: 'reconectando…',
attackers_title: '▶ TOP ATACANTES // ÚLTIMOS 30 DÍAS',
col_rank: 'RANGO',
col_ip: 'DIRECCIÓN IP',
col_hits: 'TOTAL IMPACTOS',
col_freq: 'FRECUENCIA',
loading: 'Cargando…',
no_data: 'Sin datos aún',
footer_copy: 'MONITOR DE RED HONEYPOT // INTELIGENCIA CENTRALIZADA DE AMENAZAS',
refreshed: 'ACTUALIZADO:',
made_in_eu: '🇪🇺 Hecho y alojado en la UE por',
},
ro: {
live_feed: 'LIVE',
stat_total: 'TOTAL BLOCATE',
stat_today: 'AZI',
stat_7d: 'ULTIMELE 7 ZILE',
stat_30d: 'ULTIMELE 30 ZILE',
stat_sites: 'SITE-URI RAPORTÂND',
top_target_label: '▶ FORMULARUL CEL MAI ATACAT (30Z):',
chart_title: '▶ TENDINȚĂ ACTIVITATE 24H',
breakdown_title: '▶ ANALIZA ATACURILOR // ULTIMELE 30 ZILE',
form_types: 'TIPURI FORMULAR',
bot_toolkit: 'INSTRUMENTE BOT',
reasons_title: '▶ MOTIVE BLOCARE // ULTIMELE 30 ZILE',
feed_title: '▶ FLUX LIVE AMENINȚĂRI',
events: 'evenimente',
connecting: 'conectare…',
connected: 'conectat',
reconnecting: 'reconectare…',
attackers_title: '▶ TOP ATACATORI // ULTIMELE 30 ZILE',
col_rank: 'RANG',
col_ip: 'ADRESĂ IP',
col_hits: 'TOTAL ACCESĂRI',
col_freq: 'FRECVENȚĂ',
loading: 'Se încarcă…',
no_data: 'Fără date încă',
footer_copy: 'MONITOR REȚEA HONEYPOT // INFORMAȚII CENTRALIZATE DESPRE AMENINȚĂRI',
refreshed: 'ACTUALIZAT:',
made_in_eu: '🇪🇺 Realizat și găzduit în UE de',
},
};
function detectLang() {
const saved = localStorage.getItem('hp_lang');
if (saved && I18N[saved]) return saved;
const nav = (navigator.language || 'en').slice(0, 2).toLowerCase();
return I18N[nav] ? nav : 'en';
}
let currentLang = detectLang();
function t(key) {
return (I18N[currentLang] || I18N.en)[key] || (I18N.en[key] || key);
}
function setLang(lang) {
if (!I18N[lang]) return;
currentLang = lang;
localStorage.setItem('hp_lang', lang);
document.getElementById('html-root').lang = lang;
applyTranslations();
updateLangButtons();
}
function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
// Don't overwrite elements that also have dynamic child content
if (el.children.length === 0) el.textContent = t(key);
});
document.title = t('chart_title').replace('▶ ', '').split('//')[0].trim() + ' // NETWORK MONITOR';
}
function updateLangButtons() {
document.querySelectorAll('.lang-btn').forEach(btn => {
const active = btn.dataset.lang === currentLang;
btn.className = 'lang-btn ' + (active ? 'active' : 'inactive');
});
}
// ── Clock ─────────────────────────────────────────────────────────────────────
const clockEl = document.getElementById('clock');
function tick() {
@@ -475,7 +625,7 @@ function countUp(el, to) {
// ── Bar charts ────────────────────────────────────────────────────────────────
function renderBars(listEl, items) {
if (!items || !items.length) {
listEl.innerHTML = '<li style="color:var(--dim);font-size:11px;padding:4px 0">No data yet</li>';
listEl.innerHTML = `<li style="color:var(--dim);font-size:11px;padding:4px 0">${t('no_data')}</li>`;
return;
}
const max = items[0].hits;
@@ -505,7 +655,7 @@ function drawChart(hourly) {
if (!hourly || !hourly.length) {
ctx.fillStyle = '#005500';
ctx.font = '12px Courier New';
ctx.fillText('No activity in last 24h', 10, 44);
ctx.fillText(t('no_data'), 10, 44);
return;
}
@@ -555,7 +705,7 @@ window.addEventListener('resize', () => { if (window._hourly) drawChart(window._
function renderAttackers(ips) {
const tbody = document.getElementById('atk-body');
if (!ips || !ips.length) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;padding:20px;color:var(--dim)">No data yet</td></tr>';
tbody.innerHTML = `<tr><td colspan="4" style="text-align:center;padding:20px;color:var(--dim)">${t('no_data')}</td></tr>`;
return;
}
const max = ips[0].hits;
@@ -579,7 +729,7 @@ function fmtTime(ts) {
// ── Live feed ─────────────────────────────────────────────────────────────────
let feedCount = 0;
let autoScroll = true;
const feedEl = document.getElementById('feed');
const feedEl = document.getElementById('feed');
const feedCount$ = document.getElementById('feed-count');
const feedStatus = document.getElementById('feed-status');
@@ -588,7 +738,7 @@ feedEl.addEventListener('mouseleave', () => { autoScroll = true; });
function addRow(row) {
feedCount++;
feedCount$.textContent = `${feedCount.toLocaleString()} events`;
feedCount$.textContent = `${feedCount.toLocaleString()} ${t('events')}`;
const el = document.createElement('div');
el.className = 'feed-row';
el.innerHTML = `
@@ -614,13 +764,13 @@ async function seedFeed() {
// SSE for live updates
function connectSSE() {
const es = new EventSource('/api/v1/stream');
es.onopen = () => { feedStatus.textContent = 'connected'; };
es.onopen = () => { feedStatus.textContent = t('connected'); };
es.onmessage = e => {
try { JSON.parse(e.data).reverse().forEach(addRow); } catch {}
};
es.onerror = () => {
es.close();
feedStatus.textContent = 'reconnecting';
feedStatus.textContent = t('reconnecting');
setTimeout(connectSSE, 5000);
};
}
@@ -646,10 +796,11 @@ async function fetchStats() {
// Top attacked form banner
if (s.top_forms && s.top_forms.length) {
const top = s.top_forms[0];
document.getElementById('tt-form').textContent = top.form_type;
document.getElementById('tt-hits').textContent = `${top.hits.toLocaleString()} hits`;
document.getElementById('tt-label').textContent = t('top_target_label');
document.getElementById('tt-form').textContent = top.form_type;
document.getElementById('tt-hits').textContent = `${top.hits.toLocaleString()} hits`;
const pct = s.last_30d > 0 ? Math.round(top.hits / s.last_30d * 100) : 0;
document.getElementById('tt-pct').textContent = `(${pct}% of all blocks)`;
document.getElementById('tt-pct').textContent = `(${pct}% of all blocks)`;
}
window._hourly = s.hourly;
@@ -661,6 +812,9 @@ async function fetchStats() {
}
// ── Boot ──────────────────────────────────────────────────────────────────────
feedStatus.textContent = t('connecting');
applyTranslations();
updateLangButtons();
seedFeed();
connectSSE();
fetchStats();