' . esc_html__( 'The sync report has expired. Run Sync content with AI again to see results.', 'ai-search' ) . '

'; } else { $ok_count = count( $report['ok'] ); $failed_count = count( $report['failed'] ); if ( (int) $report['requested'] === 0 ) { echo '

' . esc_html__( 'Nothing to do: every published item of the selected type is already synced. To force a re-sync, delete existing data in the Cache & Clean tab first.', 'ai-search' ) . '

'; } else { $class = $failed_count === 0 ? 'notice-success' : ( $ok_count === 0 ? 'notice-error' : 'notice-warning' ); echo '

'; printf( /* translators: 1: number synced, 2: number failed, 3: number requested */ esc_html__( 'Sync finished: %1$d synced, %2$d failed (of %3$d selected).', 'ai-search' ), (int) $ok_count, (int) $failed_count, (int) $report['requested'] ); echo ''; if ( ! empty( $report['stopped'] ) ) { echo ' ' . esc_html__( 'Sync was stopped early because the provider returned an error that would repeat for every item. Fix the cause below and run again — items already synced are kept.', 'ai-search' ); } echo '

'; } if ( $failed_count > 0 ) { // Group failures by error code so one root cause reads as one line. $groups = []; foreach ( $report['failed'] as $f ) { $code = isset( $f['code'] ) ? (string) $f['code'] : 'unknown'; if ( ! isset( $groups[ $code ] ) ) { $groups[ $code ] = [ 'message' => (string) $f['message'], 'titles' => [] ]; } $groups[ $code ]['titles'][] = (string) $f['title']; } echo '

' . esc_html__( 'Why items failed:', 'ai-search' ) . '

'; echo '

' . esc_html__( 'The same details are written to the PHP error log (wp-content/debug.log when WP_DEBUG_LOG is on).', 'ai-search' ) . '

'; } if ( $ok_count > 0 ) { echo '
'; /* translators: %d: number of items */ printf( esc_html__( 'Show %d generated items', 'ai-search' ), (int) $ok_count ); echo '
'; } } } ?> true, 'exclude_from_search' => false ], 'objects' ); unset( $ai_status_types['attachment'] ); $ai_status_rows = $wpdb->get_results( "SELECT p.post_type, COUNT(*) AS total, SUM( CASE WHEN pm.post_id IS NULL THEN 0 ELSE 1 END ) AS indexed FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_ai_search_embedding' WHERE p.post_status = 'publish' GROUP BY p.post_type", OBJECT_K ); $ai_status_pending = 0; // v1.35.0: per-model breakdown per post type. Posts embedded before 1.35 // do not have the _ai_search_embedding_model sidecar - they show up in // the "unknown" bucket and get resolved the moment the user regenerates. $ai_model_rows = $wpdb->get_results( "SELECT p.post_type, COALESCE( mm.meta_value, '' ) AS model, COUNT(*) AS c FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_ai_search_embedding' LEFT JOIN {$wpdb->postmeta} mm ON mm.post_id = p.ID AND mm.meta_key = '_ai_search_embedding_model' WHERE p.post_status = 'publish' GROUP BY p.post_type, model" ); $ai_status_by_model = []; foreach ( (array) $ai_model_rows as $r ) { $pt = (string) $r->post_type; $model = (string) $r->model; $bucket = 'unknown'; if ( 'text-embedding-3-large' === $model ) { $bucket = 'large'; } elseif ( 'text-embedding-3-small' === $model ) { $bucket = 'small'; } if ( ! isset( $ai_status_by_model[ $pt ] ) ) { $ai_status_by_model[ $pt ] = [ 'large' => 0, 'small' => 0, 'unknown' => 0 ]; } $ai_status_by_model[ $pt ][ $bucket ] += (int) $r->c; } ?>

name ] ) ? $ai_status_rows[ $ai_status_type->name ] : null; $total = $row ? (int) $row->total : 0; $indexed = $row ? (int) $row->indexed : 0; $missing = max( 0, $total - $indexed ); $ai_status_pending += $missing; if ( 0 === $total ) { continue; } $models = isset( $ai_status_by_model[ $ai_status_type->name ] ) ? $ai_status_by_model[ $ai_status_type->name ] : [ 'large' => 0, 'small' => 0, 'unknown' => 0 ]; $ai_status_unknown_total += $models['unknown']; ?>
labels->name ); ?> —'; } else { $parts = []; if ( $models['large'] > 0 ) { $parts[] = '' . esc_html( number_format_i18n( $models['large'] ) ) . ' ' . esc_html__( 'on large', 'ai-search' ); } if ( $models['small'] > 0 ) { $parts[] = '' . esc_html( number_format_i18n( $models['small'] ) ) . ' ' . esc_html__( 'on small', 'ai-search' ); } if ( $models['unknown'] > 0 ) { $parts[] = '' . esc_html( number_format_i18n( $models['unknown'] ) ) . ' ' . esc_html__( 'unknown', 'ai-search' ) . ''; } echo wp_kses( implode( ' · ', $parts ), [ 'strong' => [], 'span' => [ 'class' => true ] ] ); } ?> 0 ) { echo '' . esc_html( number_format_i18n( $missing ) ) . ''; } else { echo '' . esc_html__( 'Up to date', 'ai-search' ) . ''; } ?>
0 ) : ?>

' . esc_html( number_format_i18n( $ai_status_unknown_total ) ) . '' ); ?>

0 ) : ?>

' . esc_html( number_format_i18n( $ai_status_pending ) ) . '' ); ?>

clear, then Indexing > generate) is still // available for anyone who wants it, but this covers the common case // right after upgrading plans without leaving the tab. ?>