76c01e44df
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入 並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
220 lines
9.9 KiB
PHP
220 lines
9.9 KiB
PHP
<?php
|
||
/**
|
||
* One-click User Entity Migration Wizard.
|
||
*
|
||
* Variables in scope (from class-tmdo-admin.php::render_migration_wizard):
|
||
* $preflight array TMDO_Migration_Orchestrator::preflight() snapshot.
|
||
* $status array TMDO_Migration_Orchestrator::get_status() snapshot.
|
||
*
|
||
* @package WP_Data_Optimizer
|
||
* @since 2.8.0
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
|
||
if ( ! defined( 'ABSPATH' ) ) {
|
||
exit;
|
||
}
|
||
|
||
$ratio = $preflight['ratio'] ?? 0;
|
||
$users_count = $preflight['users'] ?? 0;
|
||
$usermeta_count = $preflight['usermeta'] ?? 0;
|
||
$eav_residue = $preflight['eav_rows'] ?? 0;
|
||
$current_mode = $preflight['mode'] ?? 'unknown';
|
||
$strategy = $preflight['estimated_strategy'] ?? 'sync';
|
||
$est_sec = $preflight['estimated_sec'] ?? 0;
|
||
$groups = $preflight['groups'] ?? array();
|
||
$state = $status['state'] ?? 'idle';
|
||
$active = in_array( $state, array( 'running', 'paused' ), true );
|
||
$completed = 'completed' === $state;
|
||
$failed = 'failed' === $state;
|
||
?>
|
||
<div class="wpdo-migration-wizard" data-state="<?php echo esc_attr( $state ); ?>">
|
||
|
||
<header class="wpdo-mw-header">
|
||
<h2><?php esc_html_e( 'User Entity 遷移精靈', '2meet-data-optimizer' ); ?></h2>
|
||
<p class="description">
|
||
<?php esc_html_e( '把 wp_usermeta 中已註冊為 entity field 的 legacy keys 一次遷移到 flat tables。流程包含自動備份、demote、bulk SQL backfill、shadow_read 驗證、aeav_only cutover、以及最終的 EAV 殘留清除。', '2meet-data-optimizer' ); ?>
|
||
</p>
|
||
</header>
|
||
|
||
<!-- Pre-flight diagnostic panel ─────────────────────────────────────── -->
|
||
<section class="wpdo-mw-panel wpdo-mw-preflight" id="wpdo-mw-preflight">
|
||
<h3><?php esc_html_e( '當前狀態', '2meet-data-optimizer' ); ?></h3>
|
||
<div class="wpdo-mw-metrics">
|
||
<div class="wpdo-mw-metric">
|
||
<span class="wpdo-mw-metric-label"><?php esc_html_e( 'wp_users', '2meet-data-optimizer' ); ?></span>
|
||
<span class="wpdo-mw-metric-value" id="wpdo-mw-users"><?php echo esc_html( number_format_i18n( $users_count ) ); ?></span>
|
||
</div>
|
||
<div class="wpdo-mw-metric">
|
||
<span class="wpdo-mw-metric-label"><?php esc_html_e( 'wp_usermeta', '2meet-data-optimizer' ); ?></span>
|
||
<span class="wpdo-mw-metric-value" id="wpdo-mw-usermeta"><?php echo esc_html( number_format_i18n( $usermeta_count ) ); ?></span>
|
||
</div>
|
||
<div class="wpdo-mw-metric wpdo-mw-metric-primary">
|
||
<span class="wpdo-mw-metric-label"><?php esc_html_e( '當前 ratio', '2meet-data-optimizer' ); ?></span>
|
||
<span class="wpdo-mw-metric-value" id="wpdo-mw-ratio">1:<?php echo esc_html( (string) $ratio ); ?></span>
|
||
</div>
|
||
<div class="wpdo-mw-metric">
|
||
<span class="wpdo-mw-metric-label"><?php esc_html_e( 'EAV 殘留', '2meet-data-optimizer' ); ?></span>
|
||
<span class="wpdo-mw-metric-value" id="wpdo-mw-residue"><?php echo esc_html( number_format_i18n( $eav_residue ) ); ?></span>
|
||
</div>
|
||
<div class="wpdo-mw-metric">
|
||
<span class="wpdo-mw-metric-label"><?php esc_html_e( 'user mode', '2meet-data-optimizer' ); ?></span>
|
||
<span class="wpdo-mw-metric-value wpdo-mw-mode wpdo-mw-mode-<?php echo esc_attr( $current_mode ); ?>"><?php echo esc_html( $current_mode ); ?></span>
|
||
</div>
|
||
</div>
|
||
|
||
<details class="wpdo-mw-groups">
|
||
<summary><?php esc_html_e( '各 entity group 詳細狀態', '2meet-data-optimizer' ); ?></summary>
|
||
<table class="widefat striped">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e( 'Group', '2meet-data-optimizer' ); ?></th>
|
||
<th><?php esc_html_e( 'Keys', '2meet-data-optimizer' ); ?></th>
|
||
<th><?php esc_html_e( 'Flat rows', '2meet-data-optimizer' ); ?></th>
|
||
<th><?php esc_html_e( 'EAV 殘留', '2meet-data-optimizer' ); ?></th>
|
||
<th><?php esc_html_e( 'Backfill 策略', '2meet-data-optimizer' ); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ( $groups as $name => $g ) : ?>
|
||
<tr>
|
||
<td><code><?php echo esc_html( $name ); ?></code></td>
|
||
<td><?php echo esc_html( (string) count( $g['keys'] ) ); ?></td>
|
||
<td><?php echo esc_html( (string) $g['flat_rows'] ); ?></td>
|
||
<td>
|
||
<?php if ( $g['residue'] > 0 ) : ?>
|
||
<strong><?php echo esc_html( (string) $g['residue'] ); ?></strong>
|
||
<?php else : ?>
|
||
<span class="wpdo-mw-zero">0</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><code><?php echo esc_html( $g['strategy'] ); ?></code></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</details>
|
||
</section>
|
||
|
||
<!-- Run-migration panel (idle state) ──────────────────────────────── -->
|
||
<section class="wpdo-mw-panel wpdo-mw-run" id="wpdo-mw-run-panel"
|
||
<?php echo $active || $completed ? 'hidden' : ''; ?>>
|
||
|
||
<?php if ( 0 === $eav_residue && 'aeav_only' === $current_mode ) : ?>
|
||
<div class="wpdo-mw-nothing-todo">
|
||
<h3>✅ <?php esc_html_e( '無事可做', '2meet-data-optimizer' ); ?></h3>
|
||
<p><?php esc_html_e( '所有 entity group 已 aeav_only / 0 EAV 殘留。', '2meet-data-optimizer' ); ?></p>
|
||
<p>
|
||
<?php
|
||
echo esc_html(
|
||
sprintf(
|
||
/* translators: %s: ratio */
|
||
__( '當前 ratio:1:%s', '2meet-data-optimizer' ),
|
||
(string) $ratio
|
||
)
|
||
);
|
||
?>
|
||
</p>
|
||
</div>
|
||
<?php else : ?>
|
||
<h3><?php esc_html_e( '執行一鍵遷移', '2meet-data-optimizer' ); ?></h3>
|
||
<div class="wpdo-mw-estimate">
|
||
<?php
|
||
printf(
|
||
/* translators: 1: rows count, 2: strategy, 3: estimated seconds */
|
||
esc_html__( '將遷移 %1$s 行 EAV 殘留,預估策略:%2$s(約 %3$s 秒)。', '2meet-data-optimizer' ),
|
||
'<strong>' . esc_html( number_format_i18n( $eav_residue ) ) . '</strong>',
|
||
'<code>' . esc_html( $strategy ) . '</code>',
|
||
'<strong>' . esc_html( (string) $est_sec ) . '</strong>'
|
||
);
|
||
?>
|
||
</div>
|
||
|
||
<fieldset class="wpdo-mw-options">
|
||
<legend><?php esc_html_e( '進階選項', '2meet-data-optimizer' ); ?></legend>
|
||
<label>
|
||
<input type="checkbox" id="wpdo-mw-opt-backup" checked>
|
||
<?php esc_html_e( '自動備份 wp_usermeta 至 uploads/wpdo-backups/(推薦)', '2meet-data-optimizer' ); ?>
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="wpdo-mw-opt-strict" checked>
|
||
<?php esc_html_e( '嚴格驗證(500 users / 10% 抽樣)', '2meet-data-optimizer' ); ?>
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="wpdo-mw-opt-24h">
|
||
<?php esc_html_e( '24 小時 shadow_read 觀察視窗(生產環境用,會暫停 24h 才繼續)', '2meet-data-optimizer' ); ?>
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="wpdo-mw-opt-async">
|
||
<?php esc_html_e( '強制 async(cron 推進,不阻塞 HTTP 請求)', '2meet-data-optimizer' ); ?>
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="wpdo-mw-opt-dryrun">
|
||
<?php esc_html_e( 'Dry-run(不寫入、不刪除,僅模擬流程)', '2meet-data-optimizer' ); ?>
|
||
</label>
|
||
</fieldset>
|
||
|
||
<div class="wpdo-mw-confirm">
|
||
<label>
|
||
<input type="checkbox" id="wpdo-mw-confirm-backup" required>
|
||
<?php esc_html_e( '我已了解:流程將從 wp_usermeta 刪除已註冊欄位的 EAV 行,過程不可逆(自動備份提供救援路徑)。', '2meet-data-optimizer' ); ?>
|
||
</label>
|
||
</div>
|
||
|
||
<button type="button" class="button button-primary button-hero" id="wpdo-mw-start" disabled>
|
||
🚀 <?php esc_html_e( '開始一鍵遷移', '2meet-data-optimizer' ); ?>
|
||
</button>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<!-- Live progress panel (running state) ───────────────────────────── -->
|
||
<section class="wpdo-mw-panel wpdo-mw-progress" id="wpdo-mw-progress-panel"
|
||
<?php echo $active || $failed ? '' : 'hidden'; ?>>
|
||
|
||
<h3 id="wpdo-mw-progress-title"><?php esc_html_e( '執行中…', '2meet-data-optimizer' ); ?></h3>
|
||
|
||
<div class="wpdo-mw-progress-bar">
|
||
<div class="wpdo-mw-progress-fill" id="wpdo-mw-progress-fill"
|
||
style="width:<?php echo esc_attr( (string) ( $status['overall_progress'] ?? 0 ) ); ?>%"></div>
|
||
<span class="wpdo-mw-progress-pct" id="wpdo-mw-progress-pct"><?php echo esc_html( (string) ( $status['overall_progress'] ?? 0 ) ); ?>%</span>
|
||
</div>
|
||
|
||
<div class="wpdo-mw-current">
|
||
<span class="wpdo-mw-current-phase">
|
||
<?php esc_html_e( '當前階段:', '2meet-data-optimizer' ); ?>
|
||
<code id="wpdo-mw-current-phase-name"><?php echo esc_html( $status['phase'] ?? 'idle' ); ?></code>
|
||
</span>
|
||
<span class="wpdo-mw-elapsed" id="wpdo-mw-elapsed">0.0s</span>
|
||
</div>
|
||
|
||
<div class="wpdo-mw-live-ratio">
|
||
<span><?php esc_html_e( 'Live ratio:', '2meet-data-optimizer' ); ?></span>
|
||
<span id="wpdo-mw-live-ratio-text">
|
||
1:<?php echo esc_html( (string) ( $status['metrics']['ratio_start'] ?? $ratio ) ); ?>
|
||
→
|
||
1:<?php echo esc_html( (string) ( $status['metrics']['ratio_now'] ?? $ratio ) ); ?>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="wpdo-mw-log-wrap">
|
||
<h4><?php esc_html_e( 'Log', '2meet-data-optimizer' ); ?></h4>
|
||
<pre class="wpdo-mw-log" id="wpdo-mw-log"><?php echo esc_html( implode( "\n", (array) ( $status['log'] ?? array() ) ) ); ?></pre>
|
||
</div>
|
||
|
||
<div class="wpdo-mw-actions">
|
||
<button type="button" class="button" id="wpdo-mw-cancel"><?php esc_html_e( '取消', '2meet-data-optimizer' ); ?></button>
|
||
<button type="button" class="button button-primary" id="wpdo-mw-resume" hidden><?php esc_html_e( 'Resume', '2meet-data-optimizer' ); ?></button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Completed state ───────────────────────────────────────────────── -->
|
||
<section class="wpdo-mw-panel wpdo-mw-done" id="wpdo-mw-done-panel"
|
||
<?php echo $completed ? '' : 'hidden'; ?>>
|
||
<h3>✅ <?php esc_html_e( '遷移完成', '2meet-data-optimizer' ); ?></h3>
|
||
<div class="wpdo-mw-done-summary" id="wpdo-mw-done-summary"></div>
|
||
<button type="button" class="button" id="wpdo-mw-reset"><?php esc_html_e( '回到診斷頁', '2meet-data-optimizer' ); ?></button>
|
||
</section>
|
||
|
||
</div>
|