diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 4090331..ecafb64 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.593 重整 PChome 比價覆核工作台 UX:覆核頁不再沿用首頁商品表格,也不再把 `matcher_rescore`、`stored_status`、`rescore_accepted_current`、`HITL`、`COMPLETE` 等內部診斷/狀態碼輸出到前台;改為「商品 / MOMO、PChome 候選、覆核判讀、下一步、紀錄」六欄工作流。同步修正 catalog review status 的前台語義、決策信封中文標籤、局部 1540px 橫向工作台與手機版欄位 label,並補 `test_frontend_v2_assets.py` guard 防止 raw diagnostics 回流。 - V10.584 補 PChome Nick 去重與 stale recovery 單品窄門:`Nick` 先去 HTML / 行銷星號 / 重複品名,避免 `29g`、`100ml` 被同一商品副標重複計數成 `component_count_conflict`;同步新增 NIVEA 妮維雅霜 100ml、Schick 舒綺敏感肌除毛刀片 3 入、TS6 沁涼潔淨慕斯 100g 的具名 exact total-price alignment。IBL 沐浴精+洗髮精 vs 洗髮精仍保留 identity review,唇釉色號/目錄款與 Paula's Choice 效期/金蓋差異仍不自動寫正式價差。 - V10.583 補 Paula's Choice 身體乳 PChome Nick 具名 alignment:`2%水楊酸身體乳210ml二入` 可和 PChome `Nick` 補出的 `水楊酸身體乳雙入組 / 210ml x2` 對齊,進 `exact / total_price / price_alert_exact`;但 `118ml二入組(金蓋限定版)` 對上 PChome 效期品仍保留 `manual_review / identity_review`,不泛用放寬中文入數。 - V10.582 補 PChome 比價通知專業分級與 Nick 副標身份證據:NemoTron 價格決策信封現在保留 `momo_price`、`competitor_price`、`candidate_gap_pct` 與 `sales_7d_delta_pct`,EventRouter / Telegram 模板會把 `match_type / price_basis / alert_tier` 翻成「直接價格威脅、單位價覆核、身份覆核、壓制告警」與操作邊界;PChome crawler 會保留 `Nick` 副標為 `match_name` 給 matcher 使用,UI/DB 顯示仍維持原品名,讓容量、入數、濃度資訊可參與比對。 diff --git a/config.py b/config.py index a7eda11..796ccb3 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.591" +SYSTEM_VERSION = "V10.593" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/routes/dashboard_routes.py b/routes/dashboard_routes.py index 1be3118..6081a02 100644 --- a/routes/dashboard_routes.py +++ b/routes/dashboard_routes.py @@ -167,7 +167,7 @@ def _build_pchome_match_status(attempt=None, ineligible=None): 'tone': 'watch', 'score': score, 'summary': f'{score_text},最新版 matcher 已通過重算覆核門檻;仍需人工確認身份後才可寫入正式 PChome 價差', - 'detail': attempt.get('error_message') + 'detail': score_text, } if status == 'manual_accepted': score = _to_float(attempt.get('best_match_score')) @@ -264,6 +264,32 @@ def _build_pchome_match_status(attempt=None, ineligible=None): 'summary': '候選同核心商品,但販售組合/買送不同;不可直接比總價,需用單位價或人工覆核', 'detail': score_text, } + if status in {'catalog_variant_review', 'catalog_unit_review', 'catalog_identity_review'}: + score = _to_float(attempt.get('best_match_score')) + score_text = f"候選信心 {round(score * 100)}%" if score is not None else "候選信心待補" + candidate_count = int(attempt.get('candidate_count') or 0) + catalog_labels = { + 'catalog_variant_review': ( + '選項 / 色號待核', + '同品線候選已找到,但色號、香味、款式或任選組合仍需人工確認,避免錯配成正式價差', + ), + 'catalog_unit_review': ( + '單位價 / 入數待核', + '同核心商品可比,但容量、入數或買送組合不同;需先換算單位價與檔期條件', + ), + 'catalog_identity_review': ( + '身份採用待核', + '候選具備高信心身份證據,需人工最後確認後才寫入正式 PChome 價差', + ), + } + label, summary = catalog_labels[status] + return { + 'label': label, + 'tone': 'watch', + 'blocks_price_gap': True, + 'summary': summary, + 'detail': f'{score_text} / {candidate_count} 筆候選' if candidate_count else score_text, + } if ineligible: reason = ineligible.get('reason') or 'not_eligible' @@ -351,13 +377,13 @@ def _build_pchome_match_status(attempt=None, ineligible=None): return { 'label': '抓取異常', 'tone': 'risk', - 'summary': 'PChome 比對流程發生錯誤,請查看嘗試紀錄', - 'detail': attempt.get('error_message'), + 'summary': 'PChome 比對流程發生異常,請補抓或查看後台嘗試紀錄', + 'detail': f'{candidate_count} 筆候選' if candidate_count else '流程異常', } return { 'label': '狀態待釐清', 'tone': 'neutral', - 'summary': '已有比對紀錄但尚未分類,需檢查 matcher diagnostics 或補抓紀錄', + 'summary': '已有比對紀錄但尚未分類,需檢查補抓紀錄或重新排入覆核', 'detail': score_text, } diff --git a/templates/dashboard_v2.html b/templates/dashboard_v2.html index c422d13..9f38ad5 100644 --- a/templates/dashboard_v2.html +++ b/templates/dashboard_v2.html @@ -242,18 +242,19 @@ 候選 PChome ${{ item.candidate_pc_price | int | number_format }} {% endif %} {% if item.best_match_score %} - match {{ (item.best_match_score * 100) | round(0) | int }}% + 信心 {{ (item.best_match_score * 100) | round(0) | int }}% {% endif %}
{{ item.action_label }}
{% if item.decision_envelope %} {% set envelope = item.decision_envelope %} {% set guardrails = envelope.guardrails or {} %} + {% set data_quality = guardrails.data_quality | default('partial', true) | lower %}
- {{ envelope.severity or 'P4' }} - {{ guardrails.data_quality or 'partial' }} + 優先 {{ envelope.severity or 'P4' }} + {{ {'complete': '證據完整', 'partial': '部分證據', 'missing': '證據不足'}.get(data_quality, '部分證據') }} {% if guardrails.can_auto_execute == false %} - HITL + 需人工 {% endif %}
{% endif %} @@ -452,33 +453,42 @@ {% endif %} -
+
- - - - - - - {% if current_filter == 'ai_picks' %} - - {% elif current_filter == 'pchome_review' %} - - {% endif %} - - - - - + {% if current_filter == 'pchome_review' %} + + + + + + + + + {% else %} + + + + + + + {% if current_filter == 'ai_picks' %} + + {% endif %} + + + + + + {% endif %} {% for item in items %} @@ -487,6 +497,166 @@ {% set decision = item.competitor_decision %} {% set match_status = item.pchome_match_status %} {% set image_url = product.image_url or ('https://m.momoshop.com.tw/moscdn/goods/' ~ product.i_code ~ '_m.webp') %} + {% set safe_product_url = item.safe_momo_url or '#' %} + {% if current_filter == 'pchome_review' %} + {% set review = item.pchome_review %} + {% set envelope = review.decision_envelope if review else {} %} + {% set guardrails = envelope.guardrails if envelope else {} %} + + + + + + + + + {% else %} + {% endif %} {% else %} -
分類商品名稱 - MOMO 價格 - PChome 價格競價判讀AI 建議覆核動作 - 昨日漲跌 - - 週漲跌 - - 更新時間 - 上架時間
分類商品 / MOMOPChome 候選覆核判讀下一步紀錄
分類商品名稱 + MOMO 價格 + PChome 價格競價判讀AI 建議 + 昨日漲跌 + + 週漲跌 + + 更新時間 + 上架時間
{{ product.category or '未分類' }} +
+
+ {{ product.name }} +
+ {{ product.name }} +
+ MOMO {{ product.i_code }} + ${{ item.record.price | int | number_format }} +
+
+
+
+
+
+ {% if review and review.candidate_pc_id %} + + {{ review.candidate_pc_name or ('PChome ' ~ review.candidate_pc_id) }} + +
+ {% if review.candidate_pc_price %} + ${{ review.candidate_pc_price | int | number_format }} + {% endif %} + PChome {{ review.candidate_pc_id }} + {% if review.candidate_count %} + {{ review.candidate_count }} 筆候選 + {% endif %} +
+ {% elif competitor and competitor.product_id %} + + {{ competitor.product_name or ('PChome ' ~ competitor.product_id) }} + +
+ {% if competitor.price %} + ${{ competitor.price | int | number_format }} + {% endif %} + PChome {{ competitor.product_id }} +
+ {% else %} + {{ match_status.label | default('尚未搜尋') }} +
{{ match_status.summary | default('尚未進入 PChome 補抓佇列') }}
+ {% endif %} +
+
+
+
+ {{ review.status_label if review else match_status.label }} + {% if review and review.best_match_score %} + 信心 {{ (review.best_match_score * 100) | round(0) | int }}% + {% endif %} +
+
{{ review.action_label if review else match_status.summary }}
+ {% if review and review.identity_evidence_summary %} +
證據:{{ review.identity_evidence_summary }}
+ {% endif %} + {% if review and review.diagnostic_reasons %} +
+ {% for reason in review.diagnostic_reasons[:4] %} + {{ reason.label }} + {% endfor %} +
+ {% endif %} + {% if review and review.unit_comparison and review.unit_comparison.summary %} +
{{ review.unit_comparison.summary }}
+ {% endif %} + {% if review and review.unit_price_insight and review.unit_price_insight.summary %} +
{{ review.unit_price_insight.summary }}
+ {% endif %} + {% if envelope %} + {% set data_quality = guardrails.data_quality | default('partial', true) | lower %} +
+ 優先 {{ envelope.severity or 'P4' }} + {{ {'complete': '證據完整', 'partial': '部分證據', 'missing': '證據不足'}.get(data_quality, '部分證據') }} + {% if guardrails.can_auto_execute == false %} + 需人工 + {% endif %} + {% if envelope.decision_id %} + 追蹤 + {% endif %} +
+ {% endif %} +
+
+
+ {% if review and review.catalog_review_guidance %} +
{{ review.catalog_review_guidance.lane_label }}
+
{{ review.catalog_review_guidance.action_hint }}
+ {% else %} +
人工覆核
+
{{ match_status.summary | default('確認候選是否同款,再決定是否採用。') }}
+ {% endif %} + {% if review %} +
+ {% if review.candidate_pc_id and review.candidate_pc_price %} + + {% endif %} + + + +
+ {% endif %} +
+
+ {% if review and review.attempted_at %} +
{{ review.attempted_at }}
+ {% else %} +
{{ item.record.timestamp.strftime('%m-%d %H:%M') if item.record.timestamp else '--' }}
+ {% endif %} +
+ {% if review %} + 覆核紀錄 + {% endif %} + {% if guardrails and guardrails.can_auto_execute == false %} + 需人工 + {% endif %} +
+
{{ product.category or '未分類' }} @@ -565,7 +735,7 @@ {% if competitor and competitor.price %}
${{ competitor.price | int | number_format }}
{% if competitor.match_score %} -
match {{ (competitor.match_score * 100) | round(0) | int }}%
+
信心 {{ (competitor.match_score * 100) | round(0) | int }}%
{% endif %} {% elif item.pchome_match_attempt and item.pchome_match_attempt.best_competitor_price and match_status.label == '需單位價比較' %}
${{ item.pchome_match_attempt.best_competitor_price | int | number_format }}
@@ -632,19 +802,20 @@
{{ review.status_label if review else match_status.label }} {% if review and review.best_match_score %} - match {{ (review.best_match_score * 100) | round(0) | int }}% + 信心 {{ (review.best_match_score * 100) | round(0) | int }}% {% endif %}
{{ review.action_label if review else decision.summary }}
{% if envelope %} + {% set data_quality = guardrails.data_quality | default('partial', true) | lower %}
- {{ envelope.severity or 'P4' }} - {{ guardrails.data_quality or 'partial' }} + 優先 {{ envelope.severity or 'P4' }} + {{ {'complete': '證據完整', 'partial': '部分證據', 'missing': '證據不足'}.get(data_quality, '部分證據') }} {% if guardrails.can_auto_execute == false %} - HITL + 需人工 {% endif %} {% if envelope.decision_id %} - trace + 追蹤 {% endif %}
{% endif %} @@ -750,9 +921,10 @@ {{ item.safe_created_at.strftime('%m-%d %H:%M') if item.safe_created_at else '--' }}
+
{% if search_query %} 找不到與「{{ search_query }}」相關的商品 diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index f0b54e4..9b62fd8 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -228,7 +228,7 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data(): assert "人工閉環" in route_source assert "AI 挑品清單" in dashboard assert "比價覆核隊列" in dashboard - assert "覆核動作" in dashboard + assert "下一步" in dashboard assert "dashboard-ai-summary-grid" in dashboard assert "AI 建議" in dashboard assert "/api/export/excel/ai-picks" in dashboard @@ -265,6 +265,7 @@ def test_pchome_review_export_and_diagnostics_use_real_queue_data(): repository_source = (ROOT / "services/competitor_intel_repository.py").read_text(encoding="utf-8") dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8") dashboard_css = (ROOT / "web/static/css/page-dashboard-v2.css").read_text(encoding="utf-8") + page_guard_css = (ROOT / "web/static/css/ewoooc-v3-page-guard.css").read_text(encoding="utf-8") assert "@export_bp.route('/api/export/excel/pchome-review')" in export_source assert "fetch_competitor_review_queue_page" in export_source @@ -284,13 +285,40 @@ def test_pchome_review_export_and_diagnostics_use_real_queue_data(): assert "多款任選待確認" in repository_source assert "妝效質地不同" in route_source assert "_extract_match_diagnostic_reasons" in route_source + assert "'detail': attempt.get('error_message')" not in route_source + assert "需檢查 matcher diagnostics" not in route_source assert "匯出覆核" in dashboard + assert "商品 / MOMO" in dashboard + assert "PChome 候選" in dashboard + assert "覆核判讀" in dashboard + assert "dashboard-review-workbench-row" in dashboard + assert "dashboard-review-candidate-title" in dashboard + assert "dashboard-review-next-step" in dashboard + assert "is-review-wrap" in dashboard + assert "6 if current_filter == 'pchome_review'" in dashboard + assert "review.review_bucket" not in dashboard + assert "HITL" not in dashboard + assert "stored_status" not in dashboard + assert "stored_score" not in dashboard + assert "matcher_rescore" not in dashboard + assert "優先 {{ envelope.severity" in dashboard + assert "證據完整" in dashboard + assert "需人工" in dashboard assert "review.decision_envelope" in dashboard assert "dashboard-review-envelope" in dashboard assert "review.diagnostic_reasons" in dashboard assert "item.pchome_match_attempt.diagnostic_reasons" in dashboard assert "dashboard-review-reasons" in dashboard assert "dashboard-review-actions" in dashboard + assert ".dashboard-review-workbench-row" in dashboard_css + assert ".dashboard-review-candidate-title" in dashboard_css + assert ".dashboard-review-next-step" in dashboard_css + assert ".dashboard-table.is-review" in dashboard_css + assert "min-width: 1540px" in dashboard_css + assert ".dashboard-table-wrap.is-review-wrap" in page_guard_css + assert "width: max(100%, 1540px) !important" in page_guard_css + assert 'content: "商品/MOMO"' in page_guard_css + assert 'content: "PChome候選"' in page_guard_css assert ".dashboard-review-reasons" in dashboard_css assert ".dashboard-review-envelope" in dashboard_css assert ".dashboard-review-actions" in dashboard_css diff --git a/web/static/css/ewoooc-v3-page-guard.css b/web/static/css/ewoooc-v3-page-guard.css index b677627..51f1f90 100644 --- a/web/static/css/ewoooc-v3-page-guard.css +++ b/web/static/css/ewoooc-v3-page-guard.css @@ -288,6 +288,17 @@ width: 36%; } +.momo-app:not(.momo-observability-mode) .dashboard-table-wrap.is-review-wrap { + overflow-x: auto !important; + -webkit-overflow-scrolling: touch; +} + +.momo-app:not(.momo-observability-mode) .dashboard-table.is-review { + width: max(100%, 1540px) !important; + min-width: 1540px !important; + table-layout: fixed; +} + @media (max-width: 768px) { .momo-app:not(.momo-observability-mode) { --momo-page-title-size: 1.55rem; @@ -449,11 +460,11 @@ .momo-app:not(.momo-observability-mode) .dashboard-table.is-ai-picks td:nth-child(8)::before { content: "週"; } .momo-app:not(.momo-observability-mode) .dashboard-table.is-ai-picks td:nth-child(9)::before { content: "更新"; } .momo-app:not(.momo-observability-mode) .dashboard-table.is-ai-picks td:nth-child(10)::before { content: "上架"; } - .momo-app:not(.momo-observability-mode) .dashboard-table.is-review td:nth-child(6)::before { content: "覆核"; } - .momo-app:not(.momo-observability-mode) .dashboard-table.is-review td:nth-child(7)::before { content: "昨日"; } - .momo-app:not(.momo-observability-mode) .dashboard-table.is-review td:nth-child(8)::before { content: "週"; } - .momo-app:not(.momo-observability-mode) .dashboard-table.is-review td:nth-child(9)::before { content: "更新"; } - .momo-app:not(.momo-observability-mode) .dashboard-table.is-review td:nth-child(10)::before { content: "上架"; } + .momo-app:not(.momo-observability-mode) .dashboard-table.is-review td:nth-child(2)::before { content: "商品/MOMO"; } + .momo-app:not(.momo-observability-mode) .dashboard-table.is-review td:nth-child(3)::before { content: "PChome候選"; } + .momo-app:not(.momo-observability-mode) .dashboard-table.is-review td:nth-child(4)::before { content: "覆核判讀"; } + .momo-app:not(.momo-observability-mode) .dashboard-table.is-review td:nth-child(5)::before { content: "下一步"; } + .momo-app:not(.momo-observability-mode) .dashboard-table.is-review td:nth-child(6)::before { content: "紀錄"; } .momo-app:not(.momo-observability-mode) .edm-page .campaign-table td:nth-child(1)::before { content: "分類"; } .momo-app:not(.momo-observability-mode) .edm-page .campaign-table td:nth-child(2)::before { content: "商品"; } diff --git a/web/static/css/page-dashboard-v2.css b/web/static/css/page-dashboard-v2.css index a9df943..01c86e0 100644 --- a/web/static/css/page-dashboard-v2.css +++ b/web/static/css/page-dashboard-v2.css @@ -786,7 +786,32 @@ } .dashboard-table.is-review { - min-width: 1460px; + min-width: 1540px; + table-layout: fixed; + } + + .dashboard-table.is-review th:nth-child(1) { + width: 120px; + } + + .dashboard-table.is-review th:nth-child(2) { + width: 340px; + } + + .dashboard-table.is-review th:nth-child(3) { + width: 360px; + } + + .dashboard-table.is-review th:nth-child(4) { + width: 330px; + } + + .dashboard-table.is-review th:nth-child(5) { + width: 250px; + } + + .dashboard-table.is-review th:nth-child(6) { + width: 140px; } .dashboard-table th { @@ -813,6 +838,17 @@ vertical-align: middle; } + .dashboard-table.is-review th { + position: sticky; + top: 0; + z-index: 2; + box-shadow: 0 1px 0 var(--momo-border-light); + } + + .dashboard-table.is-review td { + padding: 16px 14px; + } + .dashboard-table tbody tr { transition: var(--momo-transition-base); } @@ -1115,6 +1151,111 @@ gap: 6px; } + .dashboard-review-workbench-row td { + vertical-align: top; + } + + .dashboard-table.is-review .dashboard-product-cell { + align-items: flex-start; + } + + .dashboard-table.is-review .dashboard-product-thumb { + width: 46px; + height: 46px; + } + + .dashboard-table.is-review .dashboard-product-name { + -webkit-line-clamp: 3; + } + + .dashboard-review-workbench-row:hover { + background: + radial-gradient(circle at 8px 8px, rgba(47, 43, 38, 0.06) 1px, transparent 1.4px), + var(--momo-bg-paper); + background-size: 16px 16px, auto; + } + + .dashboard-review-product-stack, + .dashboard-review-candidate, + .dashboard-review-decision, + .dashboard-review-next-step, + .dashboard-review-record { + display: grid; + gap: 7px; + min-width: 0; + } + + .dashboard-review-product-meta, + .dashboard-review-candidate-meta, + .dashboard-review-record-tags { + display: flex; + flex-wrap: wrap; + gap: 5px; + min-width: 0; + color: var(--momo-text-tertiary); + font-family: var(--momo-font-family-mono); + font-size: 10px; + font-weight: 800; + line-height: 1.35; + } + + .dashboard-review-product-meta span, + .dashboard-review-candidate-meta span, + .dashboard-review-record-tags span { + display: inline-flex; + max-width: 100%; + align-items: center; + padding: 2px 7px; + overflow: hidden; + background: var(--momo-bg-paper); + border: 1px solid var(--momo-border-light); + border-radius: var(--momo-radius-pill); + text-overflow: ellipsis; + white-space: nowrap; + } + + .dashboard-review-candidate-title { + display: -webkit-box; + overflow: hidden; + color: var(--momo-text-primary); + font-size: 13px; + font-weight: 800; + line-height: 1.45; + text-decoration: none; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + } + + .dashboard-review-candidate-title:hover { + color: var(--momo-accent-strong); + } + + .dashboard-review-decision-head { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + min-width: 0; + } + + .dashboard-review-next-title { + color: var(--momo-text-primary); + font-size: 12px; + font-weight: 900; + line-height: 1.35; + } + + .dashboard-review-record { + justify-items: end; + color: var(--momo-text-secondary); + font-size: 11px; + line-height: 1.45; + } + + .dashboard-review-record-tags { + justify-content: flex-end; + } + .dashboard-review-reasons { display: flex; flex-wrap: wrap; @@ -1506,7 +1647,7 @@ } .dashboard-table-wrap::before { - content: '左右滑動查看完整商品列表'; + content: '左右滑動查看完整表格'; position: sticky; left: 0; display: block;