d52d604d7a
Anti-EAV Lint + Quality Gate / anti-eav-lint (push) Successful in 8s
Tests / Unit Tests (push) Successful in 10s
Tests / Integration Tests (push) Successful in 35s
Tests / PHP Lint (push) Successful in 7s
Tests / PHPCS (push) Successful in 19s
Tests / PHPStan (push) Successful in 24s
admin/ 的 inline style= 由 348 處降到 9 處,剩下的 9 處全是 CSS custom property 載體(--wpdo-bar-width / --wpdo-cov-pct 等動態數值),與來源外掛的 設計一致。 - wpdo-admin.css 543 → 911 行:補上 148 個 class(來源檔在 selector 與 rule-block 層級都是既有內容的超集,逐條核對過),另加 .wpdo-form-inline - 5 個變數改為 class 版本:$mode_cls / $badge_mode_cls / $diffs_cls / $ap_cls / $e_mode_cls;3 個 stress-test template 補 $mode_text_cls / $mode_card_cls - 刪掉因此變成孤兒的 $mode_badge / $style / $mode_style / $badge_style / $e_mode_bg / $badge_bg - entity card 改 .wpdo-eb-card、pipeline dot 改 .wpdo-stage-pill、demote 按鈕 改 .wpdo-eb-btn-dim;.wpdo-native-counts / .wpdo-recommendation / .wpdo-setup-banner-btn 的重複 inline style 移除 - dashboard 的 top-views 查詢一併改用上一個 commit 的 TMDO_Zone_Warm::VIEW_KEY PHPStan 抓出自動轉換造成的 11 個未定義變數,已全數補回定義並複驗。
481 lines
21 KiB
PHP
481 lines
21 KiB
PHP
<?php
|
||
/**
|
||
* TMDO_Setup_Wizard — First-run onboarding wizard (v2.3.0 M5).
|
||
*
|
||
* 5 steps:
|
||
* 1. Welcome + anti-EAV concept (4-zone diagram, when needed, when not).
|
||
* 2. Baseline diagnostic — auto-runs Site Health, postmeta count, top
|
||
* meta_key consumers, predicted optimization upside.
|
||
* 3. Recommendation — checks Classifier suggestions, presents pre-checked
|
||
* modules with "OK" / "skip" choice (no auto-enable; explicit consent).
|
||
* 4. First snapshot + daily health check (already scheduled by Core, just
|
||
* confirms here).
|
||
* 5. Done — sets wpdo_setup_wizard_completed=1, wpdo_first_run_at timestamp.
|
||
*
|
||
* Trigger: admin loads any wp-admin page AND wpdo_setup_wizard_completed != '1'.
|
||
* Existing-user shielding: if any module is not idle (= already migrating),
|
||
* we mark wizard completed immediately to avoid ambushing experienced admins.
|
||
*
|
||
* @package WP_Data_Optimizer
|
||
*/
|
||
|
||
// phpcs:ignore WPDO.AntiEAV -- platform admin UI: postmeta inventory for setup wizard
|
||
|
||
declare(strict_types=1);
|
||
|
||
if ( ! defined( 'ABSPATH' ) ) {
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Setup wizard — stateless static API.
|
||
*/
|
||
class TMDO_Setup_Wizard {
|
||
|
||
public const OPT_COMPLETED = 'wpdo_setup_wizard_completed';
|
||
public const OPT_FIRST_RUN = 'wpdo_first_run_at';
|
||
public const QUERY_PARAM = 'wpdo_wizard';
|
||
public const NONCE_NAME = 'wpdo_wizard_nonce';
|
||
public const TOTAL_STEPS = 5;
|
||
|
||
/**
|
||
* Bootstrap hooks.
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function register(): void {
|
||
add_action( 'admin_init', array( __CLASS__, 'maybe_redirect_to_wizard' ) );
|
||
add_action( 'admin_menu', array( __CLASS__, 'register_menu_page' ) );
|
||
add_action( 'admin_post_wpdo_wizard_step', array( __CLASS__, 'handle_step_submit' ) );
|
||
add_action( 'admin_post_wpdo_wizard_dismiss', array( __CLASS__, 'handle_dismiss' ) );
|
||
add_action( 'admin_post_wpdo_wizard_reset', array( __CLASS__, 'handle_reset' ) );
|
||
}
|
||
|
||
/**
|
||
* On first activation, decide: run wizard, or shield (already configured).
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function maybe_redirect_to_wizard(): void {
|
||
// Don't redirect on AJAX / cron / auto-saves / wp-cli.
|
||
if ( wp_doing_ajax() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
|
||
return;
|
||
}
|
||
if ( ! TMDO_Capability::current_user_can_admin() ) {
|
||
return;
|
||
}
|
||
if ( '1' === get_option( self::OPT_COMPLETED ) ) {
|
||
return;
|
||
}
|
||
|
||
// Shield existing users: if any module is not idle, the admin already knows
|
||
// what they're doing. Mark wizard completed silently.
|
||
if ( class_exists( 'TMDO_Feature_Flags' ) ) {
|
||
$flags = TMDO_Feature_Flags::all();
|
||
foreach ( $flags as $state ) {
|
||
if ( 'idle' !== $state ) {
|
||
update_option( self::OPT_COMPLETED, '1', false );
|
||
update_option( self::OPT_FIRST_RUN, gmdate( 'Y-m-d H:i:s' ), false );
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Don't redirect if already on the wizard page or another WPDO admin page.
|
||
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( (string) $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||
if ( in_array( $page, array( 'wpdo-setup-wizard', '2meet-data-optimizer' ), true ) ) {
|
||
return;
|
||
}
|
||
|
||
// Only redirect from the WordPress dashboard top page (avoid plugin install + mu-plugins flow).
|
||
global $pagenow;
|
||
if ( 'index.php' !== $pagenow ) {
|
||
return;
|
||
}
|
||
|
||
wp_safe_redirect( admin_url( 'tools.php?page=wpdo-setup-wizard&step=1' ) );
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Register the wizard as a visible submenu item under Tools > WP Data Optimizer.
|
||
*
|
||
* Appears in the Tools section so admins can re-run the wizard at any time.
|
||
* The wizard itself is idempotent: re-running it never auto-enables modules.
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function register_menu_page(): void {
|
||
add_submenu_page(
|
||
'tools.php',
|
||
__( 'WPDO 設定嚮導', '2meet-data-optimizer' ),
|
||
__( 'WPDO 設定嚮導', '2meet-data-optimizer' ),
|
||
'manage_options',
|
||
'wpdo-setup-wizard',
|
||
array( __CLASS__, 'render_page' )
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Render the wizard.
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function render_page(): void {
|
||
if ( ! TMDO_Capability::current_user_can_admin() ) {
|
||
wp_die( esc_html__( 'Insufficient permissions.', '2meet-data-optimizer' ) );
|
||
}
|
||
$step = max( 1, min( self::TOTAL_STEPS, absint( wp_unslash( $_GET['step'] ?? 1 ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||
$completed = '1' === get_option( self::OPT_COMPLETED );
|
||
?>
|
||
<div class="wrap wpdo-max-800">
|
||
<p>
|
||
<a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer' ) ); ?>">
|
||
← <?php esc_html_e( '返回 WP Data Optimizer', '2meet-data-optimizer' ); ?>
|
||
</a>
|
||
</p>
|
||
<h1><?php esc_html_e( 'WP Data Optimizer — 設定嚮導', '2meet-data-optimizer' ); ?></h1>
|
||
|
||
<?php if ( $completed ) : ?>
|
||
<div class="wpdo-setup-banner">
|
||
<span>✅ <?php esc_html_e( '嚮導已完成。你可以重新瀏覽任何步驟,或重設為全新執行。', '2meet-data-optimizer' ); ?></span>
|
||
<a class="button button-secondary wpdo-setup-banner-btn"
|
||
href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=wpdo_wizard_reset' ), self::NONCE_NAME ) ); ?>"
|
||
>
|
||
<?php esc_html_e( '重新執行精靈', '2meet-data-optimizer' ); ?>
|
||
</a>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<p class="description">
|
||
<?php esc_html_e( '第', '2meet-data-optimizer' ); ?>
|
||
<strong><?php echo (int) $step; ?></strong> /
|
||
<strong><?php echo (int) self::TOTAL_STEPS; ?></strong>
|
||
<?php esc_html_e( '步', '2meet-data-optimizer' ); ?>
|
||
</p>
|
||
|
||
<div class="wpdo-setup-progress-track">
|
||
<div class="wpdo-setup-progress-fill" style="--wpdo-progress-pct:<?php echo (int) ( $step / self::TOTAL_STEPS * 100 ); ?>%"></div>
|
||
</div>
|
||
|
||
<div class="card wpdo-setup-card-full">
|
||
<?php call_user_func( array( __CLASS__, 'render_step_' . $step ) ); ?>
|
||
</div>
|
||
|
||
<p class="wpdo-mt-3">
|
||
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=wpdo_wizard_dismiss' ), self::NONCE_NAME ) ); ?>"
|
||
class="wpdo-text-skip">
|
||
<?php esc_html_e( '我熟悉了,跳過嚮導', '2meet-data-optimizer' ); ?>
|
||
</a>
|
||
</p>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
// ─── steps ─────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Render wizard step 1: Welcome.
|
||
*
|
||
* @return void
|
||
*/
|
||
private static function render_step_1(): void {
|
||
?>
|
||
<h2><?php esc_html_e( '👋 歡迎使用 WP Data Optimizer', '2meet-data-optimizer' ); ?></h2>
|
||
<p><?php esc_html_e( 'WPDO 解決的是 wp_postmeta 表「meta 爆炸」問題:當 postmeta 累積到數百萬行時,autoload 變大、JOIN 變慢、整站變慢。', '2meet-data-optimizer' ); ?></p>
|
||
|
||
<h3><?php esc_html_e( '4 個 Zone 是什麼?', '2meet-data-optimizer' ); ?></h3>
|
||
<table class="widefat wpdo-mb-2">
|
||
<thead>
|
||
<tr><th>Zone</th><th><?php esc_html_e( '用途', '2meet-data-optimizer' ); ?></th><th><?php esc_html_e( '舉例', '2meet-data-optimizer' ); ?></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td><strong>Hot</strong></td><td><?php esc_html_e( '高頻 + 索引欄位', '2meet-data-optimizer' ); ?></td><td><?php esc_html_e( 'price, location, vendor_id', '2meet-data-optimizer' ); ?></td></tr>
|
||
<tr><td><strong>Warm</strong></td><td><?php esc_html_e( 'TTL 暫存(會過期)', '2meet-data-optimizer' ); ?></td><td><?php esc_html_e( 'view_count, last_seen', '2meet-data-optimizer' ); ?></td></tr>
|
||
<tr><td><strong>Cold</strong></td><td><?php esc_html_e( '低頻設定型欄位', '2meet-data-optimizer' ); ?></td><td><?php esc_html_e( 'preferences, settings', '2meet-data-optimizer' ); ?></td></tr>
|
||
<tr><td><strong>Archive</strong></td><td><?php esc_html_e( '舊資料 / 已 trashed', '2meet-data-optimizer' ); ?></td><td><?php esc_html_e( '90+ 天前的記錄', '2meet-data-optimizer' ); ?></td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h3><?php esc_html_e( '何時需要 WPDO?', '2meet-data-optimizer' ); ?></h3>
|
||
<ul class="wpdo-list-disc">
|
||
<li><?php esc_html_e( '✅ wp_postmeta 行數 > 100k 開始考慮', '2meet-data-optimizer' ); ?></li>
|
||
<li><?php esc_html_e( '✅ 行數 > 1M 強烈建議啟用', '2meet-data-optimizer' ); ?></li>
|
||
<li><?php esc_html_e( '⚠️ 小站台(< 10k 行)可以裝著但別啟用 module', '2meet-data-optimizer' ); ?></li>
|
||
</ul>
|
||
|
||
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" class="wpdo-mt-3">
|
||
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
||
<input type="hidden" name="step" value="2">
|
||
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:跑健診 →', '2meet-data-optimizer' ); ?></button>
|
||
</form>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Render wizard step 2: Baseline diagnostic.
|
||
*
|
||
* @return void
|
||
*/
|
||
private static function render_step_2(): void {
|
||
global $wpdb;
|
||
$pm_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->postmeta}`" ); // phpcs:ignore WordPress.DB
|
||
$autoload_bytes = (int) $wpdb->get_var( "SELECT COALESCE(SUM(LENGTH(option_value)),0) FROM `{$wpdb->options}` WHERE autoload = 'yes'" ); // phpcs:ignore WordPress.DB
|
||
$top_keys = $wpdb->get_results( "SELECT meta_key, COUNT(*) AS c FROM `{$wpdb->postmeta}` GROUP BY meta_key ORDER BY c DESC LIMIT 10", ARRAY_A ); // phpcs:ignore WordPress.DB
|
||
?>
|
||
<h2><?php esc_html_e( '🔬 Baseline 健診', '2meet-data-optimizer' ); ?></h2>
|
||
<table class="widefat striped wpdo-max-600 wpdo-mb-2">
|
||
<tbody>
|
||
<tr><th><?php esc_html_e( 'wp_postmeta 行數', '2meet-data-optimizer' ); ?></th><td><strong><?php echo esc_html( number_format_i18n( $pm_count ) ); ?></strong></td></tr>
|
||
<tr><th><?php esc_html_e( 'autoload 大小', '2meet-data-optimizer' ); ?></th><td><strong><?php echo esc_html( size_format( $autoload_bytes, 1 ) ); ?></strong></td></tr>
|
||
<tr><th><?php esc_html_e( '建議啟用 WPDO?', '2meet-data-optimizer' ); ?></th>
|
||
<td>
|
||
<?php if ( $pm_count > 1_000_000 ) : ?>
|
||
<span class="wpdo-text-success-em">✅ <?php esc_html_e( '強烈建議', '2meet-data-optimizer' ); ?></span>
|
||
<?php elseif ( $pm_count > 100_000 ) : ?>
|
||
<span class="wpdo-text-warn-em">🟡 <?php esc_html_e( '可以考慮', '2meet-data-optimizer' ); ?></span>
|
||
<?php else : ?>
|
||
<span class="wpdo-text-neutral">⏸ <?php esc_html_e( '尚不必', '2meet-data-optimizer' ); ?></span>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h3><?php esc_html_e( 'Top 10 meta_key(依行數)', '2meet-data-optimizer' ); ?></h3>
|
||
<table class="widefat striped">
|
||
<thead><tr><th>meta_key</th><th><?php esc_html_e( '行數', '2meet-data-optimizer' ); ?></th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ( (array) $top_keys as $k ) : ?>
|
||
<tr><td><code><?php echo esc_html( (string) $k['meta_key'] ); ?></code></td><td><?php echo esc_html( number_format_i18n( (int) $k['c'] ) ); ?></td></tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
|
||
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" class="wpdo-mt-3">
|
||
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
||
<input type="hidden" name="step" value="3">
|
||
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:看建議 →', '2meet-data-optimizer' ); ?></button>
|
||
</form>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Render wizard step 3: Recommendations.
|
||
*
|
||
* @return void
|
||
*/
|
||
private static function render_step_3(): void {
|
||
?>
|
||
<h2><?php esc_html_e( '🧭 智能推薦', '2meet-data-optimizer' ); ?></h2>
|
||
<p><?php esc_html_e( '系統依你的環境(plugin / post_type / 行數)自動偵測哪些 module 適合啟用。每筆建議含 confidence + reasons。', '2meet-data-optimizer' ); ?></p>
|
||
|
||
<?php
|
||
$actionable = array();
|
||
if ( class_exists( 'TMDO_Module_Detector' ) ) {
|
||
$actionable = TMDO_Module_Detector::get_actionable( 0.5 );
|
||
}
|
||
?>
|
||
|
||
<?php if ( empty( $actionable ) ) : ?>
|
||
<div class="notice notice-info inline">
|
||
<p><?php esc_html_e( '目前環境暫無高 confidence 的 module 建議。可隨時前往「模組建議」tab 重新檢查。', '2meet-data-optimizer' ); ?></p>
|
||
</div>
|
||
<?php else : ?>
|
||
<h3>
|
||
<?php
|
||
printf(
|
||
/* translators: %d: count */
|
||
esc_html__( '✅ 偵測到 %d 個建議啟用的 module', '2meet-data-optimizer' ),
|
||
count( $actionable )
|
||
);
|
||
?>
|
||
</h3>
|
||
<table class="widefat striped">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e( 'Module', '2meet-data-optimizer' ); ?></th>
|
||
<th><?php esc_html_e( 'Confidence', '2meet-data-optimizer' ); ?></th>
|
||
<th><?php esc_html_e( '說明', '2meet-data-optimizer' ); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ( $actionable as $module => $r ) : ?>
|
||
<tr>
|
||
<td><strong><code><?php echo esc_html( (string) $module ); ?></code></strong></td>
|
||
<td><?php echo esc_html( sprintf( '%.2f', (float) $r['confidence'] ) ); ?></td>
|
||
<td>
|
||
<em><?php echo esc_html( (string) $r['description'] ); ?></em><br/>
|
||
<small><?php echo esc_html( implode( ' · ', (array) $r['reasons'] ) ); ?></small>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<p class="description"><?php esc_html_e( '完成 wizard 後,到「模組建議」tab 一鍵啟用(推進到 dual_write,FSM Guard 確保不越級)。', '2meet-data-optimizer' ); ?></p>
|
||
<?php endif; ?>
|
||
|
||
<h3><?php esc_html_e( '黃金法則', '2meet-data-optimizer' ); ?></h3>
|
||
<ul class="wpdo-list-disc">
|
||
<li><?php esc_html_e( '一次只推進 1 個 module,每階段觀察至少 24-48 小時。', '2meet-data-optimizer' ); ?></li>
|
||
<li><?php esc_html_e( '進 cutover 前一定要有 snapshot(FSM Guard 自動觸發)。', '2meet-data-optimizer' ); ?></li>
|
||
<li><?php esc_html_e( '出事第一件事:rewind 該 module 到 idle(emergency 流程)。', '2meet-data-optimizer' ); ?></li>
|
||
</ul>
|
||
|
||
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" class="wpdo-mt-3">
|
||
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
||
<input type="hidden" name="step" value="4">
|
||
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:建第一個 snapshot →', '2meet-data-optimizer' ); ?></button>
|
||
</form>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Render wizard step 4: Snapshot + health check.
|
||
*
|
||
* @return void
|
||
*/
|
||
private static function render_step_4(): void {
|
||
// On entry, take the first snapshot if not already done in this wizard run.
|
||
$snapshot_taken = isset( $_GET['snap_done'] ) ? '1' === $_GET['snap_done'] : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||
?>
|
||
<h2><?php esc_html_e( '📸 第一個 Snapshot + Daily Health Check', '2meet-data-optimizer' ); ?></h2>
|
||
|
||
<h3><?php esc_html_e( '📦 Snapshot', '2meet-data-optimizer' ); ?></h3>
|
||
<?php if ( $snapshot_taken ) : ?>
|
||
<p class="wpdo-text-success-em">✅ <?php esc_html_e( '快照已建立。可隨時於「備份快照」tab 查看與管理。', '2meet-data-optimizer' ); ?></p>
|
||
<?php else : ?>
|
||
<p><?php esc_html_e( '我們會建立一個 baseline snapshot,命名為 wizard_baseline,保留 365 天。即使你日後沒做任何 destructive 操作,這也是一個 known-good 還原點。', '2meet-data-optimizer' ); ?></p>
|
||
<form method="post" action="<?php echo esc_url( admin_url( 'tools.php?page=wpdo-setup-wizard&step=4' ) ); ?>" class="wpdo-form-inline">
|
||
<?php wp_nonce_field( self::NONCE_NAME ); ?>
|
||
<input type="hidden" name="wpdo_take_snapshot" value="1">
|
||
<p>
|
||
<button type="submit" class="button button-secondary">
|
||
<?php esc_html_e( '建立 baseline snapshot', '2meet-data-optimizer' ); ?>
|
||
</button>
|
||
</p>
|
||
</form>
|
||
<?php
|
||
// Take snapshot if action requested (POST + nonce, prevents Referer leak — v3.3.x P1-6b hardening).
|
||
if ( isset( $_POST['wpdo_take_snapshot'] ) && check_admin_referer( self::NONCE_NAME ) && class_exists( 'TMDO_Snapshot_Manager' ) ) {
|
||
$result = TMDO_Snapshot_Manager::create(
|
||
'manual',
|
||
array(),
|
||
array(
|
||
'notes' => 'wizard_baseline',
|
||
'retention_days' => 365,
|
||
)
|
||
);
|
||
if ( ! empty( $result['ok'] ) ) {
|
||
wp_safe_redirect( admin_url( 'tools.php?page=wpdo-setup-wizard&step=4&snap_done=1' ) );
|
||
exit;
|
||
}
|
||
echo '<div class="notice notice-error"><p>' . esc_html__( 'Snapshot 建立失敗,請查日誌。', '2meet-data-optimizer' ) . '</p></div>';
|
||
}
|
||
?>
|
||
<?php endif; ?>
|
||
|
||
<h3><?php esc_html_e( '🔄 Daily Health Check', '2meet-data-optimizer' ); ?></h3>
|
||
<p>
|
||
<?php
|
||
$next = wp_next_scheduled( 'wpdo_daily_health_check' );
|
||
if ( $next ) {
|
||
printf(
|
||
/* translators: %s: human-readable timestamp */
|
||
esc_html__( 'Daily cron 已排程,下次執行:%s(UTC)', '2meet-data-optimizer' ),
|
||
'<code>' . esc_html( gmdate( 'Y-m-d H:i:s', $next ) ) . '</code>'
|
||
);
|
||
} else {
|
||
esc_html_e( '⚠️ Daily cron 未排程;請重新啟用外掛。', '2meet-data-optimizer' );
|
||
}
|
||
?>
|
||
</p>
|
||
<p class="description"><?php esc_html_e( 'Cron 會跑 7 項 Site Health 檢查;critical 警告寫入 admin notice + audit log。', '2meet-data-optimizer' ); ?></p>
|
||
|
||
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" class="wpdo-mt-3">
|
||
<input type="hidden" name="page" value="wpdo-setup-wizard">
|
||
<input type="hidden" name="step" value="5">
|
||
<button class="button button-primary button-hero"><?php esc_html_e( '下一步:完成 →', '2meet-data-optimizer' ); ?></button>
|
||
</form>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Render wizard step 5: Done.
|
||
*
|
||
* @return void
|
||
*/
|
||
private static function render_step_5(): void {
|
||
// Mark complete on render of step 5.
|
||
update_option( self::OPT_COMPLETED, '1', false );
|
||
if ( ! get_option( self::OPT_FIRST_RUN ) ) {
|
||
update_option( self::OPT_FIRST_RUN, gmdate( 'Y-m-d H:i:s' ), false );
|
||
}
|
||
?>
|
||
<h2><?php esc_html_e( '🎉 完成!', '2meet-data-optimizer' ); ?></h2>
|
||
<p><?php esc_html_e( 'Setup wizard 已結束。下一步建議:', '2meet-data-optimizer' ); ?></p>
|
||
|
||
<ul class="wpdo-list-disc">
|
||
<li><a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=entity-bridge' ) ); ?>"><?php esc_html_e( '🌉 看 Entity Bridge 健康卡片', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( 'user / post / term / comment 4 entity 即時健康+模式狀態', '2meet-data-optimizer' ); ?></li>
|
||
<li><a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=migration-wizard' ) ); ?>"><?php esc_html_e( '🪄 開 User / Post 遷移精靈', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( '一鍵推進 disabled → dual_write → shadow_read → aeav_only', '2meet-data-optimizer' ); ?></li>
|
||
<li><a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer&tab=classifier' ) ); ?>"><?php esc_html_e( '🔍 跑 Classifier', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( '看推薦 zone 配置', '2meet-data-optimizer' ); ?></li>
|
||
<li><a href="<?php echo esc_url( admin_url( 'site-health.php' ) ); ?>"><?php esc_html_e( '🏥 WP Site Health', '2meet-data-optimizer' ); ?></a> — <?php esc_html_e( '7 個 WPDO 健康檢查', '2meet-data-optimizer' ); ?></li>
|
||
</ul>
|
||
|
||
<p class="wpdo-mt-3">
|
||
<a class="button button-primary button-hero" href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer' ) ); ?>">
|
||
<?php esc_html_e( '前往 WPDO 儀表板', '2meet-data-optimizer' ); ?>
|
||
</a>
|
||
</p>
|
||
<?php
|
||
}
|
||
|
||
// ─── form / dismiss handlers ──────────────────────────────────────
|
||
|
||
/**
|
||
* Handle wizard step form submit.
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function handle_step_submit(): void {
|
||
// v2.13.3: capability check first (matches handle_dismiss / handle_reset
|
||
// pattern; fixes L-AUTH-1). Reserved for future POST-based steps —
|
||
// currently steps use GET nav so body is a no-op redirect.
|
||
if ( ! TMDO_Capability::current_user_can_admin() ) {
|
||
wp_die( esc_html__( 'Insufficient permissions.', '2meet-data-optimizer' ) );
|
||
}
|
||
check_admin_referer( self::NONCE_NAME );
|
||
wp_safe_redirect( admin_url( 'tools.php?page=wpdo-setup-wizard&step=1' ) );
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Handle wizard dismiss action.
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function handle_dismiss(): void {
|
||
check_admin_referer( self::NONCE_NAME );
|
||
if ( TMDO_Capability::current_user_can_admin() ) {
|
||
update_option( self::OPT_COMPLETED, '1', false );
|
||
update_option( self::OPT_FIRST_RUN, gmdate( 'Y-m-d H:i:s' ), false );
|
||
}
|
||
wp_safe_redirect( admin_url( 'tools.php?page=wp-data-optimizer' ) );
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Handle wizard reset — clears OPT_COMPLETED so wizard reruns from step 1.
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function handle_reset(): void {
|
||
check_admin_referer( self::NONCE_NAME );
|
||
if ( TMDO_Capability::current_user_can_admin() ) {
|
||
update_option( self::OPT_COMPLETED, '0', false );
|
||
}
|
||
wp_safe_redirect( admin_url( 'tools.php?page=wpdo-setup-wizard&step=1' ) );
|
||
exit;
|
||
}
|
||
}
|