diff --git a/statuspulse.php b/statuspulse.php
index 54ed7ac..472f5e2 100644
--- a/statuspulse.php
+++ b/statuspulse.php
@@ -1,1714 +1,1727 @@
-
- .spw-widget {
- background: #fff;
- border: 1px solid #ccc;
- border-radius: 4px;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
- }
- .spw-header {
- background: #f5f5f5;
- border-bottom: 1px solid #ddd;
- padding: 8px 12px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .spw-title {
- font-weight: 600;
- font-size: 13px;
- color: #333;
- margin: 0;
- }
- .spw-status {
- display: inline-block;
- padding: 2px 8px;
- border-radius: 3px;
- font-size: 11px;
- font-weight: 600;
- background: ' . esc_attr($status_color) . ';
- color: #fff;
- }
- .spw-body {
- padding: 12px;
- }
- .spw-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 12px;
- margin-bottom: 12px;
- }
- .spw-item {
- background: #f9f9f9;
- border: 1px solid #eee;
- border-radius: 4px;
- padding: 8px;
- }
- .spw-item-label {
- font-size: 11px;
- color: #666;
- font-weight: 600;
- text-transform: uppercase;
- margin-bottom: 3px;
- display: block;
- }
- .spw-item-value {
- font-size: 20px;
- font-weight: 600;
- color: #000;
- }
- .spw-item-meta {
- font-size: 11px;
- color: #999;
- margin-top: 2px;
- }
- .spw-metrics {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 8px;
- margin-bottom: 12px;
- }
- .spw-metric {
- background: #f9f9f9;
- border: 1px solid #eee;
- border-radius: 4px;
- padding: 6px;
- text-align: center;
- }
- .spw-metric-label {
- font-size: 10px;
- color: #666;
- font-weight: 600;
- text-transform: uppercase;
- margin-bottom: 2px;
- }
- .spw-metric-value {
- font-size: 16px;
- font-weight: 600;
- color: #000;
- }
- .spw-footer {
- display: flex;
- gap: 8px;
- border-top: 1px solid #eee;
- padding-top: 8px;
- }
- .spw-link {
- flex: 1;
- text-align: center;
- padding: 6px 8px;
- background: #f5f5f5;
- border: 1px solid #ddd;
- border-radius: 3px;
- font-size: 11px;
- color: #0073aa;
- text-decoration: none;
- font-weight: 600;
- transition: background 0.2s;
- }
- .spw-link:hover {
- background: #e5e5e5;
- color: #005a87;
- text-decoration: none;
- }
- @media (max-width: 900px) {
- .spw-row { grid-template-columns: 1fr; }
- .spw-metrics { grid-template-columns: repeat(2, 1fr); }
- }
- ';
-
- echo '
';
- self::render_styles();
- self::render_scripts($refresh_nonce);
- echo '
';
- echo '
';
- echo '
Proxy Control Surface';
- echo '
StatusPulse
';
- echo '
Reduzierte Admin-Seite für Proxy-URL, Attack-Key und direkte Tests gegen dein StatusAPI-Plugin.
';
- echo '
';
- echo '
';
- echo 'Live Refresh';
- echo 'alle 15s';
- echo 'wartet auf erstes Update';
- echo '
';
- echo '
';
-
- if ($notice !== null) {
- printf(
- '
',
- esc_attr($notice['type']),
- esc_html($notice['message'])
- );
- }
-
- echo '
';
- self::render_status_card_named(
- 'statusapi-card-proxy',
- 'Proxy Status',
- $live_status['label'],
- $live_status['meta'],
- $live_status['status']
- );
- self::render_status_card_named(
- 'statusapi-card-http',
- 'Letzter HTTP-Code',
- $live_status['http_code'] > 0 ? (string) $live_status['http_code'] : 'n/a',
- $live_status['checked_at_human'],
- $live_status['status']
- );
- self::render_status_card_named(
- 'statusapi-card-success',
- 'Letzte erfolgreiche Prüfung',
- self::format_timestamp($state['last_success_at']),
- $state['last_success_code'] > 0 ? 'HTTP ' . $state['last_success_code'] : 'Noch kein erfolgreicher Check gespeichert',
- $state['last_success_at'] > 0 ? 'ok' : 'unknown'
- );
- self::render_status_card_named(
- 'statusapi-card-lastcheck',
- 'Zuletzt geprüft',
- self::format_timestamp($state['last_check_at']),
- $state['last_error'] !== '' ? $state['last_error'] : 'Keine Fehler gespeichert',
- $state['last_status']
- );
- echo '
';
-
- echo '
';
- self::render_metric_card('statusapi-metric-online', 'Spieler online', $live_status['metrics']['online_players'], $live_status['metrics']['online_meta']);
- self::render_metric_card('statusapi-metric-max', 'Max. Spieler', $live_status['metrics']['max_players'], $live_status['metrics']['max_meta']);
- self::render_metric_card('statusapi-metric-uptime', 'Uptime', $live_status['metrics']['uptime'], $live_status['metrics']['uptime_meta']);
- self::render_metric_card('statusapi-metric-memory', 'RAM Nutzung', $live_status['metrics']['memory'], $live_status['metrics']['memory_meta']);
- echo '
';
-
- echo '
';
- echo '
';
-
- echo '
';
- echo '
Snippets
Direkt für deine Proxy-Konfiguration und externe Tests vorbereitet.
';
- self::render_snippet_card('Für network-guard.properties', $network_guard_snippet, 4);
- self::render_snippet_card('Wichtiger Endpunkt', $status_json_snippet . "\nAttack POST: {$attack_endpoint}", 4);
- self::render_snippet_card('Test-Requests', $curl_examples, 8);
- echo '
';
-
- echo '
';
- echo '
Aktionen
Teste direkt aus dem Backend, ob dein Proxy erreichbar ist und Attack-Meldungen akzeptiert.
';
- echo '
';
- echo '
';
- echo '
';
- echo '
';
- }
-
- public static function render_messages_page() {
- if (!current_user_can('manage_options')) {
- return;
- }
-
- $state = self::get_state();
- $notice = self::build_notice();
- $messages_markup = self::build_messages_markup($state);
- $refresh_nonce = wp_create_nonce('statusapi_backend_helper_refresh');
-
- echo '
';
- self::render_styles();
- self::render_scripts($refresh_nonce);
- echo '
StatusPulse Meldungen
';
- echo '
Dedizierte Seite für alle laufenden Status-, Attack- und Warnmeldungen.
';
-
- if ($notice !== null) {
- printf(
- '
',
- esc_attr($notice['type']),
- esc_html($notice['message'])
- );
- }
-
- self::render_messages_center($messages_markup, self::MESSAGES_SLUG);
- echo '
';
- }
-
- public static function render_list_page() {
- if (!current_user_can('manage_options')) {
- return;
- }
-
- $options = self::get_options();
- $notice = self::build_notice();
- $attacker_data = self::fetch_attacker_entries($options['statusapi_base_url']);
- $rows = isset($attacker_data['entries']) && is_array($attacker_data['entries']) ? $attacker_data['entries'] : array();
- $list_error = isset($attacker_data['error']) ? (string) $attacker_data['error'] : '';
-
- echo '
';
- self::render_styles();
-
- echo '
StatusPulse Angriffsversuche
';
- echo '
Zeigt nur Spielername und UUID von erkannten Angriffsversuchen (inkl. Datum/Uhrzeit).
';
-
- if ($notice !== null) {
- printf(
- '
',
- esc_attr($notice['type']),
- esc_html($notice['message'])
- );
- }
-
- echo '
';
- echo '
Angriffs-Ereignisse
Neueste Einträge oben.
';
-
- if ($list_error !== '') {
- echo '
' . esc_html($list_error) . '
';
- }
-
- if (empty($rows)) {
- echo '
Keine Angriffs-Einträge mit Spielername/UUID vorhanden.
';
- } else {
- echo '
';
- echo '| Datum / Uhrzeit | Spielername | IP | UUID |
';
- echo '';
- foreach ($rows as $row) {
- $date = isset($row['datetime']) ? (string) $row['datetime'] : '';
- $player = isset($row['player']) ? (string) $row['player'] : '-';
- $ip = isset($row['ip']) ? (string) $row['ip'] : '-';
- $uuid = isset($row['uuid']) ? (string) $row['uuid'] : '-';
-
- echo '';
- echo '| ' . esc_html($date) . ' | ';
- echo '' . esc_html($player) . ' | ';
- echo '' . esc_html($ip) . ' | ';
- echo '' . esc_html($uuid) . ' | ';
- echo '
';
- }
- echo '
';
- }
-
- echo '
';
- echo '
';
- }
-
- private static function render_messages_center($messages_markup, $redirect_page) {
- echo '
';
- echo '
Meldungs-Center
Hier siehst du laufend erkannte Status-, Attack- und Warnmeldungen aus deinem Proxy.
';
- echo '
';
- echo '';
- echo '';
- echo '';
- echo '';
- echo '';
- echo '';
- echo '
';
- echo '
';
- echo '
';
- }
-
- private static function render_input_field($label, $name, $value, $description) {
- echo '
';
- }
-
- private static function render_snippet_card($title, $content, $rows) {
- echo '
';
- echo '
';
- echo '
' . esc_html($title) . '
';
- echo '';
- echo '';
- printf(
- '
',
- (int) $rows,
- esc_attr('statusapi-copy-' . md5($title . $content)),
- esc_textarea($content)
- );
- echo '
';
- }
-
- private static function render_status_card($title, $value, $meta, $status) {
- self::render_status_card_named('', $title, $value, $meta, $status);
- }
-
- private static function render_status_card_named($id, $title, $value, $meta, $status) {
- $status_class = 'statusapi-state-unknown';
- if ($status === 'ok') {
- $status_class = 'statusapi-state-ok';
- } elseif ($status === 'error') {
- $status_class = 'statusapi-state-error';
- }
-
- $id_attr = $id !== '' ? ' id="' . esc_attr($id) . '"' : '';
- echo '
';
- echo '' . esc_html($title) . '';
- echo '' . esc_html($value) . '';
- echo '' . esc_html($meta) . '';
- echo '
';
- }
-
- private static function render_metric_card($id, $title, $value, $meta) {
- echo '
';
- echo '' . esc_html($title) . '';
- echo '' . esc_html($value) . '';
- echo '' . esc_html($meta) . '';
- echo '
';
- }
-
- private static function build_curl_examples($status_endpoint, $attack_endpoint, $attack_api_key, $attack_source) {
- $payload = wp_json_encode(array(
- 'event' => 'detected',
- 'source' => $attack_source,
- 'connectionsPerSecond' => 250,
- 'ipAddressesBlocked' => 12,
- 'connectionsBlocked' => 1800,
- ));
-
- return "GET {$status_endpoint}\n\nPOST {$attack_endpoint}\nHeader: X-API-Key: {$attack_api_key}\nBody: {$payload}";
- }
-
- private static function probe_statusapi($base_url) {
- $state = self::get_state();
- $previous_status = isset($state['last_status']) ? (string) $state['last_status'] : 'unknown';
- $previous_attack_mode = array_key_exists('last_attack_mode', $state) ? $state['last_attack_mode'] : null;
- $previous_memory_high = !empty($state['last_memory_high']);
- $previous_player_high = !empty($state['last_player_high']);
-
- if ($base_url === '') {
- return array(
- 'status' => 'unknown',
- 'label' => 'Nicht konfiguriert',
- 'meta' => 'Bitte zuerst eine StatusAPI Basis-URL eintragen.',
- 'http_code' => 0,
- 'checked_at_human' => self::format_timestamp($state['last_check_at']),
- 'metrics' => self::empty_metrics(),
- );
- }
-
- $response = wp_remote_get($base_url, array('timeout' => 5));
- $now = time();
-
- if (is_wp_error($response)) {
- $state['last_check_at'] = $now;
- $state['last_http_code'] = 0;
- $state['last_status'] = 'error';
- $state['last_error'] = $response->get_error_message();
- if ($previous_status !== 'error') {
- self::push_message($state, 'error', 'Proxy offline', 'StatusAPI ist nicht erreichbar: ' . $response->get_error_message());
- }
- self::save_state($state);
-
- return array(
- 'status' => 'error',
- 'label' => 'Offline',
- 'meta' => $response->get_error_message(),
- 'http_code' => 0,
- 'checked_at_human' => self::format_timestamp($now),
- 'metrics' => self::empty_metrics(),
- );
- }
-
- $code = (int) wp_remote_retrieve_response_code($response);
- $ok = $code >= 200 && $code < 300;
- $body = wp_remote_retrieve_body($response);
- $decoded = json_decode($body, true);
- $state['last_check_at'] = $now;
- $state['last_http_code'] = $code;
- $state['last_status'] = $ok ? 'ok' : 'error';
- $state['last_error'] = $ok ? '' : 'HTTP ' . $code;
-
- if ($ok && $previous_status === 'error') {
- self::push_message($state, 'success', 'Proxy wieder online', 'StatusAPI antwortet wieder erfolgreich.');
- }
- if (!$ok && $previous_status !== 'error') {
- self::push_message($state, 'error', 'Proxy-Fehler', 'StatusAPI antwortet mit HTTP ' . $code . '.');
- }
-
- if ($ok && is_array($decoded)) {
- $network = isset($decoded['network']) && is_array($decoded['network']) ? $decoded['network'] : array();
- $antibot = isset($decoded['antibot']) && is_array($decoded['antibot']) ? $decoded['antibot'] : array();
-
- if (array_key_exists('attack_mode', $antibot)) {
- $attack_mode = (bool) $antibot['attack_mode'];
- if ($previous_attack_mode !== null && $attack_mode !== (bool) $previous_attack_mode) {
- if ($attack_mode) {
- $cps = isset($antibot['last_cps']) ? $antibot['last_cps'] : 'n/a';
- self::push_message($state, 'warning', 'Attack erkannt', 'AntiBot hat den Attack-Mode aktiviert (CPS: ' . $cps . ').');
- } else {
- self::push_message($state, 'success', 'Attack beendet', 'AntiBot hat den Attack-Mode beendet.');
- }
- }
- $state['last_attack_mode'] = $attack_mode;
- }
-
- $memory_percent = null;
- if (isset($network['memory']) && is_array($network['memory']) && isset($network['memory']['usage_percent'])) {
- $memory_percent = (int) $network['memory']['usage_percent'];
- }
- $is_memory_high = $memory_percent !== null && $memory_percent >= 90;
- if ($is_memory_high && !$previous_memory_high) {
- self::push_message($state, 'warning', 'RAM-Warnung', 'RAM-Auslastung liegt bei ' . $memory_percent . '%.');
- }
- if (!$is_memory_high && $previous_memory_high) {
- self::push_message($state, 'success', 'RAM normalisiert', 'RAM-Auslastung ist wieder unter 90%.');
- }
- $state['last_memory_high'] = $is_memory_high;
-
- $occupancy = null;
- if (isset($network['players']) && is_array($network['players']) && isset($network['players']['occupancy_percent'])) {
- $occupancy = (int) $network['players']['occupancy_percent'];
- }
- $is_player_high = $occupancy !== null && $occupancy >= 95;
- if ($is_player_high && !$previous_player_high) {
- self::push_message($state, 'warning', 'Spieler-Auslastung hoch', 'Spieler-Auslastung liegt bei ' . $occupancy . '%.');
- }
- if (!$is_player_high && $previous_player_high) {
- self::push_message($state, 'success', 'Spieler-Auslastung normalisiert', 'Spieler-Auslastung ist wieder unter 95%.');
- }
- $state['last_player_high'] = $is_player_high;
- }
-
- if ($ok) {
- $state['last_success_at'] = $now;
- $state['last_success_code'] = $code;
- }
- self::save_state($state);
-
- return array(
- 'status' => $ok ? 'ok' : 'error',
- 'label' => $ok ? 'Online' : 'Fehler',
- 'meta' => $ok ? 'Antwort von ' . untrailingslashit($base_url) : 'HTTP ' . $code,
- 'http_code' => $code,
- 'checked_at_human' => self::format_timestamp($now),
- 'metrics' => $ok ? self::extract_metrics($decoded) : self::empty_metrics(),
- );
- }
-
- private static function extract_metrics($decoded) {
- if (!is_array($decoded)) {
- return self::empty_metrics();
- }
-
- $network = isset($decoded['network']) && is_array($decoded['network']) ? $decoded['network'] : array();
- $players = isset($network['players']) && is_array($network['players']) ? $network['players'] : array();
- $memory = isset($network['memory']) && is_array($network['memory']) ? $network['memory'] : array();
-
- $online_players = self::array_value($players, 'online', self::count_players_fallback($decoded));
- $max_players = self::array_value($players, 'max', self::array_value($decoded, 'max_players', 'n/a'));
- $occupancy = self::array_value($players, 'occupancy_percent', null);
- $uptime_human = self::array_value($network, 'uptime_human', 'n/a');
- $uptime_seconds = self::array_value($network, 'uptime_seconds', null);
- $memory_percent = self::array_value($memory, 'usage_percent', 'n/a');
- $memory_used = self::array_value($memory, 'used_mb', 'n/a');
- $memory_max = self::array_value($memory, 'max_mb', 'n/a');
-
- return array(
- 'online_players' => (string) $online_players,
- 'online_meta' => $occupancy !== null ? 'Auslastung ' . $occupancy . '%' : 'Aktuell verbundene Spieler',
- 'max_players' => (string) $max_players,
- 'max_meta' => 'Player-Limit laut Proxy',
- 'uptime' => (string) $uptime_human,
- 'uptime_meta' => $uptime_seconds !== null ? ((string) $uptime_seconds) . ' Sekunden' : 'Keine Uptime-Daten',
- 'memory' => ((string) $memory_percent) . '%',
- 'memory_meta' => $memory_used . ' MB von ' . $memory_max . ' MB',
- );
- }
-
- private static function empty_metrics() {
- return array(
- 'online_players' => 'n/a',
- 'online_meta' => 'Keine Live-Daten',
- 'max_players' => 'n/a',
- 'max_meta' => 'Keine Live-Daten',
- 'uptime' => 'n/a',
- 'uptime_meta' => 'Keine Live-Daten',
- 'memory' => 'n/a',
- 'memory_meta' => 'Keine Live-Daten',
- );
- }
-
- private static function array_value($array, $key, $fallback) {
- if (!is_array($array) || !array_key_exists($key, $array)) {
- return $fallback;
- }
- return $array[$key];
- }
-
- private static function count_players_fallback($decoded) {
- if (isset($decoded['players']) && is_array($decoded['players'])) {
- return count($decoded['players']);
- }
- return 'n/a';
- }
-
- private static function format_timestamp($timestamp) {
- $timestamp = (int) $timestamp;
- if ($timestamp <= 0) {
- return 'Noch kein Wert';
- }
-
- return wp_date('d.m.Y H:i:s', $timestamp);
- }
-
- private static function build_notice() {
- if (!isset($_GET['statusapi_notice'])) {
- return null;
- }
-
- $code = sanitize_text_field(wp_unslash($_GET['statusapi_notice']));
- if ($code === 'connection_ok') {
- return array('type' => 'success', 'message' => 'StatusAPI ist erreichbar.');
- }
- if ($code === 'attack_ok') {
- return array('type' => 'success', 'message' => 'Test Attack-Meldung wurde erfolgreich an den Proxy gesendet.');
- }
- if ($code === 'missing_url') {
- return array('type' => 'error', 'message' => 'Bitte zuerst die StatusAPI Basis-URL eintragen.');
- }
- if ($code === 'missing_attack_key') {
- return array('type' => 'error', 'message' => 'Bitte zuerst einen Attack API-Key speichern.');
- }
- if ($code === 'connection_failed') {
- return array('type' => 'error', 'message' => 'Verbindungstest fehlgeschlagen. Bitte Basis-URL und Erreichbarkeit prüfen.');
- }
- if ($code === 'attack_failed') {
- return array('type' => 'error', 'message' => 'Test Attack-Meldung konnte nicht gesendet werden. API-Key und Proxy-Endpunkt prüfen.');
- }
- if ($code === 'messages_cleared') {
- return array('type' => 'success', 'message' => 'Meldungen wurden erfolgreich geleert.');
- }
-
- return null;
- }
-
- public static function handle_test_action() {
- if (!current_user_can('manage_options')) {
- wp_die('Nicht erlaubt.');
- }
-
- check_admin_referer('statusapi_backend_helper_test');
-
- $options = self::get_options();
- $base_url = $options['statusapi_base_url'];
- if ($base_url === '') {
- self::redirect_with_notice('missing_url');
- }
-
- if (isset($_POST['test_connection'])) {
- $ok = self::request_ok($base_url);
- $state = self::get_state();
- self::push_message(
- $state,
- $ok ? 'success' : 'error',
- $ok ? 'Verbindungstest erfolgreich' : 'Verbindungstest fehlgeschlagen',
- $ok ? 'Manueller Verbindungstest im WordPress-Backend war erfolgreich.' : 'Manueller Verbindungstest im WordPress-Backend war nicht erfolgreich.'
- );
- self::save_state($state);
- self::redirect_with_notice($ok ? 'connection_ok' : 'connection_failed');
- }
-
- if (isset($_POST['test_attack'])) {
- if ($options['attack_api_key'] === '') {
- self::redirect_with_notice('missing_attack_key');
- }
-
- $payload = array(
- 'event' => 'detected',
- 'source' => $options['attack_source'] !== '' ? $options['attack_source'] : 'WordPress',
- 'connectionsPerSecond' => 250,
- 'ipAddressesBlocked' => 12,
- 'connectionsBlocked' => 1800,
- );
-
- $response = wp_remote_post($base_url . '/network/attack', array(
- 'timeout' => 8,
- 'headers' => array(
- 'Content-Type' => 'application/json; charset=utf-8',
- 'X-API-Key' => $options['attack_api_key'],
- ),
- 'body' => wp_json_encode($payload),
- ));
-
- if (is_wp_error($response)) {
- $state = self::get_state();
- $state['last_check_at'] = time();
- $state['last_http_code'] = 0;
- $state['last_status'] = 'error';
- $state['last_error'] = $response->get_error_message();
- self::push_message($state, 'error', 'Test-Attack fehlgeschlagen', 'Konnte keine Test-Meldung senden: ' . $response->get_error_message());
- self::save_state($state);
- self::redirect_with_notice('attack_failed');
- }
-
- $code = (int) wp_remote_retrieve_response_code($response);
- $state = self::get_state();
- $state['last_check_at'] = time();
- $state['last_http_code'] = $code;
- $state['last_status'] = ($code >= 200 && $code < 300) ? 'ok' : 'error';
- $state['last_error'] = ($code >= 200 && $code < 300) ? '' : 'HTTP ' . $code;
- if ($code >= 200 && $code < 300) {
- $state['last_success_at'] = $state['last_check_at'];
- $state['last_success_code'] = $code;
- self::push_message($state, 'success', 'Test-Attack gesendet', 'Die Test-Attack-Meldung wurde erfolgreich an den Proxy gesendet.');
- } else {
- self::push_message($state, 'error', 'Test-Attack fehlgeschlagen', 'Der Proxy antwortete mit HTTP ' . $code . '.');
- }
- self::save_state($state);
- self::redirect_with_notice($code >= 200 && $code < 300 ? 'attack_ok' : 'attack_failed');
- }
-
- self::redirect_with_notice('connection_failed');
- }
-
- public static function handle_clear_messages() {
- if (!current_user_can('manage_options')) {
- wp_die('Nicht erlaubt.');
- }
-
- check_admin_referer('statusapi_backend_helper_clear_messages');
-
- $state = self::get_state();
- $redirect_page = isset($_POST['redirect_page']) ? sanitize_text_field(wp_unslash($_POST['redirect_page'])) : self::MENU_SLUG;
-
- if ($redirect_page === self::LIST_SLUG) {
- self::save_history(array());
- } else {
- $state['messages'] = array();
- }
- self::save_state($state);
-
- if ($redirect_page !== self::MESSAGES_SLUG) {
- if ($redirect_page !== self::LIST_SLUG) {
- $redirect_page = self::MENU_SLUG;
- }
- }
-
- self::redirect_with_notice('messages_cleared', $redirect_page);
- }
-
- public static function handle_ajax_refresh() {
- if (!current_user_can('manage_options')) {
- wp_send_json_error(array('message' => 'forbidden'), 403);
- }
-
- check_ajax_referer('statusapi_backend_helper_refresh');
-
- $options = self::get_options();
- $live_status = self::probe_statusapi($options['statusapi_base_url']);
- $state = self::get_state();
-
- wp_send_json_success(array(
- 'refresh_state' => 'Aktualisiert ' . self::format_timestamp($state['last_check_at']),
- 'messages_html' => self::build_messages_markup($state),
- 'status_cards' => array(
- 'proxy' => array(
- 'value' => $live_status['label'],
- 'meta' => $live_status['meta'],
- 'status' => $live_status['status'],
- ),
- 'http' => array(
- 'value' => $live_status['http_code'] > 0 ? (string) $live_status['http_code'] : 'n/a',
- 'meta' => $live_status['checked_at_human'],
- 'status' => $live_status['status'],
- ),
- 'success' => array(
- 'value' => self::format_timestamp($state['last_success_at']),
- 'meta' => $state['last_success_code'] > 0 ? 'HTTP ' . $state['last_success_code'] : 'Noch kein erfolgreicher Check gespeichert',
- 'status' => $state['last_success_at'] > 0 ? 'ok' : 'unknown',
- ),
- 'lastcheck' => array(
- 'value' => self::format_timestamp($state['last_check_at']),
- 'meta' => $state['last_error'] !== '' ? $state['last_error'] : 'Keine Fehler gespeichert',
- 'status' => $state['last_status'],
- ),
- ),
- 'metric_cards' => array(
- 'online' => array(
- 'value' => $live_status['metrics']['online_players'],
- 'meta' => $live_status['metrics']['online_meta'],
- ),
- 'max' => array(
- 'value' => $live_status['metrics']['max_players'],
- 'meta' => $live_status['metrics']['max_meta'],
- ),
- 'uptime' => array(
- 'value' => $live_status['metrics']['uptime'],
- 'meta' => $live_status['metrics']['uptime_meta'],
- ),
- 'memory' => array(
- 'value' => $live_status['metrics']['memory'],
- 'meta' => $live_status['metrics']['memory_meta'],
- ),
- ),
- ));
- }
-
- private static function request_ok($url) {
- $response = wp_remote_get($url, array('timeout' => 8));
- if (is_wp_error($response)) {
- return false;
- }
-
- $code = (int) wp_remote_retrieve_response_code($response);
- return $code >= 200 && $code < 300;
- }
-
- private static function fetch_attacker_entries($base_url) {
- if ($base_url === '') {
- return array(
- 'entries' => array(),
- 'error' => 'StatusAPI Basis-URL ist nicht konfiguriert.',
- );
- }
-
- $endpoint = untrailingslashit($base_url) . '/antibot/security-log';
- $response = wp_remote_get($endpoint, array('timeout' => 8));
- if (is_wp_error($response)) {
- return array(
- 'entries' => array(),
- 'error' => 'Konnte Angriffsdaten nicht laden: ' . $response->get_error_message(),
- );
- }
-
- $code = (int) wp_remote_retrieve_response_code($response);
- if ($code < 200 || $code >= 300) {
- return array(
- 'entries' => array(),
- 'error' => 'StatusAPI lieferte HTTP ' . $code . ' für /antibot/security-log.',
- );
- }
-
- $body = wp_remote_retrieve_body($response);
- $decoded = json_decode($body, true);
- if (!is_array($decoded) || !isset($decoded['events']) || !is_array($decoded['events'])) {
- return array(
- 'entries' => array(),
- 'error' => 'Antwort von /antibot/security-log ist ungültig.',
- );
- }
-
- return array(
- 'entries' => $decoded['events'],
- 'error' => '',
- );
- }
-
- private static function redirect_with_notice($code, $page = self::MENU_SLUG) {
- $target_page = self::MENU_SLUG;
- if ($page === self::MESSAGES_SLUG) {
- $target_page = self::MESSAGES_SLUG;
- } elseif ($page === self::LIST_SLUG) {
- $target_page = self::LIST_SLUG;
- }
- $url = add_query_arg(
- array(
- 'page' => $target_page,
- 'statusapi_notice' => $code,
- ),
- admin_url('admin.php')
- );
-
- wp_safe_redirect($url);
- exit;
- }
-
- private static function render_styles() {
- echo '';
- }
-
- private static function render_scripts($refresh_nonce) {
- echo '';
- }
- }
-
- StatusAPI_Backend_Helper::boot();
+
+ .spw-widget {
+ background: #fff;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ }
+ .spw-header {
+ background: #f5f5f5;
+ border-bottom: 1px solid #ddd;
+ padding: 8px 12px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+ .spw-title {
+ font-weight: 600;
+ font-size: 13px;
+ color: #333;
+ margin: 0;
+ }
+ .spw-status {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 3px;
+ font-size: 11px;
+ font-weight: 600;
+ background: ' . esc_attr($status_color) . ';
+ color: #fff;
+ }
+ .spw-body {
+ padding: 12px;
+ }
+ .spw-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+ margin-bottom: 12px;
+ }
+ .spw-item {
+ background: #f9f9f9;
+ border: 1px solid #eee;
+ border-radius: 4px;
+ padding: 8px;
+ }
+ .spw-item-label {
+ font-size: 11px;
+ color: #666;
+ font-weight: 600;
+ text-transform: uppercase;
+ margin-bottom: 3px;
+ display: block;
+ }
+ .spw-item-value {
+ font-size: 20px;
+ font-weight: 600;
+ color: #000;
+ }
+ .spw-item-meta {
+ font-size: 11px;
+ color: #999;
+ margin-top: 2px;
+ }
+ .spw-metrics {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 8px;
+ margin-bottom: 12px;
+ }
+ .spw-metric {
+ background: #f9f9f9;
+ border: 1px solid #eee;
+ border-radius: 4px;
+ padding: 6px;
+ text-align: center;
+ }
+ .spw-metric-label {
+ font-size: 10px;
+ color: #666;
+ font-weight: 600;
+ text-transform: uppercase;
+ margin-bottom: 2px;
+ }
+ .spw-metric-value {
+ font-size: 16px;
+ font-weight: 600;
+ color: #000;
+ }
+ .spw-footer {
+ display: flex;
+ gap: 8px;
+ border-top: 1px solid #eee;
+ padding-top: 8px;
+ }
+ .spw-link {
+ flex: 1;
+ text-align: center;
+ padding: 6px 8px;
+ background: #f5f5f5;
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ font-size: 11px;
+ color: #0073aa;
+ text-decoration: none;
+ font-weight: 600;
+ transition: background 0.2s;
+ }
+ .spw-link:hover {
+ background: #e5e5e5;
+ color: #005a87;
+ text-decoration: none;
+ }
+ @media (max-width: 900px) {
+ .spw-row { grid-template-columns: 1fr; }
+ .spw-metrics { grid-template-columns: repeat(2, 1fr); }
+ }
+ ';
+
+ echo '
';
+ }
+
+ public static function register_menu() {
+ add_menu_page(
+ 'StatusPulse',
+ 'StatusPulse',
+ 'manage_options',
+ self::MENU_SLUG,
+ array(__CLASS__, 'render_page'),
+ 'dashicons-shield-alt',
+ 81
+ );
+
+ add_submenu_page(
+ self::MENU_SLUG,
+ 'Meldungen',
+ 'Meldungen',
+ 'manage_options',
+ self::MESSAGES_SLUG,
+ array(__CLASS__, 'render_messages_page')
+ );
+
+ add_submenu_page(
+ self::MENU_SLUG,
+ 'Angriffsversuche',
+ 'Angriffsversuche',
+ 'manage_options',
+ self::LIST_SLUG,
+ array(__CLASS__, 'render_list_page')
+ );
+ }
+
+ public static function register_settings() {
+ register_setting(
+ 'statusapi_backend_helper_group',
+ self::OPTION_KEY,
+ array(__CLASS__, 'sanitize_options')
+ );
+ }
+
+ public static function sanitize_options($input) {
+ $input = is_array($input) ? $input : array();
+
+ return array(
+ 'statusapi_base_url' => self::sanitize_url(isset($input['statusapi_base_url']) ? $input['statusapi_base_url'] : ''),
+ 'attack_api_key' => sanitize_text_field(isset($input['attack_api_key']) ? $input['attack_api_key'] : ''),
+ 'attack_source' => sanitize_text_field(isset($input['attack_source']) ? $input['attack_source'] : 'WordPress'),
+ );
+ }
+
+ private static function sanitize_url($url) {
+ $url = trim((string) $url);
+ if ($url === '') {
+ return '';
+ }
+
+ return untrailingslashit(esc_url_raw($url));
+ }
+
+ private static function get_options() {
+ $stored = get_option(self::OPTION_KEY, array());
+ $stored = is_array($stored) ? $stored : array();
+
+ return wp_parse_args($stored, array(
+ 'statusapi_base_url' => '',
+ 'attack_api_key' => '',
+ 'attack_source' => 'WordPress',
+ ));
+ }
+
+ private static function get_state() {
+ $stored = get_option(self::STATE_OPTION_KEY, array());
+ $stored = is_array($stored) ? $stored : array();
+
+ return wp_parse_args($stored, array(
+ 'last_check_at' => 0,
+ 'last_http_code' => 0,
+ 'last_success_at' => 0,
+ 'last_success_code' => 0,
+ 'last_status' => 'unknown',
+ 'last_error' => '',
+ 'last_attack_mode' => null,
+ 'last_memory_high' => false,
+ 'last_player_high' => false,
+ 'messages' => array(),
+ ));
+ }
+
+ private static function save_state($state) {
+ update_option(self::STATE_OPTION_KEY, $state, false);
+ }
+
+ private static function push_message(&$state, $level, $title, $message) {
+ if (!isset($state['messages']) || !is_array($state['messages'])) {
+ $state['messages'] = array();
+ }
+
+ array_unshift($state['messages'], array(
+ 'ts' => time(),
+ 'level' => $level,
+ 'title' => $title,
+ 'message' => $message,
+ ));
+
+ if (count($state['messages']) > 60) {
+ $state['messages'] = array_slice($state['messages'], 0, 60);
+ }
+
+ self::push_history_entry($level, $title, $message);
+ }
+
+ private static function get_history() {
+ $stored = get_option(self::HISTORY_OPTION_KEY, array());
+ return is_array($stored) ? $stored : array();
+ }
+
+ private static function save_history($history) {
+ update_option(self::HISTORY_OPTION_KEY, is_array($history) ? $history : array(), false);
+ }
+
+ private static function push_history_entry($level, $title, $message) {
+ $history = self::get_history();
+ array_unshift($history, array(
+ 'ts' => time(),
+ 'level' => (string) $level,
+ 'title' => (string) $title,
+ 'message' => (string) $message,
+ ));
+
+ if (count($history) > 1000) {
+ $history = array_slice($history, 0, 1000);
+ }
+
+ self::save_history($history);
+ }
+
+ private static function build_messages_markup($state) {
+ $messages = isset($state['messages']) && is_array($state['messages']) ? $state['messages'] : array();
+ if (empty($messages)) {
+ return '
Noch keine MeldungenSobald Statuswechsel oder Attack-Events erkannt werden, erscheinen sie hier.
';
+ }
+
+ $html = '';
+ foreach (array_slice($messages, 0, 20) as $msg) {
+ $level = isset($msg['level']) ? (string) $msg['level'] : 'info';
+ $title = isset($msg['title']) ? (string) $msg['title'] : 'Meldung';
+ $text = isset($msg['message']) ? (string) $msg['message'] : '';
+ $ts = isset($msg['ts']) ? (int) $msg['ts'] : 0;
+
+ $html .= '
';
+ $html .= '' . esc_html($title) . '' . esc_html(self::format_timestamp($ts)) . '
';
+ $html .= '' . esc_html($text) . '
';
+ $html .= '';
+ }
+
+ return $html;
+ }
+
+ public static function render_page() {
+ if (!current_user_can('manage_options')) {
+ return;
+ }
+
+ $options = self::get_options();
+ $statusapi_base_url = $options['statusapi_base_url'];
+ $attack_api_key = $options['attack_api_key'];
+ $attack_source = $options['attack_source'];
+ $live_status = self::probe_statusapi($statusapi_base_url);
+ $state = self::get_state();
+ $notice = self::build_notice();
+ $status_endpoint = $statusapi_base_url !== '' ? $statusapi_base_url : 'http://dein-proxy:9191';
+ $attack_endpoint = $status_endpoint . '/network/attack';
+ $status_json_snippet = "Status JSON: {$status_endpoint}";
+ $network_guard_snippet = "networkinfo.attack.api_key={$attack_api_key}\nnetworkinfo.attack.source={$attack_source}";
+ $curl_examples = self::build_curl_examples($status_endpoint, $attack_endpoint, $attack_api_key, $attack_source);
+ $refresh_nonce = wp_create_nonce('statusapi_backend_helper_refresh');
+
+ echo '
';
+ self::render_styles();
+ self::render_scripts($refresh_nonce);
+ echo '
';
+ echo '
';
+ echo '
Proxy Control Surface';
+ echo '
StatusPulse
';
+ echo '
Reduzierte Admin-Seite für Proxy-URL, Attack-Key und direkte Tests gegen dein StatusAPI-Plugin.
';
+ echo '
';
+ echo '
';
+ echo 'Live Refresh';
+ echo 'alle 15s';
+ echo 'wartet auf erstes Update';
+ echo '
';
+ echo '
';
+
+ if ($notice !== null) {
+ printf(
+ '
',
+ esc_attr($notice['type']),
+ esc_html($notice['message'])
+ );
+ }
+
+ echo '
';
+ self::render_status_card_named(
+ 'statusapi-card-proxy',
+ 'Proxy Status',
+ $live_status['label'],
+ $live_status['meta'],
+ $live_status['status']
+ );
+ self::render_status_card_named(
+ 'statusapi-card-http',
+ 'Letzter HTTP-Code',
+ $live_status['http_code'] > 0 ? (string) $live_status['http_code'] : 'n/a',
+ $live_status['checked_at_human'],
+ $live_status['status']
+ );
+ self::render_status_card_named(
+ 'statusapi-card-success',
+ 'Letzte erfolgreiche Prüfung',
+ self::format_timestamp($state['last_success_at']),
+ $state['last_success_code'] > 0 ? 'HTTP ' . $state['last_success_code'] : 'Noch kein erfolgreicher Check gespeichert',
+ $state['last_success_at'] > 0 ? 'ok' : 'unknown'
+ );
+ self::render_status_card_named(
+ 'statusapi-card-lastcheck',
+ 'Zuletzt geprüft',
+ self::format_timestamp($state['last_check_at']),
+ $state['last_error'] !== '' ? $state['last_error'] : 'Keine Fehler gespeichert',
+ $state['last_status']
+ );
+ echo '
';
+
+ echo '
';
+ self::render_metric_card('statusapi-metric-online', 'Spieler online', $live_status['metrics']['online_players'], $live_status['metrics']['online_meta']);
+ self::render_metric_card('statusapi-metric-max', 'Max. Spieler', $live_status['metrics']['max_players'], $live_status['metrics']['max_meta']);
+ self::render_metric_card('statusapi-metric-uptime', 'Uptime', $live_status['metrics']['uptime'], $live_status['metrics']['uptime_meta']);
+ self::render_metric_card('statusapi-metric-memory', 'RAM Nutzung', $live_status['metrics']['memory'], $live_status['metrics']['memory_meta']);
+ echo '
';
+
+ echo '
';
+ echo '
';
+
+ echo '
';
+ echo '
Snippets
Direkt für deine Proxy-Konfiguration und externe Tests vorbereitet.
';
+ self::render_snippet_card('Für network-guard.properties', $network_guard_snippet, 4);
+ self::render_snippet_card('Wichtiger Endpunkt', $status_json_snippet . "\nAttack POST: {$attack_endpoint}", 4);
+ self::render_snippet_card('Test-Requests', $curl_examples, 8);
+ echo '
';
+
+ echo '
';
+ echo '
Aktionen
Teste direkt aus dem Backend, ob dein Proxy erreichbar ist und Attack-Meldungen akzeptiert.
';
+ echo '
';
+ echo '
';
+ echo '
';
+ echo '
';
+ }
+
+ public static function render_messages_page() {
+ if (!current_user_can('manage_options')) {
+ return;
+ }
+
+ $state = self::get_state();
+ $notice = self::build_notice();
+ $messages_markup = self::build_messages_markup($state);
+ $refresh_nonce = wp_create_nonce('statusapi_backend_helper_refresh');
+
+ echo '
';
+ self::render_styles();
+ self::render_scripts($refresh_nonce);
+ echo '
StatusPulse Meldungen
';
+ echo '
Dedizierte Seite für alle laufenden Status-, Attack- und Warnmeldungen.
';
+
+ if ($notice !== null) {
+ printf(
+ '
',
+ esc_attr($notice['type']),
+ esc_html($notice['message'])
+ );
+ }
+
+ self::render_messages_center($messages_markup, self::MESSAGES_SLUG);
+ echo '
';
+ }
+
+ public static function render_list_page() {
+ if (!current_user_can('manage_options')) {
+ return;
+ }
+
+ $options = self::get_options();
+ $notice = self::build_notice();
+ $attacker_data = self::fetch_attacker_entries($options['statusapi_base_url']);
+ $rows = isset($attacker_data['entries']) && is_array($attacker_data['entries']) ? $attacker_data['entries'] : array();
+ $list_error = isset($attacker_data['error']) ? (string) $attacker_data['error'] : '';
+
+ echo '
';
+ self::render_styles();
+
+ echo '
StatusPulse Angriffsversuche
';
+ echo '
Zeigt nur Spielername und UUID von erkannten Angriffsversuchen (inkl. Datum/Uhrzeit).
';
+
+ if ($notice !== null) {
+ printf(
+ '
',
+ esc_attr($notice['type']),
+ esc_html($notice['message'])
+ );
+ }
+
+ echo '
';
+ echo '
Angriffs-Ereignisse
Neueste Einträge oben.
';
+
+ if ($list_error !== '') {
+ echo '
' . esc_html($list_error) . '
';
+ }
+
+ if (empty($rows)) {
+ echo '
Keine Angriffs-Einträge mit Spielername/UUID vorhanden.
';
+ } else {
+ echo '
';
+ echo '| Datum / Uhrzeit | Spielername | IP | UUID |
';
+ echo '';
+ foreach ($rows as $row) {
+ $date = isset($row['datetime']) ? (string) $row['datetime'] : '';
+ $player = isset($row['player']) ? (string) $row['player'] : '-';
+ $ip = isset($row['ip']) ? (string) $row['ip'] : '-';
+ $uuid = isset($row['uuid']) ? (string) $row['uuid'] : '-';
+
+ echo '';
+ echo '| ' . esc_html($date) . ' | ';
+ echo '' . esc_html($player) . ' | ';
+ echo '' . esc_html($ip) . ' | ';
+ echo '' . esc_html($uuid) . ' | ';
+ echo '
';
+ }
+ echo '
';
+ }
+
+ echo '
';
+ echo '
';
+ }
+
+ private static function render_messages_center($messages_markup, $redirect_page) {
+ echo '
';
+ echo '
Meldungs-Center
Hier siehst du laufend erkannte Status-, Attack- und Warnmeldungen aus deinem Proxy.
';
+ echo '
';
+ echo '';
+ echo '';
+ echo '';
+ echo '';
+ echo '';
+ echo '';
+ echo '
';
+ echo '
';
+ echo '
';
+ }
+
+ private static function render_input_field($label, $name, $value, $description) {
+ echo '
';
+ }
+
+ private static function render_snippet_card($title, $content, $rows) {
+ echo '
';
+ echo '
';
+ echo '
' . esc_html($title) . '
';
+ echo '';
+ echo '';
+ printf(
+ '
',
+ (int) $rows,
+ esc_attr('statusapi-copy-' . md5($title . $content)),
+ esc_textarea($content)
+ );
+ echo '
';
+ }
+
+ private static function render_status_card($title, $value, $meta, $status) {
+ self::render_status_card_named('', $title, $value, $meta, $status);
+ }
+
+ private static function render_status_card_named($id, $title, $value, $meta, $status) {
+ $status_class = 'statusapi-state-unknown';
+ if ($status === 'ok') {
+ $status_class = 'statusapi-state-ok';
+ } elseif ($status === 'error') {
+ $status_class = 'statusapi-state-error';
+ }
+
+ $id_attr = $id !== '' ? ' id="' . esc_attr($id) . '"' : '';
+ echo '
';
+ echo '' . esc_html($title) . '';
+ echo '' . esc_html($value) . '';
+ echo '' . esc_html($meta) . '';
+ echo '
';
+ }
+
+ private static function render_metric_card($id, $title, $value, $meta) {
+ echo '
';
+ echo '' . esc_html($title) . '';
+ echo '' . esc_html($value) . '';
+ echo '' . esc_html($meta) . '';
+ echo '
';
+ }
+
+ private static function build_curl_examples($status_endpoint, $attack_endpoint, $attack_api_key, $attack_source) {
+ $payload = wp_json_encode(array(
+ 'event' => 'detected',
+ 'source' => $attack_source,
+ 'connectionsPerSecond' => 250,
+ 'ipAddressesBlocked' => 12,
+ 'connectionsBlocked' => 1800,
+ ));
+
+ return "GET {$status_endpoint}\n\nPOST {$attack_endpoint}\nHeader: X-API-Key: {$attack_api_key}\nBody: {$payload}";
+ }
+
+ private static function probe_statusapi($base_url) {
+ $state = self::get_state();
+ $previous_status = isset($state['last_status']) ? (string) $state['last_status'] : 'unknown';
+ $previous_attack_mode = array_key_exists('last_attack_mode', $state) ? $state['last_attack_mode'] : null;
+ $previous_memory_high = !empty($state['last_memory_high']);
+ $previous_player_high = !empty($state['last_player_high']);
+
+ if ($base_url === '') {
+ return array(
+ 'status' => 'unknown',
+ 'label' => 'Nicht konfiguriert',
+ 'meta' => 'Bitte zuerst eine StatusAPI Basis-URL eintragen.',
+ 'http_code' => 0,
+ 'checked_at_human' => self::format_timestamp($state['last_check_at']),
+ 'metrics' => self::empty_metrics(),
+ );
+ }
+
+ $response = wp_remote_get($base_url, array('timeout' => 5));
+ $now = time();
+
+ if (is_wp_error($response)) {
+ $state['last_check_at'] = $now;
+ $state['last_http_code'] = 0;
+ $state['last_status'] = 'error';
+ $state['last_error'] = $response->get_error_message();
+ if ($previous_status !== 'error') {
+ self::push_message($state, 'error', 'Proxy offline', 'StatusAPI ist nicht erreichbar: ' . $response->get_error_message());
+ }
+ self::save_state($state);
+
+ return array(
+ 'status' => 'error',
+ 'label' => 'Offline',
+ 'meta' => $response->get_error_message(),
+ 'http_code' => 0,
+ 'checked_at_human' => self::format_timestamp($now),
+ 'metrics' => self::empty_metrics(),
+ );
+ }
+
+ $code = (int) wp_remote_retrieve_response_code($response);
+ $ok = $code >= 200 && $code < 300;
+ $body = wp_remote_retrieve_body($response);
+ $decoded = json_decode($body, true);
+ $state['last_check_at'] = $now;
+ $state['last_http_code'] = $code;
+ $state['last_status'] = $ok ? 'ok' : 'error';
+ $state['last_error'] = $ok ? '' : 'HTTP ' . $code;
+
+ if ($ok && $previous_status === 'error') {
+ self::push_message($state, 'success', 'Proxy wieder online', 'StatusAPI antwortet wieder erfolgreich.');
+ }
+ if (!$ok && $previous_status !== 'error') {
+ self::push_message($state, 'error', 'Proxy-Fehler', 'StatusAPI antwortet mit HTTP ' . $code . '.');
+ }
+
+ if ($ok && is_array($decoded)) {
+ $network = isset($decoded['network']) && is_array($decoded['network']) ? $decoded['network'] : array();
+ $antibot = isset($decoded['antibot']) && is_array($decoded['antibot']) ? $decoded['antibot'] : array();
+
+ if (array_key_exists('attack_mode', $antibot)) {
+ $attack_mode = (bool) $antibot['attack_mode'];
+ if ($previous_attack_mode !== null && $attack_mode !== (bool) $previous_attack_mode) {
+ if ($attack_mode) {
+ $cps = isset($antibot['last_cps']) ? $antibot['last_cps'] : 'n/a';
+ self::push_message($state, 'warning', 'Attack erkannt', 'AntiBot hat den Attack-Mode aktiviert (CPS: ' . $cps . ').');
+ } else {
+ self::push_message($state, 'success', 'Attack beendet', 'AntiBot hat den Attack-Mode beendet.');
+ }
+ }
+ $state['last_attack_mode'] = $attack_mode;
+ }
+
+ $memory_percent = null;
+ if (isset($network['memory']) && is_array($network['memory']) && isset($network['memory']['usage_percent'])) {
+ $memory_percent = (int) $network['memory']['usage_percent'];
+ }
+ $is_memory_high = $memory_percent !== null && $memory_percent >= 90;
+ if ($is_memory_high && !$previous_memory_high) {
+ self::push_message($state, 'warning', 'RAM-Warnung', 'RAM-Auslastung liegt bei ' . $memory_percent . '%.');
+ }
+ if (!$is_memory_high && $previous_memory_high) {
+ self::push_message($state, 'success', 'RAM normalisiert', 'RAM-Auslastung ist wieder unter 90%.');
+ }
+ $state['last_memory_high'] = $is_memory_high;
+
+ $occupancy = null;
+ if (isset($network['players']) && is_array($network['players']) && isset($network['players']['occupancy_percent'])) {
+ $occupancy = (int) $network['players']['occupancy_percent'];
+ }
+ $is_player_high = $occupancy !== null && $occupancy >= 95;
+ if ($is_player_high && !$previous_player_high) {
+ self::push_message($state, 'warning', 'Spieler-Auslastung hoch', 'Spieler-Auslastung liegt bei ' . $occupancy . '%.');
+ }
+ if (!$is_player_high && $previous_player_high) {
+ self::push_message($state, 'success', 'Spieler-Auslastung normalisiert', 'Spieler-Auslastung ist wieder unter 95%.');
+ }
+ $state['last_player_high'] = $is_player_high;
+ }
+
+ if ($ok) {
+ $state['last_success_at'] = $now;
+ $state['last_success_code'] = $code;
+ }
+ self::save_state($state);
+
+ return array(
+ 'status' => $ok ? 'ok' : 'error',
+ 'label' => $ok ? 'Online' : 'Fehler',
+ 'meta' => $ok ? 'Antwort von ' . untrailingslashit($base_url) : 'HTTP ' . $code,
+ 'http_code' => $code,
+ 'checked_at_human' => self::format_timestamp($now),
+ 'metrics' => $ok ? self::extract_metrics($decoded) : self::empty_metrics(),
+ );
+ }
+
+ private static function extract_metrics($decoded) {
+ if (!is_array($decoded)) {
+ return self::empty_metrics();
+ }
+
+ $network = isset($decoded['network']) && is_array($decoded['network']) ? $decoded['network'] : array();
+ $players = isset($network['players']) && is_array($network['players']) ? $network['players'] : array();
+ $memory = isset($network['memory']) && is_array($network['memory']) ? $network['memory'] : array();
+
+ $online_players = self::array_value($players, 'online', self::count_players_fallback($decoded));
+ $max_players = self::array_value($players, 'max', self::array_value($decoded, 'max_players', 'n/a'));
+ $occupancy = self::array_value($players, 'occupancy_percent', null);
+ $uptime_human = self::array_value($network, 'uptime_human', 'n/a');
+ $uptime_seconds = self::array_value($network, 'uptime_seconds', null);
+ $memory_percent = self::array_value($memory, 'usage_percent', 'n/a');
+ $memory_used = self::array_value($memory, 'used_mb', 'n/a');
+ $memory_max = self::array_value($memory, 'max_mb', 'n/a');
+
+ return array(
+ 'online_players' => (string) $online_players,
+ 'online_meta' => $occupancy !== null ? 'Auslastung ' . $occupancy . '%' : 'Aktuell verbundene Spieler',
+ 'max_players' => (string) $max_players,
+ 'max_meta' => 'Player-Limit laut Proxy',
+ 'uptime' => (string) $uptime_human,
+ 'uptime_meta' => $uptime_seconds !== null ? ((string) $uptime_seconds) . ' Sekunden' : 'Keine Uptime-Daten',
+ 'memory' => ((string) $memory_percent) . '%',
+ 'memory_meta' => $memory_used . ' MB von ' . $memory_max . ' MB',
+ );
+ }
+
+ private static function empty_metrics() {
+ return array(
+ 'online_players' => 'n/a',
+ 'online_meta' => 'Keine Live-Daten',
+ 'max_players' => 'n/a',
+ 'max_meta' => 'Keine Live-Daten',
+ 'uptime' => 'n/a',
+ 'uptime_meta' => 'Keine Live-Daten',
+ 'memory' => 'n/a',
+ 'memory_meta' => 'Keine Live-Daten',
+ );
+ }
+
+ private static function array_value($array, $key, $fallback) {
+ if (!is_array($array) || !array_key_exists($key, $array)) {
+ return $fallback;
+ }
+ return $array[$key];
+ }
+
+ private static function count_players_fallback($decoded) {
+ if (isset($decoded['players']) && is_array($decoded['players'])) {
+ return count($decoded['players']);
+ }
+ return 'n/a';
+ }
+
+ private static function format_timestamp($timestamp) {
+ $timestamp = (int) $timestamp;
+ if ($timestamp <= 0) {
+ return 'Noch kein Wert';
+ }
+
+ return wp_date('d.m.Y H:i:s', $timestamp);
+ }
+
+ private static function build_notice() {
+ if (!isset($_GET['statusapi_notice'])) {
+ return null;
+ }
+
+ $code = sanitize_text_field(wp_unslash($_GET['statusapi_notice']));
+ if ($code === 'connection_ok') {
+ return array('type' => 'success', 'message' => 'StatusAPI ist erreichbar.');
+ }
+ if ($code === 'attack_ok') {
+ return array('type' => 'success', 'message' => 'Test Attack-Meldung wurde erfolgreich an den Proxy gesendet.');
+ }
+ if ($code === 'missing_url') {
+ return array('type' => 'error', 'message' => 'Bitte zuerst die StatusAPI Basis-URL eintragen.');
+ }
+ if ($code === 'missing_attack_key') {
+ return array('type' => 'error', 'message' => 'Bitte zuerst einen Attack API-Key speichern.');
+ }
+ if ($code === 'connection_failed') {
+ return array('type' => 'error', 'message' => 'Verbindungstest fehlgeschlagen. Bitte Basis-URL und Erreichbarkeit prüfen.');
+ }
+ if ($code === 'attack_failed') {
+ return array('type' => 'error', 'message' => 'Test Attack-Meldung konnte nicht gesendet werden. API-Key und Proxy-Endpunkt prüfen.');
+ }
+ if ($code === 'messages_cleared') {
+ return array('type' => 'success', 'message' => 'Meldungen wurden erfolgreich geleert.');
+ }
+
+ return null;
+ }
+
+ public static function handle_test_action() {
+ if (!current_user_can('manage_options')) {
+ wp_die('Nicht erlaubt.');
+ }
+
+ check_admin_referer('statusapi_backend_helper_test');
+
+ $options = self::get_options();
+ $base_url = $options['statusapi_base_url'];
+ if ($base_url === '') {
+ self::redirect_with_notice('missing_url');
+ }
+
+ if (isset($_POST['test_connection'])) {
+ $ok = self::request_ok($base_url);
+ $state = self::get_state();
+ self::push_message(
+ $state,
+ $ok ? 'success' : 'error',
+ $ok ? 'Verbindungstest erfolgreich' : 'Verbindungstest fehlgeschlagen',
+ $ok ? 'Manueller Verbindungstest im WordPress-Backend war erfolgreich.' : 'Manueller Verbindungstest im WordPress-Backend war nicht erfolgreich.'
+ );
+ self::save_state($state);
+ self::redirect_with_notice($ok ? 'connection_ok' : 'connection_failed');
+ }
+
+ if (isset($_POST['test_attack'])) {
+ if ($options['attack_api_key'] === '') {
+ self::redirect_with_notice('missing_attack_key');
+ }
+
+ $payload = array(
+ 'event' => 'detected',
+ 'source' => $options['attack_source'] !== '' ? $options['attack_source'] : 'WordPress',
+ 'connectionsPerSecond' => 250,
+ 'ipAddressesBlocked' => 12,
+ 'connectionsBlocked' => 1800,
+ );
+
+ $response = wp_remote_post($base_url . '/network/attack', array(
+ 'timeout' => 8,
+ 'headers' => array(
+ 'Content-Type' => 'application/json; charset=utf-8',
+ 'X-API-Key' => $options['attack_api_key'],
+ ),
+ 'body' => wp_json_encode($payload),
+ ));
+
+ if (is_wp_error($response)) {
+ $state = self::get_state();
+ $state['last_check_at'] = time();
+ $state['last_http_code'] = 0;
+ $state['last_status'] = 'error';
+ $state['last_error'] = $response->get_error_message();
+ self::push_message($state, 'error', 'Test-Attack fehlgeschlagen', 'Konnte keine Test-Meldung senden: ' . $response->get_error_message());
+ self::save_state($state);
+ self::redirect_with_notice('attack_failed');
+ }
+
+ $code = (int) wp_remote_retrieve_response_code($response);
+ $state = self::get_state();
+ $state['last_check_at'] = time();
+ $state['last_http_code'] = $code;
+ $state['last_status'] = ($code >= 200 && $code < 300) ? 'ok' : 'error';
+ $state['last_error'] = ($code >= 200 && $code < 300) ? '' : 'HTTP ' . $code;
+ if ($code >= 200 && $code < 300) {
+ $state['last_success_at'] = $state['last_check_at'];
+ $state['last_success_code'] = $code;
+ self::push_message($state, 'success', 'Test-Attack gesendet', 'Die Test-Attack-Meldung wurde erfolgreich an den Proxy gesendet.');
+ } else {
+ self::push_message($state, 'error', 'Test-Attack fehlgeschlagen', 'Der Proxy antwortete mit HTTP ' . $code . '.');
+ }
+ self::save_state($state);
+ self::redirect_with_notice($code >= 200 && $code < 300 ? 'attack_ok' : 'attack_failed');
+ }
+
+ self::redirect_with_notice('connection_failed');
+ }
+
+ public static function handle_clear_messages() {
+ if (!current_user_can('manage_options')) {
+ wp_die('Nicht erlaubt.');
+ }
+
+ check_admin_referer('statusapi_backend_helper_clear_messages');
+
+ $state = self::get_state();
+ $redirect_page = isset($_POST['redirect_page']) ? sanitize_text_field(wp_unslash($_POST['redirect_page'])) : self::MENU_SLUG;
+
+ if ($redirect_page === self::LIST_SLUG) {
+ self::save_history(array());
+ } else {
+ $state['messages'] = array();
+ }
+ self::save_state($state);
+
+ if ($redirect_page !== self::MESSAGES_SLUG) {
+ if ($redirect_page !== self::LIST_SLUG) {
+ $redirect_page = self::MENU_SLUG;
+ }
+ }
+
+ self::redirect_with_notice('messages_cleared', $redirect_page);
+ }
+
+ public static function handle_ajax_refresh() {
+ if (!current_user_can('manage_options')) {
+ wp_send_json_error(array('message' => 'forbidden'), 403);
+ }
+
+ check_ajax_referer('statusapi_backend_helper_refresh');
+
+ $options = self::get_options();
+ $live_status = self::probe_statusapi($options['statusapi_base_url']);
+ $state = self::get_state();
+
+ wp_send_json_success(array(
+ 'refresh_state' => 'Aktualisiert ' . self::format_timestamp($state['last_check_at']),
+ 'messages_html' => self::build_messages_markup($state),
+ 'status_cards' => array(
+ 'proxy' => array(
+ 'value' => $live_status['label'],
+ 'meta' => $live_status['meta'],
+ 'status' => $live_status['status'],
+ ),
+ 'http' => array(
+ 'value' => $live_status['http_code'] > 0 ? (string) $live_status['http_code'] : 'n/a',
+ 'meta' => $live_status['checked_at_human'],
+ 'status' => $live_status['status'],
+ ),
+ 'success' => array(
+ 'value' => self::format_timestamp($state['last_success_at']),
+ 'meta' => $state['last_success_code'] > 0 ? 'HTTP ' . $state['last_success_code'] : 'Noch kein erfolgreicher Check gespeichert',
+ 'status' => $state['last_success_at'] > 0 ? 'ok' : 'unknown',
+ ),
+ 'lastcheck' => array(
+ 'value' => self::format_timestamp($state['last_check_at']),
+ 'meta' => $state['last_error'] !== '' ? $state['last_error'] : 'Keine Fehler gespeichert',
+ 'status' => $state['last_status'],
+ ),
+ ),
+ 'metric_cards' => array(
+ 'online' => array(
+ 'value' => $live_status['metrics']['online_players'],
+ 'meta' => $live_status['metrics']['online_meta'],
+ ),
+ 'max' => array(
+ 'value' => $live_status['metrics']['max_players'],
+ 'meta' => $live_status['metrics']['max_meta'],
+ ),
+ 'uptime' => array(
+ 'value' => $live_status['metrics']['uptime'],
+ 'meta' => $live_status['metrics']['uptime_meta'],
+ ),
+ 'memory' => array(
+ 'value' => $live_status['metrics']['memory'],
+ 'meta' => $live_status['metrics']['memory_meta'],
+ ),
+ ),
+ ));
+ }
+
+ private static function request_ok($url) {
+ $response = wp_remote_get($url, array('timeout' => 8));
+ if (is_wp_error($response)) {
+ return false;
+ }
+
+ $code = (int) wp_remote_retrieve_response_code($response);
+ return $code >= 200 && $code < 300;
+ }
+
+ private static function fetch_attacker_entries($base_url) {
+ if ($base_url === '') {
+ return array(
+ 'entries' => array(),
+ 'error' => 'StatusAPI Basis-URL ist nicht konfiguriert.',
+ );
+ }
+
+ $endpoint = untrailingslashit($base_url) . '/antibot/security-log';
+ $response = wp_remote_get($endpoint, array('timeout' => 8));
+ if (is_wp_error($response)) {
+ return array(
+ 'entries' => array(),
+ 'error' => 'Konnte Angriffsdaten nicht laden: ' . $response->get_error_message(),
+ );
+ }
+
+ $code = (int) wp_remote_retrieve_response_code($response);
+ if ($code < 200 || $code >= 300) {
+ return array(
+ 'entries' => array(),
+ 'error' => 'StatusAPI lieferte HTTP ' . $code . ' für /antibot/security-log.',
+ );
+ }
+
+ $body = wp_remote_retrieve_body($response);
+ $decoded = json_decode($body, true);
+ if (!is_array($decoded) || !isset($decoded['events']) || !is_array($decoded['events'])) {
+ return array(
+ 'entries' => array(),
+ 'error' => 'Antwort von /antibot/security-log ist ungültig.',
+ );
+ }
+
+ return array(
+ 'entries' => $decoded['events'],
+ 'error' => '',
+ );
+ }
+
+ private static function redirect_with_notice($code, $page = self::MENU_SLUG) {
+ $target_page = self::MENU_SLUG;
+ if ($page === self::MESSAGES_SLUG) {
+ $target_page = self::MESSAGES_SLUG;
+ } elseif ($page === self::LIST_SLUG) {
+ $target_page = self::LIST_SLUG;
+ }
+ $url = add_query_arg(
+ array(
+ 'page' => $target_page,
+ 'statusapi_notice' => $code,
+ ),
+ admin_url('admin.php')
+ );
+
+ wp_safe_redirect($url);
+ exit;
+ }
+
+ private static function render_styles() {
+ echo '';
+ }
+
+ private static function render_scripts($refresh_nonce) {
+ echo '';
+ }
+ }
+
+ StatusAPI_Backend_Helper::boot();
}
\ No newline at end of file