feat(market-intel): 新增既有資料橋接預覽
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s

This commit is contained in:
OoO
2026-05-18 14:19:43 +08:00
parent c021945047
commit bb6a862dbe
9 changed files with 762 additions and 4 deletions

View File

@@ -396,6 +396,24 @@
</div>
</div>
<div class="market-intel-panel" data-market-intel-legacy-bridge>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">LEGACY SOURCE / BRIDGE PREVIEW</p>
<h2 class="market-intel-preview-title">既有資料橋接預覽</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理橋接預覽" data-market-intel-legacy-bridge-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-legacy-bridge-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-legacy-bridge-body>
<div class="market-intel-empty">讀取既有資料橋接預覽中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-migration>
<div class="market-intel-preview-head">
<div>
@@ -460,10 +478,11 @@
const cliRoot = document.querySelector('[data-market-intel-cli]');
const dbProbeRoot = document.querySelector('[data-market-intel-db-probe]');
const seedDiffRoot = document.querySelector('[data-market-intel-seed-diff]');
const legacyBridgeRoot = document.querySelector('[data-market-intel-legacy-bridge]');
const migrationRoot = document.querySelector('[data-market-intel-migration]');
const approvalRoot = document.querySelector('[data-market-intel-approval]');
const deployRoot = document.querySelector('[data-market-intel-deploy]');
if (!root && !writerRoot && !cliRoot && !dbProbeRoot && !seedDiffRoot && !migrationRoot && !approvalRoot && !deployRoot) return;
if (!root && !writerRoot && !cliRoot && !dbProbeRoot && !seedDiffRoot && !legacyBridgeRoot && !migrationRoot && !approvalRoot && !deployRoot) return;
const meta = root ? root.querySelector('[data-market-intel-preview-meta]') : null;
const body = root ? root.querySelector('[data-market-intel-preview-body]') : null;
@@ -485,6 +504,10 @@
const seedDiffBody = seedDiffRoot ? seedDiffRoot.querySelector('[data-market-intel-seed-diff-body]') : null;
const seedDiffRefresh = seedDiffRoot ? seedDiffRoot.querySelector('[data-market-intel-seed-diff-refresh]') : null;
const seedDiffEndpoint = "{{ url_for('market_intel.market_intel_platform_seed_db_diff') }}?execute=false";
const legacyBridgeMeta = legacyBridgeRoot ? legacyBridgeRoot.querySelector('[data-market-intel-legacy-bridge-meta]') : null;
const legacyBridgeBody = legacyBridgeRoot ? legacyBridgeRoot.querySelector('[data-market-intel-legacy-bridge-body]') : null;
const legacyBridgeRefresh = legacyBridgeRoot ? legacyBridgeRoot.querySelector('[data-market-intel-legacy-bridge-refresh]') : null;
const legacyBridgeEndpoint = "{{ url_for('market_intel.market_intel_legacy_source_bridge') }}?execute=false&limit=5";
const migrationMeta = migrationRoot ? migrationRoot.querySelector('[data-market-intel-migration-meta]') : null;
const migrationBody = migrationRoot ? migrationRoot.querySelector('[data-market-intel-migration-body]') : null;
const migrationRefresh = migrationRoot ? migrationRoot.querySelector('[data-market-intel-migration-refresh]') : null;
@@ -730,6 +753,95 @@
}
};
const renderLegacyBridgeMeta = data => {
legacyBridgeMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`execute=${data.execute_requested ? 'true' : 'false'}`,
`query=${data.read_only_query_executed ? 'yes' : 'no'}`,
`sources=${data.existing_source_count || 0}/${data.source_count || 0}`,
`rows=${data.total_existing_rows || 0}`,
`writes=${data.writes_executed ? 'executed' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderLegacyBridgeBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const sources = data.source_summaries || [];
const operations = data.bridge_operations || [];
const controls = data.duplicate_controls || [];
const renderSource = item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.table)}</strong>
<small>${escapeHtml(item.description || '')}</small>
<small>target=${escapeHtml((item.planned_targets || []).join(' / '))}</small>
</div>
<span>${item.exists ? escapeHtml(item.row_count || 0) : 'PENDING'}</span>
</div>
`;
const renderOperation = item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.source_table)}${escapeHtml(item.target_table)}</strong>
<small>${escapeHtml(item.operation)} / ${escapeHtml(item.write_status)}</small>
<small>dedupe=${escapeHtml(item.dedupe_key)}</small>
</article>
`;
const renderControl = item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.rule)}</small>
</div>
<span>RULE</span>
</div>
`;
legacyBridgeBody.innerHTML = `
<div class="market-intel-empty mb-3">這裡只盤點既有資料來源與導入規則,不搬資料、不寫 market_*。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-legacy-sources>
<p class="market-intel-deploy-section-title">SOURCE TABLES</p>
<div class="market-intel-check-list">${
sources.length
? sources.map(renderSource).join('')
: '<div class="market-intel-empty">尚未提供來源摘要。</div>'
}</div>
</div>
<div data-market-intel-legacy-operations>
<p class="market-intel-deploy-section-title">BRIDGE OPERATIONS</p>
<div class="market-intel-operation-list">${
operations.length
? operations.map(renderOperation).join('')
: '<div class="market-intel-empty">尚未提供橋接操作。</div>'
}</div>
</div>
<div data-market-intel-legacy-controls>
<p class="market-intel-deploy-section-title">DUPLICATE CONTROLS</p>
<div class="market-intel-check-list">${
controls.length
? controls.map(renderControl).join('')
: '<div class="market-intel-empty">尚未提供去重規則。</div>'
}</div>
</div>
</div>
`;
};
const loadLegacyBridge = async () => {
if (!legacyBridgeMeta || !legacyBridgeBody) return;
legacyBridgeBody.innerHTML = '<div class="market-intel-empty">讀取既有資料橋接預覽中...</div>';
try {
const response = await fetch(legacyBridgeEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderLegacyBridgeMeta(data);
renderLegacyBridgeBody(data);
} catch (error) {
legacyBridgeMeta.innerHTML = '<span class="market-intel-pill">error</span>';
legacyBridgeBody.innerHTML = `<div class="market-intel-empty">既有資料橋接預覽讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderMigrationMeta = data => {
const seedWriter = data.command_plan && data.command_plan.seed_writer_command
? data.command_plan.seed_writer_command
@@ -979,6 +1091,9 @@
if (seedDiffRefresh) {
seedDiffRefresh.addEventListener('click', loadSeedDiff);
}
if (legacyBridgeRefresh) {
legacyBridgeRefresh.addEventListener('click', loadLegacyBridge);
}
if (migrationRefresh) {
migrationRefresh.addEventListener('click', loadMigration);
}
@@ -993,6 +1108,7 @@
loadCli();
loadDbProbe();
loadSeedDiff();
loadLegacyBridge();
loadMigration();
loadApproval();
loadDeploy();