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(
'
',
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.
}
?>
|
|
|
|
|
|
|
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) | |
| |
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 . '%';
?>
|
'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' );
?>
$m ) :
$active = ( $m === $mode );
?>
0 ) : ?>
→
|
= 99 ? '#28a745' : ( $cov_pct >= 50 ? '#ffc107' : '#dc3545' );
?>
0 ) : ?>
—
✓
✓
—
-
disabled → dual_write:
-
:
-
dual_write → shadow_read:
-
:
-
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 );
?>
▍
▍
▍
|
|
|
|
|
__( '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 );
?>
' . sprintf( esc_html__( '已清除 %1$d 筆超過 %2$d 天的日誌。', '2meet-data-optimizer' ), (int) $deleted, (int) $days ) . '
';
}
$errors = TMDO_Logger::get_recent( '', 100 );
?>
'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 ) :
?>
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(
'',
esc_html( sprintf( /* translators: %d: count */ __( '已清除 %d 個過期快照。', '2meet-data-optimizer' ), $n ) )
);
} elseif ( isset( $msg_map[ $msg_key ] ) ) {
printf(
'',
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(
'| %s | %s %s | %s |
',
esc_html( $info[1] ),
esc_html( $badge ),
esc_html( $status ),
wp_kses_post( (string) ( $result['description'] ?? '' ) )
);
}
?>
%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(
'',
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(
'| %s | %s | %s | %s | %s | ',
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(
'%s | | ',
esc_html( $module ),
esc_attr( $color ),
(int) $bar_w,
esc_html( sprintf( '%.2f', $conf ) )
);
echo '';
if ( ! empty( $r['description'] ) ) {
echo '' . esc_html( (string) $r['description'] ) . ' ';
}
if ( ! empty( $r['reasons'] ) ) {
echo '' . esc_html( implode( ' · ', $r['reasons'] ) ) . '';
}
echo ' | ';
printf( '%s | ', esc_html( (string) $r['current_state'] ) );
echo ' | ';
echo '
';
}
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(
'%s | %s |
',
esc_html( $module ),
esc_html( implode( ' · ', $r['blockers'] ?? array() ) )
);
}
echo '
';
}
// ─── 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(
'%s | %s |
',
esc_html( $module ),
esc_html( implode( ' · ', $r['blockers'] ?? array() ) )
);
}
echo '
';
}
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;
?>