新增市場情報樣本結果審核預覽
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-19 00:28:20 +08:00
parent 2de9386a40
commit 5908d1bdca
9 changed files with 624 additions and 34 deletions

View File

@@ -522,6 +522,24 @@
</div>
</div>
<div class="market-intel-panel" data-market-intel-sample-review>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">MANUAL SAMPLE / REVIEW</p>
<h2 class="market-intel-preview-title">樣本結果審核預覽</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理樣本結果審核預覽" data-market-intel-sample-review-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-sample-review-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-sample-review-body>
<div class="market-intel-empty">讀取樣本結果審核預覽中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-scheduler>
<div class="market-intel-preview-head">
<div>
@@ -773,6 +791,7 @@
const mcpFetchGateRoot = document.querySelector('[data-market-intel-mcp-fetch-gate]');
const manualSampleRoot = document.querySelector('[data-market-intel-manual-sample]');
const sampleAcceptanceRoot = document.querySelector('[data-market-intel-sample-acceptance]');
const sampleReviewRoot = document.querySelector('[data-market-intel-sample-review]');
const schedulerRoot = document.querySelector('[data-market-intel-scheduler]');
const matchReviewRoot = document.querySelector('[data-market-intel-match-review]');
const opportunityRoot = document.querySelector('[data-market-intel-opportunity]');
@@ -786,7 +805,7 @@
const liveInventoryRoot = document.querySelector('[data-market-intel-live-inventory]');
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 && !manualSampleRoot && !sampleAcceptanceRoot && !schedulerRoot && !matchReviewRoot && !opportunityRoot && !opportunityScoringRoot && !opportunityEvidenceRoot && !opportunityAlertRoot && !migrationRoot && !migrationDrillRoot && !catalogReviewRoot && !liveSmokeRoot && !liveInventoryRoot && !approvalRoot && !deployRoot) return;
if (!root && !writerRoot && !cliRoot && !dbProbeRoot && !seedDiffRoot && !legacyBridgeRoot && !mcpReadinessRoot && !mcpPreflightRoot && !mcpActivationRoot && !mcpFetchGateRoot && !manualSampleRoot && !sampleAcceptanceRoot && !sampleReviewRoot && !schedulerRoot && !matchReviewRoot && !opportunityRoot && !opportunityScoringRoot && !opportunityEvidenceRoot && !opportunityAlertRoot && !migrationRoot && !migrationDrillRoot && !catalogReviewRoot && !liveSmokeRoot && !liveInventoryRoot && !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;
@@ -836,6 +855,10 @@
const sampleAcceptanceBody = sampleAcceptanceRoot ? sampleAcceptanceRoot.querySelector('[data-market-intel-sample-acceptance-body]') : null;
const sampleAcceptanceRefresh = sampleAcceptanceRoot ? sampleAcceptanceRoot.querySelector('[data-market-intel-sample-acceptance-refresh]') : null;
const sampleAcceptanceEndpoint = "{{ url_for('market_intel.market_intel_manual_sample_acceptance') }}";
const sampleReviewMeta = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-review-meta]') : null;
const sampleReviewBody = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-review-body]') : null;
const sampleReviewRefresh = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-review-refresh]') : null;
const sampleReviewEndpoint = "{{ url_for('market_intel.market_intel_manual_sample_review') }}";
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;
@@ -1809,6 +1832,99 @@
}
};
const renderSampleReviewMeta = data => {
sampleReviewMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`loaded=${data.sample_result_loaded ? 'yes' : 'no'}`,
`reviewed=${data.sample_result_reviewed ? 'yes' : 'no'}`,
`result=${data.review_result || 'planned'}`,
`import=${data.candidate_import_allowed ? 'open' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderSampleReviewBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const checks = data.review_checks || [];
const findings = data.review_findings || [];
const candidates = data.candidate_summary || {};
const actions = data.operator_next_actions || [];
const boundaries = data.safe_boundaries || [];
const renderCheck = item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.key || 'check')}</strong>
<small>${escapeHtml(item.label || '')}</small>
</div>
<span>${escapeHtml(item.status || 'planned').toUpperCase()}</span>
</div>
`;
const renderNamedItem = (item, status) => {
const normalized = typeof item === 'string' ? { key: item, label: item } : (item || {});
return `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(normalized.key || normalized.label || 'item')}</strong>
<small>${escapeHtml(normalized.label || normalized.key || '')}</small>
</div>
<span>${escapeHtml(normalized.severity || normalized.write_status || status || 'blocked').toUpperCase()}</span>
</div>
`;
};
sampleReviewBody.innerHTML = `
<div class="market-intel-empty mb-3">此卡只用純函式審核人工 sample result目前預設不載入結果、不抓外站、不存檔、不寫 DB、不建立候選活動。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-empty mb-3">candidate_count=${escapeHtml(candidates.candidate_count || 0)} / accepted=${escapeHtml(candidates.accepted_candidate_count || 0)} / bands=${escapeHtml((candidates.accepted_candidate_bands || []).join('/'))}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-sample-review-checks>
<p class="market-intel-deploy-section-title">REVIEW CHECKS</p>
<div class="market-intel-check-list">${
checks.length
? checks.map(renderCheck).join('')
: '<div class="market-intel-empty">尚未載入 sample result審核檢查維持 planned。</div>'
}</div>
</div>
<div data-market-intel-sample-review-findings>
<p class="market-intel-deploy-section-title">FINDINGS</p>
<div class="market-intel-check-list">${
findings.length
? findings.map(item => renderNamedItem(item, 'block')).join('')
: '<div class="market-intel-empty">目前沒有審核發現;尚未載入結果時代表未評估。</div>'
}</div>
</div>
<div data-market-intel-sample-review-actions>
<p class="market-intel-deploy-section-title">NEXT ACTIONS</p>
<div class="market-intel-check-list">${
actions.length
? actions.map(item => renderNamedItem(item, item.write_status || 'blocked')).join('')
: '<div class="market-intel-empty">尚未提供下一步。</div>'
}</div>
</div>
<div data-market-intel-sample-review-boundaries>
<p class="market-intel-deploy-section-title">BOUNDARIES</p>
<div class="market-intel-check-list">${
boundaries.length
? boundaries.map(item => renderNamedItem(String(item), 'required')).join('')
: '<div class="market-intel-empty">尚未提供安全邊界。</div>'
}</div>
</div>
</div>
`;
};
const loadSampleReview = async () => {
if (!sampleReviewMeta || !sampleReviewBody) return;
sampleReviewBody.innerHTML = '<div class="market-intel-empty">讀取樣本結果審核預覽中...</div>';
try {
const response = await fetch(sampleReviewEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderSampleReviewMeta(data);
renderSampleReviewBody(data);
} catch (error) {
sampleReviewMeta.innerHTML = '<span class="market-intel-pill">error</span>';
sampleReviewBody.innerHTML = `<div class="market-intel-empty">樣本結果審核預覽讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderSchedulerMeta = data => {
schedulerMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
@@ -3297,6 +3413,9 @@
if (sampleAcceptanceRefresh) {
sampleAcceptanceRefresh.addEventListener('click', loadSampleAcceptance);
}
if (sampleReviewRefresh) {
sampleReviewRefresh.addEventListener('click', loadSampleReview);
}
if (schedulerRefresh) {
schedulerRefresh.addEventListener('click', loadScheduler);
}
@@ -3348,6 +3467,7 @@
loadMcpFetchGate();
loadManualSample();
loadSampleAcceptance();
loadSampleReview();
loadScheduler();
loadMatchReview();
loadOpportunity();