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:
@@ -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 & hosted in the EU by</span>
|
||||
<a href="https://cloudhost.es" target="_blank" rel="noopener">Cloud Host</a>
|
||||
|
|
||||
<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();
|
||||
|
||||
Reference in New Issue
Block a user