feat(growth): automate sales source reconciliation

This commit is contained in:
ogt
2026-07-14 18:37:14 +08:00
parent 5cdf1403bf
commit 3c142c1950
10 changed files with 705 additions and 43 deletions

View File

@@ -155,7 +155,19 @@
element.hidden = false;
}
function sourceStatusLabel(source) {
function sourceStatusLabel(source, runtimeState) {
const runtimeLabels = {
imported: '已匯入',
duplicate: '無需更新',
candidate_found: '處理中',
empty: '待新檔',
failed: '重試中',
not_ready: '修復中',
not_probed: '待探測',
ready: '可用',
disabled: '未啟用'
};
if (runtimeState && runtimeLabels[runtimeState]) return runtimeLabels[runtimeState];
if (source.ready) return '可用';
if (!source.enabled) return '未啟用';
return source.status === 'incomplete' ? '設定未完整' : source.status === 'path_missing' ? '路徑不存在' : '不可用';
@@ -176,14 +188,23 @@
const freshness = payload.freshness || {};
const coverage = payload.asset_coverage || {};
const receipt = payload.last_receipt || null;
const reconciliation = payload.source_reconciliation || {};
const runtimeSources = new Map((reconciliation.sources || []).map(source => [source.id, source]));
const nextRetry = reconciliation.next_retry_at;
const lag = freshness.data_lag_days;
document.getElementById('freshnessValue').textContent = freshness.latest_sales_date || '無資料';
document.getElementById('freshnessMeta').textContent = lag === null || lag === undefined ? '尚未建立基準' : lag === 0 ? '今天已更新' : `落後 ${lag}`;
document.getElementById('closureValue').textContent = freshness.decision_ready ? '可決策' : '受阻';
document.getElementById('closureValue').dataset.state = freshness.decision_ready ? 'ready' : 'blocked';
document.getElementById('closureMeta').textContent = freshness.decision_ready ? '新鮮度守門通過' : '等待最新業績';
document.getElementById('closureMeta').textContent = freshness.decision_ready
? '新鮮度守門通過'
: nextRetry
? `已排程 ${formatTime(nextRetry)}`
: '等待最新業績';
document.getElementById('sourceCoverageValue').textContent = `${coverage.ready || 0}/${coverage.total || 0}`;
document.getElementById('sourceCoverageMeta').textContent = `${coverage.percent || 0}% 來源可用`;
document.getElementById('sourceCoverageMeta').textContent = reconciliation.single_source_dependency
? '僅 1 個來源可用'
: `${coverage.percent || 0}% 來源可用`;
document.getElementById('lastRunValue').textContent = receipt ? formatTime(receipt.completed_at) : '尚無執行';
document.getElementById('lastRunMeta').textContent = receipt ? receiptStatusLabel(receipt.status) : '尚無執行紀錄';
document.getElementById('pageRuntimeSummary').textContent = freshness.decision_ready
@@ -191,13 +212,17 @@
: `正式業績${lag === null || lag === undefined ? '尚未建立' : `已落後 ${lag}`}`;
const list = document.getElementById('sourceList');
list.innerHTML = (payload.sources || []).map(source => `
list.innerHTML = (payload.sources || []).map(source => {
const runtime = runtimeSources.get(source.id) || {};
const state = runtime.runtime_state || (source.ready ? 'ready' : source.enabled ? 'not_ready' : 'disabled');
return `
<div class="ai-source-row">
<span class="ai-source-row__indicator ai-source-row__indicator--${source.ready ? 'ready' : source.enabled ? 'degraded' : 'disabled'}" aria-hidden="true"></span>
<span class="ai-source-row__indicator ai-source-row__indicator--${state === 'imported' || state === 'ready' ? 'ready' : state === 'disabled' ? 'disabled' : 'degraded'}" aria-hidden="true"></span>
<span class="ai-source-row__name">${escapeHtml(SOURCE_LABELS[source.id] || source.id)}</span>
<span class="ai-source-row__state">${escapeHtml(sourceStatusLabel(source))}</span>
<span class="ai-source-row__state">${escapeHtml(sourceStatusLabel(source, state))}</span>
</div>
`).join('');
`;
}).join('');
}
async function loadReadiness() {