| @@ -78,15 +78,45 @@ | ||
| 78 | 78 | * Constructor |
| 79 | 79 | */ |
| 80 | 80 | public function __construct() { |
| 81 | 81 | $this->board_id = WPF()->board->get_current( 'boardid' ); |
| 82 | - $this->register_cron_hooks(); | |
| 83 | 82 | |
| 83 | + // Register the callback for the cron hook so any already-scheduled | |
| 84 | + // cron event from a prior connected state can still execute (and so | |
| 85 | + // schedule_cron_jobs() can rely on the action being wired). Scheduling | |
| 86 | + // of the event itself is gated by AI connection — see schedule_cron_jobs(). | |
| 87 | + add_action( 'wpforo_ai_cleanup_expired_cache', [ $this, 'cleanup_expired_cache' ] ); | |
| 88 | + | |
| 84 | 89 | // Update board_id when WPF()->change_board() is called |
| 85 | 90 | add_action( 'wpforo_after_change_board', [ $this, 'on_board_change' ] ); |
| 86 | 91 | } |
| 87 | 92 | |
| 88 | 93 | /** |
| 94 | + * Schedule recurring local-storage maintenance crons. | |
| 95 | + * | |
| 96 | + * Called from AIClient::register_ai_crons() on tenant connect. | |
| 97 | + * Safe to call repeatedly — it skips if already scheduled. | |
| 98 | + */ | |
| 99 | + public function schedule_cron_jobs() { | |
| 100 | + if ( ! wp_next_scheduled( 'wpforo_ai_cleanup_expired_cache' ) ) { | |
| 101 | + wp_schedule_event( time(), 'hourly', 'wpforo_ai_cleanup_expired_cache' ); | |
| 102 | + } | |
| 103 | + } | |
| 104 | + | |
| 105 | + /** | |
| 106 | + * Unschedule recurring local-storage maintenance crons. | |
| 107 | + * | |
| 108 | + * Called from AIClient::unregister_ai_crons() on tenant disconnect. | |
| 109 | + */ | |
| 110 | + public function unschedule_cron_jobs() { | |
| 111 | + $ts = wp_next_scheduled( 'wpforo_ai_cleanup_expired_cache' ); | |
| 112 | + if ( $ts ) { | |
| 113 | + wp_unschedule_event( $ts, 'wpforo_ai_cleanup_expired_cache' ); | |
| 114 | + } | |
| 115 | + wp_clear_scheduled_hook( 'wpforo_ai_cleanup_expired_cache' ); | |
| 116 | + } | |
| 117 | + | |
| 118 | + /** | |
| 89 | 119 | * Handle board change event |
| 90 | 120 | * |
| 91 | 121 | * Updates internal board_id to match the new board context. |
| 92 | 122 | * Called by wpforo_after_change_board hook. |
| @@ -98,21 +128,8 @@ | ||
| 98 | 128 | $this->storage_mode = null; // Reset cached storage mode |
| 99 | 129 | } |
| 100 | 130 | |
| 101 | 131 | /** |
| 102 | - * Register cron hooks for local storage maintenance | |
| 103 | - */ | |
| 104 | - private function register_cron_hooks() { | |
| 105 | - // Register cleanup action - must be done here so callback exists when cron fires | |
| 106 | - add_action( 'wpforo_ai_cleanup_expired_cache', [ $this, 'cleanup_expired_cache' ] ); | |
| 107 | - | |
| 108 | - // Schedule if not already scheduled | |
| 109 | - if ( ! wp_next_scheduled( 'wpforo_ai_cleanup_expired_cache' ) ) { | |
| 110 | - wp_schedule_event( time(), 'hourly', 'wpforo_ai_cleanup_expired_cache' ); | |
| 111 | - } | |
| 112 | - } | |
| 113 | - | |
| 114 | - /** | |
| 115 | 132 | * Cleanup expired cache entries (cron callback) |
| 116 | 133 | */ |
| 117 | 134 | public function cleanup_expired_cache() { |
| 118 | 135 | if ( $this->is_local_mode() ) { |
| @@ -314,8 +331,12 @@ | ||
| 314 | 331 | * |
| 315 | 332 | * Reads the queue and settings options to calculate how many topics |
| 316 | 333 | * have been processed out of the total queued for indexing. |
| 317 | 334 | * |
| 335 | + * Checks both queue key patterns: | |
| 336 | + * - wpforo_ai_indexing_queue_{board_id} (manual reindex via UI) | |
| 337 | + * - wpforo_ai_indexing_queue_local_{board_id} (auto-indexing new topics) | |
| 338 | + * | |
| 318 | 339 | * @param int $board_id Board ID |
| 319 | 340 | * @return int Progress percentage (0-100), or 0 if no indexing in progress |
| 320 | 341 | */ |
| 321 | 342 | private function calculate_local_indexing_progress( $board_id ) { |
| @@ -327,13 +348,21 @@ | ||
| 327 | 348 | if ( $total <= 0 ) { |
| 328 | 349 | return 0; |
| 329 | 350 | } |
| 330 | 351 | |
| 331 | - // Count remaining topics in queue | |
| 332 | - $queue_key = 'wpforo_ai_indexing_queue_' . $board_id; | |
| 333 | - $pending = get_option( $queue_key, [] ); | |
| 334 | - $remaining = is_array( $pending ) ? count( $pending ) : 0; | |
| 352 | + // Count remaining topics in both queue patterns: | |
| 353 | + // - Manual reindex queue (legacy pattern without mode prefix) | |
| 354 | + // - Auto-indexing queue (new pattern with mode prefix) | |
| 355 | + $manual_queue_key = 'wpforo_ai_indexing_queue_' . $board_id; | |
| 356 | + $auto_queue_key = 'wpforo_ai_indexing_queue_local_' . $board_id; | |
| 335 | 357 | |
| 358 | + $manual_pending = get_option( $manual_queue_key, [] ); | |
| 359 | + $auto_pending = get_option( $auto_queue_key, [] ); | |
| 360 | + | |
| 361 | + $manual_remaining = is_array( $manual_pending ) ? count( $manual_pending ) : 0; | |
| 362 | + $auto_remaining = is_array( $auto_pending ) ? count( $auto_pending ) : 0; | |
| 363 | + $remaining = $manual_remaining + $auto_remaining; | |
| 364 | + | |
| 336 | 365 | // Calculate progress |
| 337 | 366 | $processed = $total - $remaining; |
| 338 | 367 | if ( $processed < 0 ) { |
| 339 | 368 | $processed = 0; // Safety: queue grew larger than original total |
| @@ -356,11 +385,16 @@ | ||
| 356 | 385 | if ( is_wp_error( $rag_status ) ) { |
| 357 | 386 | $rag_status = []; |
| 358 | 387 | } |
| 359 | 388 | |
| 360 | - // Only report is_indexing when the backend is actively processing. | |
| 361 | - // Queued WP Cron topics are reported separately via pending_cron_jobs. | |
| 362 | - $is_indexing = (bool) ( $rag_status['is_indexing'] ?? false ); | |
| 389 | + // Report is_indexing when: | |
| 390 | + // 1. Backend is actively processing (from API response), OR | |
| 391 | + // 2. WP-Cron is actively processing cloud queue locally (lock transient held) | |
| 392 | + // This ensures UI shows indexing state even when API response is stale. | |
| 393 | + $board_id = $this->board_id; | |
| 394 | + $backend_indexing = (bool) ( $rag_status['is_indexing'] ?? false ); | |
| 395 | + $local_cron_indexing = (bool) get_transient( 'wpforo_ai_indexing_lock_cloud_' . $board_id ); | |
| 396 | + $is_indexing = $backend_indexing || $local_cron_indexing; | |
| 363 | 397 | |
| 364 | 398 | // Use local cloud column for accurate count (reflects manual changes) |
| 365 | 399 | // Cache for 5 minutes to avoid heavy COUNT on large forums |
| 366 | 400 | $cache_key = 'wpforo_ai_cloud_indexed_count_' . $this->board_id; |
| @@ -529,13 +563,15 @@ | ||
| 529 | 563 | if ( isset( $topic['status'] ) && (int) $topic['status'] !== 0 ) { |
| 530 | 564 | return new \WP_Error( 'unapproved_topic', wpforo_phrase( 'Unapproved topics cannot be indexed', false ) ); |
| 531 | 565 | } |
| 532 | 566 | |
| 533 | - // Get all posts for this topic ordered by creation date | |
| 567 | + // Get all approved posts for this topic ordered by creation date | |
| 568 | + // Only index approved posts (status=0) to prevent unapproved content from appearing in search | |
| 534 | 569 | $posts = WPF()->post->get_posts( [ |
| 535 | 570 | 'topicid' => $topicid, |
| 536 | 571 | 'orderby' => 'created', |
| 537 | 572 | 'order' => 'ASC', |
| 573 | + 'status' => 0, | |
| 538 | 574 | ] ); |
| 539 | 575 | if ( empty( $posts ) ) { |
| 540 | 576 | return new \WP_Error( 'no_posts', wpforo_phrase( 'No posts found for topic', false ) ); |
| 541 | 577 | } |
| @@ -792,12 +828,14 @@ | ||
| 792 | 828 | } |
| 793 | 829 | |
| 794 | 830 | // Bypass user permission check - this is admin-initiated backend indexing. |
| 795 | 831 | // Private/unapproved topics are already filtered above. |
| 832 | + // Only index approved posts (status=0) to prevent unapproved content from appearing in search. | |
| 796 | 833 | $posts = WPF()->post->get_posts( [ |
| 797 | 834 | 'topicid' => $topicid, |
| 798 | 835 | 'orderby' => 'created', |
| 799 | 836 | 'order' => 'ASC', |
| 837 | + 'status' => 0, | |
| 800 | 838 | 'check_private' => false, |
| 801 | 839 | ] ); |
| 802 | 840 | |
| 803 | 841 | if ( empty( $posts ) ) { |
| @@ -3216,8 +3254,15 @@ | ||
| 3216 | 3254 | |
| 3217 | 3255 | $column = $this->is_local_mode() ? 'local' : 'cloud'; |
| 3218 | 3256 | $topics_table = WPF()->tables->topics; |
| 3219 | 3257 | |
| 3258 | + // Cache for 1 day - these counts don't change frequently | |
| 3259 | + $cache_key = 'wpforo_ai_status_breakdown_' . $this->board_id . '_' . $this->get_storage_mode(); | |
| 3260 | + $cached = get_transient( $cache_key ); | |
| 3261 | + if ( false !== $cached && is_array( $cached ) ) { | |
| 3262 | + return $cached; | |
| 3263 | + } | |
| 3264 | + | |
| 3220 | 3265 | // Get counts for each category in a single query |
| 3221 | 3266 | $results = $wpdb->get_row( |
| 3222 | 3267 | "SELECT |
| 3223 | 3268 | COUNT(*) as total, |
| @@ -3239,9 +3284,9 @@ | ||
| 3239 | 3284 | 'storage_mode' => $this->get_storage_mode(), |
| 3240 | 3285 | ]; |
| 3241 | 3286 | } |
| 3242 | 3287 | |
| 3243 | - return [ | |
| 3288 | + $breakdown = [ | |
| 3244 | 3289 | 'total' => (int) $results['total'], |
| 3245 | 3290 | 'indexed' => (int) $results['indexed'], |
| 3246 | 3291 | 'pending' => (int) $results['pending'], |
| 3247 | 3292 | 'private' => (int) $results['private_topics'], |
| @@ -3247,8 +3292,24 @@ | ||
| 3247 | 3292 | 'private' => (int) $results['private_topics'], |
| 3248 | 3293 | 'unapproved' => (int) $results['unapproved'], |
| 3249 | 3294 | 'storage_mode' => $this->get_storage_mode(), |
| 3250 | 3295 | ]; |
| 3296 | + | |
| 3297 | + set_transient( $cache_key, $breakdown, DAY_IN_SECONDS ); | |
| 3298 | + | |
| 3299 | + return $breakdown; | |
| 3300 | + } | |
| 3301 | + | |
| 3302 | + /** | |
| 3303 | + * Clear the indexing status breakdown cache | |
| 3304 | + * | |
| 3305 | + * Should be called when topics are approved/unapproved or privacy changes. | |
| 3306 | + */ | |
| 3307 | + public function clear_indexing_status_breakdown_cache() { | |
| 3308 | + $cache_key_local = 'wpforo_ai_status_breakdown_' . $this->board_id . '_local'; | |
| 3309 | + $cache_key_cloud = 'wpforo_ai_status_breakdown_' . $this->board_id . '_cloud'; | |
| 3310 | + delete_transient( $cache_key_local ); | |
| 3311 | + delete_transient( $cache_key_cloud ); | |
| 3251 | 3312 | } |
| 3252 | 3313 | |
| 3253 | 3314 | /** |
| 3254 | 3315 | * Get sample topics that are pending indexing |