6 Commits

Author SHA1 Message Date
wpdev 9fa84845be fix: NinjaFirewall 相容性 — 保護 WAF 設定選項不被重導向
評估 NinjaFirewall (WP Edition) 4.9 的相容性,結論是兩者可共存且
不需要開發 AddOn(該外掛全 codebase 零個 apply_filters('nfw_*') /
do_action('nfw_*'),官方相容手段全在部署層)。但核心有一個隱患必須補。

TMDO_Options_Manager::register_settings_group() 以 pre_update_option_{key}
回傳 $old_value,讓選項不再落地 wp_options、改存專屬設定表。而
NinjaFirewall 的 Full WAF 走 auto_prepend_file,在 WordPress 載入前就以
原生 mysqli 直查 wp_options 取 nfw_options / nfw_rules。一旦這些鍵被
重導向,WAF 會讀不到設定而靜默停止防護 —— 不報錯、不寫 log。

nfw_rules 約 77KB 且 autoload=auto,正是 autoload 瘦身最誘人的目標,
因此這條路徑並非理論風險。

Added
- PROTECTED_OPTIONS 常數與註冊守衛(nfw_options / nfw_rules / nfw_checked),
  命中時發出 _doing_it_wrong()。守衛置於方法開頭,全部鍵都被擋時提前返回,
  不再建立空的設定表。
- tests/unit/OptionsManagerProtectedTest.php(4 tests / 10 assertions),
  鎖住「受保護鍵絕不會被掛上 pre_option_* / pre_update_option_* 攔截」。
- docs/WAF-COMPATIBILITY.md:模式差異、symlink 多租戶部署、WP SaaS 開站
  流程與驗證清單、三條開發約束、實測風險矩陣。

Changed
- Migration Wizard 輪詢 500ms → 2s,與四個 stress-test 面板一致。
  原本 2 req/s 打同一 REST endpoint,易觸發 WAF rate-limit 與 bot 偵測。

autoload 最佳化不受影響:optimize_autoload() 只改 autoload 欄位、不刪列,
而 Full WAF 的 SELECT * 不看 autoload。

驗證:591 tests / 1166 assertions 通過,PHPCS 零違規,版本一致性 1.0.2。
dev30 於 Full WAF 與 WP WAF 兩種模式下實測,firewall log 中 TMDO 相關
攔截 0 筆。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdjXAU473eekjPBB8vPVRS
2026-08-15 20:38:09 +08:00
wpdev 9f587c39dc fix: Schema Registry cold zone 註冊改為冪等(修復欄位重複累積)
Tests / PHP Lint (pull_request) Successful in 13s
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Failing after 25s
Tests / Unit Tests (pull_request) Successful in 36s
Tests / PHPCS (pull_request) Successful in 30s
Tests / PHPStan (pull_request) Successful in 53s
Tests / Integration Tests (pull_request) Successful in 2m47s
三個 zone 索引的寫入方式並不一致:
- hot  -> hot_columns[post_type][column]  以 column 為 key,天生冪等
- warm -> warm_fields[meta_key]           以 meta_key 為 key,天生冪等
- cold -> cold_fields[post_type][]        append,無去重

而 wpdo_register_fields 本來就會被觸發多次:核心自身於 plugins_loaded:4
觸發,主檔另有 late-bind safety net 於 :30 再觸發一次(讓在 :6 之後才
bootstrap 的 AddOn 趕上);加上主外掛與其 AddOn 可能同時註冊同一批欄位。

實測影響:某站台 cold_fields['hp_vendor'] 累積到 35 筆但只有 9 個相異值,
每個 key 重複 4 次。修正後為 9 筆、零重複。

修法:cold 分支加入 in_array() 去重,使 register() 對同一
(post_type, meta_key) 成為冪等操作,與 hot / warm 既有行為一致。

驗證:核心單元測試 587 tests / 1156 assertions 全綠;
wp wpdo doctor 於兩個站台皆 0 FAIL。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 14:27:15 +08:00
wpdev c5027b0fce docs: CHANGELOG 補上階段 6/7 的 Admin、邊界與品質數據
Anti-EAV Lint + Quality Gate / anti-eav-lint (push) Successful in 7s
Tests / Unit Tests (push) Successful in 9s
Tests / Integration Tests (push) Successful in 36s
Tests / PHP Lint (push) Successful in 7s
Tests / PHPCS (push) Successful in 20s
Tests / PHPStan (push) Successful in 24s
2026-07-31 10:44:46 +08:00
wpdev e33ae4e626 test(boundary): 新增 CoreBoundaryTest,並補完 PR-I 的兩個缺漏測試
Anti-EAV Lint + Quality Gate / anti-eav-lint (push) Successful in 9s
Tests / Unit Tests (push) Successful in 9s
Tests / Integration Tests (push) Successful in 31s
Tests / PHP Lint (push) Successful in 8s
Tests / PHPCS (push) Successful in 20s
Tests / PHPStan (push) Successful in 24s
CoreBoundaryTest 靜態掃描核心 126 個生產檔,斷言每一處對 AddOn 類別的
static 呼叫都在同一個函式內有 class_exists() 守衛。上一個 commit 修的
TMDO_Listing_Stats fatal 就是這類缺陷,這個測試讓它不會再回來。

它當場又抓到 3 處同類違規(都是實際會 fatal 的路徑),一併修掉:
- admin render_hpct_import():改印 admin notice 並 return
- wp tmdo import-hpct:改 WP_CLI::error 明示需要 hivepress-addon
- cli-post cleanup-hp-transients 其實早有守衛,是測試的行距啟發式太窄;
  判斷範圍改成「同一個函式內」而非固定 12 行

負向驗證:暫時注入一處無守衛呼叫 → 測試如預期失敗;還原後回綠。

同時補完計畫階段 7 PR-I 列的兩個缺漏測試:
- 核心 tests/unit/StandardPostInterceptorTest.php(10 tests)
- HP AddOn tests/unit/ListingStatsTest.php(9 tests)——AddOn 的 unit
  bootstrap 先前刻意不載入真實 TMDO_Listing_Stats,改以
  TMDO_TEST_SKIP_LISTING_STATS_STUB 常數讓它跳過核心的 stub
- 核心 unit bootstrap 補 add_post_meta() stub(flush 路徑用得到)

核心 unit 451 → 587、HP AddOn 145 → 154。
2026-07-31 10:41:13 +08:00
wpdev d52d604d7a style(admin): inline style 全面改用 CSS class(PR-H)
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 個未定義變數,已全數補回定義並複驗。
2026-07-31 10:34:57 +08:00
wpdev 6751c69bc2 fix(boundary): 核心不得無守衛呼叫 AddOn 的 TMDO_Listing_Stats
實機渲染時 Dashboard 分頁 fatal:"Class TMDO_Listing_Stats not found"。
該類別住在 hivepress-addon,核心有 6 處直呼,沒裝 AddOn 的站台會炸掉
Dashboard 與兩個 REST 端點(GET /listing/{id}、POST /listing/{id}/view)。

- TMDO_Zone_Warm 新增 VIEW_KEY / VIEW_TTL 常數(值與 AddOn 的
  TMDO_Listing_Stats::VIEW_KEY 完全相同的 'wpdo_views',指向同一批列,
  無資料遷移)
- CLI benchmark 改用核心常數
- REST 兩個 handler 改走新的 read_view_count() / bump_view_count():
  AddOn 在場時仍委派過去(保留 hp_view_count postmeta fallback),
  否則核心自己讀寫 warm 列
- wp tmdo cleanup --archive-expired 加守衛,AddOn 缺席時印 warning 並跳過
2026-07-31 10:34:40 +08:00
24 changed files with 1765 additions and 416 deletions
+2 -2
View File
@@ -3,7 +3,7 @@
* Plugin Name: 2meet Data Optimizer
* Plugin URI: https://2meet.io/2meet-data-optimizer
* Description: 通用 WordPress 反 EAV 引擎:將 postmeta / usermeta / termmeta / commentmeta 自動分流至四象限扁平表(Hot / Warm / Cold / Archive),大幅提升搜尋與篩選效能。從 wp-data-optimizer v2.16.0 提煉的純核心,整合層交給 11 個 AddOn。
* Version: 1.0.0
* Version: 1.0.2
* Requires at least: 6.0
* Tested up to: 6.9.4
* Requires PHP: 8.1
@@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
}
// ── Constants ──────────────────────────────────────────────────────────────
define( 'TMDO_VERSION', '1.0.0' );
define( 'TMDO_VERSION', '1.0.2' );
define( 'TMDO_DB_VERSION', '2.1.0' );
define( 'TMDO_PATH', plugin_dir_path( __FILE__ ) );
define( 'TMDO_URL', plugin_dir_url( __FILE__ ) );
+96 -1
View File
@@ -7,6 +7,81 @@ Versioning follows [Semantic Versioning](https://semver.org/).
---
## [1.0.2] — 2026-08-15 — NinjaFirewall 相容性:WAF 設定選項保護
**性質**:防禦性修正 + 文件。無 schema 變更,無破壞性變更。
### Added
- **`docs/WAF-COMPATIBILITY.md`** — NinjaFirewall (WP Edition) 4.9 相容性評估
結論:兩者可共存,**不需要開發 AddOn**NinjaFirewall 全 codebase 零個
`apply_filters('nfw_*')` / `do_action('nfw_*')`,官方相容手段全在部署層)。
文件涵蓋 WP WAF 與 Full WAF 的模式差異、symlink 多租戶部署的必要設定、
三條開發約束,以及實測風險矩陣。
- **`TMDO_Options_Manager::PROTECTED_OPTIONS`** 常數與註冊守衛
`modules/options/class-tmdo-options-manager.php`
`register_settings_group()` 會以 `pre_update_option_{key}` 回傳 `$old_value`
讓選項不再落地 `wp_options`、改存專屬設定表。但 NinjaFirewall 的 Full WAF 走
`auto_prepend_file`,在 WordPress 載入前就以**原生 mysqli 直查 `wp_options`**
`nfw_options` / `nfw_rules`。一旦這些鍵被重導向,WAF 會讀不到設定而
**靜默停止防護 —— 不報錯、不寫 log**
現以 `PROTECTED_OPTIONS``nfw_options` / `nfw_rules` / `nfw_checked`)在註冊
階段擋下,並發出 `_doing_it_wrong()`。守衛置於方法開頭,全部鍵都被擋時提前
返回,不再建立空的設定表。
註:autoload 最佳化不受影響 —— `optimize_autoload()` 只改 `autoload` 欄位、
不刪列,而該處的 `SELECT *` 不看 autoload。
- **`tests/unit/OptionsManagerProtectedTest.php`** — 4 tests / 10 assertions
鎖住「受保護鍵絕不會被掛上 `pre_option_*` / `pre_update_option_*` 攔截」這個
核心保證,並驗證全數受保護時不對資料庫發出查詢。
### Changed
- **Migration Wizard 輪詢間隔 500ms → 2s**`admin/assets/wpdo-migration-wizard.js`
原本 2 req/s 打同一個 REST endpoint,容易觸發 WAF 的 rate-limit 與 bot 偵測。
改為 2 秒,與四個 stress-test 面板既有的輪詢節奏一致。
---
## [1.0.1] — 2026-08-08 — Schema Registry 冪等性修正(cold zone 欄位重複累積)
**性質**:核心 bug 修復。無 schema 變更,無 API 變更,無破壞性變更。
### Fixed
- **`TMDO_Schema_Registry::register()` 的 cold zone 分支會無限累積重複項**
`includes/class-tmdo-schema-registry.php`
三個 zone 索引的寫入方式並不一致:
- `hot``$this->hot_columns[$post_type][$column] = ...`(以 column 為 key,天生冪等)
- `warm``$this->warm_fields[$meta_key] = ...`(以 meta_key 為 key,天生冪等)
- `cold``$this->cold_fields[$post_type][] = ...`**append,無去重**
`wpdo_register_fields` 本來就會被觸發多次——核心自身在 `plugins_loaded:4` 觸發,
主檔另有 late-bind safety net 於 `plugins_loaded:30` 再次觸發,好讓在 `:6` 之後才
bootstrap 的 AddOn 也能趕上;再加上主外掛與其 AddOn 可能同時註冊同一批欄位。
**實測影響**:某站台 `cold_fields['hp_vendor']` 累積到 **35 筆但只有 9 個相異值**
每個 key 重複 4 次。修正後為 9 筆、零重複。
修法:cold 分支加入 `in_array( ..., true )` 去重,使 `register()` 對同一
`(post_type, meta_key)` 成為冪等操作,與 hot / warm 的既有行為一致。
### 驗證
- 核心單元測試 **587 tests / 1156 assertions** 全綠
- `wp wpdo doctor` 於兩個站台皆 0 FAIL
- Version bump 1.0.0 → 1.0.12 點一致:header / `TMDO_VERSION`
---
## [1.0.0] — 2026-07-31 — 取代 wp-data-optimizer v3.4.6
本版把 `wp-data-optimizer` v3.0.1v3.4.692 個 commit)全數 backport 進來,
@@ -49,12 +124,32 @@ Versioning follows [Semantic Versioning](https://semver.org/).
- 選單 slug 統一回 `wp-data-optimizer`(B 內部 10+ 處連結本來就指向它)
- HP adapter 介面改為 `TMDO_ extends WPDO_`,讓 `instanceof WPDO_HivePress_Adapter` 成立
### 🖥 Admin
- Settings 七個分區各加「儲存此區塊」AJAX 存檔(`wp_ajax_wpdo_save_settings_section`
+ 7 個 `save_section_*()` + `admin/assets/wpdo-settings.js`),整頁送出仍可用
- inline `style=` 由 348 處降到 9 處(剩下的全是 `--wpdo-*` CSS custom property
載體);`wpdo-admin.css` 543 → 912 行,補 149 個 class
### 🚧 邊界
- 核心不再無守衛呼叫 AddOn 類別。`TMDO_Listing_Stats`HivePress AddOn)先前
在 admin Dashboard、兩個公開 REST 端點、`wp tmdo cleanup --archive-expired`
被直呼,沒裝該 AddOn 的站台會 fatal`TMDO_HPCT_Import` 在 admin 與
`wp tmdo import-hpct` 同樣缺守衛
- `TMDO_Zone_Warm::VIEW_KEY` / `VIEW_TTL` 由核心持有(值與 AddOn 常數相同的
`'wpdo_views'`,指向同一批列,無資料遷移)
- 新增 `CoreBoundaryTest`:靜態掃描 126 個核心生產檔,強制每處 AddOn 類別
呼叫都在同一函式內有 `class_exists()` 守衛
### ✅ 品質
- 128 個生產檔 `declare(strict_types=1)`
- PHPCS 0 errors / PHPStan level 6baseline 710
- 測試:核心 unit 451 / integration 416、hivepress-addon 145、woocommerce-addon 10
- 測試:核心 unit 587 / integration 416、hivepress-addon unit 154 /
integration 57、woocommerce-addon unit 10 / integration 18
- gitea CI:核心 6 個 job 全綠 + 11 個 AddOn workflow + branch protection
- 打包 10 項終檢全 PASS569 KB / 187 entriesschema drift 034 CREATE / 34 DROP
### ⚠️ ABI / 行為變更
+26 -1
View File
@@ -6,7 +6,7 @@
## Current state
- **Version**: 0.1.0 (scaffold + 4 phase 完成 2026-05-15)
- **Version**: 1.0.2 (2026-08-15scaffold + 4 phase 完成 2026-05-15)
- **Phase**: 全 4 phase 已完成
- **Source**: 從 `wp-data-optimizer v2.16.0` 提煉
@@ -287,3 +287,28 @@ AddOn 測試 bootstrap 直接 `require` 核心 plugin 的 `tests/bootstrap.php`
- `v0.1.0` — 本 releasePhase 0-4 完成)
- `v0.1.1` — Phase 5 follow-uptests / PHPCS / packaging
- `v0.2.0` — WPDO_ deprecation notice + wpdo → tmdo migration CLI
---
## NinjaFirewall 相容性 ✅(2026-08-15v1.0.2
計畫檔:`~/.claude/plans/2meet-data-optimizer-ninjafirewall-effervescent-lighthouse.md`
- [x] 評估與 NinjaFirewall 4.9 的相容性 → **不需要開發 AddOn**
- [x] 診斷 `Cannot retrieve user options from database (#3)` → 與本外掛無關,根因為 symlink 部署
- [x] 建立 `/var/www/Studio/.htninja`symlink-safe 站台錨點,全租戶共用)
- [x] dev30 啟用 ninjafirewall,補上缺失的 `nfw_options` / `nfw_rules`
- [x] `TMDO_Options_Manager::PROTECTED_OPTIONS` 守衛 + 4 個單元測試
- [x] Migration Wizard 輪詢 500ms → 2s
- [x] `docs/WAF-COMPATIBILITY.md`
- [x] 全套件回歸:591 tests / 1166 assertions OK
### Lessons learnedWAF
- **`__DIR__` 會解析 symlink**。NinjaFirewall Full WAF 用 `dirname(dirname(dirname(__DIR__)))``lib/firewall.php:78`)推導站台位置,在 symlink 共享 codebase 的 SaaS 架構下,所有租戶都會被判定成「共享目錄所屬的那個站」,於是連錯資料庫、撈不到自己的 `nfw_options`,回錯誤碼 6(訊息寫作 `#3`)。**per-site 的可靠錨點是 `$_SERVER['DOCUMENT_ROOT']`**web server 的 root 指令值,不受 symlink 影響),這正是 `.htninja` 的搜尋基準。
- **`.htninja` 放在 `dirname(DOCUMENT_ROOT)` 可服務全部租戶**`firewall.php:47-48` 的第二順位),因為內容以 DOCUMENT_ROOT 動態推導,一份檔案通用。切勿在其中寫 `return` —— `'ALLOW'` / `'BLOCK'` 是有意義的回傳值。
- **`NFW_LOG_DIR` 對 WP WAF 模式同樣必要**:L78 的誤判在兩種模式都存在,只是 WP WAF 不用它連 DB,但 log / cache / session 仍會全部寫進共享目錄互相覆蓋。
- **WAF 失效是靜默的**。讀不到設定時 `nfw_quit()` 直接返回,不擋任何請求也不寫 log —— 「your site is not protected」是字面意思。因此任何會讓 `nfw_options` 離開 `wp_options` 的機制(例如本外掛的 `register_settings_group()` 重導向)都必須擋在註冊階段。
- **`.user.ini` 必須讓 php-fpm worker 可讀**。dev32 那份是 `root:root 0640`www-data 讀不到 → `auto_prepend_file` 靜默失效、Full WAF 等同沒裝。排查時容易誤判成「WAF 有在跑」,實際上擋下請求的是共享 mu-plugin 的 WP WAF。
- **判斷 WAF 是否真的生效,要用會被規則擋的請求實測**(例如 `?x=../../etc/passwd` → 規則 1 → 403),光看首頁 200 或後台無錯誤訊息都不算數。
- WP-CLI 完全豁免(`firewall.php:18-23`),所以 CLI 全綠**不能**當作「WAF 與外掛相容」的證據,必須另外走 HTTP 驗證。
+374 -3
View File
@@ -1,8 +1,8 @@
/**
* WP Data Optimizer — Admin Styles (Morandi Design System)
* 2meet Data Optimizer — Admin Styles (Morandi Design System)
*
* Morandi tokens are provided via the 'morandi-design-system' CSS dependency
* registered in WPDO_Admin::enqueue_assets(). No @import needed.
* registered in TMDO_Admin::enqueue_assets(). No @import needed.
*/
/* ── KPI Hero Strip (v2.16.0) ────────────────────────────────────────── */
@@ -512,10 +512,377 @@
.nav-tab .wpdo-tab-dot {
animation: none;
}
.wpdo-bar-fill,
.wpdo-eb-bar-fill,
.wpdo-setup-progress-fill { transition-duration: 0.01ms; }
}
/* ── Settings per-section instant save (v3.0.2 backport) ────────────── */
/* ── v3.0.2 — Inline-style Elimination Utilities ────────────────────── */
/* Margin / visibility */
.wpdo-mt-0 { margin-top: 0; }
.wpdo-mt-24 { margin-top: 24px; }
.wpdo-hidden { display: none; }
/* Semantic text colours */
.wpdo-text-success { color: #155724; }
.wpdo-text-error { color: #721c24; }
.wpdo-text-muted { color: #999; }
.wpdo-text-warn { color: #856404; }
.wpdo-text-success-em { color: #46b450; font-weight: bold; }
.wpdo-text-warn-em { color: #dba617; font-weight: bold; }
.wpdo-text-neutral { color: #8c8f94; }
.wpdo-text-skip { color: #888; font-size: 0.9em; }
.wpdo-check-success { color: #155724; font-size: 12px; }
/* Mode badges — small pill (Entity Bridge status table) */
.wpdo-mode-sm {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 12px;
}
/* Mode badges — round pill (Entity Bridge health cards) */
.wpdo-mode-pill {
display: inline-block;
padding: 2px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.wpdo-mode--disabled { background: #e0e0e0; color: #444; }
.wpdo-mode--dual-write { background: #d4edda; color: #155724; }
.wpdo-mode--shadow-read { background: #fff3cd; color: #856404; }
.wpdo-mode--aeav-only { background: #cce5ff; color: #004085; }
/* Entity Bridge grid */
.wpdo-eb-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
gap: 20px;
margin-top: 20px;
}
.wpdo-eb-card {
padding: 20px;
border-radius: 8px;
background: #fff;
box-shadow: 0 1px 4px rgba(0,0,0,.1);
}
.wpdo-eb-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.wpdo-eb-title { margin: 0 0 4px; font-size: 16px; }
.wpdo-mode-days { margin-left: 8px; font-size: 12px; color: var(--color-text-secondary, #666); }
.wpdo-mode-warn-sm { font-size: 12px; color: #856404; background: #fff3cd; padding: 2px 8px; border-radius: 4px; }
.wpdo-eb-pipeline { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; font-size: 11px; }
.wpdo-pipeline-arrow { color: #999; }
.wpdo-stage-pill { padding: 2px 8px; border-radius: 3px; }
.wpdo-stage-pill--inactive { background: #f0f0f0; color: #666; }
.wpdo-eb-tables-row {
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;
}
.wpdo-eb-tables-label { font-weight: 600; margin-right: 4px; }
.wpdo-eb-sep { color: #ccc; }
.wpdo-code-xs { font-size: 11px; }
.wpdo-eb-section { margin-bottom: 14px; }
.wpdo-eb-section-title { font-size: 12px; text-transform: uppercase; color: var(--color-text-secondary, #666); letter-spacing: .5px; font-weight: 600; }
.wpdo-eb-group { margin-top: 8px; }
.wpdo-eb-group-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px; }
.wpdo-eb-group-name { font-size: 13px; font-weight: 500; }
.wpdo-eb-group-meta { font-size: 12px; color: var(--color-text-secondary, #666); }
.wpdo-eb-group-bar-row { display: flex; align-items: center; gap: 8px; }
.wpdo-eb-bar-track { flex: 1; height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden; }
.wpdo-eb-bar-fill { height: 100%; width: var(--wpdo-eb-pct, 0%); background: var(--wpdo-eb-color, #28a745); transition: width .4s ease; border-radius: 4px; }
.wpdo-eb-pct-label { font-size: 12px; min-width: 42px; text-align: right; color: #333; }
.wpdo-eb-key-hint { font-size: 11px; color: #888; margin-top: 2px; }
.wpdo-eb-actions { margin-top: 6px; }
.wpdo-eb-shadow-row { margin-bottom: 12px; padding: 8px; background: #f8f9fa; border-radius: 4px; font-size: 13px; }
.wpdo-eb-shadow-ok { color: #28a745; font-size: 12px; }
.wpdo-eb-shadow-warn { color: #dc3545; }
.wpdo-eb-promote-row { margin-bottom: 12px; font-size: 12px; color: var(--color-text-secondary, #666); }
.wpdo-eb-auto-ok { color: #155724; }
.wpdo-eb-auto-warn { color: #856404; }
.wpdo-eb-auto-badge { background: #d4edda; color: #155724; padding: 1px 5px; border-radius: 3px; margin-left: 4px; font-size: 11px; }
.wpdo-eb-info-row {
margin-bottom: 14px;
padding: 8px 10px;
background: #f0f4ff;
border-left: 3px solid #4f6ef7;
border-radius: 0 4px 4px 0;
font-size: 13px;
color: #333;
}
.wpdo-eb-button-row { display: flex; gap: 8px; flex-wrap: wrap; }
.wpdo-eb-btn-dim { opacity: .5; }
.wpdo-eb-footer { margin-top: 24px; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
.wpdo-eb-ol { margin-left: 20px; line-height: 1.9; font-size: 13px; }
/* Stress-test table cells */
.wpdo-td { padding: 6px; }
.wpdo-td-muted { padding: 6px; color: var(--color-text-secondary, #666); }
.wpdo-td-val { padding: 6px; font-weight: 600; }
.wpdo-stats-table { width: 100%; font-size: 13px; margin-top: 10px; border-collapse: collapse; }
/* Full-width progress bar */
.wpdo-bar-track { height: 14px; background: var(--color-bg-tertiary, #e0e0e0); border-radius: 7px; overflow: hidden; }
.wpdo-bar-fill { height: 100%; background: linear-gradient(90deg, #28a745, #20c997); width: var(--wpdo-bar-width, 0%); transition: width 0.4s; }
.wpdo-bar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 13px; }
.wpdo-bar-pct { font-weight: 600; }
.wpdo-bar-section { margin-bottom: 10px; }
/* Layouts */
.wpdo-grid-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; }
.wpdo-grid-4col { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; max-width: 720px; margin: 1rem 0; }
/* Notices */
.wpdo-notice-error {
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;
}
.wpdo-notice-warn-inline { margin: 0; font-size: 12px; color: #856404; background: #fff3cd; padding: 6px 10px; border-radius: 4px; line-height: 1.6; }
/* Count badge */
.wpdo-count-danger { font-size: 18px; color: #dc3545; }
/* Form helpers */
.wpdo-th-main { width: 35%; }
.wpdo-label-block { display: block; margin-bottom: 6px; }
.wpdo-hr-section { margin: 18px 0; }
/* Lists */
.wpdo-list-disc { list-style: disc; padding-left: 1.5em; }
.wpdo-list-ol { margin-left: 20px; line-height: 1.9; font-size: 13px; }
.wpdo-list-ol-tight { margin: 6px 0 8px 22px; padding: 0; }
/* Max-width constraints */
.wpdo-max-520 { max-width: 520px; }
.wpdo-max-600 { max-width: 600px; }
.wpdo-max-720 { max-width: 720px; }
.wpdo-max-800 { max-width: 800px; }
/* Dashboard widget */
.wpdo-widget-intro { font-size: 1.1em; margin-bottom: 0.6em; }
.wpdo-widget-link-right { float: right; padding-top: 4px; }
.wpdo-widget-notice { margin: 0.6em 0; padding: 0.6em 0.8em; border-radius: 3px; }
.wpdo-widget-notice-warn { background: #fff8e5; border-left: 3px solid #dba617; }
.wpdo-widget-notice-error { background: #fef0f0; border-left: 3px solid #dc3232; }
.wpdo-widget-notice-info { background: #e5f5fa; border-left: 3px solid #00a0d2; }
.wpdo-widget-notice-neutral { background: #f6f7f7; border-left: 3px solid #2271b1; }
.wpdo-widget-notice-ok { background: #ecf7ed; border-left: 3px solid #46b450; }
/* Setup wizard */
.wpdo-setup-progress-track { background: #f0f0f1; height: 8px; border-radius: 4px; overflow: hidden; margin-bottom: 2em; }
.wpdo-setup-progress-fill { background: #2271b1; height: 100%; width: var(--wpdo-progress-pct, 0%); transition: width 0.3s; }
.wpdo-setup-card-full { max-width: none; padding: 2em; }
.wpdo-setup-banner {
background: #f0f6fc;
border-left: 4px solid #2271b1;
padding: 0.75em 1em;
margin-bottom: 1em;
display: flex;
align-items: center;
gap: 1em;
}
.wpdo-setup-banner-btn { white-space: nowrap; }
/* HivePress / WC tabs */
.wpdo-cli-block { background: #f6f7f7; padding: 12px; border-left: 4px solid #2271b1; }
.wpdo-opacity-60 { opacity: .6; }
/* Text colour — strong danger / amber (stress-test status) */
.wpdo-text-danger { color: #dc3545; }
.wpdo-text-amber { color: #dba617; }
/* Inline code on white bg (mode-card context) */
.wpdo-code-white { background: #fff; padding: 2px 6px; border-radius: 3px; }
/* Mode info card (stress-test modal-status section) */
.wpdo-mode-card {
border-left-width: 4px;
border-left-style: solid;
border-radius: 6px;
margin-top: 16px;
font-size: 13px;
line-height: 1.7;
}
.wpdo-mode-card--ok { background: #e8f5e9; border-left-color: #28a745; }
.wpdo-mode-card--warn { background: #fff3cd; border-left-color: #dba617; }
/* Settings link margin */
.wpdo-ml-2 { margin-left: 8px; }
/* Matrix / details table helpers */
.wpdo-summary-toggle { cursor: pointer; color: #0073aa; font-weight: 600; }
.wpdo-table-inset { background: #fff; font-size: 12px; }
.wpdo-tr-header th, .wpdo-tr-header td { background: #f0f0f0; }
.wpdo-tr-highlight { background: #fff8e1; }
.wpdo-tr-success { background: #e8f5e9; font-weight: 600; }
/* Entity Bridge JS-hook class static styles (admin.php, updated by wpdo-entity-bridge.js) */
.wpdo-entity-bridge-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
gap: 20px;
margin-top: 20px;
}
.wpdo-mode-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
transition: background-color var(--duration-normal, 200ms) var(--ease-default, ease),
color var(--duration-normal, 200ms) var(--ease-default, ease);
}
/* JS sets className = 'wpdo-mode-badge wpdo-mode-disabled' etc. */
.wpdo-mode-badge.wpdo-mode-disabled { background: #e0e0e0; color: #444; }
.wpdo-mode-badge.wpdo-mode-dual-write { background: #d4edda; color: #155724; }
.wpdo-mode-badge.wpdo-mode-shadow-read { background: #fff3cd; color: #856404; }
.wpdo-mode-badge.wpdo-mode-aeav-only { background: #cce5ff; color: #004085; }
.wpdo-pipeline {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 16px;
font-size: 11px;
}
.wpdo-pipeline-dot { padding: 2px 8px; border-radius: 3px; background: #f0f0f0; color: #666; }
.wpdo-pipeline-inactive { background: #f0f0f0; color: #666; }
.wpdo-native-counts {
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;
}
.wpdo-native-counts-label { font-weight: 600; margin-right: 4px; }
.wpdo-native-sep { color: #ccc; }
.wpdo-mig-status { font-size: 11px; padding: 1px 6px; border-radius: 3px; background: #f0f0f0; }
.wpdo-cov-pct { font-size: 12px; min-width: 42px; text-align: right; color: #333; }
.wpdo-cov-bar-fill { height: 100%; width: var(--wpdo-cov-pct, 0%); background: var(--wpdo-cov-color, #28a745); transition: width .4s ease; border-radius: 4px; }
.wpdo-recommendation {
margin-bottom: 14px;
padding: 8px 10px;
background: #f0f4ff;
border-left: 3px solid #4f6ef7;
border-radius: 0 4px 4px 0;
font-size: 13px;
color: #333;
}
/* Settings tab helpers */
.wpdo-h3-section { margin-top: 2em; }
.wpdo-h4-mb { margin-bottom: 6px; }
.wpdo-table-mb { margin-bottom: 14px; }
.wpdo-table-mb-sm { margin-bottom: 8px; }
.wpdo-pre-code { background: #f0f0f1; padding: 1em; overflow: auto; max-height: 400px; }
.wpdo-details-section {
margin-top: 1.5em;
border: 1px solid #c3c4c7;
border-radius: 3px;
padding: 0.6em 1em;
}
.wpdo-label-inline { display: inline-block; margin-right: 1em; margin-bottom: 0.3em; }
.wpdo-description-mt { margin-top: 1em; }
.wpdo-description-sm { margin-top: 0.25em; }
.wpdo-description-xs { margin-top: 0.5em; font-size: 12px; }
.wpdo-text-ok { color: #46b450; font-weight: bold; }
.wpdo-text-err { color: #dc3232; font-weight: bold; }
.wpdo-text-muted { color: #666; font-size: 12px; }
.wpdo-text-green { color: #155724; }
.wpdo-text-olive { color: #856404; }
.wpdo-text-green-bold { color: #28a745; font-weight: 600; }
.wpdo-text-red-bold { color: #dc3545; font-weight: 600; }
.wpdo-code-sm { margin-left: 8px; font-size: 11px; }
.wpdo-badge-mode { display: inline-block; margin-left: 8px; padding: 2px 8px; border-radius: 3px; font-size: 11px; }
/* Advisor confidence bar (Classifier tab) */
.wpdo-advisor-bar-track {
display: inline-block;
background: #f0f0f1;
border-radius: 3px;
width: 80px;
height: 18px;
position: relative;
vertical-align: middle;
}
.wpdo-advisor-bar-fill {
position: absolute;
inset: 0 auto 0 0;
background: var(--wpdo-adv-color, #c3c4c7);
width: var(--wpdo-adv-width, 0%);
border-radius: 3px;
}
.wpdo-advisor-bar-label {
position: absolute;
inset: 0;
text-align: center;
font-size: 11px;
line-height: 18px;
}
/* EB card group section: empty state warning */
.wpdo-eb-warn-box {
margin: 0;
font-size: 12px;
color: #856404;
background: #fff3cd;
padding: 6px 10px;
border-radius: 4px;
line-height: 1.6;
}
/* Additional utilities */
.wpdo-text-dark-red { color: #721c24; }
.wpdo-text-red { color: #dc3545; }
.wpdo-ml-2 { margin-left: 8px; }
.wpdo-form-table-mt0 { margin-top: 0; }
.wpdo-badge-mode.wpdo-mode-disabled { background: #e0e0e0; color: #444; }
.wpdo-badge-mode.wpdo-mode-dual-write { background: #d4edda; color: #155724; }
.wpdo-badge-mode.wpdo-mode-shadow-read { background: #fff3cd; color: #856404; }
.wpdo-badge-mode.wpdo-mode-aeav-only { background: #cce5ff; color: #004085; }
.wpdo-summary-section { cursor: pointer; font-weight: 600; font-size: 1.1em; }
.wpdo-notice-warn-desc { margin-top: 8px; color: #856404; background: #fff3cd; padding: 8px 12px; border-radius: 4px; }
.wpdo-card--light { background: #f6f7f7; }
.wpdo-h3-sm { font-size: 14px; }
/* ── Settings section cards (task E) ─────────────────────────────── */
.wpdo-settings-section {
background: var(--color-surface-card, #fbf7f0);
border: 1px solid var(--color-border-soft, #e5dccd);
@@ -541,3 +908,7 @@
.wpdo-section-save-status.visible {
opacity: 1;
}
/* Inline form wrapper — the GET → POST hardening (v1.0.0) turned several
action links into single-button forms; they must not break the row flow. */
.wpdo-form-inline { display: inline; }
+2 -2
View File
@@ -3,7 +3,7 @@
*
* - Confirms options + checkbox before starting.
* - Calls REST endpoints under /wp-json/wpdo/v1/migration/.
* - Polls /status every 500ms while job is active.
* - Polls /status every 2s while job is active.
* - Streams log lines with fade-in; live-updates ratio + progress bar.
*
* @since 2.8.0
@@ -11,7 +11,7 @@
(function () {
'use strict';
const POLL_INTERVAL_MS = 500;
const POLL_INTERVAL_MS = 2000;
const config = window.wpdoMigrationWizard || {};
const i18n = config.i18n || {};
const restUrl = (config.restUrl || '').replace(/\/$/, '');
+172 -178
View File
@@ -997,7 +997,7 @@ class TMDO_Admin {
$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,
TMDO_Zone_Warm::VIEW_KEY,
$now_sql
),
ARRAY_A
@@ -1295,7 +1295,7 @@ class TMDO_Admin {
<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">
<form method="post" class="wpdo-form-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>
@@ -1343,12 +1343,6 @@ class TMDO_Admin {
<?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>
@@ -1363,9 +1357,9 @@ class TMDO_Admin {
<?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;
$mode_cls = 'wpdo-badge-mode wpdo-mode-' . str_replace( '_', '-', $mode );
?>
<tr>
<td><code><?php echo esc_html( $etype ); ?></code></td>
@@ -1373,22 +1367,22 @@ class TMDO_Admin {
<?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>
<span class="wpdo-text-green">✓ <?php esc_html_e( '啟用', '2meet-data-optimizer' ); ?></span>
<?php else : ?>
<span style="color:#721c24">✗ <?php esc_html_e( '停用', '2meet-data-optimizer' ); ?></span>
<span class="wpdo-text-dark-red">✗ <?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 ); ?>">
<span class="<?php echo esc_attr( $mode_cls ); ?>">
<?php echo esc_html( $mode ); ?>
</span>
</td>
<td>
<?php if ( empty( $groups ) ) : ?>
<span style="color:#999"><?php esc_html_e( '(無)', '2meet-data-optimizer' ); ?></span>
<span class="wpdo-text-muted"><?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>
<code><?php echo esc_html( $g ); ?></code>
<?php endforeach; ?>
<?php endif; ?>
</td>
@@ -1536,11 +1530,11 @@ class TMDO_Admin {
'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',
$mode_cls_map = array(
'disabled' => 'wpdo-mode-disabled',
'dual_write' => 'wpdo-mode-dual-write',
'shadow_read' => 'wpdo-mode-shadow-read',
'aeav_only' => 'wpdo-mode-aeav-only',
);
$health_data = class_exists( 'TMDO_Entity_Health' ) ? TMDO_Entity_Health::get_all() : array();
@@ -1559,7 +1553,7 @@ class TMDO_Admin {
<?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;">
<div class="wpdo-eb-grid">
<?php
foreach ( array( 'user', 'post', 'term', 'comment' ) as $etype ) :
$info = $health_data[ $etype ] ?? array();
@@ -1572,44 +1566,42 @@ class TMDO_Admin {
$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';
$badge_mode_cls = $mode_cls_map[ $mode ] ?? 'wpdo-mode-disabled';
$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);">
<div class="wpdo-eb-card" data-entity-type="<?php echo esc_attr( $etype ); ?>">
<!-- Header -->
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px;">
<div class="wpdo-eb-header">
<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 ); ?>">
<h3 class="wpdo-eb-title"><?php echo esc_html( ucfirst( $etype ) ); ?> Entity</h3>
<span class="wpdo-mode-badge <?php echo esc_attr( $badge_mode_cls ); ?>">
<?php echo esc_html( $mode ); ?>
</span>
<span class="wpdo-mode-days" style="margin-left:8px;font-size:12px;color:#666;">
<span class="wpdo-mode-days">
<?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;">
<span class="wpdo-mode-warn-sm">
⟳ <?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;">
<div class="wpdo-pipeline wpdo-eb-pipeline">
<?php
foreach ( $mode_order as $i => $m ) :
$active = ( $m === $mode );
?>
<?php if ( $i > 0 ) : ?>
<span style="color:#999;">→</span>
<span class="wpdo-pipeline-arrow">→</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'; ?>">
<span class="wpdo-pipeline-dot wpdo-stage-pill <?php echo $active ? esc_attr( $badge_mode_cls ) : 'wpdo-stage-pill--inactive'; ?>">
<?php echo esc_html( $mode_labels[ $m ] ); ?>
</span>
<?php endforeach; ?>
@@ -1617,18 +1609,17 @@ class TMDO_Admin {
<!-- 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>
<div class="wpdo-native-counts">
<span class="wpdo-eb-tables-label"><?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>
<code class="wpdo-code-xs"><?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>
<span class="wpdo-eb-sep">|</span>
<?php endif; ?>
<?php if ( $native_meta_table ) : ?>
<code style="font-size:11px;"><?php echo esc_html( $native_meta_table ); ?></code>
<code class="wpdo-code-xs"><?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>
@@ -1636,8 +1627,8 @@ class TMDO_Admin {
<!-- Groups / Coverage -->
<?php if ( ! empty( $groups ) ) : ?>
<div style="margin-bottom:14px;">
<strong style="font-size:12px;text-transform:uppercase;color:#666;letter-spacing:.5px;">
<div class="wpdo-eb-section">
<strong class="wpdo-eb-section-title">
<?php esc_html_e( '群組覆蓋率', '2meet-data-optimizer' ); ?>
</strong>
<?php
@@ -1645,35 +1636,35 @@ class TMDO_Admin {
$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>
<div data-group="<?php echo esc_attr( $g['name'] ); ?>" class="wpdo-eb-group">
<div class="wpdo-eb-group-header">
<span class="wpdo-eb-group-name"><?php echo esc_html( $g['name'] ); ?></span>
<span class="wpdo-eb-group-meta">
<code class="wpdo-code-xs"><?php echo esc_html( $g['fields_count'] ); ?> 欄位</code>
&nbsp;
<span class="wpdo-mig-status" style="font-size:11px;padding:1px 6px;border-radius:3px;background:#f0f0f0;">
<span class="wpdo-mig-status">
<?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-eb-group-bar-row">
<div class="wpdo-eb-bar-track">
<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;">
style="--wpdo-cov-pct:<?php echo esc_attr( (string) $cov_pct ); ?>%;--wpdo-cov-color:<?php echo esc_attr( $bar_color ); ?>">
</div>
</div>
<span class="wpdo-cov-pct" style="font-size:12px;min-width:42px;text-align:right;color:#333;">
<span class="wpdo-cov-pct">
<?php echo esc_html( number_format( $cov_pct, 1 ) ); ?>%
</span>
</div>
<div style="font-size:11px;color:#888;margin-top:2px;">
<div class="wpdo-eb-key-hint">
<?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;">
<div class="wpdo-eb-actions">
<button type="button"
class="button button-small"
data-wpdo-action="backfill"
@@ -1687,11 +1678,11 @@ class TMDO_Admin {
<?php endforeach; ?>
</div>
<?php else : ?>
<div style="margin-bottom:14px;">
<p class="description" style="margin:0 0 6px;font-size:13px;">
<div class="wpdo-eb-section">
<p class="description">
<?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;">
<p class="wpdo-eb-warn-box">
<?php
esc_html_e(
'Hook Bus 已設定攔截此 entity type,但目前尚無 partner plugin 透過 wpdo_register_entity_fields 登錄欄位群組。Flat table 不會自動建立——直到有群組被登錄後才開始生效。',
@@ -1704,24 +1695,26 @@ class TMDO_Admin {
<!-- Shadow diffs -->
<?php if ( 'shadow_read' === $mode ) : ?>
<div style="margin-bottom:12px;padding:8px;background:#f8f9fa;border-radius:4px;font-size:13px;">
<div class="wpdo-eb-shadow-row">
<?php esc_html_e( 'Shadow Diffs', '2meet-data-optimizer' ); ?>
<strong class="wpdo-shadow-diffs" style="color:<?php echo $diffs > 0 ? '#dc3545' : '#28a745'; ?>;">
<?php $diffs_cls = $diffs > 0 ? 'wpdo-eb-shadow-warn' : 'wpdo-text-green-bold'; ?>
<strong class="wpdo-shadow-diffs <?php echo esc_attr( $diffs_cls ); ?>">
<?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>
<span class="wpdo-text-muted"> — <?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>
<span class="wpdo-eb-shadow-ok"> ✓ <?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;">
<div class="wpdo-eb-promote-row">
<?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 $ap_cls = ! empty( $ap['eligible'] ) ? 'wpdo-eb-auto-ok' : 'wpdo-eb-auto-warn'; ?>
<span class="wpdo-auto-promote-eligible <?php echo esc_attr( $ap_cls ); ?>">
<?php if ( ! empty( $ap['eligible'] ) ) : ?>
✓ <?php esc_html_e( '可升級', '2meet-data-optimizer' ); ?>
<?php else : ?>
@@ -1729,7 +1722,7 @@ class TMDO_Admin {
<?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;">
<span class="wpdo-eb-auto-badge">
<?php esc_html_e( '自動', '2meet-data-optimizer' ); ?>
</span>
<?php endif; ?>
@@ -1738,14 +1731,13 @@ class TMDO_Admin {
<!-- 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;">
<div class="wpdo-recommendation">
<?php echo esc_html( $rec ); ?>
</div>
<?php endif; ?>
<!-- Action buttons -->
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<div class="wpdo-eb-button-row">
<button type="button"
class="button wpdo-btn-promote"
data-wpdo-action="promote"
@@ -1756,25 +1748,25 @@ class TMDO_Admin {
↑ <?php esc_html_e( '升級模式', '2meet-data-optimizer' ); ?>
</button>
<button type="button"
class="button wpdo-btn-demote"
class="button wpdo-btn-demote <?php echo $prev_mode ? '' : 'wpdo-eb-btn-dim'; ?>"
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;'; ?>">
class="<?php echo $prev_mode ? '' : 'wpdo-eb-btn-dim'; ?>">
↓ <?php esc_html_e( '降級模式', '2meet-data-optimizer' ); ?>
</button>
</div>
</div><!-- .wpdo-card -->
</div><!-- .wpdo-eb-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;">
<div class="wpdo-eb-footer">
<h3 class="wpdo-mt-0"><?php esc_html_e( '遷移路徑說明', '2meet-data-optimizer' ); ?></h3>
<ol class="wpdo-eb-ol">
<li>
<strong>disabled → dual_write</strong>
<?php esc_html_e( 'Hook Bus 開始對 flat table 雙寫(讀取仍走原生 EAV)。此步驟安全,可隨時回退。', '2meet-data-optimizer' ); ?>
@@ -1796,7 +1788,7 @@ class TMDO_Admin {
<?php esc_html_e( '所有讀寫均走 flat table,原生 EAV 不再寫入。效能最佳,適合生產環境。', '2meet-data-optimizer' ); ?>
</li>
</ol>
<p class="description" style="margin-top:8px;">
<p class="description wpdo-mt-1">
<?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' ); ?>
@@ -1980,7 +1972,7 @@ class TMDO_Admin {
<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;">
<div class="wpdo-notice-error">
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
<?php esc_html_e( '它會建立大量測試使用者並填滿所有 user 相關 flat tableshot / cold / membership / activity / profile / sso / points_ledger)。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
</div>
@@ -1989,15 +1981,15 @@ class TMDO_Admin {
<?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;">
<div class="wpdo-grid-2col">
<!-- 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>
<div class="wpdo-eb-card">
<h3 class="wpdo-mt-0"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
<table class="form-table" style="margin-top:0;">
<table class="form-table wpdo-form-table-mt0">
<tr>
<th style="width:35%;"><label for="wpdo-st-target"><?php esc_html_e( '要建立的使用者數', '2meet-data-optimizer' ); ?></label></th>
<th class="wpdo-th-main"><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>
@@ -2006,11 +1998,11 @@ class TMDO_Admin {
<tr>
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
<td>
<label style="display:block;margin-bottom:6px;">
<label class="wpdo-label-block">
<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;">
<label>
<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>
@@ -2036,11 +2028,11 @@ class TMDO_Admin {
</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>
<div class="wpdo-eb-card">
<h3 class="wpdo-mt-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;">
<strong id="wpdo-st-count" class="wpdo-count-danger">
<?php echo esc_html( number_format( $test_user_count ) ); ?>
</strong>
</p>
@@ -2053,7 +2045,7 @@ class TMDO_Admin {
</button>
</p>
<hr style="margin:18px 0;" />
<hr class="wpdo-hr-section" />
<p>
<button type="button" class="button" id="wpdo-st-rerun-bench" <?php disabled( $is_running || 0 === $test_user_count ); ?>>
@@ -2067,42 +2059,42 @@ class TMDO_Admin {
</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;">
<div id="wpdo-st-progress-card" class="wpdo-eb-card wpdo-mt-3<?php echo $is_running ? '' : ' wpdo-hidden'; ?>">
<h3 class="wpdo-mt-0"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
<div class="wpdo-bar-section">
<div class="wpdo-bar-header">
<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>
<span id="wpdo-st-pg-pct" class="wpdo-bar-pct"><?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 class="wpdo-bar-track">
<div id="wpdo-st-pg-bar" class="wpdo-bar-fill" style="--wpdo-bar-width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%"></div>
</div>
</div>
<table style="width:100%;font-size:13px;margin-top:10px;border-collapse:collapse;">
<table class="wpdo-stats-table">
<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>
<td class="wpdo-td-muted"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td-val"><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 class="wpdo-td-muted"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td-val"><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>
<td class="wpdo-td-muted"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-st-pg-elapsed"><?php echo esc_html( $state['elapsed_sec'] ?? 0 ); ?></span> 秒</td>
<td class="wpdo-td-muted"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><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>
<td class="wpdo-td-muted"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-st-pg-batches"><?php echo esc_html( $state['batches_done'] ?? 0 ); ?></span></td>
<td class="wpdo-td-muted"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><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-card" class="wpdo-eb-card wpdo-mt-3<?php echo ! empty( $state['benchmark'] ) ? '' : ' wpdo-hidden'; ?>">
<h3 class="wpdo-mt-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'] ); ?>
@@ -2124,8 +2116,8 @@ class TMDO_Admin {
$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;">
<h4 class="wpdo-h4-mb">▍ <?php esc_html_e( '寫入指標', '2meet-data-optimizer' ); ?></h4>
<table class="widefat wpdo-table-mb">
<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>
@@ -2137,8 +2129,8 @@ class TMDO_Admin {
</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;">
<h4 class="wpdo-h4-mb">▍ <?php esc_html_e( 'DB 容量(user 相關表)', '2meet-data-optimizer' ); ?></h4>
<table class="widefat striped wpdo-table-mb">
<thead>
<tr>
<th><?php esc_html_e( 'Table', '2meet-data-optimizer' ); ?></th>
@@ -2163,8 +2155,8 @@ class TMDO_Admin {
</tbody>
</table>
<h4 style="margin-bottom:6px;">▍ <?php esc_html_e( '查詢效能', '2meet-data-optimizer' ); ?></h4>
<table class="widefat striped" style="margin-bottom:8px;">
<h4 class="wpdo-h4-mb">▍ <?php esc_html_e( '查詢效能', '2meet-data-optimizer' ); ?></h4>
<table class="widefat striped wpdo-table-mb-sm">
<thead>
<tr>
<th><?php esc_html_e( '測試項目', '2meet-data-optimizer' ); ?></th>
@@ -2259,7 +2251,7 @@ class TMDO_Admin {
}
}
?>
<hr style="margin:2em 0">
<hr>
<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' ); ?>
@@ -2276,37 +2268,31 @@ class TMDO_Admin {
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';
$e_mode_cls = 'wpdo-badge-mode wpdo-mode-' . str_replace( '_', '-', $e_mode );
?>
<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 ); ?>">
<span class="<?php echo esc_attr( $e_mode_cls ); ?>">
<?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">
<h4 class="wpdo-h4-mb">
<?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>';
echo '<span class="wpdo-text-green wpdo-code-xs"> ✓ ' . esc_html( $e_tbl ) . '</span>';
} else {
echo '<span style="color:#856404;font-size:12px"> ⏳ ' . esc_html__( '表格尚未建立', '2meet-data-optimizer' ) . '</span>';
echo '<span class="wpdo-text-olive wpdo-code-xs"> ⏳ ' . esc_html__( '表格尚未建立', '2meet-data-optimizer' ) . '</span>';
}
}
?>
</h4>
<table class="widefat striped wpdo-table" style="margin-bottom:1.5em">
<table class="widefat striped wpdo-table wpdo-table-mb">
<thead>
<tr>
<th><?php esc_html_e( 'Meta Key', '2meet-data-optimizer' ); ?></th>
@@ -2735,6 +2721,14 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
* HPCT Import tab: preview and execute import.
*/
private static function render_hpct_import(): void {
// HPCT is a HivePress-family plugin; the importer ships in that AddOn.
if ( ! class_exists( 'TMDO_HPCT_Import' ) ) {
echo '<div class="notice notice-warning"><p>'
. esc_html__( 'HPCT 匯入需要啟用 2meet-data-optimizer-hivepress-addon。', '2meet-data-optimizer' )
. '</p></div>';
return;
}
// Handle import action.
if ( isset( $_POST['wpdo_run_import'] ) && check_admin_referer( 'wpdo_hpct_import' ) ) {
$result = TMDO_HPCT_Import::run();
@@ -2873,12 +2867,12 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
</p>
<p>
<form method="post" style="display:inline">
<form method="post" class="wpdo-form-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">
<form method="post" class="wpdo-form-inline">
<input type="hidden" name="wpdo_prune_snapshots" value="1">
<?php wp_nonce_field( 'wpdo_prune_snapshots' ); ?>
<button type="submit" class="button"
@@ -2913,7 +2907,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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">
<form method="post" class="wpdo-form-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"
@@ -2953,16 +2947,16 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
$summary = TMDO_Conflict_Monitor::get_summary();
$total = (int) ( $summary['total'] ?? 0 );
?>
<table class="widefat striped" style="max-width:600px;">
<table class="widefat striped wpdo-max-600">
<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>
<span class="wpdo-text-ok">●</span>
<?php esc_html_e( '無衝突', '2meet-data-optimizer' ); ?>
<?php else : ?>
<span style="color:#dc3232;font-weight:bold;">●</span>
<span class="wpdo-text-err">●</span>
<?php echo esc_html( (string) $total ); ?>
<?php endif; ?>
</td>
@@ -2979,7 +2973,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
</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;">
<pre class="wpdo-pre-code">
<?php
echo esc_html( wp_json_encode( $summary, JSON_PRETTY_PRINT ) );
?>
@@ -3044,7 +3038,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
?>
</tbody>
</table>
<p class="description" style="margin-top:1em;">
<p class="description wpdo-description-mt">
<?php esc_html_e( '結果由 5 分鐘 transient 快取;若剛操作完想立即重檢,請點刷新或等下一個快取週期。', '2meet-data-optimizer' ); ?>
</p>
<?php
@@ -3137,7 +3131,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
$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>',
'<tr><td><code>%s</code></td><td><div class="wpdo-advisor-bar-track" style="--wpdo-adv-color:%s;--wpdo-adv-width:%d%%"><div class="wpdo-advisor-bar-fill"></div><span class="wpdo-advisor-bar-label">%s</span></div></td>',
esc_html( $module ),
esc_attr( $color ),
(int) $bar_w,
@@ -3152,7 +3146,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
}
echo '</td>';
printf( '<td><code>%s</code></td>', esc_html( (string) $r['current_state'] ) );
echo '<td><form method="post" style="display:inline">';
echo '<td><form method="post" class="wpdo-form-inline">';
printf( '<input type="hidden" name="wpdo_enable_module" value="%s">', esc_attr( $module ) );
wp_nonce_field( 'wpdo_enable_module' );
printf(
@@ -3215,7 +3209,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
echo '</tbody></table></details>';
}
echo '<p class="description" style="margin-top:1em;">' . esc_html__( '結果由 1 小時 transient 快取;每日健康檢查 cron 也會自動更新。', '2meet-data-optimizer' ) . '</p>';
echo '<p class="description wpdo-description-mt">' . esc_html__( '結果由 1 小時 transient 快取;每日健康檢查 cron 也會自動更新。', '2meet-data-optimizer' ) . '</p>';
}
/**
@@ -3279,8 +3273,8 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
$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 '<details' . ( $ch_enabled ? ' open' : '' ) . ' class="wpdo-details-section">';
echo '<summary class="wpdo-summary-section">' . 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>',
@@ -3341,7 +3335,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
</div>
</div><!-- /.wpdo-settings-section -->
<div class="wpdo-settings-section" data-section="entity-bridge">
<h3 style="margin-top: 2em;"><?php esc_html_e( '⚡ Entity Bridgev2.5.4 / post v2.11.0', '2meet-data-optimizer' ); ?></h3>
<h3 class="wpdo-h3-section"><?php esc_html_e( '⚡ Entity Bridgev2.5.4 / post v2.11.0', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( 'Hook Bus 攔截 WordPress 原生 metadata filter,將 user / post / term / comment meta 路由至扁平化資料表。', '2meet-data-optimizer' ); ?><br>
<?php esc_html_e( '模式說明:', '2meet-data-optimizer' ); ?>
@@ -3389,7 +3383,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
</option>
<?php endforeach; ?>
</select>
<p class="description" style="margin-top:0.25em;">
<p class="description wpdo-description-sm">
<?php
if ( class_exists( 'TMDO_Mode_Manager' ) ) {
echo esc_html( TMDO_Mode_Manager::description( $current_mode ) );
@@ -3423,7 +3417,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
</div>
</div><!-- /.wpdo-settings-section -->
<div class="wpdo-settings-section" data-section="hp-transient">
<h3 style="margin-top: 2em;"><?php esc_html_e( '🌿 HivePress Transient Filterv2.11.5', '2meet-data-optimizer' ); ?></h3>
<h3 class="wpdo-h3-section"><?php esc_html_e( '🌿 HivePress Transient Filterv2.11.5', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( 'HivePress 內部用 update_post_meta($post_id, \'_transient_<name>\', $value) 把 TTL cache 寫進 wp_postmeta(每個 hp_listing publish 觸發 8-16 個 transient row)。本 filter 在 metadata 層攔截並重新路由到 wp_optionsnative transient API),HivePress 完全無感,wp_postmeta 保持乾淨。Filter 是 metadata 層運作,不依賴 mode promote。', '2meet-data-optimizer' ); ?>
</p>
@@ -3436,7 +3430,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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;">
<p class="description wpdo-description-xs">
<?php esc_html_e( '停用此選項會讓未來新建的 hp_listing 重新把 transient cache 寫進 wp_postmeta(不建議,除非要 debug HivePress 行為)。', '2meet-data-optimizer' ); ?>
</p>
</td>
@@ -3444,14 +3438,14 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<tr>
<th scope="row"><?php esc_html_e( '當前狀態', '2meet-data-optimizer' ); ?></th>
<td>
<table class="widefat striped" style="max-width:600px;">
<table class="widefat striped wpdo-max-600">
<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>
<span class="wpdo-text-green-bold">✓ <?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>
<span class="wpdo-text-red-bold">✗ <?php esc_html_e( '已停用', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
@@ -3459,11 +3453,11 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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>
<strong class="wpdo-text-red-bold"><?php echo esc_html( number_format_i18n( $hp_legacy_count ) ); ?></strong> rows
<code class="wpdo-code-sm">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>
<strong class="wpdo-text-green-bold">0 rows ✓</strong>
<span class="wpdo-text-muted wpdo-ml-2"><?php esc_html_e( '(已清乾淨)', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
@@ -3471,14 +3465,14 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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;">
<span class="wpdo-text-muted wpdo-ml-2">
<?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;">
<p class="description wpdo-notice-warn-desc">
⚠️
<?php
printf(
@@ -3519,7 +3513,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
</div>
</div><!-- /.wpdo-settings-section -->
<div class="wpdo-settings-section" data-section="garbage-filter">
<h3 style="margin-top: 2em;"><?php esc_html_e( '🗑 Term + Comment Garbage Filterv2.12.1', '2meet-data-optimizer' ); ?></h3>
<h3 class="wpdo-h3-section"><?php esc_html_e( '🗑 Term + Comment Garbage Filterv2.12.1', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( '攔截 wp_termmeta / wp_commentmeta 已知垃圾 keys 的寫入並 silent drop_wxr_import_* WP 匯入殘留、_2meet_demo_* demo 標記、commentmeta 中 8 個誤寫的 post-domain orphan keys)。Phase 0 cleanup CLI 清歷史,本 filter 防再次累積。Read 路徑不攔截(向後相容)。', '2meet-data-optimizer' ); ?>
</p>
@@ -3532,7 +3526,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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;">
<p class="description wpdo-description-xs">
<?php esc_html_e( '⚠️ 跑 WXR import 工具前可暫時停用本 filter_wxr_import_* 為 importer 內部追蹤),完成後再啟用 + 跑 cleanup CLI。一般 production 維持啟用。', '2meet-data-optimizer' ); ?>
</p>
</td>
@@ -3540,14 +3534,14 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<tr>
<th scope="row"><?php esc_html_e( '當前狀態', '2meet-data-optimizer' ); ?></th>
<td>
<table class="widefat striped" style="max-width:600px;">
<table class="widefat striped wpdo-max-600">
<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>
<span class="wpdo-text-green-bold">✓ <?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>
<span class="wpdo-text-red-bold">✗ <?php esc_html_e( '已停用', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
@@ -3555,7 +3549,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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;">
<span class="wpdo-text-muted wpdo-ml-2">
<?php esc_html_e( 'rolling 24h,自動重置)', '2meet-data-optimizer' ); ?>
</span>
</td>
@@ -3564,10 +3558,10 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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>
<strong class="wpdo-text-red-bold"><?php echo esc_html( number_format_i18n( $term_garbage_count ) ); ?></strong> rows
<code class="wpdo-code-sm">wp wpdo termmeta-cleanup --confirm</code>
<?php else : ?>
<strong style="color:#28a745;">0 rows ✓</strong>
<strong class="wpdo-text-green-bold">0 rows ✓</strong>
<?php endif; ?>
</td>
</tr>
@@ -3575,10 +3569,10 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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>
<strong class="wpdo-text-red-bold"><?php echo esc_html( number_format_i18n( $comment_garbage_count ) ); ?></strong> rows
<code class="wpdo-code-sm">wp wpdo commentmeta-cleanup --confirm</code>
<?php else : ?>
<strong style="color:#28a745;">0 rows ✓</strong>
<strong class="wpdo-text-green-bold">0 rows ✓</strong>
<?php endif; ?>
</td>
</tr>
@@ -3605,7 +3599,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
</div>
</div><!-- /.wpdo-settings-section -->
<div class="wpdo-settings-section" data-section="wc-term-count">
<h3 style="margin-top: 2em;"><?php esc_html_e( '🛒 WooCommerce Term Count Filterv2.12.3', '2meet-data-optimizer' ); ?></h3>
<h3 class="wpdo-h3-section"><?php esc_html_e( '🛒 WooCommerce Term Count Filterv2.12.3', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( '攔截 WooCommerce 寫入 wp_termmeta 的 product_count_<taxonomy> cache rows,重新路由到 wp_optionsnative transient 結構)。WC 自身 cache 失效邏輯不變(每次新增/刪除 product 時會重算寫入),僅儲存位置改變。Read 路徑亦會優先從 wp_options 讀回,cache miss 才 fall-through 至 wp_termmeta(向後相容)。', '2meet-data-optimizer' ); ?>
</p>
@@ -3623,14 +3617,14 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<tr>
<th scope="row"><?php esc_html_e( '當前狀態', '2meet-data-optimizer' ); ?></th>
<td>
<table class="widefat striped" style="max-width:600px;">
<table class="widefat striped wpdo-max-600">
<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>
<span class="wpdo-text-green-bold">✓ <?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>
<span class="wpdo-text-red-bold">✗ <?php esc_html_e( '已停用', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
@@ -3638,9 +3632,9 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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
<strong class="wpdo-text-red-bold"><?php echo esc_html( number_format_i18n( $wctc_legacy ) ); ?></strong> rows
<?php else : ?>
<strong style="color:#28a745;">0 rows ✓</strong>
<strong class="wpdo-text-green-bold">0 rows ✓</strong>
<?php endif; ?>
</td>
</tr>
@@ -3648,7 +3642,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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>
<span class="wpdo-text-muted wpdo-ml-2"><?php esc_html_e( 'filter 已成功路由的 product_count cache', '2meet-data-optimizer' ); ?></span>
</td>
</tr>
</table>
@@ -3673,7 +3667,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
</div>
</div><!-- /.wpdo-settings-section -->
<div class="wpdo-settings-section" data-section="misc-bucket">
<h3 style="margin-top: 2em;"><?php esc_html_e( '📦 Term + Comment Misc Bucketv2.12.4', '2meet-data-optimizer' ); ?></h3>
<h3 class="wpdo-h3-section"><?php esc_html_e( '📦 Term + Comment Misc Bucketv2.12.4', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( 'Catch-all flat 表,捕捉所有未被前面 filter 處理的 term/comment meta keyspriority 99,整條 filter chain 的最後一棒)。讓 wp_termmeta / wp_commentmeta 完全可避開(v3.0.0 DROP 前置條件)。寫入 wp_wpdo_term_misc / wp_wpdo_comment_misc 兩張 K/V 表,PRIMARY KEY (entity_id, meta_key)。讀取 cache miss 時 fall-through 至 wp_*meta 維持向後相容。', '2meet-data-optimizer' ); ?>
</p>
@@ -3686,7 +3680,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<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;">
<p class="description wpdo-description-xs">
<?php esc_html_e( '⚠️ 停用此選項時,未註冊 keys 會回到 wp_termmeta / wp_commentmeta。Phase 5 升 mode 前必須維持啟用。', '2meet-data-optimizer' ); ?>
</p>
</td>
@@ -3694,14 +3688,14 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
<tr>
<th scope="row"><?php esc_html_e( '當前狀態', '2meet-data-optimizer' ); ?></th>
<td>
<table class="widefat striped" style="max-width:600px;">
<table class="widefat striped wpdo-max-600">
<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>
<span class="wpdo-text-green-bold">✓ <?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>
<span class="wpdo-text-red-bold">✗ <?php esc_html_e( '已停用', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
@@ -3725,7 +3719,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
</div>
</div><!-- /.wpdo-settings-section -->
<div class="wpdo-settings-section" data-section="automator">
<h3 style="margin-top: 2em;"><?php esc_html_e( '🤖 FSM Automatorv2.5.0 M13', '2meet-data-optimizer' ); ?></h3>
<h3 class="wpdo-h3-section"><?php esc_html_e( '🤖 FSM Automatorv2.5.0 M13', '2meet-data-optimizer' ); ?></h3>
<p class="description"><?php esc_html_e( '預設關閉。啟用後,每日 04:30 UTC 跑一次,自動執行 FSM Advisor 標記為 PROMOTE 的 module 推進。Destructive 轉態(verify→cutover、cutover→cleanup、cleanup→complete)即使 enabled 也永不自動。', '2meet-data-optimizer' ); ?></p>
<table class="form-table" role="presentation">
<tbody>
@@ -3745,10 +3739,10 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'
$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;">';
echo '<div class="wpdo-details-section">';
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>',
'<label class="wpdo-label-inline"><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 )
+12 -12
View File
@@ -79,7 +79,7 @@ class TMDO_Dashboard_Widget {
.wpdo-widget-actions a { margin-right: 0.5em; }
</style>
<p style="font-size: 1.1em; margin-bottom: 0.6em;">
<p class="wpdo-widget-intro">
<span class="wpdo-widget-light <?php echo esc_attr( $light['class'] ); ?>"></span>
<strong><?php echo esc_html( $light['label'] ); ?></strong>
<?php if ( $status['streak'] > 0 ) : ?>
@@ -95,7 +95,7 @@ class TMDO_Dashboard_Widget {
</p>
<?php if ( '' !== $status['summary_msg'] ) : ?>
<p class="description" style="margin-bottom: 0.8em;"><?php echo esc_html( $status['summary_msg'] ); ?></p>
<p class="description wpdo-mb-2"><?php echo esc_html( $status['summary_msg'] ); ?></p>
<?php endif; ?>
<div class="wpdo-widget-stats">
@@ -113,7 +113,7 @@ class TMDO_Dashboard_Widget {
<strong><?php echo esc_html( (string) (int) $status['conflicts'] ); ?></strong>
<?php if ( (int) $status['conflicts'] > 0 ) : ?>
<a href="<?php echo esc_url( add_query_arg( 'tab', 'conflicts', $page_url ) ); ?>"
style="margin-left:0.5em;"><?php esc_html_e( '查看', '2meet-data-optimizer' ); ?></a>
class="wpdo-mt-1"><?php esc_html_e( '查看', '2meet-data-optimizer' ); ?></a>
<?php endif; ?>
</span>
</div>
@@ -140,7 +140,7 @@ class TMDO_Dashboard_Widget {
if ( $actionable_count > 0 ) :
$ms_url = add_query_arg( 'tab', 'module-suggestions', $page_url );
?>
<p style="margin: 0.6em 0; padding: 0.5em 0.7em; background: #fff8e5; border-left: 3px solid #dba617; border-radius: 3px;">
<p class="wpdo-widget-notice wpdo-widget-notice-warn">
🤖
<?php
printf(
@@ -162,7 +162,7 @@ class TMDO_Dashboard_Widget {
if ( ! empty( $attn['needs'] ) && 'running' !== ( $attn['job_state'] ?? '' ) ) :
$wizard_url = add_query_arg( 'tab', 'migration-wizard', $page_url );
?>
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: #fef0f0; border-left: 3px solid #dc3232; border-radius: 3px;">
<p class="wpdo-widget-notice wpdo-widget-notice-error">
🔴
<?php
/* translators: 1: EAV row count, 2: group count, 3: ratio. */
@@ -177,7 +177,7 @@ class TMDO_Dashboard_Widget {
<a href="<?php echo esc_url( $wizard_url ); ?>"><strong><?php esc_html_e( 'User 遷移精靈 →', '2meet-data-optimizer' ); ?></strong></a>
</p>
<?php elseif ( 'running' === ( $attn['job_state'] ?? '' ) ) : ?>
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: #e5f5fa; border-left: 3px solid #00a0d2; border-radius: 3px;">
<p class="wpdo-widget-notice wpdo-widget-notice-info">
⏳ <?php esc_html_e( 'User 遷移精靈正在執行中 —', '2meet-data-optimizer' ); ?>
<a href="<?php echo esc_url( add_query_arg( 'tab', 'migration-wizard', $page_url ) ); ?>"><?php esc_html_e( '查看進度', '2meet-data-optimizer' ); ?></a>
</p>
@@ -195,7 +195,7 @@ class TMDO_Dashboard_Widget {
number_format_i18n( (int) $gc['total'] )
);
?>
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: #f6f7f7; border-left: 3px solid #2271b1; border-radius: 3px;">
<p class="wpdo-widget-notice wpdo-widget-notice-neutral">
🧹
<?php
/* translators: 1: total rows, 2: transients, 3: wp_old_date, 4: edit_locks */
@@ -208,7 +208,7 @@ class TMDO_Dashboard_Widget {
esc_html( number_format_i18n( (int) $gc['edit_locks'] ) )
);
?>
<form method="post" action="<?php echo esc_url( $page_url ); ?>" style="display:inline">
<form method="post" action="<?php echo esc_url( $page_url ); ?>" class="wpdo-form-inline">
<input type="hidden" name="wpdo_postmeta_cleanup" value="1">
<?php wp_nonce_field( 'wpdo_postmeta_cleanup' ); ?>
<button type="submit" class="button-link"
@@ -246,7 +246,7 @@ class TMDO_Dashboard_Widget {
'%2$s'
);
?>
<p style="margin: 0.6em 0; padding: 0.6em 0.8em; background: <?php echo esc_attr( $post_bg ); ?>; border-left: 3px solid <?php echo esc_attr( $post_color ); ?>; border-radius: 3px;">
<p class="wpdo-widget-notice <?php echo $total_eav > 0 ? 'wpdo-widget-notice-warn' : 'wpdo-widget-notice-ok'; ?>">
📦
<?php
if ( $total_eav > 0 ) {
@@ -276,7 +276,7 @@ class TMDO_Dashboard_Widget {
<?php endif; ?>
<div class="wpdo-widget-actions">
<form method="post" action="<?php echo esc_url( $page_url ); ?>" style="display:inline">
<form method="post" action="<?php echo esc_url( $page_url ); ?>" class="wpdo-form-inline">
<input type="hidden" name="wpdo_run_health" value="1">
<?php wp_nonce_field( 'wpdo_run_health' ); ?>
<button type="submit" class="button button-small button-primary"><?php esc_html_e( '跑健康檢查', '2meet-data-optimizer' ); ?></button>
@@ -284,12 +284,12 @@ class TMDO_Dashboard_Widget {
<a class="button button-small" href="<?php echo esc_url( $bridge_url ); ?>">
<?php esc_html_e( 'Entity Bridge', '2meet-data-optimizer' ); ?>
</a>
<form method="post" action="<?php echo esc_url( $page_url ); ?>" style="display:inline">
<form method="post" action="<?php echo esc_url( $page_url ); ?>" class="wpdo-form-inline">
<input type="hidden" name="wpdo_create_snapshot" value="1">
<?php wp_nonce_field( 'wpdo_create_snapshot' ); ?>
<button type="submit" class="button button-small"><?php esc_html_e( '建立快照', '2meet-data-optimizer' ); ?></button>
</form>
<a href="<?php echo esc_url( $page_url ); ?>" style="float: right; padding-top: 4px;">
<a href="<?php echo esc_url( $page_url ); ?>" class="wpdo-widget-link-right">
<?php esc_html_e( '完整儀表板 →', '2meet-data-optimizer' ); ?>
</a>
</div>
+7 -7
View File
@@ -106,7 +106,7 @@ class TMDO_Help_Tabs {
*/
private static function content_dashboard(): string {
return '<p>' . esc_html__( '儀表板顯示:', '2meet-data-optimizer' ) . '</p>'
. '<ul style="list-style: disc; padding-left: 1.5em;">'
. '<ul class="wpdo-list-disc">'
. '<li>' . esc_html__( 'System Overview — DB 引擎、HivePress、HPCT、Object Cache 是否啟用', '2meet-data-optimizer' ) . '</li>'
. '<li>' . esc_html__( 'Zone 行數統計 — Hot/Warm/Cold/Archive 各自累積多少資料', '2meet-data-optimizer' ) . '</li>'
. '<li>' . esc_html__( 'Module 狀態 — 每個 module 在 7-state FSM 哪一格', '2meet-data-optimizer' ) . '</li>'
@@ -123,7 +123,7 @@ class TMDO_Help_Tabs {
*/
private static function content_zones(): string {
return '<p>' . esc_html__( '4 個 Zone 對應不同存取頻率與保留需求:', '2meet-data-optimizer' ) . '</p>'
. '<ul style="list-style: disc; padding-left: 1.5em;">'
. '<ul class="wpdo-list-disc">'
. '<li><strong>Hot</strong> — ' . esc_html__( '高頻索引欄位,如 listing 的 price / location。獨立 column + indexWP_Query 可 JOIN。', '2meet-data-optimizer' ) . '</li>'
. '<li><strong>Warm</strong> — ' . esc_html__( 'TTL 暫存(如 view count、cache stats)。固定表 wp_wpdo_warm 含 expires_at。', '2meet-data-optimizer' ) . '</li>'
. '<li><strong>Cold</strong> — ' . esc_html__( '低頻 metasettings / preferences)。讀寫透過 interceptor 攔截後保持 EAV 形式。', '2meet-data-optimizer' ) . '</li>'
@@ -139,7 +139,7 @@ class TMDO_Help_Tabs {
*/
private static function content_classifier(): string {
return '<p>' . esc_html__( 'Classifier 分析 wp_postmeta 給每個 meta_key 一個 zone 建議:', '2meet-data-optimizer' ) . '</p>'
. '<ul style="list-style: disc; padding-left: 1.5em;">'
. '<ul class="wpdo-list-disc">'
. '<li><strong>Confidence</strong> — ' . esc_html__( '0.0~1.0,越高代表分類越確定。≥ 0.8 可放心採納,< 0.5 建議 manual review。', '2meet-data-optimizer' ) . '</li>'
. '<li><strong>Reasons</strong> — ' . esc_html__( '說明為什麼建議這個 zoneaccess frequency / row count / TTL hints)。', '2meet-data-optimizer' ) . '</li>'
. '<li><strong>Already-assigned</strong> — ' . esc_html__( '已透過 Schema_Registry 註冊的 meta_key 數量。', '2meet-data-optimizer' ) . '</li>'
@@ -153,7 +153,7 @@ class TMDO_Help_Tabs {
*/
private static function content_snapshots(): string {
return '<p>' . esc_html__( '快照保留政策(v2.2.0):', '2meet-data-optimizer' ) . '</p>'
. '<ul style="list-style: disc; padding-left: 1.5em;">'
. '<ul class="wpdo-list-disc">'
. '<li>' . esc_html__( '預設 30 天 TTL,可在 wp wpdo snapshot create 時用 --retention-days 覆蓋。', '2meet-data-optimizer' ) . '</li>'
. '<li>' . esc_html__( 'pre_uninstall / pre_v2_upgrade triggers 受 size-cap 保護(不會被自動 evict)。', '2meet-data-optimizer' ) . '</li>'
. '<li>' . esc_html__( '檔案存於 wp-content/uploads/wpdo-backups/,含 .htaccess deny all + 每個檔 sha256 校驗。', '2meet-data-optimizer' ) . '</li>'
@@ -171,7 +171,7 @@ class TMDO_Help_Tabs {
private static function content_conflicts(): string {
return '<p>' . esc_html__( 'Hook 衝突偵測:當多個 plugin 在同一 WordPress 的 metadata filter 上掛 callback 時,可能造成資料寫入順序不確定 / 重複處理。', '2meet-data-optimizer' ) . '</p>'
. '<p>' . esc_html__( '常見原因:', '2meet-data-optimizer' ) . '</p>'
. '<ul style="list-style: disc; padding-left: 1.5em;">'
. '<ul class="wpdo-list-disc">'
. '<li>' . esc_html__( 'Hook Bus 啟用(wpdo_hook_bus_enabled = 1+ legacy interceptors 還沒卸載', '2meet-data-optimizer' ) . '</li>'
. '<li>' . esc_html__( 'HPCT (HP Custom Tables) plugin 還沒移除 — 與 WPDO 同時攔截', '2meet-data-optimizer' ) . '</li>'
. '</ul>'
@@ -185,7 +185,7 @@ class TMDO_Help_Tabs {
*/
private static function content_doctor(): string {
return '<p>' . esc_html__( '7 項自動健康檢查:', '2meet-data-optimizer' ) . '</p>'
. '<ol style="padding-left: 1.5em;">'
. '<ol class="wpdo-list-disc">'
. '<li><strong>schema_drift</strong> — ' . esc_html__( '所有 v2 表是否存在', '2meet-data-optimizer' ) . '</li>'
. '<li><strong>error_budget</strong> — ' . esc_html__( '7 天內 wp_wpdo_errors 行數', '2meet-data-optimizer' ) . '</li>'
. '<li><strong>hook_conflicts</strong> — ' . esc_html__( '同上 conflicts tab', '2meet-data-optimizer' ) . '</li>'
@@ -204,7 +204,7 @@ class TMDO_Help_Tabs {
*/
private static function content_logs(): string {
return '<p>' . esc_html__( '日誌讀取:', '2meet-data-optimizer' ) . '</p>'
. '<ul style="list-style: disc; padding-left: 1.5em;">'
. '<ul class="wpdo-list-disc">'
. '<li>' . esc_html__( '每筆對應 wp_wpdo_errors 一行:module / zone / hook / message / timestamp。', '2meet-data-optimizer' ) . '</li>'
. '<li>' . esc_html__( '預設保留 90 天(wpdo_errors_gc daily cron 自動清)。', '2meet-data-optimizer' ) . '</li>'
. '<li>' . esc_html__( '看 message 開頭 [WARN] 是 warning level(不影響運作但需注意)。', '2meet-data-optimizer' ) . '</li>'
+25 -25
View File
@@ -128,7 +128,7 @@ class TMDO_Setup_Wizard {
$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" style="max-width: 800px;">
<div class="wrap wpdo-max-800">
<p>
<a href="<?php echo esc_url( admin_url( 'tools.php?page=wp-data-optimizer' ) ); ?>">
&larr; <?php esc_html_e( '返回 WP Data Optimizer', '2meet-data-optimizer' ); ?>
@@ -137,11 +137,11 @@ class TMDO_Setup_Wizard {
<h1><?php esc_html_e( 'WP Data Optimizer — 設定嚮導', '2meet-data-optimizer' ); ?></h1>
<?php if ( $completed ) : ?>
<div style="background:#f0f6fc; border-left:4px solid #2271b1; padding:0.75em 1em; margin-bottom:1em; display:flex; align-items:center; gap:1em;">
<div class="wpdo-setup-banner">
<span>✅ <?php esc_html_e( '嚮導已完成。你可以重新瀏覽任何步驟,或重設為全新執行。', '2meet-data-optimizer' ); ?></span>
<a class="button button-secondary"
<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 ) ); ?>"
style="white-space:nowrap;">
>
<?php esc_html_e( '重新執行精靈', '2meet-data-optimizer' ); ?>
</a>
</div>
@@ -154,17 +154,17 @@ class TMDO_Setup_Wizard {
<?php esc_html_e( '步', '2meet-data-optimizer' ); ?>
</p>
<div style="background: #f0f0f1; height: 8px; border-radius: 4px; overflow: hidden; margin-bottom: 2em;">
<div style="background: #2271b1; height: 100%; width: <?php echo (int) ( $step / self::TOTAL_STEPS * 100 ); ?>%; transition: width 0.3s;"></div>
<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" style="max-width: none; padding: 2em;">
<div class="card wpdo-setup-card-full">
<?php call_user_func( array( __CLASS__, 'render_step_' . $step ) ); ?>
</div>
<p style="margin-top: 2em;">
<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 ) ); ?>"
style="color: #888; font-size: 0.9em;">
class="wpdo-text-skip">
<?php esc_html_e( '我熟悉了,跳過嚮導', '2meet-data-optimizer' ); ?>
</a>
</p>
@@ -185,7 +185,7 @@ class TMDO_Setup_Wizard {
<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" style="margin-bottom: 1em;">
<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>
@@ -198,13 +198,13 @@ class TMDO_Setup_Wizard {
</table>
<h3><?php esc_html_e( '何時需要 WPDO', '2meet-data-optimizer' ); ?></h3>
<ul style="list-style: disc; padding-left: 1.5em;">
<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' ) ); ?>" style="margin-top: 2em;">
<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>
@@ -224,18 +224,18 @@ class TMDO_Setup_Wizard {
$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" style="max-width: 600px; margin-bottom: 1em;">
<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 style="color: #46b450; font-weight: bold;">✅ <?php esc_html_e( '強烈建議', '2meet-data-optimizer' ); ?></span>
<span class="wpdo-text-success-em">✅ <?php esc_html_e( '強烈建議', '2meet-data-optimizer' ); ?></span>
<?php elseif ( $pm_count > 100_000 ) : ?>
<span style="color: #dba617; font-weight: bold;">🟡 <?php esc_html_e( '可以考慮', '2meet-data-optimizer' ); ?></span>
<span class="wpdo-text-warn-em">🟡 <?php esc_html_e( '可以考慮', '2meet-data-optimizer' ); ?></span>
<?php else : ?>
<span style="color: #8c8f94;">⏸ <?php esc_html_e( '尚不必', '2meet-data-optimizer' ); ?></span>
<span class="wpdo-text-neutral">⏸ <?php esc_html_e( '尚不必', '2meet-data-optimizer' ); ?></span>
<?php endif; ?>
</td>
</tr>
@@ -252,7 +252,7 @@ class TMDO_Setup_Wizard {
</tbody>
</table>
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" style="margin-top: 2em;">
<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>
@@ -278,7 +278,7 @@ class TMDO_Setup_Wizard {
?>
<?php if ( empty( $actionable ) ) : ?>
<div class="notice notice-info inline" style="padding: 1em;">
<div class="notice notice-info inline">
<p><?php esc_html_e( '目前環境暫無高 confidence 的 module 建議。可隨時前往「模組建議」tab 重新檢查。', '2meet-data-optimizer' ); ?></p>
</div>
<?php else : ?>
@@ -316,13 +316,13 @@ class TMDO_Setup_Wizard {
<?php endif; ?>
<h3><?php esc_html_e( '黃金法則', '2meet-data-optimizer' ); ?></h3>
<ul style="list-style: disc; padding-left: 1.5em;">
<ul class="wpdo-list-disc">
<li><?php esc_html_e( '一次只推進 1 個 module,每階段觀察至少 24-48 小時。', '2meet-data-optimizer' ); ?></li>
<li><?php esc_html_e( '進 cutover 前一定要有 snapshotFSM Guard 自動觸發)。', '2meet-data-optimizer' ); ?></li>
<li><?php esc_html_e( '出事第一件事:rewind 該 module 到 idleemergency 流程)。', '2meet-data-optimizer' ); ?></li>
</ul>
<form method="get" action="<?php echo esc_url( admin_url( 'tools.php' ) ); ?>" style="margin-top: 2em;">
<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>
@@ -343,10 +343,10 @@ class TMDO_Setup_Wizard {
<h3><?php esc_html_e( '📦 Snapshot', '2meet-data-optimizer' ); ?></h3>
<?php if ( $snapshot_taken ) : ?>
<p style="color: #46b450; font-weight: bold;">✅ <?php esc_html_e( '快照已建立。可隨時於「備份快照」tab 查看與管理。', '2meet-data-optimizer' ); ?></p>
<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' ) ); ?>" style="display:inline">
<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>
@@ -392,7 +392,7 @@ class TMDO_Setup_Wizard {
</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' ) ); ?>" style="margin-top: 2em;">
<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>
@@ -415,14 +415,14 @@ class TMDO_Setup_Wizard {
<h2><?php esc_html_e( '🎉 完成!', '2meet-data-optimizer' ); ?></h2>
<p><?php esc_html_e( 'Setup wizard 已結束。下一步建議:', '2meet-data-optimizer' ); ?></p>
<ul style="list-style: disc; padding-left: 1.5em; line-height: 1.8;">
<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 style="margin-top: 2em;">
<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>
+47 -45
View File
@@ -28,6 +28,8 @@ $is_running = ( 'running' === $status_str || 'benchmarking' === $status_str );
$current_mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'comment' ) : 'disabled';
$mode_color = 'aeav_only' === $current_mode ? '#28a745' : ( 'shadow_read' === $current_mode ? '#dba617' : '#dc3545' );
$mode_optimal = 'aeav_only' === $current_mode;
$mode_text_cls = 'aeav_only' === $current_mode ? 'wpdo-text-success' : ( 'shadow_read' === $current_mode ? 'wpdo-text-amber' : 'wpdo-text-danger' );
$mode_card_cls = $mode_optimal ? 'ok' : 'warn';
$settings_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=settings' );
global $wpdb;
@@ -40,7 +42,7 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
<div class="wpdo-comment-stress-test-tab">
<h2><?php esc_html_e( 'Comment Entity 壓力測試 & Benchmark', '2meet-data-optimizer' ); ?></h2>
<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;">
<div class="wpdo-notice-error">
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
<?php esc_html_e( '會建立大量測試 comment 並 seed 對應 hp_review 群組 keys。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
</div>
@@ -50,8 +52,8 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
</p>
<!-- Diagnose snapshot -->
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin-top:20px;">
<h3 style="margin-top:0;"><?php esc_html_e( '當前 Comment Entity 狀態', '2meet-data-optimizer' ); ?></h3>
<div class="wpdo-card wpdo-mt-3">
<h3 class="wpdo-mt-0"><?php esc_html_e( '當前 Comment Entity 狀態', '2meet-data-optimizer' ); ?></h3>
<table class="widefat striped">
<tr>
<td><?php esc_html_e( 'wp_comments', '2meet-data-optimizer' ); ?></td>
@@ -61,7 +63,7 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
</tr>
<tr>
<td><?php esc_html_e( 'Mode', '2meet-data-optimizer' ); ?></td>
<td><strong style="color:<?php echo esc_attr( $mode_color ); ?>;"><?php echo esc_html( $current_mode ); ?></strong></td>
<td><strong class="<?php echo esc_attr( $mode_text_cls ); ?>"><?php echo esc_html( $current_mode ); ?></strong></td>
<td><?php esc_html_e( 'Ratio', '2meet-data-optimizer' ); ?></td>
<td><strong>1:<?php echo esc_html( (string) ( $total_comments > 0 ? round( $total_commentmeta / $total_comments, 2 ) : 0 ) ); ?></strong></td>
</tr>
@@ -73,14 +75,14 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
</tr>
<tr>
<td colspan="2"><?php esc_html_e( 'Stress 測試 comment 數', '2meet-data-optimizer' ); ?></td>
<td colspan="2"><strong id="wpdo-cst-count" style="color:#dc3545;font-size:18px;"><?php echo esc_html( number_format_i18n( $test_comment_count ) ); ?></strong></td>
<td colspan="2"><strong id="wpdo-cst-count" class="wpdo-count-danger"><?php echo esc_html( number_format_i18n( $test_comment_count ) ); ?></strong></td>
</tr>
</table>
</div>
<!-- 驗證反 EAV 優化指南 -->
<div class="wpdo-card" style="padding:20px;background:<?php echo $mode_optimal ? '#e8f5e9' : '#fff3cd'; ?>;border-left:4px solid <?php echo esc_attr( $mode_color ); ?>;border-radius:6px;margin-top:16px;font-size:13px;line-height:1.7;">
<h3 style="margin-top:0;font-size:14px;">
<div class="wpdo-card wpdo-mode-card wpdo-mode-card--<?php echo esc_attr( $mode_card_cls ); ?>">
<h3 class="wpdo-mt-0 wpdo-h3-sm">
<?php if ( $mode_optimal ) : ?>
✅ <?php esc_html_e( 'Comment Mode = aeav_only — 已具備驗證優化的條件', '2meet-data-optimizer' ); ?>
<?php else : ?>
@@ -89,26 +91,26 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
printf(
/* translators: %s: current mode */
esc_html__( 'Comment Mode = %s — 此模式下壓力測試結果不會展示完整反 EAV 優化效果', '2meet-data-optimizer' ),
'<code style="background:#fff;padding:2px 6px;border-radius:3px;">' . esc_html( $current_mode ) . '</code>'
'<code class="wpdo-code-white">' . esc_html( $current_mode ) . '</code>'
);
?>
<?php endif; ?>
</h3>
<p style="margin:8px 0 0 0;">
<p class="wpdo-mt-1">
<strong><?php esc_html_e( '想看 wp_commentmeta 真實減量?必須兩條件同時滿足:', '2meet-data-optimizer' ); ?></strong>
</p>
<ol style="margin:6px 0 8px 22px;padding:0;">
<ol class="wpdo-list-ol-tight">
<li>
<?php
printf(
/* translators: 1: open code, 2: close code */
esc_html__( 'Comment mode 設為 %1$saeav_only%2$s(前往設定 tab → Entity Bridge → Comment entity', '2meet-data-optimizer' ),
'<code style="background:#fff;padding:1px 5px;border-radius:3px;">',
'<code class="wpdo-code-white">',
'</code>'
);
?>
<?php if ( ! $mode_optimal ) : ?>
<a href="<?php echo esc_url( $settings_url ); ?>" class="button button-small" style="margin-left:8px;">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
<a href="<?php echo esc_url( $settings_url ); ?>" class="button button-small wpdo-ml-2">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
<?php endif; ?>
</li>
<li>
@@ -117,15 +119,15 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
</ol>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px;">
<div class="wpdo-grid-2col">
<!-- 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>
<div class="wpdo-card">
<h3 class="wpdo-mt-0"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
<table class="form-table" style="margin-top:0;">
<table class="form-table wpdo-mt-0">
<tr>
<th style="width:35%;"><label for="wpdo-cst-post-id"><?php esc_html_e( '目標 Post', '2meet-data-optimizer' ); ?></label></th>
<th class="wpdo-th-main"><label for="wpdo-cst-post-id"><?php esc_html_e( '目標 Post', '2meet-data-optimizer' ); ?></label></th>
<td>
<select id="wpdo-cst-post-id" class="regular-text">
<?php if ( empty( $posts ) ) : ?>
@@ -151,11 +153,11 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
<tr>
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
<td>
<label style="display:block;margin-bottom:6px;">
<label class="wpdo-label-block">
<input type="radio" name="wpdo-cst-mode" value="fast" checked />
<strong>Fast</strong> — <?php esc_html_e( '直接 $wpdb->insert,跳過 WP filter chain(最快,但不測 Hook Bus', '2meet-data-optimizer' ); ?>
</label>
<label style="display:block;">
<label class="wpdo-label-block">
<input type="radio" name="wpdo-cst-mode" value="realistic" />
<strong>Realistic</strong> — <?php esc_html_e( '走 wp_insert_comment + update_comment_meta(較慢,模擬生產路徑 + 觸發 Hook Bus', '2meet-data-optimizer' ); ?>
</label>
@@ -181,11 +183,11 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
</div>
<!-- Right: Cleanup + Re-run benchmark -->
<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>
<div class="wpdo-card">
<h3 class="wpdo-mt-0"><?php esc_html_e( '清除測試資料', '2meet-data-optimizer' ); ?></h3>
<p>
<?php esc_html_e( '目前 stress test comment 數:', '2meet-data-optimizer' ); ?>
<strong id="wpdo-cst-count-mirror" style="font-size:18px;color:#dc3545;">
<strong id="wpdo-cst-count-mirror" class="wpdo-count-danger">
<?php echo esc_html( number_format_i18n( $test_comment_count ) ); ?>
</strong>
</p>
@@ -198,7 +200,7 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
</button>
</p>
<hr style="margin:18px 0;" />
<hr class="wpdo-hr-section" />
<p>
<button type="button" class="button" id="wpdo-cst-rerun-bench" <?php disabled( $is_running || 0 === $test_comment_count ); ?>>
@@ -209,46 +211,46 @@ $total_commentmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->comment
</div>
<!-- Progress card (live) -->
<div id="wpdo-cst-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;">
<div id="wpdo-cst-progress-card" class="wpdo-card wpdo-mt-3<?php echo $is_running ? '' : ' wpdo-hidden'; ?>">
<h3 class="wpdo-mt-0"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
<div class="wpdo-bar-section">
<div class="wpdo-bar-header">
<span>
<strong id="wpdo-cst-pg-status"><?php echo esc_html( $status_str ); ?></strong>
· <code id="wpdo-cst-pg-post-id">post #<?php echo esc_html( (string) ( $state['post_id'] ?? '' ) ); ?></code>
· <span id="wpdo-cst-pg-mode"><?php echo esc_html( (string) ( $state['mode'] ?? '' ) ); ?></span> mode
</span>
<span id="wpdo-cst-pg-pct" style="font-weight:600;"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
<span id="wpdo-cst-pg-pct" class="wpdo-bar-pct"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
</div>
<div style="height:14px;background:#e0e0e0;border-radius:7px;overflow:hidden;">
<div id="wpdo-cst-pg-bar" style="height:100%;background:linear-gradient(90deg,#28a745,#20c997);width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%;transition:width .4s;"></div>
<div class="wpdo-bar-track">
<div id="wpdo-cst-pg-bar" class="wpdo-bar-fill" style="--wpdo-bar-width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%"></div>
</div>
</div>
<table style="width:100%;font-size:13px;margin-top:10px;border-collapse:collapse;">
<table class="wpdo-stats-table">
<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-cst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-cst-pg-target"><?php echo esc_html( (string) ( $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-cst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> comments/sec</td>
<td class="wpdo-td-muted"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td-val"><span id="wpdo-cst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-cst-pg-target"><?php echo esc_html( (string) ( $state['target'] ?? 0 ) ); ?></span></td>
<td class="wpdo-td-muted"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td-val"><span id="wpdo-cst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> comments/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-cst-pg-elapsed"><?php echo esc_html( (string) ( $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-cst-pg-eta"><?php echo esc_html( (string) ( $state['eta_sec'] ?? 0 ) ); ?></span> 秒</td>
<td class="wpdo-td-muted"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-cst-pg-elapsed"><?php echo esc_html( (string) ( $state['elapsed_sec'] ?? 0 ) ); ?></span> 秒</td>
<td class="wpdo-td-muted"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-cst-pg-eta"><?php echo esc_html( (string) ( $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-cst-pg-batches"><?php echo esc_html( (string) ( $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-cst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
<td class="wpdo-td-muted"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-cst-pg-batches"><?php echo esc_html( (string) ( $state['batches_done'] ?? 0 ) ); ?></span></td>
<td class="wpdo-td-muted"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-cst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
</tr>
</table>
</div>
<!-- Benchmark report -->
<div id="wpdo-cst-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-cst-bench-card" class="wpdo-card wpdo-mt-3<?php echo ! empty( $state['benchmark'] ) ? '' : ' wpdo-hidden'; ?>">
<h3 class="wpdo-mt-0"><?php esc_html_e( '3. Benchmark 報告', '2meet-data-optimizer' ); ?></h3>
<div id="wpdo-cst-bench-content"></div>
</div>
+1 -1
View File
@@ -176,7 +176,7 @@ $failed = 'failed' === $state;
<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>
style="--wpdo-bar-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>
+6 -6
View File
@@ -215,7 +215,7 @@ $wpdo_format_msg = static function ( string $code ): string {
?>
</p>
<div class="actions">
<form method="post" style="display:inline">
<form method="post">
<input type="hidden" name="wpdo_postmeta_cleanup" value="1">
<?php wp_nonce_field( 'wpdo_postmeta_cleanup' ); ?>
<button type="submit" class="button button-primary <?php echo (int) $garbage['total'] > 0 ? '' : 'disabled'; ?>"
@@ -230,7 +230,7 @@ $wpdo_format_msg = static function ( string $code ): string {
<h3><?php esc_html_e( '步驟 2:把 wp_postmeta 既有資料 backfill 至 flat 表(v2.9.3', '2meet-data-optimizer' ); ?></h3>
<p><?php esc_html_e( 'Idempotent — 重跑安全。每個 group 跑一次 bulk SQL pivot。', '2meet-data-optimizer' ); ?></p>
<div class="actions">
<form method="post" style="display:inline">
<form method="post">
<input type="hidden" name="wpdo_post_backfill_all" value="1">
<?php wp_nonce_field( 'wpdo_post_backfill_all' ); ?>
<button type="submit" class="button button-primary"
@@ -245,7 +245,7 @@ $wpdo_format_msg = static function ( string $code ): string {
<h3><?php esc_html_e( '步驟 3:把 legacy wpdo_hot_hp_listing 抄到 flat 表(v2.9.5', '2meet-data-optimizer' ); ?></h3>
<p><?php esc_html_e( '非破壞 — legacy hot 表保留作為 v3.0.0 rollback safety net。', '2meet-data-optimizer' ); ?></p>
<div class="actions">
<form method="post" style="display:inline">
<form method="post">
<input type="hidden" name="wpdo_post_cutover_legacy" value="1">
<?php wp_nonce_field( 'wpdo_post_cutover_legacy' ); ?>
<button type="submit" class="button"
@@ -260,7 +260,7 @@ $wpdo_format_msg = static function ( string $code ): string {
<h3><?php esc_html_e( '步驟 4:升級 mode 至 dual_write', '2meet-data-optimizer' ); ?></h3>
<p><?php esc_html_e( 'wp_postmeta 與 flat 表同時寫入。讀仍走 wp_postmeta(生產 safe)。建議至少觀察 24h 後再升級下一階。', '2meet-data-optimizer' ); ?></p>
<div class="actions">
<form method="post" style="display:inline">
<form method="post">
<input type="hidden" name="wpdo_post_promote_dual_write" value="1">
<?php wp_nonce_field( 'wpdo_post_promote_dual_write' ); ?>
<button type="submit" class="button"
@@ -275,7 +275,7 @@ $wpdo_format_msg = static function ( string $code ): string {
<h3><?php esc_html_e( '步驟 5:升級 mode 至 aeav_only(最終 cutover', '2meet-data-optimizer' ); ?></h3>
<p><?php esc_html_e( 'flat 表成為 source-of-truth,讀寫都走 flat。完成後可 wp wpdo post-cleanup --confirm 清掉 wp_postmeta 已遷移 keys。', '2meet-data-optimizer' ); ?></p>
<div class="actions">
<form method="post" style="display:inline">
<form method="post">
<input type="hidden" name="wpdo_post_promote_aeav" value="1">
<?php wp_nonce_field( 'wpdo_post_promote_aeav' ); ?>
<button type="submit" class="button"
@@ -286,7 +286,7 @@ $wpdo_format_msg = static function ( string $code ): string {
</div>
</div>
<div class="wpdo-card" style="background: #f6f7f7;">
<div class="wpdo-card wpdo-card--light">
<h3><?php esc_html_e( '回退路徑(rollback', '2meet-data-optimizer' ); ?></h3>
<p>
<?php esc_html_e( '若任一步驟出問題,可下降 mode', '2meet-data-optimizer' ); ?>
+58 -56
View File
@@ -65,7 +65,7 @@ $post_type_options = array(
<div class="wpdo-post-stress-test-tab">
<h2><?php esc_html_e( 'Post Entity 壓力測試 & Benchmark', '2meet-data-optimizer' ); ?></h2>
<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;">
<div class="wpdo-notice-error">
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
<?php esc_html_e( '會建立大量測試 post 並填滿對應 wp_postmeta + flat 表。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
</div>
@@ -85,10 +85,12 @@ $post_type_options = array(
$current_post_mode = (string) ( $diagnose['mode'] ?? 'disabled' );
$mode_color = 'aeav_only' === $current_post_mode ? '#28a745' : '#dc3545';
$mode_optimal = 'aeav_only' === $current_post_mode;
$mode_text_cls = $mode_optimal ? 'wpdo-text-success' : 'wpdo-text-danger';
$mode_card_cls = $mode_optimal ? 'ok' : 'warn';
$settings_tab_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=settings' );
?>
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin-top:20px;">
<h3 style="margin-top:0;"><?php esc_html_e( '當前 Post Entity 狀態', '2meet-data-optimizer' ); ?></h3>
<div class="wpdo-card wpdo-mt-3">
<h3 class="wpdo-mt-0"><?php esc_html_e( '當前 Post Entity 狀態', '2meet-data-optimizer' ); ?></h3>
<table class="widefat striped">
<tr>
<td><?php esc_html_e( 'Posts', '2meet-data-optimizer' ); ?></td>
@@ -100,18 +102,18 @@ $post_type_options = array(
<td><?php esc_html_e( 'Ratio', '2meet-data-optimizer' ); ?></td>
<td><strong>1:<?php echo esc_html( (string) $diagnose['ratio'] ); ?></strong></td>
<td><?php esc_html_e( 'Mode', '2meet-data-optimizer' ); ?></td>
<td><strong style="color:<?php echo esc_attr( $mode_color ); ?>;"><?php echo esc_html( $current_post_mode ); ?></strong></td>
<td><strong class="<?php echo esc_attr( $mode_text_cls ); ?>"><?php echo esc_html( $current_post_mode ); ?></strong></td>
</tr>
<tr>
<td colspan="2"><?php esc_html_e( 'Stress 測試 post 數', '2meet-data-optimizer' ); ?></td>
<td colspan="2"><strong id="wpdo-pst-count" style="color:#dc3545;font-size:18px;"><?php echo esc_html( number_format_i18n( $test_post_count ) ); ?></strong></td>
<td colspan="2"><strong id="wpdo-pst-count" class="wpdo-count-danger"><?php echo esc_html( number_format_i18n( $test_post_count ) ); ?></strong></td>
</tr>
</table>
</div>
<!-- 驗證反 EAV 優化指南 -->
<div class="wpdo-card" style="padding:20px;background:<?php echo $mode_optimal ? '#e8f5e9' : '#fff3cd'; ?>;border-left:4px solid <?php echo esc_attr( $mode_color ); ?>;border-radius:6px;margin-top:16px;font-size:13px;line-height:1.7;">
<h3 style="margin-top:0;font-size:14px;">
<div class="wpdo-card wpdo-mode-card wpdo-mode-card--<?php echo esc_attr( $mode_card_cls ); ?>">
<h3 class="wpdo-mt-0 wpdo-h3-sm">
<?php if ( $mode_optimal ) : ?>
✅ <?php esc_html_e( 'Post Mode = aeav_only — 已具備驗證優化的條件', '2meet-data-optimizer' ); ?>
<?php else : ?>
@@ -120,27 +122,27 @@ $post_type_options = array(
printf(
/* translators: %s: current mode */
esc_html__( 'Post Mode = %s — 此模式下壓力測試結果不會展示反 EAV 優化效果', '2meet-data-optimizer' ),
'<code style="background:#fff;padding:2px 6px;border-radius:3px;">' . esc_html( $current_post_mode ) . '</code>'
'<code class="wpdo-code-white">' . esc_html( $current_post_mode ) . '</code>'
);
?>
<?php endif; ?>
</h3>
<p style="margin:8px 0 0 0;">
<p class="wpdo-mt-1">
<strong><?php esc_html_e( '想看 wp_postmeta 真實減量?必須同時滿足兩個條件:', '2meet-data-optimizer' ); ?></strong>
</p>
<ol style="margin:6px 0 8px 22px;padding:0;">
<ol class="wpdo-list-ol-tight">
<li>
<?php
printf(
/* translators: 1: settings tab anchor open, 2: settings tab anchor close */
esc_html__( 'Post mode 設為 %1$saeav_only%2$s(前往設定 tab → Entity Bridge → Post entity', '2meet-data-optimizer' ),
'<code style="background:#fff;padding:1px 5px;border-radius:3px;">',
'<code class="wpdo-code-white">',
'</code>'
);
?>
<?php if ( ! $mode_optimal ) : ?>
<a href="<?php echo esc_url( $settings_tab_url ); ?>" class="button button-small" style="margin-left:8px;">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
<a href="<?php echo esc_url( $settings_tab_url ); ?>" class="button button-small wpdo-ml-2">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
<?php endif; ?>
</li>
<li>
@@ -148,54 +150,54 @@ $post_type_options = array(
</li>
</ol>
<details style="margin-top:8px;">
<summary style="cursor:pointer;color:#0073aa;font-weight:600;"><?php esc_html_e( '📐 模式 × 寫入路徑 → 預期結果矩陣', '2meet-data-optimizer' ); ?></summary>
<table class="widefat" style="margin-top:8px;background:#fff;font-size:12px;">
<details class="wpdo-mt-1">
<summary class="wpdo-summary-toggle"><?php esc_html_e( '📐 模式 × 寫入路徑 → 預期結果矩陣', '2meet-data-optimizer' ); ?></summary>
<table class="widefat wpdo-mt-1 wpdo-table-inset">
<thead>
<tr style="background:#f0f0f0;">
<tr class="wpdo-tr-header">
<th><?php esc_html_e( 'Post Mode', '2meet-data-optimizer' ); ?></th>
<th>⚡ Fast</th>
<th>🐢 Realistic</th>
</tr>
</thead>
<tbody>
<tr<?php echo 'disabled' === $current_post_mode ? ' style="background:#fff8e1;"' : ''; ?>>
<tr class="<?php echo 'disabled' === $current_post_mode ? 'wpdo-tr-highlight' : ''; ?>">
<td><code>disabled</code></td>
<td>wp_postmeta 5 rows / flat 0 → <strong>1:5(無優化)</strong></td>
<td>wp_postmeta 5 / flat 0 → 1:5(無優化)</td>
</tr>
<tr<?php echo 'dual_write' === $current_post_mode ? ' style="background:#fff8e1;"' : ''; ?>>
<tr class="<?php echo 'dual_write' === $current_post_mode ? 'wpdo-tr-highlight' : ''; ?>">
<td><code>dual_write</code></td>
<td>wp_postmeta 5 / flat 0 → 1:5</td>
<td>wp_postmeta 5 + flat 1 → 1:5(有 flat 但 wp_postmeta 不減)</td>
</tr>
<tr<?php echo 'shadow_read' === $current_post_mode ? ' style="background:#fff8e1;"' : ''; ?>>
<tr class="<?php echo 'shadow_read' === $current_post_mode ? 'wpdo-tr-highlight' : ''; ?>">
<td><code>shadow_read</code></td>
<td>wp_postmeta 5 / flat 0 → 1:5</td>
<td>wp_postmeta 5 + flat 1 → 1:5(讀走 flat,寫仍雙寫)</td>
</tr>
<tr<?php echo 'aeav_only' === $current_post_mode ? ' style="background:#e8f5e9;font-weight:600;"' : ''; ?>>
<tr class="<?php echo 'aeav_only' === $current_post_mode ? 'wpdo-tr-success' : ''; ?>">
<td><code>aeav_only</code></td>
<td>wp_postmeta 5(直 SQL 繞過 Hook Bus/ flat 0 → 1:5 ⚠️</td>
<td>wp_postmeta <strong>0</strong> / flat 1 → <strong>0:1(完全優化)</strong> ✅</td>
</tr>
</tbody>
</table>
<p class="description" style="margin-top:6px;">
<p class="description wpdo-mt-1">
<?php esc_html_e( '範例以 nav_menu_item5 keys/post)為基準。Fast 模式直接 $wpdb->insert,故意繞過 Hook Bus → 即使 mode=aeav_only 也會寫滿 wp_postmeta(用途:快速灌 fixture 給 Query Router benchmark)。驗證反 EAV 優化效果一律用 Realistic。', '2meet-data-optimizer' ); ?>
</p>
</details>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px;">
<div class="wpdo-grid-2col">
<!-- 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>
<div class="wpdo-card">
<h3 class="wpdo-mt-0"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
<table class="form-table" style="margin-top:0;">
<table class="form-table wpdo-mt-0">
<tr>
<th style="width:35%;"><label for="wpdo-pst-post-type"><?php esc_html_e( 'Post Type', '2meet-data-optimizer' ); ?></label></th>
<th class="wpdo-th-main"><label for="wpdo-pst-post-type"><?php esc_html_e( 'Post Type', '2meet-data-optimizer' ); ?></label></th>
<td>
<select id="wpdo-pst-post-type" class="regular-text">
<?php foreach ( $post_type_options as $pt => $info ) : ?>
@@ -228,11 +230,11 @@ $post_type_options = array(
<tr>
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
<td>
<label style="display:block;margin-bottom:6px;">
<label class="wpdo-label-block">
<input type="radio" name="wpdo-pst-mode" value="fast" checked />
<strong>Fast</strong> — <?php esc_html_e( '直接 $wpdb->insert,跳過 WP filter chain(最快,但不測 Hook Bus', '2meet-data-optimizer' ); ?>
</label>
<label style="display:block;">
<label class="wpdo-label-block">
<input type="radio" name="wpdo-pst-mode" value="realistic" />
<strong>Realistic</strong> — <?php esc_html_e( '走 wp_insert_post + update_post_meta(較慢,模擬生產路徑 + 觸發 Hook Bus → mode=dual_write+ 時 flat 表自動填入)', '2meet-data-optimizer' ); ?>
</label>
@@ -258,11 +260,11 @@ $post_type_options = array(
</div>
<!-- Right: Cleanup + Re-run benchmark -->
<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>
<div class="wpdo-card">
<h3 class="wpdo-mt-0"><?php esc_html_e( '清除測試資料', '2meet-data-optimizer' ); ?></h3>
<p>
<?php esc_html_e( '目前 stress test post 數:', '2meet-data-optimizer' ); ?>
<strong id="wpdo-pst-count-mirror" style="font-size:18px;color:#dc3545;">
<strong id="wpdo-pst-count-mirror" class="wpdo-count-danger">
<?php echo esc_html( number_format_i18n( $test_post_count ) ); ?>
</strong>
</p>
@@ -275,7 +277,7 @@ $post_type_options = array(
</button>
</p>
<hr style="margin:18px 0;" />
<hr class="wpdo-hr-section" />
<p>
<button type="button" class="button" id="wpdo-pst-rerun-bench" <?php disabled( $is_running || 0 === $test_post_count ); ?>>
@@ -289,52 +291,52 @@ $post_type_options = array(
</div>
<!-- Progress section (live) -->
<div id="wpdo-pst-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;">
<div id="wpdo-pst-progress-card" class="wpdo-card wpdo-mt-3<?php echo $is_running ? '' : ' wpdo-hidden'; ?>">
<h3 class="wpdo-mt-0"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
<div class="wpdo-bar-section">
<div class="wpdo-bar-header">
<span>
<strong id="wpdo-pst-pg-status"><?php echo esc_html( $wpdo_pst_stat ); ?></strong>
· <code id="wpdo-pst-pg-post-type"><?php echo esc_html( (string) ( $state['post_type'] ?? '' ) ); ?></code>
· <span id="wpdo-pst-pg-mode"><?php echo esc_html( (string) ( $state['mode'] ?? '' ) ); ?></span> mode
</span>
<span id="wpdo-pst-pg-pct" style="font-weight:600;"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
<span id="wpdo-pst-pg-pct" class="wpdo-bar-pct"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
</div>
<div style="height:14px;background:#e0e0e0;border-radius:7px;overflow:hidden;">
<div id="wpdo-pst-pg-bar" style="height:100%;background:linear-gradient(90deg,#28a745,#20c997);width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%;transition:width .4s;"></div>
<div class="wpdo-bar-track">
<div id="wpdo-pst-pg-bar" class="wpdo-bar-fill" style="--wpdo-bar-width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%"></div>
</div>
</div>
<table style="width:100%;font-size:13px;margin-top:10px;border-collapse:collapse;">
<table class="wpdo-stats-table">
<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-pst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-pst-pg-target"><?php echo esc_html( (string) ( $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-pst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> posts/sec</td>
<td class="wpdo-td-muted"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td-val"><span id="wpdo-pst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-pst-pg-target"><?php echo esc_html( (string) ( $state['target'] ?? 0 ) ); ?></span></td>
<td class="wpdo-td-muted"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td-val"><span id="wpdo-pst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> posts/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-pst-pg-elapsed"><?php echo esc_html( (string) ( $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-pst-pg-eta"><?php echo esc_html( (string) ( $state['eta_sec'] ?? 0 ) ); ?></span> 秒</td>
<td class="wpdo-td-muted"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-pst-pg-elapsed"><?php echo esc_html( (string) ( $state['elapsed_sec'] ?? 0 ) ); ?></span> 秒</td>
<td class="wpdo-td-muted"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-pst-pg-eta"><?php echo esc_html( (string) ( $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-pst-pg-batches"><?php echo esc_html( (string) ( $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-pst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
<td class="wpdo-td-muted"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-pst-pg-batches"><?php echo esc_html( (string) ( $state['batches_done'] ?? 0 ) ); ?></span></td>
<td class="wpdo-td-muted"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-pst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
</tr>
</table>
</div>
<!-- Benchmark report -->
<div id="wpdo-pst-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-pst-bench-card" class="wpdo-card wpdo-mt-3<?php echo ! empty( $state['benchmark'] ) ? '' : ' wpdo-hidden'; ?>">
<h3 class="wpdo-mt-0"><?php esc_html_e( '3. Benchmark 報告', '2meet-data-optimizer' ); ?></h3>
<div id="wpdo-pst-bench-content"></div>
</div>
<!-- 7-group seed map info -->
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin-top:20px;">
<h3 style="margin-top:0;"><?php esc_html_e( '4. TMDO_Post_Stress_Tester seed map7 個 post_type', '2meet-data-optimizer' ); ?></h3>
<div class="wpdo-card wpdo-mt-3">
<h3 class="wpdo-mt-0"><?php esc_html_e( '4. TMDO_Post_Stress_Tester seed map7 個 post_type', '2meet-data-optimizer' ); ?></h3>
<p class="description">
<?php esc_html_e( '每個 stress post 自動 seed 對應 group 的 canonical meta keysv2.9.1 entity registry 定義)。', '2meet-data-optimizer' ); ?>
</p>
+47 -45
View File
@@ -28,6 +28,8 @@ $is_running = ( 'running' === $status_str || 'benchmarking' === $status_str );
$current_mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( 'term' ) : 'disabled';
$mode_color = 'aeav_only' === $current_mode ? '#28a745' : ( 'shadow_read' === $current_mode ? '#dba617' : '#dc3545' );
$mode_optimal = 'aeav_only' === $current_mode;
$mode_text_cls = 'aeav_only' === $current_mode ? 'wpdo-text-success' : ( 'shadow_read' === $current_mode ? 'wpdo-text-amber' : 'wpdo-text-danger' );
$mode_card_cls = $mode_optimal ? 'ok' : 'warn';
$settings_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=settings' );
global $wpdb;
@@ -40,7 +42,7 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
<div class="wpdo-term-stress-test-tab">
<h2><?php esc_html_e( 'Term Entity 壓力測試 & Benchmark', '2meet-data-optimizer' ); ?></h2>
<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;">
<div class="wpdo-notice-error">
⚠️ <strong><?php esc_html_e( '此工具僅供開發 / 測試環境使用。', '2meet-data-optimizer' ); ?></strong>
<?php esc_html_e( '會建立大量測試 term 並 seed 對應 hp_taxonomy 群組 keys。請勿在生產環境執行。', '2meet-data-optimizer' ); ?>
</div>
@@ -50,8 +52,8 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
</p>
<!-- Diagnose snapshot -->
<div class="wpdo-card" style="padding:20px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin-top:20px;">
<h3 style="margin-top:0;"><?php esc_html_e( '當前 Term Entity 狀態', '2meet-data-optimizer' ); ?></h3>
<div class="wpdo-card wpdo-mt-3">
<h3 class="wpdo-mt-0"><?php esc_html_e( '當前 Term Entity 狀態', '2meet-data-optimizer' ); ?></h3>
<table class="widefat striped">
<tr>
<td><?php esc_html_e( 'wp_terms', '2meet-data-optimizer' ); ?></td>
@@ -61,7 +63,7 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
</tr>
<tr>
<td><?php esc_html_e( 'Mode', '2meet-data-optimizer' ); ?></td>
<td><strong style="color:<?php echo esc_attr( $mode_color ); ?>;"><?php echo esc_html( $current_mode ); ?></strong></td>
<td><strong class="<?php echo esc_attr( $mode_text_cls ); ?>"><?php echo esc_html( $current_mode ); ?></strong></td>
<td><?php esc_html_e( 'Ratio', '2meet-data-optimizer' ); ?></td>
<td><strong>1:<?php echo esc_html( (string) ( $total_terms > 0 ? round( $total_termmeta / $total_terms, 2 ) : 0 ) ); ?></strong></td>
</tr>
@@ -73,14 +75,14 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
</tr>
<tr>
<td colspan="2"><?php esc_html_e( 'Stress 測試 term 數', '2meet-data-optimizer' ); ?></td>
<td colspan="2"><strong id="wpdo-tst-count" style="color:#dc3545;font-size:18px;"><?php echo esc_html( number_format_i18n( $test_term_count ) ); ?></strong></td>
<td colspan="2"><strong id="wpdo-tst-count" class="wpdo-count-danger"><?php echo esc_html( number_format_i18n( $test_term_count ) ); ?></strong></td>
</tr>
</table>
</div>
<!-- 驗證反 EAV 優化指南 -->
<div class="wpdo-card" style="padding:20px;background:<?php echo $mode_optimal ? '#e8f5e9' : '#fff3cd'; ?>;border-left:4px solid <?php echo esc_attr( $mode_color ); ?>;border-radius:6px;margin-top:16px;font-size:13px;line-height:1.7;">
<h3 style="margin-top:0;font-size:14px;">
<div class="wpdo-card wpdo-mode-card wpdo-mode-card--<?php echo esc_attr( $mode_card_cls ); ?>">
<h3 class="wpdo-mt-0 wpdo-h3-sm">
<?php if ( $mode_optimal ) : ?>
✅ <?php esc_html_e( 'Term Mode = aeav_only — 已具備驗證優化的條件', '2meet-data-optimizer' ); ?>
<?php else : ?>
@@ -89,26 +91,26 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
printf(
/* translators: %s: current mode */
esc_html__( 'Term Mode = %s — 此模式下壓力測試結果不會展示完整反 EAV 優化效果', '2meet-data-optimizer' ),
'<code style="background:#fff;padding:2px 6px;border-radius:3px;">' . esc_html( $current_mode ) . '</code>'
'<code class="wpdo-code-white">' . esc_html( $current_mode ) . '</code>'
);
?>
<?php endif; ?>
</h3>
<p style="margin:8px 0 0 0;">
<p class="wpdo-mt-1">
<strong><?php esc_html_e( '想看 wp_termmeta 真實減量?必須兩條件同時滿足:', '2meet-data-optimizer' ); ?></strong>
</p>
<ol style="margin:6px 0 8px 22px;padding:0;">
<ol class="wpdo-list-ol-tight">
<li>
<?php
printf(
/* translators: 1: open code, 2: close code */
esc_html__( 'Term mode 設為 %1$saeav_only%2$s(前往設定 tab → Entity Bridge → Term entity', '2meet-data-optimizer' ),
'<code style="background:#fff;padding:1px 5px;border-radius:3px;">',
'<code class="wpdo-code-white">',
'</code>'
);
?>
<?php if ( ! $mode_optimal ) : ?>
<a href="<?php echo esc_url( $settings_url ); ?>" class="button button-small" style="margin-left:8px;">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
<a href="<?php echo esc_url( $settings_url ); ?>" class="button button-small wpdo-ml-2">→ <?php esc_html_e( '前往設定', '2meet-data-optimizer' ); ?></a>
<?php endif; ?>
</li>
<li>
@@ -117,15 +119,15 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
</ol>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px;">
<div class="wpdo-grid-2col">
<!-- 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>
<div class="wpdo-card">
<h3 class="wpdo-mt-0"><?php esc_html_e( '1. 設定並啟動測試', '2meet-data-optimizer' ); ?></h3>
<table class="form-table" style="margin-top:0;">
<table class="form-table wpdo-mt-0">
<tr>
<th style="width:35%;"><label for="wpdo-tst-taxonomy"><?php esc_html_e( 'Taxonomy', '2meet-data-optimizer' ); ?></label></th>
<th class="wpdo-th-main"><label for="wpdo-tst-taxonomy"><?php esc_html_e( 'Taxonomy', '2meet-data-optimizer' ); ?></label></th>
<td>
<select id="wpdo-tst-taxonomy" class="regular-text">
<?php foreach ( $taxonomies as $slug => $label ) : ?>
@@ -147,11 +149,11 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
<tr>
<th><label><?php esc_html_e( '寫入模式', '2meet-data-optimizer' ); ?></label></th>
<td>
<label style="display:block;margin-bottom:6px;">
<label class="wpdo-label-block">
<input type="radio" name="wpdo-tst-mode" value="fast" checked />
<strong>Fast</strong> — <?php esc_html_e( '直接 $wpdb->insert,跳過 WP filter chain(最快,但不測 Hook Bus', '2meet-data-optimizer' ); ?>
</label>
<label style="display:block;">
<label class="wpdo-label-block">
<input type="radio" name="wpdo-tst-mode" value="realistic" />
<strong>Realistic</strong> — <?php esc_html_e( '走 wp_insert_term + update_term_meta(較慢,模擬生產路徑 + 觸發 Hook Bus', '2meet-data-optimizer' ); ?>
</label>
@@ -177,11 +179,11 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
</div>
<!-- Right: Cleanup + Re-run benchmark -->
<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>
<div class="wpdo-card">
<h3 class="wpdo-mt-0"><?php esc_html_e( '清除測試資料', '2meet-data-optimizer' ); ?></h3>
<p>
<?php esc_html_e( '目前 stress test term 數:', '2meet-data-optimizer' ); ?>
<strong id="wpdo-tst-count-mirror" style="font-size:18px;color:#dc3545;">
<strong id="wpdo-tst-count-mirror" class="wpdo-count-danger">
<?php echo esc_html( number_format_i18n( $test_term_count ) ); ?>
</strong>
</p>
@@ -194,7 +196,7 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
</button>
</p>
<hr style="margin:18px 0;" />
<hr class="wpdo-hr-section" />
<p>
<button type="button" class="button" id="wpdo-tst-rerun-bench" <?php disabled( $is_running || 0 === $test_term_count ); ?>>
@@ -205,46 +207,46 @@ $total_termmeta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->termmeta
</div>
<!-- Progress card (live) -->
<div id="wpdo-tst-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;">
<div id="wpdo-tst-progress-card" class="wpdo-card wpdo-mt-3<?php echo $is_running ? '' : ' wpdo-hidden'; ?>">
<h3 class="wpdo-mt-0"><?php esc_html_e( '2. 即時進度', '2meet-data-optimizer' ); ?></h3>
<div class="wpdo-bar-section">
<div class="wpdo-bar-header">
<span>
<strong id="wpdo-tst-pg-status"><?php echo esc_html( $status_str ); ?></strong>
· <code id="wpdo-tst-pg-taxonomy"><?php echo esc_html( (string) ( $state['taxonomy'] ?? '' ) ); ?></code>
· <span id="wpdo-tst-pg-mode"><?php echo esc_html( (string) ( $state['mode'] ?? '' ) ); ?></span> mode
</span>
<span id="wpdo-tst-pg-pct" style="font-weight:600;"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
<span id="wpdo-tst-pg-pct" class="wpdo-bar-pct"><?php echo esc_html( (string) ( $state['pct'] ?? 0 ) ); ?>%</span>
</div>
<div style="height:14px;background:#e0e0e0;border-radius:7px;overflow:hidden;">
<div id="wpdo-tst-pg-bar" style="height:100%;background:linear-gradient(90deg,#28a745,#20c997);width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%;transition:width .4s;"></div>
<div class="wpdo-bar-track">
<div id="wpdo-tst-pg-bar" class="wpdo-bar-fill" style="--wpdo-bar-width:<?php echo esc_attr( (string) ( $state['pct'] ?? 0 ) ); ?>%"></div>
</div>
</div>
<table style="width:100%;font-size:13px;margin-top:10px;border-collapse:collapse;">
<table class="wpdo-stats-table">
<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-tst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-tst-pg-target"><?php echo esc_html( (string) ( $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-tst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> terms/sec</td>
<td class="wpdo-td-muted"><?php esc_html_e( '已建立', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td-val"><span id="wpdo-tst-pg-processed"><?php echo esc_html( (string) ( $state['processed'] ?? 0 ) ); ?></span> / <span id="wpdo-tst-pg-target"><?php echo esc_html( (string) ( $state['target'] ?? 0 ) ); ?></span></td>
<td class="wpdo-td-muted"><?php esc_html_e( '速率', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td-val"><span id="wpdo-tst-pg-rate"><?php echo esc_html( (string) ( $state['rate_per_sec'] ?? 0 ) ); ?></span> terms/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-tst-pg-elapsed"><?php echo esc_html( (string) ( $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-tst-pg-eta"><?php echo esc_html( (string) ( $state['eta_sec'] ?? 0 ) ); ?></span> 秒</td>
<td class="wpdo-td-muted"><?php esc_html_e( '已耗時', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-tst-pg-elapsed"><?php echo esc_html( (string) ( $state['elapsed_sec'] ?? 0 ) ); ?></span> 秒</td>
<td class="wpdo-td-muted"><?php esc_html_e( '預估剩餘', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-tst-pg-eta"><?php echo esc_html( (string) ( $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-tst-pg-batches"><?php echo esc_html( (string) ( $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-tst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
<td class="wpdo-td-muted"><?php esc_html_e( '完成批次', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-tst-pg-batches"><?php echo esc_html( (string) ( $state['batches_done'] ?? 0 ) ); ?></span></td>
<td class="wpdo-td-muted"><?php esc_html_e( 'PHP Peak Mem', '2meet-data-optimizer' ); ?></td>
<td class="wpdo-td"><span id="wpdo-tst-pg-mem"><?php echo esc_html( (string) round( ( (int) ( $state['peak_memory'] ?? 0 ) ) / 1048576, 1 ) ); ?></span> MB</td>
</tr>
</table>
</div>
<!-- Benchmark report -->
<div id="wpdo-tst-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-tst-bench-card" class="wpdo-card wpdo-mt-3<?php echo ! empty( $state['benchmark'] ) ? '' : ' wpdo-hidden'; ?>">
<h3 class="wpdo-mt-0"><?php esc_html_e( '3. Benchmark 報告', '2meet-data-optimizer' ); ?></h3>
<div id="wpdo-tst-bench-content"></div>
</div>
+12 -2
View File
@@ -648,6 +648,11 @@ class TMDO_CLI {
* @subcommand import-hpct
*/
public function import_hpct( $args, $assoc_args ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
// HPCT is a HivePress-family plugin; the importer ships in that AddOn.
if ( ! class_exists( 'TMDO_HPCT_Import' ) ) {
WP_CLI::error( 'import-hpct needs 2meet-data-optimizer-hivepress-addon to be active.' );
}
if ( TMDO_HPCT_Import::is_imported() ) {
WP_CLI::warning( 'HPCT settings have already been imported.' );
return;
@@ -798,12 +803,12 @@ class TMDO_CLI {
if ( ! empty( $warm_posts ) ) {
// Seed warm zone entries so reads are non-trivial.
foreach ( $warm_posts as $pid ) {
TMDO_Zone_Warm::set( (int) $pid, TMDO_Listing_Stats::VIEW_KEY, '1', DAY_IN_SECONDS );
TMDO_Zone_Warm::set( (int) $pid, TMDO_Zone_Warm::VIEW_KEY, '1', DAY_IN_SECONDS );
}
$start = microtime( true );
foreach ( $warm_posts as $pid ) {
TMDO_Zone_Warm::get( (int) $pid, TMDO_Listing_Stats::VIEW_KEY );
TMDO_Zone_Warm::get( (int) $pid, TMDO_Zone_Warm::VIEW_KEY );
}
$warm_ms = ( microtime( true ) - $start ) * 1000;
@@ -1148,6 +1153,10 @@ class TMDO_CLI {
WP_CLI::log( "Deleted {$warm_deleted} expired warm entries." );
if ( $archive_expired ) {
// Listing archival is HivePress-specific and ships in that AddOn.
if ( ! class_exists( 'TMDO_Listing_Stats' ) ) {
WP_CLI::warning( '--archive-expired needs 2meet-data-optimizer-hivepress-addon; skipping.' );
} else {
WP_CLI::log( 'Archiving expired listing fields to Zone D...' );
$archived = TMDO_Listing_Stats::archive_expired_listings();
WP_CLI::log( "Archived {$archived} fields from expired listings." );
@@ -1155,6 +1164,7 @@ class TMDO_CLI {
$stats = TMDO_Zone_Archive::stats();
WP_CLI::log( "Zone D total: {$stats['total_rows']} rows, {$stats['compressed_rows']} compressed." );
}
}
WP_CLI::success( 'Cleanup complete.' );
}
+279
View File
@@ -0,0 +1,279 @@
# WAF 相容性 — NinjaFirewall
適用:2meet-data-optimizer v1.0.2+ / NinjaFirewall (WP Edition) 4.9
**結論:兩者可共存,且不需要開發 AddOn。** 本文記錄實測結果、symlink 多租戶部署的必要設定、WP SaaS 的開站流程與驗證清單,以及開發時必須遵守的三條約束。
> 只想開新站台的話,直接看 §3;踩到 `Cannot retrieve user options from database (#3)` 看 §2。
---
## 1. 兩種模式的差異
NinjaFirewall 只有兩種模式(「WP+」是付費版本,不是模式):
| | WordPress WAF | Full WAF |
|---|---|---|
| 載入方式 | mu-plugin `0-ninjafirewall.php` | `auto_prepend_file``.user.ini` / php.ini |
| 執行時機 | WordPress 載入中 | **WordPress 載入之前** |
| 讀設定 | `get_option('nfw_options')` | 自行解析 wp-config.php**原生 mysqli 直查 `{prefix}options`** |
| symlink 安全 | ✅ 站台身分由 WordPress 決定 | ❌ 自行以 `__DIR__` 推導(見 §2 |
這個差異是後續所有注意事項的根源:**Full WAF 完全繞過 WordPress**,因此 TMDO 的任何 PHP 攔截器對它都不存在,但反過來,TMDO 對 `wp_options` 的**持久性**改動會影響它。
---
## 2. Symlink / 多租戶部署(必讀)
WP SaaS 常以 symlink 共享 codebase
```
tenant-a/wp-content/plugins -> /shared/plugins
tenant-b/wp-content/plugins -> /shared/plugins
```
Full WAF 在 `lib/firewall.php:78` 這樣推導站台位置:
```php
$nfw_['wp_content'] = dirname(dirname(dirname( __DIR__ )));
```
**PHP 的 `__DIR__` 會解析 symlink 到真實路徑**,於是每個租戶都被判定為「共享目錄所屬的那個站」。接著 `firewall.php:120-121` 讀到錯誤的 wp-config.php、連到錯誤的資料庫,撈不到該站的 `nfw_options`,最後在 `firewall.php:613` 回傳錯誤碼 6
```
NinjaFirewall fatal error: Cannot retrieve user options from database (#3).
Review your installation, your site is not protected.
```
最後那句是字面意義的 —— 此時 WAF 已 `nfw_quit()`**完全不做任何過濾**。
### 解法:以 DOCUMENT_ROOT 為錨點
`.htninja` 的搜尋位置是 `$_SERVER['DOCUMENT_ROOT']``firewall.php:47-48`),那是 web server 的 per-site 值,不受 symlink 影響。第二順位是 `dirname(DOCUMENT_ROOT)`,因此**一份檔案即可服務所有租戶**:
```php
<?php
// 置於各站 DOCUMENT_ROOT 的共同父目錄,例如 /var/www/Studio/.htninja
$nfw_site_root = rtrim( $_SERVER['DOCUMENT_ROOT'] ?? '', '/' );
if ( '' !== $nfw_site_root && is_file( $nfw_site_root . '/wp-config.php' ) ) {
if ( ! defined( 'NFW_LOG_DIR' ) ) {
define( 'NFW_LOG_DIR', $nfw_site_root . '/wp-content' ); // firewall.php:82-83
}
$wp_config = $nfw_site_root . '/wp-config.php'; // firewall.php:120-122
}
unset( $nfw_site_root );
```
覆寫時序:`.htninja` 於 L50 載入 → `NFW_LOG_DIR` 於 L82 生效 → `NFWSESSION_DIR` 跟著 log_dir → `$wp_config` 於 L120 生效。
**注意事項**
- **不要在 `.htninja` 使用 `return`** —— 回傳 `'ALLOW'` / `'BLOCK'` 會改變過濾行為(`firewall.php:54-71`)。
- `NFW_LOG_DIR`**WP WAF 模式同樣重要**`firewall.php:78` 在該模式下一樣會誤判,導致所有租戶的 firewall log、`cache/db_hash.N.php`、session 全寫進共享目錄、互相覆蓋。
- **每個租戶的 DB 都要有自己的 `nfw_options`**。修好路徑後 WAF 會連到正確的租戶資料庫,但該站若從未啟用過 NinjaFirewall,那一列不存在,照樣 #3。開站流程必須包含啟用步驟。
- `nfw_rules` 約 77KB、`autoload=auto`,每個租戶一份,規則更新也要逐站執行。
- 若租戶 DB 憑證不在 wp-config.php,可改用 `firewall.php:466-470`:在 `.htninja``$GLOBALS['nfw_mysqli']``$GLOBALS['nfw_table_prefix']` 直接提供連線。代價是每 request 多一條 mysqli 連線。
- **`.user.ini` 必須讓 php-fpm worker(通常是 www-data)可讀**,否則 `auto_prepend_file` 靜默失效、Full WAF 等同未安裝。`root:root 0640` 是常見的踩雷組合。
---
## 3. WP SaaS 開站流程
以 symlink 共享 codebase 的多租戶環境,NinjaFirewall 的每一項設定都分成「平台層做一次」與「每站都要做」兩類。混淆這兩者是最常見的失誤來源。
### 3.1 平台層(全租戶共用,只做一次)
**A. 站台錨點 `.htninja`** — 見 §2。放在各站 DOCUMENT_ROOT 的共同父目錄(例如 `/var/www/Studio/.htninja`),內容以 `$_SERVER['DOCUMENT_ROOT']` 動態推導,因此一份即可服務所有租戶,新增租戶時不需修改。
**B. nginx 規則** — 這是 per-site 設定檔,但內容對所有站相同,建議做成 snippet 讓各站 `include`
```nginx
# NinjaFirewall 的 log / cache / loader 目錄。
# 它自帶的 .htaccess 在 nginx 下完全無效,且 nginx 與 php-fpm 同為 www-data
# 檔案權限無法區分「WAF 寫入」與「對外 serve」,因此只能在這裡封鎖。
# ^~ 是必要的:讓前綴匹配優先於 \.php$ 正則,否則 .php 請求會落到 PHP handler。
location ^~ /wp-content/nfwlog/ { deny all; }
# 所有 dotfile.htaccess / .htninja / .user.ini / .env / .git ...
# 放行 .well-known,否則 Certbot 的 ACME challenge 會失敗、憑證無法續期。
location ~ /\.(?!well-known) { deny all; }
```
漏掉這段的後果:`nfwlog/` 下的 `readme.txt` 會被公開讀取(等於告訴掃描器這站跑 NinjaFirewall),未來的 `session/sess_*` 檔也不是 `.php`、同樣裸奔。`.php` 檔本身有雙層保護(引擎開頭的 `die('Forbidden')` + 檔內 `<?php exit; ?>`)不會外洩內容,但不該依賴它。
### 3.2 每個新租戶站台(順序不可顛倒)
**順序很重要**:先確保錨點與設定就緒,再安裝 Full WAF。順序顛倒會直接撞上 §2 的 `#3` 錯誤。
**Step 1 — 啟用外掛,建立該站自己的設定**
```bash
wp --path=/var/www/sites/<tenant> plugin activate ninjafirewall
wp --path=/var/www/sites/<tenant> option list --search='nfw*' --fields=option_name,autoload
# 必須看到 nfw_options 與 nfw_rules,否則後續一定 #3
```
`nfw_options` / `nfw_rules` 存在**該租戶自己的資料庫**,不會因為 codebase 共享而自動存在。這是 `#3` 最常見的成因 —— 路徑修對了,但那個站從沒跑過 installer。
**Step 2 — 確認目錄權限**
php-fpm worker(通常 www-data)必須能在 `nfwlog/` 建立與寫入檔案:
| 路徑 | 建議 | 理由 |
|---|---|---|
| `wp-content/nfwlog/` 及子目錄 | `2775` `wpdev:www-data` | setgid 確保新檔繼承 groupgroup 需 `w` 否則 WAF 寫不了 log |
| `nfwlog/` 內檔案 | `664` | 同上 |
| `nfwlog/session/` | 不用管 | 由 WAF 自建,它會用自己的嚴格權限(`0700` |
| `.user.ini`(裝 Full WAF 後才有) | `664` `wpdev:www-data` | www-data 需可讀,否則 auto_prepend 靜默失效 |
```bash
NFWLOG=/var/www/sites/<tenant>/wp-content/nfwlog
sudo find "$NFWLOG" -type d -exec chmod 2775 {} +
sudo find "$NFWLOG" -type f -exec chmod 664 {} +
```
`nfwlog/` 的 owner 本來就是 www-data(例如全程由後台建立),預設 `0755` 也可運作 —— owner 有寫入權。會出事的是「owner 是別人、group 只有 `r-x`」這種組合。
**Step 3 — 安裝 Full WAF(從後台)**
務必從 WordPress 後台操作,不要手工建檔:後台以 www-data 執行,權限天然正確;且官方安裝流程有 sandbox 驗證,會先確認 `auto_prepend_file` 真的生效才寫入設定,避免寫出讓整站 500 的 `.user.ini`
裝完立刻檢查 `.user.ini` 權限(見上表)。`root:root 0640` 是常見的踩雷組合 —— PHP 讀不到,Full WAF 等同沒裝,而表面上一切正常。
**Step 4 — 套用 nginx 規則並 reload**
```bash
sudo nginx -t && sudo systemctl reload nginx
```
### 3.3 開站後驗證(每站必跑)
四項缺一不可。只做前兩項會漏掉最隱蔽的失效模式。
```bash
SITE=https://<tenant>.example.com
NFWLOG=/var/www/sites/<tenant>/wp-content/nfwlog
# (1) 模式判別 —— 回 Forbidden 代表 Full WAF 生效(auto_prepend 已套用到所有 PHP 請求)
curl -sk "$SITE/wp-content/plugins/ninjafirewall/lib/i18n.php" | head -c 9; echo
# (2) 攔截能力 —— 應為 403
curl -sk -o /dev/null -w '%{http_code}\n' "$SITE/?t=../../etc/passwd"
# (3) 記錄能力 —— 最容易被漏掉的一項,log 必須增長
before=$(stat -c%s "$NFWLOG/firewall_$(date +%Y-%m).php")
curl -sk -o /dev/null "$SITE/?t=../../etc/passwd"
after=$(stat -c%s "$NFWLOG/firewall_$(date +%Y-%m).php")
[ "$after" -gt "$before" ] && echo "log OK" || echo "log FAIL — 檢查 nfwlog 權限"
# (4) web 封鎖與功能未損
for p in /wp-content/nfwlog/ /wp-content/nfwlog/readme.txt /.user.ini /.htaccess; do
printf "%-40s %s\n" "$p" "$(curl -sk -o /dev/null -w '%{http_code}' "$SITE$p")" # 全部應為 403
done
for p in / /wp-json/ /wp-admin/; do
printf "%-40s %s\n" "$p" "$(curl -sk -o /dev/null -w '%{http_code}' "$SITE$p")" # 200 / 200 / 302
done
```
**第 (3) 項為什麼不能省**:攔截與記錄是兩件事。權限不足時 WAF 照樣回 403,但一筆記錄都寫不進去 —— 你得到一個沒有稽核軌跡的防火牆,事故調查時等於沒有。更嚴重的是同一個權限問題會讓 `session/` 建不出來,而 `wl_admin` 管理員白名單靠 `NFWSESSID` session 承載 `nfw_goodguy` 旗標,於是管理員實際上是被全規則掃描的 —— 這正是「後台操作偶爾莫名 403」的來源。
### 3.4 排查時的常見誤判
這幾項在實測中都出現過,全部會誤導判斷:
| 現象 | 直覺結論 | 實際 |
|---|---|---|
| 首頁 200、後台沒有錯誤訊息 | WAF 正常 | 完全不能推論。WAF 失效時是 `nfw_quit()` 靜默放行,不擋也不記 |
| 攻擊 payload 回 403 | Full WAF 生效 | 可能來自共享 mu-plugin 的 WP WAF。用 §3.3 第 (1) 項區分 |
| `nfwlog/` 下的檔案回 403 | nginx 規則生效 | 也可能是 nginx 以 www-data 開不了 root-only 檔案。修好權限後會突然變 200 |
| `/.well-known/` 回 403 | dotfile 規則誤擋了 ACME | 多半是目錄存在但 autoindex off。用實檔測:`/.well-known/acme-challenge/<file>` 若回 404WordPress 頁面)而非 403,代表規則有正確放行 |
| `wp-login.php` 回 404 | nginx 改壞了 | 檢查是否啟用 `wps-hide-login` 之類的登入頁隱藏外掛 |
| `wp tmdo doctor` 全綠 | WAF 與外掛相容 | WP-CLI 完全豁免 WAF`firewall.php:18-23`),CLI 結果不能當相容性證據,必須另外走 HTTP 驗證 |
### 3.5 每租戶獨立的維運負擔
symlink 共享的是 codebase,**不是設定與狀態**。以下每一項都是 per-tenant
- **`nfw_options` / `nfw_rules`** 存在各自的 `wp_options``nfw_rules` 約 77 KB 且 `autoload=auto`N 個租戶就是 N 份。
- **規則更新**要逐站執行,沒有集中派送機制。
- **log / cache / session** 各自獨立(前提是 §2 的 `NFW_LOG_DIR` 已正確設定,否則全部寫進共享目錄互相覆蓋)。
- **停用租戶站台時**`nfwlog/readme.txt` 明示:解除安裝後要等 5 分鐘再刪除該目錄,否則站台可能崩潰。
規劃階段就要把這些算進成本 —— 特別是「規則更新要逐站跑」這點,租戶數量上去之後需要自動化。
---
## 4. 開發約束(三條)
### 4.1 不得重導向 WAF 的設定選項
`TMDO_Options_Manager::register_settings_group()``pre_update_option_{key}` 回傳 `$old_value`,讓選項不再落地 `wp_options`。而 Full WAF 是用原生 mysqli 直查該表 —— 一旦 `nfw_options` / `nfw_rules` 被重導向,WAF 會讀不到設定而**靜默停止防護:不報錯、不寫 log**。
v1.0.2 起由 `PROTECTED_OPTIONS` 常數擋下,測試見 `tests/unit/OptionsManagerProtectedTest.php`。新增任何「WordPress 載入前就會以原生 SQL 讀取 `wp_options`」的第三方元件時,必須把它的設定鍵加進該清單。
**autoload 最佳化不受此限** —— `optimize_autoload()` 只改 `autoload` 欄位、不刪列,而 Full WAF 那句 `SELECT *` 不看 autoload。
### 4.2 不得把 option 名稱字串放進 GET/POST
規則 **322**`lev=3`CRITICAL):
```
(^|\S['"])nfw_(?:options|rules)\b
```
任何 GET/POST 值含 `nfw_options``nfw_rules` 字串即 **403**why: "Attempt to modify NinjaFirewall settings")。
這對「列出 autoload 清單讓使用者勾選清理」這類 UI 是直接的地雷 —— 而 `nfw_rules` 正好是最大的 autoload 項目之一,必然出現在清單裡。若要做這種介面,用索引或 hash 當作 POST 值,或直接走 CLI。
### 4.3 不得以 base64 傳送含 SQL 語意的 payload
`nfw_check_b64()``firewall.php:1412-1448``post_b64` 選項,預設開)會把每個 POST 值 base64 解碼後再比對。明文的 SQL 規則多半需要「以數字/引號開頭」或「以註解結尾」才命中,**base64 版只要「含有」就 CRITICAL 403** —— 涵蓋 `SELECT...FROM...WHERE``INSERT INTO``UNION SELECT``UPDATE...SET`、以及序列化物件 `O:n:"..."`
換言之,**編碼會讓事情變糟,不是變好**。硬編碼白名單只有 `fpd_print_order``g-recaptcha-response` 兩個欄位名。
---
## 5. 風險矩陣(實測結果)
於 dev30**Full WAF**、`wl_admin=1``no_restapi=0``admin_ajax` 未設)實測。**同一組項目在 WP WAF 模式下結果完全相同** —— 兩種模式都跑過:
| 項目 | 結果 |
|---|---|
| `wp tmdo status` / `wp tmdo doctor` | ✅ 不受影響(CLI 豁免) |
| `GET /wp-json/wpdo/v1/listings`**無 cookie** | ✅ 200,且回的是 JSON 而非 WAF 的 HTML 403 頁 |
| `?hp_price_min=100&hp_featured=1` 動態欄位 filter | ✅ 200 |
| 首頁 / REST 根 | ✅ 200 |
| 後台 `/wp-admin/` | ✅ 302(導向登入) |
| firewall log 中的 TMDO 相關攔截 | ✅ **0 筆**(log 內全部攔截皆為刻意送出的測試 payload) |
最後一項是判斷相容性的核心證據:不是「沒看到錯誤」,而是逐筆檢查 firewall log 的攔截記錄,確認沒有任何一筆來自本外掛的正常操作。
仍需留意的情境:
| 風險 | 觸發條件 | 緩解 |
|---|---|---|
| Full WAF 靜默失效 | WAF 設定選項被重導向 | §4.1(已由 `PROTECTED_OPTIONS` 擋下) |
| 規則 322 誤擋 | option 名稱字串進 GET/POST | §4.2 |
| migration 中斷 | `run_sync_loop()` 的 110 秒同步 POST`class-tmdo-migration-orchestrator.php:443` `set_time_limit(120)`)被代理切斷;鎖 `LOCK_TTL_SEC=1800` 才釋放 | 大站用 `force_async=true` 或走 CLI |
| REST 回 HTML 而非 JSON | 非 administrator 角色,或以 Application Password / JWT 呼叫(無 `NFWSESSID` cookie)→ 不在 `wl_admin` 白名單。403 頁面是 HTML`firewall.php:1585-1590`),client 端看到的是「JSON parse error」這種難查的錯 | 程式化呼叫走 CLI;或確認帶 cookie |
| admin-ajax 被當 bot | `admin_ajax` 選項開啟時,缺 `HTTP_ACCEPT` / `Accept-Language` / UA 不含 `Mozilla` 的請求回 **404**`firewall.php:1760-1802` | curl / server-to-server 呼叫需帶完整 header;預設此選項未開 |
**WP-CLI 完全豁免**`firewall.php:18-23``defined('WP_CLI') && WP_CLI && PHP_SAPI === 'cli'` 直接 `return`。TMDO 全部 63 個 CLI 子命令零風險 —— 這也是 migration、snapshot restore、backfill 等重操作建議走 CLI 的另一個理由。
**管理員幾乎豁免**`wl_admin=1` 時,帶 `NFWSESSID` cookie 的 administrator 只跑 3 條規則就 `nfw_quit(20)``firewall.php:233-252`)。
---
## 6. 為什麼不需要 AddOn
| 理由 | 證據 |
|---|---|
| 無對外 hook API | 全 codebase 零個 `apply_filters('nfw_*')` / `do_action('nfw_*')` |
| 官方相容手段都在部署層 | `.htninja``exclude_waf_list` UI、wp-config 常數 —— 沒有一項是 plugin 程式碼掛得上的 |
| 常見衝突模式在 TMDO 全不存在 | 無 loopback self-POST、無 `db.php`/`object-cache.php` drop-in、無 wp-config 改動、無 `auto_prepend` 操作、無 `$_FILES` 上傳 |
NinjaFirewall 處理「合法外掛送 SQL 被擋」的官方做法是**在引擎裡硬編碼白名單**(例:`firewall.php:1434-1439` 的 JetPack 例外),需向 NinTechNet 回報才會納入,第三方無法自行擴充。
因此本外掛的相容性工作全部落在**核心的三條約束**(§4)與**部署設定**(§2),沒有 AddOn 的著力點。
+36 -3
View File
@@ -646,7 +646,7 @@ class TMDO_REST_API {
return $err;
}
$views = TMDO_Listing_Stats::get_view_count( $post_id );
$views = self::read_view_count( $post_id );
return new WP_REST_Response(
array(
@@ -730,8 +730,8 @@ class TMDO_REST_API {
// Mark as counted for this IP/session.
set_transient( $rl_key, 1, HOUR_IN_SECONDS );
TMDO_Listing_Stats::increment_view( $post_id );
$views = TMDO_Listing_Stats::get_view_count( $post_id );
self::bump_view_count( $post_id );
$views = self::read_view_count( $post_id );
$response = new WP_REST_Response(
array(
@@ -1642,4 +1642,37 @@ class TMDO_REST_API {
$result = TMDO_Migration_Orchestrator::resume();
return new WP_REST_Response( $result, ! empty( $result['ok'] ) ? 200 : 409 );
}
/**
* Read a post's view counter.
*
* Delegates to the HivePress AddOn when it is active, because that class
* adds an hp_view_count postmeta fallback for listings migrated before the
* warm zone existed. Without the AddOn — the normal case for a plain
* install — core reads its own warm row directly. Calling the AddOn class
* unconditionally used to fatal these endpoints on any site without it.
*
* @param int $post_id Post ID.
* @return int
*/
private static function read_view_count( int $post_id ): int {
if ( class_exists( 'TMDO_Listing_Stats' ) ) {
return (int) TMDO_Listing_Stats::get_view_count( $post_id );
}
return (int) ( TMDO_Zone_Warm::get( $post_id, TMDO_Zone_Warm::VIEW_KEY ) ?? 0 );
}
/**
* Increment a post's view counter.
*
* @param int $post_id Post ID.
* @return void
*/
private static function bump_view_count( int $post_id ): void {
if ( class_exists( 'TMDO_Listing_Stats' ) ) {
TMDO_Listing_Stats::increment_view( $post_id );
return;
}
TMDO_Zone_Warm::increment( $post_id, TMDO_Zone_Warm::VIEW_KEY, 1, TMDO_Zone_Warm::VIEW_TTL );
}
}
+9
View File
@@ -145,7 +145,16 @@ class TMDO_Schema_Registry {
break;
case 'cold':
// 去重:hot 以 column 為 key、warm 以 meta_key 為 key,兩者天生冪等;
// 唯獨 cold 是 append,重複註冊會無限膨脹。而 `wpdo_register_fields`
// 本來就會被 fire 多次(core plugins_loaded:4 + late-bind safety net :30),
// 加上主外掛與 AddOn 可能同時註冊同一批欄位,實測曾出現同一 key 重複 4 次。
if ( ! isset( $this->cold_fields[ $config['post_type'] ] ) ) {
$this->cold_fields[ $config['post_type'] ] = array();
}
if ( ! in_array( $config['meta_key'], $this->cold_fields[ $config['post_type'] ], true ) ) {
$this->cold_fields[ $config['post_type'] ][] = $config['meta_key'];
}
break;
}
}
+13
View File
@@ -24,6 +24,19 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class TMDO_Zone_Warm {
/**
* Warm key used for per-post view counters.
*
* Owned by core because the row lives in this zone's table and core's admin,
* CLI and REST layers all read it. The HivePress AddOn's
* TMDO_Listing_Stats::VIEW_KEY carries the identical literal, so the two
* address the same rows — nothing to migrate either way.
*/
const VIEW_KEY = 'wpdo_views';
/** TTL applied when core increments the view counter itself. */
const VIEW_TTL = DAY_IN_SECONDS;
/**
* Get the warm table name.
*/
@@ -46,6 +46,22 @@ final class TMDO_Options_Manager {
'wpb_backup_options',
);
/**
* 禁止重導向的選項名稱清單
*
* 這些選項會被「WordPress 載入之前」執行的元件以原生 SQL 直查 wp_options
* NinjaFirewall Full WAF 走 auto_prepend_file,以 mysqli 讀取 nfw_options
* 與 nfw_rules)。一旦被 register_settings_group() 重導向至專屬設定表,
* 該元件會讀不到設定而靜默停止運作 —— 不報錯、不寫 log。
*
* 註:autoload 最佳化不受此限,因為那些元件的 SELECT 不看 autoload 欄位。
*/
private const PROTECTED_OPTIONS = array(
'nfw_options',
'nfw_rules',
'nfw_checked',
);
/** autoload 合計閾值(MB),超過則警告 */
private const AUTOLOAD_WARNING_THRESHOLD_MB = 1;
@@ -202,6 +218,22 @@ final class TMDO_Options_Manager {
public static function register_settings_group( string $group_name, array $option_keys ): void {
global $wpdb;
// 受保護的選項不得重導向,否則會讓 WordPress 載入前讀取它的元件靜默失效
foreach ( array_intersect( $option_keys, self::PROTECTED_OPTIONS ) as $protected_key ) {
_doing_it_wrong(
__METHOD__,
esc_html( "選項 {$protected_key} 由 WordPress 載入前的元件以原生 SQL 直讀 wp_options,重導向會使其靜默失效,已略過。" ),
'1.0.2'
);
}
$option_keys = array_diff( $option_keys, self::PROTECTED_OPTIONS );
// 全部都被擋下就不必建表
if ( empty( $option_keys ) ) {
return;
}
$table = $wpdb->prefix . TMDO_TABLE_PREFIX . 'settings_' . sanitize_key( $group_name );
// 建立設定專屬表
+12 -1
View File
@@ -253,6 +253,15 @@ if ( ! function_exists( 'update_post_meta' ) ) {
return true;
}
}
if ( ! function_exists( 'add_post_meta' ) ) {
function add_post_meta( int $post_id, string $key, $value, bool $unique = false ): int|bool {
if ( $unique && isset( $GLOBALS['_wp_postmeta'][ $post_id ][ $key ] ) ) {
return false;
}
$GLOBALS['_wp_postmeta'][ $post_id ][ $key ] = $value;
return 1;
}
}
if ( ! function_exists( 'get_user_meta' ) ) {
function get_user_meta( int $uid, string $key = '', bool $single = false ) {
return $GLOBALS['_wp_usermeta'][ $uid ][ $key ] ?? ( $single ? '' : [] );
@@ -661,7 +670,9 @@ add_filter( 'wpdo/fsm_guard/bypass', '__return_true' );
// TMDO_Listing_Stats → 2meet-data-optimizer-hivepress-addon.
// Tests that hit REST endpoints using listing stats receive stub responses.
if ( ! class_exists( 'TMDO_Listing_Stats' ) ) {
// The HivePress AddOn's own suite defines TMDO_TEST_SKIP_LISTING_STATS_STUB so
// it can load the real class instead of this stub.
if ( ! defined( 'TMDO_TEST_SKIP_LISTING_STATS_STUB' ) && ! class_exists( 'TMDO_Listing_Stats' ) ) {
class TMDO_Listing_Stats {
public static function register(): void {}
public static function get_view_count( int $post_id ): int { return 0; }
+152
View File
@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Architectural guard: core must never hard-depend on an AddOn's classes.
*
* The 11 AddOns are optional. A site can run this plugin alone, so every
* reference core makes to an AddOn-owned class has to sit behind a
* class_exists() check — otherwise that code path fatals.
*
* This caught a real defect: the admin Dashboard tab and two public REST
* endpoints called TMDO_Listing_Stats (which ships in the HivePress AddOn)
* unconditionally, so they died with "Class not found" on any plain install.
*
* Static analysis on purpose — the alternative, booting core once per AddOn
* combination, is far more machinery for a weaker signal.
*/
class CoreBoundaryTest extends TestCase {
/**
* Classes that are declared by an AddOn, never by core.
*
* Kept as prefixes so a newly added TMDO_HivePress_* adapter is covered
* without touching this list.
*
* @var string[]
*/
private const ADDON_CLASS_PREFIXES = array(
'TMDO_HivePress',
'TMDO_Hivepress',
'TMDO_HP_',
'TMDO_HPCT_',
'TMDO_Listing_',
'TMDO_Favorites_',
'TMDO_Messages_',
'TMDO_Memberships_',
'TMDO_Requests_',
'TMDO_Reviews_',
'TMDO_Statistics_',
'TMDO_WooCommerce',
'TMDO_Woocommerce',
'TMDO_WC_',
'TMDO_LatePoint',
'TMDO_Latepoint',
'TMDO_Infocards',
'TMDO_Bookings',
'TMDO_Collab',
'TMDO_Playlist',
'TMDO_Quotation',
'TMDO_Mobile_Bridge',
'TMDO_Hub_',
'TMDO_Spoke_',
'TMDO_Admin_HivePress',
'TMDO_Admin_WC',
'TMDO_CLI_HivePress',
);
/**
* Every core production file, excluding vendor and the test suite itself.
*
* @return array<string, array{string}>
*/
public static function coreFileProvider(): array {
$root = dirname( __DIR__, 2 );
$files = array();
foreach ( array( 'includes', 'admin', 'cli', 'modules' ) as $dir ) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $root . '/' . $dir, FilesystemIterator::SKIP_DOTS )
);
foreach ( $iterator as $file ) {
if ( 'php' === $file->getExtension() ) {
$rel = substr( $file->getPathname(), strlen( $root ) + 1 );
$files[ $rel ] = array( $file->getPathname() );
}
}
}
ksort( $files );
return $files;
}
/**
* @dataProvider coreFileProvider
*
* @param string $path Absolute path to a core production file.
*/
public function test_addon_classes_are_only_used_behind_a_guard( string $path ): void {
$source = file_get_contents( $path );
$this->assertIsString( $source, "Unreadable: {$path}" );
$lines = explode( "\n", $source );
$offences = array();
foreach ( $lines as $i => $line ) {
$code = $this->strip_comments_and_strings( $line );
foreach ( self::ADDON_CLASS_PREFIXES as $prefix ) {
// Only static calls / constant reads bind at runtime; a bare
// mention (e.g. inside an array of names to probe) does not.
if ( ! preg_match( '/\b(' . preg_quote( $prefix, '/' ) . '\w*)::/', $code, $m ) ) {
continue;
}
if ( $this->is_guarded( $lines, $i, $m[1] ) ) {
continue;
}
$offences[] = sprintf( '%s:%d — %s', basename( $path ), $i + 1, trim( $line ) );
}
}
$this->assertSame(
array(),
$offences,
"Core calls an AddOn class without a class_exists() guard:\n" . implode( "\n", $offences )
);
}
/**
* Remove line comments and string literals so mentions inside them are ignored.
*/
private function strip_comments_and_strings( string $line ): string {
$line = preg_replace( '#(//|\*|\#).*$#', '', $line ) ?? $line;
$line = preg_replace( "/'[^']*'/", "''", $line ) ?? $line;
return preg_replace( '/"[^"]*"/', '""', $line ) ?? $line;
}
/**
* A guard counts if class_exists() for the same class appears earlier in
* the same function (or, for top-level code, earlier in the file).
*
* Function scope rather than a fixed line window: an early-return guard at
* the top of a 60-line CLI command legitimately protects every call below
* it, and a window tight enough to be meaningful would reject that.
*
* @param string[] $lines Whole file, split on newlines.
* @param int $index Zero-based line index of the call site.
* @param string $class Class name being called.
*/
private function is_guarded( array $lines, int $index, string $class ): bool {
$from = 0;
for ( $i = $index; $i >= 0; $i-- ) {
if ( preg_match( '/^\t{1,2}(?:(?:public|private|protected|static|final|abstract)\s+)*function\s/', $lines[ $i ] ) ) {
$from = $i;
break;
}
}
$slice = implode( "\n", array_slice( $lines, $from, $index - $from + 1 ) );
return (bool) preg_match(
'/class_exists\(\s*[\'"]' . preg_quote( $class, '/' ) . '[\'"]/',
$slice
);
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Unit tests for TMDO_Options_Manager 的受保護選項守衛。
*
* 背景:register_settings_group() 會以 pre_update_option_{key} 回傳 $old_value
* 讓選項不再落地 wp_options、改存專屬設定表。但 NinjaFirewall Full WAF 走
* auto_prepend_file,在 WordPress 載入前就以原生 mysqli 直查 wp_options 取
* nfw_options / nfw_rules。若這些鍵被重導向,WAF 會讀不到設定而靜默停止防護
* (不報錯、不寫 log),因此必須在註冊階段就擋下。
*
* 註:混合「受保護 + 一般」鍵的情境需要走到 dbDelta(),而單元測試 bootstrap
* 沒有該 stub(ABSPATH 為假路徑),故此處只覆蓋純受保護鍵與常數契約 —— 這已
* 足以鎖住「受保護鍵絕不會被掛上攔截 filter」這個核心保證。
*/
class OptionsManagerProtectedTest extends TestCase {
/** 每個測試前清空靜態註冊表,避免跨測試污染。 */
protected function setUp(): void {
parent::setUp();
$prop = new ReflectionProperty( 'TMDO_Options_Manager', 'redirected_options' );
$prop->setAccessible( true );
$prop->setValue( null, array() );
}
// ── 常數契約 ────────────────────────────────────────────────────────────
public function test_protected_options_covers_ninjafirewall_keys(): void {
$ref = new ReflectionClass( 'TMDO_Options_Manager' );
$protected = $ref->getConstant( 'PROTECTED_OPTIONS' );
$this->assertIsArray( $protected );
foreach ( array( 'nfw_options', 'nfw_rules', 'nfw_checked' ) as $key ) {
$this->assertContains(
$key,
$protected,
"{$key} 必須列入 PROTECTED_OPTIONS,否則重導向後 NinjaFirewall Full WAF 會靜默失效"
);
}
}
// ── 註冊守衛 ────────────────────────────────────────────────────────────
public function test_protected_options_are_not_registered(): void {
TMDO_Options_Manager::register_settings_group(
'waf_guard_test',
array( 'nfw_options', 'nfw_rules', 'nfw_checked' )
);
$this->assertSame(
array(),
TMDO_Options_Manager::get_redirected_options(),
'受保護的選項不得進入重導向清單'
);
}
public function test_protected_options_get_no_interception_filters(): void {
TMDO_Options_Manager::register_settings_group(
'waf_guard_test',
array( 'nfw_options', 'nfw_rules' )
);
$hooks = $GLOBALS['_wp_filter_callbacks'] ?? array();
foreach ( array( 'nfw_options', 'nfw_rules' ) as $key ) {
$this->assertArrayNotHasKey(
"pre_option_{$key}",
$hooks,
"pre_option_{$key} 不得被掛上 —— 會讓 WAF 讀到轉址後的值"
);
$this->assertArrayNotHasKey(
"pre_update_option_{$key}",
$hooks,
"pre_update_option_{$key} 不得被掛上 —— 會讓設定不再寫回 wp_options"
);
}
}
/** 全部鍵都被擋下時應提前返回,不建立設定表。 */
public function test_all_protected_keys_skips_table_creation(): void {
global $wpdb;
$before = $wpdb->last_query ?? null;
TMDO_Options_Manager::register_settings_group(
'waf_guard_test',
array( 'nfw_options' )
);
$this->assertSame(
$before,
$wpdb->last_query ?? null,
'不應對資料庫發出任何查詢'
);
}
}
+219
View File
@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
if ( ! class_exists( 'WP_Post' ) ) {
class WP_Post {
public int $ID = 0;
public string $post_type = '';
public string $post_status = 'publish';
public string $post_title = '';
public string $post_content = '';
public int $post_author = 0;
public int $post_parent = 0;
public function __construct( object $data ) {
foreach ( (array) $data as $k => $v ) {
$this->$k = $v;
}
}
}
}
/**
* Concrete test double for TMDO_Standard_Post_Interceptor.
*/
class TMDO_Test_Post_Interceptor extends TMDO_Standard_Post_Interceptor {
protected string $module = 'test_module';
public const FIELD_MAP = array(
'test_meta' => 'test_col',
'other_meta' => 'other_col',
);
protected function get_post_type(): string {
return 'test_post';
}
protected function get_table_key(): string {
return 'test_table';
}
protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array {
return array(
'values' => array(
'post_id' => $post_id,
'status' => $post->post_status,
'created_at' => $now,
'updated_at' => $now,
),
'formats' => array( '%d', '%s', '%s', '%s' ),
);
}
}
/**
* Unit tests for TMDO_Standard_Post_Interceptor abstract base.
*
* Exercises the three shared hook methods (action_delete_post,
* filter_update_meta, action_insert_post) via the TMDO_Test_Post_Interceptor
* concrete subclass, which never needs to live outside this test file.
*/
class StandardPostInterceptorTest extends TestCase {
private TMDO_Test_Post_Interceptor $interceptor;
/** Last $wpdb->delete() call: [table, where, formats]. */
public static array $last_delete = [];
/** Last SQL passed to $wpdb->query(). */
public static string $last_query = '';
/** Last $wpdb->insert() call: [table, data, formats]. */
public static array $last_insert = [];
protected function setUp(): void {
self::$last_delete = [];
self::$last_query = '';
self::$last_insert = [];
$this->interceptor = new TMDO_Test_Post_Interceptor();
// Reset Feature Flags request cache.
$ff = new ReflectionClass( TMDO_Feature_Flags::class );
$ff->getProperty( 'cache' )->setValue( null, null );
$GLOBALS['_wp_options'] = [];
$this->setup_wpdb_mock();
}
private function setup_wpdb_mock(): void {
global $wpdb;
$wpdb = new class {
public string $prefix = 'wp_';
public function prepare( string $sql, mixed ...$args ): string {
$i = 0;
return preg_replace_callback( '/%([sd])/', static function ( $m ) use ( &$i, $args ) {
$val = $args[ $i++ ] ?? '';
return $m[1] === 'd' ? (string) (int) $val : "'" . addslashes( (string) $val ) . "'";
}, $sql );
}
public function query( string $sql ): int|bool {
StandardPostInterceptorTest::$last_query = $sql;
return 1;
}
public function delete( string $table, array $where, array $formats ): int|false {
StandardPostInterceptorTest::$last_delete = [ $table, $where, $formats ];
return 1;
}
public function insert( string $table, array $data, array $formats ): int|false {
StandardPostInterceptorTest::$last_insert = [ $table, $data, $formats ];
return 1;
}
};
}
private function make_post( string $type = 'test_post', int $id = 1 ): WP_Post {
$post = new WP_Post( (object) [] );
$post->ID = $id;
$post->post_type = $type;
$post->post_status = 'publish';
$post->post_title = 'Test';
$post->post_content = '';
$post->post_author = 0;
$post->post_parent = 0;
return $post;
}
// ── action_delete_post ────────────────────────────────────────────────────
public function test_delete_post_calls_wpdb_delete_for_correct_type(): void {
TMDO_Feature_Flags::set( 'test_module', 'complete' );
$post = $this->make_post( 'test_post', 99 );
$this->interceptor->action_delete_post( 99, $post );
$this->assertStringContainsString( 'wp_test_table', self::$last_delete[0] ?? '' );
$this->assertSame( [ 'post_id' => 99 ], self::$last_delete[1] );
}
public function test_delete_post_skips_wrong_post_type(): void {
TMDO_Feature_Flags::set( 'test_module', 'complete' );
$post = $this->make_post( 'other_type', 99 );
$this->interceptor->action_delete_post( 99, $post );
$this->assertSame( [], self::$last_delete );
}
public function test_delete_post_skips_when_not_active(): void {
TMDO_Feature_Flags::set( 'test_module', 'idle' );
$post = $this->make_post( 'test_post', 99 );
$this->interceptor->action_delete_post( 99, $post );
$this->assertSame( [], self::$last_delete );
}
// ── filter_update_meta ────────────────────────────────────────────────────
public function test_update_meta_issues_sql_update_for_known_key(): void {
TMDO_Feature_Flags::set( 'test_module', 'complete' );
// get_post_type() stub returns 'test_post' for post ID 5.
$GLOBALS['_wp_post_types'][5] = 'test_post';
$result = $this->interceptor->filter_update_meta( null, 5, 'test_meta', 'newval', '' );
$this->assertNull( $result );
$this->assertStringContainsString( 'UPDATE', self::$last_query );
$this->assertStringContainsString( 'test_col', self::$last_query );
$this->assertStringContainsString( 'newval', self::$last_query );
}
public function test_update_meta_skips_unregistered_key(): void {
TMDO_Feature_Flags::set( 'test_module', 'complete' );
$GLOBALS['_wp_post_types'][5] = 'test_post';
$this->interceptor->filter_update_meta( null, 5, 'unknown_key', 'val', '' );
$this->assertSame( '', self::$last_query );
}
public function test_update_meta_skips_wrong_post_type(): void {
TMDO_Feature_Flags::set( 'test_module', 'complete' );
$GLOBALS['_wp_post_types'][5] = 'other_type';
$this->interceptor->filter_update_meta( null, 5, 'test_meta', 'val', '' );
$this->assertSame( '', self::$last_query );
}
public function test_update_meta_passes_through_check_unchanged(): void {
TMDO_Feature_Flags::set( 'test_module', 'complete' );
$GLOBALS['_wp_post_types'][5] = 'test_post';
$sentinel = 'original_check';
$result = $this->interceptor->filter_update_meta( $sentinel, 5, 'test_meta', 'v', '' );
$this->assertSame( $sentinel, $result );
}
// ── action_insert_post ────────────────────────────────────────────────────
public function test_insert_post_calls_wpdb_insert_for_new_post(): void {
TMDO_Feature_Flags::set( 'test_module', 'complete' );
$post = $this->make_post( 'test_post', 42 );
$this->interceptor->action_insert_post( 42, $post, false );
$this->assertStringContainsString( 'wp_test_table', self::$last_insert[0] ?? '' );
$this->assertSame( 42, self::$last_insert[1]['post_id'] ?? 0 );
}
public function test_insert_post_skips_updates(): void {
TMDO_Feature_Flags::set( 'test_module', 'complete' );
$post = $this->make_post( 'test_post', 42 );
$this->interceptor->action_insert_post( 42, $post, true );
$this->assertSame( [], self::$last_insert );
}
public function test_insert_post_skips_wrong_post_type(): void {
TMDO_Feature_Flags::set( 'test_module', 'complete' );
$post = $this->make_post( 'other_type', 42 );
$this->interceptor->action_insert_post( 42, $post, false );
$this->assertSame( [], self::$last_insert );
}
}