feat(market-intel): add scheduler attach plan preview
Some checks failed
CD Pipeline / deploy (push) Failing after 45s

This commit is contained in:
OoO
2026-05-18 15:48:59 +08:00
parent d2d8dbab65
commit a3fb476eed
8 changed files with 335 additions and 11 deletions

View File

@@ -486,6 +486,24 @@
</div>
</div>
<div class="market-intel-panel" data-market-intel-scheduler>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">SCHEDULER / ATTACH PLAN</p>
<h2 class="market-intel-preview-title">排程掛載計畫</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理排程掛載計畫" data-market-intel-scheduler-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-scheduler-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-scheduler-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>
@@ -555,10 +573,11 @@
const mcpPreflightRoot = document.querySelector('[data-market-intel-mcp-preflight]');
const mcpActivationRoot = document.querySelector('[data-market-intel-mcp-activation]');
const mcpFetchGateRoot = document.querySelector('[data-market-intel-mcp-fetch-gate]');
const schedulerRoot = document.querySelector('[data-market-intel-scheduler]');
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 && !legacyBridgeRoot && !mcpReadinessRoot && !mcpPreflightRoot && !mcpActivationRoot && !mcpFetchGateRoot && !migrationRoot && !approvalRoot && !deployRoot) return;
if (!root && !writerRoot && !cliRoot && !dbProbeRoot && !seedDiffRoot && !legacyBridgeRoot && !mcpReadinessRoot && !mcpPreflightRoot && !mcpActivationRoot && !mcpFetchGateRoot && !schedulerRoot && !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;
@@ -600,6 +619,10 @@
const mcpFetchGateBody = mcpFetchGateRoot ? mcpFetchGateRoot.querySelector('[data-market-intel-mcp-fetch-gate-body]') : null;
const mcpFetchGateRefresh = mcpFetchGateRoot ? mcpFetchGateRoot.querySelector('[data-market-intel-mcp-fetch-gate-refresh]') : null;
const mcpFetchGateEndpoint = "{{ url_for('market_intel.market_intel_mcp_fetch_gate') }}?fetch=false&execute=false";
const schedulerMeta = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-meta]') : null;
const schedulerBody = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-body]') : null;
const schedulerRefresh = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-refresh]') : null;
const schedulerEndpoint = "{{ url_for('market_intel.market_intel_scheduler_plan') }}";
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;
@@ -1305,6 +1328,101 @@
}
};
const renderSchedulerMeta = data => {
schedulerMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`ready=${data.ready_to_attach_scheduler ? 'yes' : 'no'}`,
`jobs=${data.job_count || 0}`,
`scheduler=${data.scheduler_attached ? 'on' : 'off'}`,
`crawler=${data.crawler_job_started ? 'started' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderSchedulerBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const checks = Object.entries(data.gate_checks || {});
const jobs = data.jobs || [];
const sequence = data.attach_sequence || [];
const fallback = data.fallback_plan || [];
const renderCheck = ([name, passed]) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(name)}</strong>
</div>
<span>${passed ? 'PASS' : 'BLOCK'}</span>
</div>
`;
const renderJob = item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.label)} / ${escapeHtml(item.cadence)}</small>
<small>${escapeHtml(item.entrypoint)} / max=${escapeHtml(item.max_runtime_minutes)}m</small>
<small>network=${item.requires_external_network ? 'yes' : 'no'} / write=${item.requires_database_write ? 'yes' : 'no'}</small>
</article>
`;
const renderPlanItem = (item, status) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.key || status)}</strong>
<small>${escapeHtml(item.label || item)}</small>
</div>
<span>${escapeHtml(status).toUpperCase()}</span>
</div>
`;
schedulerBody.innerHTML = `
<div class="market-intel-empty mb-3">這是排程掛載計畫,不會從 API 註冊 job、不啟動 crawler、不連外、不寫 DB。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-scheduler-checks>
<p class="market-intel-deploy-section-title">SCHEDULER GATES</p>
<div class="market-intel-check-list">${
checks.length
? checks.map(renderCheck).join('')
: '<div class="market-intel-empty">尚未提供排程 gate。</div>'
}</div>
</div>
<div data-market-intel-scheduler-jobs>
<p class="market-intel-deploy-section-title">PLANNED JOBS</p>
<div class="market-intel-operation-list">${
jobs.length
? jobs.map(renderJob).join('')
: '<div class="market-intel-empty">尚未提供排程 job。</div>'
}</div>
</div>
<div data-market-intel-scheduler-sequence>
<p class="market-intel-deploy-section-title">ATTACH SEQUENCE</p>
<div class="market-intel-check-list">${
sequence.length
? sequence.map(item => renderPlanItem(item, 'required')).join('')
: '<div class="market-intel-empty">尚未提供掛載順序。</div>'
}</div>
</div>
<div data-market-intel-scheduler-fallback>
<p class="market-intel-deploy-section-title">FALLBACK</p>
<div class="market-intel-check-list">${
fallback.length
? fallback.map(item => renderPlanItem(item, 'ready')).join('')
: '<div class="market-intel-empty">尚未提供備援方案。</div>'
}</div>
</div>
</div>
`;
};
const loadScheduler = async () => {
if (!schedulerMeta || !schedulerBody) return;
schedulerBody.innerHTML = '<div class="market-intel-empty">讀取排程掛載計畫中...</div>';
try {
const response = await fetch(schedulerEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderSchedulerMeta(data);
renderSchedulerBody(data);
} catch (error) {
schedulerMeta.innerHTML = '<span class="market-intel-pill">error</span>';
schedulerBody.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
@@ -1569,6 +1687,9 @@
if (mcpFetchGateRefresh) {
mcpFetchGateRefresh.addEventListener('click', loadMcpFetchGate);
}
if (schedulerRefresh) {
schedulerRefresh.addEventListener('click', loadScheduler);
}
if (migrationRefresh) {
migrationRefresh.addEventListener('click', loadMigration);
}
@@ -1588,6 +1709,7 @@
loadMcpPreflight();
loadMcpActivation();
loadMcpFetchGate();
loadScheduler();
loadMigration();
loadApproval();
loadDeploy();