[V10.334] 強化 PChome 比價重評與補抓可觀測性
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s

This commit is contained in:
OoO
2026-05-20 14:45:41 +08:00
parent 9c45a87723
commit b636303481
19 changed files with 1099 additions and 41 deletions

View File

@@ -163,6 +163,87 @@
color: var(--momo-accent-strong);
}
.dashboard-backfill-card {
display: grid;
grid-template-columns: minmax(220px, 1fr) minmax(160px, 280px) minmax(240px, 1fr) auto;
gap: 14px;
align-items: center;
min-width: 0;
margin-top: 12px;
padding: 14px 16px;
overflow: hidden;
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
border-radius: 8px;
}
.dashboard-backfill-card[data-status="running"] {
border-color: rgba(190, 106, 45, 0.36);
box-shadow: inset 3px 0 0 var(--momo-warm-caramel);
}
.dashboard-backfill-card[data-status="failed"],
.dashboard-backfill-card[data-status="stale"] {
border-color: rgba(188, 75, 49, 0.32);
box-shadow: inset 3px 0 0 var(--momo-danger);
}
.dashboard-backfill-main {
min-width: 0;
}
.dashboard-backfill-label {
margin-bottom: 3px;
color: var(--momo-text-tertiary);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.10em;
}
.dashboard-backfill-title {
color: var(--momo-text-primary);
font-size: 15px;
font-weight: 800;
line-height: 1.25;
}
.dashboard-backfill-meta,
.dashboard-backfill-status {
min-width: 0;
color: var(--momo-text-secondary);
font-size: 11px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.dashboard-backfill-progress {
position: relative;
width: 100%;
height: 8px;
overflow: hidden;
background: rgba(42, 37, 32, 0.08);
border: 1px solid rgba(42, 37, 32, 0.08);
border-radius: 999px;
}
.dashboard-backfill-progress span {
position: absolute;
inset: 0 auto 0 0;
width: 0%;
background: linear-gradient(90deg, var(--momo-warm-caramel), var(--momo-success));
transition: width 240ms ease;
}
.dashboard-backfill-card[data-status="failed"] .dashboard-backfill-progress span,
.dashboard-backfill-card[data-status="stale"] .dashboard-backfill-progress span {
background: linear-gradient(90deg, var(--momo-danger), var(--momo-warm-rust));
}
.dashboard-backfill-status {
display: grid;
gap: 2px;
}
.dashboard-focus-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -1151,6 +1232,16 @@
line-height: 1.35;
}
.dashboard-backfill-card {
grid-template-columns: 1fr;
gap: 10px;
padding: 14px;
}
.dashboard-backfill-card .dashboard-action-button {
width: 100%;
}
.dashboard-search,
.dashboard-select,
.dashboard-segmented {

View File

@@ -280,6 +280,142 @@ let priceChartInstance = null;
button.addEventListener('click', () => runDashboardTask(button.dataset.dashboardTask));
});
let pchomeBackfillPollTimer = null;
function getPchomeBackfillElements() {
const card = document.querySelector('[data-pchome-backfill-card]');
return {
card,
trigger: document.querySelector('[data-pchome-backfill-trigger]'),
status: document.querySelector('[data-pchome-backfill-status]'),
result: document.querySelector('[data-pchome-backfill-result]'),
progress: document.querySelector('[data-pchome-backfill-progress]'),
backfillEndpoint: card ? card.dataset.backfillEndpoint : '/api/ai/pchome-match/backfill',
statusEndpoint: card ? card.dataset.statusEndpoint : '/api/ai/pchome-match/backfill/status'
};
}
function formatBackfillCount(value) {
return Number(value || 0).toLocaleString();
}
function schedulePchomeBackfillPoll() {
if (pchomeBackfillPollTimer) {
clearTimeout(pchomeBackfillPollTimer);
}
pchomeBackfillPollTimer = setTimeout(loadPchomeBackfillStatus, 5000);
}
function renderPchomeBackfillStatus(payload) {
const status = payload && payload.data ? payload.data : (payload || {});
const elements = getPchomeBackfillElements();
if (!elements.card) return;
const currentRun = status.current_run || {};
const result = currentRun.result || status.last_result || {};
const pickResult = currentRun.pick_result || {};
const running = Boolean(status.running || currentRun.running);
const progressPct = Math.max(0, Math.min(Number(status.progress_pct || currentRun.progress_pct || 0), 100));
const statusKey = status.status || currentRun.status || 'idle';
const stageLabel = status.stage_label || currentRun.stage_label || '尚未執行';
const updatedAt = status.updated_at || currentRun.updated_at || currentRun.finished_at || '';
elements.card.dataset.status = statusKey;
if (elements.progress) {
elements.progress.style.width = `${progressPct}%`;
}
if (elements.status) {
elements.status.textContent = updatedAt ? `${stageLabel} · ${updatedAt}` : stageLabel;
}
if (elements.result) {
if (status.last_error || currentRun.last_error) {
elements.result.textContent = status.last_error || currentRun.last_error;
} else if (result && Object.keys(result).length > 0) {
const pickWritten = pickResult.written !== undefined ? ` · 挑品 ${formatBackfillCount(pickResult.written)}` : '';
elements.result.textContent = (
`比對 ${formatBackfillCount(result.total_skus)} · 成功 ${formatBackfillCount(result.matched)}`
+ ` · 待覆核 ${formatBackfillCount(result.skipped_low_score)}`
+ ` · 無結果 ${formatBackfillCount(result.skipped_no_result)}`
+ pickWritten
);
} else {
elements.result.textContent = running ? '正在累積結果' : '尚無最近結果';
}
}
if (elements.trigger) {
elements.trigger.disabled = running;
elements.trigger.classList.toggle('is-loading', running);
elements.trigger.innerHTML = running
? '<i class="fas fa-spinner fa-spin"></i> 補抓中'
: '<i class="fas fa-search"></i> 補抓 60 筆';
}
if (running) {
schedulePchomeBackfillPoll();
} else if (pchomeBackfillPollTimer) {
clearTimeout(pchomeBackfillPollTimer);
pchomeBackfillPollTimer = null;
}
}
function loadPchomeBackfillStatus() {
const elements = getPchomeBackfillElements();
if (!elements.card) return Promise.resolve();
return fetch(elements.statusEndpoint, {
headers: { 'Accept': 'application/json' }
})
.then(response => response.json())
.then(renderPchomeBackfillStatus)
.catch(error => {
console.warn('[DashboardV2] PChome backfill status load failed:', error);
if (elements.status) {
elements.status.textContent = '狀態讀取失敗';
}
});
}
function backfillPchomeMatches() {
const elements = getPchomeBackfillElements();
if (!elements.card || !elements.trigger) return;
const limit = Number(elements.trigger.dataset.limit || 60);
if (!confirm(`啟動 PChome 待比對補抓 ${limit} 筆?`)) return;
elements.trigger.disabled = true;
if (elements.status) {
elements.status.textContent = '正在送出補抓任務';
}
fetch(elements.backfillEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
},
body: JSON.stringify({ limit })
})
.then(response => response.json().then(data => ({ ok: response.ok, status: response.status, data })))
.then(({ ok, status, data }) => {
renderPchomeBackfillStatus(data);
if (!ok && status !== 409) {
throw new Error(data.message || data.error || 'PChome 補抓啟動失敗');
}
schedulePchomeBackfillPoll();
})
.catch(error => {
if (elements.status) {
elements.status.textContent = error.message || 'PChome 補抓啟動失敗';
}
if (elements.trigger) {
elements.trigger.disabled = false;
}
});
}
window.backfillPchomeMatches = backfillPchomeMatches;
document.querySelectorAll('[data-pchome-backfill-trigger]').forEach(button => {
button.addEventListener('click', backfillPchomeMatches);
});
loadPchomeBackfillStatus();
function runPchomeReviewDecision(button) {
const sku = button.dataset.reviewSku || '';
const action = button.dataset.reviewAction || '';