__( 'Action Scheduler Cleanup', 'ai-eshop-optimizer' ), 'description' => __( 'Active maintenance for Action Scheduler — purges stale runs, defragments the queue, and prevents the slow accumulation of completed jobs that drags every WooCommerce cron tick once the table crosses a million rows.', 'ai-eshop-optimizer' ), 'bullets' => array( __( 'Chunked purge — never times out, never locks the queue.', 'ai-eshop-optimizer' ), __( 'AJAX-flood protection that throttles runaway action handlers before they take the site down.', 'ai-eshop-optimizer' ), __( 'Drop-in maintenance you can leave running — no manual intervention needed.', 'ai-eshop-optimizer' ), ), ); } public function register_hooks() { add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'ajax_cleanup_step' ) ); add_action( 'admin_notices', array( $this, 'maybe_emit_bypass_notice' ) ); } private function read_bypass_setting() { $val = get_option( self::BYPASS_OPT, null ); if ( $val !== null ) { return (bool) $val; } return (bool) get_option( self::BYPASS_OPT_LEGACY, false ); } private function form_nonce() { return str_replace( ' id="aieo_dmm_as_cleanup_form_nonce"', '', wp_nonce_field( self::FORM_NONCE, 'aieo_dmm_as_cleanup_form_nonce', true, false ) ); } public function enqueue_admin_assets() { $css = 'assets/css/aieo-as-cleanup-admin.css'; wp_enqueue_style( 'aieo-as-cleanup-admin', AIEO_URI . $css, array(), file_exists( AIEO_DIR . $css ) ? (string) filemtime( AIEO_DIR . $css ) : '1' ); } public function maybe_emit_bypass_notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen ) { return; } $allowed = array( 'dashboard', 'plugins' ); if ( ! in_array( $screen->id, $allowed, true ) ) { return; } if ( ! $this->read_bypass_setting() ) { return; } printf( '

%s %s

%s

', esc_html__( 'AS BYPASS MODE ACTIVE', 'ai-eshop-optimizer' ), esc_html__( '— All Action Scheduler optimizations are DISABLED. Testing only!', 'ai-eshop-optimizer' ), esc_url( $this->get_url() ), esc_html__( 'Manage AS settings', 'ai-eshop-optimizer' ) ); } public function render_subtab() { if ( ! current_user_can( 'manage_options' ) ) { return; } $form_message = ''; $form_card = ''; if ( $_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_POST['aieo_dmm_as_cleanup_form_nonce'] ) && wp_verify_nonce( wp_unslash( $_POST['aieo_dmm_as_cleanup_form_nonce'] ), self::FORM_NONCE ) ) { if ( isset( $_POST['aieo_dmm_as_toggle_bypass'] ) ) { $current = $this->read_bypass_setting(); update_option( self::BYPASS_OPT, ! $current, false ); delete_option( self::BYPASS_OPT_LEGACY ); $form_card = 'bypass'; $form_message = ! $current ? __( 'Bypass mode ENABLED — reload to apply.', 'ai-eshop-optimizer' ) : __( 'Bypass mode DISABLED — reload to apply.', 'ai-eshop-optimizer' ); } if ( isset( $_POST['aieo_dmm_as_toggle_ajax_protection'] ) ) { $current = (bool) get_option( self::AJAX_PROT_OPT, true ); update_option( self::AJAX_PROT_OPT, ! $current, true ); $form_card = 'ajax'; $form_message = $current ? __( 'AS AJAX query protection DISABLED — reload to apply. (Plugins that read AS state during AJAX may now slow down admin pages.)', 'ai-eshop-optimizer' ) : __( 'AS AJAX query protection ENABLED — reload to apply.', 'ai-eshop-optimizer' ); } } $bypass_all = $this->read_bypass_setting(); $ajax_prot = (bool) get_option( self::AJAX_PROT_OPT, true ); global $wpdb; $table = $wpdb->prefix . 'actionscheduler_actions'; $stats = array(); $hook_breakdown = array(); if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table ) { $stats = $wpdb->get_results( "SELECT status, COUNT(*) as count FROM {$table} GROUP BY status", ARRAY_A ); $hook_breakdown = $wpdb->get_results( "SELECT hook, COUNT(*) as total_pending, COUNT(DISTINCT args) as distinct_args FROM {$table} WHERE status = 'pending' GROUP BY hook ORDER BY total_pending DESC LIMIT 20", ARRAY_A ); } $nonce = wp_create_nonce( self::NONCE_ACTION ); $by_status = array(); foreach ( (array) $stats as $s ) { $by_status[ (string) $s['status'] ] = (int) $s['count']; } $n_finished = ( $by_status['complete'] ?? 0 ) + ( $by_status['failed'] ?? 0 ) + ( $by_status['canceled'] ?? 0 ); $n_pending = $by_status['pending'] ?? 0; $note = ''; if ( $form_message !== '' ) { $note = \Rooster\Admin\Ui::notice( esc_html( $form_message ), 'warning', array( 'inline' => true ) ); } rooster_admin_band_open( array( 'cols' => 2 ) ); list( $open, $close ) = AIEO_DMM_UI::card( array( 'anchor' => 'as-cleanup-bypass-mode-active', 'description' => __('Shows whether Rooster One\'s protections for the Action Scheduler are on, and lets you switch them off while testing.', 'ai-eshop-optimizer'), 'icon' => 'zap', 'title' => $bypass_all ? __( 'Bypass mode active', 'ai-eshop-optimizer' ) : __( 'Optimizations active', 'ai-eshop-optimizer' ), 'meta' => $bypass_all ? \Rooster\Admin\States::status( 'attention' ) : '', 'footer' => \Rooster\Admin\Ui::button( $bypass_all ? __( 'Disable Bypass (Re-enable Optimizations)', 'ai-eshop-optimizer' ) : __( 'Enable Bypass Mode (For Testing)', 'ai-eshop-optimizer' ), array( 'type' => 'submit', 'variant' => $bypass_all ? 'primary' : 'outline', 'attrs' => array( 'name' => 'aieo_dmm_as_toggle_bypass', 'form' => 'aieo-as-bypass-form' ), ) ), ) ); echo $open; if ( 'bypass' === $form_card ) { echo $note; } if ( $bypass_all ) { $state = \Rooster\Admin\Ui::notice( esc_html__( 'Action Scheduler runs without any intervention.', 'ai-eshop-optimizer' ), 'warning', array( 'title' => __( 'All AS optimizations are DISABLED.', 'ai-eshop-optimizer' ), 'inline' => true ) ); echo $state; } else { echo \Rooster\Admin\Guidance::text( __( 'Rooster One optimizations are protecting the site from memory issues and slow queries.', 'ai-eshop-optimizer' ) ); } echo '
'; echo $this->form_nonce(); echo '
'; echo $close; list( $open, $close ) = AIEO_DMM_UI::card( array( 'anchor' => 'as-cleanup-as-ajax-query-protection-active', 'icon' => 'shield', 'title' => __( 'AS AJAX query protection', 'ai-eshop-optimizer' ), 'description' => __( 'Short-circuits Action Scheduler queries during admin AJAX so one request cannot stall for minutes; switch it off only while debugging.', 'ai-eshop-optimizer' ), 'meta' => \Rooster\Admin\States::status( $ajax_prot ? 'active' : 'off' ), 'footer' => \Rooster\Admin\Ui::button( $ajax_prot ? __( 'Disable AS AJAX Query Protection', 'ai-eshop-optimizer' ) : __( 'Re-enable AS AJAX Query Protection', 'ai-eshop-optimizer' ), array( 'type' => 'submit', 'variant' => $ajax_prot ? 'outline' : 'primary', 'attrs' => array( 'name' => 'aieo_dmm_as_toggle_ajax_protection', 'form' => 'aieo-as-ajax-form' ), ) ), ) ); echo $open; if ( 'ajax' === $form_card ) { echo $note; } if ( $ajax_prot ) { echo \Rooster\Admin\Guidance::text( __( 'Short-circuits Action Scheduler SELECT queries on admin AJAX so badly-behaved plugins (AIOSEO, AutomateWoo, WC Admin, etc.) can\'t turn a single AJAX call into a 90s+ delay. Scheduling and processing via cron/CLI are untouched.', 'ai-eshop-optimizer' ), 'guidance', array( 'question' => __( 'What does it do?', 'ai-eshop-optimizer' ) ) ); echo \Rooster\Admin\Guidance::text( __( 'Disable this only if you\'re debugging an AS-related issue or if a custom plugin genuinely needs to read AS state from admin AJAX.', 'ai-eshop-optimizer' ), 'guidance', array( 'question' => __( 'When to turn it off?', 'ai-eshop-optimizer' ) ) ); } else { $state = \Rooster\Admin\Ui::notice( esc_html__( 'Every admin AJAX request can now run heavy Action Scheduler queries. Expect slow admin pages until you re-enable this.', 'ai-eshop-optimizer' ), 'warning', array( 'title' => __( 'AS protection is OFF.', 'ai-eshop-optimizer' ), 'inline' => true ) ); echo $state; } echo '
'; echo $this->form_nonce(); echo '
'; echo $close; rooster_admin_band_close(); rooster_admin_band_open( array( 'cols' => 1 ) ); list( $open, $close ) = AIEO_DMM_UI::card( array( 'anchor' => 'as-cleanup-current-status', 'flush' => true, 'icon' => 'activity', 'title' => __( 'Action queue status', 'ai-eshop-optimizer' ), 'description' => __( 'Row counts by status in wp_actionscheduler_actions, plus the 20 busiest pending hooks, flagged when a hook has more pending rows than distinct arguments.', 'ai-eshop-optimizer' ), ) ); echo $open; AIEO_Table::render( array( 'page_id' => $this->get_id(), 'row_key' => 'status', 'rows' => array_map( static function ( $s ) { return array( 'status' => (string) $s['status'], 'count' => (int) $s['count'] ); }, (array) $stats ), 'columns' => array( array( 'key' => 'status', 'label' => __( 'Status', 'ai-eshop-optimizer' ), 'type' => 'badge' ), array( 'key' => 'count', 'label' => __( 'Count', 'ai-eshop-optimizer' ), 'type' => 'integer' ), ), ) ); $hooks = array(); foreach ( (array) $hook_breakdown as $row ) { $dup = (int) $row['total_pending'] > (int) $row['distinct_args']; $hooks[] = array( 'hook' => (string) $row['hook'], 'pending' => (int) $row['total_pending'], 'args' => (int) $row['distinct_args'], 'dup' => $dup ? __( 'duplicates', 'ai-eshop-optimizer' ) : '', 'dup_tip' => $dup ? __( 'Duplicates detected: more pending than distinct args', 'ai-eshop-optimizer' ) : '', ); } echo rooster_admin_card_section_html( __( 'Top 20 Pending Hooks', 'ai-eshop-optimizer' ), array( 'id' => 'aieo-section-as-cleanup-top-20-pending-hooks' ) ); AIEO_Table::card( array( 'wrap' => false, 'page_id' => $this->get_id(), 'row_key' => 'hook', 'rows' => $hooks, 'caps' => array( 'search' => true ), 'cap' => 20, 'row_class' => static function ( $r ) { return '' !== $r['dup'] ? 'aieo-t-tone--warning' : ''; }, 'columns' => array( array( 'key' => 'hook', 'label' => __( 'Hook', 'ai-eshop-optimizer' ), 'type' => 'code' ), array( 'key' => 'pending', 'label' => __( 'Pending', 'ai-eshop-optimizer' ), 'type' => 'integer', 'suffix' => 'dup', 'tooltip' => 'dup_tip', ), array( 'key' => 'args', 'label' => __( 'Distinct Args', 'ai-eshop-optimizer' ), 'type' => 'integer' ), ), ) ); echo $close; rooster_admin_band_close(); rooster_admin_band_open( array( 'cols' => 2 ) ); list( $open, $close ) = AIEO_DMM_UI::card( array( 'anchor' => 'as-cleanup-cleanup-actions', 'icon' => 'trash', 'title' => __( 'Cleanup Actions', 'ai-eshop-optimizer' ), 'description' => __( 'Diagnose and clean the Action Scheduler tables (wp_actionscheduler_actions/logs/claims/groups). Use the Deep Clean flow in steady state; the Nuclear flow only when blocked-hook actions have piled up.', 'ai-eshop-optimizer' ), 'col' => 1, ) ); echo $open; $finished_line = sprintf( _n( 'Deletes %s finished action (completed, failed or canceled).', 'Deletes %s finished actions (completed, failed or canceled).', $n_finished, 'ai-eshop-optimizer' ), number_format_i18n( $n_finished ) ); $pending_line = sprintf( _n( 'Of the %s pending action, only the newest of each job is kept, and one per singleton hook.', 'Of the %s pending actions, only the newest of each job is kept, and one per singleton hook.', $n_pending, 'ai-eshop-optimizer' ), number_format_i18n( $n_pending ) ); $tidy_line = __( 'Orphaned logs, stale claims and empty groups are cleaned too.', 'ai-eshop-optimizer' ); $final_line = __( 'This cannot be undone.', 'ai-eshop-optimizer' ); $dialogs = array( 'deep_clean' => \Rooster\Admin\States::confirm( 'aieo-as-confirm-deep_clean', 'replace', __( 'Run Deep Clean?', 'ai-eshop-optimizer' ), '

' . esc_html( $finished_line . ' ' . $pending_line ) . '

' . esc_html( $tidy_line . ' ' . $final_line ) . '

', array( 'confirm' => __( 'Run Deep Clean', 'ai-eshop-optimizer' ) ) ), 'purge_finished' => \Rooster\Admin\States::confirm( 'aieo-as-confirm-purge_finished', 'replace', __( 'Purge finished actions?', 'ai-eshop-optimizer' ), '

' . esc_html( $finished_line . ' ' . $tidy_line ) . '

' . esc_html( $final_line ) . '

', array( 'confirm' => __( 'Purge', 'ai-eshop-optimizer' ) ) ), 'deduplicate' => \Rooster\Admin\States::confirm( 'aieo-as-confirm-deduplicate', 'replace', __( 'Deduplicate pending actions?', 'ai-eshop-optimizer' ), '

' . esc_html( $pending_line . ' ' . $final_line ) . '

', array( 'confirm' => __( 'Deduplicate', 'ai-eshop-optimizer' ) ) ), 'nuclear' => \Rooster\Admin\States::confirm( 'aieo-as-confirm-nuclear', 'replace', __( 'Run the NUCLEAR cleanup?', 'ai-eshop-optimizer' ), '

' . esc_html__( 'Deletes every action of a blocked hook, pending ones included.', 'ai-eshop-optimizer' ) . '

' . esc_html( $finished_line . ' ' . $pending_line . ' ' . $tidy_line . ' ' . $final_line ) . '

', array( 'confirm' => __( 'Wipe', 'ai-eshop-optimizer' ) ) ), ); $recommended = \Rooster\Admin\Ui::notice( esc_html__( 'One-click comprehensive cleanup with real-time progress. Purges ALL completed/failed/canceled actions, deduplicates ALL pending hooks (keeps 1 newest per job), cleans orphaned logs, stale claims, and empty groups.', 'ai-eshop-optimizer' ), 'info', array( 'title' => __( 'Recommended: Deep Clean', 'ai-eshop-optimizer' ), 'inline' => true, 'detail' => '

' . esc_html__( 'Processes 50,000 actions per batch. Safe for tables with millions of rows.', 'ai-eshop-optimizer' ) . '

', 'actions' => \Rooster\Admin\Ui::button( __( 'Deep Clean (All-in-One)', 'ai-eshop-optimizer' ), array( 'variant' => 'primary', 'attrs' => array( 'data-aieo-as-flow' => 'deep_clean' ) + \Rooster\Admin\States::confirm_attrs( 'aieo-as-confirm-deep_clean' ) ) ), ) ); echo $recommended; echo rooster_admin_card_section_html( __( 'Individual Actions', 'ai-eshop-optimizer' ), array( 'id' => 'aieo-section-as-cleanup-individual-actions' ) ); $individual = array( 'purge_finished' => array( __( 'Purge Finished Actions (all completed/failed/canceled)', 'ai-eshop-optimizer' ), 'outline' ), 'deduplicate' => array( __( 'Deduplicate ALL Pending Hooks (keep 1 newest per job)', 'ai-eshop-optimizer' ), 'outline' ), 'nuclear' => array( __( 'NUCLEAR: Wipe Blocked Hooks + All Finished', 'ai-eshop-optimizer' ), 'destructive' ), ); foreach ( $individual as $flow_key => $flow_btn ) { $btn = \Rooster\Admin\Ui::button( $flow_btn[0], array( 'variant' => $flow_btn[1], 'attrs' => array( 'data-aieo-as-flow' => $flow_key ) + \Rooster\Admin\States::confirm_attrs( 'aieo-as-confirm-' . $flow_key ) ) ); echo '

' . $btn . '

'; } echo implode( '', $dialogs ); echo $close; $stop = \Rooster\Admin\Ui::button( __( 'Stop', 'ai-eshop-optimizer' ), array( 'variant' => 'destructive', 'attrs' => array( 'id' => 'aieo-as-stop-btn' ) ) ); list( $open, $close ) = AIEO_DMM_UI::card( array( 'anchor' => 'as-cleanup-cleanup-in-progress', 'icon' => 'timer', 'title' => __( 'Cleanup in Progress', 'ai-eshop-optimizer' ), 'tools' => '0m 00s ' . $stop, 'col' => 1, 'movable' => false, 'attrs' => array( 'hidden' => true ), ) ); echo $open; echo \Rooster\Admin\Ui::progress( 0, 100, array( 'id' => 'aieo-as-progress', 'label' => __( 'Cleanup in Progress', 'ai-eshop-optimizer' ), 'show_value' => true ) ); echo '
    '; echo ''; echo '
    '; echo $close; list( $open, $close ) = AIEO_DMM_UI::card( array( 'anchor' => 'as-cleanup-wp-cli-commands', 'description' => __('Database commands that delete failed, cancelled and old completed actions and orphaned logs, to run from a terminal with WP-CLI.', 'ai-eshop-optimizer'), 'icon' => 'braces', 'title' => __( 'WP-CLI Commands', 'ai-eshop-optimizer' ), 'col' => 2, ) ); echo $open; echo '
    ';
            echo "# Delete failed/canceled actions\n";
            echo "wp db query \"DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE status IN ('failed','canceled') LIMIT 50000\"\n\n"; 
            echo "# Delete old completed actions\n";
            echo "wp db query \"DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE status = 'complete' LIMIT 50000\"\n\n"; 
            echo "# Clean orphaned logs\n";
            echo "wp db query \"DELETE FROM {$wpdb->prefix}actionscheduler_logs WHERE action_id NOT IN (SELECT action_id FROM {$wpdb->prefix}actionscheduler_actions) LIMIT 100000\""; 
            echo '
    '; echo $close; rooster_admin_band_close(); $this->print_inline_js( $nonce ); } private function print_inline_js( $nonce ) { $ajax_url = admin_url( 'admin-ajax.php' ); ?> prefix . 'actionscheduler_actions'; $logs = $wpdb->prefix . 'actionscheduler_logs'; $claims = $wpdb->prefix . 'actionscheduler_claims'; $groups = $wpdb->prefix . 'actionscheduler_groups'; $batch_size = 50000; $step = isset( $_POST['step'] ) ? sanitize_text_field( wp_unslash( $_POST['step'] ) ) : ''; switch ( $step ) { case 'stats': $statuses = array(); $rows = $wpdb->get_results( "SELECT status, COUNT(*) as cnt FROM {$table} GROUP BY status", ARRAY_A ); foreach ( $rows as $r ) { $statuses[ $r['status'] ] = (int) $r['cnt']; } $total = array_sum( $statuses ); $log_count = 0; $claim_count = 0; $group_count = 0; if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $logs ) ) === $logs ) { $log_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$logs}" ); } if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $claims ) ) === $claims ) { $claim_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$claims}" ); } if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $groups ) ) === $groups ) { $group_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$groups}" ); } $pending_hooks = $wpdb->get_results( "SELECT hook, COUNT(*) as cnt, COUNT(DISTINCT args) as distinct_args FROM {$table} WHERE status = 'pending' GROUP BY hook ORDER BY cnt DESC LIMIT 20", ARRAY_A ); wp_send_json_success( array( 'statuses' => $statuses, 'total' => $total, 'logs' => $log_count, 'claims' => $claim_count, 'groups' => $group_count, 'pending_hooks' => $pending_hooks, ) ); break; case 'purge': $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : 'complete'; if ( ! in_array( $status, array( 'complete', 'failed', 'canceled' ), true ) ) { wp_send_json_error( 'Invalid status' ); } $deleted = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE status = %s LIMIT %d", $status, $batch_size ) ); wp_send_json_success( array( 'deleted' => $deleted, 'done' => ( $deleted < $batch_size ), ) ); break; case 'deduplicate': $dupes = $wpdb->get_results( "SELECT hook, args, COUNT(*) as cnt, MAX(action_id) as keep_id FROM {$table} WHERE status = 'pending' GROUP BY hook, args HAVING cnt > 1" ); $total_deleted = 0; $hooks_cleaned = 0; foreach ( $dupes as $dup ) { $del = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE hook = %s AND args = %s AND status = 'pending' AND action_id != %d", $dup->hook, $dup->args, $dup->keep_id ) ); if ( $del > 0 ) { $total_deleted += $del; $hooks_cleaned++; } } wp_send_json_success( array( 'deleted' => $total_deleted, 'hooks_cleaned' => $hooks_cleaned, ) ); break; case 'deduplicate_singletons': $singleton_hooks = defined( 'AIEO_AS_SINGLETON_HOOKS' ) ? AIEO_AS_SINGLETON_HOOKS : ( defined( 'OM_AS_SINGLETON_HOOKS' ) ? OM_AS_SINGLETON_HOOKS : array() ); if ( empty( $singleton_hooks ) ) { wp_send_json_success( array( 'deleted' => 0, 'hooks_cleaned' => 0 ) ); break; } $total_deleted = 0; $hooks_cleaned = 0; foreach ( $singleton_hooks as $hook ) { $keep_id = $wpdb->get_var( $wpdb->prepare( "SELECT MAX(action_id) FROM {$table} WHERE hook = %s AND status = 'pending'", $hook ) ); if ( ! $keep_id ) { continue; } $del = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE hook = %s AND status = 'pending' AND action_id != %d", $hook, $keep_id ) ); if ( $del > 0 ) { $total_deleted += $del; $hooks_cleaned++; } } wp_send_json_success( array( 'deleted' => $total_deleted, 'hooks_cleaned' => $hooks_cleaned, ) ); break; case 'orphan_logs': if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $logs ) ) !== $logs ) { wp_send_json_success( array( 'deleted' => 0, 'done' => true ) ); break; } $log_batch = 100000; $deleted = (int) $wpdb->query( "DELETE FROM {$logs} WHERE action_id NOT IN (SELECT action_id FROM {$table}) LIMIT {$log_batch}" ); wp_send_json_success( array( 'deleted' => $deleted, 'done' => ( $deleted < $log_batch ), ) ); break; case 'truncate_claims': $count = 0; if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $claims ) ) === $claims ) { $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$claims}" ); if ( $count > 0 ) { $wpdb->query( "TRUNCATE TABLE {$claims}" ); } } wp_send_json_success( array( 'deleted' => $count ) ); break; case 'clean_groups': $deleted = 0; if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $groups ) ) === $groups ) { $doomed = $wpdb->get_results( "SELECT g.group_id, g.slug FROM {$groups} g LEFT JOIN {$table} a ON g.group_id = a.group_id WHERE a.action_id IS NULL", ARRAY_A ); if ( $doomed ) { $ids = implode( ',', array_map( 'intval', array_column( $doomed, 'group_id' ) ) ); $deleted = (int) $wpdb->query( "DELETE g FROM {$groups} g LEFT JOIN {$table} a ON g.group_id = a.group_id WHERE g.group_id IN ({$ids}) AND a.action_id IS NULL" ); if ( function_exists( 'aieo_as_dedup_forget_groups' ) ) { aieo_as_dedup_forget_groups( array_column( $doomed, 'slug', 'group_id' ) ); } } } wp_send_json_success( array( 'deleted' => $deleted ) ); break; case 'nuclear_hooks': $blocked_hooks = defined( 'AIEO_AS_BLOCKED_HOOKS' ) ? AIEO_AS_BLOCKED_HOOKS : ( defined( 'OM_AS_BLOCKED_HOOKS' ) ? OM_AS_BLOCKED_HOOKS : array() ); $blocked_patterns = defined( 'AIEO_AS_BLOCKED_PATTERNS' ) ? AIEO_AS_BLOCKED_PATTERNS : ( defined( 'OM_AS_BLOCKED_PATTERNS' ) ? OM_AS_BLOCKED_PATTERNS : array() ); $total = 0; $all_done = true; foreach ( $blocked_hooks as $hook ) { $del = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE hook = %s LIMIT %d", $hook, $batch_size ) ); $total += $del; if ( $del >= $batch_size ) { $all_done = false; } } foreach ( $blocked_patterns as $pattern ) { $safe = $wpdb->esc_like( $pattern ) . '%'; $del = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE hook LIKE %s LIMIT %d", $safe, $batch_size ) ); $total += $del; if ( $del >= $batch_size ) { $all_done = false; } } wp_send_json_success( array( 'deleted' => $total, 'done' => $all_done, ) ); break; default: wp_send_json_error( 'Unknown step: ' . $step ); } } }