977 lines
45 KiB
HTML
977 lines
45 KiB
HTML
{% 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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
|
||
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 %}
|