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 '
'; - - echo '
'; - echo '

StatusPulse Monitor

'; - echo '' . esc_html($status_text) . ''; - echo '
'; - - echo '
'; - - echo '
'; - - echo '
'; - echo 'Status'; - echo '
' . esc_html($live_status['label']) . '
'; - echo '
' . esc_html($live_status['meta']) . '
'; - echo '
'; - - echo '
'; - echo 'Letzte Meldung'; - if ($last_message !== null) { - $msg_title = isset($last_message['title']) ? (string) $last_message['title'] : 'Meldung'; - $msg_text = isset($last_message['message']) ? (string) $last_message['message'] : ''; - echo '
' . esc_html($msg_title) . '
'; - echo '
' . esc_html(substr($msg_text, 0, 50)) . '
'; - } else { - echo '
Keine Meldungen
'; - } - echo '
'; - - echo '
'; - - echo '
'; - echo '
HTTP
' . esc_html($live_status['http_code'] > 0 ? (string) $live_status['http_code'] : '—') . '
'; - echo '
Spieler
' . esc_html($live_status['metrics']['online_players']) . '
'; - echo '
RAM
' . esc_html($live_status['metrics']['memory']) . '
'; - echo '
Check
' . esc_html(self::format_timestamp($state['last_check_at'])) . '
'; - echo '
'; - - echo ''; - - echo '
'; - 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 Meldungen

    Sobald 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( - '

    %2$s

    ', - 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 '

    Konfiguration

    Die Werte werden lokal in WordPress gespeichert und als Snippets für deine Proxy-Dateien angezeigt.

    '; - echo '
    '; - settings_fields('statusapi_backend_helper_group'); - echo '
    '; - self::render_input_field('StatusAPI Basis-URL', self::OPTION_KEY . '[statusapi_base_url]', $statusapi_base_url, 'z. B. http://127.0.0.1:9191'); - self::render_input_field('Attack API-Key', self::OPTION_KEY . '[attack_api_key]', $attack_api_key, 'Entspricht networkinfo.attack.api_key in network-guard.properties.'); - self::render_input_field('Attack Source', self::OPTION_KEY . '[attack_source]', $attack_source, 'Wird in Testmeldungen als Quelle angezeigt.'); - echo '
    '; - echo '
    '; - submit_button('Einstellungen speichern', 'primary', 'submit', false); - 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 '
    '; - wp_nonce_field('statusapi_backend_helper_test'); - echo ''; - echo '
    '; - submit_button('StatusAPI Verbindung prüfen', 'secondary', 'test_connection', false); - submit_button('Test Attack-Meldung senden', 'secondary', 'test_attack', false); - echo '
    '; - echo '
    '; - echo '
    '; - echo '
    Gespeicherte Basis-URL' . esc_html($statusapi_base_url !== '' ? $statusapi_base_url : 'nicht gesetzt') . '
    '; - echo '
    Attack-Key' . esc_html($attack_api_key !== '' ? 'gesetzt' : 'leer') . '
    '; - echo '
    Quelle' . esc_html($attack_source !== '' ? $attack_source : 'WordPress') . '
    '; - 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( - '

    %2$s

    ', - 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( - '

    %2$s

    ', - 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 ''; - 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 ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
    Datum / UhrzeitSpielernameIPUUID
    ' . esc_html($date) . '' . esc_html($player) . '' . esc_html($ip) . '' . esc_html($uuid) . '
    '; - } - - 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 '
    '; - wp_nonce_field('statusapi_backend_helper_clear_messages'); - 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 '
    '; + + echo '
    '; + echo '

    StatusPulse Monitor

    '; + echo '' . esc_html($status_text) . ''; + echo '
    '; + + echo '
    '; + + echo '
    '; + + echo '
    '; + echo 'Status'; + echo '
    ' . esc_html($live_status['label']) . '
    '; + echo '
    ' . esc_html($live_status['meta']) . '
    '; + echo '
    '; + + echo '
    '; + echo 'Letzte Meldung'; + if ($last_message !== null) { + $msg_title = isset($last_message['title']) ? (string) $last_message['title'] : 'Meldung'; + $msg_text = isset($last_message['message']) ? (string) $last_message['message'] : ''; + echo '
    ' . esc_html($msg_title) . '
    '; + echo '
    ' . esc_html(substr($msg_text, 0, 50)) . '
    '; + } else { + echo '
    Keine Meldungen
    '; + } + echo '
    '; + + echo '
    '; + + echo '
    '; + echo '
    HTTP
    ' . esc_html($live_status['http_code'] > 0 ? (string) $live_status['http_code'] : '—') . '
    '; + echo '
    Spieler
    ' . esc_html($live_status['metrics']['online_players']) . '
    '; + echo '
    RAM
    ' . esc_html($live_status['metrics']['memory']) . '
    '; + echo '
    Check
    ' . esc_html(self::format_timestamp($state['last_check_at'])) . '
    '; + echo '
    '; + + echo ''; + + echo '
    '; + 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 Meldungen

    Sobald 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( + '

    %2$s

    ', + 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 '

    Konfiguration

    Die Werte werden lokal in WordPress gespeichert und als Snippets für deine Proxy-Dateien angezeigt.

    '; + echo '
    '; + settings_fields('statusapi_backend_helper_group'); + echo '
    '; + self::render_input_field('StatusAPI Basis-URL', self::OPTION_KEY . '[statusapi_base_url]', $statusapi_base_url, 'z. B. http://127.0.0.1:9191'); + self::render_input_field('Attack API-Key', self::OPTION_KEY . '[attack_api_key]', $attack_api_key, 'Entspricht networkinfo.attack.api_key in network-guard.properties.'); + self::render_input_field('Attack Source', self::OPTION_KEY . '[attack_source]', $attack_source, 'Wird in Testmeldungen als Quelle angezeigt.'); + echo '
    '; + echo '
    '; + submit_button('Einstellungen speichern', 'primary', 'submit', false); + 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 '
    '; + wp_nonce_field('statusapi_backend_helper_test'); + echo ''; + echo '
    '; + submit_button('StatusAPI Verbindung prüfen', 'secondary', 'test_connection', false); + submit_button('Test Attack-Meldung senden', 'secondary', 'test_attack', false); + echo '
    '; + echo '
    '; + echo '
    '; + echo '
    Gespeicherte Basis-URL' . esc_html($statusapi_base_url !== '' ? $statusapi_base_url : 'nicht gesetzt') . '
    '; + echo '
    Attack-Key' . esc_html($attack_api_key !== '' ? 'gesetzt' : 'leer') . '
    '; + echo '
    Quelle' . esc_html($attack_source !== '' ? $attack_source : 'WordPress') . '
    '; + 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( + '

    %2$s

    ', + 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( + '

    %2$s

    ', + 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 ''; + 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 ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
    Datum / UhrzeitSpielernameIPUUID
    ' . esc_html($date) . '' . esc_html($player) . '' . esc_html($ip) . '' . esc_html($uuid) . '
    '; + } + + 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 '
    '; + wp_nonce_field('statusapi_backend_helper_clear_messages'); + 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