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,29 +350,29 @@ 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>
@@ -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;
@@ -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,6 +796,7 @@ async function fetchStats() {
// Top attacked form banner
if (s.top_forms && s.top_forms.length) {
const top = s.top_forms[0];
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;
@@ -661,6 +812,9 @@ async function fetchStats() {
}
// ── Boot ──────────────────────────────────────────────────────────────────────
feedStatus.textContent = t('connecting');
applyTranslations();
updateLangButtons();
seedFeed();
connectSSE();
fetchStats();

View File

@@ -3,7 +3,7 @@
* Plugin Name: Honeypot Fields
* Plugin URI: https://informatiq.services
* Description: Adds invisible honeypot fields to all forms to block spam bots. Works with WordPress core forms, Elementor, Gravity Forms, Contact Form 7, WooCommerce, and more.
* Version: 2.2.0
* Version: 2.3.0
* Author: Malin
* Author URI: https://malin.ro
* License: GPL v2 or later
@@ -184,8 +184,9 @@ class SmartHoneypotAPIClient {
$base = trailingslashit(esc_url_raw($s['api_url']));
$headers = ['Content-Type' => 'application/json'];
if (!empty($s['api_token'])) {
$headers['Authorization'] = 'Bearer ' . $s['api_token'];
$token = self::resolve_token();
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
// Step 1 — reachability
@@ -256,8 +257,9 @@ class SmartHoneypotAPIClient {
], $rows);
$headers = ['Content-Type' => 'application/json'];
if (!empty($s['api_token'])) {
$headers['Authorization'] = 'Bearer ' . $s['api_token'];
$token = self::resolve_token();
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
$response = wp_remote_post(
@@ -329,8 +331,9 @@ class SmartHoneypotAPIClient {
$site_hash = hash('sha256', home_url());
$headers = ['Content-Type' => 'application/json'];
if (!empty($s['api_token'])) {
$headers['Authorization'] = 'Bearer ' . $s['api_token'];
$token = self::resolve_token();
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
$response = wp_remote_post(
@@ -358,6 +361,292 @@ class SmartHoneypotAPIClient {
public static function queue_size(): int {
return count((array) get_option(self::OPT_QUEUE, []));
}
/**
* Returns the API token.
* Checks the HP_API_TOKEN constant (defined in wp-config.php) first,
* then falls back to the value stored in the database.
*/
private static function resolve_token(): string {
if (defined('HP_API_TOKEN') && HP_API_TOKEN !== '') {
return (string) HP_API_TOKEN;
}
return self::settings()['api_token'] ?? '';
}
}
/* ======================================================================
* INTERNATIONALISATION
* ====================================================================*/
class SmartHoneypotI18n {
const USER_META = 'hp_lang';
// phpcs:disable Generic.Files.LineLength
const STRINGS = [
'en' => [
'tab_logs' => 'Blocked Logs',
'tab_api' => 'Central API',
'cleared' => 'Logs cleared.',
'saved' => 'API settings saved.',
'flushed' => 'Queue flushed to central API.',
'history_reset_msg' => 'History sync progress has been reset. You can re-send from the beginning.',
'click_next' => 'Click "Send Next Batch" to continue.',
'total_blocked' => 'Total Blocked',
'today' => 'Today',
'unique_ips' => 'Unique IPs',
'form_types_hit' => 'Form Types Hit',
'search_ph' => 'Search IP, UA, reason…',
'filter_ip_ph' => 'Filter by IP',
'all_forms' => 'All form types',
'filter_btn' => 'Filter',
'reset_btn' => 'Reset',
'clear_logs' => 'Clear All Logs',
'clear_confirm' => 'Delete ALL log entries permanently?',
'col_date' => 'Date / Time',
'col_ip' => 'IP Address',
'col_form' => 'Form Type',
'col_reason' => 'Reason',
'col_uri' => 'URI',
'col_ua' => 'User Agent',
'no_results' => 'No blocked attempts recorded yet.',
'filter_link' => 'filter',
'lookup_link' => 'lookup ↗',
'prev' => '← Prev',
'next' => 'Next →',
'showing' => 'Showing',
'results' => 'result(s)',
'page_of' => 'page %d of %d',
'settings_title' => 'Central API Settings',
'settings_desc' => 'Submit blocked attempts to a central dashboard for aggregate threat intelligence. Data sent: full IP address, form type, block reason, UA family, and timestamp. No site URL or usernames are ever sent.',
'enable_label' => 'Enable Submission',
'enable_cb' => 'Send blocked attempts to the central API',
'url_label' => 'API Endpoint URL',
'url_desc' => 'Base URL of your Honeypot API Docker container.',
'token_label' => 'API Token',
'token_desc' => 'Must match the <code>API_TOKEN</code> set in your Docker container\'s environment. Leave empty only if the API runs without a token (not recommended).',
'token_const_note' => 'Token is set via the <code>HP_API_TOKEN</code> constant in <code>wp-config.php</code>.',
'save_settings' => 'Save Settings',
'conn_status' => 'Connection Status',
'conn_label' => 'Connection',
'conn_ok' => 'Verified — connected and token accepted',
'conn_fail' => 'Connection failed',
'conn_untested' => 'Not tested yet',
'last_verified' => 'Last Verified',
'enabled_lbl' => 'Enabled',
'yes' => 'Yes',
'no' => 'No',
'last_sync' => 'Last Sync',
'never' => 'Never',
'total_sent' => 'Total Sent',
'blocks_unit' => 'blocks',
'queue_size' => 'Queue Size',
'pending_unit' => 'pending',
'next_flush' => 'Next Auto-Flush',
'not_scheduled' => 'Not scheduled',
'test_conn' => 'Test Connection',
'flush_queue' => 'Flush Queue Now',
'history_title' => 'Send History to API',
'history_desc' => 'Populate the central dashboard with your existing log so charts and stats are meaningful right away, without waiting for new attacks. Records are sent in batches of 50.',
'local_log' => 'Local Log',
'local_log_val' => '%s records in this site\'s database',
'sent_api' => 'Sent to API',
'remaining_lbl' => 'Remaining',
'remaining_val' => '%s records not yet sent',
'send_history' => 'Send History',
'send_next' => 'Send Next Batch',
'all_sent' => '✓ All history sent',
'reset_progress' => 'Reset Progress',
],
'es' => [
'tab_logs' => 'Registros Bloqueados',
'tab_api' => 'API Central',
'cleared' => 'Registros eliminados.',
'saved' => 'Configuración de API guardada.',
'flushed' => 'Cola enviada a la API central.',
'history_reset_msg' => 'El progreso de sincronización se ha restablecido. Puedes reenviar desde el principio.',
'click_next' => 'Haz clic en "Enviar Siguiente Lote" para continuar.',
'total_blocked' => 'Total Bloqueados',
'today' => 'Hoy',
'unique_ips' => 'IPs Únicas',
'form_types_hit' => 'Tipos de Formulario',
'search_ph' => 'Buscar IP, UA, razón…',
'filter_ip_ph' => 'Filtrar por IP',
'all_forms' => 'Todos los tipos',
'filter_btn' => 'Filtrar',
'reset_btn' => 'Reiniciar',
'clear_logs' => 'Borrar Todos los Registros',
'clear_confirm' => '¿Eliminar TODOS los registros permanentemente?',
'col_date' => 'Fecha / Hora',
'col_ip' => 'Dirección IP',
'col_form' => 'Tipo de Formulario',
'col_reason' => 'Razón',
'col_uri' => 'URI',
'col_ua' => 'User Agent',
'no_results' => 'No hay intentos bloqueados aún.',
'filter_link' => 'filtrar',
'lookup_link' => 'consultar ↗',
'prev' => '← Anterior',
'next' => 'Siguiente →',
'showing' => 'Mostrando',
'results' => 'resultado(s)',
'page_of' => 'página %d de %d',
'settings_title' => 'Configuración de la API Central',
'settings_desc' => 'Envía intentos bloqueados a un panel central de inteligencia de amenazas. Se envía: IP completa, tipo de formulario, razón de bloqueo, familia de UA y marca de tiempo. Nunca se envían URLs del sitio ni nombres de usuario.',
'enable_label' => 'Activar Envío',
'enable_cb' => 'Enviar intentos bloqueados a la API central',
'url_label' => 'URL del Endpoint de la API',
'url_desc' => 'URL base del contenedor Docker de la API Honeypot.',
'token_label' => 'Token de la API',
'token_desc' => 'Debe coincidir con <code>API_TOKEN</code> en el entorno del contenedor Docker.',
'token_const_note' => 'El token está definido mediante la constante <code>HP_API_TOKEN</code> en <code>wp-config.php</code>.',
'save_settings' => 'Guardar Configuración',
'conn_status' => 'Estado de la Conexión',
'conn_label' => 'Conexión',
'conn_ok' => 'Verificada — conectada y token aceptado',
'conn_fail' => 'Error de conexión',
'conn_untested' => 'No probada todavía',
'last_verified' => 'Última Verificación',
'enabled_lbl' => 'Activado',
'yes' => 'Sí',
'no' => 'No',
'last_sync' => 'Última Sincronización',
'never' => 'Nunca',
'total_sent' => 'Total Enviados',
'blocks_unit' => 'bloqueos',
'queue_size' => 'Tamaño de Cola',
'pending_unit' => 'pendientes',
'next_flush' => 'Próximo Envío Automático',
'not_scheduled' => 'No programado',
'test_conn' => 'Probar Conexión',
'flush_queue' => 'Enviar Cola Ahora',
'history_title' => 'Enviar Historial a la API',
'history_desc' => 'Rellena el panel central con tu registro existente para que las estadísticas sean significativas de inmediato, sin esperar nuevos ataques. Los registros se envían en lotes de 50.',
'local_log' => 'Registro Local',
'local_log_val' => '%s registros en la base de datos de este sitio',
'sent_api' => 'Enviados a la API',
'remaining_lbl' => 'Restantes',
'remaining_val' => '%s registros aún no enviados',
'send_history' => 'Enviar Historial',
'send_next' => 'Enviar Siguiente Lote',
'all_sent' => '✓ Todo el historial enviado',
'reset_progress' => 'Reiniciar Progreso',
],
'ro' => [
'tab_logs' => 'Jurnale Blocate',
'tab_api' => 'API Central',
'cleared' => 'Jurnale șterse.',
'saved' => 'Setări API salvate.',
'flushed' => 'Coada trimisă la API-ul central.',
'history_reset_msg' => 'Progresul sincronizării a fost resetat. Poți retrimite de la început.',
'click_next' => 'Apasă „Trimite Lotul Următor" pentru a continua.',
'total_blocked' => 'Total Blocate',
'today' => 'Azi',
'unique_ips' => 'IP-uri Unice',
'form_types_hit' => 'Tipuri de Formulare Atacate',
'search_ph' => 'Caută IP, UA, motiv…',
'filter_ip_ph' => 'Filtrează după IP',
'all_forms' => 'Toate tipurile',
'filter_btn' => 'Filtrează',
'reset_btn' => 'Resetează',
'clear_logs' => 'Șterge Toate Jurnalele',
'clear_confirm' => 'Ștergi TOATE înregistrările permanent?',
'col_date' => 'Dată / Oră',
'col_ip' => 'Adresă IP',
'col_form' => 'Tip Formular',
'col_reason' => 'Motiv',
'col_uri' => 'URI',
'col_ua' => 'User Agent',
'no_results' => 'Nu există tentative blocate încă.',
'filter_link' => 'filtrează',
'lookup_link' => 'caută ↗',
'prev' => '← Anterior',
'next' => 'Următor →',
'showing' => 'Afișând',
'results' => 'rezultat(e)',
'page_of' => 'pagina %d din %d',
'settings_title' => 'Setări API Central',
'settings_desc' => 'Trimite tentativele blocate la un panou central de informații despre amenințări. Date trimise: IP complet, tip formular, motiv blocare, familie UA și marcă temporală. Nu se trimit niciodată URL-ul site-ului sau nume de utilizatori.',
'enable_label' => 'Activează Trimiterea',
'enable_cb' => 'Trimite tentativele blocate la API-ul central',
'url_label' => 'URL Endpoint API',
'url_desc' => 'URL-ul de bază al containerului Docker API Honeypot.',
'token_label' => 'Token API',
'token_desc' => 'Trebuie să coincidă cu <code>API_TOKEN</code> din mediul containerului Docker.',
'token_const_note' => 'Tokenul este setat prin constanta <code>HP_API_TOKEN</code> în <code>wp-config.php</code>.',
'save_settings' => 'Salvează Setările',
'conn_status' => 'Status Conexiune',
'conn_label' => 'Conexiune',
'conn_ok' => 'Verificată — conectată și token acceptat',
'conn_fail' => 'Conexiune eșuată',
'conn_untested' => 'Netestată încă',
'last_verified' => 'Ultima Verificare',
'enabled_lbl' => 'Activat',
'yes' => 'Da',
'no' => 'Nu',
'last_sync' => 'Ultima Sincronizare',
'never' => 'Niciodată',
'total_sent' => 'Total Trimise',
'blocks_unit' => 'blocări',
'queue_size' => 'Dimensiune Coadă',
'pending_unit' => 'în așteptare',
'next_flush' => 'Următoarea Trimitere Automată',
'not_scheduled' => 'Neprogramată',
'test_conn' => 'Testează Conexiunea',
'flush_queue' => 'Trimite Coada Acum',
'history_title' => 'Trimite Istoricul la API',
'history_desc' => 'Populează panoul central cu jurnalul existent pentru ca statisticile să fie relevante imediat, fără a aștepta noi atacuri. Înregistrările se trimit în loturi de 50.',
'local_log' => 'Jurnal Local',
'local_log_val' => '%s înregistrări în baza de date a acestui site',
'sent_api' => 'Trimise la API',
'remaining_lbl' => 'Rămase',
'remaining_val' => '%s înregistrări netrimise',
'send_history' => 'Trimite Istoricul',
'send_next' => 'Trimite Lotul Următor',
'all_sent' => '✓ Tot istoricul a fost trimis',
'reset_progress' => 'Resetează Progresul',
],
];
// phpcs:enable
public static function get_lang(): string {
$user_id = get_current_user_id();
$lang = $user_id ? (string) get_user_meta($user_id, self::USER_META, true) : '';
return in_array($lang, ['en', 'es', 'ro'], true) ? $lang : 'en';
}
public static function t(string $key): string {
$lang = self::get_lang();
$strings = self::STRINGS[$lang] ?? self::STRINGS['en'];
return $strings[$key] ?? (self::STRINGS['en'][$key] ?? $key);
}
public static function flag_switcher(): string {
$lang = self::get_lang();
$nonce = wp_nonce_field('hp_admin_action', '_wpnonce', true, false);
$tab = sanitize_key($_GET['tab'] ?? 'logs');
$html = '<div style="display:inline-flex;gap:4px;align-items:center">';
foreach (['en' => '🇬🇧', 'es' => '🇪🇸', 'ro' => '🇷🇴'] as $code => $flag) {
$style = $lang === $code
? 'background:none;border:none;cursor:pointer;padding:2px;font-size:22px;opacity:1'
: 'background:none;border:none;cursor:pointer;padding:2px;font-size:16px;opacity:0.45';
$html .= sprintf(
'<form method="post" style="margin:0">%s'
. '<input type="hidden" name="tab" value="%s">'
. '<input type="hidden" name="hp_action" value="set_language">'
. '<input type="hidden" name="hp_lang" value="%s">'
. '<button type="submit" title="%s" style="%s">%s</button></form>',
$nonce,
esc_attr($tab),
esc_attr($code),
esc_attr(strtoupper($code)),
esc_attr($style),
$flag
);
}
$html .= '</div>';
return $html;
}
}
/* ======================================================================
@@ -441,7 +730,7 @@ class SmartHoneypotAdmin {
if ($_POST['hp_action'] === 'save_api_settings') {
$current = SmartHoneypotAPIClient::settings();
$new_url = esc_url_raw(trim($_POST['hp_api_url'] ?? ''));
$new_token = sanitize_text_field($_POST['hp_api_token'] ?? '');
$new_token = defined('HP_API_TOKEN') ? $current['api_token'] : sanitize_text_field($_POST['hp_api_token'] ?? '');
$url_changed = $new_url !== $current['api_url'];
$tok_changed = $new_token !== $current['api_token'];
@@ -492,6 +781,16 @@ class SmartHoneypotAdmin {
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'history_reset' => 1], admin_url('admin.php')));
exit;
}
if ($_POST['hp_action'] === 'set_language') {
$lang = sanitize_key($_POST['hp_lang'] ?? 'en');
if (in_array($lang, ['en', 'es', 'ro'], true)) {
update_user_meta(get_current_user_id(), SmartHoneypotI18n::USER_META, $lang);
}
$tab = sanitize_key($_GET['tab'] ?? 'logs');
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => $tab], admin_url('admin.php')));
exit;
}
}
public static function render_page() {
@@ -503,15 +802,16 @@ class SmartHoneypotAdmin {
?>
<div class="wrap" id="hp-wrap">
<h1 class="wp-heading-inline">Honeypot Fields</h1>
<span style="margin-left:14px;vertical-align:middle"><?= SmartHoneypotI18n::flag_switcher() ?></span>
<?php if (!empty($_GET['cleared'])): ?>
<div class="notice notice-success is-dismissible"><p>Logs cleared.</p></div>
<div class="notice notice-success is-dismissible"><p><?= SmartHoneypotI18n::t('cleared') ?></p></div>
<?php endif; ?>
<?php if (!empty($_GET['saved'])): ?>
<div class="notice notice-success is-dismissible"><p>API settings saved.</p></div>
<div class="notice notice-success is-dismissible"><p><?= SmartHoneypotI18n::t('saved') ?></p></div>
<?php endif; ?>
<?php if (!empty($_GET['flushed'])): ?>
<div class="notice notice-success is-dismissible"><p>Queue flushed to central API.</p></div>
<div class="notice notice-success is-dismissible"><p><?= SmartHoneypotI18n::t('flushed') ?></p></div>
<?php endif; ?>
<?php if (!empty($_GET['tested'])):
$res = get_transient('hp_conn_result');
@@ -527,24 +827,24 @@ class SmartHoneypotAdmin {
<div class="notice <?= $cls ?> is-dismissible">
<p><?= esc_html($res['message']) ?>
<?php if (!empty($res['has_more'])): ?>
&nbsp;<strong>Click "Send Next Batch" to continue.</strong>
&nbsp;<strong><?= SmartHoneypotI18n::t('click_next') ?></strong>
<?php endif; ?>
</p>
</div>
<?php endif;
endif; ?>
<?php if (!empty($_GET['history_reset'])): ?>
<div class="notice notice-info is-dismissible"><p>History sync progress has been reset. You can re-send from the beginning.</p></div>
<div class="notice notice-info is-dismissible"><p><?= SmartHoneypotI18n::t('history_reset_msg') ?></p></div>
<?php endif; ?>
<nav class="nav-tab-wrapper hp-tabs">
<a href="<?= esc_url(admin_url('admin.php?page=' . self::MENU_SLUG . '&tab=logs')) ?>"
class="nav-tab <?= $tab === 'logs' ? 'nav-tab-active' : '' ?>">
Blocked Logs
<?= SmartHoneypotI18n::t('tab_logs') ?>
</a>
<a href="<?= esc_url(admin_url('admin.php?page=' . self::MENU_SLUG . '&tab=settings')) ?>"
class="nav-tab <?= $tab === 'settings' ? 'nav-tab-active' : '' ?>">
Central API
<?= SmartHoneypotI18n::t('tab_api') ?>
</a>
</nav>
@@ -560,6 +860,7 @@ class SmartHoneypotAdmin {
/* ── Logs tab ─────────────────────────────────────────────────── */
private static function render_logs_tab() {
$T = fn(string $k): string => SmartHoneypotI18n::t($k);
$search = sanitize_text_field($_GET['hp_search'] ?? '');
$filter_ip = sanitize_text_field($_GET['hp_ip'] ?? '');
$filter_form = sanitize_text_field($_GET['hp_form'] ?? '');
@@ -586,10 +887,10 @@ class SmartHoneypotAdmin {
?>
<!-- Stats -->
<div class="hp-stats">
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($total_ever) ?></div><div class="hp-stat-lbl">Total Blocked</div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($today) ?></div><div class="hp-stat-lbl">Today</div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($unique_ips) ?></div><div class="hp-stat-lbl">Unique IPs</div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= count($form_types) ?></div><div class="hp-stat-lbl">Form Types Hit</div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($total_ever) ?></div><div class="hp-stat-lbl"><?= $T('total_blocked') ?></div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($today) ?></div><div class="hp-stat-lbl"><?= $T('today') ?></div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($unique_ips) ?></div><div class="hp-stat-lbl"><?= $T('unique_ips') ?></div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= count($form_types) ?></div><div class="hp-stat-lbl"><?= $T('form_types_hit') ?></div></div>
</div>
<!-- Filters + clear -->
@@ -597,44 +898,44 @@ class SmartHoneypotAdmin {
<input type="hidden" name="page" value="<?= esc_attr(self::MENU_SLUG) ?>">
<input type="hidden" name="tab" value="logs">
<div class="hp-filters">
<input type="text" name="hp_search" placeholder="Search IP, UA, reason…" value="<?= esc_attr($search) ?>" size="28">
<input type="text" name="hp_ip" placeholder="Filter by IP" value="<?= esc_attr($filter_ip) ?>">
<input type="text" name="hp_search" placeholder="<?= esc_attr($T('search_ph')) ?>" value="<?= esc_attr($search) ?>" size="28">
<input type="text" name="hp_ip" placeholder="<?= esc_attr($T('filter_ip_ph')) ?>" value="<?= esc_attr($filter_ip) ?>">
<select name="hp_form">
<option value="">All form types</option>
<option value=""><?= $T('all_forms') ?></option>
<?php foreach ($form_types as $ft): ?>
<option value="<?= esc_attr($ft) ?>" <?= selected($filter_form, $ft, false) ?>><?= esc_html($ft) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="button">Filter</button>
<button type="submit" class="button"><?= $T('filter_btn') ?></button>
<?php if ($search || $filter_ip || $filter_form): ?>
<a href="<?= esc_url($base) ?>" class="button">Reset</a>
<a href="<?= esc_url($base) ?>" class="button"><?= $T('reset_btn') ?></a>
<?php endif; ?>
<span style="flex:1"></span>
<form method="post" style="display:inline" onsubmit="return confirm('Delete ALL log entries permanently?')">
<form method="post" style="display:inline" onsubmit="return confirm('<?= esc_js($T('clear_confirm')) ?>')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="hp_action" value="clear_logs">
<button type="submit" class="button hp-red">Clear All Logs</button>
<button type="submit" class="button hp-red"><?= $T('clear_logs') ?></button>
</form>
</div>
</form>
<p>Showing <strong><?= number_format($total) ?></strong> result<?= $total !== 1 ? 's' : '' ?> (page <?= $paged ?> of <?= $total_pages ?>)</p>
<p><?= $T('showing') ?> <strong><?= number_format($total) ?></strong> <?= $T('results') ?> (<?= sprintf($T('page_of'), $paged, $total_pages) ?>)</p>
<table class="hp-log widefat">
<thead>
<tr><th>#</th><th>Date / Time</th><th>IP Address</th><th>Form Type</th><th>Reason</th><th>URI</th><th>User Agent</th></tr>
<tr><th>#</th><th><?= $T('col_date') ?></th><th><?= $T('col_ip') ?></th><th><?= $T('col_form') ?></th><th><?= $T('col_reason') ?></th><th><?= $T('col_uri') ?></th><th><?= $T('col_ua') ?></th></tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="7" style="text-align:center;padding:24px;color:#646970">No blocked attempts recorded yet.</td></tr>
<tr><td colspan="7" style="text-align:center;padding:24px;color:#646970"><?= $T('no_results') ?></td></tr>
<?php else: foreach ($rows as $row): ?>
<tr>
<td><?= esc_html($row->id) ?></td>
<td style="white-space:nowrap"><?= esc_html($row->blocked_at) ?></td>
<td>
<code><?= esc_html($row->ip_address) ?></code><br>
<a href="<?= esc_url(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'logs', 'hp_ip' => $row->ip_address], admin_url('admin.php'))) ?>" style="font-size:11px">filter</a>
&nbsp;<a href="https://ipinfo.io/<?= esc_attr(urlencode($row->ip_address)) ?>" target="_blank" style="font-size:11px">lookup ↗</a>
<a href="<?= esc_url(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'logs', 'hp_ip' => $row->ip_address], admin_url('admin.php'))) ?>" style="font-size:11px"><?= $T('filter_link') ?></a>
&nbsp;<a href="https://ipinfo.io/<?= esc_attr(urlencode($row->ip_address)) ?>" target="_blank" style="font-size:11px"><?= $T('lookup_link') ?></a>
</td>
<td><span class="hp-badge"><?= esc_html($row->form_type) ?></span></td>
<td><?= esc_html($row->reason) ?></td>
@@ -648,7 +949,7 @@ class SmartHoneypotAdmin {
<?php if ($total_pages > 1): ?>
<div class="hp-pager">
<?php if ($paged > 1): ?>
<a href="<?= esc_url(add_query_arg(array_merge($_GET, ['paged' => $paged - 1]))) ?>">← Prev</a>
<a href="<?= esc_url(add_query_arg(array_merge($_GET, ['paged' => $paged - 1]))) ?>"><?= $T('prev') ?></a>
<?php endif; ?>
<?php for ($p = max(1, $paged-3); $p <= min($total_pages, $paged+3); $p++):
$url = esc_url(add_query_arg(array_merge($_GET, ['paged' => $p]))); ?>
@@ -659,7 +960,7 @@ class SmartHoneypotAdmin {
<?php endif; ?>
<?php endfor; ?>
<?php if ($paged < $total_pages): ?>
<a href="<?= esc_url(add_query_arg(array_merge($_GET, ['paged' => $paged + 1]))) ?>">Next →</a>
<a href="<?= esc_url(add_query_arg(array_merge($_GET, ['paged' => $paged + 1]))) ?>"><?= $T('next') ?></a>
<?php endif; ?>
</div>
<?php endif; ?>
@@ -668,16 +969,15 @@ class SmartHoneypotAdmin {
/* ── Settings / API tab ───────────────────────────────────────── */
private static function render_settings_tab() {
$T = fn(string $k): string => SmartHoneypotI18n::t($k);
$s = SmartHoneypotAPIClient::settings();
$queue_size = SmartHoneypotAPIClient::queue_size();
$next_run = wp_next_scheduled('hp_api_flush');
$token_via_const = defined('HP_API_TOKEN') && HP_API_TOKEN !== '';
?>
<div style="max-width:700px;margin-top:20px">
<h2>Central API Settings</h2>
<p style="color:#646970;margin-bottom:16px">
Submit blocked attempts to a central dashboard for aggregate threat intelligence.
Data sent: full IP address, form type, block reason, UA family, and timestamp. No site URL or usernames are ever sent.
</p>
<h2><?= $T('settings_title') ?></h2>
<p style="color:#646970;margin-bottom:16px"><?= $T('settings_desc') ?></p>
<form method="post">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
@@ -685,61 +985,62 @@ class SmartHoneypotAdmin {
<table class="form-table">
<tr>
<th>Enable Submission</th>
<th><?= $T('enable_label') ?></th>
<td>
<label>
<input type="checkbox" name="hp_api_enabled" value="1" <?= checked($s['enabled']) ?>>
Send blocked attempts to the central API
<?= $T('enable_cb') ?>
</label>
</td>
</tr>
<tr>
<th>API Endpoint URL</th>
<th><?= $T('url_label') ?></th>
<td>
<input type="url" name="hp_api_url" value="<?= esc_attr($s['api_url']) ?>"
class="regular-text" placeholder="https://your-api-host:3000">
<p class="description">Base URL of your Honeypot API Docker container.</p>
<p class="description"><?= $T('url_desc') ?></p>
</td>
</tr>
<tr>
<th>API Token</th>
<th><?= $T('token_label') ?></th>
<td>
<?php if ($token_via_const): ?>
<input type="text" class="regular-text" value="••••••••••••" disabled>
<p class="description"><?= $T('token_const_note') ?></p>
<?php else: ?>
<input type="password" name="hp_api_token" value="<?= esc_attr($s['api_token']) ?>"
class="regular-text" placeholder="Bearer token (matches API_TOKEN in docker-compose)">
<p class="description">
Must match the <code>API_TOKEN</code> set in your Docker container's environment.
Leave empty only if the API is running without a token (not recommended).
</p>
<p class="description"><?= $T('token_desc') ?></p>
<?php endif; ?>
</td>
</tr>
</table>
<?php submit_button('Save Settings'); ?>
<?php submit_button($T('save_settings')); ?>
</form>
<hr>
<h3>Connection Status</h3>
<h3><?= $T('conn_status') ?></h3>
<?php
// Resolve dot state: null=untested, true=verified, false=failed
$conn = $s['connection_ok'];
if ($conn === true) {
$dot_class = 'dot-on';
$dot_label = 'Verified — connected and token accepted';
$dot_label = $T('conn_ok');
$dot_style = '';
} elseif ($conn === false) {
$dot_class = 'dot-fail';
$dot_label = 'Connection failed';
$dot_label = $T('conn_fail');
$dot_style = 'background:#b32d2e;';
} else {
$dot_class = 'dot-off';
$dot_label = 'Not tested yet';
$dot_label = $T('conn_untested');
$dot_style = '';
}
?>
<table class="form-table">
<tr>
<th>Connection</th>
<th><?= $T('conn_label') ?></th>
<td>
<span class="hp-api-status">
<span class="dot <?= $dot_class ?>" style="<?= $dot_style ?>"></span>
@@ -751,13 +1052,13 @@ class SmartHoneypotAdmin {
</td>
</tr>
<?php if ($s['last_verified']): ?>
<tr><th>Last Verified</th><td><?= esc_html(date('Y-m-d H:i:s', $s['last_verified'])) ?></td></tr>
<tr><th><?= $T('last_verified') ?></th><td><?= esc_html(date('Y-m-d H:i:s', $s['last_verified'])) ?></td></tr>
<?php endif; ?>
<tr><th>Enabled</th><td><?= $s['enabled'] ? 'Yes' : 'No' ?></td></tr>
<tr><th>Last Sync</th><td><?= $s['last_sync'] ? esc_html(date('Y-m-d H:i:s', $s['last_sync'])) : 'Never' ?></td></tr>
<tr><th>Total Sent</th><td><?= number_format((int) $s['sent_total']) ?> blocks</td></tr>
<tr><th>Queue Size</th><td><?= number_format($queue_size) ?> pending</td></tr>
<tr><th>Next Auto-Flush</th><td><?= $next_run ? esc_html(date('Y-m-d H:i:s', $next_run)) . ' (every 5 min)' : 'Not scheduled' ?></td></tr>
<tr><th><?= $T('enabled_lbl') ?></th><td><?= $s['enabled'] ? $T('yes') : $T('no') ?></td></tr>
<tr><th><?= $T('last_sync') ?></th><td><?= $s['last_sync'] ? esc_html(date('Y-m-d H:i:s', $s['last_sync'])) : $T('never') ?></td></tr>
<tr><th><?= $T('total_sent') ?></th><td><?= number_format((int) $s['sent_total']) ?> <?= $T('blocks_unit') ?></td></tr>
<tr><th><?= $T('queue_size') ?></th><td><?= number_format($queue_size) ?> <?= $T('pending_unit') ?></td></tr>
<tr><th><?= $T('next_flush') ?></th><td><?= $next_run ? esc_html(date('Y-m-d H:i:s', $next_run)) . ' (every 5 min)' : $T('not_scheduled') ?></td></tr>
</table>
<div style="display:flex;gap:10px;margin-top:14px;flex-wrap:wrap;align-items:center">
@@ -765,25 +1066,22 @@ class SmartHoneypotAdmin {
<form method="post">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="hp_action" value="test_connection">
<button type="submit" class="button button-primary">Test Connection</button>
<button type="submit" class="button button-primary"><?= $T('test_conn') ?></button>
</form>
<?php endif; ?>
<?php if ($queue_size > 0 && $s['enabled'] && $s['api_url']): ?>
<form method="post">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="hp_action" value="flush_queue">
<button type="submit" class="button button-secondary">Flush Queue Now (<?= $queue_size ?> pending)</button>
<button type="submit" class="button button-secondary"><?= $T('flush_queue') ?> (<?= $queue_size ?> <?= $T('pending_unit') ?>)</button>
</form>
<?php endif; ?>
</div>
<?php if ($s['api_url'] && $s['enabled']): ?>
<hr>
<h3>Send History to API</h3>
<p style="color:#646970;margin-bottom:12px">
Populate the central dashboard with your existing log so the charts and stats are meaningful right away,
without waiting for new attacks. Records are sent in batches of 50.
</p>
<h3><?= $T('history_title') ?></h3>
<p style="color:#646970;margin-bottom:12px"><?= $T('history_desc') ?></p>
<?php
$local_total = SmartHoneypotDB::count();
$history_sent = (int) get_option('hp_history_total_sent', 0);
@@ -792,13 +1090,13 @@ class SmartHoneypotAdmin {
?>
<table class="form-table">
<tr>
<th>Local Log</th>
<td><?= number_format($local_total) ?> records in this site's database</td>
<th><?= $T('local_log') ?></th>
<td><?= sprintf($T('local_log_val'), number_format($local_total)) ?></td>
</tr>
<tr>
<th>Sent to API</th>
<th><?= $T('sent_api') ?></th>
<td>
<?= number_format($history_sent) ?> records (<?= $pct ?>%)
<?= number_format($history_sent) ?> (<?= $pct ?>%)
<?php if ($local_total > 0): ?>
<div style="margin-top:6px;background:#f0f0f1;border-radius:3px;height:8px;max-width:300px">
<div style="background:#2271b1;height:100%;border-radius:3px;width:<?= $pct ?>%;transition:width .4s"></div>
@@ -807,8 +1105,8 @@ class SmartHoneypotAdmin {
</td>
</tr>
<tr>
<th>Remaining</th>
<td><?= number_format($remaining) ?> records not yet sent</td>
<th><?= $T('remaining_lbl') ?></th>
<td><?= sprintf($T('remaining_val'), number_format($remaining)) ?></td>
</tr>
</table>
@@ -818,18 +1116,18 @@ class SmartHoneypotAdmin {
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="hp_action" value="send_history">
<button type="submit" class="button button-secondary">
<?= $history_sent === 0 ? 'Send History' : 'Send Next Batch' ?>
(<?= min(50, $remaining) ?> of <?= number_format($remaining) ?> remaining)
<?= $history_sent === 0 ? $T('send_history') : $T('send_next') ?>
(<?= min(50, $remaining) ?> of <?= number_format($remaining) ?> <?= $T('pending_unit') ?>)
</button>
</form>
<?php else: ?>
<span style="color:#00a32a;font-weight:600">✓ All history sent</span>
<span style="color:#00a32a;font-weight:600"><?= $T('all_sent') ?></span>
<?php endif; ?>
<?php if ($history_sent > 0): ?>
<form method="post" onsubmit="return confirm('Reset history sync progress? This allows re-sending all records from the beginning.')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="hp_action" value="reset_history">
<button type="submit" class="button">Reset Progress</button>
<button type="submit" class="button"><?= $T('reset_progress') ?></button>
</form>
<?php endif; ?>
</div>