is_plugin_settings_page( $hook_suffix ) ) { return; } wp_enqueue_style( 'aisignal-markdown-converter-admin', plugins_url( 'assets/css/admin.css', AISIGNAL_MARKDOWN_CONVERTER_PLUGIN_FILE ), [], AISIGNAL_MARKDOWN_CONVERTER_VERSION ); wp_enqueue_script( 'chartjs', plugins_url( 'assets/js/vendor/chart.min.js', AISIGNAL_MARKDOWN_CONVERTER_PLUGIN_FILE ), [], '4.5.1', true ); wp_enqueue_script( 'aisignal-markdown-converter-admin-script', plugins_url( 'assets/js/admin.js', AISIGNAL_MARKDOWN_CONVERTER_PLUGIN_FILE ), [ 'chartjs' ], AISIGNAL_MARKDOWN_CONVERTER_VERSION, true ); } /** * Register settings. * * @return void */ public function register_settings(): void { foreach ( $this->get_general_setting_definitions() as $option => $args ) { register_setting( self::OPTION_GROUP_GENERAL, $option, $args ); } foreach ( $this->get_crawler_setting_definitions() as $option => $args ) { register_setting( self::OPTION_GROUP_CRAWLER, $option, $args ); } } /** * Sanitize the frontmatter checkbox. * * @param mixed $value Raw option value. * * @return bool */ public function sanitize_frontmatter_enabled( $value ): bool { return (bool) rest_sanitize_boolean( $value ); } /** * Sanitize the crawler insights checkbox. * * @param mixed $value Raw option value. * * @return bool */ public function sanitize_crawler_insights_enabled( $value ): bool { return (bool) rest_sanitize_boolean( $value ); } /** * Sanitize enabled Markdown post types. * * @param mixed $value Raw option value. * * @return array */ public function sanitize_markdown_post_types( $value ): array { $value = is_array( $value ) ? array_map( 'sanitize_key', $value ) : []; return array_values( array_intersect( $value, Helpers::get_public_post_types() ) ); } /** * Sanitize excluded post IDs. * * @param mixed $value Raw option value. * * @return array */ public function sanitize_excluded_post_ids( $value ): array { return MarkdownAvailability::normalize_excluded_post_ids( $value ); } /** * Sanitize crawler insights retention. * * @param mixed $value Raw option value. * * @return int */ public function sanitize_crawler_retention_days( $value ): int { return $this->get_crawler_insights_service()->sanitize_retention_days( $value ); } /** * Add a settings link on the Plugins screen. * * @param array $links Existing action links. * * @return array */ public function add_settings_link( array $links ): array { $links[] = '' . esc_html__( 'Settings', 'aisignal-markdown-converter' ) . ''; return $links; } /** * Render the settings page. * * @return void */ public function render_page(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $active_tab = $this->get_active_tab(); ?>

render_tab_navigation( $active_tab ); ?> render_crawler_insights_tab(); ?> render_general_tab(); ?>
__( 'General', 'aisignal-markdown-converter' ), 'crawler-insights' => __( 'Crawler Insights', 'aisignal-markdown-converter' ), ]; ?> get_public_post_type_objects(); ?>
get_crawler_insights_service(); $insights_enabled = $service->is_enabled(); $retention_days = $service->get_retention_days(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin UI state. $selected_bot = isset( $_GET['bot'] ) ? sanitize_key( wp_unslash( (string) $_GET['bot'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin UI state. $current_page = isset( $_GET['paged'] ) ? max( 1, absint( wp_unslash( $_GET['paged'] ) ) ) : 1; $per_page = 25; $stats = $service->get_stats(); $available_bots = $service->get_available_bots(); $log_result = $service->get_logs( $selected_bot, $current_page, $per_page ); $total_pages = max( 1, (int) ceil( ( (int) $log_result['total_items'] ) / $per_page ) ); $items = isset( $log_result['items'] ) && is_array( $log_result['items'] ) ? $log_result['items'] : []; $requests_per_bot = $service->get_requests_per_bot(); $requests_per_day_by_bot = $service->get_requests_per_day_by_bot(); // Prepare data for charts. $per_bot_data = [ 'labels' => array_column( $requests_per_bot, 'bot_label' ), 'data' => array_column( $requests_per_bot, 'count' ), ]; // For bar chart. $dates = array_unique( array_column( $requests_per_day_by_bot, 'date' ) ); sort( $dates ); $bots = array_unique( array_column( $requests_per_day_by_bot, 'bot_key' ) ); $bot_labels = []; foreach ( $bots as $bot_key ) { $found = array_filter( $requests_per_day_by_bot, fn( $row ) => $row['bot_key'] === $bot_key ); if ( $found ) { $bot_labels[ $bot_key ] = reset( $found )['bot_label']; } } $colors = [ '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0', '#FF6384' ]; // Assign consistent colors to bots. $all_bot_keys = array_unique( array_merge( array_column( $requests_per_bot, 'bot_key' ), array_column( $requests_per_day_by_bot, 'bot_key' ) ) ); sort( $all_bot_keys ); $bot_colors = []; foreach ( $all_bot_keys as $index => $bot_key ) { $bot_colors[ $bot_key ] = $colors[ $index % count( $colors ) ]; } $datasets = []; foreach ( $bots as $bot_key ) { $data = []; foreach ( $dates as $date ) { $found = array_filter( $requests_per_day_by_bot, fn( $row ) => $row['date'] === $date && $row['bot_key'] === $bot_key ); $data[] = $found ? reset( $found )['count'] : 0; } $datasets[] = [ 'label' => $bot_labels[ $bot_key ] ?? $bot_key, 'data' => $data, 'backgroundColor' => $bot_colors[ $bot_key ] ?? '#C9CBCF', ]; } $per_day_data = [ 'labels' => $dates, 'datasets' => $datasets, ]; // Doughnut colors in label order. $bot_key_by_label = array_column( $requests_per_bot, 'bot_key', 'bot_label' ); $doughnut_colors = []; foreach ( $per_bot_data['labels'] as $label ) { $bot_key = $bot_key_by_label[ $label ] ?? ''; $doughnut_colors[] = $bot_colors[ $bot_key ] ?? '#C9CBCF'; } $requests_per_bot_chart = [ 'type' => 'doughnut', 'data' => [ 'labels' => $per_bot_data['labels'], 'datasets' => [ [ 'data' => $per_bot_data['data'], 'backgroundColor' => $doughnut_colors, ], ], ], 'options' => [ 'responsive' => true, 'animation' => false, 'plugins' => [ 'legend' => [ 'position' => 'bottom', ], ], ], ]; $requests_per_day_chart = [ 'type' => 'bar', 'data' => [ 'labels' => $per_day_data['labels'], 'datasets' => $per_day_data['datasets'], ], 'options' => [ 'responsive' => true, 'animation' => false, 'scales' => [ 'x' => [ 'stacked' => true, ], 'y' => [ 'stacked' => true, ], ], 'plugins' => [ 'legend' => [ 'position' => 'bottom', ], ], ], ]; ?>

occurred_at_gmt, 'Y-m-d H:i:s' ) ); ?> request_url ); ?> bot_label ); ?> request_method ); ?>
1 && function_exists( 'paginate_links' ) ) : ?>
add_query_arg( [ 'page' => 'aisignal-markdown-converter', 'tab' => 'crawler-insights', 'bot' => $selected_bot, 'paged' => '%#%', ], admin_url( 'options-general.php' ) ), 'format' => '', 'current' => $current_page, 'total' => $total_pages, 'prev_text' => __( '«', 'aisignal-markdown-converter' ), 'next_text' => __( '»', 'aisignal-markdown-converter' ), ] ) ); ?>
403 ] ); } check_admin_referer( 'aisignal_markdown_converter_clear_crawler_log' ); $this->get_crawler_insights_service()->clear_logs(); wp_safe_redirect( add_query_arg( [ 'page' => 'aisignal-markdown-converter', 'tab' => 'crawler-insights', 'crawler-log-cleared' => 1, ], admin_url( 'options-general.php' ) ) ); exit; } /** * Add per-post markdown settings metaboxes. * * @return void */ public function add_post_settings_meta_boxes(): void { foreach ( Helpers::get_public_post_types() as $post_type ) { add_meta_box( 'aisignal-markdown-converter-post-settings', __( 'AISignal Markdown Converter', 'aisignal-markdown-converter' ), [ $this, 'render_post_settings_meta_box' ], $post_type, 'side', 'default' ); } } /** * Render the per-post markdown settings metabox. * * @param \WP_Post $post Post object. * * @return void */ public function render_post_settings_meta_box( \WP_Post $post ): void { $availability = MarkdownAvailability::get_markdown_availability( $post ); wp_nonce_field( 'aisignal_markdown_converter_post_settings', 'aisignal_markdown_converter_post_settings_nonce' ); ?>

> */ protected function get_general_setting_definitions(): array { return [ 'aisignal_markdown_converter_enable_frontmatter' => [ 'type' => 'boolean', 'sanitize_callback' => [ $this, 'sanitize_frontmatter_enabled' ], 'default' => false, ], 'aisignal_markdown_converter_post_types' => [ 'type' => 'array', 'sanitize_callback' => [ $this, 'sanitize_markdown_post_types' ], 'default' => [ 'post', 'page' ], ], MarkdownAvailability::OPTION_EXCLUDED_POST_IDS => [ 'type' => 'array', 'sanitize_callback' => [ $this, 'sanitize_excluded_post_ids' ], 'default' => [], ], ]; } /** * Get settings definitions for the crawler insights tab. * * @return array> */ protected function get_crawler_setting_definitions(): array { return [ CrawlerInsights::OPTION_ENABLED => [ 'type' => 'boolean', 'sanitize_callback' => [ $this, 'sanitize_crawler_insights_enabled' ], 'default' => false, ], CrawlerInsights::OPTION_RETENTION_DAYS => [ 'type' => 'integer', 'sanitize_callback' => [ $this, 'sanitize_crawler_retention_days' ], 'default' => 30, ], ]; } /** * Get visible public post type objects for the settings UI. * * @return array */ protected function get_public_post_type_objects(): array { $post_types = get_post_types( [ 'public' => true ], 'objects' ); if ( ! is_array( $post_types ) ) { return []; } unset( $post_types['attachment'] ); return array_values( $post_types ); } /** * Return the crawler insights service. * * @return CrawlerInsights */ protected function get_crawler_insights_service(): CrawlerInsights { if ( ! $this->crawler_insights instanceof CrawlerInsights ) { $this->crawler_insights = new CrawlerInsights(); } return $this->crawler_insights; } /** * Determine whether the current admin screen is the plugin settings page. * * @param string $hook_suffix Current admin page hook suffix. * * @return bool */ protected function is_plugin_settings_page( string $hook_suffix ): bool { if ( 'settings_page_aisignal-markdown-converter' === $hook_suffix ) { return true; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin screen routing check. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( (string) $_GET['page'] ) ) : ''; return 'aisignal-markdown-converter' === $page; } }