f524ea3f16
- phpcs.xml(自 A 移植):ruleset 改名、*/tools/* 例外換成 */back-compat/*、 中文註解全域排除 Squiz.Commenting.InlineComment.InvalidEndChar、 interface 別名檔排除 OneObjectStructurePerFile - 檔頭正規化:16 個檔案的 declare(strict_types=1) 與前導 // 註解移到 file docblock 之後,並移除 <?php 後多餘空行(phpcbf 另自動修 190 處) - phpstan.neon + .phpstan/stubs.php(TMDO_ 與 WPDO_ 兩套常數)+ 重新產生的 phpstan-baseline.neon(710 errors,A 的 3877 行 baseline 因前綴與路徑不同無法沿用) 現況:PHPCS 0 errors / 0 warnings、PHPStan L6 No errors、 unit 451 / integration 398 GREEN Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
3714 lines
155 KiB
PHP
3714 lines
155 KiB
PHP
<?php
|
||
/**
|
||
* WP Data Optimizer admin UI class.
|
||
*
|
||
* @package WP_Data_Optimizer
|
||
*/
|
||
|
||
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: native meta queries for management dashboard
|
||
|
||
declare(strict_types=1);
|
||
|
||
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>
|
||
|
|
||
<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>
|
||
|
||
<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 tables(hot / 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-2000;Realistic 建議 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' => __( 'JOIN:top gold + active LIMIT 100', '2meet-data-optimizer' ),
|
||
'eav_range_baseline' => __( '原生 EAV 等價查詢(baseline)', '2meet-data-optimizer' ),
|
||
);
|
||
foreach ( $query_labels as $key => $label ) :
|
||
$q = $query[ $key ] ?? null;
|
||
if ( ! $q ) {
|
||
continue;
|
||
}
|
||
?>
|
||
<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 nonce(X-WP-Nonce header)。warm zone cutover 前自動 fallback postmeta hp_view_count。', '2meet-data-optimizer' ),
|
||
'params' => array(
|
||
'id' => __( 'Post ID(路徑參數)', '2meet-data-optimizer' ),
|
||
'X-WP-Nonce' => __( 'WP REST nonce(wp_create_nonce("wp_rest"))', '2meet-data-optimizer' ),
|
||
),
|
||
'headers' => array(),
|
||
'curl' => "curl -X POST '{$base}/listings/123/view' -H 'X-WP-Nonce: <nonce>'",
|
||
),
|
||
array(
|
||
'method' => 'GET',
|
||
'path' => '/listings',
|
||
'desc' => __( '查詢 Zone A 扁平欄位(搜尋/篩選),支援分頁與排序。', '2meet-data-optimizer' ),
|
||
'params' => array(
|
||
'post_type' => __( 'post type(預設 hp_listing)', '2meet-data-optimizer' ),
|
||
'per_page' => __( '每頁筆數 1–100(預設 20)', '2meet-data-optimizer' ),
|
||
'page' => __( '頁碼(預設 1)', '2meet-data-optimizer' ),
|
||
'orderby' => __( '排序欄位(預設 post_id)', '2meet-data-optimizer' ),
|
||
'order' => __( 'ASC 或 DESC(預設 DESC)', '2meet-data-optimizer' ),
|
||
'{col}_min' => __( '數值篩選下限,例如 hp_price_min=1000', '2meet-data-optimizer' ),
|
||
'{col}_max' => __( '數值篩選上限,例如 hp_price_max=5000', '2meet-data-optimizer' ),
|
||
'{col}' => __( '精確值篩選,例如 hp_featured=1', '2meet-data-optimizer' ),
|
||
),
|
||
'headers' => array(
|
||
'X-WP-Total' => __( '符合條件的總筆數', '2meet-data-optimizer' ),
|
||
'X-WP-TotalPages' => __( '總頁數', '2meet-data-optimizer' ),
|
||
),
|
||
'curl' => "curl '{$base}/listings?per_page=5&hp_price_min=1000&order=ASC'",
|
||
),
|
||
array(
|
||
'method' => 'GET',
|
||
'path' => '/listings/{id}',
|
||
'desc' => __( '單筆 listing:Zone A(熱區欄位)+ Zone C(JSON blob)合併回傳。Zone 未啟用時自動 fallback postmeta。', '2meet-data-optimizer' ),
|
||
'params' => array( 'id' => __( 'Post ID(路徑參數)', '2meet-data-optimizer' ) ),
|
||
'headers' => array(),
|
||
'curl' => "curl '{$base}/listings/123'",
|
||
),
|
||
array(
|
||
'method' => 'GET',
|
||
'path' => '/stats/{id}',
|
||
'desc' => __( 'Zone B 瀏覽計數(warm zone),warm 未啟用時 fallback hp_view_count postmeta。', '2meet-data-optimizer' ),
|
||
'params' => array( 'id' => __( 'Post ID(路徑參數)', '2meet-data-optimizer' ) ),
|
||
'headers' => array(),
|
||
'curl' => "curl '{$base}/stats/123'",
|
||
),
|
||
array(
|
||
'method' => 'GET',
|
||
'path' => '/status',
|
||
'desc' => __( '版本、引擎、欄位統計、模組狀態。需要 manage_options 權限(帶 WP Nonce)。', '2meet-data-optimizer' ),
|
||
'params' => array(),
|
||
'headers' => array(),
|
||
'curl' => "curl '{$base}/status' -H 'X-WP-Nonce: <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 ); ?>
|
||
<?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 <snapshot_id> --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_write(FSM 第 1 個 active state,FSM 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 Bridge(v2.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 Filter(v2.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_options(native 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 Filter(v2.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 Filter(v2.12.3)', '2meet-data-optimizer' ); ?></h3>
|
||
<p class="description">
|
||
<?php esc_html_e( '攔截 WooCommerce 寫入 wp_termmeta 的 product_count_<taxonomy> cache rows,重新路由到 wp_options(native 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 Bucket(v2.12.4)', '2meet-data-optimizer' ); ?></h3>
|
||
<p class="description">
|
||
<?php esc_html_e( 'Catch-all flat 表,捕捉所有未被前面 filter 處理的 term/comment meta keys(priority 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 Automator(v2.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();
|