diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 160027f..7ef632e 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.542 拆開「可用比價覆蓋率」與「身份覆蓋率」:`decision_ready_rate = fresh identity / ACTIVE 商品數`,Dashboard 第一張 KPI 改顯示真正可進入決策、圖表、簡報的比價資料比例;daily / growth / Webcrumbs / OpenClaw payload 同步輸出,避免把身份覆蓋、新鮮率、價格可用率混成單一數字。 - V10.541 補正式覆核頁高信心 exact 線:The Ordinary 咖啡因 EGCG 單側漏 30ml、Natures Care 綿羊油同入數 125ml/125m、TOMOON 指甲剪同 L/S 尺寸、HH 私密潔淨露+衣物手洗精雙 200ml、SEBAMED 護潔露 200ml x2、YES 德悅氏 9cm 剪刀;都同步進 revalidation SQL,且 TOMOON/O.P.I 不同型號或尺寸仍不得自動通過。 - V10.540 補 O.P.I 類光繚指彩精準型號線:雙方同為 O.P.I 類光繚 / 如膠似漆指甲油或指彩,且共享 `ISL...` 型號 token 時才允許 total-price;不同型號/色號仍不得自動寫入。同步把此族群接進 `true_low_confidence` revalidation 窄門,降低高信心指彩候選卡在人工覆核池的比例。 - V10.539 補 PChome 任選 catalog focused exact 線:FLORTTE 水果沙拉眼線液筆 0.5ml、露得清護手霜 56g 無香/有香、Kanebo ALLIE 持采亮化 UV 防曬水凝乳 60g 雙方皆任選時可走 total-price;同步接進 revalidation queue。focused bypass 新增 commercial condition 防線,`即期品` 等商業狀態差異不會被自動寫入正式價差。 diff --git a/config.py b/config.py index ae4a5dd..82c92ed 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.541" +SYSTEM_VERSION = "V10.542" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 0aa31fb..f226d95 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -92,7 +92,7 @@ SQL漏斗(~300筆) - 比對覆蓋率補強入口:`POST /api/ai/pchome-match/backfill`,優先補抓仍無有效 PChome 配對的高價 ACTIVE 商品,完成後自動重算 AI 挑品清單。 - 過期價格刷新入口:`POST /api/ai/pchome-match/refresh-stale`,只針對已建立 `identity_v2` 但 `expires_at` 過期的 PChome product_id 執行 `run_expired_identity_refresh()`;不得跑 fresh search recovery,不得呼叫 LLM,完成後重算 AI 挑品並清除 Dashboard / competitor intel cache。 - 過期 identity 搜尋救援入口:`POST /api/ai/pchome-match/recover-stale` 預設必須關閉主操作入口,僅保留只讀 preview;正式 smoke 顯示小批次成功率不足且耗時偏高時,不得在 Dashboard 顯示日常操作按鈕。若需操作員手動執行,必須先明確設定 `PCHOME_STALE_RECOVERY_ENABLED=true`,再對已過期 `identity_v2` 先走既有 PChome product_id refresh;只有舊 ID 查無商品或重評低於門檻時,才允許受控 fresh search recovery。救援隊列必須先排除 variant、catalog、commercial condition、count、bundle、unit-price 與任選 / 多款 / 香味 / 色號 / 即期 / 融燭燈 / 香氛蠟燭 / `+` / `xN` / `*N` / 具名香味或膚感版本等高風險名稱訊號。這條路徑可抓 PChome,但不得呼叫 LLM;正式寫入仍必須通過 matcher、hard veto、auto price write safety 與 overwrite protection。 -- 補抓狀態入口:`GET /api/ai/pchome-match/backfill/status` 除背景任務狀態外,必須回傳 read-only coverage snapshot:`active_with_price` / `valid_matches` / `match_rate` / `fresh_matches` / `fresh_match_rate` / `stale_matches` / `pending` / `actionable_review_count`,供 Dashboard 顯示目前該刷新過期價格或補抓未搜尋商品;此端點不寫 DB、不呼叫 LLM、不抓外站。 +- 補抓狀態入口:`GET /api/ai/pchome-match/backfill/status` 除背景任務狀態外,必須回傳 read-only coverage snapshot:`active_with_price` / `valid_matches` / `match_rate` / `fresh_matches` / `fresh_match_rate` / `decision_ready_matches` / `decision_ready_rate` / `stale_matches` / `pending` / `actionable_review_count`,供 Dashboard 顯示目前該刷新過期價格或補抓未搜尋商品;此端點不寫 DB、不呼叫 LLM、不抓外站。`match_rate` 是身份覆蓋率,`fresh_match_rate` 是已配對 identity 內的新鮮比例,`decision_ready_rate` 才是可直接進入決策、圖表與簡報的 ACTIVE 商品比價覆蓋率。 - 排程閉環:`run_pchome_match_backfill_task` 每日 10:30 執行,補抓 PChome 待比對商品、寫入歷史價格,再重算 `strategy='product_pick'` 清單。 - PChome / MOMO 競價摘要出口 `services/competitor_intel_repository.py` 使用 30 分鐘共享快取(`COMPETITOR_INTEL_CACHE_TTL_SECONDS` 可調),避免 `/growth_analysis`、`/daily_sales`、PPT/AI 報表每次請求重跑昂貴覆蓋率與價差趨勢查詢;`run_competitor_price_feeder_task` 與 PChome backfill 完成後會主動清除快取。快取只包摘要輸出,不改 matcher 的高信心門檻與 identity_v2 準確性規則。 - 商品看板第一屏:`/` 的 V2 看板直接以 `products`、`price_records`、`competitor_prices`、`competitor_match_attempts`、`competitor_match_reviews`、`ai_price_recommendations` 顯示比對覆蓋率、PChome 優勢、MOMO 威脅、AI 挑品、待比對優先清單與 PChome 覆核隊列;`filter=ai_picks` 可查看 50 品 AI 挑品列表,`filter=pchome_review` 可直接查看需人工處理的比價覆核 SKU,並以 DB 分頁支援 search/category/status 後的完整隊列,不得只截前 50 筆。覆核狀態篩選必須至少包含全部、需單位價、已排除、低信心、價格過期、找不到同款與人工閉環,讓人工可依 matcher 診斷類型分批處理。列內顯示候選 PChome 商品、候選價、match score、單位價換算摘要、人工動作與 matcher 診斷原因標籤(品牌不符、商品線不符、容量差異、組合差異、需單位價、價差極端等),不得只顯示籠統「待比對」。`/api/export/excel/pchome-review` 必須匯出同一套覆核隊列、人工處置、候選 PChome、單位價比較與原始診斷,讓人工覆核、簡報與後續 AI 分析共用同一份證據。`/api/pchome-review//decision` 是人工閉環入口:`accept_identity` 才可把候選寫入 `competitor_prices` 與 `competitor_price_history` 並打上 `manual_review/manual_accept/identity_v2`;`reject_identity`、`unit_price_required` 與 `needs_research` 只寫 `competitor_match_reviews` 並追加 manual attempt,不得把不同販售組合或否決候選灌入正式價差。PChome feeder 後續搜尋同一候選時必須讀取 `competitor_match_reviews`:已否決候選寫 `manual_rejected` 並跳過正式寫入,且必須繼續評估下一個候選,不能讓已否決候選長期阻塞同 SKU;已標記單位價候選寫 `manual_unit_price_required`;已要求補搜尋候選寫 `manual_needs_research` 並停留在覆核隊列;已採用候選可保守補到最低門檻並保留 `manual_review/manual_accept` 標籤。搜尋候選池只有強同款分數達 `0.90` 才可提前停止,避免 0.76 灰區候選卡掉後續更精準搜尋詞。人工 `reject_identity`、`unit_price_required`、`needs_research` 若命中當前正式候選,必須將同候選 `competitor_prices` 過期,不得繼續顯示正式總價差。商品列表必須將 `manual_rejected`、`manual_unit_price_required`、`manual_needs_research` 顯示為明確人工閉環狀態,不可回落成籠統「待比對」。`fetch_competitor_coverage()` 必須輸出人工採用、人工否決、人工單位價與採用率,daily/growth/PPT 共用 payload 必須顯示人工閉環成效,避免只呈現待審數。商品看板深度快取同時寫入 `data/dashboard_full_cache.pkl`,供多個 Gunicorn worker 共用,避免部署後各 worker 重複重建 7,000+ 商品統計造成開頁變慢;所有資料異動與 AI 挑品重算都透過 `clear_dashboard_cache()` 同步清除記憶體與共享快取,手動重算 API 會立即預熱商品看板快取,避免第一位使用者承擔重建成本。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index c62da16..0be36e5 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **V10.542 可用比價覆蓋率口徑拆分**: `fetch_competitor_coverage()` 新增 `decision_ready_matches` / `decision_ready_rate`,以「高信心 identity 且價格仍新鮮」除以 ACTIVE 商品數,和 `match_rate`(身份覆蓋)及 `fresh_match_rate`(已配對中的新鮮率)分開。Dashboard 第一屏改顯示可用比價覆蓋率,daily / growth / Webcrumbs / OpenClaw payload 同步輸出,避免使用者把舊截圖的低價格可用率、身份覆蓋率與新鮮率混成同一個 KPI。 - **V10.541 高信心覆核池 exact 線再收斂**: 從正式覆核頁抓到多筆 `rescore_accepted_current` 仍卡人工的同款:The Ordinary 咖啡因 EGCG(MOMO 單側漏 30ml)、Natures Care 綿羊油同入數、TOMOON 指甲剪同 L/S 尺寸、HH 私密潔淨露+私密衣物手洗精雙 200ml、SEBAMED 護潔露 200ml x2、YES 德悅氏 9cm 剪刀。新增 focused total-price 規則與 revalidation SQL;TOMOON / O.P.I 仍要求同型號或同尺寸,避免跨款式誤配。 - **V10.540 O.P.I 指彩精準型號救回**: 正式覆核頁面顯示多筆 O.P.I 類光繚 / 如膠似漆指彩因 score 0.76~0.83 停在 `rescore_accepted_current` 人工池,但雙方都有明確 `ISL...` 型號(如 ISLL87、ISLL00、ISLN25)。新增 `opi_gel_polish_exact_model` focused total-price 規則:必須同品牌、同類光繚指彩線、共享型號 token;不同型號/色號不自動通過。同步接入 revalidation SQL,讓舊 `true_low_confidence` 指彩候選可批次重評。 - **V10.539 任選 catalog focused exact 與 commercial 防線**: 新增 FLORTTE 水果沙拉眼線液筆 0.5ml、露得清護手霜 56g 無香/有香、Kanebo ALLIE 持采亮化 UV 防曬水凝乳 60g 的 focused total-price 規則,條件是雙方同品牌、同品線、同規格且任選 catalog 語意一致;同步接入 `run_retryable_candidate_revalidation()`。同時修正 focused bypass:若存在 `commercial_condition_gap`(例如即期品),不得移除 `variant_selection_review`,避免商業狀態差異被自動寫入正式價差。 diff --git a/routes/ai_routes.py b/routes/ai_routes.py index f36f451..1a4e498 100644 --- a/routes/ai_routes.py +++ b/routes/ai_routes.py @@ -2137,6 +2137,10 @@ def _build_pchome_backfill_coverage_payload(): 'match_rate': float(coverage.get('match_rate') or 0), 'fresh_matches': int(coverage.get('fresh_matches') or 0), 'fresh_match_rate': float(coverage.get('fresh_match_rate') or 0), + 'decision_ready_matches': int( + coverage.get('decision_ready_matches') or coverage.get('fresh_matches') or 0 + ), + 'decision_ready_rate': float(coverage.get('decision_ready_rate') or 0), 'stale_matches': int(coverage.get('stale_matches') or 0), 'pending': int(coverage.get('pending') or 0), 'actionable_review_count': int(coverage.get('actionable_review_count') or 0), diff --git a/routes/dashboard_routes.py b/routes/dashboard_routes.py index af7badc..c476e7e 100644 --- a/routes/dashboard_routes.py +++ b/routes/dashboard_routes.py @@ -686,6 +686,8 @@ def _merge_competitor_review_context(overview, review_context): 'match_rate': coverage.get('match_rate', overview.get('match_rate') or 0), 'fresh_match_count': int(coverage.get('fresh_matches') or 0), 'fresh_match_rate': coverage.get('fresh_match_rate', 0), + 'decision_ready_count': int(coverage.get('decision_ready_matches') or coverage.get('fresh_matches') or 0), + 'decision_ready_rate': coverage.get('decision_ready_rate', 0), 'stale_match_count': int(coverage.get('stale_matches') or 0), 'pending_match_count': int(coverage.get('pending') or overview.get('pending_match_count') or 0), 'review_queue_count': int(coverage.get('actionable_review_count') or len(review_queue) or 0), @@ -829,6 +831,8 @@ def _load_competitor_decision_overview(session, latest_items=None): 'match_rate': 0, 'fresh_match_count': 0, 'fresh_match_rate': 0, + 'decision_ready_count': 0, + 'decision_ready_rate': 0, 'stale_match_count': 0, 'pchome_advantage_count': 0, 'momo_threat_count': 0, @@ -1686,6 +1690,8 @@ def _load_cached_competitor_overview_for_review(now_taipei, review_queue, review 'total_active': 0, 'matched_count': 0, 'match_rate': 0, + 'decision_ready_count': 0, + 'decision_ready_rate': 0, 'pchome_advantage_count': 0, 'momo_threat_count': 0, 'near_count': 0, diff --git a/services/competitor_intel_repository.py b/services/competitor_intel_repository.py index f6760dd..2b3fd39 100644 --- a/services/competitor_intel_repository.py +++ b/services/competitor_intel_repository.py @@ -705,7 +705,7 @@ def _cached_payload(cache_key: str, producer, ttl_seconds: int = COMPETITOR_INTE def fetch_competitor_coverage(engine) -> dict: return _cached_payload( - f"coverage:v7:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1:rescore=1:review_no_fresh=1", + f"coverage:v8:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1:rescore=1:review_no_fresh=1:decision_ready=1", lambda: _fetch_competitor_coverage_uncached(engine), ) @@ -723,8 +723,10 @@ def _fetch_competitor_coverage_uncached(engine) -> dict: "fresh_matches": 0, "stale_matches": 0, "pending": 0, + "decision_ready_matches": 0, "match_rate": 0, "fresh_match_rate": 0, + "decision_ready_rate": 0, "attempt_status": {}, "unit_comparable_count": 0, "rescore_accepted_count": 0, @@ -838,8 +840,10 @@ def _fetch_competitor_coverage_uncached(engine) -> dict: "fresh_matches": fresh, "stale_matches": stale, "pending": pending, + "decision_ready_matches": fresh, "match_rate": round(valid / max(active, 1) * 100, 1), "fresh_match_rate": round(fresh / max(valid, 1) * 100, 1), + "decision_ready_rate": round(fresh / max(active, 1) * 100, 1), "attempt_status": statuses, "unit_comparable_count": unit_count, "rescore_accepted_count": rescore_accepted_count, diff --git a/services/openclaw_strategist_service.py b/services/openclaw_strategist_service.py index 1edf48b..9bdbbe6 100644 --- a/services/openclaw_strategist_service.py +++ b/services/openclaw_strategist_service.py @@ -607,6 +607,10 @@ def _fetch_competitor_summary() -> Dict[str, Any]: "premium_count": int((row[3] if row else 0) or 0), "match_rate": float(coverage.get("match_rate") or 0), "active_with_price": int(coverage.get("active_with_price") or 0), + "decision_ready_matches": int( + coverage.get("decision_ready_matches") or coverage.get("fresh_matches") or 0 + ), + "decision_ready_rate": float(coverage.get("decision_ready_rate") or 0), "unit_comparable_count": int(coverage.get("unit_comparable_count") or 0), "rescore_accepted_count": int(coverage.get("rescore_accepted_count") or 0), "review_queue_count": int(coverage.get("actionable_review_count") or 0), diff --git a/services/webcrumbs_host_data_service.py b/services/webcrumbs_host_data_service.py index 290784b..d127581 100644 --- a/services/webcrumbs_host_data_service.py +++ b/services/webcrumbs_host_data_service.py @@ -80,6 +80,14 @@ def _coverage_metadata(coverage: dict, row_count: int) -> dict: "source": "competitor_intel_repository", "matched_count": int(coverage.get("valid_matches") or coverage.get("matched_count") or 0), "coverage_rate": _num(coverage.get("match_rate")), + "identity_coverage_rate": _num(coverage.get("match_rate")), + "decision_ready_count": int( + coverage.get("decision_ready_matches") + or coverage.get("fresh_matches") + or coverage.get("fresh_match_count") + or 0 + ), + "decision_ready_rate": _num(coverage.get("decision_ready_rate")), "fresh_match_count": int(coverage.get("fresh_matches") or coverage.get("fresh_match_count") or 0), "fresh_match_rate": _num(coverage.get("fresh_match_rate")), "stale_match_count": int(coverage.get("stale_matches") or coverage.get("stale_match_count") or 0), diff --git a/templates/daily_sales.html b/templates/daily_sales.html index 76def2b..8c5525b 100644 --- a/templates/daily_sales.html +++ b/templates/daily_sales.html @@ -343,6 +343,10 @@
+
+ 可用比價覆蓋率 + {{ comp_coverage.decision_ready_rate | default(0) }}% +
身份配對 {{ comp_coverage.valid_matches | default(0) | number_format }} @@ -353,7 +357,7 @@
價格新鮮 - {{ comp_coverage.fresh_matches | default(0) | number_format }} + {{ comp_coverage.decision_ready_matches | default(comp_coverage.fresh_matches | default(0)) | number_format }}
價格過期 diff --git a/templates/dashboard_v2.html b/templates/dashboard_v2.html index 31112cd..b344b7b 100644 --- a/templates/dashboard_v2.html +++ b/templates/dashboard_v2.html @@ -17,11 +17,11 @@
-
身份覆蓋率
-
{{ overview.match_rate | default(0) }}%
+
可用比價覆蓋率
+
{{ overview.decision_ready_rate | default(0) }}%
- {{ overview.matched_count | default(0) | number_format }} / {{ overview.total_active | default(total_products) | number_format }} ACTIVE - · 新鮮 {{ overview.fresh_match_count | default(0) | number_format }} + {{ overview.decision_ready_count | default(0) | number_format }} / {{ overview.total_active | default(total_products) | number_format }} ACTIVE + · 身份 {{ overview.match_rate | default(0) }}% · 過期 {{ overview.stale_match_count | default(0) | number_format }}
diff --git a/templates/growth_analysis.html b/templates/growth_analysis.html index e06c676..d41f041 100644 --- a/templates/growth_analysis.html +++ b/templates/growth_analysis.html @@ -141,6 +141,8 @@
高信心門檻 {{ coverage.match_score_floor | default(0.76) }} + 可用比價覆蓋率 + {{ coverage.decision_ready_rate | default(0) }}% 身份配對 {{ coverage.valid_matches | default(0) | number_format }} 身份覆蓋率 diff --git a/tests/test_competitor_intel_cache.py b/tests/test_competitor_intel_cache.py index aa175b3..33dc84d 100644 --- a/tests/test_competitor_intel_cache.py +++ b/tests/test_competitor_intel_cache.py @@ -75,7 +75,7 @@ def test_competitor_coverage_counts_only_active_product_intersection(): "def _fetch_manual_review_summary", 1 )[0] - assert "coverage:v7" in source + assert "coverage:v8" in source assert "rescore_accepted_count" in coverage_source assert "(SELECT COUNT(*) FROM valid_competitor) AS valid_matches" not in coverage_source assert "identity_competitor AS" in coverage_source @@ -85,6 +85,8 @@ def test_competitor_coverage_counts_only_active_product_intersection(): assert "WHERE fc.sku IS NULL" in coverage_source assert "\"fresh_matches\": fresh" in coverage_source assert "\"stale_matches\": stale" in coverage_source + assert "\"decision_ready_matches\": fresh" in coverage_source + assert "\"decision_ready_rate\": round(fresh / max(active, 1) * 100, 1)" in coverage_source assert "FROM products p\n JOIN LATERAL" in coverage_source assert "WHERE p.status = 'ACTIVE'" in coverage_source @@ -129,6 +131,7 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff(): assert "competitor_intel.review_queue" in daily_template assert "coverage.fresh_matches" in growth_template assert "coverage.fresh_match_rate" in growth_template + assert "coverage.decision_ready_rate" in growth_template assert "coverage.stale_matches" in growth_template assert "coverage.unit_comparable_count" in growth_template assert "coverage.rescore_accepted_count" in growth_template @@ -139,6 +142,7 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff(): assert "comp_coverage.rescore_accepted_count" in daily_template assert "重算待人工覆核" in daily_template assert "comp_coverage.stale_matches" in daily_template + assert "comp_coverage.decision_ready_rate" in daily_template def test_competitor_review_filters_split_low_score_operational_buckets(): diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index 7cca949..34cbbba 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -542,6 +542,7 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action(): assert "JSON.stringify({ limit: 50 })" in template assert "完成後會重算 AI 挑品清單" in route_source assert "match_rate" in route_source + assert "decision_ready_rate" in route_source assert "product_pick_count" in route_source dashboard_route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8") dashboard_template = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8") @@ -586,6 +587,7 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action(): assert "formatBackfillCoverageSummary" in dashboard_js assert "formatBackfillLimitedCount" in dashboard_js assert "status.coverage" in dashboard_js + assert "可用比價 ${formatBackfillRate(coverage.decision_ready_rate)}" in dashboard_js assert "待刷新 ${formatBackfillCount(coverage.stale_matches)}" in dashboard_js assert "待補抓 ${formatBackfillCount(coverage.pending)}" in dashboard_js assert "可重評 ${formatBackfillLimitedCount(preview.candidate_count" in dashboard_js diff --git a/tests/test_webcrumbs_host_data_service.py b/tests/test_webcrumbs_host_data_service.py index 71af86b..9226309 100644 --- a/tests/test_webcrumbs_host_data_service.py +++ b/tests/test_webcrumbs_host_data_service.py @@ -43,6 +43,8 @@ def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch): "match_rate": 12.3, "fresh_matches": 70, "fresh_match_rate": 79.5, + "decision_ready_matches": 70, + "decision_ready_rate": 9.8, "stale_matches": 18, "pending": 612, }, @@ -62,6 +64,9 @@ def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch): assert payload["metadata"]["fetches_external"] is False assert payload["metadata"]["matched_count"] == 88 assert payload["metadata"]["coverage_rate"] == 12.3 + assert payload["metadata"]["identity_coverage_rate"] == 12.3 + assert payload["metadata"]["decision_ready_count"] == 70 + assert payload["metadata"]["decision_ready_rate"] == 9.8 assert payload["metadata"]["fresh_match_count"] == 70 assert payload["metadata"]["fresh_match_rate"] == 79.5 assert payload["metadata"]["stale_match_count"] == 18 @@ -82,6 +87,7 @@ def test_webcrumbs_host_data_uses_empty_state_without_risks(monkeypatch): assert payload["marketSnapshot"][0]["freshness_status"] == "no_current_exact_risk" assert payload["aiCandidate"]["release_status"] == "blocked" assert payload["metadata"]["matched_count"] == 3 + assert payload["metadata"]["decision_ready_count"] == 1 assert payload["metadata"]["fresh_match_count"] == 1 assert payload["metadata"]["stale_match_count"] == 2 assert payload["metadata"]["pending_match_count"] == 9 diff --git a/web/static/js/page-dashboard-v2.js b/web/static/js/page-dashboard-v2.js index 4b1f8d5..62f49c7 100644 --- a/web/static/js/page-dashboard-v2.js +++ b/web/static/js/page-dashboard-v2.js @@ -328,7 +328,8 @@ let priceChartInstance = null; ? ` · 可救援 ${formatBackfillLimitedCount(staleRecovery.candidate_count, staleRecovery.has_more)}` : ''; return ( - `身份覆蓋 ${formatBackfillRate(coverage.match_rate)}` + `可用比價 ${formatBackfillRate(coverage.decision_ready_rate)}` + + ` · 身份 ${formatBackfillRate(coverage.match_rate)}` + ` · 新鮮 ${formatBackfillRate(coverage.fresh_match_rate)}` + ` · 待刷新 ${formatBackfillCount(coverage.stale_matches)}` + ` · 待補抓 ${formatBackfillCount(coverage.pending)}`