WP Data Optimizer page with tabs: * Dashboard, Zones, Migration, Classifier, HPCT Import, Logs. */ class TMDO_Admin { /** Menu slug. */ /** * Admin page slug. * * Kept as `wp-data-optimizer` on purpose: this plugin already inherits the * wpdo_ table / option / cron / hook / CLI namespace, existing bookmarks and * sister-plugin links point here, and ~10 links inside this plugin (help tabs, * setup wizard, conflict monitor, stress-test templates) hardcode it. */ private const MENU_SLUG = 'wp-data-optimizer'; /** Nonce action. */ private const NONCE_ACTION = 'wpdo_admin_action'; /** * Boot admin hooks. */ public static function init(): void { add_action( 'admin_menu', array( __CLASS__, 'register_menu' ) ); add_action( 'network_admin_menu', array( __CLASS__, 'register_menu' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) ); add_action( 'network_admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) ); add_action( 'wp_ajax_wpdo_admin_action', array( __CLASS__, 'handle_ajax' ) ); add_action( 'admin_notices', array( __CLASS__, 'maybe_nginx_backup_notice' ) ); } /** * Show a one-time admin notice when the site runs on nginx and snapshot files exist. * Nginx ignores .htaccess so wpdo-backups/ is publicly accessible without a deny rule. * * @return void */ public static function maybe_nginx_backup_notice(): void { if ( ! TMDO_Capability::current_user_can_admin() ) { return; } // Only fire on nginx — Apache's .htaccess already protects the directory. $server_software = isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : ''; if ( stripos( $server_software, 'nginx' ) === false ) { return; } // Only warn when the backup directory actually contains snapshot files. if ( ! class_exists( 'TMDO_Snapshot_Manager' ) ) { return; } $dir = TMDO_Snapshot_Manager::backup_dir(); $files = glob( trailingslashit( $dir ) . '*.gz' ); if ( empty( $files ) ) { return; } $notice_id = 'wpdo_nginx_backup_dismissed'; if ( get_user_meta( get_current_user_id(), $notice_id, true ) ) { return; } $snippet = 'location ~* /wpdo-backups/ { deny all; }'; printf( '

WP Data Optimizer: %s
%s

', esc_attr( $notice_id ), esc_html__( 'Your server runs nginx. Snapshot backup files may be publicly accessible. Add this rule to your nginx site config:', '2meet-data-optimizer' ), esc_html( $snippet ) ); } /** * Register the admin menu page under Tools. */ public static function register_menu(): void { add_management_page( __( 'WP Data Optimizer', '2meet-data-optimizer' ), __( 'WP Data Optimizer', '2meet-data-optimizer' ), 'manage_options', self::MENU_SLUG, array( __CLASS__, 'render_page' ) ); } /** * Enqueue admin CSS/JS on our page only. * * @param string $hook_suffix The current admin page hook suffix. * @return void */ public static function enqueue_assets( string $hook_suffix ): void { $wpdo_pages = array( 'tools_page_wp-data-optimizer', 'tools_page_wpdo-setup-wizard' ); if ( ! in_array( $hook_suffix, $wpdo_pages, true ) ) { return; } // Register Morandi design system so wpdo-admin can declare it as a CSS dependency // instead of doing a blocking @import. Skip registration if the file is missing — // the plugin still functions, just without the design tokens. $morandi_path = get_stylesheet_directory() . '/morandi-design-system.css'; $morandi_url = get_stylesheet_directory_uri() . '/morandi-design-system.css'; $deps = array(); if ( file_exists( $morandi_path ) && ! wp_style_is( 'morandi-design-system', 'registered' ) ) { wp_register_style( 'morandi-design-system', $morandi_url, array(), (string) filemtime( $morandi_path ) ); } if ( wp_style_is( 'morandi-design-system', 'registered' ) ) { $deps[] = 'morandi-design-system'; } wp_enqueue_style( 'wpdo-admin', TMDO_URL . 'admin/assets/wpdo-admin.css', $deps, TMDO_VERSION ); wp_enqueue_script( 'wpdo-admin', TMDO_URL . 'admin/assets/wpdo-admin.js', array(), TMDO_VERSION, true ); wp_localize_script( 'wpdo-admin', 'wpdoAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::NONCE_ACTION ), 'exportNonce' => wp_create_nonce( 'wpdo_export' ), 'i18n' => array( 'flushing' => __( 'Flushing…', '2meet-data-optimizer' ), 'flushed' => __( 'Flushed!', '2meet-data-optimizer' ), 'flushLabel' => __( 'Flush Cache', '2meet-data-optimizer' ), 'error' => __( '操作失敗,請重試。', '2meet-data-optimizer' ), 'retry' => __( '重試', '2meet-data-optimizer' ), 'dismiss' => __( '關閉', '2meet-data-optimizer' ), /* translators: 1: module name (e.g. hot_hp_listing), 2: new state (e.g. cutover) */ 'moduleState' => __( '模組 %1$s 狀態已更新為 %2$s', '2meet-data-optimizer' ), ), ) ); // Enqueue REST SDK and inject config (for REST API tab preview). wp_enqueue_script( 'wpdo-rest-sdk', TMDO_URL . 'admin/assets/wpdo-rest-sdk.js', array(), TMDO_VERSION, true ); wp_localize_script( 'wpdo-rest-sdk', 'wpdo_sdk_config', array( 'rest_url' => esc_url_raw( rest_url( 'wpdo/v1' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'post_type' => 'hp_listing', ) ); // v2.6.6: Entity Bridge polling + actions JS. wp_enqueue_script( 'wpdo-entity-bridge', TMDO_URL . 'admin/assets/wpdo-entity-bridge.js', array(), TMDO_VERSION, true ); wp_localize_script( 'wpdo-entity-bridge', 'wpdoEntityBridge', array( 'restUrl' => esc_url_raw( rest_url( 'wpdo/v1' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), ) ); // v2.6.7: User stress-test JS. wp_enqueue_script( 'wpdo-stress-test', TMDO_URL . 'admin/assets/wpdo-stress-test.js', array(), TMDO_VERSION, true ); wp_localize_script( 'wpdo-stress-test', 'wpdoStressTest', array( 'restUrl' => esc_url_raw( rest_url( 'wpdo/v1' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), ) ); // v2.11.4: Post stress-test JS (separate script, coexists with user side). wp_enqueue_script( 'wpdo-post-stress-test', TMDO_URL . 'admin/assets/wpdo-post-stress-test.js', array(), TMDO_VERSION, true ); wp_localize_script( 'wpdo-post-stress-test', 'wpdoPostStressTest', array( 'restUrl' => esc_url_raw( rest_url( 'wpdo/v1' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'postMode' => class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'post' ) : 'disabled', ) ); // v2.13.0: Term stress-test JS. wp_enqueue_script( 'wpdo-term-stress-test', TMDO_URL . 'admin/assets/wpdo-term-stress-test.js', array(), TMDO_VERSION, true ); wp_localize_script( 'wpdo-term-stress-test', 'wpdoTermStressTest', array( 'restUrl' => esc_url_raw( rest_url( 'wpdo/v1' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'termMode' => class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'term' ) : 'disabled', ) ); // v2.13.1: Comment stress-test JS. wp_enqueue_script( 'wpdo-comment-stress-test', TMDO_URL . 'admin/assets/wpdo-comment-stress-test.js', array(), TMDO_VERSION, true ); wp_localize_script( 'wpdo-comment-stress-test', 'wpdoCommentStressTest', array( 'restUrl' => esc_url_raw( rest_url( 'wpdo/v1' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'commentMode' => class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'comment' ) : 'disabled', ) ); // v2.8.0: One-click User Migration Wizard. wp_enqueue_style( 'wpdo-migration-wizard', TMDO_URL . 'admin/assets/wpdo-migration-wizard.css', array( 'wpdo-admin' ), TMDO_VERSION ); wp_enqueue_script( 'wpdo-migration-wizard', TMDO_URL . 'admin/assets/wpdo-migration-wizard.js', array(), TMDO_VERSION, true ); wp_localize_script( 'wpdo-migration-wizard', 'wpdoMigrationWizard', array( 'restUrl' => esc_url_raw( rest_url( 'wpdo/v1' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'i18n' => array( 'confirmStart' => __( '確定執行?流程啟動後會立即開始備份 + 遷移。', '2meet-data-optimizer' ), 'confirmCancel' => __( '確定要取消?已執行的階段會自動 rollback 到 dual_write。', '2meet-data-optimizer' ), 'nothingToDo' => __( '✅ 所有 entity group 已完成遷移,無事可做。', '2meet-data-optimizer' ), 'failedRetry' => __( '失敗 — 點選「Resume」重試,或「Cancel」結束 job。', '2meet-data-optimizer' ), ), ) ); } /** * Render the admin page. */ public static function render_page(): void { if ( ! TMDO_Capability::current_user_can_admin() ) { return; } // Handle rate limit stats reset. if ( isset( $_POST['wpdo_reset_rl_stats'] ) && check_admin_referer( 'wpdo_reset_rl_stats' ) ) { delete_option( 'wpdo_rl_stats' ); wp_safe_redirect( remove_query_arg( array( 'wpdo_reset_rl_stats', '_wpnonce' ) ) ); exit; } // v2.5.0 M16: one-click enable a module from suggestions tab. if ( isset( $_POST['wpdo_enable_module'] ) && check_admin_referer( 'wpdo_enable_module' ) && TMDO_Capability::current_user_can_admin() ) { $module = sanitize_key( wp_unslash( (string) $_POST['wpdo_enable_module'] ) ); $flag = 'enable_failed'; if ( '' !== $module && class_exists( 'TMDO_Feature_Flags' ) ) { $result = TMDO_Feature_Flags::set( $module, 'dual_write' ); if ( true === $result ) { $flag = 'module_enabled'; // Bust detector transient + cache so the suggestion disappears immediately. if ( class_exists( 'TMDO_Module_Detector' ) ) { delete_transient( 'wpdo_module_detector_results' ); } } elseif ( $result instanceof \WP_Error ) { $flag = 'enable_blocked'; } } wp_safe_redirect( add_query_arg( array( 'tab' => 'module-suggestions', 'wpdo_msg' => $flag, 'wpdo_module' => $module, ), remove_query_arg( array( 'wpdo_enable_module', '_wpnonce' ) ) ) ); exit; } // v2.4.0 M10 + v2.5.0 M13: settings save (email alerts + automator). if ( isset( $_POST['wpdo_save_settings'] ) && check_admin_referer( 'wpdo_save_settings' ) && TMDO_Capability::current_user_can_admin() ) { update_option( 'wpdo_email_alerts_enabled', isset( $_POST['wpdo_email_alerts_enabled'] ) ? '1' : '0', false ); $recipient = isset( $_POST['wpdo_alert_email'] ) ? sanitize_email( wp_unslash( (string) $_POST['wpdo_alert_email'] ) ) : ''; update_option( 'wpdo_alert_email', $recipient, false ); $throttle = isset( $_POST['wpdo_alert_throttle_hours'] ) ? max( 1, min( 168, (int) $_POST['wpdo_alert_throttle_hours'] ) ) : 24; update_option( 'wpdo_alert_throttle_hours', $throttle, false ); // v2.5.0 M13: FSM Automator settings. update_option( 'wpdo_automator_enabled', isset( $_POST['wpdo_automator_enabled'] ) ? '1' : '0', false ); $blacklist = isset( $_POST['wpdo_automator_blacklist'] ) && is_array( $_POST['wpdo_automator_blacklist'] ) ? array_values( array_filter( array_map( 'sanitize_key', wp_unslash( $_POST['wpdo_automator_blacklist'] ) ) ) ) : array(); update_option( 'wpdo_automator_blacklist', $blacklist, false ); // v2.5.4: Entity Bridge settings (v2.11.0: post added). update_option( 'wpdo_hook_bus_enabled', isset( $_POST['wpdo_hook_bus_enabled'] ) ? '1' : '0', false ); if ( class_exists( 'TMDO_Mode_Manager' ) ) { foreach ( array( 'user', 'post', 'term', 'comment' ) as $entity_type ) { $new_mode = isset( $_POST[ 'wpdo_bridge_mode_' . $entity_type ] ) ? sanitize_key( wp_unslash( (string) $_POST[ 'wpdo_bridge_mode_' . $entity_type ] ) ) : ''; if ( TMDO_Mode_Manager::is_valid_mode( $new_mode ) ) { TMDO_Mode_Manager::set( $entity_type, $new_mode ); } } TMDO_Mode_Manager::reset_cache(); } if ( class_exists( 'TMDO_Hook_Bus_Bridge' ) ) { TMDO_Hook_Bus_Bridge::reset_cache(); } // v2.11.6: HivePress transient filter toggle. update_option( 'wpdo_hp_transient_filter_enabled', isset( $_POST['wpdo_hp_transient_filter_enabled'] ) ? '1' : '0', false ); // v2.12.1: Term + Comment garbage filter toggle. update_option( 'wpdo_term_comment_garbage_filter_enabled', isset( $_POST['wpdo_term_comment_garbage_filter_enabled'] ) ? '1' : '0', false ); // v2.12.3: WC term count filter toggle. update_option( 'wpdo_wc_term_count_filter_enabled', isset( $_POST['wpdo_wc_term_count_filter_enabled'] ) ? '1' : '0', false ); // v2.12.4: Term + Comment misc bucket toggle. update_option( 'wpdo_term_comment_misc_bucket_enabled', isset( $_POST['wpdo_term_comment_misc_bucket_enabled'] ) ? '1' : '0', false ); // v2.5.0 M15: multi-channel notifiers. foreach ( array( 'slack', 'discord', 'telegram' ) as $ch ) { update_option( "wpdo_{$ch}_enabled", isset( $_POST[ "wpdo_{$ch}_enabled" ] ) ? '1' : '0', false ); $thr = isset( $_POST[ "wpdo_{$ch}_throttle_hours" ] ) ? max( 1, min( 168, (int) $_POST[ "wpdo_{$ch}_throttle_hours" ] ) ) : 24; update_option( "wpdo_{$ch}_throttle_hours", $thr, false ); $sev = isset( $_POST[ "wpdo_{$ch}_severity" ] ) ? sanitize_key( wp_unslash( (string) $_POST[ "wpdo_{$ch}_severity" ] ) ) : 'critical_only'; if ( ! in_array( $sev, array( 'critical_only', 'critical_and_recommended' ), true ) ) { $sev = 'critical_only'; } update_option( "wpdo_{$ch}_severity", $sev, false ); } // Webhook secrets — encrypt at rest via TMDO_Crypto. if ( isset( $_POST['wpdo_slack_webhook'] ) ) { $webhook = esc_url_raw( wp_unslash( (string) $_POST['wpdo_slack_webhook'] ) ); if ( '' !== $webhook ) { class_exists( 'TMDO_Crypto' ) ? TMDO_Crypto::set_option( 'wpdo_slack_webhook', $webhook ) : update_option( 'wpdo_slack_webhook', $webhook, false ); } } if ( isset( $_POST['wpdo_discord_webhook'] ) ) { $webhook = esc_url_raw( wp_unslash( (string) $_POST['wpdo_discord_webhook'] ) ); if ( '' !== $webhook ) { class_exists( 'TMDO_Crypto' ) ? TMDO_Crypto::set_option( 'wpdo_discord_webhook', $webhook ) : update_option( 'wpdo_discord_webhook', $webhook, false ); } } if ( isset( $_POST['wpdo_telegram_bot_token'] ) ) { $token = sanitize_text_field( wp_unslash( (string) $_POST['wpdo_telegram_bot_token'] ) ); if ( '' !== $token ) { class_exists( 'TMDO_Crypto' ) ? TMDO_Crypto::set_option( 'wpdo_telegram_bot_token', $token ) : update_option( 'wpdo_telegram_bot_token', $token, false ); } } if ( isset( $_POST['wpdo_telegram_chat_id'] ) ) { $chat = sanitize_text_field( wp_unslash( (string) $_POST['wpdo_telegram_chat_id'] ) ); update_option( 'wpdo_telegram_chat_id', $chat, false ); } wp_safe_redirect( add_query_arg( array( 'tab' => 'settings', 'wpdo_msg' => 'settings_saved', ), remove_query_arg( array( '_wpnonce' ) ) ) ); exit; } // v2.3.0 M6: run health check on demand from Doctor tab. if ( isset( $_POST['wpdo_run_health'] ) && check_admin_referer( 'wpdo_run_health' ) && class_exists( 'TMDO_Health_Cron' ) ) { $res = TMDO_Health_Cron::run(); $flag = ( $res['critical_count'] ?? 0 ) > 0 ? 'health_critical' : 'health_ok'; wp_safe_redirect( add_query_arg( array( 'tab' => 'doctor', 'wpdo_msg' => $flag, ), remove_query_arg( array( 'wpdo_run_health', '_wpnonce' ) ) ) ); exit; } // v2.2.0 M4: snapshot create / delete admin actions. if ( isset( $_POST['wpdo_create_snapshot'] ) && check_admin_referer( 'wpdo_create_snapshot' ) && class_exists( 'TMDO_Snapshot_Manager' ) ) { $result = TMDO_Snapshot_Manager::create( 'manual', array(), array( 'notes' => __( 'Created from admin UI', '2meet-data-optimizer' ), ) ); $flag = ! empty( $result['ok'] ) ? 'created' : 'create_failed'; wp_safe_redirect( add_query_arg( array( 'tab' => 'snapshots', 'wpdo_msg' => $flag, ), remove_query_arg( array( 'wpdo_create_snapshot', '_wpnonce' ) ) ) ); exit; } if ( isset( $_POST['wpdo_delete_snapshot'] ) && check_admin_referer( 'wpdo_delete_snapshot' ) && class_exists( 'TMDO_Snapshot_Manager' ) ) { $id = sanitize_text_field( wp_unslash( (string) $_POST['wpdo_delete_snapshot'] ) ); $ok = '' !== $id && TMDO_Snapshot_Manager::delete( $id ); wp_safe_redirect( add_query_arg( array( 'tab' => 'snapshots', 'wpdo_msg' => $ok ? 'deleted' : 'delete_failed', ), remove_query_arg( array( 'wpdo_delete_snapshot', '_wpnonce' ) ) ) ); exit; } if ( isset( $_POST['wpdo_prune_snapshots'] ) && check_admin_referer( 'wpdo_prune_snapshots' ) && class_exists( 'TMDO_Snapshot_Manager' ) ) { $res = TMDO_Snapshot_Manager::prune(); $msg = sprintf( 'pruned_%d', (int) ( $res['pruned'] ?? 0 ) ); wp_safe_redirect( add_query_arg( array( 'tab' => 'snapshots', 'wpdo_msg' => $msg, ), remove_query_arg( array( 'wpdo_prune_snapshots', '_wpnonce' ) ) ) ); exit; } // v2.10.0: Post Migration Wizard — backfill all 7 groups. if ( isset( $_POST['wpdo_post_backfill_all'] ) && check_admin_referer( 'wpdo_post_backfill_all' ) && class_exists( 'TMDO_Post_Migration' ) ) { $total_migrated = 0; $errors = array(); foreach ( array( 'wp_core', 'attachment', 'wc_product', 'hp_listing_core', 'hp_request_core', 'hp_vendor_core', 'nav_menu_item' ) as $group ) { try { $result = TMDO_Post_Migration::backfill_group( $group ); $total_migrated += (int) $result['migrated']; } catch ( \Throwable $e ) { $errors[] = $group . ':' . $e->getMessage(); } } if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::info( 'post_backfill_all', array( 'total_migrated' => $total_migrated, 'errors' => $errors, ) ); } $msg = $errors ? 'err_backfill_failed' : 'backfill_' . $total_migrated; wp_safe_redirect( add_query_arg( array( 'tab' => 'post-migration-wizard', 'wpdo_msg' => $msg, ), remove_query_arg( array( 'wpdo_post_backfill_all', '_wpnonce' ) ) ) ); exit; } // v2.10.0: Post Migration Wizard — copy legacy hot table. if ( isset( $_POST['wpdo_post_cutover_legacy'] ) && check_admin_referer( 'wpdo_post_cutover_legacy' ) && class_exists( 'TMDO_Post_Migration' ) ) { global $wpdb; $hot = $wpdb->prefix . 'wpdo_hot_hp_listing'; $flat = $wpdb->prefix . 'wpdo_post_hp_listing_core'; $copied = 0; $err = ''; try { $result = TMDO_Post_Migration::copy_legacy_hot_table( 'hp_listing', $hot, $flat ); $verify = TMDO_Post_Migration::verify_legacy_cutover( $hot, $flat ); $copied = (int) $result['copied']; if ( ! $verify['ok'] ) { $err = 'verify_mismatched_' . $verify['mismatched_rows']; } } catch ( \Throwable $e ) { $err = $e->getMessage(); } if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::info( 'post_cutover_legacy_admin', array( 'copied' => $copied, 'error' => $err, ) ); } $msg = $err ? 'err_cutover_failed' : 'cutover_' . $copied; wp_safe_redirect( add_query_arg( array( 'tab' => 'post-migration-wizard', 'wpdo_msg' => $msg, ), remove_query_arg( array( 'wpdo_post_cutover_legacy', '_wpnonce' ) ) ) ); exit; } // v2.10.0: Post Migration Wizard — promote mode (dual_write or aeav_only). if ( ( isset( $_POST['wpdo_post_promote_dual_write'] ) || isset( $_POST['wpdo_post_promote_aeav'] ) ) && class_exists( 'TMDO_Post_Migration' ) ) { $target = isset( $_POST['wpdo_post_promote_dual_write'] ) ? 'dual_write' : 'aeav_only'; $nonce = isset( $_POST['wpdo_post_promote_dual_write'] ) ? 'wpdo_post_promote_dual_write' : 'wpdo_post_promote_aeav'; if ( check_admin_referer( $nonce ) ) { $result = TMDO_Post_Migration::set_mode( $target ); $err = is_wp_error( $result ) ? $result->get_error_message() : ''; if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::info( 'post_promote_mode', array( 'target' => $target, 'error' => $err, ) ); } $msg = $err ? 'err_promote_' . str_replace( ' ', '_', sanitize_key( $err ) ) : 'promote_' . $target; wp_safe_redirect( add_query_arg( array( 'tab' => 'post-migration-wizard', 'wpdo_msg' => $msg, ), remove_query_arg( array( 'wpdo_post_promote_dual_write', 'wpdo_post_promote_aeav', '_wpnonce' ) ) ) ); exit; } } // v2.11.0: Post Stress Test — bulk create test posts. // v2.11.2: optional `mode` query arg (fast|realistic). if ( isset( $_POST['wpdo_post_stress_create'] ) && check_admin_referer( 'wpdo_post_stress_create' ) && class_exists( 'TMDO_Post_Stress_Tester' ) ) { $post_type = isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( (string) $_POST['post_type'] ) ) : 'product'; $count = isset( $_POST['count'] ) ? max( 1, min( 10000, absint( wp_unslash( $_POST['count'] ) ) ) ) : 100; $mode = isset( $_POST['mode'] ) && 'realistic' === sanitize_key( wp_unslash( (string) $_POST['mode'] ) ) ? 'realistic' : 'fast'; try { $result = 'realistic' === $mode ? TMDO_Post_Stress_Tester::create_realistic( $post_type, $count ) : TMDO_Post_Stress_Tester::create( $post_type, $count ); $msg = 'stress_' . $mode . '_' . (int) $result['created']; } catch ( \Throwable $e ) { $msg = 'err_stress_create'; if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::error( 'post_stress_create_admin', 'create', $e->getMessage() ); } } wp_safe_redirect( add_query_arg( array( 'tab' => 'post-stress-test', 'wpdo_msg' => $msg, ), remove_query_arg( array( 'wpdo_post_stress_create', 'post_type', 'count', 'mode', '_wpnonce' ) ) ) ); exit; } // v2.11.0: Post Stress Test — cleanup all test posts. if ( isset( $_POST['wpdo_post_stress_cleanup'] ) && check_admin_referer( 'wpdo_post_stress_cleanup' ) && class_exists( 'TMDO_Post_Stress_Tester' ) ) { try { $result = TMDO_Post_Stress_Tester::cleanup(); $msg = 'stress_cleanup_' . (int) $result['deleted_posts']; } catch ( \Throwable $e ) { $msg = 'err_stress_cleanup'; if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::error( 'post_stress_cleanup_admin', 'cleanup', $e->getMessage() ); } } wp_safe_redirect( add_query_arg( array( 'tab' => 'post-stress-test', 'wpdo_msg' => $msg, ), remove_query_arg( array( 'wpdo_post_stress_cleanup', '_wpnonce' ) ) ) ); exit; } // v2.11.0: Post Stress Test — run benchmark on all 7 groups. if ( isset( $_POST['wpdo_post_stress_bench'] ) && check_admin_referer( 'wpdo_post_stress_bench' ) && class_exists( 'TMDO_Post_Migration' ) ) { $samples = isset( $_POST['samples'] ) ? max( 10, min( 1000, absint( wp_unslash( $_POST['samples'] ) ) ) ) : 100; set_transient( 'wpdo_post_stress_bench_samples', $samples, 60 ); $msg = 'stress_bench_ready_' . $samples; wp_safe_redirect( add_query_arg( array( 'tab' => 'post-stress-test', 'wpdo_msg' => $msg, ), remove_query_arg( array( 'wpdo_post_stress_bench', 'samples', '_wpnonce' ) ) ) ); exit; } // v2.9.0 Phase 0: wp_postmeta garbage cleanup (transients/_wp_old_date/stale _edit_lock). if ( isset( $_POST['wpdo_postmeta_cleanup'] ) && check_admin_referer( 'wpdo_postmeta_cleanup' ) && class_exists( 'TMDO_Postmeta_Cleaner' ) ) { $deleted = TMDO_Postmeta_Cleaner::delete_garbage( TMDO_Postmeta_Cleaner::TARGET_ALL ); if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::info( 'postmeta_cleanup_admin', array( 'transients' => $deleted['transients'], 'wp_old_date' => $deleted['wp_old_date'], 'edit_locks' => $deleted['edit_locks'], 'total' => $deleted['total'], ) ); } wp_safe_redirect( add_query_arg( array( 'wpdo_msg' => 'postmeta_cleanup_done_' . (int) $deleted['total'], ), remove_query_arg( array( 'wpdo_postmeta_cleanup', '_wpnonce' ) ) ) ); exit; } $tabs = array( 'dashboard' => __( '儀表板', '2meet-data-optimizer' ), 'entity-bridge' => __( 'Entity Bridge', '2meet-data-optimizer' ), 'migration-wizard' => __( 'User 遷移精靈', '2meet-data-optimizer' ), 'post-migration-wizard' => __( 'Post 遷移精靈', '2meet-data-optimizer' ), 'stress-test' => __( 'User 壓力測試', '2meet-data-optimizer' ), 'post-stress-test' => __( 'Post 壓力測試', '2meet-data-optimizer' ), 'term-stress-test' => __( 'Term 壓力測試', '2meet-data-optimizer' ), 'comment-stress-test' => __( 'Comment 壓力測試', '2meet-data-optimizer' ), 'zones' => __( 'Zone 配置', '2meet-data-optimizer' ), 'classifier' => __( '自動分類', '2meet-data-optimizer' ), 'snapshots' => __( '備份快照', '2meet-data-optimizer' ), 'conflicts' => __( '衝突檢測', '2meet-data-optimizer' ), 'doctor' => __( '健康檢查', '2meet-data-optimizer' ), 'module-suggestions' => __( '模組建議', '2meet-data-optimizer' ), 'settings' => __( '設定', '2meet-data-optimizer' ), 'logs' => __( '日誌', '2meet-data-optimizer' ), 'rest-api' => __( 'REST API', '2meet-data-optimizer' ), 'hivepress' => __( 'HivePress 整合', '2meet-data-optimizer' ), ); $tab = sanitize_key( wp_unslash( $_GET['tab'] ?? 'dashboard' ) ); // v2.11.8: legacy tabs removed — redirect bookmarks to current main maintenance entry. if ( in_array( $tab, array( 'sop', 'migration' ), true ) ) { $tab = 'entity-bridge'; } if ( ! array_key_exists( $tab, $tabs ) ) { $tab = 'dashboard'; } // Show HPCT Import tab only when HPCT is detected. if ( TMDO_Compatibility::should_show_hpct_notice() ) { $tabs['hpct-import'] = __( 'HPCT 匯入', '2meet-data-optimizer' ); } // v2.8.1: red-dot badge on tabs that need operator attention. $needs_dot = array(); if ( class_exists( 'TMDO_Migration_Orchestrator' ) ) { $attn = TMDO_Migration_Orchestrator::needs_attention(); if ( ! empty( $attn['needs'] ) || 'failed' === ( $attn['job_state'] ?? '' ) ) { $needs_dot['migration-wizard'] = (int) ( $attn['eav_rows'] ?? 0 ); } } ?>

array( 'label' => esc_html__( '📊 概覽', '2meet-data-optimizer' ), 'tabs' => array( 'dashboard', 'doctor', 'module-suggestions', 'conflicts', 'logs' ), ), 'wizards' => array( 'label' => esc_html__( '🧙 遷移精靈', '2meet-data-optimizer' ), 'tabs' => array( 'migration-wizard', 'post-migration-wizard', 'hpct-import' ), ), 'stress' => array( 'label' => esc_html__( '⚡ 壓力測試', '2meet-data-optimizer' ), 'tabs' => array( 'stress-test', 'post-stress-test', 'term-stress-test', 'comment-stress-test' ), ), 'config' => array( 'label' => esc_html__( '⚙️ 配置', '2meet-data-optimizer' ), 'tabs' => array( 'entity-bridge', 'zones', 'classifier', 'settings', 'rest-api', 'snapshots' ), ), ); // Fallback bucket for any tab not explicitly listed (resilient to future tabs). $grouped_slugs = array(); foreach ( $tab_groups as $g ) { $grouped_slugs = array_merge( $grouped_slugs, $g['tabs'] ); } $ungrouped = array_diff( array_keys( $tabs ), $grouped_slugs ); if ( ! empty( $ungrouped ) ) { $tab_groups['other'] = array( 'label' => esc_html__( '其他', '2meet-data-optimizer' ), 'tabs' => array_values( $ungrouped ), ); } ?>
get_stats(); $flags = TMDO_Feature_Flags::all(); $compat = TMDO_Compatibility::check(); $cache = TMDO_Cache_Layer::get_stats(); // ── Rate limit stats ────────────────────────────────────────────── $rl_stats = get_option( 'wpdo_rl_stats', array() ); $rl_total = array_sum( $rl_stats ); arsort( $rl_stats ); $rl_top = array_slice( $rl_stats, 0, 10, true ); // ── Zone B / Zone D stats (60s transient — dashboard is read-heavy) ── $cache_key = 'wpdo_dashboard_stats_v1'; $cached = get_transient( $cache_key ); if ( false === $cached ) { $warm_table = TMDO_Zone_Warm::table(); $now_sql = TMDO_DB::now(); $warm_active = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal helper, no user input $wpdb->prepare( "SELECT COUNT(*) FROM `{$warm_table}` WHERE expires_at IS NULL OR expires_at > %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $now_sql ) ); $warm_soon = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->prepare( "SELECT COUNT(*) FROM `{$warm_table}` WHERE expires_at IS NOT NULL AND expires_at > %s AND expires_at < DATE_ADD(%s, INTERVAL 24 HOUR)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $now_sql, $now_sql ) ); $top_views = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->prepare( "SELECT post_id, CAST(meta_value AS UNSIGNED) as views FROM `{$warm_table}` WHERE meta_key = %s AND (expires_at IS NULL OR expires_at > %s) ORDER BY views DESC LIMIT 10", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared TMDO_Listing_Stats::VIEW_KEY, $now_sql ), ARRAY_A ); $cached = array( 'warm_active' => $warm_active, 'warm_soon' => $warm_soon, 'top_views' => $top_views, 'archive_stats' => TMDO_Zone_Archive::stats(), ); set_transient( $cache_key, $cached, MINUTE_IN_SECONDS ); } $warm_active = (int) $cached['warm_active']; $warm_soon = (int) $cached['warm_soon']; $top_views = $cached['top_views']; $archive_stats = $cached['archive_stats']; // v2.16.0: KPI hero metrics — read-only, transient-cached 5min. $kpi = self::compute_dashboard_kpi(); ?>
0 ) { echo esc_html( number_format_i18n( $kpi['speedup_x'], 1 ) ) . '×'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- unit span hardcoded. } else { echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- hardcoded. } ?>
/100

Active' : 'N/A' ); ?>
Imported'; } elseif ( $compat['hpct_active'] ) { echo 'Needs Import'; } else { echo 'N/A'; } ?>
External' : 'Built-in'; ?> Flush Support

Zone
Hot (A)
Warm (B)
Cold (C)
Archive (D)

HPCT Modules

$state ) : ?>

Zone Modules

$state ) : ?>

Warm (B)

Post ID

Archive (D)

0 ? round( $archive_stats['compressed_rows'] / $archive_stats['total_rows'] * 100 ) : 0; echo esc_html( number_format_i18n( $archive_stats['compressed_rows'] ) ); echo ' ' . (int) $pct . '%'; ?>

Post Type

0 ) : ?>

$count ) : ?>
Post ID

'background:#e0e0e0;color:#444', 'dual_write' => 'background:#d4edda;color:#155724', 'shadow_read' => 'background:#fff3cd;color:#856404', 'aeav_only' => 'background:#cce5ff;color:#004085', ); ?>

 | 

get_var( "SELECT COUNT(*) FROM {$wpdb->posts}" ); $postmeta_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->postmeta}" ); $ratio_num = $posts_count > 0 ? $postmeta_count / $posts_count : 0; // 2) Latest benchmark speedup. $bench_table = $wpdb->prefix . 'wpdo_benchmarks'; $row = $wpdb->get_row( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table from internal helper. "SELECT module, native_ms, custom_ms, created_at FROM `{$bench_table}` WHERE custom_ms > 0 ORDER BY id DESC LIMIT 1", ARRAY_A ); $speedup_x = 0.0; $speedup_label = ''; if ( $row && (float) $row['custom_ms'] > 0 ) { $speedup_x = (float) $row['native_ms'] / (float) $row['custom_ms']; if ( $speedup_x >= 1.0 ) { $speedup_label = sprintf( /* translators: 1: module, 2: timestamp */ __( '最近 %1$s @ %2$s', '2meet-data-optimizer' ), (string) $row['module'], mysql2date( get_option( 'date_format', 'Y-m-d' ), (string) $row['created_at'] ) ); } } // 3) Optimized field coverage from Schema Registry. $fields_hot = 0; $fields_cold = 0; if ( class_exists( 'TMDO_Schema_Registry' ) ) { $registry = TMDO_Schema_Registry::instance(); $hot_post_types = $registry->get_hot_post_types(); $cold_post_types = $registry->get_cold_post_types(); foreach ( $hot_post_types as $pt ) { $fields_hot += count( $registry->get_hot_columns( $pt ) ); } foreach ( $cold_post_types as $pt ) { $fields_cold += count( $registry->get_cold_meta_keys( $pt ) ); } } // 4) Health score: starts at 100, deducts for recent errors / conflicts / mode mismatch. $errors_24h = 0; $err_table = $wpdb->prefix . 'wpdo_errors'; $err_exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $err_table ) ); if ( $err_exists ) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table from internal helper, severity literals are static. $err_sql = "SELECT COUNT(*) FROM `{$err_table}` WHERE severity IN ('error','critical') AND created_at >= %s"; $errors_24h = (int) $wpdb->get_var( $wpdb->prepare( $err_sql, gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS ) ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared ); } $conflicts = 0; if ( class_exists( 'TMDO_Conflict_Monitor' ) ) { $summary = TMDO_Conflict_Monitor::get_summary(); $conflicts = (int) ( $summary['total'] ?? 0 ); } $score = 100; $score -= min( 30, $errors_24h * 3 ); $score -= min( 30, $conflicts * 5 ); $score = max( 0, min( 100, $score ) ); $health_class = $score >= 90 ? 'excellent' : ( $score >= 70 ? 'good' : ( $score >= 40 ? 'warn' : 'crit' ) ); $kpi = array( 'ratio_str' => $ratio_num > 0 ? number_format_i18n( $ratio_num, 2 ) : '0', 'posts' => $posts_count, 'postmeta' => $postmeta_count, 'speedup_x' => $speedup_x, 'speedup_label' => $speedup_label, 'fields_total' => $fields_hot + $fields_cold, 'fields_hot' => $fields_hot, 'fields_cold' => $fields_cold, 'health_score' => $score, 'health_class' => $health_class, 'errors_24h' => $errors_24h, 'conflicts' => $conflicts, ); set_transient( 'wpdo_kpi_hero_v1', $kpi, 5 * MINUTE_IN_SECONDS ); return $kpi; } /** * Entity Bridge tab: health cards + migration wizard for user/term/comment. */ private static function render_entity_bridge(): void { $mode_order = array( 'disabled', 'dual_write', 'shadow_read', 'aeav_only' ); $mode_labels = array( 'disabled' => 'disabled', 'dual_write' => 'dual_write', 'shadow_read' => 'shadow_read', 'aeav_only' => 'aeav_only', ); $mode_style = array( 'disabled' => 'background:#e0e0e0;color:#444', 'dual_write' => 'background:#d4edda;color:#155724', 'shadow_read' => 'background:#fff3cd;color:#856404', 'aeav_only' => 'background:#cce5ff;color:#004085', ); $health_data = class_exists( 'TMDO_Entity_Health' ) ? TMDO_Entity_Health::get_all() : array(); // v2.11.0: post entity health is computed on-demand (post is not in // TMDO_Entity_Health::MANAGED_TYPES yet — kept out to preserve user-side // JS polling assumption of 3 types). Add post to the render loop only. if ( class_exists( 'TMDO_Entity_Health' ) ) { $health_data['post'] = TMDO_Entity_Health::get_one( 'post' ); } $bridge_page = admin_url( 'tools.php?page=wp-data-optimizer&tab=entity-bridge' ); ?>

Entity

$m ) : $active = ( $m === $mode ); ?> 0 ) : ?>
|
= 99 ? '#28a745' : ( $cov_pct >= 50 ? '#ffc107' : '#dc3545' ); ?>
欄位  
%
/ EAV

0 ) : ?>

  1. disabled → dual_write
  2. dual_write → shadow_read
  3. shadow_read → aeav_only

'idle' ); $template = TMDO_PATH . 'admin/templates/migration-wizard.php'; if ( file_exists( $template ) ) { include $template; } } /** * Post Migration Wizard tab — Post Entity Bridge orchestration UI (v2.10.0). * * Independent of user-side migration-wizard tab (frozen contract). Sync * execution model — each action runs to completion in one request. Async * polling / cron pump are out of scope for v2.10.0; admin can re-trigger * manually if a phase needs to be re-run. * * @return void */ /** * Post Stress Test tab — sync-execution sister of render_stress_test(). * * Simplified UI introduced in v2.11.0 — async progress polling deferred * (user side uses ~700 lines of JS + REST). Sync model: each button * POSTs back, runs to completion, redirects with msg flag. * * @return void */ private static function render_post_stress_test(): void { $test_post_count = class_exists( 'TMDO_Post_Stress_Tester' ) ? TMDO_Post_Stress_Tester::count_test_posts() : 0; $diagnose = class_exists( 'TMDO_Post_Migration' ) ? TMDO_Post_Migration::diagnose() : array( 'posts' => 0, 'postmeta' => 0, 'ratio' => 0, 'mode' => 'disabled', 'groups' => array(), ); $template = TMDO_PATH . 'admin/templates/post-stress-test.php'; if ( file_exists( $template ) ) { include $template; } } /** * Term Stress Test tab — async progress polling sister of post-stress-test * (v2.13.0). Reuses the same UI patterns; entity-specific is taxonomy * dropdown + flat-table reports. * * @return void */ private static function render_term_stress_test(): void { $state = class_exists( 'TMDO_Term_Stress_Tester' ) ? TMDO_Term_Stress_Tester::get_progress( false ) : array(); $test_term_count = class_exists( 'TMDO_Term_Stress_Tester' ) ? TMDO_Term_Stress_Tester::count_test_terms() : 0; // Available taxonomies for the dropdown (filter to non-system, public + meaningful internals). $all_taxonomies = get_taxonomies( array(), 'objects' ); $taxonomies = array(); foreach ( $all_taxonomies as $slug => $tax ) { if ( in_array( $slug, array( 'nav_menu', 'link_category', 'post_format' ), true ) ) { continue; } $taxonomies[ $slug ] = $tax->labels->singular_name ?? $slug; } $template = TMDO_PATH . 'admin/templates/term-stress-test.php'; if ( file_exists( $template ) ) { include $template; } } /** * Comment Stress Test tab — async progress polling sister of term-stress-test * (v2.13.1). Entity-specific is post_id dropdown (target post for comments) * + flat-table reports for wpdo_comment_hp_review / wpdo_comment_misc. * * @return void */ private static function render_comment_stress_test(): void { $state = class_exists( 'TMDO_Comment_Stress_Tester' ) ? TMDO_Comment_Stress_Tester::get_progress( false ) : array(); $test_comment_count = class_exists( 'TMDO_Comment_Stress_Tester' ) ? TMDO_Comment_Stress_Tester::count_test_comments() : 0; // Available posts for the dropdown (top 20 by comment_count, fallback to most recent). global $wpdb; $posts = array(); if ( isset( $wpdb ) ) { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT ID, post_title, comment_count FROM {$wpdb->posts} WHERE post_status = %s AND post_type IN ('post','page','hp_listing') ORDER BY comment_count DESC, ID DESC LIMIT 20", 'publish' ) ); if ( is_array( $rows ) ) { foreach ( $rows as $r ) { $posts[ (int) $r->ID ] = sprintf( '#%d %s (%d)', (int) $r->ID, $r->post_title ?: '(無標題)', (int) $r->comment_count ); } } } $template = TMDO_PATH . 'admin/templates/comment-stress-test.php'; if ( file_exists( $template ) ) { include $template; } } /** * Post Migration Wizard tab — render handler (v2.9.3+). * * @return void */ private static function render_post_migration_wizard(): void { $diagnose = class_exists( 'TMDO_Post_Migration' ) ? TMDO_Post_Migration::diagnose() : array( 'posts' => 0, 'postmeta' => 0, 'ratio' => 0, 'mode' => 'disabled', 'groups' => array(), ); $garbage = class_exists( 'TMDO_Postmeta_Cleaner' ) ? TMDO_Postmeta_Cleaner::count_garbage( 'all' ) : array( 'total' => 0, 'transients' => 0, 'wp_old_date' => 0, 'edit_locks' => 0, ); $template = TMDO_PATH . 'admin/templates/post-migration-wizard.php'; if ( file_exists( $template ) ) { include $template; } } /** * Stress-test tab — User Entity stress test & benchmark UI. */ private static function render_stress_test(): void { $state = class_exists( 'TMDO_User_Stress_Tester' ) ? TMDO_User_Stress_Tester::get_progress() : array(); $status = $state['status'] ?? 'idle'; $test_user_count = isset( $state['test_user_count'] ) ? (int) $state['test_user_count'] : ( class_exists( 'TMDO_User_Stress_Tester' ) ? TMDO_User_Stress_Tester::count_test_users() : 0 ); $is_running = ( 'running' === $status || 'benchmarking' === $status ); ?>

⚠️


· mode %
/ users/sec
MB

/
users/sec
/ / ms
MB

__( 'TMDO_API::get_field (membership_level) ×100', '2meet-data-optimizer' ), 'get_entity_full' => __( 'TMDO_API::get_entity (整筆) ×100', '2meet-data-optimizer' ), 'range_gold_high_points' => __( '索引範圍:gold + points>5000', '2meet-data-optimizer' ), 'sort_recent_active_100' => __( '排序:last_active_at DESC LIMIT 100', '2meet-data-optimizer' ), 'join_top_gold_active' => __( 'JOIN:top gold + active LIMIT 100', '2meet-data-optimizer' ), 'eav_range_baseline' => __( '原生 EAV 等價查詢(baseline)', '2meet-data-optimizer' ), ); foreach ( $query_labels as $key => $label ) : $q = $query[ $key ] ?? null; if ( ! $q ) { continue; } ?>

all(); ?>

Post Type Meta Key Zone Column Data Type Indexed Provider

'#e0e0e0', 'dual_write' => '#d4edda', 'shadow_read' => '#fff3cd', 'aeav_only' => '#cce5ff', ); $badge_bg = $e_mode_bg[ $e_mode ] ?? '#e0e0e0'; ?>

✓ ' . esc_html( $e_tbl ) . ''; } else { echo ' ⏳ ' . esc_html__( '表格尚未建立', '2meet-data-optimizer' ) . ''; } } ?>

get_col( "SELECT DISTINCT p.post_type FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID WHERE p.post_type NOT IN ('revision', 'nav_menu_item', 'customize_changeset', 'oembed_cache') ORDER BY p.post_type ASC LIMIT 200" ); set_transient( 'wpdo_classifier_post_types_v1', $types, HOUR_IN_SECONDS ); } $post_type = sanitize_key( wp_unslash( $_GET['classify_type'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only classifier filter; no state change. if ( $post_type && ! in_array( $post_type, $types ?: array(), true ) ) { $post_type = ''; } ?>

' . esc_html__( '此 post type 沒有可分析的 postmeta 欄位。', '2meet-data-optimizer' ) . '

'; return; } $summary = TMDO_Zone_Classifier::summary( $post_type ); ?>

Hot
Warm
Cold
Archive
Meta Key

' . sprintf( esc_html__( '已清除 %1$d 筆超過 %2$d 天的日誌。', '2meet-data-optimizer' ), (int) $deleted, (int) $days ) . '

'; } $errors = TMDO_Logger::get_recent( '', 100 ); ?>

ID Zone Hook

'POST', 'path' => '/listings/{id}/view', 'desc' => __( '增加 Zone B 瀏覽計數。需要 WP REST nonce(X-WP-Nonce header)。warm zone cutover 前自動 fallback postmeta hp_view_count。', '2meet-data-optimizer' ), 'params' => array( 'id' => __( 'Post ID(路徑參數)', '2meet-data-optimizer' ), 'X-WP-Nonce' => __( 'WP REST nonce(wp_create_nonce("wp_rest"))', '2meet-data-optimizer' ), ), 'headers' => array(), 'curl' => "curl -X POST '{$base}/listings/123/view' -H 'X-WP-Nonce: '", ), array( 'method' => 'GET', 'path' => '/listings', 'desc' => __( '查詢 Zone A 扁平欄位(搜尋/篩選),支援分頁與排序。', '2meet-data-optimizer' ), 'params' => array( 'post_type' => __( 'post type(預設 hp_listing)', '2meet-data-optimizer' ), 'per_page' => __( '每頁筆數 1–100(預設 20)', '2meet-data-optimizer' ), 'page' => __( '頁碼(預設 1)', '2meet-data-optimizer' ), 'orderby' => __( '排序欄位(預設 post_id)', '2meet-data-optimizer' ), 'order' => __( 'ASC 或 DESC(預設 DESC)', '2meet-data-optimizer' ), '{col}_min' => __( '數值篩選下限,例如 hp_price_min=1000', '2meet-data-optimizer' ), '{col}_max' => __( '數值篩選上限,例如 hp_price_max=5000', '2meet-data-optimizer' ), '{col}' => __( '精確值篩選,例如 hp_featured=1', '2meet-data-optimizer' ), ), 'headers' => array( 'X-WP-Total' => __( '符合條件的總筆數', '2meet-data-optimizer' ), 'X-WP-TotalPages' => __( '總頁數', '2meet-data-optimizer' ), ), 'curl' => "curl '{$base}/listings?per_page=5&hp_price_min=1000&order=ASC'", ), array( 'method' => 'GET', 'path' => '/listings/{id}', 'desc' => __( '單筆 listing:Zone A(熱區欄位)+ Zone C(JSON blob)合併回傳。Zone 未啟用時自動 fallback postmeta。', '2meet-data-optimizer' ), 'params' => array( 'id' => __( 'Post ID(路徑參數)', '2meet-data-optimizer' ) ), 'headers' => array(), 'curl' => "curl '{$base}/listings/123'", ), array( 'method' => 'GET', 'path' => '/stats/{id}', 'desc' => __( 'Zone B 瀏覽計數(warm zone),warm 未啟用時 fallback hp_view_count postmeta。', '2meet-data-optimizer' ), 'params' => array( 'id' => __( 'Post ID(路徑參數)', '2meet-data-optimizer' ) ), 'headers' => array(), 'curl' => "curl '{$base}/stats/123'", ), array( 'method' => 'GET', 'path' => '/status', 'desc' => __( '版本、引擎、欄位統計、模組狀態。需要 manage_options 權限(帶 WP Nonce)。', '2meet-data-optimizer' ), 'params' => array(), 'headers' => array(), 'curl' => "curl '{$base}/status' -H 'X-WP-Nonce: '", ), ); foreach ( $endpoints as $ep ) : ?>

$pdesc ) : ?>

$hdesc ) : ?>  

WpdoClient

		 rest_url( 'wpdo/v1' ),
    'nonce'     => wp_create_nonce( 'wp_rest' ),
    'post_type' => 'hp_listing',
] );"
		);
		?>
		

		
		

		 console.log(r));'
		);
		?>
		

' . esc_html( $result->get_error_message() ) . '

'; } else { echo '

' . esc_html__( 'HPCT 匯入成功!建議停用 HP Custom Tables 外掛。', '2meet-data-optimizer' ) . '

'; } } $can_import = TMDO_HPCT_Import::can_import(); $is_imported = TMDO_HPCT_Import::is_imported(); ?>

'Cache flushed successfully.' ) ); } wp_send_json_error( 'Missing post_type' ); break; case 'module_status': $flags = TMDO_Feature_Flags::all(); wp_send_json_success( $flags ); break; default: wp_send_json_error( 'Unknown action' ); } } // ─── v2.2.0 M4 — Snapshots / Conflicts / Doctor tabs ────────────────── /** * Snapshots tab — list / create / restore / delete (v2.2.0 M1+M4). */ private static function render_snapshots(): void { if ( ! class_exists( 'TMDO_Snapshot_Manager' ) ) { echo '

' . esc_html__( 'Snapshot system not available.', '2meet-data-optimizer' ) . '

'; return; } // Show post-action notice. $msg_key = isset( $_GET['wpdo_msg'] ) ? sanitize_key( wp_unslash( (string) $_GET['wpdo_msg'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( '' !== $msg_key ) { $msg_map = array( 'created' => array( 'success', __( '快照已成功建立。', '2meet-data-optimizer' ) ), 'create_failed' => array( 'error', __( '快照建立失敗,請查看日誌。', '2meet-data-optimizer' ) ), 'deleted' => array( 'success', __( '快照已刪除。', '2meet-data-optimizer' ) ), 'delete_failed' => array( 'error', __( '快照刪除失敗。', '2meet-data-optimizer' ) ), ); if ( str_starts_with( $msg_key, 'pruned_' ) ) { $n = (int) substr( $msg_key, 7 ); printf( '

%s

', esc_html( sprintf( /* translators: %d: count */ __( '已清除 %d 個過期快照。', '2meet-data-optimizer' ), $n ) ) ); } elseif ( isset( $msg_map[ $msg_key ] ) ) { printf( '

%s

', esc_attr( $msg_map[ $msg_key ][0] ), esc_html( $msg_map[ $msg_key ][1] ) ); } } $rows = TMDO_Snapshot_Manager::list_recent( 50, null ); ?>

wp wpdo snapshot restore <snapshot_id> --apply

wp wpdo conflict-scan

' . esc_html__( '衝突監控模組未載入。', '2meet-data-optimizer' ) . '

'; return; } $summary = TMDO_Conflict_Monitor::get_summary(); $total = (int) ( $summary['total'] ?? 0 ); ?>
Hook overlap
UAEPG overlap
0 ) : ?>

			
			

' . esc_html__( 'Site Health 模組未載入。', '2meet-data-optimizer' ) . '

'; return; } $tests = array( 'wpdo_schema_drift' => array( 'check_schema_drift', __( 'Schema 完整性', '2meet-data-optimizer' ) ), 'wpdo_error_budget' => array( 'check_error_budget', __( '錯誤預算(過去 7 天)', '2meet-data-optimizer' ) ), 'wpdo_hook_conflicts' => array( 'check_hook_conflicts', __( 'Hook 衝突', '2meet-data-optimizer' ) ), 'wpdo_autoload_bloat' => array( 'check_autoload_bloat', __( 'Autoload 大小', '2meet-data-optimizer' ) ), 'wpdo_postmeta_explosion' => array( 'check_postmeta_explosion', __( 'wp_postmeta 爆量', '2meet-data-optimizer' ) ), 'wpdo_orphan_zone_rows' => array( 'check_orphan_zone_rows', __( 'Orphan zone rows', '2meet-data-optimizer' ) ), 'wpdo_missing_snapshot' => array( 'check_missing_snapshot', __( '缺少最近快照', '2meet-data-optimizer' ) ), ); ?> $info ) { $result = call_user_func( array( 'TMDO_Site_Health', $info[0] ) ); $status = (string) ( $result['status'] ?? 'good' ); $badge = 'good' === $status ? '✅' : ( 'critical' === $status ? '🔴' : '🟡' ); printf( '', esc_html( $info[1] ), esc_html( $badge ), esc_html( $status ), wp_kses_post( (string) ( $result['description'] ?? '' ) ) ); } ?>
%s%s %s%s

%s

', esc_html( sprintf( /* translators: %s: module name */ __( '✅ Module %s 已切換至 dual_write 狀態。建議觀察 ≥ 24h 後再推進到 shadow_read(用 Entity Bridge tab 或 wp wpdo bridge-mode-set)。', '2meet-data-optimizer' ), $mod ) ) ); } elseif ( 'enable_blocked' === $msg ) { printf( '

%s

', esc_html( sprintf( /* translators: %s: module name */ __( '⛔ Module %s 啟用被 FSM Guard 擋下(可能 module 已不在 idle 狀態)。', '2meet-data-optimizer' ), $mod ) ) ); } elseif ( 'enable_failed' === $msg ) { echo '

' . esc_html__( '⛔ 啟用失敗,請查日誌。', '2meet-data-optimizer' ) . '

'; } echo '

' . esc_html__( '🤖 模組建議', '2meet-data-optimizer' ) . '

'; echo '

' . esc_html__( '系統依環境自動偵測哪些 module 適合啟用。每筆建議含 confidence score + reasons + blockers。一鍵啟用會把 module 推進到 dual_write(FSM 第 1 個 active state,FSM Guard 確保不越級)。', '2meet-data-optimizer' ) . '

'; if ( ! class_exists( 'TMDO_Module_Detector' ) ) { echo '

' . esc_html__( '模組偵測器未載入。', '2meet-data-optimizer' ) . '

'; return; } $all = TMDO_Module_Detector::detect_all( true ); // Sort: actionable enable (high confidence first) → wait → skip. $buckets = array( 'enable' => array(), 'wait' => array(), 'skip' => array(), ); foreach ( $all as $module => $r ) { $rec = $r['recommendation'] ?? 'skip'; if ( ! isset( $buckets[ $rec ] ) ) { $rec = 'skip'; } $buckets[ $rec ][ $module ] = $r; } uasort( $buckets['enable'], static fn( $a, $b ) => (float) $b['confidence'] <=> (float) $a['confidence'] ); // ─── enable bucket(重點)───────────────────────────────── $enable = $buckets['enable']; printf( '

%s

', esc_html( sprintf( /* translators: %d: number of recommended modules */ __( '✅ 建議啟用(%d 個 module)', '2meet-data-optimizer' ), count( $enable ) ) ) ); if ( empty( $enable ) ) { echo '

' . esc_html__( '目前沒有可立即啟用的 module 建議。', '2meet-data-optimizer' ) . '

'; } else { echo ''; printf( '', esc_html__( 'Module', '2meet-data-optimizer' ), esc_html__( 'Confidence', '2meet-data-optimizer' ), esc_html__( '說明 / 理由', '2meet-data-optimizer' ), esc_html__( '當前狀態', '2meet-data-optimizer' ), esc_html__( '操作', '2meet-data-optimizer' ) ); echo ''; foreach ( $enable as $module => $r ) { $conf = (float) $r['confidence']; $bar_w = (int) round( $conf * 100 ); $color = $conf >= 0.7 ? '#46b450' : ( $conf >= 0.5 ? '#dba617' : '#c3c4c7' ); printf( '', esc_html( $module ), esc_attr( $color ), (int) $bar_w, esc_html( sprintf( '%.2f', $conf ) ) ); echo ''; printf( '', esc_html( (string) $r['current_state'] ) ); echo ''; echo ''; } echo '
%s%s%s%s%s
%s
%s
'; if ( ! empty( $r['description'] ) ) { echo '' . esc_html( (string) $r['description'] ) . '
'; } if ( ! empty( $r['reasons'] ) ) { echo '' . esc_html( implode( ' · ', $r['reasons'] ) ) . ''; } echo '
%s
'; printf( '', esc_attr( $module ) ); wp_nonce_field( 'wpdo_enable_module' ); printf( '', esc_js( __( '確定啟用此 module(推進到 dual_write)?', '2meet-data-optimizer' ) ), esc_html__( '✅ 啟用', '2meet-data-optimizer' ) ); echo '
'; } // ─── wait bucket(條件未滿)──────────────────────────────── $wait = $buckets['wait']; if ( ! empty( $wait ) ) { printf( '

%s

', esc_html( sprintf( /* translators: %d: number of modules not yet ready */ __( '⏳ 條件未滿 / 暫不建議(%d 個)', '2meet-data-optimizer' ), count( $wait ) ) ) ); echo ''; foreach ( $wait as $module => $r ) { printf( '', esc_html( $module ), esc_html( implode( ' · ', $r['blockers'] ?? array() ) ) ); } echo '
%s%s
'; } // ─── skip bucket(已啟用 / 不適用)──────────────────────── $skip = $buckets['skip']; if ( ! empty( $skip ) ) { $skip_count = count( $skip ); printf( '
%s', esc_html( sprintf( /* translators: %d: number of modules already enabled or not applicable */ __( '已啟用 / 不適用(%d 個)— 點擊展開', '2meet-data-optimizer' ), $skip_count ) ) ); echo ''; foreach ( $skip as $module => $r ) { printf( '', esc_html( $module ), esc_html( implode( ' · ', $r['blockers'] ?? array() ) ) ); } echo '
%s%s
'; } echo '

' . esc_html__( '結果由 1 小時 transient 快取;每日健康檢查 cron 也會自動更新。', '2meet-data-optimizer' ) . '

'; } /** * Settings tab — email alerts + throttle (v2.4.0 M10). */ private static function render_settings(): void { if ( isset( $_GET['wpdo_msg'] ) && 'settings_saved' === sanitize_key( wp_unslash( (string) $_GET['wpdo_msg'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended echo '

' . esc_html__( '設定已儲存。', '2meet-data-optimizer' ) . '

'; } $enabled = class_exists( 'TMDO_Email_Notifier' ) && TMDO_Email_Notifier::is_enabled(); $email = class_exists( 'TMDO_Email_Notifier' ) ? TMDO_Email_Notifier::recipient() : ''; $throttle = class_exists( 'TMDO_Email_Notifier' ) ? TMDO_Email_Notifier::throttle_hours() : 24; ?>

array( '💬 Slack', 'webhook_url', 'wpdo_slack_webhook', 'https://hooks.slack.com/services/...' ), 'discord' => array( '🎮 Discord', 'webhook_url', 'wpdo_discord_webhook', 'https://discord.com/api/webhooks/...' ), 'telegram' => array( '📱 Telegram', 'bot', null, null ), ); foreach ( $channels as $ch => $cfg ) { $ch_enabled = '1' === (string) get_option( "wpdo_{$ch}_enabled", '0' ); $ch_throttle = (int) get_option( "wpdo_{$ch}_throttle_hours", 24 ); $ch_severity = (string) get_option( "wpdo_{$ch}_severity", 'critical_only' ); echo ''; echo '' . esc_html( $cfg[0] ) . ''; echo ''; printf( '', esc_html__( '啟用', '2meet-data-optimizer' ), esc_attr( $ch ), checked( $ch_enabled, true, false ), esc_html__( '啟用', '2meet-data-optimizer' ) ); if ( 'slack' === $ch || 'discord' === $ch ) { $webhook = class_exists( 'TMDO_Crypto' ) ? TMDO_Crypto::get_option( (string) $cfg[2] ) : (string) get_option( $cfg[2], '' ); printf( '', esc_attr( $cfg[2] ), esc_attr( $cfg[2] ), esc_attr( $cfg[2] ), esc_attr( $webhook ), esc_attr( $cfg[3] ) ); } if ( 'telegram' === $ch ) { $token = class_exists( 'TMDO_Crypto' ) ? TMDO_Crypto::get_option( 'wpdo_telegram_bot_token' ) : (string) get_option( 'wpdo_telegram_bot_token', '' ); $chat = (string) get_option( 'wpdo_telegram_chat_id', '' ); printf( '', esc_attr( $token ) ); printf( '', esc_attr( $chat ) ); } printf( '', esc_html__( 'Throttle 小時', '2meet-data-optimizer' ), esc_attr( $ch ), absint( $ch_throttle ), esc_html__( '同 fingerprint 不重發(1-168)', '2meet-data-optimizer' ) ); printf( '', esc_html__( 'Severity 訂閱', '2meet-data-optimizer' ), esc_attr( $ch ), selected( 'critical_only', $ch_severity, false ), selected( 'critical_and_recommended', $ch_severity, false ) ); echo ''; echo ''; } ?>


disabled dual_write shadow_read aeav_only

__( 'disabled — 原生 EAV', '2meet-data-optimizer' ), 'dual_write' => __( 'dual_write — 雙寫,讀走 EAV(安全起點)', '2meet-data-optimizer' ), 'shadow_read' => __( 'shadow_read — 雙寫 + 讀走 flat(驗證期)', '2meet-data-optimizer' ), 'aeav_only' => __( 'aeav_only — 僅 flat table(生產模式)', '2meet-data-optimizer' ), ); foreach ( array( 'user', 'post', 'term', 'comment' ) as $entity_type ) { $current_mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( $entity_type ) : 'disabled'; $field_id = 'wpdo_bridge_mode_' . $entity_type; ?> get_var( "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '\\_transient\\_wpdo\\_hp\\_pm\\_%' OR option_name LIKE '\\_transient\\_timeout\\_wpdo\\_hp\\_pm\\_%'" ); ?>

\', $value) 把 TTL cache 寫進 wp_postmeta(每個 hp_listing publish 觸發 8-16 個 transient row)。本 filter 在 metadata 層攔截並重新路由到 wp_options(native transient API),HivePress 完全無感,wp_postmeta 保持乾淨。Filter 是 metadata 層運作,不依賴 mode promote。', '2meet-data-optimizer' ); ?>

get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta} WHERE meta_key LIKE '\\_wxr\\_import\\_%' OR meta_key LIKE '\\_2meet\\_demo\\_%'" ); $comment_garbage_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->commentmeta} WHERE meta_key LIKE '\\_wxr\\_import\\_%' OR meta_key LIKE '\\_2meet\\_demo\\_%' OR meta_key IN ('_hp_price','_hp_status','_hp_featured','_hp_verified','_hp_view_count','_thumbnail_id','_edit_lock','_edit_last')" ); ?>

get_var( "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '\\_transient\\_wpdo\\_wc\\_termcount\\_%'" ); ?>

cache rows,重新路由到 wp_options(native transient 結構)。WC 自身 cache 失效邏輯不變(每次新增/刪除 product 時會重算寫入),僅儲存位置改變。Read 路徑亦會優先從 wp_options 讀回,cache miss 才 fall-through 至 wp_termmeta(向後相容)。', '2meet-data-optimizer' ); ?>