Files
ewoooc/templates/market_intel/disabled.html

977 lines
45 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "ewoooc_base.html" %}
{% block title %}市場情報EwoooC{% endblock %}
{% block extra_css %}
<style>
.market-intel-status {
display: grid;
gap: 1rem;
}
.market-intel-panel {
border: 1px solid var(--momo-border, #d8c8aa);
border-radius: 8px;
background:
radial-gradient(circle at 1px 1px, rgba(120, 83, 44, 0.16) 1px, transparent 1.35px),
var(--momo-paper, #f8f2e7);
background-size: 10px 10px, auto;
box-shadow: var(--momo-shadow-sm, 0 8px 18px rgba(72, 49, 28, 0.08));
padding: 1.25rem;
}
.market-intel-title {
color: var(--momo-ink, #30251b);
font-size: 1.35rem;
font-weight: 800;
margin: 0;
}
.market-intel-muted {
color: var(--momo-muted, #756a5b);
margin: 0;
}
.market-intel-flags {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
margin-top: 1rem;
}
.market-intel-flag {
border-left: 3px solid var(--momo-accent, #c8752d);
background: rgba(255, 250, 241, 0.84);
padding: 0.8rem 0.9rem;
}
.market-intel-flag span {
color: var(--momo-muted, #756a5b);
display: block;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0;
}
.market-intel-flag strong {
color: var(--momo-ink, #30251b);
font-family: "JetBrains Mono", monospace;
font-size: 1rem;
}
.market-intel-preview-head {
align-items: center;
display: flex;
gap: 0.75rem;
justify-content: space-between;
margin-bottom: 1rem;
}
.market-intel-preview-title {
color: var(--momo-ink, #30251b);
font-size: 1rem;
font-weight: 800;
margin: 0;
}
.market-intel-icon-button {
align-items: center;
background: rgba(255, 250, 241, 0.9);
border: 1px solid var(--momo-border, #d8c8aa);
border-radius: 8px;
color: var(--momo-ink, #30251b);
display: inline-flex;
height: 2.25rem;
justify-content: center;
width: 2.25rem;
}
.market-intel-icon-button:hover {
background: rgba(201, 117, 45, 0.12);
color: var(--momo-accent-700, #8f4530);
}
.market-intel-preview-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.market-intel-pill {
background: rgba(255, 250, 241, 0.82);
border: 1px solid rgba(120, 83, 44, 0.14);
border-radius: 8px;
color: var(--momo-muted, #756a5b);
font-family: "JetBrains Mono", monospace;
font-size: 0.78rem;
font-weight: 700;
padding: 0.35rem 0.5rem;
}
.market-intel-empty {
background: rgba(255, 250, 241, 0.72);
border: 1px dashed rgba(120, 83, 44, 0.28);
border-radius: 8px;
color: var(--momo-muted, #756a5b);
padding: 1rem;
}
.market-intel-candidate-list {
display: grid;
gap: 0.75rem;
}
.market-intel-candidate {
background: rgba(255, 250, 241, 0.82);
border: 1px solid rgba(120, 83, 44, 0.14);
border-left: 3px solid var(--momo-accent, #c8752d);
border-radius: 8px;
padding: 0.8rem 0.9rem;
}
.market-intel-candidate a {
color: var(--momo-ink, #30251b);
font-weight: 800;
text-decoration: none;
word-break: break-word;
}
.market-intel-candidate small {
color: var(--momo-muted, #756a5b);
display: block;
margin-top: 0.35rem;
}
.market-intel-operation-list {
display: grid;
gap: 0.75rem;
}
.market-intel-operation {
background: rgba(255, 250, 241, 0.82);
border: 1px solid rgba(120, 83, 44, 0.14);
border-left: 3px solid var(--momo-accent, #c8752d);
border-radius: 8px;
padding: 0.8rem 0.9rem;
}
.market-intel-operation strong {
color: var(--momo-ink, #30251b);
display: block;
font-family: "JetBrains Mono", monospace;
font-size: 0.92rem;
word-break: break-word;
}
.market-intel-operation small {
color: var(--momo-muted, #756a5b);
display: block;
margin-top: 0.35rem;
}
.market-intel-check-list {
display: grid;
gap: 0.6rem;
}
.market-intel-deploy-grid {
display: grid;
gap: 0.9rem;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
margin-top: 1rem;
}
.market-intel-deploy-section-title {
color: var(--momo-muted, #756a5b);
font-family: "JetBrains Mono", monospace;
font-size: 0.78rem;
font-weight: 800;
margin: 0 0 0.55rem;
}
.market-intel-check {
align-items: center;
background: rgba(255, 250, 241, 0.82);
border: 1px solid rgba(120, 83, 44, 0.14);
border-radius: 8px;
color: var(--momo-muted, #756a5b);
display: flex;
gap: 0.65rem;
justify-content: space-between;
padding: 0.65rem 0.75rem;
}
.market-intel-check div {
min-width: 0;
}
.market-intel-check strong {
color: var(--momo-ink, #30251b);
font-family: "JetBrains Mono", monospace;
font-size: 0.84rem;
word-break: break-word;
}
.market-intel-check small {
color: var(--momo-muted, #756a5b);
display: block;
line-height: 1.45;
margin-top: 0.25rem;
}
.market-intel-check span {
color: var(--momo-muted, #756a5b);
font-family: "JetBrains Mono", monospace;
font-size: 0.78rem;
font-weight: 800;
white-space: nowrap;
}
@media (max-width: 520px) {
.market-intel-preview-head,
.market-intel-check {
align-items: flex-start;
flex-direction: column;
}
.market-intel-check span {
white-space: normal;
}
}
</style>
{% endblock %}
{% block ewooo_content %}
<section class="market-intel-status">
<div class="market-intel-panel">
<p class="market-intel-muted momo-mono mb-2">MARKET INTEL / {{ status.phase }}</p>
<h1 class="market-intel-title">市場情報尚未啟用</h1>
<p class="market-intel-muted mt-2">目前只載入安全骨架;爬蟲、正式寫入與排程都尚未掛載。</p>
<div class="market-intel-flags">
<div class="market-intel-flag">
<span>模組開關</span>
<strong>{{ 'ON' if status.enabled else 'OFF' }}</strong>
</div>
<div class="market-intel-flag">
<span>爬蟲開關</span>
<strong>{{ 'ON' if status.crawler_enabled else 'OFF' }}</strong>
</div>
<div class="market-intel-flag">
<span>資料寫入</span>
<strong>{{ 'ON' if status.write_enabled else 'OFF' }}</strong>
</div>
<div class="market-intel-flag">
<span>排程掛載</span>
<strong>{{ 'ON' if status.scheduler_attached else 'OFF' }}</strong>
</div>
<div class="market-intel-flag">
<span>DB 寫入許可</span>
<strong>{{ 'ON' if status.database_write_allowed else 'OFF' }}</strong>
</div>
<div class="market-intel-flag">
<span>已註冊 Adapter</span>
<strong>{{ adapter_count|default(0) }}</strong>
</div>
<div class="market-intel-flag">
<span>手動 Fetch</span>
<strong>{{ 'ON' if manual_fetch_allowed|default(false) else 'OFF' }}</strong>
</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-preview>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">CANDIDATE PREVIEW / SAFE</p>
<h2 class="market-intel-preview-title">候選預覽</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理候選預覽" data-market-intel-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-preview-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-preview-body>
<div class="market-intel-empty">讀取候選預覽中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-writer>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">SEED WRITER / DRY RUN</p>
<h2 class="market-intel-preview-title">平台種子寫入預覽</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理寫入預覽" data-market-intel-writer-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-writer-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-writer-body>
<div class="market-intel-empty">讀取寫入預覽中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-cli>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">SEED CLI / TRANSACTION PREVIEW</p>
<h2 class="market-intel-preview-title">Seed CLI 交易預覽</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理交易預覽" data-market-intel-cli-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-cli-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-cli-body>
<div class="market-intel-empty">讀取交易預覽中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-db-probe>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">DB SCHEMA / READ ONLY PROBE</p>
<h2 class="market-intel-preview-title">正式 DB Schema 探針</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理 DB 探針" data-market-intel-db-probe-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-db-probe-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-db-probe-body>
<div class="market-intel-empty">讀取 DB 探針中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-seed-diff>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">PLATFORM SEED / READ ONLY DIFF</p>
<h2 class="market-intel-preview-title">平台 Seed DB 差異探針</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理 Seed 差異" data-market-intel-seed-diff-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-seed-diff-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-seed-diff-body>
<div class="market-intel-empty">讀取 Seed 差異探針中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-migration>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">MIGRATION / BLUEPRINT</p>
<h2 class="market-intel-preview-title">Schema migration 草案</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理 migration 草案" data-market-intel-migration-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-migration-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-migration-body>
<div class="market-intel-empty">讀取 migration 草案中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-approval>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">WRITE APPROVAL / RUNBOOK</p>
<h2 class="market-intel-preview-title">正式寫入批准檢查</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理批准檢查" data-market-intel-approval-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-approval-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-approval-body>
<div class="market-intel-empty">讀取批准檢查中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-deploy>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">DEPLOYMENT / READINESS</p>
<h2 class="market-intel-preview-title">推版準備檢查</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理推版準備" data-market-intel-deploy-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-deploy-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-deploy-body>
<div class="market-intel-empty">讀取推版準備中...</div>
</div>
</div>
</section>
{% endblock %}
{% block extra_js %}
<script>
(function () {
const root = document.querySelector('[data-market-intel-preview]');
const writerRoot = document.querySelector('[data-market-intel-writer]');
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 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;
const meta = root ? root.querySelector('[data-market-intel-preview-meta]') : null;
const body = root ? root.querySelector('[data-market-intel-preview-body]') : null;
const refresh = root ? root.querySelector('[data-market-intel-refresh]') : null;
const endpoint = "{{ url_for('market_intel.market_intel_candidate_preview') }}?fetch=false&limit=20";
const writerMeta = writerRoot ? writerRoot.querySelector('[data-market-intel-writer-meta]') : null;
const writerBody = writerRoot ? writerRoot.querySelector('[data-market-intel-writer-body]') : null;
const writerRefresh = writerRoot ? writerRoot.querySelector('[data-market-intel-writer-refresh]') : null;
const writerEndpoint = "{{ url_for('market_intel.market_intel_platform_seed_writer_plan') }}";
const cliMeta = cliRoot ? cliRoot.querySelector('[data-market-intel-cli-meta]') : null;
const cliBody = cliRoot ? cliRoot.querySelector('[data-market-intel-cli-body]') : null;
const cliRefresh = cliRoot ? cliRoot.querySelector('[data-market-intel-cli-refresh]') : null;
const cliEndpoint = "{{ url_for('market_intel.market_intel_seed_writer_cli_status') }}";
const dbProbeMeta = dbProbeRoot ? dbProbeRoot.querySelector('[data-market-intel-db-probe-meta]') : null;
const dbProbeBody = dbProbeRoot ? dbProbeRoot.querySelector('[data-market-intel-db-probe-body]') : null;
const dbProbeRefresh = dbProbeRoot ? dbProbeRoot.querySelector('[data-market-intel-db-probe-refresh]') : null;
const dbProbeEndpoint = "{{ url_for('market_intel.market_intel_schema_db_probe') }}?execute=false";
const seedDiffMeta = seedDiffRoot ? seedDiffRoot.querySelector('[data-market-intel-seed-diff-meta]') : null;
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 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;
const migrationEndpoint = "{{ url_for('market_intel.market_intel_migration_blueprint') }}";
const approvalMeta = approvalRoot ? approvalRoot.querySelector('[data-market-intel-approval-meta]') : null;
const approvalBody = approvalRoot ? approvalRoot.querySelector('[data-market-intel-approval-body]') : null;
const approvalRefresh = approvalRoot ? approvalRoot.querySelector('[data-market-intel-approval-refresh]') : null;
const approvalEndpoint = "{{ url_for('market_intel.market_intel_write_approval_runbook') }}";
const deployMeta = deployRoot ? deployRoot.querySelector('[data-market-intel-deploy-meta]') : null;
const deployBody = deployRoot ? deployRoot.querySelector('[data-market-intel-deploy-body]') : null;
const deployRefresh = deployRoot ? deployRoot.querySelector('[data-market-intel-deploy-refresh]') : null;
const deployEndpoint = "{{ url_for('market_intel.market_intel_deployment_readiness') }}";
const escapeHtml = value => String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const renderMeta = data => {
meta.innerHTML = [
`candidates=${data.candidate_count || 0}`,
`fetch=${data.fetch_requested ? 'true' : 'false'}`,
`manual=${data.manual_fetch_allowed ? 'on' : 'off'}`,
`db_write=${data.database_write_allowed ? 'on' : 'off'}`,
`scheduler=${data.scheduler_attached ? 'on' : 'off'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderBody = data => {
if (!data.candidates || data.candidates.length === 0) {
const status = (data.run_statuses || []).map(item => `${item.platform_code}:${item.status}`).join(' / ');
body.innerHTML = `<div class="market-intel-empty">目前沒有候選連結。${status ? `狀態:${escapeHtml(status)}` : ''}</div>`;
return;
}
body.innerHTML = `<div class="market-intel-candidate-list">${
data.candidates.map(item => `
<article class="market-intel-candidate">
<a href="${escapeHtml(item.href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(item.text || item.href)}</a>
<small>${escapeHtml(item.platform_code)} / ${escapeHtml(item.confidence_band)} / score=${escapeHtml(item.score)}</small>
</article>
`).join('')
}</div>`;
};
const loadPreview = async () => {
if (!meta || !body) return;
body.innerHTML = '<div class="market-intel-empty">讀取候選預覽中...</div>';
try {
const response = await fetch(endpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderMeta(data);
renderBody(data);
} catch (error) {
meta.innerHTML = '<span class="market-intel-pill">error</span>';
body.innerHTML = `<div class="market-intel-empty">候選預覽讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderWriterMeta = data => {
writerMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`operations=${data.operation_count || 0}`,
`schema=${data.schema_smoke && data.schema_smoke.passed ? 'pass' : 'fail'}`,
`writes=${data.writes_executed ? 'executed' : 'blocked'}`,
`db_write=${data.database_write_allowed ? 'on' : 'off'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderWriterBody = data => {
const reasons = (data.blocked_reasons || []).join(' / ');
const operations = (data.operations || []).slice(0, 6);
if (operations.length === 0) {
writerBody.innerHTML = `<div class="market-intel-empty">沒有 upsert 預覽。${reasons ? `阻擋:${escapeHtml(reasons)}` : ''}</div>`;
return;
}
writerBody.innerHTML = `
<div class="market-intel-empty mb-3">正式寫入仍被阻擋。${reasons ? `阻擋:${escapeHtml(reasons)}` : ''}</div>
<div class="market-intel-operation-list">${
operations.map(item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.lookup && item.lookup.code ? item.lookup.code : item.table)}</strong>
<small>${escapeHtml(item.operation)} / ${escapeHtml(item.table)} / ${escapeHtml(item.write_status)}</small>
</article>
`).join('')
}</div>
`;
};
const loadWriter = async () => {
if (!writerMeta || !writerBody) return;
writerBody.innerHTML = '<div class="market-intel-empty">讀取寫入預覽中...</div>';
try {
const response = await fetch(writerEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderWriterMeta(data);
renderWriterBody(data);
} catch (error) {
writerMeta.innerHTML = '<span class="market-intel-pill">error</span>';
writerBody.innerHTML = `<div class="market-intel-empty">寫入預覽讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderCliMeta = data => {
const preview = data.transaction_preview || {};
cliMeta.innerHTML = [
`mode=${preview.mode || data.mode || 'unknown'}`,
`exit=${data.exit_code == null ? 'n/a' : data.exit_code}`,
`statements=${preview.statement_count || 0}`,
`session=${data.database_session_created ? 'yes' : 'no'}`,
`commit=${data.database_commit_executed ? 'yes' : 'no'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderCliBody = data => {
const preview = data.transaction_preview || {};
const statements = (preview.statements || []).slice(0, 6);
const blockers = (data.blocked_reasons || []).join(' / ');
if (!statements.length) {
cliBody.innerHTML = `<div class="market-intel-empty">沒有交易預覽。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>`;
return;
}
cliBody.innerHTML = `
<div class="market-intel-empty mb-3">只產生 transaction preview沒有 DB session、沒有 transaction、沒有 commit。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-operation-list">${
statements.map(item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.idempotency_key || item.table)}</strong>
<small>${escapeHtml(item.operation)} / ${escapeHtml(item.table)} / ${escapeHtml(item.diff_status)} / hash=${escapeHtml(item.parameter_payload_hash)}</small>
</article>
`).join('')
}</div>
`;
};
const loadCli = async () => {
if (!cliMeta || !cliBody) return;
cliBody.innerHTML = '<div class="market-intel-empty">讀取交易預覽中...</div>';
try {
const response = await fetch(cliEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderCliMeta(data);
renderCliBody(data);
} catch (error) {
cliMeta.innerHTML = '<span class="market-intel-pill">error</span>';
cliBody.innerHTML = `<div class="market-intel-empty">交易預覽讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderDbProbeMeta = data => {
dbProbeMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`execute=${data.execute_requested ? 'true' : 'false'}`,
`query=${data.read_only_query_executed ? 'yes' : 'no'}`,
`tables=${(data.expected_tables || []).length}`,
`missing=${(data.missing_tables || []).length}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderDbProbeBody = data => {
const statuses = (data.table_statuses || []).slice(0, 8);
const blockers = (data.blocked_reasons || []).join(' / ');
dbProbeBody.innerHTML = `
<div class="market-intel-empty mb-3">預設只顯示 planned不自動查正式 DB人工 smoke 時才可明確開啟只讀查詢參數。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-check-list">${
statuses.map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.table)}</strong>
<small>${data.read_only_query_executed ? 'read_only_catalog_query' : 'planned_no_db_connection'}</small>
</div>
<span>${item.exists ? 'EXISTS' : 'PENDING'}</span>
</div>
`).join('')
}</div>
`;
};
const loadDbProbe = async () => {
if (!dbProbeMeta || !dbProbeBody) return;
dbProbeBody.innerHTML = '<div class="market-intel-empty">讀取 DB 探針中...</div>';
try {
const response = await fetch(dbProbeEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderDbProbeMeta(data);
renderDbProbeBody(data);
} catch (error) {
dbProbeMeta.innerHTML = '<span class="market-intel-pill">error</span>';
dbProbeBody.innerHTML = `<div class="market-intel-empty">DB 探針讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderSeedDiffMeta = data => {
seedDiffMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`execute=${data.execute_requested ? 'true' : 'false'}`,
`query=${data.read_only_query_executed ? 'yes' : 'no'}`,
`expected=${data.expected_seed_count || 0}`,
`missing=${(data.missing_codes || []).length}`,
`changed=${(data.changed_codes || []).length}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderSeedDiffBody = data => {
const diffs = (data.seed_diffs || []).slice(0, 8);
const blockers = (data.blocked_reasons || []).join(' / ');
seedDiffBody.innerHTML = `
<div class="market-intel-empty mb-3">預設只顯示 seed 差異 planned不自動查正式 DB人工 smoke 時才可明確開啟只讀查詢參數。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-check-list">${
diffs.map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.code)}</strong>
<small>${escapeHtml(item.diff_status || 'unknown')}</small>
</div>
<span>${item.exists ? 'EXISTS' : 'PENDING'}</span>
</div>
`).join('')
}</div>
`;
};
const loadSeedDiff = async () => {
if (!seedDiffMeta || !seedDiffBody) return;
seedDiffBody.innerHTML = '<div class="market-intel-empty">讀取 Seed 差異探針中...</div>';
try {
const response = await fetch(seedDiffEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderSeedDiffMeta(data);
renderSeedDiffBody(data);
} catch (error) {
seedDiffMeta.innerHTML = '<span class="market-intel-pill">error</span>';
seedDiffBody.innerHTML = `<div class="market-intel-empty">Seed 差異探針讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderMigrationMeta = data => {
const seedWriter = data.command_plan && data.command_plan.seed_writer_command
? data.command_plan.seed_writer_command
: {};
migrationMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`tables=${data.table_count || 0}`,
`file=${data.file_created ? 'created' : 'preview'}`,
`executed=${data.migration_executed ? 'yes' : 'no'}`,
`additive=${data.safety_checks && data.safety_checks.forward_sql_additive_only ? 'yes' : 'no'}`,
`seed_script=${seedWriter.script_created ? 'created' : 'missing'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderMigrationBody = data => {
const operations = (data.table_operations || []).slice(0, 7);
const blockers = (data.blocked_reasons || []).join(' / ');
const commands = data.command_plan || {};
const migrationCommand = commands.migration_apply_command || {};
const seedCommand = commands.seed_writer_command || {};
migrationBody.innerHTML = `
<div class="market-intel-empty mb-3">
建議檔名:${escapeHtml(data.suggested_filename || '')}${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}
</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-migration-tables>
<p class="market-intel-deploy-section-title">TABLE DRAFT</p>
<div class="market-intel-operation-list">${
operations.map(item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.table)}</strong>
<small>${escapeHtml(item.operation)} / ${escapeHtml(item.write_status)}</small>
</article>
`).join('')
}</div>
</div>
<div data-market-intel-migration-commands>
<p class="market-intel-deploy-section-title">COMMAND DESIGN</p>
<div class="market-intel-check-list">
<div class="market-intel-check">
<div>
<strong>migration_apply_command</strong>
<small>${escapeHtml(migrationCommand.command || '')}</small>
</div>
<span>${migrationCommand.executed ? 'EXECUTED' : 'BLOCKED'}</span>
</div>
<div class="market-intel-check">
<div>
<strong>seed_writer_command</strong>
<small>${escapeHtml(seedCommand.command || '')}</small>
<small>${escapeHtml(seedCommand.notes || '')}</small>
</div>
<span>${seedCommand.executed ? 'EXECUTED' : seedCommand.script_created ? 'SCRIPT' : 'DESIGN'}</span>
</div>
</div>
</div>
</div>
`;
};
const loadMigration = async () => {
if (!migrationMeta || !migrationBody) return;
migrationBody.innerHTML = '<div class="market-intel-empty">讀取 migration 草案中...</div>';
try {
const response = await fetch(migrationEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderMigrationMeta(data);
renderMigrationBody(data);
} catch (error) {
migrationMeta.innerHTML = '<span class="market-intel-pill">error</span>';
migrationBody.innerHTML = `<div class="market-intel-empty">migration 草案讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderApprovalMeta = data => {
approvalMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`ready=${data.ready_for_real_write ? 'yes' : 'no'}`,
`gates=${(data.approval_gates || []).length}`,
`blocked=${(data.blocked_reasons || []).length}`,
`db_commit=${data.database_commit_executed ? 'yes' : 'no'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderApprovalBody = data => {
const gates = data.approval_gates || [];
const sequence = (data.operator_sequence || []).slice(0, 6);
const rollback = data.rollback_plan || [];
const renderNamedItem = (item, status) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.key || item.label || 'item')}</strong>
<small>${escapeHtml(item.label || item.key || '')}</small>
</div>
<span>${escapeHtml(status).toUpperCase()}</span>
</div>
`;
approvalBody.innerHTML = `
<div class="market-intel-empty mb-3">正式寫入尚未批准;目前沒有 DB session、沒有 commit、沒有 scheduler attach。</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-approval-gates>
<p class="market-intel-deploy-section-title">APPROVAL GATES</p>
<div class="market-intel-check-list">${
gates.map(item => renderNamedItem(item, item.passed ? 'pass' : 'block')).join('')
}</div>
</div>
<div data-market-intel-approval-sequence>
<p class="market-intel-deploy-section-title">OPERATOR SEQUENCE</p>
<div class="market-intel-check-list">${
sequence.map(item => renderNamedItem(item, 'pending')).join('')
}</div>
</div>
<div data-market-intel-approval-rollback>
<p class="market-intel-deploy-section-title">ROLLBACK</p>
<div class="market-intel-check-list">${
rollback.map(item => renderNamedItem(item, 'ready')).join('')
}</div>
</div>
</div>
`;
};
const loadApproval = async () => {
if (!approvalMeta || !approvalBody) return;
approvalBody.innerHTML = '<div class="market-intel-empty">讀取批准檢查中...</div>';
try {
const response = await fetch(approvalEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderApprovalMeta(data);
renderApprovalBody(data);
} catch (error) {
approvalMeta.innerHTML = '<span class="market-intel-pill">error</span>';
approvalBody.innerHTML = `<div class="market-intel-empty">批准檢查讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderDeployMeta = data => {
deployMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`deployed=${data.production_deployed ? 'yes' : 'no'}`,
`commit=${data.git_committed ? 'yes' : 'no'}`,
`push=${data.git_pushed ? 'yes' : 'no'}`,
`ready=${data.ready_for_production_deploy ? 'yes' : 'no'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderDeployBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const checks = Object.entries(data.checks || {});
const steps = data.required_manual_steps || [];
const fallback = data.fallback_plan || [];
const boundaries = data.safe_deploy_boundaries || [];
const smokeTargets = data.production_smoke_targets || [];
const renderItem = (item, fallbackStatus) => {
const normalized = typeof item === 'string' ? { key: item, label: item } : (item || {});
const status = normalized.status || fallbackStatus || 'required';
return `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(normalized.key || normalized.label || 'item')}</strong>
<small>${escapeHtml(normalized.label || normalized.key || '')}</small>
${normalized.trigger ? `<small>trigger=${escapeHtml(normalized.trigger)}</small>` : ''}
</div>
<span>${escapeHtml(status).toUpperCase()}</span>
</div>
`;
};
deployBody.innerHTML = `
<div class="market-intel-empty mb-3">API 不執行推版;需由操作員依 app-only SOP 完成備份、同步、重啟與正式 smoke。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-check-list">${
checks.map(([name, passed]) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(name)}</strong>
</div>
<span>${passed ? 'PASS' : 'BLOCK'}</span>
</div>
`).join('')
}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-deploy-steps>
<p class="market-intel-deploy-section-title">MANUAL STEPS</p>
<div class="market-intel-check-list">${
steps.length
? steps.map(item => renderItem(item, 'pending')).join('')
: '<div class="market-intel-empty">尚未提供人工步驟。</div>'
}</div>
</div>
<div data-market-intel-deploy-fallback>
<p class="market-intel-deploy-section-title">備援方案</p>
<div class="market-intel-check-list">${
fallback.length
? fallback.map(item => renderItem(item, 'ready')).join('')
: '<div class="market-intel-empty">尚未提供備援方案。</div>'
}</div>
</div>
<div data-market-intel-deploy-boundaries>
<p class="market-intel-deploy-section-title">DEPLOY BOUNDARIES</p>
<div class="market-intel-check-list">${
boundaries.length
? boundaries.map(item => renderItem(item, 'required')).join('')
: '<div class="market-intel-empty">尚未提供部署邊界。</div>'
}</div>
</div>
<div data-market-intel-deploy-smoke>
<p class="market-intel-deploy-section-title">SMOKE TARGETS</p>
<div class="market-intel-check-list">${
smokeTargets.length
? smokeTargets.map(item => renderItem(String(item), 'required')).join('')
: '<div class="market-intel-empty">尚未提供 smoke 目標。</div>'
}</div>
</div>
</div>
`;
};
const loadDeploy = async () => {
if (!deployMeta || !deployBody) return;
deployBody.innerHTML = '<div class="market-intel-empty">讀取推版準備中...</div>';
try {
const response = await fetch(deployEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderDeployMeta(data);
renderDeployBody(data);
} catch (error) {
deployMeta.innerHTML = '<span class="market-intel-pill">error</span>';
deployBody.innerHTML = `<div class="market-intel-empty">推版準備讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
if (refresh) {
refresh.addEventListener('click', loadPreview);
}
if (writerRefresh) {
writerRefresh.addEventListener('click', loadWriter);
}
if (cliRefresh) {
cliRefresh.addEventListener('click', loadCli);
}
if (dbProbeRefresh) {
dbProbeRefresh.addEventListener('click', loadDbProbe);
}
if (seedDiffRefresh) {
seedDiffRefresh.addEventListener('click', loadSeedDiff);
}
if (migrationRefresh) {
migrationRefresh.addEventListener('click', loadMigration);
}
if (approvalRefresh) {
approvalRefresh.addEventListener('click', loadApproval);
}
if (deployRefresh) {
deployRefresh.addEventListener('click', loadDeploy);
}
loadPreview();
loadWriter();
loadCli();
loadDbProbe();
loadSeedDiff();
loadMigration();
loadApproval();
loadDeploy();
})();
</script>
{% endblock %}