新增市場情報 review decision writer postwrite smoke
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-19 14:39:35 +08:00
parent f046f4c07f
commit ac2f712354
10 changed files with 767 additions and 66 deletions

View File

@@ -646,6 +646,9 @@
<button class="market-intel-icon-button" type="button" title="檢查 queue review decision writer preflight" data-market-intel-sample-candidate-queue-review-decision-preflight>
<i class="fas fa-database" aria-hidden="true"></i>
</button>
<button class="market-intel-icon-button" type="button" title="檢查 queue review decision writer post-write smoke" data-market-intel-sample-candidate-queue-review-decision-postwrite-smoke>
<i class="fas fa-search" aria-hidden="true"></i>
</button>
<button class="market-intel-icon-button" type="button" title="檢查 queue review decision writer CLI gate" data-market-intel-sample-candidate-queue-review-decision-writer>
<i class="fas fa-terminal" aria-hidden="true"></i>
</button>
@@ -992,6 +995,7 @@
const sampleCandidateQueueReviewDecisionApproval = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-decision-approval]') : null;
const sampleCandidateQueueReviewDecisionTransaction = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-decision-transaction]') : null;
const sampleCandidateQueueReviewDecisionPreflight = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-decision-preflight]') : null;
const sampleCandidateQueueReviewDecisionPostwriteSmoke = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-decision-postwrite-smoke]') : null;
const sampleCandidateQueueReviewDecisionWriter = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-decision-writer]') : null;
const sampleReviewEndpoint = "{{ url_for('market_intel.market_intel_manual_sample_review') }}";
const sampleReviewEvaluateEndpoint = "{{ url_for('market_intel.market_intel_manual_sample_review_evaluate') }}";
@@ -1013,6 +1017,7 @@
const sampleCandidateQueueReviewDecisionApprovalEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_approval') }}";
const sampleCandidateQueueReviewDecisionTransactionEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_transaction') }}";
const sampleCandidateQueueReviewDecisionPreflightEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_preflight') }}";
const sampleCandidateQueueReviewDecisionPostwriteSmokeEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_postwrite_smoke') }}";
const sampleCandidateQueueReviewDecisionWriterEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_writer_status') }}";
const schedulerMeta = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-meta]') : null;
const schedulerBody = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-body]') : null;
@@ -3738,6 +3743,117 @@
}
};
const renderCandidateQueueReviewDecisionPostwriteSmoke = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const rows = data.row_summaries || [];
const mismatches = data.state_mismatches || [];
sampleReviewMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`passed=${data.postwrite_smoke_passed ? 'yes' : 'no'}`,
`expected=${data.expected_dedupe_key_count || 0}`,
`found=${data.found_count || 0}`,
`mismatch=${data.state_mismatch_count || 0}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
sampleReviewBody.innerHTML = `
<div class="market-intel-empty mb-3">此卡只做 review_state writer post-write 只讀驗證API/UI 不讀 token、不更新 review_state、不 commit、不掛 scheduler。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div>
<p class="market-intel-deploy-section-title">EXPECTED UPDATES</p>
<div class="market-intel-check-list">${
(data.expected_review_state_updates || []).map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.dedupe_key || 'unknown')}</strong>
<small>${escapeHtml(item.expected_current_review_state || 'unknown')} -> ${escapeHtml(item.expected_review_state || 'none')}</small>
</div>
<span>${escapeHtml(item.parameter_payload_hash || 'preview')}</span>
</div>
`).join('') || '<div class="market-intel-empty">尚未提供 review_state update payload。</div>'
}</div>
</div>
<div>
<p class="market-intel-deploy-section-title">READ-ONLY RESULT</p>
<div class="market-intel-check-list">
${[
['read_only_query', data.read_only_query_executed],
['db_connection', data.database_connection_opened],
['api_updates_review_state', data.api_updates_review_state],
['review_state_update_executed', data.review_state_update_executed],
['database_write', data.database_write_executed],
['scheduler', data.scheduler_attached]
].map(([key, value]) => `
<div class="market-intel-check">
<div><strong>${escapeHtml(key)}</strong></div>
<span>${escapeHtml(String(value))}</span>
</div>
`).join('')}
</div>
</div>
</div>
<div class="market-intel-deploy-grid mt-3">
<div>
<p class="market-intel-deploy-section-title">ROWS</p>
<div class="market-intel-check-list">${
rows.map(row => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(row.dedupe_key || 'unknown')}</strong>
<small>${escapeHtml(row.review_state || 'unknown')}</small>
</div>
<span>${escapeHtml(row.updated_at || 'pending')}</span>
</div>
`).join('') || '<div class="market-intel-empty">尚未執行只讀查詢或查無 row。</div>'
}</div>
</div>
<div>
<p class="market-intel-deploy-section-title">MISMATCH</p>
<div class="market-intel-check-list">${
mismatches.map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.dedupe_key || 'unknown')}</strong>
<small>${escapeHtml(item.expected_review_state || 'none')} / ${escapeHtml(item.actual_review_state || 'none')}</small>
</div>
<span>CHECK</span>
</div>
`).join('') || '<div class="market-intel-empty">目前沒有 state mismatch。</div>'
}</div>
</div>
</div>
`;
};
const loadCandidateQueueReviewDecisionPostwriteSmoke = async () => {
if (!sampleReviewMeta || !sampleReviewBody || !sampleReviewInput) return;
let parsed;
try {
parsed = JSON.parse(sampleReviewInput.value || '{}');
} catch (error) {
sampleReviewMeta.innerHTML = '<span class="market-intel-pill">json_error</span>';
sampleReviewBody.innerHTML = `<div class="market-intel-empty">JSON 格式錯誤:${escapeHtml(error.message)}</div>`;
return;
}
const body = parsed && parsed.sample_result ? parsed : { sample_result: parsed };
sampleReviewBody.innerHTML = '<div class="market-intel-empty">檢查 queue review decision writer post-write smoke 中...</div>';
try {
const response = await fetch(sampleCandidateQueueReviewDecisionPostwriteSmokeEndpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(body)
});
const data = await response.json();
if (!response.ok && !data.mode) throw new Error(`HTTP ${response.status}`);
renderCandidateQueueReviewDecisionPostwriteSmoke(data);
} catch (error) {
sampleReviewMeta.innerHTML = '<span class="market-intel-pill">error</span>';
sampleReviewBody.innerHTML = `<div class="market-intel-empty">queue review decision writer post-write smoke 失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderCandidateQueueReviewDecisionWriter = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const summary = data.statement_summary || {};
@@ -5399,6 +5515,9 @@
if (sampleCandidateQueueReviewDecisionPreflight) {
sampleCandidateQueueReviewDecisionPreflight.addEventListener('click', loadCandidateQueueReviewDecisionPreflight);
}
if (sampleCandidateQueueReviewDecisionPostwriteSmoke) {
sampleCandidateQueueReviewDecisionPostwriteSmoke.addEventListener('click', loadCandidateQueueReviewDecisionPostwriteSmoke);
}
if (sampleCandidateQueueReviewDecisionWriter) {
sampleCandidateQueueReviewDecisionWriter.addEventListener('click', loadCandidateQueueReviewDecisionWriter);
}