Files
ewoooc/templates/market_intel/disabled.html
OoO 9ed0805697
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
feat(market-intel): add opportunity alert plan preview
2026-05-18 19:18:00 +08:00

2420 lines
127 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;
min-width: 0;
max-width: 100%;
}
.market-intel-panel {
box-sizing: border-box;
min-width: 0;
max-width: 100%;
overflow: hidden;
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;
overflow-wrap: anywhere;
}
.market-intel-flags {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
margin-top: 1rem;
min-width: 0;
}
.market-intel-flag {
min-width: 0;
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 {
min-width: 0;
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;
overflow-wrap: anywhere;
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;
min-width: 0;
}
.market-intel-candidate {
min-width: 0;
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;
min-width: 0;
}
.market-intel-operation {
min-width: 0;
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;
min-width: 0;
}
.market-intel-deploy-grid {
display: grid;
gap: 0.9rem;
grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
margin-top: 1rem;
min-width: 0;
}
.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;
min-width: 0;
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;
overflow-wrap: anywhere;
}
.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: 760px) {
.market-intel-preview-head,
.market-intel-check {
align-items: flex-start;
flex-direction: column;
}
.market-intel-panel {
padding: 1rem;
}
.market-intel-check span {
max-width: 100%;
overflow-wrap: anywhere;
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-legacy-bridge>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">LEGACY SOURCE / BRIDGE PREVIEW</p>
<h2 class="market-intel-preview-title">既有資料橋接預覽</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理橋接預覽" data-market-intel-legacy-bridge-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-legacy-bridge-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-legacy-bridge-body>
<div class="market-intel-empty">讀取既有資料橋接預覽中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-mcp-readiness>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">MCP / READINESS PREVIEW</p>
<h2 class="market-intel-preview-title">MCP 整合就緒度</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理 MCP 就緒度" data-market-intel-mcp-readiness-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-mcp-readiness-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-mcp-readiness-body>
<div class="market-intel-empty">讀取 MCP 整合就緒度中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-mcp-preflight>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">MCP / EXTERNAL DEPLOY PREFLIGHT</p>
<h2 class="market-intel-preview-title">外部 MCP 部署預檢</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理 MCP 部署預檢" data-market-intel-mcp-preflight-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-mcp-preflight-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-mcp-preflight-body>
<div class="market-intel-empty">讀取 MCP 部署預檢中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-mcp-activation>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">MCP / ACTIVATION RUNBOOK</p>
<h2 class="market-intel-preview-title">MCP 啟用 Runbook</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理 MCP 啟用 Runbook" data-market-intel-mcp-activation-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-mcp-activation-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-mcp-activation-body>
<div class="market-intel-empty">讀取 MCP 啟用 Runbook 中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-mcp-fetch-gate>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">MCP / FETCH GATE</p>
<h2 class="market-intel-preview-title">人工 Fetch 安全閘門</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理人工 Fetch 安全閘門" data-market-intel-mcp-fetch-gate-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-mcp-fetch-gate-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-mcp-fetch-gate-body>
<div class="market-intel-empty">讀取人工 Fetch 安全閘門中...</div>
</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-match-review>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">MATCH REVIEW / PLAN</p>
<h2 class="market-intel-preview-title">商品比對審核計畫</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理商品比對審核計畫" data-market-intel-match-review-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-match-review-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-match-review-body>
<div class="market-intel-empty">讀取商品比對審核計畫中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-opportunity>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">OPPORTUNITY / THREAT PLAN</p>
<h2 class="market-intel-preview-title">市場機會與威脅計畫</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理市場機會與威脅計畫" data-market-intel-opportunity-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-opportunity-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-opportunity-body>
<div class="market-intel-empty">讀取市場機會與威脅計畫中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-opportunity-scoring>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">OPPORTUNITY / SCORING MODEL</p>
<h2 class="market-intel-preview-title">機會威脅分數模型</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理機會威脅分數模型" data-market-intel-opportunity-scoring-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-opportunity-scoring-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-opportunity-scoring-body>
<div class="market-intel-empty">讀取機會威脅分數模型中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-opportunity-evidence>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">OPPORTUNITY / EVIDENCE BUNDLE</p>
<h2 class="market-intel-preview-title">機會威脅證據包</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理機會威脅證據包" data-market-intel-opportunity-evidence-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-opportunity-evidence-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-opportunity-evidence-body>
<div class="market-intel-empty">讀取機會威脅證據包中...</div>
</div>
</div>
<div class="market-intel-panel" data-market-intel-opportunity-alert>
<div class="market-intel-preview-head">
<div>
<p class="market-intel-muted momo-mono mb-1">OPPORTUNITY / ALERT CANDIDATE</p>
<h2 class="market-intel-preview-title">機會威脅告警候選</h2>
</div>
<button class="market-intel-icon-button" type="button" title="重新整理機會威脅告警候選" data-market-intel-opportunity-alert-refresh>
<i class="fas fa-rotate-right" aria-hidden="true"></i>
</button>
</div>
<div class="market-intel-preview-meta" data-market-intel-opportunity-alert-meta>
<span class="market-intel-pill">loading</span>
</div>
<div data-market-intel-opportunity-alert-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>
<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 legacyBridgeRoot = document.querySelector('[data-market-intel-legacy-bridge]');
const mcpReadinessRoot = document.querySelector('[data-market-intel-mcp-readiness]');
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 matchReviewRoot = document.querySelector('[data-market-intel-match-review]');
const opportunityRoot = document.querySelector('[data-market-intel-opportunity]');
const opportunityScoringRoot = document.querySelector('[data-market-intel-opportunity-scoring]');
const opportunityEvidenceRoot = document.querySelector('[data-market-intel-opportunity-evidence]');
const opportunityAlertRoot = document.querySelector('[data-market-intel-opportunity-alert]');
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 && !schedulerRoot && !matchReviewRoot && !opportunityRoot && !opportunityScoringRoot && !opportunityEvidenceRoot && !opportunityAlertRoot && !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 legacyBridgeMeta = legacyBridgeRoot ? legacyBridgeRoot.querySelector('[data-market-intel-legacy-bridge-meta]') : null;
const legacyBridgeBody = legacyBridgeRoot ? legacyBridgeRoot.querySelector('[data-market-intel-legacy-bridge-body]') : null;
const legacyBridgeRefresh = legacyBridgeRoot ? legacyBridgeRoot.querySelector('[data-market-intel-legacy-bridge-refresh]') : null;
const legacyBridgeEndpoint = "{{ url_for('market_intel.market_intel_legacy_source_bridge') }}?execute=false&limit=5";
const mcpReadinessMeta = mcpReadinessRoot ? mcpReadinessRoot.querySelector('[data-market-intel-mcp-readiness-meta]') : null;
const mcpReadinessBody = mcpReadinessRoot ? mcpReadinessRoot.querySelector('[data-market-intel-mcp-readiness-body]') : null;
const mcpReadinessRefresh = mcpReadinessRoot ? mcpReadinessRoot.querySelector('[data-market-intel-mcp-readiness-refresh]') : null;
const mcpReadinessEndpoint = "{{ url_for('market_intel.market_intel_mcp_readiness') }}?execute=false&timeout=3";
const mcpPreflightMeta = mcpPreflightRoot ? mcpPreflightRoot.querySelector('[data-market-intel-mcp-preflight-meta]') : null;
const mcpPreflightBody = mcpPreflightRoot ? mcpPreflightRoot.querySelector('[data-market-intel-mcp-preflight-body]') : null;
const mcpPreflightRefresh = mcpPreflightRoot ? mcpPreflightRoot.querySelector('[data-market-intel-mcp-preflight-refresh]') : null;
const mcpPreflightEndpoint = "{{ url_for('market_intel.market_intel_mcp_deploy_preflight') }}";
const mcpActivationMeta = mcpActivationRoot ? mcpActivationRoot.querySelector('[data-market-intel-mcp-activation-meta]') : null;
const mcpActivationBody = mcpActivationRoot ? mcpActivationRoot.querySelector('[data-market-intel-mcp-activation-body]') : null;
const mcpActivationRefresh = mcpActivationRoot ? mcpActivationRoot.querySelector('[data-market-intel-mcp-activation-refresh]') : null;
const mcpActivationEndpoint = "{{ url_for('market_intel.market_intel_mcp_activation_runbook') }}";
const mcpFetchGateMeta = mcpFetchGateRoot ? mcpFetchGateRoot.querySelector('[data-market-intel-mcp-fetch-gate-meta]') : null;
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 matchReviewMeta = matchReviewRoot ? matchReviewRoot.querySelector('[data-market-intel-match-review-meta]') : null;
const matchReviewBody = matchReviewRoot ? matchReviewRoot.querySelector('[data-market-intel-match-review-body]') : null;
const matchReviewRefresh = matchReviewRoot ? matchReviewRoot.querySelector('[data-market-intel-match-review-refresh]') : null;
const matchReviewEndpoint = "{{ url_for('market_intel.market_intel_match_review_plan') }}";
const opportunityMeta = opportunityRoot ? opportunityRoot.querySelector('[data-market-intel-opportunity-meta]') : null;
const opportunityBody = opportunityRoot ? opportunityRoot.querySelector('[data-market-intel-opportunity-body]') : null;
const opportunityRefresh = opportunityRoot ? opportunityRoot.querySelector('[data-market-intel-opportunity-refresh]') : null;
const opportunityEndpoint = "{{ url_for('market_intel.market_intel_opportunity_plan') }}";
const opportunityScoringMeta = opportunityScoringRoot ? opportunityScoringRoot.querySelector('[data-market-intel-opportunity-scoring-meta]') : null;
const opportunityScoringBody = opportunityScoringRoot ? opportunityScoringRoot.querySelector('[data-market-intel-opportunity-scoring-body]') : null;
const opportunityScoringRefresh = opportunityScoringRoot ? opportunityScoringRoot.querySelector('[data-market-intel-opportunity-scoring-refresh]') : null;
const opportunityScoringEndpoint = "{{ url_for('market_intel.market_intel_opportunity_scoring_plan') }}";
const opportunityEvidenceMeta = opportunityEvidenceRoot ? opportunityEvidenceRoot.querySelector('[data-market-intel-opportunity-evidence-meta]') : null;
const opportunityEvidenceBody = opportunityEvidenceRoot ? opportunityEvidenceRoot.querySelector('[data-market-intel-opportunity-evidence-body]') : null;
const opportunityEvidenceRefresh = opportunityEvidenceRoot ? opportunityEvidenceRoot.querySelector('[data-market-intel-opportunity-evidence-refresh]') : null;
const opportunityEvidenceEndpoint = "{{ url_for('market_intel.market_intel_opportunity_evidence_plan') }}";
const opportunityAlertMeta = opportunityAlertRoot ? opportunityAlertRoot.querySelector('[data-market-intel-opportunity-alert-meta]') : null;
const opportunityAlertBody = opportunityAlertRoot ? opportunityAlertRoot.querySelector('[data-market-intel-opportunity-alert-body]') : null;
const opportunityAlertRefresh = opportunityAlertRoot ? opportunityAlertRoot.querySelector('[data-market-intel-opportunity-alert-refresh]') : null;
const opportunityAlertEndpoint = "{{ url_for('market_intel.market_intel_opportunity_alert_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;
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 renderLegacyBridgeMeta = data => {
legacyBridgeMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`execute=${data.execute_requested ? 'true' : 'false'}`,
`query=${data.read_only_query_executed ? 'yes' : 'no'}`,
`sources=${data.existing_source_count || 0}/${data.source_count || 0}`,
`rows=${data.total_existing_rows || 0}`,
`writes=${data.writes_executed ? 'executed' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderLegacyBridgeBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const sources = data.source_summaries || [];
const operations = data.bridge_operations || [];
const controls = data.duplicate_controls || [];
const renderSource = item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.table)}</strong>
<small>${escapeHtml(item.description || '')}</small>
<small>target=${escapeHtml((item.planned_targets || []).join(' / '))}</small>
</div>
<span>${item.exists ? escapeHtml(item.row_count || 0) : 'PENDING'}</span>
</div>
`;
const renderOperation = item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.source_table)}${escapeHtml(item.target_table)}</strong>
<small>${escapeHtml(item.operation)} / ${escapeHtml(item.write_status)}</small>
<small>dedupe=${escapeHtml(item.dedupe_key)}</small>
</article>
`;
const renderControl = item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.rule)}</small>
</div>
<span>RULE</span>
</div>
`;
legacyBridgeBody.innerHTML = `
<div class="market-intel-empty mb-3">這裡只盤點既有資料來源與導入規則,不搬資料、不寫 market_*。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-legacy-sources>
<p class="market-intel-deploy-section-title">SOURCE TABLES</p>
<div class="market-intel-check-list">${
sources.length
? sources.map(renderSource).join('')
: '<div class="market-intel-empty">尚未提供來源摘要。</div>'
}</div>
</div>
<div data-market-intel-legacy-operations>
<p class="market-intel-deploy-section-title">BRIDGE OPERATIONS</p>
<div class="market-intel-operation-list">${
operations.length
? operations.map(renderOperation).join('')
: '<div class="market-intel-empty">尚未提供橋接操作。</div>'
}</div>
</div>
<div data-market-intel-legacy-controls>
<p class="market-intel-deploy-section-title">DUPLICATE CONTROLS</p>
<div class="market-intel-check-list">${
controls.length
? controls.map(renderControl).join('')
: '<div class="market-intel-empty">尚未提供去重規則。</div>'
}</div>
</div>
</div>
`;
};
const loadLegacyBridge = async () => {
if (!legacyBridgeMeta || !legacyBridgeBody) return;
legacyBridgeBody.innerHTML = '<div class="market-intel-empty">讀取既有資料橋接預覽中...</div>';
try {
const response = await fetch(legacyBridgeEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderLegacyBridgeMeta(data);
renderLegacyBridgeBody(data);
} catch (error) {
legacyBridgeMeta.innerHTML = '<span class="market-intel-pill">error</span>';
legacyBridgeBody.innerHTML = `<div class="market-intel-empty">既有資料橋接預覽讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderMcpReadinessMeta = data => {
mcpReadinessMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`router=${data.router_enabled ? 'on' : 'off'}`,
`external=${data.external_mcp_complete ? 'complete' : 'pending'}`,
`internal=${data.internal_mcp_complete ? 'complete' : 'pending'}`,
`market_tools=${data.market_intel_tool_count || 0}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderMcpReadinessBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const servers = data.server_statuses || [];
const checks = Object.entries(data.readiness_checks || {});
const expectedTools = data.expected_market_intel_tools || [];
const registeredCallers = data.registered_callers || [];
const toolContract = data.mcp_tool_contract || {};
const contractTools = toolContract.tools || [];
const telemetry = data.telemetry || {};
mcpReadinessBody.innerHTML = `
<div class="market-intel-empty mb-3">目前只做 MCP readiness planned preview不自動呼叫外部平台、不建立 DB session、不寫入 telemetry。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-mcp-servers>
<p class="market-intel-deploy-section-title">EXTERNAL MCP SERVERS</p>
<div class="market-intel-check-list">${
servers.length
? servers.map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.server)}</strong>
<small>${escapeHtml(item.base_url || 'not configured')}</small>
<small>${escapeHtml(item.status || '')}</small>
</div>
<span>${item.healthy ? 'HEALTHY' : item.configured ? 'PENDING' : 'MISSING'}</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供 MCP server 狀態。</div>'
}</div>
</div>
<div data-market-intel-mcp-checks>
<p class="market-intel-deploy-section-title">READINESS CHECKS</p>
<div class="market-intel-check-list">${
checks.length
? checks.map(([name, passed]) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(name)}</strong>
</div>
<span>${passed ? 'PASS' : 'BLOCK'}</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供 readiness checks。</div>'
}</div>
</div>
<div data-market-intel-mcp-tools>
<p class="market-intel-deploy-section-title">INTERNAL TOOL CONTRACT</p>
<div class="market-intel-check-list">
<div class="market-intel-check">
<div>
<strong>registered_callers</strong>
<small>${escapeHtml(registeredCallers.join(' / ') || 'none')}</small>
</div>
<span>${registeredCallers.length}</span>
</div>
<div class="market-intel-check">
<div>
<strong>market_intel_contract</strong>
<small>${escapeHtml((toolContract.blocked_reasons || []).join(' / ') || 'ready')}</small>
</div>
<span>${toolContract.contract_ready ? 'READY' : 'PENDING'}</span>
</div>
<div class="market-intel-check">
<div>
<strong>expected_tool_names</strong>
<small>${escapeHtml(expectedTools.join(' / ') || 'none')}</small>
</div>
<span>${escapeHtml(data.market_intel_tool_count || 0)}</span>
</div>
<div class="market-intel-check">
<div>
<strong>mcp_calls telemetry</strong>
<small>${escapeHtml(telemetry.mode || 'unknown')} / table=${telemetry.table_exists ? 'exists' : 'pending'}</small>
</div>
<span>${telemetry.read_only_query_executed ? 'QUERY' : 'PLANNED'}</span>
</div>
${contractTools.map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.name)}</strong>
<small>${escapeHtml(item.server)} / ${escapeHtml((item.router_tools || []).join(' + '))}</small>
</div>
<span>${item.router_tools_whitelisted ? 'ALLOW' : 'BLOCK'}</span>
</div>
`).join('')}
</div>
</div>
</div>
`;
};
const loadMcpReadiness = async () => {
if (!mcpReadinessMeta || !mcpReadinessBody) return;
mcpReadinessBody.innerHTML = '<div class="market-intel-empty">讀取 MCP 整合就緒度中...</div>';
try {
const response = await fetch(mcpReadinessEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderMcpReadinessMeta(data);
renderMcpReadinessBody(data);
} catch (error) {
mcpReadinessMeta.innerHTML = '<span class="market-intel-pill">error</span>';
mcpReadinessBody.innerHTML = `<div class="market-intel-empty">MCP 就緒度讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderMcpPreflightMeta = data => {
const missingRequiredEnv = (data.env_statuses || []).filter(item => item.required && !item.present).length;
mcpPreflightMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`ready=${data.ready_to_start_stack ? 'yes' : 'no'}`,
`compose=${data.compose_file_present ? 'present' : 'missing'}`,
`missing_env=${missingRequiredEnv}`,
`docker=${data.docker_command_executed ? 'executed' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderMcpPreflightBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const envs = data.env_statuses || [];
const services = data.service_statuses || [];
const ports = data.port_statuses || [];
const fallback = data.fallback_plan || [];
const renderCheck = (key, label, status) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(key)}</strong>
<small>${escapeHtml(label || '')}</small>
</div>
<span>${escapeHtml(status)}</span>
</div>
`;
mcpPreflightBody.innerHTML = `
<div class="market-intel-empty mb-3">這裡只做外部 MCP 部署預檢,不執行 docker、SSH、DB role 建立或 crawler。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-mcp-preflight-env>
<p class="market-intel-deploy-section-title">ENV GATES</p>
<div class="market-intel-check-list">${
envs.map(item => renderCheck(
item.name,
item.required ? 'required' : 'optional',
item.present ? 'SET' : 'MISSING'
)).join('')
}</div>
</div>
<div data-market-intel-mcp-preflight-services>
<p class="market-intel-deploy-section-title">COMPOSE SERVICES</p>
<div class="market-intel-check-list">${
services.map(item => renderCheck(
item.service,
data.compose_path || '',
item.declared ? 'DECLARED' : 'MISSING'
)).join('')
}</div>
</div>
<div data-market-intel-mcp-preflight-ports>
<p class="market-intel-deploy-section-title">LOCALHOST PORTS</p>
<div class="market-intel-check-list">${
ports.map(item => renderCheck(
`localhost:${item.port}`,
item.localhost_only ? '127.0.0.1 only' : 'not localhost only',
item.localhost_only ? 'SAFE' : 'BLOCK'
)).join('')
}</div>
</div>
<div data-market-intel-mcp-preflight-fallback>
<p class="market-intel-deploy-section-title">FALLBACK</p>
<div class="market-intel-check-list">${
fallback.map(item => renderCheck(
item.key,
item.label,
'READY'
)).join('')
}</div>
</div>
</div>
`;
};
const loadMcpPreflight = async () => {
if (!mcpPreflightMeta || !mcpPreflightBody) return;
mcpPreflightBody.innerHTML = '<div class="market-intel-empty">讀取 MCP 部署預檢中...</div>';
try {
const response = await fetch(mcpPreflightEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderMcpPreflightMeta(data);
renderMcpPreflightBody(data);
} catch (error) {
mcpPreflightMeta.innerHTML = '<span class="market-intel-pill">error</span>';
mcpPreflightBody.innerHTML = `<div class="market-intel-empty">MCP 部署預檢讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderMcpActivationMeta = data => {
mcpActivationMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`ready=${data.ready_for_operator_activation ? 'yes' : 'no'}`,
`stages=${(data.stages || []).length}`,
`blocked=${(data.blocked_reasons || []).length}`,
`docker=${data.docker_command_executed ? 'executed' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderMcpActivationBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const stages = data.stages || [];
const fallback = data.fallback_plan || [];
const safety = Object.entries(data.safety_contract || {});
const renderStage = item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.label)}</small>
<small>gate=${escapeHtml(item.gate)} / status=${escapeHtml(item.status)}</small>
${item.command_preview ? `<small>${escapeHtml(item.command_preview)}</small>` : ''}
${item.notes ? `<small>${escapeHtml(item.notes)}</small>` : ''}
</article>
`;
const renderCheck = (key, label, status) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(key)}</strong>
<small>${escapeHtml(label || '')}</small>
</div>
<span>${escapeHtml(status)}</span>
</div>
`;
mcpActivationBody.innerHTML = `
<div class="market-intel-empty mb-3">這是 MCP 啟用順序預覽;不寫 env、不執行 docker/SSH、不建立 DB role、不開 router。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-mcp-activation-stages>
<p class="market-intel-deploy-section-title">ACTIVATION STAGES</p>
<div class="market-intel-operation-list">${
stages.length
? stages.map(renderStage).join('')
: '<div class="market-intel-empty">尚未提供啟用步驟。</div>'
}</div>
</div>
<div data-market-intel-mcp-activation-safety>
<p class="market-intel-deploy-section-title">SAFETY CONTRACT</p>
<div class="market-intel-check-list">${
safety.length
? safety.map(([key, value]) => renderCheck(key, '', value ? 'YES' : 'NO')).join('')
: '<div class="market-intel-empty">尚未提供安全合約。</div>'
}</div>
</div>
<div data-market-intel-mcp-activation-fallback>
<p class="market-intel-deploy-section-title">FALLBACK</p>
<div class="market-intel-check-list">${
fallback.length
? fallback.map(item => renderCheck(item.key, item.label, 'READY')).join('')
: '<div class="market-intel-empty">尚未提供備援方案。</div>'
}</div>
</div>
</div>
`;
};
const loadMcpActivation = async () => {
if (!mcpActivationMeta || !mcpActivationBody) return;
mcpActivationBody.innerHTML = '<div class="market-intel-empty">讀取 MCP 啟用 Runbook 中...</div>';
try {
const response = await fetch(mcpActivationEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderMcpActivationMeta(data);
renderMcpActivationBody(data);
} catch (error) {
mcpActivationMeta.innerHTML = '<span class="market-intel-pill">error</span>';
mcpActivationBody.innerHTML = `<div class="market-intel-empty">MCP 啟用 Runbook 讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderMcpFetchGateMeta = data => {
mcpFetchGateMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`fetch=${data.fetch_requested ? 'true' : 'false'}`,
`gate=${data.manual_fetch_gate_open ? 'open' : 'blocked'}`,
`network=${data.network_request_allowed ? 'allow' : 'block'}`,
`blocked=${(data.blocked_reasons || []).length}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderMcpFetchGateBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const checks = Object.entries(data.gate_checks || {});
const sequence = data.required_sequence || [];
const readiness = data.mcp_readiness_summary || {};
mcpFetchGateBody.innerHTML = `
<div class="market-intel-empty mb-3">人工 fetch 目前先由 MCP gate 接管;頁面預設只做 planned preview不抓外站、不寫 DB、不掛 scheduler。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-mcp-fetch-gate-checks>
<p class="market-intel-deploy-section-title">FETCH GATE CHECKS</p>
<div class="market-intel-check-list">${
checks.length
? checks.map(([name, passed]) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(name)}</strong>
</div>
<span>${passed ? 'PASS' : 'BLOCK'}</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供 fetch gate checks。</div>'
}</div>
</div>
<div data-market-intel-mcp-fetch-gate-sequence>
<p class="market-intel-deploy-section-title">REQUIRED SEQUENCE</p>
<div class="market-intel-check-list">${
sequence.length
? sequence.map((item, index) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(`step_${index + 1}`)}</strong>
<small>${escapeHtml(item)}</small>
</div>
<span>REQUIRED</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供啟用順序。</div>'
}</div>
</div>
<div data-market-intel-mcp-fetch-gate-readiness>
<p class="market-intel-deploy-section-title">READINESS SUMMARY</p>
<div class="market-intel-check-list">
<div class="market-intel-check">
<div>
<strong>router</strong>
<small>${escapeHtml(readiness.mode || 'unknown')}</small>
</div>
<span>${readiness.router_enabled ? 'ON' : 'OFF'}</span>
</div>
<div class="market-intel-check">
<div>
<strong>external_mcp</strong>
<small>${escapeHtml((readiness.blocked_reasons || []).join(' / ') || 'ready')}</small>
</div>
<span>${readiness.external_mcp_complete ? 'READY' : 'PENDING'}</span>
</div>
</div>
</div>
</div>
`;
};
const loadMcpFetchGate = async () => {
if (!mcpFetchGateMeta || !mcpFetchGateBody) return;
mcpFetchGateBody.innerHTML = '<div class="market-intel-empty">讀取人工 Fetch 安全閘門中...</div>';
try {
const response = await fetch(mcpFetchGateEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderMcpFetchGateMeta(data);
renderMcpFetchGateBody(data);
} catch (error) {
mcpFetchGateMeta.innerHTML = '<span class="market-intel-pill">error</span>';
mcpFetchGateBody.innerHTML = `<div class="market-intel-empty">人工 Fetch 安全閘門讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
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 renderMatchReviewMeta = data => {
const thresholds = data.thresholds || {};
matchReviewMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`ready=${data.ready_for_review_queue ? 'yes' : 'no'}`,
`signals=${(data.scoring_signals || []).length}`,
`auto_confirm=${thresholds.auto_confirm_enabled ? 'on' : 'off'}`,
`writes=${data.writes_executed ? 'executed' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderMatchReviewBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const checks = Object.entries(data.gate_checks || {});
const signals = data.scoring_signals || [];
const actions = data.review_actions || [];
const sequence = data.operator_sequence || [];
const renderCheck = ([name, passed]) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(name)}</strong>
</div>
<span>${passed ? 'PASS' : 'BLOCK'}</span>
</div>
`;
const renderSignal = item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.label)}</small>
<small>weight=${escapeHtml(item.weight)} / ${escapeHtml(item.source)}</small>
</article>
`;
const renderAction = item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.label)}</small>
</div>
<span>${item.requires_operator ? 'HITL' : 'AUTO'}</span>
</div>
`;
matchReviewBody.innerHTML = `
<div class="market-intel-empty mb-3">商品比對目前只建立審核規則與 HITL 流程;不建立 queue、不自動確認、不寫 DB、不呼叫外部 MCP。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-match-review-checks>
<p class="market-intel-deploy-section-title">MATCH 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-match-review-signals>
<p class="market-intel-deploy-section-title">SCORING SIGNALS</p>
<div class="market-intel-operation-list">${
signals.length
? signals.map(renderSignal).join('')
: '<div class="market-intel-empty">尚未提供比對訊號。</div>'
}</div>
</div>
<div data-market-intel-match-review-actions>
<p class="market-intel-deploy-section-title">REVIEW ACTIONS</p>
<div class="market-intel-check-list">${
actions.length
? actions.map(renderAction).join('')
: '<div class="market-intel-empty">尚未提供審核動作。</div>'
}</div>
</div>
<div data-market-intel-match-review-sequence>
<p class="market-intel-deploy-section-title">OPERATOR SEQUENCE</p>
<div class="market-intel-check-list">${
sequence.length
? sequence.map((item, index) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(`step_${index + 1}`)}</strong>
<small>${escapeHtml(item)}</small>
</div>
<span>REQUIRED</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供操作順序。</div>'
}</div>
</div>
</div>
`;
};
const loadMatchReview = async () => {
if (!matchReviewMeta || !matchReviewBody) return;
matchReviewBody.innerHTML = '<div class="market-intel-empty">讀取商品比對審核計畫中...</div>';
try {
const response = await fetch(matchReviewEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderMatchReviewMeta(data);
renderMatchReviewBody(data);
} catch (error) {
matchReviewMeta.innerHTML = '<span class="market-intel-pill">error</span>';
matchReviewBody.innerHTML = `<div class="market-intel-empty">商品比對審核計畫讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderOpportunityMeta = data => {
opportunityMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`ready=${data.ready_for_opportunity_queue ? 'yes' : 'no'}`,
`rules=${data.rule_count || 0}`,
`alerts=${data.threat_alert_dispatched ? 'sent' : 'blocked'}`,
`ai=${data.ai_summary_generated ? 'generated' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderOpportunityBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const checks = Object.entries(data.gate_checks || {});
const rules = data.rules || [];
const sequence = data.operator_sequence || [];
const severity = Object.entries(data.severity_policy || {});
const renderCheck = ([name, passed]) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(name)}</strong>
</div>
<span>${passed ? 'PASS' : 'BLOCK'}</span>
</div>
`;
const renderRule = item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.label)} / severity=${escapeHtml(item.severity)}</small>
<small>${escapeHtml((item.minimum_evidence || []).join(' + '))}</small>
<small>${escapeHtml(item.action_hint)}</small>
</article>
`;
opportunityBody.innerHTML = `
<div class="market-intel-empty mb-3">目前只建立機會與威脅判讀規則;不建立 queue、不派送 Telegram、不產生 AI 摘要、不寫 DB。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-opportunity-checks>
<p class="market-intel-deploy-section-title">OPPORTUNITY 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-opportunity-rules>
<p class="market-intel-deploy-section-title">RULES</p>
<div class="market-intel-operation-list">${
rules.length
? rules.map(renderRule).join('')
: '<div class="market-intel-empty">尚未提供規則。</div>'
}</div>
</div>
<div data-market-intel-opportunity-severity>
<p class="market-intel-deploy-section-title">SEVERITY POLICY</p>
<div class="market-intel-check-list">${
severity.length
? severity.map(([key, label]) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(key)}</strong>
<small>${escapeHtml(label)}</small>
</div>
<span>POLICY</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供分級策略。</div>'
}</div>
</div>
<div data-market-intel-opportunity-sequence>
<p class="market-intel-deploy-section-title">OPERATOR SEQUENCE</p>
<div class="market-intel-check-list">${
sequence.length
? sequence.map((item, index) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(`step_${index + 1}`)}</strong>
<small>${escapeHtml(item)}</small>
</div>
<span>REQUIRED</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供操作順序。</div>'
}</div>
</div>
</div>
`;
};
const loadOpportunity = async () => {
if (!opportunityMeta || !opportunityBody) return;
opportunityBody.innerHTML = '<div class="market-intel-empty">讀取市場機會與威脅計畫中...</div>';
try {
const response = await fetch(opportunityEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderOpportunityMeta(data);
renderOpportunityBody(data);
} catch (error) {
opportunityMeta.innerHTML = '<span class="market-intel-pill">error</span>';
opportunityBody.innerHTML = `<div class="market-intel-empty">市場機會與威脅計畫讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderOpportunityScoringMeta = data => {
opportunityScoringMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`ready=${data.ready_for_scoring_job ? 'yes' : 'no'}`,
`dimensions=${data.dimension_count || 0}`,
`weight=${data.total_weight || 0}`,
`score=${data.score_calculation_executed ? 'executed' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderOpportunityScoringBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const checks = Object.entries(data.gate_checks || {});
const dimensions = data.dimensions || [];
const thresholds = data.thresholds || [];
const evidenceTables = data.required_evidence_tables || [];
const sequence = data.operator_sequence || [];
const renderCheck = ([name, passed]) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(name)}</strong>
</div>
<span>${passed ? 'PASS' : 'BLOCK'}</span>
</div>
`;
const renderDimension = item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.key)} / ${escapeHtml(item.weight)}%</strong>
<small>${escapeHtml(item.label)}</small>
<small>${escapeHtml((item.evidence || []).join(' + '))}</small>
<small>${escapeHtml(item.calculation)}</small>
</article>
`;
opportunityScoringBody.innerHTML = `
<div class="market-intel-empty mb-3">目前只定義機會與威脅分數模型;不計分、不建立 queue、不派送 Telegram、不產生 AI 摘要、不寫 DB。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-opportunity-scoring-checks>
<p class="market-intel-deploy-section-title">SCORING 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-opportunity-scoring-dimensions>
<p class="market-intel-deploy-section-title">DIMENSIONS</p>
<div class="market-intel-operation-list">${
dimensions.length
? dimensions.map(renderDimension).join('')
: '<div class="market-intel-empty">尚未提供分數欄位。</div>'
}</div>
</div>
<div data-market-intel-opportunity-scoring-thresholds>
<p class="market-intel-deploy-section-title">THRESHOLDS</p>
<div class="market-intel-check-list">${
thresholds.length
? thresholds.map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.level)}</strong>
<small>${escapeHtml(item.action)}</small>
</div>
<span>${escapeHtml(item.minimum_score)}+</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供門檻。</div>'
}</div>
</div>
<div data-market-intel-opportunity-scoring-evidence>
<p class="market-intel-deploy-section-title">EVIDENCE TABLES</p>
<div class="market-intel-check-list">${
evidenceTables.length
? evidenceTables.map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item)}</strong>
</div>
<span>REQUIRED</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供 evidence tables。</div>'
}</div>
</div>
<div data-market-intel-opportunity-scoring-sequence>
<p class="market-intel-deploy-section-title">OPERATOR SEQUENCE</p>
<div class="market-intel-check-list">${
sequence.length
? sequence.map((item, index) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(`step_${index + 1}`)}</strong>
<small>${escapeHtml(item)}</small>
</div>
<span>REQUIRED</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供操作順序。</div>'
}</div>
</div>
</div>
`;
};
const loadOpportunityScoring = async () => {
if (!opportunityScoringMeta || !opportunityScoringBody) return;
opportunityScoringBody.innerHTML = '<div class="market-intel-empty">讀取機會威脅分數模型中...</div>';
try {
const response = await fetch(opportunityScoringEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderOpportunityScoringMeta(data);
renderOpportunityScoringBody(data);
} catch (error) {
opportunityScoringMeta.innerHTML = '<span class="market-intel-pill">error</span>';
opportunityScoringBody.innerHTML = `<div class="market-intel-empty">機會威脅分數模型讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderOpportunityEvidenceMeta = data => {
opportunityEvidenceMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`ready=${data.ready_for_evidence_bundle ? 'yes' : 'no'}`,
`sections=${data.section_count || 0}`,
`query=${data.evidence_query_executed ? 'yes' : 'no'}`,
`bundle=${data.evidence_bundle_created ? 'created' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderOpportunityEvidenceBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const checks = Object.entries(data.gate_checks || {});
const sections = data.sections || [];
const gates = data.escalation_gates || [];
const tables = data.required_market_tables || [];
const sequence = data.operator_sequence || [];
const contract = data.bundle_contract || {};
const renderCheck = ([name, passed]) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(name)}</strong>
</div>
<span>${passed ? 'PASS' : 'BLOCK'}</span>
</div>
`;
const renderSection = item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.label)}</small>
<small>${escapeHtml((item.source_tables || []).join(' + '))}</small>
<small>${escapeHtml((item.required_fields || []).join(' / '))}</small>
</article>
`;
opportunityEvidenceBody.innerHTML = `
<div class="market-intel-empty mb-3">目前只定義 evidence bundle contract不查 DB、不產生 sample evidence、不建立 alert candidate、不派送 Telegram、不產生 AI 摘要。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-empty mb-3">dedupe=${escapeHtml(contract.dedupe_key || '')} / freshness=${escapeHtml(contract.freshness_window_hours || 'n/a')}h</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-opportunity-evidence-checks>
<p class="market-intel-deploy-section-title">EVIDENCE GATES</p>
<div class="market-intel-check-list">${
checks.length
? checks.map(renderCheck).join('')
: '<div class="market-intel-empty">尚未提供 evidence gate。</div>'
}</div>
</div>
<div data-market-intel-opportunity-evidence-sections>
<p class="market-intel-deploy-section-title">BUNDLE SECTIONS</p>
<div class="market-intel-operation-list">${
sections.length
? sections.map(renderSection).join('')
: '<div class="market-intel-empty">尚未提供 evidence section。</div>'
}</div>
</div>
<div data-market-intel-opportunity-evidence-escalation>
<p class="market-intel-deploy-section-title">ESCALATION GATES</p>
<div class="market-intel-check-list">${
gates.length
? gates.map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.label)}</small>
</div>
<span>${escapeHtml((item.required_for || []).join('/'))}</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供升級 gate。</div>'
}</div>
</div>
<div data-market-intel-opportunity-evidence-tables>
<p class="market-intel-deploy-section-title">REQUIRED TABLES</p>
<div class="market-intel-check-list">${
tables.length
? tables.map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item)}</strong>
</div>
<span>REQUIRED</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供 required tables。</div>'
}</div>
</div>
<div data-market-intel-opportunity-evidence-sequence>
<p class="market-intel-deploy-section-title">OPERATOR SEQUENCE</p>
<div class="market-intel-check-list">${
sequence.length
? sequence.map((item, index) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(`step_${index + 1}`)}</strong>
<small>${escapeHtml(item)}</small>
</div>
<span>REQUIRED</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供操作順序。</div>'
}</div>
</div>
</div>
`;
};
const loadOpportunityEvidence = async () => {
if (!opportunityEvidenceMeta || !opportunityEvidenceBody) return;
opportunityEvidenceBody.innerHTML = '<div class="market-intel-empty">讀取機會威脅證據包中...</div>';
try {
const response = await fetch(opportunityEvidenceEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderOpportunityEvidenceMeta(data);
renderOpportunityEvidenceBody(data);
} catch (error) {
opportunityEvidenceMeta.innerHTML = '<span class="market-intel-pill">error</span>';
opportunityEvidenceBody.innerHTML = `<div class="market-intel-empty">機會威脅證據包讀取失敗:${escapeHtml(error.message)}</div>`;
}
};
const renderOpportunityAlertMeta = data => {
opportunityAlertMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
`ready=${data.ready_for_alert_candidates ? 'yes' : 'no'}`,
`channels=${data.channel_count || 0}`,
`telegram=${data.telegram_dispatched ? 'sent' : 'blocked'}`,
`llm=${data.llm_call_executed ? 'called' : 'blocked'}`
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
};
const renderOpportunityAlertBody = data => {
const blockers = (data.blocked_reasons || []).join(' / ');
const checks = Object.entries(data.gate_checks || {});
const channels = data.channels || [];
const gates = data.alert_gates || [];
const throttle = data.throttle_policy || {};
const sequence = data.operator_sequence || [];
const payload = data.payload_contract || {};
const renderCheck = ([name, passed]) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(name)}</strong>
</div>
<span>${passed ? 'PASS' : 'BLOCK'}</span>
</div>
`;
const renderChannel = item => `
<article class="market-intel-operation">
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.label)} / minimum=${escapeHtml(item.minimum_level)}</small>
<small>approval=${item.requires_operator_approval ? 'required' : 'not_required'} / target=${escapeHtml(item.dispatch_target)}</small>
</article>
`;
opportunityAlertBody.innerHTML = `
<div class="market-intel-empty mb-3">目前只定義告警候選 gate不建立 alert queue、不派送 Telegram、不呼叫 LLM、不寫 DB。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
<div class="market-intel-empty mb-3">dedupe=${escapeHtml(throttle.dedupe_window_hours || 'n/a')}h / telegram_daily=${escapeHtml(throttle.max_telegram_candidates_per_day || 0)} / ai_digest=${escapeHtml(throttle.max_ai_briefing_items_per_digest || 0)}</div>
<div class="market-intel-deploy-grid">
<div data-market-intel-opportunity-alert-checks>
<p class="market-intel-deploy-section-title">ALERT 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-opportunity-alert-channels>
<p class="market-intel-deploy-section-title">CHANNELS</p>
<div class="market-intel-operation-list">${
channels.length
? channels.map(renderChannel).join('')
: '<div class="market-intel-empty">尚未提供 channel。</div>'
}</div>
</div>
<div data-market-intel-opportunity-alert-escalation>
<p class="market-intel-deploy-section-title">ESCALATION</p>
<div class="market-intel-check-list">${
gates.length
? gates.map(item => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(item.key)}</strong>
<small>${escapeHtml(item.label)}</small>
</div>
<span>${escapeHtml((item.required_for || []).join('/'))}</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供升級 gate。</div>'
}</div>
</div>
<div data-market-intel-opportunity-alert-payload>
<p class="market-intel-deploy-section-title">PAYLOAD CONTRACT</p>
<div class="market-intel-check-list">
<div class="market-intel-check">
<div>
<strong>required_fields</strong>
<small>${escapeHtml((payload.required_fields || []).join(' / '))}</small>
</div>
<span>REQUIRED</span>
</div>
<div class="market-intel-check">
<div>
<strong>forbidden_fields</strong>
<small>${escapeHtml((payload.forbidden_fields || []).join(' / '))}</small>
</div>
<span>BLOCKED</span>
</div>
</div>
</div>
<div data-market-intel-opportunity-alert-sequence>
<p class="market-intel-deploy-section-title">OPERATOR SEQUENCE</p>
<div class="market-intel-check-list">${
sequence.length
? sequence.map((item, index) => `
<div class="market-intel-check">
<div>
<strong>${escapeHtml(`step_${index + 1}`)}</strong>
<small>${escapeHtml(item)}</small>
</div>
<span>REQUIRED</span>
</div>
`).join('')
: '<div class="market-intel-empty">尚未提供操作順序。</div>'
}</div>
</div>
</div>
`;
};
const loadOpportunityAlert = async () => {
if (!opportunityAlertMeta || !opportunityAlertBody) return;
opportunityAlertBody.innerHTML = '<div class="market-intel-empty">讀取機會威脅告警候選中...</div>';
try {
const response = await fetch(opportunityAlertEndpoint, { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderOpportunityAlertMeta(data);
renderOpportunityAlertBody(data);
} catch (error) {
opportunityAlertMeta.innerHTML = '<span class="market-intel-pill">error</span>';
opportunityAlertBody.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
: {};
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 (legacyBridgeRefresh) {
legacyBridgeRefresh.addEventListener('click', loadLegacyBridge);
}
if (mcpReadinessRefresh) {
mcpReadinessRefresh.addEventListener('click', loadMcpReadiness);
}
if (mcpPreflightRefresh) {
mcpPreflightRefresh.addEventListener('click', loadMcpPreflight);
}
if (mcpActivationRefresh) {
mcpActivationRefresh.addEventListener('click', loadMcpActivation);
}
if (mcpFetchGateRefresh) {
mcpFetchGateRefresh.addEventListener('click', loadMcpFetchGate);
}
if (schedulerRefresh) {
schedulerRefresh.addEventListener('click', loadScheduler);
}
if (matchReviewRefresh) {
matchReviewRefresh.addEventListener('click', loadMatchReview);
}
if (opportunityRefresh) {
opportunityRefresh.addEventListener('click', loadOpportunity);
}
if (opportunityScoringRefresh) {
opportunityScoringRefresh.addEventListener('click', loadOpportunityScoring);
}
if (opportunityEvidenceRefresh) {
opportunityEvidenceRefresh.addEventListener('click', loadOpportunityEvidence);
}
if (opportunityAlertRefresh) {
opportunityAlertRefresh.addEventListener('click', loadOpportunityAlert);
}
if (migrationRefresh) {
migrationRefresh.addEventListener('click', loadMigration);
}
if (approvalRefresh) {
approvalRefresh.addEventListener('click', loadApproval);
}
if (deployRefresh) {
deployRefresh.addEventListener('click', loadDeploy);
}
loadPreview();
loadWriter();
loadCli();
loadDbProbe();
loadSeedDiff();
loadLegacyBridge();
loadMcpReadiness();
loadMcpPreflight();
loadMcpActivation();
loadMcpFetchGate();
loadScheduler();
loadMatchReview();
loadOpportunity();
loadOpportunityScoring();
loadOpportunityEvidence();
loadOpportunityAlert();
loadMigration();
loadApproval();
loadDeploy();
})();
</script>
{% endblock %}