feat(growth): automate sales source reconciliation
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user