Files
2meet-data-optimizer/admin/class-tmdo-admin.php
T
wpdev 76c01e44df refactor: 全部 128 個生產檔加入 declare(strict_types=1)(PR-H)
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入
並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 06:13:33 +08:00

3713 lines
155 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: native meta queries for management dashboard
/**
* WP Data Optimizer admin UI class.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WP Data Optimizer admin UI.
*
* Registers the Tools > 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(
'<div class="notice notice-warning is-dismissible" data-dismiss-meta="%s"><p><strong>WP Data Optimizer</strong>: %s<br><code>%s</code></p></div>',
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 );
}
}
?>
<div class="wrap wpdo-wrap">
<h1><?php esc_html_e( 'WP Data Optimizer', '2meet-data-optimizer' ); ?></h1>
<?php
// v2.16.0: Grouped tab navigation — same 20 tabs (URL slugs unchanged),
// but visually clustered into 4 sections so admins can find tabs faster.
// Any tab not in a group falls back to the trailing "其他" section.
$tab_groups = array(
'overview' => 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 ),
);
}
?>
<nav class="nav-tab-wrapper wpdo-tab-nav" aria-label="<?php esc_attr_e( '主要頁籤', '2meet-data-optimizer' ); ?>">
<?php foreach ( $tab_groups as $group_key => $group ) : ?>
<?php
// Filter tabs that actually exist in $tabs (e.g. hpct-import only on HPCT envs).
$group_tabs = array_filter(
$group['tabs'],
static fn( $slug ) => array_key_exists( $slug, $tabs )
);
if ( empty( $group_tabs ) ) {
continue;
}
?>
<div class="wpdo-tab-group" data-group="<?php echo esc_attr( $group_key ); ?>">
<span class="wpdo-tab-group-label" aria-hidden="true"><?php echo esc_html( $group['label'] ); ?></span>
<?php foreach ( $group_tabs as $slug ) : ?>
<a href="<?php echo esc_url( add_query_arg( 'tab', $slug, admin_url( 'tools.php?page=' . self::MENU_SLUG ) ) ); ?>"
class="nav-tab <?php echo $tab === $slug ? 'nav-tab-active' : ''; ?>"
<?php echo $tab === $slug ? 'aria-current="page"' : ''; ?>>
<?php echo esc_html( $tabs[ $slug ] ); ?>
<?php if ( isset( $needs_dot[ $slug ] ) ) : ?>
<span class="wpdo-tab-dot" title="
<?php
echo esc_attr(
sprintf(
/* translators: %d: row count */
_n( '%d 行 EAV 殘留待遷移', '%d 行 EAV 殘留待遷移', $needs_dot[ $slug ], '2meet-data-optimizer' ),
$needs_dot[ $slug ]
)
);
?>
" aria-label="<?php esc_attr_e( '需要操作員注意', '2meet-data-optimizer' ); ?>"></span>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</nav>
<div class="wpdo-tab-content">
<?php self::render_tab( $tab ); ?>
</div>
</div>
<?php
}
/**
* Render the content for a specific tab.
*
* @param string $tab Tab slug to render.
* @return void
*/
private static function render_tab( string $tab ): void {
switch ( $tab ) {
case 'dashboard':
self::render_dashboard();
break;
case 'entity-bridge':
self::render_entity_bridge();
break;
case 'migration-wizard':
self::render_migration_wizard();
break;
case 'post-migration-wizard':
self::render_post_migration_wizard();
break;
case 'stress-test':
self::render_stress_test();
break;
case 'post-stress-test':
self::render_post_stress_test();
break;
case 'term-stress-test':
self::render_term_stress_test();
break;
case 'comment-stress-test':
self::render_comment_stress_test();
break;
case 'zones':
self::render_zones();
break;
case 'classifier':
self::render_classifier();
break;
case 'snapshots':
self::render_snapshots();
break;
case 'conflicts':
self::render_conflicts();
break;
case 'doctor':
self::render_doctor();
break;
case 'module-suggestions':
self::render_module_suggestions();
break;
case 'settings':
self::render_settings();
break;
case 'logs':
self::render_logs();
break;
case 'rest-api':
self::render_rest_api();
break;
case 'hpct-import':
self::render_hpct_import();
break;
case 'hivepress':
if ( class_exists( 'TMDO_Admin_HivePress' ) ) {
TMDO_Admin_HivePress::render();
} else {
self::render_dashboard();
}
break;
default:
self::render_dashboard();
}
}
// ── Tab renderers ─────────────────────────────────────────────────────
/**
* Dashboard tab: zone summary and health overview.
*/
private static function render_dashboard(): void {
global $wpdb;
$registry = TMDO_Schema_Registry::instance();
$stats = $registry->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();
?>
<section class="wpdo-kpi-hero" aria-label="<?php esc_attr_e( '關鍵指標', '2meet-data-optimizer' ); ?>">
<div class="wpdo-kpi-card wpdo-kpi--ratio">
<div class="wpdo-kpi-label"><?php esc_html_e( 'Postmeta 比例', '2meet-data-optimizer' ); ?></div>
<div class="wpdo-kpi-value">
<?php echo esc_html( '1:' . $kpi['ratio_str'] ); ?>
</div>
<div class="wpdo-kpi-sub">
<?php
printf(
/* translators: 1: posts count, 2: postmeta count */
esc_html__( '%1$s posts × %2$s postmeta', '2meet-data-optimizer' ),
esc_html( number_format_i18n( $kpi['posts'] ) ),
esc_html( number_format_i18n( $kpi['postmeta'] ) )
);
?>
</div>
</div>
<div class="wpdo-kpi-card wpdo-kpi--speedup">
<div class="wpdo-kpi-label"><?php esc_html_e( '查詢加速', '2meet-data-optimizer' ); ?></div>
<div class="wpdo-kpi-value">
<?php
if ( $kpi['speedup_x'] > 0 ) {
echo esc_html( number_format_i18n( $kpi['speedup_x'], 1 ) ) . '<span class="wpdo-kpi-unit">×</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- unit span hardcoded.
} else {
echo '<span class="wpdo-kpi-empty">—</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- hardcoded.
}
?>
</div>
<div class="wpdo-kpi-sub">
<?php
if ( '' !== $kpi['speedup_label'] ) {
echo esc_html( $kpi['speedup_label'] );
} else {
esc_html_e( '尚無 benchmark 資料', '2meet-data-optimizer' );
}
?>
</div>
</div>
<div class="wpdo-kpi-card wpdo-kpi--coverage">
<div class="wpdo-kpi-label"><?php esc_html_e( '優化欄位', '2meet-data-optimizer' ); ?></div>
<div class="wpdo-kpi-value">
<?php echo esc_html( number_format_i18n( $kpi['fields_total'] ) ); ?>
</div>
<div class="wpdo-kpi-sub">
<?php
printf(
/* translators: 1: hot fields, 2: cold fields */
esc_html__( 'Hot %1$d · Cold %2$d', '2meet-data-optimizer' ),
(int) $kpi['fields_hot'],
(int) $kpi['fields_cold']
);
?>
</div>
</div>
<div class="wpdo-kpi-card wpdo-kpi--health <?php echo esc_attr( 'wpdo-kpi--health-' . $kpi['health_class'] ); ?>">
<div class="wpdo-kpi-label"><?php esc_html_e( '健康分數', '2meet-data-optimizer' ); ?></div>
<div class="wpdo-kpi-value">
<?php echo esc_html( $kpi['health_score'] ); ?><span class="wpdo-kpi-unit">/100</span>
</div>
<div class="wpdo-kpi-sub">
<?php
if ( 0 === $kpi['errors_24h'] && 0 === $kpi['conflicts'] ) {
esc_html_e( '24 小時內無錯誤 / 衝突', '2meet-data-optimizer' );
} else {
printf(
/* translators: 1: error count, 2: conflict count */
esc_html__( '錯誤 %1$d · 衝突 %2$d', '2meet-data-optimizer' ),
(int) $kpi['errors_24h'],
(int) $kpi['conflicts']
);
}
?>
</div>
</div>
</section>
<div class="wpdo-grid">
<div class="wpdo-card">
<h2><?php esc_html_e( '系統概覽', '2meet-data-optimizer' ); ?></h2>
<table class="widefat striped">
<tbody>
<tr>
<th><?php esc_html_e( '資料庫引擎', '2meet-data-optimizer' ); ?></th>
<td><code><?php echo TMDO_IS_MYSQL ? 'MySQL' : 'SQLite'; ?></code></td>
</tr>
<tr>
<th><?php esc_html_e( '外掛版本', '2meet-data-optimizer' ); ?></th>
<td><code><?php echo esc_html( TMDO_VERSION ); ?></code></td>
</tr>
<tr>
<th><?php esc_html_e( 'DB Schema 版本', '2meet-data-optimizer' ); ?></th>
<td><code><?php echo esc_html( get_option( 'wpdo_db_version', 'N/A' ) ); ?></code></td>
</tr>
<tr>
<th><?php esc_html_e( 'HivePress', '2meet-data-optimizer' ); ?></th>
<td><?php echo wp_kses_post( $compat['hivepress'] ? '<span class="wpdo-badge wpdo-badge-ok">Active</span>' : '<span class="wpdo-badge">N/A</span>' ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'HP Custom Tables', '2meet-data-optimizer' ); ?></th>
<td>
<?php
if ( $compat['hpct_active'] && $compat['hpct_imported'] ) {
echo '<span class="wpdo-badge wpdo-badge-ok">Imported</span>';
} elseif ( $compat['hpct_active'] ) {
echo '<span class="wpdo-badge wpdo-badge-warn">Needs Import</span>';
} else {
echo '<span class="wpdo-badge">N/A</span>';
}
?>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Object Cache', '2meet-data-optimizer' ); ?></th>
<td>
<?php echo wp_using_ext_object_cache() ? '<span class="wpdo-badge wpdo-badge-ok">External</span>' : '<span class="wpdo-badge">Built-in</span>'; ?>
<?php if ( $cache['flush_support'] ) : ?>
<span class="wpdo-badge wpdo-badge-ok">Flush Support</span>
<?php endif; ?>
</td>
</tr>
</tbody>
</table>
</div>
<div class="wpdo-card">
<h2><?php esc_html_e( 'Zone 欄位統計', '2meet-data-optimizer' ); ?></h2>
<table class="widefat striped">
<thead>
<tr><th>Zone</th><th><?php esc_html_e( '欄位數', '2meet-data-optimizer' ); ?></th></tr>
</thead>
<tbody>
<tr><td><span class="wpdo-zone wpdo-zone-hot">Hot (A)</span></td><td><?php echo (int) $stats['hot']; ?></td></tr>
<tr><td><span class="wpdo-zone wpdo-zone-warm">Warm (B)</span></td><td><?php echo (int) $stats['warm']; ?></td></tr>
<tr><td><span class="wpdo-zone wpdo-zone-cold">Cold (C)</span></td><td><?php echo (int) $stats['cold']; ?></td></tr>
<tr><td><span class="wpdo-zone wpdo-zone-archive">Archive (D)</span></td><td><?php echo (int) $stats['archive']; ?></td></tr>
<tr><th><?php esc_html_e( '總計', '2meet-data-optimizer' ); ?></th><th><?php echo (int) $stats['total']; ?></th></tr>
</tbody>
</table>
</div>
</div>
<div class="wpdo-card wpdo-card-wide">
<h2><?php esc_html_e( '模組狀態', '2meet-data-optimizer' ); ?></h2>
<div class="wpdo-grid">
<div>
<h3>HPCT Modules</h3>
<table class="widefat striped">
<thead><tr><th><?php esc_html_e( '模組', '2meet-data-optimizer' ); ?></th><th><?php esc_html_e( '狀態', '2meet-data-optimizer' ); ?></th></tr></thead>
<tbody>
<?php foreach ( TMDO_Feature_Flags::hpct_modules() as $module => $state ) : ?>
<tr>
<td><code><?php echo esc_html( $module ); ?></code></td>
<td><span class="wpdo-state wpdo-state-<?php echo esc_attr( $state ); ?>"><?php echo esc_html( $state ); ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div>
<h3>Zone Modules</h3>
<table class="widefat striped">
<thead><tr><th><?php esc_html_e( '模組', '2meet-data-optimizer' ); ?></th><th><?php esc_html_e( '狀態', '2meet-data-optimizer' ); ?></th></tr></thead>
<tbody>
<?php foreach ( TMDO_Feature_Flags::zone_modules() as $module => $state ) : ?>
<tr>
<td><code><?php echo esc_html( $module ); ?></code></td>
<td><span class="wpdo-state wpdo-state-<?php echo esc_attr( $state ); ?>"><?php echo esc_html( $state ); ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="wpdo-card wpdo-card-wide">
<h2><?php esc_html_e( 'Zone 即時狀態', '2meet-data-optimizer' ); ?></h2>
<div class="wpdo-grid">
<div class="wpdo-card">
<h3><span class="wpdo-zone wpdo-zone-warm">Warm (B)</span> <?php esc_html_e( '即時概覽', '2meet-data-optimizer' ); ?></h3>
<table class="widefat striped">
<tbody>
<tr>
<th><?php esc_html_e( '有效條目', '2meet-data-optimizer' ); ?></th>
<td><strong><?php echo esc_html( number_format_i18n( $warm_active ) ); ?></strong></td>
</tr>
<tr>
<th><?php esc_html_e( '24h 內到期', '2meet-data-optimizer' ); ?></th>
<td><?php echo esc_html( number_format_i18n( $warm_soon ) ); ?></td>
</tr>
</tbody>
</table>
<?php if ( ! empty( $top_views ) ) : ?>
<h4 class="wpdo-mt-2"><?php esc_html_e( 'Top 10 瀏覽數 (Warm)', '2meet-data-optimizer' ); ?></h4>
<table class="widefat striped">
<thead>
<tr>
<th>Post ID</th>
<th><?php esc_html_e( '瀏覽數', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $top_views as $v ) : ?>
<tr>
<td><a href="<?php echo esc_url( get_edit_post_link( (int) $v['post_id'] ) ); ?>"><?php echo (int) $v['post_id']; ?></a></td>
<td><?php echo esc_html( number_format_i18n( (int) $v['views'] ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else : ?>
<p class="description wpdo-mt-1"><?php esc_html_e( '目前無暖區瀏覽數據。', '2meet-data-optimizer' ); ?></p>
<?php endif; ?>
</div>
<div class="wpdo-card">
<h3><span class="wpdo-zone wpdo-zone-archive">Archive (D)</span> <?php esc_html_e( '歸檔統計', '2meet-data-optimizer' ); ?></h3>
<table class="widefat striped">
<tbody>
<tr>
<th><?php esc_html_e( '總歸檔筆數', '2meet-data-optimizer' ); ?></th>
<td><strong><?php echo esc_html( number_format_i18n( $archive_stats['total_rows'] ) ); ?></strong></td>
</tr>
<tr>
<th><?php esc_html_e( '已壓縮', '2meet-data-optimizer' ); ?></th>
<td>
<?php
$pct = $archive_stats['total_rows'] > 0
? round( $archive_stats['compressed_rows'] / $archive_stats['total_rows'] * 100 )
: 0;
echo esc_html( number_format_i18n( $archive_stats['compressed_rows'] ) );
echo ' <span class="wpdo-badge">' . (int) $pct . '%</span>';
?>
</td>
</tr>
</tbody>
</table>
<?php if ( ! empty( $archive_stats['post_types'] ) ) : ?>
<h4 class="wpdo-mt-2"><?php esc_html_e( '按 Post Type', '2meet-data-optimizer' ); ?></h4>
<table class="widefat striped">
<thead>
<tr>
<th>Post Type</th>
<th><?php esc_html_e( '筆數', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $archive_stats['post_types'] as $pt ) : ?>
<tr>
<td><code><?php echo esc_html( $pt['post_type'] ); ?></code></td>
<td><?php echo esc_html( number_format_i18n( (int) $pt['cnt'] ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else : ?>
<p class="description wpdo-mt-1"><?php esc_html_e( '目前無歸檔資料。', '2meet-data-optimizer' ); ?></p>
<?php endif; ?>
</div>
</div>
</div>
<div class="wpdo-card wpdo-card-wide">
<h2><?php esc_html_e( 'REST API 速率限制統計', '2meet-data-optimizer' ); ?></h2>
<p class="description">
<?php esc_html_e( 'POST /view 端點因 IP 或 Cookie 重複計數而被拒絕的次數(HTTP 429)。', '2meet-data-optimizer' ); ?>
<?php if ( $rl_total > 0 ) : ?>
<form method="post" style="display:inline">
<input type="hidden" name="wpdo_reset_rl_stats" value="1">
<?php wp_nonce_field( 'wpdo_reset_rl_stats' ); ?>
<button type="submit" class="button button-small"><?php esc_html_e( '重置統計', '2meet-data-optimizer' ); ?></button>
</form>
<?php endif; ?>
</p>
<table class="widefat striped wpdo-table--narrow">
<tbody>
<tr>
<th><?php esc_html_e( '總 429 事件', '2meet-data-optimizer' ); ?></th>
<td><strong><?php echo esc_html( number_format_i18n( $rl_total ) ); ?></strong></td>
</tr>
<tr>
<th><?php esc_html_e( '受影響的 Post 數', '2meet-data-optimizer' ); ?></th>
<td><?php echo esc_html( number_format_i18n( count( $rl_stats ) ) ); ?></td>
</tr>
</tbody>
</table>
<?php if ( ! empty( $rl_top ) ) : ?>
<h4 class="wpdo-mt-2"><?php esc_html_e( 'Top 10 被限速 Post', '2meet-data-optimizer' ); ?></h4>
<table class="widefat striped wpdo-table--narrow">
<thead>
<tr>
<th>Post ID</th>
<th><?php esc_html_e( '429 次數', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $rl_top as $post_id => $count ) : ?>
<tr>
<td><a href="<?php echo esc_url( get_edit_post_link( (int) $post_id ) ); ?>"><?php echo (int) $post_id; ?></a></td>
<td><?php echo esc_html( number_format_i18n( (int) $count ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else : ?>
<p class="description wpdo-mt-1"><?php esc_html_e( '目前無速率限制事件記錄。', '2meet-data-optimizer' ); ?></p>
<?php endif; ?>
</div>
<div class="wpdo-card wpdo-card-wide">
<h2><?php esc_html_e( 'Entity Bridge 狀態', '2meet-data-optimizer' ); ?></h2>
<?php
$hb_enabled = class_exists( 'TMDO_Hook_Bus_Bridge' ) && TMDO_Hook_Bus_Bridge::is_enabled();
$bridge_modes = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::all() : array();
$mode_badge = array(
'disabled' => 'background:#e0e0e0;color:#444',
'dual_write' => 'background:#d4edda;color:#155724',
'shadow_read' => 'background:#fff3cd;color:#856404',
'aeav_only' => 'background:#cce5ff;color:#004085',
);
?>
<table class="widefat striped wpdo-table--narrow">
<thead>
<tr>
<th><?php esc_html_e( 'Entity', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( 'Hook Bus', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '目前模式', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '已登錄欄位群組', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( array( 'post', 'user', 'term', 'comment' ) as $etype ) : ?>
<?php
$mode = $bridge_modes[ $etype ] ?? 'disabled';
$style = $mode_badge[ $mode ] ?? 'background:#e0e0e0;color:#444';
$groups = class_exists( 'TMDO_Entity_Registry' ) ? TMDO_Entity_Registry::get_groups_for_type( $etype ) : array();
$is_post = 'post' === $etype;
?>
<tr>
<td><code><?php echo esc_html( $etype ); ?></code></td>
<td>
<?php if ( $is_post ) : ?>
<em><?php esc_html_e( '由 Feature_Flags FSM 管理', '2meet-data-optimizer' ); ?></em>
<?php elseif ( $hb_enabled ) : ?>
<span style="color:#155724">✓ <?php esc_html_e( '啟用', '2meet-data-optimizer' ); ?></span>
<?php else : ?>
<span style="color:#721c24">✗ <?php esc_html_e( '停用', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
<td>
<span style="display:inline-block;padding:2px 8px;border-radius:3px;font-size:12px;<?php echo esc_attr( $style ); ?>">
<?php echo esc_html( $mode ); ?>
</span>
</td>
<td>
<?php if ( empty( $groups ) ) : ?>
<span style="color:#999"><?php esc_html_e( '(無)', '2meet-data-optimizer' ); ?></span>
<?php else : ?>
<?php foreach ( $groups as $g ) : ?>
<code style="margin-right:4px"><?php echo esc_html( $g ); ?></code>
<?php endforeach; ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="description wpdo-mt-1">
<a href="<?php echo esc_url( add_query_arg( 'tab', 'settings', admin_url( 'tools.php?page=wp-data-optimizer' ) ) ); ?>">
<?php esc_html_e( '→ 設定 Entity Bridge 模式', '2meet-data-optimizer' ); ?>
</a>
&nbsp;|&nbsp;
<a href="<?php echo esc_url( add_query_arg( 'tab', 'zones', admin_url( 'tools.php?page=wp-data-optimizer' ) ) ); ?>">
<?php esc_html_e( '→ 查看 Entity 欄位登錄', '2meet-data-optimizer' ); ?>
</a>
</p>
</div>
<?php
}
/**
* Compute KPI hero metrics for the dashboard top strip (v2.16.0).
*
* Read-only — purely composes existing data sources:
* - posts/postmeta count from wp_posts/wp_postmeta
* - speedup ratio from latest wpdo_benchmarks row
* - field coverage from TMDO_Schema_Registry
* - 24h error count from wpdo_errors + conflict count from TMDO_Conflict_Monitor
*
* Cached in a 5-minute transient — dashboard reads stay cheap on busy sites.
*
* @return array{
* ratio_str:string, posts:int, postmeta:int,
* speedup_x:float, speedup_label:string,
* fields_total:int, fields_hot:int, fields_cold:int,
* health_score:int, health_class:string, errors_24h:int, conflicts:int
* }
*/
private static function compute_dashboard_kpi(): array {
$cached = get_transient( 'wpdo_kpi_hero_v1' );
if ( is_array( $cached ) ) {
return $cached;
}
global $wpdb;
// 1) Postmeta:Posts ratio.
$posts_count = (int) $wpdb->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' );
?>
<div class="wpdo-entity-bridge-tab">
<h2><?php esc_html_e( 'Entity Bridge — 遷移嚮導', '2meet-data-optimizer' ); ?></h2>
<p class="description">
<?php esc_html_e( '以下四張健康卡片顯示 user / post / term / comment entity 目前的遷移狀態與覆蓋率。可在此直接觸發 Backfill、升級或降級模式。進度每 5 秒自動更新(user/term/comment);post 進度需手動重整。', '2meet-data-optimizer' ); ?>
</p>
<div class="wpdo-entity-bridge-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(380px,1fr));gap:20px;margin-top:20px;">
<?php
foreach ( array( 'user', 'post', 'term', 'comment' ) as $etype ) :
$info = $health_data[ $etype ] ?? array();
$mode = $info['mode'] ?? 'disabled';
$mode_days = $info['mode_days'] ?? 0;
$groups = $info['groups'] ?? array();
$diffs = $info['shadow_diffs'] ?? 0;
$rec = $info['recommendation'] ?? '';
$ap = $info['auto_promote'] ?? array();
$next_mode = $info['next_mode'] ?? null;
$prev_mode = $info['prev_mode'] ?? null;
$bf_active = $info['backfill_active'] ?? false;
$badge_style = $mode_style[ $mode ] ?? 'background:#e0e0e0;color:#444';
$native_entity_table = $info['native_entity_table'] ?? '';
$native_entity_count = (int) ( $info['native_entity_count'] ?? 0 );
$native_meta_table = $info['native_meta_table'] ?? '';
$native_meta_count = (int) ( $info['native_meta_count'] ?? 0 );
?>
<div class="wpdo-card" data-entity-type="<?php echo esc_attr( $etype ); ?>"
style="padding:20px;border-radius:8px;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.1);">
<!-- Header -->
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px;">
<div>
<h3 style="margin:0 0 4px;font-size:16px;"><?php echo esc_html( ucfirst( $etype ) ); ?> Entity</h3>
<span class="wpdo-mode-badge" style="display:inline-block;padding:2px 10px;border-radius:20px;font-size:12px;font-weight:600;<?php echo esc_attr( $badge_style ); ?>">
<?php echo esc_html( $mode ); ?>
</span>
<span class="wpdo-mode-days" style="margin-left:8px;font-size:12px;color:#666;">
<?php echo esc_html( $mode_days ); ?> 天
</span>
</div>
<?php if ( $bf_active ) : ?>
<span style="font-size:12px;color:#856404;background:#fff3cd;padding:2px 8px;border-radius:4px;">
⟳ <?php esc_html_e( 'Backfill 執行中', '2meet-data-optimizer' ); ?>
</span>
<?php endif; ?>
</div>
<!-- Pipeline -->
<div class="wpdo-pipeline" style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:16px;font-size:11px;">
<?php
foreach ( $mode_order as $i => $m ) :
$active = ( $m === $mode );
?>
<?php if ( $i > 0 ) : ?>
<span style="color:#999;">→</span>
<?php endif; ?>
<span class="wpdo-pipeline-dot <?php echo $active ? 'wpdo-pipeline-active' : ''; ?>"
style="padding:2px 8px;border-radius:3px;<?php echo $active ? esc_attr( $badge_style ) : 'background:#f0f0f0;color:#666'; ?>">
<?php echo esc_html( $mode_labels[ $m ] ); ?>
</span>
<?php endforeach; ?>
</div>
<!-- Native EAV counts -->
<?php if ( $native_entity_table || $native_meta_table ) : ?>
<div class="wpdo-native-counts"
style="margin-bottom:14px;padding:6px 10px;background:#f8f9fa;border-radius:4px;font-size:12px;color:#555;display:flex;flex-wrap:wrap;gap:4px;align-items:center;">
<span style="font-weight:600;margin-right:4px;"><?php esc_html_e( '原生資料:', '2meet-data-optimizer' ); ?></span>
<?php if ( $native_entity_table ) : ?>
<code style="font-size:11px;"><?php echo esc_html( $native_entity_table ); ?></code>
<span><?php echo esc_html( number_format( $native_entity_count ) ); ?> <?php esc_html_e( '筆', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
<?php if ( $native_entity_table && $native_meta_table ) : ?>
<span style="color:#ccc;">|</span>
<?php endif; ?>
<?php if ( $native_meta_table ) : ?>
<code style="font-size:11px;"><?php echo esc_html( $native_meta_table ); ?></code>
<span><?php echo esc_html( number_format( $native_meta_count ) ); ?> <?php esc_html_e( '筆 meta', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Groups / Coverage -->
<?php if ( ! empty( $groups ) ) : ?>
<div style="margin-bottom:14px;">
<strong style="font-size:12px;text-transform:uppercase;color:#666;letter-spacing:.5px;">
<?php esc_html_e( '群組覆蓋率', '2meet-data-optimizer' ); ?>
</strong>
<?php
foreach ( $groups as $g ) :
$cov_pct = (float) ( $g['coverage_pct'] ?? 0 );
$bar_color = $cov_pct >= 99 ? '#28a745' : ( $cov_pct >= 50 ? '#ffc107' : '#dc3545' );
?>
<div data-group="<?php echo esc_attr( $g['name'] ); ?>" style="margin-top:8px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">
<span style="font-size:13px;font-weight:500;"><?php echo esc_html( $g['name'] ); ?></span>
<span style="font-size:12px;color:#666;">
<code style="font-size:11px;"><?php echo esc_html( $g['fields_count'] ); ?> 欄位</code>
&nbsp;
<span class="wpdo-mig-status" style="font-size:11px;padding:1px 6px;border-radius:3px;background:#f0f0f0;">
<?php echo esc_html( $g['migration_status'] ?? 'not_started' ); ?>
</span>
</span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<div style="flex:1;height:8px;background:#e0e0e0;border-radius:4px;overflow:hidden;">
<div class="wpdo-cov-bar-fill"
style="height:100%;background:<?php echo esc_attr( $bar_color ); ?>;width:<?php echo esc_attr( $cov_pct ); ?>%;transition:width .4s ease;border-radius:4px;">
</div>
</div>
<span class="wpdo-cov-pct" style="font-size:12px;min-width:42px;text-align:right;color:#333;">
<?php echo esc_html( number_format( $cov_pct, 1 ) ); ?>%
</span>
</div>
<div style="font-size:11px;color:#888;margin-top:2px;">
<?php esc_html_e( 'flat:', '2meet-data-optimizer' ); ?>
<span class="wpdo-cov-rows">
<?php echo esc_html( $g['flat_rows'] ); ?> / <?php echo esc_html( $g['eav_rows'] ); ?> EAV
</span>
</div>
<?php if ( $g['table_exists'] ) : ?>
<div style="margin-top:6px;">
<button type="button"
class="button button-small"
data-wpdo-action="backfill"
data-entity-type="<?php echo esc_attr( $etype ); ?>"
data-group-name="<?php echo esc_attr( $g['name'] ); ?>">
<?php esc_html_e( '啟動 Backfill', '2meet-data-optimizer' ); ?>
</button>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php else : ?>
<div style="margin-bottom:14px;">
<p class="description" style="margin:0 0 6px;font-size:13px;">
<?php esc_html_e( '(尚無已登錄的欄位群組)', '2meet-data-optimizer' ); ?>
</p>
<p style="margin:0;font-size:12px;color:#856404;background:#fff3cd;padding:6px 10px;border-radius:4px;line-height:1.6;">
<?php
esc_html_e(
'Hook Bus 已設定攔截此 entity type,但目前尚無 partner plugin 透過 wpdo_register_entity_fields 登錄欄位群組。Flat table 不會自動建立——直到有群組被登錄後才開始生效。',
'2meet-data-optimizer'
);
?>
</p>
</div>
<?php endif; ?>
<!-- Shadow diffs -->
<?php if ( 'shadow_read' === $mode ) : ?>
<div style="margin-bottom:12px;padding:8px;background:#f8f9fa;border-radius:4px;font-size:13px;">
<?php esc_html_e( 'Shadow Diffs', '2meet-data-optimizer' ); ?>
<strong class="wpdo-shadow-diffs" style="color:<?php echo $diffs > 0 ? '#dc3545' : '#28a745'; ?>;">
<?php echo esc_html( $diffs ); ?>
</strong>
<?php if ( $diffs > 0 ) : ?>
<span style="color:#666;font-size:12px;"> — <?php esc_html_e( '請調查差異後再升級', '2meet-data-optimizer' ); ?></span>
<?php else : ?>
<span style="color:#28a745;font-size:12px;"> ✓ <?php esc_html_e( '無差異', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Auto-promote status -->
<?php if ( ! empty( $ap ) ) : ?>
<div style="margin-bottom:12px;font-size:12px;color:#666;">
<?php esc_html_e( 'Auto-promote', '2meet-data-optimizer' ); ?>
<span class="wpdo-auto-promote-eligible" style="color:<?php echo ! empty( $ap['eligible'] ) ? '#155724' : '#856404'; ?>;">
<?php if ( ! empty( $ap['eligible'] ) ) : ?>
✓ <?php esc_html_e( '可升級', '2meet-data-optimizer' ); ?>
<?php else : ?>
— <?php echo esc_html( $ap['reason'] ?? '' ); ?>
<?php endif; ?>
</span>
<?php if ( $ap['enabled'] ?? false ) : ?>
<span style="background:#d4edda;color:#155724;padding:1px 5px;border-radius:3px;margin-left:4px;font-size:11px;">
<?php esc_html_e( '自動', '2meet-data-optimizer' ); ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Recommendation -->
<?php if ( $rec ) : ?>
<div class="wpdo-recommendation"
style="margin-bottom:14px;padding:8px 10px;background:#f0f4ff;border-left:3px solid #4f6ef7;border-radius:0 4px 4px 0;font-size:13px;color:#333;">
<?php echo esc_html( $rec ); ?>
</div>
<?php endif; ?>
<!-- Action buttons -->
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<button type="button"
class="button wpdo-btn-promote"
data-wpdo-action="promote"
data-entity-type="<?php echo esc_attr( $etype ); ?>"
data-next-mode="<?php echo esc_attr( $next_mode ?? '' ); ?>"
<?php disabled( ! $next_mode ); ?>
title="<?php echo esc_attr( $next_mode ? '升級到 ' . $next_mode : '已在最高模式' ); ?>">
↑ <?php esc_html_e( '升級模式', '2meet-data-optimizer' ); ?>
</button>
<button type="button"
class="button wpdo-btn-demote"
data-wpdo-action="demote"
data-entity-type="<?php echo esc_attr( $etype ); ?>"
data-prev-mode="<?php echo esc_attr( $prev_mode ?? '' ); ?>"
<?php disabled( ! $prev_mode ); ?>
title="<?php echo esc_attr( $prev_mode ? '降級到 ' . $prev_mode : '已在最低模式' ); ?>"
style="<?php echo $prev_mode ? '' : 'opacity:.5;'; ?>">
↓ <?php esc_html_e( '降級模式', '2meet-data-optimizer' ); ?>
</button>
</div>
</div><!-- .wpdo-card -->
<?php endforeach; ?>
</div><!-- .wpdo-entity-bridge-grid -->
<!-- Migration Guide -->
<div class="wpdo-card wpdo-card-wide" style="margin-top:24px;padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.1);">
<h3 style="margin-top:0;"><?php esc_html_e( '遷移路徑說明', '2meet-data-optimizer' ); ?></h3>
<ol style="margin-left:20px;line-height:1.9;font-size:13px;">
<li>
<strong>disabled → dual_write</strong>
<?php esc_html_e( 'Hook Bus 開始對 flat table 雙寫(讀取仍走原生 EAV)。此步驟安全,可隨時回退。', '2meet-data-optimizer' ); ?>
</li>
<li>
<strong><?php esc_html_e( '啟動 Backfill', '2meet-data-optimizer' ); ?></strong>
<?php esc_html_e( '將歷史 EAV meta 資料非同步搬移到 flat table。進度條顯示覆蓋率,完成前不建議升級到 shadow_read。', '2meet-data-optimizer' ); ?>
</li>
<li>
<strong>dual_write → shadow_read</strong>
<?php esc_html_e( '讀取切換至 flat table,同時比對 EAV 值差異(Shadow Diff)。若有差異會記錄供調查。', '2meet-data-optimizer' ); ?>
</li>
<li>
<strong><?php esc_html_e( '觀察穩定期', '2meet-data-optimizer' ); ?></strong>
<?php esc_html_e( '建議至少觀察 7 天(可於設定中調整)。Shadow Diffs 歸零且無異常後,即可升級到最終模式。', '2meet-data-optimizer' ); ?>
</li>
<li>
<strong>shadow_read → aeav_only</strong>
<?php esc_html_e( '所有讀寫均走 flat table,原生 EAV 不再寫入。效能最佳,適合生產環境。', '2meet-data-optimizer' ); ?>
</li>
</ol>
<p class="description" style="margin-top:8px;">
<?php esc_html_e( '任何步驟均可透過「降級模式」安全回退(EAV 資料一直保留直到 aeav_only)。Auto-promote 設定請前往', '2meet-data-optimizer' ); ?>
<a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=settings' ) ); ?>">
<?php esc_html_e( '設定頁面', '2meet-data-optimizer' ); ?>
</a>。
</p>
</div>
</div><!-- .wpdo-entity-bridge-tab -->
<?php
}
/**
* Stress Test tab (v2.6.7): user entity 壓力測試 + benchmark 工具。
*/
/**
* Migration Wizard tab — one-click User Entity migration UI.
*
* @since 2.8.0
*/
private static function render_migration_wizard(): void {
$preflight = class_exists( 'TMDO_Migration_Orchestrator' )
? TMDO_Migration_Orchestrator::preflight()
: array();
$status = class_exists( 'TMDO_Migration_Orchestrator' )
? TMDO_Migration_Orchestrator::get_status()
: array( 'state' => '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 );
?>
<div class="wpdo-stress-test-tab">
<h2><?php esc_html_e( 'User Entity 壓力測試 & Benchmark', '2meet-data-optimizer' ); ?></h2>
<!-- Warning banner -->
<div style="margin:14px 0;padding:12px 14px;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545;border-radius:4px;font-size:13px;line-height:1.6;">
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
<?php esc_html_e( '它會建立大量測試使用者並填滿所有 user 相關 flat tableshot / cold / membership / activity / profile / sso / points_ledger)。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
</div>
<p class="description">
<?php esc_html_e( '透過自動產生大量測試使用者,評估 user entity 反 EAV 系統在不同規模下的寫入吞吐、DB 容量與查詢效能。所有測試使用者均以 test{n} 命名、密碼為 PassWord2026!,可一鍵清除。', '2meet-data-optimizer' ); ?>
</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px;">
<!-- Left: Configure & Start -->
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);">
<h3 style="margin-top:0;"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
<table class="form-table" style="margin-top:0;">
<tr>
<th style="width:35%;"><label for="wpdo-st-target"><?php esc_html_e( '要建立的使用者數', '2meet-data-optimizer' ); ?></label></th>
<td>
<input type="number" id="wpdo-st-target" min="1" max="1000000" value="1000" class="regular-text" />
<p class="description"><?php esc_html_e( '常用:100 / 1,000 / 10,000 / 100,000', '2meet-data-optimizer' ); ?></p>
</td>
</tr>
<tr>
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
<td>
<label style="display:block;margin-bottom:6px;">
<input type="radio" name="wpdo-st-mode" value="fast" checked />
<strong>Fast</strong> — <?php esc_html_e( '直接 bulk INSERT,跳過 hooks~5,000-10,000 users/秒)', '2meet-data-optimizer' ); ?>
</label>
<label style="display:block;">
<input type="radio" name="wpdo-st-mode" value="realistic" />
<strong>Realistic</strong> — <?php esc_html_e( '走 wp_insert_user + Hook Bus~50-200 users/秒,測試生產路徑)', '2meet-data-optimizer' ); ?>
</label>
</td>
</tr>
<tr>
<th><label for="wpdo-st-batch"><?php esc_html_e( '批次大小', '2meet-data-optimizer' ); ?></label></th>
<td>
<input type="number" id="wpdo-st-batch" min="1" max="2000" value="500" class="small-text" />
<p class="description"><?php esc_html_e( '每批執行有 25 秒 wall-clock 上限(避免 nginx 504)。Fast 建議 500-2000Realistic 建議 5-10(每 user ~3 秒)', '2meet-data-optimizer' ); ?></p>
</td>
</tr>
</table>
<p>
<button type="button" class="button button-primary button-large" id="wpdo-st-start" <?php disabled( $is_running ); ?>>
<?php esc_html_e( '🚀 啟動壓力測試', '2meet-data-optimizer' ); ?>
</button>
<button type="button" class="button" id="wpdo-st-cancel" <?php disabled( ! $is_running ); ?>>
<?php esc_html_e( '⏹ 取消', '2meet-data-optimizer' ); ?>
</button>
</p>
</div>
<!-- Right: Cleanup -->
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);">
<h3 style="margin-top:0;"><?php esc_html_e( '清除測試資料', '2meet-data-optimizer' ); ?></h3>
<p>
<?php esc_html_e( '目前 test_* 使用者數量:', '2meet-data-optimizer' ); ?>
<strong id="wpdo-st-count" style="font-size:18px;color:#dc3545;">
<?php echo esc_html( number_format( $test_user_count ) ); ?>
</strong>
</p>
<p class="description">
<?php esc_html_e( '一鍵清除所有 user_login LIKE "test%" 的使用者,連同所有 user 相關 flat table 中對應 user_id 的資料一併移除。', '2meet-data-optimizer' ); ?>
</p>
<p>
<button type="button" class="button button-secondary" id="wpdo-st-cleanup" <?php disabled( $is_running || 0 === $test_user_count ); ?>>
<?php esc_html_e( '🗑 清除全部測試使用者', '2meet-data-optimizer' ); ?>
</button>
</p>
<hr style="margin:18px 0;" />
<p>
<button type="button" class="button" id="wpdo-st-rerun-bench" <?php disabled( $is_running || 0 === $test_user_count ); ?>>
<?php esc_html_e( '📊 重跑 Benchmark(不新增資料)', '2meet-data-optimizer' ); ?>
</button>
</p>
<p class="description">
<?php esc_html_e( '針對目前 DB 狀態重新執行查詢效能測試。', '2meet-data-optimizer' ); ?>
</p>
</div>
</div>
<!-- Progress section (live) -->
<div id="wpdo-st-progress-card" class="wpdo-card" style="margin-top:20px;padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);<?php echo $is_running ? '' : 'display:none;'; ?>">
<h3 style="margin-top:0;"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
<div style="margin-bottom:10px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;font-size:13px;">
<span><strong id="wpdo-st-pg-status"><?php echo esc_html( $status ); ?></strong> · <span id="wpdo-st-pg-mode"><?php echo esc_html( $state['mode'] ?? '' ); ?></span> mode</span>
<span id="wpdo-st-pg-pct" style="font-weight:600;"><?php echo esc_html( $state['pct'] ?? 0 ); ?>%</span>
</div>
<div style="height:14px;background:#e0e0e0;border-radius:7px;overflow:hidden;">
<div id="wpdo-st-pg-bar" style="height:100%;background:linear-gradient(90deg,#28a745,#20c997);width:<?php echo esc_attr( $state['pct'] ?? 0 ); ?>%;transition:width .4s;"></div>
</div>
</div>
<table style="width:100%;font-size:13px;margin-top:10px;border-collapse:collapse;">
<tr>
<td style="padding:6px;color:#666;"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
<td style="padding:6px;font-weight:600;"><span id="wpdo-st-pg-processed"><?php echo esc_html( $state['processed'] ?? 0 ); ?></span> / <span id="wpdo-st-pg-target"><?php echo esc_html( $state['target'] ?? 0 ); ?></span></td>
<td style="padding:6px;color:#666;"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
<td style="padding:6px;font-weight:600;"><span id="wpdo-st-pg-rate"><?php echo esc_html( $state['rate_per_sec'] ?? 0 ); ?></span> users/sec</td>
</tr>
<tr>
<td style="padding:6px;color:#666;"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
<td style="padding:6px;"><span id="wpdo-st-pg-elapsed"><?php echo esc_html( $state['elapsed_sec'] ?? 0 ); ?></span> 秒</td>
<td style="padding:6px;color:#666;"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
<td style="padding:6px;"><span id="wpdo-st-pg-eta"><?php echo esc_html( $state['eta_sec'] ?? 0 ); ?></span> 秒</td>
</tr>
<tr>
<td style="padding:6px;color:#666;"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
<td style="padding:6px;"><span id="wpdo-st-pg-batches"><?php echo esc_html( $state['batches_done'] ?? 0 ); ?></span></td>
<td style="padding:6px;color:#666;"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
<td style="padding:6px;"><span id="wpdo-st-pg-mem"><?php echo esc_html( round( ( $state['peak_memory'] ?? 0 ) / 1048576, 1 ) ); ?></span> MB</td>
</tr>
</table>
</div>
<!-- Benchmark report -->
<div id="wpdo-st-bench-card" class="wpdo-card" style="margin-top:20px;padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);<?php echo ! empty( $state['benchmark'] ) ? '' : 'display:none;'; ?>">
<h3 style="margin-top:0;"><?php esc_html_e( '3. Benchmark 報告', '2meet-data-optimizer' ); ?></h3>
<div id="wpdo-st-bench-content">
<?php if ( ! empty( $state['benchmark'] ) ) : ?>
<?php self::render_stress_benchmark( $state['benchmark'] ); ?>
<?php endif; ?>
</div>
</div>
</div><!-- .wpdo-stress-test-tab -->
<?php
}
/**
* 渲染 benchmark 報告(Server-side rendered,亦可由 JS 呼叫等價結構)。
*
* @param array $bench Benchmark report payload.
*/
private static function render_stress_benchmark( array $bench ): void {
$write = $bench['write'] ?? array();
$db_sizes = $bench['db_sizes'] ?? array();
$query = $bench['query'] ?? array();
?>
<h4 style="margin-bottom:6px;">▍ <?php esc_html_e( '寫入指標', '2meet-data-optimizer' ); ?></h4>
<table class="widefat" style="margin-bottom:14px;">
<tbody>
<tr><td><?php esc_html_e( '模式', '2meet-data-optimizer' ); ?></td><td><code><?php echo esc_html( $write['mode'] ?? '' ); ?></code></td></tr>
<tr><td><?php esc_html_e( '完成 / 目標', '2meet-data-optimizer' ); ?></td><td><?php echo esc_html( number_format( (int) ( $write['processed'] ?? 0 ) ) ); ?> / <?php echo esc_html( number_format( (int) ( $write['target'] ?? 0 ) ) ); ?></td></tr>
<tr><td><?php esc_html_e( '總耗時', '2meet-data-optimizer' ); ?></td><td><?php echo esc_html( number_format( (int) ( $write['elapsed_sec'] ?? 0 ) ) ); ?> 秒</td></tr>
<tr><td><?php esc_html_e( '平均速率', '2meet-data-optimizer' ); ?></td><td><strong><?php echo esc_html( $write['rate_per_sec'] ?? 0 ); ?></strong> users/sec</td></tr>
<tr><td><?php esc_html_e( '批次數', '2meet-data-optimizer' ); ?></td><td><?php echo esc_html( $write['batches_done'] ?? 0 ); ?></td></tr>
<tr><td><?php esc_html_e( '批次最快/平均/最慢', '2meet-data-optimizer' ); ?></td><td><?php echo esc_html( $write['batch_min_ms'] ?? 0 ); ?> / <?php echo esc_html( $write['batch_avg_ms'] ?? 0 ); ?> / <?php echo esc_html( $write['batch_max_ms'] ?? 0 ); ?> ms</td></tr>
<tr><td><?php esc_html_e( 'PHP Peak Memory', '2meet-data-optimizer' ); ?></td><td><?php echo esc_html( $write['peak_memory_mb'] ?? 0 ); ?> MB</td></tr>
</tbody>
</table>
<h4 style="margin-bottom:6px;">▍ <?php esc_html_e( 'DB 容量(user 相關表)', '2meet-data-optimizer' ); ?></h4>
<table class="widefat striped" style="margin-bottom:14px;">
<thead>
<tr>
<th><?php esc_html_e( 'Table', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( 'Rows', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( 'Data MB', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( 'Index MB', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( 'Total MB', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( 'Avg bytes/row', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $db_sizes as $row ) : ?>
<tr>
<td><code><?php echo esc_html( $row['table'] ); ?></code></td>
<td><?php echo esc_html( number_format( (int) ( $row['rows'] ?? 0 ) ) ); ?></td>
<td><?php echo esc_html( $row['data_mb'] ?? '—' ); ?></td>
<td><?php echo esc_html( $row['index_mb'] ?? '—' ); ?></td>
<td><strong><?php echo esc_html( $row['total_mb'] ?? '—' ); ?></strong></td>
<td><?php echo esc_html( $row['avg_bytes'] ?? '—' ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h4 style="margin-bottom:6px;">▍ <?php esc_html_e( '查詢效能', '2meet-data-optimizer' ); ?></h4>
<table class="widefat striped" style="margin-bottom:8px;">
<thead>
<tr>
<th><?php esc_html_e( '測試項目', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '樣本', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '總時間 (ms)', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '平均 (ms)', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( 'QPS', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php
$query_labels = array(
'get_field_membership_level' => __( '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' => __( 'JOINtop 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;
}
?>
<tr>
<td><?php echo esc_html( $label ); ?></td>
<td><?php echo esc_html( $q['n'] ?? 1 ); ?></td>
<td><strong><?php echo esc_html( $q['total_ms'] ?? $q['duration_ms'] ?? '—' ); ?></strong></td>
<td><?php echo esc_html( $q['avg_ms'] ?? '—' ); ?></td>
<td><?php echo esc_html( $q['qps'] ?? '—' ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="description">
<?php esc_html_e( '原生 EAV baseline 與 flat table 範圍查詢的時間差,即代表此規模下反 EAV 帶來的查詢加速倍數。', '2meet-data-optimizer' ); ?>
</p>
<?php
}
/**
* Zones tab: show all registered field mappings.
*/
private static function render_zones(): void {
$registry = TMDO_Schema_Registry::instance();
$fields = $registry->all();
?>
<h2><?php esc_html_e( 'Zone 欄位映射', '2meet-data-optimizer' ); ?></h2>
<p class="description"><?php esc_html_e( '以下是所有已註冊的 postmeta 欄位及其 Zone 分配。可透過 wpdo_register_fields action 或 HivePress 整合自動註冊。', '2meet-data-optimizer' ); ?></p>
<?php if ( empty( $fields ) ) : ?>
<div class="notice notice-info inline"><p><?php esc_html_e( '尚無已註冊的欄位映射。啟用 HivePress 或手動註冊欄位後即會顯示。', '2meet-data-optimizer' ); ?></p></div>
<?php else : ?>
<table class="widefat striped wpdo-table">
<thead>
<tr>
<th>Post Type</th>
<th>Meta Key</th>
<th>Zone</th>
<th>Column</th>
<th>Data Type</th>
<th>Indexed</th>
<th>Provider</th>
</tr>
</thead>
<tbody>
<?php foreach ( $fields as $field ) : ?>
<tr>
<td><code><?php echo esc_html( $field['post_type'] ); ?></code></td>
<td><code><?php echo esc_html( $field['meta_key'] ); ?></code></td>
<td><span class="wpdo-zone wpdo-zone-<?php echo esc_attr( $field['zone'] ); ?>"><?php echo esc_html( ucfirst( $field['zone'] ) ); ?></span></td>
<td><code><?php echo esc_html( $field['column'] ); ?></code></td>
<td><code><?php echo esc_html( $field['data_type'] ); ?></code></td>
<td><?php echo ! empty( $field['indexed'] ) ? esc_html__( 'Yes', '2meet-data-optimizer' ) : '—'; ?></td>
<td><?php echo esc_html( $field['provider'] ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
// ── Entity Registry 欄位(user / term / comment) ──────────────────
if ( class_exists( 'TMDO_Entity_Registry' ) ) :
$non_post_types = array( 'user', 'term', 'comment' );
$has_any = false;
foreach ( $non_post_types as $etype ) {
if ( ! empty( TMDO_Entity_Registry::get_groups_for_type( $etype ) ) ) {
$has_any = true;
break;
}
}
?>
<hr style="margin:2em 0">
<h2><?php esc_html_e( 'Entity 欄位登錄(user / term / comment', '2meet-data-optimizer' ); ?></h2>
<p class="description">
<?php esc_html_e( '透過 wpdo_register_entity_fields action 登錄的非 postmeta 欄位。Hook Bus 啟用後,這些欄位的讀寫會被路由至對應的 flat table。', '2meet-data-optimizer' ); ?>
</p>
<?php if ( ! $has_any ) : ?>
<div class="notice notice-info inline">
<p><?php esc_html_e( '目前無已登錄的 user / term / comment 欄位。透過 wpdo_register_entity_fields action 可登錄欄位;WooCommerce 整合啟用後會自動登錄客戶欄位。', '2meet-data-optimizer' ); ?></p>
</div>
<?php else : ?>
<?php foreach ( $non_post_types as $etype ) : ?>
<?php
$e_groups = TMDO_Entity_Registry::get_groups_for_type( $etype );
if ( empty( $e_groups ) ) {
continue;
}
$e_mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( $etype ) : 'disabled';
$e_mode_bg = array(
'disabled' => '#e0e0e0',
'dual_write' => '#d4edda',
'shadow_read' => '#fff3cd',
'aeav_only' => '#cce5ff',
);
$badge_bg = $e_mode_bg[ $e_mode ] ?? '#e0e0e0';
?>
<h3>
<code><?php echo esc_html( $etype ); ?></code>
<span style="display:inline-block;margin-left:8px;padding:2px 8px;border-radius:3px;font-size:11px;background:<?php echo esc_attr( $badge_bg ); ?>">
<?php echo esc_html( $e_mode ); ?>
</span>
</h3>
<?php foreach ( $e_groups as $e_group ) : ?>
<?php $e_fields = TMDO_Entity_Registry::get_group_fields( $etype, $e_group ); ?>
<h4 style="margin-bottom:0.5em">
<?php esc_html_e( '群組:', '2meet-data-optimizer' ); ?>
<code><?php echo esc_html( $e_group ); ?></code>
<?php
if ( class_exists( 'TMDO_Schema_Manager' ) ) {
$e_tbl = TMDO_Schema_Manager::get_table_name( $etype, $e_group );
if ( TMDO_Schema_Manager::table_exists( $e_tbl ) ) {
echo '<span style="color:#155724;font-size:12px"> ✓ ' . esc_html( $e_tbl ) . '</span>';
} else {
echo '<span style="color:#856404;font-size:12px"> ⏳ ' . esc_html__( '表格尚未建立', '2meet-data-optimizer' ) . '</span>';
}
}
?>
</h4>
<table class="widefat striped wpdo-table" style="margin-bottom:1.5em">
<thead>
<tr>
<th><?php esc_html_e( 'Meta Key', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '型別', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '索引', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '必填', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '標籤', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $e_fields as $ef ) : ?>
<tr>
<td><code><?php echo esc_html( $ef['key'] ); ?></code></td>
<td><code><?php echo esc_html( $ef['type'] ); ?></code></td>
<td>
<?php
$ef_idx = array();
if ( ! empty( $ef['searchable'] ) ) {
$ef_idx[] = 'B-Tree';
}
if ( ! empty( $ef['unique'] ) ) {
$ef_idx[] = 'UNIQUE';
}
if ( ! empty( $ef['fulltext'] ) ) {
$ef_idx[] = 'FULLTEXT';
}
echo esc_html( empty( $ef_idx ) ? '—' : implode( ', ', $ef_idx ) );
?>
</td>
<td><?php echo ! empty( $ef['required'] ) ? esc_html__( 'Yes', '2meet-data-optimizer' ) : '—'; ?></td>
<td><?php echo esc_html( $ef['label'] ?? '' ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach; ?>
<?php endforeach; ?>
<?php endif; ?>
<?php endif; ?>
<?php
// Collect unique post_types that have hot or cold zones (use object cache).
$cache_post_types = array();
foreach ( $fields as $field ) {
if ( in_array( $field['zone'], array( 'hot', 'cold' ), true ) ) {
$cache_post_types[ $field['post_type'] ] = true;
}
}
if ( ! empty( $cache_post_types ) ) :
?>
<h3 class="wpdo-mt-3"><?php esc_html_e( 'Object Cache 管理', '2meet-data-optimizer' ); ?></h3>
<p class="description"><?php esc_html_e( '清除指定 Post Type 的 Zone C Object Cache 群組。', '2meet-data-optimizer' ); ?></p>
<table class="widefat striped wpdo-table--narrow">
<thead>
<tr>
<th><?php esc_html_e( 'Post Type', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '操作', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( array_keys( $cache_post_types ) as $pt ) : ?>
<tr>
<td><code><?php echo esc_html( $pt ); ?></code></td>
<td>
<button class="button button-secondary wpdo-flush-cache"
data-post-type="<?php echo esc_attr( $pt ); ?>">
<?php esc_html_e( 'Flush Cache', '2meet-data-optimizer' ); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php endif; ?>
<?php
}
/**
* Classifier tab: analyze postmeta and suggest zone assignments.
*/
private static function render_classifier(): void {
// Get available post types that have postmeta.
// Unbounded JOIN DISTINCT is expensive on large sites — cache for 1 hour.
$types = get_transient( 'wpdo_classifier_post_types_v1' );
if ( false === $types ) {
global $wpdb;
$types = $wpdb->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 = '';
}
?>
<h2><?php esc_html_e( 'Zone 自動分類器', '2meet-data-optimizer' ); ?></h2>
<p class="description"><?php esc_html_e( '分析 wp_postmeta 中的欄位,根據值的特徵自動建議最佳 Zone 分配。', '2meet-data-optimizer' ); ?></p>
<form method="get" class="wpdo-form-row">
<input type="hidden" name="page" value="<?php echo esc_attr( self::MENU_SLUG ); ?>">
<input type="hidden" name="tab" value="classifier">
<label for="classify_type"><strong><?php esc_html_e( '選擇 Post Type', '2meet-data-optimizer' ); ?></strong></label>
<select name="classify_type" id="classify_type">
<option value=""><?php esc_html_e( '— 選擇 —', '2meet-data-optimizer' ); ?></option>
<?php foreach ( $types ?: array() as $pt ) : ?>
<option value="<?php echo esc_attr( $pt ); ?>" <?php selected( $post_type, $pt ); ?>><?php echo esc_html( $pt ); ?></option>
<?php endforeach; ?>
</select>
<?php submit_button( __( '分析', '2meet-data-optimizer' ), 'secondary', 'submit', false ); ?>
</form>
<?php
if ( ! $post_type ) {
return;
}
$suggestions = TMDO_Zone_Classifier::analyze( $post_type );
if ( empty( $suggestions ) ) {
echo '<div class="notice notice-info inline"><p>' . esc_html__( '此 post type 沒有可分析的 postmeta 欄位。', '2meet-data-optimizer' ) . '</p></div>';
return;
}
$summary = TMDO_Zone_Classifier::summary( $post_type );
?>
<div class="wpdo-grid wpdo-mb-2">
<div class="wpdo-card">
<h3><?php esc_html_e( '分類摘要', '2meet-data-optimizer' ); ?> — <code><?php echo esc_html( $post_type ); ?></code></h3>
<table class="widefat striped">
<tbody>
<tr><td><span class="wpdo-zone wpdo-zone-hot">Hot</span></td><td><?php echo (int) $summary['hot']; ?> <?php esc_html_e( '建議欄位', '2meet-data-optimizer' ); ?></td></tr>
<tr><td><span class="wpdo-zone wpdo-zone-warm">Warm</span></td><td><?php echo (int) $summary['warm']; ?> <?php esc_html_e( '建議欄位', '2meet-data-optimizer' ); ?></td></tr>
<tr><td><span class="wpdo-zone wpdo-zone-cold">Cold</span></td><td><?php echo (int) $summary['cold']; ?> <?php esc_html_e( '建議欄位', '2meet-data-optimizer' ); ?></td></tr>
<tr><td><span class="wpdo-zone wpdo-zone-archive">Archive</span></td><td><?php echo (int) $summary['archive']; ?> <?php esc_html_e( '建議欄位', '2meet-data-optimizer' ); ?></td></tr>
<tr><td><strong><?php esc_html_e( '已分配', '2meet-data-optimizer' ); ?></strong></td><td><?php echo (int) $summary['already_assigned']; ?> <?php esc_html_e( '欄位', '2meet-data-optimizer' ); ?></td></tr>
</tbody>
</table>
</div>
</div>
<table class="widefat striped wpdo-table">
<thead>
<tr>
<th>Meta Key</th>
<th><?php esc_html_e( '資料筆數', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '建議 Zone', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '信心度', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '目前分配', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '原因', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $suggestions as $s ) : ?>
<tr>
<td><code><?php echo esc_html( $s['meta_key'] ); ?></code></td>
<td><?php echo esc_html( number_format_i18n( $s['row_count'] ) ); ?></td>
<td><span class="wpdo-zone wpdo-zone-<?php echo esc_attr( $s['suggested_zone'] ); ?>"><?php echo esc_html( ucfirst( $s['suggested_zone'] ) ); ?></span></td>
<td>
<?php
$conf = max( 0.0, min( 1.0, (float) $s['confidence'] ) );
?>
<div class="wpdo-confidence"
role="progressbar"
aria-valuenow="<?php echo (int) round( $conf * 100 ); ?>"
aria-valuemin="0"
aria-valuemax="100"
aria-label="<?php echo esc_attr( sprintf( /* translators: %s: meta key */ __( '%s 信心度', '2meet-data-optimizer' ), $s['meta_key'] ) ); ?>">
<div class="wpdo-confidence-bar" style="--wpdo-confidence: <?php echo esc_attr( (string) $conf ); ?>;"></div>
</div>
<span><?php echo esc_html( $s['confidence'] ); ?></span>
</td>
<td>
<?php if ( $s['already_assigned'] ) : ?>
<span class="wpdo-zone wpdo-zone-<?php echo esc_attr( $s['already_assigned'] ); ?>"><?php echo esc_html( ucfirst( $s['already_assigned'] ) ); ?></span>
<?php else : ?>
<span class="wpdo-badge">—</span>
<?php endif; ?>
</td>
<td><small><?php echo esc_html( implode( '; ', $s['reasons'] ) ); ?></small></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
}
/**
* Logs tab: show recent errors with purge action.
*/
private static function render_logs(): void {
// Handle purge action.
if ( isset( $_POST['wpdo_purge_logs'] ) && check_admin_referer( 'wpdo_purge_logs' ) ) {
$days = absint( wp_unslash( $_POST['wpdo_purge_days'] ?? 30 ) );
$deleted = TMDO_Logger::purge( $days );
// translators: 1: number of deleted log entries, 2: number of days.
echo '<div class="notice notice-success"><p>' . sprintf( esc_html__( '已清除 %1$d 筆超過 %2$d 天的日誌。', '2meet-data-optimizer' ), (int) $deleted, (int) $days ) . '</p></div>';
}
$errors = TMDO_Logger::get_recent( '', 100 );
?>
<h2><?php esc_html_e( '錯誤日誌', '2meet-data-optimizer' ); ?></h2>
<form method="post" class="wpdo-form-row">
<?php wp_nonce_field( 'wpdo_purge_logs' ); ?>
<label for="wpdo_purge_days"><?php esc_html_e( '清除超過', '2meet-data-optimizer' ); ?></label>
<input type="number" id="wpdo_purge_days" name="wpdo_purge_days" value="30" min="1" max="365" class="wpdo-input--xs">
<span><?php esc_html_e( '天的日誌', '2meet-data-optimizer' ); ?></span>
<?php submit_button( __( '清除', '2meet-data-optimizer' ), 'secondary', 'wpdo_purge_logs', false ); ?>
</form>
<?php if ( empty( $errors ) ) : ?>
<div class="notice notice-success inline"><p><?php esc_html_e( '沒有錯誤記錄。系統運作正常。', '2meet-data-optimizer' ); ?></p></div>
<?php else : ?>
<table class="widefat striped wpdo-table">
<thead>
<tr>
<th>ID</th>
<th><?php esc_html_e( '模組', '2meet-data-optimizer' ); ?></th>
<th>Zone</th>
<th>Hook</th>
<th><?php esc_html_e( '訊息', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '時間', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $errors as $err ) : ?>
<tr>
<td><?php echo (int) $err['id']; ?></td>
<td><code><?php echo esc_html( $err['module'] ); ?></code></td>
<td><?php echo esc_html( $err['zone'] ?: '—' ); ?></td>
<td><code><?php echo esc_html( $err['hook'] ); ?></code></td>
<td class="wpdo-log-msg"><?php echo esc_html( $err['message'] ); ?></td>
<td><?php echo esc_html( $err['created_at'] ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php
}
/**
* REST API tab: endpoint documentation + JS SDK snippet.
*/
private static function render_rest_api(): void {
$base = esc_url( rest_url( 'wpdo/v1' ) );
?>
<h2><?php esc_html_e( 'REST API', '2meet-data-optimizer' ); ?></h2>
<p><?php esc_html_e( '以下端點讓前端直接查詢 Zone 資料,取代 WP_Query / postmeta。', '2meet-data-optimizer' ); ?></p>
<?php
$endpoints = array(
array(
'method' => 'POST',
'path' => '/listings/{id}/view',
'desc' => __( '增加 Zone B 瀏覽計數。需要 WP REST nonceX-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 noncewp_create_nonce("wp_rest")', '2meet-data-optimizer' ),
),
'headers' => array(),
'curl' => "curl -X POST '{$base}/listings/123/view' -H 'X-WP-Nonce: <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' => __( '每頁筆數 1100(預設 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' => __( '單筆 listingZone A(熱區欄位)+ Zone CJSON 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: <nonce>'",
),
);
foreach ( $endpoints as $ep ) :
?>
<div class="wpdo-card wpdo-block--wide wpdo-mb-2">
<h3>
<code class="wpdo-method-pill"><?php echo esc_html( $ep['method'] ); ?></code>
<code><?php echo esc_html( $base . $ep['path'] ); ?></code>
</h3>
<p><?php echo esc_html( $ep['desc'] ); ?></p>
<?php if ( $ep['params'] ) : ?>
<table class="widefat striped wpdo-mb-2">
<thead><tr>
<th class="wpdo-col-param"><?php esc_html_e( '參數', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '說明', '2meet-data-optimizer' ); ?></th>
</tr></thead>
<tbody>
<?php foreach ( $ep['params'] as $pname => $pdesc ) : ?>
<tr>
<td><code><?php echo esc_html( $pname ); ?></code></td>
<td><?php echo esc_html( $pdesc ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php if ( $ep['headers'] ) : ?>
<p class="wpdo-mt-1 wpdo-mb-2"><strong><?php esc_html_e( '回應 Headers', '2meet-data-optimizer' ); ?></strong>
<?php foreach ( $ep['headers'] as $hname => $hdesc ) : ?>
<code><?php echo esc_html( $hname ); ?></code> — <?php echo esc_html( $hdesc ); ?>&nbsp;
<?php endforeach; ?></p>
<?php endif; ?>
<pre class="wpdo-code-block"><?php echo esc_html( $ep['curl'] ); ?></pre>
</div>
<?php endforeach; ?>
<h2 class="wpdo-mt-3"><?php esc_html_e( 'JavaScript SDK', '2meet-data-optimizer' ); ?></h2>
<p>
<?php esc_html_e( '在主題或外掛中引入 SDK,即可使用 ', '2meet-data-optimizer' ); ?>
<code>WpdoClient</code>
<?php esc_html_e( ' 類別操作所有端點,支援分頁生成器(async generator)。', '2meet-data-optimizer' ); ?>
</p>
<h3><?php esc_html_e( '方式一:wp_enqueue_script(推薦)', '2meet-data-optimizer' ); ?></h3>
<pre class="wpdo-code-block">
<?php
echo esc_html(
"// 在主題 functions.php 中加入:
wp_enqueue_script(
'wpdo-rest-sdk',
'" . esc_url( TMDO_URL . 'admin/assets/wpdo-rest-sdk.js' ) . "',
[],
'" . TMDO_VERSION . "',
true
);
wp_localize_script( 'wpdo-rest-sdk', 'wpdo_sdk_config', [
'rest_url' => rest_url( 'wpdo/v1' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'post_type' => 'hp_listing',
] );"
);
?>
</pre>
<h3><?php esc_html_e( '方式二:直接使用(免 localize', '2meet-data-optimizer' ); ?></h3>
<pre class="wpdo-code-block">
<?php
echo esc_html(
"const wpdo = new WpdoClient({ baseUrl: '/wp-json/wpdo/v1', postType: 'hp_listing' });
// 查詢最新 5 筆 listing
const { items, total } = await wpdo.getListings({ per_page: 5, orderby: 'post_id', order: 'DESC' });
console.log(total + ' 筆 listing,本頁:', items);
// 價格篩選
const result = await wpdo.getListings({ hp_price_min: 1000, hp_price_max: 5000 });
// 單筆(Zone A + Zone C 合併)
const listing = await wpdo.getListing(123);
console.log(listing.hp_price, listing.hp_description);
// 瀏覽計數(Zone B
const { view_count } = await wpdo.getStats(123);
// 增加瀏覽計數(需 nonce
const { view_count: updated } = await wpdo.incrementView(123);
// 迭代所有頁面
for await (const page of wpdo.paginateListings({ hp_featured: 1, per_page: 20 })) {
console.log('第', page.page, '頁:', page.items.length, '筆');
}"
);
?>
</pre>
<h3><?php esc_html_e( '即時測試', '2meet-data-optimizer' ); ?></h3>
<p><em><?php esc_html_e( '在瀏覽器 Console 輸入(需已載入 SDK):', '2meet-data-optimizer' ); ?></em></p>
<pre class="wpdo-code-block">
<?php
echo esc_html(
'// SDK 已自動建立 window.wpdo 實例(透過 wpdo_sdk_config
wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
);
?>
</pre>
<?php
}
/**
* HPCT Import tab: preview and execute import.
*/
private static function render_hpct_import(): void {
// Handle import action.
if ( isset( $_POST['wpdo_run_import'] ) && check_admin_referer( 'wpdo_hpct_import' ) ) {
$result = TMDO_HPCT_Import::run();
if ( is_wp_error( $result ) ) {
echo '<div class="notice notice-error"><p>' . esc_html( $result->get_error_message() ) . '</p></div>';
} else {
echo '<div class="notice notice-success"><p>' . esc_html__( 'HPCT 匯入成功!建議停用 HP Custom Tables 外掛。', '2meet-data-optimizer' ) . '</p></div>';
}
}
$can_import = TMDO_HPCT_Import::can_import();
$is_imported = TMDO_HPCT_Import::is_imported();
?>
<h2><?php esc_html_e( 'HP Custom Tables 匯入', '2meet-data-optimizer' ); ?></h2>
<?php if ( $is_imported ) : ?>
<div class="notice notice-success inline">
<p><?php esc_html_e( 'HPCT 設定已匯入完成。如果 HP Custom Tables 外掛仍然啟用,建議停用它。', '2meet-data-optimizer' ); ?></p>
</div>
<?php elseif ( $can_import ) : ?>
<p class="description"><?php esc_html_e( '偵測到 HP Custom Tables 外掛。匯入會將 HPCT 的模組狀態和遷移記錄複製到 WPDO,然後由 WPDO 接管所有攔截器。', '2meet-data-optimizer' ); ?></p>
<?php
$preview_data = TMDO_HPCT_Import::preview();
if ( ! empty( $preview_data['modules'] ) ) :
$preview = $preview_data['modules'];
?>
<h3><?php esc_html_e( '匯入預覽', '2meet-data-optimizer' ); ?></h3>
<table class="widefat striped wpdo-table--narrow">
<thead>
<tr>
<th><?php esc_html_e( 'HPCT 模組', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( 'HPCT 狀態', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( 'WPDO 狀態', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $preview as $item ) : ?>
<tr>
<td><code><?php echo esc_html( $item['module'] ); ?></code></td>
<td><?php echo esc_html( $item['hpct_status'] ); ?></td>
<td><span class="wpdo-state wpdo-state-<?php echo esc_attr( $item['wpdo_state'] ); ?>"><?php echo esc_html( $item['wpdo_state'] ); ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<form method="post" class="wpdo-mt-3">
<?php wp_nonce_field( 'wpdo_hpct_import' ); ?>
<?php submit_button( __( '執行匯入', '2meet-data-optimizer' ), 'primary', 'wpdo_run_import', false ); ?>
</form>
<?php endif; ?>
<?php else : ?>
<div class="notice notice-info inline">
<p><?php esc_html_e( '未偵測到 HP Custom Tables 外掛,或已完成匯入。', '2meet-data-optimizer' ); ?></p>
</div>
<?php endif; ?>
<?php
}
// ── AJAX handler ──────────────────────────────────────────────────────
/**
* Handle AJAX requests from admin JS.
*/
public static function handle_ajax(): void {
check_ajax_referer( self::NONCE_ACTION, 'nonce' );
if ( ! TMDO_Capability::current_user_can_admin() ) {
wp_send_json_error( 'Unauthorized' );
}
$action_type = sanitize_key( wp_unslash( $_POST['action_type'] ?? '' ) );
switch ( $action_type ) {
case 'flush_cache':
$post_type = sanitize_key( wp_unslash( $_POST['post_type'] ?? '' ) );
if ( $post_type ) {
TMDO_Cache_Layer::flush_group( $post_type );
wp_send_json_success( array( 'message' => '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 '<div class="notice notice-error"><p>' . esc_html__( 'Snapshot system not available.', '2meet-data-optimizer' ) . '</p></div>';
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(
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
esc_html( sprintf( /* translators: %d: count */ __( '已清除 %d 個過期快照。', '2meet-data-optimizer' ), $n ) )
);
} elseif ( isset( $msg_map[ $msg_key ] ) ) {
printf(
'<div class="notice notice-%s is-dismissible"><p>%s</p></div>',
esc_attr( $msg_map[ $msg_key ][0] ),
esc_html( $msg_map[ $msg_key ][1] )
);
}
}
$rows = TMDO_Snapshot_Manager::list_recent( 50, null );
?>
<h2><?php esc_html_e( '備份快照', '2meet-data-optimizer' ); ?></h2>
<p class="description">
<?php esc_html_e( '快照在 FSM 轉態(cutover→cleanup→complete)前自動建立,也可手動觸發。檔案存於 wp-content/uploads/wpdo-backups/ 並由 wp_wpdo_snapshots 表編目。', '2meet-data-optimizer' ); ?>
</p>
<p>
<form method="post" style="display:inline">
<input type="hidden" name="wpdo_create_snapshot" value="1">
<?php wp_nonce_field( 'wpdo_create_snapshot' ); ?>
<button type="submit" class="button button-primary"><?php esc_html_e( '立即建立快照', '2meet-data-optimizer' ); ?></button>
</form>
<form method="post" style="display:inline">
<input type="hidden" name="wpdo_prune_snapshots" value="1">
<?php wp_nonce_field( 'wpdo_prune_snapshots' ); ?>
<button type="submit" class="button"
onclick="return confirm('<?php echo esc_js( __( '確定要清除過期快照嗎?', '2meet-data-optimizer' ) ); ?>');"><?php esc_html_e( '清除過期快照', '2meet-data-optimizer' ); ?></button>
</form>
</p>
<?php if ( empty( $rows ) ) : ?>
<p><em><?php esc_html_e( '目前沒有任何快照。', '2meet-data-optimizer' ); ?></em></p>
<?php else : ?>
<table class="widefat striped">
<thead>
<tr>
<th><?php esc_html_e( '快照 ID', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '觸發', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '行數', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '大小', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '儲存', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '建立時間', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '到期', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '操作', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $rows as $row ) : ?>
<tr>
<td><code><?php echo esc_html( $row['snapshot_id'] ); ?></code></td>
<td><?php echo esc_html( $row['trigger_type'] ); ?></td>
<td><?php echo esc_html( number_format_i18n( (int) $row['row_count'] ) ); ?></td>
<td><?php echo esc_html( size_format( (int) $row['size_bytes'], 1 ) ); ?></td>
<td><?php echo esc_html( $row['storage'] ); ?></td>
<td><?php echo esc_html( $row['created_at'] ); ?></td>
<td><?php echo esc_html( (string) ( $row['expires_at'] ?? '—' ) ); ?></td>
<td>
<form method="post" style="display:inline">
<input type="hidden" name="wpdo_delete_snapshot" value="<?php echo esc_attr( $row['snapshot_id'] ); ?>">
<?php wp_nonce_field( 'wpdo_delete_snapshot' ); ?>
<button type="submit" class="button button-small button-link-delete"
onclick="return confirm('<?php echo esc_js( __( '確定要刪除此快照?無法復原。', '2meet-data-optimizer' ) ); ?>');"><?php esc_html_e( '刪除', '2meet-data-optimizer' ); ?></button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="description">
<?php
echo esc_html__( '如需還原快照,請使用 CLI', '2meet-data-optimizer' );
?>
<code>wp wpdo snapshot restore &lt;snapshot_id&gt; --apply</code>
</p>
<?php endif; ?>
<?php
}
/**
* Conflicts tab — fixes the broken &tab=conflicts notice link (v2.2.0 M4).
*/
private static function render_conflicts(): void {
?>
<h2><?php esc_html_e( '衝突檢測', '2meet-data-optimizer' ); ?></h2>
<p class="description">
<?php esc_html_e( '掃描 WPDO 各 interceptor / hook 是否有衝突的 priority 或 callback 重複註冊。CLI 等價:', '2meet-data-optimizer' ); ?>
<code>wp wpdo conflict-scan</code>
</p>
<?php
if ( ! class_exists( 'TMDO_Conflict_Monitor' ) ) {
echo '<p><em>' . esc_html__( '衝突監控模組未載入。', '2meet-data-optimizer' ) . '</em></p>';
return;
}
$summary = TMDO_Conflict_Monitor::get_summary();
$total = (int) ( $summary['total'] ?? 0 );
?>
<table class="widefat striped" style="max-width:600px;">
<tbody>
<tr>
<th scope="row"><?php esc_html_e( '衝突總數', '2meet-data-optimizer' ); ?></th>
<td>
<?php if ( 0 === $total ) : ?>
<span style="color:#46b450;font-weight:bold;">●</span>
<?php esc_html_e( '無衝突', '2meet-data-optimizer' ); ?>
<?php else : ?>
<span style="color:#dc3232;font-weight:bold;">●</span>
<?php echo esc_html( (string) $total ); ?>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row">Hook overlap</th>
<td><?php echo esc_html( (string) ( $summary['hook_overlap'] ?? 0 ) ); ?></td>
</tr>
<tr>
<th scope="row">UAEPG overlap</th>
<td><?php echo esc_html( (string) ( $summary['uaepg_overlap'] ?? 0 ) ); ?></td>
</tr>
</tbody>
</table>
<?php if ( $total > 0 ) : ?>
<h3><?php esc_html_e( '詳細', '2meet-data-optimizer' ); ?></h3>
<pre style="background:#f0f0f1;padding:1em;overflow:auto;max-height:400px;">
<?php
echo esc_html( wp_json_encode( $summary, JSON_PRETTY_PRINT ) );
?>
</pre>
<?php endif; ?>
<?php
}
/**
* Doctor tab — surfaces wp wpdo doctor + Site Health summary inside the
* plugin's admin (v2.2.0 M4).
*/
private static function render_doctor(): void {
?>
<h2><?php esc_html_e( '健康檢查(Doctor', '2meet-data-optimizer' ); ?></h2>
<p class="description">
<?php esc_html_e( '7 項自我診斷檢查:schema 完整性、錯誤預算、hook 衝突、autoload 大小、postmeta 爆量、orphan zone rows、缺少快照。', '2meet-data-optimizer' ); ?>
</p>
<p>
<a class="button" href="<?php echo esc_url( admin_url( 'site-health.php' ) ); ?>">
<?php esc_html_e( '前往 Tools → Site Health 查看完整結果', '2meet-data-optimizer' ); ?>
</a>
</p>
<?php
if ( ! class_exists( 'TMDO_Site_Health' ) ) {
echo '<p><em>' . esc_html__( 'Site Health 模組未載入。', '2meet-data-optimizer' ) . '</em></p>';
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' ) ),
);
?>
<table class="widefat striped">
<thead>
<tr>
<th><?php esc_html_e( '檢查', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '狀態', '2meet-data-optimizer' ); ?></th>
<th><?php esc_html_e( '說明', '2meet-data-optimizer' ); ?></th>
</tr>
</thead>
<tbody>
<?php
foreach ( $tests as $key => $info ) {
$result = call_user_func( array( 'TMDO_Site_Health', $info[0] ) );
$status = (string) ( $result['status'] ?? 'good' );
$badge = 'good' === $status ? '✅' : ( 'critical' === $status ? '🔴' : '🟡' );
printf(
'<tr><td>%s</td><td>%s %s</td><td>%s</td></tr>',
esc_html( $info[1] ),
esc_html( $badge ),
esc_html( $status ),
wp_kses_post( (string) ( $result['description'] ?? '' ) )
);
}
?>
</tbody>
</table>
<p class="description" style="margin-top:1em;">
<?php esc_html_e( '結果由 5 分鐘 transient 快取;若剛操作完想立即重檢,請點刷新或等下一個快取週期。', '2meet-data-optimizer' ); ?>
</p>
<?php
}
/**
* Module Suggestions tab (v2.5.0 M16) — auto-detect + one-click enable.
*/
private static function render_module_suggestions(): void {
// Post-action notice.
$msg = isset( $_GET['wpdo_msg'] ) ? sanitize_key( wp_unslash( (string) $_GET['wpdo_msg'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$mod = isset( $_GET['wpdo_module'] ) ? sanitize_key( wp_unslash( (string) $_GET['wpdo_module'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( 'module_enabled' === $msg && '' !== $mod ) {
printf(
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
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(
'<div class="notice notice-error is-dismissible"><p>%s</p></div>',
esc_html(
sprintf(
/* translators: %s: module name */
__( '⛔ Module %s 啟用被 FSM Guard 擋下(可能 module 已不在 idle 狀態)。', '2meet-data-optimizer' ),
$mod
)
)
);
} elseif ( 'enable_failed' === $msg ) {
echo '<div class="notice notice-error is-dismissible"><p>' . esc_html__( '⛔ 啟用失敗,請查日誌。', '2meet-data-optimizer' ) . '</p></div>';
}
echo '<h2>' . esc_html__( '🤖 模組建議', '2meet-data-optimizer' ) . '</h2>';
echo '<p class="description">' . esc_html__( '系統依環境自動偵測哪些 module 適合啟用。每筆建議含 confidence score + reasons + blockers。一鍵啟用會把 module 推進到 dual_writeFSM 第 1 個 active stateFSM Guard 確保不越級)。', '2meet-data-optimizer' ) . '</p>';
if ( ! class_exists( 'TMDO_Module_Detector' ) ) {
echo '<p><em>' . esc_html__( '模組偵測器未載入。', '2meet-data-optimizer' ) . '</em></p>';
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(
'<h3>%s</h3>',
esc_html(
sprintf(
/* translators: %d: number of recommended modules */
__( '✅ 建議啟用(%d 個 module', '2meet-data-optimizer' ),
count( $enable )
)
)
);
if ( empty( $enable ) ) {
echo '<p><em>' . esc_html__( '目前沒有可立即啟用的 module 建議。', '2meet-data-optimizer' ) . '</em></p>';
} else {
echo '<table class="widefat striped"><thead><tr>';
printf(
'<th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th>',
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 '</tr></thead><tbody>';
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(
'<tr><td><code>%s</code></td><td><div style="background:#f0f0f1;border-radius:3px;width:80px;height:18px;position:relative;"><div style="background:%s;width:%d%%;height:100%%;border-radius:3px;"></div><span style="position:absolute;inset:0;text-align:center;font-size:11px;line-height:18px;">%s</span></div></td>',
esc_html( $module ),
esc_attr( $color ),
(int) $bar_w,
esc_html( sprintf( '%.2f', $conf ) )
);
echo '<td>';
if ( ! empty( $r['description'] ) ) {
echo '<em>' . esc_html( (string) $r['description'] ) . '</em><br/>';
}
if ( ! empty( $r['reasons'] ) ) {
echo '<small>' . esc_html( implode( ' · ', $r['reasons'] ) ) . '</small>';
}
echo '</td>';
printf( '<td><code>%s</code></td>', esc_html( (string) $r['current_state'] ) );
echo '<td><form method="post" style="display:inline">';
printf( '<input type="hidden" name="wpdo_enable_module" value="%s">', esc_attr( $module ) );
wp_nonce_field( 'wpdo_enable_module' );
printf(
'<button type="submit" class="button button-primary button-small" onclick="return confirm(\'%s\');">%s</button>',
esc_js( __( '確定啟用此 module(推進到 dual_write)?', '2meet-data-optimizer' ) ),
esc_html__( '✅ 啟用', '2meet-data-optimizer' )
);
echo '</form></td>';
echo '</tr>';
}
echo '</tbody></table>';
}
// ─── wait bucket(條件未滿)────────────────────────────────
$wait = $buckets['wait'];
if ( ! empty( $wait ) ) {
printf(
'<h3>%s</h3>',
esc_html(
sprintf(
/* translators: %d: number of modules not yet ready */
__( '⏳ 條件未滿 / 暫不建議(%d 個)', '2meet-data-optimizer' ),
count( $wait )
)
)
);
echo '<table class="widefat"><tbody>';
foreach ( $wait as $module => $r ) {
printf(
'<tr><td><code>%s</code></td><td>%s</td></tr>',
esc_html( $module ),
esc_html( implode( ' · ', $r['blockers'] ?? array() ) )
);
}
echo '</tbody></table>';
}
// ─── skip bucket(已啟用 / 不適用)────────────────────────
$skip = $buckets['skip'];
if ( ! empty( $skip ) ) {
$skip_count = count( $skip );
printf(
'<details><summary>%s</summary>',
esc_html(
sprintf(
/* translators: %d: number of modules already enabled or not applicable */
__( '已啟用 / 不適用(%d 個)— 點擊展開', '2meet-data-optimizer' ),
$skip_count
)
)
);
echo '<table class="widefat"><tbody>';
foreach ( $skip as $module => $r ) {
printf(
'<tr><td><code>%s</code></td><td>%s</td></tr>',
esc_html( $module ),
esc_html( implode( ' · ', $r['blockers'] ?? array() ) )
);
}
echo '</tbody></table></details>';
}
echo '<p class="description" style="margin-top:1em;">' . esc_html__( '結果由 1 小時 transient 快取;每日健康檢查 cron 也會自動更新。', '2meet-data-optimizer' ) . '</p>';
}
/**
* 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 '<div class="notice notice-success is-dismissible"><p>' . esc_html__( '設定已儲存。', '2meet-data-optimizer' ) . '</p></div>';
}
$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;
?>
<h2><?php esc_html_e( '設定', '2meet-data-optimizer' ); ?></h2>
<form method="post" action="">
<?php wp_nonce_field( 'wpdo_save_settings' ); ?>
<input type="hidden" name="wpdo_save_settings" value="1">
<h3><?php esc_html_e( '📧 Email 警報', '2meet-data-optimizer' ); ?></h3>
<p class="description"><?php esc_html_e( '預設關閉。啟用後,每日健康檢查 cron 發現 critical 警告時會發信。同一個警告在 throttle 視窗內不重發。', '2meet-data-optimizer' ); ?></p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><label><?php esc_html_e( '啟用 email 警報', '2meet-data-optimizer' ); ?></label></th>
<td>
<label>
<input type="checkbox" name="wpdo_email_alerts_enabled" value="1" <?php checked( $enabled ); ?>>
<?php esc_html_e( '啟用', '2meet-data-optimizer' ); ?>
</label>
</td>
</tr>
<tr>
<th scope="row"><label for="wpdo_alert_email"><?php esc_html_e( '收件人 email', '2meet-data-optimizer' ); ?></label></th>
<td>
<input type="email" name="wpdo_alert_email" id="wpdo_alert_email"
class="regular-text" value="<?php echo esc_attr( $email ); ?>"
placeholder="<?php echo esc_attr( (string) get_option( 'admin_email', '' ) ); ?>">
<p class="description"><?php esc_html_e( '留空使用 admin_email。', '2meet-data-optimizer' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="wpdo_alert_throttle_hours"><?php esc_html_e( 'Throttle(小時)', '2meet-data-optimizer' ); ?></label></th>
<td>
<input type="number" name="wpdo_alert_throttle_hours" id="wpdo_alert_throttle_hours"
min="1" max="168" value="<?php echo esc_attr( (string) $throttle ); ?>" class="small-text">
<p class="description"><?php esc_html_e( '同一警告 fingerprint 在此視窗內不重複寄送(1-168 小時)。', '2meet-data-optimizer' ); ?></p>
</td>
</tr>
</tbody>
</table>
<?php
// v2.5.0 M15: 3 multi-channel notifier sections (collapsible).
$channels = array(
'slack' => 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 '<details' . ( $ch_enabled ? ' open' : '' ) . ' style="margin-top:1.5em;border:1px solid #c3c4c7;border-radius:3px;padding:0.6em 1em;">';
echo '<summary style="cursor:pointer;font-weight:600;font-size:1.1em;">' . esc_html( $cfg[0] ) . '</summary>';
echo '<table class="form-table" role="presentation"><tbody>';
printf(
'<tr><th scope="row"><label>%s</label></th><td><label><input type="checkbox" name="wpdo_%s_enabled" value="1" %s> %s</label></td></tr>',
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(
'<tr><th scope="row"><label for="%s">Webhook URL</label></th><td><input type="password" name="%s" id="%s" class="regular-text" value="%s" placeholder="%s" autocomplete="new-password"></td></tr>',
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(
'<tr><th scope="row"><label for="wpdo_telegram_bot_token">Bot Token</label></th><td><input type="password" name="wpdo_telegram_bot_token" id="wpdo_telegram_bot_token" class="regular-text" value="%s" placeholder="123456:ABC-DEF..." autocomplete="new-password"></td></tr>',
esc_attr( $token )
);
printf(
'<tr><th scope="row"><label for="wpdo_telegram_chat_id">Chat ID</label></th><td><input type="text" name="wpdo_telegram_chat_id" id="wpdo_telegram_chat_id" class="regular-text" value="%s" placeholder="@channel or 123456789"></td></tr>',
esc_attr( $chat )
);
}
printf(
'<tr><th scope="row"><label>%s</label></th><td><input type="number" name="wpdo_%s_throttle_hours" min="1" max="168" value="%d" class="small-text"> %s</td></tr>',
esc_html__( 'Throttle 小時', '2meet-data-optimizer' ),
esc_attr( $ch ),
absint( $ch_throttle ),
esc_html__( '同 fingerprint 不重發(1-168', '2meet-data-optimizer' )
);
printf(
'<tr><th scope="row"><label>%s</label></th><td><select name="wpdo_%s_severity"><option value="critical_only" %s>critical_only</option><option value="critical_and_recommended" %s>critical + recommended</option></select></td></tr>',
esc_html__( 'Severity 訂閱', '2meet-data-optimizer' ),
esc_attr( $ch ),
selected( 'critical_only', $ch_severity, false ),
selected( 'critical_and_recommended', $ch_severity, false )
);
echo '</tbody></table>';
echo '</details>';
}
?>
<h3 style="margin-top: 2em;"><?php esc_html_e( '⚡ Entity Bridgev2.5.4 / post v2.11.0', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( 'Hook Bus 攔截 WordPress 原生 metadata filter,將 user / post / term / comment meta 路由至扁平化資料表。', '2meet-data-optimizer' ); ?><br>
<?php esc_html_e( '模式說明:', '2meet-data-optimizer' ); ?>
<strong>disabled</strong> — <?php esc_html_e( '完全走原生 EAV', '2meet-data-optimizer' ); ?>
<strong>dual_write</strong> — <?php esc_html_e( '雙寫 flat + EAV,讀仍走 EAV(安全起點);', '2meet-data-optimizer' ); ?>
<strong>shadow_read</strong> — <?php esc_html_e( '雙寫 + 讀走 flat 並比對差異(驗證期);', '2meet-data-optimizer' ); ?>
<strong>aeav_only</strong> — <?php esc_html_e( '僅讀寫 flat table(最終生產模式)', '2meet-data-optimizer' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><label><?php esc_html_e( 'Hook Bus', '2meet-data-optimizer' ); ?></label></th>
<td>
<label>
<input type="checkbox" name="wpdo_hook_bus_enabled" value="1"
<?php checked( class_exists( 'TMDO_Hook_Bus_Bridge' ) && TMDO_Hook_Bus_Bridge::is_enabled() ); ?>>
<?php esc_html_e( '啟用(預設開啟)', '2meet-data-optimizer' ); ?>
</label>
<p class="description"><?php esc_html_e( 'v2.11.0 起 post entity 也走 Hook Bus(透過 Mode_Manager 控制),與 user / term / comment 統一路徑。', '2meet-data-optimizer' ); ?></p>
</td>
</tr>
<?php
$mode_labels = array(
'disabled' => __( '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;
?>
<tr>
<th scope="row">
<label for="<?php echo esc_attr( $field_id ); ?>">
<?php echo esc_html( ucfirst( $entity_type ) ); ?> entity
</label>
</th>
<td>
<select name="<?php echo esc_attr( $field_id ); ?>" id="<?php echo esc_attr( $field_id ); ?>">
<?php foreach ( $mode_labels as $mode_val => $mode_label ) : ?>
<option value="<?php echo esc_attr( $mode_val ); ?>"
<?php selected( $current_mode, $mode_val ); ?>>
<?php echo esc_html( $mode_label ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description" style="margin-top:0.25em;">
<?php
if ( class_exists( 'TMDO_Mode_Manager' ) ) {
echo esc_html( TMDO_Mode_Manager::description( $current_mode ) );
}
?>
</p>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
<?php
// v2.11.6: HivePress transient filter toggle + status.
$hp_filter_enabled = (bool) get_option( 'wpdo_hp_transient_filter_enabled', '1' );
$hp_legacy_count = class_exists( 'TMDO_Hivepress_Transient_Filter' )
? TMDO_Hivepress_Transient_Filter::count_legacy_postmeta_rows()
: 0;
global $wpdb;
$hp_routed_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->options}
WHERE option_name LIKE '\\_transient\\_wpdo\\_hp\\_pm\\_%'
OR option_name LIKE '\\_transient\\_timeout\\_wpdo\\_hp\\_pm\\_%'"
);
?>
<h3 style="margin-top: 2em;"><?php esc_html_e( '🌿 HivePress Transient Filterv2.11.5', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( 'HivePress 內部用 update_post_meta($post_id, \'_transient_<name>\', $value) 把 TTL cache 寫進 wp_postmeta(每個 hp_listing publish 觸發 8-16 個 transient row)。本 filter 在 metadata 層攔截並重新路由到 wp_optionsnative transient API),HivePress 完全無感,wp_postmeta 保持乾淨。Filter 是 metadata 層運作,不依賴 mode promote。', '2meet-data-optimizer' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><label><?php esc_html_e( '啟用 Filter', '2meet-data-optimizer' ); ?></label></th>
<td>
<label>
<input type="checkbox" name="wpdo_hp_transient_filter_enabled" value="1" <?php checked( $hp_filter_enabled ); ?>>
<?php esc_html_e( '攔截 _transient_hp_* postmeta 讀寫並路由至 wp_options(預設啟用)', '2meet-data-optimizer' ); ?>
</label>
<p class="description" style="margin-top:0.5em;font-size:12px;">
<?php esc_html_e( '停用此選項會讓未來新建的 hp_listing 重新把 transient cache 寫進 wp_postmeta(不建議,除非要 debug HivePress 行為)。', '2meet-data-optimizer' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( '當前狀態', '2meet-data-optimizer' ); ?></th>
<td>
<table class="widefat striped" style="max-width:600px;">
<tr>
<td><?php esc_html_e( 'Filter 啟用', '2meet-data-optimizer' ); ?></td>
<td>
<?php if ( $hp_filter_enabled ) : ?>
<span style="color:#28a745;font-weight:600;">✓ <?php esc_html_e( '啟用中', '2meet-data-optimizer' ); ?></span>
<?php else : ?>
<span style="color:#dc3545;font-weight:600;">✗ <?php esc_html_e( '已停用', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
<tr>
<td><?php esc_html_e( 'wp_postmeta 殘留 (legacy)', '2meet-data-optimizer' ); ?></td>
<td>
<?php if ( $hp_legacy_count > 0 ) : ?>
<strong style="color:#dc3545;"><?php echo esc_html( number_format_i18n( $hp_legacy_count ) ); ?></strong> rows
<code style="margin-left:8px;font-size:11px;">wp wpdo cleanup-hp-transients --dry-run</code>
<?php else : ?>
<strong style="color:#28a745;">0 rows ✓</strong>
<span style="color:#666;font-size:12px;margin-left:8px;"><?php esc_html_e( '(已清乾淨)', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
<tr>
<td><?php esc_html_e( 'wp_options 路由總量', '2meet-data-optimizer' ); ?></td>
<td>
<strong><?php echo esc_html( number_format_i18n( $hp_routed_count ) ); ?></strong> rows
<span style="color:#666;font-size:12px;margin-left:8px;">
<?php esc_html_e( 'filter 已成功路由的 transient 對數,含值 + timeout 兩半)', '2meet-data-optimizer' ); ?>
</span>
</td>
</tr>
</table>
<?php if ( $hp_legacy_count > 0 ) : ?>
<p class="description" style="margin-top:8px;color:#856404;background:#fff3cd;padding:8px 12px;border-radius:4px;">
⚠️
<?php
printf(
/* translators: %s: row count */
esc_html__( '偵測到 %s 個 _transient_hp_* row 殘留在 wp_postmeta。執行 wp wpdo cleanup-hp-transients --confirm 即可一次清空。HivePress 會在第一次需要時自動 rebuild cache(無資料遺失風險)。', '2meet-data-optimizer' ),
'<strong>' . esc_html( number_format_i18n( $hp_legacy_count ) ) . '</strong>'
);
?>
</p>
<?php endif; ?>
</td>
</tr>
</tbody>
</table>
<?php
// v2.12.1: Term + Comment garbage write-time filter toggle + status.
$tcg_enabled = (bool) get_option( 'wpdo_term_comment_garbage_filter_enabled', '1' );
$tcg_drops_24h = class_exists( 'TMDO_Term_Comment_Garbage_Filter' )
? TMDO_Term_Comment_Garbage_Filter::get_drop_count_24h()
: 0;
global $wpdb;
$term_garbage_count = (int) $wpdb->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')"
);
?>
<h3 style="margin-top: 2em;"><?php esc_html_e( '🗑 Term + Comment Garbage Filterv2.12.1', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( '攔截 wp_termmeta / wp_commentmeta 已知垃圾 keys 的寫入並 silent drop_wxr_import_* WP 匯入殘留、_2meet_demo_* demo 標記、commentmeta 中 8 個誤寫的 post-domain orphan keys)。Phase 0 cleanup CLI 清歷史,本 filter 防再次累積。Read 路徑不攔截(向後相容)。', '2meet-data-optimizer' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><label><?php esc_html_e( '啟用 Filter', '2meet-data-optimizer' ); ?></label></th>
<td>
<label>
<input type="checkbox" name="wpdo_term_comment_garbage_filter_enabled" value="1" <?php checked( $tcg_enabled ); ?>>
<?php esc_html_e( '攔截 garbage 寫入到 wp_termmeta / wp_commentmeta(預設啟用)', '2meet-data-optimizer' ); ?>
</label>
<p class="description" style="margin-top:0.5em;font-size:12px;">
<?php esc_html_e( '⚠️ 跑 WXR import 工具前可暫時停用本 filter_wxr_import_* 為 importer 內部追蹤),完成後再啟用 + 跑 cleanup CLI。一般 production 維持啟用。', '2meet-data-optimizer' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( '當前狀態', '2meet-data-optimizer' ); ?></th>
<td>
<table class="widefat striped" style="max-width:600px;">
<tr>
<td><?php esc_html_e( 'Filter 啟用', '2meet-data-optimizer' ); ?></td>
<td>
<?php if ( $tcg_enabled ) : ?>
<span style="color:#28a745;font-weight:600;">✓ <?php esc_html_e( '啟用中', '2meet-data-optimizer' ); ?></span>
<?php else : ?>
<span style="color:#dc3545;font-weight:600;">✗ <?php esc_html_e( '已停用', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
<tr>
<td><?php esc_html_e( '24h 內 drop 計數', '2meet-data-optimizer' ); ?></td>
<td>
<strong><?php echo esc_html( number_format_i18n( $tcg_drops_24h ) ); ?></strong> writes
<span style="color:#666;font-size:12px;margin-left:8px;">
<?php esc_html_e( 'rolling 24h,自動重置)', '2meet-data-optimizer' ); ?>
</span>
</td>
</tr>
<tr>
<td><?php esc_html_e( 'wp_termmeta 殘留 (legacy)', '2meet-data-optimizer' ); ?></td>
<td>
<?php if ( $term_garbage_count > 0 ) : ?>
<strong style="color:#dc3545;"><?php echo esc_html( number_format_i18n( $term_garbage_count ) ); ?></strong> rows
<code style="margin-left:8px;font-size:11px;">wp wpdo termmeta-cleanup --confirm</code>
<?php else : ?>
<strong style="color:#28a745;">0 rows ✓</strong>
<?php endif; ?>
</td>
</tr>
<tr>
<td><?php esc_html_e( 'wp_commentmeta 殘留 (legacy)', '2meet-data-optimizer' ); ?></td>
<td>
<?php if ( $comment_garbage_count > 0 ) : ?>
<strong style="color:#dc3545;"><?php echo esc_html( number_format_i18n( $comment_garbage_count ) ); ?></strong> rows
<code style="margin-left:8px;font-size:11px;">wp wpdo commentmeta-cleanup --confirm</code>
<?php else : ?>
<strong style="color:#28a745;">0 rows ✓</strong>
<?php endif; ?>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<?php
// v2.12.3: WC term count filter toggle + status.
$wctc_enabled = (bool) get_option( 'wpdo_wc_term_count_filter_enabled', '1' );
$wctc_legacy = class_exists( 'TMDO_WC_Term_Count_Filter' )
? TMDO_WC_Term_Count_Filter::count_legacy_termmeta_rows()
: 0;
$wctc_routed = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->options}
WHERE option_name LIKE '\\_transient\\_wpdo\\_wc\\_termcount\\_%'"
);
?>
<h3 style="margin-top: 2em;"><?php esc_html_e( '🛒 WooCommerce Term Count Filterv2.12.3', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( '攔截 WooCommerce 寫入 wp_termmeta 的 product_count_<taxonomy> cache rows,重新路由到 wp_optionsnative transient 結構)。WC 自身 cache 失效邏輯不變(每次新增/刪除 product 時會重算寫入),僅儲存位置改變。Read 路徑亦會優先從 wp_options 讀回,cache miss 才 fall-through 至 wp_termmeta(向後相容)。', '2meet-data-optimizer' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><label><?php esc_html_e( '啟用 Filter', '2meet-data-optimizer' ); ?></label></th>
<td>
<label>
<input type="checkbox" name="wpdo_wc_term_count_filter_enabled" value="1" <?php checked( $wctc_enabled ); ?>>
<?php esc_html_e( '攔截 product_count_* 寫入並路由至 wp_options(預設啟用)', '2meet-data-optimizer' ); ?>
</label>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( '當前狀態', '2meet-data-optimizer' ); ?></th>
<td>
<table class="widefat striped" style="max-width:600px;">
<tr>
<td><?php esc_html_e( 'Filter 啟用', '2meet-data-optimizer' ); ?></td>
<td>
<?php if ( $wctc_enabled ) : ?>
<span style="color:#28a745;font-weight:600;">✓ <?php esc_html_e( '啟用中', '2meet-data-optimizer' ); ?></span>
<?php else : ?>
<span style="color:#dc3545;font-weight:600;">✗ <?php esc_html_e( '已停用', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
<tr>
<td><?php esc_html_e( 'wp_termmeta 殘留 (legacy)', '2meet-data-optimizer' ); ?></td>
<td>
<?php if ( $wctc_legacy > 0 ) : ?>
<strong style="color:#dc3545;"><?php echo esc_html( number_format_i18n( $wctc_legacy ) ); ?></strong> rows
<?php else : ?>
<strong style="color:#28a745;">0 rows ✓</strong>
<?php endif; ?>
</td>
</tr>
<tr>
<td><?php esc_html_e( 'wp_options 路由總量', '2meet-data-optimizer' ); ?></td>
<td>
<strong><?php echo esc_html( number_format_i18n( $wctc_routed ) ); ?></strong> rows
<span style="color:#666;font-size:12px;margin-left:8px;"><?php esc_html_e( 'filter 已成功路由的 product_count cache', '2meet-data-optimizer' ); ?></span>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<?php
// v2.12.4: Term + Comment misc bucket toggle + status.
$misc_enabled = (bool) get_option( 'wpdo_term_comment_misc_bucket_enabled', '1' );
$term_misc_rows = class_exists( 'TMDO_Term_Comment_Misc_Bucket' )
? TMDO_Term_Comment_Misc_Bucket::count_rows( 'term' )
: 0;
$comment_misc_rows = class_exists( 'TMDO_Term_Comment_Misc_Bucket' )
? TMDO_Term_Comment_Misc_Bucket::count_rows( 'comment' )
: 0;
?>
<h3 style="margin-top: 2em;"><?php esc_html_e( '📦 Term + Comment Misc Bucketv2.12.4', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( 'Catch-all flat 表,捕捉所有未被前面 filter 處理的 term/comment meta keyspriority 99,整條 filter chain 的最後一棒)。讓 wp_termmeta / wp_commentmeta 完全可避開(v3.0.0 DROP 前置條件)。寫入 wp_wpdo_term_misc / wp_wpdo_comment_misc 兩張 K/V 表,PRIMARY KEY (entity_id, meta_key)。讀取 cache miss 時 fall-through 至 wp_*meta 維持向後相容。', '2meet-data-optimizer' ); ?>
</p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><label><?php esc_html_e( '啟用 Misc Bucket', '2meet-data-optimizer' ); ?></label></th>
<td>
<label>
<input type="checkbox" name="wpdo_term_comment_misc_bucket_enabled" value="1" <?php checked( $misc_enabled ); ?>>
<?php esc_html_e( '攔截未註冊 keys 寫入並路由至 wpdo_*_misc 表(預設啟用)', '2meet-data-optimizer' ); ?>
</label>
<p class="description" style="margin-top:0.5em;font-size:12px;">
<?php esc_html_e( '⚠️ 停用此選項時,未註冊 keys 會回到 wp_termmeta / wp_commentmeta。Phase 5 升 mode 前必須維持啟用。', '2meet-data-optimizer' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( '當前狀態', '2meet-data-optimizer' ); ?></th>
<td>
<table class="widefat striped" style="max-width:600px;">
<tr>
<td><?php esc_html_e( 'Misc bucket 啟用', '2meet-data-optimizer' ); ?></td>
<td>
<?php if ( $misc_enabled ) : ?>
<span style="color:#28a745;font-weight:600;">✓ <?php esc_html_e( '啟用中', '2meet-data-optimizer' ); ?></span>
<?php else : ?>
<span style="color:#dc3545;font-weight:600;">✗ <?php esc_html_e( '已停用', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
<tr>
<td><code>wp_wpdo_term_misc</code></td>
<td><strong><?php echo esc_html( number_format_i18n( $term_misc_rows ) ); ?></strong> rows</td>
</tr>
<tr>
<td><code>wp_wpdo_comment_misc</code></td>
<td><strong><?php echo esc_html( number_format_i18n( $comment_misc_rows ) ); ?></strong> rows</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<h3 style="margin-top: 2em;"><?php esc_html_e( '🤖 FSM Automatorv2.5.0 M13', '2meet-data-optimizer' ); ?></h3>
<p class="description"><?php esc_html_e( '預設關閉。啟用後,每日 04:30 UTC 跑一次,自動執行 FSM Advisor 標記為 PROMOTE 的 module 推進。Destructive 轉態(verify→cutover、cutover→cleanup、cleanup→complete)即使 enabled 也永不自動。', '2meet-data-optimizer' ); ?></p>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><label><?php esc_html_e( '啟用 Automator', '2meet-data-optimizer' ); ?></label></th>
<td>
<label>
<input type="checkbox" name="wpdo_automator_enabled" value="1" <?php checked( class_exists( 'TMDO_FSM_Automator' ) && TMDO_FSM_Automator::is_enabled() ); ?>>
<?php esc_html_e( '啟用每日自動推進(cool-off:過去 7 天內任何 critical health 警告會暫停 automator', '2meet-data-optimizer' ); ?>
</label>
</td>
</tr>
<tr>
<th scope="row"><label><?php esc_html_e( 'Module 黑名單', '2meet-data-optimizer' ); ?></label></th>
<td>
<?php
$blacklist = class_exists( 'TMDO_FSM_Automator' ) ? TMDO_FSM_Automator::blacklist() : array();
if ( class_exists( 'TMDO_Feature_Flags' ) ) {
$all = array_merge( TMDO_Feature_Flags::HPCT_MODULES, TMDO_Feature_Flags::ZONE_MODULES );
echo '<div style="max-height:180px;overflow-y:auto;border:1px solid #c3c4c7;padding:0.6em;border-radius:3px;">';
foreach ( $all as $m ) {
printf(
'<label style="display:inline-block;margin-right:1em;margin-bottom:0.3em;"><input type="checkbox" name="wpdo_automator_blacklist[]" value="%s" %s> <code>%s</code></label>',
esc_attr( $m ),
checked( in_array( $m, $blacklist, true ), true, false ),
esc_html( $m )
);
}
echo '</div>';
}
?>
<p class="description"><?php esc_html_e( '勾選的 module 不會被 automator 自動推進。', '2meet-data-optimizer' ); ?></p>
</td>
</tr>
</tbody>
</table>
<?php submit_button( __( '儲存設定', '2meet-data-optimizer' ) ); ?>
</form>
<?php
}
}
// Boot admin hooks.
TMDO_Admin::init();