[V10.334] 強化 PChome 比價重評與補抓可觀測性
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 || '';
|
||||
|
||||
Reference in New Issue
Block a user