feat: send local history to central API dashboard

- Add SmartHoneypotAPIClient::send_history_batch():
  reads local wp_honeypot_log in pages of 50, starting from
  hp_history_last_id option, sends to API, tracks progress
- Two new options: hp_history_last_id, hp_history_total_sent
- Action handlers: send_history (one batch), reset_history (start over)
- Settings tab: new 'Send History to API' section showing:
  * Local record count, sent count, remaining count
  * Progress bar (blue WP style)
  * 'Send History' / 'Send Next Batch' button with remaining count
  * 'Reset Progress' to re-send from beginning
  * Section only shown when API is enabled and URL is configured
- Notices: per-action feedback with 'Click Send Next Batch to continue'
This commit is contained in:
2026-03-09 19:54:50 +01:00
parent b6be526b46
commit 92e0522a03

View File

@@ -217,6 +217,85 @@ class SmartHoneypotAPIClient {
return ['ok' => true, 'message' => 'Connection verified. API is reachable and token is accepted.'];
}
/**
* Sends one batch of existing local log records to the central API.
* Picks up from where it left off using the hp_history_last_id option.
*
* Returns ['ok', 'sent', 'remaining', 'has_more', 'message']
*/
public static function send_history_batch(int $batch_size = 50): array {
$s = self::settings();
if (empty($s['api_url'])) {
return ['ok' => false, 'message' => 'No API URL configured.'];
}
global $wpdb;
$table = SmartHoneypotDB::table();
$last_id = (int) get_option('hp_history_last_id', 0);
$total = SmartHoneypotDB::count();
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE id > %d ORDER BY id ASC LIMIT %d",
$last_id, $batch_size
),
ARRAY_A
);
if (empty($rows)) {
return ['ok' => true, 'sent' => 0, 'remaining' => 0, 'has_more' => false,
'message' => 'All records have already been sent.'];
}
$blocks = array_map(fn($r) => [
'ip' => $r['ip_address'],
'form_type' => $r['form_type'],
'reason' => $r['reason'],
'user_agent' => $r['user_agent'],
'blocked_at' => $r['blocked_at'],
], $rows);
$headers = ['Content-Type' => 'application/json'];
if (!empty($s['api_token'])) {
$headers['Authorization'] = 'Bearer ' . $s['api_token'];
}
$response = wp_remote_post(
trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit',
[
'timeout' => 30,
'headers' => $headers,
'body' => wp_json_encode([
'site_hash' => hash('sha256', home_url()),
'blocks' => $blocks,
]),
]
);
if (is_wp_error($response)) {
return ['ok' => false, 'message' => 'Request failed: ' . $response->get_error_message()];
}
$code = wp_remote_retrieve_response_code($response);
if ($code !== 200) {
return ['ok' => false, 'message' => "API returned HTTP {$code}. Check connection settings."];
}
$new_last_id = (int) end($rows)['id'];
$sent_total = (int) get_option('hp_history_total_sent', 0) + count($rows);
update_option('hp_history_last_id', $new_last_id);
update_option('hp_history_total_sent', $sent_total);
$remaining = max(0, $total - $sent_total);
return [
'ok' => true,
'sent' => count($rows),
'remaining' => $remaining,
'has_more' => $remaining > 0,
'message' => sprintf('Sent %d records. %d remaining.', count($rows), $remaining),
];
}
public static function settings(): array {
return wp_parse_args(get_option(self::OPT_SETTINGS, []), self::defaults());
}
@@ -399,6 +478,20 @@ class SmartHoneypotAdmin {
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'flushed' => 1], admin_url('admin.php')));
exit;
}
if ($_POST['hp_action'] === 'send_history') {
$result = SmartHoneypotAPIClient::send_history_batch();
set_transient('hp_history_result', $result, 60);
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'history' => 1], admin_url('admin.php')));
exit;
}
if ($_POST['hp_action'] === 'reset_history') {
delete_option('hp_history_last_id');
delete_option('hp_history_total_sent');
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'history_reset' => 1], admin_url('admin.php')));
exit;
}
}
public static function render_page() {
@@ -427,6 +520,22 @@ class SmartHoneypotAdmin {
<div class="notice <?= $cls ?> is-dismissible"><p><?= esc_html($res['message']) ?></p></div>
<?php endif;
endif; ?>
<?php if (!empty($_GET['history'])):
$res = get_transient('hp_history_result');
if ($res):
$cls = $res['ok'] ? 'notice-success' : 'notice-error'; ?>
<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>
<?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>
<?php endif; ?>
<nav class="nav-tab-wrapper hp-tabs">
<a href="<?= esc_url(admin_url('admin.php?page=' . self::MENU_SLUG . '&tab=logs')) ?>"
@@ -667,6 +776,65 @@ class SmartHoneypotAdmin {
</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>
<?php
$local_total = SmartHoneypotDB::count();
$history_sent = (int) get_option('hp_history_total_sent', 0);
$remaining = max(0, $local_total - $history_sent);
$pct = $local_total > 0 ? min(100, round($history_sent / $local_total * 100)) : 0;
?>
<table class="form-table">
<tr>
<th>Local Log</th>
<td><?= number_format($local_total) ?> records in this site's database</td>
</tr>
<tr>
<th>Sent to API</th>
<td>
<?= number_format($history_sent) ?> records (<?= $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>
</div>
<?php endif; ?>
</td>
</tr>
<tr>
<th>Remaining</th>
<td><?= number_format($remaining) ?> records not yet sent</td>
</tr>
</table>
<div style="display:flex;gap:10px;margin-top:12px;flex-wrap:wrap;align-items:center">
<?php if ($remaining > 0): ?>
<form method="post">
<?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)
</button>
</form>
<?php else: ?>
<span style="color:#00a32a;font-weight:600">✓ All history 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>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php
}