Files
StatusPulse/statuspulse.php
2026-04-03 01:36:24 +02:00

2257 lines
100 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/*
Plugin Name: StatusPulse
Plugin URI:https://git.viper.ipv64.net/M_Viper/StatusPulse
Description: Moderne WordPress-Admin-Seite für StatusAPI-Konfiguration, Attack-API-Key und Proxy-Tests.
Version: 1.0.1
Author: M_Viper
Author URI: https://m-viper.de
Requires at least: 6.8
Tested up to: 6.8
PHP Version: 7.4
License: GPL2
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: statuspulse
Tags: status, statusapi, monitoring, admin, dashboard, proxy, api, tools
Support: [Discord Support](https://discord.com/invite/FdRs4BRd8D)
Support: [Telegram Support](https://t.me/M_Viper04)
*/
if (!defined('ABSPATH')) {
exit;
}
if (!class_exists('StatusAPI_Backend_Helper')) {
class StatusAPI_Backend_Helper {
const OPTION_KEY = 'statusapi_backend_helper_options';
const STATE_OPTION_KEY = 'statusapi_backend_helper_state';
const HISTORY_OPTION_KEY = 'statusapi_backend_helper_history';
const MENU_SLUG = 'statuspulse';
const MESSAGES_SLUG = 'statuspulse-meldungen';
const LIST_SLUG = 'statuspulse-liste';
public static function boot() {
add_action('admin_menu', array(__CLASS__, 'register_menu'));
add_action('admin_init', array(__CLASS__, 'register_settings'));
add_action('wp_dashboard_setup', array(__CLASS__, 'register_dashboard_widget'));
add_action('admin_post_statusapi_backend_helper_test', array(__CLASS__, 'handle_test_action'));
add_action('admin_post_statusapi_backend_helper_clear_messages', array(__CLASS__, 'handle_clear_messages'));
add_action('wp_ajax_statusapi_backend_helper_refresh', array(__CLASS__, 'handle_ajax_refresh'));
}
public static function register_dashboard_widget() {
if (!current_user_can('manage_options')) {
return;
}
wp_add_dashboard_widget(
'statuspulse_dashboard_widget',
'StatusPulse - Live-Status',
array(__CLASS__, 'render_dashboard_widget')
);
}
public static function render_dashboard_widget() {
$options = self::get_options();
$live_status = self::probe_statusapi($options['statusapi_base_url']);
$state = self::get_state();
$messages = isset($state['messages']) && is_array($state['messages']) ? $state['messages'] : array();
$last_message = !empty($messages) ? $messages[0] : null;
$status_color = '#dc3545';
$status_text = 'OFFLINE';
if ($live_status['status'] === 'ok') {
$status_color = '#28a745';
$status_text = 'ONLINE';
} elseif ($live_status['status'] === 'warn') {
$status_color = '#ffc107';
$status_text = 'WARNUNG';
}
echo '<style>
.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); }
}
</style>';
echo '<div class="spw-widget">';
echo '<div class="spw-header">';
echo '<h3 class="spw-title">StatusPulse Monitor</h3>';
echo '<span class="spw-status">' . esc_html($status_text) . '</span>';
echo '</div>';
echo '<div class="spw-body">';
echo '<div class="spw-row">';
echo '<div class="spw-item">';
echo '<span class="spw-item-label">Status</span>';
echo '<div class="spw-item-value">' . esc_html($live_status['label']) . '</div>';
echo '<div class="spw-item-meta">' . esc_html($live_status['meta']) . '</div>';
echo '</div>';
echo '<div class="spw-item">';
echo '<span class="spw-item-label">Letzte Meldung</span>';
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 '<div class="spw-item-value" style="font-size: 14px;">' . esc_html($msg_title) . '</div>';
echo '<div class="spw-item-meta">' . esc_html(substr($msg_text, 0, 50)) . '</div>';
} else {
echo '<div class="spw-item-meta">Keine Meldungen</div>';
}
echo '</div>';
echo '</div>';
echo '<div class="spw-metrics">';
echo '<div class="spw-metric"><div class="spw-metric-label">HTTP</div><div class="spw-metric-value">' . esc_html($live_status['http_code'] > 0 ? (string) $live_status['http_code'] : '—') . '</div></div>';
echo '<div class="spw-metric"><div class="spw-metric-label">Spieler</div><div class="spw-metric-value">' . esc_html($live_status['metrics']['online_players']) . '</div></div>';
echo '<div class="spw-metric"><div class="spw-metric-label">RAM</div><div class="spw-metric-value">' . esc_html($live_status['metrics']['memory']) . '</div></div>';
echo '<div class="spw-metric"><div class="spw-metric-label">Check</div><div class="spw-metric-value" style="font-size: 12px;">' . esc_html(self::format_timestamp($state['last_check_at'])) . '</div></div>';
echo '</div>';
echo '<div class="spw-footer">';
echo '<a class="spw-link" href="' . esc_url(admin_url('admin.php?page=' . self::MENU_SLUG)) . '">Einstellungen</a>';
echo '<a class="spw-link" href="' . esc_url(admin_url('admin.php?page=' . self::MESSAGES_SLUG)) . '">Meldungen</a>';
echo '<a class="spw-link" href="' . esc_url(admin_url('admin.php?page=' . self::LIST_SLUG)) . '">Angriffe</a>';
echo '</div>';
echo '</div>';
echo '</div>';
}
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 '';
}
$url = untrailingslashit(esc_url_raw($url));
// Falls aus anderen Plugins ein Endpoint eingetragen wurde,
// auf reine Basis-URL zurückführen.
$known_suffixes = array(
'/broadcast',
'/network/attack',
'/antibot/security-log',
'/health',
);
foreach ($known_suffixes as $suffix) {
if (substr($url, -strlen($suffix)) === $suffix) {
$url = substr($url, 0, -strlen($suffix));
break;
}
}
return untrailingslashit($url);
}
private static function get_pulsecast_api_url() {
$settings = get_option('pulsecast_settings', array());
if (!is_array($settings)) {
return '';
}
$protocol = isset($settings['api_protocol']) ? strtolower(trim((string) $settings['api_protocol'])) : 'http';
if ($protocol !== 'http' && $protocol !== 'https') {
$protocol = 'http';
}
$host = isset($settings['api_host']) ? trim((string) $settings['api_host']) : '';
if ($host === '') {
return '';
}
$port = isset($settings['api_port']) ? trim((string) $settings['api_port']) : '';
$path = isset($settings['api_path']) ? trim((string) $settings['api_path']) : '/broadcast';
if ($path === '') {
$path = '/broadcast';
}
if (substr($path, 0, 1) !== '/') {
$path = '/' . $path;
}
$url = $protocol . '://' . $host;
if ($port !== '' && !(($protocol === 'http' && $port === '80') || ($protocol === 'https' && $port === '443'))) {
$url .= ':' . $port;
}
// Wichtig: exakt wie PulseCast verwenden (inkl. konfiguriertem Pfad).
// So verhalten wir uns identisch zum funktionierenden Plugin.
$url .= $path;
return esc_url_raw($url);
}
private static function get_candidate_base_urls($base_url) {
$candidates = array();
$add = function($url) use (&$candidates) {
$u = trim((string) $url);
if ($u !== '' && !in_array($u, $candidates, true)) {
$candidates[] = $u;
}
};
$add_base_variants = function($url) use ($add) {
$u = trim((string) $url);
if ($u === '') {
return;
}
// Wenn nur Domain ohne Schema gespeichert wurde: wie in den anderen Plugins auf http normalisieren.
if (strpos($u, 'http://') !== 0 && strpos($u, 'https://') !== 0) {
$u = 'http://' . $u;
}
// Primär: exakt die konfigurierte URL testen.
$root = untrailingslashit($u);
$add($root);
// Direkt danach Port 9191 testen falls NPM nicht korrekt routet, finden
// wir den StatusAPI-Server so viel schneller (vor /health etc.)
$parts = wp_parse_url($u);
if (is_array($parts) && !empty($parts['host']) && empty($parts['port'])) {
$host_9191 = 'http://' . $parts['host'] . ':9191';
$add($host_9191);
$add($host_9191 . '/');
$add($host_9191 . '/health');
}
$add($root . '/');
$add($root . '/health');
// Falls https gesetzt ist, auch http testen (häufiger NPM/SSL Sonderfall).
if (strpos($root, 'https://') === 0) {
$http_root = 'http://' . substr($root, 8);
$add($http_root);
$add($http_root . '/');
$add($http_root . '/health');
}
};
$primary = trim((string) $base_url);
if ($primary !== '') {
$add_base_variants($primary);
}
$pulsecast_url = self::get_pulsecast_api_url();
if ($pulsecast_url !== '') {
$add_base_variants($pulsecast_url);
}
return $candidates;
}
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 '<li data-level="empty" class="statusapi-message-item statusapi-message-empty"><div><strong>Noch keine Meldungen</strong><p>Sobald Statuswechsel oder Attack-Events erkannt werden, erscheinen sie hier.</p></div></li>';
}
$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 .= '<li data-level="' . esc_attr($level) . '" class="statusapi-message-item statusapi-level-' . esc_attr($level) . '">';
$html .= '<div class="statusapi-message-head"><strong>' . esc_html($title) . '</strong><span>' . esc_html(self::format_timestamp($ts)) . '</span></div>';
$html .= '<p>' . esc_html($text) . '</p>';
$html .= '</li>';
}
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();
$candidate_urls_for_ui = self::get_candidate_base_urls($statusapi_base_url);
$status_endpoint = !empty($candidate_urls_for_ui)
? $candidate_urls_for_ui[0]
: '';
$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');
$debug_data = isset($live_status['debug']) && is_array($live_status['debug']) ? $live_status['debug'] : array();
$debug_raw_data = isset($debug_data['raw_data']) ? (string) $debug_data['raw_data'] : '';
$debug_error = isset($debug_data['error']) ? (string) $debug_data['error'] : '';
$debug_url_used = isset($debug_data['url_used']) ? (string) $debug_data['url_used'] : '';
$debug_attempts = isset($debug_data['attempts']) && is_array($debug_data['attempts']) ? $debug_data['attempts'] : array();
echo '<div class="wrap statusapi-admin">';
self::render_styles();
self::render_scripts($refresh_nonce);
echo '<div class="statusapi-hero">';
echo '<div>';
echo '<span class="statusapi-kicker">Proxy Control Surface</span>';
echo '<h1>StatusPulse</h1>';
echo '<p>Reduzierte Admin-Seite für Proxy-URL, Attack-Key und direkte Tests gegen dein StatusAPI-Plugin.</p>';
echo '</div>';
echo '<div class="statusapi-hero-badge">';
echo '<span>Live Refresh</span>';
echo '<strong>alle 15s</strong>';
echo '<em id="statusapi-refresh-state">wartet auf erstes Update</em>';
echo '</div>';
echo '</div>';
if ($notice !== null) {
printf(
'<div class="notice notice-%1$s statusapi-notice"><p>%2$s</p></div>',
esc_attr($notice['type']),
esc_html($notice['message'])
);
}
echo '<div class="statusapi-status-grid">';
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 '</div>';
echo '<div class="statusapi-metric-grid">';
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 '</div>';
echo '<div class="statusapi-card" style="background: #f9f9f9; border: 2px solid #cbd5e1; margin: 20px 0;">';
echo '<div class="statusapi-card-head" style="cursor:pointer;user-select:none;" onclick="var d=document.getElementById(\'statuspulse-debug-body\');d.style.display=(d.style.display===\'none\'?\'block\':\'none\')">';
echo '<h2 style="color:#64748b;font-size:16px;">🔍 DEBUG: API Response <span style="font-size:12px;font-weight:normal;">(klicken zum Ein-/Ausblenden)</span></h2>';
echo '</div>';
echo '<div id="statuspulse-debug-body" style="display:none;">';
echo '<div style="padding: 0 15px 10px 15px; font-size: 12px; color: #333;">';
echo '<div><strong>URL:</strong> <span id="statuspulse-debug-url">' . esc_html($debug_url_used !== '' ? $debug_url_used : $statusapi_base_url) . '</span></div>';
if (!empty($debug_attempts)) {
echo '<div><strong>Versuche:</strong> <span id="statuspulse-debug-attempts">' . esc_html(implode(' | ', $debug_attempts)) . '</span></div>';
} else {
echo '<div><strong>Versuche:</strong> <span id="statuspulse-debug-attempts"></span></div>';
}
if ($debug_error !== '') {
echo '<div id="statuspulse-debug-error" style="color:#c0392b;font-weight:600;margin-top:4px;"><strong>⚠ Fehler:</strong> ' . esc_html($debug_error) . '</div>';
} else {
echo '<div id="statuspulse-debug-error" style="color:#c0392b;font-weight:600;margin-top:4px;display:none;"></div>';
}
echo '</div>';
echo '<pre id="statuspulse-debug-raw" style="background: #222; color: #0f0; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; max-height: 400px; border: 1px solid #444;">';
echo esc_html($debug_raw_data !== '' ? $debug_raw_data : '(keine Response)');
echo '</pre>';
echo '</div>';
echo '</div>';
echo '<div class="statusapi-grid">';
echo '<div class="statusapi-card statusapi-card-form">';
echo '<div class="statusapi-card-head"><h2>Konfiguration</h2><p>Die Werte werden lokal in WordPress gespeichert und als Snippets für deine Proxy-Dateien angezeigt.</p></div>';
echo '<form method="post" action="options.php">';
settings_fields('statusapi_backend_helper_group');
echo '<div class="statusapi-fields">';
self::render_input_field('StatusAPI Basis-URL', self::OPTION_KEY . '[statusapi_base_url]', $statusapi_base_url, 'Bei NPM: https://deine-domain.tld (ohne :9191, ohne /broadcast)');
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 '</div>';
echo '<div class="statusapi-actions">';
submit_button('Einstellungen speichern', 'primary', 'submit', false);
echo '</div>';
echo '</form>';
echo '</div>';
echo '<div class="statusapi-card">';
echo '<div class="statusapi-card-head"><h2>Snippets</h2><p>Direkt für deine Proxy-Konfiguration und externe Tests vorbereitet.</p></div>';
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 '</div>';
echo '<div class="statusapi-card">';
echo '<div class="statusapi-card-head"><h2>Aktionen</h2><p>Teste direkt aus dem Backend, ob dein Proxy erreichbar ist und Attack-Meldungen akzeptiert.</p></div>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="statusapi-test-form">';
wp_nonce_field('statusapi_backend_helper_test');
echo '<input type="hidden" name="action" value="statusapi_backend_helper_test" />';
echo '<div class="statusapi-test-buttons">';
submit_button('StatusAPI Verbindung prüfen', 'secondary', 'test_connection', false);
submit_button('Test Attack-Meldung senden', 'secondary', 'test_attack', false);
echo '</div>';
echo '</form>';
echo '<div class="statusapi-meta">';
echo '<div><span>Gespeicherte Basis-URL</span><strong>' . esc_html($statusapi_base_url !== '' ? $statusapi_base_url : 'nicht gesetzt') . '</strong></div>';
echo '<div><span>Attack-Key</span><strong>' . esc_html($attack_api_key !== '' ? 'gesetzt' : 'leer') . '</strong></div>';
echo '<div><span>Quelle</span><strong>' . esc_html($attack_source !== '' ? $attack_source : 'WordPress') . '</strong></div>';
echo '</div>';
echo '</div>';
echo '</div>';
echo '</div>';
}
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 '<div class="wrap statusapi-admin">';
self::render_styles();
self::render_scripts($refresh_nonce);
echo '<h1>StatusPulse Meldungen</h1>';
echo '<p>Dedizierte Seite für alle laufenden Status-, Attack- und Warnmeldungen.</p>';
if ($notice !== null) {
printf(
'<div class="notice notice-%1$s statusapi-notice"><p>%2$s</p></div>',
esc_attr($notice['type']),
esc_html($notice['message'])
);
}
self::render_messages_center($messages_markup, self::MESSAGES_SLUG);
echo '</div>';
}
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 '<div class="wrap statusapi-admin">';
self::render_styles();
echo '<h1>StatusPulse Angriffsversuche</h1>';
echo '<p>Zeigt nur Spielername und UUID von erkannten Angriffsversuchen (inkl. Datum/Uhrzeit).</p>';
if ($notice !== null) {
printf(
'<div class="notice notice-%1$s statusapi-notice"><p>%2$s</p></div>',
esc_attr($notice['type']),
esc_html($notice['message'])
);
}
echo '<div class="statusapi-card statusapi-card-messages">';
echo '<div class="statusapi-card-head"><h2>Angriffs-Ereignisse</h2><p>Neueste Einträge oben.</p></div>';
if ($list_error !== '') {
echo '<div class="notice notice-error inline"><p>' . esc_html($list_error) . '</p></div>';
}
if (empty($rows)) {
echo '<p>Keine Angriffs-Einträge mit Spielername/UUID vorhanden.</p>';
} else {
echo '<table class="widefat striped">';
echo '<thead><tr><th style="width:190px;">Datum / Uhrzeit</th><th style="width:220px;">Spielername</th><th style="width:180px;">IP</th><th>UUID</th></tr></thead>';
echo '<tbody>';
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 '<tr>';
echo '<td>' . esc_html($date) . '</td>';
echo '<td><strong>' . esc_html($player) . '</strong></td>';
echo '<td>' . esc_html($ip) . '</td>';
echo '<td>' . esc_html($uuid) . '</td>';
echo '</tr>';
}
echo '</tbody></table>';
}
echo '</div>';
echo '</div>';
}
private static function render_messages_center($messages_markup, $redirect_page) {
echo '<div class="statusapi-card statusapi-card-messages">';
echo '<div class="statusapi-card-head"><h2>Meldungs-Center</h2><p>Hier siehst du laufend erkannte Status-, Attack- und Warnmeldungen aus deinem Proxy.</p></div>';
echo '<div class="statusapi-message-filters">';
echo '<button type="button" class="button statusapi-filter-btn is-active" data-filter="all">Alle</button>';
echo '<button type="button" class="button statusapi-filter-btn" data-filter="error">Fehler</button>';
echo '<button type="button" class="button statusapi-filter-btn" data-filter="warning">Warnung</button>';
echo '<button type="button" class="button statusapi-filter-btn" data-filter="success">Erfolg</button>';
echo '<button type="button" class="button statusapi-filter-btn" data-filter="info">Info</button>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="statusapi-clear-form">';
wp_nonce_field('statusapi_backend_helper_clear_messages');
echo '<input type="hidden" name="action" value="statusapi_backend_helper_clear_messages" />';
echo '<input type="hidden" name="redirect_page" value="' . esc_attr($redirect_page) . '" />';
echo '<button type="submit" class="button statusapi-clear-btn">Meldungen leeren</button>';
echo '</form>';
echo '</div>';
echo '<ul id="statusapi-message-list" class="statusapi-message-list">' . $messages_markup . '</ul>';
echo '</div>';
}
private static function render_input_field($label, $name, $value, $description) {
echo '<label class="statusapi-field" for="' . esc_attr($name) . '">';
echo '<span class="statusapi-field-label">' . esc_html($label) . '</span>';
printf(
'<input class="statusapi-input" type="text" id="%1$s" name="%1$s" value="%2$s" />',
esc_attr($name),
esc_attr($value)
);
if ($description !== '') {
echo '<span class="statusapi-field-description">' . esc_html($description) . '</span>';
}
echo '</label>';
}
private static function render_snippet_card($title, $content, $rows) {
echo '<div class="statusapi-snippet-card">';
echo '<div class="statusapi-snippet-head">';
echo '<h3>' . esc_html($title) . '</h3>';
echo '<button type="button" class="button button-secondary statusapi-copy" data-copy-target="statusapi-copy-' . esc_attr(md5($title . $content)) . '">Kopieren</button>';
echo '</div>';
printf(
'<textarea readonly rows="%1$d" id="%2$s" class="statusapi-snippet">%3$s</textarea>',
(int) $rows,
esc_attr('statusapi-copy-' . md5($title . $content)),
esc_textarea($content)
);
echo '</div>';
}
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 '<div' . $id_attr . ' class="statusapi-status-card ' . esc_attr($status_class) . '">';
echo '<span class="statusapi-status-title">' . esc_html($title) . '</span>';
echo '<strong class="statusapi-status-value" data-role="value">' . esc_html($value) . '</strong>';
echo '<span class="statusapi-status-meta" data-role="meta">' . esc_html($meta) . '</span>';
echo '</div>';
}
private static function render_metric_card($id, $title, $value, $meta) {
echo '<div id="' . esc_attr($id) . '" class="statusapi-metric-card">';
echo '<span class="statusapi-metric-title">' . esc_html($title) . '</span>';
echo '<strong class="statusapi-metric-value" data-role="value">' . esc_html($value) . '</strong>';
echo '<span class="statusapi-metric-meta" data-role="meta">' . esc_html($meta) . '</span>';
echo '</div>';
}
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 is_supported_api_payload($decoded) {
if (!is_array($decoded)) {
return false;
}
$is_statusapi = isset($decoded['network']) && is_array($decoded['network']);
$has_players_array = isset($decoded['players']) && is_array($decoded['players']);
$has_online_flag = isset($decoded['online'])
&& (is_bool($decoded['online']) || $decoded['online'] === 0 || $decoded['online'] === 1 || $decoded['online'] === '0' || $decoded['online'] === '1' || $decoded['online'] === 'true' || $decoded['online'] === 'false');
$is_bungeecord = $has_online_flag && isset($decoded['players']) && is_array($decoded['players']);
// Wie in mc-player-history: players[] gilt bereits als verwertbares StatusAPI-JSON.
return $is_statusapi || $is_bungeecord || $has_players_array;
}
private static function should_try_stream_fallback($error_message) {
$msg = strtolower((string) $error_message);
return strpos($msg, 'curl error 35') !== false
|| strpos($msg, 'tlsv1 unrecognized name') !== false
|| strpos($msg, 'ssl routines') !== false;
}
private static function stream_http_request($url, $method, $headers, $body, $timeout) {
$parts = wp_parse_url($url);
if (!is_array($parts) || empty($parts['host'])) {
return new WP_Error('statuspulse_stream_bad_url', 'Ungueltige URL fuer Stream-Fallback.');
}
$scheme = isset($parts['scheme']) ? strtolower((string) $parts['scheme']) : 'http';
$host = (string) $parts['host'];
$port = isset($parts['port']) ? (int) $parts['port'] : ($scheme === 'https' ? 443 : 80);
$path = isset($parts['path']) ? (string) $parts['path'] : '/';
if ($path === '') {
$path = '/';
}
if (!empty($parts['query'])) {
$path .= '?' . $parts['query'];
}
$transport = $scheme === 'https' ? 'ssl://' : 'tcp://';
$context_options = array();
if ($scheme === 'https') {
$context_options['ssl'] = array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
// Workaround fuer Umgebungen mit problematischem SNI/TLS Handshake via cURL.
'SNI_enabled' => false,
);
}
$context = stream_context_create($context_options);
$errno = 0;
$errstr = '';
$fp = @stream_socket_client($transport . $host . ':' . $port, $errno, $errstr, (float) $timeout, STREAM_CLIENT_CONNECT, $context);
if (!$fp) {
return new WP_Error('statuspulse_stream_connect_failed', 'Stream-Verbindung fehlgeschlagen: ' . $errstr);
}
stream_set_timeout($fp, (int) max(1, (int) $timeout));
$request_headers = array(
'Host' => $host,
'Connection' => 'close',
'User-Agent' => 'StatusPulse/1.0',
'Accept' => 'application/json, */*',
);
foreach ((array) $headers as $hk => $hv) {
$request_headers[(string) $hk] = (string) $hv;
}
$payload = (string) $body;
if ($payload !== '') {
$request_headers['Content-Length'] = (string) strlen($payload);
}
$raw = strtoupper($method) . ' ' . $path . " HTTP/1.1\r\n";
foreach ($request_headers as $hk => $hv) {
$raw .= $hk . ': ' . $hv . "\r\n";
}
$raw .= "\r\n";
if ($payload !== '') {
$raw .= $payload;
}
fwrite($fp, $raw);
$response_raw = '';
while (!feof($fp)) {
$chunk = fread($fp, 8192);
if ($chunk === false) {
break;
}
$response_raw .= $chunk;
}
fclose($fp);
if ($response_raw === '') {
return new WP_Error('statuspulse_stream_empty', 'Leere Antwort vom Stream-Fallback.');
}
$parts_resp = explode("\r\n\r\n", $response_raw, 2);
$head = isset($parts_resp[0]) ? $parts_resp[0] : '';
$resp_body = isset($parts_resp[1]) ? $parts_resp[1] : '';
$status_code = 0;
$head_lines = explode("\r\n", $head);
if (!empty($head_lines)) {
if (preg_match('#HTTP/\d\.\d\s+(\d{3})#', (string) $head_lines[0], $m)) {
$status_code = (int) $m[1];
}
}
return array(
'code' => $status_code,
'body' => $resp_body,
'error' => '',
'via' => 'stream',
);
}
private static function perform_request_with_fallback($url, $method, $headers = array(), $body = '', $timeout = 5) {
$args = array(
'timeout' => $timeout,
'sslverify' => false,
'headers' => is_array($headers) ? $headers : array(),
);
if (strtoupper($method) !== 'GET') {
$args['body'] = (string) $body;
}
$response = wp_remote_request($url, array_merge($args, array('method' => strtoupper($method))));
if (!is_wp_error($response)) {
return array(
'code' => (int) wp_remote_retrieve_response_code($response),
'body' => (string) wp_remote_retrieve_body($response),
'error' => '',
'via' => 'wp',
);
}
$error_message = $response->get_error_message();
if (!self::should_try_stream_fallback($error_message)) {
return array('code' => 0, 'body' => '', 'error' => $error_message, 'via' => 'wp');
}
$fallback = self::stream_http_request($url, $method, $headers, $body, $timeout);
if (is_wp_error($fallback)) {
return array('code' => 0, 'body' => '', 'error' => $error_message . ' | Stream-Fallback: ' . $fallback->get_error_message(), 'via' => 'wp+stream');
}
return $fallback;
}
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']);
$candidate_urls = self::get_candidate_base_urls($base_url);
if (empty($candidate_urls)) {
return array(
'status' => 'unknown',
'label' => 'Nicht konfiguriert',
'meta' => 'Bitte zuerst eine StatusAPI Basis-URL eintragen (oder PulseCast konfigurieren).',
'http_code' => 0,
'checked_at_human' => self::format_timestamp($state['last_check_at']),
'metrics' => self::empty_metrics(),
'debug' => array(
'url_used' => '',
'attempts' => array(),
'error' => 'Keine Basis-URL gesetzt.',
'raw_data' => '',
),
);
}
$response_url = $candidate_urls[0];
$response = null;
$last_error = '';
$is_valid_response = false;
$is_reachable = false;
$reachable_code = 0;
$attempts = array();
$raw_fallback = '';
foreach ($candidate_urls as $try_url) {
$response = self::perform_request_with_fallback($try_url, 'GET', array(), '', 5);
if (!empty($response['error'])) {
$last_error = $response['error'];
$attempts[] = $try_url . ' -> ERROR: ' . $response['error'];
continue;
}
$code_tmp = (int) $response['code'];
$body_tmp = (string) $response['body'];
$decoded_tmp = json_decode($body_tmp, true);
if ($code_tmp > 0 && $code_tmp < 500 && !$is_reachable) {
$is_reachable = true;
$reachable_code = $code_tmp;
$response_url = $try_url;
}
if ($code_tmp >= 200 && $code_tmp < 300 && self::is_supported_api_payload($decoded_tmp)) {
$response_url = $try_url;
$is_valid_response = true;
$last_error = '';
$attempts[] = $try_url . ' -> HTTP ' . $code_tmp . ' (StatusAPI JSON)';
break;
}
if ($code_tmp >= 200 && $code_tmp < 300 && !self::is_supported_api_payload($decoded_tmp)) {
$attempts[] = $try_url . ' -> HTTP ' . $code_tmp . ' (kein StatusAPI JSON)';
if ($raw_fallback === '') {
$raw_fallback = $body_tmp;
}
if (is_string($body_tmp) && stripos($body_tmp, 'successfully started the Nginx Proxy Manager') !== false) {
// NPM-Domain ist nicht korrekt auf Port 9191 geroutet
$parsed_npm = wp_parse_url($try_url);
$npm_host = is_array($parsed_npm) && !empty($parsed_npm['host']) ? $parsed_npm['host'] : '';
$last_error = 'Nginx Proxy Manager Default-Seite erkannt Proxy-Host für diese Domain fehlt.'
. ($npm_host !== '' ? ' Trage stattdessen direkt ein: http://' . $npm_host . ':9191' : '');
} else {
$last_error = 'Antwort ist kein StatusAPI-JSON.';
}
} else {
$attempts[] = $try_url . ' -> HTTP ' . $code_tmp;
$last_error = 'HTTP ' . $code_tmp;
}
}
$now = time();
if (!$is_valid_response) {
$state['last_check_at'] = $now;
$state['last_http_code'] = $is_reachable ? $reachable_code : 0;
$state['last_status'] = 'error';
$state['last_error'] = $last_error !== '' ? $last_error : 'Ungueltige Antwort vom Proxy';
if ($previous_status !== 'error') {
self::push_message($state, 'error', 'Proxy-Fehlkonfiguration', 'StatusAPI liefert keine gueltigen JSON-Daten. Pruefe Nginx Proxy Host fuer die Domain.');
}
self::save_state($state);
error_log('StatusPulse: Fehler fuer ' . implode(', ', $candidate_urls) . ': ' . $state['last_error']);
return array(
'status' => 'error',
'label' => 'Fehlkonfiguration',
'meta' => $state['last_error'],
'http_code' => 0,
'checked_at_human' => self::format_timestamp($now),
'metrics' => self::empty_metrics(),
'debug' => array(
'url_used' => $response_url,
'attempts' => $attempts,
'error' => $state['last_error'],
'raw_data' => $raw_fallback,
),
);
}
$code = (int) $response['code'];
$ok = $code >= 200 && $code < 300;
$body = (string) $response['body'];
$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($response_url) : 'HTTP ' . $code,
'http_code' => $code,
'checked_at_human' => self::format_timestamp($now),
'metrics' => $ok ? self::extract_metrics($decoded) : self::empty_metrics(),
'debug' => array(
'url_used' => $response_url,
'attempts' => $attempts,
'error' => $ok ? '' : 'HTTP ' . $code,
'raw_data' => $body,
),
);
}
private static function extract_metrics($decoded) {
if (!is_array($decoded)) {
return self::empty_metrics();
}
$players_list_count = self::count_players_fallback($decoded);
// StatusAPI-Format
$network = isset($decoded['network']) && is_array($decoded['network']) ? $decoded['network'] : array();
if (!empty($network)) {
$players = isset($network['players']) && is_array($network['players']) ? $network['players'] : array();
$memory = isset($network['memory']) && is_array($network['memory']) ? $network['memory'] : array();
// Wie in MC-Player-History: players[] als primäre Quelle bevorzugen.
$online_players = $players_list_count !== 'n/a'
? $players_list_count
: self::array_value($players, 'online', 'n/a');
$max_players_raw = self::array_value($players, 'max', self::array_value($decoded, 'max_players', 'n/a'));
$max_players = self::normalize_max_players($max_players_raw);
$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', null);
$memory_used = self::array_value($memory, 'used_mb', 'n/a');
$memory_max = self::array_value($memory, 'max_mb', 'n/a');
$memory_value = 'n/a';
if ($memory_percent !== null && $memory_percent !== 'n/a' && $memory_percent !== '') {
$memory_value = ((string) $memory_percent) . '%';
}
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' => $memory_value,
'memory_meta' => $memory_used . ' MB von ' . $memory_max . ' MB',
);
}
// BungeeCord-Format
$is_bungeecord = isset($decoded['players']) && is_array($decoded['players']) && isset($decoded['online']);
if ($is_bungeecord) {
return self::extract_metrics_bungeecord($decoded);
}
return self::empty_metrics();
}
private static function extract_metrics_bungeecord($decoded) {
if (!is_array($decoded)) {
return self::empty_metrics();
}
$players_array = isset($decoded['players']) && is_array($decoded['players']) ? $decoded['players'] : array();
$online_raw = self::array_value($decoded, 'online', null);
$is_online = $online_raw === true || $online_raw === 1 || $online_raw === '1' || $online_raw === 'true';
$online_count = $is_online ? count($players_array) : '0';
$max_players = self::array_value($decoded, 'max_players', 'n/a');
if ($max_players === 'n/a' && !empty($players_array) && is_array($players_array[0]) && isset($players_array[0]['max'])) {
$max_players = (int) $players_array[0]['max'];
}
$max_players = self::normalize_max_players($max_players);
return array(
'online_players' => (string) $online_count,
'online_meta' => 'Spieler online (BungeeCord)',
'max_players' => (string) $max_players,
'max_meta' => 'Max Spieler',
'uptime' => 'n/a',
'uptime_meta' => 'BungeeCord Format (nicht verfügbar)',
'memory' => 'n/a',
'memory_meta' => 'BungeeCord Format (nicht verfügbar)',
);
}
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 normalize_max_players($value) {
// -1 bedeutet: BungeeCord hat keinen Listener-Wert und kein globales Limit.
// Wird ab StatusAPI 4.1+ eigentlich nicht mehr auftreten da Java den Listener-Wert liefert.
if ($value === -1 || $value === '-1' || (is_numeric($value) && (int) $value === -1)) {
return '∞';
}
if (is_numeric($value) && (int) $value <= 0) {
return 'n/a';
}
return $value;
}
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') {
$state = self::get_state();
$suffix = '';
if (!empty($state['last_error'])) {
$suffix = ' Letzter Fehler: ' . (string) $state['last_error'];
}
return array('type' => 'error', 'message' => 'Verbindungstest fehlgeschlagen. Bitte Basis-URL und Erreichbarkeit prüfen.' . $suffix);
}
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'])) {
$probe = self::probe_statusapi($base_url);
$ok = isset($probe['status']) && $probe['status'] === 'ok';
$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. ' . (isset($probe['meta']) ? (string) $probe['meta'] : '')
);
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 = self::perform_request_with_fallback(
$base_url . '/network/attack',
'POST',
array(
'Content-Type' => 'application/json; charset=utf-8',
'X-API-Key' => $options['attack_api_key'],
),
wp_json_encode($payload),
8
);
if (!empty($response['error'])) {
$state = self::get_state();
$state['last_check_at'] = time();
$state['last_http_code'] = 0;
$state['last_status'] = 'error';
$state['last_error'] = $response['error'];
self::push_message($state, 'error', 'Test-Attack fehlgeschlagen', 'Konnte keine Test-Meldung senden: ' . $response['error']);
self::save_state($state);
self::redirect_with_notice('attack_failed');
}
$code = (int) $response['code'];
$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();
$debug_data = isset($live_status['debug']) && is_array($live_status['debug']) ? $live_status['debug'] : array();
wp_send_json_success(array(
'refresh_state' => 'Aktualisiert ' . self::format_timestamp($state['last_check_at']),
'messages_html' => self::build_messages_markup($state),
'debug' => array(
'url_used' => isset($debug_data['url_used']) ? (string) $debug_data['url_used'] : '',
'attempts' => isset($debug_data['attempts']) && is_array($debug_data['attempts']) ? $debug_data['attempts'] : array(),
'error' => isset($debug_data['error']) ? (string) $debug_data['error'] : '',
'raw_data' => isset($debug_data['raw_data']) ? (string) $debug_data['raw_data'] : '',
),
'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) {
$candidate_urls = self::get_candidate_base_urls($url);
if (empty($candidate_urls)) {
return false;
}
foreach ($candidate_urls as $try_url) {
$response = self::perform_request_with_fallback($try_url, 'GET', array(), '', 8);
if (!empty($response['error'])) {
error_log('StatusPulse: request_ok() fehlgeschlagen fuer ' . $try_url . ' - ' . $response['error']);
continue;
}
$code = (int) $response['code'];
$decoded = json_decode((string) $response['body'], true);
error_log('StatusPulse: request_ok() fuer ' . $try_url . ' -> HTTP ' . $code);
if ($code >= 200 && $code < 300 && self::is_supported_api_payload($decoded)) {
return true;
}
}
return false;
}
private static function fetch_attacker_entries($base_url) {
// Schritt 1: Arbeitsfähige Basis-URL ermitteln (erste die valide antwortet)
$candidate_urls = self::get_candidate_base_urls($base_url);
if (empty($candidate_urls)) {
return array(
'entries' => array(),
'error' => 'StatusAPI Basis-URL ist nicht konfiguriert.',
);
}
// Finde die funktionierende Basis-URL (die echtes StatusAPI-JSON liefert)
$working_base = '';
foreach ($candidate_urls as $try_url) {
// Entferne bekannte Pfad-Suffixe damit wir nur die Root-URL testen
$root_url = rtrim(preg_replace('#/(health|antibot/security-log|broadcast|network/attack)$#', '', $try_url), '/');
if ($root_url === '') {
continue;
}
$probe = self::perform_request_with_fallback($root_url, 'GET', array(), '', 8);
if (empty($probe['error'])) {
$probe_code = (int) $probe['code'];
$probe_decoded = json_decode((string) $probe['body'], true);
if ($probe_code >= 200 && $probe_code < 300 && self::is_supported_api_payload($probe_decoded)) {
$working_base = $root_url;
break;
}
}
}
if ($working_base === '') {
return array(
'entries' => array(),
'error' => 'StatusAPI konnte nicht erreicht werden. Bitte Basis-URL und Erreichbarkeit prüfen.',
);
}
// Schritt 2: Dedizierter Endpunkt /antibot/security-log abrufen
$security_log_url = rtrim($working_base, '/') . '/antibot/security-log';
$response = self::perform_request_with_fallback($security_log_url, 'GET', array(), '', 8);
if (!empty($response['error'])) {
return array(
'entries' => array(),
'error' => 'Konnte Security-Log nicht laden: ' . $response['error'],
);
}
$code = (int) $response['code'];
if ($code < 200 || $code >= 300) {
return array(
'entries' => array(),
'error' => 'StatusAPI /antibot/security-log lieferte HTTP ' . $code . '.',
);
}
$body = (string) $response['body'];
$decoded = json_decode($body, true);
if (!is_array($decoded)) {
return array(
'entries' => array(),
'error' => 'Antwort vom Security-Log-Endpunkt ist kein gültiges JSON.',
);
}
$entries = array();
// StatusAPI liefert { "success": true, "events": [...] }
if (isset($decoded['events']) && is_array($decoded['events'])) {
foreach (array_slice($decoded['events'], 0, 100) as $entry) {
if (is_array($entry)) {
$entries[] = $entry;
}
}
}
return array(
'entries' => $entries,
'error' => count($entries) === 0 ? 'Keine Angriffsversuche im Log vorhanden.' : '',
);
}
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 '<style>
.statusapi-admin {
max-width: 1280px;
}
.statusapi-hero {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: flex-end;
padding: 32px;
margin: 20px 0 24px;
border-radius: 24px;
background: linear-gradient(135deg, #0f172a 0%, #1d4ed8 55%, #38bdf8 100%);
color: #f8fafc;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.22);
}
.statusapi-hero h1 {
margin: 6px 0 10px;
color: #ffffff;
font-size: 34px;
line-height: 1.1;
}
.statusapi-hero p {
margin: 0;
max-width: 760px;
color: rgba(248, 250, 252, 0.96);
font-size: 15px;
}
.statusapi-kicker {
display: inline-block;
padding: 6px 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
color: #eff6ff;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.statusapi-hero-badge {
min-width: 220px;
padding: 18px 20px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(8px);
}
.statusapi-hero-badge span {
display: block;
margin-bottom: 6px;
color: #dbeafe;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.statusapi-hero-badge strong {
font-size: 22px;
color: #ffffff;
}
.statusapi-hero-badge em {
display: block;
margin-top: 6px;
color: rgba(255, 255, 255, 0.92);
font-style: normal;
font-size: 12px;
}
.statusapi-grid {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 24px;
}
.statusapi-metric-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
margin: 0 0 24px;
}
.statusapi-status-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
margin: 0 0 24px;
}
.statusapi-status-card {
position: relative;
overflow: hidden;
padding: 18px 20px;
border-radius: 20px;
background: #ffffff;
border: 1px solid #dbe4f0;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
}
.statusapi-status-card::after {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 5px;
border-radius: 20px 0 0 20px;
background: #94a3b8;
}
.statusapi-state-ok::after {
background: #16a34a;
}
.statusapi-state-error::after {
background: #dc2626;
}
.statusapi-state-unknown::after {
background: #f59e0b;
}
.statusapi-status-title {
display: block;
margin-bottom: 10px;
color: #64748b;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.07em;
}
.statusapi-status-value {
display: block;
margin-bottom: 8px;
color: #0f172a;
font-size: 24px;
line-height: 1.2;
}
.statusapi-status-meta {
display: block;
color: #334155;
font-size: 13px;
line-height: 1.5;
}
.statusapi-metric-card {
padding: 20px;
border-radius: 20px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
border: 1px solid #dbe4f0;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
}
.statusapi-metric-title {
display: block;
margin-bottom: 10px;
color: #64748b;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.07em;
}
.statusapi-metric-value {
display: block;
margin-bottom: 8px;
color: #0f172a;
font-size: 24px;
line-height: 1.2;
}
.statusapi-metric-meta {
display: block;
color: #334155;
font-size: 13px;
line-height: 1.5;
}
.statusapi-card {
background: #ffffff;
border: 1px solid #dbe4f0;
border-radius: 22px;
padding: 24px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
}
.statusapi-card-form {
grid-row: span 2;
}
.statusapi-card-head h2 {
margin: 0 0 6px;
font-size: 22px;
}
.statusapi-card-head p {
margin: 0 0 20px;
color: #334155;
}
.statusapi-fields {
display: grid;
gap: 18px;
}
.statusapi-field {
display: block;
}
.statusapi-field-label {
display: block;
margin-bottom: 8px;
color: #0f172a;
font-weight: 600;
}
.statusapi-field-description {
display: block;
margin-top: 8px;
color: #334155;
font-size: 13px;
}
.statusapi-input {
width: 100%;
min-height: 48px;
padding: 12px 14px;
border: 1px solid #cbd5e1;
border-radius: 14px;
background: #f8fafc;
font-size: 14px;
}
.statusapi-input:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.16);
outline: none;
background: #ffffff;
}
.statusapi-actions {
margin-top: 20px;
}
.statusapi-snippet-card + .statusapi-snippet-card {
margin-top: 18px;
}
.statusapi-snippet-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin-bottom: 10px;
}
.statusapi-snippet-head h3 {
margin: 0;
font-size: 15px;
}
.statusapi-snippet {
width: 100%;
padding: 14px;
border: 1px solid #dbe4f0;
border-radius: 16px;
background: #0b1220 !important;
color: #f8fafc !important;
font-family: Consolas, Monaco, monospace;
resize: vertical;
opacity: 1 !important;
-webkit-text-fill-color: #f8fafc;
}
.statusapi-snippet:focus {
background: #0b1220 !important;
color: #f8fafc !important;
}
.statusapi-test-form {
margin-top: 4px;
}
.statusapi-test-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.statusapi-meta {
display: grid;
gap: 12px;
}
.statusapi-meta div {
padding: 14px 16px;
border-radius: 16px;
background: #f8fafc;
border: 1px solid #e2e8f0;
}
.statusapi-meta span {
display: block;
margin-bottom: 6px;
color: #334155;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.statusapi-meta strong {
color: #0f172a;
font-size: 15px;
}
.statusapi-notice {
margin: 0 0 16px;
border-radius: 14px;
overflow: hidden;
}
.statusapi-notice p {
color: #0f172a !important;
font-weight: 600;
}
.statusapi-card-messages {
margin-bottom: 24px;
}
.statusapi-message-list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 10px;
max-height: 300px;
overflow: auto;
}
.statusapi-message-filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.statusapi-filter-btn {
border-radius: 999px !important;
}
.statusapi-filter-btn.is-active {
background: #0f172a !important;
border-color: #0f172a !important;
color: #ffffff !important;
}
.statusapi-clear-form {
margin-left: auto;
}
.statusapi-clear-btn {
border-color: #dc2626 !important;
color: #dc2626 !important;
}
.statusapi-clear-btn:hover {
border-color: #b91c1c !important;
color: #b91c1c !important;
}
.statusapi-message-item {
border: 1px solid #dbe4f0;
border-left-width: 5px;
border-radius: 12px;
background: #f8fafc;
padding: 10px 12px;
}
.statusapi-message-item.is-hidden {
display: none;
}
.statusapi-message-item p {
margin: 6px 0 0;
color: #334155;
}
.statusapi-message-head {
display: flex;
justify-content: space-between;
gap: 10px;
align-items: baseline;
}
.statusapi-message-head strong {
color: #0f172a;
}
.statusapi-message-head span {
color: #475569;
font-size: 12px;
}
.statusapi-level-success {
border-left-color: #16a34a;
}
.statusapi-level-warning {
border-left-color: #d97706;
}
.statusapi-level-error {
border-left-color: #dc2626;
}
.statusapi-level-info,
.statusapi-message-empty {
border-left-color: #2563eb;
}
.statusapi-level-pill {
display: inline-block;
padding: 4px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: .04em;
}
.statusapi-level-pill.statusapi-level-success {
background: #dcfce7;
color: #166534;
}
.statusapi-level-pill.statusapi-level-warning {
background: #fef3c7;
color: #92400e;
}
.statusapi-level-pill.statusapi-level-error {
background: #fee2e2;
color: #991b1b;
}
.statusapi-level-pill.statusapi-level-info {
background: #dbeafe;
color: #1e40af;
}
@media (max-width: 1100px) {
.statusapi-metric-grid,
.statusapi-status-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.statusapi-grid {
grid-template-columns: 1fr;
}
.statusapi-card-form {
grid-row: auto;
}
}
@media (max-width: 782px) {
.statusapi-metric-grid,
.statusapi-status-grid {
grid-template-columns: 1fr;
}
.statusapi-hero {
flex-direction: column;
align-items: stretch;
}
.statusapi-snippet-head,
.statusapi-test-buttons {
flex-direction: column;
align-items: stretch;
}
.statusapi-message-filters {
gap: 6px;
}
.statusapi-clear-form {
margin-left: 0;
}
}
</style>';
}
private static function render_scripts($refresh_nonce) {
echo '<script>
window.StatusApiBridgeConfig = {
ajaxUrl: ' . wp_json_encode(admin_url('admin-ajax.php')) . ',
nonce: ' . wp_json_encode($refresh_nonce) . ',
intervalMs: 15000
};
document.addEventListener("click", function(event) {
var button = event.target.closest(".statusapi-copy");
if (!button) {
return;
}
var targetId = button.getAttribute("data-copy-target");
var target = document.getElementById(targetId);
if (!target) {
return;
}
target.select();
target.setSelectionRange(0, 99999);
try {
document.execCommand("copy");
button.textContent = "Kopiert";
window.setTimeout(function() {
button.textContent = "Kopieren";
}, 1400);
} catch (error) {
button.textContent = "Fehlgeschlagen";
}
});
document.addEventListener("DOMContentLoaded", function() {
var config = window.StatusApiBridgeConfig || null;
if (!config) {
return;
}
var refreshState = document.getElementById("statusapi-refresh-state");
var currentFilter = "all";
try {
var remembered = window.localStorage.getItem("statuspulse_message_filter");
if (remembered) {
currentFilter = remembered;
}
} catch (error) {
}
function applyStatusCard(id, payload) {
var card = document.getElementById(id);
if (!card || !payload) {
return;
}
var valueNode = card.querySelector("[data-role=value]");
var metaNode = card.querySelector("[data-role=meta]");
if (valueNode && typeof payload.value !== "undefined") {
valueNode.textContent = payload.value;
}
if (metaNode && typeof payload.meta !== "undefined") {
metaNode.textContent = payload.meta;
}
card.classList.remove("statusapi-state-ok", "statusapi-state-error", "statusapi-state-unknown");
if (payload.status === "ok") {
card.classList.add("statusapi-state-ok");
} else if (payload.status === "error") {
card.classList.add("statusapi-state-error");
} else {
card.classList.add("statusapi-state-unknown");
}
}
function applyMetricCard(id, payload) {
var card = document.getElementById(id);
if (!card || !payload) {
return;
}
var valueNode = card.querySelector("[data-role=value]");
var metaNode = card.querySelector("[data-role=meta]");
if (valueNode && typeof payload.value !== "undefined") {
valueNode.textContent = payload.value;
}
if (metaNode && typeof payload.meta !== "undefined") {
metaNode.textContent = payload.meta;
}
}
function applyMessageFilter(filter) {
currentFilter = filter || "all";
try {
window.localStorage.setItem("statuspulse_message_filter", currentFilter);
} catch (error) {
}
var buttons = document.querySelectorAll(".statusapi-filter-btn");
buttons.forEach(function(btn) {
var isActive = btn.getAttribute("data-filter") === currentFilter;
btn.classList.toggle("is-active", isActive);
});
var items = document.querySelectorAll("#statusapi-message-list .statusapi-message-item");
items.forEach(function(item) {
var level = item.getAttribute("data-level") || "info";
var show = currentFilter === "all" || level === "empty" || level === currentFilter;
item.classList.toggle("is-hidden", !show);
});
}
document.addEventListener("click", function(event) {
var filterBtn = event.target.closest(".statusapi-filter-btn");
if (!filterBtn) {
return;
}
applyMessageFilter(filterBtn.getAttribute("data-filter") || "all");
});
function refreshDashboard() {
var formData = new window.FormData();
formData.append("action", "statusapi_backend_helper_refresh");
formData.append("_ajax_nonce", config.nonce);
if (refreshState) {
refreshState.textContent = "aktualisiert...";
}
window.fetch(config.ajaxUrl, {
method: "POST",
credentials: "same-origin",
body: formData
}).then(function(response) {
return response.json();
}).then(function(result) {
if (!result || !result.success || !result.data) {
throw new Error("invalid_response");
}
applyStatusCard("statusapi-card-proxy", result.data.status_cards.proxy);
applyStatusCard("statusapi-card-http", result.data.status_cards.http);
applyStatusCard("statusapi-card-success", result.data.status_cards.success);
applyStatusCard("statusapi-card-lastcheck", result.data.status_cards.lastcheck);
applyMetricCard("statusapi-metric-online", result.data.metric_cards.online);
applyMetricCard("statusapi-metric-max", result.data.metric_cards.max);
applyMetricCard("statusapi-metric-uptime", result.data.metric_cards.uptime);
applyMetricCard("statusapi-metric-memory", result.data.metric_cards.memory);
// Debug-Panel live aktualisieren
if (result.data.debug) {
var dbg = result.data.debug;
var urlEl = document.getElementById("statuspulse-debug-url");
var attEl = document.getElementById("statuspulse-debug-attempts");
var rawEl = document.getElementById("statuspulse-debug-raw");
var errEl = document.getElementById("statuspulse-debug-error");
if (urlEl) urlEl.textContent = dbg.url_used || "(nicht gesetzt)";
if (attEl) attEl.textContent = dbg.attempts && dbg.attempts.length ? dbg.attempts.join(" | ") : "";
if (rawEl) rawEl.textContent = dbg.raw_data || "(keine Response)";
if (errEl) {
errEl.textContent = dbg.error || "";
errEl.style.display = dbg.error ? "block" : "none";
}
}
var messageList = document.getElementById("statusapi-message-list");
if (messageList && typeof result.data.messages_html !== "undefined") {
messageList.innerHTML = result.data.messages_html;
applyMessageFilter(currentFilter);
}
if (refreshState && result.data.refresh_state) {
refreshState.textContent = result.data.refresh_state;
}
}).catch(function() {
if (refreshState) {
refreshState.textContent = "Auto-Refresh fehlgeschlagen";
}
});
}
applyMessageFilter(currentFilter);
window.setTimeout(refreshDashboard, 1200);
window.setInterval(refreshDashboard, config.intervalMs);
});
</script>';
}
}
StatusAPI_Backend_Helper::boot();
}